From ed27cd2821e44d7446327fcff41cdd1b9317b698 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:49 +0200 Subject: [PATCH 01/11] Translate Emerald ability names Pokemon.abilityName() returns Strings(name), so the RSE summary and the battle ability placeholders show a translated name, as FireRed's summary already did. FireRed's summary no longer translates the getter's result a second time, and the battle adapter keys an ability outside its built-in table by Pokemon.romAbilityName(), so a translated name cannot become an unknown ability key. --- src/core/game3/battle/adapter.lua | 5 ++- src/core/game3/pokemon.lua | 3 +- src/ui/game3/summary_menu.lua | 9 ++++- ...game3_emerald_translation_runtime_test.lua | 37 +++++++++++++++++++ tests/engine/game3_summary_layout_s3_test.lua | 26 ++++++++++++- tests/game3_summary_description_test.lua | 9 +++++ 6 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 tests/engine/game3_emerald_translation_runtime_test.lua diff --git a/src/core/game3/battle/adapter.lua b/src/core/game3/battle/adapter.lua index 16557dde..ae3c6bec 100644 --- a/src/core/game3/battle/adapter.lua +++ b/src/core/game3/battle/adapter.lua @@ -494,8 +494,9 @@ function Adapter.new(battleState, sayFn) if id and id > 0 then if ABILITY_BY_ID[id] then return ABILITY_BY_ID[id] end local ok, Pokemon = pcall(require, "src.core.game3.pokemon") - if ok and Pokemon and Pokemon.abilityName then - local n = Pokemon.abilityName(id) + -- The ROM's English name, not the display name a translation shows. + if ok and Pokemon and Pokemon.romAbilityName then + local n = Pokemon.romAbilityName(id) if n and n ~= "" and not n:match("^ABILITY") then return (tostring(n):upper():gsub("%s+", "_")) end diff --git a/src/core/game3/pokemon.lua b/src/core/game3/pokemon.lua index b265086f..d9da4323 100644 --- a/src/core/game3/pokemon.lua +++ b/src/core/game3/pokemon.lua @@ -4,6 +4,7 @@ local PokemonExtract = require("src.import.gba.pokemon_extract") local Versions = require("src.import.gba.versions") local ModRuntime = require("src.mods.Runtime") local CacheBlob = require("src.import.CacheBlob") +local Strings = require("src.core.Strings") local Pokemon = {} @@ -331,7 +332,7 @@ function Pokemon.abilityName(abilityId) if not abilityId or abilityId < 1 then return "-------" end if not Pokemon._abilityNames then Pokemon.install(Pokemon._cache) end local n = Pokemon._abilityNames and Pokemon._abilityNames[abilityId] - if n and n ~= "" then return n end + if n and n ~= "" then return Strings(n) end error("no ROM ability name for ability " .. abilityId, 2) end diff --git a/src/ui/game3/summary_menu.lua b/src/ui/game3/summary_menu.lua index 1cc2ded6..f169936a 100644 --- a/src/ui/game3/summary_menu.lua +++ b/src/ui/game3/summary_menu.lua @@ -905,23 +905,28 @@ local function draw_page_skills(mon) -- Party stores ability as numeric id (e.g. 65 = OVERGROW); resolve to name. local abilityId = tonumber(mon.abilityId) or tonumber(mon.ability) local ability = mon.abilityName + local abilityNameTranslated = false if type(mon.ability) == "string" and mon.ability ~= "" and not tonumber(mon.ability) then ability = mon.ability end if (not ability or ability == "") and abilityId and abilityId > 0 then ability = Pokemon.abilityName(abilityId) + abilityNameTranslated = true end if not ability or ability == "" then local aid = Pokemon.abilityId and Pokemon.abilityId(Pokemon.speciesOf(mon), mon.personality or 0) if aid and aid > 0 then abilityId = aid ability = Pokemon.abilityName(aid) + abilityNameTranslated = true end end ability = ability or "—" local ax, ay = cxy("abilityName", 74, 129) - -- No registry renames abilities; a translation reaches the name through Strings(). - draw_text(Strings(tostring(ability)), ax, ay, 80, "NORMAL") + -- abilityName() already translates numeric IDs; external labels still need one lookup. + local abilityText = tostring(ability) + if not abilityNameTranslated then abilityText = Strings(abilityText) end + draw_text(abilityText, ax, ay, 80, "NORMAL") local desc = SummaryData.abilityDescription(abilityId, tostring(ability)) local ad = coords().abilityDesc or { x = 10, y = 143, w = 232 } draw_text(desc, ad.x or 10, ad.y or 143, ad.w or 232, "NORMAL") diff --git a/tests/engine/game3_emerald_translation_runtime_test.lua b/tests/engine/game3_emerald_translation_runtime_test.lua new file mode 100644 index 00000000..642890ba --- /dev/null +++ b/tests/engine/game3_emerald_translation_runtime_test.lua @@ -0,0 +1,37 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local Strings = require("src.core.Strings") +local Pokemon = require("src.core.game3.pokemon") +local BattleText = require("src.core.game3.battle.battle_text") + +Pokemon._abilityNames = { [9] = "STATIC" } +Pokemon._romAbilityNames = { [9] = "STATIC" } + +-- A modded ability past the built-in id table resolves to a key from its name. +local Adapter = require("src.core.game3.battle.adapter") +Pokemon._abilityNames[200] = "NEW SKILL" +Pokemon._romAbilityNames[200] = "NEW SKILL" +local function adapterAbilityKey() + return Adapter.new({}):abilityOf({ ability = 200 }) +end + +Strings.load({}) +assert(Pokemon.abilityName(9) == "STATIC", + "an ability name keeps its English ROM value without translations") +assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keys an unlisted ability by its ROM name") + +Strings.load({ strings = { + STATIC = "STATIQUE", + ["NEW SKILL"] = "NOUVEAU TALENT", +} }) +assert(Pokemon.abilityName(9) == "STATIQUE", + "the shared ability-name getter translates the displayed name") +assert(Pokemon.romAbilityName(9) == "STATIC", + "the original ROM name stays available for stable ID lookups") +assert(BattleText.RESOLVE_RSE[0x17]({ lastAbility = 9 }) == "STATIQUE", + "the RSE battle ability placeholder uses the translated shared getter") +assert(Pokemon.abilityName(200) == "NOUVEAU TALENT", "the display name is translated") +assert(adapterAbilityKey() == "NEW_SKILL", + "the battle adapter keeps keying on the ROM name when the display name is translated") + +print("game3_emerald_translation_runtime_test: PASS") diff --git a/tests/engine/game3_summary_layout_s3_test.lua b/tests/engine/game3_summary_layout_s3_test.lua index 449fc9a4..1bcaf43e 100644 --- a/tests/engine/game3_summary_layout_s3_test.lua +++ b/tests/engine/game3_summary_layout_s3_test.lua @@ -50,12 +50,13 @@ SummaryChrome.manifest = function() return MANIFEST end local SESSION = nil package.loaded["src.core.game3.runtime"] = { getSession = function() return SESSION end } local SummaryData = require("src.core.game3.summary_data") +local Strings = require("src.core.Strings") SummaryData.moveDescription = function() return "" end SummaryData.abilityDescription = function() return "" end local realMemo = SummaryData.formatTrainerMemo SummaryData.formatTrainerMemo = function() return {} end SummaryData.expProgress = function() return { totalExp = 0, expNeeded = 0, progressPercent = 0 } end -Pokemon.abilityName = function() return "STATIC" end +Pokemon.abilityName = function() return Strings("STATIC") end Pokemon.abilityId = function() return 9 end Pokemon.monFrontPic = function() calls[#calls + 1] = { name = "pic", args = {} } return nil end Pokemon.monIcon = function() calls[#calls + 1] = { name = "icon", args = {} } return nil end @@ -215,6 +216,29 @@ local def = text_at("7") eq(def and def.x, 210 + 27 - 6, "a one-digit stat right-aligns too") SummaryMenu.close() +print("[test] 5b. translated ability names are looked up once") +Strings.load({ strings = { STATIC = "STATIQUE", STATIQUE = "NOM ENCORE TRADUIT" } }) +local numericAbility = mon() +numericAbility.ability = 9 +SummaryMenu.openMenu({ numericAbility }, 1, { page = SummaryMenu.PAGE_SKILLS }) +draw() +check(text_at("STATIQUE") ~= nil, + "the numeric ability getter's translated name is not translated a second time") +check(text_at("NOM ENCORE TRADUIT") == nil, + "a chained catalog entry does not change the numeric ability label") +SummaryMenu.close() + +local externalAbility = mon() +externalAbility.ability = "STATIC" +SummaryMenu.openMenu({ externalAbility }, 1, { page = SummaryMenu.PAGE_SKILLS }) +draw() +check(text_at("STATIQUE") ~= nil, + "a raw external ability label is translated once at the summary consumer") +check(text_at("NOM ENCORE TRADUIT") == nil, + "a raw external ability label also avoids chained translation") +SummaryMenu.close() +Strings.load({}) + local function text_xy(x, y) for _, t in ipairs(texts) do if t.x == x and t.y == y then return t end end return nil diff --git a/tests/game3_summary_description_test.lua b/tests/game3_summary_description_test.lua index f1a6d0cb..6b4b7f16 100644 --- a/tests/game3_summary_description_test.lua +++ b/tests/game3_summary_description_test.lua @@ -30,6 +30,15 @@ check(SummaryData.moveDescription(71, "VOL-VIE"):find("absorbs half", 1, true) ~ "a renamed move still finds its description") check(SummaryData.abilityDescription(9, "STATIK") == "Paralyzes on contact.", "a renamed ability still finds its description") +check(Pokemon.abilityName(9) == "STATIK", + "an ability name falls back to the current displayed source") + +Strings.load({ strings = { STATIK = "STATIQUE" } }) +check(Pokemon.abilityName(9) == "STATIQUE", + "the shared ability-name getter translates the current displayed name") +check(Pokemon.romAbilityName(9) == "STATIC", + "the ROM ability name remains stable for ID-based lookups") +Strings.load({}) Strings.load({ strings = { ["Paralyzes on contact."] = "Paralyse au contact." } }) check(SummaryData.abilityDescription(9, "STATIK") == "Paralyse au contact.", From 53af9e925e3daee16a514ad4dab6ddc68e6f7a01 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:49 +0200 Subject: [PATCH 02/11] Translate Emerald contest categories and effect descriptions SummaryData.contestEffectDescription() and contestCategoryName() translate by the English text, like the move and ability descriptions, and the RSE summary, the move relearner and the contest move window print them. --- src/core/game3/summary_data.lua | 11 ++++++ src/ui/game3/rse/contest.lua | 6 +++- src/ui/game3/rse/move_relearner.lua | 7 ++-- src/ui/game3/rse/summary_menu.lua | 2 +- ...game3_emerald_translation_runtime_test.lua | 35 +++++++++++++++++++ 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/core/game3/summary_data.lua b/src/core/game3/summary_data.lua index 6bb5e3cf..946ec654 100644 --- a/src/core/game3/summary_data.lua +++ b/src/core/game3/summary_data.lua @@ -416,6 +416,17 @@ function SummaryData.abilityDescription(abilityId, abilityName) return Strings(d.ABILITIES["ABILITY_"]) end +-- pokeemerald/src/data/contest_text_tables.h:220: keyed by the English +-- text, like the move and ability descriptions. Takes the effect row or its text. +function SummaryData.contestEffectDescription(effect) + local text = type(effect) == "table" and effect.description or effect + return Strings(type(text) == "string" and text or "") +end + +function SummaryData.contestCategoryName(name) + return Strings(name or "") +end + function SummaryData.moveDescription(moveId, moveName) local d = get_descriptions() moveName = rom_name("romMoveName", moveId, moveName) diff --git a/src/ui/game3/rse/contest.lua b/src/ui/game3/rse/contest.lua index 30c03099..c057ef5a 100644 --- a/src/ui/game3/rse/contest.lua +++ b/src/ui/game3/rse/contest.lua @@ -730,7 +730,11 @@ function UI:printContestMoveDescription(move) self:fillBox(0, TILE_EMPTY_JAM_HEART, 0x15, 0x20, MAX_CONTEST_MOVE_HEARTS, 0x01, 0x11) self:fillBox(0, TILE_FILLED_JAM_HEART, 0x15, 0x20, hearts, 0x01, 0x11) local key = "gContestEffectDescriptionPointers[" .. (cm.effect or 0) .. "]" - local desc = UI.has(key) and UI.plain(key) or "" + local desc = "" + if UI.has(key) then + -- Translated by its English text, like the summary and the move relearner. + desc = require("src.core.game3.summary_data").contestEffectDescription(UI.plain(key)) + end self.win[WIN_MOVE_DESCRIPTION] = { text = desc, x = 0, y = 1, fg = 15, shadow = 8 } self.win[WIN_SLASH] = { text = UI.plain("gText_Slash"), x = 0, y = 1, fg = 15, shadow = 8 } end diff --git a/src/ui/game3/rse/move_relearner.lua b/src/ui/game3/rse/move_relearner.lua index 1e2a838a..80e0c9f0 100644 --- a/src/ui/game3/rse/move_relearner.lua +++ b/src/ui/game3/rse/move_relearner.lua @@ -399,8 +399,11 @@ local function draw_contest(moveId) right(RomText.plain("gText_MoveRelearnerJam"), 92, 41) if not moveId then return end local cm, eff, c = contest_row(moveId) - if cm then print_at(c.categories and c.categories[cm.category] or "", 4, 25) end - if eff then print_lines(eff.description, 0, 65, NARROW) end + if cm then + local category = c.categories and c.categories[cm.category] + print_at(SummaryData.contestCategoryName(category), 4, 25) + end + if eff then print_lines(SummaryData.contestEffectDescription(eff), 0, 65, NARROW) end draw_hearts(eff) end diff --git a/src/ui/game3/rse/summary_menu.lua b/src/ui/game3/rse/summary_menu.lua index b9133318..c4d1ca3d 100644 --- a/src/ui/game3/rse/summary_menu.lua +++ b/src/ui/game3/rse/summary_menu.lua @@ -545,7 +545,7 @@ local function drawMoves(m, Sm, mon, contest, detail) drawTile(m, "hearts", i < appeal and 1 or 0, tx * 8, ty * 8) drawTile(m, "hearts", i < jam and 3 or 4, tx * 8, (ty + 2) * 8) end - put(m, dw, eff.description or "", 6, 1, 0) + put(m, dw, SummaryData.contestEffectDescription(eff), 6, 1, 0) end else local pa = win(m, W.POWER_ACC) diff --git a/tests/engine/game3_emerald_translation_runtime_test.lua b/tests/engine/game3_emerald_translation_runtime_test.lua index 642890ba..8ad37585 100644 --- a/tests/engine/game3_emerald_translation_runtime_test.lua +++ b/tests/engine/game3_emerald_translation_runtime_test.lua @@ -3,10 +3,29 @@ package.path = "./?.lua;./?/init.lua;" .. package.path local Strings = require("src.core.Strings") local Pokemon = require("src.core.game3.pokemon") local BattleText = require("src.core.game3.battle.battle_text") +local SummaryData = require("src.core.game3.summary_data") Pokemon._abilityNames = { [9] = "STATIC" } Pokemon._romAbilityNames = { [9] = "STATIC" } +-- pokeemerald/src/contest.c:3237 +local ContestUI = require("src.ui.game3.rse.contest") +ContestUI.has = function(key) return key == "gContestEffectDescriptionPointers[1]" end +ContestUI.plain = function(key) + if key == "gContestEffectDescriptionPointers[1]" then return "Startles the audience." end + return key +end +local function contestMoveDescription() + local view = setmetatable({ + c = { data = { moves = { [33] = { category = 0, effect = 1 } }, effects = { [1] = { appeal = 20, jam = 0 } } } }, + win = {}, + fillBox = function() end, + fillBoxInc = function() end, + }, { __index = ContestUI }) + view:printContestMoveDescription(33) + return view.win[10].text +end + -- A modded ability past the built-in id table resolves to a key from its name. local Adapter = require("src.core.game3.battle.adapter") Pokemon._abilityNames[200] = "NEW SKILL" @@ -18,10 +37,19 @@ end Strings.load({}) assert(Pokemon.abilityName(9) == "STATIC", "an ability name keeps its English ROM value without translations") +assert(SummaryData.contestCategoryName("Cool") == "Cool", + "a contest category keeps its English source without translations") +assert(SummaryData.contestEffectDescription({ description = "Startles the audience." }) + == "Startles the audience.", + "a contest effect description keeps its English source without translations") +assert(contestMoveDescription() == "Startles the audience.", + "the contest move window keeps its English effect text without translations") assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keys an unlisted ability by its ROM name") Strings.load({ strings = { STATIC = "STATIQUE", + Cool = "Sang-froid", + ["Startles the audience."] = "Surprend le public.", ["NEW SKILL"] = "NOUVEAU TALENT", } }) assert(Pokemon.abilityName(9) == "STATIQUE", @@ -30,6 +58,13 @@ assert(Pokemon.romAbilityName(9) == "STATIC", "the original ROM name stays available for stable ID lookups") assert(BattleText.RESOLVE_RSE[0x17]({ lastAbility = 9 }) == "STATIQUE", "the RSE battle ability placeholder uses the translated shared getter") +assert(SummaryData.contestCategoryName("Cool") == "Sang-froid", + "the move relearner translates the contest category source") +assert(SummaryData.contestEffectDescription({ description = "Startles the audience." }) + == "Surprend le public.", + "the RSE summary and move relearner translate the contest effect source") +assert(contestMoveDescription() == "Surprend le public.", + "the contest move window translates the effect text like the summary") assert(Pokemon.abilityName(200) == "NOUVEAU TALENT", "the display name is translated") assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keeps keying on the ROM name when the display name is translated") From 6b5993f7debcbe17b357023097ebc0e9086e0256 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:49 +0200 Subject: [PATCH 03/11] =?UTF-8?q?Translate=20Emerald=20Pok=C3=A9dex=20cate?= =?UTF-8?q?gories=20and=20descriptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pokédex entry passes its category and description through Strings() when drawn, and the starter selection and the Battle Factory build the same CopyMonCategoryText string the same way. --- src/ui/game3/rse/factory_select.lua | 3 +- src/ui/game3/rse/pokedex.lua | 10 ++- src/ui/game3/rse/starter_choose.lua | 3 +- ...game3_emerald_translation_runtime_test.lua | 81 +++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/ui/game3/rse/factory_select.lua b/src/ui/game3/rse/factory_select.lua index 903ea3b0..aac3bb32 100644 --- a/src/ui/game3/rse/factory_select.lua +++ b/src/ui/game3/rse/factory_select.lua @@ -1,6 +1,7 @@ local Stack = require("src.ui.game3.stack") local FrlgFont = require("src.ui.game3.frlg_font") local RomText = require("src.core.game3.rom_text") +local Strings = require("src.core.Strings") local Kit = require("src.ui.game3.rse.scene_kit") local Gfx = require("src.ui.game3.rse.pokeblock_gfx") local PalFade = require("src.core.game3.pal_fade") @@ -162,7 +163,7 @@ function Common.categoryText(species) entriesPack = Gfx.loadLua("data/generated/gba/pokemon/pokedex/entries.lua") or {} end local e = entriesPack[tonumber(species) or 0] or {} - return (e.category or "") .. " " .. RomText.plain("gText_Pokemon") + return Strings(e.category or "") .. " " .. RomText.plain("gText_Pokemon") end function Common.speciesName(species) diff --git a/src/ui/game3/rse/pokedex.lua b/src/ui/game3/rse/pokedex.lua index 1806aa1e..31fd9e80 100644 --- a/src/ui/game3/rse/pokedex.lua +++ b/src/ui/game3/rse/pokedex.lua @@ -1,4 +1,5 @@ local Kit = require("src.ui.game3.rse.scene_kit") +local Strings = require("src.core.Strings") local FrlgFont = require("src.ui.game3.frlg_font") local RomText = require("src.core.game3.rom_text") local Pal = require("src.core.game3.pal_fade") @@ -935,7 +936,12 @@ local function monInfo(s, nat, nationalNumber, owned, newEntry) out[#out + 1] = { text = RomText.plain("gText_NumberClear01") .. string.format("%03d", num), x = 0x60, y = 0x19 } local sp = Pokedex.speciesOf(nat) out[#out + 1] = { text = sp ~= 0 and pokemon().name(sp) or Gfx.manifest().tenDashes, x = 0x84, y = 0x19 } - local category = owned and ((e.category or "") .. " " .. RomText.plain("gText_Pokemon")) or RomText.plain("gText_5MarksPokemon") + local category + if owned then + category = Strings(e.category or "") .. " " .. RomText.plain("gText_Pokemon") + else + category = RomText.plain("gText_5MarksPokemon") + end out[#out + 1] = { text = category, x = 0x64, y = 0x29 } out[#out + 1] = { text = RomText.plain("gText_HTHeight"), x = 0x60, y = 0x39 } out[#out + 1] = { text = RomText.plain("gText_WTWeight"), x = 0x60, y = 0x49 } @@ -946,7 +952,7 @@ local function monInfo(s, nat, nationalNumber, owned, newEntry) out[#out + 1] = { text = RomText.plain("gText_UnkHeight"), x = 0x81, y = 0x39 } out[#out + 1] = { text = RomText.plain("gText_UnkWeight"), x = 0x81, y = 0x49 } end - local desc = owned and (e.description or "") or "" + local desc = owned and Strings(e.description or "") or "" local w = 0 for line in (desc .. "\n"):gmatch("(.-)\n") do w = math.max(w, FrlgFont.measure(line) or 0) end out[#out + 1] = { text = desc, x = w < 240 and math.floor((240 - w) / 2) or 0, y = 95 } diff --git a/src/ui/game3/rse/starter_choose.lua b/src/ui/game3/rse/starter_choose.lua index cfad4ca0..e6d93125 100644 --- a/src/ui/game3/rse/starter_choose.lua +++ b/src/ui/game3/rse/starter_choose.lua @@ -1,5 +1,6 @@ local Kit = require("src.ui.game3.rse.scene_kit") local RomText = require("src.core.game3.rom_text") +local Strings = require("src.core.Strings") local FrlgFont = require("src.ui.game3.frlg_font") local Pal = require("src.core.game3.pal_fade") local Trig = require("src.core.game3.trig") @@ -126,7 +127,7 @@ local function categoryText(species, policy) local nat = Pokemon.national and Pokemon.national(species) or species local row = dex and dex[nat] -- pokeemerald/src/international_string_util.c:86 - local category = (row and row.category) or "" + local category = Strings((row and row.category) or "") if policy then return policy.categoryText(category, RomText.plain(policy.categoryKey)) end return category .. " " .. RomText.plain("gText_Pokemon") end diff --git a/tests/engine/game3_emerald_translation_runtime_test.lua b/tests/engine/game3_emerald_translation_runtime_test.lua index 8ad37585..8e4c5ea6 100644 --- a/tests/engine/game3_emerald_translation_runtime_test.lua +++ b/tests/engine/game3_emerald_translation_runtime_test.lua @@ -1,6 +1,9 @@ package.path = "./?.lua;./?/init.lua;" .. package.path local Strings = require("src.core.Strings") +local Mapsec = require("src.ui.game3.rse.mapsec") +local RomText = require("src.core.game3.rom_text") +local FrlgFont = require("src.ui.game3.frlg_font") local Pokemon = require("src.core.game3.pokemon") local BattleText = require("src.core.game3.battle.battle_text") local SummaryData = require("src.core.game3.summary_data") @@ -8,6 +11,59 @@ local SummaryData = require("src.core.game3.summary_data") Pokemon._abilityNames = { [9] = "STATIC" } Pokemon._romAbilityNames = { [9] = "STATIC" } +Mapsec.readLua = function(rel) + assert(rel == "pokemon/pokedex/entries.lua") + return { + [252] = { + category = "Wood Gecko", + description = "A small gecko.", + height = 5, + weight = 50, + }, + } +end +RomText.plain = function(key) return key end +FrlgFont.measure = function(text) return #text end +Pokemon.name = function() return "TREECKO" end + +local Pokedex = require("src.ui.game3.rse.pokedex") +Pokedex.speciesOf = function() return 252 end +Pokedex.hoennNumber = function() return 1 end + +local function info() + return Pokedex.monInfo({}, 252, false, true, false) +end + +-- pokeemerald/src/starter_choose.c:579 and battle_factory_screen.c:2007 print +-- the same CopyMonCategoryText string as the Pokédex. +local Kit = require("src.ui.game3.rse.scene_kit") +Kit.loadLua = function(rel) + if rel == "data/generated/gba/region_map/map_sections.lua" then + return { sections = { [0] = { name = "LITTLEROOT TOWN" } } } + end + -- the Pokédex reads its layout from the graphics manifest (Emerald's, not + -- Ruby/Sapphire's native one) + if rel == "data/generated/gba/rse/pokedex/manifest.lua" then return {} end + assert(rel == "data/generated/gba/pokemon/dex.lua", rel) + return { [252] = { category = "Wood Gecko" } } +end +Pokemon.national = function(species) return species end +Pokemon._names = { [252] = "TREECKO" } -- keeps the starter label from reinstalling the packs +local StarterChoose = require("src.ui.game3.rse.starter_choose") +StarterChoose.species = function() return 252 end +local function starterCategory() + local view = setmetatable({ man = {}, selection = 1 }, StarterChoose) + view:_createLabel() + return view.label.category +end + +local PokeblockGfx = require("src.ui.game3.rse.pokeblock_gfx") +PokeblockGfx.loadLua = function(rel) + assert(rel == "data/generated/gba/pokemon/pokedex/entries.lua", rel) + return { [252] = { category = "Wood Gecko" } } +end +local FactoryCommon = require("src.ui.game3.rse.factory_select").Common + -- pokeemerald/src/contest.c:3237 local ContestUI = require("src.ui.game3.rse.contest") ContestUI.has = function(key) return key == "gContestEffectDescriptionPointers[1]" end @@ -42,6 +98,15 @@ assert(SummaryData.contestCategoryName("Cool") == "Cool", assert(SummaryData.contestEffectDescription({ description = "Startles the audience." }) == "Startles the audience.", "a contest effect description keeps its English source without translations") +local english = info() +assert(english[3].text == "Wood Gecko gText_Pokemon", + "the category keeps its English source when no translation is loaded") +assert(english[#english].text == "A small gecko.", + "the description keeps its English source when no translation is loaded") +assert(starterCategory() == "Wood Gecko gText_Pokemon", + "the starter label keeps its English category without translations") +assert(FactoryCommon.categoryText(252) == "Wood Gecko gText_Pokemon", + "the Battle Factory keeps its English category without translations") assert(contestMoveDescription() == "Startles the audience.", "the contest move window keeps its English effect text without translations") assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keys an unlisted ability by its ROM name") @@ -50,6 +115,8 @@ Strings.load({ strings = { STATIC = "STATIQUE", Cool = "Sang-froid", ["Startles the audience."] = "Surprend le public.", + ["Wood Gecko"] = "Gecko des bois", + ["A small gecko."] = "Un petit gecko.", ["NEW SKILL"] = "NOUVEAU TALENT", } }) assert(Pokemon.abilityName(9) == "STATIQUE", @@ -63,10 +130,24 @@ assert(SummaryData.contestCategoryName("Cool") == "Sang-froid", assert(SummaryData.contestEffectDescription({ description = "Startles the audience." }) == "Surprend le public.", "the RSE summary and move relearner translate the contest effect source") +local french = info() +assert(french[3].text == "Gecko des bois gText_Pokemon", + "the RSE Pokédex translates the source category at presentation time") +assert(french[#french].text == "Un petit gecko.", + "the RSE Pokédex translates the source description at presentation time") +assert(starterCategory() == "Gecko des bois gText_Pokemon", + "the starter label translates the category like the Pokédex") +assert(FactoryCommon.categoryText(252) == "Gecko des bois gText_Pokemon", + "the Battle Factory translates the category like the Pokédex") assert(contestMoveDescription() == "Surprend le public.", "the contest move window translates the effect text like the summary") assert(Pokemon.abilityName(200) == "NOUVEAU TALENT", "the display name is translated") assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keeps keying on the ROM name when the display name is translated") +Strings.load({}) +local reloaded = info() +assert(reloaded[#reloaded].text == "A small gecko.", + "reloading without a catalog restores the English description") + print("game3_emerald_translation_runtime_test: PASS") From e23432306031da2ea373487f7d0f58a7d8432b39 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:49 +0200 Subject: [PATCH 04/11] Translate Emerald map-section names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mapsec.name() returns Strings(name): every caller displays the name, so the region map, the PokéNav, the map name popup, Match Call, TV and the other screens that print it follow, and the RSE summary's met location is translated the same way. --- src/ui/game3/rse/mapsec.lua | 5 +- src/ui/game3/rse/summary_menu.lua | 3 +- ...game3_emerald_translation_runtime_test.lua | 48 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/ui/game3/rse/mapsec.lua b/src/ui/game3/rse/mapsec.lua index adee424c..7dcd1edf 100644 --- a/src/ui/game3/rse/mapsec.lua +++ b/src/ui/game3/rse/mapsec.lua @@ -1,4 +1,5 @@ local MapSections = require("src.import.gba.rse.map_sections_extract") +local Strings = require("src.core.Strings") local Mapsec = {} @@ -43,9 +44,11 @@ function Mapsec.entry(sec) end -- pokeemerald/src/region_map.c:1568 +-- Every caller displays the name, so it goes through Strings() here, keyed by +-- the English name like FireRed's region map. function Mapsec.name(sec) local e = Mapsec.entry(sec) - return e and e.name or "" + return e and e.name and Strings(e.name) or "" end -- pokeemerald/src/map_name_popup.c:403 diff --git a/src/ui/game3/rse/summary_menu.lua b/src/ui/game3/rse/summary_menu.lua index c4d1ca3d..7ec92bab 100644 --- a/src/ui/game3/rse/summary_menu.lua +++ b/src/ui/game3/rse/summary_menu.lua @@ -5,6 +5,7 @@ local RomText = require("src.core.game3.rom_text") local Pokemon = require("src.core.game3.pokemon") local ItemsData = require("src.core.game3.items_data") local SummaryData = require("src.core.game3.summary_data") +local Strings = require("src.core.Strings") local RseSummary = {} @@ -416,7 +417,7 @@ local function drawInfo(m, Sm, mon, egg) dynamic = { [0] = RomText.ir("sMemoNatureTextColor"), [1] = RomText.ir("sMemoMiscTextColor"), [2] = RomText.at("gNatureNamePointers", natureId), [3] = tostring(metLevel == 0 and 5 or metLevel), - [4] = secName or "", [5] = RomText.ir("gText_EmptyString5"), + [4] = secName and Strings(secName) or "", [5] = RomText.ir("gText_EmptyString5"), }, }) end diff --git a/tests/engine/game3_emerald_translation_runtime_test.lua b/tests/engine/game3_emerald_translation_runtime_test.lua index 8e4c5ea6..11775b06 100644 --- a/tests/engine/game3_emerald_translation_runtime_test.lua +++ b/tests/engine/game3_emerald_translation_runtime_test.lua @@ -82,6 +82,44 @@ local function contestMoveDescription() return view.win[10].text end +-- pokeemerald/src/region_map.c:1568 +Mapsec.pack = function() + return { count = 1, sections = { [0] = { name = "LITTLEROOT TOWN" } } } +end + +-- pokeemerald/src/pokemon_summary_screen.c:3116: the trainer memo's met +-- location is a map section name. +local RseSummary = require("src.ui.game3.rse.summary_menu") +local PalText = require("src.ui.game3.rse.pal_text") +local drawInfo +for i = 1, 60 do + local name, value = debug.getupvalue(RseSummary.draw, i) + if not name then break end + if name == "drawInfo" then drawInfo = value; break end +end +assert(drawInfo, "RSE summary info page drawer") +RomText.ir = function() return {} end +RomText.at = function() return {} end +PalText.width = function() return 0 end +Pokemon.types = function() return {} end +Pokemon.speciesOf = function() return 252 end +SummaryData.nature = function() return 0 end +local function metLocation() + local shown + local draw = PalText.draw + PalText.draw = function(_, _, _, ctx) + if ctx and ctx.dynamic then shown = ctx.dynamic[4] end + end + local w = { left = 0, top = 0, paletteNum = 0 } + local m = { palette = {}, textColors = {}, windows = {}, pageWindows = { info = { w, w, w, w } } } + local describe = SummaryData.abilityDescription + SummaryData.abilityDescription = function() return "" end + drawInfo(m, { _playerState = { name = "MAY", trainerId = 1 } }, + { species = 252, ability = 9, otName = "MAY", otId = 1, metLevel = 5, metLocation = 0 }, false) + PalText.draw, SummaryData.abilityDescription = draw, describe + return shown +end + -- A modded ability past the built-in id table resolves to a key from its name. local Adapter = require("src.core.game3.battle.adapter") Pokemon._abilityNames[200] = "NEW SKILL" @@ -109,6 +147,11 @@ assert(FactoryCommon.categoryText(252) == "Wood Gecko gText_Pokemon", "the Battle Factory keeps its English category without translations") assert(contestMoveDescription() == "Startles the audience.", "the contest move window keeps its English effect text without translations") +assert(Mapsec.name(0) == "LITTLEROOT TOWN", + "a map section keeps its English name without translations") +assert(Mapsec.name(99) == "", "an unknown map section has no name") +assert(metLocation() == "LITTLEROOT TOWN", + "the RSE summary keeps the English met location without translations") assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keys an unlisted ability by its ROM name") Strings.load({ strings = { @@ -117,6 +160,7 @@ Strings.load({ strings = { ["Startles the audience."] = "Surprend le public.", ["Wood Gecko"] = "Gecko des bois", ["A small gecko."] = "Un petit gecko.", + ["LITTLEROOT TOWN"] = "BOURG-EN-VOL", ["NEW SKILL"] = "NOUVEAU TALENT", } }) assert(Pokemon.abilityName(9) == "STATIQUE", @@ -141,6 +185,10 @@ assert(FactoryCommon.categoryText(252) == "Gecko des bois gText_Pokemon", "the Battle Factory translates the category like the Pokédex") assert(contestMoveDescription() == "Surprend le public.", "the contest move window translates the effect text like the summary") +assert(Mapsec.name(0) == "BOURG-EN-VOL", + "the Emerald map section name goes through the registry") +assert(metLocation() == "BOURG-EN-VOL", + "the RSE summary translates the met location like the region map") assert(Pokemon.abilityName(200) == "NOUVEAU TALENT", "the display name is translated") assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keeps keying on the ROM name when the display name is translated") From 6eb2b6413b41564c5f8278e0c207c4b07e2283ab Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:50 +0200 Subject: [PATCH 05/11] Read the Battle Frontier's cart text from the script cache Several Battle Frontier screens print a cart string from the pack their extractor wrote, so a text override never reaches them although the script cache holds the same string. RomText.irOr() and RomText.refIr() read the cache's text by label, or by the symbol and address a pack reference keeps, and fall back on the pack's copy. The records window, the S.S. Tidal menu, the Battle Dome, the Battle Arena, the Apprentice, the Frontier Pass, the Trainer Hill records and the Pyramid bag's "Return to" line use them; the Dome's and the Arena's strings, kept without a symbol, are read by the label of their table. --- src/core/game3/battle/facility_arena.lua | 22 +++--- src/core/game3/rom_text.lua | 15 ++++ src/core/game3/rse/frontier/apprentice.lua | 2 +- src/core/game3/rse/frontier/dome.lua | 18 ++++- src/core/game3/scripting/natives_frontier.lua | 3 +- src/ui/game3/rse/dome_tourney.lua | 7 +- src/ui/game3/rse/frontier_pass.lua | 30 +++++-- src/ui/game3/rse/frontier_records.lua | 8 +- src/ui/game3/rse/pyramid_bag.lua | 2 +- src/ui/game3/rse/trainer_hill_records.lua | 2 +- tests/engine/game3_emerald_pack_text_test.lua | 79 +++++++++++++++++++ 11 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 tests/engine/game3_emerald_pack_text_test.lua diff --git a/src/core/game3/battle/facility_arena.lua b/src/core/game3/battle/facility_arena.lua index 422d370f..2a0c4ab7 100644 --- a/src/core/game3/battle/facility_arena.lua +++ b/src/core/game3/battle/facility_arena.lua @@ -228,7 +228,8 @@ local function fill(st, extra) end function Fac:refText(st, which, extra) - local ir = Data.arena().refereeStrings[which + 1] + local RomText = require("src.core.game3.rom_text") + local ir = RomText.irOr(RomText.key("gRefereeStringsTable", which), Data.arena().refereeStrings[which + 1]) return Data.battleText(ir, fill(st, extra)) end @@ -519,16 +520,17 @@ function Fac:draw(st) Chrome.userFrame(Chrome._frameType or 0, 6, 1, 18, 12) local W, I = A.windows, A.textInfo local fl = fill(st, j.buffs) - local function put(win, ir) - windowText(W[win + 1], I[win + 1], fontText(ir, fl)) + local RomText = require("src.core.game3.rom_text") + local function put(win, label, ir) + windowText(W[win + 1], I[win + 1], fontText(RomText.irOr(label, ir), fl)) end - put(Fac.WIN.PLAYER_NAME, A.text.playerMon1Name) - put(Fac.WIN.VS, A.text.vs) - put(Fac.WIN.OPPONENT_NAME, A.text.opponentMon1Name) - put(Fac.WIN.MIND, A.text.mind) - put(Fac.WIN.SKILL, A.text.skill) - put(Fac.WIN.BODY, A.text.body) - put(Fac.WIN.JUDGMENT_TITLE, A.text.judgment) + put(Fac.WIN.PLAYER_NAME, "gText_PlayerMon1Name", A.text.playerMon1Name) + put(Fac.WIN.VS, "gText_Vs", A.text.vs) + put(Fac.WIN.OPPONENT_NAME, "gText_OpponentMon1Name", A.text.opponentMon1Name) + put(Fac.WIN.MIND, "gText_Mind", A.text.mind) + put(Fac.WIN.SKILL, "gText_Skill", A.text.skill) + put(Fac.WIN.BODY, "gText_Body", A.text.body) + put(Fac.WIN.JUDGMENT_TITLE, "gText_Judgment", A.text.judgment) if j.line then local img = self:iconImage(Fac.ANIM.LINE) for i = 0, 7 do love.graphics.draw(img, 64 + i * 16 - 8, 84 - 8) end diff --git a/src/core/game3/rom_text.lua b/src/core/game3/rom_text.lua index 52f7ac48..c7c9f1d5 100644 --- a/src/core/game3/rom_text.lua +++ b/src/core/game3/rom_text.lua @@ -56,6 +56,21 @@ function RomText.has(key) return cachedText(b, key) ~= nil end +-- A cart string a screen also keeps a copy of in its own pack: the script +-- cache's text under `key` when the cache holds it (a mod's text overrides +-- land there), else that copy. +function RomText.irOr(key, fallback) + if key ~= nil and RomText.has(key) then return RomText.ir(key) end + return fallback +end + +-- A pack's text reference, { name = , key = "g3:
", +-- ir = }: the script cache's text by its symbol, then by its address. +function RomText.refIr(ref) + if type(ref) ~= "table" then return nil end + return RomText.irOr(ref.name, RomText.irOr(ref.key, ref.ir)) +end + local SOURCE_FORMS = {} for _, named in ipairs({ false, true }) do for _, nl in ipairs({ "\n", "\\n" }) do diff --git a/src/core/game3/rse/frontier/apprentice.lua b/src/core/game3/rse/frontier/apprentice.lua index 723fba25..535bdbdf 100644 --- a/src/core/game3/rse/frontier/apprentice.lua +++ b/src/core/game3/rse/frontier/apprentice.lua @@ -593,7 +593,7 @@ function Apprentice.message(sess, which) local e = map[which] if not e then return nil end local ref = e[1][id] and e[1][id][e[2]] - return ref and ref.ir + return ref and require("src.core.game3.rom_text").refIr(ref) end -- pokeemerald/src/apprentice.c:564 diff --git a/src/core/game3/rse/frontier/dome.lua b/src/core/game3/rse/frontier/dome.lua index 096e5000..2d7e2d47 100644 --- a/src/core/game3/rse/frontier/dome.lua +++ b/src/core/game3/rse/frontier/dome.lua @@ -617,10 +617,19 @@ local function setStr(ctx, adapters, n, v) Util.setStringVar(ctx, adapters, n, v) end +-- The i-th string of one of pret's tables, from the script cache, which keys +-- it by the table's label (a mod's text overrides land there), else the copy +-- this pack holds. +local function tableText(list, name, i) + local RomText = require("src.core.game3.rom_text") + return RomText.irOr(RomText.key(name, i - 1), list[i]) +end +Dome.tableText = tableText + -- pokeemerald/src/battle_dome.c:2564 function Dome.roundText(sess) local f = Util.frontier(sess) - return Data.dome().text.rounds[(tonumber(f.curChallengeBattleNum) or 0) + 1] + return tableText(Data.dome().text.rounds, "gRoundsStringTable", (tonumber(f.curChallengeBattleNum) or 0) + 1) end -- pokeemerald/src/battle_dome.c:3028 @@ -1033,7 +1042,8 @@ function Dome.trainerCard(sess, tid) card.species[i] = facilityMon(sess, f.domeMonIds[tid + 1][i]).species end end - card.potential = M.text.potential[(trainerId == D.TRAINER_FRONTIER_BRAIN) and Dome.TRAINERS_COUNT + 1 or tid + 1] + card.potential = tableText(M.text.potential, "sBattleDomePotentialTexts", + (trainerId == D.TRAINER_FRONTIER_BRAIN) and Dome.TRAINERS_COUNT + 1 or tid + 1) local pts = {} for k = 0, Dome.NUM_MOVE_POINT_TYPES - 1 do pts[k] = 0 end for i = 0, D.PARTY_SIZE - 1 do @@ -1061,8 +1071,8 @@ function Dome.trainerCard(sess, tid) style = style + 1 end card.style = style - card.styleText = M.text.styles[style + 1] - card.statText = M.text.stats[Dome.statTextId(sess, tid) + 1] + card.styleText = tableText(M.text.styles, "sBattleDomeOpponentStyleTexts", style + 1) + card.statText = tableText(M.text.stats, "sBattleDomeOpponentStatsTexts", Dome.statTextId(sess, tid) + 1) return card end diff --git a/src/core/game3/scripting/natives_frontier.lua b/src/core/game3/scripting/natives_frontier.lua index 6a1d5d82..7e9b1b91 100644 --- a/src/core/game3/scripting/natives_frontier.lua +++ b/src/core/game3/scripting/natives_frontier.lua @@ -1,6 +1,7 @@ local Rse = require("src.core.game3.rse.init") local D = require("src.core.game3.rse.frontier.trainers") local Util = require("src.core.game3.rse.frontier.util") +local RomText = require("src.core.game3.rom_text") local NativesFrontier = {} @@ -307,7 +308,7 @@ function NativesFrontier.ssTidalMultichoice(ctx, adapters) local FrlgFont = require("src.ui.game3.frlg_font") for i, id in ipairs(sel) do local ref = D.manifest().ssTidalDestinations[id + 1] - labels[i] = TextIR.toPlain(ref.ir, {}) + labels[i] = TextIR.toPlain(RomText.refIr(ref), {}) local w = FrlgFont.measure(labels[i]) if w > widest then widest = w end end diff --git a/src/ui/game3/rse/dome_tourney.lua b/src/ui/game3/rse/dome_tourney.lua index f7c92916..7df9ed57 100644 --- a/src/ui/game3/rse/dome_tourney.lua +++ b/src/ui/game3/rse/dome_tourney.lua @@ -280,7 +280,7 @@ local function drawTree() drawLines() love.graphics.draw(treeImage(), 0, 0) local W = M().treeWindows - local title = textOf(M().text.battleTourney) + local title = textOf(require("src.core.game3.rom_text").irOr("gText_BattleTourney", M().text.battleTourney)) local tw = W[3] drawSpaced(title, tw.left * 8 + centerX(title, 0x70, "short", 2), tw.top * 8 + 1, colors(COLOR.DYNAMIC_5, COLOR.DYNAMIC_4), "short", 2) @@ -425,12 +425,13 @@ local function drawMatchCard(card, ox, oy) local W = M().cardWindows local c = colors(COLOR.DYNAMIC_5, COLOR.DYNAMIC_4) local d = card.data - local wt = textOf(M().text.wins[d.win.id + 1], { d.win.var1, d.win.var2 }) + local Dome = require("src.core.game3.rse.frontier.dome") + local wt = textOf(Dome.tableText(M().text.wins, "sBattleDomeWinTexts", d.win.id + 1), { d.win.var1, d.win.var2 }) winText(W[9], wt, 0, 0, c, "normal", 0, ox, oy) local left, right = d.sides[1], d.sides[2] winText(W[7], left.name, centerX(left.name, 0x40, "short", 2), 2, c, "short", 2, ox, oy) winText(W[8], right.name, centerX(right.name, 0x40, "short", 2), 2, c, "short", 2, ox, oy) - local mn = textOf(M().text.matchNumbers[d.matchNo + 1]) + local mn = textOf(Dome.tableText(M().text.matchNumbers, "sBattleDomeMatchNumberTexts", d.matchNo + 1)) winText(W[6], mn, centerX(mn, 0xA0, "short", 0), 2, c, "short", 0, ox, oy) drawPic(picFor(left.trainerId), ox + 48, oy + 88, left.lost) drawPic(picFor(right.trainerId), ox + 192, oy + 88, right.lost) diff --git a/src/ui/game3/rse/frontier_pass.lua b/src/ui/game3/rse/frontier_pass.lua index 3c1b963f..9aef1350 100644 --- a/src/ui/game3/rse/frontier_pass.lua +++ b/src/ui/game3/rse/frontier_pass.lua @@ -149,12 +149,32 @@ local function updateHighlight(cur, prev) local _ = g end +-- pokeemerald/src/frontier_pass.c:530 +local function description(i) + return RomText.irOr(RomText.key("sPassAreaDescriptions", i), man().descriptions[i + 1]) +end + +-- pokeemerald/src/frontier_pass.c:556 +local LANDMARK_TEXTS = { + { "gText_BattleTower3", "gText_BattleTowerDesc" }, + { "gText_BattleDome2", "gText_BattleDomeDesc" }, + { "gText_BattlePalace2", "gText_BattlePalaceDesc" }, + { "gText_BattleArena2", "gText_BattleArenaDesc" }, + { "gText_BattleFactory2", "gText_BattleFactoryDesc" }, + { "gText_BattlePike2", "gText_BattlePikeDesc" }, + { "gText_BattlePyramid2", "gText_BattlePyramidDesc" }, +} + +local function landmarkText(i, field) + local labels = LANDMARK_TEXTS[i + 1] + return RomText.irOr(labels and labels[field == "name" and 1 or 2], man().landmarks[i + 1][field]) +end + local function describe(area) - local d = man().descriptions if area == A.RECORD and not st.hasRecord then - st.desc = textOf(d[A.NOTHING + 1]) + st.desc = textOf(description(A.NOTHING)) elseif area ~= A.NOTHING then - st.desc = textOf(d[area + 1]) + st.desc = textOf(description(area)) else st.desc = nil end @@ -544,9 +564,9 @@ function Map.draw() love.graphics.draw(Map.image, 0, 0) local lm = man().landmarks for i = 0, NUM_FACILITIES - 1 do - drawText(textOf(lm[i + 1].name), 160 + 4, 8 + i * 16 + 1, colors(i == Map.pos and 2 or 1), "narrow") + drawText(textOf(landmarkText(i, "name")), 160 + 4, 8 + i * 16 + 1, colors(i == Map.pos and 2 or 1), "narrow") end - drawText(textOf(lm[Map.pos + 1].description), 16 + 4, 128, colors(0)) + drawText(textOf(landmarkText(Map.pos, "description")), 16 + 4, 128, colors(0)) local cur = spriteFrame("cursor", 4, 16, 16, p.cursor) love.graphics.draw(cur, 155 + 8, Map.cursorY - 8, 0, -1, 1) local mark = lm[Map.pos + 1] diff --git a/src/ui/game3/rse/frontier_records.lua b/src/ui/game3/rse/frontier_records.lua index 4f4214a1..4d1e2304 100644 --- a/src/ui/game3/rse/frontier_records.lua +++ b/src/ui/game3/rse/frontier_records.lua @@ -338,12 +338,12 @@ function Records.buildRankingHall(sess, hallId, lvlMode) local win = newWindow(HALL_TPL) local man = D().manifest() local pair = man.recordsChallengeTexts[hallId + 1] - local facilityName = plainIr(pair[1].ir) - put(win, plainIr(pair[2].ir, { facilityName }), 0, 1) - local lvl = plainIr(man.levelModeText[lvlMode + 1].ir) + local facilityName = plainIr(RomText.refIr(pair[1])) + put(win, plainIr(RomText.refIr(pair[2]), { facilityName }), 0, 1) + local lvl = plainIr(RomText.refIr(man.levelModeText[lvlMode + 1])) put(win, lvl, (240 - 32) - width(lvl), 1) local rows = U().rankingHall(sess, hallId, lvlMode) - local recordText = man.hallFacilityToRecordsText[hallId + 1].ir + local recordText = RomText.refIr(man.hallFacilityToRecordsText[hallId + 1]) local link = hallId == U().RANKING_HALL.TOWER_LINK for i, r in ipairs(rows) do local pos = i - 1 diff --git a/src/ui/game3/rse/pyramid_bag.lua b/src/ui/game3/rse/pyramid_bag.lua index 6f59c6ac..3582425d 100644 --- a/src/ui/game3/rse/pyramid_bag.lua +++ b/src/ui/game3/rse/pyramid_bag.lua @@ -467,7 +467,7 @@ local function description() if item then return ItemsData.description(item) or "" end local ret = Py().manifest().bagReturnTo[(PBag.LOCATION[st.location] or 0) + 1] local TextIR = require("src.core.game3.scripting.text_ir") - return RomText.plain("gText_ReturnToVar1", { stringVars = { ret and TextIR.toPlain(ret.ir, {}) or "" } }) + return RomText.plain("gText_ReturnToVar1", { stringVars = { ret and TextIR.toPlain(RomText.refIr(ret), {}) or "" } }) end PBag.description = description diff --git a/src/ui/game3/rse/trainer_hill_records.lua b/src/ui/game3/rse/trainer_hill_records.lua index 95ffe7a1..b15a93f4 100644 --- a/src/ui/game3/rse/trainer_hill_records.lua +++ b/src/ui/game3/rse/trainer_hill_records.lua @@ -32,7 +32,7 @@ function Records.lines(sess) local y = 18 local modes = Hl.manifest().modeStrings for i = 1, Hl.NUM_MODES do - out[#out + 1] = { text = TextIR.toPlain(modes[i].ir, {}), x = 0, y = y } + out[#out + 1] = { text = TextIR.toPlain(RomText.refIr(modes[i]), {}), x = 0, y = y } y = y + 15 local m, s, f = Hl.timeParts(sess.trainerHillTimes[i]) local vars = { string.format("%2d", m), string.format("%2d", s), string.format("%02d", f) } diff --git a/tests/engine/game3_emerald_pack_text_test.lua b/tests/engine/game3_emerald_pack_text_test.lua new file mode 100644 index 00000000..62a9433e --- /dev/null +++ b/tests/engine/game3_emerald_pack_text_test.lua @@ -0,0 +1,79 @@ +-- 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") } +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 the field", "a place missing from the cache keeps the copy") + +if failed > 0 then + print(failed .. " check(s) failed") + os.exit(1) +end +print("all checks passed") From 09ace425f4d4f294f76204d0f3d3c8fc31ad76cd Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:50 +0200 Subject: [PATCH 06/11] Read the Pyramid's floor names and Ever Grande City's fly destinations from the script cache The map name popup prints a Battle Pyramid floor's name through RomText.refIr(), and the region map reads Ever Grande City's two fly destinations by sEverGrandeCityNames' labels, so both follow a text override. --- src/ui/game3/map_name_popup.lua | 2 +- src/ui/game3/rse/region_map.lua | 11 +++++++++-- tests/engine/game3_emerald_pack_text_test.lua | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/ui/game3/map_name_popup.lua b/src/ui/game3/map_name_popup.lua index 7319106b..25648907 100644 --- a/src/ui/game3/map_name_popup.lua +++ b/src/ui/game3/map_name_popup.lua @@ -155,7 +155,7 @@ local function rsePrint() local idx = pyramid.location(sess) == pyramid.LOCATION.TOP and #headers or (tonumber(pyramid.frontier(sess).curChallengeBattleNum) or 0) + 1 local ref = headers[idx] - if ref then name = require("src.core.game3.scripting.text_ir").toPlain(ref.ir) end + if ref then name = require("src.core.game3.scripting.text_ir").toPlain(RomText.refIr(ref)) end elseif sec == rseConstants():id("region_map_sections", "MAPSEC_SECRET_BASE") then -- pokeemerald/src/secret_base.c:735 local sb = R.system("secretBase") diff --git a/src/ui/game3/rse/region_map.lua b/src/ui/game3/rse/region_map.lua index d93cc41d..19b8d157 100644 --- a/src/ui/game3/rse/region_map.lua +++ b/src/ui/game3/rse/region_map.lua @@ -353,14 +353,21 @@ local function newState(opts) end RegionMap.newState = newState +-- pokeemerald/src/region_map.c:349: the name list each multi-name +-- destination points at, which the script cache keys by its label. +local MULTI_NAME_TABLES = { "sEverGrandeCityNames" } + -- pokeemerald/src/region_map.c:1760 function RegionMap.updateFlyText(s) s.flyText = nil if s.mapSecType > TYPE.NONE then - for _, m in ipairs(RegionMap.manifest().multiNameFlyDestinations) do + for i, m in ipairs(RegionMap.manifest().multiNameFlyDestinations) do if s.mapSecId == m.mapSecId then if flagSet(s, m.flag) then - s.flyText = { tall = true, name = s.mapSecName, sub = m.names[s.posWithinMapSec + 1] or "" } + local sub = m.names[s.posWithinMapSec + 1] or "" + local key = MULTI_NAME_TABLES[i] and RomText.key(MULTI_NAME_TABLES[i], s.posWithinMapSec) + if key and RomText.has(key) then sub = RomText.plain(key) end + s.flyText = { tall = true, name = s.mapSecName, sub = sub } end break end diff --git a/tests/engine/game3_emerald_pack_text_test.lua b/tests/engine/game3_emerald_pack_text_test.lua index 62a9433e..c4f3a6fd 100644 --- a/tests/engine/game3_emerald_pack_text_test.lua +++ b/tests/engine/game3_emerald_pack_text_test.lua @@ -72,6 +72,22 @@ check(PyramidBag.description() == "Retourner au jeu", "the bag's return line nam BUNDLE.text.gText_TheField = nil 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") + if failed > 0 then print(failed .. " check(s) failed") os.exit(1) From 62e62842abfe4bbe5121f9e534259c1c4dc7e00b Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:50 +0200 Subject: [PATCH 07/11] =?UTF-8?q?Read=20the=20Emerald=20Pok=C3=A9dex=20sea?= =?UTF-8?q?rch=20screen's=20text=20from=20the=20cart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search screen's option titles and descriptions and its help lines come from the chrome pack's English copies. They are read by their pret labels instead (gText_DexSortNumericalTitle, gText_SearchForPkmnBasedOnParameters, gTypeNames[type]...), with the copies as the fallback. --- src/ui/game3/rse/pokedex.lua | 90 ++++++++++++++++--- tests/engine/game3_emerald_pack_text_test.lua | 32 +++++++ 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/src/ui/game3/rse/pokedex.lua b/src/ui/game3/rse/pokedex.lua index 31fd9e80..f907064e 100644 --- a/src/ui/game3/rse/pokedex.lua +++ b/src/ui/game3/rse/pokedex.lua @@ -1593,8 +1593,57 @@ function tasks.switchFromSize(s) end end +-- The search screen's strings, by the labels the script cache keys them +-- under (a mod's text overrides land there); the chrome pack keeps English +-- copies without their labels, the fallback. +local function cartText(key, fallback) + if key and RomText.has(key) then return RomText.plain(key) end + return fallback +end + +-- pokeemerald/src/pokedex.c:1330: { description, title } of each option. +local TYPE_OPTION = { "gText_DexSearchTypeNone" } +for _, t in ipairs({ 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17 }) do + TYPE_OPTION[#TYPE_OPTION + 1] = RomText.key("gTypeNames", t) +end +local SEARCH_OPTION_TEXTS = { + [SEARCH.NAME] = { titles = { "gText_DexSearchDontSpecify", "gText_DexSearchAlphaABC", "gText_DexSearchAlphaDEF", + "gText_DexSearchAlphaGHI", "gText_DexSearchAlphaJKL", "gText_DexSearchAlphaMNO", "gText_DexSearchAlphaPQR", + "gText_DexSearchAlphaSTU", "gText_DexSearchAlphaVWX", "gText_DexSearchAlphaYZ" } }, + [SEARCH.COLOR] = { titles = { "gText_DexSearchDontSpecify", "gText_DexSearchColorRed", "gText_DexSearchColorBlue", + "gText_DexSearchColorYellow", "gText_DexSearchColorGreen", "gText_DexSearchColorBlack", "gText_DexSearchColorBrown", + "gText_DexSearchColorPurple", "gText_DexSearchColorGray", "gText_DexSearchColorWhite", "gText_DexSearchColorPink" } }, + [SEARCH.TYPE_LEFT] = { titles = TYPE_OPTION }, + [SEARCH.TYPE_RIGHT] = { titles = TYPE_OPTION }, + [SEARCH.ORDER] = { + titles = { "gText_DexSortNumericalTitle", "gText_DexSortAtoZTitle", "gText_DexSortHeaviestTitle", + "gText_DexSortLightestTitle", "gText_DexSortTallestTitle", "gText_DexSortSmallestTitle" }, + descriptions = { "gText_DexSortNumericalDescription", "gText_DexSortAtoZDescription", + "gText_DexSortHeaviestDescription", "gText_DexSortLightestDescription", "gText_DexSortTallestDescription", + "gText_DexSortSmallestDescription" }, + }, + [SEARCH.MODE] = { + titles = { "gText_DexHoennTitle", "gText_DexNatTitle" }, + descriptions = { "gText_DexHoennDescription", "gText_DexNatDescription" }, + }, +} +-- pokeemerald/src/pokedex.c:1017 +local TOPBAR_DESCRIPTIONS = { "gText_SearchForPkmnBasedOnParameters", "gText_SwitchPokedexListings", + "gText_ReturnToPokedex" } +-- pokeemerald/src/pokedex.c:1042 +local ITEM_DESCRIPTIONS = { "gText_ListByFirstLetter", "gText_ListByBodyColor", "gText_ListByType", + "gText_ListByType", "gText_SelectPokedexListingMode", "gText_SelectPokedexMode", "gText_ExecuteSearchSwitch" } + +local function topBarDescription(i) + return cartText(TOPBAR_DESCRIPTIONS[i + 1], Gfx.manifest().search.topBar[i + 1].description) +end + +local function itemDescription(i) + return cartText(ITEM_DESCRIPTIONS[i + 1], Gfx.manifest().search.items[i + 1].description) +end + -- pokeemerald/src/pokedex.c:1437 -local function searchOptionTexts(which) +local function searchOptionList(which) local sm = Gfx.manifest().search if which == SEARCH.NAME then return sm.names end if which == SEARCH.COLOR then return sm.colors end @@ -1604,6 +1653,23 @@ local function searchOptionTexts(which) return {} end +local function searchOptionTexts(which) + local list = searchOptionList(which) + local keys = SEARCH_OPTION_TEXTS[which] + if not keys then return list end + local out = {} + for i, t in ipairs(list) do + out[i] = { + title = cartText(keys.titles[i], t.title), + description = cartText(keys.descriptions and keys.descriptions[i], t.description), + } + end + return out +end +Pokedex.searchOptionTexts = searchOptionTexts +Pokedex.topBarDescription = topBarDescription +Pokedex.itemDescription = itemDescription + local function searchSel(q, which) local c = q.cursor[which] or 0 local o = q.scroll[which] or 0 @@ -1772,7 +1838,7 @@ function tasks.loadSearch(s) elseif st == 1 then local q = s.searchState setDefaultSearchModeAndOrder(s, q) - setSearchMessage(q, Gfx.manifest().search.topBar[TOPBAR.SEARCH + 1].description) + setSearchMessage(q, topBarDescription(TOPBAR.SEARCH)) s.searchArrows = { { x = 184, y = 4, down = false, data2 = 0, invisible = true }, { x = 184, y = 108, down = true, data2 = 0, invisible = true }, @@ -1790,7 +1856,7 @@ function tasks.loadSearch(s) s.state = 0 s.fn = "searchTopBar" s.searchState.phase = "topbar" - setSearchMessage(s.searchState, Gfx.manifest().search.topBar[s.searchState.topBar + 1].description) + setSearchMessage(s.searchState, topBarDescription(s.searchState.topBar)) refreshSearch(s) end end @@ -1800,7 +1866,6 @@ end function tasks.searchTopBar(s, inp) local new = inp.new or {} local q = s.searchState - local sm = Gfx.manifest().search if new.b then se("SE_PC_OFF") s.fn = "exitSearch" @@ -1821,20 +1886,20 @@ function tasks.searchTopBar(s, inp) return end q.phase = "menu" - setSearchMessage(q, sm.items[q.menuItem + 1].description) + setSearchMessage(q, itemDescription(q.menuItem)) refreshSearch(s) return end if new.left and q.topBar > TOPBAR.SEARCH then se("SE_DEX_PAGE") q.topBar = q.topBar - 1 - setSearchMessage(q, sm.topBar[q.topBar + 1].description) + setSearchMessage(q, topBarDescription(q.topBar)) refreshSearch(s) end if new.right and q.topBar < TOPBAR.CANCEL then se("SE_DEX_PAGE") q.topBar = q.topBar + 1 - setSearchMessage(q, sm.topBar[q.topBar + 1].description) + setSearchMessage(q, topBarDescription(q.topBar)) refreshSearch(s) end end @@ -1856,7 +1921,7 @@ function tasks.searchMenu(s, inp) setDefaultSearchModeAndOrder(s, q) q.phase = "topbar" s.fn = "searchTopBar" - setSearchMessage(q, sm.topBar[q.topBar + 1].description) + setSearchMessage(q, topBarDescription(q.topBar)) refreshSearch(s) return end @@ -1902,7 +1967,7 @@ function tasks.searchMenu(s, inp) if nxt ~= 0xFF then se(sound) q.menuItem = nxt - setSearchMessage(q, sm.items[q.menuItem + 1].description) + setSearchMessage(q, itemDescription(q.menuItem)) refreshSearch(s) end end @@ -1953,7 +2018,7 @@ function tasks.searchDone(s, inp) else s.fn = "searchMenu" q.phase = "menu" - setSearchMessage(q, Gfx.manifest().search.items[q.menuItem + 1].description) + setSearchMessage(q, itemDescription(q.menuItem)) refreshSearch(s) se("SE_BALL") end @@ -1975,7 +2040,7 @@ function tasks.searchParam(s, inp) end q.paramBox = false q.phase = "menu" - setSearchMessage(q, Gfx.manifest().search.items[mi + 1].description) + setSearchMessage(q, itemDescription(mi)) refreshSearch(s) s.fn = "searchMenu" return @@ -2144,8 +2209,7 @@ function Pokedex.frame(s, inp) end if s.searchArrows and s.fn == "searchParam" then local q = s.searchState - local texts = searchOptionTexts(q.menuItem) - local last = #texts - 1 + local last = #searchOptionList(q.menuItem) - 1 for _, a in ipairs(s.searchArrows) do local off = q.scroll[q.menuItem] or 0 if a.down then diff --git a/tests/engine/game3_emerald_pack_text_test.lua b/tests/engine/game3_emerald_pack_text_test.lua index c4f3a6fd..2fc31fcb 100644 --- a/tests/engine/game3_emerald_pack_text_test.lua +++ b/tests/engine/game3_emerald_pack_text_test.lua @@ -88,6 +88,38 @@ check(flySub() == "CENTRE POKéMON", "a fly destination reads its name from the 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") + if failed > 0 then print(failed .. " check(s) failed") os.exit(1) From 686bcc74f7a9d42783199ff21736d68c9315a410 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:50 +0200 Subject: [PATCH 08/11] Print Emerald's party menu actions from the cart's text The rse/menus pack's cursor options are plain English strings. The Emerald profile lists sCursorOptions' strings, an action prints the cart's text for its option and a field move prints the move's name, as the cart does; the pack's labels still identify the actions. --- src/core/game3/profiles/emerald/ui.lua | 8 ++++++++ src/ui/game3/party_menu.lua | 16 ++++++++++++--- tests/engine/game3_emerald_pack_text_test.lua | 20 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/core/game3/profiles/emerald/ui.lua b/src/core/game3/profiles/emerald/ui.lua index fd9eb6b6..28e90624 100644 --- a/src/core/game3/profiles/emerald/ui.lua +++ b/src/core/game3/profiles/emerald/ui.lua @@ -52,6 +52,14 @@ return { -- pokeemerald/src/menu_specialized.c:1503 levelUpStats = { "gText_MaxHP", "gText_Attack", "gText_Defense", "gText_SpAtk", "gText_SpDef", "gText_Speed" }, }, + -- pokeemerald/src/data/party_menu.h:658: sCursorOptions' strings up to + -- the field moves, which print gMoveNames. + cursorOptionTexts = { + "gText_Summary5", "gText_Switch2", "gText_Cancel2", "gText_Item", "gMenuText_Give", "gText_Take", + "gText_Mail", "gText_Take2", "gText_Read2", "gText_Cancel2", "gText_Shift", "gText_SendOut", + "gText_Enter", "gText_NoEntry", "gText_Store", "gText_Register", "gText_Trade4", "gText_Trade4", + "gMenuText_Toss", + }, -- pokeemerald/src/party_menu.c:2101 buttons = { cancel = "gText_Cancel", confirm = "gMenuText_Confirm" }, -- pokeemerald/src/party_menu.c:2557 diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index a4157498..55734d90 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -90,7 +90,7 @@ local function gameFieldMoves() index[label] = j - 1 byMove[move] = label end - return { labels = labels, base = base, index = index, byMove = byMove } + return { labels = labels, base = base, index = index, byMove = byMove, moves = moves, texts = p.cursorOptionTexts } end local function fieldMoveIndex() @@ -111,14 +111,24 @@ local function cursor_option_text(act) local p = partyUi() if p and p.actionText then return p.actionText(act, g) end if g then + -- The pack's labels are the English the cart prints; show the cart's + -- text instead (a mod's text overrides land there) and the move's name + -- for a field move, as sCursorOptions does. local fm = g.index[act] - if fm then return g.labels[g.base + fm + 1] end - return g.labels[(assert(CURSOR_OPTION[act], act)) + 1] + if fm then + local move = g.moves[fm + 1] + return move and Pokemon.moveName(move) or g.labels[g.base + fm + 1] + end + local i = assert(CURSOR_OPTION[act], act) + local key = g.texts and g.texts[i + 1] + if key and RomText.has(key) then return RomText.plain(key) end + return g.labels[i + 1] end local fm = FIELD_MOVE_INDEX[act] if fm then return RomText.at("sCursorOptions", CURSOR_OPTION_FIELD_MOVES + fm) end return RomText.at("sCursorOptions", (assert(CURSOR_OPTION[act], act))) end +PartyMenu._cursorOptionText = cursor_option_text local FR_INSETS = { msgX = 2, msgY = 2, actX = 9, actY = 2, cursorX = 1 } local function textInsets() diff --git a/tests/engine/game3_emerald_pack_text_test.lua b/tests/engine/game3_emerald_pack_text_test.lua index 2fc31fcb..b87ea947 100644 --- a/tests/engine/game3_emerald_pack_text_test.lua +++ b/tests/engine/game3_emerald_pack_text_test.lua @@ -120,6 +120,26 @@ check(Pokedex.topBarDescription(0) == "Chercher des POKéMON.", "a top bar descr 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") + if failed > 0 then print(failed .. " check(s) failed") os.exit(1) From 8b9165f12113974ff6d5aafa75d0ae8b510f3b99 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sun, 4 Oct 2026 09:24:38 +0200 Subject: [PATCH 09/11] Expand Emerald's placeholders from the script cache The runtime expanded {RIVAL} and {KUN} from text/placeholders.lua, the values the extractor reads from the US cart, so a translation never reached them: the rival stayed MAY or BRENDAN and the Japanese honorific stayed empty. Each value is a cart string the script cache also holds under its pret label (gText_ExpandedPlaceholder_May, _Brendan, _Kun, _Chan...); the placeholders now read it from there, with the extract's value as the fallback. --- src/ui/game3/message.lua | 25 +++++++++++++++++-- tests/engine/game3_emerald_pack_text_test.lua | 21 ++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/ui/game3/message.lua b/src/ui/game3/message.lua index 7405c8c7..ec52a093 100644 --- a/src/ui/game3/message.lua +++ b/src/ui/game3/message.lua @@ -41,6 +41,27 @@ end local placeholderCache = {} +-- pokeemerald/src/strings.c:6: each expanded placeholder is a cart string the +-- script cache also holds under its pret label, where a mod's text overrides +-- land (the rival's name, Japanese くん/ちゃん). A value comes from the cache +-- when it has the label, and from the extract otherwise. +local function cartPlaceholders(extracted) + local Extract = require("src.import.gba.text_placeholders_extract") + local RomText = require("src.core.game3.rom_text") + local function value(name) + local label = Extract.SYMBOLS[name] + if label and RomText.has(label) then return RomText.plain(label) end + return extracted[name] + end + local byGender = setmetatable({}, { __index = function(_, name) + local pair = Extract.BY_GENDER[name] + if not pair then return extracted.byGender and extracted.byGender[name] end + return { male = value(pair.male), female = value(pair.female) } + end }) + return setmetatable({ byGender = byGender }, { __index = function(_, name) return value(name) end }) +end +Message.cartPlaceholders = cartPlaceholders + -- pokeemerald/src/string_util.c:456 TextIR.setContextProvider(function(kind, dialect, ctx) if kind == "gender" then @@ -71,8 +92,8 @@ TextIR.setContextProvider(function(kind, dialect, ctx) local okC, CacheFs = pcall(require, "src.import.CacheFs") local t = okC and CacheFs.loadActive(dialect.placeholders) or nil if type(t) == "table" then - placeholderCache[id] = t - return t + placeholderCache[id] = cartPlaceholders(t) + return placeholderCache[id] end return nil end) diff --git a/tests/engine/game3_emerald_pack_text_test.lua b/tests/engine/game3_emerald_pack_text_test.lua index b87ea947..bd1e0ec5 100644 --- a/tests/engine/game3_emerald_pack_text_test.lua +++ b/tests/engine/game3_emerald_pack_text_test.lua @@ -140,6 +140,27 @@ check(PartyMenu._cursorOptionText("SWITCH") == "ORDRE", "a second party action r 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 placeholders = Message.cartPlaceholders({ + 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("くん") } +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") +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_Brendan = ir("BRICE") +BUNDLE.text.gText_ExpandedPlaceholder_Chan = ir("ちゃん") +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") + if failed > 0 then print(failed .. " check(s) failed") os.exit(1) From 2fd5d09228f7192de3d6c1a877753ce737a59f31 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:03:50 +0200 Subject: [PATCH 10/11] Resolve the translated texts once when each screen opens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/core/game3/battle/facility_arena.lua | 35 ++++++++----- src/ui/game3/message.lua | 28 ++++++----- src/ui/game3/party_menu.lua | 18 +++++-- src/ui/game3/rse/dome_tourney.lua | 21 ++++++-- src/ui/game3/rse/frontier_pass.lua | 8 ++- src/ui/game3/rse/pokedex.lua | 5 +- src/ui/game3/rse/pyramid_bag.lua | 11 +++-- tests/engine/game3_emerald_pack_text_test.lua | 49 +++++++++++++++++-- 8 files changed, 131 insertions(+), 44 deletions(-) diff --git a/src/core/game3/battle/facility_arena.lua b/src/core/game3/battle/facility_arena.lua index 2a0c4ab7..44d2c8f2 100644 --- a/src/core/game3/battle/facility_arena.lua +++ b/src/core/game3/battle/facility_arena.lua @@ -374,11 +374,32 @@ function Fac:runStep(st, step) return true end +-- pokeemerald/src/battle_arena.c:412 +local JUDGMENT_TEXTS = { + { Fac.WIN.PLAYER_NAME, "gText_PlayerMon1Name", "playerMon1Name" }, + { Fac.WIN.VS, "gText_Vs", "vs" }, + { Fac.WIN.OPPONENT_NAME, "gText_OpponentMon1Name", "opponentMon1Name" }, + { Fac.WIN.MIND, "gText_Mind", "mind" }, + { Fac.WIN.SKILL, "gText_Skill", "skill" }, + { Fac.WIN.BODY, "gText_Body", "body" }, + { Fac.WIN.JUDGMENT_TITLE, "gText_Judgment", "judgment" }, +} + +local function judgmentTexts() + local RomText = require("src.core.game3.rom_text") + local text, out = Data.arena().text, {} + for i, row in ipairs(JUDGMENT_TEXTS) do + out[i] = { win = row[1], ir = RomText.irOr(row[2], text[row[3]]) } + end + return out +end + -- pokeemerald/src/battle_arena.c:395 function Fac:judgeStep(st, step) local j = self.window if step.state == "open" then - self.window = { fade = 0, target = 8, icons = {}, line = true, buffs = { buff1 = "0", buff2 = "0" } } + self.window = { fade = 0, target = 8, icons = {}, line = true, buffs = { buff1 = "0", buff2 = "0" }, + texts = judgmentTexts() } self.fadeWait = true return false elseif step.state == "row" then @@ -520,17 +541,9 @@ function Fac:draw(st) Chrome.userFrame(Chrome._frameType or 0, 6, 1, 18, 12) local W, I = A.windows, A.textInfo local fl = fill(st, j.buffs) - local RomText = require("src.core.game3.rom_text") - local function put(win, label, ir) - windowText(W[win + 1], I[win + 1], fontText(RomText.irOr(label, ir), fl)) + for _, t in ipairs(j.texts) do + windowText(W[t.win + 1], I[t.win + 1], fontText(t.ir, fl)) end - put(Fac.WIN.PLAYER_NAME, "gText_PlayerMon1Name", A.text.playerMon1Name) - put(Fac.WIN.VS, "gText_Vs", A.text.vs) - put(Fac.WIN.OPPONENT_NAME, "gText_OpponentMon1Name", A.text.opponentMon1Name) - put(Fac.WIN.MIND, "gText_Mind", A.text.mind) - put(Fac.WIN.SKILL, "gText_Skill", A.text.skill) - put(Fac.WIN.BODY, "gText_Body", A.text.body) - put(Fac.WIN.JUDGMENT_TITLE, "gText_Judgment", A.text.judgment) if j.line then local img = self:iconImage(Fac.ANIM.LINE) for i = 0, 7 do love.graphics.draw(img, 64 + i * 16 - 8, 84 - 8) end diff --git a/src/ui/game3/message.lua b/src/ui/game3/message.lua index ec52a093..c1b24df5 100644 --- a/src/ui/game3/message.lua +++ b/src/ui/game3/message.lua @@ -41,10 +41,7 @@ end local placeholderCache = {} --- pokeemerald/src/strings.c:6: each expanded placeholder is a cart string the --- script cache also holds under its pret label, where a mod's text overrides --- land (the rival's name, Japanese くん/ちゃん). A value comes from the cache --- when it has the label, and from the extract otherwise. +-- pokeemerald/src/strings.c:6 local function cartPlaceholders(extracted) local Extract = require("src.import.gba.text_placeholders_extract") local RomText = require("src.core.game3.rom_text") @@ -53,12 +50,15 @@ local function cartPlaceholders(extracted) if label and RomText.has(label) then return RomText.plain(label) end return extracted[name] end - local byGender = setmetatable({}, { __index = function(_, name) - local pair = Extract.BY_GENDER[name] - if not pair then return extracted.byGender and extracted.byGender[name] end - return { male = value(pair.male), female = value(pair.female) } - end }) - return setmetatable({ byGender = byGender }, { __index = function(_, name) return value(name) end }) + local out, byGender = {}, {} + for name, v in pairs(extracted) do out[name] = v end + for name in pairs(Extract.SYMBOLS) do out[name] = value(name) end + for name, pair in pairs(extracted.byGender or {}) do byGender[name] = pair end + for name, pair in pairs(Extract.BY_GENDER) do + byGender[name] = { male = out[pair.male], female = out[pair.female] } + end + out.byGender = byGender + return out end Message.cartPlaceholders = cartPlaceholders @@ -87,13 +87,15 @@ TextIR.setContextProvider(function(kind, dialect, ctx) local GameVersion = require("src.core.GameVersion") local s = liveSession() local id = (s and s.version) or GameVersion.get() or "" + local Sp = package.loaded["src.core.game3.scripting.space"] + local bundle = Sp and Sp.bundle local hit = placeholderCache[id] - if hit then return hit end + if hit and hit.bundle == bundle then return hit.values end local okC, CacheFs = pcall(require, "src.import.CacheFs") local t = okC and CacheFs.loadActive(dialect.placeholders) or nil if type(t) == "table" then - placeholderCache[id] = cartPlaceholders(t) - return placeholderCache[id] + placeholderCache[id] = { bundle = bundle, values = cartPlaceholders(t) } + return placeholderCache[id].values end return nil end) diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index 55734d90..21c62abe 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -130,6 +130,16 @@ local function cursor_option_text(act) end PartyMenu._cursorOptionText = cursor_option_text +-- pokeemerald/src/party_menu.c:2557 +local function action_texts(list) + local cached = PartyMenu._actionTexts + if cached and cached.list == list then return cached.texts end + local texts = {} + for i, act in ipairs(list) do texts[i] = cursor_option_text(act) end + PartyMenu._actionTexts = { list = list, texts = texts } + return texts +end + local FR_INSETS = { msgX = 2, msgY = 2, actX = 9, actY = 2, cursorX = 1 } local function textInsets() local p = partyUi() @@ -2869,12 +2879,13 @@ function PartyMenu.draw() local popX = 22 local popY = 19 - popH Window.stdFrame(Window.template(popX, popY, popW, popH)) - for i, act in ipairs(PartyMenu.ITEM_ACTIONS) do + local texts = action_texts(PartyMenu.ITEM_ACTIONS) + for i in ipairs(PartyMenu.ITEM_ACTIONS) do local rowY = (popY * 8) + (i - 1) * 16 + ins.actY if i == PartyMenu.itemActionCursor then Window.cursorPx(popX * 8 + ins.cursorX, rowY) end - FrlgFont.draw(cursor_option_text(act), popX * 8 + ins.actX, rowY, { colors = FrlgFont.COLOR.NORMAL }) + FrlgFont.draw(texts[i], popX * 8 + ins.actX, rowY, { colors = FrlgFont.COLOR.NORMAL }) end elseif PartyMenu.mode == "action" and p and p.drawActions then p.drawActions(PartyMenu, false) @@ -2888,6 +2899,7 @@ function PartyMenu.draw() local popX = 19 local popY = 19 - popH Window.stdFrame(Window.template(popX, popY, popW, popH)) + local texts = action_texts(PartyMenu.ACTIONS) for i, act in ipairs(PartyMenu.ACTIONS) do local rowY = (popY * 8) + (i - 1) * 16 + ins.actY if i == PartyMenu.actionCursor then @@ -2895,7 +2907,7 @@ function PartyMenu.draw() end local isFm = PartyMenu._fieldMoveNames and PartyMenu._fieldMoveNames[act] local col = isFm and (FrlgFont.COLOR.BLUE or FrlgFont.COLOR.MALE_NPC) or FrlgFont.COLOR.NORMAL - FrlgFont.draw(cursor_option_text(act), popX * 8 + ins.actX, rowY, { colors = col }) + FrlgFont.draw(texts[i], popX * 8 + ins.actX, rowY, { colors = col }) end else if p and p.drawPrompt then diff --git a/src/ui/game3/rse/dome_tourney.lua b/src/ui/game3/rse/dome_tourney.lua index 7df9ed57..7ec1bfc5 100644 --- a/src/ui/game3/rse/dome_tourney.lua +++ b/src/ui/game3/rse/dome_tourney.lua @@ -280,7 +280,7 @@ local function drawTree() drawLines() love.graphics.draw(treeImage(), 0, 0) local W = M().treeWindows - local title = textOf(require("src.core.game3.rom_text").irOr("gText_BattleTourney", M().text.battleTourney)) + local title = st.treeTitle local tw = W[3] drawSpaced(title, tw.left * 8 + centerX(title, 0x70, "short", 2), tw.top * 8 + 1, colors(COLOR.DYNAMIC_5, COLOR.DYNAMIC_4), "short", 2) @@ -425,13 +425,12 @@ local function drawMatchCard(card, ox, oy) local W = M().cardWindows local c = colors(COLOR.DYNAMIC_5, COLOR.DYNAMIC_4) local d = card.data - local Dome = require("src.core.game3.rse.frontier.dome") - local wt = textOf(Dome.tableText(M().text.wins, "sBattleDomeWinTexts", d.win.id + 1), { d.win.var1, d.win.var2 }) + local wt = textOf(d.winIr, { d.win.var1, d.win.var2 }) winText(W[9], wt, 0, 0, c, "normal", 0, ox, oy) local left, right = d.sides[1], d.sides[2] winText(W[7], left.name, centerX(left.name, 0x40, "short", 2), 2, c, "short", 2, ox, oy) winText(W[8], right.name, centerX(right.name, 0x40, "short", 2), 2, c, "short", 2, ox, oy) - local mn = textOf(Dome.tableText(M().text.matchNumbers, "sBattleDomeMatchNumberTexts", d.matchNo + 1)) + local mn = textOf(d.matchNoIr) winText(W[6], mn, centerX(mn, 0xA0, "short", 0), 2, c, "short", 0, ox, oy) drawPic(picFor(left.trainerId), ox + 48, oy + 88, left.lost) drawPic(picFor(right.trainerId), ox + 192, oy + 88, right.lost) @@ -444,7 +443,17 @@ end local function makeCard(kind, id) if kind == "trainer" then return { kind = kind, id = id, data = trainerCardData(id) } end - return { kind = kind, id = id, data = matchCardData(id) } + local data = matchCardData(id) + local Dome = require("src.core.game3.rse.frontier.dome") + -- pokeemerald/src/battle_dome.c:4931, :4977 + data.winIr = Dome.tableText(M().text.wins, "sBattleDomeWinTexts", data.win.id + 1) + data.matchNoIr = Dome.tableText(M().text.matchNumbers, "sBattleDomeMatchNumberTexts", data.matchNo + 1) + return { kind = kind, id = id, data = data } +end + +-- pokeemerald/src/battle_dome.c:5415 +local function treeTitle() + return textOf(require("src.core.game3.rom_text").irOr("gText_BattleTourney", M().text.battleTourney)) end local function drawCard(card, ox, oy) @@ -542,6 +551,7 @@ function UI.showTree(opts) st.cursor = opts.cursor or 0 st.frame, st.bg2y, st.bg3y = 0, 0, 11 buildTree(st.mode) + st.treeTitle = treeTitle() st.phase = "fadein" fadeIn() push() @@ -597,6 +607,7 @@ local function backToTree() st.mode = rt.mode st.bg2y, st.bg3y = 0, 11 buildTree(st.mode) + st.treeTitle = st.treeTitle or treeTitle() st.phase = "fadein" fadeIn() end diff --git a/src/ui/game3/rse/frontier_pass.lua b/src/ui/game3/rse/frontier_pass.lua index 9aef1350..42a4ccb8 100644 --- a/src/ui/game3/rse/frontier_pass.lua +++ b/src/ui/game3/rse/frontier_pass.lua @@ -476,6 +476,10 @@ function Map.show() Map.pal:beginFade(PalFade.ALL, 0, 16, 0, PalFade.WHITE) Map.image = nil Map.head = Map.headPosition() + Map.landmarks = {} + for i = 0, NUM_FACILITIES - 1 do + Map.landmarks[i] = { name = textOf(landmarkText(i, "name")), description = textOf(landmarkText(i, "description")) } + end end -- pokeemerald/src/frontier_pass.c:1582 @@ -564,9 +568,9 @@ function Map.draw() love.graphics.draw(Map.image, 0, 0) local lm = man().landmarks for i = 0, NUM_FACILITIES - 1 do - drawText(textOf(landmarkText(i, "name")), 160 + 4, 8 + i * 16 + 1, colors(i == Map.pos and 2 or 1), "narrow") + drawText(Map.landmarks[i].name, 160 + 4, 8 + i * 16 + 1, colors(i == Map.pos and 2 or 1), "narrow") end - drawText(textOf(landmarkText(Map.pos, "description")), 16 + 4, 128, colors(0)) + drawText(Map.landmarks[Map.pos].description, 16 + 4, 128, colors(0)) local cur = spriteFrame("cursor", 4, 16, 16, p.cursor) love.graphics.draw(cur, 155 + 8, Map.cursorY - 8, 0, -1, 1) local mark = lm[Map.pos + 1] diff --git a/src/ui/game3/rse/pokedex.lua b/src/ui/game3/rse/pokedex.lua index f907064e..f62d2bd0 100644 --- a/src/ui/game3/rse/pokedex.lua +++ b/src/ui/game3/rse/pokedex.lua @@ -2030,8 +2030,7 @@ function tasks.searchParam(s, inp) local new, rep = inp.new or {}, inp.rep or {} local q = s.searchState local mi = q.menuItem - local texts = searchOptionTexts(mi) - local maxOption = #texts - 1 + local maxOption = #searchOptionList(mi) - 1 if new.a or new.b then se(new.a and "SE_PIN" or "SE_BALL") if new.b then @@ -2065,7 +2064,7 @@ function tasks.searchParam(s, inp) end if moved then se("SE_SELECT") - local t = texts[searchSel(q, mi) + 1] + local t = searchOptionTexts(mi)[searchSel(q, mi) + 1] setSearchMessage(q, t and t.description or "") refreshSearch(s) end diff --git a/src/ui/game3/rse/pyramid_bag.lua b/src/ui/game3/rse/pyramid_bag.lua index 3582425d..160ae726 100644 --- a/src/ui/game3/rse/pyramid_bag.lua +++ b/src/ui/game3/rse/pyramid_bag.lua @@ -100,11 +100,18 @@ local function itemName(id) end -- pokeemerald/src/battle_pyramid_bag.c:379 +-- pokeemerald/src/battle_pyramid_bag.c:687 +function PBag.returnTo(location) + local ret = Py().manifest().bagReturnTo[(PBag.LOCATION[location] or 0) + 1] + return ret and require("src.core.game3.scripting.text_ir").toPlain(RomText.refIr(ret), {}) or "" +end + function PBag.show(opts) opts = opts or {} for k in pairs(st) do st[k] = nil end st.session = opts.session st.location = opts.location or "field" + st.returnTo = PBag.returnTo(st.location) st.onClose = opts.onClose st.onUse = opts.onUse st.onGive = opts.onGive @@ -465,9 +472,7 @@ local function description() return RomText.plain("gText_MoveVar1Where", { stringVars = { itemName(items[st.swapFrom + 1]) } }) end if item then return ItemsData.description(item) or "" end - local ret = Py().manifest().bagReturnTo[(PBag.LOCATION[st.location] or 0) + 1] - local TextIR = require("src.core.game3.scripting.text_ir") - return RomText.plain("gText_ReturnToVar1", { stringVars = { ret and TextIR.toPlain(RomText.refIr(ret), {}) or "" } }) + return RomText.plain("gText_ReturnToVar1", { stringVars = { st.returnTo or "" } }) end PBag.description = description diff --git a/tests/engine/game3_emerald_pack_text_test.lua b/tests/engine/game3_emerald_pack_text_test.lua index bd1e0ec5..6a3fc309 100644 --- a/tests/engine/game3_emerald_pack_text_test.lua +++ b/tests/engine/game3_emerald_pack_text_test.lua @@ -68,8 +68,11 @@ 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). @@ -143,24 +146,62 @@ check(PartyMenu._cursorOptionText("CUT") == "COUPE", "a field move prints the mo -- 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 placeholders = Message.cartPlaceholders({ +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_Brendan = ir("BRICE") -BUNDLE.text.gText_ExpandedPlaceholder_Chan = ir("ちゃん") +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) From f173250cc5f601c76d1341796e0d073e18350d85 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:06:36 +0200 Subject: [PATCH 11/11] Trim the new comments to pret references The comments this change adds keep only the pret source each line follows. --- src/core/game3/battle/adapter.lua | 1 - src/core/game3/profiles/emerald/ui.lua | 3 +-- src/core/game3/rom_text.lua | 5 ----- src/core/game3/rse/frontier/dome.lua | 3 --- src/core/game3/summary_data.lua | 3 +-- src/ui/game3/party_menu.lua | 4 +--- src/ui/game3/rse/contest.lua | 1 - src/ui/game3/rse/mapsec.lua | 2 -- src/ui/game3/rse/pokedex.lua | 5 +---- src/ui/game3/rse/region_map.lua | 3 +-- src/ui/game3/summary_menu.lua | 1 - 11 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/core/game3/battle/adapter.lua b/src/core/game3/battle/adapter.lua index ae3c6bec..1564a25b 100644 --- a/src/core/game3/battle/adapter.lua +++ b/src/core/game3/battle/adapter.lua @@ -494,7 +494,6 @@ function Adapter.new(battleState, sayFn) if id and id > 0 then if ABILITY_BY_ID[id] then return ABILITY_BY_ID[id] end local ok, Pokemon = pcall(require, "src.core.game3.pokemon") - -- The ROM's English name, not the display name a translation shows. if ok and Pokemon and Pokemon.romAbilityName then local n = Pokemon.romAbilityName(id) if n and n ~= "" and not n:match("^ABILITY") then diff --git a/src/core/game3/profiles/emerald/ui.lua b/src/core/game3/profiles/emerald/ui.lua index 28e90624..9475c252 100644 --- a/src/core/game3/profiles/emerald/ui.lua +++ b/src/core/game3/profiles/emerald/ui.lua @@ -52,8 +52,7 @@ return { -- pokeemerald/src/menu_specialized.c:1503 levelUpStats = { "gText_MaxHP", "gText_Attack", "gText_Defense", "gText_SpAtk", "gText_SpDef", "gText_Speed" }, }, - -- pokeemerald/src/data/party_menu.h:658: sCursorOptions' strings up to - -- the field moves, which print gMoveNames. + -- pokeemerald/src/data/party_menu.h:658 cursorOptionTexts = { "gText_Summary5", "gText_Switch2", "gText_Cancel2", "gText_Item", "gMenuText_Give", "gText_Take", "gText_Mail", "gText_Take2", "gText_Read2", "gText_Cancel2", "gText_Shift", "gText_SendOut", diff --git a/src/core/game3/rom_text.lua b/src/core/game3/rom_text.lua index c7c9f1d5..1b5ae8f8 100644 --- a/src/core/game3/rom_text.lua +++ b/src/core/game3/rom_text.lua @@ -56,16 +56,11 @@ function RomText.has(key) return cachedText(b, key) ~= nil end --- A cart string a screen also keeps a copy of in its own pack: the script --- cache's text under `key` when the cache holds it (a mod's text overrides --- land there), else that copy. function RomText.irOr(key, fallback) if key ~= nil and RomText.has(key) then return RomText.ir(key) end return fallback end --- A pack's text reference, { name = , key = "g3:
", --- ir = }: the script cache's text by its symbol, then by its address. function RomText.refIr(ref) if type(ref) ~= "table" then return nil end return RomText.irOr(ref.name, RomText.irOr(ref.key, ref.ir)) diff --git a/src/core/game3/rse/frontier/dome.lua b/src/core/game3/rse/frontier/dome.lua index 2d7e2d47..787dc5ae 100644 --- a/src/core/game3/rse/frontier/dome.lua +++ b/src/core/game3/rse/frontier/dome.lua @@ -617,9 +617,6 @@ local function setStr(ctx, adapters, n, v) Util.setStringVar(ctx, adapters, n, v) end --- The i-th string of one of pret's tables, from the script cache, which keys --- it by the table's label (a mod's text overrides land there), else the copy --- this pack holds. local function tableText(list, name, i) local RomText = require("src.core.game3.rom_text") return RomText.irOr(RomText.key(name, i - 1), list[i]) diff --git a/src/core/game3/summary_data.lua b/src/core/game3/summary_data.lua index 946ec654..8e0e7082 100644 --- a/src/core/game3/summary_data.lua +++ b/src/core/game3/summary_data.lua @@ -416,8 +416,7 @@ function SummaryData.abilityDescription(abilityId, abilityName) return Strings(d.ABILITIES["ABILITY_"]) end --- pokeemerald/src/data/contest_text_tables.h:220: keyed by the English --- text, like the move and ability descriptions. Takes the effect row or its text. +-- pokeemerald/src/data/contest_text_tables.h:220 function SummaryData.contestEffectDescription(effect) local text = type(effect) == "table" and effect.description or effect return Strings(type(text) == "string" and text or "") diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index 21c62abe..83cc27a7 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -111,9 +111,7 @@ local function cursor_option_text(act) local p = partyUi() if p and p.actionText then return p.actionText(act, g) end if g then - -- The pack's labels are the English the cart prints; show the cart's - -- text instead (a mod's text overrides land there) and the move's name - -- for a field move, as sCursorOptions does. + -- pokeemerald/src/data/party_menu.h:658 local fm = g.index[act] if fm then local move = g.moves[fm + 1] diff --git a/src/ui/game3/rse/contest.lua b/src/ui/game3/rse/contest.lua index c057ef5a..b451b80c 100644 --- a/src/ui/game3/rse/contest.lua +++ b/src/ui/game3/rse/contest.lua @@ -732,7 +732,6 @@ function UI:printContestMoveDescription(move) local key = "gContestEffectDescriptionPointers[" .. (cm.effect or 0) .. "]" local desc = "" if UI.has(key) then - -- Translated by its English text, like the summary and the move relearner. desc = require("src.core.game3.summary_data").contestEffectDescription(UI.plain(key)) end self.win[WIN_MOVE_DESCRIPTION] = { text = desc, x = 0, y = 1, fg = 15, shadow = 8 } diff --git a/src/ui/game3/rse/mapsec.lua b/src/ui/game3/rse/mapsec.lua index 7dcd1edf..c91ac79c 100644 --- a/src/ui/game3/rse/mapsec.lua +++ b/src/ui/game3/rse/mapsec.lua @@ -44,8 +44,6 @@ function Mapsec.entry(sec) end -- pokeemerald/src/region_map.c:1568 --- Every caller displays the name, so it goes through Strings() here, keyed by --- the English name like FireRed's region map. function Mapsec.name(sec) local e = Mapsec.entry(sec) return e and e.name and Strings(e.name) or "" diff --git a/src/ui/game3/rse/pokedex.lua b/src/ui/game3/rse/pokedex.lua index f62d2bd0..7c9e2a1c 100644 --- a/src/ui/game3/rse/pokedex.lua +++ b/src/ui/game3/rse/pokedex.lua @@ -1593,15 +1593,12 @@ function tasks.switchFromSize(s) end end --- The search screen's strings, by the labels the script cache keys them --- under (a mod's text overrides land there); the chrome pack keeps English --- copies without their labels, the fallback. local function cartText(key, fallback) if key and RomText.has(key) then return RomText.plain(key) end return fallback end --- pokeemerald/src/pokedex.c:1330: { description, title } of each option. +-- pokeemerald/src/pokedex.c:1330 local TYPE_OPTION = { "gText_DexSearchTypeNone" } for _, t in ipairs({ 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17 }) do TYPE_OPTION[#TYPE_OPTION + 1] = RomText.key("gTypeNames", t) diff --git a/src/ui/game3/rse/region_map.lua b/src/ui/game3/rse/region_map.lua index 19b8d157..8c9aedec 100644 --- a/src/ui/game3/rse/region_map.lua +++ b/src/ui/game3/rse/region_map.lua @@ -353,8 +353,7 @@ local function newState(opts) end RegionMap.newState = newState --- pokeemerald/src/region_map.c:349: the name list each multi-name --- destination points at, which the script cache keys by its label. +-- pokeemerald/src/region_map.c:349 local MULTI_NAME_TABLES = { "sEverGrandeCityNames" } -- pokeemerald/src/region_map.c:1760 diff --git a/src/ui/game3/summary_menu.lua b/src/ui/game3/summary_menu.lua index f169936a..83746751 100644 --- a/src/ui/game3/summary_menu.lua +++ b/src/ui/game3/summary_menu.lua @@ -923,7 +923,6 @@ local function draw_page_skills(mon) end ability = ability or "—" local ax, ay = cxy("abilityName", 74, 129) - -- abilityName() already translates numeric IDs; external labels still need one lookup. local abilityText = tostring(ability) if not abilityNameTranslated then abilityText = Strings(abilityText) end draw_text(abilityText, ax, ay, 80, "NORMAL")