335 lines
16 KiB
Markdown
335 lines
16 KiB
Markdown
# Bomberman (U) — Game Flow from Power-On
|
||
|
||
This document describes the execution flow of the disassembled Bomberman (U) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `copilot/main.s` in the repository.
|
||
|
||
---
|
||
|
||
## 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 | Vertical (per header byte $01) |
|
||
| PRG bank start | CPU $C000 |
|
||
| Source file | `copilot/main.s` (8,070 lines, ~614 KB) |
|
||
| Output | `reconstructed_bomberman.nes` (24,592 bytes) |
|
||
|
||
### Interrupt vectors (CPU $FFFA–$FFFF)
|
||
|
||
| Vector | Address | Handler |
|
||
|--------|---------|---------|
|
||
| NMI | $C01A | `NmiHandler` |
|
||
| RESET | $C000 | `Reset` |
|
||
| IRQ / BRK | $FFF9 | `IrqHandler` (just `rti` — unused) |
|
||
|
||
---
|
||
|
||
## 2. Build Status
|
||
|
||
`make` in `copilot/` assembles and links successfully, producing `reconstructed_bomberman.nes` (24,592 bytes).
|
||
|
||
**Verification result:** The reconstructed ROM is **byte-identical** to `bomberman.nes` — headers, CHR, and all 16,384 PRG bytes match exactly.
|
||
|
||
### Naming fixes applied
|
||
|
||
| Change | Location | Reason |
|
||
|--------|----------|--------|
|
||
| `RandomState0` kept at `$54`; added `RandomStateAlt = $E4` | `locals.inc` | Zero-page reuse: `$54` is the random-state byte used by most code; `$E4` is a distinct byte decremented at CPU `$E71B` |
|
||
| `dec RandomState0` → `dec RandomStateAlt` | `main.s:6972` | The original ROM has `dec $E4` at CPU `$E71B`, not `dec $54` |
|
||
| `ControllerState` → `ControllerState1` | `main.s` (16 refs) | Clarifies this is controller port 1; added `ControllerState2 = $13` for port 2 |
|
||
| `Main_HandleControl_C368` → `MainGameLoop` | `main.s` (3 refs) | Main per-frame game loop entry point |
|
||
| `Gameplay_HandleControl_C79D` → `GameplayUpdate` | `main.s` (4 refs) | Gameplay object dispatcher/update |
|
||
| `Sound_HandleControl_E65F` → `SoundUpdate` | `main.s` (3 refs) | Sound engine update called from NMI |
|
||
|
||
---
|
||
|
||
## 3. Reset — CPU $FFFC → $C000
|
||
|
||
**`Reset`** ($C000)
|
||
|
||
On power-on or reset:
|
||
|
||
1. `sei` — disable maskable interrupts.
|
||
2. `lda #$00 / sta PPUMASK / sta PPUCTRL` — disable PPU rendering and NMI.
|
||
3. `cld` — clear decimal mode.
|
||
4. `ldx #$FF / txs` — initialize stack pointer to $FF.
|
||
5. **Wait for two startup VBlanks** — `WaitForFirstStartupVBlank` ($C00D) and `WaitForSecondStartupVBlank` ($C012) each poll `PPUSTATUS` ($2002) for the VBlank bit.
|
||
6. `jmp InitializeSystemAndRunMainLoop` ($C30C) — transfer control to the system initializer.
|
||
|
||
---
|
||
|
||
## 4. System Initialization — `InitializeSystemAndRunMainLoop` ($C30C)
|
||
|
||
This is the main entry point after reset:
|
||
|
||
1. **Clear RAM** — writes $00 to zero page and a range of RAM via an indirect pointer at $E0/$E1 (loops through $00–$FF pages).
|
||
2. `jsr InitializeVideoSystem` ($C1F2) — disables rendering, clears shadow OAM, uploads the system palette, enables NMI.
|
||
3. `jsr InitializeApu` ($E642) — silences all APU channels, resets sound delay/state variables.
|
||
4. Sets up the sound dispatch index and writes zero to sound state variables ($E0–$E3).
|
||
5. Sets stack pointer to $FF, enables APU ($4015 = $0F).
|
||
6. `jsr ComposeStatusScreen` ($DE83) — draws the initial status/title screen.
|
||
7. `jsr EnableNmiInPpuControl` ($C2B0) — enables NMI via PPUCTRL shadow.
|
||
8. Sets `GameplayActive`-style flag $72 = $01.
|
||
9. Enters the main loop at `Main_HandleControl_C368` ($C368).
|
||
|
||
---
|
||
|
||
## 5. NMI / Frame Heartbeat — `NmiHandler` ($C01A)
|
||
|
||
The NMI fires once per frame and is the central real-time driver:
|
||
|
||
1. **Save registers** (A, X, Y on stack).
|
||
2. Set `FrameSyncFlag` ($0B) = $00.
|
||
3. Set OAM address to $00.
|
||
4. **Sprite DMA path** — if $60 is zero, write OAM entry $0700–$0703 (sprite #0 at Y=$F8) and trigger OAMDMA ($4014 = $07).
|
||
5. **VRAM transfer path** — if $14 ≠ $15, transfer VRAM data from a table at $0600–$0607, indexing into pointer data at $D9C6–$D9C9. Handles both write and read packet types.
|
||
6. **Screen state / NMI work pending** — based on $72 and $60, transfers various VRAM data blocks (title screen, gameplay screen, etc.) using `SetPpuAddress` ($C196).
|
||
7. **PPU status / audio** — reads PPUSTATUS, commits PPU state and scroll via `CommitPpuStateAndScroll` ($C296), increments a frame counter at $33, handles audio channel timing at $69/$6A.
|
||
8. `jsr Ppu_WriteZp10_C19D` ($C19D) — reads and debounces both controller ports, stores results in `ControllerState` ($12) and $13.
|
||
9. `jsr Gameplay_ReadZpB2_E4C7` ($E4C7) — gameplay input handling.
|
||
10. `jsr Sound_HandleControl_E65F` ($E65F) — sound engine update.
|
||
11. **DMC control** — if $38 and $59 are zero, starts DMC playback ($4010 = $0E, DMC address $C000, length $FF, enable channels).
|
||
12. **PPU scroll wait** — if $60 is set, waits for VBlank (polls PPUSTATUS bit 6), then applies scroll from $0E/$0F.
|
||
13. Toggles $DB with EOR #$05 (frame parity / flip-flop flag).
|
||
14. **Restore registers, `rti`.**
|
||
|
||
### Controller reading (`Ppu_ReadControllerAndReturnA_C1BF` / $C1BF)
|
||
- Strobes JOY1 ($4016) with $01 then $00.
|
||
- Reads 8 bits per controller, shifting into X.
|
||
- `Ppu_HandleControl_C1EB` masks to the low 2 bits ($03) and increments X if non-zero (button press detection).
|
||
- Port 1 result → `ControllerState` ($12); port 2 result → $13.
|
||
|
||
---
|
||
|
||
## 6. Main Loop
|
||
|
||
The main loop starts at **`Main_HandleControl_C368`** ($C368):
|
||
|
||
```
|
||
Main_HandleControl_C368 ($C368):
|
||
wait until ControllerState ($12) is nonzero (button pressed)
|
||
└─ branch on SELECT ($20) to toggle $5F
|
||
└─ decrement sub-delay $71 / main delay $70
|
||
└─ when delays expire, increment $59 (game mode / screen state)
|
||
```
|
||
|
||
Based on `$59` and `$5F`, the loop dispatches to:
|
||
- `RunMenuSelectionScreen` ($DA8E) — title/menu screen
|
||
- Gameplay initialization paths ($C3B6–$C5FF) — sets up object positions, stage, sound
|
||
- `Main_WriteStageCounter_C613` ($C613) — stage counter display
|
||
|
||
The core per-frame gameplay dispatch uses a **state table** at `MainStateDispatchTable` ($C75D–$C79C), which maps state indices to handler sub-indices ($00–$27). Gameplay object processing walks arrays at $03E6 (state), $0436, $0486 (X/Y position), $04D6, $0526 (timer/flags), with up to 80 ($4F) object slots.
|
||
|
||
Key main-loop subroutines:
|
||
- `Main_ReadGameMode_C60B` ($C60B) — waits for $B5 (frame delay) to reach zero.
|
||
- `Main_WriteStageCounter_C613` ($C613) — updates stage counter, handles START button to begin gameplay.
|
||
- `Gameplay_HandleControl_C79D` ($C79D) — main gameplay object dispatcher (80 slots).
|
||
- `Gameplay_Dispatch_C7A4` / `Gameplay_Dispatch_C835` — object state dispatch.
|
||
- `Stage_WriteObjectWorkX_D8FC` ($D8FC) — object work initialization.
|
||
|
||
---
|
||
|
||
## 7. Gameplay Systems
|
||
|
||
### Object management
|
||
- Up to 80 object slots ($00–$4F), tracked by state at $03E6,X.
|
||
- Position arrays: $0436,X / $0486,X (X/Y), $04D6,X (state/flags), $0526,X (timer/direction).
|
||
- `Stage_WriteObjectWorkX_D8FC` ($D8FC) initializes object work fields.
|
||
- Objects are processed in a loop with `dex`/index retreat patterns ($C756, $C8A6).
|
||
|
||
### Bomb / explosion / player mechanics
|
||
Referenced throughout the gameplay code:
|
||
- `Gameplay_HandleControl_C96A`, `Gameplay_HandleControl_CA1B`, `Gameplay_HandleControl_C79D` — player/bomb input and state.
|
||
- `Object_WriteStageProgress_CFB0` ($CFB0) — stage progress tracking.
|
||
- `Frame_ReadObjectIndex_CE5B` ($CE5B), `Frame_ReadObjectX_CDD4` ($CDD4) — frame-based object reading.
|
||
|
||
### Stage system
|
||
- `Stage_ReadZp58_D69A` ($D69A) — stage data lookup.
|
||
- Stage counter display via `Main_WriteStageCounter_C613`.
|
||
|
||
---
|
||
|
||
## 8. Audio / Sound Engine
|
||
|
||
### `InitializeApu` ($E642)
|
||
- Clears DMC direct register ($4011), sound delays $E1–$E3.
|
||
- Silences pulse 1, pulse 2, triangle, noise channel controls ($4000, $4004, $4008, $400C).
|
||
- Enables APU channels ($4015 = $0F).
|
||
|
||
### `Sound_HandleControl_E65F` ($E65F)
|
||
- Loops over 3 sound channels (X = 2 down to 0).
|
||
- Decrements per-channel delay at $E1,X.
|
||
- When delay expires, reads sound command state from $DF:
|
||
- If $DF < 0 → read sound RAM pointer ($E6BC table).
|
||
- If $DF == 3 → write sound command state.
|
||
- Otherwise → return or handle based on $E0 (indirect pointer).
|
||
- Calls into sound dispatch via `SoundDispatchAddressTable` ($E6AD–$E6C8).
|
||
|
||
### Sound dispatch table (`SoundDispatchAddressTable`, $E6AD)
|
||
A jump table of 14 sound handler addresses (e.g., $E641, $E6C8, $E6CC, $E6DE, $E727, $E766, $E792, $E7AB, etc.), each handling a specific sound effect or music channel.
|
||
|
||
### Channel-specific sound paths
|
||
- `Sound_UpdateAudioChannelPath_E6CF` ($E6CF) — noise channel ($400E–$400F).
|
||
- `Sound_UpdateAudioChannelPath_E6DF` ($E6DF) — pulse 2 channel: sets sweep ($4005 = $84), timer lo/hi ($4006/$4007), noise period/length.
|
||
- `Sound_UpdateAudioChannelPath_E70B` ($E70B) — decrements `RandomState0`, reconfigures pulse 2 when expired.
|
||
|
||
### Sound state (zero page)
|
||
| Address | Symbol | Use |
|
||
|---------|--------|-----|
|
||
| $DF | SoundCommandState | Sound command / state |
|
||
| $E0 | SoundDispatchIndex / IndirectPtrLo | Sound dispatch pointer low |
|
||
| $E1–$E3 | SoundDelay0–2 | Per-channel sound delays |
|
||
|
||
---
|
||
|
||
## 9. PPU / Rendering
|
||
|
||
### `InitializeVideoSystem` ($C1F2)
|
||
- `jsr DisableRendering` ($C257) — wait for VBlank, turn off PPUMASK.
|
||
- `jsr Ppu_HandleControl_C2B9` ($C2B9) — apply PPUCTRL shadow with NMI disabled.
|
||
- Set PPUCTRL shadow ($0C) = $10, clear $0E/$0F/$14/$15.
|
||
- `jsr HideAllSprites` ($C268) — fills $0700–$07FF with $F8 (offscreen) in 32-byte strides.
|
||
- `jsr UploadSystemPalette` ($C2C3) — uploads 32 bytes of palette data from `SystemPaletteData` ($C2E7) to $3F00–$3F1F.
|
||
- `jsr EnableNmiInPpuControl` ($C2B0) — enable NMI.
|
||
|
||
### `CommitPpuStateAndScroll` ($C296)
|
||
- Writes `PpuMaskShadow` ($0D) to PPUMASK ($2001).
|
||
- Sets PPU address to $0000, writes $00 to $2005 twice (scroll).
|
||
- Writes `PpuCtrlShadow` ($0C) to PPUCTRL ($2000).
|
||
|
||
### Palette data (`SystemPaletteData`, $C2E7)
|
||
32 bytes of NES palette colors starting at $3F00.
|
||
|
||
---
|
||
|
||
## 10. Zero-Page / RAM Symbols
|
||
|
||
| Address | Symbol | Use |
|
||
|---------|--------|-----|
|
||
| $0B | FrameSyncFlag | NMI frame sync |
|
||
| $0C–$0D | PpuCtrlShadow / PpuMaskShadow | PPU register shadows |
|
||
| $12 | ControllerState1 | Controller port 1 state |
|
||
| $13 | ControllerState2 | Controller port 2 state |
|
||
| $14–$15 | VramReadOffset / VramWriteLimit | NMI VRAM-transfer window (index + limit) |
|
||
| $16 | NmiPathSelector | Selects NMI sub-path (sprite DMA vs VRAM data) |
|
||
| $1F–$20 | (unnamed) | Object X/Y temp |
|
||
| $28–$2C | (unnamed) | Object spawn parameters |
|
||
| $29–$2B | (unnamed) | Object coordinates |
|
||
| $33 | FrameCounter | Frame tick counter |
|
||
| $34 | IndirectPtrHi | Pointer high (used by sound/object) |
|
||
| $36 | (unnamed) | Object X temp |
|
||
| $38 | (unnamed) | DMC / audio flag |
|
||
| $39 | (unnamed) | Stage counter flag |
|
||
| $3C–$3F | (unnamed) | Delay counters |
|
||
| $54–$57 | RandomState0–3 | PRNG state (4-byte RNG) |
|
||
| $59 | GameMode | Game mode / screen state |
|
||
| $5A | (unnamed) | Stage / level |
|
||
| $5B | (unnamed) | Object X bound |
|
||
| $5C–$5E | (unnamed) | Object state flags |
|
||
| $5F | (unnamed) | Menu toggle |
|
||
| $60 | NmiWorkPending | NMI VRAM-transfer work pending flag |
|
||
| $61–$6A | (unnamed) | Object init / temp |
|
||
| $68–$70 | MainDelay / SubDelay | Main loop delay counters |
|
||
| $72 | ScreenState | Screen state flag |
|
||
| $73 | ObjectXLimit | Object X limit |
|
||
| $76–$7A | (unnamed) | Object position/state |
|
||
| $7D–$7E | (unnamed) | Bomb / object state |
|
||
| $78–$7C | (unnamed) | Player / bomb coords |
|
||
| $79–$7A | (unnamed) | Player position |
|
||
| $93 | StageCounter | Stage counter |
|
||
| $94 | (unnamed) | Audio / object |
|
||
| $9C–$9F | ObjectWork0–3 | Object work fields |
|
||
| $A0–$A7 | (unnamed) | Object work / bonus |
|
||
| $A4 | BonusTimer | Bonus timer |
|
||
| $AC–$AE | (unnamed) | Audio data pointers |
|
||
| $B0 | GameModeCounter | Game mode counter |
|
||
| $B1 | ObjectType | Object type |
|
||
| $B2 | (unnamed) | Audio flag |
|
||
| $B5 | FrameDelay | Frame delay |
|
||
| $DB | FrameParity | Frame parity toggle (NMI toggles each frame) |
|
||
| $DF | SoundCommandState | Sound command state |
|
||
| $E0–$E3 | SoundDispatchIndex / SoundDelay0–2 | Sound dispatch & delays |
|
||
| $E4 | RandomStateAlt | Alternate random-state byte (dec'd at CPU $E71B; distinct from RandomState0 at $54) |
|
||
|
||
---
|
||
|
||
## 11. Data Tables
|
||
|
||
| Label | Address | Description |
|
||
|-------|---------|-------------|
|
||
| `InesHeader` | $C000–$C00F | iNES header (embedded in source) |
|
||
| `SystemPaletteData` | $C2E7–$C306 | 32-byte palette for $3F00 |
|
||
| `MainStateDispatchTable` | $C75D–$C79C | 64-byte gameplay state dispatch table |
|
||
| `SoundDispatchAddressTable` | $E6AD–$E6C8 | 14-entry sound handler jump table |
|
||
| `PatternTableGraphics` | $C010+ (CHR) | CHR pattern table data |
|
||
|
||
---
|
||
|
||
## 12. Summary of Execution Flow
|
||
|
||
```
|
||
Power On / Reset
|
||
│
|
||
▼
|
||
Reset ($C000) ───────────────────────────────────────┐
|
||
│ sei, disable PPU, cld, stack=$FF │
|
||
│ wait 2× VBlank │
|
||
▼ │
|
||
jmp InitializeSystemAndRunMainLoop ($C30C) │
|
||
│ │
|
||
├─ clear RAM ($0000–$07FF) │
|
||
├─ InitializeVideoSystem ($C1F2) │
|
||
│ ├─ DisableRendering │
|
||
│ ├─ HideAllSprites │
|
||
│ ├─ UploadSystemPalette ($3F00–$3F1F) │
|
||
│ └─ EnableNmiInPpuControl │
|
||
├─ InitializeApu ($E642) — silence all channels │
|
||
├─ ComposeStatusScreen ($DE83) │
|
||
├─ EnableNmiInPpuControl │
|
||
└─ MainGameLoop ($C368) ◄─────────────┘
|
||
│
|
||
MainGameLoop ($C368) ─────────────────────────────────┤
|
||
│ │
|
||
├─ wait for controller button press │
|
||
├─ handle SELECT / delay counters ($70,$71) │
|
||
├─ increment game mode ($59) │
|
||
├─ RunMenuSelectionScreen ($DA8E) [title] │
|
||
├─ gameplay init ($C3B6–$C5FF) │
|
||
├─ GameplayUpdate ($C79D) │
|
||
│ │ 80 object slots ($03E6 state array) │
|
||
│ ├─ object position ($0436/$0486) │
|
||
│ ├─ object timer ($0526) │
|
||
│ └─ object dispatch via StateDispatchTable │
|
||
└─ eventually → back to MainGameLoop │
|
||
│
|
||
NMI ($C01A) [every frame] │
|
||
│ │
|
||
├─ OAM setup + DMA ($4014) │
|
||
├─ VRAM transfer from $0600 table │
|
||
├─ screen state transfer ($D9C6–$D9C9 pointers) │
|
||
├─ controller read & debounce ($C1BF) │
|
||
├─ Gameplay_ReadZpB2_E4C7 ($E4C7) │
|
||
├─ SoundUpdate ($E65F) │
|
||
├─ DMC start ($4010–$4013) │
|
||
├─ PPU state & scroll commit ($C296) │
|
||
├─ frame parity toggle ($DB) │
|
||
└─ rti │
|
||
│
|
||
Sound Engine ($E65F) [from NMI] │
|
||
├─ per-channel delay countdown ($E1–$E3) │
|
||
├─ SoundDispatchAddressTable ($E6AD) │
|
||
├─ pulse 2, noise, DMC channel control │
|
||
└─ RandomStateAlt ($E4) decrement │
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Notes
|
||
|
||
- **Build is byte-identical:** The apparent one-byte difference (`dec $E4` vs `dec $54` at CPU $E71B) was resolved by recognizing zero-page reuse — `$54` is `RandomState0` used by most code, while `$E4` is a distinct byte (`RandomStateAlt`) decremented at that one location. Both are now separately named in `locals.inc`.
|
||
- The source uses a "ROM $00XXXX" file-offset annotation scheme on every instruction, enabling direct comparison with the binary.
|
||
- Code is organized as reachable routines traced from the NMI/RESET/IRQ control-flow graph; unreachable bytes are classified as explicit data regions.
|
||
- The disassembly is a single file (`main.s`) with embedded iNES header and CHR data segments.
|
||
- Vectors confirm NMI at $C01A and RESET at $C000, matching the reset handler location. |