Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: ModVerify Update Integration Test

on:
workflow_call:
workflow_dispatch:

jobs:
update-cycle:
name: Update cycle (${{ matrix.scenario.name }})
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
scenario:
- { name: single, script: ./test-local-update.ps1 }
- { name: dual, script: ./test-local-update-dual.ps1 }
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Run ${{ matrix.scenario.name }}-channel update cycle
shell: pwsh
run: ${{ matrix.scenario.script }}

- name: Upload diagnostics on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: update-cycle-${{ matrix.scenario.name }}-logs
# Updater log: %TEMP%\extUpdateLog-*.txt; ModVerify app log: %APPDATA%\ModVerify\*.txt
path: |
${{ runner.temp }}\extUpdateLog-*.txt
${{ env.APPDATA }}\ModVerify\*.txt
if-no-files-found: ignore
retention-days: 7
82 changes: 49 additions & 33 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,38 @@ on:

env:
TOOL_PROJ_PATH: ./src/ModVerify.CliApp/ModVerify.CliApp.csproj
CREATOR_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/ApplicationManifestCreator/ApplicationManifestCreator.csproj
UPLOADER_PROJ_PATH: ./modules/ModdingToolBase/src/AnakinApps/FtpUploader/FtpUploader.csproj
PUBLISH_SCRIPT: ./modules/ModdingToolBase/scripts/Publish-Release.ps1
TOOL_EXE: ModVerify.exe
UPDATER_EXE: AnakinRaW.ExternalUpdater.exe
MANIFEST_CREATOR: AnakinRaW.ApplicationManifestCreator.dll
SFTP_UPLOADER: AnakinRaW.FtpUploader.dll
EMBEDDED_TRUST_CERT: src/ModVerify.CliApp/Resources/Certs/AET-root.cer
ORIGIN_BASE: https://republicatwar.com/downloads/ModVerify
ORIGIN_BASE_PART: downloads/ModVerify/
SFTP_BASE_PATH: downloads/ModVerify/
BRANCH_NAME: ${{ github.event.inputs.branch || 'stable' }}

# Migration-release values. Leave empty for a normal release; populate to enable.
#
# Origin URL of the next-generation channel, written into the manifest's componentOriginInfo.
NEXT_ORIGIN_BASE: https://republicatwar.com/downloads/ModVerify/v2

# SFTP path the next-generation channel uploads to. Set together with NEXT_ORIGIN_BASE.
NEXT_SFTP_BASE_PATH: downloads/ModVerify/v2/

# Previously-deployed updater used in place of the build-output one for the primary deploy.
# Only the old-gen manifest lists this binary; the next-gen manifest still uses the build-output updater.
# Requires NEXT_ORIGIN_BASE + NEXT_SFTP_BASE_PATH to be set.
COMPAT_UPDATER: tools/v1/AnakinRaW.ExternalUpdater.exe

jobs:

# Builds and tests the solution.
test:
uses: ./.github/workflows/test.yml

# End-to-end self-update test (single-channel + dual-channel via /v2/).
integration-test:
needs: [test]
uses: ./.github/workflows/integration-test.yml

pack:
name: Pack
needs: [test]
Expand All @@ -47,8 +63,6 @@ jobs:
- name: Create Net Core Release
# use publish for .NET Core
run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net10.0 --output ./releases/net10.0 /p:DebugType=None /p:DebugSymbols=false
- name: Create Linux Release
run: dotnet publish ${{ env.TOOL_PROJ_PATH }} --configuration Release -f net10.0 --runtime linux-x64 --self-contained true --output ./releases/linux-x64 /p:DebugType=None /p:DebugSymbols=false
- name: Upload a Build Artifact
uses: actions/upload-artifact@v7
with:
Expand All @@ -59,10 +73,13 @@ jobs:

deploy:
name: Deploy
# Deploy on push to main or manual trigger
# Stable deploys are gated to 'main'. Non-stable channels (beta, canary, etc.) can be
# workflow_dispatched from any branch.
if: |
(github.ref == 'refs/heads/main' && github.event_name == 'push') || github.event_name == 'workflow_dispatch'
needs: [pack]
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.branch != 'stable' || github.ref == 'refs/heads/main'))
needs: [pack, integration-test]
runs-on: ubuntu-latest
steps:
- name: Checkout sources
Expand All @@ -75,37 +92,37 @@ jobs:
name: Binary Releases
path: ./releases

# Deploy .NET Framework self-update release
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Build Creator
run: dotnet build ${{env.CREATOR_PROJ_PATH}} --configuration Release --output ./dev
- name: Build Uploader
run: dotnet build ${{env.UPLOADER_PROJ_PATH}} --configuration Release --output ./dev
- name: Create binaries directory
run: mkdir -p ./deploy
- name: Copy self-update files

- name: Publish self-update release
shell: pwsh
run: |
cp ./releases/net481/${{env.TOOL_EXE}} ./deploy/
cp ./releases/net481/${{env.UPDATER_EXE}} ./deploy/
- name: Create Manifest
run: dotnet ./dev/${{env.MANIFEST_CREATOR}} -a deploy/${{env.TOOL_EXE}} --appDataFiles deploy/${{env.UPDATER_EXE}} --origin ${{env.ORIGIN_BASE}} -o ./deploy -b ${{env.BRANCH_NAME}}
- name: Upload Build
run: dotnet ./dev/${{env.SFTP_UPLOADER}} ftp --host $host --port $port -u ${{secrets.SFTP_USER}} -p ${{secrets.SFTP_PASSWORD}} --base $base_path -s $source
env:
host: republicatwar.com
port: 1579
base_path: ${{env.ORIGIN_BASE_PART}}
source: ./deploy
& $env:PUBLISH_SCRIPT `
-AppExePath "./releases/net481/$env:TOOL_EXE" `
-UpdaterExePath "./releases/net481/$env:UPDATER_EXE" `
-EmbeddedTrustCertPath "$env:EMBEDDED_TRUST_CERT" `
-Origin "$env:ORIGIN_BASE" `
-SftpBasePath "$env:SFTP_BASE_PATH" `
-Branch "$env:BRANCH_NAME" `
-SigningPfxBase64 "${{ secrets.UPDATER_SIGNING_PFX_B64 }}" `
-SigningPfxPassword "${{ secrets.UPDATER_SIGNING_PFX_PASSWORD }}" `
-SftpHost "republicatwar.com" `
-SftpPort 1579 `
-SftpUser "${{ secrets.SFTP_USER }}" `
-SftpPassword "${{ secrets.SFTP_PASSWORD }}" `
-NextOrigin "$env:NEXT_ORIGIN_BASE" `
-NextSftpBasePath "$env:NEXT_SFTP_BASE_PATH" `
-CompatibilityUpdaterExePath "$env:COMPAT_UPDATER"

# Deploy .NET Core and .NET Framework apps to Github
- name: Create NET Core .zip
# Change into the artifacts directory to avoid including the directory itself in the zip archive
working-directory: ./releases/net10.0
run: zip -r ../ModVerify-Net10.zip .
- uses: dotnet/nbgv@v0.5.1
- uses: dotnet/nbgv@v0.5.2
id: nbgv
- name: Create GitHub release
# Create a GitHub release on push to main only
Expand All @@ -117,7 +134,6 @@ jobs:
tag_name: v${{ steps.nbgv.outputs.SemVer2 }}
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
files: |
files: |
./releases/net481/ModVerify.exe
./releases/ModVerify-Net10.zip
./releases/linux-x64/ModVerify
./releases/ModVerify-Net10.zip
19 changes: 17 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,20 @@ jobs:
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Build & Test in Release Mode
run: dotnet test --configuration Release --report-github
- name: Build & Test in Release Mode (Windows)
if: runner.os == 'Windows'
run: dotnet test --configuration Release --report-github

- name: Build & Test in Release Mode (Linux)
if: runner.os == 'Linux'
shell: bash
# ExternalUpdater.App.Test is net481-only — it spawns the real ExternalUpdater.exe
# and uses Windows-only helpers (cmd .bat stubs, ping.exe). It cannot run on Linux
# (no mono in the runner image, and the helpers don't exist anyway). Build the
# whole solution, then test every project except that one.
run: |
set -euo pipefail
dotnet build --configuration Release
for proj in $(find . -name '*.Test.csproj' -not -path '*/ExternalUpdater.App.Test/*'); do
dotnet test "$proj" --no-build --configuration Release --report-github
done
105 changes: 105 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

ModVerify is a .NET CLI tool that statically analyzes mods for *Star Wars: Empire at War* and *Forces of Corruption* by reimplementing parts of the Petroglyph Alamo engine in managed code and running verifiers over the loaded game data. It produces JSON + text reports of XML errors, missing assets, broken model references, malformed SFX samples, etc.

Vanilla EaW is currently not supported; FoC is the primary target.

## Build / test / run

The repo uses the Microsoft Testing Platform runner (configured via `global.json`), not VSTest. `dotnet test` works, but the test project is also an `Exe` and can be invoked directly.

```bash
# Restore submodules (required — ModdingToolBase is a git submodule)
git submodule update --init --recursive

# Build everything
dotnet build ModVerify.slnx

# Run all tests (Release matches CI)
dotnet test --configuration Release

# Run the CLI from source
dotnet run --project src/ModVerify.CliApp -- verify --path "<mod path>" --useDefaultBaseline

# Build the Windows self-updating release (.NET Framework 4.8.1, Costura-packed single exe)
dotnet build src/ModVerify.CliApp/ModVerify.CliApp.csproj -c Release -f net481

# Build the cross-platform release (.NET 10)
dotnet publish src/ModVerify.CliApp/ModVerify.CliApp.csproj -c Release -f net10.0
```

The CLI has two verbs: `verify` and `createBaseline`. Running with no arguments enters interactive mode and prompts for a target. See `README.md` for full option examples.

`deploy-local.ps1` builds the net481 exe, generates an update manifest, and stages it under `.local_deploy/` so the self-update flow can be exercised end-to-end without hitting the real CDN.

## Target frameworks and why

- `src/ModVerify` (the library): `netstandard2.0;netstandard2.1;net10.0` — keeps the core consumable from older tooling.
- `src/ModVerify.CliApp`: `net10.0;net481`. The **net481** build is the user-facing Windows executable (Costura-Fody packs all dependencies into a single `ModVerify.exe`, and the AnakinRaW self-updater is wired in only on this TFM). The **net10.0** build is the cross-platform / CI variant, has no self-updater, and is shipped as a framework-dependent zip (Linux/macOS users run it via `dotnet`).
- Test project: net10.0 always; net481 only on Windows.

When editing the CLI app, be aware that `IsUpdatable()` is conditional on the net481 build — code paths guarded by `IsUpdateableApplication` or `_applicationEnvironment.IsUpdatable()` only run there. PolySharp + `Microsoft.Bcl.Memory` + `IndexRange` are pulled in to make modern C# language features compile against net481.

## Architecture

### Two-layer split: `ModVerify` (library) vs `ModVerify.CliApp` (executable)

The library knows nothing about command-line parsing, console output, file I/O for results, or the updater. The CLI app composes the library with logging (Serilog), DI (`Microsoft.Extensions.DependencyInjection`), the AnakinRaW `ApplicationBase` lifecycle, and reporters. When adding functionality, decide which layer it belongs in: anything reusable by another host (CI integration, GUI, etc.) goes in `ModVerify`; anything tied to console UX, command-line verbs, or the Windows updater goes in `ModVerify.CliApp`.

### Verification pipeline

`GameVerifierService.VerifyAsync` is the entry point from `IGameVerifierService`. It builds a `GameVerifyPipeline` (a `StepRunnerPipelineBase<AsyncStepRunner>` from `AnakinRaW.CommonUtilities.SimplePipeline`) which:

1. Initializes a re-implementation of the game engine (`IPetroglyphStarWarsGameEngine` from the `PG.StarWarsGame.Engine` project under `src/PetroglyphTools/`) against the target's location/engine type. Engine init errors are collected by `GameEngineErrorCollector`, which becomes the first pipeline step.
2. Asks `IGameVerifiersProvider` (default: `DefaultGameVerifiersProvider`) for the set of `GameVerifier` instances to run.
3. Wraps each verifier in a `GameVerifierPipelineStep` and runs them via `AsyncStepRunner` (parallelism controlled by `VerifierServiceSettings.ParallelVerifiers`; 1 falls back to a `SequentialStepRunner`).
4. Aggregates all `VerificationError`s, then applies `VerificationBaseline` and `SuppressionList` filters before returning a `VerificationResult`.

`FailFast` mode is non-trivial: the pipeline checks each thrown `GameVerificationException` against the baseline + suppressions and *swallows* it if every error is already accounted for, so fail-fast does not abort on known issues. Don't simplify this away.

When adding a new verifier: subclass `GameVerifier` (or `GameVerifier<T>` / `NamedGameEntityVerifier`), register it in `DefaultGameVerifiersProvider`, and pick a unique error code prefix (see `Verifiers/VerifierErrorCodes.cs`). `IAlreadyVerifiedCache` (registered via `RegisterVerifierCache()`) is shared across verifiers — use it to skip work that's already been done in the same run (e.g. the same texture being referenced by multiple GameObjects).

### Baselines

A baseline is a frozen JSON snapshot of "errors we already knew about, ignore them." `VerificationBaseline.LatestVersion` is **2.2** and the schema lives at `src/ModVerify/Resources/Schemas/2.2/baseline.json` (embedded). When bumping the baseline format, add a new versioned schema folder rather than mutating the existing one — old baselines on user disks must keep parsing.

`src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json` is the embedded default baseline shipped with the CLI (selected via `--useDefaultBaseline`). The legacy top-level `focBaseline.json` is *not* the embedded one and exists separately; don't conflate them.

### Settings flow (CLI)

```
argv → ModVerifyOptionsParser → ModVerifyOptionsContainer
→ SettingsBuilder.BuildSettings → AppVerifySettings | AppBaselineSettings
→ ModVerifyApplication → VerifyAction | CreateBaselineAction
```

`SelfUpdateableAppLifecycle` (from ModdingToolBase) drives `InitializeAppAsync` → `CreateAppServices` → `RunAppAsync`. The app argument parser strips trailing arguments injected by the external updater (see `ModVerifyOptionsParser.StripExternalUpdateResults`) before handing off to CommandLineParser — needed so unknown-argument errors stay strict.

`VerifyVerbOption.WithoutArguments` is the sentinel returned when the user just double-clicks the exe; this triggers interactive target selection in `ConsoleSelector` and enables the interactive update prompt.

### Logging

Two Serilog sinks, configured in `Program.ConfigureLogging`:

- **Console sink** filters by an `EventId.Id == ModVerifyConstants.ConsoleEventIdValue` expression in normal mode (so verifier output stays clean), shows everything from the `AET.ModVerify` namespace at Debug level, and shows everything at Verbose. Fatals are excluded — they're handled by the global exception handler instead.
- **File sink** writes to `ApplicationLocalPath/ModVerify_log.txt` and excludes XML-parser noise (the engine and `XmlFileParser<>` namespaces are reported by the verification pipeline).

When emitting log lines that the user *should* see in normal operation, attach `ModVerifyConstants.ConsoleEventId`. Plain `Logger.LogInformation(...)` without that EventId will be silently dropped at the console.

### Petroglyph engine reimplementation (`src/PetroglyphTools/`)

These projects (`PG.StarWarsGame.Engine`, `PG.StarWarsGame.Engine.FileSystem`, `PG.StarWarsGame.Files.ALO`, `PG.StarWarsGame.Files.ChunkFiles`, `PG.StarWarsGame.Files.XML`, etc.) are vendored copies that will eventually move to the standalone `AlamoEngine-Tools/PetroglyphTools` repo (per `src/PetroglyphTools/README.md`). Treat them as a separate library boundary — changes here may need to land upstream.

### Submodule

`modules/ModdingToolBase` is a git submodule (https://github.com/AnakinRaW/ModdingToolBase). It supplies the shared `ApplicationBase`, the AppUpdaterFramework, the manifest creator, and the FTP uploader used by the deploy pipeline. If you're touching the app lifecycle, updater, or release packaging, the relevant code likely lives there, not in this repo.

## CI

`.github/workflows/test.yml` builds + tests on Windows and Linux against .NET 10. `release.yml` (triggered on push to `main`) builds the two release artifacts (net481 self-updating exe, net10.0 portable zip), generates an update manifest via `ApplicationManifestCreator`, SFTPs the self-update bits to republicatwar.com, and creates a GitHub release tagged with the Nerdbank.GitVersioning SemVer.

Versioning is fully driven by `version.json` + `Nerdbank.GitVersioning` — do not hand-edit assembly versions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

<ItemGroup>
<PackageReference Update="SauceControl.InheritDoc" Version="2.0.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.203" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.300" PrivateAssets="All"/>
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<PrivateAssets>all</PrivateAssets>
<Version>3.9.50</Version>
Expand Down
Loading
Loading