Files
gen1recomp/tests/parity_text_markers.lua
T
bryanthaboi ca4d3d283c 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.
2026-08-23 12:10:28 -04:00

108 lines
4.8 KiB
Lua

-- Parity test: text that has to wait for the player.
-- Self-contained: run via `luajit tests/parity_text_markers.lua`; also
-- dofile'd by tests/run_tests.lua's aggregator.
--
-- The extractor emits three different break markers and they are not
-- interchangeable (src/render/TextBox.lua): \n opens the box's second line,
-- \v scrolls one line in after the down-arrow and a button press
-- (pokered ContText), and \f breaks the page, clearing and waiting.
--
-- Hand-written strings that spell `cont` or `para` as a plain \n put more
-- lines on a page than the two-line box can hold and give the player
-- nothing to press, so the text pours past by itself. Two of those shipped:
--
-- #239 the trainer-battle run refusal, which lost "trainer battle!"
-- #250 the Viridian caterpillar description, which scrolled six lines by
--
-- Pagination is what the box actually consumes, so asserting on it (rather
-- than on the raw string) is what ties these to the observable symptom.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local Data = require("src.core.Data")
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
local S = require("tests.harness").suite("parity text markers")
local check, eq = S.check, S.eq
require("src.render.Font").load(Data)
local TextBox = require("src.render.TextBox")
-- a page the box can actually show, and every line past the second on it
-- reached there by a \v the player has to release
local function pageIsPlayable(page, conts)
if #page <= 2 then return true end
for i = 3, #page do
if not (conts and conts[i]) then return false end
end
return true
end
local function assertPlayable(label, text)
local pages = TextBox.paginate(text)
for i, page in ipairs(pages) do
check(pageIsPlayable(page, pages.contBefore and pages.contBefore[i]),
("%s: page %d never runs past the box unattended"):format(label, i))
end
return pages
end
-- === #239: "No! There's no running from a trainer battle!" ===
-- The generated string is the authority; the port's literal is only a
-- fallback for a cache that predates it.
do
local generated = Data.text._NoRunningText
check(type(generated) == "string", "_NoRunningText is in the generated text")
if generated then
local pages = assertPlayable("_NoRunningText", generated)
eq(#pages, 1, "the refusal is one page")
eq(#pages[1], 3, "the refusal is three lines in a two-line box")
check(pages.contBefore[1][3] == true,
"'trainer battle!' scrolls in on a button press, not on its own (#239)")
check(generated:find("trainer battle!", 1, true) ~= nil,
"the third line is the part the bug swallowed")
end
-- the in-code fallback has to carry the same markers, or a pre-#239 cache
-- reintroduces the bug
local fallback = "No! There's no\nrunning from a\vtrainer battle!"
local pages = assertPlayable("run-refusal fallback", fallback)
eq(#pages[1], 3, "the fallback is three lines too")
check(pages.contBefore[1][3] == true, "the fallback waits on line 3")
end
-- === #250: the CATERPIE / WEEDLE description ===
-- pokered/text/ViridianCity.asm declares this one without the leading
-- underscore, and the extractor now keys on both spellings, so the port's
-- literal has to keep agreeing with what the cart actually says.
do
local desc = "CATERPIE has no\npoison, but\vWEEDLE does.\fWatch out for its\nPOISON STING!"
local extracted = Data.text.ViridianCityYoungster2CaterpieAndWeedleDescriptionText
or Data.text._ViridianCityYoungster2CaterpieAndWeedleDescriptionText
if extracted then eq(extracted, desc, "the literal still matches the cart") end
local pages = assertPlayable("caterpillar description", desc)
eq(#pages, 2, "para breaks the description into two pages (#250)")
eq(#pages[1], 3, "page 1 is text + line + cont")
eq(#pages[2], 2, "page 2 is para + line")
check(pages.contBefore[1][3] == true, "'WEEDLE does.' waits for a button")
-- and the shape the bug had: one page, six lines, nothing to press
local broken = "CATERPIE has no\npoison, but\nWEEDLE does.\n\nWatch out for its\nPOISON STING!"
local brokenPages = TextBox.paginate(broken)
eq(#brokenPages, 1, "the old spelling collapsed to a single page")
check(#brokenPages[1] > 2, "the old spelling overfilled that page")
check(not pageIsPlayable(brokenPages[1], brokenPages.contBefore[1]),
"the old spelling is exactly what this suite is here to catch")
end
-- the ask that precedes it uses the generated string, which already carries
-- \v, so it is a live example of the marker done right
do
local ask = Data.text._ViridianCityYoungster2YouWantToKnowAboutText
check(type(ask) == "string", "the caterpillar question is in generated text")
if ask then assertPlayable("caterpillar question", ask) end
end
S.finish()