17 KiB
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:
sei— disable maskable interrupts.cld— clear decimal mode.lda #$00 / sta PPUCTRL / sta PPUMASK— disable PPU rendering and NMI.- Wait for two VBlanks —
Flow_FD2EandFlow_FD33each pollPPUSTATUS($2002) for the VBlank bit. jsr SetDefaultScroll($FFEB) — set scroll to ($08, $00).FrameCounter($14) = $00;FrameCounterReload($13) = $08.JOY2($4017) = $80 — select controller latch mode / disable IRQ.- Stack pointer = $FF.
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.jsr InitializeGameSystems($C00E) — process game state at $C76E and $CB53; set up initial OAM positions ($0700=$70, $0701=$10); clear $32; callSetMainStateto enter the first state.- 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:
jsr WaitForNmi($FD9D) — block until NmiCompletedFlag ($0E) is set, then clear it.- 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.
- Low byte from
- 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:
- Save registers (A, X, Y on stack).
- OAM DMA — write $00 to OAMADDR ($2003), then $07 to OAMDMA ($4014) to DMA sprite data from $0700–$07FF.
- Set
NmiCompletedFlag($0E) = $01 — signals the main loop that a frame has elapsed. - Read
PPUSTATUS($2002) to reset the address latch. - Stage complete handling — if
StageCompleteFlag($4E) is set,jsr ProcessGameState_CEA3($CEA3), then clear the flag. jsr ProcessPendingFrameTask($C029) — if a frame task is pending, advance its counter.- If
GameMode($4C) is nonzero,jsr ProcessGameState_F493($F493) — per-frame gameplay/audio update. - Restore registers,
rti.
6. State Handlers
6.1 Title State — HandleTitleState ($C04E)
- Sets
GameMode($4C) = $00 andZp_57($57) = $00. jsr ProcessGameState_C7F7($C7F7) — title-specific game state processing.StateInitializedFlag($31) = $01.jsr ProcessGameState_CB53($CB53) andjsr ProcessGameState_C81B($C81B).jsr EnableBackgroundAndSprites($C3EE) — enables rendering.jmp Flow_C233($C233) — clears stage state, setsMainGameState= 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) andjsr ProcessGameState_C977($C977).- Decrement
Ram_0400($0400). jsr ClearNametable2000again.- 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_52is 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→ setsMainGameState= 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: clearRam_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_1Bfor player 1,Zp_23for 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 statesRunFrameSynchronizedTask_C3F8($C3F8) — during frame-synchronized tasksFlow_C615($C615) — during gameplay stateFlow_C7D6($C7D6) — tail call from another path
The audio engine:
- Clears audio RAM at $052F (167 bytes, X from $A7 down).
- Clears APU channel volume registers $4000–$400F (20 bytes).
- Writes $00 to SND_CHN ($4015) — disable all channels initially.
- Sets
Ram_05A3($05A3) = $38. - 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 withand #$0C(start/select) andand #$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 onWaitForNmibefore dispatching to the current state handler. - Gameplay updates run on an 8-frame divider (
Zp_15at $15), not every frame. - Audio is updated both from the NMI path (
ProcessGameState_F493whenGameMode != 0) and from state-specific code paths. - Many routines are named
Flow_CxxxorProcessGameState_Cxxxbased on their CPU address, reflecting the automated disassembly origins. Semantic names are provisional. - Build verification:
makeincopilot/producesburger_time_rebuilt.nesthat is byte-identical toburger_time.nes(requires a symlink namedburger_time.nespointing to the actual ROM file due to spaces/special characters in the original filename).