mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
ca4d3d283c
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.
154 lines
6.4 KiB
Lua
154 lines
6.4 KiB
Lua
-- Gen 2's engine text, the counterpart to Gen 1's data/generated/text.lua.
|
|
--
|
|
-- Gold and Silver had no label-keyed string table at all: the manifests
|
|
-- carried no `text` section, RomExtractorGen2 had no extractText, and
|
|
-- game.data.text was never assigned, so every call through
|
|
-- src/core/RomText.lua fell back to the literal written beside it. These
|
|
-- cover the three halves of closing that: the manifest names the labels and
|
|
-- resolves every one, the decoder emits the runtime name slots rather than
|
|
-- dropping them, and RomText fills those slots.
|
|
--
|
|
-- GOLD_CACHE="..." luajit tests/gen2_rom_text_test.lua
|
|
--
|
|
-- ROM-free apart from the last section, which reads an imported cache's
|
|
-- rom_text.lua and skips cleanly when there is none.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 rom text")
|
|
local check, eq = S.check, S.eq
|
|
|
|
love = require("tests.love_stub")
|
|
|
|
local Json = require("src.link.Json")
|
|
local romText = require("src.core.RomText")
|
|
|
|
local function manifest(path)
|
|
local file = assert(io.open(path, "r"))
|
|
local data = assert(Json.decode(file:read("*a")))
|
|
file:close()
|
|
return data
|
|
end
|
|
|
|
-- ---- the label list, and that every label resolves ------------------------
|
|
-- A label the list names but the symbol table cannot place would fail the
|
|
-- import at the Dialogue stage rather than at generation time, so the pairing
|
|
-- is asserted here instead.
|
|
for _, edition in ipairs({ "gold", "silver", "crystal" }) do
|
|
local data = manifest("tools/rom_manifest_" .. edition .. ".json")
|
|
local labels = (data.text or {}).labels or {}
|
|
check((data.text or {}).labels ~= nil,
|
|
edition .. " carries a text section")
|
|
check(#labels > 800,
|
|
("%s names %d text labels"):format(edition, #labels))
|
|
|
|
local unresolved = {}
|
|
for _, label in ipairs(labels) do
|
|
if not data.symbols[label] then unresolved[#unresolved + 1] = label end
|
|
end
|
|
eq(#unresolved, 0,
|
|
("every %s text label resolves to a symbol (%s)")
|
|
:format(edition, table.concat(unresolved, ", "):sub(1, 60)))
|
|
|
|
local named = {}
|
|
for _, label in ipairs(labels) do named[label] = true end
|
|
|
|
-- data/text/ also holds keyboard layouts and kana tables. Decoded as text
|
|
-- they come out as keyboard rows, so make_gold_manifest.TEXT_SOURCES leaves
|
|
-- their files out. `BattleText::` is excluded for a different reason: it
|
|
-- is a bank anchor sharing an address with the first real label under it,
|
|
-- and its own comment in the disassembly says so.
|
|
for _, excluded in ipairs({ "NameInputLower", "MailEntry_Uppercase",
|
|
"Dakutens", "Gen1TrainerClassNames", "BattleText" }) do
|
|
check(not named[excluded],
|
|
edition .. " leaves " .. excluded .. " out of the text list")
|
|
end
|
|
end
|
|
|
|
-- Gold and Silver describe the same strings; only the addresses move.
|
|
do
|
|
local gold = (manifest("tools/rom_manifest_gold.json").text or {}).labels or {}
|
|
local silver =
|
|
(manifest("tools/rom_manifest_silver.json").text or {}).labels or {}
|
|
eq(#gold, #silver, "Gold and Silver name the same number of labels")
|
|
local mismatch
|
|
for index, label in ipairs(gold) do
|
|
if silver[index] ~= label then mismatch = label; break end
|
|
end
|
|
eq(mismatch, nil, "and the same labels in the same order")
|
|
|
|
local crystal =
|
|
(manifest("tools/rom_manifest_crystal.json").text or {}).labels or {}
|
|
check(#crystal > #gold,
|
|
("Crystal names more labels than Gold (%d vs %d)"):format(#crystal, #gold))
|
|
local named = {}
|
|
for _, label in ipairs(gold) do named[label] = true end
|
|
local extra = 0
|
|
for _, label in ipairs(crystal) do
|
|
if not named[label] then extra = extra + 1 end
|
|
end
|
|
check(extra > 0,
|
|
("and %d of them are Crystal-only, so it is not a Gold alias")
|
|
:format(extra))
|
|
end
|
|
|
|
-- ---- the slots RomText fills ----------------------------------------------
|
|
-- decodeGen2Text emits {USER}, {TARGET} and {ENEMY} for the three names
|
|
-- PlaceMoveUsersName / PlaceMoveTargetsName / PlaceEnemysName write at
|
|
-- runtime (home/text.asm:302, :307, :327). Dropped, the line printed with a
|
|
-- hole where the name belongs.
|
|
do
|
|
local data = { text = {
|
|
SubTookDamageText = "The SUBSTITUTE\ntook damage for\v{TARGET}!",
|
|
WantsToBattleText = "{ENEMY}\nwants to battle!",
|
|
ConfusedNoMoreText = "{USER}'s\nconfused no more!",
|
|
SuperEffectiveText = "It's super-\neffective!",
|
|
} }
|
|
|
|
eq(romText(data, "SubTookDamageText", "fallback", "GEODUDE"),
|
|
"The SUBSTITUTE\ntook damage for\vGEODUDE!",
|
|
"a {TARGET} slot takes the name the caller passes")
|
|
eq(romText(data, "WantsToBattleText", "fallback", "FALKNER"),
|
|
"FALKNER\nwants to battle!", "and so does {ENEMY}")
|
|
eq(romText(data, "ConfusedNoMoreText", "fallback", "CYNDAQUIL"),
|
|
"CYNDAQUIL's\nconfused no more!", "and {USER}")
|
|
eq(romText(data, "SuperEffectiveText", "It's super effective!"),
|
|
"It's super-\neffective!",
|
|
"a line with no slot comes back as the cart wrote it")
|
|
eq(romText(data, "NoSuchLabel", "the engine's own wording"),
|
|
"the engine's own wording",
|
|
"and a label the cache does not carry falls back")
|
|
end
|
|
|
|
-- ---- against a real imported cache ----------------------------------------
|
|
do
|
|
local cache = os.getenv("GOLD_CACHE")
|
|
if not cache then
|
|
local home = os.getenv("HOME") or ""
|
|
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
|
|
end
|
|
local path = cache .. "/data/generated/rom_text.lua"
|
|
local file = io.open(path, "r")
|
|
if not file then
|
|
print(" (skipped: no rom_text.lua at " .. path .. ")")
|
|
else
|
|
file:close()
|
|
local texts = assert(loadfile(path))()
|
|
check(next(texts) ~= nil, "the imported cache carries strings")
|
|
-- Wording taken from pokegold's data/text/battle.asm, with \n for `line`
|
|
-- and \v for `cont`, which is what RomExtractorGen2 decodes those to.
|
|
eq(texts.SuperEffectiveText, "It's super-\neffective!",
|
|
"SuperEffectiveText comes off the cart hyphenated and broken")
|
|
eq(texts.NotVeryEffectiveText, "It's not very\neffective…",
|
|
"NotVeryEffectiveText ends on the ellipsis glyph")
|
|
eq(texts.StartPerishText, "Both POKéMON will\nfaint in 3 turns!",
|
|
"StartPerishText names both sides")
|
|
eq(texts.ButItFailedText, "But it failed!", "and a one-row line is one row")
|
|
eq(texts.SubTookDamageText, "The SUBSTITUTE\ntook damage for\v{TARGET}!",
|
|
"SpikesText's neighbour keeps its cont row and its target slot")
|
|
eq(texts.PlayerHitTimesText, "Hit {NUM} times!",
|
|
"a text_decimal reads back as {NUM}")
|
|
end
|
|
end
|
|
|
|
S.finish()
|