minimax joins the show

This commit is contained in:
csaldiasdev
2026-07-05 20:01:28 -04:00
parent 501dc4da9f
commit 0cbb693399
2 changed files with 629 additions and 0 deletions

166
AGENTS.md Normal file
View File

@@ -0,0 +1,166 @@
# AGENTS.md
Working in **reversed_stadium** — a work-in-progress *matching* decompilation of
Pokémon Stadium (US, N64). The end goal is C source whose compiled output is
**byte-identical** to the original ROM
(`pokestadium.z64`, `md5: ed1378bc12115f71209a77844965ba50`).
This file is an agent-focused checklist. For background, build commands, and
extended architecture notes see `README.md`, `CLAUDE.md`, and
`docs/c-first-plan.md` (the in-progress C-first campaign roadmap). Don't
duplicate what's already in those — link when possible.
---
## "Tests" don't exist here
There is no `make test` and no unit-test suite. The only correctness check is
that `make` produces a ROM whose md5 matches the baserom. Anything you change
in `src/`, headers, splat yaml, the Makefile, or generated assets can break
that match.
```bash
make # build + md5 verify against baseroms/us/baserom.z64
make diff-init # snapshot current build/ into expected/ for diffing
make NON_MATCHING=1 # build without requiring a byte-match (sets NON_MATCHING, AVOID_UB)
```
Baseline guard after every change: `make` must still md5-match, then
`ps-firstdiff` (Rust) or `tools/first_diff.py` must exit 0. See `CLAUDE.md`
for the Python↔Rust tooling parity table.
## Build prerequisites (not optional)
- **MIPS binutils** on `PATH`: `mips-linux-gnu-{as,ld,objcopy,objdump,nm}`.
Override prefix via `MIPS_BINUTILS_PREFIX=` if yours is named differently.
The Makefile errors out hard if `mips-linux-gnu-ld` is missing — fix your
env, don't paper over it.
- **IDO `cc`** vendored under `tools/ido/<os>/{5.3,7.1}/cc`. The build does
**not** use GCC for actual compilation; GCC is only a syntax/warning check
(`RUN_CC_CHECK`, see below). IDO codegen quirks are the source of truth —
small "cleaner" rewrites often break the byte-match. Always verify with
`./diff.py -mwo <func>` or `ps-fdiff <func>`.
- **Git submodule**: `tools/n64splat` (from `.gitmodules`). If you cloned
without `--recurse-submodules`, `git submodule update --init` it before
building — otherwise `make extract`/splat will silently misbehave.
- **Python venv** at `.venv`; populated by `make init` from `requirements.txt`.
### macOS — read before building
`make` on Darwin is GNU make, invoked as `gmake`. Two things *will* break
silently if missing:
- Install **GNU libiconv** (`brew install libiconv`). The system BSD `iconv`
mis-encodes `\` (0x5C) inside Japanese strings as the fullwidth EUC-JP
backslash (0xA1C0), corrupting `\n` escapes and diverging the ROM by ~293
bytes at the end of `fragments/62`. The Makefile auto-detects Homebrew's
libiconv and warns if it can't — that's your signal to install.
- Build with **`gmake RUN_CC_CHECK=0`**. The host-compiler `-m32` +
`-Wint-conversion` combo errors out under Apple clang without affecting
the ROM. Don't disable it on Linux.
## File layout & conventions agents get wrong
- **`src/*.c` are named by ROM offset**, not by feature (`11BA0.c`, `D470.c`,
`33FE0.c`, `src/fragments/1/fragment1_7F9A0.c`, ...). This is on purpose —
matching decomp preserves the ELF layout. **Don't rename files for clarity.**
- **Address-based identifiers** (`func_80010FA0`, `D_86002F34`,
`unk_D_8690A610`) are the norm. Renaming them to meaningful names *as
purpose becomes understood* is core documentation work; do it in commits
separate from any behavior change.
- Sources are **EUC-JP encoded** (Japanese text strings). The build pipes
through `iconv`; keep the encoding.
- Header structs carry explicit `/* 0x.. */` offset comments and trailing
`// size = 0x..` markers — **preserve them**.
- `lib/ultralib/` is the vendored N64 SDK built separately with IDO 5.3 and
archived into the ROM. Don't touch unless you know why.
- `lib/ultralib/src/os/...` is consulted via relative includes from a few
`src/*.c` files (e.g. `src/libleo/leointerrupt.c`). Path is ugly on
purpose to avoid touching the submodule tree.
- `asm/us/nonmatchings/<file>/<func>.s` is the reference asm for any function
not yet decompiled. A function is "done" once that `.s` is gone
(see `python3 progress.py` or `ps-status --build-aware`).
## `GLOBAL_ASM` and the per-file compile loop
A `pragma GLOBAL_ASM("asm/us/nonmatchings/<file>/<func>.s")` in a `.c` file
pulls in a function's reference asm. Files containing `GLOBAL_ASM` are routed
through `tools/asm-processor`. To decompile a function:
1. Generate context: `python3 tools/m2ctx.py src/<file>.c` → `ctx.c`
(types/externs/localities).
2. First-draft C: `m2c --context ctx.c --target mips-ido-c
asm/us/nonmatchings/<file>/<func>.s`. Functions with **jump tables** need
the defining `.rodata.s` passed as an extra arg or m2c returns "jump table
not provided".
3. Hand-clean types against `src/<file>.h` and raw `.s`. m2c output has known
artifacts: `M2C_ERROR(/* unset register */)`, `void*`/`?`, struct accesses
at wrong offsets, leftover-`$f0` (callee declared `void` ending in
`sqrtf(...)` whose caller uses `$f0` — fix by giving the callee a `f32`
return).
4. Build (`make` or `make NON_MATCHING=1`) until the match is byte-exact or the
guarded C compiles under `NON_MATCHING`.
5. `./diff.py -mwo <func>` / `ps-fdiff <func>` for per-function diff
(non-interactive vs interactive TUI; both validated to parity).
6. Format with `python3 format.py -j src/<file>.c` (requires `clang-format`
/ `clang-tidy` / `clang-apply-replacements` v14). Re-check `progress.py`
and confirm `make` still md5-matches.
For the broader C-first campaign plan (the 61 remaining bare `GLOBAL_ASM`
functions, tooling additions like `ps-status --c-coverage`,
`ps-fdiff --file <src>`) read **`docs/c-first-plan.md`** end-to-end before
starting a batch.
### `NON_MATCHING` gotcha
**Do not run a full `make NON_MATCHING=1` between iterations on the matching
build.** It rebuilds every object as the NON_MATCHING variant. Switching back
to plain `make` only rebuilds what's changed, leaving a mixed-object build
with a wrong md5 (e.g. observed `964842af…`). To verify a single file compiles
under NON_MATCHING, build **just its object**:
```bash
gmake NON_MATCHING=1 RUN_CC_CHECK=0 build/src/<file>.o
```
If you ever do run a full NON_MATCHING build, restore the matching build
with `gmake clean && gmake` and `gmake diff-init` to refresh `expected/`.
## Per-file Makefile overrides
Look at the bottom of the `Makefile` (search `build/src/...: %:` rules). Many
files force a specific compiler (`CC := $(CC_OLD)`), opt level (`OPTFLAGS :=
-O0`), ISA (`-mips1`), or pragma (`-Wo,-loopunroll,0`,
`-trapuv`, `-signed`). When a newly split file won't match and you can't see
why, check whether it needs an override and copy the pattern from a sibling.
## Splat / assets / linker
- Splat config lives in `yamls/us/{header,rom}.yaml`; concatenated to
`pokestadium-us.yaml` at `make extract` time. Don't hand-edit the
concatenated file — it's gitignored.
- New C from splat comes in as `GLOBAL_ASM` stubs per function.
- `linker_scripts/us/{hardware_regs,undefined_syms,unused_syms}.ld`,
`linker_scripts/common_undef_syms.ld`, and the `auto/` directory drive the
ELF layout — `auto/` is generated; don't hand-edit.
- `assets/us/*.bin` and `*.png` are extracted from the baserom at `make
extract` time and re-incorporated as `.o` via the `$(BUILD_DIR)/%.o: %.bin`
rule.
## What "done" looks like for a function
- `asm/us/nonmatchings/<file>/<func>.s` deleted (function moved out of asm).
- Function and its struct fields renamed in `src/<file>.h`, keeping offset
and size comments.
- File formatted (`python3 format.py`) and `make` still md5-matches.
- `python3 progress.py` shows the byte count tick up (or stays flat if you
only renamed), and `ps-status --c-coverage` (Rust) shows unguarded
`GLOBAL_ASM` count dropping by one.
## Skills & OpenCode config
No `.opencode/` or `opencode.json` is checked in. The
`.claude/settings.local.json` is just an outputStyle override; nothing
agent-binding. If you add agent config, scope it tightly and don't fork C
tooling knobs (RUN_CC_CHECK, NON_MATCHING, MIPS_BINUTILS_PREFIX) into
persistent `.make_options` files casually — they're per-iteration.

463
docs/rename-plan.md Normal file
View File

@@ -0,0 +1,463 @@
# Plan: rename every identifier in matching C code
## Why
The repo is at ~89.6% decomp-bytes, but the existing C still uses
address-based identifiers (`func_80029310`, `D_800AE540`, `unk_24`)
for nearly every symbol. Renaming them — **only in functions that
currently compile to byte-identical asm** — is the highest-leverage
way to finish "understanding, rewriting, and documenting" the existing
decompiled C (see `ABOUT_AI.md`). It also unlocks reading the codebase
without constantly bouncing into the map file.
This is a **Rust-toolkit-only** campaign: every measurement, worklist,
and verification step uses the `tools/rust/` workspace. The Python
matching tools (`progress.py`, `tools/first_diff.py`, `./diff.py`)
are **off-limits** — the Rust versions are validated to parity (see
`tools/rust/README.md`). The one remaining Python is `format.py`
(code-style formatter, not matching tooling).
## Goals (measurable)
- 0 `func_*` in `src/`, `include/` (kept only when purpose is genuinely unknown)
- 0 `D_*` globals in `src/`, `include/`
- 0 `unk_NN` struct-field names in headers (keep `/* 0xN */` offset comments)
- `make` still produces `md5 ed1378bc12115f71209a77844965ba50`
- `ps-status --rename-coverage` reports **100%**
## Hard precondition: md5 gate (non-negotiable)
**Every single commit MUST reproduce `md5 ed1378bc12115f71209a77844965ba50`.
No exceptions. No "I'll fix it in the next commit."**
Before **every** `git commit`:
1. Run `make`.
2. Read the md5 it prints.
3. **Only commit if the md5 is `ed1378bc12115f71209a77844965ba50`.**
4. If the md5 is anything else: **do not commit.** Run `ps-firstdiff`
to locate the divergence, fix it, rebuild, recheck. Repeat until md5
matches. Then commit.
This is enforced mechanically by the recommended verification flow:
```bash
# One-shot gate — exits 0 only if md5 matches AND rename is complete.
ps-rename-check <old> <new> && git commit
```
A commit that breaks md5 is a **revert, not a fix-forward**. The whole
point of the campaign is that *renames are semantics-preserving by
construction* — if md5 breaks, the rename was wrong (typo, missed
callsite, accidental semantic edit). Revert, then reattempt.
This rule applies equally to:
- Function renames
- Global var renames
- Struct field renames
- Enum / macro renames
- Phase 0 tooling commits — the tool itself must still build a
matching ROM (renames don't change md5, but a sloppy tool change
could)
A commit history where every commit is md5-clean is the entire
defense against silent regressions during a multi-month rename
campaign. **Do not relax this rule, ever.**
## Out of scope
- `lib/ultralib/`, `src/libultra/` — vendored
- `asm/us/*.s` — pure asm
- Anything behind `#ifdef NON_MATCHING` — not byte-matching yet
- `hasm` segments (boot, Yay0, 49190, 517A0, abs)
- `linker_scripts/us/symbol_addrs*.txt` — pins addresses; the name
change is link-invisible, **do not edit them**
- Python matching tools (`progress.py`, `tools/first_diff.py`, `./diff.py`)
— superseded by the Rust toolkit
---
## Rust toolkit extensions (Phase 0 — done first)
Add three modes to `ps-status` and one small new binary, all in
`tools/rust/crates/`. **Tooling lands in its own commits before any
rename** — no commit should mix "added tool" with "renamed something".
### `ps-status` additions
Edit `tools/rust/crates/ps-status/src/main.rs` and the helpers in
`ps-core/src/globalasm.rs` / `ps-core/src/symbols.rs` as needed:
```bash
# Campaign metric. 100% = every identifier has a meaningful name.
ps-status --rename-coverage
ps-status --rename-coverage --json
ps-status --rename-coverage --per-folder
# The worklist, sorted by call-graph depth (roots first).
ps-status --rename-list
ps-status --rename-list --funcs
ps-status --rename-list --globals
ps-status --rename-list --fields
ps-status --rename-list --enums
ps-status --rename-list --macros
ps-status --rename-list --file src/29BA0.c
# Graph queries — cheaper than re-running rg every commit.
ps-status --callees <symbol>
ps-status --callers <symbol>
```
`--callees` / `--callers` resolve direct `jal` targets via the
build map. Indirect calls through function pointers get flagged with
`?` so the agent knows to grep separately for those.
### `ps-rename-check` (new binary)
New crate at `tools/rust/crates/ps-rename-check/`. ~80 lines. Reuses
`ps-core::globalasm` and `ps-core::mapfile` — does not reinvent them.
```bash
# Verifies a single rename is complete and the build is still matching.
# Exits 0 only if ALL hold:
# (a) zero stragglers of <old> remain in src/ include/
# other than the rename commit's definition;
# (b) `make` produced md5 ed1378bc…;
# (c) `ps-firstdiff` exits 0.
# Exits 1 if any check fails. Exits 2 on tool error.
ps-rename-check <old> <new>
```
### Validation requirements
Each new tool gets:
1. Unit tests under `crates/<tool>/tests/` mirroring the existing
`ps-core` test style.
2. **Parity check** against a ground truth on a clean tree:
- `--rename-coverage` totals must match
`rg -c '\b(func_[0-9A-F]{8}|D_8[0-9A-F]{8})\b' src include`.
- `--callees <f>` for a sample function must match the manual
`rg '\bjal\b.*\b<f>\b' asm build` cross-reference.
3. `cargo test`, `cargo clippy --all-targets`,
`cargo build --release --manifest-path tools/rust/Cargo.toml` —
all green before any rename commit lands.
---
## Per-identifier workflow
The unit of work is **one identifier per commit**. Local variables
belong to their containing function — fix them when you fix the
function, in the same commit.
### Function (most common case)
```bash
1. Read src/<file>.c, understand what it does. Trace callees.
2. ps-status --callees <func> # graph query
ps-status --callers <func>
3. Pick a name matching the convention (below).
4. Update declaration:
src/<file>.h # if file-scoped
include/functions.h # if cross-file
5. Update definition in src/<file>.c.
6. Update every callsite:
rg -n '\b<old_name>\b' src include
Must show 1 occurrence (the definition) or 0 (extern-only).
7. make # md5 must match
8. ps-firstdiff # exits 0
9. ps-rename-check <old> <new> # one-shot: 1+7+8
10. python3 format.py src/<file>.c # formatter stays Python
11. Commit (template below).
```
### Global variable
Declaration in `include/variables.h` or `src/<file>.h`. Address is
pinned in `linker_scripts/us/symbol_addrs*.txt` — **leave those alone**,
the name change is link-invisible, md5 still matches. Workflow same
as function but step 6 must scan `include/` and `src/`.
### Struct field
Edit the `/* 0xN */` comment in the header to add the snake_case
field name; update every `.unk_NN` access across files.
**Preserve offset comments and trailing `// size = 0xN` markers —
non-negotiable.**
### Enum value / macro
Rename in the definition, then update every use. `switch` arms are
the most common call site.
### Local variable
In-source only, scoped to the function being renamed. Done in the
same commit as the function rename. Pattern: `s32 temp;` →
`s32 frameIndex;`.
---
## Strategy for unknowns
Never invent a name you can't justify. For functions whose purpose
you can't figure out, **leave the address-based name and document
it** in a per-file `KNOWN_UNKNOWNS` block at the top of
`src/<file>.h`:
```c
// === KNOWN_UNKNOWNS ============================================
// func_80029310: called from Game_Thread case STATE_N64_LOGO_INTRO.
// Calls: func_80005A20, func_80029500.
// Reads: gCurrentGameState. Writes: D_800AE540.
// func_800293CC: called from Game_Thread case STATE_TITLE_SCREEN.
// ...
// ===============================================================
```
The block grows with each pass while the in-code `func_*` count
shrinks. The block *is* the next pass's worklist.
**`rg KNOWN_UNKNOWNS` is the cross-file query for unknowns.**
If a function is "almost obvious" but not 100% certain, prefer to
leave it address-named and add it to `KNOWN_UNKNOWNS` with a partial
description. A wrong name is worse than no name.
---
## Naming convention
Match the closest sibling. Existing precedent in `src/`:
| Kind | Convention | Examples (current) |
| --------------- | ---------------------------- | ----------------------------------------------------------------- |
| Module function | PascalCase, `Module_` prefix | `Game_Thread`, `Cont_SetupControllers`, `Yay0_Decompress`, `Game_DoCopyProtection` |
| Utility | snake_case | `crash_screen_init`, `rsp_init`, `main_pool_push_state` |
| Global var | `g` + PascalCase | `gCurrentGameState`, `gLastGameState` |
| Enum value | `STATE_*` SCREAMING_SNAKE | `STATE_N64_LOGO_INTRO`, `STATE_BATTLE_NOW_1P` |
| Macro | SCREAMING_SNAKE_CASE | (audit per file) |
| Struct field | snake_case | replace `unk_NN`, keep offset comment |
For the `Game_Thread` state handlers, suggested pattern:
`Game_HandleN64LogoIntro`, `Game_HandleTitleScreen`, etc. —
short, verb-led, module-prefixed.
---
## Phasing
The campaign is too big to do at once. Phases give natural commit
boundaries and let you ship in increments.
### Phase 0 — Rust tooling (this week)
- `ps-status --rename-coverage` (+ JSON, + per-folder)
- `ps-status --rename-list` (+ kind filters, + file filter)
- `ps-status --callees <f>` / `--callers <f>`
- New `ps-rename-check <old> <new>` binary
- All with unit tests + parity checks against `rg`
- `cargo test && cargo clippy --all-targets` green
**Done when:** `ps-status --rename-coverage` runs on a clean tree
and reports a baseline % (the starting line for the campaign).
### Phase 1 — Root 0 boot init (~5 functions)
`src/main.c` callers and the bare init helpers. Trivial wins;
tightens the per-commit loop on real data.
- `func_80001474` (init helper in `Idle_ThreadEntry`)
- `func_8000D564`
- `func_800052B4`
- `func_800019C8`
- `func_800196DC` (called from `Game_Thread`, likely controller init)
**Done when:** all renamed, `ps-status --rename-coverage` ticks,
`make` still md5-matches.
### Phase 2 — Game_Thread state handlers (23 functions)
The `switch` arms in `src/29BA0.c:796-870`. Highest-value first
batch — each handler maps 1:1 to a known game state.
| Address-named function | Game state (`gCurrentGameState`) | Suggested name |
| --- | --- | --- |
| `func_80029310` | `STATE_N64_LOGO_INTRO` | `Game_HandleN64LogoIntro` |
| `func_800293CC` | `STATE_TITLE_SCREEN` | `Game_HandleTitleScreen` |
| `func_800296AC` | `STATE_N64DD_BOOT_UNUSED` | `Game_HandleN64DDBootUnused` |
| `func_80029828` | `STATE_AREA_SELECT` | `Game_HandleAreaSelect` |
| `func_8002A698` | `STATE_GALLERY` | `Game_HandleGallery` |
| `func_80029884` | `STATE_EVENT_BATTLE` | `Game_HandleEventBattle` |
| `func_800298D4` | `STATE_OPTIONS` | `Game_HandleOptions` |
| `func_80029924` | `STATE_MENU_SELECT` | `Game_HandleMenuSelect` |
| `func_80029BC0` | `STATE_STADIUM_MENU` | `Game_HandleStadiumMenu` |
| `func_8002A06C` | `STATE_FREE_BATTLE` | `Game_HandleFreeBattle` |
| `func_8002A400` | `STATE_VS_MEWTWO` | `Game_HandleVsMewtwo` |
| `func_8002A670` | `STATE_KIDS_CLUB` | `Game_HandleKidsClub` |
| `func_8002A6C0` | `STATE_VICTORY_PALACE` | `Game_HandleVictoryPalace` |
| `func_8002EF44` | `STATE_POKEMON_LAB` | `Game_HandlePokemonLab` |
| `func_8002A728` | `STATE_GB_TOWER` | `Game_HandleGBTower` |
| `func_8002AAA8` | `STATE_GYM_LEADER_CASTLE` | `Game_HandleGymLeaderCastle` |
| `func_8002ADE8` | `STATE_BATTLE_NOW_1P/2P` (×2) | `Game_StartBattleNow` |
| `func_8002AF38` | `STATE_BATTLE_FROM_EVENT` | `Game_HandleBattleFromEvent` |
| `func_800296E0` | `STATE_STUBBED_DEBUG` | `Game_HandleStubbedDebug` |
| `func_8002B180` | `STATE_FAST_BATTLE` | `Game_HandleFastBattle` |
| `func_8002B24C` | `STATE_KIDS_CLUB_TITLE` | `Game_HandleKidsClubTitle` |
| `func_8002B07C` | `STATE_FAST_N64_LOGO` | `Game_HandleFastN64Logo` |
| `func_8002B310` | wrapper — calls `func_80000DF4` once | (audit first) |
**Done when:** all 23 renamed; `KNOWN_UNKNOWNS` for `29BA0.c` reduced
to only the genuinely-unknown helpers.
### Phase 3 — Subsystems (multi-month)
Work downward through each state handler's callees. Order (by repo
footprint):
1. Controller (`Cont_*`, `src/controller.c`)
2. GB emulator (`src/gb_mbc.c`, `src/gb_tower.c`)
3. 64DD / `libleo/`
4. Audio (`src/libnaudio/`)
5. RSP / graphics (`src/rsp.c`, `src/geo_layout.c`, `src/stage_loader.c`)
6. `fragments/` overlays
Each subsystem = a self-contained Phase 3.x sub-phase with its own
`KNOWN_UNKNOWNS` tracking.
---
## Per-commit verification (Rust-only)
**The md5 gate is the only check that matters for commit eligibility.**
All other checks are quality bars, but a missing call-site or a typo
will *also* break md5 — so the md5 check catches everything that
could silently regress the build.
The required order, with the **commit gate** at the end:
```bash
# --- QUALITY CHECKS (do first, fix issues found) ---
# 1. Rename is complete (no stragglers of the old name)
rg -n '\b<old_name>\b' src include
# Expect: 0 hits, OR 1 hit (this commit's definition only).
# 2. Formatted
python3 format.py src/<file>.c
# Formatter stays Python — it's code-style, not matching tooling.
# --- BUILD + MD5 (the gate) ---
# 3. Build and read the md5
make
# Expect: md5sum output ends with `ed1378bc12115f71209a77844965ba50`.
# 4. Regression check via the Rust tool
ps-firstdiff
# Expect: exit 0.
# 5. One-shot all-in-one (preferred — combines 1, 3, 4)
ps-rename-check <old> <new>
# Expect: exit 0.
# --- COMMIT GATE: only proceed if md5 is ed1378bc… ---
# 6. ONLY if step 3 (or 5) reported md5 ed1378bc…:
git add -p
git commit -m "rename: ..."
# Else: STOP. Do not commit. Revert and fix.
```
**Stop-and-revert rule (repeated for emphasis):**
- If `make` prints any md5 other than `ed1378bc12115f71209a77844965ba50`,
**do not commit.** Run `ps-firstdiff` to find the first divergence,
undo the rename (`git restore --staged . && git checkout -- .` or
manually revert the straggler), rebuild, recheck md5, then commit.
- A commit with a wrong md5 contaminates the entire history. The
defense is one-line: **never commit without md5 `ed1378bc…` printed
by `make` in the same shell session.**
`make diff-init` only needs re-running when `expected/` becomes
stale (after a deliberate build-artifact change, **never after a
rename**). Renames do **not** change `expected/`.
**Never run a full `make NON_MATCHING=1`** (per `AGENTS.md`). Renames
are in matching code; `NON_MATCHING` is irrelevant.
---
## Commit message template
```
rename: <func_80029310> → <Game_HandleN64LogoIntro>
<one-line description of what it does>
Callers: Game_Thread (src/29BA0.c:798)
Callees: func_80005A20, func_80029500, …
Still matching: md5 ed1378bc… (verified by `make`)
```
The `Still matching: md5 ed1378bc…` line is **not optional metadata —
it is the commit-gate receipt.** If you cannot truthfully write that
line, you are not ready to commit (see "Hard precondition: md5 gate"
above).
For global renames, drop the Callees line. For struct fields, drop
both. For tool commits (Phase 0), use `tools:` prefix and skip the
matching-metadata lines — but **still include the md5 line**: tooling
commits must also leave the ROM matching.
---
## Pacing
Long campaign — hundreds of functions, thousands of struct fields.
Realistic cadence:
- 5–10 functions/week for obvious state handlers (Phase 2)
- 1–3 functions/week for tricky ones (need call-graph archaeology)
- **Pause to extend Rust tooling when the same question keeps coming
up** — e.g. if you find yourself grepping the map file 10× per
session, add a `--xref <symbol>` mode to `ps-status`.
Don't try to do all renames in one PR. The one-identifier-per-commit
structure keeps `git blame` and `git bisect` useful for years. A
single ill-named symbol that breaks md5 should be revertable in one
command.
**Every commit in this campaign must satisfy the md5 gate (see
"Hard precondition" above).** That is the campaign's only invariant.
If you ever find yourself rationalizing "this commit can skip the
md5 check because…" — stop, that is exactly how regressions sneak
into multi-month rename campaigns.
---
## Concrete first batch
After Phase 0 lands:
1. The 23 `Game_Thread` state handlers in `src/29BA0.c:796-870`,
starting at `func_80029310` (top of the `switch`) and walking
down. One commit per handler, easy to review.
2. Their direct helpers (`func_80000DF4`, `func_8002B310`).
3. The four Root 0 init helpers in `src/main.c` and `func_800196DC`.
That's ~30 commits for the first month. After that, Phase 3 subsystem
sub-phases.
---
## Cross-references
- `AGENTS.md` — matching-build rules, `NON_MATCHING` gotcha, per-file
Makefile overrides, `make` / `make diff-init` semantics
- `docs/c-first-plan.md` — the C-first campaign (write C for every
function); this rename plan runs in parallel and only touches
already-matching C
- `tools/rust/README.md` — Rust-toolkit parity status, build & test
commands, validation methodology