#!/usr/bin/env python3 """Build the Mario Bros (JU) Pass 2 name map. The map starts from every label the committed split already carries - the partial Pass 2 that a previous session did (47 semantic names and 223 `SubC` residue entries) - and adds the names that can be established from this dump's own code. The internal branch targets are derived mechanically by _tools/derive_branch_names.py, so they are not listed here. Usage: python3 _tools/build_mario_map.py "_wip/NROM/Mario Bros (JU)" """ import re import sys from pathlib import Path # The names this dump's own code establishes. Everything else keeps the # honest `SubC` residue form the previous session left it in. NAMES = { # --- the frame heartbeat ------------------------------------------- "C04F": "ClearFramePending", "C06A": "EndFrame", "C3A0": "CheckFrameStarted", "C3A8": "InitFrameState", "C1FD": "AdvanceFrameCounter", # --- the controller ------------------------------------------------ "CCC5": "ReadControllerPort", "CCD3": "ReadControllerBits", # --- the PPU ------------------------------------------------------- "CD43": "UpdatePpuCtrl", "CE09": "SetScroll", "CE00": "WriteVramList", "D5D5": "WriteVramAt", "E132": "DisableBackgroundAndSprites", "E13D": "EnableBackgroundAndSprites", # --- the OAM / sprite shadow --------------------------------------- "C3F5": "HideOamPair", "CB50": "CopyOamBytes", "DFBD": "AdvanceSpriteCounter", "DFD3": "CheckSpriteFlip", # --- the script interpreter ---------------------------------------- "CB85": "SetupScriptPointer", "CB9B": "SetupScriptPointerA", "CBB6": "SetupScriptPointerB", "CBAE": "AdvanceScriptPointer", "CBC4": "SetScriptTableBase", "CC22": "ReadScriptRecord", "CC29": "DispatchScriptCommand", "E2FD": "WriteScriptByte", "E9D3": "RunScript", "E9FA": "FetchScriptByte", "E21A": "RunScriptBlock", # --- the actor list ------------------------------------------------ "D9B7": "FindFreeActorSlot", "D9C9": "FindFreeActorSlotBack", "CEBA": "ResetActorState", "DB34": "SelectActorSprite", # --- the stage ----------------------------------------------------- "E333": "InitStage", "DE0E": "StartActorAction", "DCE A": "PLACEHOLDER", # --- the sound engine ---------------------------------------------- "F96D": "WritePulse2Period", "F96B": "WritePulse2PeriodA", "FC38": "StopPulse2Channel", "F8EC": "WritePulse2Ctrl", "E97A": "LookupNoteTable2", # --- the entries the CDL re-classification recovered ---------------- "D3F9": "UpdateOamSprites", "D508": "SetHudVramPointer", "D58C": "WriteVramAtF4F5", "D5BE": "WriteVramAtF660", "D60F": "CopyHudPalette", "D61D": "DrawHud", "D672": "AdvanceHudCounter", "D67F": "ClearSpriteBlock", "CA3B": "DrawTileRow", "E0B8": "WriteVramAt0300", "E1EC": "SetActorState9", # --- the entries the tracer seeded from table references ------------- "CE18": "CopyByteBlock", "D34A": "ResetRoundState", "D40B": "SetHudVramAddress", "D451": "DispatchGameState", "D47D": "SetActorFlagOn4B", "D69A": "RunScriptBlockF3BA", "E129": "SetActorStateReturn", "E4CB": "WriteVramAt22D0", "E639": "UpdateScoreCounter", # --- the three branch targets the reclassifier could not prove ------ "D47C": "ReturnWithoutAction", "D5E5": "ReturnFromActorUpdate", "E1F4": "SetActorStateAndReturn", } NAMES_EXTRA = { "C130": "HandleNmiPhase2", "C196": "HandleNmiPhase3", "C1C3": "HandleNmiPhase4", "C1EC": "SetMarioSpriteAttrs", "C22D": "HandleNmiPhase5", "C2AD": "HandleNmiPhase6", "C334": "SetMarioSpriteFlip", "C35B": "HandleNmiPhase7", "C37D": "HandleNmiPhase8", "C3E3": "SwapMarioState", "C402": "SetMarioDirection", "C41F": "GetScrollPosition", "C429": "GetScrollDelta", "C430": "SubScrollDelta", "C446": "SetVramPointer", "C44A": "SetVramPointer2", "C4B8": "UpdateGameplay", "C570": "StartEnemyAction", "C5DB": "InitEnemyScript", "C657": "RunEnemyScript", "C65D": "RunEnemyScriptEnd", "C66A": "ClearEnemyPosition", "C686": "MoveEnemyDown", "C6BF": "CheckEnemyCollision", "C708": "ResolveEnemyCollision", "C747": "UpdateEnemyScript", "C75A": "RunEnemyScriptB", "C75D": "RunEnemyScriptC", "C785": "CheckEnemyType30", "C7CD": "DispatchEnemyBehaviour", "C884": "ClearEnemyFlag", "C8B5": "SetEnemyStateFromScroll", "C8B7": "SetEnemyState", "C8D1": "DispatchEnemyBehaviourB", "C8D7": "HandleEnemyBehaviour", "C8F4": "DispatchEnemyBehaviourC", "C93A": "CheckEnemyScriptCommand", "C97D": "HandleEnemyCommand30", "C992": "SetEnemyScrollTarget", "C9A1": "ReadEnemyTileIndex", "C9CB": "ClearEnemyTileFlags", "C9DB": "AdvanceEnemyTileIndex", "C9DD": "AddEnemyTileIndex", "C9E6": "DispatchEnemyBehaviourD", "C9FE": "DispatchEnemyBehaviourE", "CA1B": "InitVramAddress", "CA20": "SetVramAddress2400", "CA22": "SetVramAddress2400B", "CA2B": "SetVramAddressF400", "CA66": "DrawTileRow2", "CA7D": "ComputeTileOffset", "CACC": "AdvanceScriptCounter", "CAF7": "UpdateEnemyScroll", "CB48": "CopyOamRow", "CB58": "UpdateOamShadow", "CC3F": "CheckEnemyTileRange", "CC73": "AddEnemyTileOffset", "CCFF": "DrawHudRow", "CD88": "DecFrameCounter", "CE16": "CopyBlockToVram", "CE86": "ClampTileIndex", "CE95": "CheckMarioFacing", "CE9C": "LoadEnemyTileIndex", "CEC3": "InitEnemyScriptBlock", "CF67": "CheckEnemySpawnFree", "CFCA": "ComputeSpriteOffset", "D019": "CheckEnemyTileRangeB", "D02F": "DrawScoreDigit", "D03F": "NextScoreDigit", "D0B4": "ComputeSpriteOffsetB", "D139": "AddDecimalDigit", "D17C": "SplitDecimalByte", "D18B": "SetVramColumn", "D202": "SelectEnemyFrame", "D27F": "ReadActorPointerTable", "D2DF": "CheckEnemyActive", "D301": "CheckMarioAlive", "D37D": "SetEnemySpawnParams", "D4FE": "ClearActorCounters", "D56E": "CheckScriptCommand", "D593": "ComputeActorTableOffset", "D5DC": "ResetOamShadow", "D5E6": "InitEnemySprites", "D5EC": "InitEnemySpritesB", "D6BA": "CheckEnemyType", "D73C": "CheckEnemySubType", "D789": "SpawnEnemySprites", "D79D": "SetEnemySpriteFlags", "D7AA": "ReadEnemyPointer", "D7C2": "HandleEnemySubType", "D80E": "HandleEnemyCommand", "D851": "CheckEnemyCommandA", "D855": "HandleEnemyCommandB", "D877": "SpawnEnemy", "D885": "HandleEnemyCommandC", "D904": "ResetEnemyList", "D93C": "SetMarioDirectionB", "D979": "ReadEnemyScriptValue", "D990": "ComputeScoreOffset", "D9B6": "FindFreeEnemySlot", "D9C5": "FindFreeEnemySlotBack", "D9E0": "ReadEnemyPointerB", "D9EA": "SelectEnemyDirection", "D9FE": "CheckEnemySpawn", "DA7E": "UpdateEnemySprite", "DB1E": "AdvanceEnemySpeed", "DB2D": "DecEnemyTimer", "DB53": "DecEnemyScriptCounter", "DBFF": "CheckEnemySpawnBlocked", "DC38": "FlipEnemySprite", "DC75": "SetEnemyVramPointer", "DCCE": "ResetEnemyListB", "DCEA": "InitEnemySpawn", "DDB6": "SetEnemyActiveFlag", "DDC7": "CheckEnemySlotUsed", "DDE0": "CheckEnemyState", "DE2F": "LoadEnemyTileIndexB", "DEA3": "InitEnemySpriteTable", "DEAF": "CheckEnemyCollisionB", "DEEC": "CheckEnemyScroll", "DF9D": "InitEnemyPalette", "DFAC": "InitEnemyPaletteB", "DFB0": "ClearEnemySprites", "DFBA": "AdvanceEnemyFrameCounter", "DFF8": "CheckEnemyVisible", "E1CE": "CheckEnemyScriptFlag", "E1F7": "CheckStageComplete", "E24E": "AdvanceEnemyTimer", "E26D": "AdvanceEnemyTimerB", "E29A": "SetEnemyScriptFlag", "E2AB": "CheckStageStarted", "E3DD": "RunStageScript", "E3E3": "RunStageScriptB", "E41E": "HandleStageCommand", "E709": "CountDownStageTimer", "E73E": "CheckMarioSprite", "E772": "CheckEnemySprite", "E783": "CopyEnemyTileTable", "E795": "CheckStageReady", "E7E4": "WriteEnemyOam", "E805": "WriteOamSlot", "E824": "SetEnemyOamSlot", "E842": "SetEnemyOamPointerB", "E86B": "CheckEnemyOamOverflow", "E883": "SetEnemyOamSlotB", "E8BE": "SpawnEnemySprite", "E911": "ScanEnemyTypeTable", "E952": "SetEnemyOamSlotC", "E95E": "ReadEnemyOamValue", "E973": "ClearStageFlag", "E9A0": "InitEnemyPaletteTable", "E9AC": "InitMarioPaletteTable", "E9B8": "CheckEnemyScriptFlagB", "E9F2": "PrepareOamWrite", "EA31": "CheckStageFlag", "EA87": "SetEnemyOamPointer", "EADF": "AdvanceEnemyFrame", "EB6B": "DecEnemyFrameCounter", "EBBC": "AddEnemyFrameCounter", "EBCC": "ScanEnemyTypeTableB", "EBE9": "SetEnemyOamSlotD", "EBF2": "WriteEnemyOamSlot", "EC14": "WriteOamRow", "EC30": "HideEnemySprite", "EC67": "ReadActorPointer", "ECA2": "CheckStageCompleteB", "ECAF": "StartEnemyStage", "ECEC": "HandleEnemyType80", "EDA9": "CheckEnemyFlags", "EDEB": "CheckStageTimer", "EE6A": "DrawStageText", "EE82": "PushActorState", "EEA5": "UpdateEnemyTile", "EEAD": "CheckStageScript", "EEBC": "AddStageCounter", "EECC": "SubStageCounter", "EEE3": "SetEnemySpeed", "EF32": "DecEnemyCounter", "EF50": "ComputeActorTableOffsetB", "F8A7": "NopPadding", "F8D5": "PlayPulseNote", "F978": "ComputeNotePeriod", "FCA7": "WritePulse2Sweep", "FCC9": "UpdateSoundState", "FD27": "UpdateSoundChannel", } # the placeholders above are dropped NAMES = {k: v for k, v in NAMES.items() if not v.startswith("PLACEHOLDER")} NAMES.update(NAMES_EXTRA) HEADER = """# Mario Bros (JU) - Pass 2 name map # # Derived from this dump's own committed split (after the 2026-10-20 Code # Data Logger re-classification). Every name below was read out of the code; # nothing was carried over from another dump. # # Format: # # The map starts from every label the committed split already carried - the # partial Pass 2 a previous session did - so the internal branch targets # can be attributed to the routine that owns them. # # Where a routine's role could not be established from the code, the honest # address-derived `SubC` residue form is kept rather than a guess. # # 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(): proj = Path(sys.argv[1]) lines = (proj / "prg.s").read_text().split("\n") # every label the file carries, with its address labels = [] cur = None 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: labels.append((int(m2.group(1), 16), cur)) cur = None # The internal branch targets must stay out of the map: they are named # mechanically by _tools/derive_branch_names.py, which skips any label # the map already carries. branch_targets = set() for line in lines: m = re.match(r"^\s*(?:b[a-z]{2}|bra)\s+(\S+)", line) if m and m.group(1) in {n for _a, n in labels}: branch_targets.add(m.group(1)) out = {} for a, n in labels: if n in branch_targets and n.startswith("Label_"): continue out[a] = n for addr, name in NAMES.items(): a = int(addr, 16) if a in out: out[a] = name else: # a call target the tracer never labelled: the map gives it a # name and _tools/apply_pass2_map.py creates the label out[a] = name print(f"note: ${addr} has no label in the file; the map will " f"create one") seen = {} for a, n in out.items(): if n in seen: raise SystemExit(f"error: name {n!r} is used for both " f"${seen[n]:04X} and ${a:04X}") seen[n] = a text = [HEADER] for a in sorted(out): text.append(f"{a:04X}\t{out[a]}\n") (proj / "routinemap.tsv").write_text("".join(text)) print(f"{len(out)} entries written " f"({len(NAMES)} new names, " f"{sum(1 for v in out.values() if v.startswith('SubC'))} SubC residue)") if __name__ == "__main__": main()