Files
disasm/Defender 2 (U)/OVERVIEW.md
T

254 lines
13 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.
# 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`).