254 lines
13 KiB
Markdown
254 lines
13 KiB
Markdown
# Defender 2 (U) — Game Flow from Power-On
|
||
|
||
This document describes the execution flow of the disassembled Defender 2 (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 `defender2.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 | Vertical |
|
||
| PRG bank start | CPU $C000 |
|
||
| Source file | `copilot/prg.s` (11,905 lines) |
|
||
| Output | `defender2_rebuilt.nes` (24,592 bytes) |
|
||
|
||
### Interrupt vectors (CPU $FFFA–$FFFF)
|
||
|
||
| Vector | Address | Handler |
|
||
|--------|---------|---------|
|
||
| NMI | $CAA1 | `NmiHandler` |
|
||
| RESET | $C983 | `ResetHandler` |
|
||
| IRQ / BRK | $CA9E | `IrqHandler` (self-loop `jmp $CA9E` — unused) |
|
||
|
||
---
|
||
|
||
## 2. Build Status
|
||
|
||
`make` in `copilot/` assembles `header.s`, `prg.s`, and `chr.s` with `ca65`, links with `ld65`, and verifies against `defender2.nes`.
|
||
|
||
**Verification result: BYTE-IDENTICAL** — `defender2_rebuilt.nes` matches `defender2.nes` exactly (headers, CHR, and all PRG bytes).
|
||
|
||
### Build fix applied
|
||
|
||
The original source referenced `LookupTableEntry_FAF5` at a `bpl` instruction (CPU $FAB4), but the symbol was undefined. The branch target CPU $FAF5 falls in the middle of what the disassembler classified as `lda #$16` (A9 at $FAF4, 16 at $FAF5). The fix:
|
||
|
||
- Replaced the symbolic `bpl LookupTableEntry_FAF5` with `bne *-$1B` at CPU $E811, which produces the exact original bytes `d0 e3` (the disassembler had mislabeled `bne` as `bpl`).
|
||
- The build now produces a byte-identical ROM.
|
||
|
||
---
|
||
|
||
## 3. Reset — CPU $FFFC → $C983
|
||
|
||
**`ResetHandler`** ($C983)
|
||
|
||
On power-on or reset:
|
||
|
||
1. `sei` — disable maskable interrupts.
|
||
2. `cld` — clear decimal mode.
|
||
3. `lda #$00 / sta PPUCTRL` — disable PPU rendering and NMI.
|
||
4. `dex / txs` — initialize stack pointer to $FF.
|
||
5. `UpdateSideScrollingGameplay_C98C` ($C98C): wait for VBlank (poll `PPUSTATUS` bit 7), then clear a block of PPU memory via `ConfigureHudAndPlayfieldPpu_C99D` ($C99D) using zero-page pointers $00/$01 and counter $01.
|
||
6. Set PPUCTRL = $00, `PpuControlShadow` ($10) = $00, PPUMASK = $02, `PpuMaskShadow` ($11) = $02.
|
||
7. Set scroll registers: `PPUSCROLL` = $00 twice, `HorizontalScroll` ($12) = $00, `VerticalScroll` ($13) = $00.
|
||
8. Configure HUD and playfield PPU regions via repeated calls to `ConfigureHudAndPlayfieldPpu_C092` ($C092) with various source/destination parameters — uploads background tiles, HUD elements, and playfield data.
|
||
9. Initialize game RAM variables: `Ram_03FB` = $80, `Ram_06AB` = $90, `Ram_06CB` = $94, `Ram_06EB` = $98, `Ram_05C0` = $03, `Ram_0580` = $04, `Ram_060B` = $68, `Ram_064B` = $70.
|
||
10. `Zp_1D` ($1D) = $55; `SND_CHN` ($4015) = $0F (enable all APU channels); `Ram_0100` = $0F; `Zp_36` ($36) = $01.
|
||
11. Enable NMI: `PpuControlShadow` ($10) \| $80, store to shadow and PPUCTRL.
|
||
12. Falls into `UpdateSideScrollingGameplay_CA6A` ($CA6A) — the permanent main loop.
|
||
|
||
---
|
||
|
||
## 4. Main Loop — `UpdateSideScrollingGameplay_CA6A` ($CA6A)
|
||
|
||
Once NMI is enabled, `ResetHandler` never returns. It enters the main loop:
|
||
|
||
```
|
||
UpdateSideScrollingGameplay_CA6A ($CA6A):
|
||
jsr UpdateSideScrollingGameplay_C40D ; $CA6A: 20 0D C4
|
||
lda Zp_A2 ; $CA6D: A5 A2
|
||
beq UpdateSideScrollingGameplay_CA6A ; $CA6F: F0 F9 (spin while Zp_A2 == 0)
|
||
dec Zp_A2 ; $CA71: C6 A2
|
||
jsr UpdateSideScrollingGameplay_E47A ; $CA73: 20 7A E4
|
||
jsr UpdateSideScrollingGameplay_DC7B ; $CA76: 20 7B DC
|
||
jsr UpdateSideScrollingGameplay_E6E1 ; $CA79: 20 E1 E6
|
||
jsr UpdateSideScrollingGameplay_D91A ; $CA7C: 20 1A D9
|
||
jsr UpdateSideScrollingGameplay_E6D5 ; $CA7F: 20 D5 E6
|
||
jsr UpdateSideScrollingGameplay_E7AA ; $CA82: 20 AA E7
|
||
jsr UpdateSideScrollingGameplay_E029 ; $CA85: 20 29 E0
|
||
jsr UpdateSideScrollingGameplay_DE78 ; $CA88: 20 78 DE
|
||
jmp UpdateSideScrollingGameplay_CA6A ; $CA8B: 4C 6A CA (loop forever)
|
||
```
|
||
|
||
The main loop spins waiting for `Zp_A2` ($A2) to become nonzero (set by the NMI), then decrements it and calls a chain of per-frame update routines covering flying entities, object updates, collisions, scrolling, and rendering. It then loops back to itself.
|
||
|
||
---
|
||
|
||
## 5. NMI / Frame Heartbeat — `NmiHandler` ($CAA1)
|
||
|
||
The NMI fires once per frame and is the central real-time driver:
|
||
|
||
1. **Save registers** (A, X, Y on stack).
|
||
2. `ldx #$00` — X = 0 for buffer copy loop.
|
||
3. `ConfigureHudAndPlayfieldPpu_CAA8` ($CAA8): copy 16 bytes from zero-page pointer $00,X into PPU registers (PPU command buffer upload), toggling `PpuControlShadow` bit 7.
|
||
4. `jsr ConfigureHudAndPlayfieldPpu_D2CF` ($CAB9) — additional PPU configuration.
|
||
5. `jsr TransferShipTargetOamBuffer` ($CABC) — OAM DMA: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014).
|
||
6. `jsr UpdateSideScrollingGameplay_C073` ($CABF) — configure HUD/playfield PPU and reset scroll.
|
||
7. Restore PPU mask from `PpuMaskShadow` ($11).
|
||
8. `jsr PollShipControllers` ($CAC7) — read controller ports $4016/$4017 into `PreviousControllerButtons` ($19) and `CurrentControllerButtons` ($1B).
|
||
9. `jsr TransformFlightState_CEE8` ($CACA) — process ship flight state.
|
||
10. If `Zp_3B` ($3B) != 0 → `UpdateSideScrollingGameplay_CAD4`; else → `UpdateSideScrollingGameplay_C3CC` ($CAD1).
|
||
11. `UpdateSideScrollingGameplay_CAD4` ($CAD4): `jsr ReadShipControls_F880`, `jsr UpdateSideScrollingGameplay_CBD4`, then `jsr UpdateSideScrollingGameplay_D6D8` ($CADA).
|
||
12. `ConfigureHudAndPlayfieldPpu_CADF` ($CADF): pop saved registers (Y, X, A) from the stack — restores the context saved at the start of NMI.
|
||
13. Re-enable NMI: `PpuControlShadow` \| $80, store to shadow and PPUCTRL.
|
||
14. `pla / rti` — restore A and return from interrupt.
|
||
|
||
The NMI is the frame heartbeat: it handles PPU updates, OAM DMA, controller polling, and gameplay state updates, all during VBlank.
|
||
|
||
---
|
||
|
||
## 6. PPU / Rendering
|
||
|
||
### `ConfigureHudAndPlayfieldPpu_C092` ($C092)
|
||
- Reads PPUSTATUS to reset address latch.
|
||
- Loads `PpuControlShadow` ($10), ANDs with $FB (disable sprite rendering bit), stores to PPUCTRL and shadow.
|
||
- Sets up PPU address and writes tile runs to $2007 — used for HUD and playfield background updates.
|
||
|
||
### `TransferShipTargetOamBuffer` ($C087)
|
||
- Write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014) — DMA sprite data from $0200–$02FF.
|
||
|
||
### `PollShipControllers` ($C434)
|
||
- Reads both controller ports, tracking previous and current button states.
|
||
|
||
### `UpdateSideScrollingGameplay_C073` ($C073)
|
||
- Configures HUD/playfield PPU upload and resets scroll. Used during NMI and gameplay state transitions.
|
||
|
||
---
|
||
|
||
## 7. Zero-Page / RAM Symbols
|
||
|
||
| Address | Symbol | Use |
|
||
|---------|--------|-----|
|
||
| $00–$03 | ScratchValue0–3 | General scratch values |
|
||
| $04–$05 | DataPointerLo/Hi | Data pointer |
|
||
| $06–$07 | TemporaryPointerLo/Hi | Temporary pointer |
|
||
| $08–$0D | Zp_08–Zp_0D | Zero-page scratch |
|
||
| $0F | Zp_0F | Zero-page scratch |
|
||
| $10 | PpuControlShadow | $2000 mirror |
|
||
| $11 | PpuMaskShadow | $2001 mirror |
|
||
| $12 | HorizontalScroll | Horizontal scroll value |
|
||
| $13 | VerticalScroll | Vertical scroll value |
|
||
| $14–$18 | Zp_14–Zp_18 | Zero-page scratch |
|
||
| $19 | PreviousControllerButtons | Previous frame controller state |
|
||
| $1A | Zp_1A | Zero-page scratch |
|
||
| $1B | CurrentControllerButtons | Current frame controller state |
|
||
| $1C–$1F | Zp_1C–Zp_1F | Zero-page scratch |
|
||
| $20–$3F | Zp_20–Zp_3F | Zero-page scratch (extensive use) |
|
||
| $36 | Zp_36 | Game flag |
|
||
| $3B | Zp_3B | Gameplay branch flag |
|
||
| $3F | Zp_3F | Zero-page scratch |
|
||
| $40–$47 | Zp_40–Zp_47 | Zero-page scratch |
|
||
| $4C | Zp_4C | Zero-page scratch |
|
||
| $54–$5F | Zp_54–Zp_5F | Zero-page scratch |
|
||
| $60–$61 | Zp_60–Zp_61 | Zero-page scratch |
|
||
| $A0–$A7 | Zp_A0–Zp_A7 | Zero-page scratch ($A2 used as NMI frame sync) |
|
||
| $A9–$AB | Zp_A9–Zp_AB | Zero-page scratch |
|
||
| $AC–$AF | Zp_AC–Zp_AF | Zero-page scratch |
|
||
| $B0–$B9 | Zp_B0–Zp_B9 | Zero-page scratch |
|
||
| $BA–$BF | Zp_BA–Zp_BF | Zero-page scratch |
|
||
| $C0–$C4 | Zp_C0–Zp_C4 | Zero-page scratch |
|
||
| $D0–$D5 | Zp_D0–Zp_D5 | Zero-page scratch |
|
||
| $D7–$DF | Zp_D7–Zp_DF | Zero-page scratch |
|
||
| $E0–$EF | Zp_E0–Zp_EF | Zero-page scratch |
|
||
| $F0–$F7 | Zp_F0–Zp_F7 | Zero-page scratch |
|
||
| $FA–$FB | Zp_FA–Zp_FB | Zero-page scratch |
|
||
| $FD–$FF | Zp_FD–Zp_FF | Zero-page scratch |
|
||
| $0100 | Ram_0100 | APU/channel state |
|
||
| $0200 | OamBuffer | Sprite DMA source |
|
||
| $0201–$0203 | Ram_0201–Ram_0203 | RAM scratch |
|
||
| $0214, $0244, $0299, $029C–$029D, $029F–$02FE | Ram_* | Game state RAM |
|
||
| $0330–$0331 | Ram_0330–Ram_0331 | Gameplay state |
|
||
| $0380–$03FF | Ram_0380–Ram_07FF | Extensive game state RAM (objects, entities, scores, etc.) |
|
||
| $0540–$05FF | Ram_0540–Ram_05FF | Gameplay variables |
|
||
| $0600–$06FF | Ram_0600–Ram_06FF | Entity/object arrays |
|
||
| $0782–$07F2 | Ram_0782–Ram_07F2 | Late-game state |
|
||
|
||
---
|
||
|
||
## 8. Routine Naming Patterns
|
||
|
||
The disassembly uses consistent name prefixes that indicate routine purpose:
|
||
|
||
| Prefix | Example | Meaning |
|
||
|--------|---------|---------|
|
||
| `UpdateSideScrollingGameplay_*` | `UpdateSideScrollingGameplay_C073` | Side-scrolling gameplay logic |
|
||
| `UpdateGameplayStateField_*` | `UpdateGameplayStateField_C1F7` | Gameplay state field updates |
|
||
| `TransformFlightState_*` | `TransformFlightState_C109` | Ship flight/transform state |
|
||
| `ConfigureHudAndPlayfieldPpu_*` | `ConfigureHudAndPlayfieldPpu_C092` | PPU HUD/playfield configuration |
|
||
| `CalculateMovementAndClamp_*` | `CalculateMovementAndClamp_C183` | Movement calculation with clamping |
|
||
| `UpdateFlyingEntity_*` | `UpdateFlyingEntity_CF11` | Flying entity (ship/enemy) updates |
|
||
| `ReadShipControls_*` | `ReadShipControls_F880` | Ship controller input |
|
||
| `ReadObjectRecord_*` | `ReadObjectRecord_DADA` | Object record reading |
|
||
| `ProcessObjectStateRoutine_*` | `ProcessObjectStateRoutine_CBE3` | Object state machine |
|
||
| `UpdateObjectBuffer_*` | `UpdateObjectBuffer_DB52` | OAM buffer updates |
|
||
| `UpdateObjectRecord_*` | `UpdateObjectRecord_D27D` | Object record maintenance |
|
||
|
||
---
|
||
|
||
## 9. Summary of Execution Flow
|
||
|
||
```
|
||
Power On / Reset
|
||
│
|
||
▼
|
||
ResetHandler ($C983) ───────────────────────────────┐
|
||
│ sei, cld, disable PPU, stack=$FF │
|
||
│ wait VBlank, clear PPU memory │
|
||
│ set PPUCTRL=$00, PPUMASK=$02, scroll=$00 │
|
||
│ upload HUD/playfield tiles via C092 │
|
||
│ init game RAM, APU channels ($4015=$0F) │
|
||
│ enable NMI (PpuControlShadow \| $80) │
|
||
▼ │
|
||
jmp UpdateSideScrollingGameplay_CA6A ($CA6A) │
|
||
│ │
|
||
│ Main loop — waits for NMI frame sync: │
|
||
│ wait until Zp_A2 ($A2) != 0 │
|
||
│ dec Zp_A2 │
|
||
│ jsr E47A, DC7B, E6E1, D91A, E6D5, E7AA, │
|
||
│ E029, DE78 │
|
||
│ jmp back to CA6A │
|
||
│ │
|
||
└────────────────────────────────────────────────┘
|
||
│
|
||
NMI ($CAA1) [once per frame] │
|
||
│ │
|
||
├─ save A, X, Y │
|
||
├─ PPU command buffer upload ($CAA8) │
|
||
├─ ConfigureHudAndPlayfieldPpu_D2CF ($CAB9) │
|
||
├─ OAM DMA ($4014 from $0200) │
|
||
├─ ConfigureHudAndPlayfieldPpu_C073 ($C073) │
|
||
├─ restore PPU mask from shadow │
|
||
├─ PollShipControllers ($CAC7) │
|
||
├─ TransformFlightState_CEE8 ($CEE8) │
|
||
├─ branch on Zp_3B: CAD4 or C3CC │
|
||
├─ CAD4: ReadShipControls_F880, CBD4, D6D8 │
|
||
├─ restore registers, re-enable NMI │
|
||
└─ rti │
|
||
|
||
IRQ ($CA9E) — self-loop (unused)
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Notes
|
||
|
||
- **Build verification:** `make` in `copilot/` produces `defender2_rebuilt.nes` that is **byte-identical** to `defender2.nes` and to the PRG bank of `Defender 2 (U).nes`.
|
||
- Vectors confirm NMI at $CAA1, RESET at $C983, and IRQ at $CA9E (self-loop, unused).
|
||
- The main loop (`CA6A`) is a tight NMI-synchronized loop; all substantive per-frame work happens either in the NMI handler or in the routines called from the main loop after the NMI sets `Zp_A2`.
|
||
- Many routines carry address-based names (`UpdateSideScrollingGameplay_C073`, `TransformFlightState_C109`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional.
|
||
- The disassembly includes inline `.byte` data blocks interleaved with code; boundaries are marked as classified by Pass 1.
|
||
- `locals.inc` provides zero-page names; `symbols.inc` provides the NES hardware register set.
|
||
- One build fix was required: a `bpl` branch to an undefined symbol `LookupTableEntry_FAF5` was replaced with `bne *-$1B` to produce the original `d0 e3` bytes (the disassembler had mislabeled `bne` as `bpl`). |