Merge pull request #2678 from thibautbus/feat/emerald-translation-runtime

Make Emerald's screens reachable by translation mods
This commit is contained in:
bryanthaboi
2026-10-05 15:46:24 -04:00
committed by GitHub
30 changed files with 742 additions and 78 deletions
+2 -2
View File
@@ -494,8 +494,8 @@ function Adapter.new(battleState, sayFn)
if id and id > 0 then
if ABILITY_BY_ID[id] then return ABILITY_BY_ID[id] end
local ok, Pokemon = pcall(require, "src.core.game3.pokemon")
if ok and Pokemon and Pokemon.abilityName then
local n = Pokemon.abilityName(id)
if ok and Pokemon and Pokemon.romAbilityName then
local n = Pokemon.romAbilityName(id)
if n and n ~= "" and not n:match("^ABILITY") then
return (tostring(n):upper():gsub("%s+", "_"))
end
+26 -11
View File
@@ -228,7 +228,8 @@ local function fill(st, extra)
end
function Fac:refText(st, which, extra)
local ir = Data.arena().refereeStrings[which + 1]
local RomText = require("src.core.game3.rom_text")
local ir = RomText.irOr(RomText.key("gRefereeStringsTable", which), Data.arena().refereeStrings[which + 1])
return Data.battleText(ir, fill(st, extra))
end
@@ -373,11 +374,32 @@ function Fac:runStep(st, step)
return true
end
-- pokeemerald/src/battle_arena.c:412
local JUDGMENT_TEXTS = {
{ Fac.WIN.PLAYER_NAME, "gText_PlayerMon1Name", "playerMon1Name" },
{ Fac.WIN.VS, "gText_Vs", "vs" },
{ Fac.WIN.OPPONENT_NAME, "gText_OpponentMon1Name", "opponentMon1Name" },
{ Fac.WIN.MIND, "gText_Mind", "mind" },
{ Fac.WIN.SKILL, "gText_Skill", "skill" },
{ Fac.WIN.BODY, "gText_Body", "body" },
{ Fac.WIN.JUDGMENT_TITLE, "gText_Judgment", "judgment" },
}
local function judgmentTexts()
local RomText = require("src.core.game3.rom_text")
local text, out = Data.arena().text, {}
for i, row in ipairs(JUDGMENT_TEXTS) do
out[i] = { win = row[1], ir = RomText.irOr(row[2], text[row[3]]) }
end
return out
end
-- pokeemerald/src/battle_arena.c:395
function Fac:judgeStep(st, step)
local j = self.window
if step.state == "open" then
self.window = { fade = 0, target = 8, icons = {}, line = true, buffs = { buff1 = "0", buff2 = "0" } }
self.window = { fade = 0, target = 8, icons = {}, line = true, buffs = { buff1 = "0", buff2 = "0" },
texts = judgmentTexts() }
self.fadeWait = true
return false
elseif step.state == "row" then
@@ -519,16 +541,9 @@ function Fac:draw(st)
Chrome.userFrame(Chrome._frameType or 0, 6, 1, 18, 12)
local W, I = A.windows, A.textInfo
local fl = fill(st, j.buffs)
local function put(win, ir)
windowText(W[win + 1], I[win + 1], fontText(ir, fl))
for _, t in ipairs(j.texts) do
windowText(W[t.win + 1], I[t.win + 1], fontText(t.ir, fl))
end
put(Fac.WIN.PLAYER_NAME, A.text.playerMon1Name)
put(Fac.WIN.VS, A.text.vs)
put(Fac.WIN.OPPONENT_NAME, A.text.opponentMon1Name)
put(Fac.WIN.MIND, A.text.mind)
put(Fac.WIN.SKILL, A.text.skill)
put(Fac.WIN.BODY, A.text.body)
put(Fac.WIN.JUDGMENT_TITLE, A.text.judgment)
if j.line then
local img = self:iconImage(Fac.ANIM.LINE)
for i = 0, 7 do love.graphics.draw(img, 64 + i * 16 - 8, 84 - 8) end
+2 -1
View File
@@ -4,6 +4,7 @@ local PokemonExtract = require("src.import.gba.pokemon_extract")
local Versions = require("src.import.gba.versions")
local ModRuntime = require("src.mods.Runtime")
local CacheBlob = require("src.import.CacheBlob")
local Strings = require("src.core.Strings")
local Pokemon = {}
@@ -331,7 +332,7 @@ function Pokemon.abilityName(abilityId)
if not abilityId or abilityId < 1 then return "-------" end
if not Pokemon._abilityNames then Pokemon.install(Pokemon._cache) end
local n = Pokemon._abilityNames and Pokemon._abilityNames[abilityId]
if n and n ~= "" then return n end
if n and n ~= "" then return Strings(n) end
error("no ROM ability name for ability " .. abilityId, 2)
end
+7
View File
@@ -52,6 +52,13 @@ return {
-- pokeemerald/src/menu_specialized.c:1503
levelUpStats = { "gText_MaxHP", "gText_Attack", "gText_Defense", "gText_SpAtk", "gText_SpDef", "gText_Speed" },
},
-- pokeemerald/src/data/party_menu.h:658
cursorOptionTexts = {
"gText_Summary5", "gText_Switch2", "gText_Cancel2", "gText_Item", "gMenuText_Give", "gText_Take",
"gText_Mail", "gText_Take2", "gText_Read2", "gText_Cancel2", "gText_Shift", "gText_SendOut",
"gText_Enter", "gText_NoEntry", "gText_Store", "gText_Register", "gText_Trade4", "gText_Trade4",
"gMenuText_Toss",
},
-- pokeemerald/src/party_menu.c:2101
buttons = { cancel = "gText_Cancel", confirm = "gMenuText_Confirm" },
-- pokeemerald/src/party_menu.c:2557
+10
View File
@@ -56,6 +56,16 @@ function RomText.has(key)
return cachedText(b, key) ~= nil
end
function RomText.irOr(key, fallback)
if key ~= nil and RomText.has(key) then return RomText.ir(key) end
return fallback
end
function RomText.refIr(ref)
if type(ref) ~= "table" then return nil end
return RomText.irOr(ref.name, RomText.irOr(ref.key, ref.ir))
end
local SOURCE_FORMS = {}
for _, named in ipairs({ false, true }) do
for _, nl in ipairs({ "\n", "\\n" }) do
+1 -1
View File
@@ -593,7 +593,7 @@ function Apprentice.message(sess, which)
local e = map[which]
if not e then return nil end
local ref = e[1][id] and e[1][id][e[2]]
return ref and ref.ir
return ref and require("src.core.game3.rom_text").refIr(ref)
end
-- pokeemerald/src/apprentice.c:564
+11 -4
View File
@@ -617,10 +617,16 @@ local function setStr(ctx, adapters, n, v)
Util.setStringVar(ctx, adapters, n, v)
end
local function tableText(list, name, i)
local RomText = require("src.core.game3.rom_text")
return RomText.irOr(RomText.key(name, i - 1), list[i])
end
Dome.tableText = tableText
-- pokeemerald/src/battle_dome.c:2564
function Dome.roundText(sess)
local f = Util.frontier(sess)
return Data.dome().text.rounds[(tonumber(f.curChallengeBattleNum) or 0) + 1]
return tableText(Data.dome().text.rounds, "gRoundsStringTable", (tonumber(f.curChallengeBattleNum) or 0) + 1)
end
-- pokeemerald/src/battle_dome.c:3028
@@ -1033,7 +1039,8 @@ function Dome.trainerCard(sess, tid)
card.species[i] = facilityMon(sess, f.domeMonIds[tid + 1][i]).species
end
end
card.potential = M.text.potential[(trainerId == D.TRAINER_FRONTIER_BRAIN) and Dome.TRAINERS_COUNT + 1 or tid + 1]
card.potential = tableText(M.text.potential, "sBattleDomePotentialTexts",
(trainerId == D.TRAINER_FRONTIER_BRAIN) and Dome.TRAINERS_COUNT + 1 or tid + 1)
local pts = {}
for k = 0, Dome.NUM_MOVE_POINT_TYPES - 1 do pts[k] = 0 end
for i = 0, D.PARTY_SIZE - 1 do
@@ -1061,8 +1068,8 @@ function Dome.trainerCard(sess, tid)
style = style + 1
end
card.style = style
card.styleText = M.text.styles[style + 1]
card.statText = M.text.stats[Dome.statTextId(sess, tid) + 1]
card.styleText = tableText(M.text.styles, "sBattleDomeOpponentStyleTexts", style + 1)
card.statText = tableText(M.text.stats, "sBattleDomeOpponentStatsTexts", Dome.statTextId(sess, tid) + 1)
return card
end
@@ -1,6 +1,7 @@
local Rse = require("src.core.game3.rse.init")
local D = require("src.core.game3.rse.frontier.trainers")
local Util = require("src.core.game3.rse.frontier.util")
local RomText = require("src.core.game3.rom_text")
local NativesFrontier = {}
@@ -307,7 +308,7 @@ function NativesFrontier.ssTidalMultichoice(ctx, adapters)
local FrlgFont = require("src.ui.game3.frlg_font")
for i, id in ipairs(sel) do
local ref = D.manifest().ssTidalDestinations[id + 1]
labels[i] = TextIR.toPlain(ref.ir, {})
labels[i] = TextIR.toPlain(RomText.refIr(ref), {})
local w = FrlgFont.measure(labels[i])
if w > widest then widest = w end
end
+10
View File
@@ -416,6 +416,16 @@ function SummaryData.abilityDescription(abilityId, abilityName)
return Strings(d.ABILITIES["ABILITY_"])
end
-- pokeemerald/src/data/contest_text_tables.h:220
function SummaryData.contestEffectDescription(effect)
local text = type(effect) == "table" and effect.description or effect
return Strings(type(text) == "string" and text or "")
end
function SummaryData.contestCategoryName(name)
return Strings(name or "")
end
function SummaryData.moveDescription(moveId, moveName)
local d = get_descriptions()
moveName = rom_name("romMoveName", moveId, moveName)
+1 -1
View File
@@ -155,7 +155,7 @@ local function rsePrint()
local idx = pyramid.location(sess) == pyramid.LOCATION.TOP and #headers or
(tonumber(pyramid.frontier(sess).curChallengeBattleNum) or 0) + 1
local ref = headers[idx]
if ref then name = require("src.core.game3.scripting.text_ir").toPlain(ref.ir) end
if ref then name = require("src.core.game3.scripting.text_ir").toPlain(RomText.refIr(ref)) end
elseif sec == rseConstants():id("region_map_sections", "MAPSEC_SECRET_BASE") then
-- pokeemerald/src/secret_base.c:735
local sb = R.system("secretBase")
+26 -3
View File
@@ -41,6 +41,27 @@ end
local placeholderCache = {}
-- pokeemerald/src/strings.c:6
local function cartPlaceholders(extracted)
local Extract = require("src.import.gba.text_placeholders_extract")
local RomText = require("src.core.game3.rom_text")
local function value(name)
local label = Extract.SYMBOLS[name]
if label and RomText.has(label) then return RomText.plain(label) end
return extracted[name]
end
local out, byGender = {}, {}
for name, v in pairs(extracted) do out[name] = v end
for name in pairs(Extract.SYMBOLS) do out[name] = value(name) end
for name, pair in pairs(extracted.byGender or {}) do byGender[name] = pair end
for name, pair in pairs(Extract.BY_GENDER) do
byGender[name] = { male = out[pair.male], female = out[pair.female] }
end
out.byGender = byGender
return out
end
Message.cartPlaceholders = cartPlaceholders
-- pokeemerald/src/string_util.c:456
TextIR.setContextProvider(function(kind, dialect, ctx)
if kind == "gender" then
@@ -66,13 +87,15 @@ TextIR.setContextProvider(function(kind, dialect, ctx)
local GameVersion = require("src.core.GameVersion")
local s = liveSession()
local id = (s and s.version) or GameVersion.get() or ""
local Sp = package.loaded["src.core.game3.scripting.space"]
local bundle = Sp and Sp.bundle
local hit = placeholderCache[id]
if hit then return hit end
if hit and hit.bundle == bundle then return hit.values end
local okC, CacheFs = pcall(require, "src.import.CacheFs")
local t = okC and CacheFs.loadActive(dialect.placeholders) or nil
if type(t) == "table" then
placeholderCache[id] = t
return t
placeholderCache[id] = { bundle = bundle, values = cartPlaceholders(t) }
return placeholderCache[id].values
end
return nil
end)
+26 -6
View File
@@ -90,7 +90,7 @@ local function gameFieldMoves()
index[label] = j - 1
byMove[move] = label
end
return { labels = labels, base = base, index = index, byMove = byMove }
return { labels = labels, base = base, index = index, byMove = byMove, moves = moves, texts = p.cursorOptionTexts }
end
local function fieldMoveIndex()
@@ -111,14 +111,32 @@ local function cursor_option_text(act)
local p = partyUi()
if p and p.actionText then return p.actionText(act, g) end
if g then
-- pokeemerald/src/data/party_menu.h:658
local fm = g.index[act]
if fm then return g.labels[g.base + fm + 1] end
return g.labels[(assert(CURSOR_OPTION[act], act)) + 1]
if fm then
local move = g.moves[fm + 1]
return move and Pokemon.moveName(move) or g.labels[g.base + fm + 1]
end
local i = assert(CURSOR_OPTION[act], act)
local key = g.texts and g.texts[i + 1]
if key and RomText.has(key) then return RomText.plain(key) end
return g.labels[i + 1]
end
local fm = FIELD_MOVE_INDEX[act]
if fm then return RomText.at("sCursorOptions", CURSOR_OPTION_FIELD_MOVES + fm) end
return RomText.at("sCursorOptions", (assert(CURSOR_OPTION[act], act)))
end
PartyMenu._cursorOptionText = cursor_option_text
-- pokeemerald/src/party_menu.c:2557
local function action_texts(list)
local cached = PartyMenu._actionTexts
if cached and cached.list == list then return cached.texts end
local texts = {}
for i, act in ipairs(list) do texts[i] = cursor_option_text(act) end
PartyMenu._actionTexts = { list = list, texts = texts }
return texts
end
local FR_INSETS = { msgX = 2, msgY = 2, actX = 9, actY = 2, cursorX = 1 }
local function textInsets()
@@ -2859,12 +2877,13 @@ function PartyMenu.draw()
local popX = 22
local popY = 19 - popH
Window.stdFrame(Window.template(popX, popY, popW, popH))
for i, act in ipairs(PartyMenu.ITEM_ACTIONS) do
local texts = action_texts(PartyMenu.ITEM_ACTIONS)
for i in ipairs(PartyMenu.ITEM_ACTIONS) do
local rowY = (popY * 8) + (i - 1) * 16 + ins.actY
if i == PartyMenu.itemActionCursor then
Window.cursorPx(popX * 8 + ins.cursorX, rowY)
end
FrlgFont.draw(cursor_option_text(act), popX * 8 + ins.actX, rowY, { colors = FrlgFont.COLOR.NORMAL })
FrlgFont.draw(texts[i], popX * 8 + ins.actX, rowY, { colors = FrlgFont.COLOR.NORMAL })
end
elseif PartyMenu.mode == "action" and p and p.drawActions then
p.drawActions(PartyMenu, false)
@@ -2878,6 +2897,7 @@ function PartyMenu.draw()
local popX = 19
local popY = 19 - popH
Window.stdFrame(Window.template(popX, popY, popW, popH))
local texts = action_texts(PartyMenu.ACTIONS)
for i, act in ipairs(PartyMenu.ACTIONS) do
local rowY = (popY * 8) + (i - 1) * 16 + ins.actY
if i == PartyMenu.actionCursor then
@@ -2885,7 +2905,7 @@ function PartyMenu.draw()
end
local isFm = PartyMenu._fieldMoveNames and PartyMenu._fieldMoveNames[act]
local col = isFm and (FrlgFont.COLOR.BLUE or FrlgFont.COLOR.MALE_NPC) or FrlgFont.COLOR.NORMAL
FrlgFont.draw(cursor_option_text(act), popX * 8 + ins.actX, rowY, { colors = col })
FrlgFont.draw(texts[i], popX * 8 + ins.actX, rowY, { colors = col })
end
else
if p and p.drawPrompt then
+4 -1
View File
@@ -730,7 +730,10 @@ function UI:printContestMoveDescription(move)
self:fillBox(0, TILE_EMPTY_JAM_HEART, 0x15, 0x20, MAX_CONTEST_MOVE_HEARTS, 0x01, 0x11)
self:fillBox(0, TILE_FILLED_JAM_HEART, 0x15, 0x20, hearts, 0x01, 0x11)
local key = "gContestEffectDescriptionPointers[" .. (cm.effect or 0) .. "]"
local desc = UI.has(key) and UI.plain(key) or ""
local desc = ""
if UI.has(key) then
desc = require("src.core.game3.summary_data").contestEffectDescription(UI.plain(key))
end
self.win[WIN_MOVE_DESCRIPTION] = { text = desc, x = 0, y = 1, fg = 15, shadow = 8 }
self.win[WIN_SLASH] = { text = UI.plain("gText_Slash"), x = 0, y = 1, fg = 15, shadow = 8 }
end
+16 -4
View File
@@ -280,7 +280,7 @@ local function drawTree()
drawLines()
love.graphics.draw(treeImage(), 0, 0)
local W = M().treeWindows
local title = textOf(M().text.battleTourney)
local title = st.treeTitle
local tw = W[3]
drawSpaced(title, tw.left * 8 + centerX(title, 0x70, "short", 2), tw.top * 8 + 1,
colors(COLOR.DYNAMIC_5, COLOR.DYNAMIC_4), "short", 2)
@@ -425,12 +425,12 @@ local function drawMatchCard(card, ox, oy)
local W = M().cardWindows
local c = colors(COLOR.DYNAMIC_5, COLOR.DYNAMIC_4)
local d = card.data
local wt = textOf(M().text.wins[d.win.id + 1], { d.win.var1, d.win.var2 })
local wt = textOf(d.winIr, { d.win.var1, d.win.var2 })
winText(W[9], wt, 0, 0, c, "normal", 0, ox, oy)
local left, right = d.sides[1], d.sides[2]
winText(W[7], left.name, centerX(left.name, 0x40, "short", 2), 2, c, "short", 2, ox, oy)
winText(W[8], right.name, centerX(right.name, 0x40, "short", 2), 2, c, "short", 2, ox, oy)
local mn = textOf(M().text.matchNumbers[d.matchNo + 1])
local mn = textOf(d.matchNoIr)
winText(W[6], mn, centerX(mn, 0xA0, "short", 0), 2, c, "short", 0, ox, oy)
drawPic(picFor(left.trainerId), ox + 48, oy + 88, left.lost)
drawPic(picFor(right.trainerId), ox + 192, oy + 88, right.lost)
@@ -443,7 +443,17 @@ end
local function makeCard(kind, id)
if kind == "trainer" then return { kind = kind, id = id, data = trainerCardData(id) } end
return { kind = kind, id = id, data = matchCardData(id) }
local data = matchCardData(id)
local Dome = require("src.core.game3.rse.frontier.dome")
-- pokeemerald/src/battle_dome.c:4931, :4977
data.winIr = Dome.tableText(M().text.wins, "sBattleDomeWinTexts", data.win.id + 1)
data.matchNoIr = Dome.tableText(M().text.matchNumbers, "sBattleDomeMatchNumberTexts", data.matchNo + 1)
return { kind = kind, id = id, data = data }
end
-- pokeemerald/src/battle_dome.c:5415
local function treeTitle()
return textOf(require("src.core.game3.rom_text").irOr("gText_BattleTourney", M().text.battleTourney))
end
local function drawCard(card, ox, oy)
@@ -541,6 +551,7 @@ function UI.showTree(opts)
st.cursor = opts.cursor or 0
st.frame, st.bg2y, st.bg3y = 0, 0, 11
buildTree(st.mode)
st.treeTitle = treeTitle()
st.phase = "fadein"
fadeIn()
push()
@@ -596,6 +607,7 @@ local function backToTree()
st.mode = rt.mode
st.bg2y, st.bg3y = 0, 11
buildTree(st.mode)
st.treeTitle = st.treeTitle or treeTitle()
st.phase = "fadein"
fadeIn()
end
+2 -1
View File
@@ -1,6 +1,7 @@
local Stack = require("src.ui.game3.stack")
local FrlgFont = require("src.ui.game3.frlg_font")
local RomText = require("src.core.game3.rom_text")
local Strings = require("src.core.Strings")
local Kit = require("src.ui.game3.rse.scene_kit")
local Gfx = require("src.ui.game3.rse.pokeblock_gfx")
local PalFade = require("src.core.game3.pal_fade")
@@ -162,7 +163,7 @@ function Common.categoryText(species)
entriesPack = Gfx.loadLua("data/generated/gba/pokemon/pokedex/entries.lua") or {}
end
local e = entriesPack[tonumber(species) or 0] or {}
return (e.category or "") .. " " .. RomText.plain("gText_Pokemon")
return Strings(e.category or "") .. " " .. RomText.plain("gText_Pokemon")
end
function Common.speciesName(species)
+29 -5
View File
@@ -149,12 +149,32 @@ local function updateHighlight(cur, prev)
local _ = g
end
-- pokeemerald/src/frontier_pass.c:530
local function description(i)
return RomText.irOr(RomText.key("sPassAreaDescriptions", i), man().descriptions[i + 1])
end
-- pokeemerald/src/frontier_pass.c:556
local LANDMARK_TEXTS = {
{ "gText_BattleTower3", "gText_BattleTowerDesc" },
{ "gText_BattleDome2", "gText_BattleDomeDesc" },
{ "gText_BattlePalace2", "gText_BattlePalaceDesc" },
{ "gText_BattleArena2", "gText_BattleArenaDesc" },
{ "gText_BattleFactory2", "gText_BattleFactoryDesc" },
{ "gText_BattlePike2", "gText_BattlePikeDesc" },
{ "gText_BattlePyramid2", "gText_BattlePyramidDesc" },
}
local function landmarkText(i, field)
local labels = LANDMARK_TEXTS[i + 1]
return RomText.irOr(labels and labels[field == "name" and 1 or 2], man().landmarks[i + 1][field])
end
local function describe(area)
local d = man().descriptions
if area == A.RECORD and not st.hasRecord then
st.desc = textOf(d[A.NOTHING + 1])
st.desc = textOf(description(A.NOTHING))
elseif area ~= A.NOTHING then
st.desc = textOf(d[area + 1])
st.desc = textOf(description(area))
else
st.desc = nil
end
@@ -456,6 +476,10 @@ function Map.show()
Map.pal:beginFade(PalFade.ALL, 0, 16, 0, PalFade.WHITE)
Map.image = nil
Map.head = Map.headPosition()
Map.landmarks = {}
for i = 0, NUM_FACILITIES - 1 do
Map.landmarks[i] = { name = textOf(landmarkText(i, "name")), description = textOf(landmarkText(i, "description")) }
end
end
-- pokeemerald/src/frontier_pass.c:1582
@@ -544,9 +568,9 @@ function Map.draw()
love.graphics.draw(Map.image, 0, 0)
local lm = man().landmarks
for i = 0, NUM_FACILITIES - 1 do
drawText(textOf(lm[i + 1].name), 160 + 4, 8 + i * 16 + 1, colors(i == Map.pos and 2 or 1), "narrow")
drawText(Map.landmarks[i].name, 160 + 4, 8 + i * 16 + 1, colors(i == Map.pos and 2 or 1), "narrow")
end
drawText(textOf(lm[Map.pos + 1].description), 16 + 4, 128, colors(0))
drawText(Map.landmarks[Map.pos].description, 16 + 4, 128, colors(0))
local cur = spriteFrame("cursor", 4, 16, 16, p.cursor)
love.graphics.draw(cur, 155 + 8, Map.cursorY - 8, 0, -1, 1)
local mark = lm[Map.pos + 1]
+4 -4
View File
@@ -338,12 +338,12 @@ function Records.buildRankingHall(sess, hallId, lvlMode)
local win = newWindow(HALL_TPL)
local man = D().manifest()
local pair = man.recordsChallengeTexts[hallId + 1]
local facilityName = plainIr(pair[1].ir)
put(win, plainIr(pair[2].ir, { facilityName }), 0, 1)
local lvl = plainIr(man.levelModeText[lvlMode + 1].ir)
local facilityName = plainIr(RomText.refIr(pair[1]))
put(win, plainIr(RomText.refIr(pair[2]), { facilityName }), 0, 1)
local lvl = plainIr(RomText.refIr(man.levelModeText[lvlMode + 1]))
put(win, lvl, (240 - 32) - width(lvl), 1)
local rows = U().rankingHall(sess, hallId, lvlMode)
local recordText = man.hallFacilityToRecordsText[hallId + 1].ir
local recordText = RomText.refIr(man.hallFacilityToRecordsText[hallId + 1])
local link = hallId == U().RANKING_HALL.TOWER_LINK
for i, r in ipairs(rows) do
local pos = i - 1
+2 -1
View File
@@ -1,4 +1,5 @@
local MapSections = require("src.import.gba.rse.map_sections_extract")
local Strings = require("src.core.Strings")
local Mapsec = {}
@@ -45,7 +46,7 @@ end
-- pokeemerald/src/region_map.c:1568
function Mapsec.name(sec)
local e = Mapsec.entry(sec)
return e and e.name or ""
return e and e.name and Strings(e.name) or ""
end
-- pokeemerald/src/map_name_popup.c:403
+5 -2
View File
@@ -399,8 +399,11 @@ local function draw_contest(moveId)
right(RomText.plain("gText_MoveRelearnerJam"), 92, 41)
if not moveId then return end
local cm, eff, c = contest_row(moveId)
if cm then print_at(c.categories and c.categories[cm.category] or "", 4, 25) end
if eff then print_lines(eff.description, 0, 65, NARROW) end
if cm then
local category = c.categories and c.categories[cm.category]
print_at(SummaryData.contestCategoryName(category), 4, 25)
end
if eff then print_lines(SummaryData.contestEffectDescription(eff), 0, 65, NARROW) end
draw_hearts(eff)
end
+84 -18
View File
@@ -1,4 +1,5 @@
local Kit = require("src.ui.game3.rse.scene_kit")
local Strings = require("src.core.Strings")
local FrlgFont = require("src.ui.game3.frlg_font")
local RomText = require("src.core.game3.rom_text")
local Pal = require("src.core.game3.pal_fade")
@@ -935,7 +936,12 @@ local function monInfo(s, nat, nationalNumber, owned, newEntry)
out[#out + 1] = { text = RomText.plain("gText_NumberClear01") .. string.format("%03d", num), x = 0x60, y = 0x19 }
local sp = Pokedex.speciesOf(nat)
out[#out + 1] = { text = sp ~= 0 and pokemon().name(sp) or Gfx.manifest().tenDashes, x = 0x84, y = 0x19 }
local category = owned and ((e.category or "") .. " " .. RomText.plain("gText_Pokemon")) or RomText.plain("gText_5MarksPokemon")
local category
if owned then
category = Strings(e.category or "") .. " " .. RomText.plain("gText_Pokemon")
else
category = RomText.plain("gText_5MarksPokemon")
end
out[#out + 1] = { text = category, x = 0x64, y = 0x29 }
out[#out + 1] = { text = RomText.plain("gText_HTHeight"), x = 0x60, y = 0x39 }
out[#out + 1] = { text = RomText.plain("gText_WTWeight"), x = 0x60, y = 0x49 }
@@ -946,7 +952,7 @@ local function monInfo(s, nat, nationalNumber, owned, newEntry)
out[#out + 1] = { text = RomText.plain("gText_UnkHeight"), x = 0x81, y = 0x39 }
out[#out + 1] = { text = RomText.plain("gText_UnkWeight"), x = 0x81, y = 0x49 }
end
local desc = owned and (e.description or "") or ""
local desc = owned and Strings(e.description or "") or ""
local w = 0
for line in (desc .. "\n"):gmatch("(.-)\n") do w = math.max(w, FrlgFont.measure(line) or 0) end
out[#out + 1] = { text = desc, x = w < 240 and math.floor((240 - w) / 2) or 0, y = 95 }
@@ -1587,8 +1593,54 @@ 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
-- pokeemerald/src/pokedex.c:1330
local TYPE_OPTION = { "gText_DexSearchTypeNone" }
for _, t in ipairs({ 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17 }) do
TYPE_OPTION[#TYPE_OPTION + 1] = RomText.key("gTypeNames", t)
end
local SEARCH_OPTION_TEXTS = {
[SEARCH.NAME] = { titles = { "gText_DexSearchDontSpecify", "gText_DexSearchAlphaABC", "gText_DexSearchAlphaDEF",
"gText_DexSearchAlphaGHI", "gText_DexSearchAlphaJKL", "gText_DexSearchAlphaMNO", "gText_DexSearchAlphaPQR",
"gText_DexSearchAlphaSTU", "gText_DexSearchAlphaVWX", "gText_DexSearchAlphaYZ" } },
[SEARCH.COLOR] = { titles = { "gText_DexSearchDontSpecify", "gText_DexSearchColorRed", "gText_DexSearchColorBlue",
"gText_DexSearchColorYellow", "gText_DexSearchColorGreen", "gText_DexSearchColorBlack", "gText_DexSearchColorBrown",
"gText_DexSearchColorPurple", "gText_DexSearchColorGray", "gText_DexSearchColorWhite", "gText_DexSearchColorPink" } },
[SEARCH.TYPE_LEFT] = { titles = TYPE_OPTION },
[SEARCH.TYPE_RIGHT] = { titles = TYPE_OPTION },
[SEARCH.ORDER] = {
titles = { "gText_DexSortNumericalTitle", "gText_DexSortAtoZTitle", "gText_DexSortHeaviestTitle",
"gText_DexSortLightestTitle", "gText_DexSortTallestTitle", "gText_DexSortSmallestTitle" },
descriptions = { "gText_DexSortNumericalDescription", "gText_DexSortAtoZDescription",
"gText_DexSortHeaviestDescription", "gText_DexSortLightestDescription", "gText_DexSortTallestDescription",
"gText_DexSortSmallestDescription" },
},
[SEARCH.MODE] = {
titles = { "gText_DexHoennTitle", "gText_DexNatTitle" },
descriptions = { "gText_DexHoennDescription", "gText_DexNatDescription" },
},
}
-- pokeemerald/src/pokedex.c:1017
local TOPBAR_DESCRIPTIONS = { "gText_SearchForPkmnBasedOnParameters", "gText_SwitchPokedexListings",
"gText_ReturnToPokedex" }
-- pokeemerald/src/pokedex.c:1042
local ITEM_DESCRIPTIONS = { "gText_ListByFirstLetter", "gText_ListByBodyColor", "gText_ListByType",
"gText_ListByType", "gText_SelectPokedexListingMode", "gText_SelectPokedexMode", "gText_ExecuteSearchSwitch" }
local function topBarDescription(i)
return cartText(TOPBAR_DESCRIPTIONS[i + 1], Gfx.manifest().search.topBar[i + 1].description)
end
local function itemDescription(i)
return cartText(ITEM_DESCRIPTIONS[i + 1], Gfx.manifest().search.items[i + 1].description)
end
-- pokeemerald/src/pokedex.c:1437
local function searchOptionTexts(which)
local function searchOptionList(which)
local sm = Gfx.manifest().search
if which == SEARCH.NAME then return sm.names end
if which == SEARCH.COLOR then return sm.colors end
@@ -1598,6 +1650,23 @@ local function searchOptionTexts(which)
return {}
end
local function searchOptionTexts(which)
local list = searchOptionList(which)
local keys = SEARCH_OPTION_TEXTS[which]
if not keys then return list end
local out = {}
for i, t in ipairs(list) do
out[i] = {
title = cartText(keys.titles[i], t.title),
description = cartText(keys.descriptions and keys.descriptions[i], t.description),
}
end
return out
end
Pokedex.searchOptionTexts = searchOptionTexts
Pokedex.topBarDescription = topBarDescription
Pokedex.itemDescription = itemDescription
local function searchSel(q, which)
local c = q.cursor[which] or 0
local o = q.scroll[which] or 0
@@ -1766,7 +1835,7 @@ function tasks.loadSearch(s)
elseif st == 1 then
local q = s.searchState
setDefaultSearchModeAndOrder(s, q)
setSearchMessage(q, Gfx.manifest().search.topBar[TOPBAR.SEARCH + 1].description)
setSearchMessage(q, topBarDescription(TOPBAR.SEARCH))
s.searchArrows = {
{ x = 184, y = 4, down = false, data2 = 0, invisible = true },
{ x = 184, y = 108, down = true, data2 = 0, invisible = true },
@@ -1784,7 +1853,7 @@ function tasks.loadSearch(s)
s.state = 0
s.fn = "searchTopBar"
s.searchState.phase = "topbar"
setSearchMessage(s.searchState, Gfx.manifest().search.topBar[s.searchState.topBar + 1].description)
setSearchMessage(s.searchState, topBarDescription(s.searchState.topBar))
refreshSearch(s)
end
end
@@ -1794,7 +1863,6 @@ end
function tasks.searchTopBar(s, inp)
local new = inp.new or {}
local q = s.searchState
local sm = Gfx.manifest().search
if new.b then
se("SE_PC_OFF")
s.fn = "exitSearch"
@@ -1815,20 +1883,20 @@ function tasks.searchTopBar(s, inp)
return
end
q.phase = "menu"
setSearchMessage(q, sm.items[q.menuItem + 1].description)
setSearchMessage(q, itemDescription(q.menuItem))
refreshSearch(s)
return
end
if new.left and q.topBar > TOPBAR.SEARCH then
se("SE_DEX_PAGE")
q.topBar = q.topBar - 1
setSearchMessage(q, sm.topBar[q.topBar + 1].description)
setSearchMessage(q, topBarDescription(q.topBar))
refreshSearch(s)
end
if new.right and q.topBar < TOPBAR.CANCEL then
se("SE_DEX_PAGE")
q.topBar = q.topBar + 1
setSearchMessage(q, sm.topBar[q.topBar + 1].description)
setSearchMessage(q, topBarDescription(q.topBar))
refreshSearch(s)
end
end
@@ -1850,7 +1918,7 @@ function tasks.searchMenu(s, inp)
setDefaultSearchModeAndOrder(s, q)
q.phase = "topbar"
s.fn = "searchTopBar"
setSearchMessage(q, sm.topBar[q.topBar + 1].description)
setSearchMessage(q, topBarDescription(q.topBar))
refreshSearch(s)
return
end
@@ -1896,7 +1964,7 @@ function tasks.searchMenu(s, inp)
if nxt ~= 0xFF then
se(sound)
q.menuItem = nxt
setSearchMessage(q, sm.items[q.menuItem + 1].description)
setSearchMessage(q, itemDescription(q.menuItem))
refreshSearch(s)
end
end
@@ -1947,7 +2015,7 @@ function tasks.searchDone(s, inp)
else
s.fn = "searchMenu"
q.phase = "menu"
setSearchMessage(q, Gfx.manifest().search.items[q.menuItem + 1].description)
setSearchMessage(q, itemDescription(q.menuItem))
refreshSearch(s)
se("SE_BALL")
end
@@ -1959,8 +2027,7 @@ function tasks.searchParam(s, inp)
local new, rep = inp.new or {}, inp.rep or {}
local q = s.searchState
local mi = q.menuItem
local texts = searchOptionTexts(mi)
local maxOption = #texts - 1
local maxOption = #searchOptionList(mi) - 1
if new.a or new.b then
se(new.a and "SE_PIN" or "SE_BALL")
if new.b then
@@ -1969,7 +2036,7 @@ function tasks.searchParam(s, inp)
end
q.paramBox = false
q.phase = "menu"
setSearchMessage(q, Gfx.manifest().search.items[mi + 1].description)
setSearchMessage(q, itemDescription(mi))
refreshSearch(s)
s.fn = "searchMenu"
return
@@ -1994,7 +2061,7 @@ function tasks.searchParam(s, inp)
end
if moved then
se("SE_SELECT")
local t = texts[searchSel(q, mi) + 1]
local t = searchOptionTexts(mi)[searchSel(q, mi) + 1]
setSearchMessage(q, t and t.description or "")
refreshSearch(s)
end
@@ -2138,8 +2205,7 @@ function Pokedex.frame(s, inp)
end
if s.searchArrows and s.fn == "searchParam" then
local q = s.searchState
local texts = searchOptionTexts(q.menuItem)
local last = #texts - 1
local last = #searchOptionList(q.menuItem) - 1
for _, a in ipairs(s.searchArrows) do
local off = q.scroll[q.menuItem] or 0
if a.down then
+8 -3
View File
@@ -100,11 +100,18 @@ local function itemName(id)
end
-- pokeemerald/src/battle_pyramid_bag.c:379
-- pokeemerald/src/battle_pyramid_bag.c:687
function PBag.returnTo(location)
local ret = Py().manifest().bagReturnTo[(PBag.LOCATION[location] or 0) + 1]
return ret and require("src.core.game3.scripting.text_ir").toPlain(RomText.refIr(ret), {}) or ""
end
function PBag.show(opts)
opts = opts or {}
for k in pairs(st) do st[k] = nil end
st.session = opts.session
st.location = opts.location or "field"
st.returnTo = PBag.returnTo(st.location)
st.onClose = opts.onClose
st.onUse = opts.onUse
st.onGive = opts.onGive
@@ -465,9 +472,7 @@ local function description()
return RomText.plain("gText_MoveVar1Where", { stringVars = { itemName(items[st.swapFrom + 1]) } })
end
if item then return ItemsData.description(item) or "" end
local ret = Py().manifest().bagReturnTo[(PBag.LOCATION[st.location] or 0) + 1]
local TextIR = require("src.core.game3.scripting.text_ir")
return RomText.plain("gText_ReturnToVar1", { stringVars = { ret and TextIR.toPlain(ret.ir, {}) or "" } })
return RomText.plain("gText_ReturnToVar1", { stringVars = { st.returnTo or "" } })
end
PBag.description = description
+8 -2
View File
@@ -353,14 +353,20 @@ local function newState(opts)
end
RegionMap.newState = newState
-- pokeemerald/src/region_map.c:349
local MULTI_NAME_TABLES = { "sEverGrandeCityNames" }
-- pokeemerald/src/region_map.c:1760
function RegionMap.updateFlyText(s)
s.flyText = nil
if s.mapSecType > TYPE.NONE then
for _, m in ipairs(RegionMap.manifest().multiNameFlyDestinations) do
for i, m in ipairs(RegionMap.manifest().multiNameFlyDestinations) do
if s.mapSecId == m.mapSecId then
if flagSet(s, m.flag) then
s.flyText = { tall = true, name = s.mapSecName, sub = m.names[s.posWithinMapSec + 1] or "" }
local sub = m.names[s.posWithinMapSec + 1] or ""
local key = MULTI_NAME_TABLES[i] and RomText.key(MULTI_NAME_TABLES[i], s.posWithinMapSec)
if key and RomText.has(key) then sub = RomText.plain(key) end
s.flyText = { tall = true, name = s.mapSecName, sub = sub }
end
break
end
+2 -1
View File
@@ -1,5 +1,6 @@
local Kit = require("src.ui.game3.rse.scene_kit")
local RomText = require("src.core.game3.rom_text")
local Strings = require("src.core.Strings")
local FrlgFont = require("src.ui.game3.frlg_font")
local Pal = require("src.core.game3.pal_fade")
local Trig = require("src.core.game3.trig")
@@ -126,7 +127,7 @@ local function categoryText(species, policy)
local nat = Pokemon.national and Pokemon.national(species) or species
local row = dex and dex[nat]
-- pokeemerald/src/international_string_util.c:86
local category = (row and row.category) or ""
local category = Strings((row and row.category) or "")
if policy then return policy.categoryText(category, RomText.plain(policy.categoryKey)) end
return category .. " " .. RomText.plain("gText_Pokemon")
end
+3 -2
View File
@@ -5,6 +5,7 @@ local RomText = require("src.core.game3.rom_text")
local Pokemon = require("src.core.game3.pokemon")
local ItemsData = require("src.core.game3.items_data")
local SummaryData = require("src.core.game3.summary_data")
local Strings = require("src.core.Strings")
local RseSummary = {}
@@ -416,7 +417,7 @@ local function drawInfo(m, Sm, mon, egg)
dynamic = {
[0] = RomText.ir("sMemoNatureTextColor"), [1] = RomText.ir("sMemoMiscTextColor"),
[2] = RomText.at("gNatureNamePointers", natureId), [3] = tostring(metLevel == 0 and 5 or metLevel),
[4] = secName or "", [5] = RomText.ir("gText_EmptyString5"),
[4] = secName and Strings(secName) or "", [5] = RomText.ir("gText_EmptyString5"),
},
})
end
@@ -545,7 +546,7 @@ local function drawMoves(m, Sm, mon, contest, detail)
drawTile(m, "hearts", i < appeal and 1 or 0, tx * 8, ty * 8)
drawTile(m, "hearts", i < jam and 3 or 4, tx * 8, (ty + 2) * 8)
end
put(m, dw, eff.description or "", 6, 1, 0)
put(m, dw, SummaryData.contestEffectDescription(eff), 6, 1, 0)
end
else
local pa = win(m, W.POWER_ACC)
+1 -1
View File
@@ -32,7 +32,7 @@ function Records.lines(sess)
local y = 18
local modes = Hl.manifest().modeStrings
for i = 1, Hl.NUM_MODES do
out[#out + 1] = { text = TextIR.toPlain(modes[i].ir, {}), x = 0, y = y }
out[#out + 1] = { text = TextIR.toPlain(RomText.refIr(modes[i]), {}), x = 0, y = y }
y = y + 15
local m, s, f = Hl.timeParts(sess.trainerHillTimes[i])
local vars = { string.format("%2d", m), string.format("%2d", s), string.format("%02d", f) }
+6 -2
View File
@@ -905,23 +905,27 @@ local function draw_page_skills(mon)
-- Party stores ability as numeric id (e.g. 65 = OVERGROW); resolve to name.
local abilityId = tonumber(mon.abilityId) or tonumber(mon.ability)
local ability = mon.abilityName
local abilityNameTranslated = false
if type(mon.ability) == "string" and mon.ability ~= "" and not tonumber(mon.ability) then
ability = mon.ability
end
if (not ability or ability == "") and abilityId and abilityId > 0 then
ability = Pokemon.abilityName(abilityId)
abilityNameTranslated = true
end
if not ability or ability == "" then
local aid = Pokemon.abilityId and Pokemon.abilityId(Pokemon.speciesOf(mon), mon.personality or 0)
if aid and aid > 0 then
abilityId = aid
ability = Pokemon.abilityName(aid)
abilityNameTranslated = true
end
end
ability = ability or "—"
local ax, ay = cxy("abilityName", 74, 129)
-- No registry renames abilities; a translation reaches the name through Strings().
draw_text(Strings(tostring(ability)), ax, ay, 80, "NORMAL")
local abilityText = tostring(ability)
if not abilityNameTranslated then abilityText = Strings(abilityText) end
draw_text(abilityText, ax, ay, 80, "NORMAL")
local desc = SummaryData.abilityDescription(abilityId, tostring(ability))
local ad = coords().abilityDesc or { x = 10, y = 143, w = 232 }
draw_text(desc, ad.x or 10, ad.y or 143, ad.w or 232, "NORMAL")
@@ -0,0 +1,209 @@
-- Emerald screens that keep their own copy of a cart string read the script
-- cache's text for it when the cache holds it, the way script messages do,
-- so a mod's text overrides reach them; the pack's copy stays the fallback.
package.path = "./?.lua;./?/init.lua;" .. package.path
local function ir(s) return { { t = "text", s = s }, { t = "eos" } } end
local BUNDLE = { text = {} }
package.loaded["src.core.game3.scripting.space"] = { ensureBundle = function() return BUNDLE end }
local RomText = require("src.core.game3.rom_text")
local TextIR = require("src.core.game3.scripting.text_ir")
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function plain(x) return x and TextIR.toPlain(x, {}) or nil end
-- RomText.irOr / RomText.refIr
BUNDLE.text = { gText_RecordsLv50 = ir("NIVEAU 50"), ["g3:08abcdef"] = ir("PAR ADRESSE") }
check(plain(RomText.irOr("gText_RecordsLv50", ir("LV. 50"))) == "NIVEAU 50", "irOr reads the cached text")
check(plain(RomText.irOr("gText_Missing", ir("LV. 50"))) == "LV. 50", "irOr falls back on the copy")
check(plain(RomText.irOr(nil, ir("LV. 50"))) == "LV. 50", "irOr without a key keeps the copy")
check(plain(RomText.refIr({ name = "gText_RecordsLv50", key = "g3:08000000", ir = ir("LV. 50") })) == "NIVEAU 50",
"refIr reads a reference by its symbol")
check(plain(RomText.refIr({ name = "sUnnamed", key = "g3:08abcdef", ir = ir("BY ADDRESS") })) == "PAR ADRESSE",
"refIr reads a reference by its address")
check(plain(RomText.refIr({ name = "sUnnamed", key = "g3:08000000", ir = ir("COPY") })) == "COPY",
"refIr falls back on the reference's copy")
check(RomText.refIr(nil) == nil, "refIr of no reference is nil")
-- Battle Dome tables, keyed by the table's label (extract_scripts.lua).
BUNDLE.text = { ["sBattleDomePotentialTexts[1]"] = ir("Fort potentiel") }
local Dome = require("src.core.game3.rse.frontier.dome")
local potential = { ir("Best candidate"), ir("Strong potential") }
check(plain(Dome.tableText(potential, "sBattleDomePotentialTexts", 2)) == "Fort potentiel",
"a Dome table string reads the cached text by its label")
check(plain(Dome.tableText(potential, "sBattleDomePotentialTexts", 1)) == "Best candidate",
"a Dome table string missing from the cache keeps the copy")
-- Apprentice messages are pack references.
local Apprentice = require("src.core.game3.rse.frontier.apprentice")
BUNDLE.text = { gText_ApprenticeWhichMon = ir("Lequel?") }
Apprentice.manifest = function()
return { whichMon = { { { name = "gText_ApprenticeWhichMon", key = "g3:08000001", ir = ir("Which one?") },
{ name = "gText_ApprenticeThanks", key = "g3:08000002", ir = ir("Thanks!") } } } }
end
Apprentice.player = function() return { id = 0 } end
check(plain(Apprentice.message({}, Apprentice.MSG.WHICH_MON)) == "Lequel?", "an Apprentice line reads the cached text")
check(plain(Apprentice.message({}, Apprentice.MSG.THANKS_MON)) == "Thanks!",
"an Apprentice line missing from the cache keeps the copy")
-- The Battle Pyramid bag's "Return to ..." line (pokeemerald/src/battle_pyramid_bag.c:687).
local Pyramid = require("src.core.game3.rse.frontier.pyramid")
local PyramidBag = require("src.ui.game3.rse.pyramid_bag")
Pyramid.bagCursor = { scroll = 0, cursor = 0 }
Pyramid.bagLists = function() return {}, {} end
Pyramid.manifest = function()
return { bagReturnTo = { { name = "gText_TheField", key = "g3:08000003", ir = ir("the field") } } }
end
PyramidBag._st.count, PyramidBag._st.location = 1, "field"
BUNDLE.text = { gText_ReturnToVar1 = { { t = "text", s = "Retourner " }, { t = "strvar", n = 1 }, { t = "eos" } },
gText_TheField = ir("au jeu") }
PyramidBag._st.returnTo = PyramidBag.returnTo("field")
check(PyramidBag.description() == "Retourner au jeu", "the bag's return line names the place from the cache")
BUNDLE.text.gText_TheField = nil
check(PyramidBag.description() == "Retourner au jeu", "the open bag keeps the place it read")
PyramidBag._st.returnTo = PyramidBag.returnTo("field")
check(PyramidBag.description() == "Retourner the field", "a place missing from the cache keeps the copy")
-- Ever Grande City's fly destinations (pokeemerald/src/region_map.c:343).
local RegionMap = require("src.ui.game3.rse.region_map")
RegionMap.manifest = function()
return { multiNameFlyDestinations = { { names = { "POKéMON LEAGUE", "POKéMON CENTER" }, mapSecId = 5, flag = 2100 } } }
end
local function flySub()
local s = { mapSecType = RegionMap.TYPE.CITY_CANFLY, mapSecId = 5, posWithinMapSec = 1, mapSecName = "EVER GRANDE",
session = { flags = { [2100] = true } } }
RegionMap.updateFlyText(s)
return s.flyText.sub
end
BUNDLE.text = { ["sEverGrandeCityNames[1]"] = ir("CENTRE POKéMON") }
check(flySub() == "CENTRE POKéMON", "a fly destination reads its name from the cache")
BUNDLE.text = {}
check(flySub() == "POKéMON CENTER", "a fly destination missing from the cache keeps the copy")
-- The Pokédex search screen (pokeemerald/src/pokedex.c:1017, 1042, 1330).
local Gfx = require("src.ui.game3.rse.pokedex_gfx")
Gfx.manifest = function()
return { search = {
topBar = { { description = "Search for POKéMON based on selected parameters." } },
items = { { description = "List by the first letter in the name." }, {}, {}, {}, {}, {},
{ description = "Execute search/switch." } },
orders = { { title = "NUMERICAL", description = "Pokédex listing by number." },
{ title = "A TO Z", description = "Alphabetical order." } },
types = { { title = "NONE" }, { title = "NORMAL" }, { title = "FIGHT" } },
} }
end
local Pokedex = require("src.ui.game3.rse.pokedex")
BUNDLE.text = {
gText_SearchForPkmnBasedOnParameters = ir("Chercher des POKéMON."),
gText_ListByFirstLetter = ir("Classer par initiale."),
gText_ExecuteSearchSwitch = ir("Lancer la recherche."),
gText_DexSortNumericalTitle = ir("NUMERIQUE"),
gText_DexSortNumericalDescription = ir("Classement par numéro."),
["gTypeNames[0]"] = ir("NORMAL"), ["gTypeNames[1]"] = ir("COMBAT"),
}
local orders = Pokedex.searchOptionTexts(Pokedex.SEARCH.ORDER)
check(orders[1].title == "NUMERIQUE" and orders[1].description == "Classement par numéro.",
"a search option reads its title and description from the cart")
check(orders[2].title == "A TO Z" and orders[2].description == "Alphabetical order.",
"a search option missing from the cache keeps the copy")
local types = Pokedex.searchOptionTexts(Pokedex.SEARCH.TYPE_LEFT)
check(types[1].title == "NONE" and types[3].title == "COMBAT", "type options read gTypeNames by type id")
check(Pokedex.topBarDescription(0) == "Chercher des POKéMON.", "a top bar description reads the cart's text")
check(Pokedex.itemDescription(0) == "Classer par initiale.", "a search item description reads the cart's text")
check(Pokedex.itemDescription(Pokedex.SEARCH.OK) == "Lancer la recherche.", "the OK item's description reads the cart's text")
-- The party menu's actions (pokeemerald/src/data/party_menu.h:658).
require("tests.game3_cache").stubSpeciesNames()
local Profile = require("src.core.game3.profile")
local Pokemon = require("src.core.game3.pokemon")
local Kit = require("src.ui.game3.rse.scene_kit")
local PartyMenu = require("src.ui.game3.party_menu")
Profile.forSession = function()
return { ui = { party = { manifest = "rse/menus", cursorOptionTexts = { "gText_Summary5", "gText_Switch2",
"gText_Cancel2" } } } }
end
Kit.manifest = function()
return { party = { cursorOptions = { "SUMMARY", "SWITCH", "CANCEL", "CUT" }, fieldMoves = { 15 } } }
end
Pokemon.moveName = function(move) return move == 15 and "COUPE" or nil end
BUNDLE.text = { gText_Summary5 = ir("RESUME"), gText_Switch2 = ir("ORDRE") }
check(PartyMenu._cursorOptionText("SUMMARY") == "RESUME", "a party action reads the cart's text")
check(PartyMenu._cursorOptionText("SWITCH") == "ORDRE", "a second party action reads the cart's text")
check(PartyMenu._cursorOptionText("CANCEL") == "CANCEL", "a party action missing from the cache keeps the label")
check(PartyMenu._cursorOptionText("CUT") == "COUPE", "a field move prints the move's name")
-- Expanded placeholders (pokeemerald/src/strings.c:6): the rival's name and
-- Japanese くん/ちゃん come from the cache, the extract's copies otherwise.
local Message = require("src.ui.game3.message")
local extracted = {
RIVAL_MALE = "MAY", RIVAL_FEMALE = "BRENDAN", KUN_MALE = "", KUN_FEMALE = "", VERSION = "EMERALD",
byGender = { RIVAL = { male = "MAY", female = "BRENDAN" }, KUN = { male = "", female = "" } },
}
BUNDLE.text = { gText_ExpandedPlaceholder_May = ir("FLORA"), gText_ExpandedPlaceholder_Kun = ir("くん") }
local placeholders = Message.cartPlaceholders(extracted)
check(placeholders.byGender.RIVAL.male == "FLORA", "the rival's name reads the cache")
check(placeholders.byGender.RIVAL.female == "BRENDAN", "a rival name missing from the cache keeps the copy")
check(placeholders.byGender.KUN.male == "くん", "the honorific reads the cache")
check(placeholders.VERSION == "EMERALD", "a placeholder missing from the cache keeps the copy")
BUNDLE.text.gText_ExpandedPlaceholder_May = ir("AURA")
check(placeholders.byGender.RIVAL.male == "FLORA", "the values are resolved once, not on every read")
local line = { { t = "player" }, { t = "ph", code = 5, name = "KUN" }, { t = "text", s = " / " },
{ t = "ph", code = 6, name = "RIVAL" }, { t = "eos" } }
BUNDLE.text = { gText_ExpandedPlaceholder_May = ir("FLORA"), gText_ExpandedPlaceholder_Kun = ir("くん"),
gText_ExpandedPlaceholder_Brendan = ir("BRICE"), gText_ExpandedPlaceholder_Chan = ir("ちゃん") }
placeholders = Message.cartPlaceholders(extracted)
check(TextIR.toPlain(line, { dialect = "rse", playerName = "RED", playerGender = 0, placeholders = placeholders })
== "REDくん / FLORA", "a boy's line expands the honorific and May's name from the cache")
check(TextIR.toPlain(line, { dialect = "rse", playerName = "RED", playerGender = 1, placeholders = placeholders })
== "REDちゃん / BRICE", "a girl's line expands the honorific and Brendan's name from the cache")
-- The context provider builds them once per script cache.
local Space = package.loaded["src.core.game3.scripting.space"]
Space.bundle = BUNDLE
package.loaded["src.import.CacheFs"] = { loadActive = function() return extracted end }
local first = TextIR.placeholdersFor({ dialect = "rse" })
check(first ~= nil and TextIR.placeholdersFor({ dialect = "rse" }) == first,
"the provider builds the placeholders once per script cache")
check(first.byGender.RIVAL.female == "BRICE", "the provider's placeholders read the cache")
BUNDLE = { text = { gText_ExpandedPlaceholder_Brendan = ir("BRUNO") } }
Space.bundle = BUNDLE
check(TextIR.placeholdersFor({ dialect = "rse" }).byGender.RIVAL.female == "BRUNO",
"a new script cache rebuilds them")
Space.bundle = nil
-- The Battle Arena's judgment window reads its titles once, when it opens
-- (pokeemerald/src/battle_arena.c:412).
local Data = require("src.core.game3.rse.frontier.f2_data")
Data.arena = function()
return { text = { playerMon1Name = ir("A"), vs = ir("VS"), opponentMon1Name = ir("B"), mind = ir("MIND"),
skill = ir("SKILL"), body = ir("BODY"), judgment = ir("JUDGMENT") } }
end
local Arena = require("src.core.game3.battle.facility_arena")
local arena = Arena.new()
BUNDLE.text = { gText_Mind = ir("ESPRIT") }
arena:judgeStep({}, { state = "open" })
local titles = {}
for _, row in ipairs(arena.window.texts) do titles[row.win] = plain(row.ir) end
check(titles[Arena.WIN.MIND] == "ESPRIT", "the judgment window reads a title from the cache when it opens")
check(titles[Arena.WIN.BODY] == "BODY", "a judgment title missing from the cache keeps the copy")
BUNDLE.text = { gText_Mind = ir("AUTRE") }
titles = {}
for _, row in ipairs(arena.window.texts) do titles[row.win] = plain(row.ir) end
check(titles[Arena.WIN.MIND] == "ESPRIT", "the open window keeps the titles it read")
if failed > 0 then
print(failed .. " check(s) failed")
os.exit(1)
end
print("all checks passed")
@@ -0,0 +1,201 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local Strings = require("src.core.Strings")
local Mapsec = require("src.ui.game3.rse.mapsec")
local RomText = require("src.core.game3.rom_text")
local FrlgFont = require("src.ui.game3.frlg_font")
local Pokemon = require("src.core.game3.pokemon")
local BattleText = require("src.core.game3.battle.battle_text")
local SummaryData = require("src.core.game3.summary_data")
Pokemon._abilityNames = { [9] = "STATIC" }
Pokemon._romAbilityNames = { [9] = "STATIC" }
Mapsec.readLua = function(rel)
assert(rel == "pokemon/pokedex/entries.lua")
return {
[252] = {
category = "Wood Gecko",
description = "A small gecko.",
height = 5,
weight = 50,
},
}
end
RomText.plain = function(key) return key end
FrlgFont.measure = function(text) return #text end
Pokemon.name = function() return "TREECKO" end
local Pokedex = require("src.ui.game3.rse.pokedex")
Pokedex.speciesOf = function() return 252 end
Pokedex.hoennNumber = function() return 1 end
local function info()
return Pokedex.monInfo({}, 252, false, true, false)
end
-- pokeemerald/src/starter_choose.c:579 and battle_factory_screen.c:2007 print
-- the same CopyMonCategoryText string as the Pokédex.
local Kit = require("src.ui.game3.rse.scene_kit")
Kit.loadLua = function(rel)
if rel == "data/generated/gba/region_map/map_sections.lua" then
return { sections = { [0] = { name = "LITTLEROOT TOWN" } } }
end
-- the Pokédex reads its layout from the graphics manifest (Emerald's, not
-- Ruby/Sapphire's native one)
if rel == "data/generated/gba/rse/pokedex/manifest.lua" then return {} end
assert(rel == "data/generated/gba/pokemon/dex.lua", rel)
return { [252] = { category = "Wood Gecko" } }
end
Pokemon.national = function(species) return species end
Pokemon._names = { [252] = "TREECKO" } -- keeps the starter label from reinstalling the packs
local StarterChoose = require("src.ui.game3.rse.starter_choose")
StarterChoose.species = function() return 252 end
local function starterCategory()
local view = setmetatable({ man = {}, selection = 1 }, StarterChoose)
view:_createLabel()
return view.label.category
end
local PokeblockGfx = require("src.ui.game3.rse.pokeblock_gfx")
PokeblockGfx.loadLua = function(rel)
assert(rel == "data/generated/gba/pokemon/pokedex/entries.lua", rel)
return { [252] = { category = "Wood Gecko" } }
end
local FactoryCommon = require("src.ui.game3.rse.factory_select").Common
-- pokeemerald/src/contest.c:3237
local ContestUI = require("src.ui.game3.rse.contest")
ContestUI.has = function(key) return key == "gContestEffectDescriptionPointers[1]" end
ContestUI.plain = function(key)
if key == "gContestEffectDescriptionPointers[1]" then return "Startles the audience." end
return key
end
local function contestMoveDescription()
local view = setmetatable({
c = { data = { moves = { [33] = { category = 0, effect = 1 } }, effects = { [1] = { appeal = 20, jam = 0 } } } },
win = {},
fillBox = function() end,
fillBoxInc = function() end,
}, { __index = ContestUI })
view:printContestMoveDescription(33)
return view.win[10].text
end
-- pokeemerald/src/region_map.c:1568
Mapsec.pack = function()
return { count = 1, sections = { [0] = { name = "LITTLEROOT TOWN" } } }
end
-- pokeemerald/src/pokemon_summary_screen.c:3116: the trainer memo's met
-- location is a map section name.
local RseSummary = require("src.ui.game3.rse.summary_menu")
local PalText = require("src.ui.game3.rse.pal_text")
local drawInfo
for i = 1, 60 do
local name, value = debug.getupvalue(RseSummary.draw, i)
if not name then break end
if name == "drawInfo" then drawInfo = value; break end
end
assert(drawInfo, "RSE summary info page drawer")
RomText.ir = function() return {} end
RomText.at = function() return {} end
PalText.width = function() return 0 end
Pokemon.types = function() return {} end
Pokemon.speciesOf = function() return 252 end
SummaryData.nature = function() return 0 end
local function metLocation()
local shown
local draw = PalText.draw
PalText.draw = function(_, _, _, ctx)
if ctx and ctx.dynamic then shown = ctx.dynamic[4] end
end
local w = { left = 0, top = 0, paletteNum = 0 }
local m = { palette = {}, textColors = {}, windows = {}, pageWindows = { info = { w, w, w, w } } }
local describe = SummaryData.abilityDescription
SummaryData.abilityDescription = function() return "" end
drawInfo(m, { _playerState = { name = "MAY", trainerId = 1 } },
{ species = 252, ability = 9, otName = "MAY", otId = 1, metLevel = 5, metLocation = 0 }, false)
PalText.draw, SummaryData.abilityDescription = draw, describe
return shown
end
-- A modded ability past the built-in id table resolves to a key from its name.
local Adapter = require("src.core.game3.battle.adapter")
Pokemon._abilityNames[200] = "NEW SKILL"
Pokemon._romAbilityNames[200] = "NEW SKILL"
local function adapterAbilityKey()
return Adapter.new({}):abilityOf({ ability = 200 })
end
Strings.load({})
assert(Pokemon.abilityName(9) == "STATIC",
"an ability name keeps its English ROM value without translations")
assert(SummaryData.contestCategoryName("Cool") == "Cool",
"a contest category keeps its English source without translations")
assert(SummaryData.contestEffectDescription({ description = "Startles the audience." })
== "Startles the audience.",
"a contest effect description keeps its English source without translations")
local english = info()
assert(english[3].text == "Wood Gecko gText_Pokemon",
"the category keeps its English source when no translation is loaded")
assert(english[#english].text == "A small gecko.",
"the description keeps its English source when no translation is loaded")
assert(starterCategory() == "Wood Gecko gText_Pokemon",
"the starter label keeps its English category without translations")
assert(FactoryCommon.categoryText(252) == "Wood Gecko gText_Pokemon",
"the Battle Factory keeps its English category without translations")
assert(contestMoveDescription() == "Startles the audience.",
"the contest move window keeps its English effect text without translations")
assert(Mapsec.name(0) == "LITTLEROOT TOWN",
"a map section keeps its English name without translations")
assert(Mapsec.name(99) == "", "an unknown map section has no name")
assert(metLocation() == "LITTLEROOT TOWN",
"the RSE summary keeps the English met location without translations")
assert(adapterAbilityKey() == "NEW_SKILL", "the battle adapter keys an unlisted ability by its ROM name")
Strings.load({ strings = {
STATIC = "STATIQUE",
Cool = "Sang-froid",
["Startles the audience."] = "Surprend le public.",
["Wood Gecko"] = "Gecko des bois",
["A small gecko."] = "Un petit gecko.",
["LITTLEROOT TOWN"] = "BOURG-EN-VOL",
["NEW SKILL"] = "NOUVEAU TALENT",
} })
assert(Pokemon.abilityName(9) == "STATIQUE",
"the shared ability-name getter translates the displayed name")
assert(Pokemon.romAbilityName(9) == "STATIC",
"the original ROM name stays available for stable ID lookups")
assert(BattleText.RESOLVE_RSE[0x17]({ lastAbility = 9 }) == "STATIQUE",
"the RSE battle ability placeholder uses the translated shared getter")
assert(SummaryData.contestCategoryName("Cool") == "Sang-froid",
"the move relearner translates the contest category source")
assert(SummaryData.contestEffectDescription({ description = "Startles the audience." })
== "Surprend le public.",
"the RSE summary and move relearner translate the contest effect source")
local french = info()
assert(french[3].text == "Gecko des bois gText_Pokemon",
"the RSE Pokédex translates the source category at presentation time")
assert(french[#french].text == "Un petit gecko.",
"the RSE Pokédex translates the source description at presentation time")
assert(starterCategory() == "Gecko des bois gText_Pokemon",
"the starter label translates the category like the Pokédex")
assert(FactoryCommon.categoryText(252) == "Gecko des bois gText_Pokemon",
"the Battle Factory translates the category like the Pokédex")
assert(contestMoveDescription() == "Surprend le public.",
"the contest move window translates the effect text like the summary")
assert(Mapsec.name(0) == "BOURG-EN-VOL",
"the Emerald map section name goes through the registry")
assert(metLocation() == "BOURG-EN-VOL",
"the RSE summary translates the met location like the region map")
assert(Pokemon.abilityName(200) == "NOUVEAU TALENT", "the display name is translated")
assert(adapterAbilityKey() == "NEW_SKILL",
"the battle adapter keeps keying on the ROM name when the display name is translated")
Strings.load({})
local reloaded = info()
assert(reloaded[#reloaded].text == "A small gecko.",
"reloading without a catalog restores the English description")
print("game3_emerald_translation_runtime_test: PASS")
+25 -1
View File
@@ -50,12 +50,13 @@ SummaryChrome.manifest = function() return MANIFEST end
local SESSION = nil
package.loaded["src.core.game3.runtime"] = { getSession = function() return SESSION end }
local SummaryData = require("src.core.game3.summary_data")
local Strings = require("src.core.Strings")
SummaryData.moveDescription = function() return "" end
SummaryData.abilityDescription = function() return "" end
local realMemo = SummaryData.formatTrainerMemo
SummaryData.formatTrainerMemo = function() return {} end
SummaryData.expProgress = function() return { totalExp = 0, expNeeded = 0, progressPercent = 0 } end
Pokemon.abilityName = function() return "STATIC" end
Pokemon.abilityName = function() return Strings("STATIC") end
Pokemon.abilityId = function() return 9 end
Pokemon.monFrontPic = function() calls[#calls + 1] = { name = "pic", args = {} } return nil end
Pokemon.monIcon = function() calls[#calls + 1] = { name = "icon", args = {} } return nil end
@@ -215,6 +216,29 @@ local def = text_at("7")
eq(def and def.x, 210 + 27 - 6, "a one-digit stat right-aligns too")
SummaryMenu.close()
print("[test] 5b. translated ability names are looked up once")
Strings.load({ strings = { STATIC = "STATIQUE", STATIQUE = "NOM ENCORE TRADUIT" } })
local numericAbility = mon()
numericAbility.ability = 9
SummaryMenu.openMenu({ numericAbility }, 1, { page = SummaryMenu.PAGE_SKILLS })
draw()
check(text_at("STATIQUE") ~= nil,
"the numeric ability getter's translated name is not translated a second time")
check(text_at("NOM ENCORE TRADUIT") == nil,
"a chained catalog entry does not change the numeric ability label")
SummaryMenu.close()
local externalAbility = mon()
externalAbility.ability = "STATIC"
SummaryMenu.openMenu({ externalAbility }, 1, { page = SummaryMenu.PAGE_SKILLS })
draw()
check(text_at("STATIQUE") ~= nil,
"a raw external ability label is translated once at the summary consumer")
check(text_at("NOM ENCORE TRADUIT") == nil,
"a raw external ability label also avoids chained translation")
SummaryMenu.close()
Strings.load({})
local function text_xy(x, y)
for _, t in ipairs(texts) do if t.x == x and t.y == y then return t end end
return nil
+9
View File
@@ -30,6 +30,15 @@ check(SummaryData.moveDescription(71, "VOL-VIE"):find("absorbs half", 1, true) ~
"a renamed move still finds its description")
check(SummaryData.abilityDescription(9, "STATIK") == "Paralyzes on contact.",
"a renamed ability still finds its description")
check(Pokemon.abilityName(9) == "STATIK",
"an ability name falls back to the current displayed source")
Strings.load({ strings = { STATIK = "STATIQUE" } })
check(Pokemon.abilityName(9) == "STATIQUE",
"the shared ability-name getter translates the current displayed name")
check(Pokemon.romAbilityName(9) == "STATIC",
"the ROM ability name remains stable for ID-based lookups")
Strings.load({})
Strings.load({ strings = { ["Paralyzes on contact."] = "Paralyse au contact." } })
check(SummaryData.abilityDescription(9, "STATIK") == "Paralyse au contact.",