#!/usr/bin/env python3 """Build the Hogan's Alley (JU) Pass 2 hand-written name map. The names come from this dump's own code. Where a routine's role could not be established from the instructions around it, the honest address-derived `SubC` residue form is used instead of a guess - the same form the promoted `Raid on Bungeling Bay` dumps use for their opaque entries. The three families of one-bit "set a flag bit" stubs (the sound engine keeps three accumulators in the zero page) are named from the immediate the stub actually loads, so the bit they set is visible in the name. """ import re import sys from pathlib import Path PROJ = Path(sys.argv[1]) # ---------------------------------------------------------------- hand names NAMES = { # reset, init and the main loop "C000": "Reset", "C065": "GameFrameDispatch", "C07E": "MainLoop", "C088": "NmiHandler", "C0B8": "GameStateDispatch", "C0E4": "InitGameplayRound", "C10E": "RunGameplayFrame", "C14A": "CopyStackToOam", "C156": "RunAlternateModeFrame", "C19E": "UpdateShots", "C1C8": "AddScore", "C221": "InitPpuAddress", "C226": "InitPpuAddress2", "C228": "SetPpuAddress", "C231": "InitPpuAndOam", "C237": "ClearOamShadow", "C247": "SetOamPointer", "C24E": "SetOamSlot", "C277": "BeginFrame", "C282": "ReadControllers", "C28F": "ReadControllerBits", "C2B7": "ClearOamEntry", "C2BD": "UpdateHud", "C305": "ResetPpuForFrame", "C337": "ReadPlayerInput", "C34D": "IndirectCall", "C39F": "WriteVramList", "C3A8": "SetScroll", "C3D0": "CheckScrollFlip", "C3F8": "UpdateAttractSprite", "C41C": "SetActorTileIndex", "C473": "WriteActorTileByte", "C47C": "ClearActorTileByte", "C484": "SetActorTileSlot", "C4E2": "SetActorTileSlot2", "C4ED": "DecodeActorTile", "C530": "ClampActorTile", "C59A": "ReadActorTileRecord", "C5AE": "RotateActorTile", "C614": "WriteActorTileTable", "C66B": "ConvertCharCode", "C68C": "ConvertCharCode2", "C6AE": "WriteCharCode", "C6BD": "AttractModeTick", "C746": "UpdateTargets", "C750": "UpdateTargetsHelper", "C758": "UpdateTargetsHelper2", "C765": "UpdateTargetsHelper3", "C7AA": "SetRoundTimer", "C7BB": "CopyActorPalette", "C816": "InitActorState", "C883": "DrawActorTile", "C897": "DrawActorTileHelper", "C8B8": "UpdateActorState", "C8F8": "ResetActorState", "C903": "ReadActorTileTable", "C9F0": "PlaceActor", "CA30": "SelectActorTile", "CA6B": "UpdateActorSprite", "CAA4": "UpdateActorSpriteHelper", "CAB1": "ClearActorFlags", "CABC": "UpdateActorTileTable", "CAC6": "UpdateActorTileHelper", "CAE7": "UpdateActorTileHelper2", "CAF7": "ReadActorTileIndex", "CB8E": "SetActorPosition", "CB97": "UpdateActorTileHelper3", "CBB9": "UpdateFruitPosition", "CBDC": "SetFruitIndex", "CBEC": "ReadFruitTile", "CBFC": "SetFruitFlag", "CC11": "IncrementActorCounter", "CC52": "ReadActorSlot", "CC87": "ReadActorIndex", "CC8E": "ReadActorByte", "CCB6": "ReadActorData", "CCD5": "DispatchActorEvent", "CD30": "InitRoundData", "CD60": "InitRoundData2", "CD77": "InitRoundData3", "CD90": "ReadActorCount", "CDAB": "ReadActorLimit", "CDB4": "InitRoundState", "CE04": "ReadActorIndex2", "CE3B": "WriteActorOam", "CE4B": "SpawnTarget", "CE64": "ReadTargetList", "CEAF": "NextTargetEntry", "CEB0": "ScanTargetList", "CEFE": "ReadActorLimit2", "CF06": "SetTilePointer", "CF38": "InvertSoundParam", "CF3E": "ReadZapper", "CF6C": "ReadTargetCount", "CFAE": "ReadShotOam", "CFEE": "ReadShotList", "D02F": "ReadShotState", "D04F": "ReadShotIndex", "D07D": "ReadShotCounter", "D087": "ReadShotTile", "D0CF": "DispatchTargetType", "D124": "SetActorPointer", "D133": "ReadActorTileData", "D14E": "ReadActorFrame", "D22F": "DispatchActorCommand", "D260": "ClearFrameFlags", "D270": "SetActorDirection", "D279": "ReadActorList", "D283": "WriteActorOam2", "D287": "ClearActorState", "D2D7": None, # filled in below: sound flag bit 3 "D2DB": None, "D2E2": None, "D2E6": None, "D2EA": None, "D2EE": None, "D2F9": "ClearSoundFlag2", "D2FD": "RedrawTargets", "D36C": "InitActorRound", "D3B5": "CheckTargetActive", "D3D1": "ReadActorSlotIndex", "D408": "AttractModeFrame", "D43D": "SoundEffectTrigger", "D45F": "ReadActorEvent", "D4E7": "ReadActorBit0Inverted", "D520": "ReadActorCounter2", "D595": "ClearActorBuffer", "D5FD": "ReadSoundParam3", "D606": "ReadSoundParam4", "D609": "ReadSoundParam5", "D630": "AdvanceTargetState", "D63E": "SetTargetSlot5", "D656": "WriteHudValue", "D661": "UpdateTargetCounter", "D69B": "SetActorPosition2", "D6EB": "ReadActorEvent2", "D701": "UpdateTargetCount", "D726": "SetScrollY20", "D72A": "SetScrollOffset", "D783": "CheckBonusCounter", "D7B1": "NextTargetEntry2", "D7C3": "InitTargetType", "D7E0": "ReadActorRecord", "D7FA": "DrawNextTargetTile", "D844": "FlipPpuCtrlMirror", "D84B": "GetTileTablePointer", "D859": "DrawTargetTileRow", "D9C8": "UpdateActorIfActive", "D9D0": "SetActorSlot2", "D9FD": "SetActorSlot5", "DA3A": "SetActorFieldIndex7", "DA3E": "ReadActorField4", "DAC1": "LookupActorTable", "DADB": "GetActorSpeed", "DAF5": "SearchActorTable", "DB0C": "SearchActorTable2", "DB1C": "ScaleActorValue", "DB38": "WriteShotOam", "DB58": "SetActorValue0A", "DB85": "SetActorSpeed", "DC10": "SetActorState9", "DC66": "InitRoundData4", # --- entries of the six inline dispatch tables ----------------------- # The engine dispatches through `jsr $C34D`, which pops the return # address of the call and reads a 16-bit handler pointer from the # word that follows the call. The table therefore sits *inline*, # right after the `jsr`, and the index selects the word. "C7EE": "ResetActorsForRound", # $C124 table, slot 2 "CD00": "SelectActorEntry0", # $C124 table, slot 4 "D267": "SyncSubState", # $C124 table, slot 5 "CDCC": "DispatchActorUpdate", # $C124 table, slot 6 "D35F": "ReadZapperAndStartRound", # $C124 table, slot 10 "D86E": "RedrawActorRow", # $C124 table, slot 13 "DC45": "SelectActorEntry0Alt", # $C124 table, slot 16 "D184": "SetupRoundScreen", # $C219 table, slot 2 "C916": "AdvanceActorFrame", # $C89C table, slot 2 "CA4C": "UpdateActorSpriteState", # $C89C table, slot 7 "CA88": "UpdateActorLife", # $C89C table, slot 8 "CAD4": "ResetActorMovement", # $C89C table, slot 9 "CB10": "StartActorBehaviour", # $C89C table, slot 10 "D4B0": "UpdateActorAttract", # $D3D6 table, slot 1 "D4F1": "SpawnNextTarget", # $D3D6 table, slot 4 "D538": "UpdateTargetNegative", # $D3D6 table, slot 6 "D57B": "UpdateTargetPositive", # $D3D6 table, slot 10 "D5A1": "UpdateTargetSideways", # $D3D6 table, slot 11 # --- code the CDL saw but nothing reaches directly ------------------- "CA00": "InitActorFields", "D0A4": "ReadScriptByte", "D74C": "SetActorTileAddress", "D882": "SetupRoundScene", "D9B3": "InitActorMovement", "DAEA": "NegateActorValue", "DB23": "ScaleActorSpeed", "DBB7": "InitActorSpeed", "DC19": "InitActorPosition", "DC27": "SetActorStateIndex", "F776": "SetNoteOffset00", "F77A": "SetNoteOffset08", "F77E": "SetNoteOffset0C", "F782": "SetNoteOffset04", "F79B": "GetNoteIndex", "F7E9": "WriteApuPeriod", "F80F": "UpdateNoteTimer", "F902": "WriteApuNoiseTable", "F91C": "CheckNotePriority", "F944": "GetVolumeLevel", "F950": "ResetVolumeIndex", "F964": "SelectApuChannel4", "F968": "SelectApuChannel8", "F96C": "AudioUpdate", "F984": "AudioInit", "F98D": "AudioState", "F9D1": "AudioWrite", "F9D7": "WriteApuEnvelopes", "F9FF": "AudioCommand", "FA90": "AudioBranch", "FAD9": "AudioState2", "FAFC": "AudioState3", "FBAA": "AudioCommand2", "FBF2": "AudioCommand3", "FC48": "AudioState4", "FCB9": "AudioCommand4", "FFF4": "AudioUpdateTrampoline", "FFF7": "AudioInitTrampoline", } # the three sound-flag bit stub families, named from the immediate they load # and the zero-page accumulator they OR it into STUB_FAMILIES = [ ("D291", "SetSoundFlagBit"), # -> ZpF0 ("D2B4", "SetSoundFlag2Bit"), # -> ZpF1 ("D2D7", "SetSoundFlag3Bit"), # -> ZpF2 ] HEADER = """# Hogan's Alley (JU) - Pass 2 name map # # Derived from this dump's own committed split (after the 2026-10-20 CDL # re-classification). Every name below was read out of the code; nothing # was carried over from another dump. # # Format: # # The game's vocabulary: the shootable objects are *targets*, the player's # projectiles are *shots*, the moving characters are *actors*, and the # bonus object is the *fruit*. # # Where a routine's role could not be established from the instructions # around it, the honest address-derived `SubC` residue form is used # instead of a guess - the same form the promoted `Raid on Bungeling Bay` # dumps use for their opaque entries. # # The engine dispatches through two mechanisms: # * `jmp ($0014)` at $C360 - an indirect call through the zero page; # * `jmp ($0610)` / `jmp ($0612)` in the sound engine at $F927/$F941. # # The routine entries are the targets of a `jsr`/`jmp`, plus the entries # the code reaches that the tracer had not labelled. The internal branch # targets are derived from the branch that reaches them by # _tools/derive_branch_names.py: # beq -> OnZero bne -> OnNonzero # bcc -> OnCarryClear bcs -> OnCarrySet # bmi -> OnNegative bpl -> OnNonnegative # bvc -> OnOverflowClear bvs -> OnOverflowSet # a backwards branch (the target is below the branch) -> Loop # """ def main(): lines = (PROJ / "prg.s").read_text().split("\n") # the immediate each stub family loads, straight out of the ROM bytes. # The family is a run of `lda #bit` / `bne ` stubs, so it is # walked byte by byte; the tracer never labelled every stub. prg = (PROJ / "prg.bin").read_bytes() for start, prefix in STUB_FAMILIES: a = int(start, 16) - 0xC000 while True: if prg[a] == 0xA9: # lda #imm imm = prg[a + 1] b = a + 2 elif prg[a] == 0x20 and prg[a + 3] == 0xA9: # a `jsr` first: the stub does extra work before the load imm = prg[a + 4] b = a + 5 NAMES[f"{0xC000 + a:04X}"] = f"RedrawTargets{prefix}{imm:02X}" if prg[b] == 0xD0: a = b + 2 continue break else: break NAMES[f"{0xC000 + a:04X}"] = f"{prefix}{imm:02X}" if prg[b] == 0xD0: # bne a = b + 2 else: break # every routine entry the code reaches entries = set() for line in lines: m = re.match(r"^\s*(?:jsr|jmp)\s+\$([0-9A-F]{4})", line) if m: entries.add(int(m.group(1), 16)) # the labels the file already carries cur = None labelled = set() for line in lines: m = re.match(r"^([A-Za-z_][A-Za-z0-9_]*):\s*$", line) if m: cur = m.group(1) continue m2 = re.search(r"; CPU:\$([0-9A-F]{4}) PRG:", line) if m2 and cur: labelled.add(int(m2.group(1), 16)) cur = None missing = sorted(entries - set(int(k, 16) for k in NAMES)) for a in missing: NAMES[f"{a:04X}"] = f"SubC{a:04X}" out = [HEADER] for a in sorted(int(k, 16) for k in NAMES): out.append(f"{a:04X}\t{NAMES[f'{a:04X}']}\n") (PROJ / "routinemap.tsv").write_text("".join(out)) print("NONE keys:", [k for k,v in NAMES.items() if v is None]) sub = sum(1 for a in NAMES if NAMES[a].startswith("SubC")) print(f"{len(NAMES)} entries written " f"({sub} honest `SubC` residue, {len(entries)} call targets, " f"{len(labelled)} labels in prg.s)") if __name__ == "__main__": main()