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
+59 -5
View File
@@ -5,8 +5,8 @@
# them fails. The tier split is what makes that possible: T1/T2/T4 need
# nothing but the committed fixture dataset, so they run anywhere --
# including CI, which has no ROM. T3 asserts Pokemon Red facts and needs
# data/generated/, so it is skipped automatically when the ROM has never
# been imported rather than failing the run.
# data/generated/ or an imported Red cache, so it is skipped automatically
# when neither exists rather than failing the run.
#
# scripts/test.sh every tier this checkout can run
# scripts/test.sh --quick skip the slow content tier
@@ -16,6 +16,13 @@
#
# LUA overrides the interpreter (luajit here; CI installs lua5.4 too, but
# the engine targets LuaJIT/5.1 semantics so luajit is the default).
#
# POKEPORT_TEST_CACHES points at one LOVE identity holding an imported cache
# per version (red/ blue/ yellow/ gold/ silver/ crystal/); each is exported as
# RED_CACHE .. CRYSTAL_CACHE for the suites that read one. Build it with:
# POKEPORT_IDENTITY=pokeport-test-caches POKEPORT_VERSION=<version> \
# POKEPORT_IMPORT_ONLY=1 POKEPORT_IMPORT_ROM="<rom>" love .
# An explicit RED_CACHE/GOLD_CACHE/... in the environment always wins.
set -uo pipefail
@@ -32,7 +39,7 @@ for arg in "$@"; do
--bless) BLESS=1 ;;
--bless-shots) SHOTS=1; BLESS=1 ;;
--quick) QUICK=1 ;;
--help|-h) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
--help|-h) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $arg" >&2; exit 2 ;;
esac
done
@@ -49,6 +56,36 @@ fi
# save-editor suite pins that to the default identity.
SANDBOX_IDENTITY="ci-$$"
# Per-version caches. POKEPORT_TEST_CACHES names one LOVE identity holding an
# imported cache per version (red/ blue/ yellow/ gold/ silver/ crystal/).
CACHE_IDENTITY=${POKEPORT_TEST_CACHE_IDENTITY:-pokeport-test-caches}
if [ -n "${POKEPORT_TEST_CACHES:-}" ]; then
CACHE_ROOT="$POKEPORT_TEST_CACHES"
elif [ -d "$HOME/Library/Application Support/LOVE/$CACHE_IDENTITY" ]; then
CACHE_ROOT="$HOME/Library/Application Support/LOVE/$CACHE_IDENTITY"
else
CACHE_ROOT="$HOME/.local/share/love/$CACHE_IDENTITY"
fi
# Only a cache with the importer's completion marker is offered: a half-written
# one would fail suites that are meant to self-skip.
adopt_cache() {
local var="$1" dir="$CACHE_ROOT/$2"
[ -z "${!var:-}" ] || return 0
[ -f "$dir/rom-cache.complete" ] || return 0
export "$var=$dir"
echo " $var=$dir"
}
echo ""
echo "-- per-version caches under $CACHE_ROOT"
adopt_cache RED_CACHE red
adopt_cache BLUE_CACHE blue
adopt_cache YELLOW_CACHE yellow
adopt_cache GOLD_CACHE gold
adopt_cache SILVER_CACHE silver
adopt_cache CRYSTAL_CACHE crystal
FAILED=()
run_tier() {
local label="$1"; shift
@@ -77,6 +114,7 @@ fi
run_tier "T0 ROM builder version routing" python3 tests/build_rom_data_cli_test.py
run_tier "T0 ROM manifest generator pin/overrides" python3 tests/rom_manifest_generator_test.py
run_tier "T0 Crystal manifest + specials coverage" "$LUA" tests/crystal_import_test.lua
run_tier "T0 switch CI workflow content gate" "$LUA" tests/switch_ci_workflows_test.lua
run_tier "T0 switch transfer docs gate" "$LUA" tests/switch_transfer_docs_test.lua
# NX Blue/Yellow asset overlay: ROM-free, must run on every checkout so a
@@ -87,6 +125,9 @@ run_tier "T0 NX Yellow/Blue boot (dynamic paths)" "$LUA" tests/engine/nx_yellow_
run_tier "T0 NX Gold cache load (maps.lua prefix)" "$LUA" tests/engine/cache_fs_gold_nx_load_test.lua
run_tier "T0 touch-controls pad cursor" "$LUA" tests/engine/touch_controls_pad_cursor_test.lua
run_tier "T1/T2 engine invariants + parity gates" "$LUA" tests/run_engine.lua
# Gen 2 / Crystal: ROM-free (own fixtures, or a self-skip on a missing cache),
# so it runs here rather than behind the Red content gate below.
run_tier "T2 Gen 2 / Crystal suites" "$LUA" tests/run_gen2.lua
run_tier "T4 mod-SDK" "$LUA" tests/run_modkit.lua
run_tier "T4 title checkpoint cold restart" \
bash tests/integration/title_checkpoint_cold_start.sh
@@ -133,7 +174,19 @@ run_content_behavior() {
return 1
}
# The Red dataset is the source tree's when tools/build_data.py wrote one, and
# otherwise the imported Red cache, through Data:load's POKEPORT_DATA_DIR hook.
HAVE_RED_DATA=0
if [ -f data/generated/maps.lua ]; then
HAVE_RED_DATA=1
elif [ -n "${RED_CACHE:-}" ] && [ -f "$RED_CACHE/data/generated/maps.lua" ]; then
HAVE_RED_DATA=1
export POKEPORT_DATA_DIR="$RED_CACHE/data/generated"
echo ""
echo "-- T3 content: reading Red from $POKEPORT_DATA_DIR"
fi
if [ "$HAVE_RED_DATA" = "1" ]; then
if [ "$QUICK" = "1" ]; then
echo ""
echo "-- T3 content: skipped (--quick)"
@@ -163,8 +216,9 @@ if [ -f data/generated/maps.lua ]; then
fi
else
echo ""
echo "-- T3 content + run_link_tests: skipped (no data/generated/ --"
echo " import a ROM to run them; the modded-link cases ran in T4)"
echo "-- T3 content + run_link_tests: skipped (no data/generated/ and no"
echo " RED_CACHE -- import a ROM to run them; the modded-link cases ran"
echo " in T4 and the Gen 2 suites in T2)"
fi
# ------- golden screenshots: needs love + a display