# Balloon Fight (JU) — Game Flow from Power-On This document describes the execution flow of the disassembled Balloon Fight (JU) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `prg.s` in this project folder. The rebuild is verified **byte-identical** to the original `prg.bin`. --- ## 1. Reset Vector — CPU $FFFC → $C000 **`Reset`** ($C000–$C07E) On power-on or reset, the 6502 jumps to `$C000`. The reset routine performs cold-start hardware initialization: | Step | Code | Purpose | |------|------|---------| | 1 | `lda #$00 / sta PPUCTRL` | Disable NMI and PPU rendering | | 2 | `lda #$00 / sta PPUMASK` | Disable screen output | | 3–6 | Poll `$2002` twice | Wait for two full VBlank periods (PPU warm-up) | | 7 | `sei / cld` | Disable interrupts, clear decimal mode | | 8 | `ldx #$FF / txs` | Initialize stack pointer | | 9–13 | Clear Zero Page | Zero all of RAM $00–$FF | | 14–21 | Warm-start signature check | Compare RAM `$07FA`–`$07FC` against `"HAL"` ($C082–$C084). If valid, skip full memory clear | | 22–31 | Initialize memory | Clear zero page + stack page ($0700–$07FF via `SortedRecordTable`, x = 0), set `SpawnPixelPosition` = $32, run block-init loop (`jsr InitializeOrQueueObject` 50 times), `jsr SortOrInitializeRecords` | | 32–44 | Copy initialization data | Copy 15 bytes from $C085 to $0629 (initial game state table) | | 45–51 | Clear local variables | Zero the five-byte `CurrentRecord` at $03–$07 | | 52–56 | Copy saved signature | Re-store `"HAL"` to $07FA–$07FC | | 57 | `jmp MainInitialization` | Transfer control to $F1D4 | **Warm-start signature:** `"HAL"` stored at $C082; RAM copy at $07FA. This lets the game resume state after a soft reset without re-initializing everything. --- ## 2. Main Initialization **`MainInitialization`** is at **$F1D4** (forward-referenced in `locals.inc`). It sets up the first round, initializes object pools, and enters the main loop. Its body lives in the upper bank region (not fully shown in the decoded $C000–$C07E range). --- ## 3. Interrupt Vectors — CPU $FFFA–$FFFF Located at the end of the bank (PRG offset $3FFA): | Vector | Address | Handler | |--------|---------|---------| | NMI | $C094 | `NmiHandler` | | RESET | $C000 | `Reset` | | IRQ | $C0F7 | `IrqHandler` (self-loop `jmp IrqHandler` — unused) | --- ## 4. NMI / Frame Heartbeat — `NmiHandler` ($C094) The NMI fires once per frame and drives the frame pacing. It: 1. **Preserves registers** (A, X, Y on stack) 2. **OAM DMA**: writes `$00` to `OAMADDR`, then `$02` to `OAMDMA` (DMA from $0200–$02FF) 3. **PPU command buffer flush**: compares read/write offsets at $52/$53; if data pending, calls `FlushPpuCommandPreserved` ($C17C) 4. **Transfer sources**: `jsr UpdatePpuTransferSources` ($D60D) 5. **Build OAM**: `jsr BuildFrameOam` ($D798) 6. **Frame counter**: `inc FrameCounter` ($19) 7. **Scroll/rendering setup**: sets the `PPUADDR`/`PPUSCROLL` registers, conditionally waits for VBlank if rendering disabled (polling `PPUSTATUS`), writes `PPUCTRL` 8. **Audio**: `jsr UpdateAudioFrame` ($FFF7 bank call → `Sound_F758`) 9. **Sets `NmiCompletedFlag`** ($02 = $01) 10. **Restores registers, `rti`** The main game loop runs during VBlank and waits for this flag. --- ## 5. Main Game Loop — $C209 After initialization, control reaches the per-frame loop at **$C209**: ``` UpdateGameplayObjects ($E691) ; update all 20 gameplay-object slots UpdateSpecialObjectSystem ($C6F9) ; every other frame (frame counter / 2) [round spawner / HUD / spawning logic] [object physics & collision] [particle system] [OAM sorting / sprite setup] WaitForFrame ($F465) ; wait for NMI-completed flag jmp back to $C209 ``` Key flow details: - **Scroll handling** ($C227): if `PpuScrollX` ($17) is non-zero, toggles `PpuControlBase` bit 0 (screen scroll page flip), decrements scroll X. - **Carrier/rescue sequence** ($C233): updates carrier position ($0488), target player, sound requests. - **Effect object pool** (8 slots, $055D–$0584): updates timers, respawns offscreen. - **Gameplay object pool** (20 slots, $0530–$055C): movement, position integration. - **Round spawner** ($C790): on frame boundaries, counts down `RoundSpawnDelay`, finds free slots, spawns round objects from tables ($C88F–$C8B5), plays spawn sound. - **HUD updates** ($C831): when spawn delay reaches 1, writes round HUD digits to PPU buffer. - **Sprite sorting**: `SortOrInitializeRecords` ($C579) maintains a sorted table of up to 50 five-byte records ($0700–$07F9) for OAM priority. --- ## 6. Subsystems ### 6.1 PPU Command Buffer ($C12D–$C1C4) A buffered PPU update system. Commands are staged at $0300 with format: - Byte 0: PPU address high | $20 - Byte 1: PPU address low - Byte 2: length (number of data bytes following) Routines: - `AppendBytesToPpuBuffer` ($C131) — appends raw bytes - `AppendLengthPrefixedPpuData` ($C139) — appends with length prefix - `BuildPpuCommand` ($C154, formerly `Sub_C154`) — formats address+length command - `FlushPpuCommandPreserved` ($C17C, formerly `Sub_C17C`) — drains buffer to $2006/$2007, preserving X/Y - Special handling for palette writes ($3Fxx range) ### 6.2 Round Spawning ($C77A–$C88A) - `ChooseRoundSpawnIndex` ($C77A): picks a random spawn index below `RoundSpawnRandomLimit`, rejecting zero - `UpdateRoundSpawner` ($C790): timer-driven spawning; initializes object position, velocity (from tables at $C89F–$C8B1), flip flags, aux state; increments `SpawnSelector`; requests spawn sound ($F0 bit $04) - `UpdateRoundHud` / `ConfigureRoundPresentation` ($C831–$C88A): writes round number / HUD tiles to PPU buffer **Spawn tables** ($C88F–$C8B5): flip X/Y, X/Y offsets, parameter, velocity Y, position hi, velocity X — six entry types. ### 6.3 Gameplay Objects ($C8B7–$C9DC) `UpdateRoundObjectSpritesAndPhysics` ($C8B7) updates slots 1 and 0: - Builds 3-sprite OAM per object from tile tables ($C9DD–$CA33) - Integrates fixed-point position ($C9B6) - Bounds testing (`TestGameplayObjectAgainstBounds`, $CA67) against rectangular regions - Velocity reflection on collision (`BounceGameplayObjectX/Y`, $CA4F/$CA37) - Object removal when offscreen ### 6.4 Bonus Objects ($CB74–$CCCA) - `UpdateBonusObjectSystem` ($CB74): per-frame phase/timer advance for the single active bonus slot - `TestBonusObjectPlayerOverlap` ($CBB2): rectangle overlap vs both players; applies horizontal/vertical impulses, collision sound - `TestBonusObjectCollection` ($CC3A): phase-3 collection; starts bonus timer - `TriggerBonusObjectEffect` ($CCBF): increments effect counter, calls `ApplyBonusEffect` ($E983) - `DrawBonusObjectHud` ($CCCB): 3-row HUD tile display from phase tables ($CD26–$D046) ### 6.5 Carrier / Rescue Sequence ($C5C3–$C715) - `UpdateCarrierSequence` ($C6F9): advances carrier target X while idle; finds eligible player; starts sequence - `FindEligiblePlayerForCarrier` ($C614): scans player slots 7→0, finds one at lower screen boundary ($B4+) matching carrier target X - `BeginCarrierTargetSequence` ($C6C4): captures player position/state, resets timer - `UpdateCarrierArrivalSequence` ($C658): moves carrier down, checks contact, transitions player to hit/rescue state, sound request - `UpdateCarrierObjectDisplay` ($C5C3): builds 8-sprite carrier metasprite, updates target player position relative to carrier - Tables: `NormalDisplayModeTable` ($C5B7), `AlternateDisplayModeTable` ($C5BB), `CarrierAnimationSequenceTable` ($C6B8) ### 6.6 Particle System ($CD4A–$CEAD) - `InitializeParticleSlots` ($CD4A): 10 slots, offscreen Y ($F0) - `SpawnTimedParticle` ($CD5A): random timer, finds free slot, assigns position/velocity from `ParticleInitialVelocityTable` ($CEAE) - `UpdateParticleSystem` ($CDAA): integrates signed motion, applies scroll delta, retires offscreen particles - `DrawParticleSprites` ($CE2F): 3-sprite OAM per particle with animation/attribute masking ### 6.7 Sound Driver ($FFF7 region, bank-switched) - `UpdateAudioFrame` jumps to `Sound_F758` in another bank - Sound sequence pointer table at $3BCA (32 entries) - Encoded sequence data $3BCA–$3FF1 (note/duration bytes with $02/$FF/$8x control codes) - APU channel init at $F7xx region (`InitializeApuChannelState`, `SilencePulseChannels`) - Sound request flags: $F0 (flags 0), $F1 (flags 1), $F2 (flags 2), $F3 (flags 3) --- ## 7. Memory Map (Key Zero Page / RAM) | Address | Name | Use | |---------|------|-----| | $00 | `PpuControlShadow` | $2000 mirror | | $01 | `PpuMaskShadow` | $2001 mirror | | $02 | `NmiCompletedFlag` | VBlank sync flag | | $12–$13 | `RecordIndex` / `ReverseRecordIndex` | Sorted-record insertion (see reuse note below) | | $14–$15 | `SpawnParameter` / `SpawnPixelPosition` | Spawn args | | $16 | `RenderingDisabledFlag` | Rendering state | | $17 | `PpuScrollX` | Scroll X | | $18 | `PpuControlBase` | Base PPU control | | $19 | `FrameCounter` | Frame tick | | $1B | `RandomState` | PRNG state | | $1D–$1E | `RecordPtrLo/Hi` | Record table pointer | | $23–$24 | `SpawnCommandPointerLo/Hi` | Command stream pointer | | $25–$26 | `DispatchPointerLo/Hi` | Spawn dispatch indirect jump | | $52–$53 | Ppu buffer read/write offsets | Command buffer | | $54–$56 | Ppu command address/length | Pending PPU command | | $7F | `PlayerLifecycleState` | Per-player lifecycle | | $88 | `PlayerSlotState` | Per-player active state | | $91 | `PlayerPosX` / `Player0PosX` | Player X | | $99 | `CarrierTargetX` | Carrier target | | $9A | `PlayerPosY` / `Player0PosY` | Player Y | | $A2 | `CarrierPosY` | Carrier Y | | $BD | `PlayerCollisionDisable` | Collision immunity | | $F0–$F3 | Sound request flags 0–3 | Audio requests | | $0300–$03FF | PPU command buffer | Staged PPU updates | | $0489–$048D | Carrier sequence state | Active, mode, target, drop, timer | | $0490–$0530 | Gameplay object pool (20 slots) | State, pos, velocity | | $055D–$0584 | Effect object pool (8 slots) | State, pos | | $0558–$05CE | Particle / sequence state | 10-slot particle pool | | $05D1–$060E | Bonus object state | Timer, phase, pos, HUD | | $0700–$07F9 | Sorted record table | 50 × 5-byte OAM sort records | | $07FA–$07FC | Warm-start signature | "HAL" | **Alias reuse.** Several zero-page addresses serve more than one subsystem, so `locals.inc` defines several names per address; `prg.s` uses whichever fits the routine reading it: | Address | Names used in `prg.s` | |---------|---------| | $12 | `RecordIndex` (sort), `Temp12` (generic scratch), `CollisionDeltaX`, `MotionDeltaLo` | | $13 | `ReverseRecordIndex` (sort), `CollisionDeltaY`, `MotionDeltaHi` | | $23–$26 | `SpawnCommandPointerLo/Hi` + `DispatchPointerLo/Hi`, `BoundsLeftPtrLo/Hi` + `BoundsRightPtrLo/Hi` (stage-collision bounds) | | $27–$2A | `BoundsTopPtrLo/Hi` + `BoundsBottomPtrLo/Hi` (stage-collision bounds) | | $3E | `SequencePlayerIndex` (bonus accumulation) / `CurrentObjectIndex` (object loops) | | $57–$5A | `PpuCommandData0/1/2` (PPU command buffer) / `HudCommandTile0`; `RoundHudCommandAddressHi/Lo` + `RoundHudCommandLength` + `RoundHudTile` in `DrawRoundStatusPanel` | | $55D/$567/$57B | `EffectObjectState` / `EffectObjectPosX` / `EffectObjectPosY` (spawn effects) vs `ParticleState` / `ParticlePosX` / `ParticlePosY` (particles) | --- ## 8. Object Pools | Pool | Slots | State array | Purpose | |------|-------|-------------|---------| | Gameplay objects | 20 ($00–$13) | $0530 | Round enemies/objects, movement & physics | | Effect objects | 8 ($00–$07) | $055D | Short-lived spawn effects | | Particles | 10 ($00–$09) | $055D | Visual particles (shared state array, different offset interpretation) | Slot state convention: **negative ($FF) = inactive/free**, zero or positive = active. --- ## 9. Summary of Execution Flow ``` Power On / Reset │ ▼ Reset ($C000) ────► hardware init, clear RAM, validate "HAL" signature │ ▼ jmp MainInitialization ($F1D4) │ ▼ [first-round setup, object pool init] │ ▼ Main Loop ($C209) ──────────────────────────────────────────────┐ │ │ ├─ UpdateGameplayObjects ($E691) │ ├─ UpdateSpecialObjectSystem ($C6F9) [alt frames] │ ├─ Carrier sequence ($C6F9 family) │ ├─ Round spawner ($C790) + HUD ($C831) │ ├─ Object physics & collision ($C8B7, $CA67) │ ├─ Bonus object system ($CB74) │ ├─ Particle system ($CDAA) │ ├─ OAM sort ($C579) │ ├─ WaitForFrame ($F465) ◄──────── waits for NMI flag ──────┘ │ ▼ (loop back to $C209) NMI ($C094) [once per frame] ├─ OAM DMA ($4014) ├─ Flush PPU command buffer ($C17C) ├─ Update transfer sources ($D60D) ├─ Build frame OAM ($D798) ├─ Frame counter++, scroll setup ├─ UpdateAudioFrame ($FFF7 → Sound_F758) └─ Set NmiCompletedFlag, rti ``` --- ## 10. Naming and Residue Pass 2 (2026-10-19) replaced every address-derived definition with a semantic name — `Sub_Cxxx` for the routines that had no behavioural name yet, `_Block[_n]` for internal branch targets — so `prg.s` now contains **zero** `Label_` definitions. The 77 `Sound_Fxxx` entry points keep their addresses as names in the banked audio region. Pass 3 applies the two symbol tables to the instruction stream: - `symbols.inc` — the NES hardware constants (`PPUCTRL` … `APUFRAMECNT`). All **125** numeric register operands are now symbolic. - `locals.inc` — zero-page/RAM names plus forward-reference aliases for routine addresses (`AbsoluteDifference` = $F08E, `MainInitialization` = $F1D4, …). All but **16** numeric operands are now symbolic. Placeholder names renamed during the same work: - `Sub_C154` → **`BuildPpuCommand`** (now `BuildPpuCommandRecord`) - `Sub_C17C` → **`FlushPpuCommandPreserved`** (now `FlushPpuCommandBuffer`) - `Sub_C40C` → **`SpawnRandomObject`** - `SpawnCommandStreamData_C4AD` → **`RoundSpawnCommandStream`** (format documented) ### The address-derived entry points (2026-10-21) A later pass went through the 180 `jsr`/`jmp` entry points that still carried an address-derived name and gave **154** of them a semantic name, taken from what the code itself shows: the routine's own body, the RAM field it reads or writes, and the callers that reach it. The routine headers were rewritten at the same time so that each `Global:` line says what that routine is for. The names follow the game's own vocabulary - the player and its balloon, the enemy objects, the object pools at `$0400` and `$055D`, the sprite pools at `$0530`, the PPU command buffer at `$0300`, the HUD panel and the sound engine. For example: - `SubCC9B6` → **`MoveObjectByVelocity`** - `SubCC17C` → **`FlushPpuCommandBuffer`** - `SubCD798` → **`BuildFrameOam`** - `SubCD60D` → **`UpdatePpuTransferSources`** - `SubCE76A` → **`StrobeControllerRead`** - `SubCF758` → **`UpdateSoundEngine`** **26 entry points remain address-derived**, and the dump does not reveal what they are for: - `SubCCA67`, `SubCCB1C`, `SubCCECE`, `SubCD0E2`, `SubCD1CB`, `SubCD77C`, `SubCD78F`, `SubCD8DD`, `SubCD8FF`, `SubCE587`, `SubCE796`, `SubCED28`, `SubCEE25`, `SubCEE27`, `SubCEF2A`, `SubCEF37` - the short helpers that prepare one field of a record for the routine that uses it. The record layout is documented (see the data-table inventory), but which object or which screen a given helper serves is not. - `SubCF353`, `SubCF3CC`, `SubCF504`, `SubCF683`, `SubCF687`, `SubCF68B`, `SubCF68F`, `SubCF77C`, `SubCF78F`, `SubCF89B` - the sound engine's per-voice set-up. The engine is a four-voice sequencer whose voices are selected by a small value (`$00`, `$04`, `$08`, `$0C`) that the dump does not tie to a named channel, so the four voice selectors keep their address-derived names. The **data tables** are interpreted in the *Pass 3 data-table inventory* below: 27 of the 44 data blocks carry a name derived from what the bytes are, and every block - named or not - is listed there with its address range, size, structure and the instruction that reads it. The blocks that no instruction reads are recorded as unreferenced data or, where the bytes decode as instructions, as code the tracer could not reach. **Residue.** `$13` carries three aliases (`ReverseRecordIndex`, `CollisionDeltaY`, `MotionDeltaHi`), none of which fits the **16** sites where it is used as an ordinary high-byte scratch beside `Temp12` — in `LoadStageObjectData`, `ReadPpuStreamByte`, `CreateBonusResultSprites`, `RenderObjectSprites_Load`, `ResolveObjectCollisionPair` and `SwapObjectIndexes`. Those operands are deliberately left numeric rather than given a speculative name; a neutral `$13` companion to `Temp12` would close them. Build verification: `make` in the project folder produces `reconstructed.nes`, compared whole-ROM against `rom.nes` (byte-identical). --- ## 11. Audit Trail (comment format) Every instruction and data line in `prg.s` carries the canonical repo audit comment — `; CPU:$XXXX PRG:XXXX AA BB` — giving the CPU address, the PRG offset and the exact emitted bytes. That comment is the byte-identity trail, so its *contents* and its *column* both matter. **What was wrong (fixed 2026-10-21).** 1. **The addresses were stale by two bytes.** The comment trail had been generated when the warm-start signature was still a single byte, so from `ResetInitializationData` ($C085) onward every comment claimed a CPU address and PRG offset two bytes below the truth — `NmiHandler` read `$C092` instead of `$C094`, `AllocateEffectObject` read `$C469` instead of `$C46B`, and so on. The `.byte "HAL"` string at $C082–$C084 is three bytes, not one. **5,944 of the 6,007** comments were affected; the routine headers (`; PRG offsets:` / `; CPU range:`) and the linker labels were right all along, so the two disagreed with each other. 2. **The comments were not in the canonical column.** They sat two spaces after the mnemonic instead of in the column-35 field that `nesdis.py` emits (`f" {text:<31}; CPU:…"`), so the file had no column discipline at all. **What was done.** `_tools/repair_audit_comments.py` rebuilt the trail from the `ca65 -l` listing (the linker is ground truth for each source line's offset and byte span) and validated every rewritten comment against `prg.bin`; `_tools/align_audit_comments.py` then restored the column. Both steps are whitespace-and-comment only — the code text of all 8,544 lines is unchanged, so the rebuild stays byte-identical. A bug in `_tools/align_audit_comments.py` was fixed at the same time: it tested the *indented* code length against 31 rather than against 35, so code texts of 28–31 characters (83 + 36 + 51 lines here) were never padded into the column. **Verification.** - `make` → `Byte-identical ROM verified.` - All **6,007** audit comments re-checked against `prg.bin`: every byte list matches the ROM at the offset it names, and every `CPU` field equals `$C000 + PRG`. - All **63** routine headers re-checked against the instruction comments they describe: every `; PRG offsets:` range and `; PRG offset:` entry point agrees. - Every label in the source agrees with the linker label file (`reconstructed.lbl`). - `python3 _tools/verify_pass1.py "Balloon Fight (JU)"` → `canonical`, verdict `OK`. --- ## 12. Routine Headers — the `Clobbers:` Section The header template `AGENTS.md` describes is *global + local description, parameters, returns, clobbers*. The first four were already here; the fifth was missing from 61 of the 63 routine headers (only `AllocateEffectObject` and `AllocateGameplayObject` had one, and both were incomplete). All 63 now carry a `; Clobbers:` block, so every header in the file is complete. **Nothing was guessed.** `_tools/derive_clobbers.py` derives the block from the source: - the **opcode** comes from the audit comment's byte list, so the addressing mode is exact (`sta $0400,x` is ABSX, `sta Zp00` is ZP) and a data directive is never mistaken for an instruction; - the **operand** is resolved through `locals.inc`, `symbols.inc` and the labels in `prg.s`, so a clobber can be named — and where several names share one address (`Temp12` and `RecordIndex` are both $12) the first name in `locals.inc` is used; - the **control flow** comes from the instructions. A *unit* is a named entry point in the source; the code a unit reaches — the shared tails it branches into, the block it falls through into — is its own, while a `jsr`/`jmp` to another unit is a call, so the callee's clobbers count but its code is not merged in; - **register preservation** is modelled: a small abstract interpreter carries the value of A, X, Y and of each processor flag through the unit, so a routine that saves a register on the stack and puts it back is not reported as clobbering it. That is why `FlushPpuCommandPreserved` reports only A, and why `NmiHandler` — which saves and restores A, X and Y around the frame — reports *no register* at all; - a call into code the project has not decoded (a forward reference in `locals.inc` that lands in the undecoded upper bank) can damage anything, so it is reported as a clobber of every register and every flag. The flags are named individually — `the N, Z, C and V flags` — because that is what the instructions actually set. **Results worth noting.** | Routine | Clobbers | Why | |---|---|---| | `NmiHandler` | no register, the N, Z, C and V flags | saves and restores A, X and Y around the frame | | `FlushPpuCommandPreserved` | A, the N, Z and C flags | the name is the contract: X and Y come back unchanged | | `IrqHandler` | nothing | a one-instruction self-loop | | `TriggerBonusObjectEffect` | A, the N, Z, C and V flags | moves Y into X and puts the old X back into X; A is left holding the old X | | `AllocateEffectObject` | A and X, the N and Z flags | the hand-written "A and X." was right about the registers and silent about the rest | **Verification.** Comment-only change: the code text of all 8,538 lines is unchanged, `make` still prints **Byte-identical ROM verified.**, the 6,007 audit comments still re-check against `prg.bin`, and the tool is idempotent — running it twice leaves the file byte-identical. ## Pass 3 data-table inventory Every `; ---- data $XXXX-$YYYY ----` region in `prg.s` is listed here, one row per region, with the bytes' structure and the instruction that reads it. A region that is only partly table and partly code says so: the tracer's data blocks are contiguous runs of bytes it never reached, so a block can hold a table, a table *and* unreached code, or code alone. ### How the blocks were classified * the **Mesen CDL capture** (`balloonfight_gameplay.cdl`). Its Data flag marks every byte the CPU read without fetching it as an opcode, its Code flag every byte it fetched as an opcode. A block whose bytes are flagged Data is a table; a block flagged Code is code the static tracer could not reach. * the **tracer's own decode**. A block that decodes as a long run of instructions with coherent operands is code, not data. * the **consuming code**. Every instruction whose operand lands inside a block was read, including the indexed forms (`lda $C6B8,y`) the analyzer's own report misses, and the indirect forms. ### Inventory | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$C082-$C093` | 18 | `CartridgeIdAndInitialValues` | `$C082-$C084` = the ASCII bytes `HAL` (HAL Laboratory); `$C085-$C093` = 15 bytes of initial counter values | `lda $C082,x` at `$C06E` copies all 18 bytes to $07FA, and `cmp $C082,x` at `$C02A` compares the RAM copy against the ROM table; `lda $C085,x` at `$C055` copies the 15 counter bytes to $0629 | | `$C3B5-$C3F6` | 66 | `ObjectHandlerPointerTable` (`$C3B5`), `ObjectHandlerIndexTable` (`$C3BF`) | `$C3B5-$C3BE` = five 16-bit LE handler pointers ($C3C9, $C3F7, $C43E, $C45F, $C45E); `$C3BF-$C3C8` = ten 8-bit handler indices (00, 00, 02, 02, 02, 02, 02, 04, 03, 01); `$C3C9-$C3F6` = unreached code | `lda $C3B5,y` at `$C2C3` and `lda $C3B6,y` at `$C2C8` with Y = 2*(the index from $C3BF), the pair becoming the Zp25/Zp26 pointer; `lda $C3BF,x` at `$C2BE` is the index itself | | `$C43A-$C43D` | 4 | `BalloonSpawnOffsetTable` | 4 x 8-bit X offsets (20, 30, 40, 60) | `adc $C43A,x` at `$C425` with X = the level parameter ZpBA; the sum becomes the balloon's X position Zp14 | | `$C45F-$C46A` | 12 | - | unreferenced; decodes as 5 instructions (`jsr $C40C` / `jsr $F1B3` / `and #$7F` / `pha` / `rts`) - code the tracer could not reach | not referenced by any instruction | | `$C4AD-$C526` | 122 | `RoundSpawnCommandStream` | a run of 16-bit LE values, mostly small (0-1A) with a few larger ones (8C, 94, 96, 90, 98, 8A, 92, 86, 8E) - the round's spawn parameters | not referenced by any instruction - no operand names an address inside it | | `$C5B7-$C5C2` | 12 | `BalloonTypeSequenceTable` | three parallel 4-byte tables: `$C5B7-$C5BA` = (01, 02, 03, 03), `$C5BB-$C5BE` = (02, 01, FF, 03), `$C5BF-$C5C2` = (04, 05, 06, FF) | `lda $C5B7,x` at `$C5CF` and `lda $C5BB,x` at `$C5D5` with X = ($048D >> 3) | | `$C6B8-$C6C3` | 12 | `BalloonTypeCycleTable` | 12 x 8-bit balloon type codes (08, 09, 0A, 0B, repeated three times) | `lda $C6B8,y` at `$C68E` with Y = the balloon's type counter $0451,x; the value becomes the balloon's new type | | `$C748-$C779` | 50 | `LevelParameterTable` (`$C748`), `LevelBalloonHeightTable` (`$C761`) | two parallel 25-byte tables, one entry per level: `$C748-$C760` = level flags (0, 0, 1, 2, ... 05) and `$C761-$C779` = the level's balloon heights (0F, 0F, 0C, 0C, 0C, 0C, 0A, ... 08, 08, 08, 08, 08, 08, 05) | `lda $C748,x` at `$C72E` (into ZpBA) and `lda $C761,x` at `$C733` (into ZpB8) | | `$C88B-$C8B6` | 44 | `BalloonSpawnParameterTable` | a table of per-direction parameter groups: `$C88B-$C88E` = (55, FF, 00, FF), `$C88F-$C892` = the four X deltas (00, 00, FF, FF), `$C893-$C896` = the four Y deltas (FF, 00, 00, FF), `$C897-$C89A` = the four X speeds (10, 10, F0, F0), `$C89B-$C89E` = the four Y speeds (DE, 22, 22, DE), `$C89F-$C8A4` = six spawn X positions (60, 70, 80, 90, A0, B0), `$C8A5-$C8AA` = (00, 00, 00, 00, 00, C0), `$C8AB-$C8B0` = (F0, 20, 50, 80, B0, 00), `$C8B1-$C8B6` = (00, 01, 01, 01, 01) | `lda $C89F,y` at `$C7D4`, `lda $C8AB,y` at `$C7DA`, `lda $C8A5,y` at `$C7E0`, `lda $C8B1,y` at `$C7E6`, `lda $C897,y` at `$C7F5`, `lda $C89B,y` at `$C7FF`, `lda $C88F,y` at `$C809` and `lda $C893,y` at `$C811` | | `$C9DD-$CA36` | 90 | `BalloonTileIndexTable` (`$C9DD`), `BalloonMovementDeltaTable` (`$C9E5`) | `$C9DD-$C9E4` = eight 8-bit CHR tile indices (9D, 9E, 9F, 9E, 9D, A0, A1, A0) read as four pairs; `$C9E5-$C9F4` = four copies of (08, 08, F0, F0), the per-direction X step; `$C9F5-$CA04` = four copies of (EE, 0A, 0A, EE), the per-direction Y step; `$CA05-$CA14` = four copies of (F8, 08, 08, F8); `$CA15-$CA24` and `$CA25-$CA34` = two 16-byte tables of 4-byte movement records (91 93 97 97, FC 92 95 9A, ... and 96 9B 9B FC, ...); `$CA35-$CA36` = (00, 80) | `lda $C9DD,y` at `$C31D` and `$C989`, `adc $C9E5,y` at `$C8CD`, `adc $C9F5,y` at `$C8DB`, `adc $CA05,y` at `$C8E1` and `$C8E7`, `lda $CA15,y` at `$C8FF`, `lda $CA1F,y` at `$C905`, `lda $CA29,y` at `$C90B` and `lda $CA33,x` at `$C911` | | `$CB1B-$CB1B` | 1 | - | a single stray byte `$60` (the `RTS` opcode) between two instructions; not referenced by any instruction | not referenced by any instruction | | `$CD26-$CD49` | 36 | `BalloonTileGridTable` | nine 4-byte tables, one for each cell of the 3x3 tile grid of a balloon, each holding the CHR tile index for one of the four balloon types (row 0: A1, A2, 24 / 24, 9E, AB / 24, AB, AC / 24, 24, 24; row 1: A3, A4, A5 / 24, 9F, 24 / AD, AE, AF / A8, A9, AA; row 2: 24, A6, A7 / 24, A0, 24 / B0, B1, 24 / 24, 24, 24). `$24` is the blank tile | `lda $CD26,y` at `$CCDC`, `lda $CD2A,y` at `$CCE1`, `lda $CD2E,y` at `$CCE6`, `lda $CD32,y` at `$CCEE`, `lda $CD36,y` at `$CCF3`, `lda $CD3A,y` at `$CCF8`, `lda $CD3E,y` at `$CD00`, `lda $CD42,y` at `$CD05` and `lda $CD46,y` at `$CD0A` with Y = the balloon type; the three tiles of a row go to Zp5A, Zp5B and Zp5C and are drawn by the routine at $D0F | | `$CEAE-$CECD` | 32 | `BalloonPositionTable` (`$CEAE`), `BalloonXPositionTable` (`$CEB2`), `BalloonSpriteAttributeTable` (`$CEB5`), `BalloonSpriteFlipMaskTable` (`$CEBD`) | `$CEAE-$CEB1` = four 8-bit balloon Y positions (20, 50, A0, D0); `$CEB2-$CEB4` = three 8-bit X positions (02, 22, 02); `$CEB5-$CEB7` = the sprite attributes of the balloon's three tiles (AA, AB, AB); `$CEBD-$CEBF` = the three horizontal-flip masks (00, 40, 40); the remaining bytes hold the same kind of attribute and flip data for the other balloon types | `lda $CEAE,y` at `$CD92` with Y = a random value & 3 (the Y position); `lda $CEB2,y` at `$CE33` (the X position); `lda $CEB5,x` at `$CE87` (the attribute, written to $0259); `eor $CEBD,x` at `$CE90` (the flip mask, EOR-ed into the sprite's attribute) | | `$D10D-$D120` | 20 | `LevelBalloonParameterTable` | four parallel 5-byte tables, one entry per level: `$D10D-$D111` = the balloon's speed (03, 05, 07, 07, 07), `$D112-$D116` = the spawn X (80, 90, 98, A0, A8), `$D117-$D11B` = the balloon type (01, 01, 02, 02, 03), `$D11C-$D120` = a flag (00, 05, 00, 05, 00) | `lda $D10D,x` at `$D0E5`, `lda $D112,x` at `$D0EB`, `lda $D117,x` at `$D0F1` and `lda $D11C,x` at `$D0F7` with X = the level number $0558 | | `$D12B-$D19F` | 117 | `StageGraphicScript` | a script in the "opcode + operands" form the game uses for drawing: `$D13E` (the part the code reads) is a run of tile codes with `$22`, `$23` and `$24` separators, `$FF` ending a group and `$23 C0`/`$23 08` marking a new row | `lda $D13E,x` at `$D1A2`, `lda $D184,x` at `$D04F` and `lda $D19E,x` at `$CF82` | | `$D437-$D45E` | 40 | `BalloonTilePlacementScript` | 20 pairs of (tile, command) bytes - the same script form as `StageGraphicScript` - e.g. (3F, 00), (20, 0F), (2A, 09), (07, 0F), (30, 27), (15, 0F), (30, 02), (21, 0F) | `lda $D437,x` at `$D3EF` copies all 40 bytes to Zp57-Zp7E; the last four bytes $D45A-$D45D are copied to Zp5A-Zp5D by `lda $D45A,y` at `$D404` | | `$D466-$D467` | 2 | - | unreferenced; two bytes (0F, 2C) between two data regions | not referenced by any instruction | | `$D47B-$D48B` | 17 | - | unreferenced; 17 bytes of tile/command pairs in the same script form as the two blocks above | not referenced by any instruction | | `$D493-$D496` | 4 | - | read by `lda $D493,y` at `$D2C8`; four descending 8-bit values (7F, 7E, 7D, 7C) used as a threshold comparison | `lda $D493,y` at `$D2C8` | | `$D564-$D56B` | 8 | - | two 4-byte masks: `$D564-$D567` = (FC, F3, CF, 3F) AND-ed into a value and `$D568-$D56B` = (01, 04, 10, 40) OR-ed into it | `and $D564,y` at `$D54D` and `ora $D568,y` at `$D550` | | `$D64C-$D650` | 5 | - | five 8-bit values (24, ED, EE, EF, 24) | `cmp $D64C,y` at `$D632` and `lda $D64D,y` at `$D645` | | `$D65C-$D6DB` | 128 | `StageNametableAddressTable` | 64 x 16-bit LE nametable addresses ($2163, $21A5, $20CB, $20B7, $217D, $229B, $20F2, $2249, ...) - the tiles the stage's background is made of | `lda $D65C,x` at `$D651` and `lda $D65D,x` at `$D656`; the pair becomes Zp50/Zp51 | | `$D779-$D77B` | 3 | - | three 8-bit values (29, 2E, 33) | `lda $D779,x` at `$D702` | | `$D84B-$D853` | 9 | - | nine 8-bit values (1B, 0E, 1F, 18, 24, 0E, 16, 0A, 10) | `lda $D84B,x` at `$D841` | | `$D86C-$D870` | 5 | - | five descending 8-bit values (FB, FA, F9, F8, F7) | `lda $D86C,y` at `$D856` | | `$D8D1-$D8DC` | 12 | - | two 6-byte tables: `$D8D1-$D8D6` = (F4, F5, F6, F7, F8, F9) and `$D8D7-$D8DC` = (FB, FB, FA, FB, FB, FB) | `lda $D8D1,y` at `$D89C` and `lda $D8D7,y` at `$D8A2` | | `$D92C-$DAC0` | 405 | `StageLayoutScript` | the stage's layout script: a long run of (tile, command) pairs with `$22`, `$23` and `$24` separators, laying out the background graphics - the clouds, the ladders and the platforms | not referenced by any instruction - no operand names an address inside it | | `$DB27-$E3A3` | 2173 | - | the game's object and level data area: a long run of records that describe the objects the boy throws and the enemies that chase the player, each record holding a tile, a position and a behaviour; the area also holds the score-value tables and the "FD"-format object scripts. The CDL flags 372 of its bytes as Data | the block is read by 23 instructions; the ones with a named sub-table are listed in the rows above | | `$E5C5-$E690` | 204 | `BalloonParameterTable` | the per-balloon-type parameter tables: `$E5C5-$E5CA` = two 6-byte groups, `$E601-$E60D` = (04, 04, 05, 06, 03, 03, 03, 06, 0A, 0A, 0A, 0A), `$E60D-$E619` = the next group of 12, and so on - 4-byte entries, one per balloon type, holding the balloon's speed, its spawn height and its collision behaviour | `lda $E5C5,x` at `$E58B`, `lda $E5CA,x` at `$E590`, `adc $E601,y` at `$EA93`, `sbc $E60D,y` at `$ECA2`, `sbc $E619,y` at `$E8DE` and `adc $E619,y` at `$E8F6`, `sbc $E625,y` at `$E898`/`$E91B` and `adc $E625,y` at `$E8AA`/`$E933`, `cmp $E631,y` at `$EB1C` and `lda $E631,y` at `$EB21`, `cmp $E63D,y` at `$EB12` and `lda $E63D,y` at `$EB27`, `cmp $E649,y` at `$EB3A` and `lda $E649,y` at `$EB3F`, `cmp $E655,y` at `$EB30` and `lda $E655,y` at `$EB45`, `cmp $E661,y` at `$EAAD` and `lda $E661,y` at `$EAB2`, `cmp $E66D,y` at `$EAA3` and `lda $E66D,y` at `$EAB8`, `cmp $E679,y` at `$EACB` and `lda $E679,y` at `$EAD0`, `cmp $E685,y` at `$EAC1` and `lda $E685,y` at `$EAD6` | | `$E762-$E767` | 6 | `BalloonMaskTable` | two 3-byte masks: `$E762-$E764` = (1F, 0F, 07) AND-ed into a value and `$E765-$E767` = (20, 10, 08) added to it | `and $E762,y` at `$E71D` and `adc $E765,y` at `$E720` | | `$ECAE-$ECB9` | 12 | `BalloonTypeTable` | 12 x 8-bit values, all `$02` | `lda $ECAE,y` at `$EC1F` with Y = the balloon's type | | `$F053-$F08D` | 59 | `BalloonItemParameterTable` | five parallel tables of 11-12 bytes - `$F053-$F05D`, `$F05E-$F069`, `$F06A-$F075`, `$F076-$F081` and `$F082-$F08D` - holding the parameters of the items the boy throws | `lda $F053,y` at `$EFAB`, `lda $F05E,y` at `$EFEF`, `lda $F06A,y` at `$F014`, `lda $F076,y` at `$F01E` and `lda $F082,y` at `$F023` | | `$F3AE-$F3AF` | 2 | - | two 8-bit values | `lda $F3AE,x` at `$F38A` | | `$F3BA-$F3CB` | 18 | - | two 9-byte tables, `$F3BA-$F3C2` and `$F3C3-$F3CB` | `lda $F3BA,x` at `$F222` and `lda $F3C3,x` at `$F227` | | `$F3F5-$F40A` | 22 | - | 22 x 8-bit values | `lda $F3F5,x` at `$F3D4`, copied to Zp57-Zp6C | | `$F43B-$F45B` | 33 | - | three sub-tables of 11 bytes - `$F43B-$F43C`, `$F43D-$F43E` and `$F43F-$F49` - the tile, the attribute and the X offset of the sprites | `lda $F43B,x` at `$F410`, `ldy $F43D,x` at `$F413` and `lda $F43F,x` at `$F42F` | | `$F4C5-$F4FF` | 59 | - | 59 bytes of `$FF` - padding | not referenced by any instruction | | `$F5FC-$F682` | 135 | - | a mixed region: `$F600-$F601` is a 16-bit LE pointer read as a pair, `$F660-$F66B` is a small 8-bit table, and the rest is unreached code | `lda $F600,y` at `$F591` and `lda $F601,y` at `$F589` (the pair), `lda $F660,y` at `$F56A` (the byte) | | `$F6E5-$F711` | 45 | `NoiseEffectTable` | `$F6E5-$F6FF` = unreferenced data; `$F700-$F710` = three parallel 15-byte tables for the noise channel - the base period, the loop period and the length | `lda $F700,y` at `$F5E7` (written to APUNOISEBASE), `lda $F701,y` at `$F5ED` (APUNOISELOOP) and `lda $F702,y` at `$F5F3` (APUNOISELENGTH) | | `$F9CD-$F9E8` | 28 | - | unreferenced; 28 bytes of audio data | not referenced by any instruction | | `$FA8A-$FA95` | 12 | - | unreferenced; 12 bytes of audio data | not referenced by any instruction | | `$FBCC-$FBD1` | 6 | - | unreferenced; six bytes of audio data | not referenced by any instruction | | `$FBD6-$FFF6` | 1057 | - | unreferenced; the audio engine's data area - the music and sound-effect command streams, the note tables and the waveform data, ending just before the 6502 vectors | not referenced by any instruction | | `$FFFA-$FFFF` | 6 | - | the three 6502 vectors (NMI $C094, RESET $C000, IRQ $C02C) | not referenced by any instruction (the CPU reads them through the vector table, not through an operand) | ### Notes on the blocks that are not tables - The CDL capture settles the code/data question for the blocks the CPU actually touched: `$C082-$C093`, `$C3B5-$C3F6`'s first bytes, `$D437-$D45E`, `$D493-$D496`, `$D564-$D56B`, `$D64C-$D650`, `$D65C-$D6DB`, `$F3F5-$F40A` and `$F4C5`-neighbours are flagged Data, while `$C45F-$C46A` and `$C4AD-$C526`'s neighbours are flagged Code. The blocks the capture never reached at all are classified from their decode alone. - `$DB27-$E3A3` (2,173 bytes) is the one large block whose layout is only partly established. Its structure is described in the row above and the sub-tables the code reads by name are listed there; the object record format inside it is not established from this dump. - `$F4C5-$F4FF` is 59 bytes of `$FF` and is recorded as padding. --- ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry.