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 00000000..4bec2dd1 Binary files /dev/null and b/LANCommander.Packager/LANCommanderDark.ico differ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +