Normalize: XCOM 2#23076
Conversation
0feb4c4 to
81b91cc
Compare
|
This PR has conflicts. You need to rebase the PR before it can be merged. |
f27503c to
177594c
Compare
da7838a to
598d4c3
Compare
177594c to
24672c9
Compare
f9f6528 to
540a62b
Compare
|
This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed. |
24672c9 to
b20df22
Compare
|
This PR has conflicts. You need to rebase the PR before it can be merged. |
b0e6deb to
5ad04b1
Compare
540a62b to
b2d6fbf
Compare
|
This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed. |
❌ Code is not formattedTo fix this:
Then commit the formatting changes. View logs |
|
This PR has conflicts. You need to rebase the PR before it can be merged. |
… 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.
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.
a3badce to
3e6e946
Compare
|
This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed. |
|
This PR has conflicts. You need to rebase the PR before it can be merged. |
Summary
Normalize
game-xcom2extension per project checklist.1:1 behavioral replacement, no feature changes (plus one bug fix noted below).
Changes
queryPath(GameStoreHelper.findByAppId) withqueryArgsshorthand for Steam, GOG, and Epicelectron.remotewith@electron/remotefs.ensureDirAsyncwithfs.ensureDirWritableAsyncin setupsupportedTools()instead of WOTC_IDVerification