Files
disasm/Bomberman (U)/OVERVIEW.md
T

335 lines
16 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.
# 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.