364 lines
17 KiB
Markdown
364 lines
17 KiB
Markdown
# Burger Time (U) — Game Flow from Power-On
|
||
|
||
This document describes the execution flow of the disassembled Burger Time (U) 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 `burger_time.nes`.
|
||
|
||
---
|
||
|
||
## 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 | Horizontal |
|
||
| PRG bank start | CPU $C000 |
|
||
| Source files | `copilot/prg.s` (9,323 lines), `copilot/header.s`, `copilot/chr.s` |
|
||
| Output | `burger_time_rebuilt.nes` (24,592 bytes) |
|
||
|
||
### Interrupt vectors (CPU $FFFA–$FFFF)
|
||
|
||
| Vector | Address | Handler |
|
||
|--------|---------|---------|
|
||
| NMI | $FF7F | `NmiHandler` |
|
||
| RESET | $FD24 | `ResetHandler` |
|
||
| IRQ / BRK | $FD24 | `ResetHandler` (reused) |
|
||
|
||
---
|
||
|
||
## 2. Build Status
|
||
|
||
`make` in `copilot/` assembles `header.s`, `prg.s`, and `chr.s`, links them via `ld65`, and verifies the result against `burger_time.nes`.
|
||
|
||
**Verification result: BYTE-IDENTICAL** — headers, CHR, and all PRG bytes match exactly.
|
||
|
||
Note: the Makefile references `burger_time.nes`, while the actual ROM file is named `Burger Time (U) [!].nes`. A symlink `burger_time.nes → "../Burger Time (U) [!].nes"` is required before building.
|
||
|
||
---
|
||
|
||
## 3. Reset — CPU $FFFC → $FD24
|
||
|
||
**`ResetHandler`** ($FD24)
|
||
|
||
On power-on or reset:
|
||
|
||
1. `sei` — disable maskable interrupts.
|
||
2. `cld` — clear decimal mode.
|
||
3. `lda #$00 / sta PPUCTRL / sta PPUMASK` — disable PPU rendering and NMI.
|
||
4. **Wait for two VBlanks** — `Flow_FD2E` and `Flow_FD33` each poll `PPUSTATUS` ($2002) for the VBlank bit.
|
||
5. `jsr SetDefaultScroll` ($FFEB) — set scroll to ($08, $00).
|
||
6. `FrameCounter` ($14) = $00; `FrameCounterReload` ($13) = $08.
|
||
7. `JOY2` ($4017) = $80 — select controller latch mode / disable IRQ.
|
||
8. Stack pointer = $FF.
|
||
9. `jsr InitializeRuntimeState` ($FD63) — clear NmiCompletedFlag ($0E), GameMode ($4C), PendingFrameTask ($8F), PlayerDeathFlag ($4F), StageCompleteFlag ($4E); set PPUCTRL shadow to $90; clear nametables; load initial palette; wait for NMI; set PPUMASK shadow to $1A.
|
||
10. `jsr InitializeGameSystems` ($C00E) — process game state at $C76E and $CB53; set up initial OAM positions ($0700=$70, $0701=$10); clear $32; call `SetMainState` to enter the first state.
|
||
11. Falls into **`Flow_FD4F`** ($FD4F) — the main loop.
|
||
|
||
---
|
||
|
||
## 4. Main Loop — State Machine at $FD4F
|
||
|
||
**`Flow_FD4F`** ($FD4F) is the central main loop. Every iteration:
|
||
|
||
1. `jsr WaitForNmi` ($FD9D) — block until NmiCompletedFlag ($0E) is set, then clear it.
|
||
2. Load `MainGameState` ($16), multiply by 2, use as index into a pointer table:
|
||
- Low byte from `MainStateDispatchTable` ($C000)
|
||
- High byte from `LookupTable_C001` ($C001)
|
||
- The two tables are interleaved in the same $C000–$C00D region.
|
||
3. Indirect jump (`jmp (DataPointerLo)`) to the state handler.
|
||
|
||
### State Dispatch Table (CPU $C000–$C00D)
|
||
|
||
The table is structured as alternating low/high bytes indexed by `MainGameState * 2`:
|
||
|
||
| State | CPU Addr | Handler Routine | Purpose |
|
||
|-------|----------|-----------------|---------|
|
||
| 0 | $C3F8 | `RunFrameSynchronizedTask_C3F8` | Initial / boot frame task |
|
||
| 1 | $C04E | `HandleTitleState` | Title screen |
|
||
| 2 | $C12F | `HandleGameStartState` | Game start / level init |
|
||
| 3 | $4DC2 | *(unmapped / bank-switched)* | *(state 3 entry)* |
|
||
| 4 | $C24D | `Flow_C24D` | Stage transition / between-levels |
|
||
| 5 | $C14F | `HandleGameplayState` | Main gameplay |
|
||
| 6 | $C1AA | `HandlePlayerDeathState` | Player death sequence |
|
||
| *(state 7+)* | *(from table)* | *(various)* | *(additional states)* |
|
||
|
||
`SetMainState` ($FF7C) stores the accumulator into `MainGameState` ($16) and returns — used by every state handler to transition to the next state.
|
||
|
||
---
|
||
|
||
## 5. NMI / Frame Heartbeat — `NmiHandler` ($FF7F)
|
||
|
||
The NMI fires once per frame and is the real-time driver:
|
||
|
||
1. **Save registers** (A, X, Y on stack).
|
||
2. **OAM DMA** — write $00 to OAMADDR ($2003), then $07 to OAMDMA ($4014) to DMA sprite data from $0700–$07FF.
|
||
3. Set `NmiCompletedFlag` ($0E) = $01 — signals the main loop that a frame has elapsed.
|
||
4. Read `PPUSTATUS` ($2002) to reset the address latch.
|
||
5. **Stage complete handling** — if `StageCompleteFlag` ($4E) is set, `jsr ProcessGameState_CEA3` ($CEA3), then clear the flag.
|
||
6. `jsr ProcessPendingFrameTask` ($C029) — if a frame task is pending, advance its counter.
|
||
7. If `GameMode` ($4C) is nonzero, `jsr ProcessGameState_F493` ($F493) — per-frame gameplay/audio update.
|
||
8. **Restore registers, `rti`.**
|
||
|
||
---
|
||
|
||
## 6. State Handlers
|
||
|
||
### 6.1 Title State — `HandleTitleState` ($C04E)
|
||
|
||
- Sets `GameMode` ($4C) = $00 and `Zp_57` ($57) = $00.
|
||
- `jsr ProcessGameState_C7F7` ($C7F7) — title-specific game state processing.
|
||
- `StateInitializedFlag` ($31) = $01.
|
||
- `jsr ProcessGameState_CB53` ($CB53) and `jsr ProcessGameState_C81B` ($C81B).
|
||
- `jsr EnableBackgroundAndSprites` ($C3EE) — enables rendering.
|
||
- `jmp Flow_C233` ($C233) — clears stage state, sets `MainGameState` = 4, returns to main loop.
|
||
|
||
### 6.2 Game Start State — `HandleGameStartState` ($C12F)
|
||
|
||
- `jsr ProcessGameState_C7F7` ($C7F7).
|
||
- `GameActiveFlag` ($4D) = $01; `StateInitializedFlag` ($31) = $01.
|
||
- `jsr ClearNametable2000` ($FDD0) — clear nametable at $2000.
|
||
- `jsr ProcessGameState_C81B` ($C81B) and `jsr ProcessGameState_C977` ($C977).
|
||
- Decrement `Ram_0400` ($0400).
|
||
- `jsr ClearNametable2000` again.
|
||
- Set `MainGameState` = 6 → gameplay state.
|
||
- Return to main loop via `Flow_FD4F`.
|
||
|
||
### 6.3 Gameplay State — `HandleGameplayState` ($C14F)
|
||
|
||
The core gameplay state. Checks `Zp_52` ($52):
|
||
- If `Zp_52` is nonzero → `Flow_C161` (second-player / alternate player path).
|
||
- If zero → `Flow_C153` (first player path).
|
||
|
||
Checks `PlayerDeathFlag` ($4F):
|
||
- Bit 0 = player 1 dead; bit 1 = player 2 dead.
|
||
- If a player is dead → `Flow_C19E` → sets `MainGameState` = 5 (player death state).
|
||
- Otherwise continues gameplay.
|
||
|
||
When transitioning to death (`Flow_C177`):
|
||
- Disable background/sprites, set state initialized, process game state, re-enable rendering.
|
||
- Store pending sound command $03 (death sound), set GameMode = $01.
|
||
- Run a frame-synchronized task at $CA80, store sound command $04.
|
||
- Jump to `Flow_C233` → `MainGameState` = 4 (stage transition).
|
||
|
||
### 6.4 Player Death State — `HandlePlayerDeathState` ($C1AA)
|
||
|
||
- Disable rendering, clear nametable.
|
||
- Loads player-specific data based on `CurrentPlayerIndex` ($50): X = $C1 (player 1) or $12 (player 2).
|
||
- `jsr ProcessGameState_C715` ($C715) with player data.
|
||
- Store player sprite Y at $0701.
|
||
- Load palette, wait for NMI, set PPUCTRL/PPUMASK.
|
||
- Run frame-synchronized task, clear nametable, process game state at $CEC8.
|
||
- Set `MainGameState` = 5, return to main loop.
|
||
|
||
### 6.5 Stage Complete / Between-Stage State — `HandleStageCompleteState` ($C22A) / `Flow_C233` ($C233)
|
||
|
||
- Disable rendering, process game state, enable rendering.
|
||
- `Flow_C233`: clear `Ram_046E`–$470, `Zp_2E`, `Zp_2F`, `Ram_045A`.
|
||
- Set `MainGameState` = 4 → stage transition state.
|
||
- Return to main loop.
|
||
|
||
`Flow_C24D` ($C24D) — the stage transition update (runs every 8th frame via `Zp_15` counter):
|
||
- Processes a long chain of game state routines: $C9F9, $D7D3, $D980, $E11E, $E0B7, $E71A, $E6AE.
|
||
- If `GameMode` != 0: reads controllers for current player.
|
||
- More game state processing: $D1C1, $D6BF, $CB6F, $DB45, $DBD7, $DC82, $DD61, $EA1A, $E7DE, $E81A.
|
||
- If `GameMode` != 0: `ProcessGameState_C5FF` ($C5FF) — score/remaining-life logic.
|
||
- Handles player lives (`Zp_1B` for player 1, `Zp_23` for player 2), wraps at 7.
|
||
- Clears nametable, sets `MainGameState` = 5 or 6 depending on mode.
|
||
|
||
---
|
||
|
||
## 7. Core Gameplay Update Loop
|
||
|
||
Every 8 frames (when `Zp_15 & $07 == 0`), `Flow_C258` ($C258) runs the primary gameplay update:
|
||
|
||
| Routine | Address | Purpose |
|
||
|---------|---------|---------|
|
||
| `ProcessGameState_C9F9` | $C9F9 | Object/enemy movement |
|
||
| `CalculateAndClampValue_D7D3` | $D7D3 | Value clamping |
|
||
| `ProcessGameState_D980` | $D980 | Object processing |
|
||
| `ProcessGameState_E11E` | $E11E | Collision / interaction |
|
||
| `ProcessGameState_E0B7` | $E0B7 | State update |
|
||
| `ProcessGameState_E71A` | $E71A | Animation |
|
||
| `ProcessGameState_E6AE` | $E6AE | Object cleanup |
|
||
| *(player input)* | — | Controller reading when GameMode != 0 |
|
||
| `ProcessGameState_D1C1` | $D1C1 | Player movement |
|
||
| `ProcessGameState_D6BF` | $D6BF | Player animation |
|
||
| `ProcessGameState_CB6F` | $CB6F | Burger/part assembly |
|
||
| `ProcessGameState_DB45` | $DB45 | Enemy AI |
|
||
| `ProcessGameState_DBD7` | $DBD7 | Collision detection |
|
||
| `ProcessGameState_DC82` | $DC82 | Score / points |
|
||
| `ProcessGameState_DD61` | $DD61 | Bonus items |
|
||
| `ProcessGameState_EA1A` | $EA1A | Pepper / power-up |
|
||
| `ProcessGameState_E7DE` | $E7DE | Round timing |
|
||
| `ProcessGameState_E81A` | $E81A | Level progression |
|
||
| `ProcessGameState_C5FF` | $C5FF | Lives / continues (GameMode != 0) |
|
||
|
||
---
|
||
|
||
## 8. Audio Engine
|
||
|
||
**`UpdateAudioEngine`** ($F478)
|
||
|
||
Called from:
|
||
- `Flow_C368` ($C368) — during title/transition states
|
||
- `RunFrameSynchronizedTask_C3F8` ($C3F8) — during frame-synchronized tasks
|
||
- `Flow_C615` ($C615) — during gameplay state
|
||
- `Flow_C7D6` ($C7D6) — tail call from another path
|
||
|
||
The audio engine:
|
||
1. Clears audio RAM at $052F (167 bytes, X from $A7 down).
|
||
2. Clears APU channel volume registers $4000–$400F (20 bytes).
|
||
3. Writes $00 to SND_CHN ($4015) — disable all channels initially.
|
||
4. Sets `Ram_05A3` ($05A3) = $38.
|
||
5. Processes sound commands and music updates via subroutines at $F4B3, $F5B3, $F748, $F6E4.
|
||
|
||
**`StorePendingSoundCommand`** ($C026) — stores a sound command in `PendingSoundCommand` ($90), processed by the NMI or audio engine.
|
||
|
||
**`ProcessPendingFrameTask`** ($C029) — processes `PendingFrameTask` ($8F); if $FF, resets to $00 and advances; otherwise increments the task counter up to $F1.
|
||
|
||
---
|
||
|
||
## 9. PPU / Rendering
|
||
|
||
### `EnableBackgroundAndSprites` ($C3EE)
|
||
Waits for PPU VBlank, loads `PpuMaskShadow` ($11), ORs with $18 (enable background + sprites), calls `SetPpuMaskShadow`.
|
||
|
||
### `DisableBackgroundAndSprites` ($C3E4)
|
||
Waits for PPU VBlank, loads `PpuMaskShadow`, ANDs with $E7 (disable background + sprites).
|
||
|
||
### `SetPpuControlShadow` ($C3D8) / `SetPpuMaskShadow` ($C3DE)
|
||
Store accumulator to the shadow register ($0C/$0D) and to the corresponding PPU register ($2000/$2001).
|
||
|
||
### `ClearNametables` ($FDAE)
|
||
Clears both nametables ($2000 and $2800) via `ClearNametable2000` and `ClearNametable2800`.
|
||
|
||
### `LoadPalette` ($FDF0)
|
||
Waits for PPU VBlank, sets PPU address to $3F00 (palette), writes 16 bytes of palette data from a pointer in X/Y.
|
||
|
||
### `SetDefaultScroll` ($FFEB)
|
||
Writes $08 then $00 to PPUSCROLL ($2005).
|
||
|
||
### `DecodeNametableStream` ($FE74)
|
||
Decodes a run-length-encoded nametable stream from a pointer (X/Y) into PPU memory. Used extensively during screen initialization (`RunFrameSynchronizedTask_C3F8` calls it multiple times for different screen regions).
|
||
|
||
---
|
||
|
||
## 10. Controller Input
|
||
|
||
**`ReadControllers`** ($FE2B)
|
||
|
||
Reads both controller ports ($4016/$4017) by strobing JOY1, then shifting in 8 bits per controller. Results are packed into scratch variables:
|
||
- `ScratchValue0` ($01), `ScratchValue1` ($02), `ScratchIndex` ($03) — raw button bits.
|
||
- The result is processed into directional/button flags stored in `Zp_2F` ($2F), checked with `and #$0C` (start/select) and `and #$08` (specific button) throughout the code.
|
||
|
||
---
|
||
|
||
## 11. Key Zero-Page / RAM Symbols
|
||
|
||
| Address | Symbol | Use |
|
||
|---------|--------|-----|
|
||
| $0E | NmiCompletedFlag | NMI frame-sync flag |
|
||
| $10 | PpuControlShadow | $2000 mirror |
|
||
| $11 | PpuMaskShadow | $2001 mirror |
|
||
| $13 | FrameCounterReload | Frame counter reload value |
|
||
| $14 | FrameCounter | Frame tick counter |
|
||
| $15 | Zp_15 | Frame divider (gameplay runs every 8 frames) |
|
||
| $16 | MainGameState | Current state machine state |
|
||
| $1B | Zp_1B | Player 1 lives counter |
|
||
| $1F | Zp_1F | (unnamed) |
|
||
| $23 | Zp_23 | Player 2 lives counter |
|
||
| $2E–$2F | Zp_2E / Zp_2F | Controller/button state |
|
||
| $31 | StateInitializedFlag | State init flag |
|
||
| $32–$35 | Zp_32–$35 | Indirect pointers / coordinates |
|
||
| $3F | Zp_3F | (unnamed) |
|
||
| $44 | Zp_44 | (unnamed) |
|
||
| $4C | GameMode | Game mode flag (0=title/demo, nonzero=gameplay) |
|
||
| $4D | GameActiveFlag | Game active flag |
|
||
| $4E | StageCompleteFlag | Stage completed flag |
|
||
| $4F | PlayerDeathFlag | Player death flags (bit0=P1, bit1=P2) |
|
||
| $50 | CurrentPlayerIndex | Current player (0 or 1) |
|
||
| $52 | Zp_52 | Gameplay sub-state |
|
||
| $56 | Zp_56 | (unnamed) |
|
||
| $57 | Zp_57 | Title state variable |
|
||
| $5A–$5B | Zp_5A / Zp_5B | (unnamed) |
|
||
| $8F | PendingFrameTask | Pending frame task counter |
|
||
| $90 | PendingSoundCommand | Pending sound command |
|
||
| $95–$96 | DispatchPointerLo / DispatchPointerHi | State dispatch indirect pointer |
|
||
| $0400 | Ram_0400 | (unnamed) |
|
||
| $040D–$040F | Ram_040D–$040F | (unnamed) |
|
||
| $045A–$045F | Ram_045A–$045F | (unnamed) |
|
||
| $0460–$0467 | Ram_0460–$0467 | Object/enemy state |
|
||
| $046E–$0470 | Ram_046E–$0470 | Stage state |
|
||
| $04E0 | Ram_04E0 | (unnamed) |
|
||
| $0500–$0535 | Ram_0500–$0535 | Object arrays |
|
||
| $052F–$05A7 | Ram_052F–$05A7 | Audio engine state |
|
||
| $0536 | CurrentObjectIndex | Current object being processed |
|
||
| $0700–$07FF | Ram_0700–$07FF | OAM buffer + sprite data |
|
||
|
||
---
|
||
|
||
## 12. Summary of Execution Flow
|
||
|
||
```
|
||
Power On / Reset
|
||
│
|
||
▼
|
||
ResetHandler ($FD24) ───────────────────────────────┐
|
||
│ sei, cld, disable PPU, wait 2× VBlank │
|
||
│ SetDefaultScroll, init frame counters │
|
||
│ InitializeRuntimeState ($FD63) │
|
||
│ ├─ clear NmiCompletedFlag, GameMode, etc. │
|
||
│ ├─ set PPUCTRL shadow=$90, clear nametables │
|
||
│ ├─ load palette, wait NMI │
|
||
│ └─ set PPUMASK shadow=$1A │
|
||
│ InitializeGameSystems ($C00E) │
|
||
│ ├─ process game state ($C76E, $CB53) │
|
||
│ ├─ set initial OAM ($0700=$70, $0701=$10) │
|
||
│ └─ SetMainState → first state │
|
||
▼ │
|
||
Flow_FD4F ($FD4F) ───────────────────────────────────┤
|
||
│ │
|
||
├─ WaitForNmi ($FD9D) ◄────── waits for NMI ───┘
|
||
│ │
|
||
├─ MainGameState ($16) → dispatch table │
|
||
│ ├─ State 0: $C3F8 (frame sync task) │
|
||
│ ├─ State 1: $C04E (HandleTitleState) │
|
||
│ ├─ State 2: $C12F (HandleGameStartState) │
|
||
│ ├─ State 4: $C24D (stage transition) │
|
||
│ ├─ State 5: $C1AA (HandlePlayerDeathState) │
|
||
│ ├─ State 6: $C14F (HandleGameplayState) │
|
||
│ └─ ... │
|
||
│ │
|
||
└─ (state handler returns via SetMainState) │
|
||
│ │
|
||
▼ │
|
||
back to Flow_FD4F ◄───────────────────────────────┘
|
||
|
||
NMI ($FF7F) [every frame]
|
||
├─ OAM DMA from $0700 ($4014)
|
||
├─ NmiCompletedFlag ($0E) = $01
|
||
├─ StageCompleteFlag ($4E) handling ($CEA3)
|
||
├─ ProcessPendingFrameTask ($C029)
|
||
├─ if GameMode != 0: ProcessGameState_F493 ($F493)
|
||
└─ rti
|
||
|
||
Gameplay Update (every 8 frames, Flow_C258 at $C258)
|
||
├─ object movement & collision ($C9F9–$E6AE)
|
||
├─ controller input (when GameMode != 0)
|
||
├─ player movement & animation ($D1C1, $D6BF)
|
||
├─ burger assembly, enemy AI ($CB6F, $DB45)
|
||
├─ collision, score, bonus ($DBD7–$DD61)
|
||
├─ pepper, round timing ($EA1A–$E81A)
|
||
└─ lives/continues ($C5FF)
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Notes
|
||
|
||
- The disassembly is organized as a state machine driven by `MainGameState` ($16), with the dispatch table at CPU $C000–$C00D.
|
||
- The NMI is the sole frame-sync source; the main loop (`Flow_FD4F`) blocks on `WaitForNmi` before dispatching to the current state handler.
|
||
- Gameplay updates run on an 8-frame divider (`Zp_15` at $15), not every frame.
|
||
- Audio is updated both from the NMI path (`ProcessGameState_F493` when `GameMode != 0`) and from state-specific code paths.
|
||
- Many routines are named `Flow_Cxxx` or `ProcessGameState_Cxxx` based on their CPU address, reflecting the automated disassembly origins. Semantic names are provisional.
|
||
- Build verification: `make` in `copilot/` produces `burger_time_rebuilt.nes` that is **byte-identical** to `burger_time.nes` (requires a symlink named `burger_time.nes` pointing to the actual ROM file due to spaces/special characters in the original filename). |