Read Ruby and Sapphire's native screens' cart text from the script cache

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.
This commit is contained in:
thibautbus
2026-10-05 17:29:15 +02:00
parent bb568d3283
commit 31351ae2e9
5 changed files with 98 additions and 10 deletions
+12 -6
View File
@@ -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)
+8
View File
@@ -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)
+12 -3
View File
@@ -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
+7 -1
View File
@@ -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)
@@ -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)