Add Pokemon Crystal as a sixth supported version

Crystal boots from a user-supplied ROM, imports a full cache and is playable:
copyright, the Crystal intro movie, the animated title, gender select, Oak,
and out into Johto. 122 of the cart's 169 script specials are implemented.

Import and data
- tools/make_crystal_manifest.py derives the manifest by importing
  make_gold_manifest as a library, with three additive keyword seams. Gold and
  Silver still regenerate byte-identical, which is the standing requirement for
  touching that generator.
- crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta:
  Crystal renames the credits mons, splits the trainer card, Pokegear and
  pack-pal blocks by gender, and replaces the intro and title outright.
- Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the
  badge block sits one higher) and unownCharmap (the main charmap parser stops
  at the first newcharmap so the two cannot contaminate each other).

Extractor
- RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK
  0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole,
  seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim
  steps with per-row degrade, and the column-major trainer card portraits.
- New: animated front sprites (frames, bitmasks, play and idle scripts), the
  Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal
  intro and title via src/import/CrystalMovie.lua.

Engine
- GameVersion gains engine(id) and fixes(id). Gold and Silver keep their
  original bugs where the bug is not hardware dependent; Crystal gets the fixes
  Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the
  Reflect and Light Screen defence overflow.
- Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins
  of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower
  including the wInBattleTowerBattle badge-boost guard.
- Kris and the gender flag, animated fronts in battle and the summary screen,
  and mon caught data.

Verification
- Every extracted asset is pixel-compared against pret's own source PNGs.
- Gold caches are byte-identical before and after, file for file.
- New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
This commit is contained in:
bryanthaboi
2026-08-23 12:10:28 -04:00
parent e88f2ef060
commit ca4d3d283c
141 changed files with 43306 additions and 583 deletions
+25 -3
View File
@@ -34,6 +34,28 @@ DATASETS = (
"text", "field", "battle_anims",
)
# Sound is decoded by the Lua importer (src/import/RomExtractor.lua), not here,
# so --clean must step around it rather than delete what it cannot rebuild.
UNOWNED_DATA = ("audio.lua",)
UNOWNED_ASSETS = ("audio",)
def clean_generated(out_dir, assets_dir):
"""Empty the generated dirs, keeping artifacts this tool never writes."""
kept = []
for path, spared in ((out_dir, UNOWNED_DATA), (assets_dir, UNOWNED_ASSETS)):
if not os.path.isdir(path):
continue
for name in os.listdir(path):
target = os.path.join(path, name)
if name in spared:
kept.append(target)
elif os.path.isdir(target):
shutil.rmtree(target)
else:
os.remove(target)
return kept
_TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
VERSION_MANIFESTS = {
"red": os.path.join(_TOOLS_DIR, "rom_manifest.json"),
@@ -2207,9 +2229,9 @@ def main(argv=None):
out_dir = args.out or prefix + os.path.join("data", "generated")
assets_dir = args.assets or prefix + os.path.join("assets", "generated")
if args.clean:
for path in (out_dir, assets_dir):
if os.path.isdir(path):
shutil.rmtree(path)
kept = clean_generated(out_dir, assets_dir)
for path in kept:
print(f"kept {path} (not produced by this tool)")
os.makedirs(out_dir, exist_ok=True)
os.makedirs(assets_dir, exist_ok=True)
datasets = tuple(args.only) if args.only else DATASETS
+72
View File
@@ -0,0 +1,72 @@
"""Crystal intro-movie and title-screen symbols for the Crystal manifest.
Gold's title/intro symbols do not exist in Crystal: CrystalIntro is a different
program with its own asset set (engine/movie/intro.asm:1678-1777) and the title
composes on the fly with no tilemap (engine/movie/title.asm:364-374).
"""
# engine/movie/intro.asm:1678-1777
INTRO_SYMBOLS = [
"IntroSuicuneRunGFX",
"IntroPichuWooperGFX",
"IntroBackgroundGFX",
"IntroBackgroundTilemap",
"IntroBackgroundAttrmap",
"IntroBackgroundPalette",
"IntroUnownsGFX",
"IntroPulseGFX",
"IntroUnownATilemap",
"IntroUnownAAttrmap",
"IntroUnownHITilemap",
"IntroUnownHIAttrmap",
"IntroUnownsTilemap",
"IntroUnownsAttrmap",
"IntroUnownsPalette",
"IntroCrystalUnownsGFX",
"IntroCrystalUnownsTilemap",
"IntroCrystalUnownsAttrmap",
"IntroCrystalUnownsPalette",
"IntroSuicuneCloseGFX",
"IntroSuicuneCloseTilemap",
"IntroSuicuneCloseAttrmap",
"IntroSuicuneClosePalette",
"IntroSuicuneJumpGFX",
"IntroSuicuneBackGFX",
"IntroSuicuneJumpTilemap",
"IntroSuicuneJumpAttrmap",
"IntroSuicuneBackTilemap",
"IntroSuicuneBackAttrmap",
"IntroSuicunePalette",
"IntroUnownBackGFX",
"IntroGrass1GFX",
"IntroGrass2GFX",
"IntroGrass3GFX",
"IntroGrass4GFX",
]
# Palette fades that live as local labels inside their scene routines.
# engine/movie/intro.asm:1189 (fade.pal), :1385-1388 (unown_1.pal / unown_2.pal)
INTRO_FADE_SYMBOLS = [
"Intro_Scene24_ApplyPaletteFade.FadePals",
"Intro_Scene20_AppearUnown.pal1",
"Intro_Scene20_AppearUnown.pal2",
"Intro_FadeUnownWordPals.FastFadePalettes",
"Intro_FadeUnownWordPals.SlowFadePalettes",
]
# engine/movie/title.asm:364-374
TITLE_SYMBOLS = [
"TitleSuicuneGFX",
"TitleLogoGFX",
"TitleCrystalGFX",
"TitleScreenPalettes",
]
# engine/movie/splash.asm:344 -- replaces Gold's GameFreakLogoStarsGFX
SPLASH_SYMBOLS = [
"GameFreakDittoGFX",
"GameFreakDittoPaletteFade",
]
MOVIE_SYMBOLS = (
INTRO_SYMBOLS + INTRO_FADE_SYMBOLS + TITLE_SYMBOLS + SPLASH_SYMBOLS)
+147
View File
@@ -0,0 +1,147 @@
"""REQUIRED_SYMBOLS delta between pokegold.sym and pokecrystal.sym.
make_gold_manifest.REQUIRED_SYMBOLS names 340 symbols by hand. 25 of them do
not exist in pokecrystal.sym: Crystal renamed the credits mons, split the
trainer-card / Pokegear / pack-pals blocks by player gender, split the two
FontsExtra tiles apart, and replaced the whole Gold/Silver intro movie and
title screen. DROP_SYMBOLS lists those 25 and ADD_SYMBOLS the verified
replacements; the intro, title and splash names come from
crystal_movie_symbols.MOVIE_SYMBOLS (that file is the pinned CT-9 list and is
not edited here). MOBILE_SYMBOLS is the Mobile System GB art, which no Gold
or Silver ROM carries at all.
Every name below was checked against ../pokecrystal-symbols/pokecrystal.sym.
"""
from __future__ import annotations
from crystal_movie_symbols import MOVIE_SYMBOLS
# Gold-only names, grouped by what replaces them.
DROP_SYMBOLS = frozenset({
# engine/movie/title.asm:364-373 -- no tilemap; DrawTitleGraphic composes.
"TitleScreenGFX1", "TitleScreenGFX2", "TitleScreenGFX3",
"TitleScreenGFX4", "TitleScreenTilemap",
# engine/movie/credits.asm:610-613
"CreditsBellossomGFX", "CreditsTogepiGFX",
"CreditsElekidGFX", "CreditsSentretGFX",
# gfx/misc.asm:44
"GameFreakLogoStarsGFX",
# engine/gfx/player_gfx.asm:114,117,120,203,206
"ChrisPicAndTrainerCardGFX",
# engine/gfx/color.asm:1330,1333
"PokegearPals",
# engine/gfx/cgb_layouts.asm:818,821
"_CGB_PackPals.PackPals",
# gfx/font.asm:51,63
"FontsExtra_SolidBlackAndUpArrowGFX",
# engine/movie/intro.asm:1 -- CrystalIntro is a different program.
"Intro_WaterGFX1", "Intro_WaterTilemap", "Intro_WaterMeta",
"Intro_WaterGFX2",
"Intro_GrassGFX1", "Intro_GrassTilemap", "Intro_GrassMeta",
"Intro_GrassGFX2",
"Intro_FireGFX1", "Intro_FireGFX2", "Intro_FireGFX3",
})
# Crystal replacements, minus the intro/title/splash ones MOVIE_SYMBOLS owns.
ADD_SYMBOLS = frozenset({
# engine/movie/credits.asm:610-613
"CreditsPichuGFX", "CreditsSmoochumGFX",
"CreditsDittoGFX", "CreditsIgglybuffGFX",
# engine/gfx/player_gfx.asm:114,117,120,203,206
"ChrisCardPic", "KrisCardPic", "TrainerCardGFX", "ChrisPic", "KrisPic",
# engine/gfx/color.asm:1330,1333
"MalePokegearPals", "FemalePokegearPals",
# engine/gfx/cgb_layouts.asm:818,821
"_CGB_PackPals.ChrisPackPals", "_CGB_PackPals.KrisPackPals",
# gfx/font.asm:51,63 -- black is 1bpp at tile $60, up_arrow 2bpp at $61.
"FontsExtra_SolidBlackGFX", "FontsExtra2_UpArrowGFX",
# main.asm:425-448 -- the pic-animation pointer tables, absent from Gold.
"AnimationPointers", "AnimationIdlePointers",
"BitmasksPointers", "FramesPointers",
"UnownAnimationPointers", "UnownAnimationIdlePointers",
"UnownBitmasksPointers", "UnownFramesPointers",
# engine/tilesets/tileset_anims.asm -- the five Crystal-only anim steps.
"AnimateFountainTile",
"ForestTreeLeftAnimation", "ForestTreeRightAnimation",
"ForestTreeLeftAnimation2", "ForestTreeRightAnimation2",
# engine/overworld/wildmons.asm:493-524 -- Raikou and Entei only.
"InitRoamMons",
# data/events/unown_walls.asm:7,15 -- the four Ruins of Alph wall words
# and the menu box each one is drawn in.
"UnownWalls", "MenuHeaders_UnownWalls",
# data/battle_tower/classes.asm:6 and data/battle_tower/parties.asm:1 --
# the Battle Tower roster, which no Gold or Silver ROM carries; the
# sprites table is engine/events/battle_tower/battle_tower.asm:1578
# INCLUDE "data/trainers/sprites.asm".
"BattleTowerTrainers", "BattleTowerMons", "BTTrainerClassSprites",
# data/battle_tower/unknown.asm:1, copied into wBT_OTTrainerData whole.
"BattleTowerTrainerData",
# engine/events/battle_tower/load_trainer.asm:24,112 -- the two rejection
# loops whose `maskbits` / `cp` pair carries the sample ceiling. Crystal
# 1.0 and 1.1 differ only in the trainer one (:29-37), so it is read out
# of the cart rather than written down.
"LoadOpponentTrainerAndPokemon.resample",
"LoadRandomBattleTowerMon.resample",
})
# Mobile System GB art, all of it present in the international v1.0 object:
# mobile/*.asm assembles there, only the menus that reach it are Japan-only.
MOBILE_SYMBOLS = frozenset({
# mobile/mobile_5c.asm:290,293,296,753,756-771,866,874,878
"AsciiFontGFX", "PichuAnimatedMobileGFX", "ElectroBallMobileGFX",
"PichuBorderMobileGFX", "Stadium2N64GFX", "Stadium2N64Tilemap",
"Stadium2N64Attrmap", "PasswordTopTilemap", "PasswordBottomTilemap",
"PasswordShiftTilemap", "ChooseMobileCenterTilemap",
"MobilePasswordAttrmap", "ChooseMobileCenterAttrmap",
"MobilePasswordPalettes",
# mobile/mobile_5e.asm:2,5,8,11,14,18,925,928,931,934,941
"MobileCardGFX", "ChrisSilhouetteGFX", "KrisSilhouetteGFX",
"MobileCard2GFX", "CardLargeSpriteAndFolderGFX", "CardSpriteGFX",
"DialpadTilemap", "DialpadAttrmap", "DialpadGFX", "DialpadCursorGFX",
"MobileCardListGFX",
# mobile/mobile_5f.asm:84,87,91,3528,3534,3537
"HaveWantGFX", "MobileSelectGFX", "HaveWantMap", "PokemonNewsGFX",
"PokemonNewsTileAttrmap", "PokemonNewsPalettes",
# mobile/mobile_5b.asm:206,209,212,215,758
"MobileSystemSplashScreen_InitGFX.Tiles",
"MobileSystemSplashScreen_InitGFX.Tilemap",
"MobileSystemSplashScreen_InitGFX.Attrmap",
"MobileSplashScreenPalettes", "MobileAdapterCheckGFX",
# mobile/mobile_42.asm:1733-1763 and mobile/mobile_40.asm:6921,6924
"MobileTradeSpritesGFX", "MobileTradeGFX", "MobileTradeTilemapLZ",
"MobileTradeAttrmapLZ", "MobileCable1GFX", "MobileCable2GFX",
"UnusedMobilePulsePalettes", "MobileTradeBGPalettes",
"MobileTradeOB1Palettes", "MobileTradeOB2Palettes",
"MobileAdapterPalettes", "MobileTradeLightsGFX",
"MobileTradeLightsPalettes",
# mobile/mobile_45_2.asm:1362,1365,1368 and
# mobile/mobile_45_sprite_engine.asm:311
"PichuBorderMobileOBPalettes", "PichuBorderMobileBGPalettes",
"PichuBorderMobileTilemapAttrmap", "MobileDialingGFX",
# mobile/mobile_12.asm:1007,1010, mobile/mobile_22.asm:518,
# mobile/mobile_41.asm:1115, mobile/fixed_words.asm:3231
"MobileUpArrowGFX", "MobileDownArrowGFX", "EZChatCursorGFX",
"MobileDialingFrameGFX", "SelectStartGFX",
# engine/menus/main_menu.asm:24 and gfx/font.asm:58
"MobileMenuGFX", "MobilePhoneTilesGFX",
# engine/link/mystery_gift.asm:1606,1920,1923
"MysteryGiftGFX", "CardTradeGFX", "CardTradeSpriteGFX",
})
def crystal_required(gold_required):
"""Apply the delta to make_gold_manifest.REQUIRED_SYMBOLS."""
gold = frozenset(gold_required)
stale = sorted(DROP_SYMBOLS - gold)
if stale:
raise SystemExit(
"crystal_symbol_deltas: DROP_SYMBOLS names that Gold no longer "
"requires: " + ", ".join(stale))
extra = ADD_SYMBOLS | MOBILE_SYMBOLS | set(MOVIE_SYMBOLS)
collisions = sorted(extra & gold)
if collisions:
raise SystemExit(
"crystal_symbol_deltas: ADD names already in the Gold set: "
+ ", ".join(collisions))
return frozenset((gold - DROP_SYMBOLS) | extra)
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Generate tools/rom_manifest_crystal.json from pret/pokecrystal.
Crystal is its own pret tree, but the manifest SHAPE is Gold's: same constant
blocks, same charmap parser, same symbol resolution. So this drives
make_gold_manifest.generate() over ../pokecrystal instead of forking it, the
way make_yellow_manifest.py drives make_rom_manifest's helpers over
pokeyellow. Three things are passed in:
* defines -- the EMPTY set. ../pokecrystal/Makefile:129 builds the retail
international v1.0 object with no -D at all (:130-134 add
_CRYSTAL11 / _CRYSTAL_AU / _DEBUG / _CRYSTAL11_VC for the four
other targets). Leaving Gold's {"_GOLD"} in place would take
an `IF DEF(_GOLD)` arm that Crystal never assembles.
* required -- crystal_symbol_deltas.crystal_required(), the Gold symbol set
with the 25 Gold-only names swapped for their Crystal
replacements plus crystal_movie_symbols.MOVIE_SYMBOLS.
* sha1 -- the Crystal cart hash.
`anim_labels` is on: Crystal has per-species pic-animation tables
(../pokecrystal/main.asm:425-448) and Gold has none.
Crystal also declares two extra RGBDS charmaps that Gold has not got at all
(../pokecrystal/constants/charmap.asm:422-442): `unown`, used by the Ruins of
Alph wall words, and `ascii`, used only by the Mobile System GB code. Neither
is more rows of the main charmap -- the same byte means a different thing in
each -- so make_gold_manifest.charmap deliberately STOPS at the first
`newcharmap`, and that guard has to keep protecting the main table. The Unown
one is carried here instead, as its own top-level `unownCharmap` key: that is
where `charmap` and `fontCharmap` already live, so a consumer picks the map it
wants by name and no reader of the main table can see these bytes. The `ascii`
map is not emitted, because nothing outside the Mobile System reads it.
Usage: python3 tools/make_crystal_manifest.py
Default paths: pokecrystal at ../pokecrystal (relative to the repo) or
/Users/bryanbassett/Documents/development/pokecrystal; symbols at
/Users/bryanbassett/Documents/development/pokecrystal-symbols/pokecrystal.sym.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import make_gold_manifest as gold # noqa: E402
from crystal_symbol_deltas import crystal_required # noqa: E402
from rom_data import CANONICAL_CRYSTAL_SHA1 # noqa: E402
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEV = "/Users/bryanbassett/Documents/development"
DEFAULT_POKECRYSTAL_CANDIDATES = [
os.path.join(os.path.dirname(REPO_ROOT), "pokecrystal"),
os.path.join(DEV, "pokecrystal"),
]
DEFAULT_SYMBOLS = os.path.join(DEV, "pokecrystal-symbols/pokecrystal.sym")
DEFAULT_OUT = os.path.join(
os.path.dirname(__file__), "rom_manifest_crystal.json")
# ../pokecrystal/Makefile:129
CRYSTAL_ASM_DEFINES = frozenset()
CRYSTAL_REQUIRED_SYMBOLS = crystal_required(gold.REQUIRED_SYMBOLS)
def _rgbds_int(expr, i):
"""Evaluate one `charmap` value expression for loop counter `i`.
RGBDS `$xx` is hex and `/` is integer division; nothing else in the block
needs modelling. The whitelist is what keeps this from being eval() on
arbitrary asm.
"""
text = re.sub(r"\$([0-9a-fA-F]+)", lambda m: str(int(m.group(1), 16)), expr)
text = text.replace("/", "//")
if not re.fullmatch(r"[0-9i()+\-*/ ]+", text):
raise ValueError(f"unsupported charmap expression: {expr}")
return int(eval(text, {"__builtins__": {}}, {"i": i})) # noqa: S307
def unown_charmap(pokecrystal, defines=None):
"""The `unown` charmap: byte -> letter, same shape as gold.charmap.
../pokecrystal/constants/charmap.asm:422-431 is a `pushc` block that names
the map, DEFs a string of every printable character, and emits one
`charmap STRSLICE(...)` per character inside a `for`. So the parse has to
follow the loop rather than read literal rows -- but it still reads the
letter set and the tile arithmetic out of the asm, not out of a copy here.
"""
path = os.path.join(pokecrystal, "constants", "charmap.asm")
out, strings, inside, loop = {}, {}, False, None
for _, line in gold.read_asm(path, defines):
s = line.strip()
if re.match(r"newcharmap\s+unown\b", s):
inside = True
continue
if not inside:
continue
if re.match(r"(popc|newcharmap)\b", s):
break
m = re.match(r'DEF\s+(\w+)\s+EQUS\s+"(.*)"$', s)
if m:
strings[m.group(1)] = m.group(2)
continue
m = re.match(r"for\s+(\w+),\s*STRLEN\(#(\w+)\)\s*$", s)
if m:
loop = (m.group(1), strings[m.group(2)])
continue
if re.match(r"endr\s*$", s):
loop = None
continue
m = re.match(
r"charmap\s+STRSLICE\(#(\w+),\s*\w+,\s*\w+\s*\+\s*1\),\s*(.+)$", s)
if m and loop:
for index, char in enumerate(strings[m.group(1)]):
out[str(_rgbds_int(m.group(2), index))] = char
continue
m = re.match(r'charmap\s+"(.*)",\s*(\$[0-9a-fA-F]+)$', s)
if m:
out[str(int(m.group(2)[1:], 16))] = m.group(1)
if not out:
raise SystemExit("charmap.asm has no `unown` charmap")
return out
def generate(pokecrystal, symbols_path):
data = gold.generate(
pokecrystal, symbols_path,
defines=CRYSTAL_ASM_DEFINES,
required=CRYSTAL_REQUIRED_SYMBOLS,
sha1=CANONICAL_CRYSTAL_SHA1,
anim_labels=True)
# Crystal renumbers the block: 162 flags to Gold's 93, so a consumer that
# hardcodes Gold's indices reads the wrong flag.
data["constants"]["engineFlagOrder"] = gold.parse_const_block(
os.path.join(pokecrystal, "constants", "engine_flags.asm"),
defines=CRYSTAL_ASM_DEFINES)
data["unownCharmap"] = unown_charmap(pokecrystal, CRYSTAL_ASM_DEFINES)
return data
def find_pokecrystal():
for candidate in DEFAULT_POKECRYSTAL_CANDIDATES:
if os.path.isfile(os.path.join(candidate, "main.asm")):
return candidate
return DEFAULT_POKECRYSTAL_CANDIDATES[-1]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--pokecrystal", default=find_pokecrystal())
parser.add_argument("--symbols", default=DEFAULT_SYMBOLS)
parser.add_argument("--out", default=DEFAULT_OUT)
args = parser.parse_args()
pokecrystal = os.path.abspath(args.pokecrystal)
if not os.path.isfile(os.path.join(pokecrystal, "main.asm")):
raise SystemExit(f"{pokecrystal} is not a pokecrystal checkout")
data = generate(pokecrystal, 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()
+152 -86
View File
@@ -69,8 +69,14 @@ def strip_comment(line):
return "".join(out).rstrip()
def read_asm(path):
"""Read an asm file as (lineno, text), comments stripped, IF resolved."""
def read_asm(path, defines=None):
"""Read an asm file as (lineno, text), comments stripped, IF resolved.
`defines` overrides ASM_DEFINES for another tree; pokecrystal's retail
v1.0 target passes no -D at all (../pokecrystal/Makefile:129).
"""
if defines is None:
defines = ASM_DEFINES
lines = []
stack = [] # stack of [taking, condition_known]
with open(path, encoding="utf-8") as f:
@@ -79,7 +85,7 @@ def read_asm(path):
s = line.strip()
m = re.match(r"IF\s+(!)?DEF\((\w+)\)\s*$", s, re.IGNORECASE)
if m:
defined = m.group(2) in ASM_DEFINES
defined = m.group(2) in defines
taking = (not defined) if m.group(1) else defined
stack.append([taking, True])
continue
@@ -115,7 +121,7 @@ def parse_number(tok):
return -val if neg else val
def parse_const_block(path, stop_at=None):
def parse_const_block(path, stop_at=None, defines=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);
@@ -123,7 +129,7 @@ def parse_const_block(path, stop_at=None):
"""
names = []
value = None
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
s = line.strip()
if not s:
continue
@@ -153,7 +159,7 @@ def parse_const_block(path, stop_at=None):
return names
def parse_const_block_at(path, first_const, stop_at=None):
def parse_const_block_at(path, first_const, stop_at=None, defines=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
@@ -166,7 +172,7 @@ def parse_const_block_at(path, first_const, stop_at=None):
names = []
value = None
started = False
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
s = line.strip()
if not s:
continue
@@ -201,7 +207,7 @@ def parse_const_block_at(path, first_const, stop_at=None):
return names
def parse_prefixed_consts(path, prefixes, exact=()):
def parse_prefixed_consts(path, prefixes, exact=(), defines=None):
"""Ordered const names from a file, filtered by prefix (mixed blocks).
map_data_constants.asm and friends stack several unrelated const_def
@@ -210,7 +216,7 @@ def parse_prefixed_consts(path, prefixes, exact=()):
even when they carry no shared prefix (TOWN, BALL, ...).
"""
out = []
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
m = re.match(r"const\s+(\w+)", line.strip())
if not m:
continue
@@ -220,7 +226,7 @@ def parse_prefixed_consts(path, prefixes, exact=()):
return out
def parse_sparse_consts(path, prefixes):
def parse_sparse_consts(path, prefixes, defines=None):
"""Ordered const names where index IS the const value (sparse blocks).
parse_prefixed_consts packs a block densely, which is wrong for a block
@@ -233,7 +239,7 @@ def parse_sparse_consts(path, prefixes):
"""
out = []
value = None
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
s = line.strip()
m = re.match(r"const_def(?:\s+(\d+))?\s*$", s)
if m:
@@ -259,7 +265,7 @@ def parse_sparse_consts(path, prefixes):
return out
def extract_trainer_classes(pokegold):
def extract_trainer_classes(pokegold, defines=None):
"""Ordered trainer class names (constants/trainer_constants.asm).
`trainerclass NAME` bumps its own counter and resets the const_def used
@@ -271,7 +277,7 @@ def extract_trainer_classes(pokegold):
classes = []
members = {}
current = None
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
s = line.strip()
m = re.match(r"trainerclass\s+(\w+)", s)
if m:
@@ -287,7 +293,7 @@ def extract_trainer_classes(pokegold):
return classes, members
def extract_items(pokegold):
def extract_items(pokegold, defines=None):
"""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
@@ -304,7 +310,7 @@ def extract_items(pokegold):
order = []
name_count = None
in_macro = False
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
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.
@@ -340,7 +346,7 @@ def extract_items(pokegold):
return order[1:], name_count - 1
def extract_specials(pokegold):
def extract_specials(pokegold, defines=None):
"""Ordered SpecialsPointers labels (data/events/special_pointers.asm).
The `special` script command carries an index into this table, not a name,
@@ -350,7 +356,7 @@ def extract_specials(pokegold):
path = os.path.join(pokegold, "data/events/special_pointers.asm")
names = []
in_macro = False
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
s = line.strip()
if re.match(r"MACRO\b", s):
in_macro = True
@@ -368,11 +374,11 @@ def extract_specials(pokegold):
return names
def extract_std_scripts(pokegold):
def extract_std_scripts(pokegold, defines=None):
"""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):
for _, line in read_asm(path, defines):
m = re.match(r"add_stdscript\s+(\w+)", line.strip())
if m:
names.append(m.group(1))
@@ -381,14 +387,14 @@ def extract_std_scripts(pokegold):
return names
def extract_map_groups(pokegold):
def extract_map_groups(pokegold, defines=None):
"""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):
for _, line in read_asm(path, defines):
s = line.strip()
if re.match(r"newgroup\s+\w+", s):
group += 1
@@ -411,12 +417,12 @@ def extract_map_groups(pokegold):
return order, groups
def extract_types(pokegold):
def extract_types(pokegold, defines=None):
"""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):
for _, line in read_asm(path, defines):
s = line.strip()
m = re.match(r"const_def(?:\s+(\$?\w+))?$", s)
if m:
@@ -435,12 +441,14 @@ def extract_types(pokegold):
return types
def charmap(pokegold):
def charmap(pokegold, defines=None):
"""Byte -> text charmap, same shape as make_rom_manifest.charmap."""
expansions = {
"<DOT>": ".",
"<LV>": "{LV}",
"<ID>": "{ID}",
# ../pokecrystal/constants/charmap.asm:6 -- $14 is "<PLAYER>" in English
"<PLAY_G>": "<PLAYER>",
# 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
@@ -453,10 +461,15 @@ def charmap(pokegold):
}
out = {}
path = os.path.join(pokegold, "constants/charmap.asm")
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
stripped = line.strip()
# ../pokecrystal/constants/charmap.asm:423,433 -- the unown and ascii
# charmaps are separate RGBDS charmaps, not more rows of the main one.
if re.match(r"(pushc|newcharmap)\b", stripped):
break
m = re.match(
r'charmap\s+"((?:[^"\\]|\\.)*)",\s*(\$[0-9a-fA-F]+)',
line.strip())
stripped)
if not m:
continue
value = int(m.group(2)[1:], 16)
@@ -478,22 +491,22 @@ def species_label(species_id):
return "".join(part.capitalize() for part in parts)
def pokemon_names(pokegold):
def pokemon_names(pokegold, defines=None):
"""PokemonNames dname entries, in declared (dex) order."""
names = []
path = os.path.join(pokegold, "data/pokemon/names.asm")
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
m = re.match(r'dname\s+"([^"]*)"', line.strip())
if m:
names.append(m.group(1))
return names
def music_order(pokegold):
def music_order(pokegold, defines=None):
"""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):
for _, line in read_asm(path, defines):
m = re.match(r"dba\s+(Music_\w+)\s*$", line.strip())
if m:
order.append(m.group(1))
@@ -502,11 +515,11 @@ def music_order(pokegold):
return order
def sfx_order(pokegold):
def sfx_order(pokegold, defines=None):
"""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):
for _, line in read_asm(path, defines):
m = re.match(r"dba\s+(Sfx_\w+)\s*$", line.strip())
if m:
order.append(m.group(1))
@@ -515,7 +528,27 @@ def sfx_order(pokegold):
return order
def pokemon_assets(pokegold, species_order, symbols):
# ../pokecrystal/main.asm:425-448 -- the four per-species pic-animation
# tables; field name in the manifest, symbol suffix in pokecrystal.sym.
ANIM_LABEL_SUFFIXES = (
("animLabel", "Animation"),
("idleLabel", "AnimationIdle"),
("bitmaskLabel", "Bitmasks"),
("framesLabel", "Frames"),
)
def _anim_labels(base, symbols):
"""{animLabel/idleLabel/bitmaskLabel/framesLabel: label or None}."""
out = {}
for field, suffix in ANIM_LABEL_SUFFIXES:
label = base + suffix
out[field] = label if label in symbols.by_name else None
return out
def pokemon_assets(pokegold, species_order, symbols, defines=None,
anim_labels=False):
"""{species: {id, name, front, back, frontLabel, backLabel}}.
Unown shares one pic per letter through UnownPicPointers (its
@@ -523,11 +556,15 @@ def pokemon_assets(pokegold, species_order, symbols):
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.
`anim_labels` adds the four pic-animation labels per species; off by
default because pokegold.sym carries none of them.
"""
names = pokemon_names(pokegold)
names = pokemon_names(pokegold, defines)
assets = {}
for index, species in enumerate(species_order):
name = names[index] if index < len(names) else species
base = species_label(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.
@@ -537,8 +574,9 @@ def pokemon_assets(pokegold, species_order, symbols):
"frontLabel": None, "backLabel": None,
"dexLabel": "UnownPokedexEntry",
}
if anim_labels:
assets[species].update(_anim_labels(base, symbols))
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(
@@ -554,6 +592,8 @@ def pokemon_assets(pokegold, species_order, symbols):
"frontLabel": front_label, "backLabel": back_label,
"dexLabel": dex_label if dex_label in symbols.by_name else None,
}
if anim_labels:
assets[species].update(_anim_labels(base, symbols))
return assets
@@ -927,7 +967,7 @@ TEXT_SOURCES = (
)
def text_labels(pokegold):
def text_labels(pokegold, defines=None):
"""Every text label in TEXT_SOURCES, in sorted order.
Unlike Gen 1 there is no `dynamic` map beside this. pokered's decoder is
@@ -939,7 +979,7 @@ def text_labels(pokegold):
for name in TEXT_SOURCES:
path = os.path.join(pokegold, "data/text", name)
pending = None
for _, line in read_asm(path):
for _, line in read_asm(path, defines):
stripped = line.strip()
if not stripped:
continue
@@ -959,9 +999,11 @@ def text_labels(pokegold):
def embedded_symbols(symbols, pokemon_labels, song_labels=(),
text_label_names=()):
text_label_names=(), required=None):
"""Resolve REQUIRED_SYMBOLS + pic labels + songs + text labels."""
names = (set(REQUIRED_SYMBOLS) | set(pokemon_labels)
if required is None:
required = REQUIRED_SYMBOLS
names = (set(required) | set(pokemon_labels)
| set(song_labels) | set(text_label_names))
for symbol_name in symbols.by_name:
# Pokedex entries are split across four banks and the game derives the
@@ -985,23 +1027,24 @@ def embedded_symbols(symbols, pokemon_labels, song_labels=(),
}
def generate(pokegold, symbols_path):
def generate(pokegold, symbols_path, defines=None, required=None,
sha1=None, anim_labels=False):
symbols = SymbolTable(symbols_path)
species = parse_const_block(
os.path.join(pokegold, "constants/pokemon_constants.asm"),
stop_at="NUM_POKEMON")
stop_at="NUM_POKEMON", defines=defines)
species_order = [n or "UNUSED" for n in species[1:]]
map_order, map_groups = extract_map_groups(pokegold)
map_order, map_groups = extract_map_groups(pokegold, defines)
tilesets = [n for n in parse_const_block(
os.path.join(pokegold, "constants/tileset_constants.asm"),
stop_at="NUM_TILESETS") if n]
stop_at="NUM_TILESETS", defines=defines) if n]
moves = parse_const_block(
os.path.join(pokegold, "constants/move_constants.asm"),
stop_at="NUM_ATTACKS")
stop_at="NUM_ATTACKS", defines=defines)
move_order = [n or "UNUSED" for n in moves[1:]]
# sprite_constants.asm stacks the ids of two different tables. $01..
@@ -1019,26 +1062,29 @@ def generate(pokegold, symbols_path):
# 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")
sprites = parse_const_block(
sprite_path, stop_at="NUM_POKEMON_SPRITES", defines=defines)
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
parse_const_block(
sprite_path, stop_at="NUM_OVERWORLD_SPRITES",
defines=defines)) - 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)
types = extract_types(pokegold, defines)
# 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):
for _, line in read_asm(path, defines):
s = line.strip()
m = re.match(r"const\s+(\w+)", s)
if not m:
@@ -1060,7 +1106,8 @@ def generate(pokegold, symbols_path):
# 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"))
os.path.join(pokegold, "constants/move_effect_constants.asm"),
defines=defines)
move_effect_order = [n or "EFFECT_UNUSED" for n in move_effects]
# Battle animations. constants/battle_anim_constants.asm stacks nine
@@ -1070,55 +1117,66 @@ def generate(pokegold, symbols_path):
# 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_objects = parse_prefixed_consts(
battle_anim, ("BATTLE_ANIM_OBJ_",), defines=defines)
battle_anim_funcs = parse_prefixed_consts(
battle_anim, ("BATTLE_ANIM_FUNC_",), defines=defines)
battle_anim_framesets = parse_prefixed_consts(
battle_anim, ("BATTLE_ANIM_FRAMESET_",))
battle_anim_oamsets = parse_prefixed_consts(battle_anim, ("BATTLE_ANIM_OAMSET_",))
battle_anim, ("BATTLE_ANIM_FRAMESET_",), defines=defines)
battle_anim_oamsets = parse_prefixed_consts(
battle_anim, ("BATTLE_ANIM_OAMSET_",), defines=defines)
# 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_",))
battle_anim, ("BATTLE_ANIM_GFX_",), defines=defines)
battle_bg_effects = parse_prefixed_consts(
battle_anim, ("BATTLE_BG_EFFECT_",), defines=defines)
# 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_",))
battle_anim_bg_pals = parse_prefixed_consts(
battle_anim, ("PAL_BATTLE_BG_",), defines=defines)
battle_anim_ob_pals = parse_prefixed_consts(
battle_anim, ("PAL_BATTLE_OB_",), defines=defines)
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_",))
item_data, (), exact=("ITEM", "KEY_ITEM", "BALL", "TM_HM"),
defines=defines)
item_menu_order = parse_prefixed_consts(
item_data, ("ITEMMENU_",), defines=defines)
# 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_",))
held_effect_order = parse_sparse_consts(
item_data, ("HELD_",), defines=defines)
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_",))
growth_order = parse_prefixed_consts(mon_data, ("GROWTH_",), defines=defines)
egg_group_order = parse_prefixed_consts(mon_data, ("EGG_",), defines=defines)
evolve_order = parse_prefixed_consts(mon_data, ("EVOLVE_",), defines=defines)
# 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_",))
map_setup, ("MAPCALLBACK_",), defines=defines)
# 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")
script_consts, "CMDQUEUE_NULL", stop_at="NUM_CMDQUEUE_TYPES",
defines=defines)
floor_order = parse_const_block_at(
script_consts, "FLOOR_B4F", stop_at="NUM_FLOORS")
script_consts, "FLOOR_B4F", stop_at="NUM_FLOORS", defines=defines)
deco_desc_order = parse_const_block_at(
script_consts, "DECODESC_POSTER", stop_at="NUM_DECODESCS")
script_consts, "DECODESC_POSTER", stop_at="NUM_DECODESCS",
defines=defines)
# The phone. PHONE_* doubles as the PhoneContacts row index and carries
# four const_skip holes that are real rows (the wrong-number fillers), so
@@ -1129,33 +1187,41 @@ def generate(pokegold, symbols_path):
# 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")]
phone_consts, "PHONE_00", stop_at="NUM_PHONE_CONTACTS",
defines=defines)]
special_call_order = parse_const_block_at(
phone_consts, "SPECIALCALL_NONE", stop_at="NUM_SPECIALCALLS")
phone_consts, "SPECIALCALL_NONE", stop_at="NUM_SPECIALCALLS",
defines=defines)
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_",))
trade_gender_order = parse_prefixed_consts(
npc_trade, ("TRADE_GENDER_",), defines=defines)
trade_dialog_order = parse_prefixed_consts(
npc_trade, ("TRADE_DIALOGSET_",), defines=defines)
trainer_classes, trainer_members = extract_trainer_classes(pokegold)
trainer_classes, trainer_members = extract_trainer_classes(
pokegold, defines)
trainer_types = parse_prefixed_consts(
os.path.join(pokegold, "constants/trainer_data_constants.asm"),
("TRAINERTYPE_",))
("TRAINERTYPE_",), defines=defines)
landmarks = parse_const_block(
os.path.join(pokegold, "constants/landmark_constants.asm"),
stop_at="NUM_LANDMARKS")
stop_at="NUM_LANDMARKS", defines=defines)
landmark_order = [n or "UNUSED" for n in landmarks]
icons = parse_prefixed_consts(
os.path.join(pokegold, "constants/icon_constants.asm"), ("ICON_",))
os.path.join(pokegold, "constants/icon_constants.asm"), ("ICON_",),
defines=defines)
tree_sets = parse_prefixed_consts(mon_data, ("TREEMON_SET_",))
tree_sets = parse_prefixed_consts(
mon_data, ("TREEMON_SET_",), defines=defines)
std_scripts = extract_std_scripts(pokegold)
specials = extract_specials(pokegold)
std_scripts = extract_std_scripts(pokegold, defines)
specials = extract_specials(pokegold, defines)
assets = pokemon_assets(pokegold, species_order, symbols)
assets = pokemon_assets(pokegold, species_order, symbols, defines,
anim_labels=anim_labels)
pokemon_labels = []
for asset in assets.values():
if asset["frontLabel"]:
@@ -1163,19 +1229,19 @@ def generate(pokegold, symbols_path):
if asset["backLabel"]:
pokemon_labels.append(asset["backLabel"])
songs = music_order(pokegold)
text_label_names = text_labels(pokegold)
sfx = sfx_order(pokegold)
songs = music_order(pokegold, defines)
text_label_names = text_labels(pokegold, defines)
sfx = sfx_order(pokegold, defines)
# 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)
item_order, item_name_count = extract_items(pokegold, defines)
data = {
"format": 3,
"generation": 2,
"romSha1": CANONICAL_GOLD_SHA1,
"romSha1": sha1 or CANONICAL_GOLD_SHA1,
"constants": {
"source": "pret/pokegold constants/*.asm",
"speciesOrder": species_order,
@@ -1259,7 +1325,7 @@ def generate(pokegold, symbols_path):
# Label -> decoded string is built at import time from these, the
# same way Gen 1 builds data/generated/text.lua from its own list.
"text": {"labels": text_label_names},
"charmap": charmap(pokegold),
"charmap": charmap(pokegold, defines),
"fontCharmap": font_extract.parse_charmap(pokegold),
"pokemonAssets": assets,
# Per-map metadata (group/map/width/height/name). RomExtractorGen2
@@ -1268,7 +1334,7 @@ def generate(pokegold, symbols_path):
"tilesets": {name: {} for name in tilesets},
}
data["symbols"] = embedded_symbols(
symbols, pokemon_labels, songs, text_label_names)
symbols, pokemon_labels, songs, text_label_names, required=required)
return data
+3 -3
View File
@@ -741,7 +741,7 @@ def imported_data_dir(repo):
return path if _generated_data_dir_ok(path) else None
version = (os.environ.get("POKEPORT_VERSION") or "red").lower()
if version not in ("red", "blue", "yellow", "gold", "silver"):
if version not in ("red", "blue", "yellow", "gold", "silver", "crystal"):
version = "red"
candidates = [
@@ -2404,8 +2404,8 @@ UPVALUE_CALL = re.compile(
UPVALUE_ARGS = re.compile(
r"""^\s*([A-Za-z_]\w*)\s*\.\s*(\w+)\s*,\s*["'](\w+)["']""")
VERSION_MATCH = re.compile(
r"""[=~]=\s*["'](red|blue|yellow|gold|silver)["']"""
r"""|["'](red|blue|yellow|gold|silver)["']\s*[=~]=""")
r"""[=~]=\s*["'](red|blue|yellow|gold|silver|crystal)["']"""
r"""|["'](red|blue|yellow|gold|silver|crystal)["']\s*[=~]=""")
def _line_of(body, offset):
+3
View File
@@ -14,6 +14,9 @@ CANONICAL_YELLOW_SHA1 = "cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1"
# Gold and Silver are Gen 2: 2 MiB carts, twice the size of the Gen 1 ROMs above.
CANONICAL_GOLD_SHA1 = "d8b8a3600a465308c9953dfa04f0081c05bdcb94"
CANONICAL_SILVER_SHA1 = "49b163f7e57702bc939d642a18f591de55d92dae"
# Crystal is the retail international v1.0 build (pret/pokecrystal's default
# `make` target), also 2 MiB.
CANONICAL_CRYSTAL_SHA1 = "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"
ROM_BANK_SIZE = 0x4000
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -103,6 +103,9 @@ local function applyLoaded(path, statusVerb)
S.loadError = false
S.allowSave = true
end
if Gen.of(S.save, S.version) == 2 then
S.events = Catalog.gen2EventList(Gen.engineOf(S.save, S.version), S.modRoots)
end
local mapId = Gen.playerMap(S.save)
S.mapId = mapId
S.dirty = false
@@ -169,16 +172,14 @@ function App.load(pathOverride, opts)
for _, mod in ipairs(S.mods:status().loaded) do
modRoots[#modRoots + 1] = mod.path
end
S.modRoots = modRoots
if Gen.of(nil, opts.version) == 2 or require("src.core.GameVersion").generation() == 2 then
S.events = Catalog.goldEventList(modRoots)
S.events = Catalog.gen2EventList(Gen.engineOf(nil, opts.version), modRoots)
else
S.events = Catalog.scrapeEvents("data/scripts", "data/generated/trainer_headers.lua",
nil, modRoots)
end
applyLoaded(pathOverride or SaveIO.defaultPath(), "Loaded")
if Gen.of(S.save, S.version) == 2 then
S.events = Catalog.goldEventList(modRoots)
end
end
-- Switch to another save file (Open button, drag-drop, or --save arg).
+6 -9
View File
@@ -125,15 +125,8 @@ function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
return sortedKeys(found)
end
function Catalog.goldEventList(extraDirs)
local names = {}
local ok, flags = pcall(require, "src.core.gen2.FlagNames")
if ok and flags and flags.events then
for name in pairs(flags.events) do
names[#names + 1] = name
end
end
table.sort(names)
function Catalog.gen2EventList(engine, extraDirs)
local names = require("Gen2Flags").names(engine)
local modFlags = Catalog.scrapeEvents(nil, nil, nil, extraDirs)
local seen = {}
for _, name in ipairs(names) do seen[name] = true end
@@ -146,4 +139,8 @@ function Catalog.goldEventList(extraDirs)
return names
end
function Catalog.goldEventList(extraDirs)
return Catalog.gen2EventList("gs", extraDirs)
end
return Catalog
+73 -8
View File
@@ -24,6 +24,37 @@ function Gen.of(save, version)
return GameVersion.generation()
end
local function versionOf(save, version)
if type(save) == "table" and GameVersion.VERSIONS[save.version] then
return save.version
end
if type(version) == "string" and GameVersion.VERSIONS[version] then
return version
end
return GameVersion.get()
end
Gen.versionOf = versionOf
function Gen.engineOf(save, version)
return GameVersion.engine(versionOf(save, version))
end
function Gen.editionLabel(save, version)
local info = GameVersion.info(versionOf(save, version))
return tostring((info and info.label) or "GEN 2"):upper()
end
function Gen.hasCaughtData(save, version)
if Gen.of(save, version) ~= 2 then return false end
return require("src.battle.gen2.Mon").hasCaughtData(versionOf(save, version))
end
-- engine/menus/init_gender.asm:23-38
function Gen.hasPlayerGender(save, version)
if Gen.of(save, version) ~= 2 then return false end
return Gen.engineOf(save, version) == "crystal"
end
function Gen.ofState(S)
if not S then return GameVersion.generation() end
return Gen.of(S.save, S.version)
@@ -110,10 +141,17 @@ function Gen.bindGoldData(data)
end
function Gen.newGame(version)
if (versionGeneration(version) or GameVersion.generation(version)) == 2 then
return require("src.core.gen2.Save").newGame()
local id = type(version) == "string" and GameVersion.VERSIONS[version] and version
or nil
if (versionGeneration(id) or GameVersion.generation()) == 2 then
local Save2 = require("src.core.gen2.Save")
local save = Save2.newGame({
playerName = id and Save2.defaultPlayerName(id) or nil,
})
if id then save.version = id end
return save
end
return require("src.core.SaveData").newGame()
return require("src.core.SaveData").newGame({ version = id })
end
function Gen.validate(save, data)
@@ -223,6 +261,34 @@ function Gen.setCoins(save, amount)
end
end
-- constants/ram_constants.asm:176-177
function Gen.playerGender(save)
local stored = save and save.player and save.player.gender
return stored == "female" and "female" or "male"
end
function Gen.setPlayerGender(save, gender)
save.player = save.player or {}
save.player.gender = (gender == "female") and "female" or "male"
return save.player.gender
end
-- constants/landmark_constants.asm:3, :111-113
function Gen.landmarkName(data, index)
index = math.floor(tonumber(index) or 0)
local Mon = require("src.battle.gen2.Mon")
if index == Mon.LANDMARK_EVENT then return "EVENT" end
if index == Mon.LANDMARK_GIFT then return "GIFT" end
if index == 0 then return "UNKNOWN" end
local rows = data and data.gen2Landmarks and data.gen2Landmarks.landmarks
for _, rec in pairs(rows or {}) do
if type(rec) == "table" and rec.index == index then
return (tostring(rec.name or rec.id):gsub("\n", " "))
end
end
return ("#%d"):format(index)
end
function Gen.dexOwnedKey(save)
if Gen.of(save) == 2 then return "caught" end
return "owned"
@@ -317,14 +383,13 @@ function Gen.toggleBadge(save, id)
return not on
end
local function goldFlagId(name)
local flags = require("src.core.gen2.FlagNames")
return flags.events and flags.events[name]
local function gen2FlagId(save, name)
return require("Gen2Flags").byName(Gen.engineOf(save))[name]
end
function Gen.getFlag(save, name)
if Gen.of(save) == 2 then
local id = goldFlagId(name)
local id = gen2FlagId(save, name)
if id then
local Events2 = require("src.world.gen2.Events")
local ev = Events2.new()
@@ -338,7 +403,7 @@ end
function Gen.setFlag(save, name, on)
if Gen.of(save) == 2 then
local id = goldFlagId(name)
local id = gen2FlagId(save, name)
if id then
local Events2 = require("src.world.gen2.Events")
local ev = Events2.new()
+275
View File
@@ -0,0 +1,275 @@
-- ../pokecrystal/constants/event_flags.asm:1 against
-- ../pokegold/constants/event_flags.asm:1
local Gen2Flags = {}
Gen2Flags.CRYSTAL_ADDED = {
EVENT_ALAN_GAVE_FIRE_STONE = 257,
EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG = 193,
EVENT_AZALEA_TOWN_KURT = 1956,
EVENT_BATTLE_TOWER_BATTLE_ROOM_YOUNGSTER = 1937,
EVENT_BATTLE_TOWER_OPEN_CIVILIANS = 1999,
EVENT_BEAT_BEAUTY_OLIVIA = 1473,
EVENT_BEAT_BUG_CATCHER_WAYNE = 1472,
EVENT_BEAT_CAMPER_QUENTIN = 1475,
EVENT_BEAT_COOLTRAINERF_CARA = 1470,
EVENT_BEAT_COOLTRAINERM_DARIN = 1469,
EVENT_BEAT_FISHER_TULLY = 1108,
EVENT_BEAT_FISHER_TULLY2 = 1119,
EVENT_BEAT_FISHER_TULLY3 = 1120,
EVENT_BEAT_POKEFANF_JAIME = 1474,
EVENT_BEAT_POKEFANM_ALLAN = 1480,
EVENT_BEAT_POKEFANM_REX = 1479,
EVENT_BEAT_POKEMANIAC_MILLER = 1476,
EVENT_BEAT_SAGE_GAKU = 1481,
EVENT_BEAT_SAGE_KOJI = 1483,
EVENT_BEAT_SAGE_MASA = 1482,
EVENT_BEAT_SUPER_NERD_HUGH = 1477,
EVENT_BEAT_SUPER_NERD_MARKUS = 1478,
EVENT_BEAT_SUPER_NERD_STAN = 1409,
EVENT_BEAT_TWINS_LEA_AND_PIA = 1471,
EVENT_BUENA_OFFERED_HER_PHONE_NUMBER = 828,
EVENT_BUENA_OFFERED_HER_PHONE_NUMBER_NO_BLUE_CARD = 670,
EVENT_BUGGING_KURT_TOO_MUCH = 187,
EVENT_BURNED_TOWER_1F_EUSINE = 1893,
EVENT_BURNED_TOWER_1F_HIDDEN_ULTRA_BALL = 255,
EVENT_BURNED_TOWER_1F_HP_UP = 1622,
EVENT_BURNED_TOWER_MORTY = 1892,
EVENT_CAN_GIVE_GS_BALL_TO_KURT = 190,
EVENT_CIANWOOD_CITY_EUSINE = 1965,
EVENT_COPYCAT_1 = 1774,
EVENT_COPYCAT_2 = 1775,
EVENT_DANA_GAVE_THUNDERSTONE = 258,
EVENT_DARK_CAVE_VIOLET_ENTRANCE_DIRE_HIT = 1998,
EVENT_DRAGONS_DEN_B1F_CALCIUM = 1983,
EVENT_DRAGONS_DEN_B1F_MAX_ELIXER = 1984,
EVENT_DRAGON_SHRINE_CLAIR = 1936,
EVENT_ECRUTEAK_CITY_GRAMPS = 1961,
EVENT_ECRUTEAK_GYM_GRAMPS = 1960,
EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_WANDERING_SAGE = 1969,
EVENT_ERIN_CALCIUM = 827,
EVENT_EUSINE_IN_BURNED_TOWER = 1962,
EVENT_FLORIA_AT_FLOWER_SHOP = 1896,
EVENT_FLORIA_AT_SUDOWOODO = 1897,
EVENT_FOREST_IS_RESTLESS = 192,
EVENT_FOUGHT_EUSINE = 819,
EVENT_FOUGHT_SUICUNE = 821,
EVENT_GAVE_GS_BALL_TO_KURT = 191,
EVENT_GINA_GAVE_LEAF_STONE = 256,
EVENT_GOLDENROD_CITY_MOVE_TUTOR = 1898,
EVENT_GOLDENROD_GAME_CORNER_MOVE_TUTOR = 1899,
EVENT_GOLDENROD_SALE_OFF = 1776,
EVENT_GOLDENROD_SALE_ON = 1777,
EVENT_GOLDENROD_UNDERGROUND_WAREHOUSE_ULTRA_BALL = 1621,
EVENT_GOT_CLEAR_BELL = 120,
EVENT_GOT_DRATINI = 189,
EVENT_GOT_GS_BALL_FROM_GOLDENROD_POKEMON_CENTER = 832,
EVENT_GOT_ODD_EGG = 830,
EVENT_GOT_RAINBOW_WING = 822,
EVENT_HOLE_IN_BURNED_TOWER = 818,
EVENT_HUEY_PROTEIN = 823,
EVENT_ICE_PATH_1F_PROTEIN = 1982,
EVENT_ILEX_FOREST_ANTIDOTE = 1978,
EVENT_ILEX_FOREST_ETHER = 1979,
EVENT_ILEX_FOREST_FARFETCHD = 1769,
EVENT_ILEX_FOREST_KURT = 1957,
EVENT_ILEX_FOREST_LASS = 1773,
EVENT_ILEX_FOREST_X_ATTACK = 1977,
EVENT_JOEY_HP_UP = 824,
EVENT_KOJI_ALLOWS_YOU_PASSAGE_TO_TIN_TOWER = 820,
EVENT_KURTS_HOUSE_GRANDDAUGHTER_1 = 1932,
EVENT_KURTS_HOUSE_GRANDDAUGHTER_2 = 1933,
EVENT_LAKE_OF_RAGE_ELIXER = 1605,
EVENT_LAKE_OF_RAGE_ELIXIR_ON_STANDBY = 58,
EVENT_MET_BUENA = 829,
EVENT_MET_FLORIA = 185,
EVENT_MOUNT_MORTAR_1F_INSIDE_IRON = 1992,
EVENT_MOUNT_MORTAR_1F_INSIDE_MAX_POTION = 1958,
EVENT_MOUNT_MORTAR_1F_INSIDE_NUGGET = 1959,
EVENT_MOUNT_MORTAR_1F_INSIDE_ULTRA_BALL = 1993,
EVENT_MOUNT_MORTAR_B1F_CARBOS = 1671,
EVENT_MOUNT_MORTAR_B1F_FULL_RESTORE = 1994,
EVENT_MOUNT_MORTAR_B1F_MAX_ETHER = 1995,
EVENT_MOUNT_MORTAR_B1F_PP_UP = 1996,
EVENT_OLIVINE_LIGHTHOUSE_5F_SUPER_REPEL = 1638,
EVENT_PARRY_IRON = 826,
EVENT_PICKED_UP_BERRY_FROM_KABUTO_ITEM_ROOM = 1944,
EVENT_PICKED_UP_CHARCOAL_FROM_HO_OH_ITEM_ROOM = 1943,
EVENT_PICKED_UP_ENERGYPOWDER_FROM_KABUTO_ITEM_ROOM = 1947,
EVENT_PICKED_UP_ENERGY_ROOT_FROM_AERODACTYL_ITEM_ROOM = 1955,
EVENT_PICKED_UP_GOLD_BERRY_FROM_AERODACTYL_ITEM_ROOM = 1952,
EVENT_PICKED_UP_GOLD_BERRY_FROM_HO_OH_ITEM_ROOM = 1940,
EVENT_PICKED_UP_HEAL_POWDER_FROM_AERODACTYL_ITEM_ROOM = 1954,
EVENT_PICKED_UP_HEAL_POWDER_FROM_KABUTO_ITEM_ROOM = 1946,
EVENT_PICKED_UP_MOON_STONE_FROM_AERODACTYL_ITEM_ROOM = 1953,
EVENT_PICKED_UP_MYSTERYBERRY_FROM_HO_OH_ITEM_ROOM = 1941,
EVENT_PICKED_UP_MYSTERYBERRY_FROM_OMANYTE_ITEM_ROOM = 1948,
EVENT_PICKED_UP_MYSTIC_WATER_FROM_OMANYTE_ITEM_ROOM = 1949,
EVENT_PICKED_UP_PSNCUREBERRY_FROM_KABUTO_ITEM_ROOM = 1945,
EVENT_PICKED_UP_REVIVAL_HERB_FROM_HO_OH_ITEM_ROOM = 1942,
EVENT_PICKED_UP_STARDUST_FROM_OMANYTE_ITEM_ROOM = 1950,
EVENT_PICKED_UP_STAR_PIECE_FROM_OMANYTE_ITEM_ROOM = 1951,
EVENT_PLAYERS_HOUSE_1F_NEIGHBOR = 1938,
EVENT_PLAYERS_NEIGHBORS_HOUSE_NEIGHBOR = 1939,
EVENT_RADIO_TOWER_5F_ULTRA_BALL = 1997,
EVENT_RANG_CLEAR_BELL_1 = 1894,
EVENT_RANG_CLEAR_BELL_2 = 1895,
EVENT_ROUTE_30_ANTIDOTE = 1976,
EVENT_ROUTE_31_POTION = 1710,
EVENT_ROUTE_32_REPEL = 1713,
EVENT_ROUTE_34_ILEX_FOREST_GATE_LASS = 1771,
EVENT_ROUTE_34_ILEX_FOREST_GATE_TEACHER_BEHIND_COUNTER = 1770,
EVENT_ROUTE_34_ILEX_FOREST_GATE_TEACHER_IN_WALKWAY = 1772,
EVENT_ROUTE_34_NUGGET = 1980,
EVENT_ROUTE_44_MAX_REPEL = 1981,
EVENT_ROUTE_45_NUGGET = 1720,
EVENT_ROUTE_46_X_SPEED = 1724,
EVENT_RUINS_OF_ALPH_OUTSIDE_TOURIST_FISHER = 1934,
EVENT_RUINS_OF_ALPH_OUTSIDE_TOURIST_YOUNGSTERS = 1935,
EVENT_SAW_SUICUNE_AT_CIANWOOD_CITY = 1966,
EVENT_SAW_SUICUNE_ON_ROUTE_36 = 1968,
EVENT_SAW_SUICUNE_ON_ROUTE_42 = 1967,
EVENT_SET_WHEN_FOUGHT_HO_OH = 1975,
EVENT_SILVER_CAVE_ROOM_1_PROTEIN = 1690,
EVENT_SILVER_CAVE_ROOM_1_ULTRA_BALL = 1985,
EVENT_SILVER_CAVE_ROOM_2_CALCIUM = 1986,
EVENT_SILVER_CAVE_ROOM_2_PP_UP = 1988,
EVENT_SILVER_CAVE_ROOM_2_ULTRA_BALL = 1987,
EVENT_SPROUT_TOWER_2F_X_ACCURACY = 1608,
EVENT_TALKED_TO_FLORIA_AT_FLOWER_SHOP = 186,
EVENT_TALKED_TO_RUINS_COWARD = 188,
EVENT_TEAM_ROCKET_BASE_B1F_GUARD_SPEC = 1643,
EVENT_TEAM_ROCKET_BASE_B3F_FULL_HEAL = 1647,
EVENT_TEAM_ROCKET_BASE_B3F_PROTEIN = 1645,
EVENT_TEAM_ROCKET_BASE_B3F_ULTRA_BALL = 1620,
EVENT_TEAM_ROCKET_BASE_B3F_X_SPECIAL = 1646,
EVENT_TIFFANY_GAVE_PINK_BOW = 260,
EVENT_TIN_TOWER_1F_ENTEI = 1971,
EVENT_TIN_TOWER_1F_EUSINE = 1973,
EVENT_TIN_TOWER_1F_RAIKOU = 1972,
EVENT_TIN_TOWER_1F_SUICUNE = 1970,
EVENT_TIN_TOWER_1F_WISE_TRIO_1 = 1974,
EVENT_TIN_TOWER_1F_WISE_TRIO_2 = 1989,
EVENT_TIN_TOWER_4F_PP_UP = 1613,
EVENT_TIN_TOWER_6F_MAX_POTION = 1990,
EVENT_TIN_TOWER_9F_HP_UP = 1991,
EVENT_TULLY_ASKED_FOR_PHONE_NUMBER = 655,
EVENT_TULLY_GAVE_WATER_STONE = 259,
EVENT_UNION_CAVE_1F_POTION = 1628,
EVENT_UNION_CAVE_1F_X_ATTACK = 1627,
EVENT_VANCE_CARBOS = 825,
EVENT_VICTORY_ROAD_HP_UP = 1703,
EVENT_WADE_HAS_BERRY = 811,
EVENT_WADE_HAS_BITTER_BERRY = 814,
EVENT_WADE_HAS_PRZCUREBERRY = 813,
EVENT_WADE_HAS_PSNCUREBERRY = 812,
EVENT_WALL_OPENED_IN_AERODACTYL_CHAMBER = 809,
EVENT_WALL_OPENED_IN_HO_OH_CHAMBER = 806,
EVENT_WALL_OPENED_IN_KABUTO_CHAMBER = 807,
EVENT_WALL_OPENED_IN_OMANYTE_CHAMBER = 808,
EVENT_WELCOMED_TO_POKECOM_CENTER = 810,
EVENT_WHIRL_ISLAND_SW_ULTRA_BALL = 1680,
EVENT_WILTON_HAS_GREAT_BALL = 816,
EVENT_WILTON_HAS_POKE_BALL = 817,
EVENT_WILTON_HAS_ULTRA_BALL = 815,
EVENT_WISE_TRIOS_ROOM_WISE_TRIO_1 = 1963,
EVENT_WISE_TRIOS_ROOM_WISE_TRIO_2 = 1964,
}
Gen2Flags.CRYSTAL_REMOVED = {
EVENT_ALAN_READY_FOR_REMATCH = true,
EVENT_ANTHONY_READY_FOR_REMATCH = true,
EVENT_ARNIE_READY_FOR_REMATCH = true,
EVENT_BEAT_FISHER_CHRIS = true,
EVENT_BEAT_FISHER_CHRIS2 = true,
EVENT_BEAT_FISHER_CHRIS3 = true,
EVENT_BEAT_SUPER_NERD_ERIC_UNUSED = true,
EVENT_BETH_READY_FOR_REMATCH = true,
EVENT_BEVERLY_READY_FOR_REMATCH = true,
EVENT_BRENT_READY_FOR_REMATCH = true,
EVENT_BURNED_TOWER_1F_BURN_HEAL = true,
EVENT_BURNED_TOWER_1F_X_SPEED = true,
EVENT_BURNED_TOWER_B1F_HIDDEN_BURN_HEAL = true,
EVENT_BURNED_TOWER_B1F_HIDDEN_NUGGET = true,
EVENT_BURNED_TOWER_B1F_HIDDEN_ULTRA_BALL = true,
EVENT_BURNED_TOWER_B1F_HP_UP = true,
EVENT_BURNED_TOWER_FIREBREATHER_DICK_ASHES = true,
EVENT_BURNED_TOWER_FIREBREATHER_DICK_NORMAL = true,
EVENT_CHAD_READY_FOR_REMATCH = true,
EVENT_CHRIS_ASKED_FOR_PHONE_NUMBER = true,
EVENT_CHRIS_READY_FOR_REMATCH = true,
EVENT_DANA_READY_FOR_REMATCH = true,
EVENT_DEREK_READY_FOR_REMATCH = true,
EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_SAGE_LEFT = true,
EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_SAGE_RIGHT = true,
EVENT_ERIN_READY_FOR_REMATCH = true,
EVENT_GAVEN_READY_FOR_REMATCH = true,
EVENT_GINA_READY_FOR_REMATCH = true,
EVENT_HUEY_READY_FOR_REMATCH = true,
EVENT_ILEX_FOREST_FARFETCHD_1 = true,
EVENT_ILEX_FOREST_FARFETCHD_10 = true,
EVENT_ILEX_FOREST_FARFETCHD_2 = true,
EVENT_ILEX_FOREST_FARFETCHD_3 = true,
EVENT_ILEX_FOREST_FARFETCHD_4 = true,
EVENT_ILEX_FOREST_FARFETCHD_5 = true,
EVENT_ILEX_FOREST_FARFETCHD_6 = true,
EVENT_ILEX_FOREST_FARFETCHD_7 = true,
EVENT_ILEX_FOREST_FARFETCHD_8 = true,
EVENT_ILEX_FOREST_FARFETCHD_9 = true,
EVENT_IRWIN_READY_FOR_REMATCH = true,
EVENT_JACK_READY_FOR_REMATCH = true,
EVENT_JOEY_READY_FOR_REMATCH = true,
EVENT_JOSE_READY_FOR_REMATCH = true,
EVENT_KENJI_READY_FOR_REMATCH = true,
EVENT_LAKE_OF_RAGE_ETHER_ON_STANDBY = true,
EVENT_LAKE_OF_RAGE_MAX_ETHER = true,
EVENT_LIZ_READY_FOR_REMATCH = true,
EVENT_MOUNT_MORTAR_B1F_FULL_HEAL = true,
EVENT_OLIVINE_LIGHTHOUSE_5F_GREAT_BALL = true,
EVENT_PARRY_READY_FOR_REMATCH = true,
EVENT_RALPH_READY_FOR_REMATCH = true,
EVENT_REENA_READY_FOR_REMATCH = true,
EVENT_ROUTE_31_ANTIDOTE = true,
EVENT_ROUTE_32_POTION = true,
EVENT_ROUTE_45_X_SPECIAL = true,
EVENT_ROUTE_46_DIRE_HIT = true,
EVENT_SILVER_CAVE_ROOM_1_X_ACCURACY = true,
EVENT_SPROUT_TOWER_2F_X_DEFEND = true,
EVENT_TEAM_ROCKET_BASE_B1F_X_ACCURACY = true,
EVENT_TEAM_ROCKET_BASE_B3F_DIRE_HIT = true,
EVENT_TIFFANY_READY_FOR_REMATCH = true,
EVENT_TIN_TOWER_4F_SUPER_POTION = true,
EVENT_TODD_READY_FOR_REMATCH = true,
EVENT_VANCE_READY_FOR_REMATCH = true,
EVENT_VICTORY_ROAD_X_SPECIAL = true,
EVENT_WADE_READY_FOR_REMATCH = true,
EVENT_WHIRL_ISLAND_SW_GUARD_SPEC = true,
EVENT_WILTON_READY_FOR_REMATCH = true,
}
local cache = {}
function Gen2Flags.byName(engine)
engine = (engine == "crystal") and "crystal" or "gs"
if cache[engine] then return cache[engine] end
local ids = {}
local ok, flags = pcall(require, "src.core.gen2.FlagNames")
if ok and type(flags) == "table" and type(flags.events) == "table" then
for name, id in pairs(flags.events) do ids[name] = id end
end
if engine == "crystal" then
for name in pairs(Gen2Flags.CRYSTAL_REMOVED) do ids[name] = nil end
for name, id in pairs(Gen2Flags.CRYSTAL_ADDED) do ids[name] = id end
end
cache[engine] = ids
return ids
end
function Gen2Flags.names(engine)
local names = {}
for name in pairs(Gen2Flags.byName(engine)) do names[#names + 1] = name end
table.sort(names)
return names
end
return Gen2Flags
+70
View File
@@ -1141,4 +1141,74 @@ function Ops.setPokerus(S, mon, value)
return Ops.mark(S, ("%s pokerus byte %d"):format(mon.species, want))
end
-- engine/pokemon/caught_data.asm:169-172
Ops.CAUGHT_TIMES = { "UNKNOWN", "MORN", "DAY", "NITE" }
local function caughtGuard(S, mon)
if not mon then return false end
if not Gen.hasCaughtData(S.save, S.version) then
return Ops.say(S, "This game has no caught data")
end
return true
end
function Ops.setCaughtTime(S, mon, value)
if not caughtGuard(S, mon) then return false end
local want = clamp(math.floor(tonumber(value) or 0), 0, 3)
if want == (mon.caughtTime or 0) then
return Ops.say(S, ("Caught time is already %s"):format(Ops.CAUGHT_TIMES[want + 1]))
end
mon.caughtTime = want
return Ops.mark(S, ("%s caught time %s")
:format(mon.species, Ops.CAUGHT_TIMES[want + 1]))
end
-- constants/pokemon_data_constants.asm:120-121
function Ops.setCaughtLevel(S, mon, value)
if not caughtGuard(S, mon) then return false end
local Mon = require("src.battle.gen2.Mon")
local want = clamp(math.floor(tonumber(value) or 0), 0, Mon.CAUGHT_LEVEL_MASK)
if want == (mon.caughtLevel or 0) then
return Ops.say(S, ("Caught level is already %d"):format(want))
end
mon.caughtLevel = want
return Ops.mark(S, ("%s caught level %d"):format(mon.species, want))
end
-- constants/landmark_constants.asm:111-113
function Ops.setCaughtLocation(S, mon, value)
if not caughtGuard(S, mon) then return false end
local Mon = require("src.battle.gen2.Mon")
local span = Mon.CAUGHT_LOCATION_MASK + 1
local want = math.floor(tonumber(value) or 0) % span
if want == (mon.caughtLocation or 0) then
return Ops.say(S, ("Caught location is already %s")
:format(Gen.landmarkName(S.data, want)))
end
mon.caughtLocation = want
return Ops.mark(S, ("%s caught at %s")
:format(mon.species, Gen.landmarkName(S.data, want)))
end
function Ops.setCaughtByGender(S, mon, gender)
if not caughtGuard(S, mon) then return false end
local Mon = require("src.battle.gen2.Mon")
local want = Mon.caughtGenderOf(gender)
if want == mon.caughtByGender then
return Ops.say(S, ("Caught by is already %s"):format(tostring(want or "none")))
end
mon.caughtByGender = want
return Ops.mark(S, ("%s caught by %s"):format(mon.species, tostring(want or "none")))
end
function Ops.setPlayerGender(S, gender)
if not Gen.hasPlayerGender(S.save, S.version) then
return Ops.say(S, "This game has no player gender")
end
if Gen.playerGender(S.save) == gender then
return Ops.say(S, ("Player is already %s"):format(tostring(gender)))
end
return Ops.mark(S, ("Player gender %s"):format(Gen.setPlayerGender(S.save, gender)))
end
return Ops
+1
View File
@@ -26,6 +26,7 @@ function State.new()
-- App.load's opts; nil in a bare `love . --editor` run.
version = nil,
slotId = nil,
modRoots = nil,
-- Hosted inside the launcher process (Edit on a save row) rather than a
-- standalone `--editor` window: Close returns to the launcher instead of
-- quitting, and App calls onClose() to do it.
+37 -2
View File
@@ -101,6 +101,31 @@ local function drawMoney(S, Kit, x, y, w, h)
end
end
-- engine/menus/init_gender.asm:55-56
local GENDERS = { { "BOY", "male" }, { "GIRL", "female" } }
local function trainerHeight(Kit, s, pad)
return pad * 2 + Kit.textHeight("caption") + 10 * s + 28 * s
end
local function drawTrainer(S, Kit, x, y, w, h)
local s = Kit.scale
local pad = 16 * s
Kit.card(x, y, w, h)
Kit.caption(x + pad, y + pad, "TRAINER")
Kit.textRight("mono", tostring((S.save.player and S.save.player.name) or "?"),
x + w - pad, y + pad, PAL.caption)
local cy = y + pad + Kit.textHeight("caption") + 10 * s
local chipW = (w - 2 * pad - 8 * s) / 2
local gender = Gen.playerGender(S.save)
for i, pair in ipairs(GENDERS) do
if Kit.chip(x + pad + (i - 1) * (chipW + 8 * s), cy, chipW, 28 * s,
pair[1], gender == pair[2], PAL.green, PAL.steel) then
Ops.setPlayerGender(S, pair[2])
end
end
end
local BADGE_COLS = 4
local function badgeHeight(S, Kit, s, pad)
@@ -273,6 +298,10 @@ function M.draw(S, Kit, x, y, w, h)
local listH = 300 * s
Kit.pushClip(x, y, w, h)
local cy = y - off
if Gen.hasPlayerGender(S.save, S.version) then
local trainerH = trainerHeight(Kit, s, pad)
drawTrainer(S, Kit, x, cy, w, trainerH); cy = cy + trainerH + gap
end
drawMoney(S, Kit, x, cy, w, moneyH); cy = cy + moneyH + gap
drawPicker(S, Kit, x, cy, w, pickH); cy = cy + pickH + gap
drawBadges(S, Kit, x, cy, w, badgeH); cy = cy + badgeH + gap
@@ -294,8 +323,14 @@ function M.draw(S, Kit, x, y, w, h)
-- made the old panel unusable.
local moneyH = moneyHeight(Kit, s, pad, S)
local badgeH = badgeHeight(S, Kit, s, pad)
drawMoney(S, Kit, x, y, leftW, moneyH)
drawPicker(S, Kit, x, y + moneyH + gap, leftW, h - moneyH - badgeH - 2 * gap)
local topH = 0
if Gen.hasPlayerGender(S.save, S.version) then
topH = trainerHeight(Kit, s, pad) + gap
drawTrainer(S, Kit, x, y, leftW, topH - gap)
end
drawMoney(S, Kit, x, y + topH, leftW, moneyH)
drawPicker(S, Kit, x, y + topH + moneyH + gap, leftW,
h - topH - moneyH - badgeH - 2 * gap)
drawBadges(S, Kit, x, y + h - badgeH, leftW, badgeH)
drawBag(S, Kit, bagX, y, listW, h)
drawPc(S, Kit, pcX, y, listW, h)
+67 -2
View File
@@ -147,6 +147,66 @@ local function drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
end
end
-- engine/pokemon/caught_data.asm:168-199
local CAUGHT_BY = { { "-", "none" }, { "BOY", "boy" }, { "GIRL", "girl" } }
local function drawCaughtRows(S, Kit, mon, cx, y, inner, row)
local s = Kit.scale
local chipH = 22 * s
local gap = 6 * s
local tinyH = Kit.textHeight("tiny")
Kit.text("tiny", "CAUGHT", cx, y + (row - tinyH) / 2, PAL.caption)
local timeX = cx + 62 * s
local timeW = math.max(34 * s, (cx + inner - timeX - 3 * gap) / 4)
for i, label in ipairs(Ops.CAUGHT_TIMES) do
if Kit.chip(timeX + (i - 1) * (timeW + gap), y + (row - chipH) / 2,
timeW, chipH, label, (mon.caughtTime or 0) == i - 1, PAL.blue, PAL.steel) then
Ops.setCaughtTime(S, mon, i - 1)
end
end
y = y + row + gap
local btn = 24 * s
local lvX = cx + inner - 2 * btn - gap
if Kit.stepper(lvX, y + (row - btn) / 2, btn, btn, "-", { font = "small" }) then
Ops.setCaughtLevel(S, mon, (mon.caughtLevel or 0) - 1)
end
if Kit.stepper(lvX + btn + gap, y + (row - btn) / 2, btn, btn, "+",
{ font = "small" }) then
Ops.setCaughtLevel(S, mon, (mon.caughtLevel or 0) + 1)
end
Kit.textRight("tiny", ("MET LV %d"):format(mon.caughtLevel or 0), lvX - 10 * s,
y + (row - tinyH) / 2, PAL.text)
Kit.text("tiny", "OT", cx, y + (row - tinyH) / 2, PAL.caption)
local otX = cx + 26 * s
local otW = math.max(30 * s, (inner * 0.42 - 26 * s - 2 * gap) / 3)
for i, pair in ipairs(CAUGHT_BY) do
if Kit.chip(otX + (i - 1) * (otW + gap), y + (row - chipH) / 2, otW, chipH,
pair[1], (mon.caughtByGender or "none") == pair[2], PAL.blue, PAL.steel) then
Ops.setCaughtByGender(S, mon, pair[2])
end
end
y = y + row + gap
local whereX = cx + inner - 3 * btn - 2 * gap
if Kit.stepper(whereX, y + (row - btn) / 2, btn, btn, "-", { font = "small" }) then
Ops.setCaughtLocation(S, mon, (mon.caughtLocation or 0) - 1)
end
if Kit.stepper(whereX + btn + gap, y + (row - btn) / 2, btn, btn, "+",
{ font = "small" }) then
Ops.setCaughtLocation(S, mon, (mon.caughtLocation or 0) + 1)
end
if Kit.button(whereX + 2 * (btn + gap), y + (row - btn) / 2, btn, btn, "0",
{ kind = "danger", font = "micro", radius = 6 * s }) then
Ops.setCaughtLocation(S, mon, 0)
end
local where = ("WHERE %s"):format(Gen.landmarkName(S.data, mon.caughtLocation or 0))
Kit.text("tiny", Kit.ellipsize("tiny", where, whereX - 10 * s - cx), cx,
y + (row - tinyH) / 2, PAL.text)
return y + row + gap
end
local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
local s = Kit.scale
for slot = 1, 4 do
@@ -231,6 +291,7 @@ function MonEditor.draw(S, Kit, x, y, w, h)
-- the field + Set row
local extraH = 0
if Gen.ofState(S) == 2 then extraH = 88 * s end
if Gen.hasCaughtData(S.save, S.version) then extraH = extraH + 102 * s end
local nickFieldH = 30 * s
local contentH = pad + headerH + 18 * s
+ capH + 10 * s + nickFieldH + 18 * s
@@ -341,7 +402,7 @@ function MonEditor.draw(S, Kit, x, y, w, h)
local colY = statsY + cellH + 18 * s
if Gen.ofState(S) == 2 then
local extraY = colY
Kit.caption(cx, extraY, "GOLD")
Kit.caption(cx, extraY, Gen.editionLabel(S.save, S.version))
extraY = extraY + capH + 8 * s
local row = 28 * s
Kit.text("tiny", "HELD " .. tostring(mon.item or "none"), cx, extraY, PAL.text)
@@ -371,7 +432,11 @@ function MonEditor.draw(S, Kit, x, y, w, h)
if mon.unownLetter then bits[#bits + 1] = "Unown " .. tostring(mon.unownLetter) end
Kit.text("tiny", table.concat(bits, " ") ~= "" and table.concat(bits, " ")
or "gender/shiny follow DVs", cx, extraY, PAL.caption)
colY = extraY + 22 * s
extraY = extraY + 22 * s
if Gen.hasCaughtData(S.save, S.version) then
extraY = drawCaughtRows(S, Kit, mon, cx, extraY, inner, row)
end
colY = extraY
end
if narrow then
-- stacked: DVs first, then moves, then the two actions side by side at