From b5988d46540f597bc49a8df8b971738d283fbe85 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:14:42 +0200 Subject: [PATCH] Expand Ruby and Sapphire's placeholders from the script cache The runtime expanded Ruby and Sapphire's {EVIL_TEAM}, {GOOD_LEADER}, {VERSION}, {RIVAL} and {KUN} (and the other version-dependent names) from text/placeholders.lua, the values the extractor reads from the US cart, so a translation never reached them: the team leaders stayed MAXIE and ARCHIE, the rival MAY or BRENDAN, the version RUBY or SAPPHIRE, and the Japanese honorific empty. Each value is a cart string the script cache also holds under its pokeruby label (gExpandedPlaceholder_Maxie, _May, _Ruby, _Kun...); the placeholders now read it from there, with the extract's value as the fallback, the way Emerald's do. The labels are defined once, in text_placeholders_extract.lua (rsSymbols, the evil and good teams swapped by edition), and both the Ruby/Sapphire import and the message box read them from there. --- src/import/gba/games/rs/text.lua | 16 +---- src/import/gba/text_placeholders_extract.lua | 24 +++++++ src/ui/game3/message.lua | 9 +-- .../game3_rs_translation_runtime_test.lua | 67 +++++++++++++++++++ 4 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 tests/engine/game3_rs_translation_runtime_test.lua diff --git a/src/import/gba/games/rs/text.lua b/src/import/gba/games/rs/text.lua index d27b59cd..b7699f30 100644 --- a/src/import/gba/games/rs/text.lua +++ b/src/import/gba/games/rs/text.lua @@ -17,19 +17,7 @@ return function(V) } end - local stems = { - UNKNOWN = "Empty", KUN_MALE = "Kun", KUN_FEMALE = "Chan", - RIVAL_MALE = "May", RIVAL_FEMALE = "Brendan", - VERSION = V.GAME == "sapphire" and "Sapphire" or "Ruby", - AQUA = "Aqua", MAGMA = "Magma", ARCHIE = "Archie", MAXIE = "Maxie", - KYOGRE = "Kyogre", GROUDON = "Groudon", - } - -- pokeruby/src/string_util.c:476 - local evil, good = { "Magma", "Maxie", "Groudon" }, { "Aqua", "Archie", "Kyogre" } - if V.GAME == "sapphire" then evil, good = good, evil end - for i, name in ipairs({ "TEAM", "LEADER", "LEGENDARY" }) do - stems["EVIL_" .. name], stems["GOOD_" .. name] = evil[i], good[i] - end V.TEXT_PLACEHOLDERS = {} - for name, stem in pairs(stems) do V.TEXT_PLACEHOLDERS[name] = V.sym("gExpandedPlaceholder_" .. stem) end + local Placeholders = require("src.import.gba.text_placeholders_extract") + for name, label in pairs(Placeholders.rsSymbols(V.GAME)) do V.TEXT_PLACEHOLDERS[name] = V.sym(label) end end diff --git a/src/import/gba/text_placeholders_extract.lua b/src/import/gba/text_placeholders_extract.lua index 9f43bfbe..42d78fdd 100644 --- a/src/import/gba/text_placeholders_extract.lua +++ b/src/import/gba/text_placeholders_extract.lua @@ -20,6 +20,30 @@ TextPlaceholders.SYMBOLS = { GROUDON = "gText_ExpandedPlaceholder_Groudon", } +-- pokeruby/src/string_util.c:476 +function TextPlaceholders.rsSymbols(game) + local stems = { + UNKNOWN = "Empty", KUN_MALE = "Kun", KUN_FEMALE = "Chan", + RIVAL_MALE = "May", RIVAL_FEMALE = "Brendan", + VERSION = game == "sapphire" and "Sapphire" or "Ruby", + AQUA = "Aqua", MAGMA = "Magma", ARCHIE = "Archie", MAXIE = "Maxie", + KYOGRE = "Kyogre", GROUDON = "Groudon", + } + local evil, good = { "Magma", "Maxie", "Groudon" }, { "Aqua", "Archie", "Kyogre" } + if game == "sapphire" then evil, good = good, evil end + for i, name in ipairs({ "TEAM", "LEADER", "LEGENDARY" }) do + stems["EVIL_" .. name], stems["GOOD_" .. name] = evil[i], good[i] + end + local out = {} + for name, stem in pairs(stems) do out[name] = "gExpandedPlaceholder_" .. stem end + return out +end + +function TextPlaceholders.symbolsFor(game) + if game == "ruby" or game == "sapphire" then return TextPlaceholders.rsSymbols(game) end + return TextPlaceholders.SYMBOLS +end + -- pokeemerald/src/string_util.c:448 TextPlaceholders.BY_GENDER = { KUN = { male = "KUN_MALE", female = "KUN_FEMALE" }, diff --git a/src/ui/game3/message.lua b/src/ui/game3/message.lua index c1b24df5..5bb979cd 100644 --- a/src/ui/game3/message.lua +++ b/src/ui/game3/message.lua @@ -42,17 +42,18 @@ end local placeholderCache = {} -- pokeemerald/src/strings.c:6 -local function cartPlaceholders(extracted) +local function cartPlaceholders(extracted, game) local Extract = require("src.import.gba.text_placeholders_extract") local RomText = require("src.core.game3.rom_text") + local symbols = Extract.symbolsFor(game) local function value(name) - local label = Extract.SYMBOLS[name] + local label = symbols[name] if label and RomText.has(label) then return RomText.plain(label) end return extracted[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 in pairs(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] } @@ -94,7 +95,7 @@ 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] = { bundle = bundle, values = cartPlaceholders(t) } + placeholderCache[id] = { bundle = bundle, values = cartPlaceholders(t, id) } return placeholderCache[id].values end return nil diff --git a/tests/engine/game3_rs_translation_runtime_test.lua b/tests/engine/game3_rs_translation_runtime_test.lua new file mode 100644 index 00000000..217ee6e3 --- /dev/null +++ b/tests/engine/game3_rs_translation_runtime_test.lua @@ -0,0 +1,67 @@ +-- Ruby and Sapphire read the cart strings a translation mod overrides from +-- the script cache, where the mod's text overrides land, as Emerald does +-- (game3_emerald_pack_text_test.lua); the extract's or 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 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 + +-- Expanded placeholders (pokeruby/src/string_util.c:476): the teams, their +-- leaders and legendaries by edition, the version, the rival and the +-- honorific. +local Extract = require("src.import.gba.text_placeholders_extract") +local ruby, sapphire = Extract.rsSymbols("ruby"), Extract.rsSymbols("sapphire") +check(ruby.EVIL_TEAM == "gExpandedPlaceholder_Magma" and sapphire.EVIL_TEAM == "gExpandedPlaceholder_Aqua", + "the evil team is Magma in Ruby and Aqua in Sapphire") +check(ruby.GOOD_LEADER == "gExpandedPlaceholder_Archie" and sapphire.GOOD_LEADER == "gExpandedPlaceholder_Maxie", + "the good team's leader follows the edition") +check(ruby.VERSION == "gExpandedPlaceholder_Ruby" and sapphire.VERSION == "gExpandedPlaceholder_Sapphire", + "the version names the edition") +check(Extract.symbolsFor("emerald") == Extract.SYMBOLS, "Emerald keeps its own labels") + +local Message = require("src.ui.game3.message") +BUNDLE.text = { + gExpandedPlaceholder_Maxie = ir("MAX"), gExpandedPlaceholder_Archie = ir("ARTHUR"), + gExpandedPlaceholder_Ruby = ir("RUBIS"), gExpandedPlaceholder_May = ir("FLORA"), + gExpandedPlaceholder_Kun = ir("くん"), +} +local placeholders = Message.cartPlaceholders({ + EVIL_TEAM = "MAGMA", EVIL_LEADER = "MAXIE", GOOD_LEADER = "ARCHIE", VERSION = "RUBY", + RIVAL_MALE = "MAY", RIVAL_FEMALE = "BRENDAN", KUN_MALE = "", KUN_FEMALE = "", + byGender = { RIVAL = { male = "MAY", female = "BRENDAN" }, KUN = { male = "", female = "" } }, +}, "ruby") +check(placeholders.EVIL_LEADER == "MAX" and placeholders.GOOD_LEADER == "ARTHUR", + "the leaders' names read the cache by the edition's labels") +check(placeholders.VERSION == "RUBIS", "the version's name reads the cache") +check(placeholders.EVIL_TEAM == "MAGMA", "a placeholder missing from the cache keeps the copy") +check(placeholders.byGender.RIVAL.male == "FLORA", "the rival's name reads the cache") +local line = { { t = "player" }, { t = "ph", code = 5, name = "KUN" }, { t = "text", s = ": " }, + { t = "ph", code = 10, name = "EVIL_LEADER" }, { t = "text", s = " / " }, + { t = "ph", code = 6, name = "RIVAL" }, { t = "eos" } } +check(TextIR.toPlain(line, { dialect = "rs", playerName = "RED", playerGender = 0, placeholders = placeholders }) + == "REDくん: MAX / FLORA", "a Ruby line expands the honorific, the leader and the rival from the cache") +BUNDLE.text = { gExpandedPlaceholder_Archie = ir("ARTHUR"), gExpandedPlaceholder_Maxie = ir("MAX") } +local sapphireLine = Message.cartPlaceholders({ EVIL_LEADER = "ARCHIE", byGender = {} }, "sapphire") +check(sapphireLine.EVIL_LEADER == "ARTHUR", "Sapphire's evil leader is Archie's row") + +if failed > 0 then + print(failed .. " check(s) failed") + os.exit(1) +end +print("all checks passed")