Files
disasm/Pac-Man (USA) (Namco)/OVERVIEW.md
T

467 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pac-Man (USA) (Namco) — Game Flow from Power-On
This document describes the execution flow of the disassembled Pac-Man (USA) (Namco) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `copilot/prg.s` in the repository. The rebuild is verified **byte-identical** to the original `prg.bin`.
---
## 1. ROM and Memory Layout
| Property | Value |
|----------|-------|
| Format | iNES / NES 2.0 |
| PRG | 1 × 16 KB (mapped at CPU $C000–$FFFF) |
| CHR | 1 × 8 KB |
| Mirroring | Vertical |
| PRG bank start | CPU $C000 |
| Source file | `copilot/prg.s` (14,332 lines) |
| Output | `reconstructed_prg.bin` (16,384 bytes) |
### Interrupt vectors (CPU $FFFA–$FFFF)
| Vector | Address | Handler |
|--------|---------|---------|
| NMI | $C10E | `NmiHandler` |
| RESET | $C033 | `Reset` |
| IRQ / BRK | $C17E | `IrqHandler` (just `rti` — unused) |
Additional ROM data at $FFF6–$FFF9:
- $FFF6/$FFF7 = $ECEC (little-endian), used by `DrawMazeNametable` as a source pointer ($00/$01)
- $FFF8/$FFF9 = $C10E (little-endian)
---
## 2. Build Status
`make` in `copilot/` assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `prg.bin`.
**Verification result: BYTE-IDENTICAL** — `reconstructed_prg.bin` matches `prg.bin` exactly (all 16,384 bytes), and `prg.bin` matches the PRG bank of `rom.nes` (skipping the 16-byte iNES header).
---
## 3. Reset — CPU $FFFC → $C033
**`Reset`** ($C033)
On power-on or reset:
1. `ldx #$00 / stx PPUCTRL` — disable PPU rendering/NMI.
2. `sei / cld` — disable maskable interrupts, clear decimal mode.
3. `dex / txs` — initialize stack pointer to $FF.
4. `nop x3` — three NOP padding bytes.
5. **Wait for two VBlanks** — `@WaitForPpu` loop: set PPUMASK=$06, poll PPUSTATUS bit 7 twice (X counts down from 2). This is the PPU warm-up delay.
6. **Clear low RAM** — `@ClearLowRam`: zero $0000–$003D.
7. **Clear RAM pages** — `@ClearRamPages`: using indirect pointer at $00/$01, clear pages starting at $0100 upward until high byte reaches $08 (effectively clearing $0100–$07FF).
8. `PPUMASK=$06`, scroll=$00 — enable background + sprites, reset scroll.
9. **Warm-start signature check** — `@CheckRamSignature`: compare RAM at $0052 against the 15-byte string `"HIROKI AOYAGI"` plus `$AA,$55` (stored at `RamSignature`, $C354–$C366):
- If all 15 bytes match → warm start: skip full RAM clear, go to `@SignatureAlreadyPresent`.
- If any byte differs → cold start: `@InitializeRamSignature` clears all of zero page, then copies the signature string to $0052–$0060.
10. Set $64=$01 (some flag), set gameState=$00.
11. Set $45=$07, then check cocktail/active-player flags at $46/$47. If both nonzero, perform a 16-byte swap loop (`@SwapLoop`) swapping bytes between $67,y and $77,y for Y=0..15.
12. `APUCTRL=$1F`, `APUFRAMECNT=$C0` — enable all APU channels, set frame counter mode.
13. `jsr InitializeAudioEngine` ($E1F8 region) — clear audio request/channel state.
14. `jsr $EEB4` — audio/sound engine init.
15. Set PPUCTRL=$88, `ppuCtrlShadow`=$88 — enable NMI, $2000 nametable base.
16. Set $48=$FF (gameplay suspended flag), clear $023F and $024B (PPU update buffers).
17. Set $36=$01 (some state flag).
18. `jmp MainEntry` — transfer control to the main program.
**`RamSignature`** ($C354): `.byte "HIROKI AOYAGI", $AA, $55` — developer credit / warm-start validation signature.
---
## 4. NMI / Frame Heartbeat — `NmiHandler` ($C10E)
The NMI fires once per frame and is the central real-time driver:
1. **Save registers** (A, X, Y on stack).
2. **PPUMASK=$1E** — enable background + sprites with specific emphasis settings.
3. **OAM DMA**: write $00 to OAMADDR ($2003), $00 to $40, then `lda $45 / sta OAMDMA` ($4014) — DMA from page $45 (i.e., $4500–$45FF) to sprite RAM.
4. **`jsr FlushGameplayPpuUpdates`** ($DE3B region) — flush pending PPU gameplay update blocks at $023F and $0245.
5. **`jsr ReadNearbyNametableTiles`** — read nametable tile data around Pac-Man (likely for collision/tunnel logic).
6. **`jsr UpdatePalettes`** — upload palette data from RAM buffers $0299 and $02A9 to PPU palette $3F00/$3F10 when those buffers are marked active ($0F marker).
7. **Restore scroll**: write $41 and $42 to PPUSCROLL (two writes — horizontal then vertical).
8. **Construct PPUCTRL**: `lda $47 / and $46 / and #$01 / asl / ora $43` — builds the PPUCTRL value from shadow/flag bits, writes to PPUCTRL.
9. **Controller strobe**: write $01 then $00 to JOY1 ($4016).
10. **Read both controllers** — 8 iterations each:
- Controller 1: read JOY1, mask with #$03, compare with #$01, rotate into $4D (`currentInput`).
- Controller 2: read `APUFRAMECNT` ($4017), mask with #$03, compare with #$01, rotate into $4E.
11. **Select active controller**: X = `currentInput` ($4D) by default; if `cocktailModeFlag` ($46) & `activePlayerFlag` ($47) both nonzero, X = `newlyPressedInput` ($4E). Store X to `previousInput` ($4F).
12. **`inc frameCounter`** ($4B) — increment per-frame counter.
13. **$48 gate**: if $48 (`gameplaySuspended`) != 0, skip to `@NmiDone`.
14. **`jsr InitializeAudioChannels`** — scan and activate audio channel slots.
15. **`@NmiDone`**: restore registers (Y, X, A), `rti`.
The label `IrqHandler` appears at the `rti` following NMI's register restore — it is the same code address, just an alternate entry label for the IRQ vector (which points to the same `rti`).
### `UpdatePalettes`
Conditionally uploads 16-byte palette blocks from RAM $0299 (to PPU $3F00) and $02A9 (to PPU $3F10) when the corresponding buffer marker byte equals $0F. After upload, resets the marker and PPU address to $3000.
---
## 5. Main Entry — `MainEntry` ($C573)
After Reset, control transfers to `MainEntry`. This is the foreground state-machine entry:
1. **`$40=$01`** — set the NMI sync flag (NMI will clear it each frame).
2. **`@WaitForInitialNmi`** — spin until NMI clears $40 to $00 (synchronize to first VBlank).
3. **PPUCTRL=$88 / ppuCtrlShadow=$88** — enable NMI.
4. **PPUMASK=$00** — disable rendering.
5. **Clear OAM page** — fill $0700–$07FF with 0.
6. `cocktailModeFlag=$00`, then call:
- `jsr ClearGameplayNametables`
- `jsr InitializeNametableAttributes`
- `jsr InitializeNametableData`
- `jsr $E419` — additional init (address-named).
7. **Initialize palette buffers** — copy 32 bytes from `TitleScreenPalette` to RAM $0299–$02B8.
8. Set `actorStatusDisplay` ($6C–$6F) = $2D, set $023F=$FF, `gameplaySuspended`=$FF.
9. Clear $47, $46, $42, $41, `stateTimer` ($4C), $87, $88.
10. **PPUCTRL selection**: if `gameState` ($3F) != 0, use $8A; else use $8A as well (both paths end up with $8A; the `bne` is effectively a no-op in current code). Write to PPUCTRL and `ppuCtrlShadow`.
11. **`MainDispatchLoop`** ($C645):
- `$40=$01`, spin until NMI clears it (`@WaitForNmi`).
- `Y = gameState` ($3F), load 16-bit handler address from `MainStateHandlerTable` ($C267) into $10/$11.
- `jmp ($0010)` — indirect jump to the state handler.
**`MainStateHandlerTable`** ($C267–$C26E) — 4 entries, indexed by `gameState` as a byte offset (even values $00, $02, $04, $06):
| Offset | Handler | Description |
|--------|---------|-------------|
| $00 | `InitialStateHandler` | Title/input-wait state |
| $02 | `PlayerSelectionState` | 1P/2P selection |
| $04 | `State04Handler` | Substate dispatch machine |
| $06 | `TimedStateTransition` | Timed state transition |
---
## 6. Main State Handlers
### `TimedStateTransition` ($C27A) — state offset $06
- Decrement `$0F` countdown. If nonzero, return to `MainDispatchLoop`.
- When zero: set `gameState`=$02, return to `MainDispatchLoop`.
- This is a timed transitional state: another state places a duration in `$0F` and selects state $06; this handler counts down before advancing to state $02.
### `InitialStateHandler` ($C27A region, actually at $C27A? No — at $C27A is TimedStateTransition; InitialStateHandler is at $C27A? Let me re-check)
Actually from the listing:
- `TimedStateTransition` is at $C27A
- `InitialStateHandler` is at $C27A? No. Let me re-read:
Line 730: `TimedStateTransition:` is at $C27A (based on comment "$C27A-$C2A5: Timed State Transition Handler" — wait, the comment says $C27A but the code shows `dec $0F` at line 731. Then line 740: `; $C27A-$C2A5: Initial/Input State Handler` and line 766: `InitialStateHandler:`.
So there's a discrepancy in comments. The actual code:
- `TimedStateTransition` = `dec $0F / bne MainDispatchLoop / lda #$02 / sta gameState / jmp MainDispatchLoop` — this is state $06's handler.
- `InitialStateHandler` = checks controller bits $0C; if set → gameState=$06, $0F=$5A, jump MainEntry. Else increment $42; if $42 reaches $F0 → reset $42, ppuCtrlShadow=$88, gameState=$06, $0F=$5A. Otherwise return to MainDispatchLoop.
### `PlayerSelectionState` ($C546 region) — state offset $02
- If $87|$88 != 0 → `@AdvanceTimeout` (16-bit timeout counter).
- Else (first entry): copy `PlayerCountSelectionDisplayData` ($C507–$C50E) to RAM $024B until $FF terminator. Capture controller bits $04 and $08 into `pacmanRequestedDirection` ($50) and `pacmanDirection` ($51).
- `@AdvanceTimeout`: increment 16-bit counter at $87/$88. If high byte reaches $02 → reset counter, set gameState=$04, jump MainDispatchLoop.
- `@CheckSelectionInput`: if controller bit $04 changed → store as `pacmanRequestedDirection`, reset counter, set $87=$01, toggle `activePlayerFlag` ($47) between 0/1, update display marker tiles at $024D/$0251 from `PlayerCountSelectionMarkerTiles` ($C50F–$C511).
- `@CheckConfirmInput`: if controller bit $08 changed → store as `pacmanDirection`. If bit $08 now active → clear `gameplaySuspended`, `jsr $EEB4`, jump `GameModeEntry`.
**`PlayerCountSelectionDisplayData`** ($C507): `$22,$0A,$5C,$00, $22,$4A,$20,$FF` — copied to $024B on entry.
**`PlayerCountSelectionMarkerTiles`** ($C50F): `$5C,$20,$5C` — selection marker toggle data.
### `State04Handler` ($C52E region) — state offset $04
- If controller bits $0C set → gameState=$02, jump MainEntry (restart).
- Else: `Y = $87` (substate offset), load 16-bit handler from `State04SubstateTable` ($C52E), jump indirect via $10/$11.
**`State04SubstateTable`** ($C52E) — 12 entries indexed by even offsets $00–$16:
| Offset | Handler |
|--------|---------|
| $00 | `State04Initialize` |
| $02–$12 | `State04TimedSubstate` (shared, 6 entries) |
| $14 | `State04LongDelay` |
| $16 | `State04FinalPhase` |
- `State04Initialize` ($C573): sync to NMI via $40, PPUCTRL=$08, disable rendering, clear $88, call `$C5D8`, `InitializeState04PpuData` ($C608), `LoadState04DisplayData` ($C5A6), advance $87 by 2, PPUCTRL=$88, return to loop.
- `State04TimedSubstate` ($C589): increment $88; at $30 frames → clear $88, call `$C5A6`, advance $87 by 2.
- `State04LongDelay` ($C5A6): increment $88; at $80 frames → advance $87 by 2 to substate $16.
- `State04FinalPhase` ($C5D7): sets $47=$01, tail-jumps to `GameModeEntry`.
- `SetSelectionFlagAndContinue` ($C5D7): `activePlayerFlag=$01`, `jmp GameModeEntry`.
### `LoadState04DisplayData` ($C5A6)
Uses $87 as offset into `CharacterIntroDisplayDataTable` ($C687), copies $FF-terminated stream to RAM $024B. Optional secondary copy of 16 bytes from `CharacterIntroSpriteData` based on substate offset.
**`CharacterIntroDisplayDataTable`** ($C687) — 10 pointers to display streams for characters/nicknames (SHADOW, BLINKY, SPEEDY, PINKY, etc.) plus scoring legend.
---
## 7. Game Mode Entry — `GameModeEntry` ($C328)
Reached from `SetSelectionFlagAndContinue` (state $04 final phase). This transitions from the attract/selection sequence into gameplay:
1. PPUCTRL=$88, ppuCtrlShadow=$88.
2. Wait for VBlank.
3. PPUMASK=$00, gameState=$00, cocktailModeFlag=$00, $69=$00, $4A=$00.
4. `jsr ClearGameplayNametables`, `jsr DrawGameplayHudLabels`, `jsr DrawGameplayScoreFields`.
5. If `gameplaySuspended` != 0 → roundNumber=$01 (continue existing game).
6. Else → clear 32 bytes starting at roundNumber ($67–$86), set roundNumber=$03, $77=$03, clear $0600/$0601.
7. Common init: roundDisplayValue=$FF, $78=$FF, $0245=$FF, ppuCtrlShadow=$88, PPUCTRL=$88.
8. **`GameModeFrameSync`** ($C360): `$40=$01`, wait for NMI clear ($@WaitForNmi`), then:
- If `gameplaySuspended` != 0 AND controller bits $0C set → gameState=$02, jump MainEntry (restart).
- If `stateTimer` ($4C) != 0 → decrement, return to `@WaitForNextFrame`.
- Else `@DispatchState`: `Y = gameState`, load handler from `GameModeStateTable` ($CAD7) into $10/$11, indirect jump.
**`GameModeStateTable`** ($CAD7) — 8 entries indexed by even offsets $00–$0E:
| Offset | Handler |
|--------|---------|
| $00 | `GameModeState00` |
| $02 | `GameModeState02` |
| $04 | `GameModeState04` |
| $06 | `GameModeState06` |
| $08 | `GameModeState08` |
| $0A | `GameModeState0A` |
| $0C | `GameModeState0C` |
| $0E | `InitializeGameplayTransitionState` |
### `GameModeState04` ($CAE9) — pause/per-frame handler
- If $060F != 0 → return to `GameModeFrameSync`.
- Else: edge-detect controller bit $08 using $49 as previous state. On new press → increment $4A, install pause display commands at $024B, toggle pause display via bit 0 of $4A.
- When not paused and bit 0 of $4A clear → call gameplay helper chain before returning to `GameModeFrameSync`.
- Adjacent data at $CB5B contains tile values for "PAUSE" text — confirms this is the pause-state handler.
---
## 8. PPU / Rendering
### `DrawMazeNametable` ($E2E4)
Draws the maze nametable using compressed data:
- PPU destination selected based on cocktail mode ($47 & $46): $2000 or $2080.
- Source pointer from `MazeDataPointerBytes` at $FFF6–$FFF7 ($ECEC → points to ROM data).
- Each row: set PPU address, decode 2-bit-run-length-encoded bytes from the source (2 bits = repeat count-1, 6 bits = tile value), write $16 (22) bytes per row.
- Advance PPU destination by $20 per row.
### `FlushGameplayPpuUpdates` ($DE3B)
- If $48 (`gameplaySuspended`) == 0 → process update block at $023F (6 bytes + terminator to PPU).
- Else → jump to `UpdateAnimatedGameplayPpuFields`.
- Block at $0245 processed similarly if nonzero.
---
## 9. Audio System
**`InitializeAudioEngine`** ($E1F8):
- Clear audio request pointer ($F0/$F1 = $0006), channel table pointer ($F2/$F3 = $0620), sequence table pointer from ROM $F100/$F101.
- audioWorkValue=$40, APUSTATUS=$1F, APUFRAMECNT=$C0.
- Clear 16 bytes through ($F0), clear 16 entries at 8-byte stride through ($F2).
**`InitializeAudioChannels`** ($EED0):
- Clear audio channel flags and work values, scan 16 eight-byte slots through ($F2).
- Slots with values $01–$04 activate a channel: copy 4-byte parameters through indirect pointer $F6.
- Falls through to runtime command processing at $EF2D.
**`UpdateAudioChannels`** ($EF2D):
- For each active slot: decrement countdown, fetch command byte from sequence table, decode command ($F0+ = special, $C0+ = delay, else = note/frequency), update APU registers ($4000–$400F).
**`UpdateAudio`** ($F8DE):
- APUFRAMECNT=$C0, update Pulse 1, Pulse 2, Noise, Sound Effect channels.
- Clear $FF–$FB; if $07FF != 0 → APUDMALOAD, decrement $07FF.
Audio data tables at $FF00–$FFF5 (period/mask tables), $FF64–$FFEA (sequences/instruments), $F450–$F49A (runtime command data).
---
## 10. Zero-Page / RAM Symbols
| Address | Symbol | Use |
|---------|--------|-----|
| $00–$01 | workPtr0/1 | General indirect pointers |
| $02–$09 | workValue0–5 | General scratch values |
| $0A | pathCompareValue | Maze pathfinding |
| $0B | pathFallbackValue | Maze pathfinding |
| $0C | legalDirections | Maze pathfinding |
| $10 | dispatchVector | State dispatch indirect pointer |
| $12–$15 | arithmeticValue/Operand | 16-bit arithmetic |
| $1A–$2D | pacmanMotion–ghost4Motion | Actor motion records (4 bytes each) |
| $2E–$2F | pacmanTargetRow/Column | Pac-Man maze target |
| $32 | pacmanSpriteFrame | Pac-Man sprite frame |
| $33 | ghostSpriteFrames | Ghost sprite frames |
| $39 | ghostDirectionFlags | Ghost direction/control state |
| $3F | gameState | Main gameplay state |
| $40 | — | NMI sync flag ($01=pending, NMI clears to $00) |
| $41 | ppuCtrlShadow | PPUCTRL mirror |
| $42 | — | Second PPUSCROLL write |
| $46 | cocktailModeFlag | Cocktail cabinet mode |
| $47 | activePlayerFlag | Active player (0/1) |
| $48 | gameplaySuspended | Gameplay suspended flag |
| $4B | frameCounter | Per-frame counter |
| $4C | stateTimer | State countdown timer |
| $4D | currentInput | Controller 1 input |
| $4E | newlyPressedInput | Newly pressed input |
| $4F | previousInput | Previous input state |
| $50 | pacmanRequestedDirection | Requested direction |
| $51 | pacmanDirection | Current direction |
| $61 | highScoreDigits | High score digits |
| $67 | roundNumber | Round number |
| $68 | roundDisplayValue | Round display value |
| $6A | pelletsRemaining | Pellets remaining |
| $6B | extraLifeAwarded | Extra life flag |
| $6C–$6F | actorStatusDisplay | Actor status display |
| $70 | playerScoreDigits | Player score digits |
| $F0–$F7 | audioRequestPtr…audioWorkValue | Audio engine pointers/values |
| $F8 | audioChannelFlags | Audio channel flags |
| $FC | audioRequestIndex | Audio request index |
| $FD | audioChannelOffset | Audio channel offset |
| $FE–$FF | audioIndirectPtr | Audio indirect pointer |
---
## 11. Data Tables
| Label | Address | Description |
|-------|---------|-------------|
| `CopyrightText` | $C004 | "COPYRIGHT 1980 1993 NAMCO LTD. ALL RIGHTS RESERVED" |
| `RamSignature` | $C354 | `"HIROKI AOYAGI" $AA $55` — warm-start signature |
| `MainStateHandlerTable` | $C267 | 4 main state handler pointers |
| `State04SubstateTable` | $C52E | 12 state-$04 substate handler pointers |
| `GameModeStateTable` | $CAD7 | 8 game-mode state handler pointers |
| `CharacterIntroDisplayDataTable` | $C687 | 10 display data pointers for character intro |
| `TitleScreenTileBlock` | $C326 | 6 rows × $17 bytes of title tile data |
| `TitleScreenPpuRecords` | $C3B0 | 9 PPU update records for title screen |
| `TitleScreenPalette` | $C43F | 32-byte initial palette |
| `TitleScreenAttributeData` | $C45F | $18 attribute bytes |
| `PlayerCountSelectionDisplayData` | $C507 | 8 bytes for player-count display |
| `PlayerCountSelectionMarkerTiles` | $C50F | $5C,$20,$5C marker toggle |
| `CharacterIntroPalette` | $C667 | 32-byte character intro palette |
| `CharacterIntroDisplayDataTable` | $C687 | Character intro display pointers |
| `CharacterNicknameHeaderData` | $C69B | "CHARACTER ; NICKNAME" display data |
| `MazeDataPointerBytes` | $FFF6 | $EC,$EC,$0E,$C1 — maze data pointer ($C10E) |
| Audio tables | $FF00–$FFEA | Period/mask tables, sequences, instruments |
---
## 12. Summary of Execution Flow
```
Power On / Reset
│
▼
Reset ($C033) ───────────────────────────────────────────────┐
│ sei, cld, disable PPU, wait 2× VBlank │
│ clear RAM $0000–$003D, clear pages $0100–$07FF │
│ PPUMASK=$06, scroll=$00 │
│ check warm-start signature "HIROKI AOYAGI" + $AA,$55 │
│ valid → skip full clear; invalid → clear + save sig │
│ set flags, optional 16-byte swap ($67↔$77) │
│ APUCTRL=$1F, APUFRAMECNT=$C0 │
│ jsr InitializeAudioEngine, jsr $EEB4 │
│ PPUCTRL=$88 (NMI enabled), $48=$FF (suspended) │
│ jmp MainEntry │
│ │
MainEntry ($C573) │
│ $40=$1, wait for NMI to clear it │
│ PPUCTRL=$88, PPUMASK=$00, clear OAM │
│ clear cocktail/player flags, $41/$42/$4C/$87/$88 │
│ init nametables, attributes, palette buffers │
│ select PPUCTRL ($8A), set ppuCtrlShadow │
│ │
MainDispatchLoop ($C645) ─────────────────────────────────────┤
│ $40=$1, wait for NMI │
│ Y = gameState ($3F) │
│ load handler from MainStateHandlerTable ($C267) │
│ jmp ($0010) → state handler │
│ │
├─ State $00: InitialStateHandler │
│ ├─ controller bits $0C → gameState=$06, $0F=$5A │
│ │ → jump MainEntry (restart sync) │
│ └─ else inc $42; if $42==$F0 → reset, gameState=$06 │
│ │
├─ State $02: PlayerSelectionState │
│ ├─ 1P/2P selection via controller bits $04/$08 │
│ ├─ timeout ($87/$88 reaches $0200) → gameState=$04 │
│ └─ confirm (bit $08) → clear suspended, jsr $EEB4, │
│ jmp GameModeEntry │
│ │
├─ State $04: State04Handler │
│ ├─ controller bits $0C → gameState=$02, restart │
│ └─ Y=$87 → dispatch via State04SubstateTable ($C52E) │
│ ├─ $00: State04Initialize (PPU setup, $87+=2) │
│ ├─ $02–$12: State04TimedSubstate ($30 frames) │
│ ├─ $14: State04LongDelay ($80 frames) │
│ └─ $16: State04FinalPhase → $47=$1, │
│ jmp GameModeEntry │
│ │
└─ State $06: TimedStateTransition │
│ dec $0F; if nonzero → MainDispatchLoop │
│ else gameState=$02 → MainDispatchLoop │
│ │
└─ loop back to MainDispatchLoop │
│ │
GameModeEntry ($C328) [from SetSelectionFlagAndContinue] │
│ │
├─ PPUCTRL=$88, wait VBlank, PPUMASK=$00 │
├─ clear gameplay state, HUD labels, score fields │
├─ if not suspended: clear player state, roundNumber=$3 │
│ else: roundNumber=$1 │
├─ common init: roundDisplayValue=$FF, PPUCTRL=$88 │
│ │
GameModeFrameSync ($C360) ────────────────────────────────────┤
│ $40=$1, wait for NMI │
│ │
├─ if suspended && controller bits $0C → gameState=$02, │
│ jmp MainEntry (restart) │
│ │
├─ if stateTimer != 0 → dec, return to @WaitForNextFrame │
│ │
└─ Y = gameState → dispatch via GameModeStateTable ($CAD7)│
│ │
├─ $00: GameModeState00 │
├─ $02: GameModeState02 │
├─ $04: GameModeState04 (pause/input handler) │
│ ├─ $060F != 0 → return to frame sync │
│ ├─ edge-detect controller bit $08 ($49) │
│ │ → new press: inc $4A, install pause cmds │
│ │ → bit 0 of $4A toggles pause display │
│ └─ not paused, bit0 of $4A clear → gameplay │
│ helper chain → return to frame sync │
├─ $06–$0C: GameModeState06/08/0A/0C │
└─ $0E: InitializeGameplayTransitionState │
│ │
└─ loop back to GameModeFrameSync │
│ │
└─ (eventually enters gameplay via $0E handler) │
│
NMI ($C10E) [once per frame] │
│ │
├─ save A, X, Y │
├─ PPUMASK=$1E │
├─ OAM DMA ($4500–$45FF → $0200–$02FF) │
├─ jsr FlushGameplayPpuUpdates ($DE3B) │
├─ jsr ReadNearbyNametableTiles │
├─ jsr UpdatePalettes ($3F00/$3F10 from $0299/$02A9) │
├─ restore scroll ($41,$42 → PPUSCROLL) │
├─ construct PPUCTRL from $47,$46,$43 │
├─ strobe JOY1, read both controllers (8 bits each) │
│ → $4D (current), $4E (newly pressed) │
├─ select active controller → $4F (previousInput) │
├─ inc frameCounter ($4B) │
├─ if $48==0: jsr InitializeAudioChannels │
├─ restore registers, rti │
│ │
IrqHandler ($C17E): rti (unused) │
│
Audio Engine (from NMI via InitializeAudioChannels/$EEB4) │
├─ 16 eight-byte channel slots scanned │
│ │ values $01–$04 activate a channel │
│ │ 4-byte parameters copied via $F6 pointer │
├─ UpdateAudioChannels ($EF2D): │
│ │ decrement countdowns, fetch command bytes, │
│ │ decode ($F0+=special, $C0+=delay, else=note), │
│ │ write APU registers $4000–$400F │
└─ UpdateAudio ($F8DE): 4 channels + SFX + DMA trigger │
```
---
## 13. Notes
- **Build verification:** `make` in `copilot/` produces `reconstructed_prg.bin` that is **byte-identical** to `prg.bin` and to the PRG bank of `rom.nes`.
- Vectors confirm NMI at $C10E, RESET at $C033, IRQ at $C17E (a bare `rti`, unused).
- The foreground main loop (`MainDispatchLoop` at $C645) is strictly NMI-synchronized via `$40`: NMI clears it each frame, and the main code spins waiting for it to clear before dispatching to the next state handler.
- `GameModeEntry` ($C328) is a second, separate state machine entered from the state-$04 final phase. It has its own frame sync (`$40`) and its own 8-state dispatch table (`GameModeStateTable` at $CAD7), leading into gameplay initialization.
- State $04 contains a nested secondary state machine (`State04SubstateTable` at $C52E) with 12 substate offsets, implementing a timed character-introduction sequence (GHOST names, nicknames, scoring legend).
- The warm-start signature `"HIROKI AOYAGI"` at $0052 allows the game to preserve RAM across a soft reset.
- Many routine labels are address-based (`State04TimedSubstate`, `LoadState04DisplayData`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional.
- `symbols.inc` (embedded at the top of `prg.s`) provides NES hardware register names, controller masks, and extensive zero-page symbol definitions.