# Sqoon (U) ## Status **Pass 3 complete; promoted.** `prg.s` is a traced disassembly with all address-derived labels replaced, all `jsr`/`jmp` operands symbolic, hardware registers named via `symbols.inc`, and `apply_symbols.py` reporting 0 residue. The 271 entry points that still carried an address-derived `Sub_` name were given semantic names on 2026-10-21 — see [Entry-point naming](#entry-point-naming-2026-10-21); **0 address-derived `jsr`/`jmp` targets now remain.** Code coverage: 18,471 / 32,768 bytes (56.4%). `make` assembles and verifies byte-identical to `rom.nes`. Data tables: all 17 data blocks in the PRG are interpreted in the [Pass 3 data-table inventory](#pass-3-data-table-inventory) below - 15 of them carry a name (27 labels in all) and two are left unnamed because nothing in the dump reads them (`$84A0-$84AF` and `$B5D5-$B5DD`). The 12 `.byte` rows whose audit comment carried a wrong PRG offset (and, in four cases, a second, damaged copy of the comment) were repaired to the canonical `; CPU:$XXXX PRG:XXXX AA BB` form; the bytes themselves are untouched. ## Entry-point naming (2026-10-21) Every `jsr`/`jmp` target that still carried an address-derived name (`Sub_` / `SubC`) was re-read against the code that calls it and given a name that says what the ROM shows the routine is for. **271 entry points were renamed; 0 address-derived `jsr`/`jmp` targets remain in `prg.s`.** Only labels and the comments that name them changed — not one instruction byte — and the whole-ROM `cmp` still reports *Byte-identical ROM verified.* What the evidence was: | Area | Evidence in the dump | Names given | |---|---|---| | Phase machine | `DispatchPhaseHandler` reads a 16-bit handler address out of `PhaseHandlerPointerTable` ($83C2), indexed by the phase counter `$83A4`/`Zp50`, and jumps through it; each handler counts `X` down and then advances the phase or sub-state counter | `DispatchPhaseHandler`, `PhaseTitleHandler`, `PhaseLevelInitHandler`, `PhasePaletteLoadHandler`, `PhaseDelayHandlerOne`/`Two`, `PhaseWaitForButton`, `PhaseSetVramFlagHandler`, `AdvancePhaseIndex`, `AdvancePhaseSubIndex`, `ResetPhaseSubIndex`, `SetPhaseIndex` | | Player ship | `WritePlayerToOamBuffer` copies `PlayerXHigh`/`PlayerYHigh` into the OAM shadow at `$0200`/`$0300` and is called from every sprite-drawing routine with a different tile offset | `WritePlayerToOamBuffer`, `ReturnFromPlayerOamUpdate`, `SetPlayerSpriteBaseY`, `SetPlayerSpriteLeftEdge`, `SetPlayerSpriteRightEdge`, `ResetPlayerPosition`, `InitPlayerSpriteState`, `InitPlayerGameState` | | Pods | the `$0700`-per-slot records hold a position, a velocity and a life byte; `DecrementPodLife` decrements the life byte and clears the slot's state when it reaches zero, and the `$0706`/`$0708` bytes select a pod's movement and firing behaviour | `UpdatePodPosition`, `UpdatePodVelocity`, `UpdatePodSubPixelPosition`, `UpdatePodMovement`, `CheckPodOnScreen`, `CheckPodFullyOnScreen`, `CheckPodNearPlayer`, `CheckPodCollision`, `DecrementPodLife`, `DecrementPodLifeTypeOne`…`Fourteen`, `MovePodUp`/`Down`/`Left`/`Right`, `CanPodShoot`, `FirePodShot`, `RegisterPodHit`, `SpawnNewPod`, `RestartPodGrowth` | | Shots | `$051B`/`$051D`/`$0521` hold the player shot's position and life; `$0577`/`$0579` hold the pod shot's | `SpawnPlayerShot`, `MovePlayerShot`, `RetirePlayerShot`, `SpawnPodShot`, `UpdatePodShots`, `CheckShotOnScreen`, `FindFreeShotSlot`, `IsShotSlotFree` | | VRAM writer | `LoadVramScript` walks the sprite-definition strings and `DrawVramText` pushes `PPUMASK`/`PPUADDR` before the tile bytes are pushed to `$2000` | `LoadVramScript`, `QueueVramAddress`, `QueueVramTileBytes`, `DrawVramScript`, `DrawVramText`, `ClearNametableTiles`, `FlushVramByteQueue`, `IsVramWritePending`, `ClearVramUpdateFlag` | | Sound | `PlaySoundEffectById` indexes the sound-id table at `$E134` and the script reader `ReadSoundScriptByte` walks a byte script into `APUPULSE1CTRL`/`APUPULSE2CTRL` | `PlaySoundEffectById`, `PlaySoundEffect`, `RunSoundScript`, `ReadSoundScriptByte`, `BackSoundScriptPointer`, `WriteSoundChannelRegisters`, `InitSoundChannelScript`, `UpdateSoundChannelState`, `UpdateSoundChannelDuration`, `InitSoundEngine`, `DisableApuChannels`, `IsSoundReady` | | Sprite command lists | `ProcessSpriteCommand` walks a byte-coded sprite-definition string and `WriteSpriteBytesToPpu` pushes the tile bytes to `PPUDATA` | `ProcessSpriteCommand`, `ProcessSpriteCommandTwo`…`Four`, `ReadSpriteCommandRecord`, `ReadSpriteCommandByte`, `ReadSpriteCommandPointer`, `WriteSpriteBytesToPpu`, `CheckSpriteEndMarker` | | HUD / phases | the `PhaseLabelText`, `PhaseRemainingText` and `LevelIntervalText` strings are pushed by the text printer; the phase counters `$0473`-`$047B` gate the level music | `DrawHudScript`, `DrawPhaseLabelText`, `DrawPhaseBannerText`, `DrawGameOverBanner`, `UpdateHudScore`, `StartLevelOneMusic`…`Seven`, `SetScrollingMode`, `UpdateScrollSplit` | **Residue: none.** `grep -nE "^(Sub_|SubC)[0-9A-F]{4}" prg.s` returns nothing: every `jsr`/`jmp` target carries a semantic name. One separate observation that is **not** address-derived naming: the tracer attached the prefix `IrqHandler_` to several hundred internal branch targets (`IrqHandler_Loop*`, `IrqHandler_OnNonzero*`) that are in fact in the main game code, not in the IRQ handler. Those names are not address-derived, so they are outside the scope of this pass, but they are misleading and would be worth re-deriving from the control flow. ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry. **RESET** is at `$8000` (`Reset`). The CPU comes here out of reset with the PPU and APU in their power-on state and the zero page and RAM uninitialised. The reset path calls: | Address | Routine | |---------|---------| | `$82B2` | `CheckRomMagicBytes` | | `$817B` | `SetBackgroundPatternTable` | | `$80EE` | `WaitForIrqLoop` | | `$82D9` | `IrqHandler_OnNonzero6` (falls through) | | `$8180` | `IrqHandler_OnZero` (falls through) | | `$80EE` | `WaitForIrqLoop` | | `$82E3` | `IrqHandler_Loop10` (falls through) | | `$81E0` | `ClearScreenAndFlags` | | `$8324` | `ResetOamShadow` | | `$82EB` | `IrqHandler_OnNonzero7` (falls through) | | `$81E7` | `ClearNametableTiles` | | `$821C` | `ClearVramUpdateFlag` | | `$8326` | `IrqHandler_OnNonzero10` (falls through) | **NMI** is at `$8014` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$80F1` | `IrqHandler` | **IRQ** is at `$80F1` (`IrqHandler`) - a different address from RESET. ## ROM | Property | Value | |----------|-------| | Format | iNES | | Mapper | 0 (NROM) | | PRG | 32 KB | | CHR | 8 KB | | Mirroring | Vertical | | NMI vector | $8014 | | RESET vector | $8000 | | IRQ vector | $80F1 | ## Power-on flow RESET enters at `$8000`: ldx #$FF ; CPU:$8000 PRG:0000 A2 FF txs ; CPU:$8002 PRG:0002 9A stx NmiFrameFlag ; CPU:$8003 PRG:0003 86 FE jsr CheckRomMagicBytes ; CPU:$8005 PRG:0005 20 B2 82 jsr SetBackgroundPatternTable ; CPU:$8008 PRG:0008 20 7B 81 lda #$00 ; CPU:$800B PRG:000B A9 00 sta NmiFrameFlag ; CPU:$800D PRG:000D 85 FE sta Zp9A ; CPU:$800F PRG:000F 85 9A jmp WaitForIrqLoop ; CPU:$8011 PRG:0011 4C EE 80 pha ; CPU:$8014 PRG:0014 48 lda NmiFrameFlag ; CPU:$8015 PRG:0015 A5 FE beq NmiHandler_Loop ; CPU:$8017 PRG:0017 F0 03 NMI handler at `$8014`: pha ; CPU:$8014 PRG:0014 48 lda NmiFrameFlag ; CPU:$8015 PRG:0015 A5 FE beq NmiHandler_Loop ; CPU:$8017 PRG:0017 F0 03 jmp IrqHandler ; CPU:$8019 PRG:0019 4C F1 80 inc NmiFrameFlag ; CPU:$801C PRG:001C E6 FE inc FrameCounter ; CPU:$801E PRG:001E E6 54 jsr StartSpriteDma ; CPU:$8020 PRG:0020 20 58 81 lda PpuCtrlShadow ; CPU:$8023 PRG:0023 A5 1D ## Pass 3 data-table inventory Every data block in the PRG is listed below. The block boundaries are the tracer's (`; ---- data $XXXX-$YYYY` markers in `prg.s`); where a block holds more than one structure the sub-labels in `prg.s` mark the split. Reading the bytes: Sqoon's CHR holds its 8x8 font at the ASCII tile positions (`$41` = 'A' ... `$5A` = 'Z'), so a text string in the PRG is stored as plain ASCII and the character code *is* the tile index the text routine hands to the PPU. | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$833A-$8399` | 96 | `PlayerShipStateByteTable` | 96 single-byte values in `$00-$31`, laid out as 24 four-byte groups; no field meaning established. Copied into zero page `$56-$75`. | `lda $8359,x` at `$8326` and `lda $8379,x` at `$8331` - both loops walk *backwards* from the end of the table with X = `$20`..`$01` and store into `Zp55,x`; the second loop overwrites the first, so only `$838A-$8399` survives in zero page | | `$839A-$83A2` | 9 | `HomeDataMarkerText` | ASCII `":HOMEDATA"` (no terminator) | no instruction operand lands here; the same string also sits at `$B5D5` | | `$83C2-$83D3` | 18 | `PhaseHandlerPointerTable` | 9 little-endian 16-bit code addresses: `$83D4`, `$83F5`, `$84BF`, `$8521`, `$8530`, `$8550`, `$8589`, `$85FC`, `$85FF` - one entry per game phase | indirect: `DispatchPhaseHandler` sets `DispatchPtrLo`/`Hi` to `$83C2`, reads the entry with `lda (DispatchPtrLo),y` at `$83B4`/`$83B9` and reaches it with `jmp (PlayerIndex)` at `$83BF` | | `$84A0-$84AF` | 16 | *(unnamed - unreferenced)* | 16 single-byte values in four groups of four (`$0A`,`$12`,`$3B`,`$FE` / `$0A`,`$14`,`$00`,`$FF` / `$0A`,`$12`,`$00`,`$FE` / `$0A`,`$14`,`$3B`,`$FF`); the layout is not established | **no instruction in this dump reads it.** No operand lands inside the block, and a scan of the whole PRG finds no 16-bit pointer to `$84A0`. Unreferenced data, ROM offset `$04A0` | | `$85F3-$85FB` | 9 | `GameOverText` | ASCII `"GAME OVER"` (9 characters, no terminator) | `lda $85F3,x` at `$85CC`; the routine stops the line at a space (`cmp #$20` at `$85CF`) and wraps to the next row after nine characters, so the space in the middle of the string starts the second row | | `$85FC-$8606` | 11 | *(code inside the data block)* | `JMP $850D` at `$85FC` and `JSR $E239` / `LDA #$01` / `JMP $850D` at `$85FF` - the phase-7 and phase-8 handlers | reached through `PhaseHandlerPointerTable` entries 8 and 9 | | `$8690-$8699` | 10 | `PhaseSpritePointerTable` | 5 little-endian 16-bit pointers to sprite-definition strings: `$DF93`, `$DFCF`, `$E000`, `$E02D`, `$E05B` - one per game phase | `lda $8690,x` at `$8646` and `lda $8691,x` at `$864B`, with X = 2 x the phase counter `$047E`; the two bytes become `Zp12`/`Zp13`, the pointer consumed by `LoadVramScript` | | `$869A-$869C` | 3 | `HudSpriteTileOverrideTable` | 3 tile indices (`$BE`, `$CD`, `$CE`) | `lda $869A,y` at `$8660`; a non-zero entry replaces the default tile | | `$869E-$86A0` | 3 | `HudSpriteTileTable` | 3 default tile indices (`$B9`, `$BA`, `$BB`) | `lda $869E,y` at `$8665`, used when the override byte is zero; the tile becomes `PlayerXLow` (`$1A`) | | `$86A2-$86A4` | 3 | `HudSpriteRowTable` | 3 row values in pixels (`$BC`, `$20`, `$28`) | `lda $86A2,y` at `$867A` -> `PlayerYHigh` (`$1C`) | | `$86A6-$86A8` | 3 | `HudSpriteColumnTable` | 3 column values in pixels (`$20`, `$28`, `$B0`) | `lda $86A6,y` at `$867F` -> `PlayerXHigh` (`$19`); `$86A9` holds a fourth, unused column value | | `$8814-$8825` | 18 | `LevelStartScrollTable` | 18 single-byte values, read as six three-byte records; only the first record is used | `lda $8814,y` at `$87DA` copies exactly three bytes (`$A0`,`$86`,`$01`) into zero page `$4D`-`$4F` (`ScrollX`, `ScrollY`) when a level starts; the remaining fifteen bytes are not read | | `$8AB4-$8ACB` | 24 | `ScreenStateSaveAddressTable` | 24 zero-page addresses (`$8F`..`$96`, `$AE`..`$B1`, `$C6`,`$CA`,`$CE`,`$D2`,`$D6`,`$DA`,`$DE`,`$E2`, `$BE`,`$EA`,`$EC`, `$B2`,`$B6`, `$AC`,`$C0`) - the zero-page fields whose contents are saved before a screen change and restored after it | `lda $8AB4,y` at `$8A59` saves them into `$063D`..`$0657`; `lda $8AB4,y` at `$8A93` writes them back. The loop count is `$1B` (27), so three bytes of the string that follows are also used as addresses | | `$8ACC-$8AD4` | 9 | `LevelIntervalText` | ASCII `"INTERVAL"` + `$00` terminator | the text printer `SetPlayerSpriteBaseY` is entered with the pointer `$8ACF` built at `$8A40`/`$8A44`; `$8ACC`-`$8ACE` are also read as zero-page addresses `$49`,`$4E`,`$54` by the save/restore loop | | `$8B48-$8B4E` | 7 | `PhaseLabelText` | ASCII `"PHASE "` + `$00` | text printer `SetPlayerSpriteBaseY`, pointer `$8B48` built at `$8B0A`/`$8B0E` | | `$8B4F-$8B55` | 7 | `PhaseRemainingText` | ASCII `"LEFT "` + `$00` | text printer `SetPlayerSpriteBaseY`, pointer `$8B4F` built at `$8B2F`/`$8B33` | | `$8BBD-$8BC7` | 11 | `LevelStartParameterTable` | 11 single-byte values; only the first (`$02`) is non-zero | `lda $8BBD,x` at `$8BAE` copies all eleven bytes into both `Zp24,x` and `Zp2F,x` (`$24`-`$2E` and `$2F`-`$39`) at a level start | | `$8C41-$8C50` | 16 | `EnemySpawnPositionTable` | 8 pairs of (X, Y) spawn coordinates: `($ED,$39)`, `($FD,$75)`, `($E5,$67)`, `($BA,$26)`, `($AA,$39)`, `($5F,$75)`, `($54,$67)`, `($09,$7A)` | `lda $8C41,x` at `$8C23` -> `SpriteX`, and `lda $8C41,x` at `$8C2E` -> `SpriteY`, with X = 2 x (enemy number - 1) | | `$8C51-$8C60` | 16 | `EnemySpriteTileAttributeTable` | 8 pairs of (tile index, sprite attribute): `($C9,$BB)`, `($CB,$BF)`, `($CD,$C3)`, `($CF,$C7)`, `($D1,$BB)`, `($D3,$BF)`, `($D5,$C3)`, `($D7,$BD)` | `lda $8C51,x` at `$8C28` -> `SpriteTile`, `lda $8C51,x` at `$8C33` -> `SpriteAttr`, and `lda $8C51,x` at `$8C68`/`$8C6E` in the variant that skips the position bytes | | `$B5D5-$B5DD` | 9 | *(unnamed - unreferenced)* | ASCII `":HOMEDATA"` (no terminator) | **no instruction in this dump reads it.** No operand lands inside the block and a scan of the whole PRG finds no 16-bit pointer to `$B5D5`. Unreferenced data, ROM offset `$35D5`; the identical string at `$839A` is the copy the game shows | | `$BB39-$DC07` | 8400 | `LevelLayoutData` | 8,400 single-byte values in `$01`-`$FF`; no record size or encoding could be established from the bytes alone | **no instruction operand lands in this range.** Only the last sixteen bytes of the block are read (below). Unreferenced data, ROM offset `$3B39` | | `$DC08-$DC0F` | 8 | `EnemySpeedPresetTable` | 8 single-byte values (`$0F`,`$09`,`$15`,`$11`,`$25`,`$17`,`$13`,`$05`) | `lda $DC08,x` at `$B433` -> `Zp65`, `Zp6F`, `Zp70`, `Zp73`, `Zp74`; the value is compared with `$0F` at `$B440` | | `$DC10-$DC17` | 8 | `EnemySpeedIncrementTable` | 8 single-byte values (`$07`,`$03`,`$3A`,`$05`,`$15`,`$05`,`$0F`,`$15`,`$0F` - the first is split by the label boundary) | `lda $DC13,x` at `$905E` -> `Zp67`/`Zp6B`, indexed by a counter derived from `$0461` | | `$DC6E-$DC8A` | 29 | `EnemySpeedParameterTable` | 29 per-frame movement increments (`$02` x4, `$05` x8, `$07`, `$0A` x3, `$0D` x3, `$14`, `$1E`, `$64`, `$C8`, `$F0`, `$00`, `$14`, `$0A`, `$05`, `$0A`); `$F0` is a marker meaning "step by `$F4` (-12)" | `lda $DC6E,x` at `$DC2B`, walked from index 1 up to a count taken from `$0461`; the byte is added into the 16-bit accumulator `Zp7B`/`Zp7C` | | `$DD1A-$E08A` | 881 | `SpriteDefinitionStringTable` | a run of variable-length sprite-definition strings in the format `LoadVramScript` expects: two scroll bytes, then a list of tile indices terminated by a byte `>= $FE` | reached through pointers built at run time (`$DD1A` at `$8440`, `$DD23`/`$DD24` at `$8BC8`/`$8BE1`, `$DDBE` at `$8BF6`/`$8C07`, `$DDCF` at `$8460`); `LoadVramScript` and `DrawVramText` read them and write the tiles into the OAM buffer at `$0100` | | `$E134-$E238` | 261 | `SoundEffectHandlerTable` | 87 three-byte records `[flags, pointer low, pointer high]`. The flags byte is tested against `$00`, `$01`, `$FE` and masked with `$80`; the pointer is the address of the sound-effect data the record describes | `lda $E134,y` at `$E098`, `$E0A9`, `$E0AE`, `$E0B8`, `$E0C3`, `lda $E135,y` at `$E0C8` and `lda $E136,y` at `$E0CD`, with Y = 3 x the sound-effect number | | `$E4B7-$E4D2` | 28 | `SoundEffectPeriodTable` | 14 little-endian 16-bit APU pulse-1 timer periods (`$06AE`, `$064E`, `$05F4`, `$059E`, `$0540`, `$0501`, `$04B9`, `$0475`, `$0435`, `$03F9`, `$03D0`, `$038A`, `$0000`, `$001E`); the high nibble of the sound number rotates the period right (`lsr $47` / `ror $46` at `$E308`/`$E30A`) to pick the octave | `lda $E4B7,y` at `$E2E7` and `lda $E4B8,y` at `$E2EC`, with Y = 2 x the sound number | | `$E4D3-$F5D3` | 3847 | `SoundDriverScriptData` | a long run of single-byte values; the bytes decode as 6502 opcodes only by coincidence (`ASL $80`, `BRK`, `DEC $01` ... in a row make no sense), so this is data, not code | **no instruction operand lands in this range** and no 16-bit pointer to it exists in the PRG. Unreferenced data, ROM offset `$64D3`; it sits directly after the sound driver's code, so it is presumably the driver's script/envelope data, but nothing in this dump proves it | | `$FFE8-$FFF9` | 18 | `VectorTablePadding` | 18 bytes of `$FF` | none - fill before the vectors | | `$FFFA-$FFFF` | 6 | `CpuVectorTable` | 3 little-endian 16-bit vectors: NMI `$8014`, RESET `$8000`, IRQ `$80F1` | the CPU reads them out of the vector table | ## Build ``` Byte-identical ROM verified. ```