From 31351ae2e9a45f0af3152e6cc0acc58da063ddc5 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:29:15 +0200 Subject: [PATCH] Read Ruby and Sapphire's native screens' cart text from the script cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Ruby/Sapphire shop, decoration menus, move relearner and contest paintings printed the cart strings their extractors copied into their packs (textBytes, keyed by the strings' pokeruby labels), so a translation never reached them. The script cache holds the same strings under the same labels, where a mod's text overrides land: these screens now read it there, with the pack's copy as the fallback, the way Emerald's screens do (RomText.irOr). The shop, the decoration menus and the move relearner keep each text they resolve until the screen opens again, the painting its caption once it is built. A hall painting's caption names the trainer, then the POKéMON, as the US cart does; the European carts name the POKéMON first, their possessive row starting with a space (" von ", " de ", " di ", pokeruby/src/contest_painting.c:234), and so does the caption now when the row does. --- src/ui/game3/rs/contest_painting.lua | 18 ++++-- src/ui/game3/rs/decoration.lua | 8 +++ src/ui/game3/rs/move_relearner.lua | 15 ++++- src/ui/game3/rs/shop_menu.lua | 8 ++- .../game3_rs_translation_runtime_test.lua | 59 +++++++++++++++++++ 5 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/ui/game3/rs/contest_painting.lua b/src/ui/game3/rs/contest_painting.lua index c0be9d62..34618ce9 100644 --- a/src/ui/game3/rs/contest_painting.lua +++ b/src/ui/game3/rs/contest_painting.lua @@ -1,5 +1,6 @@ local Base = require("src.ui.game3.rse.contest_painting") local IR = require("src.core.game3.scripting.text_ir") +local RomText = require("src.core.game3.rom_text") local Kit = require("src.ui.game3.rse.scene_kit") local Font = require("src.ui.game3.frlg_font") local Painting = {ID = Base.ID, SUB = Base.SUB} @@ -11,9 +12,9 @@ local function manifest() return man end -local function render(bytes) +local function render(ir) local out = {} - for _, seg in ipairs(IR.decode(bytes, {dialect = "rs"})) do + for _, seg in ipairs(ir) do if seg.t == "ext" then out[#out + 1] = string.char(252, seg.cmd) for _, arg in ipairs(seg.args or {}) do out[#out + 1] = string.char(arg) end @@ -21,7 +22,10 @@ local function render(bytes) end return table.concat(out) end -local function text(man, key) return render(assert(man.textBytes[key], "native painting caption missing: " .. key)) end +local function text(man, key) + local bytes = assert(man.textBytes[key], "native painting caption missing: " .. key) + return render(RomText.irOr(key, IR.decode(bytes, {dialect = "rs"}))) +end local function name(value, limit) if type(value) == "table" then local bytes = {} @@ -30,7 +34,7 @@ local function name(value, limit) bytes[#bytes + 1] = value[i] end bytes[#bytes + 1] = 255 - return render(bytes) + return render(IR.decode(bytes, {dialect = "rs"})) end if type(value) ~= "string" then return "" end if not limit then return value end @@ -48,9 +52,11 @@ function Painting.caption(saveIdx, winner, artist, man) local nickname = name(winner.monName or winner.nickname, man.captionLayout.nicknameBytes) local category = tonumber(winner.contestCategory) or 0 if saveIdx < man.captionLayout.museumStart then + -- pokeruby/src/contest_painting.c:234 + local possessive, first, second = text(man, man.hallPossessive), name(winner.trainerName), nickname + if possessive:find("^[ ,]") then first, second = nickname, name(winner.trainerName) end return text(man, assert(man.rankNames[category], "native painting category")) .. text(man, man.hallCaption) - .. name(winner.trainerName) .. string.char(unpack(man.captionLayout.hallLatinControl)) - .. text(man, man.hallPossessive) .. nickname + .. first .. string.char(unpack(man.captionLayout.hallLatinControl)) .. possessive .. second end local pair = assert(man.captionParts[category], "native museum caption pair") return text(man, pair.prefix) .. nickname .. text(man, pair.suffix) diff --git a/src/ui/game3/rs/decoration.lua b/src/ui/game3/rs/decoration.lua index ebcb86dd..4407424d 100644 --- a/src/ui/game3/rs/decoration.lua +++ b/src/ui/game3/rs/decoration.lua @@ -27,9 +27,16 @@ local aliases = { UI.TEXT_ALIASES = {gText_ApostropheSBase = "gOtherText_PlayersBase"} local cats = {gText_Desk = 0, gText_Chair = 1, gText_Plant = 2, gText_Ornament = 3, gText_Mat = 4, gText_Poster = 5, gText_Doll = 6, gText_Cushion = 7} +local resolved = {} local function nativeText(key, vars) if cats[key] ~= nil then return Inv.categoryName(cats[key]) end local native, m = aliases[key] or key, Decor.manifest() + if not vars and resolved[native] then return resolved[native] end + if Text.has(native) then + local value = Text.plain(native, vars and {stringVars = vars} or nil) + if not vars then resolved[native] = value end + return value + end if m.textBytes and m.textBytes[native] then return IR.toPlain(IR.decode(m.textBytes[native], {dialect = "rs"}), {stringVars = vars or {}}) end @@ -116,6 +123,7 @@ function UI.draw(d, host) end local function options(opts) opts = opts or {} + resolved = {} opts.text, opts.draw = nativeText, UI.draw opts.recordSecretBaseVisit = false opts.avatarGfx = function(s, female) diff --git a/src/ui/game3/rs/move_relearner.lua b/src/ui/game3/rs/move_relearner.lua index 174b7bab..5abc762c 100644 --- a/src/ui/game3/rs/move_relearner.lua +++ b/src/ui/game3/rs/move_relearner.lua @@ -3,6 +3,7 @@ local Kit = require("src.ui.game3.rse.scene_kit") local Font = require("src.ui.game3.frlg_font") local Pokemon = require("src.core.game3.pokemon") local IR = require("src.core.game3.scripting.text_ir") +local RomText = require("src.core.game3.rom_text") local bit = require("bit") local UI = {} local keys = {gText_TeachWhichMoveToPkmn = "gOtherText_TeachWhichMove", @@ -13,13 +14,20 @@ local function manifest() assert(m.assetLayout == "rs", "native RS move tutor pack required") return m end +local resolved = {} +local function cartIr(native) + if not resolved[native] then + resolved[native] = RomText.irOr(native, + IR.decode(assert(manifest().textBytes[native], "native tutor text " .. native), {dialect = "rs"})) + end + return resolved[native] +end local function text(key, ctx) - local m, native = manifest(), keys[key] or key - return IR.toPlain(IR.decode(assert(m.textBytes[native], "native tutor text " .. native), {dialect = "rs"}), ctx or {}) + return IR.toPlain(cartIr(keys[key] or key), ctx or {}) end local function pages(key, vars) local out = {} - local value = IR.toAscii(IR.decode(assert(manifest().textBytes[key]), {dialect = "rs"}), {stringVars = vars}) + local value = IR.toAscii(cartIr(key), {stringVars = vars}) for page in (value .. "\\p"):gmatch("(.-)\\p") do if page ~= "" then out[#out + 1] = page end end return out end @@ -164,6 +172,7 @@ function UI.draw(st) end function UI.show(mon, opts) manifest(); opts = opts or {} + resolved = {} opts.maxShown, opts.text, opts.draw, opts.startLearn = 3, text, UI.draw, startLearn return Base.show(mon, opts) end diff --git a/src/ui/game3/rs/shop_menu.lua b/src/ui/game3/rs/shop_menu.lua index 55e59b8c..5c7e8877 100644 --- a/src/ui/game3/rs/shop_menu.lua +++ b/src/ui/game3/rs/shop_menu.lua @@ -5,6 +5,7 @@ local Chrome = require("src.ui.game3.chrome") local Cursor = require("src.ui.game3.rs.menu_cursor") local Window = require("src.ui.game3.window") local TextIR = require("src.core.game3.scripting.text_ir") +local RomText = require("src.core.game3.rom_text") local Items = require("src.core.game3.items_data") local UI = {CAMERA_OFFSET = {x = -3, y = -3}} UI.WIN = {money = Window.template(1, 1, 12, 2), qty = Window.template(1, 11, 12, 2), @@ -15,10 +16,14 @@ local function manifest() assert(man.assetLayout == "rs" and man.shopVersion == 2, "native RS shop schema required") return man end +local resolved = {} function UI.ir(key) + if resolved[key] then return resolved[key] end local man = manifest() local native = assert(man.textAliases[key] or (man.textBytes[key] and key), "native RS shop text policy missing: " .. key) - return TextIR.decode(assert(man.textBytes[native], "native RS shop text bytes missing: " .. native), {dialect = "rs"}) + resolved[key] = RomText.irOr(native, + TextIR.decode(assert(man.textBytes[native], "native RS shop text bytes missing: " .. native), {dialect = "rs"})) + return resolved[key] end function UI.plain(key, ctx) return TextIR.toPlain(UI.ir(key), ctx or {}) end function UI.box(key, ctx) return UI.plain(key, ctx) end @@ -176,6 +181,7 @@ function UI.drawSell(sale) end function UI.show(shop, opts) opts = opts or {}; opts.nativePolicy = UI + resolved = {} manifest() shop._rsShopFrame = 0 return Base.show(shop, opts) diff --git a/tests/engine/game3_rs_translation_runtime_test.lua b/tests/engine/game3_rs_translation_runtime_test.lua index 7cc2bd02..c9a08427 100644 --- a/tests/engine/game3_rs_translation_runtime_test.lua +++ b/tests/engine/game3_rs_translation_runtime_test.lua @@ -134,6 +134,65 @@ check(Entries.ready(entriesCache("return { [277] = { descriptionLabel = \"DexDes "an R/S cache with the labels keeps its entries") Registry.active = active +-- Native screens that keep a cart string in their pack under its pret label +-- read the script cache's text under that label first. +local COPY = { 0xBD, 0xC9, 0xCA, 0xD3, 0xFF } -- "COPY" +local Kit = require("src.ui.game3.rse.scene_kit") +local MANIFESTS = { + ["items/shop"] = { assetLayout = "rs", shopVersion = 2, textAliases = { gText_HowMayIServeYou = "gOtherText_HowMayIServe" }, + textBytes = { gOtherText_HowMayIServe = COPY, gOtherText_Unnamed = COPY } }, +} +Kit.manifest = function(sub) + if MANIFESTS[sub] then return MANIFESTS[sub] end + return { assetLayout = "rs", textBytes = { gOtherText_TeachWhichMove = COPY, gOtherText_Unnamed = COPY } } +end +local Shop = require("src.ui.game3.rs.shop_menu") +BUNDLE.text = { gOtherText_HowMayIServe = ir("Que puis-je faire pour vous ?"), + gOtherText_TeachWhichMove = ir("Quelle capacité apprendre ?") } +check(Shop.plain("gText_HowMayIServeYou") == "Que puis-je faire pour vous ?", "the shop reads the cache by its label") +check(Shop.plain("gOtherText_Unnamed") == "COPY", "a shop line missing from the cache keeps the pack's copy") + +-- The move relearner, the decoration menus and the museum's painting +-- captions, through the text function each screen hands its shared base. +package.loaded["src.ui.game3.rse.move_relearner"] = { SUB = "tutor", show = function(_, opts) return opts end } +local tutorText = require("src.ui.game3.rs.move_relearner").show({}, {}).text +check(tutorText("gText_TeachWhichMoveToPkmn") == "Quelle capacité apprendre ?", "the move relearner reads the cache by its label") +check(tutorText("gOtherText_Unnamed") == "COPY", "a move relearner line missing from the cache keeps the pack's copy") +BUNDLE.text.gOtherText_TeachWhichMove = ir("Autre texte") +check(tutorText("gText_TeachWhichMoveToPkmn") == "Quelle capacité apprendre ?", "a screen resolves its text once while it is open") +tutorText = require("src.ui.game3.rs.move_relearner").show({}, {}).text +check(tutorText("gText_TeachWhichMoveToPkmn") == "Autre texte", "and again when it opens") + +package.loaded["src.ui.game3.rse.decoration"] = { open = function(opts) return opts end } +package.loaded["src.core.game3.rse.decoration"] = { manifest = function() + return { textBytes = { gSecretBaseText_DecorReturned = COPY, gSecretBaseText_NoDecor = COPY }, strings = {} } +end } +package.loaded["src.core.game3.rse.decoration_inventory"] = { categoryName = function() return "" end } +local decorText = require("src.ui.game3.rs.decoration").open().text +BUNDLE.text = { gSecretBaseText_DecorReturned = ir("La décoration est retournée au PC.") } +check(decorText("gText_DecorationReturnedToPC") == "La décoration est retournée au PC.", "the decoration menus read the cache by their labels") +check(decorText("gText_NoDecorationHere") == "COPY", "a decoration line missing from the cache keeps the pack's copy") + +package.loaded["src.ui.game3.rse.contest_painting"] = { ID = "contest_painting", SUB = "contest_painting" } +local Painting = require("src.ui.game3.rs.contest_painting") +local PAINT = { captionLayout = { museumStart = 5, nicknameBytes = 10 }, + captionParts = { [0] = { prefix = "gContestPaintingCool1", suffix = "gContestPaintingCool2" } }, + textBytes = { gContestPaintingCool1 = COPY, gContestPaintingCool2 = COPY } } +BUNDLE.text = { gContestPaintingCool1 = ir("Le POKéMON ") } +check(Painting.caption(5, { nickname = "ZIGZATON", contestCategory = 0 }, nil, PAINT) == "Le POKéMON ZIGZATONCOPY", + "a painting's caption reads the cache by its labels, the pack's copy where the cache has none") +-- pokeruby/src/contest_painting.c:234: the European carts name the POKéMON +-- first ("ZIGZATON von MAY"), the US and Japanese ones the trainer. +PAINT.rankNames = { [0] = "gContestRankNormal" } +PAINT.hallCaption, PAINT.hallPossessive = "gContestText_ContestWinner", "gOtherText_Unknown1" +PAINT.captionLayout.hallLatinControl = {} +PAINT.textBytes.gContestRankNormal, PAINT.textBytes.gContestText_ContestWinner, PAINT.textBytes.gOtherText_Unknown1 = COPY, COPY, COPY +local winner = { nickname = "ZIGZATON", trainerName = "MAY", contestCategory = 0 } +BUNDLE.text = { gContestRankNormal = ir("NORMAL "), gContestText_ContestWinner = ir("WINNER "), gOtherText_Unknown1 = ir("'s ") } +check(Painting.caption(0, winner, nil, PAINT) == "NORMAL WINNER MAY's ZIGZATON", "the US hall caption names the trainer first") +BUNDLE.text.gOtherText_Unknown1 = ir(" von ") +check(Painting.caption(0, winner, nil, PAINT) == "NORMAL WINNER ZIGZATON von MAY", "a European hall caption names the POKéMON first") + if failed > 0 then print(failed .. " check(s) failed") os.exit(1)