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.
433 lines
25 KiB
Markdown
433 lines
25 KiB
Markdown
# Baseball (UE) — Game Flow from Power-On
|
||
|
||
This document describes the execution flow of the disassembled Baseball (UE) 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 | Horizontal |
|
||
| PRG bank start | CPU $C000 (`.org $C000`) |
|
||
| Linker output | `reconstructed.nes`, byte-identical to `rom.nes` |
|
||
|
||
### Interrupt vectors (CPU $FFFA–$FFFF)
|
||
|
||
| Vector | Address | Handler | Notes |
|
||
|--------|---------|---------|-------|
|
||
| NMI | $C046 | `NMI` | Runs once per frame; the primary frame driver |
|
||
| RESET | $C40B | `Reset` | Cold-start initialization |
|
||
| IRQ / BRK | $C43E | `IRQ` | Just `rti` (unused) |
|
||
|
||
---
|
||
|
||
## 2. Reset — CPU $FFFC → $C40B
|
||
|
||
**`Reset`** ($C40B)
|
||
|
||
On power-on or reset:
|
||
|
||
1. `sei` / `cld` — disable maskable interrupts, clear decimal mode.
|
||
2. **Clear CPU RAM** $0000–$07FF — one 256-byte page at a time using the indirect pointer at $00/$01, repeatedly writing $2000 to PPUCTRL during the clear.
|
||
3. `jsr InitializeSpriteSortTables` ($C7B1) — set up the sprite sort/index tables at $0377, $038C, $0399.
|
||
4. `jsr DisablePpuRendering` ($E96C) — turn off PPU output.
|
||
5. **Disable APU** — write $00 to APUSTATUS ($4015) to silence all channels, then select the five-step frame-counter mode and inhibit frame-counter IRQ via JOY2 ($4017).
|
||
6. `jsr ExpandRecordPointerList` ($C8BB) with inline data — builds record pointer structures.
|
||
7. `lda #$1A / sta PpuMaskShadow` — initial PPU mask shadow value.
|
||
8. `jmp InitializeTitleScreen` ($C74A) — RESET does not return; it transfers directly to the title screen.
|
||
|
||
---
|
||
|
||
## 3. NMI / Frame Heartbeat — `NMI` ($C046)
|
||
|
||
The NMI fires once per frame and is the central frame driver. It does **not** simply return; it performs substantial per-frame work:
|
||
|
||
1. **Save X and Y** into `NmiSavedX` ($0D) / `NmiSavedY` ($0E).
|
||
2. **Read PPUSTATUS** ($2002) to reset the PPU address latch.
|
||
3. **OAM DMA** — write $02 to OAMDMA ($4014) to copy $0200–$02FF to sprite memory.
|
||
4. **Process the PPU command queue** at $0300 — walk variable-length packets (write packets to $2007, read packets discarded), terminating on a zero-length entry, then reset the queue head (`stx $0300`).
|
||
5. **Apply scroll** — write `ScrollX` ($DF) and `ScrollY` ($E0) to $2005.
|
||
6. **Apply PPU registers** — write `PpuMaskShadow` ($DE) to $2001 and `PpuCtrlShadow` ($DD) to $2000.
|
||
7. `jsr InitializeAudioEngine` ($F9D0) — initialize/advance the audio engine.
|
||
8. **Latch controllers** — strobe JOY1 ($4016) with $01 then $00.
|
||
9. **Read both controllers** — sample JOY1/JOY2 eight times each, shifting button states into `ControllerPressed1` ($15) / `ControllerPressed2` ($16).
|
||
10. **Update controller state** — compute newly-pressed edges (`Pressed = (Pressed & ~Held) | raw`) and update `ControllerHeld1/2` ($11/$12).
|
||
11. **START-button branch**:
|
||
- If START pressed and `SessionActive` ($1E) is set → `jsr ProcessQueuedObjectEvent` with command $0B.
|
||
- If START pressed and session is **not** active → set `TransitionActive` ($1D) and `GameplaySuspended` ($EF), then `jmp InitializeNewSession` ($C49C).
|
||
12. `NMI_CheckState` — if `NmiStateFlag` ($1F) is nonzero, skip ahead and restore registers.
|
||
13. Otherwise `jsr QueueInlineObjectEvent` ($EB55) with inline command bytes `$05,$21,$C1`, then `jsr ProcessQueuedObjectEvent` ($EB2A) with command $0B.
|
||
14. `inc FrameCounter` ($E3).
|
||
15. `jsr CalculateControllerAssignments` ($C87E) — derive per-player control state.
|
||
16. Store derived input into `InputState1/2` ($13/$14) and `InputAux1/2` ($17/$18).
|
||
17. **Restore X, Y, A**, `rti`.
|
||
|
||
Because the NMI does controller reading and event dispatch, the main loop largely waits on the NMI-driven queued-event system rather than polling hardware directly.
|
||
|
||
---
|
||
|
||
## 4. Title Screen — InitializeTitleScreen ($C74A)
|
||
|
||
Shared entry point used by RESET and by post-match/transition paths:
|
||
|
||
1. `jsr InitializeQueuedEventSystem` ($EB9D) — clear the $0600–$06FF event page, the queue count ($E2), active-event slots, and set `CurrentObjectIndex` ($E1) = 1.
|
||
2. `GameplaySuspended` ($EF) = 0; `TransitionActive` ($1D) = 1.
|
||
3. `PpuCtrlShadow` = $80; disable PPU rendering; fill PPU memory (`FillPpuMemoryAfterValueSaved` at $E985 with value $25).
|
||
4. `jsr ProcessInlinePpuStream` ($EA63) twice — streams title PPU data (inline records `$8D,$F8` and `$00,$C0`).
|
||
5. `jsr EnableNmiAndWaitForFrame` ($E978) — enable NMI and synchronize to a frame.
|
||
6. Enable APU channels ($4015 = $0F), set timing byte $FD = $08.
|
||
7. `RefreshTitleScreen` ($C77A) loop:
|
||
- Update `ModeFlags` ($40).
|
||
- `jsr QueueModeIndicatorTiles` ($E5CF).
|
||
- Wait for controller input release (SELECT/$20) and for the frame counter / delay byte $1B to elapse.
|
||
8. On exit: `TransitionActive` = 0, `GameplaySuspended` = 1, clear audio ($4015), re-init the event system, queue an inline command `$03,$88,$C4`, then `jmp InitializeSessionDisplay` ($C4AA).
|
||
|
||
### Attract mode — RunAttractMessageLoop ($C43F)
|
||
|
||
Before reaching the title input loop, `Reset` can flow into `RunAttractMessageLoop`, which:
|
||
- Clears `NmiStateFlag`, selects object event bank 1.
|
||
- Draws encoded six-character messages via `CopyInlineDisplayRecord` ($EA17) with inline text bytes.
|
||
- Exchanges the object event bank ($EB4E), increments `NmiStateFlag`, and loops back to itself — this is the idle attract display until the player presses START.
|
||
|
||
---
|
||
|
||
## 5. Session and Match Initialization
|
||
|
||
### InitializeSessionDisplay ($C4AA)
|
||
PPU shadow setup, disable rendering, fill PPU memory with $26, then process an inline PPU stream (`$F2,$F4`).
|
||
|
||
### InitializeMatchState ($C4B9)
|
||
- Clears base position and acceleration fields ($C94F/$C943).
|
||
- Sets `$053B` and `PositionSignState` ($B7) to $80.
|
||
- `jsr EnableNmiAndWaitForFrame`.
|
||
- **Player/team selection**:
|
||
- `Random0To5` ($C850) → `PlayerOneSelection` ($34).
|
||
- Repeatedly `Random0To5` until a value different from player one → `PlayerTwoSelection` ($35).
|
||
- If `GameplaySuspended` is set, skip side selection; otherwise derive `ActivePlayerSide` ($4B) from mode flags or a random bit.
|
||
- Queue inline object events `$03,$19,$C8` and `$02,$19,$C8`, set `PlayerSelectionActive` ($44) = 1.
|
||
- `WaitForPlayerSelection` ($C4FF) spins until `$43` (`SelectionState`) becomes negative, then queues `$03,$3E,$C4` and falls into `CompleteMatchSetup` ($C50C).
|
||
|
||
### CompleteMatchSetup ($C50C)
|
||
- `FillSelectionBackgroundBlocks` ($C85A), `QueuePlayerSelectionDisplay` ($C7DD), `InitializeRandomBitfields` ($C899), `QueuePlayerValueDisplay` ($CF8B).
|
||
- Queue inline command `$09,$37,$CE`, then enter `InitializeRoundState` ($C51E).
|
||
|
||
---
|
||
|
||
## 6. Round / Gameplay State Machine
|
||
|
||
### InitializeRoundState ($C51E)
|
||
Round-setup entry point:
|
||
- If gameplay is not suspended and `RoundStateIndex` ($33) < $12, select round music cue; otherwise reset round counters.
|
||
- `EnableRoundAudio` ($C52A): enable APU channels ($4015 = $0F), set $FD = $08.
|
||
- `ResetRoundCounters` ($C536): clear scores ($67/$68) and `RoundStateIndex`.
|
||
- `jsr InitializeObjectsFromTable` ($D06B) — initialize objects from the table at $D0A0–$D131.
|
||
- Set `$053B` / `PositionSignState` = $80.
|
||
- Store a $3C-tick delay, draw an 8-character message, store another $3C delay.
|
||
- `SelectRoundMusicCue` ($C566): choose a music delay value ($20 or $40) based on `RoundStateIndex` parity → store in $FD.
|
||
- `PrepareRoundObjects` ($C573): queue player-selection display, queue random-state record, clear `MatchCompleteFlag` ($45) and `SecondarySequenceState` ($64).
|
||
|
||
### ResetGameplayState ($C57F)
|
||
- Clear `InningOrSharedScore` ($66), `BaseMotionActive` ($5C).
|
||
- Mark four state entries ($0598–$059B) inactive ($FF).
|
||
- `jsr QueueRoundNumberDisplay` ($CF67), re-init objects, set sign state to $80.
|
||
- Store a $B4-tick delay.
|
||
|
||
### Round setup continuation points
|
||
- `ResumeRoundSetupAtRandomDirection` ($C5A4): queue random-direction record, clear primary/transition sequence state, set `StateSequenceActive` ($46) = 1, clear state field pairs ($03,$23 via $CF51).
|
||
- `ResumeRoundSetupAtSelectionDisplay` ($C5BA): clear the 8-character field, clear $0597–$05A6, clear the 38-byte motion field range $056F–$0594.
|
||
- Based on `RoundStateIndex` and score comparison ($67 vs $68), either jump to `CompleteMatchState` ($C6F7) or finish the reset.
|
||
|
||
### FinishGameplayReset ($C5F1)
|
||
- `InitializeDefaultObjects` ($D030), clear sign state, select event bank 1 → enter `RunRoundStateLoop` ($C5FE).
|
||
|
||
### RunRoundStateLoop ($C5FE) — main per-round loop
|
||
This is the core gameplay loop:
|
||
- If `PendingStateTransition` ($60) is set → `InitializeObjectStateLookup` ($C8A3).
|
||
- If either `PendingStateTransition` or `StateSequenceActive` ($46) is nonzero → dispatch state-specific handlers via queued inline commands (`$0D,$77,$D4`, `$0F,$81,$D3`).
|
||
- `RunPrimaryRoundHandlers` ($C619):
|
||
- `InitializeSecondaryActors` ($D633), `InitializeConditionalObject` ($D126).
|
||
- Clear working state: `PendingScoreIncrement`, `BaseMotionPhase`, `BaseMotionEventCount`, `TrajectoryResult`, `PendingStateTransition`, `SpecialSequenceState`, `ObjectTransitionState`, `BaseMotionActive`, `ActionSequenceCounter`, `TrajectoryEvaluationEnabled`, and several others.
|
||
- Clear base acceleration and position fields.
|
||
- Queue three inline object events (`$25,$5A,$D8`, `$0E,$66,$D1`, `$1F,$76,$CA`).
|
||
- `jsr UpdateObjectEventBank_Exchange` with command $08.
|
||
- Walk the 22-entry `RoundEventSlotTable` ($C6E1) in reverse, dispatching each via `DispatchNextQueuedEvent` ($EB66).
|
||
- Write one inline character ($3F, value $28) to the PPU.
|
||
- `ApplyPendingScoreUpdate` ($C67E): if a score increment is pending and no state transition, `jsr UpdateCountersAndDisplay` ($CFA6), clear the increment.
|
||
- `StartPostRoundDelay` ($C68D): wait for `PendingEvent` ($6F) to clear, then count down delay counter $48.
|
||
- If match is not complete and no sequence is active → `jmp ResumeRoundSetupAtSelectionDisplay` ($C5BA) to start the next round.
|
||
- `ProcessPostRoundEvents` ($C6A5): dispatch event slots $0D and $0F, store a $0A-tick delay, decrement a counter based on `RoundStateIndex` parity.
|
||
- `CheckPostRoundContinuation` ($C6C5): if match complete → `AdvanceRoundNumber` ($C6CC); otherwise `jmp ResumeRoundSetupAtRandomDirection` ($C5A4).
|
||
- `AdvanceRoundNumber` ($C6CC): update counters/display, increment `RoundStateIndex`; if index reaches $12 → `InitializeResultsPosition` ($C708); otherwise `SelectRoundMusicCue` ($C566) to begin the next round.
|
||
|
||
---
|
||
|
||
## 7. Per-Frame Object and Motion Updates
|
||
|
||
### UpdateFrameObjectsAndTimers ($C122)
|
||
Called each frame to drive object motion:
|
||
- Reset PPU queue offset, OAM write offset, and `FrameState20` ($20).
|
||
- Queue/process inline object events ($07,$64,$C2 via bank, then command $01).
|
||
- Decrement per-object timers at $063F,X (30 slots); when a timer reaches zero, queue a decoded object event.
|
||
- Decrement four short countdowns at $05AD,X.
|
||
- If a transition is active, jump to `CommonEngineExit` ($EB6A); otherwise `IntegrateAllObjectPositions` ($C15B).
|
||
|
||
### IntegrateAllObjectPositions ($C15B)
|
||
- For 36 object slots (X stepping by 2, each logical object uses two indices):
|
||
- Sign-extend and add horizontal delta ($050B/$050C,X from $056F/$0570,X).
|
||
- Sign-extend and add vertical delta ($053C/$053D,X from $0570/$0571,X).
|
||
|
||
### UpdateBaseMotionSequence ($C18D)
|
||
- Integrate velocity/position for three base object indices ($00, $31, $62).
|
||
- Reflect base motion values when $056C is negative (negate $056B/$056C and halve $0569/$056A).
|
||
- Apply doubled horizontal/vertical velocity adjustments to $0507/$0508 and $0538/$0539.
|
||
- `jsr EvaluateTrajectoryResult` ($CAD2).
|
||
- Advance `BaseMotionPhase` ($5D); when motion settles, clear base acceleration/position fields.
|
||
- `UpdatePositionSignState` ($C228): compute a signed position delta relative to a $0A threshold, track sign changes in `PositionSignState` ($B7), and update rendering flags.
|
||
|
||
### IntegrateObjectVelocityAndPosition ($C3CA)
|
||
Signed fixed-point position integration used by the three base motion call sites: shifts the signed 16-bit velocity right twice (sign-preserving), adds to position ($0507/$0508), then adds the result to the secondary position ($0509/$050A).
|
||
|
||
---
|
||
|
||
## 8. Rendering and OAM
|
||
|
||
### BuildFrameOam ($C2DE)
|
||
- If `TransitionActive` is set: copy an 88-byte sprite/OAM template from $F94D into shadow OAM at $0200, set `OamWriteOffset` ($3F), and jump to `TerminatePpuUpdateQueue`.
|
||
- Otherwise, if `PlayerSelectionActive` ($44) is set: copy an 8-byte OAM template from $C331, position two sprites from player selection coordinates (scaled ×16 + $50), choose tiles based on `ModeFlags`.
|
||
- Otherwise: `SortObjectsByY` ($C339) — sort object indices by signed 16-bit Y coordinates ($0538/$0539) using the index table at $0377/$0378, then render each sorted object via `RenderSpriteRecordList` ($E77B).
|
||
|
||
### FinalizeOamAndPpuQueue ($C386)
|
||
- If `ObjectListFlag` ($C7) is set, skip status OAM; otherwise copy 20 bytes from $EBC2 into shadow OAM and select two extra bytes from $EBBC based on player selections.
|
||
- `TerminatePpuUpdateQueue` ($C3AA): write $00 to terminate the PPU command queue at $0300,Y.
|
||
- Hide unused sprites by writing $F0 to each remaining OAM Y coordinate (stepping by 4).
|
||
- `jsr AdvanceRandomState` ($E5F4), then `jmp CommonEngineExit` ($EB6A).
|
||
|
||
---
|
||
|
||
## 9. Batting and Pitching
|
||
|
||
### Pitching (RunPitcherWindupSequence, $C6044)
|
||
- `RunPitcherWindupSequence` animates the pitcher windup (frames selected via `SetWindupFrame`).
|
||
- `SelectPitchAction` ($C6139): reads controller input to choose the pitch action.
|
||
- `SelectPitchType` ($C6178): searches `PitchTypeThresholdTable` / `PitchDirectionThresholdTable` ($C5973/$C5980) to determine pitch type and direction.
|
||
- `AnimatePitchStart` ($C6229), `UpdatePitchMovement` ($C6321): apply pitch trajectory and controller-based horizontal/vertical input.
|
||
- Pitch tables: `PitchVelocityBiasTable` ($C6399), `PitchDelayTable` ($C6402), `PitchRenderFlagTable` ($C6405).
|
||
|
||
### Batting (SelectBatterAction, $C6414)
|
||
- `WaitForBatterAction` ($C6422): wait for batter input.
|
||
- `UseBatterControllerInput` ($C6464): map controller to batter action slot.
|
||
- `SelectActionFromObjectState` ($C6590) with `ObjectActionLookupTable` ($C6502).
|
||
- `DispatchActionType` ($C6614): route to the appropriate action handler.
|
||
|
||
### Object motion and collision
|
||
- `MoveControlledObject` ($D371): apply controlled horizontal/vertical offsets (`ControlledHorizontalOffsetTable` / `ControlledVerticalOffsetTable` at $D384/$D38C).
|
||
- `TestObjectCollision` ($D38B): object-vs-object collision testing.
|
||
- `TestCurrentObjectInput` ($D7D7): resolve controller input for the current object.
|
||
- Geometry helpers: `CalculateCurrentObjectGeometry` ($D7AE), `CalculateGeometryDifference` ($D566), `BuildGeometryVector` ($D74D), extensive trigonometry/vector math routines ($D5xx–$D8xx).
|
||
|
||
---
|
||
|
||
## 10. Audio Engine
|
||
|
||
### InitializeAudioEngine ($F9D0)
|
||
Initialize the APU: write DMC settings, configure pulse/triangle/noise channel presets, write pulse periods from tables ($FA00–), set up audio lookup data.
|
||
|
||
### UpdateAudioEngine ($FB05)
|
||
Per-frame audio update:
|
||
- Clear/finalize primary audio effects.
|
||
- Update pulse 1 and pulse 2 effects (preset effects, descending effects, state-change effects, phase effects).
|
||
- Update triangle pitch sweeps.
|
||
- Dispatch music and noise (`DispatchMusicAndNoise` at $FB65): update music sequencer, noise envelope, timed triangle effects, DMC output.
|
||
|
||
### Music
|
||
- `StartMusicSequence` ($FD34): initialize a music sequence by finding its bank and copying the descriptor.
|
||
- Channels: pulse 1 ($FD96), pulse 2 ($FDCF), triangle ($FE0B).
|
||
- Music sequence data tables at $FE60–$FFD0 (`MusicSequenceData_FE60` … `MusicSequenceData_FFD0`).
|
||
- Sound effect presets and noise/triangle envelopes throughout the $F9xx–$FE2x region.
|
||
|
||
Audio is advanced from the NMI via `jsr InitializeAudioEngine` ($F9D0) each frame; the broader `UpdateAudioEngine` ($FB05) is called from the event system.
|
||
|
||
---
|
||
|
||
## 11. Object Event System (the main-loop engine)
|
||
|
||
The game is driven by a **queued event system** rather than a simple polling loop:
|
||
|
||
- `InitializeQueuedEventSystem` ($EB9D): clear $0600–$06FF, queue count $E2, active slots, set current object index.
|
||
- `QueueInlineObjectEvent` ($EB55) / `QueueDecodedObjectEvent` ($EB2A): push an event onto the queue.
|
||
- `ProcessQueuedObjectEvent` ($EB2A): walk the queue and process active events.
|
||
- `DispatchNextQueuedEvent` ($EB66): pop the newest event, save its continuation address on the hardware stack (SP = $FF), and `rts` into it — this is how control transfers between event handlers.
|
||
- `CommonEngineExit` ($EB6A): mark the current object inactive; if the queue is empty, wait (`beq DispatchNextQueuedEvent_Wait`) polling $E2; otherwise disable NMI bit 7 and dispatch the next entry.
|
||
|
||
The main gameplay loop (`RunRoundStateLoop`) and most state handlers ultimately pass through `CommonEngineExit`, which blocks until the NMI or another producer queues the next event. This makes the NMI the real-time heartbeat and the main loop an event consumer.
|
||
|
||
---
|
||
|
||
## 12. PPU Streaming Helpers ($E9xx–$EBxx)
|
||
|
||
A family of routines drives PPU updates from inline byte streams embedded in the code:
|
||
- `DisablePpuRendering` ($E96C), `EnableNmiAndWaitForFrame` ($E978).
|
||
- `FillPpuMemoryWith26` / `FillPpuMemoryAfterValueSaved` ($E983/$E985): fill PPU memory with a byte value.
|
||
- `DecodeInlineParameters` ($EA09), `ReadInlineWord` ($EA28), `CopyInlineBytesToZeroPage` ($EA45).
|
||
- `WritePpuControlFromCarry` ($EA6E), `SetPpuAddressFromInlineRecord` ($EA97), `WritePendingPpuByte` ($EABF), `WriteInlinePpuBlock` ($EAC9), `PPUWriteString` ($EAFB).
|
||
- `CopyInlineDisplayRecord` ($EA17): draw an encoded text record (count byte followed by character data) — used heavily for titles, scores, round numbers, and announcements.
|
||
- `ProcessInlinePpuStream` ($EA63): process a stream of PPU commands.
|
||
|
||
Many routines are followed by `.byte` inline data that must **not** be disassembled as instructions; the disassembly marks these explicitly.
|
||
|
||
---
|
||
|
||
## 13. Match Completion and Results
|
||
|
||
### CompleteMatchState ($C6F7)
|
||
- Increment `RoundStateIndex`.
|
||
- If `InningOrSharedScore` ($66) is nonzero → `InitializeResultsPosition`; otherwise draw a single encoded character ($32).
|
||
- `InitializeResultsPosition` ($C708): set sign state to $80, draw a 10-character message (`$25,$10,$0A,$16,$0E,$25,$1C,$0E,$1D,$25`), set timing $FD = $4, dispatch event slot 2, queue inline `$02,$88,$C4`.
|
||
- `ResultsDelayLoop` ($C737): repeat a $3C-tick delay 10 times.
|
||
- Disable APU ($4015 = $00), clear `SessionActive` ($1E), then fall into `InitializeTitleScreen` ($C74A) to return to the title.
|
||
|
||
---
|
||
|
||
## 14. Summary of Execution Flow
|
||
|
||
```
|
||
Power On / Reset
|
||
│
|
||
▼
|
||
Reset ($C40B) ───► clear RAM $0000-$07FF, init sprite sort, disable PPU/APU
|
||
│
|
||
▼
|
||
jmp InitializeTitleScreen ($C74A)
|
||
│
|
||
├─ init event system, draw title screen
|
||
├─ EnableNmiAndWaitForFrame (NMI now runs each frame)
|
||
├─ WaitForTitleInputRelease (wait for START + input release)
|
||
▼
|
||
jmp InitializeSessionDisplay ($C4AA)
|
||
│
|
||
▼
|
||
jmp InitializeMatchState ($C4B9)
|
||
│
|
||
├─ Random0To5 → PlayerOneSelection ($34)
|
||
├─ Random0To5 (≠ P1) → PlayerTwoSelection ($35)
|
||
├─ derive ActivePlayerSide ($4B)
|
||
└─ WaitForPlayerSelection (spin until $43 < 0)
|
||
│
|
||
▼
|
||
CompleteMatchSetup ($C50C)
|
||
│
|
||
▼
|
||
InitializeRoundState ($C51E)
|
||
│
|
||
├─ enable audio, reset counters, init objects from table ($D06B)
|
||
├─ draw messages, select music cue
|
||
▼
|
||
ResetGameplayState ($C57F) → FinishGameplayReset ($C5F1)
|
||
│
|
||
▼
|
||
RunRoundStateLoop ($C5FE) ◄───────────────────┐
|
||
│ │
|
||
├─ InitializeObjectStateLookup │
|
||
├─ RunPrimaryRoundHandlers │
|
||
│ ├─ InitializeSecondaryActors │
|
||
│ ├─ InitializeConditionalObject │
|
||
│ ├─ walk RoundEventSlotTable │
|
||
│ └─ dispatch queued events │
|
||
├─ ApplyPendingScoreUpdate │
|
||
├─ StartPostRoundDelay (wait $48 ticks) │
|
||
├─ ProcessPostRoundEvents │
|
||
├─ CheckPostRoundContinuation │
|
||
│ ├─ match complete → results → title │
|
||
│ └─ AdvanceRoundNumber → next round │
|
||
└─ eventually → CommonEngineExit ($EB6A) │
|
||
wait for queued event ($E2) │
|
||
dispatch next event (rts into it)│
|
||
└────────────────────────────────┘
|
||
|
||
NMI ($C046) [every frame]
|
||
├─ PPU command queue flush ($0300)
|
||
├─ OAM DMA ($4014)
|
||
├─ scroll ($2005), PPU mask/ctrl ($2001/$2000)
|
||
├─ InitializeAudioEngine ($F9D0)
|
||
├─ controller latch + read (JOY1/JOY2)
|
||
├─ controller state (pressed/held edges)
|
||
├─ START branch → new session or command $0B
|
||
├─ QueueInlineObjectEvent + ProcessQueuedObjectEvent
|
||
├─ FrameCounter++, CalculateControllerAssignments
|
||
└─ restore registers, rti
|
||
```
|
||
|
||
---
|
||
|
||
## 15. Key Zero-Page / RAM Symbols
|
||
|
||
| Address | Symbol | Use |
|
||
|---------|--------|-----|
|
||
| $00–$01 | IndirectPtrLo/Hi | RAM-clear indirect pointer |
|
||
| $0D–$0E | NmiSavedX/Y | NMI register save |
|
||
| $0F | DerivedControl0 | Derived control input |
|
||
| $11–$12 | ControllerHeld1/2 | Currently held buttons |
|
||
| $13–$14 | InputState1/2 | Current input state |
|
||
| $15–$16 | ControllerPressed1/2 | Newly pressed buttons |
|
||
| $17–$18 | InputAux1/2 | Auxiliary input |
|
||
| $19–$1A | RandomStateLo/Hi | PRNG state |
|
||
| $1B | TransitionCounter | Transition delay |
|
||
| $1D | TransitionActive | Transition in progress |
|
||
| $1E | SessionActive | Session/game active flag |
|
||
| $1F | NmiStateFlag | NMI state flag |
|
||
| $20 | FrameState20 | Frame state |
|
||
| $21 | SelectedPitchAction | Current pitch action |
|
||
| $33 | RoundStateIndex | Current round/inning index |
|
||
| $34–$35 | PlayerOne/TwoSelection | Selected teams |
|
||
| $36–$39 | ObjectScreenX/Y (+Hi) | Object screen coordinates |
|
||
| $3E–$3F | PpuQueueOffset / OamWriteOffset | PPU/OAM write positions |
|
||
| $40 | ModeFlags | Display/mode flags |
|
||
| $42 | CurrentObjectArrayOffset | Current object slot |
|
||
| $43 | SelectionState | Selection state |
|
||
| $44 | PlayerSelectionActive | Player selection active |
|
||
| $45 | MatchCompleteFlag | Match finished |
|
||
| $46 | StateSequenceActive | State sequence running |
|
||
| $48 | DelayCounter | General delay |
|
||
| $4B | ActivePlayerSide | Active player (0/1) |
|
||
| $4D–$50 | RandomState0/1, RandomBits0/1 | Randomization state |
|
||
| $53–$56 | SavedBaseX/Y (Lo/Hi) | Saved base coordinates |
|
||
| $5C | BaseMotionActive | Base motion active |
|
||
| $5D | BaseMotionPhase | Base motion phase |
|
||
| $5E | BaseMotionEventCount | Base motion event count |
|
||
| $5F | TrajectoryResult | Trajectory result |
|
||
| $60 | PendingStateTransition | Pending state change |
|
||
| $66 | InningOrSharedScore | Inning / shared score |
|
||
| $67–$68 | PlayerOne/TwoScore | Scores |
|
||
| $69–$6A | EventWrite/ReadIndex | Event queue indices |
|
||
| $6F | PendingEvent | Pending event |
|
||
| $7E | ActionSequenceCounter | Action sequence counter |
|
||
| $83 | ObjectApproachCount | Approach counter |
|
||
| $86 | TrajectoryEvaluationEnabled | Trajectory eval enabled |
|
||
| $8E–$90 | RandomHorizontal/VerticalMotion | Random motion |
|
||
| $9D | CurrentActionState | Current action |
|
||
| $9F–$A1 | SecondaryActorState0–2 | Secondary actor states |
|
||
| $A2 | ObjectSequenceState | Object sequence state |
|
||
| $B7 | PositionSignState | Position sign tracking |
|
||
| $C7 | ObjectListFlag | Object list flag |
|
||
| $C9–$CA | SpriteSortBaseIndex / SpriteArrayEnd | Sort table bounds |
|
||
| $DD–$E0 | PpuCtrl/MaskShadow, ScrollX/Y | PPU/shadow/scroll state |
|
||
| $E1 | CurrentObjectIndex | Current object being processed |
|
||
| $E2 | ScriptDepth / queue count | Queued event count |
|
||
| $E3 | FrameCounter | Frame tick counter |
|
||
| $EF | GameplaySuspended | Gameplay suspended |
|
||
| $F6–$F7 | SoundPtrLo/Hi | Sound pointer |
|
||
| $FE | PendingAudioFlags | Pending audio flags |
|
||
| $FF | RenderStateFlags | Render state flags |
|
||
|
||
---
|
||
|
||
## 16. Notes on the Disassembly
|
||
|
||
- The source is **21,624 lines** and thoroughly annotated with symbolic names for nearly every routine, table, and data block — there are no generic `Label_Cxxxx` placeholders.
|
||
- Numerous routines are followed by inline `.byte` data blocks consumed by `QueueInlineObjectEvent`, `CopyInlineDisplayRecord`, or `ProcessInlinePpuStream`; these are intentionally left as raw bytes and marked in comments.
|
||
- The PRG is a single 16 KB bank at $C000–$FFFF; calls to $Dxxx/$Exxx/$Fxxx targets are all within the same bank (no bank switching).
|
||
- Build verification: `make` in `copilot/` produces `reconstructed.nes`, compared whole-ROM against `rom.nes`. |