| name | migrate-game-version |
| description | Migrate the All Aboard! mod's decompiled transport AI systems (PatchedTransportCarAISystem / PatchedTransportTrainAISystem) to a new Cities: Skylines II game version. Use this whenever the user mentions a new CS2 version or game patch (e.g. "1.5.9f1", "CO shipped an update", "the game updated", "new Paradox build"), wants to re-decompile / rebase / migrate the patched systems, is working on an `integration/<version>` branch, or the mod stopped compiling after a game update. It decompiles Game.dll with ilspycmd, rewrites the namespace/class, splices the configurable dwell-cap hook into StopBoarding, build-verifies, and bumps the version + changelog. Prefer this skill over doing the decompile-and-patch by hand. |
Migrate All Aboard! to a new game version
When Colossal Order ships a CS2 patch, the mod's two Patched*AISystem files —
verbatim decompiles of the stock TransportCarAISystem / TransportTrainAISystem
with one hook spliced into each — must be re-rebased onto the new game code. The
mod replaces those systems wholesale (Burst compilation rules out a lighter Harmony
patch), so a fresh decompile is unavoidable on each update.
This skill splits the job into the part that's mechanical and stable (scripted) and
the part that needs judgement every time because CO reshapes the code (you do it,
guided by references/hook-splices.md). The compile is the correctness gate.
Prerequisites
- ilspycmd on PATH (
dotnet tool install -g ilspycmd). This is the
ICSharpCode.Decompiler CLI — the same engine Rider bundles. The mod's older files
carry a dotPeek header, but dotPeek-the-standalone isn't required; ilspycmd output
at -lv CSharp7_3 is block-scoped and net48-compatible, which is what matters.
- The game installed and updated to the target version — the script reads the
live
Game.dll via the CSII_INSTALLATIONPATH / CSII_MANAGEDPATH env vars the
modding toolchain sets. Confirm Game.dll's timestamp matches the new patch.
CSII_TOOLPATH set (the build imports Mod.props/Mod.targets from it).
Workflow
1. Decompile + rewrite (scripted)
Run the helper from the repo root. Dry-run first to eyeball the output, then apply:
# stage only — nothing tracked changes
powershell -NoProfile -ExecutionPolicy Bypass -File .claude/skills/migrate-game-version/scripts/Decompile-Systems.ps1
# write the Patched skeletons into the repo (overwrites System/Patched/*.cs)
powershell -NoProfile -ExecutionPolicy Bypass -File .claude/skills/migrate-game-version/scripts/Decompile-Systems.ps1 -Apply
This decompiles both systems at -lv CSharp7_3 and applies every rewrite the
relocated decomp needs to drop into the project AND compile: namespace + class +
constructor rename to Patched*; inject using AllAboard.System.Utility; plus
using Game; and using Game.Simulation; (which restore implicit same-namespace
resolution); drop the class-level [CompilerGenerated] and add partial (so the
Unity SystemGenerator's shell merges cleanly); and normalize leading tabs to 4
spaces. On -Apply it also runs dotnet format whitespace against the just-
copied files so they match the repo's .editorconfig exactly — the migration
diff stays focused on real code changes rather than formatting churn. The
resulting Patched*.cs are pure vanilla logic otherwise — the hook is not yet
spliced.
Both Unpatched*.cs (verbatim, namespace Game.Simulation) and Patched*.cs
(rewritten skeleton) land in the staging -OutDir for side-by-side diffing.
Only Patched*.cs is copied into the repo: the Unpatched files keep the game's
namespace + class names and would collide with Game.dll types (CS0433) if
dropped into a compiled folder.
Commit this as the "decomp migration" before splicing — matching the repo's
convention (chore: migrate decomp to <version>). That keeps the next commit (the
splice) showing only the hook, which is what a reviewer cares about.
2. Splice the hook (judgement)
Read references/hook-splices.md and apply the hook to each Patched*.cs. There's
one hook per file, inside StopBoarding. In short: excise vanilla's hardcoded
1800-frame dwell cap and route the passenger-readiness decision through
PublicTransportBoardingHelper.ArePassengersReady(...) so the user's slider is the
sole authority.
Do not pattern-match last version's exact text — CO renames locals (flag2 vs
flag3) and refactors freely (1.5.9 moved the train's per-vehicle check into its own
method). Find the hook by its role (the loop/method touching CreatureVehicleFlags.Ready
inside if (!forcedStop)), preserve everything else the new decomp added, and apply
the documented transform.
3. Build-verify (the gate)
dotnet build AllAboard.sln -c Release
A clean build is the real correctness check — it confirms the fresh decomp + your
splice line up with the new game API. Note Mod.targets deploys the built mod to your
local Mods folder on success, so a green build also stages it for in-game testing.
If it fails to compile, it's almost always because CO changed an API the system uses
(a renamed field, a new method parameter). The fix is not to hand-port logic — the
fresh decomp already contains the new vanilla code. Re-check that your splice didn't
drop or mistype something, and that any field the hook references (m_CurrentVehicleData,
m_Passengers, m_SimulationFrameIndex) still exists under that name in the new decomp.
4. Bump version + changelog
First confirm with the user the exact game-version string (e.g. 1.6.0f1 — the
f suffix isn't derivable from Game.dll, which reports 0.0.0.0) and the mod
version bump (normally a patch increment). Both are user-facing published copy —
don't invent them.
Version fields (there are two ModVersions — don't miss the second):
AllAboard/Settings/AllAboardSettings.cs — ModVersion string.
AllAboard/Properties/PublishConfiguration.xml — <ModVersion Value="…"/>, and
<GameVersion Value="…"/> if the major.minor changed (e.g. 1.5.* → 1.6.*;
the old wildcard would no longer match the new game and the listing would read as
incompatible). Leave <ModId> untouched — it's the live Paradox Mods id.
Changelog — two files, two different conventions:
README.md keeps the full history. ### Release Changelog holds only the new
release; move the entry that was there down to the top of ### Previous Releases
(newest-first). Don't stack multiple versions under Release Changelog.
AllAboard/Properties/PublishConfiguration.xml (inside <LongDescription>) keeps a
rolling window of the last 3 releases only — it renders better in Skyve short.
So: new release alone under ### Release Changelog, previous two under
### Previous Releases, and drop anything older than those three. Keep every line
flush-left (no indentation) — Paradox's markdown renderer breaks on indented
content. Also update the separate <ChangeLog>…</ChangeLog> element near the bottom
(it holds just the current release's bullet).
The changelog bullet itself is typically - Update systems for <version>; match the
phrasing of the existing entries unless the user wants something more specific.
5. Hand off
Report the build result and the version bump. In-game behavior (does boarding actually
cap at the slider value?) can only be confirmed by the user playing a save — say so
rather than claiming the migration is "done/working" off a green build alone.
Notes
- Keep edits to
Patched*.cs surgical. They're decompiler output; the smaller your
footprint, the cleaner the diff against next version's fresh decomp.
- The
EnableDiagnostics path (BoardingDiagnosticsSystem) and the Experimental/
dead ends are unrelated to this migration — don't touch them.
- If
ilspycmd or the game install can't be found, the script fails loudly with the
fix; don't paper over it by guessing paths.
- This is a "trust but verify" pipeline: the script handles the boring 99%, but the
splice and the build are where correctness is won. Don't skip the build.