Files
gen1recomp/tests/engine/game3_emerald_pack_text_test.lua
T
thibautbus 2fd5d09228 Resolve the translated texts once when each screen opens
Several screens looked their cart strings up in the script cache on every draw, every frame or every read. Each now resolves them once:

- the Battle Arena's judgment titles, when the window opens;
- the Battle Dome's tourney title, when the tree opens, and a match card's win and match-number texts, when the card is built;
- the Frontier Pass map's landmark names and descriptions, when the map opens;
- the Pokédex search screen's option texts, when the cursor moves instead of on every frame of the option list;
- the party menu's action labels, once per action list;
- the Battle Pyramid bag's "Return to" place, when the bag opens;
- the expanded placeholders, when their table is built for the current script cache.
2026-10-05 18:19:57 +02:00

210 lines
12 KiB
Lua

-- Emerald screens that keep their own copy of a cart string read the script
-- cache's text for it when the cache holds it, the way script messages do,
-- so a mod's text overrides reach them; the pack's copy stays the fallback.
package.path = "./?.lua;./?/init.lua;" .. package.path
local function ir(s) return { { t = "text", s = s }, { t = "eos" } } end
local BUNDLE = { text = {} }
package.loaded["src.core.game3.scripting.space"] = { ensureBundle = function() return BUNDLE end }
local RomText = require("src.core.game3.rom_text")
local TextIR = require("src.core.game3.scripting.text_ir")
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function plain(x) return x and TextIR.toPlain(x, {}) or nil end
-- RomText.irOr / RomText.refIr
BUNDLE.text = { gText_RecordsLv50 = ir("NIVEAU 50"), ["g3:08abcdef"] = ir("PAR ADRESSE") }
check(plain(RomText.irOr("gText_RecordsLv50", ir("LV. 50"))) == "NIVEAU 50", "irOr reads the cached text")
check(plain(RomText.irOr("gText_Missing", ir("LV. 50"))) == "LV. 50", "irOr falls back on the copy")
check(plain(RomText.irOr(nil, ir("LV. 50"))) == "LV. 50", "irOr without a key keeps the copy")
check(plain(RomText.refIr({ name = "gText_RecordsLv50", key = "g3:08000000", ir = ir("LV. 50") })) == "NIVEAU 50",
"refIr reads a reference by its symbol")
check(plain(RomText.refIr({ name = "sUnnamed", key = "g3:08abcdef", ir = ir("BY ADDRESS") })) == "PAR ADRESSE",
"refIr reads a reference by its address")
check(plain(RomText.refIr({ name = "sUnnamed", key = "g3:08000000", ir = ir("COPY") })) == "COPY",
"refIr falls back on the reference's copy")
check(RomText.refIr(nil) == nil, "refIr of no reference is nil")
-- Battle Dome tables, keyed by the table's label (extract_scripts.lua).
BUNDLE.text = { ["sBattleDomePotentialTexts[1]"] = ir("Fort potentiel") }
local Dome = require("src.core.game3.rse.frontier.dome")
local potential = { ir("Best candidate"), ir("Strong potential") }
check(plain(Dome.tableText(potential, "sBattleDomePotentialTexts", 2)) == "Fort potentiel",
"a Dome table string reads the cached text by its label")
check(plain(Dome.tableText(potential, "sBattleDomePotentialTexts", 1)) == "Best candidate",
"a Dome table string missing from the cache keeps the copy")
-- Apprentice messages are pack references.
local Apprentice = require("src.core.game3.rse.frontier.apprentice")
BUNDLE.text = { gText_ApprenticeWhichMon = ir("Lequel?") }
Apprentice.manifest = function()
return { whichMon = { { { name = "gText_ApprenticeWhichMon", key = "g3:08000001", ir = ir("Which one?") },
{ name = "gText_ApprenticeThanks", key = "g3:08000002", ir = ir("Thanks!") } } } }
end
Apprentice.player = function() return { id = 0 } end
check(plain(Apprentice.message({}, Apprentice.MSG.WHICH_MON)) == "Lequel?", "an Apprentice line reads the cached text")
check(plain(Apprentice.message({}, Apprentice.MSG.THANKS_MON)) == "Thanks!",
"an Apprentice line missing from the cache keeps the copy")
-- The Battle Pyramid bag's "Return to ..." line (pokeemerald/src/battle_pyramid_bag.c:687).
local Pyramid = require("src.core.game3.rse.frontier.pyramid")
local PyramidBag = require("src.ui.game3.rse.pyramid_bag")
Pyramid.bagCursor = { scroll = 0, cursor = 0 }
Pyramid.bagLists = function() return {}, {} end
Pyramid.manifest = function()
return { bagReturnTo = { { name = "gText_TheField", key = "g3:08000003", ir = ir("the field") } } }
end
PyramidBag._st.count, PyramidBag._st.location = 1, "field"
BUNDLE.text = { gText_ReturnToVar1 = { { t = "text", s = "Retourner " }, { t = "strvar", n = 1 }, { t = "eos" } },
gText_TheField = ir("au jeu") }
PyramidBag._st.returnTo = PyramidBag.returnTo("field")
check(PyramidBag.description() == "Retourner au jeu", "the bag's return line names the place from the cache")
BUNDLE.text.gText_TheField = nil
check(PyramidBag.description() == "Retourner au jeu", "the open bag keeps the place it read")
PyramidBag._st.returnTo = PyramidBag.returnTo("field")
check(PyramidBag.description() == "Retourner the field", "a place missing from the cache keeps the copy")
-- Ever Grande City's fly destinations (pokeemerald/src/region_map.c:343).
local RegionMap = require("src.ui.game3.rse.region_map")
RegionMap.manifest = function()
return { multiNameFlyDestinations = { { names = { "POKéMON LEAGUE", "POKéMON CENTER" }, mapSecId = 5, flag = 2100 } } }
end
local function flySub()
local s = { mapSecType = RegionMap.TYPE.CITY_CANFLY, mapSecId = 5, posWithinMapSec = 1, mapSecName = "EVER GRANDE",
session = { flags = { [2100] = true } } }
RegionMap.updateFlyText(s)
return s.flyText.sub
end
BUNDLE.text = { ["sEverGrandeCityNames[1]"] = ir("CENTRE POKéMON") }
check(flySub() == "CENTRE POKéMON", "a fly destination reads its name from the cache")
BUNDLE.text = {}
check(flySub() == "POKéMON CENTER", "a fly destination missing from the cache keeps the copy")
-- The Pokédex search screen (pokeemerald/src/pokedex.c:1017, 1042, 1330).
local Gfx = require("src.ui.game3.rse.pokedex_gfx")
Gfx.manifest = function()
return { search = {
topBar = { { description = "Search for POKéMON based on selected parameters." } },
items = { { description = "List by the first letter in the name." }, {}, {}, {}, {}, {},
{ description = "Execute search/switch." } },
orders = { { title = "NUMERICAL", description = "Pokédex listing by number." },
{ title = "A TO Z", description = "Alphabetical order." } },
types = { { title = "NONE" }, { title = "NORMAL" }, { title = "FIGHT" } },
} }
end
local Pokedex = require("src.ui.game3.rse.pokedex")
BUNDLE.text = {
gText_SearchForPkmnBasedOnParameters = ir("Chercher des POKéMON."),
gText_ListByFirstLetter = ir("Classer par initiale."),
gText_ExecuteSearchSwitch = ir("Lancer la recherche."),
gText_DexSortNumericalTitle = ir("NUMERIQUE"),
gText_DexSortNumericalDescription = ir("Classement par numéro."),
["gTypeNames[0]"] = ir("NORMAL"), ["gTypeNames[1]"] = ir("COMBAT"),
}
local orders = Pokedex.searchOptionTexts(Pokedex.SEARCH.ORDER)
check(orders[1].title == "NUMERIQUE" and orders[1].description == "Classement par numéro.",
"a search option reads its title and description from the cart")
check(orders[2].title == "A TO Z" and orders[2].description == "Alphabetical order.",
"a search option missing from the cache keeps the copy")
local types = Pokedex.searchOptionTexts(Pokedex.SEARCH.TYPE_LEFT)
check(types[1].title == "NONE" and types[3].title == "COMBAT", "type options read gTypeNames by type id")
check(Pokedex.topBarDescription(0) == "Chercher des POKéMON.", "a top bar description reads the cart's text")
check(Pokedex.itemDescription(0) == "Classer par initiale.", "a search item description reads the cart's text")
check(Pokedex.itemDescription(Pokedex.SEARCH.OK) == "Lancer la recherche.", "the OK item's description reads the cart's text")
-- The party menu's actions (pokeemerald/src/data/party_menu.h:658).
require("tests.game3_cache").stubSpeciesNames()
local Profile = require("src.core.game3.profile")
local Pokemon = require("src.core.game3.pokemon")
local Kit = require("src.ui.game3.rse.scene_kit")
local PartyMenu = require("src.ui.game3.party_menu")
Profile.forSession = function()
return { ui = { party = { manifest = "rse/menus", cursorOptionTexts = { "gText_Summary5", "gText_Switch2",
"gText_Cancel2" } } } }
end
Kit.manifest = function()
return { party = { cursorOptions = { "SUMMARY", "SWITCH", "CANCEL", "CUT" }, fieldMoves = { 15 } } }
end
Pokemon.moveName = function(move) return move == 15 and "COUPE" or nil end
BUNDLE.text = { gText_Summary5 = ir("RESUME"), gText_Switch2 = ir("ORDRE") }
check(PartyMenu._cursorOptionText("SUMMARY") == "RESUME", "a party action reads the cart's text")
check(PartyMenu._cursorOptionText("SWITCH") == "ORDRE", "a second party action reads the cart's text")
check(PartyMenu._cursorOptionText("CANCEL") == "CANCEL", "a party action missing from the cache keeps the label")
check(PartyMenu._cursorOptionText("CUT") == "COUPE", "a field move prints the move's name")
-- Expanded placeholders (pokeemerald/src/strings.c:6): the rival's name and
-- Japanese くん/ちゃん come from the cache, the extract's copies otherwise.
local Message = require("src.ui.game3.message")
local extracted = {
RIVAL_MALE = "MAY", RIVAL_FEMALE = "BRENDAN", KUN_MALE = "", KUN_FEMALE = "", VERSION = "EMERALD",
byGender = { RIVAL = { male = "MAY", female = "BRENDAN" }, KUN = { male = "", female = "" } },
}
BUNDLE.text = { gText_ExpandedPlaceholder_May = ir("FLORA"), gText_ExpandedPlaceholder_Kun = ir("くん") }
local placeholders = Message.cartPlaceholders(extracted)
check(placeholders.byGender.RIVAL.male == "FLORA", "the rival's name reads the cache")
check(placeholders.byGender.RIVAL.female == "BRENDAN", "a rival name missing from the cache keeps the copy")
check(placeholders.byGender.KUN.male == "くん", "the honorific reads the cache")
check(placeholders.VERSION == "EMERALD", "a placeholder missing from the cache keeps the copy")
BUNDLE.text.gText_ExpandedPlaceholder_May = ir("AURA")
check(placeholders.byGender.RIVAL.male == "FLORA", "the values are resolved once, not on every read")
local line = { { t = "player" }, { t = "ph", code = 5, name = "KUN" }, { t = "text", s = " / " },
{ t = "ph", code = 6, name = "RIVAL" }, { t = "eos" } }
BUNDLE.text = { gText_ExpandedPlaceholder_May = ir("FLORA"), gText_ExpandedPlaceholder_Kun = ir("くん"),
gText_ExpandedPlaceholder_Brendan = ir("BRICE"), gText_ExpandedPlaceholder_Chan = ir("ちゃん") }
placeholders = Message.cartPlaceholders(extracted)
check(TextIR.toPlain(line, { dialect = "rse", playerName = "RED", playerGender = 0, placeholders = placeholders })
== "REDくん / FLORA", "a boy's line expands the honorific and May's name from the cache")
check(TextIR.toPlain(line, { dialect = "rse", playerName = "RED", playerGender = 1, placeholders = placeholders })
== "REDちゃん / BRICE", "a girl's line expands the honorific and Brendan's name from the cache")
-- The context provider builds them once per script cache.
local Space = package.loaded["src.core.game3.scripting.space"]
Space.bundle = BUNDLE
package.loaded["src.import.CacheFs"] = { loadActive = function() return extracted end }
local first = TextIR.placeholdersFor({ dialect = "rse" })
check(first ~= nil and TextIR.placeholdersFor({ dialect = "rse" }) == first,
"the provider builds the placeholders once per script cache")
check(first.byGender.RIVAL.female == "BRICE", "the provider's placeholders read the cache")
BUNDLE = { text = { gText_ExpandedPlaceholder_Brendan = ir("BRUNO") } }
Space.bundle = BUNDLE
check(TextIR.placeholdersFor({ dialect = "rse" }).byGender.RIVAL.female == "BRUNO",
"a new script cache rebuilds them")
Space.bundle = nil
-- The Battle Arena's judgment window reads its titles once, when it opens
-- (pokeemerald/src/battle_arena.c:412).
local Data = require("src.core.game3.rse.frontier.f2_data")
Data.arena = function()
return { text = { playerMon1Name = ir("A"), vs = ir("VS"), opponentMon1Name = ir("B"), mind = ir("MIND"),
skill = ir("SKILL"), body = ir("BODY"), judgment = ir("JUDGMENT") } }
end
local Arena = require("src.core.game3.battle.facility_arena")
local arena = Arena.new()
BUNDLE.text = { gText_Mind = ir("ESPRIT") }
arena:judgeStep({}, { state = "open" })
local titles = {}
for _, row in ipairs(arena.window.texts) do titles[row.win] = plain(row.ir) end
check(titles[Arena.WIN.MIND] == "ESPRIT", "the judgment window reads a title from the cache when it opens")
check(titles[Arena.WIN.BODY] == "BODY", "a judgment title missing from the cache keeps the copy")
BUNDLE.text = { gText_Mind = ir("AUTRE") }
titles = {}
for _, row in ipairs(arena.window.texts) do titles[row.win] = plain(row.ir) end
check(titles[Arena.WIN.MIND] == "ESPRIT", "the open window keeps the titles it read")
if failed > 0 then
print(failed .. " check(s) failed")
os.exit(1)
end
print("all checks passed")