diff --git a/src/core/game3/rs/easy_chat_contracts.lua b/src/core/game3/rs/easy_chat_contracts.lua index f2e75383..8b238795 100644 --- a/src/core/game3/rs/easy_chat_contracts.lua +++ b/src/core/game3/rs/easy_chat_contracts.lua @@ -231,7 +231,7 @@ function M.commit(token, words) elseif token.type == 6 then writeBard(token, words) elseif token.type == 10 then token.session.gabbyAndTyData.quote[0] = words[1] elseif token.type == 9 then - out.stringVar2 = Easy.rawWord(words[1]) .. " " .. Easy.rawWord(words[2]) + out.stringVar2 = Easy.word(words[1]) .. " " .. Easy.word(words[2]) out.input = require("src.core.game3.rs.dewford_trend").trySetTrendyPhrase(words, token.session) and 1 or 0 elseif token.type ~= 13 then writeTv(token, words) end if token.type == 0 then out.input = M.passphrase(words) and 1 or 0 end diff --git a/src/core/game3/rs/tv_playback.lua b/src/core/game3/rs/tv_playback.lua index 081e6517..9dfef818 100644 --- a/src/core/game3/rs/tv_playback.lua +++ b/src/core/game3/rs/tv_playback.lua @@ -118,7 +118,7 @@ function M.word(id) local E = require("src.core.game3.easy_chat_text") local group = E.group(math.floor(value / 512) % 128) if group then - for _, word in ipairs(group.words) do if word.id == value then return word.text end end + for _, word in ipairs(group.words) do if word.id == value then return E.word(value) end end end return romText("gOtherText_ThreeQuestions") end, id) diff --git a/src/core/game3/scripting/natives_rs_tv_routes.lua b/src/core/game3/scripting/natives_rs_tv_routes.lua index 196f245b..693eedfa 100644 --- a/src/core/game3/scripting/natives_rs_tv_routes.lua +++ b/src/core/game3/scripting/natives_rs_tv_routes.lua @@ -79,7 +79,12 @@ M.BY_NAME.GabbyAndTyGetLastQuote = function(ctx, adapters) local session, g = R.session(), gabby() local quote = n(g.quote[0]) % 65536 if quote == 65535 then return false, 0 end - stringVar(ctx, adapters, 1, session, Q.encode(session, Playback.word(quote))) + local word = Playback.word(quote) + local bytes = Q.encode(session, word) + if Playback.displayBytes(session, bytes) ~= word then + bytes = Q.encode(session, require("src.core.game3.easy_chat_text").rawWord(quote)) + end + stringVar(ctx, adapters, 1, session, bytes) g.quote[0] = 65535 return false, 1 end 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/pokedex_chrome_extract.lua b/src/import/gba/pokedex_chrome_extract.lua index 4d6b55f9..b58987ad 100644 --- a/src/import/gba/pokedex_chrome_extract.lua +++ b/src/import/gba/pokedex_chrome_extract.lua @@ -153,6 +153,12 @@ function PokedexChromeExtract.extractEntries(rom, cache, root) end end + local syms = Versions.SYMS + local function symbolAt(ptr) + if not (syms and syms.namesAt and ptr and ptr >= 0x08000000 and ptr < 0x0A000000) then return nil end + return syms.namesAt(ptr - 0x08000000)[1] + end + local entries = {} -- src/data/pokemon/pokedex_entries.h:3 for nat = 0, natDexCount do @@ -177,6 +183,8 @@ function PokedexChromeExtract.extractEntries(rom, cache, root) weight = weight, description = desc1, description2 = desc2, + descriptionLabel = symbolAt(descPtr1), + descriptionLabel2 = E.desc2 and symbolAt(descPtr2) or nil, pokemonScale = pokemonScale, pokemonOffset = pokemonOffset, trainerScale = trainerScale, @@ -195,8 +203,12 @@ function PokedexChromeExtract.extractEntries(rom, cache, root) table.sort(ids) for _, id in ipairs(ids) do local e = entries[id] + local labels = "" + for _, field in ipairs({ "descriptionLabel", "descriptionLabel2" }) do + if e[field] then labels = labels .. string.format(", %s = \"%s\"", field, escape_lua(e[field])) end + end lines[#lines + 1] = string.format( - " [%d] = { category = \"%s\", height = %d, weight = %d, description = \"%s\", description2 = \"%s\", pokemonScale = %d, pokemonOffset = %d, trainerScale = %d, trainerOffset = %d },", + " [%d] = { category = \"%s\", height = %d, weight = %d, description = \"%s\", description2 = \"%s\", pokemonScale = %d, pokemonOffset = %d, trainerScale = %d, trainerOffset = %d%s },", id, escape_lua(e.category), e.height or 0, @@ -206,7 +218,8 @@ function PokedexChromeExtract.extractEntries(rom, cache, root) e.pokemonScale or 256, e.pokemonOffset or 0, e.trainerScale or 256, - e.trainerOffset or 0 + e.trainerOffset or 0, + labels ) end lines[#lines + 1] = "}" diff --git a/src/import/gba/pokedex_entries_extract.lua b/src/import/gba/pokedex_entries_extract.lua index d8f87ef3..aa7af8af 100644 --- a/src/import/gba/pokedex_entries_extract.lua +++ b/src/import/gba/pokedex_entries_extract.lua @@ -23,6 +23,11 @@ function PokedexEntriesExtract.ready(cache, cacheRoot) for _, f in ipairs(PokedexEntriesExtract.files()) do if not cache:exists(root .. f) then return false end end + local okL, L = pcall(function() return require("src.import.gba.layouts.registry").active() end) + if okL and type(L) == "table" and L.id == "rs" then + local body = cache.read and cache:read(root .. "entries.lua") + return body ~= nil and body:find("descriptionLabel", 1, true) ~= nil + end return true end diff --git a/src/import/gba/rs/assets.lua b/src/import/gba/rs/assets.lua index cb63866c..88578459 100644 --- a/src/import/gba/rs/assets.lua +++ b/src/import/gba/rs/assets.lua @@ -25,6 +25,9 @@ function M.text(c, off) end error("RS text is unterminated") end +function M.label(c, off) + return off and c.S.namesAt(off)[1] or nil +end function M.pairs(c, name, signed) local off, out = c:off(name), {} for i = 0, c.S.size(name) / 2 - 1 do diff --git a/src/import/gba/rs/cache_readiness.lua b/src/import/gba/rs/cache_readiness.lua index 1ed2666f..5524401d 100644 --- a/src/import/gba/rs/cache_readiness.lua +++ b/src/import/gba/rs/cache_readiness.lua @@ -19,7 +19,7 @@ local PACKS = { {"naming", {version = 3}}, {"rse/menus"}, {"rse/common_ui"}, {"rse/bag"}, {"rse/berry_tag"}, {"rse/party"}, {"rse/summary"}, {"rse/mail"}, {"rse/easy_chat"}, {"rse/trendy_phrase"}, {"rse/easy_chat_editor", {editorVersion = 2}}, {"rse/rs_egg_hatch"}, - {"pokemon/storage", {version = 5}}, {"rse/pokedex"}, {"rse/pokedex_detail"}, {"rse/diploma"}, + {"pokemon/storage", {version = 5}}, {"rse/pokedex", {packVersion = 1}}, {"rse/pokedex_detail"}, {"rse/diploma"}, {"rse/trainer_card"}, {"rse/trade"}, {"rse/pokenav"}, {"rse/pokenav_detail"}, {"rse/pokenav_shell"}, {"rse/pokenav_condition"}, {"rse/pokenav_ribbons"}, {"rs/assets"}, } diff --git a/src/import/gba/rs/extract_pokedex.lua b/src/import/gba/rs/extract_pokedex.lua index cfedae3f..ecd31859 100644 --- a/src/import/gba/rs/extract_pokedex.lua +++ b/src/import/gba/rs/extract_pokedex.lua @@ -1,6 +1,6 @@ local K = require("src.import.gba.rse.boot_gfx") local A = require("src.import.gba.rs.assets") -local M = {SUB = "rse/pokedex"} +local M = {SUB = "rse/pokedex", packVersion = 1} local N = "pokedex.o:" local maps = {{"list", "gUnknown_08E96738"}, {"list_underlay", "gUnknown_08E9C6DC"}, {"start_menu_main", "gPokedexStartMenuMain_Tilemap"}, {"start_menu_search", "gPokedexStartMenuSearchResults_Tilemap"}, @@ -18,18 +18,24 @@ local function bytes(c, name, stride) for i = 0, c.S.count(name, stride or 1) - 1 do out[i + 1] = stride == 2 and c:u16(off + i * 2) or c:u8(off + i) end return out end +-- pokeruby/src/pokedex.c:1116 +local function ownLabel(c, off) + local label = A.label(c, off) + return label and label:match("^DexText_") and label or nil +end local function options(c, name) local out, off = {}, c:off(N .. name) for i = 0, c.S.count(N .. name, 8) - 1 do local a, b = c:ptr(off + i * 8), c:ptr(off + i * 8 + 4) if not b then break end - out[#out + 1] = {description = a and A.text(c, a) or "", title = A.text(c, b)} + out[#out + 1] = {description = a and A.text(c, a) or "", title = A.text(c, b), + descriptionKey = ownLabel(c, a), titleKey = ownLabel(c, b)} end return out end function M.run(rom, cache, opts) local c = A.context(rom, cache, opts, M.SUB) - local man = {screen = "pokedex", coverage = "native_list_detail_search_backgrounds_and_search_tables", gfx = {}, maps = {}, layers = {}, palettes = {}} + local man = {screen = "pokedex", packVersion = M.packVersion, coverage = "native_list_detail_search_backgrounds_and_search_tables", gfx = {}, maps = {}, layers = {}, palettes = {}} -- pokeruby/src/pokedex.c:1967 local pals = {hoenn = c:palAt(c:off("gPokedexMenu_Pal"), 96), national = c:pal(N .. "sNationalPokedexPalette", 96), searchResults = c:pal(N .. "sPokedexSearchPalette", 96), search = c:pal("gPokedexMenuSearch_Pal", 64)} @@ -54,12 +60,14 @@ function M.run(rom, cache, opts) local top = c:off(N .. "sSearchMenuTopBarItems") for i = 0, c.S.count(N .. "sSearchMenuTopBarItems", 8) - 1 do local o = top + i * 8 - search.topBar[i + 1] = {description = A.text(c, assert(c:ptr(o))), x = c:u8(o + 4), y = c:u8(o + 5), width = c:u8(o + 6)} + search.topBar[i + 1] = {description = A.text(c, assert(c:ptr(o))), descriptionKey = A.label(c, c:ptr(o)), + x = c:u8(o + 4), y = c:u8(o + 5), width = c:u8(o + 6)} end local io = c:off(N .. "sSearchMenuItems") for i = 0, c.S.count(N .. "sSearchMenuItems", 12) - 1 do local o = io + i * 12 - search.items[i + 1] = {description = A.text(c, assert(c:ptr(o))), titleX = c:u8(o + 4), titleY = c:u8(o + 5), titleWidth = c:u8(o + 6), + search.items[i + 1] = {description = A.text(c, assert(c:ptr(o))), descriptionKey = A.label(c, c:ptr(o)), + titleX = c:u8(o + 4), titleY = c:u8(o + 5), titleWidth = c:u8(o + 6), selX = c:u8(o + 7), selY = c:u8(o + 8), selWidth = c:u8(o + 9)} end for _, k in ipairs({"SearchNatDex", "ShiftNatDex", "SearchHoennDex", "ShiftHoennDex"}) do @@ -74,5 +82,9 @@ function M.run(rom, cache, opts) c.files[#c.files + 1] = "orders.lua" return A.finish(c, man) end -function M.ready(cache, root) return A.ready(M.SUB, cache, root) end +function M.ready(cache, root) + if not A.ready(M.SUB, cache, root) then return false end + local body = cache:read((root or "data/generated/gba") .. "/" .. M.SUB .. "/manifest.lua") + return body:find("packVersion = " .. M.packVersion, 1, true) ~= nil +end return M 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/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index 83cc27a7..306cf006 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -137,6 +137,7 @@ local function action_texts(list) PartyMenu._actionTexts = { list = list, texts = texts } return texts end +PartyMenu._actionTextsFor = action_texts local FR_INSETS = { msgX = 2, msgY = 2, actX = 9, actY = 2, cursorX = 1 } local function textInsets() @@ -944,6 +945,7 @@ function PartyMenu.show(sessionParty, moveOverlay, opts) end opts = opts or {} PartyMenu._order = nil + PartyMenu._rsPrompt = nil if opts.mode == "battle_switch" or opts.mode == "battle_faint" or (opts.mode == "use" and opts.battleOrder) then local party0 = sessionParty or (opts.session and opts.session.party) local ov0 = moveOverlay or (opts.session and (opts.session.move_overlay or opts.session.moveOverlay)) 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/easy_chat_editor.lua b/src/ui/game3/rs/easy_chat_editor.lua index 0e59546a..be8014e4 100644 --- a/src/ui/game3/rs/easy_chat_editor.lua +++ b/src/ui/game3/rs/easy_chat_editor.lua @@ -6,6 +6,15 @@ local Cursor = require("src.ui.game3.rs.menu_cursor") local Pal = require("src.core.game3.pal_fade") local Fx = require("src.core.game3.gba_fx") local Easy = require("src.core.game3.easy_chat_text") +local words, groups = {}, {} +local function wordText(id) + if words[id] == nil then words[id] = Easy.word(id) end + return words[id] +end +local function groupText(group) + if groups[group] == nil then groups[group] = Easy.groupName(group) end + return groups[group] +end local Policy = require("src.ui.game3.rs.easy_chat_editor_policy") local Frames = require("src.ui.game3.rs.easy_chat_editor_frames") local C = {isMenu = true, open = false} @@ -33,6 +42,7 @@ function C.manifest(kind) end function C.show(opts) opts = opts or {} + words, groups = {}, {} assert(opts.token and opts.type ~= 9, "native RS editor requires its save contract; type9 uses its separate host") C._man, C._session, C._opts = C.manifest(opts.type), assert(opts.session), opts C._before, C.words = copyWords(opts.words), copyWords(opts.words) @@ -347,7 +357,7 @@ local function phraseWords() local line = {} for col = 0, C._man.columns - 1 do local i = row * C._man.columns + col + 1 - if i <= C._man.wordCount and C.words[i] ~= Policy.EMPTY then line[#line + 1] = Easy.rawWord(C.words[i]) .. " " end + if i <= C._man.wordCount and C.words[i] ~= Policy.EMPTY then line[#line + 1] = wordText(C.words[i]) .. " " end end local pen = C._man.wordPens[row * C._man.columns + 1] if pen then text(table.concat(line), pen.x, pen.y, "phrase") end @@ -360,7 +370,7 @@ local function phraseWords() local image = assert(Kit.image(C._man.blankWord)) love.graphics.setColor(1, 1, 1, 1) Fx.draw(function() love.graphics.draw(image, x, y) end, C._pal:fx(0)) - else text(Easy.rawWord(C.words[i]), x, y, "phrase") end + else text(wordText(C.words[i]), x, y, "phrase") end end end local function pickerText() @@ -382,7 +392,7 @@ local function pickerText() local word = (view == "groups" and C.groups or C._list)[row * 2 + col + 1] if word then local x = (view == "groups" and 16 or 48) + col * 88 - text(view == "groups" and word.name or word.text, x, 88 + (row - state.top) * 16 + offset, "picker") + text(view == "groups" and groupText(word) or wordText(word.id), x, 88 + (row - state.top) * 16 + offset, "picker") end end end diff --git a/src/ui/game3/rs/mail_composer.lua b/src/ui/game3/rs/mail_composer.lua index bac46f43..050ded2d 100644 --- a/src/ui/game3/rs/mail_composer.lua +++ b/src/ui/game3/rs/mail_composer.lua @@ -6,6 +6,15 @@ local Cursor = require("src.ui.game3.rs.menu_cursor") local Pal = require("src.core.game3.pal_fade") local Fx = require("src.core.game3.gba_fx") local Easy = require("src.core.game3.easy_chat_text") +local words, groups = {}, {} +local function wordText(id) + if words[id] == nil then words[id] = Easy.word(id) end + return words[id] +end +local function groupText(group) + if groups[group] == nil then groups[group] = Easy.groupName(group) end + return groups[group] +end local Policy = require("src.ui.game3.rs.mail_composer_policy") local Frames = require("src.ui.game3.rs.mail_composer_frames") local C = {isMenu = true, open = false} @@ -23,6 +32,7 @@ function C.manifest() end function C.write(record, opts) opts = opts or {} + words, groups = {}, {} C._man, C._record, C._session, C._opts = C.manifest(), assert(record), assert(opts.session), opts C._before, C.words = copyWords(record.words), copyWords(record.words) C.groups = Policy.groups(C._session, opts.gates) @@ -259,7 +269,7 @@ local function phraseWords() local parts = {} for col = 0, 1 do local i = row * 2 + col + 1 - if i <= 9 and C.words[i] ~= Policy.EMPTY then parts[#parts + 1] = Easy.rawWord(C.words[i]) .. " " end + if i <= 9 and C.words[i] ~= Policy.EMPTY then parts[#parts + 1] = wordText(C.words[i]) .. " " end end text(table.concat(parts), 40, row * 16, "phrase") end @@ -271,7 +281,7 @@ local function phraseWords() local image = assert(Kit.image(C._man.blankWord)) love.graphics.setColor(1, 1, 1, 1) Fx.draw(function() love.graphics.draw(image, x, y) end, C._pal:fx(0)) - else text(Easy.rawWord(C.words[i]), x, y, "phrase") end + else text(wordText(C.words[i]), x, y, "phrase") end end end local function pickerText() @@ -293,7 +303,7 @@ local function pickerText() local word = (view == "groups" and C.groups or C._list)[row * 2 + col + 1] if word then local x = (view == "groups" and 16 or 48) + col * 88 - text(view == "groups" and word.name or word.text, x, 88 + (row - state.top) * 16 + offset, "picker") + text(view == "groups" and groupText(word) or wordText(word.id), x, 88 + (row - state.top) * 16 + offset, "picker") end end end diff --git a/src/ui/game3/rs/mail_reader.lua b/src/ui/game3/rs/mail_reader.lua index b587e5f2..5da71047 100644 --- a/src/ui/game3/rs/mail_reader.lua +++ b/src/ui/game3/rs/mail_reader.lua @@ -43,7 +43,7 @@ function Reader.content(mail, man) local parts = {} for i = 1, line.words do local id = tonumber(mail.words and mail.words[word]) or 65535 - parts[#parts + 1] = Easy.rawWord(id) + parts[#parts + 1] = Easy.word(id) if i < line.words and id ~= 65535 then parts[#parts + 1] = " " end word = word + 1 end 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/party_menu_data.lua b/src/ui/game3/rs/party_menu_data.lua index 1134d402..5805a602 100644 --- a/src/ui/game3/rs/party_menu_data.lua +++ b/src/ui/game3/rs/party_menu_data.lua @@ -12,12 +12,23 @@ local function menuData() return assert(require("src.ui.game3.rse.scene_kit").manifest(Data.manifest).party) end +-- pokeruby/src/pokemon_menu.c:126 +Data.actionLabels = {"OtherText_Summary", "OtherText_Switch2", "OtherText_Item", "gOtherText_CancelNoTerminator", + "OtherText_Give2", "OtherText_Take2", "OtherText_Take", "OtherText_Mail", "OtherText_Read2", + "gOtherText_CancelNoTerminator"} + function Data.actionText(action, fields) + local RomText = require("src.core.game3.rom_text") local nativeId = Data.actionIds[action] - if nativeId then return assert(menuData().cursorOptions[nativeId + 1]) end - if Data.extraLabels[action] then return require("src.core.game3.rom_text").plain(Data.extraLabels[action]) end + if nativeId then + local label = Data.actionLabels[nativeId + 1] + if RomText.has(label) then return RomText.plain(label) end + return assert(menuData().cursorOptions[nativeId + 1]) + end + if Data.extraLabels[action] then return RomText.plain(Data.extraLabels[action]) end assert(fields and fields.index[action] ~= nil, "unknown native RS party action: " .. tostring(action)) - return action + local move = fields.moves and fields.moves[fields.index[action] + 1] + return move and require("src.core.game3.pokemon").moveName(move) or action end function Data.buildActions(mon, party, fields) @@ -152,11 +163,23 @@ function Data.drawDescription(desc, x, y) love.graphics.draw(image, quad, x, y) end -local function prompt(id, width) +local function promptText(id, name) + local RomText = require("src.core.game3.rom_text") + local key = RomText.key("PartyMenuPromptTexts", id) + if RomText.has(key) then return RomText.plain(key, {stringVars = {name or ""}}) end + return assert(require("src.ui.game3.rse.scene_kit").manifest("rse/party").prompts[id + 1]) +end + +local function prompt(ui, anchor, id, width) + local memo = ui._rsPrompt + if not (memo and memo.anchor == anchor and memo.id == id) then + local mon = id == 5 and ui._party and ui._party[ui.cursor] + memo = {anchor = anchor, id = id, text = promptText(id, mon and require("src.core.game3.pokemon").displayName(mon))} + ui._rsPrompt = memo + end local Chrome = require("src.ui.game3.chrome") Chrome.stdFrame(1, 17, width, 2) - require("src.ui.game3.frlg_font").draw(assert(require("src.ui.game3.rse.scene_kit").manifest("rse/party").prompts[id + 1]), 8, 136, - require("src.ui.game3.rs.party_chrome").textOptions("menu")) + require("src.ui.game3.frlg_font").draw(memo.text, 8, 136, require("src.ui.game3.rs.party_chrome").textOptions("menu")) end function Data.drawActions(ui, item) @@ -166,9 +189,10 @@ function Data.drawActions(ui, item) local left = item and (30 - (width + 1)) or 19 local top = item and (20 - (count * 2 + 2)) or (18 - count * 2) local Font = require("src.ui.game3.frlg_font") - prompt(item and ui._submenuKind ~= "MAIL" and 13 or 5, item and (ui._submenuKind == "MAIL" and 18 or 21) or 17) + prompt(ui, actions, item and ui._submenuKind ~= "MAIL" and 13 or 5, item and (ui._submenuKind == "MAIL" and 18 or 21) or 17) require("src.ui.game3.chrome").stdFrame(left + 1, top + 1, width - 1, count * 2) - for i, action in ipairs(actions) do Font.draw(Data.actionText(action, ui._fieldMoveData), (left + 1) * 8, (top + 1) * 8 + (i - 1) * 16, + local texts = ui._actionTextsFor(actions) + for i in ipairs(actions) do Font.draw(texts[i], (left + 1) * 8, (top + 1) * 8 + (i - 1) * 16, require("src.ui.game3.rs.party_chrome").textOptions("menu")) end local cursor = item and ui.itemActionCursor or ui.actionCursor require("src.ui.game3.rs.menu_cursor").draw((left + 1) * 8, (top + 1) * 8 + (cursor - 1) * 16, (width - 1) * 8) @@ -180,7 +204,7 @@ function Data.drawPrompt(ui) elseif ui.mode == "give" then id = 4 elseif ui.mode == "move_tutor" then id = 20 elseif ui.mode == "use" then id = require("src.core.game3.items_data").isTm(ui._item) and 2 or 3 end - prompt(id, 22) + prompt(ui, ui.mode, id, 22) end function Data.yesNoLabels() 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/src/ui/game3/rs/summary_menu.lua b/src/ui/game3/rs/summary_menu.lua index 4aff20cf..2ba53e66 100644 --- a/src/ui/game3/rs/summary_menu.lua +++ b/src/ui/game3/rs/summary_menu.lua @@ -50,6 +50,7 @@ local function detailRows(open) S._detailRowsOpen = open end function S.openMenu(party, index, opts) opts = opts or {} S._man, S._party, S._opts = S.manifest(), party or {}, opts + S._contestDescriptions = {} S._cursor = index or 1 S._playerState = opts.playerState or opts.session or require("src.core.game3.runtime").getSession() @@ -500,7 +501,10 @@ local function moves() local amount = value == 255 and 0 or math.floor(value / 10) for i = 0, 7 do frame(S._man.hearts, row == 1 and (i < amount and 1 or 0) or (i < amount and 3 or 4), 48 + i % 4 * 8, 120 + (row - 1) * 16 + math.floor(i / 4) * 8) end end - print(effect.description, 88, 120, 255) + local descriptions = S._contestDescriptions or {} + S._contestDescriptions = descriptions + descriptions[effect] = descriptions[effect] or SummaryData.contestEffectDescription(effect) + print(descriptions[effect], 88, 120, 255) else local def = Pokemon.battleMove(id) text("gOtherText_Power2", 8, 120); text("gOtherText_Accuracy2", 8, 136) diff --git a/src/ui/game3/rs/trainer_card.lua b/src/ui/game3/rs/trainer_card.lua index 6640066b..caadbd55 100644 --- a/src/ui/game3/rs/trainer_card.lua +++ b/src/ui/game3/rs/trainer_card.lua @@ -18,6 +18,12 @@ local function copiedCard(card) out.stars = math.max(0, math.min(4, math.floor(tonumber(out.stars) or 0))) return out end +local function profilePhrase(card) + local Easy, words = require("src.core.game3.easy_chat_text"), card.easyChatProfile or {} + local out = {} + for row = 0, 1 do out[row + 1] = Easy.word(words[row * 2 + 1] or 65535) .. " " .. Easy.word(words[row * 2 + 2] or 65535) end + return out +end function T.show(opts) opts = opts or {} local rt = package.loaded["src.core.game3.runtime"] @@ -25,6 +31,7 @@ function T.show(opts) T._clockSession = opts.clockSession or (rt and rt.getSession and rt.getSession()) or T._session T._link = opts.linkCard == true or (T._session.dex == nil and T._session.stars ~= nil) T._card = T._link and copiedCard(T._session) or Policy.generate(T._session) + T._phrase = T._link and profilePhrase(T._card) or nil T._man, T._onClose = T.manifest(), opts.onClose T.open, T.side, T._phase, T._setup = true, "front", "setup", 0 T._pal, T._stepper = Pal.new(), Kit.stepper() @@ -143,8 +150,8 @@ function T.texts(card, side, colon) local time = T._link and c or T._session put("time", tostring(tonumber(time.playTimeHours) or 0) .. (colon and " : " or " ") .. string.format("%02d", tonumber(time.playTimeMinutes) or 0), 80, 96, "values", false, 48) if T._link then - local Easy, words = require("src.core.game3.easy_chat_text"), c.easyChatProfile or {} - for row = 0, 1 do put("phrase" .. row, Easy.rawWord(words[row * 2 + 1] or 65535) .. " " .. Easy.rawWord(words[row * 2 + 2] or 65535), 16, 112 + row * 16) end + local phrase = T._phrase or profilePhrase(c) + for row = 0, 1 do put("phrase" .. row, phrase[row + 1], 16, 112 + row * 16) end end else put("name", c.name .. s.nameSuffix, 224, 16, "values", true) diff --git a/src/ui/game3/rs/trendy_phrase.lua b/src/ui/game3/rs/trendy_phrase.lua index 5e3407e7..37ee7b6c 100644 --- a/src/ui/game3/rs/trendy_phrase.lua +++ b/src/ui/game3/rs/trendy_phrase.lua @@ -6,6 +6,15 @@ local Cursor = require("src.ui.game3.rs.menu_cursor") local Pal = require("src.core.game3.pal_fade") local Fx = require("src.core.game3.gba_fx") local Easy = require("src.core.game3.easy_chat_text") +local words, groups = {}, {} +local function wordText(id) + if words[id] == nil then words[id] = Easy.word(id) end + return words[id] +end +local function groupText(group) + if groups[group] == nil then groups[group] = Easy.groupName(group) end + return groups[group] +end local Policy = require("src.ui.game3.rs.trendy_phrase_policy") local Frames = require("src.ui.game3.rs.trendy_phrase_frames") local C = {isMenu = true, open = false} @@ -27,6 +36,7 @@ function C.manifest() end function C.show(opts) opts = opts or {} + words, groups = {}, {} C._man, C._session, C._opts = C.manifest(), assert(opts.session), opts C._before, C.words = copyWords(opts.words), copyWords(opts.words) C.groups = Policy.groups(C._session, opts.gates) @@ -264,7 +274,7 @@ local function triangle(x, y) end local function phraseWords() if C._confirm and C._confirm.kind == "save" then - text(Easy.rawWord(C.words[1]) .. " " .. Easy.rawWord(C.words[2]) .. " ", 48, 24, "phrase") + text(wordText(C.words[1]) .. " " .. wordText(C.words[2]) .. " ", 48, 24, "phrase") return end for i = 1, 2 do @@ -273,7 +283,7 @@ local function phraseWords() local image = assert(Kit.image(C._man.blankWord)) love.graphics.setColor(1, 1, 1, 1) Fx.draw(function() love.graphics.draw(image, x, y) end, C._pal:fx(0)) - else text(Easy.rawWord(C.words[i]), x, y, "phrase") end + else text(wordText(C.words[i]), x, y, "phrase") end end end local function pickerText() @@ -295,7 +305,7 @@ local function pickerText() local word = (view == "groups" and C.groups or C._list)[row * 2 + col + 1] if word then local x = (view == "groups" and 16 or 48) + col * 88 - text(view == "groups" and word.name or word.text, x, 88 + (row - state.top) * 16 + offset, "picker") + text(view == "groups" and groupText(word) or wordText(word.id), x, 88 + (row - state.top) * 16 + offset, "picker") end end end diff --git a/src/ui/game3/rse/pokedex.lua b/src/ui/game3/rse/pokedex.lua index ca8d6294..8a067251 100644 --- a/src/ui/game3/rse/pokedex.lua +++ b/src/ui/game3/rse/pokedex.lua @@ -12,8 +12,32 @@ local RsPolicy = require("src.ui.game3.rs.pokedex_policy") local function nativeRs() return Gfx.manifest().assetLayout == "rs" end local rsTextNames = {gText_CryOf = "CryOf", gText_SizeComparedTo = "SizeComparedTo", gText_SelectorArrow = "RightPointingTriangle", gText_SearchingPleaseWait = "Searching", gText_SearchCompleted = "SearchComplete", gText_NoMatchingPkmnWereFound = "NoMatching"} +local function cached(label, copy) + if label and RomText.has(label) then return RomText.plain(label) end + return copy +end +local RS_LABELS = {RightPointingTriangle = "DexText_RightPointingTriangle"} +local function rsString(key) + local copy = assert(Gfx.manifest().strings[key], "native RS dex text " .. tostring(key)) + local text = cached(RS_LABELS[key] or ("gDexText_" .. key), nil) + if text == nil then return copy end + return (copy:match("^\252\019.") or "") .. text +end +-- pokeruby/src/pokedex.c:4228 +local function categorySuffix(unknown) + local i, marks = 1, 0 + while i <= #unknown do + local c = unknown:sub(i, i) + if c == "?" then marks, i = marks + 1, i + 1 + elseif c == " " then i = i + 1 + elseif unknown:sub(i, i + 2) == "?" then marks, i = marks + 1, i + 3 + else break end + end + if marks == 0 or i > #unknown then return "" end + return (unknown:sub(i - 1, i - 1) == " " and " " or "") .. unknown:sub(i) +end local function dexText(key) - return nativeRs() and assert(Gfx.manifest().strings[assert(rsTextNames[key], "native RS dex text alias")]) or RomText.plain(key) + return nativeRs() and rsString(assert(rsTextNames[key], "native RS dex text alias")) or RomText.plain(key) end local Pokedex = {} @@ -910,20 +934,26 @@ end local function monInfo(s, nat, nationalNumber, owned, newEntry) local e = entryFor(nat) if nativeRs() then - local strings, out = Gfx.manifest().strings, {} + local out = {} if newEntry then - local t = strings.RegisterComplete + local t = rsString("RegisterComplete") out[#out + 1] = {text = t, x = 16 + math.floor((208 - FrlgFont.measure(t)) / 2), y = 0} end local num = nationalNumber and nat or (Pokedex.hoennNumber(nat) or nat) out[#out + 1] = {text = string.format("%03d", num), x = 104, y = 24} out[#out + 1] = {text = pokemon().name(Pokedex.speciesOf(nat)) or Gfx.manifest().tenDashes, x = 128, y = 24} - local category = owned and ((e.category or "") .. " " .. strings.UnknownPoke:match("[^? ]+.*$")) or strings.UnknownPoke - local cx = 88 + (owned and (FrlgFont.measure(strings.UnknownPoke) - FrlgFont.measure(category)) or 0) + local unknown = rsString("UnknownPoke") + local category = owned and (Strings(e.category or "") .. categorySuffix(unknown)) or unknown + local cx = 88 + (owned and (FrlgFont.measure(unknown) - FrlgFont.measure(category)) or 0) out[#out + 1] = {text = category, x = cx, y = 40} - out[#out + 1] = {text = owned and Pokedex.heightText(e.height or 0) or strings.UnknownHeight, x = 128, y = 56} - out[#out + 1] = {text = owned and RsPolicy.weightText(e.weight or 0) or strings.UnknownWeight, x = 128, y = 72} - local desc = s.descriptionPage == 1 and e.description2 or e.description + out[#out + 1] = {text = owned and Pokedex.heightText(e.height or 0) or rsString("UnknownHeight"), x = 128, y = 56} + out[#out + 1] = {text = owned and RsPolicy.weightText(e.weight or 0) or rsString("UnknownWeight"), x = 128, y = 72} + local desc + if s.descriptionPage == 1 then + desc = cached(e.descriptionLabel2, e.description2) + else + desc = cached(e.descriptionLabel, e.description) + end out[#out + 1] = {text = owned and (desc or "") or "", x = 16, y = 104} return out end @@ -1535,7 +1565,8 @@ function tasks.loadSize(s) s.state = 3 elseif st == 3 then local name = s.session and (s.session.name or s.session.playerName) or "" - local t = dexText("gText_SizeComparedTo") .. tostring(name) + local label = dexText("gText_SizeComparedTo") + local t = label:find("^の") and tostring(name) .. label or label .. tostring(name) local w = FrlgFont.measure(t) or 0 s.sizeText = { text = t, x = nativeRs() and (24 + 96 - math.floor(w / 2)) or (w < 240 and math.floor((240 - w) / 2) or 0), y = nativeRs() and 120 or 121 } s.state = 4 @@ -1593,10 +1624,7 @@ function tasks.switchFromSize(s) end end -local function cartText(key, fallback) - if key and RomText.has(key) then return RomText.plain(key) end - return fallback -end +local cartText = cached -- pokeemerald/src/pokedex.c:1330 local TYPE_OPTION = { "gText_DexSearchTypeNone" } @@ -1632,11 +1660,13 @@ local ITEM_DESCRIPTIONS = { "gText_ListByFirstLetter", "gText_ListByBodyColor", "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) + local row = Gfx.manifest().search.topBar[i + 1] + return cartText(row.descriptionKey or TOPBAR_DESCRIPTIONS[i + 1], row.description) end local function itemDescription(i) - return cartText(ITEM_DESCRIPTIONS[i + 1], Gfx.manifest().search.items[i + 1].description) + local row = Gfx.manifest().search.items[i + 1] + return cartText(row.descriptionKey or ITEM_DESCRIPTIONS[i + 1], row.description) end -- pokeemerald/src/pokedex.c:1437 @@ -1657,8 +1687,8 @@ local function searchOptionTexts(which) 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), + title = cartText(t.titleKey or keys.titles[i], t.title), + description = cartText(t.descriptionKey or (keys.descriptions and keys.descriptions[i]), t.description), } end return out 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..70c7421c --- /dev/null +++ b/tests/engine/game3_rs_translation_runtime_test.lua @@ -0,0 +1,280 @@ +-- 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") + +-- The native Pokédex (src/ui/game3/rse/pokedex.lua): the entry's pages by +-- their own labels, its labels and the search screen by the labels the +-- packs name next to their copies. +local MANIFEST = { + assetLayout = "rs", + strings = { UnknownPoke = " ????? POKéMON", CryOf = "\252\019\002CRY OF", RegisterComplete = "REGISTERED", + UnknownHeight = "\252\019\012??'??\"", UnknownWeight = "????.? lbs.", SizeComparedTo = "SIZE COMPARED TO ", + Searching = "Searching...", SearchComplete = "Search completed.", NoMatching = "No matching POKéMON.", + RightPointingTriangle = ">" }, + search = { + topBar = { { description = "Search for POKéMON.", descriptionKey = "DexText_SearchForPoke" } }, + items = { { description = "List by the first letter.", descriptionKey = "DexText_ListByABC" } }, + names = { { title = "ABC", titleKey = "DexText_ABC", description = "" } }, + colors = {}, types = {}, orders = {}, modes = {}, + }, +} +package.loaded["src.ui.game3.rse.pokedex_gfx"] = setmetatable({ manifest = function() return MANIFEST end }, + { __index = function() return function() end end }) +package.loaded["src.ui.game3.rse.mapsec"] = setmetatable({ readLua = function(path) + if path:find("entries", 1, true) then + return { [277] = { category = "WOOD GECKO", height = 5, weight = 50, description = "Ruby page one.", + description2 = "Ruby page two.", descriptionLabel = "DexDescription_Treecko_1", + descriptionLabel2 = "DexDescription_Treecko_2" } } + end + return { nationalToRegional = {} } +end }, { __index = function() return function() end end }) +package.loaded["src.core.game3.pokemon"] = { speciesFromNational = function() return 277 end, + name = function() return "ARCKO" end } +local Pokedex = require("src.ui.game3.rse.pokedex") +BUNDLE.text = { + DexDescription_Treecko_2 = ir("Page deux."), gDexText_UnknownPoke = ir(" ????? POKéMON"), + gDexText_CryOf = ir("CRI DE"), DexText_SearchForPoke = ir("Chercher des POKéMON."), DexText_ABC = ir("ABC FR"), +} +local function texts(rows) + local out = {} + for _, row in ipairs(rows) do out[#out + 1] = row.text end + return table.concat(out, "|") +end +local page1 = texts(Pokedex.monInfo({ descriptionPage = 0 }, 252, true, true, false)) +local page2 = texts(Pokedex.monInfo({ descriptionPage = 1 }, 252, true, true, false)) +check(page1:find("Ruby page one.", 1, true) ~= nil, "a page missing from the cache keeps the pack's copy") +check(page2:find("Page deux.", 1, true) ~= nil, "the second page reads the cache by its own label") +check(page1:find("WOOD GECKO POKéMON", 1, true) ~= nil, "the category keeps the cart's POKéMON after it") +check(Pokedex.topBarDescription(0) == "Chercher des POKéMON.", "the search top bar reads its own label") +check(Pokedex.itemDescription(0) == "List by the first letter.", "a search line missing from the cache keeps the copy") +check(Pokedex.searchOptionTexts(Pokedex.SEARCH.NAME)[1].title == "ABC FR", "a search option reads its own label") +-- pokeruby/src/pokedex.c:4228: only the English cart follows the category +-- with the word after its unknown-category string's question marks. +local function infoRow(owned, y) + for _, row in ipairs(Pokedex.monInfo({ descriptionPage = 0 }, 252, true, owned, false)) do + if row.y == y then return row.text end + end +end +local function category(unknown) + BUNDLE.text.gDexText_UnknownPoke = ir(unknown) + return infoRow(true, 40) +end +check(category(" ????? POKéMON") == "WOOD GECKO POKéMON", "the English cart follows the category with POKéMON") +check(category("?????") == "WOOD GECKO", "the French and German carts print the category alone") +check(category("POKéMON ?????") == "WOOD GECKO", "so does the Italian cart, whose word comes first") +check(category("?????ポケモン") == "WOOD GECKOポケモン", "the Japanese word follows the category without a space") +BUNDLE.text.gDexText_UnknownHeight = ir("???,? m") +check(infoRow(false, 56) == "\252\019\012???,? m", "a label keeps its copy's CLEAR_TO in front of the cache's text") +local Registry = require("src.import.gba.layouts.registry") +local active = Registry.active +Registry.active = function() return { id = "rs" } end +local Entries = require("src.import.gba.pokedex_entries_extract") +local function entriesCache(body) return { exists = function() return true end, read = function() return body end } end +check(not Entries.ready(entriesCache("return { [277] = { description = \"x\" } }")), + "an R/S cache without the description labels extracts its entries again") +check(Entries.ready(entriesCache("return { [277] = { descriptionLabel = \"DexDescription_Treecko_1\" } }")), + "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") + +-- Easy Chat: what the player reads prints each word translated +-- (EasyChatText.word); what the scripts compare keeps the cart's word. +package.loaded["src.core.game3.easy_chat_text"] = { + -- 1513 is 513's cart word under another id, translated differently + rawWord = function(id) return "RAW" .. id % 1000 end, word = function(id) return "MOT" .. id end, + group = function() return { words = { { id = 513, text = "RAW513" } } } end, +} +package.loaded["src.core.game3.rs.dewford_trend"] = { editorValid = function() return true end, + trySetTrendyPhrase = function() return true end } +local Contracts = require("src.core.game3.rs.easy_chat_contracts") +local trend = Contracts.commit({ type = 9, wordCount = 2, before = { 0xFFFF, 0xFFFF }, session = {} }, { 513, 514 }) +check(trend.stringVar2 == "MOT513 MOT514", "the trendy phrase the script prints is translated") +check(trend.result == 1, "a new trend counts as a change") +local same = Contracts.commit({ type = 9, wordCount = 2, before = { 1513, 514 }, session = {} }, { 513, 514 }) +check(same.result == 0, "the change is measured on the cart's words, not their translation") +check(require("src.core.game3.rs.tv_playback").word(513) == "MOT513", "the TV prints a word translated") +-- Gabby and Ty's last quote is printed from the cart's bytes: a translated +-- word the cart's charset cannot hold keeps the cart's word. +local Routes = require("src.core.game3.scripting.natives_rs_tv_routes") +local Rse, Tv = require("src.core.game3.rse.init"), require("src.core.game3.rse.tv") +local session, gabbyData = { version = "ruby" }, { quote = {} } +local rseSession, tvState = Rse.session, Tv.state +Rse.session = function() return session end +Tv.state = function() return { gabbyAndTyData = gabbyData } end +local function lastQuote() + gabbyData.quote[0] = 513 + local ctx = {} + Routes.BY_NAME.GabbyAndTyGetLastQuote(ctx, nil) + return ctx.stringVars[1] +end +check(lastQuote() == "MOT513", "Gabby and Ty quote a word translated") +local EASY = package.loaded["src.core.game3.easy_chat_text"] +local word = EASY.word +EASY.word = function() return "ともだち" end +check(lastQuote() == "RAW513", "a word the cart cannot print keeps the cart's word") +EASY.word, Rse.session, Tv.state = word, rseSession, tvState + + +-- The party menu's actions (pokeruby/src/pokemon_menu.c:126) read the +-- script cache by their pret labels, and a field move prints its move's name. +MANIFESTS["rse/menus"] = { party = { cursorOptions = { "SUMMARY", "SWITCH", "ITEM", "CANCEL", "GIVE", "TAKE", + "TAKE", "MAIL", "READ", "CANCEL", "CUT" } } } +package.loaded["src.core.game3.pokemon"].moveName = function(move) return move == 15 and "COUPE" or nil end +local PartyData = require("src.ui.game3.rs.party_menu_data") +BUNDLE.text = { OtherText_Summary = ir("RESUME"), OtherText_Take = ir("PRENDRE") } +check(PartyData.actionText("SUMMARY") == "RESUME", "a party action reads the cache by its label") +check(PartyData.actionText("TAKE_MAIL") == "PRENDRE", "the mail's TAKE reads its own label") +check(PartyData.actionText("SWITCH") == "SWITCH", "an action missing from the cache keeps the pack's copy") +check(PartyData.actionText("CUT", { index = { CUT = 0 }, moves = { 15 } }) == "COUPE", "a field move prints its move's name") +MANIFESTS["rse/party"] = { prompts = setmetatable({}, { __index = function() return "PROMPT" end }) } +local drawn, resolved = {}, 0 +local font, chrome, cursor, partyChrome = package.loaded["src.ui.game3.frlg_font"], package.loaded["src.ui.game3.chrome"], + package.loaded["src.ui.game3.rs.menu_cursor"], package.loaded["src.ui.game3.rs.party_chrome"] +package.loaded["src.ui.game3.frlg_font"] = { draw = function(text) drawn[#drawn + 1] = text end } +package.loaded["src.ui.game3.chrome"] = { stdFrame = function() end } +package.loaded["src.ui.game3.rs.menu_cursor"] = { draw = function() end } +package.loaded["src.ui.game3.rs.party_chrome"] = { textOptions = function() return {} end } +local menu = { ACTIONS = { "SUMMARY", "CANCEL" }, actionCursor = 1, + _actionTextsFor = function(list) resolved = resolved + 1 return { "RESUME", "RETOUR" } end } +PartyData.drawActions(menu, false) +check(drawn[2] == "RESUME" and drawn[3] == "RETOUR" and resolved == 1, "the action menu draws the texts resolved for its list") +local displayName = package.loaded["src.core.game3.pokemon"].displayName +package.loaded["src.core.game3.pokemon"].displayName = function(mon) return mon.nickname end +BUNDLE.text = { ["PartyMenuPromptTexts[5]"] = { { t = "text", s = "Que faire avec " }, { t = "strvar", n = 1 }, + { t = "text", s = "?" }, { t = "eos" } } } +drawn, menu.ACTIONS, menu._party, menu.cursor = {}, { "SUMMARY", "CANCEL" }, { { nickname = "POUSSIFEU" } }, 1 +PartyData.drawActions(menu, false) +check(drawn[1] == "Que faire avec POUSSIFEU?", "the action prompt reads the cache and names the POKeMON") +BUNDLE.text = {} +drawn = {} +PartyData.drawActions(menu, false) +check(drawn[1] == "Que faire avec POUSSIFEU?", "the prompt resolved for this menu is drawn again without a lookup") +BUNDLE.text = { ["PartyMenuPromptTexts[5]"] = { { t = "text", s = "Que faire avec " }, { t = "strvar", n = 1 }, + { t = "text", s = "?" }, { t = "eos" } } } +drawn, menu.ACTIONS, menu._party = {}, { "SUMMARY", "CANCEL" }, { { nickname = "GOBOU" } } +PartyData.drawActions(menu, false) +check(drawn[1] == "Que faire avec GOBOU?", "a new action menu names its own POKeMON") +package.loaded["src.core.game3.pokemon"].displayName = displayName +package.loaded["src.ui.game3.frlg_font"], package.loaded["src.ui.game3.chrome"] = font, chrome +package.loaded["src.ui.game3.rs.menu_cursor"], package.loaded["src.ui.game3.rs.party_chrome"] = cursor, partyChrome +if failed > 0 then + print(failed .. " check(s) failed") + os.exit(1) +end +print("all checks passed")