Files
disasm/Baseball (UE)/OVERVIEW.md
T
tmerritt 71ada15de0 Promote Donkey Kong (JU): Pass 3 complete, whole-ROM byte-identical
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.
2026-10-19 14:55:08 -04:00

433 lines
25 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.
# 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`.