Skip to content

Normalize: XCOM 2#23076

Open
halgari wants to merge 20 commits into
masterfrom
normalize/game-xcom2
Open

Normalize: XCOM 2#23076
halgari wants to merge 20 commits into
masterfrom
normalize/game-xcom2

Conversation

@halgari
Copy link
Copy Markdown
Contributor

@halgari halgari commented May 8, 2026

Summary

Normalize game-xcom2 extension per project checklist.
1:1 behavioral replacement, no feature changes (plus one bug fix noted below).

Changes

  • Converted from JavaScript to TypeScript with strict null checks
  • Replaced queryPath (GameStoreHelper.findByAppId) with queryArgs shorthand for Steam, GOG, and Epic
  • Removed Bluebird dependency
  • Converted gameart-xcom2.jpg and gameart-wotc.jpg to WebP
  • Added rolldown build.mjs and tsconfig.json
  • Used framework shorthands: dropped mergeMods (defaults to true), environment.SteamAPPId, details.steamAppId (all auto-derived)
  • Replaced deprecated electron.remote with @electron/remote
  • Replaced fs.ensureDirAsync with fs.ensureDirWritableAsync in setup
  • Fixed bug where WOTC passed WOTC_MODS (a path string) to supportedTools() instead of WOTC_ID

Verification

  • Independent review subagent verified no regressions
  • All discovery paths preserved (Steam 268500, GOG 1482002159, Epic 3be3c4d681bc46b3b8b26c5df3ae0a18)
  • Both game registrations (XCOM 2, War of the Chosen) unchanged
  • Load order and installer behavior unchanged

@github-actions
Copy link
Copy Markdown

This PR has conflicts. You need to rebase the PR before it can be merged.

@halgari halgari force-pushed the normalize/game-xrebirth branch 2 times, most recently from f27503c to 177594c Compare May 12, 2026 21:48
@halgari halgari force-pushed the normalize/game-xcom2 branch from da7838a to 598d4c3 Compare May 12, 2026 23:49
@halgari halgari force-pushed the normalize/game-xrebirth branch from 177594c to 24672c9 Compare May 13, 2026 02:25
@halgari halgari force-pushed the normalize/game-xcom2 branch from f9f6528 to 540a62b Compare May 13, 2026 02:44
@github-actions
Copy link
Copy Markdown

This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed.

@github-actions
Copy link
Copy Markdown

This PR has conflicts. You need to rebase the PR before it can be merged.

@halgari halgari force-pushed the normalize/game-xrebirth branch 2 times, most recently from b0e6deb to 5ad04b1 Compare May 18, 2026 21:11
Base automatically changed from normalize/game-xrebirth to master May 19, 2026 14:10
@halgari halgari force-pushed the normalize/game-xcom2 branch from 540a62b to b2d6fbf Compare May 19, 2026 18:00
@github-actions
Copy link
Copy Markdown

This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

❌ Code is not formatted

To fix this:

  1. Set up the pre-commit hook (prevents future issues):

    pnpm run prepare

    This auto-formats staged files on every commit. (You will never see this error again)

  2. Format all files manually:

    pnpm run format

Then commit the formatting changes. View logs

@halgari halgari marked this pull request as ready for review May 20, 2026 03:08
@halgari halgari requested a review from a team as a code owner May 20, 2026 03:08
@github-actions
Copy link
Copy Markdown

This PR has conflicts. You need to rebase the PR before it can be merged.

halgari and others added 6 commits May 20, 2026 05:33
… harness

LAZ-334

X Rebirth normalization
  Convert the extension from JavaScript to TypeScript and replace the
  per-store discovery shim with the new IGame.queryArgs API
  (`{ steam: "2870" }`). Migrate to declarative installer specs driven
  by `util.declareInstallers` — one row per installer (content.xml,
  savegame, shader-injector, utility, drop-in, save-patch, documentation),
  ordered by explicit PRIORITIES. The content.xml installer stays
  hand-written because it parses XML and emits attribute instructions.
  Stop patterns live in stopPatterns.ts and double as
  IGame.details.stopPatterns. Asset bitmaps converted to WebP.

  Per-mod diagnostics in src/diagnostic.ts (modHasFilesCheck,
  contentXmlCustomFileNameCheck, modShapeRecognisedCheck) verify install
  output rather than just the archive shape, and are registered via the
  new context.registerHealthCheck API.

  installContentXml accepts both `/` and `\\` as wrapping-dir separators
  so Windows-extracted archives with forward-slash paths still match.

  Unit tests for every spec match kind, every healthcheck, and the
  hand-written content.xml installer.

Per-mod health-check API
  - IModHealthCheck variant adds a `checkMod(api, modCtx)` signature with
    an IModCheckContext (modId, files, readFile, attributes).
  - HealthCheckRegistry routes IModHealthCheck through a new
    perModRunner that enumerates installed mods for the active game,
    builds an IModCheckContext for each, calls checkMod, and aggregates
    per-mod outcomes into a single IHealthCheckResult.
  - context.registerHealthCheck is wired synchronously in the
    health_check extension's init(); registrations before AND after
    once() reach the same registry. Test coverage for both paths.
  - perModRunner tolerates a mod's staging dir disappearing between
    enumeration and the FS walk by reporting empty files.

packages/game-extension-test
  New Vitest-based harness package that:
   - discovers each game extension opting in via
     package.json#vortex.gameExtensionTest,
   - loads each extension's installer + diagnostic exports through a
     stubbed IExtensionContext,
   - resolves real mod-file fixtures from the Nexus API (mostPopular,
     mostRecent, oldest, collections, "all"), throttled to the public
     ~25 req/s,
   - drives each archive through the extension's installer chain,
     materialises the IInstruction[] into an IModCheckContext, then
     runs every IModHealthCheck against the result.
  Fixture resolution is parallelised via vitest's worker pool with a
  fan-out of one `test.concurrent` per Nexus file.

  A nightly GitHub Actions workflow (.github/workflows/
  game-extension-test.yml) runs the harness against every opted-in
  extension and opens / updates a rolling tracking issue on failure.

  Shared `vortex-api/testing` module hosts hand-mirrored mocks for the
  enums, fs.readFileAsync, and util.declareInstallers so every game
  extension's vitest config can alias `vortex-api` to it without
  shipping its own __mocks__.

Installer spec helpers
  IInstallerSpec types and the implementation of declareInstallers,
  makeInstallerFromSpec, buildCopyInstructions, compileStopPatterns,
  findCommonRootDir, and matchesAnyStopPattern live under
  src/renderer/src/extensions/mod_management/{types,util}, re-exported
  through src/renderer/src/util/api.ts as part of the public API.

IGame API ergonomics
  - queryArgs accepts a bare string, a single IStoreQuery, or an
    IStoreQuery[]. A new normalizeStoreQuery() helper consolidates the
    three-way branch that GameStoreHelper.find() and
    GameModeManager.extractSteamId() previously open-coded.
  - mergeMods is optional; deploy.ts and LinkingDeployment.ts each
    default it to `true` at point of use.
  - Steam app id, environment.SteamAPPId, and details.steamAppId are
    auto-derived from queryArgs.steam when not explicitly set.

Build
  - Workspace root declares `tsx` as a devDep so the assets script's
    `npx tsx` finds a properly hoisted binary on CI.
  - .github/workflows/game-extension-test.yml has an explicit
    `permissions:` block.

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Apply normalization checklist: convert to TypeScript, replace queryPath
with queryArgs, remove Bluebird, convert images to WebP, use framework
shorthands for mergeMods/steamAppId/SteamAPPId. Fixed original bug
where WOTC passed a mod path instead of game ID to supportedTools().

LAZ-333
Unify the two game registrations (XCOM 2 / War of the Chosen) into a
single loop over a GameDef array. Replace per-game switch functions
with derived helpers. Use flatMap in the installer. Extract shared
discovery logic.
28 tests covering game registration, installer behavior, and load
order serialization. 77% statement coverage, 82% function coverage.
Tests encode the behavioral contract so future changes can be
verified against the same expectations.
Add vortex.gameExtensionTest opt-in, @vortex/game-extension-test devDep,
nx test:game-extensions target, src/test-descriptor.ts, and three health
checks in src/diagnostic.ts (mod-has-files, has-xcommod-file,
xCommods-attribute-set) mirroring the X Rebirth pattern.

One descriptor covers xcom2 and xcom2-wotc since WOTC uses
nexusPageId: "xcom2" and shares the Nexus domain.
Extends the declarative installer table with two install-time primitives:

- filter: per-file selector (extensions / regex / filename / custom).
  Files that don't match are dropped from the install output. Targets
  cases where archives bundle readme/screenshots/source-only files
  alongside the deployable content.
- flatten: destinations reduced to basename(source). For importer-style
  targets that read a directory non-recursively (character pools,
  savegames). Takes precedence over stripCommonRoot.

Mirrors the change into vortex-api/testing's hand-mirror of
declareInstallers and extends the drift-guard tests to keep the two
implementations in lock-step. Regenerated etc/vortex.api.md.

Also fixes four broken relative imports in installerHelpers.{ts,test.ts}
left over from the c90a48c refactor (file moved without updating its
own imports) so the existing renderer test suite can run.
halgari added 14 commits May 20, 2026 05:34
Adds a declarative installer that routes character pool archives
(.bin files, no .XComMod descriptor) to the in-game importable
directory:
  xcom2:      <gamePath>/XComGame/CharacterPool/Importable/
  xcom2-wotc: <gamePath>/XCom2-WarOfTheChosen/XComGame/CharacterPool/Importable/

Uses the new install-spec primitives:
- filter: { extensions: [".bin"] } drops bundled readmes/screenshots
- flatten: true reduces destinations to basename so the in-game
  importer (which reads Importable/ non-recursively) finds them

Registers an xcom2-character-pool modType per game id, adds a
companion health check, and updates the local vortex-api mock to
include the HealthCheck enums + a hand-mirror of declareInstallers
so the extension's unit tests stay self-contained.

Converts ~192 fixture rejections (the entire character-pool category)
from failing to passing in the @vortex/game-extension-test harness.
Adds nine skip predicates to the test descriptor so the harness reports
these archives as skipped rather than failing them through the
installer chain:

- ModBuddy source projects (.uc/.x2proj) — need compilation first
- nested archives (.7z/.rar/.zip inside) — user must extract
- instructions-only uploads (all-doc archives)
- ReShade / shader injectors (.fx/.fxh/.hlsl)
- standalone Windows tools (all-.exe/.dll/.config/etc.)
- Access database tools (.accdb)
- controller profiles (.xpadderprofile)
- movie/intro replacements (all .bk2)
- cheat-engine tables (single .ct)

Predicates are tight enough not to false-positive on the legitimate
mod shapes the existing installers handle (character pools, the
incoming config/loc drop-in installer).
Adds a hand-written installer at priority 40 (after .XComMod and
character-pool) that handles archives containing .ini / .int /
lang-tagged-loc files but no .XComMod descriptor.

Routes each file to its game-tree destination:
- paths explicitly inside XCom2-WarOfTheChosen/XComGame/ → keep suffix
- paths explicitly inside XComGame/ → keep suffix (prepended with
  XCom2-WarOfTheChosen/ when installing for WOTC)
- bare .ini at archive root → XComGame/Config/<basename>
- bare .int (and other loc) at archive root → XComGame/Localization/<basename>

Drops readmes / screenshots / wrapper directories from the install
output. Registers a single xcom2-config-drop-in modType whose install
path is the game's discovered root, so destinations can carry the
WOTC prefix when needed.

Hand-written rather than declarative because the destination depends
on both the source-path content (find the XComGame/ marker, preserve
the suffix) and the active gameId (WOTC prepend), neither of which
fits the filter / flatten primitives in IInstallerInstall.

Converts ~150 fixture rejections (config/loc drop-in category) into
passing tests in the @vortex/game-extension-test harness.
Adds two more skip predicates to the test descriptor:

- raw cooked content: archives containing .upk or .u files but no
  .XComMod descriptor. These replace stock packages (Long War 2's
  XComGame.u, LWOTC_Overhaul_epic.zip's bulk .upk drop, single
  ResistanceMusic*.upk uploads) and aren't safe to auto-install.
- voice packs: all-.wav/.ogg/.mp3/.wem archives. They need the
  third-party Voice Pack Toolkit mod to consume them at runtime;
  Vortex has no safe destination without knowing the toolkit's
  expected layout.

Reduces remaining harness failures from 41 to 14.
Adds an xcom2-save modType + declarative installer at priority 50
that handles archives whose data is save-named files
(save_<name> / save<digits>, no extension):

- match: archive contains 1+ save-named file and no .XComMod
- install: regex filter to save-named files, flatten to basenames

The modType deploys to the per-game user-docs SaveData folder
(<documents>/My Games/<gameDocsDir>/XComGame/SaveData), with
<gameDocsDir> = "XCOM2" for vanilla and "XCOM2 War of the Chosen"
for WOTC. Resolved via util.getVortexPath("documents") like other
extensions that deploy to user-docs (BG3, Battletech, Sims 3).

Companion health check xcom2-save-deployed verifies the post-install
output contains a save-named file. The .XComMod-shape health checks
now skip save mods too (already skipped character-pool mods).

Reduces remaining harness failures from 14 to 10.
Extends the documentation-only skip heuristic in two ways:

- DOC_EXT_RE now includes image extensions (.jpg/.png/.gif/.bmp/
  .webp/.svg/.tiff). Image-only archives (wallpaper packs) and mixed
  doc+image archives (modding guides bundling PDFs with screenshots)
  collapse to the same conclusion as text-only docs — no installable
  content.
- An empty manifest (CDN content-preview lists no files for the
  archive) is also reported as documentation-only since the harness
  has nothing installable to act on either way.

Reduces remaining harness failures from 10 to 7 (the 6 still-failing
fixtures are all ReShade/SweetFX .cfg variants — a separate category).
The existing ReShade heuristic only matched shader source files
(.fx/.fxh/.hlsl) so config-only uploads slipped through:

- Real Vision ReShade variants ship with .cfg + .undef + a ReShade/
  wrapper directory (the wrapper isn't always present).
- Real Vision ReShade SSAO Boost has no wrapper at all — just SSAO.h
  + McFX.cfg at root.
- X-Com2.cfg is a bare SweetFX drop.

Two new recognition arms:

- .cfg / .undef anywhere in the manifest. XCOM 2 itself uses .ini for
  config and doesn't ship .undef, so in this game's fixture set these
  extensions are exclusively ReShade/SweetFX preset files.
- A ReShade/ or SweetFX/ path component (with / or . separator)
  catches the wrapper-folder case independent of file extension.

Reduces remaining fixture failures from 6 to 0. The only failing test
in the suite now is the intentional "harness lacks coverage for
extension points" notice for registerLoadOrder.
The ModBuddy source heuristic matched any archive containing a .uc or
.x2proj file. Legitimate XCOM 2 mod uploads commonly bundle source
.uc files alongside the compiled .upk and the .XComMod descriptor,
so the heuristic was skipping 1047 of 1728 fixtures — over 60% of
the set — including most properly-built mods.

Add the same `&& !hasXComMod` guard the raw-cooked heuristic already
uses: a .uc/.x2proj archive is only treated as source-only when there
is no .XComMod descriptor to claim it via the canonical installer.

Tally (from a SKIP_REASON_LOG instrumented run before the fix):

  1047 ModBuddy  ← was over-firing
    28 documentation
    26 raw cooked content
    20 nested archive
    15 ReShade
     7 voice pack
     5 standalone Windows tool
     3 Access database tool
     2 controller profile
     1 movies replacement
     1 cheat-engine table

Post-fix harness re-run blocked by a Nexus 429 rate limit; the
correction follows the same pattern the other no-XComMod-guarded
heuristics use, so the corrected count for ModBuddy is expected to
match the original categorization (~25 source-only uploads).
…typo

Adds 7 tests filling gaps in the existing load-order describe block so
every callback Vortex invokes at runtime is exercised:

- validate: empty load order; mixed clean+bad input → only bad ones
  flagged in `invalid`.
- deserializeLoadOrder: empty mods folder returns empty load order;
  deployed manifest populates `modId` on matching entries; Steam
  workshop install path scans `steamapps/workshop/content/268500/`
  and produces `steam-<lowercased>` ids.
- serializeLoadOrder: WOTC writes under XCom2-WarOfTheChosen/XComGame/
  Config/; an all-disabled load order still writes the INI header.

The steam-workshop test exposed a string-template typo in index.ts:

  `steam-${xmod}.toLowerCase()`

`.toLowerCase()` was text inside the template literal, not a method
call, so workshop mod `MyMod` got id `steam-MyMod.toLowerCase()`.
Fix moves the `.toLowerCase()` inside the interpolation expression.
Adds a "mod types" describe block invoking every registerModType
callback Vortex exercises at runtime, for all three modTypes
(character-pool, config-drop-in, save):

- isGameSupported(gameId) accepts xcom2 + xcom2-wotc, rejects others.
- test(instructions) returns false (production never auto-classifies;
  these modTypes are only set via the installers' setmodtype
  instruction).
- getInstallPath(game):
  - character-pool routes to XComGame/CharacterPool/Importable, WOTC
    nests under XCom2-WarOfTheChosen.
  - config-drop-in install path is the discovered game root (the
    install function emits the WOTC prefix in destinations when
    needed).
  - save reads util.getVortexPath("documents") and builds
    "<docs>/My Games/<gameDocsDir>/XComGame/SaveData", with
    gameDocsDir = "XCOM2" vs "XCOM2 War of the Chosen".
- Missing-discovery branch (util.getSafe → undefined) returns
  undefined for both per-game-path modTypes.

Adds util.getVortexPath to the local vortex-api mock so the save
modType's path resolver works under test.
Adds a "health checks" describe block invoking checkMod for each of
the five registered IModHealthCheck callbacks Vortex runs at health-
check time:

- xcom2-mod-has-files: empty files warns, non-empty passes.
- xcom2-has-xcommod-file: no .XComMod warns; .XComMod present passes;
  character-pool and save modTypes skip with status=passed.
- xcom2-xCommods-attribute-set: missing/empty array warns; non-empty
  passes; character-pool skips.
- xcom2-character-pool-has-bin: non-pool mods skip with status=passed;
  pool mod with no .bin warns; pool mod with .bin passes.
- xcom2-save-deployed: non-save mods skip; save mod with no save-named
  file warns; save_<name> and save<digits> both pass.

Each test invokes the registered checkMod against a minimal IMod
fixture (files + attributes), matching how Vortex itself calls these
at runtime.
Delete the per-extension __mocks__/vortex-api.ts and alias to the
shared packages/vortex-api/src/testing/index.ts (adding the missing
fs/util stubs it needs). Consolidate diagnostic.ts health checks via
a makeCheck helper, DRY up index.test.ts with initAndRegister and
mockDeserializeFs, and move platformPath before first usage in
installers.test.ts.
@github-actions
Copy link
Copy Markdown

This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed.

@github-actions
Copy link
Copy Markdown

This PR has conflicts. You need to rebase the PR before it can be merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant