Files
disasm/Balloon Fight (JU)/OVERVIEW.md
T

13 KiB
Raw Blame History

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 copilot/prg.s in the repository. 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), set $15 = $32, run block-init loop (jsr $D6DE 50 times), jsr $C579
32–44 Copy initialization data Copy 15 bytes from $C085 to $0629 (initial game state table)
45–51 Clear local variables Zero $03–$06
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 $C0F7 — 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 $2003, then $02 to $4014 (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 $2006/$2005 scroll registers, conditionally waits for VBlank if rendering disabled, writes $2000
  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
$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"

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. Notes on Provisional Areas

Some routines in the disassembly still carry placeholder names (Label_Cxxxx, Sound_FBxx) and some data blocks were previously marked "undecoded." These have been cleaned up in this review:

  • Sub_C154 → BuildPpuCommand
  • Sub_C17C → FlushPpuCommandPreserved
  • Sub_C40C → SpawnRandomObject
  • SpawnCommandStreamData_C4AD → RoundSpawnCommandStream (format documented)

The 497 Label_ branch targets are local intra-routine labels; none are subroutine entry points (confirmed against all jsr/jmp targets in the source). The 77 Sound_FBxx labels are sound-driver entry points in the banked audio region.

Build verification: make in copilot/ produces reconstructed_prg.bin that is byte-identical to prg.bin and to the PRG bank of balloon.nes.