From 8917a2d79f1f331370f9f1dead13051f498ed3d3 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 30 May 2026 13:10:01 -0500 Subject: [PATCH] Add package application for auto-building LCX files --- .github/workflows/LANCommander.Packager.yml | 100 +++++ .github/workflows/LANCommander.Release.yml | 18 + Directory.Packages.props | 1 + LANCommander.Documentation/Overview.md | 1 + .../Packager/Getting Started.md | 34 ++ .../Packager/LCX Format.md | 47 ++ .../Packager/Overview.md | 14 + LANCommander.Documentation/Packager/Wizard.md | 114 +++++ LANCommander.Packager/App.axaml | 18 + LANCommander.Packager/App.axaml.cs | 23 + .../LANCommander.Packager.csproj | 46 ++ LANCommander.Packager/LANCommanderDark.ico | Bin 0 -> 9096 bytes LANCommander.Packager/MainWindow.axaml | 86 ++++ LANCommander.Packager/MainWindow.axaml.cs | 205 +++++++++ .../Models/CheckableTreeNode.cs | 226 ++++++++++ .../Models/FileChangeEntry.cs | 7 + .../Models/GeneratedScript.cs | 11 + .../Models/PackageContext.cs | 21 + .../Models/RegistryChangeEntry.cs | 8 + .../Models/WizardStepItem.cs | 46 ++ LANCommander.Packager/Options.cs | 12 + LANCommander.Packager/Program.cs | 32 ++ .../Services/InstallerMonitorService.cs | 400 ++++++++++++++++++ .../Services/LcxBuilderService.cs | 118 ++++++ .../Services/ScriptGeneratorService.cs | 127 ++++++ .../Theme/ColorPalette.axaml | 144 +++++++ LANCommander.Packager/Theme/Packager.axaml | 85 ++++ LANCommander.Packager/Views/ActionView.axaml | 26 ++ .../Views/ActionView.axaml.cs | 85 ++++ .../Views/FileSelectionView.axaml | 41 ++ .../Views/FileSelectionView.axaml.cs | 74 ++++ .../Views/InstallDirectoryView.axaml | 12 + .../Views/InstallDirectoryView.axaml.cs | 50 +++ .../Views/MetadataView.axaml | 41 ++ .../Views/MetadataView.axaml.cs | 36 ++ .../Views/MonitoringView.axaml | 31 ++ .../Views/MonitoringView.axaml.cs | 157 +++++++ LANCommander.Packager/Views/OutputView.axaml | 60 +++ .../Views/OutputView.axaml.cs | 190 +++++++++ .../Views/RegistrySelectionView.axaml | 51 +++ .../Views/RegistrySelectionView.axaml.cs | 46 ++ LANCommander.Packager/app.manifest | 10 + .../PowerShell/Cmdlets/Edit-PatchGameSpy.cs | 6 +- LANCommander.slnx | 1 + 44 files changed, 2858 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/LANCommander.Packager.yml create mode 100644 LANCommander.Documentation/Packager/Getting Started.md create mode 100644 LANCommander.Documentation/Packager/LCX Format.md create mode 100644 LANCommander.Documentation/Packager/Overview.md create mode 100644 LANCommander.Documentation/Packager/Wizard.md create mode 100644 LANCommander.Packager/App.axaml create mode 100644 LANCommander.Packager/App.axaml.cs create mode 100644 LANCommander.Packager/LANCommander.Packager.csproj create mode 100644 LANCommander.Packager/LANCommanderDark.ico create mode 100644 LANCommander.Packager/MainWindow.axaml create mode 100644 LANCommander.Packager/MainWindow.axaml.cs create mode 100644 LANCommander.Packager/Models/CheckableTreeNode.cs create mode 100644 LANCommander.Packager/Models/FileChangeEntry.cs create mode 100644 LANCommander.Packager/Models/GeneratedScript.cs create mode 100644 LANCommander.Packager/Models/PackageContext.cs create mode 100644 LANCommander.Packager/Models/RegistryChangeEntry.cs create mode 100644 LANCommander.Packager/Models/WizardStepItem.cs create mode 100644 LANCommander.Packager/Options.cs create mode 100644 LANCommander.Packager/Program.cs create mode 100644 LANCommander.Packager/Services/InstallerMonitorService.cs create mode 100644 LANCommander.Packager/Services/LcxBuilderService.cs create mode 100644 LANCommander.Packager/Services/ScriptGeneratorService.cs create mode 100644 LANCommander.Packager/Theme/ColorPalette.axaml create mode 100644 LANCommander.Packager/Theme/Packager.axaml create mode 100644 LANCommander.Packager/Views/ActionView.axaml create mode 100644 LANCommander.Packager/Views/ActionView.axaml.cs create mode 100644 LANCommander.Packager/Views/FileSelectionView.axaml create mode 100644 LANCommander.Packager/Views/FileSelectionView.axaml.cs create mode 100644 LANCommander.Packager/Views/InstallDirectoryView.axaml create mode 100644 LANCommander.Packager/Views/InstallDirectoryView.axaml.cs create mode 100644 LANCommander.Packager/Views/MetadataView.axaml create mode 100644 LANCommander.Packager/Views/MetadataView.axaml.cs create mode 100644 LANCommander.Packager/Views/MonitoringView.axaml create mode 100644 LANCommander.Packager/Views/MonitoringView.axaml.cs create mode 100644 LANCommander.Packager/Views/OutputView.axaml create mode 100644 LANCommander.Packager/Views/OutputView.axaml.cs create mode 100644 LANCommander.Packager/Views/RegistrySelectionView.axaml create mode 100644 LANCommander.Packager/Views/RegistrySelectionView.axaml.cs create mode 100644 LANCommander.Packager/app.manifest diff --git a/.github/workflows/LANCommander.Packager.yml b/.github/workflows/LANCommander.Packager.yml new file mode 100644 index 00000000..570eaab3 --- /dev/null +++ b/.github/workflows/LANCommander.Packager.yml @@ -0,0 +1,100 @@ +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 diff --git a/.github/workflows/LANCommander.Release.yml b/.github/workflows/LANCommander.Release.yml index d56084ee..a48c705b 100644 --- a/.github/workflows/LANCommander.Release.yml +++ b/.github/workflows/LANCommander.Release.yml @@ -165,6 +165,16 @@ jobs: build_platform: Windows build_configuration: Release + # Packager (Windows x86 only) + build_packager: + needs: [prep] + uses: ./.github/workflows/LANCommander.Packager.yml + with: + build_dotnet_version: '10.0.x' + version_semver: ${{ needs.prep.outputs.version_semver }} + version_tag: ${{ needs.prep.outputs.version_tag }} + build_configuration: Release + build_release: runs-on: ubuntu-latest needs: @@ -179,6 +189,7 @@ jobs: - build_launcher_avalonia_osx_arm64 - build_launcher_avalonia_osx_x64 - build_launcher_avalonia_win_x64 + - build_packager steps: - name: Create Temp Directory @@ -245,6 +256,12 @@ jobs: name: LANCommander.Launcher-Windows-x64-v${{ needs.prep.outputs.version_tag }}.zip path: artifacts + - name: Download Packager Windows x86 + uses: actions/download-artifact@v4 + with: + name: LANCommander.Packager-Windows-x86-v${{ needs.prep.outputs.version_tag }}.zip + path: artifacts + - name: Debug - List Artifact Files run: | echo "Contents of ./artifacts:" @@ -267,6 +284,7 @@ jobs: 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-x64-v${{ needs.prep.outputs.version_tag }}.zip + artifacts/LANCommander.Packager-Windows-x86-v${{ needs.prep.outputs.version_tag }}.zip - name: Checkout Repo for Docker build uses: actions/checkout@v4 diff --git a/Directory.Packages.props b/Directory.Packages.props index ba77b808..27070e87 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -92,6 +92,7 @@ + diff --git a/LANCommander.Documentation/Overview.md b/LANCommander.Documentation/Overview.md index f446de80..9968fb3c 100644 --- a/LANCommander.Documentation/Overview.md +++ b/LANCommander.Documentation/Overview.md @@ -25,5 +25,6 @@ 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) \ No newline at end of file diff --git a/LANCommander.Documentation/Packager/Getting Started.md b/LANCommander.Documentation/Packager/Getting Started.md new file mode 100644 index 00000000..a66ade30 --- /dev/null +++ b/LANCommander.Documentation/Packager/Getting Started.md @@ -0,0 +1,34 @@ +--- +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. diff --git a/LANCommander.Documentation/Packager/LCX Format.md b/LANCommander.Documentation/Packager/LCX Format.md new file mode 100644 index 00000000..abac23cc --- /dev/null +++ b/LANCommander.Documentation/Packager/LCX Format.md @@ -0,0 +1,47 @@ +--- +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. diff --git a/LANCommander.Documentation/Packager/Overview.md b/LANCommander.Documentation/Packager/Overview.md new file mode 100644 index 00000000..d6e3bad8 --- /dev/null +++ b/LANCommander.Documentation/Packager/Overview.md @@ -0,0 +1,14 @@ +--- +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. + +import DocCardList from '@theme/DocCardList'; + + diff --git a/LANCommander.Documentation/Packager/Wizard.md b/LANCommander.Documentation/Packager/Wizard.md new file mode 100644 index 00000000..bc5bcea8 --- /dev/null +++ b/LANCommander.Documentation/Packager/Wizard.md @@ -0,0 +1,114 @@ +--- +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. diff --git a/LANCommander.Packager/App.axaml b/LANCommander.Packager/App.axaml new file mode 100644 index 00000000..386716a0 --- /dev/null +++ b/LANCommander.Packager/App.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/LANCommander.Packager/App.axaml.cs b/LANCommander.Packager/App.axaml.cs new file mode 100644 index 00000000..264bb3f8 --- /dev/null +++ b/LANCommander.Packager/App.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace LANCommander.Packager; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(Program.Context); + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/LANCommander.Packager/LANCommander.Packager.csproj b/LANCommander.Packager/LANCommander.Packager.csproj new file mode 100644 index 00000000..dbc9e51f --- /dev/null +++ b/LANCommander.Packager/LANCommander.Packager.csproj @@ -0,0 +1,46 @@ + + + + WinExe + net10.0 + enable + enable + x86 + app.manifest + LANCommanderDark.ico + LANCommander + Packager + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LANCommander.Packager/LANCommanderDark.ico b/LANCommander.Packager/LANCommanderDark.ico new file mode 100644 index 0000000000000000000000000000000000000000..4bec2dd126b64e1983125a64369391170e3e6050 GIT binary patch literal 9096 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 literal 0 HcmV?d00001 diff --git a/LANCommander.Packager/MainWindow.axaml b/LANCommander.Packager/MainWindow.axaml new file mode 100644 index 00000000..f71d0be6 --- /dev/null +++ b/LANCommander.Packager/MainWindow.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +