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.
467 lines
28 KiB
Markdown
467 lines
28 KiB
Markdown
# Pac-Man (USA) (Namco) — Game Flow from Power-On
|
||
|
||
This document describes the execution flow of the disassembled Pac-Man (USA) (Namco) 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 |
|
||
| Source file | `copilot/prg.s` (14,332 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 | $C10E | `NmiHandler` |
|
||
| RESET | $C033 | `Reset` |
|
||
| IRQ / BRK | $C17E | `IrqHandler` (just `rti` — unused) |
|
||
|
||
Additional ROM data at $FFF6–$FFF9:
|
||
- $FFF6/$FFF7 = $ECEC (little-endian), used by `DrawMazeNametable` as a source pointer ($00/$01)
|
||
- $FFF8/$FFF9 = $C10E (little-endian)
|
||
|
||
---
|
||
|
||
## 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 → $C033
|
||
|
||
**`Reset`** ($C033)
|
||
|
||
On power-on or reset:
|
||
|
||
1. `ldx #$00 / stx PPUCTRL` — disable PPU rendering/NMI.
|
||
2. `sei / cld` — disable maskable interrupts, clear decimal mode.
|
||
3. `dex / txs` — initialize stack pointer to $FF.
|
||
4. `nop x3` — three NOP padding bytes.
|
||
5. **Wait for two VBlanks** — `@WaitForPpu` loop: set PPUMASK=$06, poll PPUSTATUS bit 7 twice (X counts down from 2). This is the PPU warm-up delay.
|
||
6. **Clear low RAM** — `@ClearLowRam`: zero $0000–$003D.
|
||
7. **Clear RAM pages** — `@ClearRamPages`: using indirect pointer at $00/$01, clear pages starting at $0100 upward until high byte reaches $08 (effectively clearing $0100–$07FF).
|
||
8. `PPUMASK=$06`, scroll=$00 — enable background + sprites, reset scroll.
|
||
9. **Warm-start signature check** — `@CheckRamSignature`: compare RAM at $0052 against the 15-byte string `"HIROKI AOYAGI"` plus `$AA,$55` (stored at `RamSignature`, $C354–$C366):
|
||
- If all 15 bytes match → warm start: skip full RAM clear, go to `@SignatureAlreadyPresent`.
|
||
- If any byte differs → cold start: `@InitializeRamSignature` clears all of zero page, then copies the signature string to $0052–$0060.
|
||
10. Set $64=$01 (some flag), set gameState=$00.
|
||
11. Set $45=$07, then check cocktail/active-player flags at $46/$47. If both nonzero, perform a 16-byte swap loop (`@SwapLoop`) swapping bytes between $67,y and $77,y for Y=0..15.
|
||
12. `APUCTRL=$1F`, `APUFRAMECNT=$C0` — enable all APU channels, set frame counter mode.
|
||
13. `jsr InitializeAudioEngine` ($E1F8 region) — clear audio request/channel state.
|
||
14. `jsr $EEB4` — audio/sound engine init.
|
||
15. Set PPUCTRL=$88, `ppuCtrlShadow`=$88 — enable NMI, $2000 nametable base.
|
||
16. Set $48=$FF (gameplay suspended flag), clear $023F and $024B (PPU update buffers).
|
||
17. Set $36=$01 (some state flag).
|
||
18. `jmp MainEntry` — transfer control to the main program.
|
||
|
||
**`RamSignature`** ($C354): `.byte "HIROKI AOYAGI", $AA, $55` — developer credit / warm-start validation signature.
|
||
|
||
---
|
||
|
||
## 4. NMI / Frame Heartbeat — `NmiHandler` ($C10E)
|
||
|
||
The NMI fires once per frame and is the central real-time driver:
|
||
|
||
1. **Save registers** (A, X, Y on stack).
|
||
2. **PPUMASK=$1E** — enable background + sprites with specific emphasis settings.
|
||
3. **OAM DMA**: write $00 to OAMADDR ($2003), $00 to $40, then `lda $45 / sta OAMDMA` ($4014) — DMA from page $45 (i.e., $4500–$45FF) to sprite RAM.
|
||
4. **`jsr FlushGameplayPpuUpdates`** ($DE3B region) — flush pending PPU gameplay update blocks at $023F and $0245.
|
||
5. **`jsr ReadNearbyNametableTiles`** — read nametable tile data around Pac-Man (likely for collision/tunnel logic).
|
||
6. **`jsr UpdatePalettes`** — upload palette data from RAM buffers $0299 and $02A9 to PPU palette $3F00/$3F10 when those buffers are marked active ($0F marker).
|
||
7. **Restore scroll**: write $41 and $42 to PPUSCROLL (two writes — horizontal then vertical).
|
||
8. **Construct PPUCTRL**: `lda $47 / and $46 / and #$01 / asl / ora $43` — builds the PPUCTRL value from shadow/flag bits, writes to PPUCTRL.
|
||
9. **Controller strobe**: write $01 then $00 to JOY1 ($4016).
|
||
10. **Read both controllers** — 8 iterations each:
|
||
- Controller 1: read JOY1, mask with #$03, compare with #$01, rotate into $4D (`currentInput`).
|
||
- Controller 2: read `APUFRAMECNT` ($4017), mask with #$03, compare with #$01, rotate into $4E.
|
||
11. **Select active controller**: X = `currentInput` ($4D) by default; if `cocktailModeFlag` ($46) & `activePlayerFlag` ($47) both nonzero, X = `newlyPressedInput` ($4E). Store X to `previousInput` ($4F).
|
||
12. **`inc frameCounter`** ($4B) — increment per-frame counter.
|
||
13. **$48 gate**: if $48 (`gameplaySuspended`) != 0, skip to `@NmiDone`.
|
||
14. **`jsr InitializeAudioChannels`** — scan and activate audio channel slots.
|
||
15. **`@NmiDone`**: restore registers (Y, X, A), `rti`.
|
||
|
||
The label `IrqHandler` appears at the `rti` following NMI's register restore — it is the same code address, just an alternate entry label for the IRQ vector (which points to the same `rti`).
|
||
|
||
### `UpdatePalettes`
|
||
Conditionally uploads 16-byte palette blocks from RAM $0299 (to PPU $3F00) and $02A9 (to PPU $3F10) when the corresponding buffer marker byte equals $0F. After upload, resets the marker and PPU address to $3000.
|
||
|
||
---
|
||
|
||
## 5. Main Entry — `MainEntry` ($C573)
|
||
|
||
After Reset, control transfers to `MainEntry`. This is the foreground state-machine entry:
|
||
|
||
1. **`$40=$01`** — set the NMI sync flag (NMI will clear it each frame).
|
||
2. **`@WaitForInitialNmi`** — spin until NMI clears $40 to $00 (synchronize to first VBlank).
|
||
3. **PPUCTRL=$88 / ppuCtrlShadow=$88** — enable NMI.
|
||
4. **PPUMASK=$00** — disable rendering.
|
||
5. **Clear OAM page** — fill $0700–$07FF with 0.
|
||
6. `cocktailModeFlag=$00`, then call:
|
||
- `jsr ClearGameplayNametables`
|
||
- `jsr InitializeNametableAttributes`
|
||
- `jsr InitializeNametableData`
|
||
- `jsr $E419` — additional init (address-named).
|
||
7. **Initialize palette buffers** — copy 32 bytes from `TitleScreenPalette` to RAM $0299–$02B8.
|
||
8. Set `actorStatusDisplay` ($6C–$6F) = $2D, set $023F=$FF, `gameplaySuspended`=$FF.
|
||
9. Clear $47, $46, $42, $41, `stateTimer` ($4C), $87, $88.
|
||
10. **PPUCTRL selection**: if `gameState` ($3F) != 0, use $8A; else use $8A as well (both paths end up with $8A; the `bne` is effectively a no-op in current code). Write to PPUCTRL and `ppuCtrlShadow`.
|
||
11. **`MainDispatchLoop`** ($C645):
|
||
- `$40=$01`, spin until NMI clears it (`@WaitForNmi`).
|
||
- `Y = gameState` ($3F), load 16-bit handler address from `MainStateHandlerTable` ($C267) into $10/$11.
|
||
- `jmp ($0010)` — indirect jump to the state handler.
|
||
|
||
**`MainStateHandlerTable`** ($C267–$C26E) — 4 entries, indexed by `gameState` as a byte offset (even values $00, $02, $04, $06):
|
||
|
||
| Offset | Handler | Description |
|
||
|--------|---------|-------------|
|
||
| $00 | `InitialStateHandler` | Title/input-wait state |
|
||
| $02 | `PlayerSelectionState` | 1P/2P selection |
|
||
| $04 | `State04Handler` | Substate dispatch machine |
|
||
| $06 | `TimedStateTransition` | Timed state transition |
|
||
|
||
---
|
||
|
||
## 6. Main State Handlers
|
||
|
||
### `TimedStateTransition` ($C27A) — state offset $06
|
||
- Decrement `$0F` countdown. If nonzero, return to `MainDispatchLoop`.
|
||
- When zero: set `gameState`=$02, return to `MainDispatchLoop`.
|
||
- This is a timed transitional state: another state places a duration in `$0F` and selects state $06; this handler counts down before advancing to state $02.
|
||
|
||
### `InitialStateHandler` ($C27A region, actually at $C27A? No — at $C27A is TimedStateTransition; InitialStateHandler is at $C27A? Let me re-check)
|
||
|
||
Actually from the listing:
|
||
- `TimedStateTransition` is at $C27A
|
||
- `InitialStateHandler` is at $C27A? No. Let me re-read:
|
||
|
||
Line 730: `TimedStateTransition:` is at $C27A (based on comment "$C27A-$C2A5: Timed State Transition Handler" — wait, the comment says $C27A but the code shows `dec $0F` at line 731. Then line 740: `; $C27A-$C2A5: Initial/Input State Handler` and line 766: `InitialStateHandler:`.
|
||
|
||
So there's a discrepancy in comments. The actual code:
|
||
- `TimedStateTransition` = `dec $0F / bne MainDispatchLoop / lda #$02 / sta gameState / jmp MainDispatchLoop` — this is state $06's handler.
|
||
- `InitialStateHandler` = checks controller bits $0C; if set → gameState=$06, $0F=$5A, jump MainEntry. Else increment $42; if $42 reaches $F0 → reset $42, ppuCtrlShadow=$88, gameState=$06, $0F=$5A. Otherwise return to MainDispatchLoop.
|
||
|
||
### `PlayerSelectionState` ($C546 region) — state offset $02
|
||
- If $87|$88 != 0 → `@AdvanceTimeout` (16-bit timeout counter).
|
||
- Else (first entry): copy `PlayerCountSelectionDisplayData` ($C507–$C50E) to RAM $024B until $FF terminator. Capture controller bits $04 and $08 into `pacmanRequestedDirection` ($50) and `pacmanDirection` ($51).
|
||
- `@AdvanceTimeout`: increment 16-bit counter at $87/$88. If high byte reaches $02 → reset counter, set gameState=$04, jump MainDispatchLoop.
|
||
- `@CheckSelectionInput`: if controller bit $04 changed → store as `pacmanRequestedDirection`, reset counter, set $87=$01, toggle `activePlayerFlag` ($47) between 0/1, update display marker tiles at $024D/$0251 from `PlayerCountSelectionMarkerTiles` ($C50F–$C511).
|
||
- `@CheckConfirmInput`: if controller bit $08 changed → store as `pacmanDirection`. If bit $08 now active → clear `gameplaySuspended`, `jsr $EEB4`, jump `GameModeEntry`.
|
||
|
||
**`PlayerCountSelectionDisplayData`** ($C507): `$22,$0A,$5C,$00, $22,$4A,$20,$FF` — copied to $024B on entry.
|
||
**`PlayerCountSelectionMarkerTiles`** ($C50F): `$5C,$20,$5C` — selection marker toggle data.
|
||
|
||
### `State04Handler` ($C52E region) — state offset $04
|
||
- If controller bits $0C set → gameState=$02, jump MainEntry (restart).
|
||
- Else: `Y = $87` (substate offset), load 16-bit handler from `State04SubstateTable` ($C52E), jump indirect via $10/$11.
|
||
|
||
**`State04SubstateTable`** ($C52E) — 12 entries indexed by even offsets $00–$16:
|
||
|
||
| Offset | Handler |
|
||
|--------|---------|
|
||
| $00 | `State04Initialize` |
|
||
| $02–$12 | `State04TimedSubstate` (shared, 6 entries) |
|
||
| $14 | `State04LongDelay` |
|
||
| $16 | `State04FinalPhase` |
|
||
|
||
- `State04Initialize` ($C573): sync to NMI via $40, PPUCTRL=$08, disable rendering, clear $88, call `$C5D8`, `InitializeState04PpuData` ($C608), `LoadState04DisplayData` ($C5A6), advance $87 by 2, PPUCTRL=$88, return to loop.
|
||
- `State04TimedSubstate` ($C589): increment $88; at $30 frames → clear $88, call `$C5A6`, advance $87 by 2.
|
||
- `State04LongDelay` ($C5A6): increment $88; at $80 frames → advance $87 by 2 to substate $16.
|
||
- `State04FinalPhase` ($C5D7): sets $47=$01, tail-jumps to `GameModeEntry`.
|
||
- `SetSelectionFlagAndContinue` ($C5D7): `activePlayerFlag=$01`, `jmp GameModeEntry`.
|
||
|
||
### `LoadState04DisplayData` ($C5A6)
|
||
Uses $87 as offset into `CharacterIntroDisplayDataTable` ($C687), copies $FF-terminated stream to RAM $024B. Optional secondary copy of 16 bytes from `CharacterIntroSpriteData` based on substate offset.
|
||
|
||
**`CharacterIntroDisplayDataTable`** ($C687) — 10 pointers to display streams for characters/nicknames (SHADOW, BLINKY, SPEEDY, PINKY, etc.) plus scoring legend.
|
||
|
||
---
|
||
|
||
## 7. Game Mode Entry — `GameModeEntry` ($C328)
|
||
|
||
Reached from `SetSelectionFlagAndContinue` (state $04 final phase). This transitions from the attract/selection sequence into gameplay:
|
||
|
||
1. PPUCTRL=$88, ppuCtrlShadow=$88.
|
||
2. Wait for VBlank.
|
||
3. PPUMASK=$00, gameState=$00, cocktailModeFlag=$00, $69=$00, $4A=$00.
|
||
4. `jsr ClearGameplayNametables`, `jsr DrawGameplayHudLabels`, `jsr DrawGameplayScoreFields`.
|
||
5. If `gameplaySuspended` != 0 → roundNumber=$01 (continue existing game).
|
||
6. Else → clear 32 bytes starting at roundNumber ($67–$86), set roundNumber=$03, $77=$03, clear $0600/$0601.
|
||
7. Common init: roundDisplayValue=$FF, $78=$FF, $0245=$FF, ppuCtrlShadow=$88, PPUCTRL=$88.
|
||
8. **`GameModeFrameSync`** ($C360): `$40=$01`, wait for NMI clear ($@WaitForNmi`), then:
|
||
- If `gameplaySuspended` != 0 AND controller bits $0C set → gameState=$02, jump MainEntry (restart).
|
||
- If `stateTimer` ($4C) != 0 → decrement, return to `@WaitForNextFrame`.
|
||
- Else `@DispatchState`: `Y = gameState`, load handler from `GameModeStateTable` ($CAD7) into $10/$11, indirect jump.
|
||
|
||
**`GameModeStateTable`** ($CAD7) — 8 entries indexed by even offsets $00–$0E:
|
||
|
||
| Offset | Handler |
|
||
|--------|---------|
|
||
| $00 | `GameModeState00` |
|
||
| $02 | `GameModeState02` |
|
||
| $04 | `GameModeState04` |
|
||
| $06 | `GameModeState06` |
|
||
| $08 | `GameModeState08` |
|
||
| $0A | `GameModeState0A` |
|
||
| $0C | `GameModeState0C` |
|
||
| $0E | `InitializeGameplayTransitionState` |
|
||
|
||
### `GameModeState04` ($CAE9) — pause/per-frame handler
|
||
- If $060F != 0 → return to `GameModeFrameSync`.
|
||
- Else: edge-detect controller bit $08 using $49 as previous state. On new press → increment $4A, install pause display commands at $024B, toggle pause display via bit 0 of $4A.
|
||
- When not paused and bit 0 of $4A clear → call gameplay helper chain before returning to `GameModeFrameSync`.
|
||
- Adjacent data at $CB5B contains tile values for "PAUSE" text — confirms this is the pause-state handler.
|
||
|
||
---
|
||
|
||
## 8. PPU / Rendering
|
||
|
||
### `DrawMazeNametable` ($E2E4)
|
||
Draws the maze nametable using compressed data:
|
||
- PPU destination selected based on cocktail mode ($47 & $46): $2000 or $2080.
|
||
- Source pointer from `MazeDataPointerBytes` at $FFF6–$FFF7 ($ECEC → points to ROM data).
|
||
- Each row: set PPU address, decode 2-bit-run-length-encoded bytes from the source (2 bits = repeat count-1, 6 bits = tile value), write $16 (22) bytes per row.
|
||
- Advance PPU destination by $20 per row.
|
||
|
||
### `FlushGameplayPpuUpdates` ($DE3B)
|
||
- If $48 (`gameplaySuspended`) == 0 → process update block at $023F (6 bytes + terminator to PPU).
|
||
- Else → jump to `UpdateAnimatedGameplayPpuFields`.
|
||
- Block at $0245 processed similarly if nonzero.
|
||
|
||
---
|
||
|
||
## 9. Audio System
|
||
|
||
**`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 and work values, scan 16 eight-byte slots through ($F2).
|
||
- Slots with values $01–$04 activate a channel: copy 4-byte parameters through indirect pointer $F6.
|
||
- Falls through to runtime command processing at $EF2D.
|
||
|
||
**`UpdateAudioChannels`** ($EF2D):
|
||
- For each active slot: decrement countdown, fetch command byte from sequence table, decode command ($F0+ = special, $C0+ = delay, else = note/frequency), update APU registers ($4000–$400F).
|
||
|
||
**`UpdateAudio`** ($F8DE):
|
||
- APUFRAMECNT=$C0, update Pulse 1, Pulse 2, Noise, Sound Effect channels.
|
||
- Clear $FF–$FB; if $07FF != 0 → APUDMALOAD, decrement $07FF.
|
||
|
||
Audio data tables at $FF00–$FFF5 (period/mask tables), $FF64–$FFEA (sequences/instruments), $F450–$F49A (runtime command data).
|
||
|
||
---
|
||
|
||
## 10. Zero-Page / RAM Symbols
|
||
|
||
| Address | Symbol | Use |
|
||
|---------|--------|-----|
|
||
| $00–$01 | workPtr0/1 | General indirect pointers |
|
||
| $02–$09 | workValue0–5 | General scratch values |
|
||
| $0A | pathCompareValue | Maze pathfinding |
|
||
| $0B | pathFallbackValue | Maze pathfinding |
|
||
| $0C | legalDirections | Maze pathfinding |
|
||
| $10 | dispatchVector | State dispatch indirect pointer |
|
||
| $12–$15 | arithmeticValue/Operand | 16-bit arithmetic |
|
||
| $1A–$2D | pacmanMotion–ghost4Motion | Actor motion records (4 bytes each) |
|
||
| $2E–$2F | pacmanTargetRow/Column | Pac-Man maze target |
|
||
| $32 | pacmanSpriteFrame | Pac-Man sprite frame |
|
||
| $33 | ghostSpriteFrames | Ghost sprite frames |
|
||
| $39 | ghostDirectionFlags | Ghost direction/control state |
|
||
| $3F | gameState | Main gameplay state |
|
||
| $40 | — | NMI sync flag ($01=pending, NMI clears to $00) |
|
||
| $41 | ppuCtrlShadow | PPUCTRL mirror |
|
||
| $42 | — | Second PPUSCROLL write |
|
||
| $46 | cocktailModeFlag | Cocktail cabinet mode |
|
||
| $47 | activePlayerFlag | Active player (0/1) |
|
||
| $48 | gameplaySuspended | Gameplay suspended flag |
|
||
| $4B | frameCounter | Per-frame counter |
|
||
| $4C | stateTimer | State countdown timer |
|
||
| $4D | currentInput | Controller 1 input |
|
||
| $4E | newlyPressedInput | Newly pressed input |
|
||
| $4F | previousInput | Previous input state |
|
||
| $50 | pacmanRequestedDirection | Requested direction |
|
||
| $51 | pacmanDirection | Current direction |
|
||
| $61 | highScoreDigits | High score digits |
|
||
| $67 | roundNumber | Round number |
|
||
| $68 | roundDisplayValue | Round display value |
|
||
| $6A | pelletsRemaining | Pellets remaining |
|
||
| $6B | extraLifeAwarded | Extra life flag |
|
||
| $6C–$6F | actorStatusDisplay | Actor status display |
|
||
| $70 | playerScoreDigits | Player score digits |
|
||
| $F0–$F7 | audioRequestPtr…audioWorkValue | Audio engine pointers/values |
|
||
| $F8 | audioChannelFlags | Audio channel flags |
|
||
| $FC | audioRequestIndex | Audio request index |
|
||
| $FD | audioChannelOffset | Audio channel offset |
|
||
| $FE–$FF | audioIndirectPtr | Audio indirect pointer |
|
||
|
||
---
|
||
|
||
## 11. Data Tables
|
||
|
||
| Label | Address | Description |
|
||
|-------|---------|-------------|
|
||
| `CopyrightText` | $C004 | "COPYRIGHT 1980 1993 NAMCO LTD. ALL RIGHTS RESERVED" |
|
||
| `RamSignature` | $C354 | `"HIROKI AOYAGI" $AA $55` — warm-start signature |
|
||
| `MainStateHandlerTable` | $C267 | 4 main state handler pointers |
|
||
| `State04SubstateTable` | $C52E | 12 state-$04 substate handler pointers |
|
||
| `GameModeStateTable` | $CAD7 | 8 game-mode state handler pointers |
|
||
| `CharacterIntroDisplayDataTable` | $C687 | 10 display data pointers for character intro |
|
||
| `TitleScreenTileBlock` | $C326 | 6 rows × $17 bytes of title tile data |
|
||
| `TitleScreenPpuRecords` | $C3B0 | 9 PPU update records for title screen |
|
||
| `TitleScreenPalette` | $C43F | 32-byte initial palette |
|
||
| `TitleScreenAttributeData` | $C45F | $18 attribute bytes |
|
||
| `PlayerCountSelectionDisplayData` | $C507 | 8 bytes for player-count display |
|
||
| `PlayerCountSelectionMarkerTiles` | $C50F | $5C,$20,$5C marker toggle |
|
||
| `CharacterIntroPalette` | $C667 | 32-byte character intro palette |
|
||
| `CharacterIntroDisplayDataTable` | $C687 | Character intro display pointers |
|
||
| `CharacterNicknameHeaderData` | $C69B | "CHARACTER ; NICKNAME" display data |
|
||
| `MazeDataPointerBytes` | $FFF6 | $EC,$EC,$0E,$C1 — maze data pointer ($C10E) |
|
||
| Audio tables | $FF00–$FFEA | Period/mask tables, sequences, instruments |
|
||
|
||
---
|
||
|
||
## 12. Summary of Execution Flow
|
||
|
||
```
|
||
Power On / Reset
|
||
│
|
||
▼
|
||
Reset ($C033) ───────────────────────────────────────────────┐
|
||
│ sei, cld, disable PPU, wait 2× VBlank │
|
||
│ clear RAM $0000–$003D, clear pages $0100–$07FF │
|
||
│ PPUMASK=$06, scroll=$00 │
|
||
│ check warm-start signature "HIROKI AOYAGI" + $AA,$55 │
|
||
│ valid → skip full clear; invalid → clear + save sig │
|
||
│ set flags, optional 16-byte swap ($67↔$77) │
|
||
│ APUCTRL=$1F, APUFRAMECNT=$C0 │
|
||
│ jsr InitializeAudioEngine, jsr $EEB4 │
|
||
│ PPUCTRL=$88 (NMI enabled), $48=$FF (suspended) │
|
||
│ jmp MainEntry │
|
||
│ │
|
||
MainEntry ($C573) │
|
||
│ $40=$1, wait for NMI to clear it │
|
||
│ PPUCTRL=$88, PPUMASK=$00, clear OAM │
|
||
│ clear cocktail/player flags, $41/$42/$4C/$87/$88 │
|
||
│ init nametables, attributes, palette buffers │
|
||
│ select PPUCTRL ($8A), set ppuCtrlShadow │
|
||
│ │
|
||
MainDispatchLoop ($C645) ─────────────────────────────────────┤
|
||
│ $40=$1, wait for NMI │
|
||
│ Y = gameState ($3F) │
|
||
│ load handler from MainStateHandlerTable ($C267) │
|
||
│ jmp ($0010) → state handler │
|
||
│ │
|
||
├─ State $00: InitialStateHandler │
|
||
│ ├─ controller bits $0C → gameState=$06, $0F=$5A │
|
||
│ │ → jump MainEntry (restart sync) │
|
||
│ └─ else inc $42; if $42==$F0 → reset, gameState=$06 │
|
||
│ │
|
||
├─ State $02: PlayerSelectionState │
|
||
│ ├─ 1P/2P selection via controller bits $04/$08 │
|
||
│ ├─ timeout ($87/$88 reaches $0200) → gameState=$04 │
|
||
│ └─ confirm (bit $08) → clear suspended, jsr $EEB4, │
|
||
│ jmp GameModeEntry │
|
||
│ │
|
||
├─ State $04: State04Handler │
|
||
│ ├─ controller bits $0C → gameState=$02, restart │
|
||
│ └─ Y=$87 → dispatch via State04SubstateTable ($C52E) │
|
||
│ ├─ $00: State04Initialize (PPU setup, $87+=2) │
|
||
│ ├─ $02–$12: State04TimedSubstate ($30 frames) │
|
||
│ ├─ $14: State04LongDelay ($80 frames) │
|
||
│ └─ $16: State04FinalPhase → $47=$1, │
|
||
│ jmp GameModeEntry │
|
||
│ │
|
||
└─ State $06: TimedStateTransition │
|
||
│ dec $0F; if nonzero → MainDispatchLoop │
|
||
│ else gameState=$02 → MainDispatchLoop │
|
||
│ │
|
||
└─ loop back to MainDispatchLoop │
|
||
│ │
|
||
GameModeEntry ($C328) [from SetSelectionFlagAndContinue] │
|
||
│ │
|
||
├─ PPUCTRL=$88, wait VBlank, PPUMASK=$00 │
|
||
├─ clear gameplay state, HUD labels, score fields │
|
||
├─ if not suspended: clear player state, roundNumber=$3 │
|
||
│ else: roundNumber=$1 │
|
||
├─ common init: roundDisplayValue=$FF, PPUCTRL=$88 │
|
||
│ │
|
||
GameModeFrameSync ($C360) ────────────────────────────────────┤
|
||
│ $40=$1, wait for NMI │
|
||
│ │
|
||
├─ if suspended && controller bits $0C → gameState=$02, │
|
||
│ jmp MainEntry (restart) │
|
||
│ │
|
||
├─ if stateTimer != 0 → dec, return to @WaitForNextFrame │
|
||
│ │
|
||
└─ Y = gameState → dispatch via GameModeStateTable ($CAD7)│
|
||
│ │
|
||
├─ $00: GameModeState00 │
|
||
├─ $02: GameModeState02 │
|
||
├─ $04: GameModeState04 (pause/input handler) │
|
||
│ ├─ $060F != 0 → return to frame sync │
|
||
│ ├─ edge-detect controller bit $08 ($49) │
|
||
│ │ → new press: inc $4A, install pause cmds │
|
||
│ │ → bit 0 of $4A toggles pause display │
|
||
│ └─ not paused, bit0 of $4A clear → gameplay │
|
||
│ helper chain → return to frame sync │
|
||
├─ $06–$0C: GameModeState06/08/0A/0C │
|
||
└─ $0E: InitializeGameplayTransitionState │
|
||
│ │
|
||
└─ loop back to GameModeFrameSync │
|
||
│ │
|
||
└─ (eventually enters gameplay via $0E handler) │
|
||
│
|
||
NMI ($C10E) [once per frame] │
|
||
│ │
|
||
├─ save A, X, Y │
|
||
├─ PPUMASK=$1E │
|
||
├─ OAM DMA ($4500–$45FF → $0200–$02FF) │
|
||
├─ jsr FlushGameplayPpuUpdates ($DE3B) │
|
||
├─ jsr ReadNearbyNametableTiles │
|
||
├─ jsr UpdatePalettes ($3F00/$3F10 from $0299/$02A9) │
|
||
├─ restore scroll ($41,$42 → PPUSCROLL) │
|
||
├─ construct PPUCTRL from $47,$46,$43 │
|
||
├─ strobe JOY1, read both controllers (8 bits each) │
|
||
│ → $4D (current), $4E (newly pressed) │
|
||
├─ select active controller → $4F (previousInput) │
|
||
├─ inc frameCounter ($4B) │
|
||
├─ if $48==0: jsr InitializeAudioChannels │
|
||
├─ restore registers, rti │
|
||
│ │
|
||
IrqHandler ($C17E): rti (unused) │
|
||
│
|
||
Audio Engine (from NMI via InitializeAudioChannels/$EEB4) │
|
||
├─ 16 eight-byte channel slots scanned │
|
||
│ │ values $01–$04 activate a channel │
|
||
│ │ 4-byte parameters copied via $F6 pointer │
|
||
├─ UpdateAudioChannels ($EF2D): │
|
||
│ │ decrement countdowns, fetch command bytes, │
|
||
│ │ decode ($F0+=special, $C0+=delay, else=note), │
|
||
│ │ write APU registers $4000–$400F │
|
||
└─ UpdateAudio ($F8DE): 4 channels + SFX + DMA trigger │
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Notes
|
||
|
||
- **Build verification:** `make` in `copilot/` produces `reconstructed.nes`, compared whole-ROM against `rom.nes`.
|
||
- Vectors confirm NMI at $C10E, RESET at $C033, IRQ at $C17E (a bare `rti`, unused).
|
||
- The foreground main loop (`MainDispatchLoop` at $C645) is strictly NMI-synchronized via `$40`: NMI clears it each frame, and the main code spins waiting for it to clear before dispatching to the next state handler.
|
||
- `GameModeEntry` ($C328) is a second, separate state machine entered from the state-$04 final phase. It has its own frame sync (`$40`) and its own 8-state dispatch table (`GameModeStateTable` at $CAD7), leading into gameplay initialization.
|
||
- State $04 contains a nested secondary state machine (`State04SubstateTable` at $C52E) with 12 substate offsets, implementing a timed character-introduction sequence (GHOST names, nicknames, scoring legend).
|
||
- The warm-start signature `"HIROKI AOYAGI"` at $0052 allows the game to preserve RAM across a soft reset.
|
||
- Many routine labels are address-based (`State04TimedSubstate`, `LoadState04DisplayData`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional.
|
||
- `symbols.inc` (embedded at the top of `prg.s`) provides NES hardware register names, controller masks, and extensive zero-page symbol definitions. |