# Elevator Action (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. Code coverage: 19,446 / 32,768 bytes (59.3%). `make` assembles and verifies byte-identical to `rom.nes`. **Pass 3 data tables** — all 25 data blocks are named, interpreted and listed with the instruction that reads each one in *Data-table inventory (Pass 3, re-interpreted 2026-10-21)* below. Fifteen blocks were renamed in that pass because the consuming code did not support their first-pass names; three new labels were split out of neighbouring blocks. ## 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 | |---------|---------| | `$800A` | `Reset_OnNonnegative` (falls through) | | `$8013` | `Reset_OnNonnegative2` (falls through) | | `$8027` | `Reset_Loop` (falls through) | | `$8031` | `Reset_OnNonzero` (falls through) | **NMI** is at `$8081` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$81D6` | `ReadControllerButtons` | **IRQ** is at `$8000` (`Reset`) - the same address as RESET, so the IRQ vector reaches the reset path. ## ROM | Property | Value | |----------|-------| | Format | iNES | | Mapper | 0 (NROM) | | PRG | 32 KB | | CHR | 8 KB | | Mirroring | Horizontal | | NMI vector | $8081 | | RESET vector | $8000 | | IRQ vector | $8000 | ## Power-on flow RESET enters at `$8000`: sei ; CPU:$8000 PRG:0000 78 cld ; CPU:$8001 PRG:0001 D8 ldx #$FF ; CPU:$8002 PRG:0002 A2 FF txs ; CPU:$8004 PRG:0004 9A lda #$10 ; CPU:$8005 PRG:0005 A9 10 sta PPUCTRL ; CPU:$8007 PRG:0007 8D 00 20 lda PPUSTATUS ; CPU:$800A PRG:000A AD 02 20 bpl Reset_OnNonnegative ; CPU:$800D PRG:000D 10 FB lda #$FF ; CPU:$800F PRG:000F A9 FF ldy #$24 ; CPU:$8011 PRG:0011 A0 24 sta $0200,y ; CPU:$8013 PRG:0013 99 00 02 dey ; CPU:$8016 PRG:0016 88 NMI handler at `$8081`: sta NmiSaveA ; CPU:$8081 PRG:0081 85 9A stx NmiSaveX ; CPU:$8083 PRG:0083 86 9B sty NmiSaveY ; CPU:$8085 PRG:0085 84 9C lda ZpFA ; CPU:$8087 PRG:0087 A5 FA beq NmiHandler_Loop ; CPU:$8089 PRG:0089 F0 03 jmp ReadControllerButtons ; CPU:$808B PRG:008B 4C D6 81 lda PpuCtrlShadow ; CPU:$808E PRG:008E A5 88 and #$7F ; CPU:$8090 PRG:0090 29 7F ## Address-derived entry points (residue) `PROMPTS.md` asks every `jsr`/`jmp` entry point to carry a semantic name and accepts the address-derived `Sub_XXXX` form **only where the dump does not reveal a better one**. This dump started with **339** address-derived entry points; **all 339 now carry a semantic name and none remain address-derived.** The names come from the game's own subject matter - the elevator car, the floors of the building, the passengers waiting to get in or out, and the sprites the game draws - together with the names already used by the data-table inventory above (`SpriteHandlerJumpTable`, `SpriteOamOffsetTable`, `FloorBoundaryTable`, `VramDrawScriptTable`, `HandlerDispatchJumpTable`, `MovementDeltaTable`, `DescentSpeedTable`). Representative examples: | Address | Name | Evidence | |---------|------|----------| | `$82A3` | `UpdateElevatorState` | the elevator's per-frame state machine, called from 61 sites | | `$8334` | `ReadVramScriptByte` | the byte-script interpreter's operand fetch, called from 74 sites | | `$8358` | `UpdateSpriteHandler` | the per-sprite handler dispatch, called from 32 sites | | `$8610` | `AdvanceVramAndOamPointers` | the VRAM and OAM shadow write cursors | | `$973E` | `RunVramDrawScript` | the byte-coded screen-draw script runner, called from 17 sites | | `$9A0B` | `ComputeVramAddressForSpriteType` | the sprite-type-to-VRAM-address calculation | | `$9D62` | `StartLevel` | resets the level state and installs the elevator handler | | `$BDE6` | `ResetLevelAndDraw` | clears the sprite tables and restarts the level draw | | `$DFF1` | `MoveSpriteBySpeed` | adds the sprite's speed byte to its Y position, called from 19 sites | | `$E2D7` | `IsSpriteActive` | the sprite's active-flag test, called from 14 sites | | `$F6DB` | `PlaySoundEffect` | the sound-effect entry, called from 39 sites | The names are derived from what the code *does* - which field it writes, which table it reads, which sprite slot it touches - and not from the game's on-screen vocabulary, which the dump does not carry: the ROM never says which sprite type is a passenger and which is a decoration, so the names stay at the level the instructions support ("sprite", "floor", "elevator"). ## Data-table inventory (Pass 3, re-interpreted 2026-10-21) Every data block in `prg.s` is listed. "Read by" gives the address of the instruction in *this* dump whose operand lands in the block; where a block is reached through an indirect pointer instead, the address that sets the pointer is given. The 2026-10-21 pass re-derived every row from the consuming code and renamed fifteen blocks whose first-pass names the code did not support. | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $83CB–$8400 | 54 | `SpriteOamOffsetTable` | 27 two-byte records; byte 0 is the byte offset of that sprite's three-byte OAM record ($00, $0F, $1F, $47, $57, $67, $77, $87, $97, $A7, $B7, $CF, $DF …), byte 1 is unused. | `lda $83CB,x` at $823A (the routine subtracts 2 and uses the result as the OAM shadow address) | | $8401–$844E | 78 | `SpriteOamTileAttributeTable` | 39 two-byte records `[tile, attribute]`: tile $61 attr $9D, tile $2A attr $BB, tile $3D attr $D7 … and the last sprite's pair is tile $00 attr $00. | `lda $8401,x` at $824D (tile) and `lda $8402,x` at $8253 (attribute) | | $8628–$8667 | 64 | `SpriteHandlerJumpTable` | 32 little-endian 16-bit handler addresses, ascending from $87C2 in steps of $0F. | none directly — no operand in this dump names the block | | $8668–$973D | 4,306 | (inside `SpriteHandlerJumpTable`) | 2,155 further little-endian 16-bit values that are not addresses: mostly (X, Y) sprite-slot position pairs ($0101, $0202 …) mixed with more handler addresses. | none | | $9CA1–$9D40 | 161 | `SpriteAttributeMaskTable` | 32 groups of five bytes: one flag byte ($00) followed by four $FF mask bytes. | none directly | | $9D41–$9D60 | 32 | `FloorBoundaryTable` | 32 byte boundaries ($70, $88, $70, $88, $10, $28, $70, $88, $D0, $E8, $10, $28, $B0, $C8, $90, $A8, $70, $88, $50, $68, $30, $48, $7C, $7C, $1C, $7C, $DC, $1C, $BC, $9C, $7C, $5C) — the floor edges the sprite's Y is tested against. | `cmp $9D41,y` at $9A55 and $9A5B; `lda $9D41,y` at $9A6F and $9A7A | | $A240–$A247 | 8 | `SpriteInitPositionTable` | The eight initial sprite Y positions: $40, $50, $60, $70, $80, $90, $A0, $B0. | `lda $A240,x` at $A234 | | $A248–$A24F | 8 | `SpritePatternIndexTable` | The eight sprite pattern indices: $50, $51, $50, $52, $53, $54, $55, $56. | `lda $A248,x` at $A239 | | $A281–$A284 | 4 | `VramCopyRoutinePointerTable` | Two little-endian 16-bit pointers, $A3E3 and $973E. $973E is `RunVramDrawScript`, the routine that copies a byte stream to VRAM; $A3E3 is the PPU-data reader it is paired with. | none directly — the block is the entry list of the copy routine | | $A348–$A3E2 | 155 | `VramDrawScriptTable` | Eleven byte-coded screen-draw sub-scripts, at $A348, $A368, $A374, $A380, $A38C, $A398, $A3A4, $A3B6, $A3C8, $A3D1 and $A3DA. | the pointer is set at $A286, $9FB3, $9FC4, $9FD5, $9FE6, $9FF7, $A00E, $A025, $A03C, $A04D, $A05E and $A06F; the stream is then copied by `RunVramDrawScript` | | $A3F9–$A46E | 118 | `VramDrawScriptTable2` | Three more byte-coded screen-draw sub-scripts, at $A40B, $A42B and $A44B. The ROM gives no further way to tell this group from the one above, so it is numbered. | the pointer is set at $A30F, $A31A and $A331; the stream is then copied by `RunVramDrawScript` | | $AA9B–$B50C | 2,674 | `LevelTilemapData` | The building's background tile map: the first 256 bytes are the blank tile $64 repeated, then tile indices run $00–$0D and upward ($00, $01, $01, $01, $02, $03, $04, $05, $06, $07, $08, $07, $09, $0A, …). | none directly | | $B525–$B58C | 104 | `SpriteTilePatternData` | 4-byte records of three tile indices plus an attribute: $FF $FF $FF $60, $16 $15 $12 $17, $FF $FF $FF $17, $17 $17 $0E $0F … | none directly | | $B599–$BB2A | 1,426 | `SpriteTileGraphicData` | The same 4-byte `[tile, tile, tile, attribute]` record shape, with two ramp tables inside it: 32 ascending values at $B82B ($00, $02, $04, $05, $07, $08, $0A, $0C …) and a repeating five-step ramp $00, $30, $60, $90, $C0 at $BADB. | `lda $BADB,y` at $A5F6; `eor $BB03,y` at $A5FF; `lda $BA7B,y` at $A90E; `lda $B82B,y` at $A9A4 | | $BDB5–$BDC9 | 21 | `SpriteByteScript` | 21 bytes of byte-coded data. It does not decode as a clean instruction stream ($92 is not an official opcode), so it is data rather than unreached code. | the pointer $BDC2 is set at $BD7E/$BD84 for the VRAM copy routine | | $BE15–$BE1D | 9 | `VramDrawScript` | Nine bytes of byte-coded screen-draw data. | the pointer $BE15 is set at $BDFD/$BE01, then `RunVramDrawScript` copies it | | $C2F1–$C320 | 48 | `SpriteOamPositionTable` | Twelve 4-byte records `[X, $FF, $02, Y]`: X = $20, $28, $30, $38, $40, $48, $B0, $B8, $C0, $C8, $D0, $D8 and Y = the same twelve values — the left and right edges of the building. | the pointer $C2F1 is set at $C2A9/$C2AF and the pointer $C309 at $C2BB/$C2C1 | | $C487–$C837 | 945 | `SpriteByteScriptTable` | Byte-coded sprite scripts. $C487–$C506 is one script; $C507–$C51E is a second in a different encoding (`$68 $89 $02 $68 $68 $7A …`); from $C51F the bytes are small counters and lengths. | `lda $C507,y` at $C15C; `ldx $C52B,y` at $C40C; `ldx $C54B,y` at $C422 and $C439 | | $D0DE–$D0ED | 16 | `ObjectSpeedTable` | Sixteen small per-object values ($02, $12, $18, $1A, $13, $15, $0E, $10, $12, $02, $0E, $10, $0E, $00, $0C, $0E). | none | | $D187–$D2B5 | 303 | `LevelObjectPointerTable` | A ten-entry pointer table at $D189 ($D1AD, $D1C5, $D1DD, $D1F5, $D1AA, $D210, $D228, $D240, $D258, $D20D), a palette-like table at $D19E and a table of 32 VRAM addresses at $D276 ($2180, $2240, $2300, $2800, $28C0, $2980, $2A40, $2B00, $2000, $20C0 …). | `lda $D276` at $CDC8 and `lda $D277` at $CDCE; the pointer $D187 is set at $D142/$D148 and $D19E at $CFA4/$CFAA and $D1AA at $D000/$D006 and $D07E/$D084 | | $D586–$D599 | 20 | `SpriteSpawnRecordTable` | Five 4-byte records `[count, X, $02, $FF]`: (0, $48, 2, $FF), (1, $45, 2, $FF), (1, $44, 2, $FF), (1, $47, 2, $FF), (1, $46, 2, $FF) — the X positions the passengers are spawned at. | none directly | | $D6B4–$D73D | 138 | `SpritePaletteScript` | Four-byte palette records ($01 $4B $03 $FF, $00 $4C $03 $4D, $03 $FF $0F $20 …) followed by a byte script that draws the passenger faces. | the pointer $D6B4 is set at $D5E5/$D5EB and $D6B8 at $D5FF/$D605 | | $E61D–$E79D | 385 | `MovementDeltaTable` | Signed per-step deltas: $FD, $FD, $FE, $FE, $FE, $FE, $FF, $01, $02 … in two interleaved runs, so the X and Y steps are separate byte streams. | `ora $E633,x` at $DA2A; `and $E638,x` at $DBF9; `lda $E62B,y` at $DE7E; `lda $E61D,y` at $DEF7; `lda $E624,y` at $DF69; `lda $E62D,y` at $DFBF. Sub-scripts are entered at $E644, $E648, $E64E, $E656, $E65E, $E672, $E67A, $E67E, $E682, $E686, $E68A, $E68E, $E692, $E696, $E69A, $E69E and $E6A2. | | $F0E1–$F0E6 | 6 | `DescentSpeedTable` | Six descending speed values: $32, $28, $1E, $14, $0A, $01 (50, 40, 30, 20, 10, 1). | `lda $F0E1,y` at $F0DA, stored to $03EF+x | | $F1E3–$F4DA | 760 | `SpriteTypeLimitTable` | Several per-sprite-type tables. $F1E3–$F1E9 holds seven Y limits in steps of five ($0F, $14, $19, $1E, $23, $28, $2D); $F1EA onward holds the Y thresholds the sprite's position is compared against; $F20A onward holds a second set; $F27A holds 4-byte records `[flag, X, $04, $FF]` giving an X offset. | `lda $F1E3,y` at $F0FB; `lda $F1EA,y` at $F182; `lda $F20A,y` at $F191; `lda $F27A,y` at $ED10 | | $F6FC–$F703 | 8 | `PpuBitMaskTable` | The eight single-bit masks $01, $02, $04, $08, $10, $20, $40, $80. | `lda $F6FC,y` at $F6F2, ORed into a byte at (VramWritePtrLo),y | | $F956–$F975 | 32 | `HandlerDispatchJumpTable` | Sixteen little-endian 16-bit handler addresses; the routine loads one into $B5/$B6 and executes `jmp ($00B5)`. | `lda $F956,x` at $F949 and `lda $F957,x` at $F94E | | $F9BB–$F9C2 | 8 | `PpuInverseBitMaskTable` | The complements of `PpuBitMaskTable`: $FE, $FD, $FB, $F7, $EF, $DF, $BF, $7F. | `lda $F9BB,y` at $F9A8, ANDed into a byte at ($82),y to clear one bit | | $FA73–$FFFF | 1,421 | `LevelLayoutScript` | Byte-coded level-layout data. $FA73–$FA9A is a run of small counters ($01, $02, $03 …); from $FA9B the records are three bytes `[address low, count, address high]` ($03F9, $03C0, $038A, $0357 …); $FAB3 onward holds a further table of two-byte entries. | `isc $FFFF,x` at $B50D and $B595 (the vector-page counter); `lda $FF6A,y` at $BFDE and $BFEA; `lda $FAB3` at $F65E and `lda $FAB4` at $F663 | ### What the 2026-10-21 pass changed The first pass gave these blocks machine-generated names ("EnemySpawnOffsetTable", "EnemyMovementDeltaTable", "LevelHandlerPointerTable" and so on) without showing which instruction read them. This pass read the consuming code for every block, which showed that most of the "enemy" names were wrong: the game has no enemy tables at these addresses, they are the sprite/OAM tables, the VRAM byte-script tables and the PPU bit-mask tables. Fifteen blocks were renamed and three new ones (`SpriteOamTileAttributeTable`, `FloorBoundaryTable`, `SpritePatternIndexTable`) were split out of their neighbours. ### Still open - The interpreter that walks `VramDrawScriptTable`, `VramDrawScriptTable2`, `VramDrawScript`, `SpriteByteScriptTable` and `LevelLayoutScript` was not reached by the tracer, so the exact grammar of those byte streams is not established; the record shapes quoted above are read off the byte patterns. - `SpriteHandlerJumpTable`'s second half (2,155 16-bit values from $8668) is not indexed by any instruction in this dump; the walker that uses it was not reached, so its exact role (sprite-slot positions, more handlers, or both) is not established. - `LevelTilemapData` and `SpriteTileGraphicData` are copied wholesale into RAM by the routines at $A31E and $A286; the consumer that reads them back out of RAM is not in this dump. ## Build ``` Byte-identical ROM verified. ```