Compare commits

..

No commits in common. "main" and "feature/redistributable-options" have entirely different histories.

1038 changed files with 28479 additions and 403658 deletions

View file

@ -124,6 +124,19 @@ jobs:
build_platform: Windows
build_configuration: Debug
build_launcher_win_x64:
needs: [prep]
if: needs.prep.outputs.changed == 'true'
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Debug
# --------------------------------------------------------------------------
# 3) FINALIZE: if changes == 'true', gather artifacts + push Docker:development
# --------------------------------------------------------------------------
@ -132,6 +145,7 @@ jobs:
- prep
- build_server_linux_x64
- build_server_win_x64
- build_launcher_win_x64
if: needs.prep.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
@ -233,6 +247,12 @@ jobs:
name: LANCommander.Server-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Windows x64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Delete existing release assets
uses: dev-drprasad/delete-tag-and-release@v1.1
with:
@ -250,5 +270,6 @@ jobs:
files: |
artifacts/LANCommander.Server-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Server-Linux-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -0,0 +1,123 @@
name: LANCommander Launcher Avalonia Build
on:
workflow_dispatch:
workflow_call:
inputs:
version_semver:
description: "Semantic Version"
required: true
type: string
version_tag:
description: 'Version Tag'
required: true
type: string
build_dotnet_version:
description: 'Build .NET Version'
required: true
type: string
build_runtime:
description: 'Build Runtime'
required: false
type: string
default: 'win-x64'
build_arch:
description: 'Build Architecture'
required: false
type: string
default: 'x64'
build_platform:
description: 'Build Platform'
required: false
type: string
default: 'Windows'
build_configuration:
description: 'Build Configuration (Debug/Release)'
required: false
type: string
default: 'Release'
permissions:
contents: write
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/package
jobs:
build:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
# Checkout code
- uses: actions/checkout@v4
with:
submodules: true
# .NET Setup
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ inputs.build_dotnet_version }}
- name: Restore dependencies
run: dotnet restore --locked-mode
- name: Publish Avalonia Launcher
run: |
# Strip leading 'v' if present
RAW_VERSION="${{ inputs.version_tag }}" # e.g. v2.0.0-rc1
SEMVER="${RAW_VERSION#v}" # 2.0.0-rc1
# Numeric part only for Assembly/FileVersion
NUMERIC="${SEMVER%%-*}" # 2.0.0
ASSEMBLY_VERSION="${NUMERIC}.0" # 2.0.0.0
echo "SEMVER=$SEMVER"
echo "ASSEMBLY_VERSION=$ASSEMBLY_VERSION"
dotnet publish "./LANCommander.Launcher.Avalonia/LANCommander.Launcher.Avalonia.csproj" \
-c "${{ inputs.build_configuration }}" \
--self-contained \
--runtime "${{ inputs.build_runtime }}" \
-p:Version="$SEMVER" \
-p:AssemblyVersion="$ASSEMBLY_VERSION" \
-p:FileVersion="$ASSEMBLY_VERSION" \
-p:InformationalVersion="$SEMVER" \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:IncludeAllContentForSelfExtract=true \
-p:EnableCompressionInSingleFile=true \
-p:DebugType=embedded
- name: Bundle and Clean
shell: pwsh
run: |
$BasePath = "LANCommander.Launcher.Avalonia/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish"
# Remove unnecessary files
$PathsToRemove = @(
'*.pdb'
)
foreach ($path in $PathsToRemove) {
Remove-Item -Recurse -Force -ErrorAction Continue "$BasePath/$path"
}
- name: Compress Build Output
shell: pwsh
run: |
$compress = @{
Path = "LANCommander.Launcher.Avalonia/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish/*"
DestinationPath = "LANCommander.Launcher.Avalonia-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip"
CompressionLevel = "Fastest"
}
Compress-Archive @compress
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
path: LANCommander.Launcher.Avalonia-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip
name: LANCommander.Launcher.Avalonia-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip

View file

@ -1,125 +0,0 @@
name: LANCommander.Launcher.Legacy
on:
push:
branches: [main, feature/legacy-launcher]
paths:
- 'LANCommander.Launcher.Legacy/**'
- 'LANCommander.SDK.Cpp/**'
- '.github/workflows/LANCommander.Launcher.Legacy.yml'
pull_request:
branches: [main]
paths:
- 'LANCommander.Launcher.Legacy/**'
- 'LANCommander.SDK.Cpp/**'
- '.github/workflows/LANCommander.Launcher.Legacy.yml'
workflow_dispatch:
jobs:
build-win9x:
name: Build (Win9x / MinGW-w64 i686)
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- name: Checkout
uses: actions/checkout@v4
# ---------------------------------------------------------------
# MSYS2 with MinGW32 (i686) toolchain
# ---------------------------------------------------------------
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW32
update: false
install: >-
mingw-w64-i686-gcc
mingw-w64-i686-cmake
mingw-w64-i686-make
make
# ---------------------------------------------------------------
# Vendor dependencies (cJSON, miniz, Allegro 4 source)
# ---------------------------------------------------------------
- name: Download vendor dependencies
shell: pwsh
run: ./setup-vendor.ps1
# ---------------------------------------------------------------
# Build Allegro 4 from source (static, no addons)
# ---------------------------------------------------------------
- name: Build Allegro 4
run: |
ALLEGRO_SRC="LANCommander.Launcher.Legacy/vendor/allegro4/allegro5-4.4.3.1"
ALLEGRO_BUILD="LANCommander.Launcher.Legacy/build-allegro-win9x"
ALLEGRO_PREFIX="$(pwd)/LANCommander.Launcher.Legacy/allegro4-win9x"
cmake -S "$ALLEGRO_SRC" -B "$ALLEGRO_BUILD" \
-G "MinGW Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$ALLEGRO_PREFIX" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DSHARED=OFF \
-DWANT_EXAMPLES=OFF \
-DWANT_TOOLS=OFF \
-DWANT_TESTS=OFF \
-DWANT_ALLEGROGL=OFF \
-DWANT_LOADPNG=OFF \
-DWANT_LOGG=OFF \
-DWANT_JPGALLEG=OFF \
-DWANT_FRAMEWORKS=OFF
mingw32-make -C "$ALLEGRO_BUILD" -j$(nproc)
mingw32-make -C "$ALLEGRO_BUILD" install
# ---------------------------------------------------------------
# Build the launcher
# ---------------------------------------------------------------
- name: Build launcher
run: |
LAUNCHER_DIR="LANCommander.Launcher.Legacy"
LAUNCHER_BUILD="$LAUNCHER_DIR/build-win9x"
ALLEGRO_PREFIX="$(pwd)/$LAUNCHER_DIR/allegro4-win9x"
cmake -S "$LAUNCHER_DIR" -B "$LAUNCHER_BUILD" \
-G "MinGW Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DALLEGRO_STATIC=ON \
-DTARGET_WIN9X=ON \
-DALLEGRO_ROOT="$ALLEGRO_PREFIX"
mingw32-make -C "$LAUNCHER_BUILD" -j$(nproc)
# ---------------------------------------------------------------
# Package
# ---------------------------------------------------------------
- name: Package artifacts
run: |
mkdir -p out-win9x
LAUNCHER_EXE=$(find LANCommander.Launcher.Legacy/build-win9x -name "launcher.exe" | head -1)
cp "$LAUNCHER_EXE" out-win9x/LANCommander.exe
strip out-win9x/LANCommander.exe
# Bundle GDI+ redistributable (MinGW CRT is statically linked)
if [ -f "$MINGW_PREFIX/bin/gdiplus.dll" ]; then
cp "$MINGW_PREFIX/bin/gdiplus.dll" out-win9x/
echo "Bundled: gdiplus.dll"
fi
# Show PE info for verification
objdump -p out-win9x/LANCommander.exe | grep -i "Version\|Subsystem" || true
echo ""
ls -lh out-win9x/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: LANCommander-Legacy-Win9x
path: out-win9x/
if-no-files-found: error

View file

@ -1,127 +0,0 @@
name: LANCommander Launcher Tests — PR Visual Diff Comment
on:
workflow_call:
inputs:
pr_number:
description: 'Pull request number to comment on'
required: true
type: string
artifact_run_id:
description: 'Workflow run ID that produced the launcher-visual-artifacts artifact'
required: true
type: string
permissions:
contents: write # push diff PNGs to the gh-visual-diffs branch
pull-requests: write # post / update the comment
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Checkout for branch push
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download visual artifacts
uses: actions/download-artifact@v4
with:
name: launcher-visual-artifacts
path: visual
run-id: ${{ inputs.artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Read regression manifest
id: manifest
run: |
if [ ! -f visual/regressions.json ]; then
echo "no manifest — assuming no visual run"
echo "count=0" >> "$GITHUB_OUTPUT"
exit 0
fi
count=$(jq '.regressed_count' visual/regressions.json)
echo "count=$count" >> "$GITHUB_OUTPUT"
- name: Skip when no regressions
if: steps.manifest.outputs.count == '0'
run: echo "No visual regressions; nothing to comment."
# Push diff/actual/baseline PNGs to an orphan branch so raw.githubusercontent.com
# URLs render inline in the PR comment. One folder per (PR, run).
- name: Push diffs to gh-visual-diffs branch
if: steps.manifest.outputs.count != '0'
env:
PR: ${{ inputs.pr_number }}
RUN: ${{ inputs.artifact_run_id }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Worktree off an orphan branch so we never touch the source tree.
if git ls-remote --exit-code --heads origin gh-visual-diffs >/dev/null; then
git fetch origin gh-visual-diffs
git worktree add /tmp/visual gh-visual-diffs
else
git worktree add --detach /tmp/visual
cd /tmp/visual
git checkout --orphan gh-visual-diffs
git rm -rf . 2>/dev/null || true
cd -
fi
DEST="/tmp/visual/pr-${PR}/run-${RUN}"
mkdir -p "$DEST"
cp -r visual/diffs "$DEST/" 2>/dev/null || true
cp -r visual/screenshots "$DEST/" 2>/dev/null || true
cp -r visual/baselines "$DEST/" 2>/dev/null || true
cd /tmp/visual
git add -A
git -c user.name=github-actions -c user.email=github-actions@github.com \
commit -m "Visual diffs for PR #${PR} run ${RUN}"
git push origin gh-visual-diffs
- name: Post / update PR comment
if: steps.manifest.outputs.count != '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ inputs.pr_number }}
RUN: ${{ inputs.artifact_run_id }}
REPO: ${{ github.repository }}
MARKER: '<!-- launcher-visual-diff-comment -->'
run: |
set -euo pipefail
base="https://raw.githubusercontent.com/${REPO}/gh-visual-diffs/pr-${PR}/run-${RUN}"
body=$(mktemp)
{
echo "$MARKER"
echo "## :art: Launcher visual regressions"
echo
echo "$(jq '.regressed_count' visual/regressions.json) baseline(s) drifted in [run ${RUN}](https://github.com/${REPO}/actions/runs/${RUN})."
echo
jq -r '.regressions[] | .name' visual/regressions.json | while read -r name; do
echo "<details><summary><strong>${name}</strong></summary>"
echo
echo "| Baseline | Actual | Diff |"
echo "| --- | --- | --- |"
echo "| ![baseline](${base}/baselines/${name}.png) | ![actual](${base}/screenshots/${name}.png) | ![diff](${base}/diffs/${name}.diff.png) |"
echo
echo "</details>"
echo
done
echo "_If these changes are intentional, run the **Update Visual Baselines** workflow to refresh._"
} >> "$body"
# Find an existing comment with our marker; update it if present.
existing=$(gh api repos/${REPO}/issues/${PR}/comments --paginate \
--jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" | head -1)
if [ -n "$existing" ]; then
gh api -X PATCH "repos/${REPO}/issues/comments/${existing}" --field "body=@${body}"
else
gh api -X POST "repos/${REPO}/issues/${PR}/comments" --field "body=@${body}"
fi

View file

@ -1,91 +0,0 @@
name: LANCommander Launcher Tests — Update Visual Baselines
# Operator-triggered: re-renders every visual test on the CI host and commits the
# new screenshots over the committed Baselines/. Only runs when a human dispatches
# it from the Actions tab — never on push or PR — so accidental drift can't slip in.
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to commit refreshed baselines to'
required: true
type: string
default: 'main'
build_dotnet_version:
description: 'Build .NET Version'
required: false
type: string
default: '9.0.102'
permissions:
contents: write
jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
submodules: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ inputs.build_dotnet_version }}
- name: Install rendering prereqs
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
libfontconfig1 libfreetype6 libice6 libsm6 libx11-6 libxcb1 libxext6
- name: Restore + build visual test project
run: |
dotnet restore LANCommander.Launcher.Tests/LANCommander.Launcher.Tests.csproj
dotnet build LANCommander.Launcher.Tests/LANCommander.Launcher.Tests.csproj -c Debug --no-restore
# Run visual tests, ignore failures (regressions are expected here — that's
# the whole point of refreshing). Then promote every captured screenshot
# to be the new baseline.
- name: Capture fresh screenshots
continue-on-error: true
run: |
dotnet test LANCommander.Launcher.Tests/LANCommander.Launcher.Tests.csproj \
-c Debug --no-build --no-restore \
--logger "console;verbosity=normal"
- name: Promote captures to baselines
run: |
set -euo pipefail
src=LANCommander.Launcher.Tests/bin/Debug/net10.0/Screenshots
dst=LANCommander.Launcher.Tests/Baselines
if [ ! -d "$src" ]; then
echo "::error::No screenshots captured at $src — visual tests didn't run."
exit 1
fi
mkdir -p "$dst"
cp -v "$src"/*.png "$dst"/
echo "## Updated baselines" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
ls -1 "$dst"/*.png | xargs -n1 basename | sed 's/^/- /' >> "$GITHUB_STEP_SUMMARY"
- name: Commit + push refreshed baselines
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git diff --quiet -- LANCommander.Launcher.Tests/Baselines; then
echo "Baselines unchanged — nothing to commit."
exit 0
fi
git add LANCommander.Launcher.Tests/Baselines
git commit -m "Refresh launcher visual baselines from CI run ${GITHUB_RUN_ID}"
git push origin HEAD:${{ inputs.branch }}

View file

@ -1,170 +0,0 @@
name: LANCommander Launcher Tests
on:
workflow_call:
inputs:
build_dotnet_version:
description: 'Build .NET Version'
required: true
type: string
outputs:
visual_diff_count:
description: 'Number of visual baselines that regressed in this run'
value: ${{ jobs.test.outputs.visual_diff_count }}
workflow_dispatch:
inputs:
build_dotnet_version:
description: 'Build .NET Version'
required: false
type: string
default: '9.0.102'
permissions:
contents: read
jobs:
test:
name: Run Launcher Tests
runs-on: ubuntu-latest
outputs:
visual_diff_count: ${{ steps.summarize.outputs.visual_diff_count }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ inputs.build_dotnet_version }}
# Avalonia headless rendering uses Skia + the Inter font. Install
# font + GPU prereqs so screenshots render consistently.
- name: Install rendering prereqs
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
libfontconfig1 libfreetype6 libice6 libsm6 libx11-6 libxcb1 libxext6
- name: Restore
run: dotnet restore
# ---- Unit + integration test projects ---------------------------------
# Services.Tests and IntegrationTests build cleanly without the
# Server/UI npm+pwsh chain. Launcher.Tests likewise just needs Skia.
- name: Build test projects
run: |
dotnet build LANCommander.Launcher.Services.Tests/LANCommander.Launcher.Services.Tests.csproj -c Debug --no-restore
dotnet build LANCommander.Launcher.IntegrationTests/LANCommander.Launcher.IntegrationTests.csproj -c Debug --no-restore
dotnet build LANCommander.Launcher.Tests/LANCommander.Launcher.Tests.csproj -c Debug --no-restore
- name: Run Services unit tests
run: |
dotnet test LANCommander.Launcher.Services.Tests/LANCommander.Launcher.Services.Tests.csproj \
-c Debug --no-build --no-restore \
--logger "trx;LogFileName=services.trx" \
--logger "console;verbosity=normal" \
--results-directory test-results \
--collect:"XPlat Code Coverage"
- name: Run integration tests
run: |
dotnet test LANCommander.Launcher.IntegrationTests/LANCommander.Launcher.IntegrationTests.csproj \
-c Debug --no-build --no-restore \
--logger "trx;LogFileName=integration.trx" \
--logger "console;verbosity=normal" \
--results-directory test-results \
--collect:"XPlat Code Coverage"
# Visual tests are continued-on-error so a baseline regression doesn't
# mask any earlier failure and so we always reach the artifact-upload
# + summary steps below. The summary step re-asserts the failure for CI.
- name: Run visual tests
id: visual_tests
continue-on-error: true
run: |
dotnet test LANCommander.Launcher.Tests/LANCommander.Launcher.Tests.csproj \
-c Debug --no-build --no-restore \
--logger "trx;LogFileName=visual.trx" \
--logger "console;verbosity=normal" \
--results-directory test-results
# ---- Visual artifact collection --------------------------------------
# Aggregate every screenshot + diff PNG into top-level folders so the
# PR-comment job downstream can find them by predictable name.
- name: Collect visual artifacts
if: always()
run: |
mkdir -p visual/screenshots visual/diffs visual/baselines
cp -r LANCommander.Launcher.Tests/bin/Debug/net10.0/Screenshots/. visual/screenshots/ 2>/dev/null || true
cp -r LANCommander.Launcher.Tests/bin/Debug/net10.0/Diffs/. visual/diffs/ 2>/dev/null || true
cp -r LANCommander.Launcher.Tests/Baselines/. visual/baselines/ 2>/dev/null || true
- name: Summarize visual diffs
id: summarize
if: always()
run: |
shopt -s nullglob
diffs=(visual/diffs/*.diff.png)
count=${#diffs[@]}
echo "visual_diff_count=$count" >> "$GITHUB_OUTPUT"
echo "## Visual test summary" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "$count baseline(s) regressed." >> "$GITHUB_STEP_SUMMARY"
# JSON manifest the PR-comment job consumes
python3 - "$count" <<'PY' > visual/regressions.json
import json, os, sys
count = int(sys.argv[1])
diffs = sorted(os.listdir("visual/diffs")) if os.path.isdir("visual/diffs") else []
data = {
"regressed_count": count,
"regressions": [
{
"name": d.replace(".diff.png", ""),
"diff": f"visual/diffs/{d}",
"actual": f"visual/screenshots/{d.replace('.diff.png', '.png')}",
"baseline": f"visual/baselines/{d.replace('.diff.png', '.png')}",
}
for d in diffs
],
}
print(json.dumps(data, indent=2))
PY
cat visual/regressions.json
- name: Upload visual artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: launcher-visual-artifacts
path: |
visual/screenshots/**
visual/diffs/**
visual/baselines/**
visual/regressions.json
if-no-files-found: warn
retention-days: 14
- name: Upload TRX + coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: launcher-test-results
path: test-results/**
if-no-files-found: warn
retention-days: 14
# Re-assert the visual test outcome so CI fails when baselines diverge,
# after artifacts are guaranteed uploaded.
- name: Fail if visual tests regressed
if: always() && steps.visual_tests.outcome == 'failure'
run: |
echo "::error::Visual tests reported regressions. See the launcher-visual-artifacts artifact."
exit 1

View file

@ -45,18 +45,27 @@ env:
jobs:
build:
runs-on: ${{ inputs.build_platform == 'Linux' && inputs.build_arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: frabert/replace-string-action@v2
name: Swap Path Backslashes
id: swap_path_backslashes
with:
string: '${{ github.workspace }}'
pattern: '\\'
replace-with: '/'
flags: g
# Checkout code
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: true
# .NET Setup
# .NET Setup and Caching
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
@ -65,7 +74,26 @@ jobs:
- name: Restore dependencies
run: dotnet restore --locked-mode
- name: Publish Launcher
# Node.js Setup and Caching
- name: Setup Node.js
uses: actions/setup-node@v3.8.1
with:
node-version: '20'
- name: Install Node Packages
run: |
npm install --prefix ./LANCommander.UI
npm install --prefix ./LANCommander.Launcher
- name: Generate PowerShell Completions
run: dotnet run --project ./LANCommander.CompletionGenerator/LANCommander.CompletionGenerator.csproj -- ./LANCommander.UI/Components/MonacoCodeEditor/PowerShellCompletions.g.ts
- name: Package Frontend
run: |
npm run package --prefix ./LANCommander.UI
npm run package --prefix ./LANCommander.Launcher
- name: Publish Updater and Launcher
run: |
# Strip leading 'v' if present
RAW_VERSION="${{ inputs.version_tag }}" # e.g. v2.0.0-rc1
@ -78,6 +106,15 @@ jobs:
echo "SEMVER=$SEMVER"
echo "ASSEMBLY_VERSION=$ASSEMBLY_VERSION"
dotnet publish "./LANCommander.AutoUpdater/LANCommander.AutoUpdater.csproj" \
-c "${{ inputs.build_configuration }}" \
--self-contained \
--runtime "${{ inputs.build_runtime }}" \
-p:Version="$SEMVER" \
-p:AssemblyVersion="$ASSEMBLY_VERSION" \
-p:FileVersion="$ASSEMBLY_VERSION" \
-p:InformationalVersion="$SEMVER"
dotnet publish "./LANCommander.Launcher/LANCommander.Launcher.csproj" \
-c "${{ inputs.build_configuration }}" \
--self-contained \
@ -85,219 +122,48 @@ jobs:
-p:Version="$SEMVER" \
-p:AssemblyVersion="$ASSEMBLY_VERSION" \
-p:FileVersion="$ASSEMBLY_VERSION" \
-p:InformationalVersion="$SEMVER" \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:IncludeAllContentForSelfExtract=true \
-p:EnableCompressionInSingleFile=true \
-p:DebugType=embedded
-p:InformationalVersion="$SEMVER"
- name: Bundle libvlc (Linux)
if: inputs.build_platform == 'Linux'
shell: bash
run: |
PUBLISH_DIR="LANCommander.Launcher/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish"
VLC_DIR="$PUBLISH_DIR/libvlc/${{ inputs.build_runtime }}"
mkdir -p "$VLC_DIR"
if [ "${{ inputs.build_arch }}" = "arm64" ]; then
# Enable arm64 multiarch and add Ubuntu Ports repository so apt can
# download arm64 packages on this x64 runner.
sudo dpkg --add-architecture arm64
CODENAME=$(lsb_release -cs)
echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${CODENAME} main restricted universe" \
| sudo tee /etc/apt/sources.list.d/ubuntu-ports-arm64.list > /dev/null
echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${CODENAME}-updates main restricted universe" \
| sudo tee -a /etc/apt/sources.list.d/ubuntu-ports-arm64.list > /dev/null
sudo apt-get update -qq
sudo apt-get download libvlc5:arm64 libvlccore9:arm64 vlc-plugin-base:arm64
mkdir -p vlc-extracted
for deb in libvlc5_*arm64.deb libvlccore9_*arm64.deb vlc-plugin-base_*arm64.deb; do
dpkg -x "$deb" vlc-extracted/
done
LIB_DIR="vlc-extracted/usr/lib/aarch64-linux-gnu"
PLUGINS_SRC="$LIB_DIR/vlc/plugins"
else
sudo apt-get install -y --no-install-recommends libvlc5 libvlccore9 vlc-plugin-base
LIB_DIR="/usr/lib/x86_64-linux-gnu"
PLUGINS_SRC="$LIB_DIR/vlc/plugins"
fi
# Copy the two core shared libraries, dereferencing symlinks so their
# content is preserved correctly when zipped.
for lib in libvlc.so.5 libvlccore.so.9; do
src=$(find "$LIB_DIR" -maxdepth 1 -name "${lib}*" | sort | head -1)
if [ -n "$src" ]; then
cp -L "$src" "$VLC_DIR/$lib"
echo "Bundled: $lib"
else
echo "WARNING: $lib not found in $LIB_DIR" >&2
fi
done
# Copy VLC plugins (vlc-plugin-base covers common codecs and demuxers)
if [ -d "$PLUGINS_SRC" ]; then
mkdir -p "$VLC_DIR/plugins"
cp -rL "$PLUGINS_SRC/." "$VLC_DIR/plugins/"
echo "Bundled $(find "$VLC_DIR/plugins" -name "*.so" | wc -l) plugin(s)"
fi
dotnet publish "./LANCommander.Launcher.CLI/LANCommander.Launcher.CLI.csproj" \
-c "${{ inputs.build_configuration }}" \
--self-contained \
--runtime "${{ inputs.build_runtime }}" \
-p:Version="$SEMVER" \
-p:AssemblyVersion="$ASSEMBLY_VERSION" \
-p:FileVersion="$ASSEMBLY_VERSION" \
-p:InformationalVersion="$SEMVER"
- name: Bundle and Clean
shell: pwsh
run: |
$BasePath = "LANCommander.Launcher/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish"
# Remove unnecessary files
Copy-Item -Force -Recurse -Verbose LANCommander.AutoUpdater/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish/* LANCommander.Launcher/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish/
Copy-Item -Force -Recurse -Verbose LANCommander.Launcher.CLI/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish/* LANCommander.Launcher/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish/
# Remove unnecessary files in a single operation
$PathsToRemove = @(
'*.pdb'
'wwwroot/_content/BootstrapBlazor.PdfReader/compat',
'wwwroot/_content/BootstrapBlazor.PdfReader/2.*',
'wwwroot/_content/BootstrapBlazor.PdfReader/build/pdf.sandbox.js',
'wwwroot/_content/BootstrapBlazor.PdfReader/build/*.map',
'wwwroot/_content/BootstrapBlazor.PdfReader/web/*.map',
'wwwroot/_content/AntDesign/less',
'wwwroot/_content/BlazorMonaco/lib/monaco-editor/min-maps',
'wwwroot/Identity/lib/bootstrap',
'LANCommander.ico',
'LANCommanderDark.ico',
'package-lock.json',
'package.json',
'*.pdb',
'hostfxr.dll.bak',
'Libraries/locales'
)
$BasePath = "LANCommander.Launcher/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish"
foreach ($path in $PathsToRemove) {
Remove-Item -Recurse -Force -ErrorAction Continue "$BasePath/$path"
}
- name: Bundle macOS .app
if: inputs.build_platform == 'macOS'
shell: bash
run: |
set -euo pipefail
# Strip leading 'v' and reduce to numeric x.y.z for the bundle version keys.
RAW_VERSION="${{ inputs.version_tag }}"
SEMVER="${RAW_VERSION#v}"
NUMERIC="${SEMVER%%-*}"
APP_NAME="LANCommander Launcher"
EXECUTABLE="LANCommander.Launcher"
BUNDLE_ID="app.lancommander.launcher"
ICON_SRC="LANCommander.Launcher/Assets/icon.icns"
PUBLISH_DIR="LANCommander.Launcher/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish"
APP_DIR="${EXECUTABLE}.app"
# Lay out the bundle skeleton.
mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
cp -a "$PUBLISH_DIR/." "$APP_DIR/Contents/MacOS/"
cp "$ICON_SRC" "$APP_DIR/Contents/Resources/AppIcon.icns"
# Write Info.plist.
cat > "$APP_DIR/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE}</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>${NUMERIC}</string>
<key>CFBundleVersion</key>
<string>${SEMVER}</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
PLIST
# Ensure the entrypoint is executable, then zip while preserving the
# permission bits (Compress-Archive drops the exec bit).
chmod +x "$APP_DIR/Contents/MacOS/${EXECUTABLE}"
zip -ry "LANCommander.Launcher-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip" "$APP_DIR"
- name: Build AppImage (Linux)
if: inputs.build_platform == 'Linux'
shell: bash
run: |
set -euo pipefail
PUBLISH_DIR="LANCommander.Launcher/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish"
APP_NAME="LANCommander.Launcher"
APPDIR="AppDir"
# appimagetool / runtime architecture identifiers
if [ "${{ inputs.build_arch }}" = "arm64" ]; then
AI_ARCH="aarch64"
else
AI_ARCH="x86_64"
fi
export ARCH="$AI_ARCH"
# --- Assemble the AppDir ---------------------------------------------
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/applications" \
"$APPDIR/usr/share/icons/hicolor/scalable/apps"
cp -r "$PUBLISH_DIR/." "$APPDIR/usr/bin/"
chmod +x "$APPDIR/usr/bin/$APP_NAME"
# Icon (use the prebuilt 256x256 project icon)
ICON_SRC="LANCommander.Launcher/Assets/icon.png"
ICON_DIR="$APPDIR/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$ICON_DIR"
cp "$ICON_SRC" "$ICON_DIR/lancommander.png"
cp "$ICON_DIR/lancommander.png" "$APPDIR/lancommander.png"
ln -sf lancommander.png "$APPDIR/.DirIcon"
# Desktop entry
cat > "$APPDIR/usr/share/applications/lancommander.desktop" <<'EOF'
[Desktop Entry]
Type=Application
Name=LANCommander Launcher
Comment=LANCommander Launcher
Exec=LANCommander.Launcher
Icon=lancommander
Categories=Game;
Terminal=false
EOF
cp "$APPDIR/usr/share/applications/lancommander.desktop" "$APPDIR/lancommander.desktop"
# AppRun entry point
cat > "$APPDIR/AppRun" <<'EOF'
#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
export PATH="${HERE}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${HERE}/usr/bin:${LD_LIBRARY_PATH:-}"
exec "${HERE}/usr/bin/LANCommander.Launcher" "$@"
EOF
chmod +x "$APPDIR/AppRun"
# --- Fetch appimagetool ----------------------------------------------
TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${AI_ARCH}.AppImage"
curl -L -o appimagetool "$TOOL_URL"
chmod +x appimagetool
# --- Build the AppImage ----------------------------------------------
OUT="LANCommander.Launcher-Linux-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.AppImage"
./appimagetool --appimage-extract-and-run "$APPDIR" "$OUT"
echo "Produced $OUT"
- name: Upload AppImage Artifact (Linux)
if: inputs.build_platform == 'Linux'
uses: actions/upload-artifact@v4
with:
path: LANCommander.Launcher-Linux-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.AppImage
name: LANCommander.Launcher-Linux-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.AppImage
- name: Compress Build Output
if: inputs.build_platform != 'macOS'
shell: pwsh
run: |
$compress = @{
@ -311,4 +177,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
path: LANCommander.Launcher-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip
name: LANCommander.Launcher-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip
name: LANCommander.Launcher-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip

View file

@ -255,6 +255,84 @@ jobs:
build_platform: Windows
build_configuration: Debug
build_launcher_avalonia_linux_arm64:
needs: [prep]
if: needs.prep.outputs.changed == 'true' && github.repository_owner == 'LANCommander'
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-arm64
build_arch: arm64
build_platform: Linux
build_configuration: Debug
build_launcher_avalonia_linux_x64:
needs: [prep]
if: needs.prep.outputs.changed == 'true' && github.repository_owner == 'LANCommander'
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-x64
build_arch: x64
build_platform: Linux
build_configuration: Debug
build_launcher_avalonia_osx_arm64:
needs: [prep]
if: needs.prep.outputs.changed == 'true' && github.repository_owner == 'LANCommander'
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-arm64
build_arch: arm64
build_platform: macOS
build_configuration: Debug
build_launcher_avalonia_osx_x64:
needs: [prep]
if: needs.prep.outputs.changed == 'true' && github.repository_owner == 'LANCommander'
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-x64
build_arch: x64
build_platform: macOS
build_configuration: Debug
build_launcher_avalonia_win_arm64:
needs: [prep]
if: needs.prep.outputs.changed == 'true' && github.repository_owner == 'LANCommander'
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-arm64
build_arch: arm64
build_platform: Windows
build_configuration: Debug
build_launcher_avalonia_win_x64:
needs: [prep]
if: needs.prep.outputs.changed == 'true' && github.repository_owner == 'LANCommander'
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Debug
# --------------------------------------------------------------------------
# 3) FINALIZE: if changes == 'true', gather artifacts + push Docker:nightly
# --------------------------------------------------------------------------
@ -267,6 +345,12 @@ jobs:
- build_server_osx_x64
- build_server_win_arm64
- build_server_win_x64
- build_launcher_linux_arm64
- build_launcher_linux_x64
- build_launcher_osx_arm64
- build_launcher_osx_x64
- build_launcher_win_arm64
- build_launcher_win_x64
if: needs.prep.outputs.changed == 'true' && github.repository_owner == 'LANCommander'
runs-on: ubuntu-latest
steps:
@ -359,12 +443,12 @@ jobs:
needs:
- prep
- publish_docker_image
- build_launcher_linux_arm64
- build_launcher_linux_x64
- build_launcher_osx_arm64
- build_launcher_osx_x64
- build_launcher_win_arm64
- build_launcher_win_x64
- build_launcher_avalonia_linux_arm64
- build_launcher_avalonia_linux_x64
- build_launcher_avalonia_osx_arm64
- build_launcher_avalonia_osx_x64
- build_launcher_avalonia_win_arm64
- build_launcher_avalonia_win_x64
steps:
- name: Check out code
uses: actions/checkout@v4
@ -444,16 +528,40 @@ jobs:
name: LANCommander.Launcher-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Linux ARM64 AppImage
- name: Download Launcher Avalonia Linux ARM64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher-Linux-arm64-v${{ needs.prep.outputs.version_tag }}.AppImage
name: LANCommander.Launcher.Avalonia-Linux-arm64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Linux x64 AppImage
- name: Download Launcher Avalonia Linux x64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher-Linux-x64-v${{ needs.prep.outputs.version_tag }}.AppImage
name: LANCommander.Launcher.Avalonia-Linux-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Avalonia macOS ARM64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher.Avalonia-macOS-arm64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Avalonia macOS x64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher.Avalonia-macOS-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Avalonia Windows ARM64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher.Avalonia-Windows-arm64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Avalonia Windows x64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher.Avalonia-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Create or update nightly release

View file

@ -1,229 +1,225 @@
name: LANCommander Pull Request
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
workflow_dispatch:
permissions:
contents: write
packages: read
checks: write
pull-requests: write
jobs:
prep:
runs-on: ubuntu-latest
outputs:
version_semver: ${{ steps.set_version.outputs.version_semver }}
version_tag: ${{ steps.set_version.outputs.version_tag }}
build_dotnet_version: 9.0.102
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine build metadata
id: set_version
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_NUMBER: ${{ github.run_number }}
run: |
TIMESTAMP=$(date -u +"%Y%m%d%H%M")
TIME_COMPONENT=$(date -u +"%H%M")
TIME_COMPONENT=$((10#$TIME_COMPONENT))
if [ -n "$PR_NUMBER" ]; then
BUILD_COMPONENT=$((PR_NUMBER % 65535))
if [ "$BUILD_COMPONENT" -eq 0 ]; then
BUILD_COMPONENT=1
fi
VERSION_SEMVER="0.0.${BUILD_COMPONENT}.${TIME_COMPONENT}"
VERSION_TAG="0.0.${BUILD_COMPONENT}-pr.${PR_NUMBER}.${TIMESTAMP}"
else
BUILD_COMPONENT=$((RUN_NUMBER % 65535))
if [ "$BUILD_COMPONENT" -eq 0 ]; then
BUILD_COMPONENT=1
fi
VERSION_SEMVER="0.0.${BUILD_COMPONENT}.${TIME_COMPONENT}"
VERSION_TAG="0.0.${BUILD_COMPONENT}-ci.${RUN_NUMBER}.${TIMESTAMP}"
fi
echo "version_semver=$VERSION_SEMVER" >> $GITHUB_OUTPUT
echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT
ui_tests:
needs: [prep]
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ needs.prep.outputs.build_dotnet_version }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Node packages
run: |
npm install --prefix ./LANCommander.UI
npm install --prefix ./LANCommander.Server
# The Monaco editor's PowerShell completions are generated (gitignored) and
# required by the frontend webpack build. The in-build MSBuild target uses
# Windows-style paths, so generate explicitly here for the Linux runner.
- name: Generate PowerShell Completions
run: dotnet run --project ./LANCommander.CompletionGenerator/LANCommander.CompletionGenerator.csproj -- ./LANCommander.UI/Components/MonacoCodeEditor/PowerShellCompletions.g.ts
- name: Restore dependencies
run: dotnet restore LANCommander.Server.UI.Tests
- name: Build test project
run: dotnet build LANCommander.Server.UI.Tests --no-restore --configuration Release
- name: Install Playwright browsers
run: pwsh LANCommander.Server.UI.Tests/bin/Release/net10.0/playwright.ps1 install --with-deps chromium
- name: Run UI tests
run: dotnet test LANCommander.Server.UI.Tests --no-build --configuration Release --logger "trx;LogFileName=ui-test-results.trx" --results-directory ./TestResults
env:
SCREENSHOT_DIR: ${{ github.workspace }}/TestResults/Screenshots
- name: Test report
if: always()
uses: dorny/test-reporter@v1
with:
name: UI Test Results
path: ./TestResults/ui-test-results.trx
reporter: dotnet-trx
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: ui-test-results
path: ./TestResults
retention-days: 7
build_server_linux_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-arm64
build_arch: arm64
build_platform: Linux
build_configuration: Debug
build_server_linux_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-x64
build_arch: x64
build_platform: Linux
build_configuration: Debug
build_server_osx_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-arm64
build_arch: arm64
build_platform: macOS
build_configuration: Debug
build_server_osx_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-x64
build_arch: x64
build_platform: macOS
build_configuration: Debug
build_server_win_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-arm64
build_arch: arm64
build_platform: Windows
build_configuration: Debug
build_server_win_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Debug
build_launcher_linux_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-x64
build_arch: x64
build_platform: Linux
build_configuration: Debug
build_launcher_win_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Debug
launcher_tests:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.Tests.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
launcher_tests_pr_comment:
# Always run after the test job — even on failure — so visual regressions
# show up as a PR comment instead of being buried in the artifact.
needs: [launcher_tests]
if: always() && needs.launcher_tests.result != 'cancelled' && github.event.pull_request.number != null
uses: ./.github/workflows/LANCommander.Launcher.Tests.PRComment.yml
with:
pr_number: ${{ github.event.pull_request.number }}
artifact_run_id: ${{ github.run_id }}
name: LANCommander Pull Request
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
workflow_dispatch:
permissions:
contents: write
packages: read
jobs:
prep:
runs-on: ubuntu-latest
outputs:
version_semver: ${{ steps.set_version.outputs.version_semver }}
version_tag: ${{ steps.set_version.outputs.version_tag }}
build_dotnet_version: 9.0.102
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine build metadata
id: set_version
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_NUMBER: ${{ github.run_number }}
run: |
TIMESTAMP=$(date -u +"%Y%m%d%H%M")
TIME_COMPONENT=$(date -u +"%H%M")
TIME_COMPONENT=$((10#$TIME_COMPONENT))
if [ -n "$PR_NUMBER" ]; then
BUILD_COMPONENT=$((PR_NUMBER % 65535))
if [ "$BUILD_COMPONENT" -eq 0 ]; then
BUILD_COMPONENT=1
fi
VERSION_SEMVER="0.0.${BUILD_COMPONENT}.${TIME_COMPONENT}"
VERSION_TAG="0.0.${BUILD_COMPONENT}-pr.${PR_NUMBER}.${TIMESTAMP}"
else
BUILD_COMPONENT=$((RUN_NUMBER % 65535))
if [ "$BUILD_COMPONENT" -eq 0 ]; then
BUILD_COMPONENT=1
fi
VERSION_SEMVER="0.0.${BUILD_COMPONENT}.${TIME_COMPONENT}"
VERSION_TAG="0.0.${BUILD_COMPONENT}-ci.${RUN_NUMBER}.${TIMESTAMP}"
fi
echo "version_semver=$VERSION_SEMVER" >> $GITHUB_OUTPUT
echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT
build_server_linux_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-arm64
build_arch: arm64
build_platform: Linux
build_configuration: Debug
build_server_linux_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-x64
build_arch: x64
build_platform: Linux
build_configuration: Debug
build_server_osx_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-arm64
build_arch: arm64
build_platform: macOS
build_configuration: Debug
build_server_osx_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-x64
build_arch: x64
build_platform: macOS
build_configuration: Debug
build_server_win_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-arm64
build_arch: arm64
build_platform: Windows
build_configuration: Debug
build_server_win_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Server.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Debug
build_launcher_linux_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-arm64
build_arch: arm64
build_platform: Linux
build_configuration: Debug
build_launcher_linux_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-x64
build_arch: x64
build_platform: Linux
build_configuration: Debug
build_launcher_osx_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-arm64
build_arch: arm64
build_platform: macOS
build_configuration: Debug
build_launcher_osx_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-x64
build_arch: x64
build_platform: macOS
build_configuration: Debug
build_launcher_win_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-arm64
build_arch: arm64
build_platform: Windows
build_configuration: Debug
build_launcher_win_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Debug
build_launcher_avalonia_linux_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-x64
build_arch: x64
build_platform: Linux
build_configuration: Debug
build_launcher_avalonia_win_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Debug

View file

@ -1,100 +0,0 @@
name: LANCommander Packager Build
on:
workflow_dispatch:
workflow_call:
inputs:
version_semver:
description: "Semantic Version"
required: true
type: string
version_tag:
description: 'Version Tag'
required: true
type: string
build_dotnet_version:
description: 'Build .NET Version'
required: false
type: string
default: '10.0.x'
build_configuration:
description: 'Build Configuration (Debug/Release)'
required: false
type: string
default: 'Release'
permissions:
contents: write
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/package
jobs:
build:
runs-on: windows-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ inputs.build_dotnet_version }}
- name: Restore dependencies
run: dotnet restore
- name: Publish Packager
shell: pwsh
run: |
# Strip leading 'v' if present
$RawVersion = "${{ inputs.version_tag }}"
$Semver = $RawVersion -replace '^v', ''
# Numeric part only for Assembly/FileVersion
$Numeric = ($Semver -split '-')[0]
$AssemblyVersion = "$Numeric.0"
Write-Host "SEMVER=$Semver"
Write-Host "ASSEMBLY_VERSION=$AssemblyVersion"
dotnet publish "./LANCommander.Packager/LANCommander.Packager.csproj" `
-c "${{ inputs.build_configuration }}" `
--self-contained `
--runtime win-x86 `
-p:Version="$Semver" `
-p:AssemblyVersion="$AssemblyVersion" `
-p:FileVersion="$AssemblyVersion" `
-p:InformationalVersion="$Semver" `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:IncludeAllContentForSelfExtract=true `
-p:EnableCompressionInSingleFile=true `
-p:DebugType=embedded
- name: Clean
shell: pwsh
run: |
$BasePath = "LANCommander.Packager/bin/${{ inputs.build_configuration }}/net10.0/win-x86/publish"
Remove-Item -Recurse -Force -ErrorAction Continue "$BasePath/*.pdb"
- name: Compress Build Output
shell: pwsh
run: |
$compress = @{
Path = "LANCommander.Packager/bin/${{ inputs.build_configuration }}/net10.0/win-x86/publish/*"
DestinationPath = "LANCommander.Packager-Windows-x86-v${{ inputs.version_tag }}.zip"
CompressionLevel = "Fastest"
}
Compress-Archive @compress
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
path: LANCommander.Packager-Windows-x86-v${{ inputs.version_tag }}.zip
name: LANCommander.Packager-Windows-x86-v${{ inputs.version_tag }}.zip

View file

@ -27,7 +27,6 @@ jobs:
outputs:
version_tag: ${{ steps.trim_tag_ref.outputs.replaced }}
version_semver: ${{ steps.trim_tag_ref.outputs.replaced }}
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
build_dotnet_version: 9.0.102
steps:
- name: Check out code
@ -44,16 +43,6 @@ jobs:
pattern: 'refs/tags/v'
replace-with: ''
- name: Check if pre-release
id: check_prerelease
run: |
VERSION="${{ steps.trim_tag_ref.outputs.replaced }}"
if [[ "$VERSION" == *-* ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi
# Server
build_server_linux_arm64:
needs: [prep]
@ -200,14 +189,53 @@ jobs:
build_platform: Windows
build_configuration: Release
# Packager (Windows x86 only)
build_packager:
# Avalonia Launcher
build_launcher_avalonia_linux_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Packager.yml
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: '10.0.x'
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: linux-x64
build_arch: x64
build_platform: Linux
build_configuration: Release
build_launcher_avalonia_osx_arm64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-arm64
build_arch: arm64
build_platform: macOS
build_configuration: Release
build_launcher_avalonia_osx_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: osx-x64
build_arch: x64
build_platform: macOS
build_configuration: Release
build_launcher_avalonia_win_x64:
needs: [prep]
uses: ./.github/workflows/LANCommander.Launcher.Avalonia.yml
with:
build_dotnet_version: ${{ needs.prep.outputs.build_dotnet_version }}
version_semver: ${{ needs.prep.outputs.version_semver }}
version_tag: ${{ needs.prep.outputs.version_tag }}
build_runtime: win-x64
build_arch: x64
build_platform: Windows
build_configuration: Release
build_release:
@ -226,7 +254,10 @@ jobs:
- build_launcher_osx_x64
- build_launcher_win_arm64
- build_launcher_win_x64
- build_packager
- build_launcher_avalonia_linux_x64
- build_launcher_avalonia_osx_arm64
- build_launcher_avalonia_osx_x64
- build_launcher_avalonia_win_x64
steps:
- name: Create Temp Directory
@ -268,7 +299,6 @@ jobs:
name: LANCommander.Server-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
# Launcher artifacts
- name: Download Launcher Linux ARM64
uses: actions/download-artifact@v4
with:
@ -305,22 +335,29 @@ jobs:
name: LANCommander.Launcher-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Linux ARM64 AppImage
# Avalonia Launcher artifacts
- name: Download Avalonia Launcher Linux x64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher-Linux-arm64-v${{ needs.prep.outputs.version_tag }}.AppImage
name: LANCommander.Launcher.Avalonia-Linux-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Launcher Linux x64 AppImage
- name: Download Avalonia Launcher macOS ARM64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher-Linux-x64-v${{ needs.prep.outputs.version_tag }}.AppImage
name: LANCommander.Launcher.Avalonia-macOS-arm64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Packager Windows x86
- name: Download Avalonia Launcher macOS x64
uses: actions/download-artifact@v4
with:
name: LANCommander.Packager-Windows-x86-v${{ needs.prep.outputs.version_tag }}.zip
name: LANCommander.Launcher.Avalonia-macOS-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Download Avalonia Launcher Windows x64
uses: actions/download-artifact@v4
with:
name: LANCommander.Launcher.Avalonia-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
path: artifacts
- name: Debug - List Artifact Files
@ -334,7 +371,6 @@ jobs:
name: v${{ needs.prep.outputs.version_tag }}
generate_release_notes: true
draft: true
prerelease: ${{ needs.prep.outputs.is_prerelease == 'true' }}
files: |
artifacts/LANCommander.Server-Windows-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Server-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
@ -342,15 +378,16 @@ jobs:
artifacts/LANCommander.Server-Linux-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Server-macOS-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Server-macOS-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Linux-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Linux-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-macOS-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-macOS-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Windows-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Linux-arm64-v${{ needs.prep.outputs.version_tag }}.AppImage
artifacts/LANCommander.Launcher-Linux-x64-v${{ needs.prep.outputs.version_tag }}.AppImage
artifacts/LANCommander.Packager-Windows-x86-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Linux-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-Linux-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-macOS-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher-macOS-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher.Avalonia-Linux-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher.Avalonia-macOS-arm64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher.Avalonia-macOS-x64-v${{ needs.prep.outputs.version_tag }}.zip
artifacts/LANCommander.Launcher.Avalonia-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip
- name: Checkout Repo for Docker build
uses: actions/checkout@v4
@ -420,9 +457,10 @@ jobs:
file: ./LANCommander.Server/Dockerfile
push: true
platforms: linux/amd64
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:latest
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:v${{ needs.prep.outputs.version_tag }}
${{ needs.prep.outputs.is_prerelease == 'true' && format('{0}/{1}:prerelease', env.REGISTRY, env.IMAGE_NAME) || format('{0}/{1}:latest', env.REGISTRY, env.IMAGE_NAME) }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:latest
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,8 +1,9 @@
name: LANCommander SDK Release
on:
release:
types: [published]
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
permissions:
contents: write
@ -12,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
version_tag: ${{ steps.trim_tag_ref.outputs.replaced }}
version_semver: ${{ steps.extract_semver.outputs.replaced }}
version_semver: ${{ steps.trim_tag_ref.outputs.replaced }}
steps:
- uses: frabert/replace-string-action@v2
name: Trim Tag Ref
@ -22,14 +23,6 @@ jobs:
pattern: 'refs/tags/v'
replace-with: ''
- uses: frabert/replace-string-action@v2
name: Extract SemVer (strip prerelease suffix for AssemblyVersion)
id: extract_semver
with:
string: '${{ steps.trim_tag_ref.outputs.replaced }}'
pattern: '-.*$'
replace-with: ''
publish:
needs: prep
runs-on: ubuntu-latest

View file

@ -45,7 +45,7 @@ env:
jobs:
build:
runs-on: ${{ inputs.build_platform == 'Linux' && inputs.build_arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -116,12 +116,6 @@ jobs:
-p:FileVersion="$ASSEMBLY_VERSION" \
-p:InformationalVersion="$SEMVER"
# Disable NetBeauty on ARM64 runners — nbeauty2 only ships x64 native binaries
DISABLE_BEAUTY="False"
if [ "${{ inputs.build_arch }}" = "arm64" ] && [ "${{ inputs.build_platform }}" = "Linux" ]; then
DISABLE_BEAUTY="True"
fi
dotnet publish "./LANCommander.Server/LANCommander.Server.csproj" \
-c "${{ inputs.build_configuration }}" \
--self-contained \
@ -129,8 +123,7 @@ jobs:
-p:Version="$SEMVER" \
-p:AssemblyVersion="$ASSEMBLY_VERSION" \
-p:FileVersion="$ASSEMBLY_VERSION" \
-p:InformationalVersion="$SEMVER" \
-p:DisableBeauty="$DISABLE_BEAUTY"
-p:InformationalVersion="$SEMVER"
- name: Bundle and Clean
@ -162,69 +155,7 @@ jobs:
Remove-Item -Recurse -Force -ErrorAction Continue "$BasePath/$path"
}
- name: Bundle macOS .app
if: inputs.build_platform == 'macOS'
shell: bash
run: |
set -euo pipefail
# Strip leading 'v' and reduce to numeric x.y.z for the bundle version keys.
RAW_VERSION="${{ inputs.version_tag }}"
SEMVER="${RAW_VERSION#v}"
NUMERIC="${SEMVER%%-*}"
APP_NAME="LANCommander Server"
EXECUTABLE="LANCommander.Server"
BUNDLE_ID="app.lancommander.server"
ICON_SRC="LANCommander.Server/icon.icns"
PUBLISH_DIR="LANCommander.Server/bin/${{ inputs.build_configuration }}/net10.0/${{ inputs.build_runtime }}/publish"
APP_DIR="${EXECUTABLE}.app"
# Lay out the bundle skeleton.
mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
cp -a "$PUBLISH_DIR/." "$APP_DIR/Contents/MacOS/"
cp "$ICON_SRC" "$APP_DIR/Contents/Resources/AppIcon.icns"
# Write Info.plist.
cat > "$APP_DIR/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE}</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>${NUMERIC}</string>
<key>CFBundleVersion</key>
<string>${SEMVER}</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
PLIST
# Ensure the entrypoint is executable, then zip while preserving the
# permission bits (Compress-Archive drops the exec bit).
chmod +x "$APP_DIR/Contents/MacOS/${EXECUTABLE}"
zip -ry "LANCommander.Server-${{ inputs.build_platform }}-${{ inputs.build_arch }}-v${{ inputs.version_tag }}.zip" "$APP_DIR"
- name: Compress Build Output
if: inputs.build_platform != 'macOS'
shell: pwsh
run: |
$compress = @{

View file

@ -12,7 +12,7 @@ jobs:
arch: ['x64', 'arm64']
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
shell: pwsh
@ -33,19 +33,13 @@ jobs:
# Install Inno Setup
- name: Install Inno Setup
run: |
curl -L -o innosetup.exe https://github.com/jrsoftware/issrc/releases/download/is-6_7_3/innosetup-6.7.3.exe
curl -L -o innosetup.exe https://files.jrsoftware.org/is/6/innosetup-6.2.2.exe
.\innosetup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
shell: cmd
# Create Inno Setup script
- name: Create installer script
run: |
$appId = if ('${{ matrix.app }}' -eq 'Server') {
'2C58E237-1D69-42A0-B702-F995B75B8A5E'
} else {
'A3D4F8E1-7B2C-4E5D-9F1A-6C8B3D7E2F4A'
}
@"
#define MyAppName "LANCommander ${{ matrix.app }}"
#define MyAppVersion "${{ env.VERSION }}"
@ -55,7 +49,7 @@ jobs:
#define Architecture "${{ matrix.arch }}"
[Setup]
AppId={{$appId}
AppId={{$(New-Guid)}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
@ -100,6 +94,7 @@ jobs:
strategy:
matrix:
app: ['Server', 'Launcher']
arch: ['x64', 'arm64']
steps:
- name: Get version
shell: pwsh
@ -108,14 +103,8 @@ jobs:
$version = $tag.TrimStart('v')
echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install wingetcreate
shell: pwsh
run: |
Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe
- name: Submit package to Windows Package Manager Community Repository
run: |
$x64Url = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/LANCommander.${{ matrix.app }}-${env:VERSION}-x64-Setup.exe"
$arm64Url = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/LANCommander.${{ matrix.app }}-${env:VERSION}-arm64-Setup.exe"
.\wingetcreate.exe update --submit --token "${{ secrets.WINGET_TOKEN }}" --urls $x64Url $arm64Url --version ${env:VERSION} LANCommander.${{ matrix.app }}
shell: pwsh
$installerUrl = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/LANCommander.${{ matrix.app }}-${env:VERSION}-${{ matrix.arch }}-Setup.exe"
wingetcreate submit --token ${{ secrets.GITHUB_TOKEN }} --urls "$installerUrl" --version ${env:VERSION} LANCommander.LANCommander.${{ matrix.app }}.${{ matrix.arch }}
shell: pwsh

View file

@ -1,94 +0,0 @@
# Contributing to LANCommander
Thanks for your interest in contributing to LANCommander! This project is primarily developed by a single developer, so community contributions are greatly appreciated.
## Getting Started
### Prerequisites
- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- [Node.js](https://nodejs.org/) (for the server UI's TypeScript/SCSS assets)
- A code editor such as [Visual Studio](https://visualstudio.microsoft.com/), [Rider](https://www.jetbrains.com/rider/), or [VS Code](https://code.visualstudio.com/)
### Building the Project
1. Clone the repository:
```bash
git clone https://github.com/LANCommander/LANCommander.git
cd LANCommander
```
2. Restore dependencies:
```bash
dotnet restore
```
3. Build the server:
```bash
dotnet build LANCommander.Server
```
4. Build the launcher:
```bash
dotnet build LANCommander.Launcher
```
### Running Locally
To run the server in development mode:
```bash
dotnet run --project LANCommander.Server
```
The server will be available at `http://localhost:1337` by default.
## How to Contribute
### Reporting Bugs
Use the [GitHub Issues](https://github.com/LANCommander/LANCommander/issues) page with the bug report template. Include:
- Steps to reproduce the issue
- Expected vs. actual behavior
- Your OS and LANCommander version
- Relevant logs or screenshots
### Submitting Changes
1. Fork the repository
2. Create a feature branch from `main` (`git checkout -b my-feature`)
3. Make your changes
4. Test your changes locally
5. Commit with a clear, descriptive message
6. Push to your fork and open a Pull Request
### What to Work On
- Check [open issues](https://github.com/LANCommander/LANCommander/issues) for bugs or feature requests
- Documentation improvements are always welcome at our [documentation site](https://docs.lancommander.app/)
- Game packaging scripts and guides for the community
### Code Guidelines
- Follow existing code style and conventions in the project
- Keep PRs focused, one feature or fix per PR when possible
- Include screenshots in your PR if you're changing UI
## Project Structure
| Directory | Description |
|-----------|-------------|
| `LANCommander.Server` | ASP.NET Blazor web application (server/admin) |
| `LANCommander.Launcher` | Avalonia desktop client (launcher) |
| `LANCommander.Packager` | Game packaging tool |
| `LANCommander.SDK` | .NET SDK for building custom clients |
| `LANCommander.Server.Data` | Entity Framework data models and migrations |
| `LANCommander.Server.Services` | Server business logic |
| `LANCommander.Documentation` | Docusaurus documentation site |
## Community
- [Discord](https://discord.gg/vDEEWVt8EM): Best place for discussion, help, and sharing game packages
## License
By contributing to LANCommander, you agree that your contributions will be licensed under the [MIT License](LICENSE).

View file

@ -5,7 +5,6 @@
<ItemGroup Label="Aspire">
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.0" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.1.1" />
<PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageVersion Include="EasyMDE.Blazor" Version="1.0.5" />
<PackageVersion Include="HotAvalonia" Version="3.1.0" />
<PackageVersion Include="LiveChartsCore" Version="2.0.0-rc5.4" />
@ -13,7 +12,7 @@
<PackageVersion Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc5.4" />
<PackageVersion Include="Markdig" Version="0.44.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.8" />
<PackageVersion Include="Notify.NET" Version="1.1.0" />
<PackageVersion Include="Notify.NET" Version="1.0.2" />
<PackageVersion Include="Svrooij.PowerShell.DI" Version="1.3.4" />
</ItemGroup>
<ItemGroup Label="AutoMapper">
@ -69,9 +68,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.12.0" />
</ItemGroup>
<ItemGroup Label="Testing">
<PackageVersion Include="bunit" Version="1.40.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Microsoft.Playwright" Version="1.51.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageVersion Include="Microsoft.TypeScript.MSBuild" Version="5.7.1" />
@ -87,19 +84,18 @@
</ItemGroup>
<ItemGroup Label="External">
<PackageVersion Include="CoreRCON" Version="5.0.5" />
<PackageVersion Include="LANCommander.HQ.SDK" Version="1.0.1" />
<PackageVersion Include="LANCommander.HQ.SDK" Version="1.0.0" />
<PackageVersion Include="craftersmine.SteamGridDB.Net" Version="1.1.7" />
<PackageVersion Include="YoutubeExplode" Version="6.4.3" />
<PackageVersion Include="Docker.DotNet.Enhanced" Version="3.129.0" />
<PackageVersion Include="Facepunch.Steamworks" Version="2.3.3" />
<PackageVersion Include="IGDB" Version="6.1.0" />
<PackageVersion Include="IPXRelayDotNet" Version="1.1.3" />
<PackageVersion Include="LANCommander.Interposer" Version="1.0.7" />
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="RestSharp" Version="112.1.0" />
<PackageVersion Include="rix0rrr.BeaconLib" Version="1.0.2" />
<PackageVersion Include="Semver" Version="3.0.0" />
<PackageVersion Include="SharpCompress" Version="0.50.0" />
<PackageVersion Include="SharpCompress" Version="0.47.0" />
<PackageVersion Include="SteamWebAPI2" Version="4.4.1" />
</ItemGroup>
<ItemGroup Label="Entity Framework Core">
@ -124,7 +120,7 @@
<ItemGroup Label="Microsoft Extensions">
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
@ -151,10 +147,6 @@
<PackageVersion Include="LibVLCSharp" Version="3.8.5" />
<PackageVersion Include="LibVLCSharp.Avalonia" Version="3.8.5" />
<PackageVersion Include="VideoLAN.LibVLC.Windows" Version="3.0.23.1" />
<PackageVersion Include="VideoLAN.LibVLC.Mac" Version="3.1.3.1" />
</ItemGroup>
<ItemGroup Label="SDL">
<PackageVersion Include="ppy.SDL3-CS" Version="2026.520.0" />
</ItemGroup>
<ItemGroup Label="Photino">
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.3.0" />

View file

@ -25,6 +25,5 @@ This site serves as the main documentation platform for the project. As such, it
- [Getting Started](/GettingStarted)
- [Server](/Server/Overview)
- [Launcher](/Launcher/Overview)
- [Packager](/Packager/Overview)
- [Scripting](/Scripting/Overview)
- [SDK Documentation](/SDK/Overview)

View file

@ -1,34 +0,0 @@
---
sidebar_label: Getting Started
sidebar_position: 2
---
# Getting Started
## Requirements
- **Windows 10 or later** (x86 or x64)
- **Administrator privileges** - the Packager requires elevation to monitor installer processes via DLL injection
The Packager is distributed as a single 32-bit executable (`LANCommander.Packager.exe`). No installation is required.
## Download
Download the latest release from the [GitHub Releases page](https://github.com/LANCommander/LANCommander/releases). The Packager artifact is named `LANCommander.Packager-Windows-x86-v{VERSION}.zip`.
Extract the archive to a directory of your choice and run `LANCommander.Packager.exe`.
## Command-Line Usage
The Packager can optionally accept arguments to skip the initial file picker dialog:
```
LANCommander.Packager.exe [installer-path] [-o output-path]
```
| Argument | Description |
|:--------:|:------------|
| `installer-path` | Path to the installer executable to monitor |
| `-o`, `--output` | Path for the output `.lcx` file |
If no installer path is provided, a file picker dialog will appear on launch.

View file

@ -1,47 +0,0 @@
---
sidebar_label: LCX Package Format
sidebar_position: 4
---
# LCX Package Format
An `.LCX` file is a standard ZIP archive containing everything needed to install and configure a game through LANCommander. The Packager generates this format automatically, but understanding its structure is useful for troubleshooting or manual editing.
## Archive Structure
```
package.lcx (ZIP)
├── manifest.yaml # Game metadata (YAML)
├── Archives/
│ └── {guid} # Inner ZIP containing game files
└── Scripts/
├── {guid} # Install script (PowerShell)
└── {guid} # Uninstall script (PowerShell)
```
### manifest.yaml
The manifest is a YAML file describing the game's metadata, actions, archive references, and script references. It follows the LANCommander SDK's `Game` manifest schema and includes:
- **Title, Sort Title, Version, Description, Notes** - basic metadata
- **Released On, Singleplayer** - classification
- **Directory Name** - the expected install directory name
- **Actions** - launch configurations (name, executable path, arguments, primary flag)
- **Archives** - references to inner archive entries with compressed/uncompressed sizes
- **Scripts** - references to script entries with type (Install/Uninstall) and admin requirements
### Archives
The `Archives/` directory contains one or more inner ZIP files, each identified by a GUID. The inner archive holds the game files with paths relative to the install directory root.
### Scripts
The `Scripts/` directory contains PowerShell scripts identified by GUID. The Packager generates up to two scripts:
**Install Script** - Recreates registry keys and values captured during monitoring. If the Patch GameSpy option was enabled, it also includes an `Edit-PatchGameSpy` call. Scripts assume `$InstallDirectory` is available in the execution environment (provided by the launcher's PowerShell runtime).
**Uninstall Script** - Removes the registry keys and values that were created by the install script.
## Importing into LANCommander
`.LCX` packages can be imported directly through the LANCommander server's web interface. The server reads the manifest, extracts the archive and scripts, and creates the corresponding game entry with all metadata, actions, and scripts pre-configured.

View file

@ -1,14 +0,0 @@
---
sidebar_label: Overview
sidebar_position: 1
---
# Packager
The LANCommander Packager is a standalone Windows utility that automates the creation of `.LCX` game packages. It monitors a game installer as it runs, captures all file and registry changes, and guides you through a wizard to produce a ready-to-import package for your LANCommander server.
Instead of manually creating archives, writing install scripts, and filling out metadata by hand, the Packager handles all of this in a single guided workflow.
- [Getting Started](/Packager/Getting%20Started) - requirements, download, and command-line usage
- [Wizard Walkthrough](/Packager/Wizard) - step-by-step guide through the seven wizard stages
- [LCX Package Format](/Packager/LCX%20Format) - internal structure of `.LCX` files

View file

@ -1,114 +0,0 @@
---
sidebar_label: Wizard Walkthrough
sidebar_position: 3
---
# Wizard Walkthrough
The Packager walks you through seven steps to create a complete `.LCX` package. Each step is shown in the sidebar with a progress indicator.
---
## Step 1: Monitor Installer
After selecting an installer executable, the Packager launches it and monitors all file and registry activity using native DLL injection (Interposer). A real-time log displays captured events as the installer runs.
The Packager automatically:
- Detects the installer's architecture (32-bit or 64-bit) and injects the appropriate Interposer DLL
- Monitors child processes spawned by the installer
- Filters out writes to system directories (Windows, temp folders)
- Captures both file writes and registry key/value creation
Once the installer exits, the captured data is summarized in the status bar. Click **Next** to continue.
:::info
The log view continues to show captured events for reference. All diagnostic output is also written to `packager.log` in the application directory.
:::
---
## Step 2: Install Directory
The Packager analyzes the captured file writes to detect the game's installation directory. This is determined by finding the most common non-system directory among the written files.
If the detected directory is incorrect, click **Browse** to manually select the correct location. This directory becomes the root of the game archive.
---
## Step 3: Select Files
All files within the install directory are displayed in a tree view with checkboxes. By default, every file is selected.
- **Check/uncheck a directory** to toggle all files within it
- **Select All** / **Select None** buttons at the top for bulk operations
- The counter at the top shows how many files are currently selected
Files outside the install directory (if any were captured) are listed by their full paths. Only files that still exist on disk at this point are shown.
---
## Step 4: Registry Entries
All captured registry writes are displayed in a tree view organized by hive and key path. Entries are deduplicated so if the same key and value were written multiple times during installation, only one entry is shown.
Each leaf entry displays an indicator:
- **Green +** - the entry was created during installation
- **Yellow ~** - the entry was updated (written to an existing key)
Selected entries will be included in the auto-generated install and uninstall scripts. The install script recreates the registry keys and values; the uninstall script removes them.
---
## Step 5: Game Metadata
Enter basic information about the game. The title is pre-populated from the installer's filename.
| Field | Description |
|:------|:------------|
| **Title** | Display name of the game (required) |
| **Sort Title** | Optional override for alphabetical sorting |
| **Version** | Game version, defaults to `1.0` |
| **Released On** | Release date of the game |
| **Singleplayer** | Whether the game supports singleplayer |
| **Description** | A description of the game |
| **Notes** | Private notes (admin-only, not shown to users) |
---
## Step 6: Game Executable
The Packager scans your selected files for `.exe` files and filters out common installer/redistributable executables (e.g. `vcredist`, `dxsetup`, `setup`, `unins`). The remaining executables are displayed in a list.
Select the primary game executable. This is the file the launcher will run when the user clicks "Play". You can also customize:
| Field | Description |
|:------|:------------|
| **Action Name** | Label shown on the play button, defaults to `Play` |
| **Arguments** | Command-line arguments passed when launching |
---
## Step 7: Generate Package
Configure the output path for the `.LCX` file and optionally adjust packaging options before generating.
### Output Path
The default output path is based on the game title in the current working directory. Click **Browse** to choose a different location.
### Options
Expand the **Options** panel to configure additional settings:
| Option | Description |
|:-------|:------------|
| **Patch GameSpy** | Adds an `Edit-PatchGameSpy -Path $InstallDirectory` call to the install script. This scans the install directory for GameSpy references and patches them for OpenSpy compatibility. |
| **Compression Level** | Controls the trade-off between archive size and packaging speed. Options: Optimal (default), Fastest, No Compression, Smallest Size. |
| **Write Summary Log** | Writes a `.Package.log` file alongside the `.LCX` output documenting the source installer, selected files, registry entries, metadata, and options used. |
Click **Generate .LCX** to build the package. A progress bar shows the current stage:
1. Creating game files archive
2. Generating scripts
3. Writing manifest
On completion, the output path and file size are displayed.

View file

@ -1,195 +0,0 @@
---
title: 2.1.0-rc1
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ScreenshotCarousel from '@site/src/components/ScreenshotCarousel';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.0 Release Candidate 1 Release Notes
## Breaking Changes
### .NET 10
LANCommander has been upgraded from .NET 8 to .NET 10. This should be a seamless transition as all LANCommander binaries are self-contained and include the appropriate .NET runtime. However, if you are running an older version of Windows that does not support .NET 10, you may encounter issues running this version of LANCommander. The minimum supported Windows version is now Windows 10 version 1809.
## Launching Into the Future
To kick things off, LANCommander 2.1.0 introduces a new launcher. As a recap, the previous launcher was built using .NET Blazor and wrapped into a Webview2 chrome using the open source project [Photino](https://www.tryphotino.io/). This decision was originally made in an effort to maintain a somewhat shared codebase with the server web UI.
Unfortunately, over time limitations of the platform made it apparent that a change had to be made. Community member [aaronpowell](https://github.com/aaronpowell) stepped up and built out a proof of concept for a launcher using Avalonia, an open-source native cross-platform UI framework for .NET. The potential was immediately clear and the decision was made to jump in head first. The following months saw the project refocus on build a new launcher with a major emphasis on performance, reliability, design, and feature parity.
The result is, well, a project that went off a little off the rails in the best possible fashion:
<ScreenshotCarousel screenshots={[
{ src: require('./_Assets/2.1.0 - Launcher.jpg').default, alt: 'Launcher', label: 'Launcher', caption: 'The brand new Avalonia-based launcher' },
{ src: require('./_Assets/2.1.0 - Shelf.jpg').default, alt: 'Shelf View', label: 'Shelf View', caption: 'Browse your library in the new shelf layout' },
{ src: require('./_Assets/2.1.0 - Depot.jpg').default, alt: 'Depot', label: 'Depot', caption: 'The redesigned depot with categorization carousels' },
{ src: require('./_Assets/2.1.0 - Screenshot.jpg').default, alt: 'Game Details', label: 'Game Details', caption: 'Screenshots and videos displayed right in the detail view' },
{ src: require('./_Assets/2.1.0 - Downloads.png').default, alt: 'Download Queue', label: 'Download Queue', caption: 'Track every step of the installation process' },
]} />
I think the screenshots speak for themselves. Okay, not quite, there's a lot of fun stuff in this build so let's dive right in!
### User Library
Previous versions of the launcher attempted to bring the art of a game to the forefront. These views are where most players spend their time interacting with the application. However, everyone's tastes are different. As such the library now contains three main views; cover grid, list, and shelf.
![Shelf View](./_Assets/2.1.0%20-%20Shelf.jpg)
### Game Details
Browsing the user library is great and all, but the detail view is where a game really gets to shine. Metadata has shifted to the right-hand side, descriptions now support more complex formatting with Markdown rendering support, and _finally_ videos and screenshots are displayed in a carousel. A lightbox viewer lets you zoom in on screenshots, watch videos, and even read game manuals right from the detail view thanks to the new built-in PDF viewer.
![Game Screenshot Detail](./_Assets/2.1.0%20-%20Screenshot.jpg)
This is really just the start of something new. Expect this part of the launcher to be built out more in the future with better publishing tools and metadata expansion.
### Depot
The depot has been overhauled too, probably more than any other part of the launcher. Previous iterations have been _fine_, but with large game sets it was hard to find just what you needed. The new depot puts a focus on categorization while maintaining granular filtering tools to get you exactly where you need to go.
![Depot](./_Assets/2.1.0%20-%20Depot.jpg)
As you can see, this section more than any other benefits from high quality metadata. We'll get into that later, but first let's talk about game installation.
### Download Queue
Game installation and downloading has been the hardest component to develop in the entire LANCommander platform. And rightly so! Without game installation, all you get is a fancy web UI and a pretty launcher that can't launch any games.
The LANCommander launcher is essentially a UI wrapper for the LANCommander SDK. The SDK is a .NET library that provides all of the useful bits of communicating with a LANCommander server install. Authentication, script execution, save management, game installation and so much more are provided through this library. In previous iterations, the SDK had a very strict execution plan for installing games. Often times this would lead to deadlocks and outright failures.
To resolve this in 2.1.0, the game install process has moved to a two-layer queuing system. The first layer of the queue manages what actually enters the download queue. This could be a game, redistributable, tool, or addon. The second layer handles any subprocessing step such as executing the different script types, save downloading, existing file verification, etc. The result is a system that is more transparent and reliable.
This queuing system has been exposed somewhat in the download queue:
![Download Queue](./_Assets/2.1.0%20-%20Downloads.png)
Each entry in the download queue will now report each step of the process during a game installation. In addition to looking a bit less mystical, this should give users an idea of what _exactly_ is happening during a game installation.
### Discord Rich Presence
The launcher now integrates with Discord Rich Presence. When you're playing a game through LANCommander, your Discord status will update to show what you're playing. By default this will show as LANCommander with the game name as the status. If the game is mapped to the correct Discord ID (via external IDs in the server), then the game's official Discord assets will be used for a more polished presentation.
### Offline Mode
Previous attempts at an offline mode in the launcher were pretty clunky and unreliable. In 2.1.0, the launcher now has a proper offline mode that allows you to launch previously installed games even when the server is unreachable.
### Single Sign-On
SSO was supported in the previous launcher, but it was a bit of an afterthought and not super reliable. In the new launcher it's been given more attention and should work as expected.
### Chat
As with the last version, the chat functionality remains pretty barebones. This is an area that will get more attention in the future, but it _is_ currently available in the new launcher. It might even work a little better.
## Server Improvements
This release should also be pretty exciting for server admins. With a new launcher with shiny new features brings a set of tools to manage them.
### LANCommander HQ
A new metadata provider has been added: LANCommander HQ. This gives server admins another option for sourcing game metadata and media alongside IGDB and SteamGridDB. In addition, support for external IDs beyond IGDB has been introduced, making it easier to cross-reference games across multiple platforms and services. To get started with the HQ, link your Discord account under "Integrations / HQ" in the server settings. Existing metadata providers such as IGDB and SteamGridDB are still available as well, so no functionality has been lost from previous versions.
### Preview Tool
A new "Preview" editing view has been added to the server for managing game art. Inspired by the launcher's presentation, this tool gives admins a better sense of how their media will look in the launcher before publishing. Screenshots and videos can now be uploaded in bulk, and the media grabber supports selecting multiple assets at once. Animated covers are now supported as well, APNG images grabbed from SteamGridDB are automatically converted to video and should display in the new launcher.
![Preview Tool](./_Assets/2.1.0%20-%20Server%20-%20Preview.png)
### Script Editor Improvements
The script editor has received a significant upgrade. Context-aware code completions now suggest built-in PowerShell cmdlets, LANCommander-specific variables, and generated type definitions. Validation has been improved, and script errors are now logged rather than silently swallowed. A new `Set-GameDetails` cmdlet has been added for specifying package details from within scripts.
### Redistributable Options
Redistributables now support configurable options. Options can be defined with a schema editor, given display names, and are preserved across import/export. Assigning redistributables to a game can now be accessed under the "Redistributables" section of the game editor. When selected, a redistributable with options will render as an easy-to-use form and per-game overrides can be defined.
![Redistributable Options](./_Assets/2.1.0%20-%20Server%20-%20Redistributable%20Options.png)
A new **RunWrapper** script type has been added as well, giving admins more control over how redistributables and games are executed. This should enhance redistributables to cover more use cases such as emulators, launchers, and compatiblity shims like Proton or dgVoodoo.
### Key Management
Key allocation has been improved. The allocation method on keys is now nullable, and a new manual key assignment dialog has been added. The logic for choosing the next available key has also been reworked.
### Media & Tooling
Server settings now include checks for ffmpeg and yt-dlp availability, with automatic installation support in Docker environments. A new streaming endpoint for media has been added, improving video playback in the launcher. Automatic downloads for Steam screenshots have been added as well. YouTube video support has been added to the media grabber, and ffmpeg is now used for video processing tasks such as APNG conversion and thumbnail generation.
## LANCommander HQ
Last but far from least, this release coincides with the launch of [LANCommander HQ](https://hq.lancommander.com), a new metadata provider and community hub for LANCommander users. The HQ aims to provide a centralized platform for game metadata and media across multiple providers, while also normalizing and enhancing that data with a focus on the needs of LANCommander users. There's a bit to cover, so let's dive in!
### Metadata
LANCommander HQ aggregates metadata from multiple sources including IGDB, SteamGridDB, and user contributions. This data is then normalized and enhanced with a focus on the needs of LANCommander users. There are many metadata providers out there, and unfortunately they all have their own unique schemas and data quality issues. The HQ's goal is to provide a single source of truth for game metadata that can be consumed by LANCommander servers and launchers.
There are a lot of exciting features in the works for the HQ, but for this release the focus is on building out the core infrastructure and integrating with the new launcher. Expect to see more features and improvements in the future as the HQ continues to evolve.
### Community
The main features of the HQ are currently focused on metadata, but there is the potential to expand it into a broader community hub for LANCommander users. This could include an area to share game configurations, scripts, and other resources. Additionally, there are some content moderation tools in place that should allow the HQ to host user-generated content and provide the best possible experience for all users. If you have a knack for curation and want to help out, there will likely be opportunities to get involved in the future!
### Tiering / Subscriptions
The HQ operates on a tiered subscription model. Currently there are only two tiers: Basic (Free) and Premium ($5/month). The Basic tier provides access to the core metadata features of the HQ, while the Premium tier provides access to additional features such as enhanced metadata, relaxed rate limiting, and more. More information will be provided in the near future and will be provided on this site under the [LANCommander HQ](/hq) section. All paid subscriptions on the HQ are handled by Stripe, so every transaction is secure and your personal information is protected.
This subscription model is primarily in place to help cover the costs of running the HQ while also providing a sustainable path forward for continued development of LANCommander itself. This entire project is developed and maintained by essentially one person with some help from the community here and there. While this is a labor of love, there are real costs associated with running the infrastructure for the HQ and continued development of LANCommander. The subscription model is designed to be as accessible as possible while also providing a way for users to support the project if they find value in it.
As such, this will probably mean that the Patreon page will be going away in the near future. For existing patrons, it is recommended to switch over to the new subscription model on the HQ as it provides a more direct way to support the project and access the benefits of the HQ _today_.
## Contributions
This release includes contributions from the following community members:
**@aaronpowell | PR [#395](https://github.com/LANCommander/LANCommander/pull/395): Avalonia Launcher**
A substantial 42-commit, 6,000-line PR that bootstrapped the cross-platform Avalonia launcher. Aaron built out the core application structure, service wiring, and async initialization, and then built out the UI with filtered depot browsing and grid layout, a download queue, install/uninstall flows, a settings page, PDF manual viewer, game action bars, and a full CI pipeline for Windows, Linux, and macOS builds. A lot of the foundation that the current launcher sits on came from this PR.
**@akifreak | PR [#398](https://github.com/LANCommander/LANCommander/pull/398): Fix Game Archive Upload**
Discovered that archive uploads through the web UI were silently broken after upgrading to v2.0.2, traced the problem through the JavaScript bundle, and submitted 11 targeted fixes rather than a single bug report. The fixes covered ES module loading, null-guards on DOM elements, a FileMode.Append seek conflict, incorrect archive ID passing, and error propagation throughout the upload pipeline. The uploading process should feel much more stable now!
**@MasterMNB | PR [#400](https://github.com/LANCommander/LANCommander/pull/400): Avalonia Launcher Back Button**
Improved the visibility and styling of the back button in the `GameDetailView`, which was previously hard to distinguish from the background. It's a small change, but it's a real usability improvement that makes navigation feel more intentional.
## Changelog
**Launcher**
- Added: Brand new launcher built on Avalonia
- Added: Screenshots and videos in game detail carousel with lightbox viewer
- Added: Redesigned depot with categorization carousels and advanced filtering
- Added: Tools support
- Added: Download transfer speed graph
- Added: OS notifications upon game installation completion
- Added: Discord Rich Presence integration
- Added: Offline mode for playing games without server connectivity
- Added: SSO/OIDC external authentication provider support
- Added: Built-in PDF viewer for game manuals
- Added: Splash screen on startup
- Added: Verify files option in game dropdown
- Added: Markdown rendering for game descriptions
- Changed: Refactored game/redistributable/tool install process to two-layer queue system
- Changed: Imports should be faster and execute in the background without causing major UI hiccups
- Changed: Improved memory management with proper disposal of HTTP clients, RPC clients, and cancellation tokens
- Fixed: Installing games that already have files on disk will show the verification process during install
- Fixed: Logging in and logging out should no longer cause a hung UI
- Fixed: Install-uninstall-reinstall cycle no longer locks the game
- Fixed: Authentication token race condition causing crashes after login
**Server**
- Added: LANCommander HQ metadata provider support
- Added: Support for external IDs beyond IGDB
- Added: New launcher-inspired "Preview" tool for managing art
- Added: Context-aware script editor with code completions, validation, and PowerShell type definitions
- Added: Redistributable options with schema editor and display names
- Added: RunWrapper script type
- Added: Package versioning for archives
- Added: Bulk upload for screenshots and videos
- Added: Multiple select support in media grabber
- Added: Animated cover support (APNG auto-conversion to video)
- Added: Steam screenshot downloads
- Added: YouTube video support in media grabber
- Added: Media streaming endpoint
- Added: ffmpeg/yt-dlp availability checks and auto-install in Docker
- Added: Manual key assignment dialog and reworked key allocation
- Added: `Set-GameDetails` cmdlet for package scripts
- Fixed: Media uploads should now behave as expected
- Fixed: Game archive uploads in web UI
- Fixed: Orphaned files tool
- Fixed: Redistributable archive downloads
- Fixed: Script working directory for games
- Fixed: SteamCMD login when SteamGuard is enabled
- Fixed: Import file cleanup after completion
**Infrastructure**
- Upgraded to .NET 10
- Added ARM64 build support
- Removed legacy Photino launcher from CI
- Added nightly builds for Avalonia launcher
## Downloads
<ReleaseDownloads release="v2.1.0-rc1" />
## Contributors
<ContributorGrid from="v2.0.2" to="v2.1.0-rc1" />

View file

@ -1,99 +0,0 @@
---
title: 2.1.0-rc2
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ScreenshotCarousel from '@site/src/components/ScreenshotCarousel';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.0 Release Candidate 2 Release Notes
## Variable Picker Dialog
A new variable picker dialog has been added to the action editor and save path editor in the server UI. When editing action arguments, working directories, or save paths, a new button opens a dialog that lets you browse and insert LANCommander variables (`{InstallDir}`, `{DisplayWidth}`, etc.), environment variables (`%APPDATA%`, `%LOCALAPPDATA%`, etc.), and special folder paths (`%MyDocuments%`, `%Desktop%`, etc.) with a single click. No more needing to remember exact variable names or syntax.
## Redistributable Improvements
### File Tracking and Cleanup
Redistributable files installed into a game's directory are now tracked. When a game is uninstalled, any files that were extracted by redistributables are cleaned up alongside the game's own files. This prevents leftover redistributable artifacts from lingering in the install directory after uninstallation.
### Detect Install Script Behavior
The redistributable install detection logic has been updated. If a redistributable does not have a detect install script defined, the install script will now always run rather than being skipped. This ensures that redistributables without detection scripts are reliably installed every time, which is the expected behavior for most simple redistributable configurations.
## Package Script Filtering
Package scripts are now filtered out of API responses for games, redistributables, and tools. These scripts are used internally during the packaging process and should not be sent to clients. This filtering is applied at both the endpoint and AutoMapper levels, ensuring that package scripts never leak to launchers or other API consumers.
## Launcher CLI Improvements
### Full Command Line Verb Support
The Avalonia launcher now supports the full set of CLI verbs: `RunScript`, `Install`, `Uninstall`, `Run`, `Sync`, `Import`, `Export`, `Upload`, `Login`, `Logout`, and `ChangeAlias`. Previously only `RunScript` was supported in headless mode. The `--help` and `--version` flags are also now recognized. Console logging has been added for headless execution.
### CLI Project Removal
The standalone `LANCommander.Launcher.CLI` project has been removed. All CLI functionality is now handled directly by the Avalonia launcher's headless mode, consolidating the codebase and eliminating the need for a separate CLI binary.
## C++ SDK
A new C++ SDK (`LANCommander.SDK.Cpp`) has been introduced. Written in C++14, it provides maximum compatibility from Windows 95 through modern platforms. The SDK includes 16 API clients covering authentication, games, library, saves, media, tools, depot, and more. It ships with two HTTP backend implementations (WinINet for Windows and libcurl for cross-platform) and uses vendored cJSON for zero mandatory external dependencies. This SDK is the foundation that powers the legacy launcher.
## Legacy Launcher
:::note A Fun Detour
The legacy launcher is a fun side project and detour from the main development track. It should not be regarded as a primary concern or a supported production launcher. Think of it as an experiment in seeing just how far back LANCommander's reach can extend.
:::
The legacy launcher is a native Win32 application written in C++ that targets Windows 9x (Windows 95/98/ME) and beyond. Built on Allegro 4 for rendering and GDI+ for image decoding (JPEG cover art, backgrounds, etc.), it provides a surprisingly full-featured LANCommander experience on vintage hardware. GDI+ is a requirement and must be available on the target system. For Windows 9x systems, this means installing the [GDI+ redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=18909).
Features include:
- **Library and Depot browsing** with cover art grid
- **Game detail view** with metadata and screenshots
- **Download queue** with progress tracking
- **Login screen** with server authentication
- **Settings screen** with configurable server address
- **SQLite-backed local database** for offline game metadata
- **Custom window chrome** with a themed UI
- **Unicode support** for international character sets
<ScreenshotCarousel screenshots={[
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Login.png').default, alt: 'Legacy Launcher Login', label: 'Login', caption: 'The login screen running on Windows 98' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Library.png').default, alt: 'Legacy Launcher Library', label: 'Library', caption: 'Browsing the game library with cover art' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Detail.png').default, alt: 'Legacy Launcher Detail', label: 'Game Detail', caption: 'Game detail view with metadata' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Downloads.png').default, alt: 'Legacy Launcher Downloads', label: 'Downloads', caption: 'The download queue in action' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Settings.png').default, alt: 'Legacy Launcher Settings', label: 'Settings', caption: 'Configuring server connection settings' },
]} />
## Changelog
**Server**
- Added: Variable picker dialog for action editor and save path editor
- Fixed: Package scripts are no longer exposed through API responses
- Fixed: Script helper now correctly maps Package and RunWrapper script filenames
**Launcher**
- Added: Full set of CLI verbs (Install, Uninstall, Run, Sync, Import, Export, Upload, Login, Logout, ChangeAlias)
- Added: Console logging for headless CLI execution
- Changed: Redistributable files are now tracked and cleaned up on uninstall
- Changed: Redistributables without detect install scripts now always run the install script
- Fixed: Script writing for all script types
- Removed: Standalone CLI project (functionality merged into Avalonia launcher)
**Legacy Launcher**
- Added: Complete native Win32 launcher targeting Windows 9x
- Added: Library and depot browsing with cover art grid
- Added: Game detail view with metadata display
- Added: Download queue with SQLite-backed local database
- Added: Settings screen with YAML configuration
- Added: Custom themed window chrome
- Added: Unicode support for Win9x targets
- Added: CI workflow for automated builds
**SDK**
- Added: C++ SDK (LANCommander.SDK.Cpp) with C++14 compatibility
- Added: WinINet and libcurl HTTP backend implementations
- Added: 16 API clients covering full LANCommander API surface
- Changed: Game list filtering now shows only standalone mods/expansions and main games
## Downloads
<ReleaseDownloads release="v2.1.0-rc2" />
## Contributors
<ContributorGrid from="v2.1.0-rc1" to="v2.1.0-rc2" />

View file

@ -1,73 +0,0 @@
---
title: 2.1.0-rc3
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.0 Release Candidate 3 Release Notes
## Server Discovery
The Avalonia launcher now supports automatic server discovery using the beacon system. When connecting to a server, the launcher can discover LANCommander servers on the local network without needing to manually enter an address. This feature was already present in the legacy Photino launcher and has now been brought forward to the new Avalonia-based launcher.
## Launcher Improvements
### List View Rework
The list view in the launcher library has been reworked for a cleaner presentation and better usability. It is essentially a reimagination of the library view in the previous launcher.
### Carousel Fixes
Cover art in carousels has received several visual fixes. The shadow overlay on covers now renders correctly, and covers are no longer cut off at the bottom when hovered. These are small polish items that improve the overall look and feel of the depot and library views.
### Play Button State
The play button now correctly reflects the current state of a running game, showing the appropriate playing/stop indicator when a game is actively running.
### Performance Optimizations
General launcher optimizations have been made to improve responsiveness and reduce resource usage.
## Bug Fixes
### Redistributable and Tool Importing
Fixed an issue where importing redistributables and tools from an LCX file was not working correctly.
### Linux Path Expansion
Path variable expansion now correctly preserves the Linux root (`/`) prefix. Previously, expanding variables like `{InstallDir}` on Linux could strip the leading slash, resulting in incorrect paths.
## Contributions
This release includes contributions from the following community members:
**@akifreak | PR [#403](https://github.com/LANCommander/LANCommander/pull/403): Fix Beacon Discovery**
Back for another round of contributions, akifreak tracked down and fixed an issue where server discovery wasn't working reliably. In some cases the launcher was actually finding servers on the network but wasn't surfacing them in the UI. The fix spans the full discovery pipeline; making the probe socket more resilient to bad data, ensuring beacon responses actually reach the client, and refreshing the server list as soon as a new server is found.
## Changelog
**Launcher**
- Added: Automatic server discovery via beacon system
- Added: Play session sync on game import
- Added: Additional save handling logging
- Changed: Reworked list view layout
- Changed: Performance optimizations
- Changed: Increased cover art rendering quality
- Changed: Action bar layout and behavior adjustments
- Fixed: Shadow overlay on covers in carousels
- Fixed: Cover art clipping on hover in carousels
- Fixed: Play button now shows correct playing/stop state when a game is running
- Fixed: Linux path variable expansion now preserves root prefix
**Server**
- Fixed: Redistributable and tool importing
- Fixed: Media settings page failing to render
**SDK**
- Fixed: Discovery probe socket resilience against malformed data
- Fixed: Broadcast send reliability in DiscoveryProbe
- Fixed: Beacon responses now forwarded to BeaconClient event
- Fixed: Save client now checks for newer server saves before uploading
- Added: Logging in save packaging and upload pipeline
## Downloads
<ReleaseDownloads release="v2.1.0-rc3" />
## Contributors
<ContributorGrid from="v2.1.0-rc2" to="v2.1.0-rc3" />

View file

@ -1,57 +0,0 @@
---
title: 2.1.0-rc4
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.0 Release Candidate 4 Release Notes
## Big Screen Mode
The Avalonia launcher now supports a big screen mode designed for use with TVs and handhelds. When enabled, the launcher switches to a fullscreen layout optimized for focused gaming. This pairs with the new gamepad navigation support to make LANCommander usable from the sofa or a handheld without a keyboard or mouse.
## Gamepad Navigation
Full gamepad support has been added to the Avalonia launcher. Controllers are detected and mapped using SDL2, and directional navigation has been implemented across the library views, carousels, game detail pages, and overlay dialogs. Focus management handles moving between UI elements naturally with a d-pad or analog stick, making it possible to browse, install, and launch games entirely with a controller. This feature is still a bit rough around the edges, but should improve in future releases.
## RunWrapper Scripts for Redistributables
RunWrapper scripts were previously defined as a script type but never actually executed. This release adds full execution support during game launch. RunWrapper scripts now run alongside the game process with proper state tracking, cancellation support, and child process cleanup when the game exits.
## Packaging Dialog
The "Package" button has been moved from individual game, redistributable, and tool edit pages to the archive editor. A new packaging dialog provides a consolidated view of the packaging process, making it easier to build and manage archives directly from the archive list.
## Bug Fixes
- Fixed scrollbar being overlapped by the titlebar in the launcher
- Fixed HQ metadata lookups by updating the HQ SDK dependency
- Fixed media downloads failing during game import
- Fixed icon downloading
- Fixed server save check incorrectly determining newer saves were available
## Changelog
**Launcher**
- Added: Big screen mode for fullscreen TV/controller use
- Added: Gamepad navigation across library, carousels, detail views, and overlays
- Fixed: Scrollbar overlapped by titlebar
- Fixed: Media download on import
- Fixed: Icon downloading
**Server**
- Added: Packaging dialog on archive editor
- Added: RunWrapper script type available for redistributable scripts
- Changed: Moved "Package" button from edit pages to archive editor
- Changed: Updated PowerShell code snippets
- Fixed: HQ metadata lookups
**SDK**
- Added: RunWrapper script execution for redistributables with process lifecycle management
- Changed: Multi-target framework support
- Changed: Source-generated cmdlet registration via new `CmdletRegistrationGenerator`
- Fixed: Save client check for newer saves on server
## Downloads
<ReleaseDownloads release="v2.1.0-rc4" />
## Contributors
<ContributorGrid from="v2.1.0-rc3" to="v2.1.0-rc4" />

View file

@ -1,59 +0,0 @@
---
title: 2.1.0-rc5
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.0 Release Candidate 5 Release Notes
## LANCommander Packager
A new standalone Avalonia application, LANCommander Packager, has been added for building LCX package files outside of the server. The packager works by monitoring the files and registry entries created by a game's installer. You can then choose which files / registry keys are bundled into the final LCX file.
## Background Uploading
Archive uploads now continue in the background, allowing you to navigate away from the upload page without losing progress. An upload indicator in the sidebar tracks active uploads so you can continue working in other areas of the server while files transfer. This also applies to the import upload dialog.
## HQ Login in First Time Setup
The first time setup wizard now includes a step to log in to LANCommander HQ directly during initial configuration. This streamlines connecting to HQ for metadata and media lookups without needing to visit the settings page after setup is complete.
## File Manager Improvements
The server file manager has received several usability improvements including faster enumeration of large directories, toggleable columns, new Created and Type columns, fixed breadcrumb navigation, and a properly built directory tree with support for cross-platform roots and special folders.
## Bug Fixes
- Fixed standalone expansions and mods appearing in nested game lists (#284)
- Fixed missing nested game data
- Fixed save path matching failing on Windows due to backslash normalization (#266)
- Fixed log viewer not working under Settings / Logs (#393)
- Fixed user approval menu item not functioning (#405)
- Fixed archive processing when using "Use Local File"
- Fixed "Use Local File" button being incorrectly disabled
## Changelog
**Server**
- Added: Background uploading with sidebar progress indicator
- Added: HQ login step in first time setup wizard
- Added: Button to download current log file
- Changed: Improved file manager with faster directory enumeration, toggleable columns, and cross-platform directory tree
- Fixed: Log viewer under Settings / Logs (#393)
- Fixed: User approval menu item (#405)
- Fixed: Archive processing for "Use Local File"
- Fixed: "Use Local File" disabled button
- Fixed: Standalone expansions/mods shown in nested game lists (#284)
- Fixed: Missing nested game data
**Packager**
- Added: New standalone application for building LCX package files
- Added: CI workflow for building and releasing the packager
- Added: Documentation for packager usage and LCX format
**SDK**
- Fixed: Save path normalization to use forward slashes before regex matching (#266)
## Downloads
<ReleaseDownloads release="v2.1.0-rc5" />
## Contributors
<ContributorGrid from="v2.1.0-rc4" to="v2.1.0-rc5" />

View file

@ -1,678 +0,0 @@
---
title: 2.1.0
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ScreenshotCarousel from '@site/src/components/ScreenshotCarousel';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.0 Release Notes
:::tip Latest Version
This page covers the full LANCommander 2.1 series. The latest patch is **2.1.9** — see [Patch Updates](#patch-updates) below for what's changed since the initial release.
:::
LANCommander 2.1.0 is a landmark release that touches virtually every part of the platform. A brand new launcher built on Avalonia, a standalone packager application, a C++ SDK powering a legacy Win32 launcher, major server improvements, and the launch of LANCommander HQ all come together in what has been the most ambitious update cycle to date.
## Breaking Changes
### .NET 10
LANCommander has been upgraded from .NET 8 to .NET 10. This should be a seamless transition as all LANCommander binaries are self-contained and include the appropriate .NET runtime. However, if you are running an older version of Windows that does not support .NET 10, you may encounter issues running this version of LANCommander. The minimum supported Windows version is now Windows 10 version 1809.
## Launching Into the Future
To kick things off, LANCommander 2.1.0 introduces a new launcher. As a recap, the previous launcher was built using .NET Blazor and wrapped into a Webview2 chrome using the open source project [Photino](https://www.tryphotino.io/). This decision was originally made in an effort to maintain a somewhat shared codebase with the server web UI.
Unfortunately, over time limitations of the platform made it apparent that a change had to be made. Community member [aaronpowell](https://github.com/aaronpowell) stepped up and built out a proof of concept for a launcher using Avalonia, an open-source native cross-platform UI framework for .NET. The potential was immediately clear and the decision was made to jump in head first. The following months saw the project refocus on building a new launcher with a major emphasis on performance, reliability, design, and feature parity.
The result is, well, a project that went off a little off the rails in the best possible fashion:
<ScreenshotCarousel screenshots={[
{ src: require('./_Assets/2.1.0 - Launcher.jpg').default, alt: 'Launcher', label: 'Launcher', caption: 'The brand new Avalonia-based launcher' },
{ src: require('./_Assets/2.1.0 - Shelf.jpg').default, alt: 'Shelf View', label: 'Shelf View', caption: 'Browse your library in the new shelf layout' },
{ src: require('./_Assets/2.1.0 - Depot.jpg').default, alt: 'Depot', label: 'Depot', caption: 'The redesigned depot with categorization carousels' },
{ src: require('./_Assets/2.1.0 - Screenshot.jpg').default, alt: 'Game Details', label: 'Game Details', caption: 'Screenshots and videos displayed right in the detail view' },
{ src: require('./_Assets/2.1.0 - Downloads.png').default, alt: 'Download Queue', label: 'Download Queue', caption: 'Track every step of the installation process' },
]} />
I think the screenshots speak for themselves. Okay, not quite, there's a lot of fun stuff in this build so let's dive right in!
### User Library
Previous versions of the launcher attempted to bring the art of a game to the forefront. These views are where most players spend their time interacting with the application. However, everyone's tastes are different. As such the library now contains three main views; cover grid, list, and shelf.
![Shelf View](./_Assets/2.1.0%20-%20Shelf.jpg)
### Game Details
Browsing the user library is great and all, but the detail view is where a game really gets to shine. Metadata has shifted to the right-hand side, descriptions now support more complex formatting with Markdown rendering support, and _finally_ videos and screenshots are displayed in a carousel. A lightbox viewer lets you zoom in on screenshots, watch videos, and even read game manuals right from the detail view thanks to the new built-in PDF viewer.
![Game Screenshot Detail](./_Assets/2.1.0%20-%20Screenshot.jpg)
This is really just the start of something new. Expect this part of the launcher to be built out more in the future with better publishing tools and metadata expansion.
### Depot
The depot has been overhauled too, probably more than any other part of the launcher. Previous iterations have been _fine_, but with large game sets it was hard to find just what you needed. The new depot puts a focus on categorization while maintaining granular filtering tools to get you exactly where you need to go.
![Depot](./_Assets/2.1.0%20-%20Depot.jpg)
As you can see, this section more than any other benefits from high quality metadata. We'll get into that later, but first let's talk about game installation.
### Download Queue
Game installation and downloading has been the hardest component to develop in the entire LANCommander platform. And rightly so! Without game installation, all you get is a fancy web UI and a pretty launcher that can't launch any games.
The LANCommander launcher is essentially a UI wrapper for the LANCommander SDK. The SDK is a .NET library that provides all of the useful bits of communicating with a LANCommander server install. Authentication, script execution, save management, game installation and so much more are provided through this library. In previous iterations, the SDK had a very strict execution plan for installing games. Often times this would lead to deadlocks and outright failures.
To resolve this in 2.1.0, the game install process has moved to a two-layer queuing system. The first layer of the queue manages what actually enters the download queue. This could be a game, redistributable, tool, or addon. The second layer handles any subprocessing step such as executing the different script types, save downloading, existing file verification, etc. The result is a system that is more transparent and reliable.
This queuing system has been exposed somewhat in the download queue:
![Download Queue](./_Assets/2.1.0%20-%20Downloads.png)
Each entry in the download queue will now report each step of the process during a game installation. In addition to looking a bit less mystical, this should give users an idea of what _exactly_ is happening during a game installation.
### Big Screen Mode & Gamepad Navigation
The launcher now supports a big screen mode designed for use with TVs and handhelds. When enabled, the launcher switches to a fullscreen layout optimized for focused gaming. Gamepad support has been added as well. Controllers are detected and mapped using SDL2, and directional navigation has been implemented across the library views, carousels, game detail pages, and overlay dialogs. Focus management handles moving between UI elements naturally with a d-pad or analog stick, making it possible to browse, install, and launch games with a controller. This feature is pretty barebones for now, but adds a foundation for deeper integration in the future.
### Discord Rich Presence
The launcher now integrates with Discord Rich Presence. When you're playing a game through LANCommander, your Discord status will update to show what you're playing. By default this will show as LANCommander with the game name as the status. If the game is mapped to the correct Discord ID (via external IDs in the server), then the game's official Discord assets will be used for a more polished presentation.
### Offline Mode
Previous attempts at an offline mode in the launcher were pretty clunky and unreliable. In 2.1.0, the launcher now has a proper offline mode that allows you to launch previously installed games even when the server is unreachable.
### Command Line Interface
The Avalonia launcher now handles all CLI functionality directly, replacing the previous standalone CLI project. The full set of CLI verbs is supported: `RunScript`, `Install`, `Uninstall`, `Run`, `Sync`, `Import`, `Export`, `Upload`, `Login`, `Logout`, and `ChangeAlias`. The `--help` and `--version` flags are also recognized, and console logging has been added for headless execution.
### Single Sign-On
SSO was supported in the previous launcher, but it was a bit of an afterthought and not super reliable. In the new launcher it's been given more attention and should work as expected.
### Chat
As with the last version, the chat functionality remains pretty barebones. This is an area that will get more attention in the future, but it _is_ currently available in the new launcher. It might even work a little better.
## Server Improvements
This release should also be pretty exciting for server admins. With a new launcher and shiny new features brings a set of tools to manage them.
### LANCommander HQ
A new metadata provider has been added: LANCommander HQ. This gives server admins another option for sourcing game metadata and media alongside IGDB and SteamGridDB. In addition, support for external IDs beyond IGDB has been introduced, making it easier to cross-reference games across multiple platforms and services. To get started with the HQ, link your Discord account under "Integrations / HQ" in the server settings. The first time setup wizard now includes a step to log in to HQ directly during initial configuration, streamlining the onboarding experience. Existing metadata providers such as IGDB and SteamGridDB are still available as well, so no functionality has been lost from previous versions. We'll cover more about HQ later on in these notes.
### Preview Tool
A new "Preview" editing view has been added to the server for managing game art. Inspired by the launcher's presentation, this tool gives admins a better sense of how their media will look in the launcher before publishing. Screenshots and videos can now be uploaded in bulk, and the media grabber supports selecting multiple assets at once. Animated covers are now supported as well, APNG images grabbed from SteamGridDB are automatically converted to video and should display in the new launcher.
![Preview Tool](./_Assets/2.1.0%20-%20Server%20-%20Preview.png)
### Script Editor Improvements
The script editor has received a significant upgrade. Context-aware code completions now suggest built-in PowerShell cmdlets, LANCommander-specific variables, and generated type definitions. Validation has been improved, and script errors are now logged rather than silently swallowed. A new `Set-GameDetails` cmdlet has been added for specifying package details from within scripts.
### Variable Picker Dialog
A new variable picker dialog has been added to the action editor and save path editor. When editing action arguments, working directories, or save paths, a new button opens a dialog that lets you browse and insert LANCommander variables (`{InstallDir}`, `{DisplayWidth}`, etc.), environment variables (`%APPDATA%`, `%LOCALAPPDATA%`, etc.), and special folder paths (`%MyDocuments%`, `%Desktop%`, etc.) with a single click.
### Redistributable Options
Redistributables now support configurable options. Options can be defined with a schema editor, given display names, and are preserved across import/export. Assigning redistributables to a game can now be accessed under the "Redistributables" section of the game editor. When selected, a redistributable with options will render as an easy-to-use form and per-game overrides can be defined.
![Redistributable Options](./_Assets/2.1.0%20-%20Server%20-%20Redistributable%20Options.png)
A new **RunWrapper** script type has been added as well, giving admins more control over how redistributables and games are executed. RunWrapper scripts now run alongside the game process with proper state tracking, cancellation support, and child process cleanup when the game exits. This should enhance redistributables to cover more use cases such as emulators, launchers, and compatibility shims like Proton or dgVoodoo.
### Packaging Dialog
The "Package" button has been moved from individual game, redistributable, and tool edit pages to the archive editor. A new packaging dialog provides a consolidated view of the packaging process, making it easier to build and manage archives directly from the archive list.
### Background Uploading
Archive uploads now continue in the background, allowing you to navigate away from the upload page without losing progress. An upload indicator in the sidebar tracks active uploads so you can continue working in other areas of the server while files transfer. This also applies to the import upload dialog.
### File Manager Improvements
The server file manager has received several usability improvements including faster enumeration of large directories, toggleable columns, new Created and Type columns, fixed breadcrumb navigation, and a properly built directory tree with support for cross-platform roots and special folders.
### Key Management
Key allocation has been improved. The allocation method on keys is now nullable, and a new manual key assignment dialog has been added. The logic for choosing the next available key has also been reworked.
### Media & Tooling
Server settings now include checks for ffmpeg and yt-dlp availability, with automatic installation support in Docker environments. A new streaming endpoint for media has been added, improving video playback in the launcher. Automatic downloads for Steam screenshots have been added as well. YouTube video support has been added to the media grabber, and ffmpeg is now used for video processing tasks such as APNG conversion and thumbnail generation.
## LANCommander Packager
A new standalone Avalonia application, LANCommander Packager, has been added for building LCX package files outside of the server. The packager works by monitoring the files and registry entries created by a game's installer. You can then choose which files and registry keys are bundled into the final LCX file. If the tool is authenticated to a LANCommander server instance, the game can be directly uploaded to the server as well.
<ScreenshotCarousel screenshots={[
{ src: require('./_Assets/2.1.0 - Packager - Monitor.png').default, alt: 'Packager Monitor', label: 'Monitor', caption: 'The Packager application will monitor the files and registry entries created during installation' },
{ src: require('./_Assets/2.1.0 - Packager - Select Files.png').default, alt: 'Packager Select Files', label: 'Select Files', caption: 'Files can be individually selected to include in the final import file' },
{ src: require('./_Assets/2.1.0 - Packager - Registry.png').default, alt: 'Packager Registry Entries', label: 'Registry Entries', caption: 'Registry entries can be individually selected and will be included in install/uninstall scripts' },
{ src: require('./_Assets/2.1.0 - Packager - Metadata.png').default, alt: 'Packager Metadata', label: 'Metadata', caption: 'Basic metadata can be defined before packaging' },
{ src: require('./_Assets/2.1.0 - Packager - Metadata Lookup.png').default, alt: 'Packager Metadata Lookup', label: 'Metadata Lookup', caption: 'Metadata can also be pulled in from metadata providers' },
{ src: require('./_Assets/2.1.0 - Packager - Select Executable.png').default, alt: 'Packager Select Executable', label: 'Select Executable', caption: 'The primary action can be defined before packaging' },
{ src: require('./_Assets/2.1.0 - Packager - Generate Package.png').default, alt: 'Packager Generate Package', label: 'Generate Package', caption: 'The final package can be created as an .LCX file or uploaded directly to a server' },
]} />
## C++ SDK & Legacy Launcher
### C++ SDK
A new C++ SDK (`LANCommander.SDK.Cpp`) has been introduced. Written in C++14, it provides maximum compatibility from Windows 95 through modern platforms. The SDK includes 16 API clients covering authentication, games, library, saves, media, tools, depot, and more. It ships with two HTTP backend implementations (WinINet for Windows and libcurl for cross-platform) and uses vendored cJSON for zero mandatory external dependencies. This SDK is the foundation that powers the legacy launcher.
### Legacy Launcher
:::note A Fun Detour
The legacy launcher is a fun side project and detour from the main development track. It should not be regarded as a primary concern or a supported production launcher. Think of it as an experiment in seeing just how far back LANCommander's reach can extend.
:::
The legacy launcher is a native Win32 application written in C++ that targets Windows 9x (Windows 95/98/ME) and beyond. Built on Allegro 4 for rendering and GDI+ for image decoding (JPEG cover art, backgrounds, etc.), it provides a surprisingly full-featured LANCommander experience on vintage hardware. GDI+ is a requirement and must be available on the target system. For Windows 9x systems, this means installing the [GDI+ redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=18909).
Features include:
- **Library and Depot browsing** with cover art grid
- **Game detail view** with metadata and screenshots
- **Download queue** with progress tracking
- **Login screen** with server authentication
- **Settings screen** with configurable server address
- **SQLite-backed local database** for offline game metadata
- **Custom window chrome** with a themed UI
- **Unicode support** for international character sets
<ScreenshotCarousel screenshots={[
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Login.png').default, alt: 'Legacy Launcher Login', label: 'Login', caption: 'The login screen running on Windows 98' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Library.png').default, alt: 'Legacy Launcher Library', label: 'Library', caption: 'Browsing the game library with cover art' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Detail.png').default, alt: 'Legacy Launcher Detail', label: 'Game Detail', caption: 'Game detail view with metadata' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Downloads.png').default, alt: 'Legacy Launcher Downloads', label: 'Downloads', caption: 'The download queue in action' },
{ src: require('./_Assets/2.1.0-rc2 - Legacy Launcher - Settings.png').default, alt: 'Legacy Launcher Settings', label: 'Settings', caption: 'Configuring server connection settings' },
]} />
## LANCommander HQ
Last but far from least, this release coincides with the launch of [LANCommander HQ](https://hq.lancommander.com), a new metadata provider and community hub for LANCommander users. The HQ aims to provide a centralized platform for game metadata and media across multiple providers, while also normalizing and enhancing that data with a focus on the needs of LANCommander users. There's a bit to cover, so let's dive in!
### Metadata
LANCommander HQ aggregates metadata from multiple sources including IGDB, SteamGridDB, and user contributions. This data is then normalized and enhanced with a focus on the needs of LANCommander users. There are many metadata providers out there, and unfortunately they all have their own unique schemas and data quality issues. The HQ's goal is to provide a single source of truth for game metadata that can be consumed by LANCommander servers and launchers.
There are a lot of exciting features in the works for the HQ, but for this release the focus is on building out the core infrastructure and integrating with the new launcher. Expect to see more features and improvements in the future as the HQ continues to evolve.
### Community
The main features of the HQ are currently focused on metadata, but there is the potential to expand it into a broader community hub for LANCommander users. This could include an area to share game configurations, scripts, and other resources. Additionally, there are some content moderation tools in place that should allow the HQ to host user-generated content and provide the best possible experience for all users. If you have a knack for curation and want to help out, there will likely be opportunities to get involved in the future!
### Tiering / Subscriptions
The HQ operates on a tiered subscription model. Currently there are only two tiers: Basic (Free) and Premium ($5/month). The Basic tier provides access to the core metadata features of the HQ, while the Premium tier provides access to additional features such as enhanced metadata, relaxed rate limiting, and more. More information will be provided in the near future and will be provided on this site under the [LANCommander HQ](/hq) section. All paid subscriptions on the HQ are handled by Stripe, so every transaction is secure and your personal information is protected.
This subscription model is primarily in place to help cover the costs of running the HQ while also providing a sustainable path forward for continued development of LANCommander itself. This entire project is developed and maintained by essentially one person with some help from the community here and there. While this is a labor of love, there are real costs associated with running the infrastructure for the HQ and continued development of LANCommander. The subscription model is designed to be as accessible as possible while also providing a way for users to support the project if they find value in it.
As such, this will probably mean that the Patreon page will be going away in the near future. For existing patrons, it is recommended to switch over to the new subscription model on the HQ as it provides a more direct way to support the project and access the benefits of the HQ _today_.
## Contributions
This release includes contributions from the following community members:
**@aaronpowell | PR [#395](https://github.com/LANCommander/LANCommander/pull/395): Avalonia Launcher**
A substantial 42-commit, 6,000-line PR that bootstrapped the cross-platform Avalonia launcher. Aaron built out the core application structure, service wiring, and async initialization, and then built out the UI with filtered depot browsing and grid layout, a download queue, install/uninstall flows, a settings page, PDF manual viewer, game action bars, and a full CI pipeline for Windows, Linux, and macOS builds. A lot of the foundation that the current launcher sits on came from this PR.
**@akifreak | PR [#398](https://github.com/LANCommander/LANCommander/pull/398): Fix Game Archive Upload**
Discovered that archive uploads through the web UI were silently broken after upgrading to v2.0.2, traced the problem through the JavaScript bundle, and submitted 11 targeted fixes rather than a single bug report. The fixes covered ES module loading, null-guards on DOM elements, a FileMode.Append seek conflict, incorrect archive ID passing, and error propagation throughout the upload pipeline. The uploading process should feel much more stable now!
**@akifreak | PR [#403](https://github.com/LANCommander/LANCommander/pull/403): Fix Beacon Discovery**
Back for another round of contributions, akifreak tracked down and fixed an issue where server discovery wasn't working reliably. In some cases the launcher was actually finding servers on the network but wasn't surfacing them in the UI. The fix spans the full discovery pipeline; making the probe socket more resilient to bad data, ensuring beacon responses actually reach the client, and refreshing the server list as soon as a new server is found.
**@MasterMNB | PR [#400](https://github.com/LANCommander/LANCommander/pull/400): Avalonia Launcher Back Button**
Improved the visibility and styling of the back button in the `GameDetailView`, which was previously hard to distinguish from the background. It's a small change, but it's a real usability improvement that makes navigation feel more intentional.
## Changelog
**Launcher**
- Added: Brand new launcher built on Avalonia
- Added: Screenshots and videos in game detail carousel with lightbox viewer
- Added: Redesigned depot with categorization carousels and advanced filtering
- Added: Tools support
- Added: Download transfer speed graph
- Added: OS notifications upon game installation completion
- Added: Discord Rich Presence integration
- Added: SSO/OIDC external authentication provider support
- Added: Built-in PDF viewer for game manuals
- Added: Splash screen on startup
- Added: Verify files option in game dropdown
- Added: Markdown rendering for game descriptions
- Added: Big screen mode for fullscreen TV/controller use
- Added: Gamepad navigation across library, carousels, detail views, and overlays
- Added: Full set of CLI verbs (Install, Uninstall, Run, Sync, Import, Export, Upload, Login, Logout, ChangeAlias)
- Added: Console logging for headless CLI execution
- Changed: Refactored game/redistributable/tool install process to two-layer queue system
- Changed: Imports should be faster and execute in the background without causing major UI hiccups
- Changed: Improved memory management with proper disposal of HTTP clients, RPC clients, and cancellation tokens
- Changed: Redistributable files are now tracked and cleaned up on uninstall
- Changed: Redistributables without detect install scripts now always run the install script
- Fixed: Installing games that already have files on disk will show the verification process during install
- Fixed: Linux path variable expansion now preserves root prefix
- Fixed: Script writing for all script types
- Removed: Standalone CLI project (functionality merged into Avalonia launcher)
**Server**
- Added: LANCommander HQ metadata provider support
- Added: Support for external IDs beyond IGDB
- Added: New launcher-inspired "Preview" tool for managing art
- Added: Context-aware script editor with code completions, validation, and PowerShell type definitions
- Added: Redistributable options with schema editor and display names
- Added: RunWrapper script type with full execution support
- Added: Package versioning for archives
- Added: Bulk upload for screenshots and videos
- Added: Multiple select support in media grabber
- Added: Animated cover support (APNG auto-conversion to video)
- Added: Steam screenshot downloads
- Added: YouTube video support in media grabber
- Added: Media streaming endpoint
- Added: ffmpeg/yt-dlp availability checks and auto-install in Docker
- Added: Manual key assignment dialog and reworked key allocation
- Added: `Set-GameDetails` cmdlet for package scripts
- Added: Variable picker dialog for action editor and save path editor
- Added: Packaging dialog on archive editor
- Added: RunWrapper script type available for redistributable scripts
- Added: Background uploading with sidebar progress indicator
- Added: HQ login step in first time setup wizard
- Added: Button to download current log file
- Changed: Moved "Package" button from edit pages to archive editor
- Changed: Updated PowerShell code snippets
- Changed: Improved file manager with faster directory enumeration, toggleable columns, and cross-platform directory tree
- Fixed: Media uploads should now behave as expected
- Fixed: Game archive uploads in web UI
- Fixed: Orphaned files tool
- Fixed: Redistributable archive downloads
- Fixed: Script working directory for games
- Fixed: SteamCMD login when SteamGuard is enabled
- Fixed: Import file cleanup after completion
- Fixed: Package scripts are no longer exposed through API responses
- Fixed: Script helper now correctly maps Package and RunWrapper script filenames
- Fixed: Redistributable and tool importing
- Fixed: Media settings page failing to render
- Fixed: HQ metadata lookups
- Fixed: Log viewer under Settings / Logs (#393)
- Fixed: User approval menu item (#405)
- Fixed: Archive processing for "Use Local File"
- Fixed: "Use Local File" disabled button
- Fixed: Standalone expansions/mods shown in nested game lists (#284)
- Fixed: Missing nested game data
**Packager**
- Added: New standalone application for building LCX package files
- Added: CI workflow for building and releasing the packager
**Legacy Launcher**
- Added: Complete native Win32 launcher targeting Windows 9x
- Added: Library and depot browsing with cover art grid
- Added: Game detail view with metadata display
- Added: Download queue with SQLite-backed local database
- Added: Settings screen with YAML configuration
- Added: Custom themed window chrome
- Added: Unicode support for Win9x targets
- Added: CI workflow for automated builds
**SDK**
- Added: C++ SDK (LANCommander.SDK.Cpp) with C++14 compatibility
- Added: WinINet and libcurl HTTP backend implementations
- Added: 16 API clients covering full LANCommander API surface
- Added: RunWrapper script execution for redistributables with process lifecycle management
- Added: Logging in save packaging and upload pipeline
- Changed: Game list filtering now shows only standalone mods/expansions and main games
- Changed: Multi-target framework support
- Changed: Source-generated cmdlet registration via new `CmdletRegistrationGenerator`
- Fixed: Discovery probe socket resilience against malformed data
- Fixed: Broadcast send reliability in DiscoveryProbe
- Fixed: Beacon responses now forwarded to BeaconClient event
- Fixed: Save client now checks for newer server saves before uploading
- Fixed: Save path normalization to use forward slashes before regex matching (#266)
**Infrastructure**
- Upgraded to .NET 10
- Added ARM64 build support
- Removed legacy Photino launcher from CI
- Added nightly builds for Avalonia launcher
## Patch Updates
### 2.1.1
<details>
<summary>View 2.1.1 patch notes</summary>
#### New Features
##### User Registration in Launcher
User registration has been added back into the launcher's login screen. This was a feature from the old launcher that was missed in the port to Avalonia.
##### Game Update Support
The Avalonia launcher's update functionality was only partially-implemented. This has been completed and moved over to the new installation workflow system. Additionally, updates for games can be ignored by clicking the dropdown next to the "Update" button and selecting "Play without updating".
#### Improvements
- Game titles are now dimmed in the library when not installed, making it easier to see what's ready to play at a glance
- The games list view and depot now display a helpful message when there are no items, instead of showing a blank screen
- The launcher now downloads media on demand if it doesn't exist locally, and gracefully falls back if LibVLC fails to load
#### Bug Fixes
- Fixed bundling of LibVLC on macOS and Windows
- Fixed the display of the empty library view
- Fixed game updating
<ReleaseDownloads release="v2.1.1" />
</details>
### 2.1.2
<details>
<summary>View 2.1.2 patch notes</summary>
#### New Features
##### Redistributable Update Checking
The launcher now checks if installed redistributables are up to date and will re-run them when updates are available on the server. This ensures games always have the correct runtime dependencies.
##### Drag and Drop File Import in Script Editor
The script editor now supports drag and drop for `.ps1`, `.reg`, `.ini`, and other text files. PowerShell scripts replace the editor contents, `.reg` files are automatically converted to PowerShell commands, and other text files are inserted as escaped strings.
##### Manifest and Script Refresh
When a game has no update available, the launcher will now refresh the manifest and scripts on disk. This ensures local files stay in sync with server-side changes that don't trigger a full update.
#### Improvements
- Entire row is now clickable in the compact library list view
- Items per page selection is now persisted in data tables
- Improved process termination handling with fallback for Windows and absolute pathing for `kill` on Linux/macOS
#### Bug Fixes
- Fixed padding in the metadata lookup dialog
- Fixed HQ token retrieval on the first-time setup and integrations settings pages
- Fixed local server engine tracking (contributed by @Mavyre)
<ReleaseDownloads release="v2.1.2" />
</details>
### 2.1.3
<details>
<summary>View 2.1.3 patch notes</summary>
#### New Features
##### Allow Registration Setting
Servers now have an "Allow Registration" setting to control whether new users can register accounts. Resolves #408.
##### Server Autostop Delay
Server lifecycle management has been moved to a dedicated `ServerManager`, and servers can now be configured with an autostop delay so they shut down after a period of inactivity. Autostart/autostop for servers should be much more reliable now.
##### Close to System Tray
The launcher can now be closed to the system tray instead of exiting, keeping it running in the background.
##### Image Optimization Tool
A new server-side tool optimizes stored images to reduce disk usage and increase performance when displayed in the launcher.
##### Play Session Rework
Play session tracking has been reworked to be more reliable. Play sessions are now tracked in realtime with the launcher sending keepalives to the server. If a session is detected as stale (e.g. from a crash or improper shutdown), it will be automatically closed and the playtime will be calculated up to that point. This should result in more accurate playtime tracking and prevent issues with sessions that never end.
If, for some reason, a session is improperly created with a suspiciously long duration, a new server-side tool can be used to clean up these sessions and recalculate playtime under Settings > Tools > **Long Play Session**.
##### Working Directory from PCGamingWiki
Save paths pulled from PCGamingWiki will now attempt to set a working directory automatically.
#### Breaking Changes
##### MySQL/PostgreSQL Database Engine Reinitialization
Previous versions of LANCommander had major issues with MySQL/MariaDB and PostgreSQL database engines that would end up softlocking the server when certain operations were performed. This was caused by connections to the database being disposed of prematurely, causing the data access layer to throw exceptions and fail to recover. This has largely been resolved and should result in a much more stable experience using these other database engines.
However, due to support for MySQL/PostgreSQL falling into neglect, many of the database migrations had become out of sync and would fail to apply correctly. To resolve this, all MySQL/PostgreSQL migrations have been regenerated. This will require deployments using MySQL/PostgreSQL to recreate the database. Normally this type of change would be reserved for a major release, but given the state of the MySQL/PostgreSQL support being completely broken, this change is necessary to provide a stable experience for users of these database engines. If you are using MySQL/PostgreSQL, please make sure to back up your data and be prepared to recreate your database when updating to this version.
#### Improvements
- Performance optimizations for game details were made in the launcher, fixing potentially high CPU usage and RAM allocation with games that have videos or screenshots in the media carousel.
- Video players in the media carousel now only play when in view and pause when scrolled out of view or when the window is inactive.
- Updated Notify.NET and fixed taskbar progress reporting
- Localized remaining time and install progress status
- Adjusted styling in the game description
- Removed dead code and fixed video/screenshot loading that could block the UI
- yt-dlp is now downloaded as a self-contained build removing the dependency on Python for Linux systems.
#### Bug Fixes
- Fixed updating one-to-many relationships on the server. This should resolve various issues with redistributables, tags, genres, and platforms not saving correctly.
- Fixed an extra gap on the compact list scrollbar in the launcher
- Fixed the missing "Starting" text in the play button when a game is launching
- Fixed an exception when adding a game without a SteamGridDB API key configured
- Fixed an error thrown during first-time setup when no storage locations exist
- Regenerated MySQL/MariaDB and PostgreSQL migrations
- Fixed an issue where the identity database context could be disposed of prematurely, causing various issues with user management and authentication when using other database engines
- Improved server process termination on Linux by directly calling `kill` via `libc`
<ReleaseDownloads release="v2.1.3" />
</details>
### 2.1.4
<details>
<summary>View 2.1.4 patch notes</summary>
#### New Features
##### External Auth Provider Enhancements
External authentication providers received a major overhaul. The provider editor now supports auto-discovery of scopes and claim mappings, and claims can be mapped to roles so users are automatically assigned the correct roles when logging in over external auth. Users logging in through an external provider are also auto-provisioned.
##### Auto Redirect to External Provider
A new "Auto Redirect to Provider" setting switches authentication challenges to redirect straight to your external auth provider instead of showing the password login form. If more than one external provider is configured, a minimal provider selection page is shown instead.
##### Repack Non-Streamable ZIP Archives
A new server-side tool in the archive editor can now be used to repack archives that were not created in a streamable format. More information about why this is an issue can be found under the [Archives](/Server/Archives) documentation page.
##### WebP Media Support
Media uploads now support the WebP image format.
##### Redistributable Uninstall Scripts
Redistributables can now define an uninstall script, allowing their runtime dependencies to be cleanly removed.
##### Single Instance Launcher
The launcher now ensures only a single instance can run at a time, preventing conflicts from accidentally opening multiple copies.
#### Improvements
- Local files selected for archive upload can now be moved instead of copied, saving disk space.
- Reworked tool installation and action resolution.
- Install notifications now only display once the entire install chain is complete.
- The "Last Played" text now updates every minute and has been localized.
- Added more logging to archive extraction and made the number of install retry attempts configurable.
- A message is now shown indicating the server needs to be restarted after changes are made to authentication providers.
- Game started/stopped server scripts now run as fire and forget and will not prevent servers from starting.
- Adjusted styling for buttons and dropdowns in the launcher.
- Updated SharpCompress to 0.49.1.
- macOS builds now generate a proper `.app` bundle, and Linux builds now produce an AppImage bundled into the normal workflows.
#### Bug Fixes
- Fixed some situations where saves were not being uploaded/downloaded.
- Fixed an error in the launcher when tool actions were not defined.
- Fixed server game actions not loading correctly from the server.
- Fixed non-standalone addons appearing in the library compact list.
- HQ authentication errors are no longer swallowed.
- Fixed user promotion / role assignment.
- Fixed application icons.
<ReleaseDownloads release="v2.1.4" />
</details>
### 2.1.5
<details>
<summary>View 2.1.5 patch notes</summary>
#### New Features
##### Auto Redirect to External Provider in the Launcher
The launcher now honors the server's "Auto Redirect to Provider" setting. When enabled, the login screen redirects straight to your external authentication provider, and the standard authentication fields and buttons are hidden based on the server's authentication settings.
##### Exit Button in Profile Dropdown
An "Exit" button is now available under the profile dropdown, making it easier to fully quit the launcher.
#### Improvements
- Consolidated the game flyout into a unified context menu for more consistent actions across the library views.
- Tools that have no archive are now hidden from the install overlay.
- Automated testing for various aspects of the server UI (thanks [aaronpowell](https://github.com/aaronpowell)!)
#### Bug Fixes
- Fixed removing games from user libraries using the launcher.
- Fixed tool installation.
- Fixed the clickability of games in the compact library list.
- Fixed updating of roles.
- Fixed saving of authentication settings.
- Fixed admin user creation in first time setup.
<ReleaseDownloads release="v2.1.5" />
</details>
### 2.1.6
<details>
<summary>View 2.1.6 patch notes</summary>
#### New Features
##### User and Role Limits
Roles and individual users can now be assigned limits for save storage, total user storage, and download speed. This gives admins finer control over resource usage on shared servers. Resolves [#85](https://github.com/LANCommander/LANCommander/issues/85), [#84](https://github.com/LANCommander/LANCommander/issues/84), and [#100](https://github.com/LANCommander/LANCommander/issues/).
##### PowerShell Modules
A new "Scripting" section has been added to the server UI where PowerShell modules can be defined. These act as a library of reusable functions that can be called from any script. Modules are automatically synced to the launcher and imported when scripts are executed.
##### Per-Game Tool Tracking
Tools are now tracked on a per-game basis. Tools can be marked to "Always Install", and any tools installed for a game are automatically uninstalled when that game is uninstalled.
##### Admin User Creation Dialog
Admins can now create new users directly from the server with a new user creation dialog. Fixes [#419](https://github.com/LANCommander/LANCommander/issues/419).
##### Database Connection Editor
The connection string input used in first-time setup and server settings has been refactored into a full database connection component, making it easier to configure database connections. Fixes [#256](https://github.com/LANCommander/LANCommander/issues/256).
#### Improvements
- Added "Select All" and "Load More" buttons to the media grabber. Fixes [#417](https://github.com/LANCommander/LANCommander/issues/417).
- Metadata lookups now preserve existing values instead of overwriting them. Fixes [#420](https://github.com/LANCommander/LANCommander/issues/420).
- The Library navigation and library/depot switcher are now hidden in both the web UI and launcher when user libraries are disabled.
- Action `ServerHost` values now default to the LANCommander server address.
#### Bug Fixes
- Fixed the display of UTC times.
- Fixed creation of entities with many-to-many relationships. Ref [#414](https://github.com/LANCommander/LANCommander/issues/414).
- Fixed reconciliation of library games based on whether user libraries are enabled or disabled. Ref [#423](https://github.com/LANCommander/LANCommander/issues/423).
- The profile cache is now invalidated when logging out. Fixes [#422](https://github.com/LANCommander/LANCommander/issues/422).
<ReleaseDownloads release="v2.1.6" />
</details>
### 2.1.7
<details>
<summary>View 2.1.7 patch notes</summary>
#### Improvements
- The launcher now loads your library from a new `/Library/Games` endpoint and displays it immediately, with images rendered directly from the server. The full import still runs in the background, so offline functionality is preserved while the library becomes usable much sooner.
- Depot images are now streamed from a remote image cache instead of being downloaded to disk, reducing UI jank. Fixes [#427](https://github.com/LANCommander/LANCommander/issues/427).
- Game installs and uninstalls are now more reliable: installs run in a consistent order (base game, addons, tools, then redistributables), tools are removed before their game is uninstalled, and the game action bar refreshes after a tool is installed. Ref [#414](https://github.com/LANCommander/LANCommander/issues/414).
- The modify menu now lists installed addons.
- Save upload failures are now logged instead of failing silently.
- A running game process is now terminated as a fallback when no spawned window is detected.
- View data is now loaded before transitioning, smoothing navigation.
#### Bug Fixes
- Save path changes on a game now take effect immediately instead of requiring a server restart. Games are correctly updated when a save path is added, updated, or deleted.
- Fixed installation of games and addons that have no dependent games.
- Fixed importing legacy LCX files that have no save paths defined. Fixes [#426](https://github.com/LANCommander/LANCommander/issues/426).
- Fixed persisted page sizes in data tables.
<ReleaseDownloads release="v2.1.7" />
</details>
### 2.1.8
<details>
<summary>View 2.1.8 patch notes</summary>
#### New Features
##### Runtime Platform Targeting
Actions, scripts, and save paths can now be scoped to a specific runtime platform (Windows, Linux, or macOS). This makes it possible to define platform-specific launch actions, install/uninstall scripts, and save paths on a single game without them conflicting across operating systems. A new `Get-Runtime` PowerShell cmdlet is also available for detecting the current platform from within scripts.
#### Improvements
- The offline mode button has been moved to the titlebar for easier access.
- The game description editor in the server UI is now a full Markdown editor.
- Page functionality has been improved with better menus for adding and deleting pages, and slugs are now generated in PascalCase.
- Client/server version mismatches are now detected and logged, making it easier to diagnose connection issues caused by version drift.
- Data table sort order is now persisted.
#### Bug Fixes
- Images now load from the local cache when the launcher is in offline mode.
- Fixed enumeration of script snippets.
- Fixed PowerShell module loading in the launcher. On Windows the execution policy is set to bypass, and module load failures are now trapped and reported as a warning per module instead of failing the entire script.
<ReleaseDownloads release="v2.1.8" />
</details>
### 2.1.9
<details>
<summary>View 2.1.9 patch notes</summary>
#### Improvements
- Path resolution has been unified across the server so that storage paths are resolved consistently everywhere. A migration aligns existing settings storage paths automatically on upgrade.
- Depot queries have been optimized for better performance.
- Server notifications now use a shorter timeout so a slow or unreachable server no longer holds up the launcher.
- Updated SharpCompress to the latest version. This should resolve most extraction issues for games with large archives.
#### Bug Fixes
- Fixed detection of the primary display's resolution on some Linux multi-display configurations.
- Fixed application path resolution on the server, correcting how saves, media, archives, and updates are located.
- Improved handling of the bypass execution policy for scripts.
- Fixed installation of wine32 and winetricks.
<ReleaseDownloads release="v2.1.9" />
</details>
## Downloads
<ReleaseDownloads release="v2.1.9" />
<details>
<summary>View 2.1.8 downloads</summary>
<ReleaseDownloads release="v2.1.8" />
</details>
<details>
<summary>View 2.1.7 downloads</summary>
<ReleaseDownloads release="v2.1.7" />
</details>
<details>
<summary>View 2.1.6 downloads</summary>
<ReleaseDownloads release="v2.1.6" />
</details>
<details>
<summary>View 2.1.5 downloads</summary>
<ReleaseDownloads release="v2.1.5" />
</details>
<details>
<summary>View 2.1.4 downloads</summary>
<ReleaseDownloads release="v2.1.4" />
</details>
<details>
<summary>View 2.1.3 downloads</summary>
<ReleaseDownloads release="v2.1.3" />
</details>
<details>
<summary>View 2.1.2 downloads</summary>
<ReleaseDownloads release="v2.1.2" />
</details>
<details>
<summary>View 2.1.1 downloads</summary>
<ReleaseDownloads release="v2.1.1" />
</details>
<details>
<summary>View 2.1.0 downloads</summary>
<ReleaseDownloads release="v2.1.0" />
</details>
## Contributors
<ContributorGrid from="v2.0.2" to="v2.1.9" />

View file

@ -1,38 +0,0 @@
---
title: 2.1.1
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.1 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## New Features
### User Registration in Launcher
User registration has been added back into the launcher's login screen. This was a feature from the old launcher that was missed in the port to Avalonia.
### Game Update Support
The Avalonia launcher's update functionality was only partially-implemented. This has been completed and moved over to the new installation workflow system. Additionally, updates for games can be ignored by clicking the dropdown next to the "Update" button and selecting "Play without updating".
## Improvements
- Game titles are now dimmed in the library when not installed, making it easier to see what's ready to play at a glance
- The games list view and depot now display a helpful message when there are no items, instead of showing a blank screen
- The launcher now downloads media on demand if it doesn't exist locally, and gracefully falls back if LibVLC fails to load
## Bug Fixes
- Fixed bundling of LibVLC on macOS and Windows
- Fixed the display of the empty library view
- Fixed game updating
## Downloads
<ReleaseDownloads release="v2.1.1" />
## Contributors
<ContributorGrid from="v2.1.0" to="v2.1.1" />

View file

@ -1,41 +0,0 @@
---
title: 2.1.2
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.2 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## New Features
### Redistributable Update Checking
The launcher now checks if installed redistributables are up to date and will re-run them when updates are available on the server. This ensures games always have the correct runtime dependencies.
### Drag and Drop File Import in Script Editor
The script editor now supports drag and drop for `.ps1`, `.reg`, `.ini`, and other text files. PowerShell scripts replace the editor contents, `.reg` files are automatically converted to PowerShell commands, and other text files are inserted as escaped strings.
### Manifest and Script Refresh
When a game has no update available, the launcher will now refresh the manifest and scripts on disk. This ensures local files stay in sync with server-side changes that don't trigger a full update.
## Improvements
- Entire row is now clickable in the compact library list view
- Items per page selection is now persisted in data tables
- Improved process termination handling with fallback for Windows and absolute pathing for `kill` on Linux/macOS
## Bug Fixes
- Fixed padding in the metadata lookup dialog
- Fixed HQ token retrieval on the first-time setup and integrations settings pages
- Fixed local server engine tracking (contributed by @Mavyre)
## Downloads
<ReleaseDownloads release="v2.1.2" />
## Contributors
<ContributorGrid from="v2.1.1" to="v2.1.2" />

View file

@ -1,67 +0,0 @@
---
title: 2.1.3
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.3 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## New Features
### Allow Registration Setting
Servers now have an "Allow Registration" setting to control whether new users can register accounts. Resolves #408.
### Server Autostop Delay
Server lifecycle management has been moved to a dedicated `ServerManager`, and servers can now be configured with an autostop delay so they shut down after a period of inactivity. Autostart/autostop for servers should be much more reliable now.
### Close to System Tray
The launcher can now be closed to the system tray instead of exiting, keeping it running in the background.
### Image Optimization Tool
A new server-side tool optimizes stored images to reduce disk usage and increase performance when displayed in the launcher.
### Play Session Rework
Play session tracking has been reworked to be more reliable. Play sessions are now tracked in realtime with the launcher sending keepalives to the server. If a session is detected as stale (e.g. from a crash or improper shutdown), it will be automatically closed and the playtime will be calculated up to that point. This should result in more accurate playtime tracking and prevent issues with sessions that never end.
If, for some reason, a session is improperly created with a suspiciously long duration, a new server-side tool can be used to clean up these sessions and recalculate playtime under Settings > Tools > **Long Play Session**.
### Working Directory from PCGamingWiki
Save paths pulled from PCGamingWiki will now attempt to set a working directory automatically.
## Breaking Changes
### MySQL/PostgreSQL Database Engine Reinitialization
Previous versions of LANCommander had major issues with MySQL/MariaDB and PostgreSQL database engines that would end up softlocking the server when certain operations were performed. This was caused by connections to the database being disposed of prematurely, causing the data access layer to throw exceptions and fail to recover. This has largely been resolved and should result in a much more stable experience using these other database engines.
However, due to support for MySQL/PostgreSQL falling into neglect, many of the database migrations had become out of sync and would fail to apply correctly. To resolve this, all MySQL/PostgreSQL migrations have been regenerated. This will require deployments using MySQL/PostgreSQL to recreate the database. Normally this type of change would be reserved for a major release, but given the state of the MySQL/PostgreSQL support being completely broken, this change is necessary to provide a stable experience for users of these database engines. If you are using MySQL/PostgreSQL, please make sure to back up your data and be prepared to recreate your database when updating to this version.
## Improvements
- Performance optimizations for game details were made in the launcher, fixing potentially high CPU usage and RAM allocation with games that have videos or screenshots in the media carousel.
- Video players in the media carousel now only play when in view and pause when scrolled out of view or when the window is inactive.
- Updated Notify.NET and fixed taskbar progress reporting
- Localized remaining time and install progress status
- Adjusted styling in the game description
- Removed dead code and fixed video/screenshot loading that could block the UI
- yt-dlp is now downloaded as a self-contained build removing the dependency on Python for Linux systems.
## Bug Fixes
- Fixed updating one-to-many relationships on the server. This should resolve various issues with redistributables, tags, genres, and platforms not saving correctly.
- Fixed an extra gap on the compact list scrollbar in the launcher
- Fixed the missing "Starting" text in the play button when a game is launching
- Fixed an exception when adding a game without a SteamGridDB API key configured
- Fixed an error thrown during first-time setup when no storage locations exist
- Regenerated MySQL/MariaDB and PostgreSQL migrations
- Fixed an issue where the identity database context could be disposed of prematurely, causing various issues with user management and authentication when using other database engines
- Improved server process termination on Linux by directly calling `kill` via `libc`
## Downloads
<ReleaseDownloads release="v2.1.3" />
## Contributors
<ContributorGrid from="v2.1.2" to="v2.1.3" />

View file

@ -1,61 +0,0 @@
---
title: 2.1.4
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.4 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## New Features
### External Auth Provider Enhancements
External authentication providers received a major overhaul. The provider editor now supports auto-discovery of scopes and claim mappings, and claims can be mapped to roles so users are automatically assigned the correct roles when logging in over external auth. Users logging in through an external provider are also auto-provisioned.
### Auto Redirect to External Provider
A new "Auto Redirect to Provider" setting switches authentication challenges to redirect straight to your external auth provider instead of showing the password login form. If more than one external provider is configured, a minimal provider selection page is shown instead.
### Repack Non-Streamable ZIP Archives
A new server-side tool in the archive editor can now be used to repack archives that were not created in a streamable format. More information about why this is an issue can be found under the [Archives](/Server/Archives) documentation page.
### WebP Media Support
Media uploads now support the WebP image format.
### Redistributable Uninstall Scripts
Redistributables can now define an uninstall script, allowing their runtime dependencies to be cleanly removed.
### Single Instance Launcher
The launcher now ensures only a single instance can run at a time, preventing conflicts from accidentally opening multiple copies.
## Improvements
- Local files selected for archive upload can now be moved instead of copied, saving disk space.
- Reworked tool installation and action resolution.
- Install notifications now only display once the entire install chain is complete.
- The "Last Played" text now updates every minute and has been localized.
- Added more logging to archive extraction and made the number of install retry attempts configurable.
- A message is now shown indicating the server needs to be restarted after changes are made to authentication providers.
- Game started/stopped server scripts now run as fire and forget and will not prevent servers from starting.
- Adjusted styling for buttons and dropdowns in the launcher.
- Updated SharpCompress to 0.49.1.
- macOS builds now generate a proper `.app` bundle, and Linux builds now produce an AppImage bundled into the normal workflows.
## Bug Fixes
- Fixed some situations where saves were not being uploaded/downloaded.
- Fixed an error in the launcher when tool actions were not defined.
- Fixed server game actions not loading correctly from the server.
- Fixed non-standalone addons appearing in the library compact list.
- HQ authentication errors are no longer swallowed.
- Fixed user promotion / role assignment.
- Fixed application icons.
## Downloads
<ReleaseDownloads release="v2.1.4" />
## Contributors
<ContributorGrid from="v2.1.3" to="v2.1.4" />

View file

@ -1,41 +0,0 @@
---
title: 2.1.5
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.5 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## New Features
### Auto Redirect to External Provider in the Launcher
The launcher now honors the server's "Auto Redirect to Provider" setting. When enabled, the login screen redirects straight to your external authentication provider, and the standard authentication fields and buttons are hidden based on the server's authentication settings.
### Exit Button in Profile Dropdown
An "Exit" button is now available under the profile dropdown, making it easier to fully quit the launcher.
## Improvements
- Consolidated the game flyout into a unified context menu for more consistent actions across the library views.
- Tools that have no archive are now hidden from the install overlay.
- Automated testing for various aspects of the server UI (thanks [aaronpowell](https://github.com/aaronpowell)!)
## Bug Fixes
- Fixed removing games from user libraries using the launcher.
- Fixed tool installation.
- Fixed the clickability of games in the compact library list.
- Fixed updating of roles.
- Fixed saving of authentication settings.
- Fixed admin user creation in first time setup.
## Downloads
<ReleaseDownloads release="v2.1.5" />
## Contributors
<ContributorGrid from="v2.1.4" to="v2.1.5" />

View file

@ -1,49 +0,0 @@
---
title: 2.1.6
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.6 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## New Features
### User and Role Limits
Roles and individual users can now be assigned limits for save storage, total user storage, and download speed. This gives admins finer control over resource usage on shared servers. Resolves [#85](https://github.com/LANCommander/LANCommander/issues/85), [#84](https://github.com/LANCommander/LANCommander/issues/84), and [#100](https://github.com/LANCommander/LANCommander/issues/).
### PowerShell Modules
A new "Scripting" section has been added to the server UI where PowerShell modules can be defined. These act as a library of reusable functions that can be called from any script. Modules are automatically synced to the launcher and imported when scripts are executed.
### Per-Game Tool Tracking
Tools are now tracked on a per-game basis. Tools can be marked to "Always Install", and any tools installed for a game are automatically uninstalled when that game is uninstalled.
### Admin User Creation Dialog
Admins can now create new users directly from the server with a new user creation dialog. Fixes [#419](https://github.com/LANCommander/LANCommander/issues/419).
### Database Connection Editor
The connection string input used in first-time setup and server settings has been refactored into a full database connection component, making it easier to configure database connections. Fixes [#256](https://github.com/LANCommander/LANCommander/issues/256).
## Improvements
- Added "Select All" and "Load More" buttons to the media grabber. Fixes [#417](https://github.com/LANCommander/LANCommander/issues/417).
- Metadata lookups now preserve existing values instead of overwriting them. Fixes [#420](https://github.com/LANCommander/LANCommander/issues/420).
- The Library navigation and library/depot switcher are now hidden in both the web UI and launcher when user libraries are disabled.
- Action `ServerHost` values now default to the LANCommander server address.
## Bug Fixes
- Fixed the display of UTC times.
- Fixed creation of entities with many-to-many relationships. Ref [#414](https://github.com/LANCommander/LANCommander/issues/414).
- Fixed reconciliation of library games based on whether user libraries are enabled or disabled. Ref [#423](https://github.com/LANCommander/LANCommander/issues/423).
- The profile cache is now invalidated when logging out. Fixes [#422](https://github.com/LANCommander/LANCommander/issues/422).
## Downloads
<ReleaseDownloads release="v2.1.6" />
## Contributors
<ContributorGrid from="v2.1.5" to="v2.1.6" />

View file

@ -1,35 +0,0 @@
---
title: 2.1.7
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.7 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## Improvements
- The launcher now loads your library from a new `/Library/Games` endpoint and displays it immediately, with images rendered directly from the server. The full import still runs in the background, so offline functionality is preserved while the library becomes usable much sooner.
- Depot images are now streamed from a remote image cache instead of being downloaded to disk, reducing UI jank. Fixes [#427](https://github.com/LANCommander/LANCommander/issues/427).
- Game installs and uninstalls are now more reliable: installs run in a consistent order (base game, addons, tools, then redistributables), tools are removed before their game is uninstalled, and the game action bar refreshes after a tool is installed. Ref [#414](https://github.com/LANCommander/LANCommander/issues/414).
- The modify menu now lists installed addons.
- Save upload failures are now logged instead of failing silently.
- A running game process is now terminated as a fallback when no spawned window is detected.
- View data is now loaded before transitioning, smoothing navigation.
## Bug Fixes
- Save path changes on a game now take effect immediately instead of requiring a server restart. Games are correctly updated when a save path is added, updated, or deleted.
- Fixed installation of games and addons that have no dependent games.
- Fixed importing legacy LCX files that have no save paths defined. Fixes [#426](https://github.com/LANCommander/LANCommander/issues/426).
- Fixed persisted page sizes in data tables.
## Downloads
<ReleaseDownloads release="v2.1.7" />
## Contributors
<ContributorGrid from="v2.1.6" to="v2.1.7" />

View file

@ -1,37 +0,0 @@
---
title: 2.1.8
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.8 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## New Features
### Runtime Platform Targeting
Actions, scripts, and save paths can now be scoped to a specific runtime platform (Windows, Linux, or macOS). This makes it possible to define platform-specific launch actions, install/uninstall scripts, and save paths on a single game without them conflicting across operating systems. A new `Get-Runtime` PowerShell cmdlet is also available for detecting the current platform from within scripts.
## Improvements
- The offline mode button has been moved to the titlebar for easier access.
- The game description editor in the server UI is now a full Markdown editor.
- Page functionality has been improved with better menus for adding and deleting pages, and slugs are now generated in PascalCase.
- Client/server version mismatches are now detected and logged, making it easier to diagnose connection issues caused by version drift.
- Data table sort order is now persisted.
## Bug Fixes
- Images now load from the local cache when the launcher is in offline mode.
- Fixed enumeration of script snippets.
- Fixed PowerShell module loading in the launcher. On Windows the execution policy is set to bypass, and module load failures are now trapped and reported as a warning per module instead of failing the entire script.
## Downloads
<ReleaseDownloads release="v2.1.8" />
## Contributors
<ContributorGrid from="v2.1.7" to="v2.1.8" />

View file

@ -1,32 +0,0 @@
---
title: 2.1.9
---
import ReleaseDownloads from '@site/src/components/ReleaseDownloads';
import ContributorGrid from '@site/src/components/ContributorGrid';
# LANCommander 2.1.9 Release Notes
:::info Full Release Notes
This is a patch release for the **2.1 series**. For the complete feature overview and all patch notes, visit the [LANCommander 2.1.0 release notes](./2.1.0).
:::
## Improvements
- Path resolution has been unified across the server so that storage paths are resolved consistently everywhere. A migration aligns existing settings storage paths automatically on upgrade.
- Depot queries have been optimized for better performance.
- Server notifications now use a shorter timeout so a slow or unreachable server no longer holds up the launcher.
- Updated SharpCompress to the latest version. This should resolve most extraction issues for games with large archives.
## Bug Fixes
- Fixed detection of the primary display's resolution on some Linux multi-display configurations.
- Fixed application path resolution on the server, correcting how saves, media, archives, and updates are located.
- Improved handling of the bypass execution policy for scripts.
- Fixed installation of wine32 and winetricks.
## Downloads
<ReleaseDownloads release="v2.1.9" />
## Contributors
<ContributorGrid from="v2.1.8" to="v2.1.9" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -248,216 +248,6 @@ The companion to `Get-UserCustomField`, this cmdlet lets you update or set the v
Update-UserCustomField -Name "SteamId" -Value "34950494"
```
## `ConvertFrom-SerializedBase64`
Deserializes a Base64-encoded YAML string back into an object.
### Syntax
```powershell
ConvertFrom-SerializedBase64
-Input <string>
```
### Description
The `ConvertFrom-SerializedBase64` cmdlet takes a Base64-encoded string containing YAML-serialized data, decodes it, and deserializes it back into a PowerShell object. This is the companion to `ConvertTo-SerializedBase64` and is useful for reading data that was previously serialized and encoded for storage or transport. Accepts pipeline input.
### Example
```powershell
$data = ConvertFrom-SerializedBase64 -Input "TmFtZTogSGVsbG8="
# Or via pipeline
"TmFtZTogSGVsbG8=" | ConvertFrom-SerializedBase64
```
## `ConvertTo-SerializedBase64`
Serializes an object to YAML and encodes it as a Base64 string.
### Syntax
```powershell
ConvertTo-SerializedBase64
-Input <object>
```
### Description
The `ConvertTo-SerializedBase64` cmdlet takes any object, serializes it to YAML, and then encodes the result as a Base64 string. This is useful for storing or transmitting structured data in a compact, text-safe format. Accepts pipeline input.
### Example
```powershell
$obj = @{ Name = "Hello"; Value = 42 }
$encoded = ConvertTo-SerializedBase64 -Input $obj
# Or via pipeline
$obj | ConvertTo-SerializedBase64
```
## `Edit-PatchGameSpy`
Patches GameSpy master server references in game files to point to a replacement server.
### Syntax
```powershell
Edit-PatchGameSpy
-Path <string>
-Hostname <string> (optional, default: "openspy.net")
-PublicKey <string> (optional, default: OpenSpy public key)
-BinariesToPatch <string[]> (optional, default: "*.dll", "*.exe")
-TextFilesToPatch <string[]> (optional, default: "*.ini", "*.cfg", "*.conf")
```
### Description
The `Edit-PatchGameSpy` cmdlet scans a game's directory for binary and text files that reference GameSpy master servers and patches them to use a replacement server (OpenSpy by default). For binary files, it replaces the `gamespy.com` hostname and public key at the byte level. For text files, it handles Unreal Engine configuration patterns including UT99 and Unreal 2 master server list entries. The replacement hostname must be exactly 12 characters to match the original `gamespy.com` length.
### Example
```powershell
# Patch all GameSpy references to use OpenSpy (default)
Edit-PatchGameSpy -Path "$InstallDirectory"
# Patch with custom hostname and file patterns
Edit-PatchGameSpy -Path "$InstallDirectory" -Hostname "openspy.net" -BinariesToPatch "*.dll","*.exe","*.so"
```
## `Get-HorizontalFov`
Calculates a horizontal field of view scaled for the current display's aspect ratio.
### Syntax
```powershell
Get-HorizontalFov
-Width <int> (optional, defaults to primary display width)
-Height <int> (optional, defaults to primary display height)
-BaseFov <int> (optional, default: 90)
```
### Description
The `Get-HorizontalFov` cmdlet calculates a scaled horizontal field of view based on the display's aspect ratio relative to a 4:3 baseline. Many older games use a default 90-degree horizontal FOV designed for 4:3 displays. This cmdlet computes the correct horizontal FOV for wider aspect ratios so that the visible area matches what was intended. If `-Width` and `-Height` are not specified, the primary display's resolution is used automatically.
### Example
```powershell
# Get FOV for the current display with default 90-degree base
$fov = Get-HorizontalFov
Write-Host "Horizontal FOV: $fov"
# Get FOV for a specific resolution with a custom base FOV
$fov = Get-HorizontalFov -Width 2560 -Height 1440 -BaseFov 90
Write-Host "Horizontal FOV: $fov" # Returns 106
```
## `Get-VerticalFov`
Calculates a vertical field of view scaled for the current display's aspect ratio.
### Syntax
```powershell
Get-VerticalFov
-Width <int> (optional, defaults to primary display width)
-Height <int> (optional, defaults to primary display height)
-BaseFov <int> (optional, default: 75)
```
### Description
The `Get-VerticalFov` cmdlet calculates a scaled vertical field of view based on the display's aspect ratio relative to a 4:3 baseline. Some games use vertical FOV for their configuration. This cmdlet computes the correct vertical FOV for wider aspect ratios. If `-Width` and `-Height` are not specified, the primary display's resolution is used automatically.
### Example
```powershell
# Get vertical FOV for the current display
$fov = Get-VerticalFov
Write-Host "Vertical FOV: $fov"
# Get vertical FOV for a specific resolution
$fov = Get-VerticalFov -Width 1920 -Height 1080 -BaseFov 75
Write-Host "Vertical FOV: $fov" # Returns 59
```
## `Get-SanitizedPath`
Removes invalid filename characters from a path string.
### Syntax
```powershell
Get-SanitizedPath
-Path <string>
```
### Description
The `Get-SanitizedPath` cmdlet strips invalid filename characters from the provided path string. This is useful when constructing file paths from user input or game titles that may contain characters not allowed in file names. Accepts pipeline input.
### Example
```powershell
$clean = Get-SanitizedPath -Path "Game: The Sequel?"
Write-Host $clean # Returns "Game The Sequel"
# Or via pipeline
"Game: The Sequel?" | Get-SanitizedPath
```
## `Expand-LatestArchive`
Downloads and extracts the latest archive for a game, redistributable, or tool.
### Syntax
```powershell
Expand-LatestArchive
-DestinationPath <string> (optional, defaults to current directory)
-GameId <Guid> (optional, resolved from $GameManifest or $Game)
-RedistributableId <Guid> (optional, resolved from $Redistributable)
-ToolId <Guid> (optional, resolved from $Tool)
```
### Description
The `Expand-LatestArchive` cmdlet extracts the latest archive for a game, redistributable, or tool. When run in a server-side packaging context (where `$LatestArchivePath` is set), it reads directly from the local archive file. When run in a client-side context, it downloads the archive from the LANCommander server via the API. The target ID is automatically resolved from context variables (`$Redistributable`, `$Tool`, `$GameManifest`, or `$Game`) if not explicitly provided. Returns a `DirectoryInfo` object pointing to the extraction destination.
### Example
```powershell
# Extract to the current directory using the context variable
Expand-LatestArchive
# Extract to a specific directory
Expand-LatestArchive -DestinationPath "C:\Games\MyGame"
# Extract a specific game's archive
Expand-LatestArchive -GameId "a1b2c3d4-e5f6-7890-abcd-ef1234567890" -DestinationPath "C:\Staging"
# Extract a redistributable's archive
Expand-LatestArchive -RedistributableId "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# Extract a tool's archive
Expand-LatestArchive -ToolId "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
```
## `New-Package`
Creates a package result object for the packaging pipeline.
### Syntax
```powershell
New-Package
-Path <string>
-Version <string>
-Changelog <string> (optional)
```
### Description
The `New-Package` cmdlet creates a `Package` object with the specified path, version, and optional changelog, and sets it as the script's return value. This cmdlet is used in [Package](/Scripting/Script Types/Package) scripts to define the output of the packaging process. The `-Path` parameter should point to the directory containing the package contents to be archived.
### Example
```powershell
# Create a package from a build output directory
New-Package -Path "C:\Build\Output" -Version "1.0.0" -Changelog "Initial release"
# Minimal usage
New-Package -Path "$BuildDirectory" -Version "2.1.0"
```
## `Out-PlayerAvatar`
Downloads the current player's avatar from the server.
### Syntax
```powershell
Out-PlayerAvatar
```
### Description
The `Out-PlayerAvatar` cmdlet retrieves the current authenticated player's avatar from the LANCommander server. Returns the avatar file path as a string. This cmdlet takes no parameters and must be run within a LANCommander script context where the player is authenticated.
### Example
```powershell
$avatarPath = Out-PlayerAvatar
Write-Host "Avatar saved to: $avatarPath"
```
# Steam-Related Cmdlets
The following cmdlets provide functionality for interacting with SteamCMD and the Steam Store API. These cmdlets enable you to install Steam games, manage SteamCMD profiles, search for games, and retrieve Steam assets.

View file

@ -1,51 +0,0 @@
---
title: Archives
---
# Archives
Games, redistributables, and tools are distributed to clients as ZIP archives. When a launcher installs a game it **streams the archive straight from the server and extracts it on the fly**. This keeps installs fast and avoids needing double the disk space, but it means the archive has to be readable from start to finish without seeking backwards.
Most ZIP files satisfy this without any special effort. A small number of archives, however, are written in a layout that a streaming reader cannot extract reliably. This page explains how to create streaming-safe archives and how to fix existing ones.
## Why some archives fail to install
A streaming reader discovers each file's boundaries as the bytes arrive. For that to work, every entry must declare its size up front, in its **local file header**.
Some archiving tools instead write entries in a *streaming* layout, where the size is unknown when the entry starts and is recorded afterwards in a trailing **data descriptor**. For compressed entries this is fine as the compressed bytes cannot be mistaken for the data descriptor. However, if you store an uncompressed entry that _happens to be another archive_, this can confuse the streaming reader and cause it to improperly determine the size of the entry being extracted.
:::info
The problematic combination is specifically **stored (uncompressed) + streaming data descriptor**. A stored entry whose size *is* in its local header installs fine at any size, and compressed entries are unaffected.
:::
## Creating streaming-safe archives
The safest rule of thumb: **create archives with a tool writing to a file** (not piping to a stream), and let large already-compressed payloads be stored with their sizes recorded normally. Writing to a real file lets the tool go back and fill in each entry's size in its local header instead of using a streaming data descriptor.
| Tool | Recommendation |
|------|----------------|
| **7-Zip** (GUI or `7z`) | Safe by defaul. Use a normal `Add to archive` / `7z a archive.zip files` and write to a _local disk_, not a network share. |
| **Windows Explorer** (Send to → Compressed folder) | Safe by default. |
| **Info-ZIP `zip`** | Safe when writing to a file (`zip -r archive.zip folder`). Avoid piping to stdout (`zip - ...`), which forces streaming data descriptors. |
| **PowerShell `Compress-Archive`** | Safe by default, do not write to a network share. |
For payloads larger than 4 GiB, make sure the tool produces a **ZIP64** archive (all the tools above do this automatically when needed).
:::info
Future versions of the launcher will have a built-in packaging tool to help in the creation of archives.
:::
## Checking and repacking existing archives
The server can detect and fix archives that use the problematic layout. On a game's **Archives** tab, each uploaded archive has a **Check Streaming Compatibility** action (the shield icon).
1. Click **Check Streaming Compatibility** on an archive.
2. If the archive is safe, you'll get a confirmation message and nothing else happens.
3. If it contains stored entries written with a streaming data descriptor, you'll be prompted to **repack** it.
4. Choosing to repack queues a background job that rewrites the archive into a streaming-safe layout. Compression is preserved per entry so the repack does not waste time re-compressing already-compressed data. The job runs in the background and can take a while for multi-gigabyte archives.
Repacking rewrites the file in place once complete and recalculates its reported sizes. File contents (and their CRCs) are unchanged; only the archive's internal layout is corrected.
:::warning
Repacking reads and rewrites the entire archive, so it temporarily needs free disk space roughly equal to the archive's size in the same storage location.
:::

View file

@ -66,73 +66,18 @@ A dictionary of option definitions. Options can be nested to create logical grou
Each option definition supports the following fields:
| Field | Type | Description |
|-------------------------|------------|----------------------------------------------------------------------------------------------------|
| `Type` | `string` | The data type: `string`, `bool`, `int`, `choice`, or `list` |
| `Default` | `string` | The default value if none is configured. For `list`, a YAML sequence (see below). |
| `Description` | `string` | A human-readable description shown in the admin UI |
| `Required` | `bool` | Whether the option must be configured |
| Field | Type | Description |
|-------------------------|------------|--------------------------------------------------------------------|
| `Type` | `string` | The data type: `string`, `bool`, or `enum` |
| `Default` | `string` | The default value if none is configured |
| `Description` | `string` | A human-readable description shown in the admin UI |
| `Required` | `bool` | Whether the option must be configured |
| `IsEnvironmentVariable` | `bool` | If `true`, the resolved value is set as a process environment variable using the option's key name |
| `Choices` | `string[]` | Available values for `choice` type options |
| `Options` | `dict` | Child options for creating nested groups |
| `ItemType` | `string` | For scalar `list` options: the type of each item (`string`, `int`, `bool`). Defaults to `string`. |
| `Fields` | `dict` | For composite `list` options: per-row sub-schema. Presence of `Fields` makes the list composite. |
| `MinItems` / `MaxItems` | `int` | For `list` options: lower/upper bounds on the number of rows. Both optional. |
| `Choices` | `string[]` | Available values for `enum` type options |
| `Options` | `dict` | Child options for creating nested groups |
Nested options are flattened using dot-notation keys for storage and resolution (e.g., `Game.GAMEID`). When `IsEnvironmentVariable` is `true`, only the leaf key name is used as the environment variable name (e.g., `GAMEID`, not `Game.GAMEID`).
### List Options
Use `Type: list` when an option needs to hold a variable number of values — for example, repeated INI entries like `ListFactories[0]=…`, `ListFactories[1]=…`, etc.
A list option is either **scalar** (a list of plain values) or **composite** (a list of records). The shape of each row is determined entirely by the schema; how the values are emitted into the target config is up to the redistributable's scripts.
**Scalar list** — each row is a single value:
```yaml
Options:
AllowedHosts:
Type: list
ItemType: string
Default:
- localhost
- example.com
```
**Composite list** — each row is a record with named fields. A list-level `Default:` seeds initial rows; per-field `Default:` inside `Fields` is used to prefill new rows when an admin clicks "+ Add Item":
```yaml
Options:
MasterServers:
Type: list
DisplayName: Master Servers
Fields:
Address:
Type: string
Default: master.example.com
Port:
Type: int
Default: 28900
GameName:
Type: string
Default: unreal
MinItems: 1
MaxItems: 16
Default:
- { Address: master.oldunreal.com, Port: 28900, GameName: unreal }
- { Address: master.hlkclan.net, Port: 28900, GameName: unreal }
```
**Consuming list values in scripts.** `Get-RedistributableOptions` hydrates list values into native PowerShell arrays — scalar lists become typed arrays (`string[]`/`int[]`/`bool[]`), composite lists become arrays of `PSObject`s keyed by `Fields`. The script decides the on-disk format:
```powershell
$opts = Get-RedistributableOptions -Path $InstallDirectory -Id $GameId -Name "UBrowser"
$lines = @()
for ($i = 0; $i -lt $opts.MasterServers.Count; $i++) {
$s = $opts.MasterServers[$i]
$lines += "ListFactories[$i]=UBrowser.UBrowserGSpyFact,MasterServerAddress=$($s.Address),MasterServerTCPPort=$($s.Port),GameName=$($s.GameName)"
}
$lines | Set-Content (Join-Path $InstallDirectory "System/UnrealTournament.ini")
```
**Environment variables.** `IsEnvironmentVariable: true` is ignored for `Type: list` because environment variables are scalar. Read list values from `Get-RedistributableOptions` in a script instead.
## Per-Game Options
When a game is assigned a redistributable that has an option schema, the game's **Redistributables** page will display form fields for each option. Administrators can configure values specific to that game (e.g., setting the correct `GAMEID` for protonfixes). These values are stored on the game-redistributable relationship and override the schema defaults.

View file

@ -13,13 +13,6 @@ Scopes:
- email
```
:::tip
Because Authentik exposes an OpenID Connect configuration URL, you can use the **Discover**
button on the provider to map the standard claims and add the base scopes automatically.
See the [Authentication overview](/Server/Settings/Authentication/Overview#discovery-oidc)
for details.
:::
Make sure to set your redirect URLs appropriately. LANCommander expects the following redirect URL scheme:
```http(s)://<ServerAddress>/SignInOIDC```

View file

@ -1,132 +0,0 @@
---
title: Authentication
sidebar_label: Overview
sidebar_position: 1
---
# Authentication
LANCommander can delegate sign-in to external identity providers in addition to its
built-in local accounts. Two provider protocols are supported:
- **OpenID Connect (OIDC)** — recommended. The provider exposes a discovery document
(the *well-known configuration URL*) that LANCommander uses to resolve all of its
endpoints automatically.
- **OAuth2** — for providers that do not offer OIDC discovery. You supply each endpoint
(authorization, token, user info) by hand.
:::info
SAML is listed in the provider type list but is **not implemented**. Selecting it will
prevent the provider from being registered.
:::
Providers are configured under **Settings → Authentication → External Providers**. A
**server restart is required** for changes to authentication providers to take effect.
## Configuring a provider
Each provider shares a common set of fields, plus a few that depend on the type.
| Field | Applies to | Description |
| --- | --- | --- |
| Name | All | Display name shown on the login button. |
| Color / Icon | All | Styling for the login button. |
| Type | All | `OAuth2` or `OpenIdConnect`. |
| Client ID / Client Secret | All | Credentials issued by the provider. |
| Configuration Endpoint | OIDC | The provider's `.well-known/openid-configuration` URL. |
| Authorization / Token / User Info Endpoint | OAuth2 | The provider's individual endpoints. |
| Scopes | All | Scopes requested during sign-in (see below). |
| Claim Mappings | All | How provider claims map onto LANCommander users (see below). |
### Redirect URLs
When registering LANCommander with your provider, configure the redirect (callback) URL
to match the protocol:
| Type | Redirect URL |
| --- | --- |
| OpenID Connect | `http(s)://<ServerAddress>/SignInOIDC` |
| OAuth2 | `http(s)://<ServerAddress>/SignInOAuth` |
:::info
If you see `Correlation failed.` errors in the logs, review your
[cookie policy settings](/Server/Settings/Authentication/Security).
:::
## Scopes
Scopes determine which information the provider releases during sign-in. At minimum an
OIDC provider needs `openid`; `profile` and `email` are commonly added so the user's
name and email claims are returned. Some providers expose a `roles` or `groups` scope
for [role synchronization](#role-synchronization).
## Claim mappings
A **claim mapping** projects a claim returned by the provider onto a destination claim
that LANCommander understands and applies to the user on login.
- **Claim** (the source) is a key in the provider's user-info response, e.g.
`preferred_username`.
- **Destination** (the target) is one of the well-known names below.
For OIDC providers the configured claim mappings run over the user-info endpoint
response, so make sure the scopes you request actually cause those claims to be returned.
### Recognized destinations
| Destination | Maps to | Notes |
| --- | --- | --- |
| `nameidentifier` | External unique ID | **Required** — links the provider login to a LANCommander account. |
| `name` | Username | |
| `email` | Email address | |
| `alias` | Display alias | |
| `role` (or `roles`) | Role name(s) | Array values are expanded into multiple roles; nested keys are supported with dotted paths (e.g. `realm_access.roles`). Each value is used directly as a role name. |
The full `http://schemas.xmlsoap.org/...` claim URIs are also accepted for `name`,
`email`, and `nameidentifier`. When no username claim is available (or it collides with
an existing local account), the user is sent to manual registration to finish linking.
## Discovery (OIDC)
For OpenID Connect providers, the **Discover** button next to the claim mappings reads
the provider's discovery document and configures the provider for you:
- **Standard claims are mapped automatically.** When the provider advertises them, the
following are mapped:
| Destination | Source claim (first advertised wins) |
| --- | --- |
| `nameidentifier` | `sub` |
| `email` | `email` |
| `name` | `preferred_username``name``username` |
| `alias` | `nickname``name` |
| `role` | `roles``groups` |
- **Base scopes are added automatically.** `openid` is always added (it is required for
the OIDC flow); `profile`, `email`, `roles`, and `groups` are added when the provider
advertises them.
- Any other advertised claims appear as clickable suggestions you can add as mappings,
and as autocomplete options while editing a mapping.
Discovery never overwrites mappings or scopes you have already configured, and re-running
it adds nothing new.
:::info
The discovery document's `claims_supported` and `scopes_supported` lists are **advisory**.
They are optional in the OIDC spec and many providers under-report them, so treat the
results as suggestions — you can always add claims and scopes manually.
:::
## Role synchronization
When a provider login supplies role claims (mapped to `role`), LANCommander syncs the
user's roles on every login:
- Roles named in the claims that don't yet exist are created automatically.
- Roles the user no longer has in the claims are removed — **except** the Administrator
role and the configured default role, which are never removed automatically.
## Provider examples
See the [External Providers](/Server/Settings/Authentication/External%20Providers/Authentik)
section for ready-to-use configuration examples.

View file

@ -4,9 +4,9 @@ using Avalonia.Skia;
// This assembly-level attribute tells Avalonia.Headless.XUnit which AppBuilder to use.
// All [AvaloniaFact] and [AvaloniaTheory] tests in this project run under this headless app.
[assembly: AvaloniaTestApplication(typeof(LANCommander.Launcher.Tests.TestAppBuilder))]
[assembly: AvaloniaTestApplication(typeof(LANCommander.Launcher.Avalonia.Tests.TestAppBuilder))]
namespace LANCommander.Launcher.Tests;
namespace LANCommander.Launcher.Avalonia.Tests;
public class TestAppBuilder
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -3,7 +3,7 @@ using System.IO;
using Avalonia.Controls;
using Avalonia.Headless;
namespace LANCommander.Launcher.Tests.Helpers;
namespace LANCommander.Launcher.Avalonia.Tests.Helpers;
/// <summary>
/// Captures and persists screenshots from headless Avalonia windows.

View file

@ -4,7 +4,7 @@ using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace LANCommander.Launcher.Tests.Helpers;
namespace LANCommander.Launcher.Avalonia.Tests.Helpers;
/// <summary>
/// Pixel-level image comparison for visual regression detection.

View file

@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LANCommander.Launcher\LANCommander.Launcher.csproj" />
<ProjectReference Include="..\LANCommander.Launcher.Avalonia\LANCommander.Launcher.Avalonia.csproj" />
</ItemGroup>
<!-- Baseline images are committed to source and copied to output for tests to load -->

View file

@ -0,0 +1,8 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LANCommander.Launcher.Avalonia.Tests.TestApp"
RequestedThemeVariant="Dark">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View file

@ -1,7 +1,7 @@
using Avalonia;
using Avalonia.Markup.Xaml;
namespace LANCommander.Launcher.Tests;
namespace LANCommander.Launcher.Avalonia.Tests;
public partial class TestApp : Application
{

View file

@ -3,17 +3,16 @@ using System.Collections.ObjectModel;
using System.IO;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using LANCommander.Launcher.Services;
using LANCommander.Launcher.Tests.Helpers;
using LANCommander.Launcher.ViewModels;
using LANCommander.Launcher.ViewModels.Components;
using LANCommander.Launcher.Views;
using LANCommander.Launcher.Views.Components;
using LANCommander.Launcher.Avalonia.Tests.Helpers;
using LANCommander.Launcher.Avalonia.ViewModels;
using LANCommander.Launcher.Avalonia.ViewModels.Components;
using LANCommander.Launcher.Avalonia.Views;
using LANCommander.Launcher.Avalonia.Views.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
namespace LANCommander.Launcher.Tests.Tests;
namespace LANCommander.Launcher.Avalonia.Tests.Tests;
/// <summary>
/// Renders each major view in the headless Avalonia environment and compares the
@ -32,23 +31,12 @@ public class ViewLayoutTests
private const int WindowWidth = 1200;
private const int WindowHeight = 800;
static ViewLayoutTests()
{
// The Login, Splash and ServerSelection views pick a random full-screen
// background on load. Disable that here so the captured screenshots — and the
// committed baselines — are deterministic; otherwise every run compares against
// a different photo and reports a spurious regression.
ViewBackground.Enabled = false;
}
// ---------------------------------------------------------------------------
// Service provider shared by all tests that need ViewModels with DI dependencies.
// Minimal: logging plus navigation — GameDetailViewModel resolves INavigationService
// in its constructor. No real SDK services needed for layout-only rendering.
// Minimal: just logging — no real SDK services needed for layout-only rendering.
// ---------------------------------------------------------------------------
private static readonly IServiceProvider _testServices = new ServiceCollection()
.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Warning))
.AddSingleton<INavigationService, NavigationService>()
.BuildServiceProvider();
// ---------------------------------------------------------------------------

View file

@ -1,15 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:LANCommander.Launcher.Converters"
x:Class="LANCommander.Launcher.App"
xmlns:converters="using:LANCommander.Launcher.Avalonia.Converters"
x:Class="LANCommander.Launcher.Avalonia.App"
RequestedThemeVariant="Dark">
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://LANCommander.Launcher/Theme/LANCommander.axaml" />
<StyleInclude Source="avares://LANCommander.Launcher/Theme/Carousel.axaml" />
<StyleInclude Source="avares://LANCommander.Launcher/Theme/Badge.axaml" />
<StyleInclude Source="avares://LANCommander.Launcher.Avalonia/Theme/LANCommander.axaml" />
<StyleInclude Source="avares://LANCommander.Launcher.Avalonia/Theme/Carousel.axaml" />
<StyleInclude Source="avares://LANCommander.Launcher.Avalonia/Theme/Badge.axaml" />
<!-- Global control defaults -->
<Style Selector="Button,IconButton">
@ -25,37 +25,6 @@
<Setter Property="FontSize" Value="16" />
</Style>
<!-- System tray menu. Avalonia's Win32 tray popup is a Window named
"AvaloniaTrayPopupRoot_<tooltip>"; our tooltip is "LANCommander"
(see TrayIconExtensions). The menu mixes icon items (recently played
games) with text-only items (Depot/Library/Settings/Exit). These styles
are scoped to that window so the in-app menus are unaffected. -->
<!-- Symmetric padding so item text is vertically centered. The Fluent default
(11,4,11,7) is bottom-heavy, which leaves the text a couple px above center. -->
<Style Selector="Window#AvaloniaTrayPopupRoot_LANCommander MenuItem">
<Setter Property="Padding" Value="11,6,11,6" />
</Style>
<!-- Text-only items have no icon, but Fluent reserves a shared icon column so
every item aligns. That column can't be removed from a style (the template
sets the shared-size scope as a local value), so instead pull icon-less
items' text left by the icon column width (icon 16 + 12 right margin = 28)
so they sit flush-left at the games' icon edge, not at the game titles. -->
<Style Selector="Window#AvaloniaTrayPopupRoot_LANCommander MenuItem:not(:icon) /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="Margin" Value="-28,0,0,0" />
</Style>
<!-- Make the section separators span the full menu width. -->
<Style Selector="Window#AvaloniaTrayPopupRoot_LANCommander Separator">
<Setter Property="Margin" Value="0,4,0,4" />
</Style>
<!-- Offset the vertical scrollbar so it doesn't overlap the floating titlebar -->
<Style Selector="ScrollViewer.TitlebarOffset /template/ ScrollBar#PART_VerticalScrollBar">
<Setter Property="Margin" Value="0,40,0,0" />
</Style>
<Style Selector="ListBox.TitlebarOffset /template/ ScrollViewer /template/ ScrollBar#PART_VerticalScrollBar">
<Setter Property="Margin" Value="0,40,0,0" />
</Style>
<!-- Icon control template (PathIcon ControlTheme is not inherited by subclasses in Avalonia 11) -->
<!-- Default: stroke-based rendering for Regular, Light, Thin, DuoTone, Bold variants -->
<Style Selector="Icon">
@ -92,14 +61,14 @@
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://LANCommander.Launcher/Assets/Icons/Bold.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher/Assets/Icons/DuoTone.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher/Assets/Icons/Fill.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher/Assets/Icons/Light.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher/Assets/Icons/Regular.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher/Assets/Icons/Thin.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher/Assets/Localization/en-US.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher/Theme/ColorPalette.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Assets/Icons/Bold.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Assets/Icons/DuoTone.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Assets/Icons/Fill.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Assets/Icons/Light.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Assets/Icons/Regular.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Assets/Icons/Thin.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Assets/Localization/en-US.axaml" />
<ResourceInclude Source="avares://LANCommander.Launcher.Avalonia/Theme/ColorPalette.axaml" />
</ResourceDictionary.MergedDictionaries>
<converters:MultiplyConverter x:Key="MultiplyConverter" />

View file

@ -1,337 +1,308 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using LANCommander.Launcher.Input;
using LANCommander.Launcher.Helpers;
using LANCommander.Launcher.Services;
using LANCommander.Launcher.ViewModels;
using LANCommander.Launcher.Views;
using LANCommander.Launcher.Services;
using LANCommander.Launcher.Services.Extensions;
using LANCommander.SDK;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Providers;
using LANCommander.SDK.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Notify.NET.Extensions;
namespace LANCommander.Launcher;
public partial class App : Application
{
public static IServiceProvider? Services { get; private set; }
private static ILogger<App>? _logger;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
try
{
// Configure services
var services = new ServiceCollection();
ConfigureServices(services);
Services = services.BuildServiceProvider();
_logger = Services.GetRequiredService<ILogger<App>>();
_logger.LogInformation("LANCommander Avalonia Launcher starting...");
// Remove Avalonia's built-in data validation plugin to avoid duplicate validations
var dataValidationPlugins = BindingPlugins.DataValidators;
for (var i = dataValidationPlugins.Count - 1; i >= 0; i--)
{
if (dataValidationPlugins[i] is DataAnnotationsValidationPlugin)
dataValidationPlugins.RemoveAt(i);
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
var mainViewModel = Services.GetRequiredService<MainWindowViewModel>();
if (Program.BigScreenMode)
mainViewModel.SetBigScreenMode();
var mainWindow = new MainWindow
{
DataContext = mainViewModel
};
mainWindow.Closed += (sender, args) =>
{
_logger?.LogWarning("MainWindow Closed event fired");
};
mainWindow.Closing += (sender, args) =>
{
_logger?.LogWarning("MainWindow Closing event fired");
};
desktop.MainWindow = mainWindow;
// Bind the taskbar progress indicator to the main window handle. This must be
// wired BEFORE Show(): on Windows, Show() raises Opened synchronously, so a
// handler attached afterwards would never fire and the progress bar would stay
// bound to Notify.NET's default GetConsoleWindow() target instead of the app.
void BindTaskbarProgress()
{
var hwnd = mainWindow.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
if (hwnd != IntPtr.Zero)
Services.GetRequiredService<TaskbarProgressService>().Initialize(hwnd);
}
mainWindow.Opened += (_, _) => BindTaskbarProgress();
mainWindow.Show();
// If Opened already fired synchronously during Show(), the handler above missed
// it; bind now since the handle is available once the window is shown.
BindTaskbarProgress();
// System tray icon: the main window hides to tray on close, so the tray
// provides navigation and an exit path. See TrayIconExtensions.
var trayIcon = mainWindow.CreateTrayIcon(mainViewModel);
TrayIcon.SetIcons(this, new TrayIcons { trayIcon });
// Single-instance pipe server: forward notification-click navigations
var singleInstance = Services.GetRequiredService<SingleInstanceService>();
singleInstance.RegisterProtocolHandler();
singleInstance.StartServer();
singleInstance.NavigateToGameRequested += async (_, gameId) =>
{
// The window may be hidden in the tray; surface it on the UI thread
// before navigating (this fires from the named-pipe listener thread).
Avalonia.Threading.Dispatcher.UIThread.Post(mainWindow.RestoreFromTray);
var shell = Services.GetRequiredService<MainWindowViewModel>().ShellViewModel;
await shell.NavigateToGameByIdAsync(gameId).ConfigureAwait(false);
};
// A second launch (e.g. the user forgot it was hiding in the tray) asks the
// running instance to restore its window instead of opening a duplicate.
singleInstance.RestoreRequested += (_, _) =>
Avalonia.Threading.Dispatcher.UIThread.Post(mainWindow.RestoreFromTray);
mainWindow.Closed += (_, _) => singleInstance.Dispose();
// Start gamepad navigation (gracefully disabled if SDL3 is absent)
var gamepadService = Services.GetRequiredService<GamepadService>();
mainWindow.Closed += (_, _) => gamepadService.Stop();
gamepadService.Start();
_logger.LogInformation("Main window created and shown, IsVisible={IsVisible}", mainWindow.IsVisible);
}
base.OnFrameworkInitializationCompleted();
// Perform async initialization AFTER framework initialization is complete
// This ensures the window is shown and the message loop is running
_ = InitializeApplicationAsync();
}
catch (Exception ex)
{
_logger?.LogCritical(ex, "Fatal error during initialization");
Console.Error.WriteLine($"Fatal error during initialization: {ex}");
throw;
}
}
private async Task InitializeApplicationAsync()
{
try
{
_logger?.LogInformation("Starting async initialization...");
// Initialize application (same order as main Launcher/Program.cs)
using (var scope = Services!.CreateScope())
{
var connectionClient = scope.ServiceProvider.GetRequiredService<IConnectionClient>();
var settingsProvider = scope.ServiceProvider.GetRequiredService<SettingsProvider<Settings.Settings>>();
var databaseContext = scope.ServiceProvider.GetRequiredService<Data.DatabaseContext>();
// Connect to server
_logger?.LogInformation("Connecting to server...");
await connectionClient.ConnectAsync().ConfigureAwait(false);
if (!await connectionClient.PingAsync().ConfigureAwait(false))
{
_logger?.LogWarning("Server not reachable, enabling offline mode");
await connectionClient.EnableOfflineModeAsync().ConfigureAwait(false);
}
// Set default install directory if not configured
if (settingsProvider.CurrentValue.Games.InstallDirectories.Length == 0)
{
_logger?.LogInformation("Setting default install directory");
settingsProvider.Update(static s => s.Games.InstallDirectories = GetOSPlatform() switch
{
var platform when platform == OSPlatform.Windows => [Path.Combine(Path.GetPathRoot(AppContext.BaseDirectory) ?? "C:", "Games")],
var platform when platform == OSPlatform.Linux => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Games")],
var platform when platform == OSPlatform.OSX => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Games")],
_ => throw new NotSupportedException("Unsupported OS platform")
});
}
// Run database migrations
_logger?.LogInformation("Running database migrations...");
await databaseContext.Database.MigrateAsync().ConfigureAwait(false);
await databaseContext.EnableWalModeAsync().ConfigureAwait(false);
_logger?.LogInformation("Database migrations complete");
}
// Initialize the view model on the UI thread
var mainViewModel = Services!.GetRequiredService<MainWindowViewModel>();
_logger?.LogInformation("Initializing view model...");
await mainViewModel.InitializeAsync().ConfigureAwait(false);
_logger?.LogInformation("View model initialized, application ready");
}
catch (Exception ex)
{
_logger?.LogCritical(ex, "Fatal error during async initialization");
Console.Error.WriteLine($"Fatal error during async initialization: {ex}");
}
}
private static void ConfigureServices(IServiceCollection services)
{
// Configure logging to console and file
var logDirectory = Path.Combine(AppPaths.GetConfigDirectory(), "Logs");
Directory.CreateDirectory(logDirectory);
var logFilePath = Path.Combine(logDirectory, $"avalonia-launcher-{DateTime.Now:yyyy-MM-dd}.log");
services.AddLogging(builder =>
{
builder.SetMinimumLevel(LogLevel.Debug);
builder.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
builder.AddFilter("System.Net.Http", LogLevel.Warning);
builder.AddConsole();
builder.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.TimestampFormat = "[HH:mm:ss] ";
});
// Add file logging via a simple provider
builder.AddProvider(new FileLoggerProvider(logFilePath));
});
// Add HttpClient (required by SDK services)
services.AddHttpClient();
// Configure settings from file (same as main launcher's AddSettings())
var configurationBuilder = new ConfigurationBuilder();
var configuration = configurationBuilder.ReadFromFile<Settings.Settings>();
var refresher = configurationBuilder.ReadFromServer<Settings.Settings>(configuration);
configuration = configurationBuilder.Build();
services.Configure<Settings.Settings>(configuration);
services.AddSingleton(refresher); // Register without interface, same as main launcher
// Add SDK client and Launcher services
services.AddLANCommanderClient<Settings.Settings>();
services.AddLANCommanderLauncher();
// InstallService must be a singleton so all consumers (GameActionBarViewModel,
// DownloadQueueViewModel, etc.) share the same queue and event subscriptions.
// This overrides the scoped registration from AddLANCommanderLauncher().
services.AddSingleton<InstallService>();
// ViewModels
services.AddSingleton<MainWindowViewModel>();
// Input
services.AddSingleton<GamepadService>();
// Platform services
services.AddNotifications(opts =>
{
opts.AppName = "LANCommander";
opts.AppUserModelId = "LANCommander.Launcher";
});
services.AddTaskbarProgress(opts =>
{
opts.DesktopFileId = "LANCommander.Launcher";
});
services.AddSingleton<NotificationService>();
services.AddSingleton<TaskbarProgressService>();
services.AddSingleton<SingleInstanceService>();
services.AddSingleton<INavigationService, NavigationService>();
}
private static OSPlatform GetOSPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return OSPlatform.Windows;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return OSPlatform.Linux;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return OSPlatform.OSX;
throw new NotSupportedException("Unsupported OS platform");
}
}
/// <summary>
/// Simple file logger provider for debugging
/// </summary>
public class FileLoggerProvider : ILoggerProvider
{
private readonly string _filePath;
private readonly object _lock = new();
public FileLoggerProvider(string filePath)
{
_filePath = filePath;
}
public ILogger CreateLogger(string categoryName) => new FileLogger(_filePath, categoryName, _lock);
public void Dispose() { }
}
public class FileLogger : ILogger
{
private readonly string _filePath;
private readonly string _categoryName;
private readonly object _lock;
public FileLogger(string filePath, string categoryName, object lockObj)
{
_filePath = filePath;
_categoryName = categoryName;
_lock = lockObj;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Debug;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel)) return;
var message = $"[{DateTime.Now:HH:mm:ss}] [{logLevel}] [{_categoryName}] {formatter(state, exception)}";
if (exception != null)
message += Environment.NewLine + exception;
lock (_lock)
{
try
{
File.AppendAllText(_filePath, message + Environment.NewLine);
}
catch
{
// Ignore file write errors
}
}
}
}
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using LANCommander.Launcher.Avalonia.Input;
using LANCommander.Launcher.Avalonia.Services;
using LANCommander.Launcher.Avalonia.ViewModels;
using LANCommander.Launcher.Avalonia.Views;
using LANCommander.Launcher.Services;
using LANCommander.Launcher.Services.Extensions;
using LANCommander.SDK;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Providers;
using LANCommander.SDK.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Notify.NET.Extensions;
namespace LANCommander.Launcher.Avalonia;
public partial class App : Application
{
public static IServiceProvider? Services { get; private set; }
private static ILogger<App>? _logger;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
try
{
// Configure services
var services = new ServiceCollection();
ConfigureServices(services);
Services = services.BuildServiceProvider();
_logger = Services.GetRequiredService<ILogger<App>>();
_logger.LogInformation("LANCommander Avalonia Launcher starting...");
// Remove Avalonia's built-in data validation plugin to avoid duplicate validations
var dataValidationPlugins = BindingPlugins.DataValidators;
for (var i = dataValidationPlugins.Count - 1; i >= 0; i--)
{
if (dataValidationPlugins[i] is DataAnnotationsValidationPlugin)
dataValidationPlugins.RemoveAt(i);
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
var mainViewModel = Services.GetRequiredService<MainWindowViewModel>();
var mainWindow = new MainWindow
{
DataContext = mainViewModel
};
mainWindow.Closed += (sender, args) =>
{
_logger?.LogWarning("MainWindow Closed event fired");
};
mainWindow.Closing += (sender, args) =>
{
_logger?.LogWarning("MainWindow Closing event fired");
};
desktop.MainWindow = mainWindow;
mainWindow.Show();
// Initialize taskbar progress service with the window handle
mainWindow.Opened += (_, _) =>
{
var hwnd = mainWindow.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
if (hwnd != IntPtr.Zero)
Services.GetRequiredService<TaskbarProgressService>().Initialize(hwnd);
};
// Single-instance pipe server: forward notification-click navigations
var singleInstance = Services.GetRequiredService<SingleInstanceService>();
singleInstance.RegisterProtocolHandler();
singleInstance.StartServer();
singleInstance.NavigateToGameRequested += async (_, gameId) =>
{
mainWindow.Activate();
var shell = Services.GetRequiredService<MainWindowViewModel>().ShellViewModel;
await shell.NavigateToGameByIdAsync(gameId).ConfigureAwait(false);
};
mainWindow.Closed += (_, _) => singleInstance.Dispose();
// Start gamepad navigation (gracefully disabled if SDL2 is absent)
var gamepadService = Services.GetRequiredService<GamepadService>();
mainWindow.Closed += (_, _) => gamepadService.Stop();
gamepadService.Start();
_logger.LogInformation("Main window created and shown, IsVisible={IsVisible}", mainWindow.IsVisible);
}
base.OnFrameworkInitializationCompleted();
// Perform async initialization AFTER framework initialization is complete
// This ensures the window is shown and the message loop is running
_ = InitializeApplicationAsync();
}
catch (Exception ex)
{
_logger?.LogCritical(ex, "Fatal error during initialization");
Console.Error.WriteLine($"Fatal error during initialization: {ex}");
throw;
}
}
private async Task InitializeApplicationAsync()
{
try
{
_logger?.LogInformation("Starting async initialization...");
// Initialize application (same order as main Launcher/Program.cs)
using (var scope = Services!.CreateScope())
{
var connectionClient = scope.ServiceProvider.GetRequiredService<IConnectionClient>();
var settingsProvider = scope.ServiceProvider.GetRequiredService<SettingsProvider<Settings.Settings>>();
var databaseContext = scope.ServiceProvider.GetRequiredService<Data.DatabaseContext>();
// Connect to server
_logger?.LogInformation("Connecting to server...");
await connectionClient.ConnectAsync().ConfigureAwait(false);
if (!await connectionClient.PingAsync().ConfigureAwait(false))
{
_logger?.LogWarning("Server not reachable, enabling offline mode");
await connectionClient.EnableOfflineModeAsync().ConfigureAwait(false);
}
// Set default install directory if not configured
if (settingsProvider.CurrentValue.Games.InstallDirectories.Length == 0)
{
_logger?.LogInformation("Setting default install directory");
settingsProvider.Update(static s => s.Games.InstallDirectories = GetOSPlatform() switch
{
var platform when platform == OSPlatform.Windows => [Path.Combine(Path.GetPathRoot(AppContext.BaseDirectory) ?? "C:", "Games")],
var platform when platform == OSPlatform.Linux => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Games")],
var platform when platform == OSPlatform.OSX => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Games")],
_ => throw new NotSupportedException("Unsupported OS platform")
});
}
// Run database migrations
_logger?.LogInformation("Running database migrations...");
await databaseContext.Database.MigrateAsync().ConfigureAwait(false);
await databaseContext.EnableWalModeAsync().ConfigureAwait(false);
_logger?.LogInformation("Database migrations complete");
}
// Initialize the view model on the UI thread
var mainViewModel = Services!.GetRequiredService<MainWindowViewModel>();
_logger?.LogInformation("Initializing view model...");
await mainViewModel.InitializeAsync().ConfigureAwait(false);
_logger?.LogInformation("View model initialized, application ready");
}
catch (Exception ex)
{
_logger?.LogCritical(ex, "Fatal error during async initialization");
Console.Error.WriteLine($"Fatal error during async initialization: {ex}");
}
}
private static void ConfigureServices(IServiceCollection services)
{
// Configure logging to console and file
var logDirectory = Path.Combine(AppPaths.GetConfigDirectory(), "Logs");
Directory.CreateDirectory(logDirectory);
var logFilePath = Path.Combine(logDirectory, $"avalonia-launcher-{DateTime.Now:yyyy-MM-dd}.log");
services.AddLogging(builder =>
{
builder.SetMinimumLevel(LogLevel.Debug);
builder.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
builder.AddFilter("System.Net.Http", LogLevel.Warning);
builder.AddConsole();
builder.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.TimestampFormat = "[HH:mm:ss] ";
});
// Add file logging via a simple provider
builder.AddProvider(new FileLoggerProvider(logFilePath));
});
// Add HttpClient (required by SDK services)
services.AddHttpClient();
// Configure settings from file (same as main launcher's AddSettings())
var configurationBuilder = new ConfigurationBuilder();
var configuration = configurationBuilder.ReadFromFile<Settings.Settings>();
var refresher = configurationBuilder.ReadFromServer<Settings.Settings>(configuration);
configuration = configurationBuilder.Build();
services.Configure<Settings.Settings>(configuration);
services.AddSingleton(refresher); // Register without interface, same as main launcher
// Add SDK client and Launcher services
services.AddLANCommanderClient<Settings.Settings>();
services.AddLANCommanderLauncher();
// InstallService must be a singleton so all consumers (GameActionBarViewModel,
// DownloadQueueViewModel, etc.) share the same queue and event subscriptions.
// This overrides the scoped registration from AddLANCommanderLauncher().
services.AddSingleton<InstallService>();
// ViewModels
services.AddSingleton<MainWindowViewModel>();
// Input
services.AddSingleton<GamepadService>();
// Platform services
services.AddNotifications(opts =>
{
opts.AppName = "LANCommander";
opts.AppUserModelId = "LANCommander.Launcher";
});
services.AddSingleton<NotificationService>();
services.AddSingleton<TaskbarProgressService>();
services.AddSingleton<SingleInstanceService>();
services.AddSingleton<INavigationService, NavigationService>();
}
private static OSPlatform GetOSPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return OSPlatform.Windows;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return OSPlatform.Linux;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return OSPlatform.OSX;
throw new NotSupportedException("Unsupported OS platform");
}
}
/// <summary>
/// Simple file logger provider for debugging
/// </summary>
public class FileLoggerProvider : ILoggerProvider
{
private readonly string _filePath;
private readonly object _lock = new();
public FileLoggerProvider(string filePath)
{
_filePath = filePath;
}
public ILogger CreateLogger(string categoryName) => new FileLogger(_filePath, categoryName, _lock);
public void Dispose() { }
}
public class FileLogger : ILogger
{
private readonly string _filePath;
private readonly string _categoryName;
private readonly object _lock;
public FileLogger(string filePath, string categoryName, object lockObj)
{
_filePath = filePath;
_categoryName = categoryName;
_lock = lockObj;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Debug;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel)) return;
var message = $"[{DateTime.Now:HH:mm:ss}] [{logLevel}] [{_categoryName}] {formatter(state, exception)}";
if (exception != null)
message += Environment.NewLine + exception;
lock (_lock)
{
try
{
File.AppendAllText(_filePath, message + Environment.NewLine);
}
catch
{
// Ignore file write errors
}
}
}
}

View file

@ -1,3 +1,3 @@
[assembly: Avalonia.Metadata.XmlnsDefinition(
"https://github.com/avaloniaui",
"LANCommander.Launcher.Controls")]
"LANCommander.Launcher.Avalonia.Controls")]

View file

@ -92,7 +92,7 @@
<!-- ── Notifications ─────────────────────────────────────────────────── -->
<sys:String x:Key="InstallComplete">Installation Complete!</sys:String>
<sys:String x:Key="InstallFailed">Installation Failed!</sys:String>
<sys:String x:Key="InstallationFailed">Installation Failed!</sys:String>
<sys:String x:Key="ViewInLibrary">View in Library</sys:String>
<sys:String x:Key="Play">Play</sys:String>
@ -101,7 +101,6 @@
<sys:String x:Key="Complete">Complete</sys:String>
<sys:String x:Key="TransferSpeed">Transfer Speed (MB/s)</sys:String>
<sys:String x:Key="TransferSpeedHistory">Transfer Speed History (MB/s)</sys:String>
<sys:String x:Key="TimeRemaining">{0} remaining</sys:String>
<!-- ── Install Tasks ─────────────────────────────────────────────────── -->
<sys:String x:Key="TaskVerifyFiles">Verify local files</sys:String>
@ -115,24 +114,6 @@
<sys:String x:Key="TaskDownloadManuals">Download manuals</sys:String>
<sys:String x:Key="TaskDownloadRedist">Download {0}</sys:String>
<sys:String x:Key="TaskInstallRedist">Install {0}</sys:String>
<!-- ── Install Status ────────────────────────────────────────────────── -->
<sys:String x:Key="InstallStatusComplete">Complete</sys:String>
<sys:String x:Key="InstallStatusFailed">Failed</sys:String>
<sys:String x:Key="InstallStatusQueued">Queued</sys:String>
<!-- ── Play Stats ────────────────────────────────────────────────────── -->
<sys:String x:Key="PlayStatNone">None</sys:String>
<sys:String x:Key="PlayTimeMinutes">{0} minutes</sys:String>
<sys:String x:Key="PlayTimeHours">{0} hours</sys:String>
<sys:String x:Key="LastPlayedNever">Never</sys:String>
<sys:String x:Key="LastPlayedJustNow">Just now</sys:String>
<sys:String x:Key="LastPlayedMinuteAgo">{0} minute ago</sys:String>
<sys:String x:Key="LastPlayedMinutesAgo">{0} minutes ago</sys:String>
<sys:String x:Key="LastPlayedHourAgo">{0} hour ago</sys:String>
<sys:String x:Key="LastPlayedHoursAgo">{0} hours ago</sys:String>
<sys:String x:Key="LastPlayedDayAgo">{0} day ago</sys:String>
<sys:String x:Key="LastPlayedDaysAgo">{0} days ago</sys:String>
<!-- ── Game Detail ───────────────────────────────────────────────────── -->
<sys:String x:Key="Singleplayer">Singleplayer</sys:String>

View file

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 681 KiB

After

Width:  |  Height:  |  Size: 681 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

@ -4,29 +4,14 @@ using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Transformation;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Input;
namespace LANCommander.Launcher.Controls;
/// <summary>
/// Implemented by carousel item content (e.g. inline video players) that should
/// only run while on-screen. The carousel toggles activity as items scroll in
/// and out of view to avoid wasting CPU on off-screen playback.
/// </summary>
public interface ICarouselPlaybackItem
{
void SetCarouselActive(bool active);
}
namespace LANCommander.Launcher.Avalonia.Controls;
public class CarouselControl : TemplatedControl
{
@ -137,9 +122,6 @@ public class CarouselControl : TemplatedControl
private Transitions? _transitions;
private RectangleGeometry? _clipGeometry;
private int _virtualIndex = 0;
private double _currentOffsetX = 0;
private WindowBase? _hostWindow;
private bool _windowActive = true;
static CarouselControl()
{
@ -158,13 +140,6 @@ public class CarouselControl : TemplatedControl
_leftButton = e.NameScope.Find<Button>("PART_LeftButton");
_rightButton = e.NameScope.Find<Button>("PART_RightButton");
if (_itemsControl != null)
{
// Items load progressively (e.g. media popping in), so recompute which
// items are on-screen whenever a container is realized.
_itemsControl.ContainerPrepared += (_, _) => UpdateItemVisibility();
}
if (_clipPanel != null)
{
_clipPanel.SizeChanged += (_, _) => { UpdateClipGeometry(); SnapToCurrentIndex(); };
@ -173,13 +148,11 @@ public class CarouselControl : TemplatedControl
if (_itemsContainer != null)
{
// Scroll via a composited RenderTransform rather than Margin so the
// animation doesn't trigger a layout pass over every item each frame.
_transitions = new Transitions
{
new TransformOperationsTransition
new ThicknessTransition
{
Property = RenderTransformProperty,
Property = MarginProperty,
Duration = TimeSpan.FromMilliseconds(350),
Easing = new CubicEaseOut()
}
@ -196,174 +169,6 @@ public class CarouselControl : TemplatedControl
RebuildInternalSource();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
// Pause playback items while the window is in the background — there's no
// point decoding video the user can't see.
_hostWindow = TopLevel.GetTopLevel(this) as WindowBase;
if (_hostWindow != null)
{
_windowActive = _hostWindow.IsActive;
_hostWindow.Activated += OnHostWindowActivated;
_hostWindow.Deactivated += OnHostWindowDeactivated;
}
UpdateItemVisibility();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
if (_hostWindow != null)
{
_hostWindow.Activated -= OnHostWindowActivated;
_hostWindow.Deactivated -= OnHostWindowDeactivated;
_hostWindow = null;
}
base.OnDetachedFromVisualTree(e);
}
private void OnHostWindowActivated(object? sender, EventArgs e)
{
_windowActive = true;
UpdateItemVisibility();
}
private void OnHostWindowDeactivated(object? sender, EventArgs e)
{
_windowActive = false;
UpdateItemVisibility();
}
/// <summary>
/// When a child item gains focus (e.g. via gamepad), scroll to keep it visible.
/// </summary>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
// Only scroll when focus was gained via keyboard/gamepad navigation.
// Pointer-initiated focus should not scroll, as it moves the item out
// from under the cursor and prevents the click from registering.
if (e.NavigationMethod == NavigationMethod.Pointer)
return;
if (_itemsControl == null || e.Source is not Visual focusedVisual)
return;
var index = GetItemIndexForVisual(focusedVisual);
if (index >= 0)
ScrollToItemIndex(index);
}
/// <summary>
/// Handle Left/Right arrow keys to navigate between carousel items.
/// </summary>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Handled) return;
if (e.Key is not (Key.Left or Key.Right))
return;
if (_itemsControl == null)
return;
var focusedElement = TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() as Visual;
if (focusedElement == null)
return;
var currentIndex = GetItemIndexForVisual(focusedElement);
if (currentIndex < 0)
return;
var itemCount = _itemsControl.ItemCount;
if (itemCount == 0)
return;
var newIndex = e.Key == Key.Right ? currentIndex + 1 : currentIndex - 1;
if (WrapItems)
{
// At logical index 0 going left, let the event bubble
// so cross-pane navigation (e.g. to sidebar) can handle it
var sourceCount = GetItemCount();
if (e.Key == Key.Left && sourceCount > 0 && currentIndex % sourceCount == 0)
return;
newIndex = ((newIndex % itemCount) + itemCount) % itemCount;
}
else
{
if (newIndex < 0 || newIndex >= itemCount)
return; // Let the event bubble to navigate to adjacent controls
}
var container = _itemsControl.ContainerFromIndex(newIndex);
if (container == null)
return;
var focusTarget = container.GetVisualDescendants()
.OfType<InputElement>()
.FirstOrDefault(el => el.Focusable) ?? container as InputElement;
if (focusTarget != null)
{
focusTarget.Focus(NavigationMethod.Directional);
ScrollToItemIndex(newIndex);
e.Handled = true;
}
}
/// <summary>
/// Determines the item index for a visual that is (or is within) an item container.
/// </summary>
private int GetItemIndexForVisual(Visual visual)
{
if (_itemsControl == null) return -1;
var panel = _itemsControl.ItemsPanelRoot;
if (panel == null) return -1;
// Walk up the visual tree until we find a direct child of the items panel
var current = visual;
while (current != null)
{
if (current is Control control && current.GetVisualParent() == panel)
{
return _itemsControl.IndexFromContainer(control);
}
current = current.GetVisualParent() as Visual;
}
return -1;
}
/// <summary>
/// Scrolls the carousel so the item at the given index is visible.
/// </summary>
private void ScrollToItemIndex(int displayIndex)
{
var sourceCount = GetItemCount();
if (sourceCount == 0) return;
if (WrapItems)
{
// In wrap mode, items are tripled. Map display index to virtual index.
_virtualIndex = displayIndex;
SelectedIndex = displayIndex % sourceCount;
ScrollAnimated(Offset(-displayIndex * (ItemWidth + Gap)));
}
else
{
SelectedIndex = Math.Clamp(displayIndex, 0, sourceCount - 1);
ScrollAnimated(Offset(-ClampedOffset(SelectedIndex, sourceCount)));
}
}
private void RebuildInternalSource()
{
if (_itemsControl == null) return;
@ -427,67 +232,18 @@ public class CarouselControl : TemplatedControl
}
}
private void ScrollAnimated(double offsetX)
private void ScrollAnimated(double leftMargin)
{
if (_itemsContainer == null) return;
_currentOffsetX = offsetX;
_itemsContainer.RenderTransform = TranslateX(offsetX);
UpdateItemVisibility();
_itemsContainer.Margin = new Thickness(leftMargin, 0, 0, 0);
}
private void ScrollInstant(double offsetX)
private void ScrollInstant(double leftMargin)
{
if (_itemsContainer == null) return;
_currentOffsetX = offsetX;
_itemsContainer.Transitions = null;
_itemsContainer.RenderTransform = TranslateX(offsetX);
_itemsContainer.Margin = new Thickness(leftMargin, 0, 0, 0);
_itemsContainer.Transitions = _transitions;
UpdateItemVisibility();
}
private static ITransform TranslateX(double x) =>
TransformOperations.Parse(
$"translateX({x.ToString("F3", CultureInfo.InvariantCulture)}px)");
/// <summary>
/// Activates the carousel items whose horizontal span currently intersects the
/// viewport (and only while the host window is active) and deactivates the rest,
/// so off-screen or background playback items (e.g. inline videos) stop consuming
/// CPU. Deferred when containers aren't realized yet.
/// </summary>
private void UpdateItemVisibility()
{
if (_itemsControl == null || _clipPanel == null) return;
var count = _itemsControl.ItemCount;
if (count == 0) return;
var viewportWidth = _clipPanel.Bounds.Width;
if (viewportWidth <= 0)
{
// Layout hasn't run yet; retry once it has.
Dispatcher.UIThread.Post(UpdateItemVisibility, DispatcherPriority.Loaded);
return;
}
// _currentOffsetX is the (negative) translate applied to the container,
// which itself starts at -ItemOverflow (see Offset). The first visible
// pixel in item-space is therefore the inverse of that, minus the padding.
var scroll = -_currentOffsetX - ItemOverflow;
var step = ItemWidth + Gap;
for (var i = 0; i < count; i++)
{
var itemLeft = i * step;
var itemRight = itemLeft + ItemWidth;
var visible = _windowActive && itemRight > scroll && itemLeft < scroll + viewportWidth;
var container = _itemsControl.ContainerFromIndex(i);
if (container == null) continue;
foreach (var item in container.GetVisualDescendants().OfType<ICarouselPlaybackItem>())
item.SetCarouselActive(visible);
}
}
private void SnapToCurrentIndex()

View file

@ -7,7 +7,7 @@ using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
namespace LANCommander.Launcher.Controls;
namespace LANCommander.Launcher.Avalonia.Controls;
/// <summary>
/// A page transition that immediately hides the old content and fades in the new content.

View file

@ -2,7 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace LANCommander.Launcher.Controls;
namespace LANCommander.Launcher.Avalonia.Controls;
/// <summary>
/// Icon variant (weight) corresponding to the Phosphor icon families.

View file

@ -2,7 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace LANCommander.Launcher.Controls;
namespace LANCommander.Launcher.Avalonia.Controls;
public enum IconPosition
{

View file

@ -2,16 +2,16 @@ using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using LANCommander.Launcher.Helpers;
using LANCommander.Launcher.Avalonia.Helpers;
namespace LANCommander.Launcher.Controls;
namespace LANCommander.Launcher.Avalonia.Controls;
/// <summary>
/// Plays a video file muted and looping, rendering frames directly to the
/// Avalonia render surface. The output preserves the source video's aspect
/// ratio (uniform stretch, centred within the control bounds).
/// </summary>
public class InlineVideoPlayer : Control, IDisposable, ICarouselPlaybackItem
public class InlineVideoPlayer : Control, IDisposable
{
public static readonly StyledProperty<string?> VideoPathProperty =
AvaloniaProperty.Register<InlineVideoPlayer, string?>(nameof(VideoPath));
@ -26,12 +26,6 @@ public class InlineVideoPlayer : Control, IDisposable, ICarouselPlaybackItem
private bool _isAttached;
private bool _disposed;
/// <summary>
/// Whether the carousel currently considers this item visible. Playback only
/// runs while both visible and attached, so off-screen videos don't decode.
/// </summary>
private bool _isActive;
/// <summary>Current playback position in milliseconds.</summary>
public long CurrentTimeMs => _renderer?.Player?.Time ?? 0;
@ -55,7 +49,7 @@ public class InlineVideoPlayer : Control, IDisposable, ICarouselPlaybackItem
StopPlayback();
var path = change.GetNewValue<string?>();
if (!string.IsNullOrEmpty(path) && _isAttached && _isActive)
if (!string.IsNullOrEmpty(path) && _isAttached)
StartPlayback(path);
}
}
@ -65,47 +59,10 @@ public class InlineVideoPlayer : Control, IDisposable, ICarouselPlaybackItem
base.OnAttachedToVisualTree(e);
_isAttached = true;
// Playback is started by the carousel via SetCarouselActive once it has
// determined which items are on-screen.
if (_isActive && !string.IsNullOrEmpty(VideoPath) && _renderer == null)
if (!string.IsNullOrEmpty(VideoPath) && _renderer == null)
StartPlayback(VideoPath);
}
// ── Carousel visibility ──────────────────────────────────────────────
/// <summary>
/// Called by the carousel to indicate whether this item is on-screen.
/// Visible items play (creating the renderer lazily); off-screen items pause
/// so they stop decoding frames and consuming CPU.
/// </summary>
public void SetCarouselActive(bool active)
{
if (_isActive == active)
return;
_isActive = active;
if (!_isAttached || _disposed)
return;
if (active)
{
if (_renderer == null)
{
if (!string.IsNullOrEmpty(VideoPath))
StartPlayback(VideoPath);
}
else
{
_renderer.Player?.SetPause(false);
}
}
else
{
_renderer?.Player?.SetPause(true);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_isAttached = false;

Some files were not shown because too many files have changed in this diff Show more