Files
disasm/Burger Time (U)/OVERVIEW.md
T

364 lines
17 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.
# 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).