#!/usr/bin/env python3 """Generate tools/rom_manifest_gold.json from pret/pokegold + pokegold.sym. Phase 1 Gen 2 (Pokemon Gold) counterpart to tools/make_rom_manifest.py. It deliberately does not reuse tools/extract/* (that package is pokered-shaped: ASM_DEFINES pins _RED, and several helpers assume Gen 1's flat map order and Kanto dex numbering), so this file carries its own small RGBDS constant parser instead. Differences from Gen 1 that shape this file: - Species order IS dex order: BaseData's rows are declared in constants/pokemon_constants.asm order, and each row's own first byte is that same dex number (data/pokemon/base_stats/*.asm `db `). There is no separate dexOrder/gen1_order remap to carry. - Maps are grouped (MAPGROUP_*/MAP_*, constants/map_constants.asm's `newgroup`/`map_const`/`endgroup`), not one flat table. - Pokemon pics and tileset graphics are lz3-compressed (home/decompress.asm), not pkmncompress'd -- see Rom.decompressLz3. Usage: python3 tools/make_gold_manifest.py Default paths: pokegold at ../pokegold (relative to the repo) or /Users/bryanbassett/Documents/development/pokegold; symbols at /Users/bryanbassett/Documents/development/pokegold-symbols/pokegold.sym. """ from __future__ import annotations import argparse import json import os import sys # Reuse Gen 1's fontCharmap parser (seq→code for Font.encode); Gold's # constants/charmap.asm is the same shape for the $60-$FF draw range. sys.path.insert(0, os.path.join(os.path.dirname(__file__))) from extract import font as font_extract # noqa: E402 import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from rom_data import CANONICAL_GOLD_SHA1, SymbolTable # noqa: E402 REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEFAULT_POKEGOLD_CANDIDATES = [ os.path.join(os.path.dirname(REPO_ROOT), "pokegold"), "/Users/bryanbassett/Documents/development/pokegold", ] DEFAULT_SYMBOLS = ( "/Users/bryanbassett/Documents/development/pokegold-symbols/pokegold.sym") # pokegold builds Gold with _GOLD defined (rgbdscheck.asm / the Makefile's # `gold` target); none of the constants files this script reads are # actually gated on it today, but resolving IF DEF(_GOLD)/_SILVER here # (rather than skipping IF entirely, like tools/extract/util.py does for # unmodeled conditions) keeps this future-proof if that ever changes. ASM_DEFINES = {"_GOLD"} def strip_comment(line): out = [] in_str = False for ch in line: if ch == '"': in_str = not in_str elif ch == ";" and not in_str: break out.append(ch) return "".join(out).rstrip() def read_asm(path): """Read an asm file as (lineno, text), comments stripped, IF resolved.""" lines = [] stack = [] # stack of [taking, condition_known] with open(path, encoding="utf-8") as f: for lineno, raw in enumerate(f, 1): line = strip_comment(raw.rstrip("\n")) s = line.strip() m = re.match(r"IF\s+(!)?DEF\((\w+)\)\s*$", s, re.IGNORECASE) if m: defined = m.group(2) in ASM_DEFINES taking = (not defined) if m.group(1) else defined stack.append([taking, True]) continue if re.match(r"IF\b", s): stack.append([True, False]) # unmodeled condition: keep body continue if re.match(r"ELSE\s*$", s, re.IGNORECASE) and stack: if stack[-1][1]: stack[-1][0] = not stack[-1][0] continue if re.match(r"ENDC\s*$", s, re.IGNORECASE) and stack: stack.pop() continue if any(not fr[0] for fr in stack): continue lines.append((lineno, line)) return lines def parse_number(tok): tok = tok.strip() neg = tok.startswith("-") if neg: tok = tok[1:].strip() if tok.startswith("$"): val = int(tok[1:], 16) elif tok.startswith("%"): val = int(tok[1:], 2) elif tok.isdigit(): val = int(tok) else: raise ValueError(f"not a number: {tok!r}") return -val if neg else val def parse_const_block(path, stop_at=None): """Parse a linear const_def/const/const_skip block into an ordered list. Index i of the returned list is the constant's value (None for a gap); the same shape tools/extract/util.parse_const_block returns for Gen 1. """ names = [] value = None for _, line in read_asm(path): s = line.strip() if not s: continue if stop_at and re.match(rf"DEF\s+{stop_at}\b", s): break m = re.match(r"const_def(?:\s+(\$?\w+))?$", s) if m: value = parse_number(m.group(1)) if m.group(1) else 0 continue m = re.match(r"const_next\s+(\$?\w+)$", s) if m: value = parse_number(m.group(1)) continue m = re.match(r"const\s+(\w+)", s) if m and value is not None: while len(names) < value: names.append(None) names.append(m.group(1)) value += 1 continue m = re.match(r"const_skip(?:\s+(\d+))?$", s) if m and value is not None: n = int(m.group(1)) if m.group(1) else 1 for _ in range(n): names.append(None) value += n return names def parse_const_block_at(path, first_const, stop_at=None): """Parse the one const_def block whose first `const` is `first_const`. parse_const_block walks a file linearly and a second `const_def` in the same file rewinds its counter without rewinding the list, so it can only read a file's FIRST block. phone_constants.asm stacks two (PHONE_* then SPECIALCALL_*) and script_constants.asm stacks a dozen, so anything past the first needs to be found by name. Holes are kept as None: PHONE_* has four `const_skip`s in the middle and they are real PhoneContacts rows. """ names = [] value = None started = False for _, line in read_asm(path): s = line.strip() if not s: continue m = re.match(r"const_def(?:\s+(\$?\w+))?$", s) if m: if started: break value = parse_number(m.group(1)) if m.group(1) else 0 names = [] continue if started and stop_at and re.match(rf"DEF\s+{stop_at}\b", s): break m = re.match(r"const\s+(\w+)", s) if m and value is not None: if not started: if m.group(1) != first_const: continue started = True while len(names) < value: names.append(None) names.append(m.group(1)) value += 1 continue m = re.match(r"const_skip(?:\s+(\d+))?$", s) if m and started and value is not None: n = int(m.group(1)) if m.group(1) else 1 for _ in range(n): names.append(None) value += n if not started: raise SystemExit(f"{path}: no const block starting at {first_const}") return names def parse_prefixed_consts(path, prefixes, exact=()): """Ordered const names from a file, filtered by prefix (mixed blocks). map_data_constants.asm and friends stack several unrelated const_def blocks in one file, so scraping by prefix is the only way to pull one of them out without the neighbours bleeding in. `exact` names are kept even when they carry no shared prefix (TOWN, BALL, ...). """ out = [] for _, line in read_asm(path): m = re.match(r"const\s+(\w+)", line.strip()) if not m: continue name = m.group(1) if name in exact or any(name.startswith(p) for p in prefixes): out.append(name) return out def parse_sparse_consts(path, prefixes): """Ordered const names where index IS the const value (sparse blocks). parse_prefixed_consts packs a block densely, which is wrong for a block that jumps: constants/item_data_constants.asm's HELD_* run uses const_skip holes and const_next jumps to 10/20/30/40/50/70, so a dense scrape shifts every value after the first hole (HELD_HEAL_POISON is 10, not 7 -- and QUICK_CLAW at 74 fell off the end of the list entirely). Holes carry placeholder names because the manifest's JSON round trip collapses nulls out of an array. """ out = [] value = None for _, line in read_asm(path): s = line.strip() m = re.match(r"const_def(?:\s+(\d+))?\s*$", s) if m: value = int(m.group(1) or 0) continue m = re.match(r"const_next\s+(\d+)\s*$", s) if m and value is not None: value = int(m.group(1)) continue m = re.match(r"const_skip(?:\s+(\d+))?\s*$", s) if m and value is not None: value += int(m.group(1) or 1) continue m = re.match(r"const\s+(\w+)", s) if not m or value is None: continue name = m.group(1) if any(name.startswith(p) for p in prefixes): while len(out) < value: out.append("%sUNUSED_%d" % (prefixes[0], len(out))) out.append(name) value += 1 return out def extract_trainer_classes(pokegold): """Ordered trainer class names (constants/trainer_constants.asm). `trainerclass NAME` bumps its own counter and resets the const_def used for that class's individual trainer ids, so parse_const_block cannot see them -- the two interleaved sequences need separate passes. Index in the returned list IS the class id (TRAINER_NONE = 0). """ path = os.path.join(pokegold, "constants/trainer_constants.asm") classes = [] members = {} current = None for _, line in read_asm(path): s = line.strip() m = re.match(r"trainerclass\s+(\w+)", s) if m: current = m.group(1) classes.append(current) members[current] = [] continue m = re.match(r"const\s+(\w+)", s) if m and current and not m.group(1).startswith("PHONECONTACT_"): members[current].append(m.group(1)) if not classes or classes[0] != "TRAINER_NONE" or classes[1] != "FALKNER": raise ValueError("trainer_constants.asm did not parse as expected") return classes, members def extract_items(pokegold): """Ordered item ids, and how many of them ItemNames actually covers. parse_const_block cannot do this file: after NUM_ITEMS the TM and HM items are declared through `add_tm DYNAMICPUNCH` / `add_hm CUT` macros that expand to `const TM_DYNAMICPUNCH` / `const HM_CUT`, and two plain `const ITEM_C3` / `ITEM_DC` sit *inside* that run consuming ids without being TMs. Stopping at NUM_ITEMS (as a linear parse must) loses all 57 TM/HM items; ignoring it loses the boundary where ItemNames stops having rows. Returns (order, name_count) where order[i - 1] is item id i; NO_ITEM (0) is dropped so the list is 1-based on the id, matching ItemNames' rows. """ path = os.path.join(pokegold, "constants/item_constants.asm") order = [] name_count = None in_macro = False for _, line in read_asm(path): s = line.strip() # The add_tm / add_hm macro bodies contain `const TM_\1` themselves; # counting those would insert a phantom "TM_" item before the real run. if re.match(r"MACRO\b", s): in_macro = True continue if re.match(r"ENDM\b", s): in_macro = False continue if in_macro: continue if re.match(r"DEF\s+NUM_ITEMS\b", s): # NUM_ITEMS is const_value - 1, i.e. however many ids came before. name_count = len(order) continue m = re.match(r"const\s+(\w+)", s) if m: order.append(m.group(1)) continue m = re.match(r"add_tm\s+(\w+)", s) if m: order.append("TM_" + m.group(1)) continue m = re.match(r"add_hm\s+(\w+)", s) if m: order.append("HM_" + m.group(1)) continue if not order or order[0] != "NO_ITEM": raise ValueError("item_constants.asm did not parse as expected") if not name_count: raise ValueError("item_constants.asm: NUM_ITEMS not found") # Drop NO_ITEM and the count that included it. return order[1:], name_count - 1 def extract_specials(pokegold): """Ordered SpecialsPointers labels (data/events/special_pointers.asm). The `special` script command carries an index into this table, not a name, so without the order a disassembled `special 27` says nothing. Index in the returned list IS the id. """ path = os.path.join(pokegold, "data/events/special_pointers.asm") names = [] in_macro = False for _, line in read_asm(path): s = line.strip() if re.match(r"MACRO\b", s): in_macro = True continue if re.match(r"ENDM\b", s): in_macro = False continue if in_macro: continue m = re.match(r"add_special\s+(\w+)", s) if m: names.append(m.group(1)) if not names or names[0] != "WarpToSpawnPoint": raise ValueError("special_pointers.asm did not parse as expected") return names def extract_std_scripts(pokegold): """Ordered StdScripts labels (engine/events/std_scripts.asm).""" path = os.path.join(pokegold, "engine/events/std_scripts.asm") names = [] for _, line in read_asm(path): m = re.match(r"add_stdscript\s+(\w+)", line.strip()) if m: names.append(m.group(1)) if not names or names[0] != "PokecenterNurseScript": raise ValueError("std_scripts.asm did not parse as expected") return names def extract_map_groups(pokegold): """Parse constants/map_constants.asm's newgroup/map_const/endgroup.""" path = os.path.join(pokegold, "constants/map_constants.asm") order = [] groups = {} group = 0 map_index = 0 for _, line in read_asm(path): s = line.strip() if re.match(r"newgroup\s+\w+", s): group += 1 map_index = 0 continue m = re.match(r"map_const\s+(\w+),\s*([\d$%-]+),\s*([\d$%-]+)", s) if m: map_index += 1 name = m.group(1) order.append(name) groups[name] = { "name": name, "group": group, "map": map_index, "width": parse_number(m.group(2)), "height": parse_number(m.group(3)), } if not order or order[0] != "OLIVINE_POKECENTER_1F": raise ValueError("map_constants.asm did not parse as expected") return order, groups def extract_types(pokegold): """Type constants are physical IDs, a gap, then special IDs (Gen 1-style).""" path = os.path.join(pokegold, "constants/type_constants.asm") types = {} value = None for _, line in read_asm(path): s = line.strip() m = re.match(r"const_def(?:\s+(\$?\w+))?$", s) if m: value = parse_number(m.group(1)) if m.group(1) else 0 continue m = re.match(r"const_next\s+(\$?\w+)$", s) if m: value = parse_number(m.group(1)) continue m = re.match(r"const\s+(\w+)", s) if m and value is not None: types[m.group(1)] = value value += 1 if types.get("NORMAL") != 0 or "DARK" not in types: raise ValueError("type_constants.asm did not parse as expected") return types def charmap(pokegold): """Byte -> text charmap, same shape as make_rom_manifest.charmap.""" expansions = { "": ".", "": "{LV}", "": "{ID}", # Compression bytes: the cart stores one byte and PlaceString expands # it into several glyphs. The font sheet only starts at $60, so these # three have no tile of their own and MUST be expanded here or the # decoded text asks the font for a glyph that cannot exist -- which is # what turns "#DEX" into "DEX" and "GEAR" into "GEAR". # The comments in charmap.asm name each expansion. "#": "POKé", # $54 "": "", # $24 "": "", # $4a } out = {} path = os.path.join(pokegold, "constants/charmap.asm") for _, line in read_asm(path): m = re.match( r'charmap\s+"((?:[^"\\]|\\.)*)",\s*(\$[0-9a-fA-F]+)', line.strip()) if not m: continue value = int(m.group(2)[1:], 16) if str(value) in out: continue seq = m.group(1).replace('\\"', '"') out[str(value)] = expansions.get(seq, seq) return out def species_label(species_id): """SPECIES_CONST_NAME -> the SpeciesConstName label pics use. Matches every dba_pics entry in data/pokemon/pic_pointers.asm: strip underscores (NIDORAN_F -> NidoranF, MR__MIME -> MrMime, FARFETCH_D -> FarfetchD, HO_OH -> HoOh) and title-case what remains. """ parts = [part for part in species_id.split("_") if part] return "".join(part.capitalize() for part in parts) def pokemon_names(pokegold): """PokemonNames dname entries, in declared (dex) order.""" names = [] path = os.path.join(pokegold, "data/pokemon/names.asm") for _, line in read_asm(path): m = re.match(r'dname\s+"([^"]*)"', line.strip()) if m: names.append(m.group(1)) return names def music_order(pokegold): """Music_* labels in MUSIC_* id order from audio/music_pointers.asm.""" path = os.path.join(pokegold, "audio/music_pointers.asm") order = [] for _, line in read_asm(path): m = re.match(r"dba\s+(Music_\w+)\s*$", line.strip()) if m: order.append(m.group(1)) if not order: raise ValueError("no Music_* dba rows in audio/music_pointers.asm") return order def sfx_order(pokegold): """Sfx_* labels in SFX_* id order from audio/sfx_pointers.asm.""" path = os.path.join(pokegold, "audio/sfx_pointers.asm") order = [] for _, line in read_asm(path): m = re.match(r"dba\s+(Sfx_\w+)\s*$", line.strip()) if m: order.append(m.group(1)) if not order: raise ValueError("no Sfx_* dba rows in audio/sfx_pointers.asm") return order def pokemon_assets(pokegold, species_order, symbols): """{species: {id, name, front, back, frontLabel, backLabel}}. Unown shares one pic per letter through UnownPicPointers (its dba_pics row in pic_pointers.asm is deliberately blank) rather than a per-species Frontpic/Backpic label, so its front/back/labels are left null; the extractor's own Unown handling (if any) goes through UnownPicPointers directly instead of this table. """ names = pokemon_names(pokegold) assets = {} for index, species in enumerate(species_order): name = names[index] if index < len(names) else species if species == "UNOWN": # No per-species pic, but it does have a dex entry like anything # else, so the #DEX screen can still read it. assets[species] = { "id": species, "name": name, "front": None, "back": None, "frontLabel": None, "backLabel": None, "dexLabel": "UnownPokedexEntry", } continue base = species_label(species) front_label, back_label = base + "Frontpic", base + "Backpic" if front_label not in symbols.by_name or back_label not in symbols.by_name: raise ValueError( f"{species}: expected pic symbols {front_label}/{back_label} " "are missing from pokegold.sym") # Pokedex entry symbol, resolved here for the same reason the pic # labels are: the entries live in four different banks and the game # derives the bank from the species id arithmetically. dex_label = base + "PokedexEntry" assets[species] = { "id": species, "name": name, "front": base.lower(), "back": base.lower() + "_back", "frontLabel": front_label, "backLabel": back_label, "dexLabel": dex_label if dex_label in symbols.by_name else None, } return assets REQUIRED_SYMBOLS = { "BaseData", "PokemonNames", "PokemonPicPointers", "UnownPicPointers", "EggPic", # gfx/evo/egg_hatch.2bpp, the two OBJ tiles EggHatch_AnimationSequence # copies to vTiles0 tile $00 for the shell crack and the ten fragments # (engine/pokemon/breeding.asm:777). "EggHatchGFX", "Font", "FontExtra", "FontBattleExtra", "Frames", # The extra font page is NOT FontExtra laid down from $60: _LoadFontsExtra # (engine/gfx/load_font.asm:7-20) builds it from three sources, and the # first three tiles come from the other two. gfx/font.asm keeps them well # away from FontExtra, so each needs its own symbol: # $60-$61 FontsExtra_SolidBlackAndUpArrowGFX, 2 tiles, 1bpp # (gfx/font/black.1bpp + gfx/font/up_arrow.1bpp) # $62 PokegearPhoneIconGFX, 1 tile, 2bpp (gfx/font/phone_icon.2bpp) # $63+ FontExtra + 3 tiles, 22 tiles, 2bpp # Loading FontExtra from $60 instead puts its unused // # in those three cells -- constants/charmap.asm:41 marks that # $62 mapping "unused" and :88 gives $62 to "☎" -- which is why the # Pokegear caller box drew a bold C where the phone icon belongs. "PokegearPhoneIconGFX", "FontsExtra_SolidBlackAndUpArrowGFX", # gfx/font/unown_font.2bpp (gfx/font.asm UnownFont): the 26 Unown letters # plus the diamond cursor, loaded at vTiles2 tile FIRST_UNOWN_CHAR ($40) # by Pokedex_LoadUnownFont for the #DEX's UNOWN MODE. "UnownFont", "TitleScreenGFX1", "TitleScreenGFX2", "TitleScreenGFX3", "TitleScreenGFX4", "TitleScreenTilemap", "CopyrightGFX", "Tilesets", "MapGroupPointers", "Music", "Cries", # _AnimateTileset's `dw arg / dw function` programs (data/tileset_anims.asm) # and the two shared frame strips (engine/tilesets/tileset_anims.asm:194, # :225): the extractor resolves a tileset's Anim pointer against these. "DoneTileAnimation", "WaitTileAnimation", "StandingTileFrame", "StandingTileFrame8", "AnimateWaterTile", "AnimateFlowerTile", "AnimateWaterPalette", "ReadTileToAnimBuffer", "WriteTileFromAnimBuffer", "ScrollTileRightLeft", "ScrollTileDown", "ScrollTileUp", "ScrollTileLeft", "ScrollTileRight", "AnimateWhirlpoolTile", "AnimateLavaBubbleTile1", "AnimateLavaBubbleTile2", "AnimateTowerPillarTile", "FlickeringCaveEntrancePalette", "AnimateWaterTile.WaterTileFrames", "AnimateFlowerTile.FlowerTileFrames", # AnimateLavaBubbleTile1/2 hardcode their source (tileset_anims.asm:251, # :276); the whirlpool and tower-pillar strips come off the pointer pairs # their own tileframe arguments name, so they need no symbol here. "LavaBubbleTileFrames", # data/sprites/emotes.asm:22 `emote GrassRustleGFX, 1, $fe`, the one tile # ShakeGrass' SPRITEMOVEDATA_GRASS object draws. "GrassRustleGFX", "PokemonCries", "SFX", "OverworldSprites", "ChrisSpriteGFX", "Moves", "EvosAttacksPointers", # data/sprites/sprite_mons.asm: one species byte per SPRITE_POKEMON id, # which GetMonSprite's .Icon arm feeds to LoadOverworldMonIcon "SpriteMons", # Phase 2: roofs overlay outdoor Johto towns (engine/tilesets/mapgroup_roofs.asm) "MapGroupRoofs", "Roofs", # New-game object visibility (engine/events/std_scripts.asm InitializeEventsScript) "InitializeEventsScript", # Gen 2 audio driver: wave RAM patterns + drum kit pointer table "WaveSamples", "Drumkits", # Oak speech intro (engine/menus/intro_menu.asm OakSpeech) "PokemonProfPic", "CalPic", "_OakText1", "_OakText2", "_OakText3", "_OakText4", "_OakText5", "_OakText6", "_OakText7", # Boot cinema (splash.asm / ShrinkPlayer). GameFreakLogoGFX is two # INCBINs run together -- gamefreak_presents.1bpp (13 tiles) then # gamefreak_logo.1bpp (15) -- and GameFreakLogoStarsGFX is another two, # logo_star.2bpp (2) then logo_sparkle.2bpp (3). "GameFreakLogoGFX", "GameFreakLogoStarsGFX", "Shrink1Pic", "Shrink2Pic", # Credits roll graphics (engine/movie/credits.asm). CreditsBorderGFX is # the 9-tile strip on rows 4 and 13; the four mon labels are 4x4-tile # frames stacked (3 frames each, Sentret 4); TheEndGFX lives in its own # "The End" section in gfx/misc.asm; CreditsPalettes is gfx/credits/ # credits.pal, six four-colour sets. "CreditsBorderGFX", "CreditsBellossomGFX", "CreditsTogepiGFX", "CreditsElekidGFX", "CreditsSentretGFX", "TheEndGFX", "CreditsPalettes", # The #DEX diploma (engine/events/diploma.asm PlaceDiplomaOnScreen). # DiplomaGFX is gfx/diploma/diploma.2bpp.lz, 112 tiles into vTiles2; # DiplomaPage1Tilemap is gfx/diploma/page1.tilemap, a whole SCREEN_AREA # copied straight over the background; DiplomaPalettes is gfx/diploma/ # diploma.pal (engine/gfx/color.asm), which _CGB_Diploma loads before # WipeAttrmap puts every tile on set 0. "DiplomaGFX", "DiplomaPage1Tilemap", "DiplomaPalettes", # The trade animation's art (engine/movie/trade_animation.asm, gfx/trade/). # TradeGameBoyLZ is game_boy_cable.2bpp.lz, 49 tiles into vTiles2 tile $31, # which both tilemaps and the jumptable's loose cable ids index; the rest # are the OAM objects -- the ball, the poof, the tube bulge (gfx/trade/ # cable.png) and the mon icon's bubble -- plus the two one-tile arrows # TradeAnim_PlaceTrademonStatsOnTubeAnim fills across the window. "TradeGameBoyLZ", "TradeGameBoyTilemap", "TradeLinkTubeTilemap", "TradeBallGFX", "TradePoofGFX", "TradeCableGFX", "TradeBubbleGFX", "TradeArrowRightGFX", "TradeArrowLeftGFX", # Item names (data/items/names.asm) for giveitem / verbosegiveitem "ItemNames", # Mart shelves (data/items/marts.asm): Marts is the NUM_MARTS pointer # table `pokemart` indexes, each list `db count, items..., -1`. # BargainShopData (data/items/bargain_shop.asm) is the Goldenrod # Underground shop's own `dbw item, price` rows -- the one shop whose # prices do not come from ItemAttributes. "Marts", "BargainShopData", # GBC colour (engine/gfx/color.asm LoadMapPals). TilesetBGPalette is the # shared pool of $2a four-colour palettes; EnvironmentColorsPointers picks # 8 of them per environment per time of day; MapObjectPals recolours OW # sprites; RoofPals overrides PAL_BG_ROOF colours 1-2 per map group. "TilesetBGPalette", "EnvironmentColorsPointers", "MapObjectPals", "RoofPals", "PokemonPalettes", "TrainerPalettes", "HPBarPals", "ExpBarPalette", "PartyMenuOBPals", # Battle + pokemon runtime data "MoveNames", "TypeNames", "TypeMatchups", "TMHMMoves", "GrowthRates", "ItemAttributes", "MoveDescriptions", "ItemDescriptions", # Wild encounters (data/wild/*) "JohtoGrassWildMons", "JohtoWaterWildMons", "KantoGrassWildMons", "KantoWaterWildMons", "FishGroups", "TreeMons", "TreeMonMaps", # data/wild/treemon_maps.asm RockMonMaps: the four maps whose smashable # rocks roll TREEMON_SET_ROCK, read by RockMonEncounter. "RockMonMaps", # data/wild/swarm_grass.asm + swarm_water.asm, searched by # _SwarmWildmonCheck BEFORE the Johto/Kanto tables while the player stands # on wSwarmMapGroup/Number, and data/wild/roammon_maps.asm RoamMaps, the # graph UpdateRoamMons and JumpRoamMon walk the beasts along. "SwarmGrassWildMons", "SwarmWaterWildMons", "RoamMaps", # data/wild/bug_contest_mons.asm, which is NOT a grass table: its rows are # `db %, species, min, max` with no map key and no time of day, and # ChooseWildEncounter_BugContest (engine/overworld/events.asm) walks them # with `ld de, 4`. "ContestMons", # data/events/bug_contest_flags.asm: the ten # EVENT_BUG_CATCHING_CONTESTANT_*A words # SelectRandomBugContestContestants (engine/events/bug_contest/ # contest_2.asm) resets and then sets five of, a set flag being what keeps # that trainer off NationalParkBugContest. "BugCatchingContestantEventFlagTable", # Trainers (data/trainers/*) "Trainers", "TrainerGroups", "TrainerClassNames", "TrainerClassAttributes", "TrainerPicPointers", "TrainerEncounterMusic", # Menus: party icons, dex ordering + entries, Pokegear landmarks. The two # held-item marker tiles ride on the party icons: GetIconGFX uploads # HeldItemIcons straight after each icon's eight tiles # (engine/gfx/mon_icons.asm:218-228), so the party list cannot draw the # marker without this symbol. "MonMenuIcons", "Icons", "IconPointers", "HeldItemIcons", "PokedexDataPointerTable", # New-game / Pokecenter respawn table (data/maps/spawn_points.asm) "SpawnPoints", "NewPokedexOrder", "AlphabeticalPokedexOrder", "Landmarks", # GoldSilverIntro (engine/movie/intro.asm): three acts of BG art, each a # compressed tile sheet plus a 2x2 metatile table and a metatile grid, and # the OBJ sheets the mons animate from "Intro_WaterGFX1", "Intro_WaterTilemap", "Intro_WaterMeta", "Intro_WaterGFX2", "Intro_GrassGFX1", "Intro_GrassTilemap", "Intro_GrassMeta", "Intro_GrassGFX2", "Intro_FireGFX1", "Intro_FireGFX2", "Intro_FireGFX3", # ...and its palettes. _CGB_GSIntro carries the water act's inline, takes # the grass and starter ones out of PredefPals, and reaches the fire act's # through the four PREDEFPAL_* indices inside PalPacket_Pack. "PredefPals", "PalPacket_Pack", "_CGB_GSIntro.ShellderLaprasBGPalette", "_CGB_GSIntro.ShellderLaprasOBPals", "Intro_LoadMagikarpPalettes.MagikarpBGPal", "Intro_LoadMagikarpPalettes.MagikarpOBPal", # Battle animations (data/moves/animations.asm + data/battle_anims/*). # BattleAnimations is a 278-entry pointer table indexed by the animation # id, which for a move IS its move id; the four tables under it are the # object rows an animation spawns, their framesets, the OAM sets those # framesets step through, and the compressed sheets the tiles come from. "BattleAnimations", "BattleAnimObjects", "BattleAnimFrameData", "BattleAnimOAMData", "AnimObjGFX", # gfx/battle_anims/battle_anims.pal: the six OBJ palettes an animation # object names by PAL_BATTLE_OB_GRAY..PAL_BATTLE_OB_BROWN. The routine # that copies them (CGBCopyBattleObjectPals) is dummied out on the cart -- # _CGB_BattleColors loads the same block -- but the label is still in the # ROM, so the colours are read rather than transcribed. "BattleObjectPals", # Battle HUD tiles (engine/gfx/load_font.asm LoadHPBar): the L-shaped # frame tiles the enemy and player HUDs are built from, and the exp bar's # nine fill cells. "HP:" and the ten HP-bar cells come from # FontBattleExtra, which is already extracted. "EnemyHPBarBorderGFX", "HPExpBarBorderGFX", "ExpBarGFX", # The player's own battle back-pic (gfx/player/chris_back.2bpp.lz). It is # what stands in the player's pic box for the whole battle intro, before # SendOutPlayerMon swaps in the mon's backpic. "ChrisBackpic", # UnownPicPointers: 26 rows of `dba_pics front, back`, one per letter. # PokemonPicPointers' UNOWN row is only the A form, so the other 25 are # unreachable without this table. "UnownPicPointers", # Naming screen chrome (gfx/naming_screen/*): the patterned tile that # fills the backdrop, the 2-tile cursor, and the middle/under lines that # mark the name-entry field "NamingScreenGFX_Border", "NamingScreenGFX_Cursor", "NamingScreenGFX_MiddleLine", "NamingScreenGFX_UnderLine", # PACK chrome (engine/items/pack.asm): the screen's own $60-tile sheet, # the four 15-tile pack pictures DrawPackGFX swaps per pocket, the 5x12 # pocket-name tilemap, and the six BG palettes _CGB_PackPals loads. "PackMenuGFX", "PackGFX", "DrawPocketName.tilemap", "_CGB_PackPals.PackPals", # #DEX chrome (engine/pokedex/pokedex.asm): Pokedex_LoadGFX decompresses # PokedexLZ over vTiles2 tile $31, and every frame, divider and label tile # the dex screens name comes out of that one sheet. # PokedexSlowpokeLZ is the dex's OBJ sheet: the search screen's Slowpoke # animation, and behind it the $30-$33 bracket tiles the listing cursor is # built from plus the $0f scrollbar thumb. "PokedexLZ", "PokedexSlowpokeLZ", "PokedexCursorPalette", "LoadQuestionMarkPic.QuestionMarkLZ", "PokedexQuestionMarkPalette", "Footprints", # POKeGEAR (engine/pokegear/pokegear.asm): TownMapGFX at $00 and # PokegearGFX at $30 are the card sheets; the three cards are RLE # tilemaps; JohtoMap/KantoMap are the painted town maps; TownMapPals.PalMap # is the tile-id -> BG palette nybble table that colours them. "TownMapGFX", "PokegearGFX", "PokegearSpritesGFX", "ClockTilemapRLE", "PhoneTilemapRLE", "RadioTilemapRLE", "JohtoMap", "KantoMap", "TownMapPals.PalMap", "PokegearPals", # Trainer card (engine/menus/trainer_card.asm): the player's portrait and # the card frame share one sheet at $00, the status/leader sheets both # load at $29, and the badges are OBJs with their own OAM template table. "ChrisPicAndTrainerCardGFX", "CardStatusGFX", "LeaderGFX", "BadgeGFX", "TrainerCard_JohtoBadgesOAM", # Unown puzzle (engine/games/unown_puzzle.asm): the four pictures the Ruins # of Alph chambers slice into sixteen panels, the START>CANCEL box and # caption sheet that lands at vTiles0 $ed, the four cursor OBJ tiles at # $e0, and the eight border tiles UnownPuzzle_AddPuzzlePieceBorders ORs # onto every panel once the picture has been doubled in size. "KabutoPuzzleLZ", "OmanytePuzzleLZ", "AerodactylPuzzleLZ", "HoOhPuzzleLZ", "UnownPuzzleStartCancelLZ", "UnownPuzzleCursorGFX", "PuzzlePieceBorderData.TileBordersGFX", # Emote bubbles (data/sprites/emotes.asm): showemote's ! over a trainer # who just spotted the player, and the other faces scripts use. "ShockEmote", "QuestionEmote", "HappyEmote", "SadEmote", # The Pokecenter heal machine's OBJ art (engine/events/ # heal_machine_anim.asm): two tiles -- the machine's light ($7c) and the # ball ($7d) -- plus the CGB palette .LoadPalettes copies over # PAL_OW_TREE for the duration of the light show. "HealMachineAnim.HealMachineGFX", "HealMachineAnim.palettes", # Magnet Train (engine/events/magnet_train.asm). Two uncompressed # tilemaps: a 2x18 strip DrawMagnetTrain repeats across the background, # and the 20x4 train laid over rows 6-9. The cutscene loads no tiles of # its own -- both index TILESET_TRAIN_STATION, already in VRAM. "MagnetTrainBGTiles", "MagnetTrainTilemap", # callstd / jumpstd targets (engine/events/std_scripts.asm) "StdScripts", # The phone (data/phone/*.asm). PhoneContacts is a row per PHONE_* # constant carrying two `dba` script pointers -- the callee half (you rang # them) and the caller half (they rang you) -- and SpecialPhoneCallList a # row per SPECIALCALL_* carrying one more. Every one of those pointers # lands in bank $41, which no map script points at, so without these two # symbols the whole bank stays unreachable and a queued call has no body. "PhoneContacts", "SpecialPhoneCallList", # The three little scripts the phone engine runs WITHOUT a contact row # (engine/phone/phone.asm): the wrong-number arm LoadCallerScript falls to, # the no-signal / wrong-hour arm, and "they are on this very map, go talk # to them". Nothing points at any of them either. "WrongNumber.script", "PhoneOutOfAreaScript", "PhoneScript_JustTalkToThem", # Egg moves (data/pokemon/egg_moves.asm): 251 `dw` into bank 8, each a # $ff-terminated move list. Breeding.canInheritMove reads def.eggMoves. "EggMovePointers", # In-game trades (data/events/npc_trades.asm), the `trade` command's table, # and the 5x3 text table PrintTradeText indexes by dialog then dialog SET # (engine/events/npc_trade.asm). NPCTradeCableText is the "cable" line # between the yes and the animation; TradedForText is the one after it. "NPCTrades", "TradeTexts", "NPCTradeCableText", "TradedForText", # The six lines the trade ANIMATION prints around the swap # (engine/movie/trade_animation.asm, strings in data/text/common_1.asm). # They are printed by asm, not by a writetext, so the text walker only # reaches them by name -- and their {STRBUF} markers are the trademon # name buffers, which is the only thing that tells "MACHOP was sent to # MIKE" from the same line with the two names the other way round. # _MonNameSentToText is deliberately absent: it is empty. "_MonWasSentToText", "_ForYourMonSendsText", "_OTSendsText", "_BidsFarewellToMonText", "_MonNameBidsFarewellText", "_TakeGoodCareOfMonText", # Elevator floor labels (data/events/elevator_floors.asm). The floor LIST # an `elevator` names lives in the script's own bank; this is the shared # FLOOR_* -> "B1F@" name table the ride menu prints. "ElevatorFloorNames", # describedecoration's five DECODESC_* arms (engine/overworld/decorations.asm). # Each arm is asm that picks a SCRIPT off what is installed in the player's # room, so the scripts are what the extractor wants, not the arms: # DecorationDesc_PosterPointers is the poster table (`dbw deco, script` # rows, `db -1` end) with NullPoster as its miss; the two ornaments and the # console all share .OrnamentConsoleScript, and the giant ornament has # .BigDollScript to itself. "DecorationDesc_PosterPointers", "DecorationDesc_NullPoster", "DecorationDesc_OrnamentOrConsole.OrnamentConsoleScript", "DecorationDesc_GiantOrnament.BigDollScript", # Text an ENGINE routine prints rather than a script (RomExtractorGen2's # NAMED_TEXT). The text walker only follows a writetext pointer, so # without these labels the Day-Care and breeding block of common_1.asm / # common_2.asm, the whole POKeMART conversation, and the Hall of Fame's # three flavour strings never reach the cache at all. "_DaycareDummyText", "_DayCareManIntroText", "_DayCareManIntroEggText", "_DayCareLadyIntroText", "_DayCareLadyIntroEggText", "_WhatShouldIRaiseText", "_OnlyOneMonText", "_CantAcceptEggText", "_RemoveMailText", "_LastHealthyMonText", "_IllRaiseYourMonText", "_ComeBackLaterText", "_AreWeGeniusesText", "_YourMonHasGrownText", "_PerfectHeresYourMonText", "_GotBackMonText", "_BackAlreadyText", "_HaveNoRoomText", "_NotEnoughMoneyText", "_OhFineThenText", "_ComeAgainText", "_NotYetText", "_FoundAnEggText", "_ReceivedEggText", "_TakeGoodCareOfEggText", "_IllKeepItThanksText", "_NoRoomForEggText", "Text_BreedHuh", "_BreedClearboxText", "_BreedEggHatchText", "_BreedAskNicknameText", "_LeftWithDayCareManText", "_LeftWithDayCareLadyText", "_BreedBrimmingWithEnergyText", "_BreedNoInterestText", "_BreedAppearsToCareForText", "_BreedFriendlyText", "_BreedShowsInterestText", "_MartWelcomeText", "_MartAskMoreText", "_MartComeAgainText", "_MartHowManyText", "_MartFinalPriceText", "_MartThanksText", "_MartNoMoneyText", "_MartPackFullText", "_HerbShopLadyIntroText", "_HerbalLadyHowManyText", "_HerbalLadyFinalPriceText", "_HerbalLadyThanksText", "_HerbalLadyPackFullText", "_HerbalLadyNoMoneyText", "_HerbalLadyComeAgainText", "_BargainShopIntroText", "_BargainShopFinalPriceText", "_BargainShopThanksText", "_BargainShopPackFullText", "_BargainShopSoldOutText", "_BargainShopNoFundsText", "_BargainShopComeAgainText", "_PharmacyIntroText", "_PharmacyHowManyText", "_PharmacyFinalPriceText", "_PharmacyThanksText", "_PharmacyPackFullText", "_PharmacyNoMoneyText", "_PharmacyComeAgainText", "_NothingToSellText", "_MartSellHowManyText", "_MartSellPriceText", "_MartCantBuyText", "_MartBoughtText", "AnimateHallOfFame.String_NewHallOfFamer", "_HallOfFamePC.TimeFamer", "_HallOfFamePC.HOFMaster", # The MAIL block of data/text/common_2.asm, printed by engine/pokemon/ # mail.asm's MailboxPC and engine/pokemon/mon_menu.asm's MonMailAction. # Same shape as the Day-Care block: asm prints these, so no bytecode # points at them and the walker has to be seeded by name. "_EmptyMailboxText", "_MailClearedPutAwayText", "_MailPackFullText", "_MailMessageLostText", "_MailAlreadyHoldingItemText", "_MailEggText", "_MailMovedFromBoxText", "_MailLoseMessageText", "_MailDetachedText", "_MailNoSpaceText", "_MailAskSendToPCText", "_MailboxFullText", "_MailSentToPCText", "_PCMonHoldingMailText", "_PokemonRemoveMailText", } def embedded_symbols(symbols, pokemon_labels, song_labels=()): """Resolve REQUIRED_SYMBOLS + pic labels + Music_* song headers.""" names = set(REQUIRED_SYMBOLS) | set(pokemon_labels) | set(song_labels) for symbol_name in symbols.by_name: # Pokedex entries are split across four banks and the game derives the # bank arithmetically from the species id (radio.asm's rlca/maskbits # dance). Taking each entry's own symbol instead means the extractor # never has to reproduce that, and a repointed entry still resolves. if symbol_name.endswith("PokedexEntry") and "." not in symbol_name: names.add(symbol_name) if symbol_name.endswith("Frontpic") or symbol_name.endswith("Backpic"): # Skip qualified locals (e.g. "Foo.BarBackpic") -- those belong # to unrelated engine routines, not a Pic Pointers table entry. if "." not in symbol_name: names.add(symbol_name) missing = sorted(name for name in names if name not in symbols.by_name) if missing: raise ValueError("required symbols are missing: " + ", ".join(missing)) return { name: [symbols[name].bank, symbols[name].address] for name in sorted(names) } def generate(pokegold, symbols_path): symbols = SymbolTable(symbols_path) species = parse_const_block( os.path.join(pokegold, "constants/pokemon_constants.asm"), stop_at="NUM_POKEMON") species_order = [n or "UNUSED" for n in species[1:]] map_order, map_groups = extract_map_groups(pokegold) tilesets = [n for n in parse_const_block( os.path.join(pokegold, "constants/tileset_constants.asm"), stop_at="NUM_TILESETS") if n] moves = parse_const_block( os.path.join(pokegold, "constants/move_constants.asm"), stop_at="NUM_ATTACKS") move_order = [n or "UNUSED" for n in moves[1:]] # sprite_constants.asm stacks the ids of two different tables. $01.. # NUM_OVERWORLD_SPRITES are OverworldSprites rows (data/sprites/ # sprites.asm); then `const_next $80` restarts at SPRITE_POKEMON and the # names from there are SpriteMons rows (data/sprites/sprite_mons.asm), # which GetMonSprite turns into a mon's menu icon instead of a sheet. # Both blocks are read into one id-indexed list, because a byte in # wVariableSprites -- what a doll or a Sudowoodo stands on -- is a raw # sprite id and the port names it through this list. # # It stops at SPRITE_HO_OH on purpose. SPRITE_DAY_CARE_MON_1/_2 ($e0) and # the SPRITE_VARS block ($f0) past it are not sprites at all: the first # pair reads a breedmon species and the second is a slot INTO # wVariableSprites, so naming them here would let World:resolveSprite hand # a slot id back as though it were something that could be drawn. sprite_path = os.path.join(pokegold, "constants/sprite_constants.asm") sprites = parse_const_block(sprite_path, stop_at="NUM_POKEMON_SPRITES") sprite_order = [n or "UNUSED" for n in sprites[1:]] # The two DEFs the block itself ends its halves on. The $60..$7f hole # between them stays in sprite_order as UNUSED rows so the ids line up. num_overworld_sprites = len( parse_const_block(sprite_path, stop_at="NUM_OVERWORLD_SPRITES")) - 1 sprite_pokemon = sprites.index("SPRITE_UNOWN") if len(sprites) - sprite_pokemon != 35: raise SystemExit( f"{sprite_path}: expected 35 SpriteMons ids, got " f"{len(sprites) - sprite_pokemon}") types = extract_types(pokegold) # Environment (1-based), palette (0-based), fish-group (0-based) name # tables -- scraped by prefix so the mixed const_def blocks in # map_data_constants.asm cannot collide with each other. environments, palettes, fish_groups, spawns = [], [], [], [] path = os.path.join(pokegold, "constants/map_data_constants.asm") for _, line in read_asm(path): s = line.strip() m = re.match(r"const\s+(\w+)", s) if not m: continue name = m.group(1) if name in ( "TOWN", "ROUTE", "INDOOR", "CAVE", "ENVIRONMENT_5", "GATE", "DUNGEON", ) or name.startswith("ENVIRONMENT_"): environments.append(name) elif name.startswith("PALETTE_"): palettes.append(name) elif name.startswith("FISHGROUP_"): fish_groups.append(name) elif name.startswith("SPAWN_"): spawns.append(name) # Move effects (EFFECT_*) index the effect jumptable; the extractor turns # Moves' effect byte into one of these names so the battle engine can # switch on a readable id instead of a raw number. move_effects = parse_const_block( os.path.join(pokegold, "constants/move_effect_constants.asm")) move_effect_order = [n or "EFFECT_UNUSED" for n in move_effects] # Battle animations. constants/battle_anim_constants.asm stacks nine # unrelated const_def blocks in one file, so each list is scraped by its # own prefix -- parse_const_block would run them together. These name the # rows of the five tables the extractor reads (objects, functions, # framesets, OAM sets, GFX sheets) plus the BG-effect and palette enums a # disassembled animation refers to by number. battle_anim = os.path.join(pokegold, "constants/battle_anim_constants.asm") battle_anim_objects = parse_prefixed_consts(battle_anim, ("BATTLE_ANIM_OBJ_",)) battle_anim_funcs = parse_prefixed_consts(battle_anim, ("BATTLE_ANIM_FUNC_",)) battle_anim_framesets = parse_prefixed_consts( battle_anim, ("BATTLE_ANIM_FRAMESET_",)) battle_anim_oamsets = parse_prefixed_consts(battle_anim, ("BATTLE_ANIM_OAMSET_",)) # BATTLE_ANIM_GFX_* is the one block here that does not start at zero # (`const_def 1`, because AnimObjGFX row 0 is the empty AnimObj00GFX). # A placeholder in front keeps every list in this group indexable as # value + 1, so the extractor never has to remember which is which. battle_anim_gfx = ["BATTLE_ANIM_GFX_NONE"] + parse_prefixed_consts( battle_anim, ("BATTLE_ANIM_GFX_",)) battle_bg_effects = parse_prefixed_consts(battle_anim, ("BATTLE_BG_EFFECT_",)) # Two separate blocks, each starting at zero: a BG palette 4 and an OBJ # palette 4 are different colours, so they cannot share one list. battle_anim_bg_pals = parse_prefixed_consts(battle_anim, ("PAL_BATTLE_BG_",)) battle_anim_ob_pals = parse_prefixed_consts(battle_anim, ("PAL_BATTLE_OB_",)) item_data = os.path.join(pokegold, "constants/item_data_constants.asm") # Pocket ids are 0-based (ITEM, KEY_ITEM, BALL, TM_HM); the ITEM_* / # ITEMMENU_* / HELD_* blocks share the file, hence the prefix scrape. pocket_order = parse_prefixed_consts( item_data, (), exact=("ITEM", "KEY_ITEM", "BALL", "TM_HM")) item_menu_order = parse_prefixed_consts(item_data, ("ITEMMENU_",)) # The HELD_* block is sparse (const_skip / const_next), so the index of # this list must BE the ItemAttributes effect byte or every held effect # past HELD_CLEANSE_TAG lands on the wrong item. held_effect_order = parse_sparse_consts(item_data, ("HELD_",)) mon_data = os.path.join(pokegold, "constants/pokemon_data_constants.asm") growth_order = parse_prefixed_consts(mon_data, ("GROWTH_",)) egg_group_order = parse_prefixed_consts(mon_data, ("EGG_",)) evolve_order = parse_prefixed_consts(mon_data, ("EVOLVE_",)) # Map callbacks (constants/map_setup_constants.asm). That block is # `const_def 1`, so MAPCALLBACK_TILES is 1 and index 0 of this list is a # placeholder -- the same "value + 1" indexing BATTLE_ANIM_GFX_* uses. map_setup = os.path.join(pokegold, "constants/map_setup_constants.asm") map_callback_order = ["MAPCALLBACK_NONE"] + parse_prefixed_consts( map_setup, ("MAPCALLBACK_",)) # constants/script_constants.asm stacks a dozen unrelated blocks, so each # of these is found by the name its own block opens with. script_consts = os.path.join(pokegold, "constants/script_constants.asm") cmd_queue_order = parse_const_block_at( script_consts, "CMDQUEUE_NULL", stop_at="NUM_CMDQUEUE_TYPES") floor_order = parse_const_block_at( script_consts, "FLOOR_B4F", stop_at="NUM_FLOORS") deco_desc_order = parse_const_block_at( script_consts, "DECODESC_POSTER", stop_at="NUM_DECODESCS") # The phone. PHONE_* doubles as the PhoneContacts row index and carries # four const_skip holes that are real rows (the wrong-number fillers), so # the gaps have to survive into the list. phone_consts = os.path.join(pokegold, "constants/phone_constants.asm") # The holes are named rather than left null: the JSON decoder the importer # runs on this file collapses nulls out of an array, which would slide # every contact past a hole four rows down its own table. phone_contact_order = [ n or "PHONE_UNUSED" for n in parse_const_block_at( phone_consts, "PHONE_00", stop_at="NUM_PHONE_CONTACTS")] special_call_order = parse_const_block_at( phone_consts, "SPECIALCALL_NONE", stop_at="NUM_SPECIALCALLS") npc_trade = os.path.join(pokegold, "constants/npc_trade_constants.asm") trade_gender_order = parse_prefixed_consts(npc_trade, ("TRADE_GENDER_",)) trade_dialog_order = parse_prefixed_consts(npc_trade, ("TRADE_DIALOGSET_",)) trainer_classes, trainer_members = extract_trainer_classes(pokegold) trainer_types = parse_prefixed_consts( os.path.join(pokegold, "constants/trainer_data_constants.asm"), ("TRAINERTYPE_",)) landmarks = parse_const_block( os.path.join(pokegold, "constants/landmark_constants.asm"), stop_at="NUM_LANDMARKS") landmark_order = [n or "UNUSED" for n in landmarks] icons = parse_prefixed_consts( os.path.join(pokegold, "constants/icon_constants.asm"), ("ICON_",)) tree_sets = parse_prefixed_consts(mon_data, ("TREEMON_SET_",)) std_scripts = extract_std_scripts(pokegold) specials = extract_specials(pokegold) assets = pokemon_assets(pokegold, species_order, symbols) pokemon_labels = [] for asset in assets.values(): if asset["frontLabel"]: pokemon_labels.append(asset["frontLabel"]) if asset["backLabel"]: pokemon_labels.append(asset["backLabel"]) songs = music_order(pokegold) sfx = sfx_order(pokegold) # Index 0 is NO_ITEM, so the parsed list is already 1-based on item id. # ItemNames only has rows for the first `item_name_count` of them; the TM # and HM items past that are named from their TM number instead. item_order, item_name_count = extract_items(pokegold) data = { "format": 3, "generation": 2, "romSha1": CANONICAL_GOLD_SHA1, "constants": { "source": "pret/pokegold constants/*.asm", "speciesOrder": species_order, "mapGroups": [map_groups[name] for name in map_order], "mapOrder": map_order, "tilesetOrder": tilesets, "moveOrder": move_order, "types": types, "spriteOrder": sprite_order, # NUM_OVERWORLD_SPRITES: how many leading spriteOrder rows are real # OverworldSprites rows. Everything from spritePokemon on names a # SpriteMons row and has no sheet of its own. "numOverworldSprites": num_overworld_sprites, # SPRITE_POKEMON ($80): the first mon-icon id, and the base # GetMonSprite subtracts to index SpriteMons. "spritePokemon": sprite_pokemon, "environmentOrder": environments, "paletteOrder": palettes, "fishGroupOrder": fish_groups, # SPAWN_* order; SPAWN_HOME is a new game's start (intro_menu.asm) "spawnOrder": spawns, # MUSIC_* id → Music_* label (audio/music_pointers.asm order) "musicOrder": songs, # SFX_* id → Sfx_* label (audio/sfx_pointers.asm order) "sfxOrder": sfx, # Item id → constant name (MASTER_BALL=1 …); used by extractItems "itemOrder": item_order, # How many leading item ids ItemNames has rows for; TM/HM items # past this point carry no name of their own. "itemNameCount": item_name_count, # Battle / pokemon runtime orders "moveEffectOrder": move_effect_order, "growthRateOrder": growth_order, "eggGroupOrder": egg_group_order, "evolveMethodOrder": evolve_order, # Bag pockets + item attribute enums (data/items/attributes.asm). # `property` is a bitfield, not an enum (shift_const CANT_SELECT # = bit 6, CANT_TOSS = bit 7), so the extractor decodes it # directly rather than naming it from a list here. "pocketOrder": pocket_order, "itemMenuOrder": item_menu_order, "heldEffectOrder": held_effect_order, # Trainers: class id → name, class → its own trainer ids "trainerClassOrder": trainer_classes, "trainerClassMembers": trainer_members, "trainerTypeOrder": trainer_types, # Pokegear town map + wild encounter side tables "landmarkOrder": landmark_order, "iconOrder": icons, "treeMonSetOrder": tree_sets, # callstd / jumpstd id → StdScripts label "stdScriptOrder": std_scripts, # Map script header: callback type id → MAPCALLBACK_* name (1-based, # index 0 is a placeholder) and the cmdqueue entry's own type enum. "mapCallbackOrder": map_callback_order, "cmdQueueOrder": cmd_queue_order, # `elevator` floor ids and `describedecoration`'s five arms "floorOrder": floor_order, "decoDescOrder": deco_desc_order, # PhoneContacts row → PHONE_* name (holes kept: they are rows), # and SpecialPhoneCallList row → SPECIALCALL_* name "phoneContactOrder": phone_contact_order, "specialCallOrder": special_call_order, # `trade` row enums (data/events/npc_trades.asm) "tradeGenderOrder": trade_gender_order, "tradeDialogOrder": trade_dialog_order, # `special` id → SpecialsPointers label "specialOrder": specials, # Battle animations: the row names of BattleAnimObjects, # BattleAnimFrameData, BattleAnimOAMData and AnimObjGFX, plus the # BG-effect and OBJ/BG palette enums an animation names by number. "battleAnimObjectOrder": battle_anim_objects, "battleAnimFuncOrder": battle_anim_funcs, "battleAnimFramesetOrder": battle_anim_framesets, "battleAnimOamsetOrder": battle_anim_oamsets, "battleAnimGfxOrder": battle_anim_gfx, "battleBgEffectOrder": battle_bg_effects, "battleAnimBgPaletteOrder": battle_anim_bg_pals, "battleAnimObPaletteOrder": battle_anim_ob_pals, }, "charmap": charmap(pokegold), "fontCharmap": font_extract.parse_charmap(pokegold), "pokemonAssets": assets, # Per-map metadata (group/map/width/height/name). RomExtractorGen2 # resolves ROM headers via MapGroupPointers + these ids. "maps": {name: map_groups[name] for name in map_order}, "tilesets": {name: {} for name in tilesets}, } data["symbols"] = embedded_symbols(symbols, pokemon_labels, songs) return data def find_pokegold(): for candidate in DEFAULT_POKEGOLD_CANDIDATES: if os.path.isfile(os.path.join(candidate, "main.asm")): return candidate return DEFAULT_POKEGOLD_CANDIDATES[-1] def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--pokegold", default=find_pokegold()) parser.add_argument("--symbols", default=DEFAULT_SYMBOLS) parser.add_argument( "--out", default=os.path.join( os.path.dirname(__file__), "rom_manifest_gold.json")) args = parser.parse_args() pokegold = os.path.abspath(args.pokegold) if not os.path.isfile(os.path.join(pokegold, "main.asm")): raise SystemExit(f"{pokegold} is not a pokegold checkout") data = generate(pokegold, os.path.abspath(args.symbols)) with open(args.out, "w", encoding="utf-8", newline="\n") as f: json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True) f.write("\n") print(f"wrote {args.out}") if __name__ == "__main__": main()