| name | implement-game-extension |
| description | Use when implementing a NEW Vortex game extension in the gdl-games monorepo — the user gives the Nexus site/extension id and the game name. Researches the game, writes games/<id>/game.yaml, fetches gameart, and drives corpus + unit tests + build to green. |
Implement Game Extension
Creates a new game extension in this monorepo from two inputs — the Nexus site/extension id
(the extension's mod page on nexusmods.com/site, used as nexus.modId) and the game name.
Researches the game, writes games/<id>/game.yaml, downloads the gameart tile, supports every
mod currently on the game's Nexus page (verified by the corpus loop), adds unit tests, and gets the
extension building green. It does not commit or release — that's left to the maintainer.
Read first: gdl/README.md is authoritative for the game.yaml DSL
and docs/corpus-manifests.md for how corpus verification
works and fails. This skill covers only how to research a game and drive it to green.
Reusable research methods (store ids, the artwork URL scheme, the template map) live in
references/research-recipes.md — read it before Step 2.
Workflow
digraph implement {
rankdir=TB; node [shape=box];
inputs [label="Ask: site/extension id + game name"];
resolve [label="1. Resolve domain, game id, fileGroupId"];
research [label="2. Research engine, exe, stores, mod layout"];
author [label="3. Author game.yaml + gameart\n3b. Document findings as game.yaml comments"];
corpus [label="4. Corpus loop: route EVERY mod\n(reconcile the count — green can be vacuous)"];
covered [label="Every mod matched\n(or documented-skip)?" shape=diamond];
units [label="5. Unit tests mirror real mod shapes"];
build [label="6. Build + test + corpus all green"];
review [label="7. Independent review subagent"];
ok [label="Review passes?" shape=diamond];
report [label="8. Report (no commit/release)"];
inputs -> resolve -> research -> author -> corpus -> covered;
covered -> corpus [label="no: add modType/installer"];
covered -> units [label="yes"];
units -> build -> review -> ok;
ok -> author [label="issues -> fix"];
ok -> report [label="pass"];
}
Inputs
Ask the user for exactly two things up front:
- Site / extension id — the extension's mod id on
nexusmods.com/site (becomes nexus.modId).
- Game name — e.g.
Solarpunk, 007: First Light.
Derive everything else. Only ask again when research is genuinely inconclusive (e.g. the domain
can't be confirmed, or fileGroupId can't be resolved).
Step 0: Preconditions
NEXUS_API_KEY is set in the environment (needed for the v1/v2 Nexus APIs and corpus fetch).
Check with echo $NEXUS_API_KEY (length only — never print it).
- The shared toolchain is built:
pnpm init-gdl (produces gdl/dist/cli.js).
- You're on a branch, not
main.
A finished game is just games/<id>/game.yaml + games/<id>/gameart.webp (plus src/hooks.ts
only if game.yaml references a hook). No per-game package.json, vitest.config, or workflow.
Step 1: Resolve identifiers
- Domain + game id: guess the domain from the name (lowercase, strip spaces/punctuation) and
verify via
GET https://api.nexusmods.com/v1/games/<domain>.json (recipe §1). This confirms the
name and gives the numeric game id (used for artwork) and the mod count. If it 404s or the
name doesn't match, ask the user for the domain.
nexus.modId = the site/extension id the user gave.
nexus.fileGroupId: resolve from the site mod (recipe §4); if it can't be resolved, ask.
Step 2: Research the game
Read references/research-recipes.md, then gather:
- Engine, executable, project folder, mod layout — from the web (store/mod pages, "UE4SS / pak
mods" searches) and, if the game is installed locally, by inspecting the Steam install: locate it
via
libraryfolders.vdf + appmanifest_<appid>.acf, then check for Engine/,
*/Binaries/Win64/*-Shipping.exe, Content/Paks (.pak/.utoc/.ucas ⇒ UE IoStore), and the
exe's version metadata. Record whether IoStore is in use — it decides
details.supportsSymlinks in Step 3, and getting it wrong crashes the game for users while every
check in this repo stays green.
- Store ids — steam app id, epic
AppName (egdata), xbox PackageIdentityName (displaycatalog),
each via recipe §2. Steam alone is enough to ship; add epic/xbox only with confirmed ids. Never
invent an id — omit the store instead.
- Closest template — pick from the recipe §6 template map (subnautica2 / solarpunk / gothic /
paralives, or "non-UE → from scratch").
If research can't pin down the engine, executable, or a store id you intend to include, ask the user
rather than guessing.
Step 3: Author game.yaml + gameart
Scaffold games/<id>/game.yaml by copying the closest template (Step 2) and adapting it. Field
semantics — every key, predicate, take/anchor/placeAt, and the built-in facts (installPath,
store, arch, appDataLocal, …) — are documented in gdl/README.md.
Don't restate them here; copy a template and change what the research in Step 2 established.
The decisions a template can't make for you:
- Which stores to declare. Steam alone ships. Add
epic/xbox only with confirmed ids
(recipe §2) — omit rather than guess, and say why in a comment (see Step 3b).
- Xbox needs three separate values, each for a different subsystem — an id alone is not enough:
the Identity Name (
stores.xbox) for discovery, appExecName (game.xboxLauncher) for
launching, and the exe path (game.executable) for process monitoring and version detection.
All three come from one displaycatalog query or one appxmanifest.xml. Recipe §2 has the commands.
Most games need none of this: a scalar executable: MyGame.exe is the normal case, and you only reach
for a storeBranch when the exe path itself differs per store (Xbox Binaries/WinGDK vs Steam
Binaries/Win64). Exemplar: games/halocampaignevolved/game.yaml.
executable / requiredFiles. Literal relative paths, no glob expansion — and they serve
different jobs. executable is the launch target; requiredFiles is a discovery fingerprint that
answers "is this folder really this game?", where every entry must exist. They're usually the
same string only because the exe is a convenient marker. When a nested exe's path differs by store
(Binaries/Win64 on Steam vs Binaries/WinGDK on Xbox), point requiredFiles at a file that lives
at the same relative path on both, and keep executable on the Steam path. For UE5 that's often
<Project>/Content/Paks/global.utoc, but it depends on IoStore being enabled — verify against a real
install rather than assuming, and see recipe §2 for fallbacks (UE4 usually has no global.*). Do
not list both arch paths: all-must-exist means that breaks both stores. A WinGDK layout is not a
reason to omit xbox.
game.details.supportsSymlinks. If the game's Content/Paks holds .utoc/.ucas it uses
UE5 IoStore, and symlinked pak files crash it — set . Never write
: Vortex only ever tests for , so does nothing while reading like a verified
claim (it reached eight game.yaml files here by copy-paste before anyone checked). Templates carry
it, so this is a key you must actively fix rather than one you can leave alone. Also check whether
any modType targets a path (, Documents) — that
removes hardlink and move deployment for the whole game for users whose staging folder is on
another drive. See .
Step 3b: Document as you go
Per-game research lives in # comment blocks inside game.yaml — not in a separate file.
Do not create games/<id>/README.md. Rationale sits on the line it explains, so it can't drift.
Exemplars: games/solarpunk/game.yaml, games/halocampaignevolved/game.yaml.
Write, as you learn it:
- A header block at the top: game (+ developer), engine and version, project folder, and which
mod categories are supported.
- Provenance for every non-obvious value — name the source and how it was checked, e.g.
# verified on a real install 2026-07-27 or # Identity/@Name from the shipped appxmanifest.xml.
Write what you actually did: copying this file's example provenance verbatim into a game.yaml
produces a citation that looks authoritative and can't be traced (it has happened).
# unverified — confirm on a real install on anything you inferred rather than observed. A
wrong path often still builds and tests green, so an honest marker is the only warning.
- Omission rationale — why
epic/xbox is absent. Silent omission reads as an oversight.
- Per-installer banners giving the priority, what it matches, and why each
unless arm exists,
citing real mod ids as evidence.
- A corpus-state note if coverage is incomplete or the cache was hand-generated (precedent:
games/assassinscreedblackflagresynced/game.yaml).
Step 4: Corpus loop — support every current mod
The corpus routes real published mods through your installers. It is a discovery tool: what it
finds must be promoted into tests.cases (Step 5), because the cache is gitignored and CI never
runs the corpus.
A green corpus run does not mean it worked. An all-failed fetch prints ✖ lines and
exits 0; an empty cache prints no archives in tests/cache/ — nothing to do and also exits 0.
Read docs/corpus-manifests.md before trusting any result.
Do these in order:
- Get the real mod count —
mods from GET https://api.nexusmods.com/v1/games/<domain>.json.
You need it to tell success from silence.
- Fetch. Add
--limit 100 on a large catalogue, or --mods 1,2,3 to scope:
pnpm nx run <id>:test-corpus -- --fetch
- Reconcile. Compare
T in summary: N matched, M unmatched, K failed, T total against the
count from (1). T=0, 0 matched, or nothing to do are FAILURES, not passes.
- If manifests 404 (currently expected for any upload after ~11 June 2026), generate listings
from the archives instead, then route them locally — note the absent
--fetch:
node tools/corpus/list-archive.mjs --domain <domain> --game-id <gameId> --all \
--out games/<id>/tests/cache
pnpm nx run <id>:test-corpus
- Only then iterate on unmatched/misrouted mods: inspect the cached listing, adjust
modTypes + installers, re-run, and confirm no regressions on already-matched mods.
Iterate until every published mod matches the correct installer, or is intentionally unsupported
with a documented reason. The bar is high — see the list of legitimate non-matches in
docs/corpus-manifests.md. When in doubt, support it.
Add validators asserting the key routings (e.g. dwmapi.dll → ue4ss-injector,
ReShade.ini → reshade, **/*.utoc without .pak → pak-iostore). A validator must be able to
fail — don't write one that passes for any input.
Mod categories that install-dir-shaped rules miss
Copying a pak-based template is not sufficient. On halocampaignevolved, 11 of 38 real mods were
unmatched under solarpunk-parity rules. Check explicitly for:
- Config-file tweaks (
Engine.ini) — UE reads user config from
${appDataLocal}/<ProjectFolder>/Saved/Config/<TargetPlatform>, not the install dir. This was
the single most common category on that game (5 of 38, plus another appearing mid-review).
- The
<TargetPlatform> leaf varies per game/build — Windows (UE5 unified),
WindowsClient (client-only), WindowsNoEditor (UE4). Verify it against a real install or
ask; do not copy another game's. Assuming WindowsClient for Halo was wrong — it's Windows.
- Authors ship both
Engine.ini and engine.ini — brace-match both spellings.
- Keep the validator's
placement loose (**/Saved/Config/**) so it doesn't bake in the leaf.
- Add the config dir to
setup.ensureDirs. UE creates it on first launch and does not
ship Engine.ini at all (verified: Meteorite and Subnautica2 have only
GameUserSettings.ini), so deploying before first launch hits a missing target — the
"Deployment target unknown" failure fixed for games/paralives. Any deploy target outside
the install dir needs this.
- Asset / media replacement — a bare file overwriting shipped content (e.g.
FMS_MainMenuBackground.mp4), needing its own modType pointing at that content subfolder
(6 of 38).
- Both need
unless guards excluding **/*.pak, **/*.utoc, **/*.dll, **/*.lua and
game-relative trees (<ProjectFolder>/**, Engine/**), so a mod bundling a config alongside a
pak keeps its pak routing instead of being reduced to a config drop. Add a tests.cases entry
asserting that guard, or it is untested.
Working reference: games/halocampaignevolved/game.yaml (configPath/menuMoviePath in context,
the config-ini and menu-movie installers, and their validators). ${appDataLocalLow} also
exists — see games/paralives/game.yaml.
Step 5: Unit tests
Add a tests.cases entry for each real mod category found in Step 4, using the actual archive
shapes (e.g. the exact Better Stacks/0_BetterStacks_P.pak triplet) — never invented ones. Each
case asserts matched, modType, and the resolved plan. Note the real mod id in a comment so the
shape can be re-checked later.
pnpm nx run <id>:test
These are deterministic and run in CI, whereas the corpus is local-only and its cache is gitignored
— so these cases are the only durable regression guard. Every category the corpus revealed must
appear here, including one case per unless guard that would fail if the guard were removed.
Step 6: Build & verify
pnpm nx run <id>:build
pnpm nx run <id>:test
pnpm nx run <id>:test-corpus -- --fetch
Confirm games/<id>/dist/info.json has the right version and a <id>-vortex-v<version>.zip is
produced by pnpm nx run <id>:package. Optionally smoke-test discovery by copying dist/* into the
local Vortex plugins/game-<id>/ and launching Vortex.
Step 7: Independent review
Spawn a clean-context review subagent (model sonnet) that reads the result fresh:
Tool: Agent
model: sonnet
description: "Review new game extension"
prompt: |
Review the new Vortex game extension at games/<id>/ (repo root is the current working
directory). Verify:
1. game.yaml is well-formed; `pnpm nx run <id>:build` succeeds.
2. Store ids match what Vortex actually matches: steam=app id, epic=manifest AppName,
xbox=package Identity Name (not the 9N store id). Flag any guessed/placeholder id.
3. `pnpm nx run <id>:test` passes and the tests.cases mirror real mod shapes (not trivial).
4. Corpus coverage is real, not vacuous: compare the `summary: … T total` line against the
`mods` count from api.nexusmods.com/v1/games/<domain>.json. `0 matched`, `T=0`, or
"nothing to do" is a FAILURE even though it exits 0 (see docs/corpus-manifests.md).
Any shortfall must be documented in game.yaml comments.
5. Every validator can actually fail (trace a bad input). Flag vacuous ones.
6. Any path that was assumed rather than observed is flagged `# unverified` — especially a UE
user-config leaf (Saved/Config/<TargetPlatform> varies per game) and store-specific exe
paths. A wrong path can build and test green.
7. game.yaml carries the comment blocks from Step 3b (header, provenance, omission rationale).
There must be no games/<id>/README.md.
For an ALREADY-PUBLISHED game, also flag any renamed/removed modType id — it orphans users'
installed mods and nothing in the build catches it (docs/published-extension-stability.md).
8. gameart.webp exists and is a valid WebP of the game's tile.jpg, saved uncropped (tiles are
portrait, e.g. 400x600). Minor art issues are low severity — it's a fallback Vortex only uses
when it can't load tile art from the site.
Report PASS, or ISSUES with specific file:line problems. Be strict.
Fix anything it flags and re-review until it passes.
Step 8: Report & hand off
Summarize: domain, game id, stores, engine/template used, mod count and how each routed,
tests added, and the green build. Do not commit, push, or tag — because the nexus ids are
real, pushing a version bump to main triggers a Nexus release. Tell the maintainer the extension
is ready and they can commit/push (and bump version) when they want to release.
Conventions
- Run Nx via
pnpm nx … (it's a local dependency — bare nx needs a global install). The corpus
target is test-corpus (hyphen); the underlying gdl CLI subcommand is test:corpus, which also
accepts --limit N and --mods 1,2,3 (pass them after --).
- Never print the
NEXUS_API_KEY value.
- Don't invent store/nexus ids, or any path. Omit, mark
# unverified, or ask — rather than guess.
- Per-game research goes in
game.yaml comments (Step 3b). No per-game README.
gdl/ is a separate repository (a submodule). Don't change it as part of authoring a game —
propose it separately.
- Never commit, push, tag, or bump
version:. A version bump merged to main publishes to Nexus.
Reference
gdl/README.md — the DSL: every game.yaml key, predicates,
take/anchor/placeAt, built-in facts, CLI flags. Authoritative; this skill does not restate it.
README.md — repo setup, Nx targets, release process.
docs/corpus-manifests.md — corpus mechanics, the
silent-pass trap, the manifest outage, and the offline workaround.
references/research-recipes.md — the curl recipes and the template map.