← all tickets

17 — Migrate: System Gateway cards

What to build: Every implemented System Gateway card (~57) plays through the registry system: each card's hooks move into one contiguous block in the pack's card module, and its match arms are deleted as it moves so no card is registered in both worlds.

Blocked by: 16 — Expand: registry infrastructure + pilot card.

Status: resolved

Comments

Implementation notes

All 57 implemented System Gateway cards (every non-vanilla implemented entry in game/cards_impl/sg.json, verified against that file) now live in cards/sg.rs as one contiguous reg.card("…") block each, in NRDB code order, followed by their hook implementations in the same order. 107 registrations across 36 registries.

Hook functions moved verbatim from hooks.rs; the only edits were signature adjustments where a port-era function was narrower than its registry type (e.g. sure_gamble(state)sure_gamble(state, _card)), and guards that used to sit in the match arm moving inside the hook that owns them ("30008" if ic.counters > 0 → the check at the top of leech_actions). send_a_message is registered on both on_score and on_steal, which is what the two arms did.

Two cards needed more than a move:

engine::fire_subroutine because diviner_sub needs deal_net_damage / end_run. Both are pub(crate), so the hook moved to cards/sg.rs unchanged and fire_subroutine now consults only the registry.

registration and the two documented engine approximations moved with it.

hooks.rs is now dispatchers plus the Elevation/Voices-of-the-Past arms (1643 → 824 lines). Twenty-nine of them, plus the SUBROUTINE_OVERRIDE site in engine::fire_subroutine, had no arms left afterwards and collapsed to a registry lookup with their default (get_ice_subs keeps its "End the run." fallback, playability_gate its true, and so on). Of the registries behind them, only ON_TURN_START still holds an unmigrated code (Azimat, 35029) — that one is a runner program, so trigger_corp_turn_start is registry-only while trigger_runner_turn_start keeps its arm. all_server_targets became pub(crate) so the RUN_EVENT_SPEC hooks in pack modules can share it.

Ordering: INSTALL_COST_MODS and ACCESS_BONUS are applied in registration order rather than looked up. Code order preserves the old order for INSTALL_COST_MODS (Carmen, DZMZ, then the still-inline Principia). For ACCESS_BONUS it reorders Jailbreak and Conduit, which is behavior-preserving: the three hooks are a sum over disjoint cards, and Docklands is the only one that mutates state (its own once-per-turn flag).

registry_manifest.rs is unchanged — a migration moves *where* a code is registered, not whether — so the frozen-fixture parity test passes untouched.

Testing: the pilot's card_30040_dispatches_through_the_registry assertion was replaced by every_system_gateway_registration_is_in_the_registry, which walks registry_manifest.rs, and for every 30xxx key asserts the live registry contains it. That is the machine-checked form of "none remain in match arms", and it covers 30040 as a special case of the general rule. The six card_30040_* behavior tests are unchanged. No new per-card tests were added — the card_<code>_* bar for these ~57 grandfathered cards is issue 05's subject, not this one.

Verified (2026-07-30)

components are not installed in this toolchain, so neither was run.)

test_registry_parity.py against the frozen golden manifest.

games (both MCTS engines) of the two System Gateway starter decks from fixed seeds, exercising most of the migrated pool. Its per-game winner and action-count output is byte-identical before and after the migration.

Sign-off (2026-07-30)

Reviewed and approved by Tomáš Musil. Marked resolved.