Standardizes build process and adds OVERVIEW.md for each

This commit is contained in:
tmerritt committed 2026-10-15 18:50:41 -04:00
1 parent 25423d3828
commit a29b03b00b
135 files changed
+26562 -420

No files matched your search

+364
View File
@@ -0,0 +1,364 @@
# 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).