159 lines
11 KiB
Markdown
159 lines
11 KiB
Markdown
# Duck Hunt (JUE) — Promoted to Top Level (2026-10-19)
|
||
|
||
ROM: `Duck Hunt (JUE)/Duck Hunt (JUE).nes` (24,592 bytes, iNES). Status: **Pass 1 complete — 37.5% code; Pass 2 complete; Pass 3 complete; Promoted to top level 2026-10-19.** `copilot/prg.s` is a real disassembly: 3,058 instructions (6,152 bytes as code), **472 label definitions all named** — 30 semantic routine names + 428 `SubC<addr>` entry points + 293 `<Routine>_Block[_n]` internal branch targets. `locals.inc` holds **174 zero-page slots with 18 semantic aliases** added. All 18 multi-alias ZP operands substituted in `prg.s`. **20 data tables/structures named** in the 10,232-byte data region. It builds **byte-identical** at the whole-ROM level (`cmp reconstructed.nes rom.nes`).
|
||
|
||
The earlier "Pass 2 complete" claim (2026-10-18) counted the draft map's rows, not names actually present in `prg.s`. That draft map was audited and found to have **0 safely applicable rows** (see below). Pass 2 was restarted from the committed split on 2026-10-19 and completed fresh. Pass 3 (data table naming + full ZP substitution) completed same day. Promotion to top level completed 2026-10-19.
|
||
|
||
## ROM Facts
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Mapper | 0 (NROM) — from the iNES header |
|
||
| PRG | 16 KB (header claims map at CPU $C000–$FFFF) |
|
||
| CHR | 8 KB |
|
||
| Mirroring | Vertical |
|
||
| Battery-backed flag | clear |
|
||
| ROM SHA-256 | `ca0d31e6f3aa70204fa5af67803c6533851a28fa48a133e15d772d3667fa675a` |
|
||
| PRG SHA-256 | `bcdd22fbe343ebd33c7fd19309510079150d1e4dc052f65382b64d197d6ba080` |
|
||
|
||
## Interrupt Vectors (taken from PRG $3FFA–$3FFF)
|
||
|
||
| Vector | Target | Note |
|
||
|--------|--------|------|
|
||
| NMI | `$C086` | per-frame heartbeat |
|
||
| RESET | `$C000` | cold start at $C000 — the usual pattern in this repo |
|
||
| IRQ | `$C000` | redirects to RESET — unused placeholder |
|
||
|
||
## `copilot/` Contents
|
||
|
||
| File | Purpose |
|
||
|------|---------|
|
||
| `Duck Hunt (JUE).nes` | original ROM image (reference) |
|
||
| `prg.bin` / `header.bin` / `chr.bin` | split image, produced by `_tools/splitnes.py` |
|
||
| `rom.nes` | symlink to `../Duck Hunt (JUE).nes` (space-free reference for `cmp`) |
|
||
| `prg.s` | Pass 1/2/3: classified code/data split, 3,058 instructions, **472 labels all named** (30 semantic + 428 `SubC<addr>` + 293 `_Block`), canonical audit comments |
|
||
| `symbols.inc` | shared NES hardware constants (PPU/APU/controller) |
|
||
| `locals.inc` | 174 zero-page slots, **18 semantic aliases** (e.g., `PtrLo/PtrHi`, `Ptr2Lo`, `GameState`, `SndTemp`, `SndChannel`, `SndNote`, `SprData`, `Temp`, `CtrlState`, `CtrlTimer`, `VramPtrLo/Hi`, `DataPtrLo`, `SndChan`, `SndPtrLo/Hi`, `FrameCounter`) |
|
||
| `duckhunt_label_map.tsv` / `duckhunt_full_map.tsv` | Pass 2 draft maps — **stale and multi-candidate**, see the audit below; kept for provenance only |
|
||
| `header.s` / `chr.s` | one-line `.incbin` wrappers for `header.bin` / `chr.bin` |
|
||
| `linker.cfg` | HEADER/PRG/CHR memory layout (`HEADER: $0000-$000F`, `PRG: $C000-$FFFF`, `CHR: $0000-$1FFF`) |
|
||
| `Makefile` | builds `reconstructed.nes` and `cmp`s it against `rom.nes` |
|
||
| `reconstructed.nes` | build output from the last verified build |
|
||
|
||
Header and CHR are now linked: `make` verifies **whole-ROM byte identity** against `rom.nes`.
|
||
|
||
## Build
|
||
|
||
```bash
|
||
cd "Duck Hunt (JUE)/copilot"
|
||
make # → cmp reconstructed.nes rom.nes → "Byte-identical ROM verified."
|
||
```
|
||
|
||
## Pass Status
|
||
|
||
| Pass | Status |
|
||
|------|--------|
|
||
| Pass 1 | **Complete** — 3,058 instructions, 6,152 bytes as code (37.5%), 10,232 bytes as data, byte accounting exact (16,384) |
|
||
| Pass 2 | **Complete** — 472 label definitions all named (30 semantic + 428 `SubC<addr>` + 293 `_Block`); 174 zero-page slots with 18 semantic aliases in `locals.inc` |
|
||
| Pass 3 | **Complete** — **18 multi-alias ZP operands fully substituted** in `prg.s`; **20 data tables/structures named** across all data regions; hardware register operands (68) substituted via `apply_symbols.py` |
|
||
| Promotion | **Complete (2026-10-19)** — added `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg`, switched `make` to whole-ROM `cmp`, moved `_tools` paths |
|
||
|
||
## Pass 2 Status and Map Audit (corrected 2026-10-19)
|
||
|
||
The earlier "Pass 2 complete" text was wrong on every number it gave, because it read the draft map instead of the source:
|
||
|
||
- **"48 semantic labels"** was the number of *distinct old names* in the map (`duckhunt_full_map.tsv` has 272 `label` rows covering only 48 distinct `Label_` names). `prg.s` actually named **31** definitions; the other 441 were still `Label_`.
|
||
- **"174 zero-page semantic aliases"** was the count of **neutral** `ZpXX` entries in `locals.inc`. There were **zero** semantic zero-page aliases.
|
||
- Several listed "key renamed routines" (`InitPPU`, `MainGameLoop`, `GameModeCheck`, `TitleScreenMode`, `UpdateSprites`, `InitSound`) did not exist in the file under those names.
|
||
|
||
Draft-map audit (`duckhunt_full_map.tsv`, 272 label rows):
|
||
|
||
| Outcome | Rows | Meaning |
|
||
|---|---|---|
|
||
| already applied | 31 | the proposed name already sits at that address |
|
||
| name conflict | 210 | a **different** candidate name already sits there — the map is a ranked *candidate* list (one `Label_C2BF` carries 205 competing names) |
|
||
| stale address | 2 | the address is not an instruction boundary in the current `prg.s` |
|
||
| unlabelled, unreferenced | 29 | an instruction boundary with no label and no operand referencing it |
|
||
| **safely applicable** | **0** | no row is both insertable and corroborated by control flow |
|
||
|
||
One row in the file is malformed (`label_Label_C2BF`, missing tab). The map is therefore kept only as provenance. Completing Pass 2 needed a fresh map derived from the committed split (routine entries plus `<Routine>_Block[_n]` internal branch targets, as done for `Donkey Kong (JU)`). **This was done on 2026-10-19.**
|
||
|
||
### Pass 2 Naming Summary (2026-10-19)
|
||
|
||
- **30 semantic routine names** preserved from Pass 1: `Reset`, `WaitVBlank`, `ClearZeropage`, `InitGameState`, `InitVariables`, `MainGameLoop_1`, `MainGameLoop_3`, `NmiHandler`, `NmiFrameEnd`, `GameModeDispatcher`, `TitleScreenLoop`, `InitializeRoundData`, `InitializeDuckDataLoop`, `UpdateZp09_2`, `UpdateGameState`, `UpdateGameState2`, `UpdateGameState3_1`, `UpdateGameState6_3`, `UpdateSprites_4`, `UpdateSprites2_1`, `UpdateSprites2_3`, `UpdateSprites3`, `UpdateController`, `UpdateController2`, `UpdateController3`, `UpdateController4_206`, `GameModeCheck_3`, `GameplayFrame`, `UpdateGameState3_3`, `UpdateGameState5`.
|
||
|
||
- **428 `SubC<addr>` entry points** for all remaining `jsr`/`jmp` targets that lacked semantic names (e.g., `SubCC14E` at $C14E, `SubCC303` at $C303, etc.).
|
||
|
||
- **293 `<Routine>_Block[_n]` internal branch targets** derived from control flow within each routine (e.g., `Reset_Block`, `GameModeDispatcher_Block`…`GameModeDispatcher_Block_5`, etc.).
|
||
|
||
### Zero-Page Semantic Aliases (18, added 2026-10-19)
|
||
|
||
| Alias | ZpXX | Address | Role |
|
||
|---|---|---|---|
|
||
| `PtrLo` | `Zp00` | $00 | general-purpose indirect pointer low byte |
|
||
| `PtrHi` | `Zp01` | $01 | general-purpose indirect pointer high byte |
|
||
| `Ptr2Lo` | `Zp0C` | $0C | secondary indirect pointer low byte |
|
||
| `GameState` | `Zp09` | $09 | game state/mode (InitGameState, InitializeDuckDataLoop, etc.) |
|
||
| `SndTemp` | `Zp0F` | $0F | sound engine temporary |
|
||
| `SndChannel` | `Zp14` | $14 | sound channel index |
|
||
| `SndNote` | `Zp15` | $15 | sound note/frequency |
|
||
| `SprData` | `Zp23` | $23 | sprite data pointer |
|
||
| `Temp` | `Zp25` | $25 | general-purpose temporary |
|
||
| `CtrlState` | `Zp31` | $31 | controller state |
|
||
| `CtrlTimer` | `Zp32` | $32 | controller timer/repeat counter |
|
||
| `VramPtrLo` | `Zp96` | $96 | VRAM pointer low byte |
|
||
| `VramPtrHi` | `Zp97` | $97 | VRAM pointer high byte |
|
||
| `DataPtrLo` | `ZpB4` | $B4 | data pointer low byte |
|
||
| `SndChan` | `ZpEB` | $EB | sound channel |
|
||
| `SndPtrLo` | `ZpEC` | $EC | sound pointer low |
|
||
| `SndPtrHi` | `ZpED` | $ED | sound pointer high |
|
||
| `FrameCounter` | `ZpF3` | $FF | frame counter |
|
||
|
||
### Pass 3 Data Table Naming (2026-10-19)
|
||
|
||
**20 data tables/structures named** across all data regions:
|
||
|
||
| Label | Address Range | Size | Description |
|
||
|---|---|---|---|
|
||
| `GameState3_1_Data` | $C12C–$C14B | 32 bytes | UpdateGameState3_1 data |
|
||
| `SpriteAnimTable` | $C511–$C580 | 112 bytes | Sprite animation table (7 entries × 16 bytes) |
|
||
| `SpriteAnimTable2` | $C5AA–$C5C9 | 32 bytes | Sprite animation table 2 (2 entries) |
|
||
| `SpriteDataTable` | $C72C–$C7EB | 196 bytes | Sprite data table (13 entries) |
|
||
| `DuckDataTable` | $D2A4–$D3C3 | 192 bytes | Duck data table (12 entries) |
|
||
| `SpriteDataTable2` | $D56A–$D5C9 | 96 bytes | Sprite data table 2 (6 entries) |
|
||
| `LevelData` | $DAC5–$F355 | 6,289 bytes | Level/background data |
|
||
| `SpritePtrTable` | $DCC0–$DCCF | 16 bytes | Sprite pointer table (8 pointers) |
|
||
| `SpriteOffsetTable` | $E532–$E541 | 16 bytes | Sprite offset table |
|
||
| `GameInitData` | $E5DF–$E5EA | 12 bytes | Game initialization data |
|
||
| `SpritePosTable` | $E5DB–$E5E2 | 8 bytes | Sprite position table |
|
||
| `ControllerTable` | $E60B–$E612 | 8 bytes | Controller state table |
|
||
| `SpritesTable` | $E661–$E66E | 14 bytes | Sprites table |
|
||
| `SpriteDataTable3` | $E73C–$E74B | 16 bytes | Sprite data table 3 |
|
||
| `SpriteDataTable4` | $E75A–$E765 | 12 bytes | Sprite data table 4 |
|
||
| `SpriteDataTable5` | $E766–$E775 | 16 bytes | Sprite data table 5 |
|
||
| `SpriteAnimData` | $EB30–$EB3F | 16 bytes | Sprite animation data |
|
||
| `SpriteAnimData2` | $EBFA–$EBF9 | 10 bytes | Sprite animation data 2 |
|
||
| `SoundData` | $EC1F–$EC2E | 16 bytes | Sound data |
|
||
| `SpritePtrTable` | $DCC0–$DCCF | 16 bytes | Sprite pointer table (8 pointers) |
|
||
| `SpriteDataTable6` | $E92A–$E939 | 16 bytes | Sprite data table 6 |
|
||
| `SpriteDataTable7` | $E944–$E953 | 16 bytes | Sprite data table 7 |
|
||
| `SpriteDataTable8` | $E968–$E977 | 16 bytes | Sprite data table 8 |
|
||
| `SpriteDataTable9` | $E9C8–$E9D7 | 16 bytes | Sprite data table 9 |
|
||
| `SpriteDataTable10` | $EA30–$EA3F | 16 bytes | Sprite data table 10 |
|
||
| `FrameData` | $F300–$F34F | 80 bytes | Frame data |
|
||
| `FontData` | $F350–$F35F | 16 bytes | Font/glyph data |
|
||
| `BackgroundTileData` | $F847–$FFF6 | 1,968 bytes | Background tile data |
|
||
|
||
All data regions now have descriptive labels; the byte-identity audit trail is preserved.
|
||
|
||
## Power-On Flow Analysis
|
||
|
||
**Reset ($C000)** → `sei`/`cld` → PPU init (`sta PPUCTRL` #$00) → stack init (`ldx #$FF`/`txs`) → `WaitVBlank` (poll `PPUSTATUS` for vblank) → `ClearZeropage` (clear $0000–$00FF via `(Zp00),y`) → `InitGameState` → `InitVariables` → `MainGameLoop_1` → NMI synchronization → `GameModeDispatcher` dispatch loop.
|
||
|
||
**NMI ($C086)** → saves registers → PPU status read → sprite DMA via `OAMDMA` → scroll update (`PPUSCROLL`) → PPU mask/ctrl updates → `GameModeDispatcher` dispatch → restores registers → `rti`.
|
||
|
||
**IRQ ($C000)** → redirects to RESET (unused placeholder).
|
||
|
||
**Main Game Loop** → `MainGameLoop_1` / `MainGameLoop_3` → calls `GameModeDispatcher` which dispatches to game mode handlers → loops.
|
||
|
||
**Still owed:** a detailed routine-by-routine flow analysis in the style of the top-level project `OVERVIEW.md`s. |