From f68358fdce0f669bd0cf5da436c932dce15ac4b4 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:00:55 +0200 Subject: [PATCH 1/3] Put a translated wild or foe word after the name when the cart does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The battle text joins a foe's name to the cart's sText_WildPkmnPrefix or sText_FoePkmnPrefix the way the US code does, word first. The French, Italian and Spanish carts write those rows to follow the name (" sauvage", " salvaje", " selvatico") and append them (pret pokeemerald multi-language, src/battle_message.c:4042 and :4649), and the German row has the same shape (" (Wild)"), so a French battle read " sauvageZIGZATON utilise RUGISSEMENT!". BattleText.withMonPrefix joins the two in the row's order: a row that starts with a space follows the name, and the US and Japanese rows ("Wild ", "やせいの ") still go first. Both the battle string placeholder (B_ATK_NAME_WITH_PREFIX and the other WITH_PREFIX codes) and the buffered name (State.prefixedName) use it, in FireRed/LeafGreen and Emerald alike. --- src/core/game3/battle/battle_text.lua | 12 ++++- src/core/game3/battle/state.lua | 3 +- .../engine/game3_european_mon_prefix_test.lua | 47 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/engine/game3_european_mon_prefix_test.lua diff --git a/src/core/game3/battle/battle_text.lua b/src/core/game3/battle/battle_text.lua index 446dd841..a09e89cc 100644 --- a/src/core/game3/battle/battle_text.lua +++ b/src/core/game3/battle/battle_text.lua @@ -39,11 +39,21 @@ local function monName(ref) return require("src.core.game3.battle.state").displayName(ref) end +-- A foe's name with the cart's wild/foe word. The US and Japanese rows go +-- before the name ("Wild ", "やせいの "); the French, Italian and Spanish +-- carts write theirs to follow it (" sauvage", " salvaje", " selvatico") and +-- append it (pret pokeemerald multi-language, src/battle_message.c:4042, +-- :4649). The German row has the same shape (" (Wild)"). +function BattleText.withMonPrefix(prefix, name) + if prefix:sub(1, 1) == " " then return name .. prefix end + return prefix .. name +end + -- src/battle_message.c:1807 HANDLE_NICKNAME_STRING_CASE local function withPrefix(fill, ref) if isPlayer(ref) then return monName(ref) end local prefix = fill.trainer and "sText_FoePkmnPrefix" or "sText_WildPkmnPrefix" - return RomText.plain(prefix) .. monName(ref) + return BattleText.withMonPrefix(RomText.plain(prefix), monName(ref)) end local function moveName(fill, move) diff --git a/src/core/game3/battle/state.lua b/src/core/game3/battle/state.lua index 6524b8f5..d8031959 100644 --- a/src/core/game3/battle/state.lua +++ b/src/core/game3/battle/state.lua @@ -466,7 +466,8 @@ function State.prefixedName(st, battler, name) local RomText = require("src.core.game3.rom_text") local prefix = (st ~= nil and not st.wild) and "sText_FoePkmnPrefix" or "sText_WildPkmnPrefix" local ok, pre = pcall(RomText.plain, prefix) - return (ok and pre or (st ~= nil and not st.wild and "Foe " or "Wild ")) .. name + pre = ok and pre or (st ~= nil and not st.wild and "Foe " or "Wild ") + return require("src.core.game3.battle.battle_text").withMonPrefix(pre, name) end function State.isFainted(battler) diff --git a/tests/engine/game3_european_mon_prefix_test.lua b/tests/engine/game3_european_mon_prefix_test.lua new file mode 100644 index 00000000..60faa070 --- /dev/null +++ b/tests/engine/game3_european_mon_prefix_test.lua @@ -0,0 +1,47 @@ +-- The wild/foe word joined to a foe's name: the US and Japanese rows go before +-- it, the European carts write theirs to follow it and append it (pret +-- pokeemerald multi-language, src/battle_message.c:4042, :4649). + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local eq = T.eq + +local RomText = require("src.core.game3.rom_text") +local BattleText = require("src.core.game3.battle.battle_text") +local State = require("src.core.game3.battle.state") + +local saved = {} +local function set(key, ir) + if saved[key] == nil then saved[key] = RomText.overrides[key] or false end + RomText.overrides[key] = ir +end +local function text(s) return { { t = "text", s = s }, { t = "eos" } } end + +-- pokeemerald multi-language src/battle_message.c:4042 +eq(BattleText.withMonPrefix("Wild ", "ZIGZAGOON"), "Wild ZIGZAGOON", "US: the wild word comes first") +eq(BattleText.withMonPrefix("やせいの ", "ジグザグマ"), "やせいの ジグザグマ", "Japanese: the wild word comes first") +eq(BattleText.withMonPrefix(" sauvage", "ZIGZATON"), "ZIGZATON sauvage", "French: the wild word follows the name") +eq(BattleText.withMonPrefix(" (Wild)", "ZIGZACHS"), "ZIGZACHS (Wild)", "German: the wild word follows the name") + +set("sText_WildPkmnPrefix", text(" sauvage")) +set("sText_FoePkmnPrefix", text(" ennemi")) +set("sText_TestAttackerUses", { { t = "bph", code = 0x0F }, { t = "text", s = " utilise" }, { t = "eos" } }) +eq(BattleText.get("sText_TestAttackerUses", { atk = { name = "ZIGZATON", side = "opponent" } }), + "ZIGZATON sauvage utilise", "B_ATK_NAME_WITH_PREFIX puts the French wild word after the name") +eq(BattleText.get("sText_TestAttackerUses", { atk = { name = "ZIGZATON", side = "opponent" }, trainer = true }), + "ZIGZATON ennemi utilise", "and the foe word against a trainer") +eq(BattleText.get("sText_TestAttackerUses", { atk = { name = "POUSSIFEU", side = "player" } }), + "POUSSIFEU utilise", "the player's own POKéMON has no prefix") +-- src/battle_message.c:4649 B_BUFF_MON_NICK_WITH_PREFIX +eq(State.prefixedName({ wild = true }, { side = "opponent" }, "ZIGZATON"), "ZIGZATON sauvage", + "a buffered name takes the same order") +eq(State.prefixedName({ wild = false }, { side = "opponent" }, "ZIGZATON"), "ZIGZATON ennemi", + "a buffered foe name too") +set("sText_WildPkmnPrefix", text("Wild ")) +eq(State.prefixedName({ wild = true }, { side = "opponent" }, "ZIGZAGOON"), "Wild ZIGZAGOON", + "the US row still goes first") + +for key, ir in pairs(saved) do RomText.overrides[key] = ir or nil end + +T.finish("game3_european_mon_prefix_test") From 6b8e275908ec817094cd316d54b01a37c3b559a9 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:00:55 +0200 Subject: [PATCH 2/3] Print a European two-stage stat change once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stat that rises or falls by two stages prints the cart's "sharply" or "harshly" row followed by its "rose!" or "fell!" row, as the US code buffers them. The French, Italian and Spanish rows for the two-stage change are the whole change ("monte beaucoup!", "sale di molto!", "subió mucho"), and their code skips the string that follows (pret pokeemerald multi-language, src/battle_message.c:4617); the German rows have the same shape ("steigt stark!"). A French battle printed "monte beaucoup!augmente!". Secondary.sharpChange keeps the plain row only after a "sharply" or "harshly" row that ends with a space, as the US and Japanese ones do ("sharply ", "ぐーんと "). The move effects and the held items that raise a stat by two use it. --- src/core/game3/battle/effects/secondary.lua | 18 ++++++- src/core/game3/battle/held_items.lua | 2 +- .../game3_european_stat_change_test.lua | 48 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 tests/engine/game3_european_stat_change_test.lua diff --git a/src/core/game3/battle/effects/secondary.lua b/src/core/game3/battle/effects/secondary.lua index 9434135b..bdda7bb9 100644 --- a/src/core/game3/battle/effects/secondary.lua +++ b/src/core/game3/battle/effects/secondary.lua @@ -32,12 +32,26 @@ function Secondary.statName(stat) return RomText.at("gStatNamesTable", Secondary.STAT_ID[stat]) end +-- A two-stage stat change: the US and Japanese "sharply"/"harshly" rows end +-- with a space before the plain change ("sharply rose!", "ぐーんと あがった!"); +-- the French, Italian and Spanish ones carry the whole change ("monte +-- beaucoup!"), and their code skips the plain string that follows (pret +-- pokeemerald multi-language, src/battle_message.c:4617). The German rows have +-- the same shape ("steigt stark!"). +function Secondary.sharpChange(sharpId, plainId) + local sharp = RomText.plain(sharpId) + if sharp:sub(-1) == " " or sharp:sub(-3) == "\227\128\128" then + return sharp .. RomText.plain(plainId) + end + return sharp +end + -- src/battle_script_commands.c:6758 local function stat_text(ad, battler, stat, delta, isUser) local change - if delta >= 2 then change = RomText.plain("STRINGID_STATSHARPLY") .. RomText.plain("STRINGID_STATROSE") + if delta >= 2 then change = Secondary.sharpChange("STRINGID_STATSHARPLY", "STRINGID_STATROSE") elseif delta >= 1 then change = RomText.plain("STRINGID_STATROSE") - elseif delta <= -2 then change = RomText.plain("STRINGID_STATHARSHLY") .. RomText.plain("STRINGID_STATFELL") + elseif delta <= -2 then change = Secondary.sharpChange("STRINGID_STATHARSHLY", "STRINGID_STATFELL") else change = RomText.plain("STRINGID_STATFELL") end local id if delta > 0 then diff --git a/src/core/game3/battle/held_items.lua b/src/core/game3/battle/held_items.lua index d2dfc4ed..d036c4b5 100644 --- a/src/core/game3/battle/held_items.lua +++ b/src/core/game3/battle/held_items.lua @@ -125,7 +125,7 @@ local function stat_up(ad, b, item, stat, delta) b.stages[stat] = math.min(6, (b.stages[stat] or 0) + delta) ad:playAnim("general", "STATS_CHANGE", b, b, Secondary.statAnimArg(stat, delta)) local change = RomText.plain("STRINGID_STATROSE") - if delta >= 2 then change = RomText.plain("STRINGID_STATSHARPLY") .. change end + if delta >= 2 then change = Secondary.sharpChange("STRINGID_STATSHARPLY", "STRINGID_STATROSE") end say_id(ad, "STRINGID_USINGITEMSTATOFPKMNROSE", { lastItem = item, buff1 = Secondary.statName(stat), scrActive = b, buff2 = change, }) diff --git a/tests/engine/game3_european_stat_change_test.lua b/tests/engine/game3_european_stat_change_test.lua new file mode 100644 index 00000000..64aefd6e --- /dev/null +++ b/tests/engine/game3_european_stat_change_test.lua @@ -0,0 +1,48 @@ +-- A two-stage stat change: the European "sharply"/"harshly" rows carry the +-- whole change, and the carts skip the plain string after them (pret +-- pokeemerald multi-language, src/battle_message.c:4617). + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local eq = T.eq + +local RomText = require("src.core.game3.rom_text") +local Secondary = require("src.core.game3.battle.effects.secondary") + +local saved = {} +local function set(key, ir) + if saved[key] == nil then saved[key] = RomText.overrides[key] or false end + RomText.overrides[key] = ir +end +local function text(s) return { { t = "text", s = s }, { t = "eos" } } end + +-- src/battle_message.c:4617 +set("STRINGID_STATSHARPLY", text("sharply ")) +set("STRINGID_STATROSE", text("rose!")) +eq(Secondary.sharpChange("STRINGID_STATSHARPLY", "STRINGID_STATROSE"), "sharply rose!", + "US: the sharply row runs on into the plain change") +set("STRINGID_STATSHARPLY", text("ぐーんと ")) +set("STRINGID_STATROSE", text("あがった!")) +eq(Secondary.sharpChange("STRINGID_STATSHARPLY", "STRINGID_STATROSE"), "ぐーんと あがった!", + "Japanese: the same") +set("STRINGID_STATSHARPLY", text("monte beaucoup!")) +set("STRINGID_STATROSE", text("augmente!")) +eq(Secondary.sharpChange("STRINGID_STATSHARPLY", "STRINGID_STATROSE"), "monte beaucoup!", + "French: the sharply row is the whole change") +set("STRINGID_STATHARSHLY", text("harshly ")) +set("STRINGID_STATFELL", text("fell!")) +eq(Secondary.sharpChange("STRINGID_STATHARSHLY", "STRINGID_STATFELL"), "harshly fell!", + "US: the harshly row runs on into the plain fall") +set("STRINGID_STATHARSHLY", text("baisse beaucoup!")) +set("STRINGID_STATFELL", text("baisse!")) +eq(Secondary.sharpChange("STRINGID_STATHARSHLY", "STRINGID_STATFELL"), "baisse beaucoup!", + "French: the harshly row is the whole fall") +set("STRINGID_STATSHARPLY", text("subió mucho")) +set("STRINGID_STATROSE", text("subió")) +eq(Secondary.sharpChange("STRINGID_STATSHARPLY", "STRINGID_STATROSE"), "subió mucho", + "Spanish: a row without an exclamation mark is the whole change too") + +for key, ir in pairs(saved) do RomText.overrides[key] = ir or nil end + +T.finish("game3_european_stat_change_test") From f26fe452e506609aa86fa87d1c5cc93112e5eafd Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:00:55 +0200 Subject: [PATCH 3/3] Fill a European row's name placeholder instead of prepending the name Two screens print a name followed by a cart string, the way the US code builds them: the nickname screen's title (the species and gText_PkmnsNickname) and a secret base's name (the owner and gText_ApostropheSBase). The French, Italian and Spanish rows name it where their language puts it, through STR_VAR_1 ("Surnom de [STR_VAR_1]?", "BASE DE [STR_VAR_1]"), and their code fills the placeholder (pret pokeemerald multi-language, src/naming_screen.c:1746, src/secret_base.c:732); the German rows do the same ("Kosename f. [STR_VAR_1]"). A French nickname screen read "POUSSIFEUSurnom de ?". Both now render the row with the name as STR_VAR_1 and use that text when the row prints it, a mod's catalog entry for the English source included; a row without the placeholder (FireRed's US and Japanese titles, the Japanese secret base row) keeps the name in front. English and Japanese text is unchanged. --- src/core/game3/rse/secret_base.lua | 13 +++-- src/ui/game3/naming.lua | 13 ++++- .../game3_european_name_placeholder_test.lua | 50 +++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 tests/engine/game3_european_name_placeholder_test.lua diff --git a/src/core/game3/rse/secret_base.lua b/src/core/game3/rse/secret_base.lua index dd62431b..85a55c62 100644 --- a/src/core/game3/rse/secret_base.lua +++ b/src/core/game3/rse/secret_base.lua @@ -580,11 +580,18 @@ function SB.ownedByAnotherPlayer(sess) return SB.base(sess, 0).secretBaseId ~= SB._curId end --- pokeemerald/src/secret_base.c:728 +-- pokeemerald/src/secret_base.c:728; the European carts put the owner in the +-- row's STR_VAR_1 ("BASE DE {STR_VAR_1}", pret pokeemerald multi-language, +-- src/secret_base.c:732), the US and Japanese ones append the row to it. function SB.name(idx, sess) local b = SB.base(sess, idx) - local ok, suffix = pcall(Rse.text, "gText_ApostropheSBase") - return tostring(b.trainerName or "") .. (ok and suffix or "'s BASE") + local owner = tostring(b.trainerName or "") + local ok, base = pcall(function() + return require("src.core.game3.rom_text").plain("gText_ApostropheSBase", { stringVars = { "\1" } }) + end) + if not ok then return owner .. "'s BASE" end + if base:find("\1", 1, true) then return (base:gsub("\1", function() return owner end)) end + return owner .. base end -- pokeemerald/src/secret_base.c:735 diff --git a/src/ui/game3/naming.lua b/src/ui/game3/naming.lua index d246021a..382a3cde 100644 --- a/src/ui/game3/naming.lua +++ b/src/ui/game3/naming.lua @@ -32,10 +32,21 @@ Naming.TEMPLATE = { -- pokeemerald/include/naming_screen.h:7 Naming.TEMPLATE_ORDER = { "PLAYER", "BOX", "CAUGHT_MON", "NICKNAME", "WALDA" } +-- A name and a cart string the US code appends after it. The French, Italian +-- and Spanish carts put the name in the string's STR_VAR_1 instead, wherever +-- the row places it (pret pokeemerald multi-language, src/naming_screen.c:1746); +-- a row without one keeps the US order. +function Naming.nameInto(key, name) + name = tostring(name or "") + local text = RomText.plain(key, { stringVars = { "\1" } }) + if text:find("\1", 1, true) then return (text:gsub("\1", function() return name end)) end + return name .. text +end + -- pokeemerald/src/naming_screen.c:1715 function Naming.monTitle(speciesName) -- pokeemerald/src/text.c:972 - return tostring(speciesName or "") .. RomText.plain("gText_PkmnsNickname", { stringVars = {} }) + return Naming.nameInto("gText_PkmnsNickname", speciesName) end -- pret sKeyboardChars + sPageColumnXPos (cursor). diff --git a/tests/engine/game3_european_name_placeholder_test.lua b/tests/engine/game3_european_name_placeholder_test.lua new file mode 100644 index 00000000..0971b06d --- /dev/null +++ b/tests/engine/game3_european_name_placeholder_test.lua @@ -0,0 +1,50 @@ +-- A name joined to a cart string the US code appends after it: the European +-- carts put it in the string's STR_VAR_1 (pret pokeemerald multi-language, +-- src/naming_screen.c:1746, src/secret_base.c:732). + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local eq = T.eq + +local RomText = require("src.core.game3.rom_text") +local Naming = require("src.ui.game3.naming") +local SB = require("src.core.game3.rse.secret_base") + +local saved = {} +local function set(key, ir) + if saved[key] == nil then saved[key] = RomText.overrides[key] or false end + RomText.overrides[key] = ir +end +local function text(s) return { { t = "text", s = s }, { t = "eos" } } end + +-- src/naming_screen.c:1746 +set("gText_PkmnsNickname", text("'s nickname?")) +eq(Naming.monTitle("BULBASAUR"), "BULBASAUR's nickname?", "FireRed's US row has no placeholder: the name goes first") +set("gText_PkmnsNickname", { { t = "strvar", n = 1 }, { t = "text", s = "'s nickname?" }, { t = "eos" } }) +eq(Naming.monTitle("MUDKIP"), "MUDKIP's nickname?", "Emerald's US row prints the same") +set("gText_PkmnsNickname", { { t = "text", s = "Surnom de " }, { t = "strvar", n = 1 }, { t = "text", s = "?" }, { t = "eos" } }) +eq(Naming.monTitle("POUSSIFEU"), "Surnom de POUSSIFEU?", "French: the species fills the row's STR_VAR_1") +eq(Naming.monTitle(nil), "Surnom de ?", "a missing name leaves the placeholder empty") +set("gText_PkmnsNickname", text(" の ニックネームは?")) +eq(Naming.monTitle("フシギダネ"), "フシギダネ の ニックネームは?", "FireRed's Japanese row has no placeholder either") + +-- a mod's catalog entry for the English source: the name still lands in it +local Strings = require("src.core.Strings") +set("gText_PkmnsNickname", { { t = "strvar", n = 1 }, { t = "text", s = "'s nickname?" }, { t = "eos" } }) +Strings.load({ strings = { ["%s's nickname?"] = "Surnom de %s?" } }) +eq(Naming.monTitle("MUDKIP"), "Surnom de MUDKIP?", "a catalog translation takes the name in its %s") +Strings.load(nil) + +-- src/secret_base.c:732 +local sess = { secretBases = { { trainerName = "SACHA" } } } +set("gText_ApostropheSBase", text("'s BASE")) +eq(SB.name(0, sess), "SACHA's BASE", "US: the row follows the owner") +set("gText_ApostropheSBase", text(" きち")) +eq(SB.name(0, sess), "SACHA きち", "Japanese: the same") +set("gText_ApostropheSBase", { { t = "text", s = "BASE DE " }, { t = "strvar", n = 1 }, { t = "eos" } }) +eq(SB.name(0, sess), "BASE DE SACHA", "French: the owner fills the row's STR_VAR_1") + +for key, ir in pairs(saved) do RomText.overrides[key] = ir or nil end + +T.finish("game3_european_name_placeholder_test")