# Pac-Man (Tengen) (U) — Game Flow from Power-On This document describes the execution flow of the disassembled **Pac-Man (Tengen) (U)** PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `prg.s` in the repository. The rebuild is verified **byte-identical to the original ROM** — `make` compares the whole `reconstructed.nes` against `rom.nes`. **Variant note:** `Pac-Man (Tengen) (U)` and `Pac-Man (USA) (Namco)` are the same game, but they are **different builds**. This dump was disassembled from scratch; no name, map or file was carried over from the sibling dump, and the two were only compared afterwards. **Build difference from the sibling dump.** The two PRGs differ in **56 bytes in 8 runs**: | Region | Tengen | `Pac-Man (USA) (Namco)` | |--------|--------|------------------------| | $C3E3–$C408 | 26 spaces — the licensee field is blanked | `LICENSED BY NAMCO AMERICA INC` | | $FF00–$FF16 | `$FF` padding — there is **no boot code** | `sei` / `cld` / PPU off / double vblank wait / `jmp ColdStart` | | $FFFC–$FFFD | RESET vector = **$C033** | RESET vector = $FF00 | Everything else is byte-identical, so the two dumps disassemble to the same structure with the same addresses. The CHR is identical; the iNES header differs only in byte 6 (mirroring). **Consequence for the reset path.** In the sibling dump the RESET vector points at $FF00, which holds real boot code. **Here it points at $C033, and the boot sequence is inline at the start of the PRG.** The code is the same, just relocated: ``` $C033 sei / cld / lda #$00 / sta $2000 / sta $2001 $C03D lda $2002 / bpl $C03D ; wait for the first vblank $C042 ldx #$FF / txs / lda #$00 / tay ``` --- ## 1. Reset Vector — CPU $FFFC → $C033 | Vector | Address | Handler | |--------|---------|---------| | NMI | $C0FA | `NmiHandler` | | RESET | $C033 | `Reset` | | IRQ | $C16A | `IrqHandler` (a bare `rti` — unused placeholder) | **`Reset`** ($C033–$C042) performs the cold start: | Step | Code | Purpose | |------|------|---------| | 1–2 | `sei` / `cld` | disable interrupts, clear decimal mode | | 3–4 | `lda #$00` / `sta PPUCTRL` / `sta PPUMASK` | silence the PPU | | 5 | `lda PPUSTATUS` / `bpl` | wait for the first vblank | | 6 | `ldx #$FF` / `txs` | stack pointer to $01FF | | 7 | `lda #$00` / `tay` | A = 0, Y = 0 | Then: | Routine | Address | What it does | |---------|---------|--------------| | `ClearZeroPage` | $C048 | clears $0000–$003D (62 bytes) | | `ClearHighRAMPages` | $C054 | clears 9 pages from $8700 to $8F00 | | `CheckCopyrightSig` | $C077 | compares the 15-byte `ProgrammerSig` at $C0EB with RAM at $0052 | | `CopyrightMismatch` | $C086 | on a mismatch, clears all 256 zero-page bytes | | `CopyCopyrightToRAM` | $C091 | on a match, copies the signature to $0052 and sets `CopyrightVerified` | | `InitGameState` | $C0A0 | clears `GameState`, sets `OamDmaPage` = 7 | | `SwapEntityBuffers` | $C0B2 | swaps two 16-byte entity buffers at $0067 and $0077 | | `InitAudioAndPPU` | $C0C7 | enables the APU, initialises the sound engine, primes the PPUCTRL shadow, then `jmp RequestNMI_Dispatch` | The signature is the string **"HIROKI AOYAGI" + $AA + "U"** — the programmer's name, not a company name. --- ## 2. NMI / Frame Heartbeat — `NmiHandler` ($C0FA) ``` NmiHandler ($C0FA): pha / txa / pha / tya / pha ; preserve A, X, Y lda #$1E / sta PPUMASK lda #$00 / sta OAMADDR / sta NmiFlag lda OamDmaPage / sta OAMDMA ; OAM DMA from $0700 jsr UpdateSprites jsr UpdateGameLogic jsr UploadPalette1 lda ScrollX / sta PPUSCROLL lda ScrollY / sta PPUSCROLL lda ActivePlayerFlag / and CocktailModeFlag / and #$01 / asl a ora PpuCtrlShadow / sta PPUCTRL lda #$01 / sta JOY1 / lda #$00 / sta JOY1 ; strobe the controllers ldx #$08 ReadControllers: lda JOY1 / and #$01 / cmp #$01 / ror Player1Input lda JOY2 / and #$01 / cmp #$01 / ror Player2Input dex / bne ReadControllers ldx Player1Input / lda CocktailModeFlag / and ActivePlayerFlag / beq NmiSelectActiveInput ldx Player2Input NmiSelectActiveInput: stx ActiveInput inc FrameCounter lda GameplaySuspended / bne NmiEpilogue jsr RunLevelScript NmiEpilogue: pla / tay / pla / tax / pla / rti ``` The NMI reads **both** controllers into 8-bit shift registers and then picks one: in two-player mode the second controller wins, otherwise the first. `GameplaySuspended` ($FF while the game is running) gates the level script, so the script only runs when the game is *not* suspended. --- ## 3. Game State Dispatch `RequestNMI_Dispatch` ($C23C) sets `NmiFlag` and waits for the NMI to clear it, then reads the game state and dispatches through `GameStateDispatchTable` ($C253): ``` ldy GameState lda GameStateDispatchTable,y sta DispatchPtrLo lda GameStateDispatchTable+1,y sta DispatchPtrHi jmp (DispatchPtrLo) ``` The same indirect-dispatch shape appears three more times — at $D3CA (`DispatchPlayerBehaviour_Dispatch`), $D90F (`DispatchGhostToTable`) and $D9B6 (`DispatchGhostState`) — each with its own table. --- ## 4. Level Graphics | Routine | Address | What it does | |---------|---------|--------------| | `DrawMazeGraphics` | $C27D | writes `MazeTileGraphics` ($C2FD) to VRAM, 16 tiles per block, 6 screens per level | | `WriteAttributeTable` | $C2BE | writes `AttributeTableData` ($C387) — the per-screen colour attributes | | `WriteBorderPattern` | $C2E2 | writes `BorderPattern` ($C432) — 40 bytes to VRAM $23C8 | --- ## 5. Ghosts, Fruit and Level Data | Routine | Address | What it does | |---------|---------|--------------| | `UpdateGhosts` | $C89B | the ghost AI: picks a target, shifts the position block, places the fruit, then `jmp UpdateMazeTiles` | | `UpdateGhostTarget` | $C8AA | finds the pellet nearest the player and records it in $028D | | `ShiftGhostPositions` | $C8ED | moves the 12-byte position block $0022–$002D to $001E–$0029 | | `UpdateFruit` | $C977 | places the fruit from `FruitTargetTable` ($C9FF) | | `GetGhostDataPointer` | $C589 | reads a pointer from `CharacterDataTable` ($C66A) | | `CopyGhostData` | $C597 | copies that data to the $024B buffer until a $FF byte | | `UpdateMazeTiles` | $DAB0 | rewrites the maze tiles through `MazeTileOverlayTable0`–`3` and `MazeTileOffsetTable` | | `ResetGhostSlots` | $D249 | clears the per-ghost slot array at $BA | `CharacterDataTable` ($C66A) holds ten pointers into `CharacterNameStrings` ($C67E), which is where the ROM names its characters: ``` $ C67E " CHARACTER" $ C68A "!M...." "!SBLINKY" $ C692 "!SBLINKY" $ C69B "!AD...." $ C6A3 "!PINKY" "\"INKY" $ C6AB "\"INKY" "\"m...." $ C6B3 "\"m...." "\"sCLYDE" $ C6BA "\"m...." "\"sCLYDE" "\" 10 PTS" $ C6C2 "\"sCLYDE" "\" 10 PTS" "\" 50 PTS" $ C6CA "\" 10 PTS" "\" 50 PTS" "#M..." ``` So the four moving entities the code drives are the four ghosts — Blinky, Pinky, Inky and Clyde — and the ROM itself supplies the names. --- ## 6. Sound and the Level Script `InitAudio` ($EE4B) enables the APU channels and sets up the level-script workspace: a source pointer at $F0/$F1, a working buffer pointer at $F2/$F3, a parameter-table pointer at $F4/$F5 and a state array at $FE. `RunLevelScript` ($EE8F) walks the 16 eight-byte records of the working buffer, copies records around, reads 16-bit parameters with `ReadScriptParam` ($F082) and dispatches a command through `ScriptCommandTable` ($EFDD) with `DispatchScriptCommand` ($EFCC): | Command | Address | What it does | |---------|---------|--------------| | `ScriptCommand_MergeMask3F` | $F00A | merges a parameter into the record with mask $3F | | `ScriptCommand_MergeMaskCF` | $F022 | the same with mask $CF | | `ScriptCommand_MergeMaskF0` | $F03A | the same with mask $F0 | | `ScriptCommand_StoreAt2` | $F052 | stores a byte at record offset 2 | | `ScriptCommand_StoreAt4` | $F062 | stores a byte at record offset 4 | | `ScriptCommand_StoreAt1` | $F072 | stores a byte at record offset 1 | | `ScriptCommand_Store` | $F001 | stores into the source buffer | --- ## 7. Pass 3 Data-Table Inventory | Address | Name | Size | What it holds | |---------|------|------|---------------| | $C0EB | `ProgrammerSig` | 15 | the copyright signature compared with RAM $0052 | | $C253 | `GameStateDispatchTable` | 42 | 16-bit handler pointers indexed by `GameState` | | $C2FD | `MazeTileGraphics` | 138 | the maze tile images | | $C387 | `AttributeTableData` | 100 | the per-screen colour attributes | | $C412 | `InitialPalette` | 32 | the palette copied to $0299 | | $C432 | `BorderPattern` | 40 | the border/tile pattern written to VRAM $23C8 | | $C66A | `CharacterDataTable` | 20 | ten pointers into `CharacterNameStrings` | | $C67E | `CharacterNameStrings` | ~80 | the ghost and score strings | | $C9FF | `FruitTargetTable` | 32 | the values the fruit placement indexes | | $D32B | `TurnOrderTable` | 16 | the order in which directions are tried when blocked | | $D33B | `DirectionBitTable` | 16 | direction to button-bit mapping | | $D3CD | `DirectionBehaviourTable` | 248 | the handler pointers the direction routine dispatches through | | $D4DE | `AnimationFrameTable` | 581 | the animation frame table | | $D830 | `CollisionTileTable` | 16 | the tiles the collision check looks for | | $D840 | `CollisionPairTable` | 146 | the tile pairs the collision check compares | | $D912 | `GhostUpdateTable` | 121 | the pointers `DispatchGhostToTable` uses | | $D9B9 | `GhostStateTable` | 247 | the pointers `DispatchGhostState` uses | | $DBAD | `MazeTileOverlayTable0` | 256 | overlay tile bytes, case 0 | | $DCAD | `MazeTileOverlayTable1` | 52 | overlay tile bytes, case 1 | | $DCE1 | `MazeTileOverlayTable2` | 256 | overlay tile bytes, case 2 | | $DDE1 | `MazeTileOffsetTable` | 52 | overlay offsets | | $DF2B | `AttributeAddressTable` | 610 | the VRAM addresses the sprite routine writes | | $E45E | `ScoreTextTable` | 18 | the address pairs the score text is read from | | $EFDD | `ScriptCommandTable` | 36 | the 16 command handler pointers | | $F0A7 | `LevelDataPointerTable` | 24 | level data pointers | | $F0BF | `ScriptParameterTable` | 30 | the 16-bit values the script reads | --- ## 8. Naming and Residue **Pass 1** — 4,595 lines. 1,637 instruction lines covering 3,273 code bytes (20.0%), 842 data lines covering 13,111 bytes, 25 explicit `; ---- data $XXXX-$YYYY` regions, canonical audit comments throughout. Every one of the 2,479 audit comments was checked against `prg.bin` at its stated PRG offset: **0 mismatches**. **Pass 2 complete** — 227 label definitions, **zero `Label_` residue**: 199 routine entries and 28 internal branch targets, named `_`. **No raw `$XXXX` operand** remains on any `jsr`, `jmp` or branch — the four indirect jumps carry their `IndDispatchXX`-style names. The map is in `routinemap.tsv`. **Pass 3** — `locals.inc` holds 30 semantic zero-page aliases, each applied only at the sites where its role is proven (the addresses that carry two roles keep the neutral `ZpXX` name elsewhere; the per-site choice is in `sites.tsv`). **182** alias references are in the source, the rest of the zero-page references keep their neutral names. `apply_symbols.py` reports **0 substitutions pending and 0 residue** — every hardware register is symbolic. **Residue (recorded, not guessed):** - **Pass 1 gap — the unreached main game logic.** The tracer never reaches the block at **$C45A–$C55F**, so it is still `.byte` data. It is code: the tracer's own decoder walks it cleanly, and the dispatch table at $C511 points into it (`$C529` and `$C556`). It holds the input handling, the 32-bit pointer copies, the direction/turn logic and a second dispatch. It cannot be entered without a guess, so it is left as data here — **and the same gap exists in the promoted `Pac-Man (USA) (Namco)` dump**. Other untraced regions with the same character: $C9FF–$D248, $DBAD–$DE3C, $DF2B–$E18C, $E4C1–$E9D7, $EA53–$EB5D, $EB75–$EE4A, $F0A7–$FFFF. Re-deriving them is a Pass 1 job — a Mesen Code Data Logger capture is the tool the repo uses for it (`_tools/cdl_reclass.py`). - **Six command handlers were recovered.** The table `ScriptCommandTable` ($EFDD) is read by the traced instruction at $EFCC, and six of its entries point at $F00A, $F022, $F03A, $F052, $F062 and $F072 — all of which the tracer had left as data, and two of which it had mis-split across the `jmp $EF61` at $F01F and $F04F. Those 120 bytes are code here, decoded straight from the ROM. - 92 zero-page slots keep their neutral name; `locals.inc` lists every one of them with the reason. - The remaining raw absolute operands against RAM addresses stay numeric where their role is only partly traced — the same residue class accepted in `Pinball (JU)` and `Ice Climber (U)`. --- ## 9. Promotion to `NROM/` (2026-10-20) Moved out of `_wip/NROM/` with `git mv`; the build now compares the **whole ROM**, not just the PRG: - `header.s` / `chr.s` added (one-line `.incbin`s of `header.bin` / `chr.bin`) - `linker.cfg` gained the `HEADER` ($0000, $0010) and `CHR` ($0000, $2000) areas ahead of `PRG` ($C000, $4000) - `rom.nes` — symlink to `Pac-Man (Tengen) (U) [!].nes` (the original ROM name contains spaces and brackets) - `Makefile` builds `header.o` / `prg.o` / `chr.o` into `reconstructed.nes` and compares it against `rom.nes` `reconstructed.nes` is 24,592 bytes, MD5 `d34e7a74…28ad0` — equal to the original ROM. The `header.bin` / `prg.bin` / `chr.bin` splits were checked against the original before promotion: they are the ROM's first 16 bytes, its next 16,384 bytes and its last 8,192 bytes, exactly. Build verification: `make` in `NROM/Pac-Man (Tengen) (U)/` produces `reconstructed.nes`, compared against `rom.nes` (**whole-ROM byte-identical**).