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

17 KiB
Raw Blame History

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).