mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
1fe35335a5
Yellow's IsSurfingPikachuInParty swaps the player's overworld sheet to a Pikachu-on-a-surfboard when the party mon that knows SURF is a Pikachu. The recomp was missing both halves of this: the sheet was never extracted, and the engine had no seam for the swap. Extraction: SurfingPikachuSprite (gfx/sprites/surfing_pikachu.2bpp) loads outside SpriteSheetPointerTable via LoadSurfingPlayerSpriteGraphics2, the same bypass RedBikeSprite uses. Added the symbol to the Yellow manifest and a parallel extract in RomExtractor / build_rom_data / extract/sprites, minting SPRITE_SURFING_PIKACHU. PaletteFX colors it (player OBP palette, same as the bike). Engine: new field.playerSprites.surfPikachu (default SPRITE_SURFING_PIKACHU, guarded so before extraction the ride keeps the Seel). Player.new caches surfPikachuSprite; pose() picks it when surfing and the SURF-mon is a Pikachu. New OverworldState:syncSurfingPikachu derives the flag from partyKnows at every surf-state toggle (mount, dismount, fly, teleport, blackout, forced-surf tile, boot-restore). Runtime-only, re-derived at load so a party change between save and load is honored. Lane B: RFC at docs/rfcs/0001-surfing-pikachu-sprite.md. Backward-compatible — existing mods see no change (surf still defaults to SPRITE_SEEL; surfPikachu only resolves on a Yellow import after regeneration). Parity tests in tests/parity_surfing_pikachu_sprite.lua (12/12) and tests/mod_world_tests.lua (19229/19229 with the new boot-seed checks). tests/parity_cinnabar_east_surf.lua (24/24) unchanged. Regeneration required: re-run make_yellow_manifest.py against a pret/pokeyellow checkout, then re-import the Yellow ROM.
534 lines
21 KiB
Python
Executable File
534 lines
21 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Derive the Pokemon Yellow import manifest from the shipped Red manifest.
|
|
|
|
Yellow is a separate pret tree (pokeyellow), not a `_YELLOW` flip of pokered.
|
|
Most of the ~3268 Red manifest symbols still exist under the same names in
|
|
pokeyellow.sym (~3123 with shifted addresses). The remainder need aliases,
|
|
synthetic addresses (Mew in BaseStats), or omission (FightIntro* -- Yellow's
|
|
intro movie is different; RomExtractor must skip those).
|
|
|
|
Map/object/sprite/tileset/text metadata diverge enough that those sections are
|
|
rebuilt from pokeyellow source (same helpers as make_rom_manifest.py), while
|
|
ROM-address tables and other Red-shaped sections keep the derive-and-remap
|
|
path.
|
|
|
|
Usage mirrors make_blue_manifest.py: deep-copy Red, remap symbols, override
|
|
version-gated field bits, write tools/rom_manifest_yellow.json.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import copy
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from extract import constants, field, util # noqa: E402
|
|
from make_rom_manifest import ( # noqa: E402
|
|
_music_label,
|
|
map_metadata,
|
|
simple_constants,
|
|
sprite_metadata,
|
|
text_metadata,
|
|
tileset_metadata,
|
|
)
|
|
import re # noqa: E402
|
|
from rom_data import SymbolTable # noqa: E402
|
|
from yellow_symbol_aliases import ( # noqa: E402
|
|
FAN_CLUB_ID_RENAMES,
|
|
GAME_CORNER_ID_RENAMES,
|
|
MAP_CONST_RENAMES,
|
|
MAP_LABEL_RENAMES,
|
|
OMIT_INTRO_SYMBOLS,
|
|
SYMBOL_ALIASES,
|
|
)
|
|
|
|
try:
|
|
from rom_data import CANONICAL_YELLOW_SHA1
|
|
except ImportError: # pragma: no cover -- constant lands with GameVersion work
|
|
CANONICAL_YELLOW_SHA1 = "cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1"
|
|
|
|
DEV = "/Users/bryanbassett/Documents/development"
|
|
DEFAULT_RED = os.path.join(os.path.dirname(__file__), "rom_manifest.json")
|
|
DEFAULT_OUT = os.path.join(os.path.dirname(__file__), "rom_manifest_yellow.json")
|
|
DEFAULT_POKEYELLOW = os.path.join(DEV, "pokeyellow")
|
|
DEFAULT_SYMBOLS = os.path.join(DEV, "pokeyellow-symbols/pokeyellow.sym")
|
|
|
|
BASE_STATS_ENTRY_SIZE = 28
|
|
MEW_DEX_NUMBER = 151
|
|
|
|
# Yellow-only symbols Red never referenced. Title / intro / CGB tables must
|
|
# be injected so RomExtractor can rip them (make_yellow_manifest only remaps
|
|
# Red's name set by default).
|
|
YELLOW_EXTRA_SYMBOLS = (
|
|
"TitlePikachuBGGraphics",
|
|
"TitlePikachuOBGraphics",
|
|
"TitleScreenPikachuTilemap",
|
|
"TitleScreenPikaBubbleTilemap",
|
|
"TitleScreenPokemonLogoTilemap",
|
|
"PokemonLogoCornerGraphics",
|
|
"YellowIntroGraphics1",
|
|
"YellowIntroGraphics2",
|
|
"YellowIntroCloudGFX",
|
|
"PikachuCriesPointerTable",
|
|
"CGBBasePalettes",
|
|
# Jessie & James share the ROCKET trainer class but battle behind their
|
|
# own pic (home/trainers2.asm IsFightingJessieJames) (#439)
|
|
"JessieJamesPic",
|
|
# the five Pikachu-only emotion bubbles (emotion_bubbles.asm)
|
|
"SkullEmote",
|
|
"HeartEmote",
|
|
"BoltEmote",
|
|
"ZzzEmote",
|
|
"FishEmote",
|
|
# Surfing Pikachu minigame sheets (gfx/surfing_pikachu.asm)
|
|
"SurfingPikachu1Graphics1",
|
|
"SurfingPikachu1Graphics2",
|
|
"SurfingPikachu1Graphics3",
|
|
# Oak's own battle back pic. LoadPlayerBackPic (engine/battle/core.asm)
|
|
# picks OldManPicBack for BATTLE_TYPE_OLD_MAN but ProfOakPicBack for
|
|
# BATTLE_TYPE_PIKACHU, the Pallet Town catch scene (#557).
|
|
"ProfOakPicBack",
|
|
# Base frames for the framed portrait TalkToPikachu draws, one per
|
|
# PikaPicAnimScript (data/pikachu/pikachu_pic_animation.asm). These are
|
|
# raw address labels because the pikapic blobs carry no named symbols;
|
|
# RomExtractor's PIKAPIC_BASE table indexes them positionally, so the
|
|
# order here is not load bearing but every entry must resolve (#561).
|
|
"Pic_e4000", "Pic_e411c", "Pic_e4272", "Pic_e4383", "Pic_e458b",
|
|
"Pic_e467b", "Pic_e476e", "Pic_e49d1", "Pic_e4b39", "Pic_e4c3e",
|
|
"Pic_e5000", "Pic_e523f", "Pic_e548e", "Pic_e56d1", "Pic_e5924",
|
|
"Pic_e5b7d", "Pic_e5ddd", "GFX_e6020", "Pic_e6340", "Pic_e6587",
|
|
"Pic_e67d6", "GFX_e6e6f", "GFX_e718f", "GFX_e74af", "Pic_e77cf",
|
|
"Pic_f0abf", "Pic_f0cf4",
|
|
# Yellow-only overworld player surf sprite, loaded outside
|
|
# SpriteSheetPointerTable (LoadSurfingPlayerSpriteGraphics2) --
|
|
# needs its own extract like RedBikeSprite. (RFC 0001)
|
|
"SurfingPikachuSprite",
|
|
)
|
|
|
|
# Yellow-only dialogue whose bank labels carry no leading underscore, so
|
|
# the text-label scan misses them (scripts/CeruleanMelaniesHouse.asm --
|
|
# the Bulbasaur gift and the pet flavor lines).
|
|
YELLOW_EXTRA_TEXT_LABELS = (
|
|
"MelanieText1", "MelanieText2", "MelanieText3",
|
|
"MelanieText4", "MelanieText5",
|
|
"MelanieBulbasaurText", "MelanieOddishText", "MelanieSandshrewText",
|
|
)
|
|
|
|
YELLOW_EXTRA_PALETTES = (
|
|
"PIKACHUS_BEACH",
|
|
"PIKACHU_PORTRAIT",
|
|
"PIKACHUS_BEACH_TITLE",
|
|
)
|
|
|
|
|
|
def _rename_keys(obj, renames):
|
|
"""Recursively rename dict keys (and rewrite matching string values)."""
|
|
if isinstance(obj, dict):
|
|
out = {}
|
|
for key, value in obj.items():
|
|
new_key = renames.get(key, key)
|
|
out[new_key] = _rename_keys(value, renames)
|
|
return out
|
|
if isinstance(obj, list):
|
|
return [_rename_keys(item, renames) for item in obj]
|
|
if isinstance(obj, str):
|
|
return renames.get(obj, obj)
|
|
return obj
|
|
|
|
|
|
def _replace_strings(obj, renames):
|
|
"""Recursively replace string values (and list entries) via renames."""
|
|
if isinstance(obj, dict):
|
|
return {k: _replace_strings(v, renames) for k, v in obj.items()}
|
|
if isinstance(obj, list):
|
|
return [_replace_strings(item, renames) for item in obj]
|
|
if isinstance(obj, str):
|
|
return renames.get(obj, obj)
|
|
return obj
|
|
|
|
|
|
def _drop_strings(obj, dropped):
|
|
"""Remove dropped names from lists and as dict keys / string values."""
|
|
if isinstance(obj, dict):
|
|
out = {}
|
|
for key, value in obj.items():
|
|
if key in dropped:
|
|
continue
|
|
if isinstance(value, str) and value in dropped:
|
|
continue
|
|
out[key] = _drop_strings(value, dropped)
|
|
return out
|
|
if isinstance(obj, list):
|
|
result = []
|
|
for item in obj:
|
|
if isinstance(item, str) and item in dropped:
|
|
continue
|
|
result.append(_drop_strings(item, dropped))
|
|
return result
|
|
return obj
|
|
|
|
|
|
def _resolve_mew_base_stats(yellow_symbols):
|
|
base = yellow_symbols.by_name.get("BaseStats")
|
|
if base is None:
|
|
raise SystemExit("pokeyellow.sym missing BaseStats (needed for Mew)")
|
|
return [base.bank, base.address + (MEW_DEX_NUMBER - 1) * BASE_STATS_ENTRY_SIZE]
|
|
|
|
|
|
def _rebuild_map_songs(pokeyellow, map_order, music_headers):
|
|
"""Zip pokeyellow data/maps/songs.asm onto the Yellow mapOrder."""
|
|
map_song_consts = []
|
|
path = os.path.join(pokeyellow, "data/maps/songs.asm")
|
|
for _, line in util.read_asm(path):
|
|
match = re.match(r"db\s+(MUSIC_\w+),", line.strip())
|
|
if match:
|
|
map_song_consts.append(match.group(1))
|
|
if len(map_song_consts) != len(map_order):
|
|
raise SystemExit(
|
|
f"Yellow songs.asm has {len(map_song_consts)} entries but "
|
|
f"mapOrder has {len(map_order)}")
|
|
|
|
out = {}
|
|
missing = []
|
|
for map_name, const_name in zip(map_order, map_song_consts):
|
|
label = _music_label(const_name, music_headers)
|
|
if label not in music_headers:
|
|
missing.append(f"{map_name}:{const_name}->{label}")
|
|
continue
|
|
out[map_name] = label
|
|
if missing:
|
|
raise SystemExit(
|
|
"Yellow map songs could not resolve music headers: "
|
|
+ ", ".join(missing[:12])
|
|
+ (" ..." if len(missing) > 12 else ""))
|
|
return out
|
|
|
|
|
|
def _rebuild_yellow_sourced(yellow, pokeyellow):
|
|
"""Replace sections that Yellow authors differently from Red."""
|
|
map_order, map_dims = constants.extract_map_constants(pokeyellow)
|
|
tileset_order = [
|
|
n for n in simple_constants(
|
|
pokeyellow, "constants/tileset_constants.asm") if n
|
|
]
|
|
sprite_consts = simple_constants(
|
|
pokeyellow, "constants/sprite_constants.asm")
|
|
sprite_order = [n or "UNUSED" for n in sprite_consts[1:]]
|
|
tile_animations = [
|
|
name or "UNUSED"
|
|
for name in simple_constants(
|
|
pokeyellow, "constants/map_data_constants.asm")
|
|
if name and name.startswith("TILEANIM_")
|
|
]
|
|
|
|
yellow["constants"]["mapOrder"] = map_order
|
|
yellow["constants"]["maps"] = map_dims
|
|
yellow["constants"]["tilesetOrder"] = tileset_order
|
|
yellow["constants"]["spriteOrder"] = sprite_order
|
|
|
|
yellow["maps"] = map_metadata(pokeyellow, map_dims)
|
|
yellow["tilesets"] = tileset_metadata(pokeyellow, tileset_order)
|
|
yellow["sprites"] = sprite_metadata(pokeyellow, sprite_order)
|
|
yellow["text"] = text_metadata(pokeyellow)
|
|
yellow["tileAnimations"] = tile_animations
|
|
|
|
yellow["audio"]["mapSongs"] = _rebuild_map_songs(
|
|
pokeyellow, map_order, yellow["audio"]["musicHeaders"])
|
|
# Red's positional header mapping lands Yellow's intro song
|
|
# (pokeyellow Music_YellowIntro, 1f:4294) under the name
|
|
# Music_IntroBattle; expose it under its own name too so the Yellow
|
|
# intro movie state can ask for the right label.
|
|
headers = yellow["audio"]["musicHeaders"]
|
|
if "Music_YellowIntro" not in headers and "Music_IntroBattle" in headers:
|
|
headers["Music_YellowIntro"] = dict(headers["Music_IntroBattle"])
|
|
return {
|
|
"mapCount": len(map_order),
|
|
"mapMeta": len(yellow["maps"]),
|
|
"tilesets": len(tileset_order),
|
|
"sprites": len(sprite_order),
|
|
"textLabels": len(yellow["text"]["labels"]),
|
|
}
|
|
|
|
|
|
def _remap_audio(yellow, yellow_symbols):
|
|
"""Re-anchor the Red-copied audio section on pokeyellow.sym (#522).
|
|
|
|
Yellow's music bank $1f shifts after 1f:4294: Music_YellowIntro is a
|
|
3-channel header (pokeyellow audio/headers/musicheaders3.asm) where
|
|
Red's Music_IntroBattle had 4, so Red's addresses for every later
|
|
header land on a channel-2 row (one voice, and no tempo command --
|
|
Viridian Forest slow and hollow). Yellow also has a single
|
|
wave-sample table at Audio1_WavePointers (audio.asm "Music 1"), not
|
|
Red's per-engine 0x4373 copies, and CryData moved to 0e:5462. Names
|
|
absent from the sym (Music_IntroBattle, which Yellow only knows as
|
|
Music_YellowIntro) keep their positional mapping.
|
|
"""
|
|
for name, header in yellow["audio"]["musicHeaders"].items():
|
|
symbol = yellow_symbols.by_name.get(name)
|
|
if symbol is not None:
|
|
header["bank"] = symbol.bank
|
|
header["address"] = symbol.address
|
|
wave = yellow_symbols.by_name.get("Audio1_WavePointers.wave0")
|
|
if wave is None:
|
|
raise SystemExit("pokeyellow.sym missing Audio1_WavePointers.wave0")
|
|
for engine in yellow["audio"]["waveBanks"]:
|
|
yellow["audio"]["waveBanks"][engine] = {
|
|
"bank": wave.bank, "address": wave.address,
|
|
}
|
|
cry = yellow_symbols.by_name.get("CryData")
|
|
if cry is None:
|
|
raise SystemExit("pokeyellow.sym missing CryData")
|
|
yellow["audio"]["cryData"] = {"bank": cry.bank, "address": cry.address}
|
|
|
|
|
|
def derive(red, pokeyellow, symbols_path):
|
|
"""Return the Yellow manifest derived from the Red manifest dict."""
|
|
yellow = copy.deepcopy(red)
|
|
yellow["romSha1"] = CANONICAL_YELLOW_SHA1
|
|
yellow_symbols = SymbolTable(symbols_path)
|
|
|
|
omit = set(OMIT_INTRO_SYMBOLS)
|
|
dropped = {name for name, alias in SYMBOL_ALIASES.items() if alias is None}
|
|
dropped |= omit
|
|
symbol_renames = {
|
|
name: alias for name, alias in SYMBOL_ALIASES.items() if alias
|
|
}
|
|
|
|
# Structural renames for Red-shaped leftovers (field townMap, etc.) before
|
|
# Yellow-sourced sections overwrite maps/text/sprites/tilesets.
|
|
structural = {}
|
|
structural.update(MAP_CONST_RENAMES)
|
|
structural.update(MAP_LABEL_RENAMES)
|
|
structural.update(GAME_CORNER_ID_RENAMES)
|
|
structural.update(FAN_CLUB_ID_RENAMES)
|
|
structural.update(symbol_renames)
|
|
yellow = _rename_keys(yellow, structural)
|
|
yellow = _replace_strings(yellow, structural)
|
|
yellow = _drop_strings(yellow, dropped)
|
|
|
|
rebuilt = _rebuild_yellow_sourced(yellow, pokeyellow)
|
|
_remap_audio(yellow, yellow_symbols) # #522
|
|
|
|
for label in YELLOW_EXTRA_TEXT_LABELS:
|
|
if label not in yellow["text"]["labels"]:
|
|
yellow["text"]["labels"].append(label)
|
|
|
|
# Rebuild symbols from Red's name set with Yellow addresses / aliases,
|
|
# then ensure every Yellow-sourced label/header is present.
|
|
resolved = {}
|
|
missing = []
|
|
alias_hits = 0
|
|
for name in red["symbols"]:
|
|
if name in omit or name in dropped:
|
|
continue
|
|
if name == "MewBaseStats":
|
|
resolved["MewBaseStats"] = _resolve_mew_base_stats(yellow_symbols)
|
|
alias_hits += 1
|
|
continue
|
|
target = symbol_renames.get(name, name)
|
|
if name in symbol_renames:
|
|
alias_hits += 1
|
|
# Structural map-header rename may already have changed the key.
|
|
target = MAP_LABEL_RENAMES.get(target, target)
|
|
if target.endswith("_h"):
|
|
base = target[:-2]
|
|
target = MAP_LABEL_RENAMES.get(base, base) + "_h" \
|
|
if base in MAP_LABEL_RENAMES else target
|
|
symbol = yellow_symbols.by_name.get(target)
|
|
if symbol is None:
|
|
# Drop Red-only symbols that Yellow-sourced sections no longer need.
|
|
continue
|
|
resolved[target] = [symbol.bank, symbol.address]
|
|
|
|
print(
|
|
"warning: omitting Yellow-incompatible intro symbols "
|
|
f"(RomExtractor must skip): {', '.join(OMIT_INTRO_SYMBOLS)}"
|
|
)
|
|
|
|
for label in yellow["text"]["labels"]:
|
|
if label in resolved:
|
|
continue
|
|
symbol = yellow_symbols.by_name.get(label)
|
|
if symbol is None:
|
|
missing.append(label)
|
|
continue
|
|
resolved[label] = [symbol.bank, symbol.address]
|
|
for spec in yellow["maps"].values():
|
|
header = spec["label"] + "_h"
|
|
if header in resolved:
|
|
continue
|
|
symbol = yellow_symbols.by_name.get(header)
|
|
if symbol is None:
|
|
missing.append(header)
|
|
continue
|
|
resolved[header] = [symbol.bank, symbol.address]
|
|
|
|
# Pointer asm labels (ViridianPokeCenterChanseyText etc.) also need symbols
|
|
# when extract_text resolves through them.
|
|
for pointers in yellow["text"]["pointers"].values():
|
|
for spec in pointers.values():
|
|
for key in ("label", "text"):
|
|
name = spec.get(key)
|
|
if not name or name in resolved:
|
|
continue
|
|
symbol = yellow_symbols.by_name.get(name)
|
|
if symbol is not None:
|
|
resolved[name] = [symbol.bank, symbol.address]
|
|
|
|
yellow["symbols"] = resolved
|
|
|
|
for name in YELLOW_EXTRA_SYMBOLS:
|
|
symbol = yellow_symbols.by_name.get(name)
|
|
if symbol is None:
|
|
raise SystemExit(f"pokeyellow.sym missing Yellow extra symbol {name}")
|
|
yellow["symbols"][name] = [symbol.bank, symbol.address]
|
|
|
|
# Yellow-only songs live in music bank $20, which Red's engine never
|
|
# had; ship the bank in the audio pack and add their headers.
|
|
yellow["audio"]["programBanks"] = [2, 8, 31, 32]
|
|
for name in ("Music_MeetJessieJames", "Music_SurfingPikachu",
|
|
"Music_GBPrinter"):
|
|
symbol = yellow_symbols.by_name.get(name)
|
|
if symbol is None:
|
|
raise SystemExit(f"pokeyellow.sym missing Yellow song {name}")
|
|
yellow["audio"]["musicHeaders"][name] = {
|
|
"bank": symbol.bank, "address": symbol.address, "engine": 3,
|
|
}
|
|
|
|
# SuperPalettes grows by three Yellow-only SGB entries after GAMEFREAK.
|
|
order = list(yellow.get("paletteOrder") or [])
|
|
for name in YELLOW_EXTRA_PALETTES:
|
|
if name not in order:
|
|
order.append(name)
|
|
yellow["paletteOrder"] = order
|
|
|
|
# Yellow appends ICON_PIKACHU ($a) after Red's ten party icons
|
|
# (constants/icon_constants.asm); MonPartyData nybble $a resolves to
|
|
# it, drawn from the overworld PikachuSprite sheet.
|
|
icons = list(yellow.get("iconOrder") or [])
|
|
if "PIKACHU" not in icons:
|
|
icons.append("PIKACHU")
|
|
yellow["iconOrder"] = icons
|
|
|
|
still_missing = []
|
|
for name in yellow["symbols"]:
|
|
if name == "MewBaseStats":
|
|
continue
|
|
if name not in yellow_symbols.by_name:
|
|
still_missing.append(name)
|
|
if still_missing or missing:
|
|
raise SystemExit(
|
|
"pokeyellow.sym is missing symbols the manifest needs: "
|
|
+ ", ".join(sorted(set(still_missing + missing))[:20])
|
|
+ (" ..." if len(set(still_missing + missing)) > 20 else ""))
|
|
|
|
# Version-gated field bits from pokeyellow.
|
|
saved = util.ASM_DEFINES
|
|
util.ASM_DEFINES = set()
|
|
try:
|
|
yellow["field"]["presetNames"] = field.parse_preset_names(pokeyellow)
|
|
try:
|
|
yellow["field"]["credits"] = field.parse_credits(pokeyellow)
|
|
except SystemExit as exc:
|
|
print(f"warning: parse_credits failed ({exc}); keeping Red credits")
|
|
# TODO: hand-author a Yellow credits banner if pret layout drifts.
|
|
yellow["field"]["trades"] = field.parse_trades(pokeyellow)
|
|
finally:
|
|
util.ASM_DEFINES = saved
|
|
|
|
presets = yellow["field"]["presetNames"]
|
|
if "YELLOW" not in presets["player"] or "BLUE" not in presets["rival"]:
|
|
raise SystemExit(
|
|
f"Yellow preset-name parse unexpected: {presets!r}")
|
|
|
|
# Fixed Pikachu title (no TitleMons cycle); extractor fills image paths.
|
|
title = yellow["field"].setdefault("title", {})
|
|
title["layout"] = "yellow_pikachu"
|
|
title["cycleSpecies"] = []
|
|
title["music"] = title.get("music") or "Music_TitleScreen"
|
|
title["pikachuBg"] = {
|
|
"path": "assets/generated/title/pikachu_bg.png",
|
|
"width": 128, "height": 32,
|
|
}
|
|
title["pikachuOb"] = {
|
|
"path": "assets/generated/title/pikachu_ob.png",
|
|
"width": 96, "height": 8,
|
|
}
|
|
title["pikachu"] = {
|
|
"path": "assets/generated/title/pikachu.png",
|
|
"width": 96, "height": 72,
|
|
}
|
|
title["pikaBubble"] = {
|
|
"path": "assets/generated/title/pika_bubble.png",
|
|
"width": 56, "height": 32,
|
|
}
|
|
|
|
# Yellow's emote sheet grows the five Pikachu-only bubbles
|
|
# (engine/overworld/emotion_bubbles.asm Skull/Heart/Bolt/Zzz/FishEmote,
|
|
# constants/script_constants.asm order); RomExtractor rips whatever
|
|
# this bubble list names.
|
|
yellow_bubbles = ["EXCLAMATION_BUBBLE", "QUESTION_BUBBLE", "SMILE_BUBBLE",
|
|
"SKULL_BUBBLE", "HEART_BUBBLE", "BOLT_BUBBLE",
|
|
"ZZZ_BUBBLE", "FISH_BUBBLE"]
|
|
yellow["field"]["emotionBubbles"] = {
|
|
"path": "assets/generated/emotes.png",
|
|
"width": 16 * len(yellow_bubbles), "height": 16,
|
|
"bubbles": [{"name": name, "x": i * 16, "y": 0, "w": 16, "h": 16}
|
|
for i, name in enumerate(yellow_bubbles)],
|
|
}
|
|
|
|
# Ensure Melanie / Summer Beach town-map entries exist after rebuild.
|
|
locations = yellow["field"]["townMap"]["locations"]
|
|
if "CERULEAN_MELANIES_HOUSE" not in locations \
|
|
and "CERULEAN_CITY" in locations:
|
|
locations["CERULEAN_MELANIES_HOUSE"] = dict(locations["CERULEAN_CITY"])
|
|
if "SUMMER_BEACH_HOUSE" not in locations and "ROUTE_19" in locations:
|
|
locations["SUMMER_BEACH_HOUSE"] = dict(locations["ROUTE_19"])
|
|
|
|
meta = {
|
|
"aliasCount": alias_hits,
|
|
"omittedIntro": list(OMIT_INTRO_SYMBOLS),
|
|
"droppedSymbols": sorted(dropped - omit),
|
|
"rebuilt": rebuilt,
|
|
}
|
|
return yellow, meta
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--red", default=DEFAULT_RED,
|
|
help="shipped Red manifest to derive from")
|
|
parser.add_argument("--pokeyellow", default=DEFAULT_POKEYELLOW,
|
|
help="pokeyellow source checkout")
|
|
parser.add_argument("--symbols", default=DEFAULT_SYMBOLS,
|
|
help="pokeyellow.sym symbol file")
|
|
parser.add_argument("--out", default=DEFAULT_OUT)
|
|
args = parser.parse_args()
|
|
|
|
pokeyellow = os.path.abspath(args.pokeyellow)
|
|
if not os.path.isfile(os.path.join(pokeyellow, "main.asm")):
|
|
raise SystemExit(f"{pokeyellow} is not a pokeyellow checkout")
|
|
with open(args.red, encoding="utf-8") as f:
|
|
red = json.load(f)
|
|
|
|
yellow, meta = derive(red, pokeyellow, os.path.abspath(args.symbols))
|
|
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
|
|
json.dump(yellow, f, ensure_ascii=False, indent=2, sort_keys=True)
|
|
f.write("\n")
|
|
|
|
print(f"wrote {args.out}")
|
|
print(f"symbols: {len(yellow['symbols'])}")
|
|
print(f"aliases applied: {meta['aliasCount']}")
|
|
print(f"omitted intro: {meta['omittedIntro']}")
|
|
print(f"dropped: {len(meta['droppedSymbols'])} "
|
|
f"({', '.join(meta['droppedSymbols'][:8])}...)")
|
|
print(f"rebuilt: {meta['rebuilt']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|