Field ports batch 1: timers, scroll, object frame and map position, party input #35

Merged
Gatchan merged 9 commits from field-batch1 into main 2026-11-03 07:27:19 +00:00
Owner

Fixes #32

First field ports: six WRAM-only leaves from the field porting order, game-terms C in core/game/field/, bridges in core/port/field_bridge.c, US and JP registry entries.

Ports

Shadow over the three committed routes (title_to_first_battle_win + field_menu_tour + narshe_to_world_map), matched/calls, 0 mismatch and 0 fallback everywhere (the rest are calls skipped by a frame boundary or nesting):

Port C function Shadow US Shadow JP Fuzz US / JP Instructions of the original reached
field/DecTimers ($C0:BBCC, JP $C0:BB01) field_count_down_timers 51 746/51 761 49 213/49 220 3 742/3 742 both 17/17
field/DecTimersMenuBattle ($C0:BB24, JP $C0:BA59) field_count_down_timers_outside_field 41 426/41 426 44 227/44 227 274/274 both 80/80
field/CalcScrollPos ($C0:2BD0) field_scroll_layers 48 755/51 696 46 147/49 153 3 313/3 742, 3 308/3 742 261/261
field/GetObjMapPtr ($C0:7CE1) field_object_update_map_position 52 466/52 467 49 566/49 568 870/870 both 20/20
field/UpdateCtrlFlags ($C0:BA81, JP $C0:B9B6) field_publish_party_input 31 635/32 438 27 192/27 924 5 298/5 299 both 17/17
field/UpdateObjFrame ($C0:5839) field_object_update_frame 401 521/403 033 390 965/392 518 25 056/25 416, 25 052/25 416 130/130

Per-route counts: docs/ports.md. The routes never run a timer, a special animation, a vehicle or stairs, so targeted fuzz from a field savestate (drive server, ports in shadow: random timers, scroll speeds/positions, object animation/vehicle/facing, tile property tables, frame counters, walking/A/menu inputs; plus a stairs session poking the tile properties every frame) covers the rest: with routes + fuzz every instruction of the six originals is executed on both versions, 0 mismatch.

Every register and flag is reproduced exactly (no don't-care declarations): the bridges compute the leftovers (X, A, C, V, N/Z) path by path and write the DP scratch only the original writes ($1A, $1B, $1E, new RAM-map entries). Entry states not supported fall back to the interpreter (D != 0, DB not $00/$7E, 8-bit indexes where the original needs 16-bit, B != 0 where a TAX would read it, offsets not at an object record, facing > 3).

Findings

  • DecTimersMenuBattle pushes and pulls every register yet has outputs: entered with 8-bit A it leaves B = high byte of D (its TDC; PLA restores only the low byte), and N/Z come from the final PLA, not from the saved P. Shadow caught the first (517 menu calls), fuzz the second. Not an original bug (all callers clear or restore A afterwards). Recorded in workdocs/lessons.md §10.
  • GetObjMapPtr runs with DB = $7E during map loading; bridges accept DB $00/$7E for these low-RAM routines.
  • No original game bug met in these routines (re/known-bugs.toml not created).

route_ports_on (all ports on, --semantic)

  • title_to_first_battle_win: US ok, JP FAILED (party HP after the battle).
  • field_menu_tour: US FAILED (party HP), JP stuck at frame 10156.
  • narshe_to_world_map: US stuck at the Marshal (lost 3 times, frame 75223), JP stuck at frame 10156.

These are timing effects, not port logic, opened as #34 (bug, tools, validation): with one port on at a time on JP title, every port moves the first battle by -7..+13 frames (even DecTimers, 20 instructions per frame), so the battle seed and the party HP change; and after: routes replay the previous movie frame by frame under ports on. Decision needed there; I did not weaken the checks.

Dropped

  • CheckTimer: its firing path calls CloseMapTitleWindow, which restores the field HDMA tables (hardware buffer) -> after the video service (#33).
  • _c0714a (object sort): writes through the WRAM port $2180-$2183 (hardware register, shared counter to model in hw/).

Build / tests

release, debug, clang builds without warnings; ff6-build.sh debug --fast 24/24 passed (before the last bridge fixes; ports are off by default so the fast tests do not exercise them). Slow suite not run.

🤖 Generated with Claude Code

Fixes #32 First field ports: six WRAM-only leaves from the field porting order, game-terms C in `core/game/field/`, bridges in `core/port/field_bridge.c`, US and JP registry entries. ## Ports Shadow over the three committed routes (title_to_first_battle_win + field_menu_tour + narshe_to_world_map), matched/calls, **0 mismatch and 0 fallback** everywhere (the rest are calls skipped by a frame boundary or nesting): | Port | C function | Shadow US | Shadow JP | Fuzz US / JP | Instructions of the original reached | |---|---|---|---|---|---| | `field/DecTimers` ($C0:BBCC, JP $C0:BB01) | `field_count_down_timers` | 51 746/51 761 | 49 213/49 220 | 3 742/3 742 both | 17/17 | | `field/DecTimersMenuBattle` ($C0:BB24, JP $C0:BA59) | `field_count_down_timers_outside_field` | 41 426/41 426 | 44 227/44 227 | 274/274 both | 80/80 | | `field/CalcScrollPos` ($C0:2BD0) | `field_scroll_layers` | 48 755/51 696 | 46 147/49 153 | 3 313/3 742, 3 308/3 742 | 261/261 | | `field/GetObjMapPtr` ($C0:7CE1) | `field_object_update_map_position` | 52 466/52 467 | 49 566/49 568 | 870/870 both | 20/20 | | `field/UpdateCtrlFlags` ($C0:BA81, JP $C0:B9B6) | `field_publish_party_input` | 31 635/32 438 | 27 192/27 924 | 5 298/5 299 both | 17/17 | | `field/UpdateObjFrame` ($C0:5839) | `field_object_update_frame` | 401 521/403 033 | 390 965/392 518 | 25 056/25 416, 25 052/25 416 | 130/130 | Per-route counts: `docs/ports.md`. The routes never run a timer, a special animation, a vehicle or stairs, so targeted fuzz from a field savestate (drive server, ports in shadow: random timers, scroll speeds/positions, object animation/vehicle/facing, tile property tables, frame counters, walking/A/menu inputs; plus a stairs session poking the tile properties every frame) covers the rest: with routes + fuzz every instruction of the six originals is executed on both versions, 0 mismatch. Every register and flag is reproduced exactly (no don't-care declarations): the bridges compute the leftovers (X, A, C, V, N/Z) path by path and write the DP scratch only the original writes (`$1A`, `$1B`, `$1E`, new RAM-map entries). Entry states not supported fall back to the interpreter (D != 0, DB not $00/$7E, 8-bit indexes where the original needs 16-bit, B != 0 where a `TAX` would read it, offsets not at an object record, facing > 3). ## Findings - `DecTimersMenuBattle` pushes and pulls every register yet has outputs: entered with 8-bit A it leaves B = high byte of D (its `TDC`; `PLA` restores only the low byte), and N/Z come from the final `PLA`, not from the saved P. Shadow caught the first (517 menu calls), fuzz the second. Not an original bug (all callers clear or restore A afterwards). Recorded in workdocs/lessons.md §10. - `GetObjMapPtr` runs with DB = $7E during map loading; bridges accept DB $00/$7E for these low-RAM routines. - No original game bug met in these routines (`re/known-bugs.toml` not created). ## route_ports_on (all ports on, `--semantic`) - title_to_first_battle_win: **US ok**, JP FAILED (party HP after the battle). - field_menu_tour: US FAILED (party HP), JP stuck at frame 10156. - narshe_to_world_map: US stuck at the Marshal (lost 3 times, frame 75223), JP stuck at frame 10156. These are timing effects, not port logic, opened as **#34** (bug, tools, validation): with one port on at a time on JP title, every port moves the first battle by -7..+13 frames (even `DecTimers`, 20 instructions per frame), so the battle seed and the party HP change; and `after:` routes replay the previous movie frame by frame under ports on. Decision needed there; I did not weaken the checks. ## Dropped - `CheckTimer`: its firing path calls `CloseMapTitleWindow`, which restores the field HDMA tables (hardware buffer) -> after the video service (#33). - `_c0714a` (object sort): writes through the WRAM port `$2180-$2183` (hardware register, shared counter to model in `hw/`). ## Build / tests release, debug, clang builds without warnings; `ff6-build.sh debug --fast` 24/24 passed (before the last bridge fixes; ports are off by default so the fast tests do not exercise them). Slow suite not run. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
battle_end_flags $1DD1, party_input_switches $1EB6, party_tile_props $B8,
scratch $1A/$1B/$1E left by UpdateObjFrame and CalcScrollPos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shadow over the three routes, 0 mismatch, no fallback:
DecTimers 51 746/51 761 US, 49 213/49 220 JP; CalcScrollPos 48 755/51 696 US,
46 147/49 153 JP; GetObjMapPtr 52 466/52 467 US, 49 566/49 568 JP;
UpdateCtrlFlags 31 635/32 438 US, 27 192/27 924 JP; UpdateObjFrame
401 521/403 033 US, 390 965/392 518 JP; DecTimersMenuBattle 41 426/41 426 US
(JP rerun in the PR). Targeted fuzz: 0 mismatch, every instruction of the six
originals reached on both versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Final code (head of field-batch1): ff6-build.sh debug --fast 24/24 passed.

Final code (head of field-batch1): `ff6-build.sh debug --fast` 24/24 passed.
Author
Owner

Updated from main (merge commit 7877fde: #34 route checks and #36 SRAM start points; the merged tree is main plus exactly the 19 files of this PR, so route_narshe_to_figaro and everything of #36 is kept), then docs/ports.md refreshed (c8c6638). Not merged: waiting for the independent readability review.

Evidence (release build unless noted)

Shadow, all ports, over the 5 routes (title_to_first_battle_win, title_to_mines_save, field_menu_tour, narshe_to_world_map, narshe_to_figaro; movies replayed, every replay matches its end hashes), totals matched/calls, 0 mismatch, 0 fallback on both versions:

Port US JP
battle/Rand 547/547 512/512
field/DecTimers 73 700/73 711 67 664/67 669
field/DecTimersMenuBattle 53 125/53 125 50 250/50 250
field/CalcScrollPos 70 244/73 612 64 102/67 574
field/GetObjMapPtr 103 414/103 417 92 893/92 897
field/UpdateCtrlFlags 52 881/54 407 46 660/48 210
field/UpdateObjFrame 662 842/665 986 597 121/600 208

(the rest are calls skipped by a frame boundary or nesting). Per route: docs/ports.md.

Fuzz (100 iterations seed 1 from the end of field_menu_tour, plus the stairs session), 0 mismatch, 0 fallback US and JP; routes + fuzz execute every instruction of the six originals on US and JP (17, 80, 261, 20, 17, 130).

ctest release, all labels (-j2), all field ports on in route_ports_on: 61/61 passed, 5 483 s wall. route_ports_on US/JP: title_to_first_battle_win ok/ok, title_to_mines_save ok/ok, field_menu_tour ok/ok, narshe_to_world_map ok/ok, narshe_to_figaro ok/ok (new definition: goal reached + timing-invariant checkpoints).

debug (gcc ASan/UBSan) ctest -LE slow: 24/24; clang ctest -LE slow: 24/24; 0 warnings in release, debug and clang. Slow tests are validated in release only (ASan emulator ~12x slower).

rom.ports (report, passes): with the field ports on, the first divergence is now at sites the census does not cover well: US frame 729, $0630 (write-only field CPU meter, E9, classified by PC as E5 because the site table has no E9 entry), JP frame 318, $00FE at $C2:FFB5 inside the multi-frame Decompress_ext loop -> FINDING. Opened as #45 (not a port bug: same PC/S at a frame end inside a long main-thread loop does not mean same progress).

Updated from `main` (merge commit 7877fde: #34 route checks and #36 SRAM start points; the merged tree is `main` plus exactly the 19 files of this PR, so `route_narshe_to_figaro` and everything of #36 is kept), then `docs/ports.md` refreshed (c8c6638). **Not merged: waiting for the independent readability review.** ## Evidence (release build unless noted) **Shadow, all ports, over the 5 routes** (title_to_first_battle_win, title_to_mines_save, field_menu_tour, narshe_to_world_map, narshe_to_figaro; movies replayed, every replay matches its end hashes), totals matched/calls, **0 mismatch, 0 fallback** on both versions: | Port | US | JP | |---|---|---| | battle/Rand | 547/547 | 512/512 | | field/DecTimers | 73 700/73 711 | 67 664/67 669 | | field/DecTimersMenuBattle | 53 125/53 125 | 50 250/50 250 | | field/CalcScrollPos | 70 244/73 612 | 64 102/67 574 | | field/GetObjMapPtr | 103 414/103 417 | 92 893/92 897 | | field/UpdateCtrlFlags | 52 881/54 407 | 46 660/48 210 | | field/UpdateObjFrame | 662 842/665 986 | 597 121/600 208 | (the rest are calls skipped by a frame boundary or nesting). Per route: `docs/ports.md`. **Fuzz** (100 iterations seed 1 from the end of field_menu_tour, plus the stairs session), 0 mismatch, 0 fallback US and JP; routes + fuzz execute **every instruction** of the six originals on US and JP (17, 80, 261, 20, 17, 130). **ctest release, all labels (-j2), all field ports on in `route_ports_on`: 61/61 passed**, 5 483 s wall. route_ports_on US/JP: title_to_first_battle_win ok/ok, title_to_mines_save ok/ok, field_menu_tour ok/ok, narshe_to_world_map ok/ok, narshe_to_figaro ok/ok (new definition: goal reached + timing-invariant checkpoints). **debug (gcc ASan/UBSan) `ctest -LE slow`: 24/24; clang `ctest -LE slow`: 24/24**; 0 warnings in release, debug and clang. Slow tests are validated in release only (ASan emulator ~12x slower). **rom.ports** (report, passes): with the field ports on, the first divergence is now at sites the census does not cover well: US frame 729, `$0630` (write-only field CPU meter, E9, classified by PC as E5 because the site table has no E9 entry), JP frame 318, `$00FE` at `$C2:FFB5` inside the multi-frame `Decompress_ext` loop -> `FINDING`. Opened as #45 (not a port bug: same PC/S at a frame end inside a long main-thread loop does not mean same progress).
Named indexes of the party's tile property bytes; the special animation step is folded into
field_object_special_anim_offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Readability review, must-fix items applied (b527aff, c839ef0, c8868cf):

  1. Object, sprite, animation and tile-property definitions live once in field_objects.h (FIELD_SPRITE_*, FIELD_ANIM_*, FIELD_TILE_*, FIELD_WALK_CYCLE_STEPS); the bridge's OBJ_* copies and its hard-coded * 4U are gone.
  2. field_party_slot(g) (returns FIELD_OBJECT_COUNT when the party pointer is not at a record start, so the ladder test stays exact) is used by field_object_shown_facing and field_publish_party_input; record offsets stay in the accessor/bridge layer.
  3. Helpers split out of a routine carry /* SNES: part of $C0:5839 UpdateObjFrame (field/obj.asm) */ (and field_scroll_speed: part of $C0:2BD0 CalcScrollPos); , JP $C0:xxxx only when the JP address differs (removed from GetObjMapPtr, UpdateObjFrame, CalcScrollPos). CLAUDE.md rule 4 states both conventions.
  4. Stairs step comment says the VBlank counter (lda $46, field/obj.asm UpdateObjFrame @5897). Tile property bytes have named indexes FIELD_TILE_PROPS_MOVEMENT (0) and FIELD_TILE_PROPS_LADDER_BYTE (1).
  5. field_facing_t enum; field_object_shown_facing returns it.

Also: field_object_special_anim_tick folded into field_object_special_anim_offset; prv_count_down shared by both timer routines; CalcScrollPos sets the tile center only on a crossing; 16-bit wrap comment on field_scroll_speed; bridge comment reflowed. Left as is: PAD_A_BUTTON and BATTLE_END_* (no shared pad or battle-flags header exists yet).

Validation (release build, c8868cf):

  • Shadow, all ports, 5 routes x US/JP: 0 mismatch, 0 fallback, movie match on all 10; every port line (calls, match, skipped) identical to the previous run (docs/ports.md numbers unchanged).
  • Fuzz (fuzz.py 100 iterations seed 1 + stairs session) US/JP: 0 mismatch, counts identical to the previous run (e.g. UpdateObjFrame 51 240 calls, 50 548/50 553 matched).
  • release ctest (all labels, field ports as in the branch): 61/61 (route_ports_on 10/10), 10 550 s.
  • debug and clang ctest -LE slow: 24/24 each.
  • Clean builds release, debug, clang: 0 warnings; clang-format 20 clean.
Readability review, must-fix items applied (b527aff, c839ef0, c8868cf): 1. Object, sprite, animation and tile-property definitions live once in `field_objects.h` (`FIELD_SPRITE_*`, `FIELD_ANIM_*`, `FIELD_TILE_*`, `FIELD_WALK_CYCLE_STEPS`); the bridge's `OBJ_*` copies and its hard-coded `* 4U` are gone. 2. `field_party_slot(g)` (returns `FIELD_OBJECT_COUNT` when the party pointer is not at a record start, so the ladder test stays exact) is used by `field_object_shown_facing` and `field_publish_party_input`; record offsets stay in the accessor/bridge layer. 3. Helpers split out of a routine carry `/* SNES: part of $C0:5839 UpdateObjFrame (field/obj.asm) */` (and `field_scroll_speed`: part of `$C0:2BD0 CalcScrollPos`); `, JP $C0:xxxx` only when the JP address differs (removed from GetObjMapPtr, UpdateObjFrame, CalcScrollPos). CLAUDE.md rule 4 states both conventions. 4. Stairs step comment says the VBlank counter (`lda $46`, `field/obj.asm` UpdateObjFrame @5897). Tile property bytes have named indexes `FIELD_TILE_PROPS_MOVEMENT` (0) and `FIELD_TILE_PROPS_LADDER_BYTE` (1). 5. `field_facing_t` enum; `field_object_shown_facing` returns it. Also: `field_object_special_anim_tick` folded into `field_object_special_anim_offset`; `prv_count_down` shared by both timer routines; CalcScrollPos sets the tile center only on a crossing; 16-bit wrap comment on `field_scroll_speed`; bridge comment reflowed. Left as is: `PAD_A_BUTTON` and `BATTLE_END_*` (no shared pad or battle-flags header exists yet). Validation (release build, c8868cf): - Shadow, all ports, 5 routes x US/JP: 0 mismatch, 0 fallback, movie match on all 10; every `port` line (calls, match, skipped) identical to the previous run (docs/ports.md numbers unchanged). - Fuzz (`fuzz.py` 100 iterations seed 1 + stairs session) US/JP: 0 mismatch, counts identical to the previous run (e.g. UpdateObjFrame 51 240 calls, 50 548/50 553 matched). - release ctest (all labels, field ports as in the branch): 61/61 (route_ports_on 10/10), 10 550 s. - debug and clang `ctest -LE slow`: 24/24 each. - Clean builds release, debug, clang: 0 warnings; clang-format 20 clean.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Gatchan/FF6Portage!35
No description provided.