Files
disasm/NROM/Pac-Man (Tengen) (U)
tmerritt df11857075 Promote Xevious (U) and Zanac (U) from _wip/ to NROM/
- Xevious (U): Promoted from _wip/NROM/Xevious (U)/ to NROM/Xevious (U)/
  - Pass 3 complete: 34.5% code coverage, 143 semantic entry points,
    4 named data tables, ~20 semantic ZP aliases
  - NROM mapper 0, 32 KB PRG, 8 KB CHR, horizontal mirroring

- Zanac (U): Promoted from _wip/UNROM/Zanac (U)/ to NROM/Zanac (U)/
  - Pass 3 complete: 34.5% code coverage (bank 7 only), 133 semantic
    entry points, 26 named data tables, 14 semantic ZP aliases
  - UNROM mapper 2, 128 KB PRG (8 banks), no CHR
  - Bank 7 fixed at CPU -; banks 0-6 switchable at 000-
  - Bank 7 assembled separately as prg.07.s with .org

Both projects build byte-identical. Updated root README.md inventory,
per-project details, and WIP.md accordingly.

Move Zanac (U) to UNROM/ mapper directory

Zanac (U) is a UNROM (mapper 2) game, not NROM (mapper 0). Move it to
the correct mapper directory and update README.md and WIP.md references
accordingly. Update project counts in WIP.md (857->856 total, UNROM
98->97 projects).

Promote Mermaids of Atlantis (U) from _wip/ to MAPPER8/

Additional cleanup of Mermaids of Atlantis
2026-10-21 07:20:59 -04:00
..

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 <Routine>_<Role>. 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 .incbins 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).