mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
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:
+25
-3
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user