Pass 3 named the 10 data regions in prg.s, then the project was promoted
out of _wip/ so `make` now proves header + PRG + CHR against the original
ROM instead of only the PRG bank.
Data regions named:
LevelPtrTable $C000-$C03B (60 B) level header data
ObjectPtrTable $C03C-$C043 (8 B) 4 pointer pairs
LevelDataPtrTable $C044-$C79D (1882 B) level data pointers
SpriteInitPtrTable labelled at $C4B0 see residue note
UnreachedCode_D650 $D650-$D696 (71 B) unreached code
UnreachedCode_E144 $E144-$E165 (34 B) unreached code
MusicDataTable $F55B-$FA47 (1261 B) music/sound data
APUDataTable $FAE6-$FB74 (143 B) APU pulse/noise tables
SfxDataTable $FE59-$FFEF (407 B) sound-effect data
VectorTable $FFF6-$FFFF (10 B) padding + NMI/RESET/IRQ
Promotion (per the AGENTS.md checklist):
- git mv "_wip/Donkey Kong (JU)" "Donkey Kong (JU)"
- add copilot/rom.nes -> ../Donkey Kong (JU).nes symlink
- add header.s / chr.s (one-line .incbin of header.bin / chr.bin)
- linker.cfg gains HEADER ($0000/16 B) and CHR ($0000/8 KB)
- Makefile switched from `cmp reconstructed_prg.bin prg.bin` to
`cmp reconstructed.nes rom.nes`
- no _tools path change (this Makefile never referenced _tools/)
and no `make regen` pipeline exists for it
Doc corrections:
- project OVERVIEW.md said Mirroring Vertical; header byte 6 = $00
means Horizontal (the root index was right) — fixed
- root OVERVIEW.md: row 18 now BYTE-IDENTICAL, tier paragraph and
summary table refreshed (they had drifted around the Lunar Pool and
Urban Champion promotions), new per-project detail entry
- counts corrected to 19 top-level / 17 _wip in OVERVIEW.md,
TODO.md and AGENTS.md
Residue recorded rather than guessed:
- Pass 2 is partial: 919 Label_ definitions remain (214 semantic
labels applied); noted in TODO.md so nobody reads the promotion as
full semantic naming
- SpriteInitPtrTable's label sits at $C4B0: the routine at $C807 reads
$C4A7,x/$C4A8,x, but that table starts mid-row and a label there
would split the canonical 16-byte audit-comment line
Verification:
make clean && make -> "Byte-identical ROM verified."
verify_pass1.py -> 19/19 top-level build, Donkey Kong (JU) OK
(6014 instr / 12505 B), all comments canonical
Doc-drift sweep: build tiers, mirroring, and stale status tails
Reconciled the docs against verified reality after the Lunar Pool / Urban
Champion / Donkey Kong (JU) promotions and the 2026-10-19 audit-comment
standardization. No source behaviour changed except the two build fixes below.
Build tier (whole-ROM vs PRG-only)
- AGENTS.md: the 17 `_wip/` projects are real disassemblies at the PRG-build
tier, not "skeletons"; header.s/chr.s/rom.nes are promotion additions;
verify_pass1.py covers the top-level projects.
- Root OVERVIEW.md: `_wip` prose, audit-comment narrative and summary table
refreshed (37.5% figure, SEEDED vs OK wording, re-trace false alarm).
- Promoted projects whose build blocks still claimed `make` emits
`reconstructed_prg.bin` compared to `prg.bin` (Balloon Fight, Baseball,
CIRCUS, Clu Clu Land, Excitebike, NES PowerPad, Pac-Man (USA), Tennis):
now describe the whole-ROM `reconstructed.nes` vs `rom.nes`.
- Lunar Pool / Urban Champion / CIRCUS also had stale `cd "_wip/..."`
build paths and "Promotion" items; retargeted to their top-level folders.
Mirroring (header byte 6 bit 0: 0 = Horizontal, 1 = Vertical)
- Corrected DK Jr. prose "horizontal mirroring (flag 6 = $01)" -> vertical,
and both `_wip/Pac-Man` variant notes "Both use vertical mirroring" ->
horizontal (both are byte-6 = $00).
- Re-audited every project OVERVIEW: 0 rows and 0 prose claims now disagree.
Stale status tails
- Retitled/restatused the `_wip` OVERVIEWs to their real `prg.s` (Golf,
Ice Climber (U), Lode Runner, Millipede, Pac-Man Tengen, Pac-Man (U),
Popeye 0/1, Raid, Raid a2, Hogan's Alley, Mario Bros); Golf's
skeleton-era contents rows and Next Steps corrected.
- Dropped the skeleton-era "## Next Steps (per `PROMPTS.md`)" heading and
the "(from the skeleton)" wording across all project OVERVIEWs.
- Rewrote Lunar Pool / Urban Champion Next Steps (both already Pass 1-3
complete and promoted; their tails still described pending Pass 2 and
promotion); fixed root row 13 (Wild Gunman) and row 20 (Duck Hunt).
Also lands two code fixes carried with this sweep:
- `_wip/Hogan's Alley (JU)`: $E9F1-$E9FD and $EA13-$EA14 reclassified as
data with canonical audit comments (the seeded build had decoded a data
table base as code, breaking the build since 2ae09f7). `make` ->
BYTE-IDENTICAL again; reconstructed_prg.bin/.lbl refreshed.
- `_wip/Pyramid (U)`: `Label_D72E` -> `NMI:` (byte-identical).
Verification:
make (Hogan's Alley, Pyramid) -> BYTE-IDENTICAL, BYTE-IDENTICAL
_tools/verify_pass1.py -> 19/19 build, all canonical,
"No truncated or unfaithful Pass 1"
mirroring audit (rows + prose) -> 0 mismatches
Donkey Kong (JU): complete Pass 2 — all 919 Label_ definitions named
The last non-zero Label_ residue in the repo is now gone. Every one of the
919 address-derived definitions in copilot/prg.s is replaced:
* 319 routine entry points (jsr/jmp targets):
- 102 named from observed behaviour (LoadSpriteInitPointerToZp00 $C807,
CopyVramDataToBuffer $CBDF, CheckSpriteListCollision $D8AD,
UpdateRandomSeed $F4ED, ReadControllers $F50E, UpdateSoundEngine $FA48, ...)
- 217 left as SubC<addr> — honest residue where the body is too
context-dependent to name without guessing (Golf: 108, Mario Bros: 223)
- the 77 already-named entries untouched
* 600 internal branch targets named <Routine>_Block / <Routine>_Block_n in
address order within their owning routine (DK Jr./Balloon Fight convention).
Ownership is control-flow containment, so a loop that physically precedes
its own entry lands correctly (FlushPpuBuffer_Block $F1EC under $F228).
* Two proposal collisions disambiguated by indexing zero-page slot:
UpdateObjectListZp5D/DispatchObjectStateZp5E ($DA16/$DA4C) vs
UpdateObjectListZpAE/DispatchObjectStateZpAF ($E1E9/$E250).
jsr/jmp operands stay numeric (Wild Gunman style); only branch operands are
symbolic. Applied with _tools/pass2.py apply (919 defs + 2,622 refs rewritten,
comments re-aligned); the ROM still rebuilds byte-identically and
verify_pass1.py reports 19/19 canonical with no truncated split.
Docs synced: project OVERVIEW.md gains a Pass 2 Details section, root
OVERVIEW.md rows + conventions, TODO.md section 4 closed. dk_full_map.tsv now
records the complete applied map (its zp rows remain unapplied proposals).
_wip status correction: five false "Pass 2 complete" claims + map audit
The `_wip` tier's "Pass 2 complete" headers counted their draft maps' rows, not
the names actually present in `prg.s`, and described the count of *neutral*
`ZpXX` slots in `locals.inc` as "zero-page semantic aliases". Verified against
the sources and corrected in the root inventory rows, the project OVERVIEWs and
TODO:
project named/total Label_ left drafted map
Pinball (JU) 180 / 681 501 210 rows, 179 applied
Ice Climber (JE) 76 / 973 897 255 rows, 76 applied
Donkey Kong Jr. Math 64 / 1035 971 243 rows, 62 applied
Duck Hunt (JUE) 31 / 472 441 272 rows, 31 applied
Ice Climber (U) 2 / 973 971 none - Pass 2 not started
No `_wip` project is Pass 2 complete. Ice Climber (U) was the worst case: the
2026-10-19 sweep itself restated it "Pass 2 complete" while describing its 971
`Label_` definitions in the same paragraph - a copy-paste from (JE); (U) has no
map file at all.
Drafted maps audited row by row against the committed split: they are not
applicable. 100/243 (DK Jr. Math), 96/255 (Ice Climber (JE)) and 17/210
(Pinball) rows point at addresses that are not instruction boundaries - they
were built against an older code/data split - while most of the rest are
unlabelled boundaries that no operand references, or (Duck Hunt) competing
candidate names for one label (`Label_C2BF` alone carries 205). Rows that are
both insertable and corroborated by control flow: 0 in every map. Recorded as
stale and kept for provenance; Pass 2 here needs a fresh map derived from the
committed split.
Also corrected while verifying:
- Mario Bros entry counts (docs said 252 / 90 + 165 / 477 sites / 156 neutral
slots; the file holds 262 distinct `jsr`/`jmp` targets = 39 semantic + 223
`SubC<addr>`, 478 sites, 151 slots).
- root `_wip` prose line/label ranges (4,482-17,180 lines, 192-971 `Label_`,
11,126 total) and the Ice Climber code figure (74.6% / 6,112 instr, not
74.7% / 6,113).
- TODO §4 re-scoped to the promoted tier, with the `_wip` residue table added.
Verification: `make` across all 17 `_wip` projects -> BYTE-IDENTICAL (17/17);
`_tools/verify_pass1.py` -> 19/19 build, all canonical, "No truncated or
unfaithful Pass 1 found". No source bytes changed.
Balloon Fight (JU): apply locals.inc/symbols.inc names to prg.s (Pass 3)
Substitutes symbol names for 922 numeric operands: 125 hardware registers
(symbols.inc) and 797 of 813 zero-page/RAM operands (locals.inc). The 15
multi-alias addresses were resolved per routine; 16 operands stay numeric
because $13 is used there as a plain high-byte scratch beside Temp12 and
none of its three aliases fits.
- _tools/apply_symbols.py: the substitution tool (dry run by default;
--sites supplies per-routine choices for multi-alias addresses, --report
lists the residue). Only the operand token is rewritten, so the
`; CPU:$.... PRG:.... XX XX` audit comments survive byte for byte.
- prg.s: operand tokens only. make prints "Byte-identical ROM verified."
- OVERVIEW.md (project): instruction quotes now match prg.s, new
alias-reuse table, and section 10 rewritten from the stale "497 Label_"
prose to the real naming state and the 16-site residue.
- OVERVIEW.md (root), TODO.md, AGENTS.md: status, counts, tool list.
verify_pass1.py: 19/19 OK, all audit comments canonical.
Bomberman (U): apply locals.inc/symbols.inc names to prg.s (Pass 3)
Substitutes 362 zero-page operands in copilot/prg.s with the names already
declared in copilot/locals.inc: 354 from single-name addresses and 8 from the
two multi-alias addresses, resolved per site:
$E0/$E1 IndirectPtrLo/Hi in the RAM-clear loop
(InitializeSystemAndRunMainLoop -> Main_WriteSoundDispatchIndex_C317,
which writes $00 across $0100-$07FF via (ptr),y),
SoundDelay0 (the base of the per-channel delay table, indexed by X)
in the sound engine;
No symbols.inc substitution was needed: copilot/prg.s contains no numeric
hardware operands ($2000-$4017) at all. All 362 operands were already written
with two hex digits, so no ca65 `a:` prefix was required.
make -> Byte-identical ROM verified. Operand token only; mnemonics, spacing and
the trailing `; CPU:$.... PRG:.... XX XX` audit comments are untouched.
Remaining numeric operands (758) address bytes that no .inc file names:
505 zero-page (133 addresses), 171 RAM $0100-$07FF (46 addresses) and 82
table reads in $8000+ (54 addresses).
Bomberman (U): substitute 24 in-file table labels at ROM operand sites
Uses the new `--labels` option of `apply_symbols.py` with the
ld65 `-Ln` output (`reconstructed.lbl`). Only labels that prg.s
actually defines and that live in the PRG (+) are considered,
so CHR/header segment labels cannot leak in.
24 numeric operands (`lda ,x`, `adc ,y`, etc.) now read
`lda SystemPaletteData,x`, `adc GameplayObjectClassTable,y`, etc.
All 24 point at exact label addresses; the remaining 58 ROM-space
numeric operands point at interior bytes or unlabelled tables and
stay numeric.
make -> Byte-identical ROM verified.
Bomberman (U): update docs for Pass 3 symbol application
- Project OVERVIEW.md: corrected source file from main.s to prg.s/header.s/chr.s;
output name from reconstructed_bomberman.nes to reconstructed.nes;
rewritten §10 Zero-Page/RAM Symbols table to match locals.inc (with aliases
and unnamed residue documented);
fixed §4 step 1/4 to use IndirectPtrLo/Hi and SoundDelay0 names;
fixed §11 InesHeader location (now header.s);
added §2 Pass 3 summary table;
added §13 notes on legacy main.s and Main_WriteSoundDispatchIndex_C317 misnomer.
- Root OVERVIEW.md §3: updated source line count (7,553), file list, and added
Pass 3 summary.
Both builds verified byte-identical.
Pass 3 symbol application to all 19 top-level projects (2026-10-19)
Ran _tools/apply_symbols.py --write on all 19 promoted projects to substitute
symbolic names from symbols.inc (hardware registers) and locals.inc (zero-page
RAM aliases, forward references) into prg.s operands. The tool preserves the
canonical audit comments (; CPU:$XXXX PRG:XXXX AA BB) and preserves byte
identity — all projects still rebuild byte-identical to their original ROMs.
Per-project operand substitutions:
Baseball (UE) 121 (85 single + 36 hardware)
Tennis (JU) 18 (18 single)
Pac-Man (USA) (Namco) 524 (524 single; 727 multi-alias residues)
CIRCUS 118 (38 single + 80 hardware; 23 multi-alias residues)
Wild Gunman (U) 76 (11 single + 65 hardware)
Joypad Test Cartridge (U) 47 (47 hardware)
NES PowerPad Test Card (U) 89 (89 hardware)
MUSCLE (U) 220 (91 single + 129 hardware)
Lunar Pool (U) 45 (18 single + 27 hardware)
Donkey Kong (JU) 75 (17 single + 58 hardware)
Urban Champion (JU) 70 (4 single + 66 hardware)
Already had symbols applied: Balloon Fight, Bomberman, Burger Time,
Clu Clu Land, Defender 2, Donkey Kong 3, Donkey Kong Jr.
Skipped: Excitebike (JU) — inline prg.s definitions conflict with locals.inc
values and source operands don't match ROM byte encodings; automatic
substitution breaks byte identity.
Documentation updated:
OVERVIEW.md — added Pass 3 symbol notes to all 19 project entries,
added Urban Champion as section #36
TODO.md — current state now reflects complete symbol application across
the promoted tier
Verification: python3 _tools/verify_pass1.py reports all 19 projects OK/SEEDED,
all canonical comments, no truncated or unfaithful Pass 1.
367 lines
19 KiB
Markdown
367 lines
19 KiB
Markdown
# Tennis (JU) — Game Flow from Power-On
|
||
|
||
This document describes the execution flow of the disassembled Tennis (JU) 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 `prg.bin`.
|
||
|
||
---
|
||
|
||
## 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` (9,268 lines) |
|
||
| Output | `reconstructed.nes` (whole ROM: 16-byte header + 16,384-byte PRG + 8,192-byte CHR) |
|
||
|
||
### Interrupt vectors (CPU $FFFA–$FFFF)
|
||
|
||
| Vector | Address | Handler |
|
||
|--------|---------|---------|
|
||
| NMI | $C07D | `NmiHandler` |
|
||
| RESET | $C000 | `Reset` |
|
||
| IRQ / BRK | $C000 | `Reset` (reused — unused) |
|
||
|
||
---
|
||
|
||
## 2. Build Status
|
||
|
||
`make` in `copilot/` assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `prg.bin`.
|
||
|
||
**Verification result: BYTE-IDENTICAL** — `reconstructed.nes` matches `rom.nes` exactly (header + PRG + CHR).
|
||
|
||
---
|
||
|
||
## 3. Reset — CPU $FFFC → $C000
|
||
|
||
**`Reset`** ($C000)
|
||
|
||
On power-on or reset:
|
||
|
||
1. `cld` — clear decimal mode.
|
||
2. `sei` — disable maskable interrupts.
|
||
3. **Wait for VBlank** — poll `PPUSTATUS` bit 7 (`@Flow_C002`).
|
||
4. `PPUCTRL=$00`, `PPUMASK=$00` — disable rendering and NMI.
|
||
5. Stack pointer = $FF.
|
||
6. `X = FrameCounter` ($27), then `Y=$06`, `WorkPtrHi=$02`, `WorkPtrLo=$00`, `A=$00`.
|
||
7. **Clear RAM** — `sta (WorkPtrLo),y` loop decrementing `WorkPtrHi` from $02 down, effectively clearing $0000–$02FF.
|
||
8. `X` is 0 at this point; if non-zero (it never is on entry), skip; else `X=$5F`.
|
||
9. `stx $0500` — set $0500 to $5F (or 0).
|
||
10. `jsr ClearNametables` ($C195) — clear nametable memory.
|
||
11. `jsr HideAllSprites` ($C1A5) — hide all sprites by writing $F4 to OAM.
|
||
12. `PPUSCROLL=$00` twice; `GameActiveFlag=$01`.
|
||
13. `APUSTATUS=$0F` — enable all four APU channels.
|
||
14. `PPUCTRL=$90`, `PpuCtrlShadow=$90` — enable NMI, $2000 base.
|
||
15. `PpuMaskShadow=$06`.
|
||
16. Falls into **`MainLoop`** ($C04F) — the permanent foreground loop.
|
||
|
||
---
|
||
|
||
## 4. Main Loop — `MainLoop` ($C04F)
|
||
|
||
After reset, control enters the infinite foreground loop:
|
||
|
||
```
|
||
MainLoop ($C04F):
|
||
NmiWorkComplete ($30) = $00
|
||
if GameActiveFlag ($38) != 0:
|
||
if GameStateStep ($3E) == 1:
|
||
jsr RunAudioEngine ($FFF7)
|
||
Flow_C060:
|
||
jsr UpdateTimers ($C375)
|
||
jsr UpdateGameState ($C0AA)
|
||
NmiUpdatesEnabled ($32) = $01
|
||
Flow_C06A:
|
||
if NmiWorkComplete ($30) != 0:
|
||
FrameCounter ($27)++
|
||
NmiUpdatesEnabled ($32) = $00
|
||
jmp MainLoop
|
||
else:
|
||
jsr RollRng ($C456)
|
||
jmp Flow_C06A
|
||
```
|
||
|
||
The main loop is **NMI-synchronized**: it waits for `NmiWorkComplete` ($30) to become non-zero (set by NMI each frame), then advances the frame counter, clears the update-enabled flag, and loops. If NMI work is not complete, it rolls the RNG and retries.
|
||
|
||
---
|
||
|
||
## 5. NMI / Frame Heartbeat — `NmiHandler` ($C07D)
|
||
|
||
The NMI fires once per frame:
|
||
|
||
1. **Save registers** (A, X, Y on stack).
|
||
2. **OAM DMA**: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014) — copy $0200–$02FF to sprite RAM.
|
||
3. If `NmiUpdatesEnabled` ($32) == 0, skip to `@Flow_C0A0`.
|
||
4. Otherwise:
|
||
- `jsr WritePalette` ($C1B5) — conditional palette upload based on `PaletteUpdateMode` ($3C).
|
||
- `jsr WritePpuBuffer` ($C2EC) — flush pending PPU command buffer at $0590.
|
||
- `jsr SetScroll` ($C3FD) — write scroll from $1E/$1F to PPUSCROLL.
|
||
- `jsr ReadControllers` ($C2A4) — read both controller ports.
|
||
5. `NmiWorkComplete ($30) = $01` — signal main loop that the frame update is done.
|
||
6. `NmiTriggerFlag ($33) = $01`.
|
||
7. Restore registers, `rti`.
|
||
|
||
The label `IrqHandler` appears at the same `rti` address — IRQ/BRK vector points here, but it's unused (same as Reset).
|
||
|
||
---
|
||
|
||
## 5. Game State Machine — `UpdateGameState` ($C0AA)
|
||
|
||
Called each main-loop iteration:
|
||
|
||
```
|
||
UpdateGameState ($C0AA):
|
||
if GameActiveFlag ($38) == 0:
|
||
Flow_C0B1:
|
||
if ControllerHeld ($1A) & START ($10):
|
||
if StartButtonLatch ($34) != 0 → skip
|
||
else: StartButtonLatch=$01
|
||
Y = GameState ($3D)
|
||
if GameState == 7 → Flow_C0EA
|
||
if GameState == 4 → Flow_C0CC
|
||
if GameState == 6 → skip
|
||
else → Flow_C0FC
|
||
Flow_C0FC:
|
||
DispatchInlineAddressTable ($C38B) with GameState index
|
||
→ GameStateHandlerTable ($C101)
|
||
else (GameActiveFlag == 0):
|
||
Flow_C119:
|
||
if ControllerHeld & $30 (START|SELECT):
|
||
if bit $10 set → Flow_C134
|
||
else: GameActiveFlag=$00, GameState=$00, PendingStateFlag=$00
|
||
jsr MatchRoutine_CBCB, jsr DisableRendering
|
||
GameMode=$SelectedGameMode ($E0), rts
|
||
Flow_C134: (complex state-transition logic with StateTransitionFlag, GameMode, etc.)
|
||
Flow_C180:
|
||
GameStateStep ($3E) → DispatchInlineAddressTable → GameStateStepHandlerTable ($C185)
|
||
```
|
||
|
||
### `MainStateHandlerTable` ($C101) — indexed by `GameState` ($3D) as a byte offset (even offsets $00–$16):
|
||
|
||
| Offset | Handler | Description |
|
||
|--------|---------|-------------|
|
||
| $00 | `GameState00_InitializeMatch` ($C70A) | Match initialization |
|
||
| $02 | `GameState01_WaitForServe` ($C797) | Wait for serve input |
|
||
| $04 | `GameState02_PrepareRally` ($C7B7) | Prepare rally |
|
||
| $06 | `GameState03_RunRally` ($C7C6) | Run rally |
|
||
| $08 | `GameState04_PointEnded` ($C907) | Point ended |
|
||
| $0A | `GameState05_UpdateScore` ($CA07) | Update score |
|
||
| $0C | `GameState06_AdvanceGame` ($C9DE) | Advance game |
|
||
| $0E | `GameState07_PauseOrTransition` ($C42B) | Pause/transition (RTS) |
|
||
| $10 | `GameState08_WaitForTimer` ($CA28) | Wait for timer |
|
||
| $12 | `GameState09_RestorePreviousState` ($CA31) | Restore state |
|
||
| $14 | `GameState0A_ResumePreviousState` ($CA3A) | Resume state |
|
||
| $16 | `GameState0B_FinishTransition` ($CA5B) | Finish transition |
|
||
|
||
### `GameStateStepHandlerTable` ($C185) — indexed by `GameStateStep` ($3E):
|
||
|
||
| Offset | Handler |
|
||
|--------|---------|
|
||
| $00 | `ModeStep00_DrawSelectionScreen` ($CA87) |
|
||
| $02 | `ModeStep01_Countdown` ($CAC9) |
|
||
| $04 | `ModeStep02_InitializeSelection` ($CADD) |
|
||
| $06 | `ModeStep03_UpdateSelection` ($CAE4) |
|
||
| $08 | `ModeStep04_ConfirmSelection` ($CAEE) |
|
||
| $0A | `ModeStep05_StartDemoOrMatch` ($CAF4) |
|
||
| $0C | `ModeStep06_CompleteSetup` ($CC04) |
|
||
|
||
### Key state handlers:
|
||
|
||
- **`GameState00_InitializeMatch`** ($C70A): First-time init — clear nametable, render routine, palette mode, set `PendingStateFlag`. Subsequent calls advance through sub-states ($87/$88 counter), render sprites, enable rendering, set `GameState=$07`, call `MatchRoutine_CBCB`.
|
||
- **`GameState01_WaitForServe`** ($C797): Disable rendering, clear nametable, render court + score, zero point winners, clear animation flags, set ball trajectory flags, advance `GameState`.
|
||
- **`GameState02_PrepareRally`** ($C7B7): Enable rendering, clear PPU mask shadow bit 4, hide sprites, render routine, advance `GameState`.
|
||
- **`GameState03_RunRally`** ($C7C6): Render routine, set `PpuBufferReady`, `MatchRoutine_C7E7` (copy initial object state), `PhysicsRoutine_D7E1`, handle serving player, `EngineRoutine_C436`, advance `GameState`.
|
||
|
||
---
|
||
|
||
## 6. NMI-Driven Per-Frame Work
|
||
|
||
The NMI handler (called every VBlank) performs:
|
||
|
||
| Step | Routine | Address | Purpose |
|
||
|------|---------|---------|---------|
|
||
| OAM DMA | — | $C084–$C089 | Copy $0200–$02FF to sprite RAM |
|
||
| Palette update | `WritePalette` | $C1B5 | Conditional palette upload ($3F00/$3F10) |
|
||
| PPU buffer flush | `WritePpuBuffer` | $C2EC | Write pending PPU commands from $0590 buffer |
|
||
| Scroll | `SetScroll` | $C3FD | Write scroll X/Y from $1E/$1F |
|
||
| Controller read | `ReadControllers` | $C2A4 | Read both ports, build held/newly-pressed |
|
||
|
||
The main loop calls `UpdateGameState` ($C0AA) which dispatches to the active game state handler, then advances timers and audio.
|
||
|
||
---
|
||
|
||
## 7. Object / Sprite System
|
||
|
||
- **OAM buffer** at $0200–$02FF, written via `WriteMetaspriteToOam` ($C440).
|
||
- **Object data pointers**: `ObjectDataPointerLo/Hi` ($A8/$A9), `ObjectDataOffset` ($AA), `ActiveObjectIndex` ($AB).
|
||
- **Animation**: `AnimationFrame` ($B0), `AnimationTimer` ($B1), `PoseFrameTimer` ($B3), `PlayerAnimationId` ($B4), `PoseSequenceOffset` ($B5), `PlayerPoseId` ($B6), `PlayerPoseFlags` ($B7).
|
||
- **Racket**: `RacketTargetX` ($B8), `RacketTargetY` ($B9), `RacketContactFlags` ($BA), `RacketAimOffset` ($BB).
|
||
- **Player physics**: `PlayerMotionControl` ($60/$65 for P1/P2), X/Y position + subpixel ($61–$69), target coordinates ($80–$83), action type/state ($84–$86), facing direction ($87), position ($88–$89).
|
||
- **Ball**: `BallMotionControl` ($6A), X/Y position + subpixel ($6B–$6E), height ($6F/$70), target offset ($71), vertical velocity ($72/$73), state flags ($7A), bounce state ($7B), contact state ($8F), trajectory flags ($8D), collision flags ($8E).
|
||
|
||
---
|
||
|
||
## 8. Audio System
|
||
|
||
**`RunAudioEngine`** ($FFF7) — jumps to `AudioEngineUpdate` ($F8E0). Called from MainLoop when `GameStateStep == 1` and `GameActiveFlag != 0`.
|
||
|
||
**`InitializeAudioEngine`** ($E1F8):
|
||
- Clear audio request pointer ($F0/$F1 = $0006), channel table pointer ($F2/$F3 = $0620), sequence table pointer from ROM $F100/$F101.
|
||
- `audioWorkValue=$40`, `APUSTATUS=$1F`, `APUFRAMECNT=$C0`.
|
||
- Clear 16 bytes through ($F0), clear 16 entries at 8-byte stride through ($F2).
|
||
|
||
**`InitializeAudioChannels`** ($EED0):
|
||
- Clear audio channel flags, scan 16 eight-byte slots through ($F2).
|
||
- Slots with values $01–$04 activate a channel: copy 4-byte parameters through indirect pointer $F6.
|
||
|
||
**Audio data** at $FF00–$FFF5 (period/mask tables, sequences, instruments), $F450–$F49A (runtime command data), AudioEventSequenceData at $FDFA–$FFEA.
|
||
|
||
Audio RAM symbols ($F0–$FF): `audioRequestPtr`, `audioChannelTablePtr`, `audioSequenceTablePtr`, `audioWorkPtr`, `audioWorkValue`, `audioChannelFlags`, `audioRequestIndex`, `audioChannelOffset`, `audioIndirectPtr`, plus channel-specific effect timers ($F5–$F6), sequence pointers/indices ($F7–$FA), stream position ($FB), event priority ($FC), request flags ($FD–$FF).
|
||
|
||
---
|
||
|
||
## 9. Zero-Page / RAM Symbols (key subset)
|
||
|
||
| Address | Symbol | Use |
|
||
|---------|--------|-----|
|
||
| $01–$02 | WorkPtrLo/Hi | General indirect pointer |
|
||
| $09 | PpuCtrlShadow | $2000 mirror |
|
||
| $0A | PpuMaskShadow | $2001 mirror |
|
||
| $1A | ControllerHeld | Controller buttons held |
|
||
| $1B | Zp1B | Previous controller state |
|
||
| $20 | Zp20 | Timer counter |
|
||
| $25 | StateTimer | State countdown timer |
|
||
| $27 | FrameCounter | Frame tick counter |
|
||
| $30 | NmiWorkComplete | NMI frame-sync flag |
|
||
| $31 | PpuBufferReady | PPU buffer ready flag |
|
||
| $32 | NmiUpdatesEnabled | NMI updates enabled flag |
|
||
| $33 | NmiTriggerFlag | NMI trigger flag |
|
||
| $34 | StartButtonLatch | Start button debounce latch |
|
||
| $35 | StateTransitionFlag | State transition flag |
|
||
| $38 | GameActiveFlag | Game active flag |
|
||
| $39 | GameMode | Current game mode |
|
||
| $3C | PaletteUpdateMode | Palette update mode |
|
||
| $3D | GameState | Main game state |
|
||
| $3E | GameStateStep | Game state step/substep |
|
||
| $3F | DemoCounter | Demo counter |
|
||
| $43 | PendingStateFlag | Pending state flag |
|
||
| $44 | StateSubstep | State substep |
|
||
| $46 | PlayerCourtOrder | Player court order |
|
||
| $48–$4B | ModeAnimTimer/Frame A&B | Mode animation timers/frames |
|
||
| $4C | ServingPlayer | Serving player |
|
||
| $4D | ModeAnimSubframe | Animation subframe |
|
||
| $4E | MessageTimer | Message timer |
|
||
| $4F | MessagePlayerIndex | Message player index |
|
||
| $50 | PointWinner | Point winner |
|
||
| $51–$53 | Player/2 Point/Game Score | Score values |
|
||
| $57–$5D | Score-related state | Score display/update state |
|
||
| $5E | PendingGameState | Pending game state |
|
||
| $60–$6E | Player/Ball motion state | Player and ball physics |
|
||
| $78–$7F | Animation/ball state | Animation flags, ball state/collision |
|
||
| $80–$8F | Target/action/collision state | Player targets, action state, collision flags |
|
||
| $A8–$AA | ObjectDataPointerLo/Hi, Offset | Object data pointers |
|
||
| $AB | ActiveObjectIndex | Active object index |
|
||
| $B0–$C9 | Animation/pose/racket state | Animation frames, pose, racket targets |
|
||
| $CA–$CC | Contact/bounce state | Racket/ball contact state |
|
||
| $CD–$CF | Movement/action state | Movement speed, computer/action state |
|
||
| $E0 | SelectedGameMode | Selected game mode |
|
||
| $E6–$FF | Audio engine state | Audio request pointers, channel flags, sequence state |
|
||
|
||
---
|
||
|
||
## 10. PPU / Rendering
|
||
|
||
- **OAM DMA**: Every NMI, DMA from $0200–$02FF via `OAMDMA` ($4014).
|
||
- **PPU buffer**: `WritePpuBuffer` ($C2EC) writes a command buffer at $0590 to PPU when `PpuBufferReady` ($31) is set.
|
||
- **Scroll**: `SetScroll` ($C3FD) writes $1E/$1F to PPUSCROLL.
|
||
- **Palette**: `WritePalette` ($C1B5) conditionally uploads from RAM buffers based on `PaletteUpdateMode` ($3C).
|
||
- **Palette shadow**: `PpuCtrlShadow` ($09), `PpuMaskShadow` ($0A).
|
||
- **Sprite hiding**: `HideAllSprites` ($C1A5) writes $F4 to OAM.
|
||
- **Metasprite rendering**: `WriteMetaspriteToOam` ($C440) writes sprite data from a pointer.
|
||
|
||
---
|
||
|
||
## 11. Data Tables
|
||
|
||
| Label | Address | Description |
|
||
|-------|---------|-------------|
|
||
| `MainStateHandlerTable` | $C101 | 12 main game state handler pointers |
|
||
| `GameStateStepHandlerTable` | $C185 | 7 mode step handler pointers |
|
||
| `GameStateHandlerTable` (game mode) | $CAD7 | 8 game-mode state handler pointers |
|
||
| `InitialMatchObjectStateData` | $C847 | Initial object state data for match start |
|
||
| `EngineTable_*` | Various | Inline data tables embedded between code |
|
||
| `AudioEventSequenceData` | $FDFA | Audio event sequence data |
|
||
| Audio tables | $FF00–$FFF5 | Period/mask tables, sequences, instruments |
|
||
|
||
---
|
||
|
||
## 12. Summary of Execution Flow
|
||
|
||
```
|
||
Power On / Reset
|
||
│
|
||
▼
|
||
Reset ($C000) ───────────────────────────────────────────────────────┐
|
||
│ cld, sei, wait VBlank, PPUCTRL/PPUMASK=$00 │
|
||
│ stack=$FF, clear RAM $0000–$02FF │
|
||
│ $0500=$5F, clear nametables, hide sprites │
|
||
│ PPUSCROLL=$00, GameActiveFlag=$01 │
|
||
│ APUSTATUS=$0F (all channels on) │
|
||
│ PPUCTRL=$90 (NMI on), PpuCtrlShadow=$90 │
|
||
│ PpuMaskShadow=$06 │
|
||
│ │
|
||
▼ │
|
||
MainLoop ($C04F) ─────────────────────────────────────────────────────┤
|
||
│ │
|
||
├─ NmiWorkComplete ($30) = $00 │
|
||
├─ if GameActiveFlag && GameStateStep==1: RunAudioEngine ($FFF7) │
|
||
├─ jsr UpdateTimers ($C375) │
|
||
├─ jsr UpdateGameState ($C0AA) ──► state machine dispatch │
|
||
│ ├─ State $00: InitializeMatch │
|
||
│ ├─ State $01: WaitForServe │
|
||
│ ├─ State $02: PrepareRally │
|
||
│ ├─ State $03: RunRally │
|
||
│ ├─ State $04: PointEnded │
|
||
│ ├─ State $05: UpdateScore │
|
||
│ ├─ State $06: AdvanceGame │
|
||
│ ├─ State $07: PauseOrTransition (RTS) │
|
||
│ ├─ State $08–$0B: WaitForTimer / Restore / Resume / Finish │
|
||
│ └─ return to MainLoop │
|
||
│ │
|
||
├─ NmiUpdatesEnabled ($32) = $01 │
|
||
├─ wait for NmiWorkComplete ($30) != 0 ◄── NMI sets this │
|
||
├─ FrameCounter++, NmiUpdatesEnabled=$00 │
|
||
└─ jmp MainLoop │
|
||
│ │
|
||
│ (if NmiWorkComplete==0: jsr RollRng ($C456), retry) │
|
||
│ │
|
||
NMI ($C07D) [once per frame] │
|
||
│ │
|
||
├─ save A, X, Y │
|
||
├─ OAM DMA ($0200–$02FF → $4014) │
|
||
├─ if NmiUpdatesEnabled: │
|
||
│ jsr WritePalette ($C1B5) │
|
||
│ jsr WritePpuBuffer ($C2EC) │
|
||
│ jsr SetScroll ($C3FD) │
|
||
├─ jsr ReadControllers ($C2A4) → $4D/$4E/$4F │
|
||
├─ NmiWorkComplete ($30) = $01 (unblocks main loop) │
|
||
├─ NmiTriggerFlag ($33) = $01 │
|
||
├─ restore registers, rti │
|
||
│ │
|
||
IrqHandler ($C17E): rti (unused) │
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Notes
|
||
|
||
- **Build verification:** `make` in `copilot/` produces `reconstructed.nes`, compared whole-ROM against `rom.nes`.
|
||
- Vectors confirm NMI at $C07D, and both RESET and IRQ at $C000 (the reset handler doubles as the IRQ handler, just a `rti`).
|
||
- The foreground main loop (`MainLoop` at $C04F) is strictly NMI-synchronized via `NmiWorkComplete` ($30): it loops until NMI sets it to $01, processes game state and timers, then waits again.
|
||
- `UpdateGameState` ($C0AA) dispatches to one of 12 main game states via `MainStateHandlerTable` at $C101, indexed by `GameState` ($3D).
|
||
- State $04 contains a nested secondary state machine (`GameStateStepHandlerTable` at $C185, 7 steps) for mode selection/setup.
|
||
- Many routine labels are address-based (`EngineRoutine_C1DD`, `MatchRoutine_C7E7`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional.
|
||
- `symbols.inc` provides NES hardware register names and controller masks; `locals.inc` provides extensive zero-page symbol definitions covering player/ball physics, audio, and game state.
|
||
- The disassembly includes inline data tables (`EngineTable_*`) embedded between code regions, preserved verbatim for byte-identity. |