467 lines
28 KiB
Markdown
467 lines
28 KiB
Markdown
# 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. |