fix(game3): PC panel species scope fix and trainer-card var key fallback

Three follow-ups to the merge snapshot, kept as their own commit so the
review of the wave commit stays intact:

- The PC panel's left data panel broke in the merge: upstream inlines the
  egg-aware species read into frontPic while the M8 dedup had removed the
  duplicate local, so `sp` was referenced with no binding and the luacheck
  gate failed. One shared local sp = Pokemon.speciesOrEgg(hoveredMon) serves
  both the TV sprite and the stats card
  (pokefirered/src/pokemon_storage_system_data.c:1034, :1057,
  MON_DATA_SPECIES_OR_EGG).
- Trainer-card vars fall back to the stringified key: persist_sidecar and
  Flags.serialize write tostring(id) keys into session.vars, so a numeric miss
  used to read every var as 0.
- tests/game3_trainer_card_photo_tint_test.lua drives the whole Game Corner
  photo -> card tint chain (setvar -> UpdateTrainerCardPhotoIcons ->
  Flags.serialize/loadInto -> cardData) and pins c.monIconTint/c.monSpecies.
This commit is contained in:
Shane McGovern
2026-09-22 18:35:22 +01:00
parent 72f083f0c4
commit 59886d9639
3 changed files with 161 additions and 2 deletions
+6 -1
View File
@@ -219,7 +219,12 @@ function PcChrome.drawLeftDataPanel(hoveredMon, hoverFrame)
-- 1. Front Sprite in TV Screen (X: 10..73, Y: 19..80, W: 64, H: 61)
-- pokefirered/src/pokemon_storage_system_data.c:1034, :1057 MON_DATA_SPECIES_OR_EGG
local sprite = Pokemon.frontPic(Pokemon.speciesOrEgg(hoveredMon))
-- Merge fix: upstream inlined the species read into frontPic (egg-aware) while
-- our M8 dedup removed block 2's duplicate local — one shared `sp` above
-- satisfies both sides (speciesOrEgg == speciesOf for every non-egg, and the
-- stats card never prints spName for an egg).
local sp = Pokemon.speciesOrEgg(hoveredMon)
local sprite = Pokemon.frontPic(sp)
if sprite and sprite.image then
love.graphics.setColor(1, 1, 1, 1)
local sw, sh = sprite.image:getDimensions()
+7 -1
View File
@@ -342,7 +342,13 @@ local function get_var(session, varId)
end
if type(store.vars) == "table" then return tonumber(store.vars[varId]) or 0 end
end
if session and type(session.vars) == "table" then return tonumber(session.vars[varId]) or 0 end
if session and type(session.vars) == "table" then
-- persist_sidecar/Flags.serialize write tostring(id) keys into session.vars;
-- a numeric miss must fall back to the string key or every var reads 0.
local v = session.vars[varId]
if v == nil then v = session.vars[tostring(varId)] end
return tonumber(v) or 0
end
return 0
end
@@ -0,0 +1,148 @@
-- End-to-end proof for the Game Corner photo -> trainer-card tint chain.
-- Mirrors the real in-game wiring:
-- setvar VAR_0x8004 (ctx.specialVars) -> special UpdateTrainerCardPhotoIcons (0x167)
-- -> Flags.setVar into THE SAME store the card reads (Space.store in-game)
-- -> Flags.serialize (game.save) -> Flags.loadInto (next boot)
-- -> TrainerCard.cardData/gather -> c.monIconTint / c.monSpecies.
-- Ground truth: saves/firered/slot1.lua (Sep 22 18:24) carries
-- ["16450"]=2, ["16451"]=4, ["16452".."16456"]=0 with a 1-mon party (Charmander).
package.path = "./?.lua;./?/init.lua;" .. package.path
local Std = require("src.core.game3.scripting.stdscripts")
local Natives = require("src.core.game3.scripting.natives")
local Flags = require("src.core.game3.scripting.flags")
local Schema = require("src.core.game3.save_schema_firered")
local TrainerCard = require("src.ui.game3.trainer_card")
local passed, failed = 0, 0
local function check(cond, msg)
if cond then
passed = passed + 1
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function checkEq(got, expected, msg)
if got == expected then
passed = passed + 1
print(string.format("[ok] %s (got %s)", msg, tostring(got)))
else
failed = failed + 1
print(string.format("[FAIL] %s: expected %s, got %s", msg, tostring(expected), tostring(got)))
end
end
local SPACE_KEY = "src.core.game3.scripting.space"
local RT_KEY = "src.core.game3.runtime"
local realLoadedSpace = package.loaded[SPACE_KEY]
local realLoadedRt = package.loaded[RT_KEY]
local function teardown()
package.loaded[SPACE_KEY] = realLoadedSpace
package.loaded[RT_KEY] = realLoadedRt
end
print("=== 1. Photo script path writes tint + species into THE store the card reads ===")
local session, store, ctx
do
session = Schema.newGame({ name = "RED" })
session.party = { { speciesId = 4, species = 4 } } -- user's save: one Charmander
store = Flags.newStore()
-- In-game both sides resolve Space from package.loaded; share one table so
-- scriptStore(ctx) (natives) and script_store() (trainer card) see ONE store.
package.loaded[SPACE_KEY] = { store = store, getStore = function() return store end }
package.loaded[RT_KEY] = { getSession = function() return session end }
ctx = {
flags = session.flags,
vars = session.vars,
specialVars = {},
}
-- Real setvar opcode path: Flags.setVar routes 0x80xx to ctx.specialVars.
Flags.setVar(store, ctx, 0x8004, 2) -- MON_ICON_TINT_PINK
local yielded = Natives.special(ctx, Std.SPECIAL.UpdateTrainerCardPhotoIcons)
check(yielded == false, "UpdateTrainerCardPhotoIcons completes without yielding")
checkEq(Flags.getVar(store, ctx, 0x4042), 2, "store 0x4042 tint idx is 2 (PINK)")
checkEq(Flags.getVar(store, ctx, 0x4043), 4, "store 0x4043 icon 1 is species 4")
for id = 0x4044, 0x4048 do
checkEq(Flags.getVar(store, ctx, id), 0, string.format("store 0x%04X empty slot", id))
end
checkEq(session.vars[0x4042], nil, "special vars do NOT leak into session.vars")
local c = TrainerCard.cardData(session)
checkEq(c.monIconTint, 2, "gather reads monIconTint=2 from the live store")
checkEq(c.monSpecies[1], 4, "gather reads monSpecies[1]=4 from the live store")
end
print("=== 2. Persist round trip: serialize (game.save) -> loadInto (next boot) ===")
do
local snap = Flags.serialize(store)
checkEq(snap.vars["16450"], 2, "serialize writes string key [\"16450\"]=2 (save format)")
checkEq(snap.vars["16451"], 4, "serialize writes string key [\"16451\"]=4")
local bootStore = Flags.newStore()
Flags.loadInto(bootStore, snap)
checkEq(bootStore.vars[0x4042], 2, "loadInto restores numeric 0x4042=2")
checkEq(bootStore.vars[0x4043], 4, "loadInto restores numeric 0x4043=4")
package.loaded[SPACE_KEY] = { store = bootStore, getStore = function() return bootStore end }
local c = TrainerCard.cardData(session)
checkEq(c.monIconTint, 2, "card gather after reload reads monIconTint=2")
checkEq(c.monSpecies[1], 4, "card gather after reload reads monSpecies[1]=4")
end
print("=== 3. Store unavailable: session.vars fallback must read string keys ===")
do
local snap = Flags.serialize(store)
session.vars = snap.vars -- save-format string keys, numeric miss
package.loaded[SPACE_KEY] = nil -- Space not resolvable -> script_store() nil
local c = TrainerCard.cardData(session)
checkEq(c.monIconTint, 2, "fallback reads [\"16450\"] -> monIconTint=2")
checkEq(c.monSpecies[1], 4, "fallback reads [\"16451\"] -> monSpecies[1]=4")
end
print("=== 4. Multi-mon party: all six species keys match snapshot order ===")
do
local session4 = Schema.newGame({ name = "RED" })
session4.party = {
{ speciesId = 1, species = 1 },
{ speciesId = 4, species = 4 },
{ speciesId = 7, species = 7 },
{ speciesId = 25, species = 25 },
{ speciesId = 133, species = 133 },
{ speciesId = 152, species = 152 },
}
local store4 = Flags.newStore()
package.loaded[SPACE_KEY] = { store = store4, getStore = function() return store4 end }
package.loaded[RT_KEY] = { getSession = function() return session4 end }
local ctx4 = { flags = session4.flags, vars = session4.vars, specialVars = {} }
Flags.setVar(store4, ctx4, 0x8004, 2) -- MON_ICON_TINT_PINK
Natives.special(ctx4, Std.SPECIAL.UpdateTrainerCardPhotoIcons)
checkEq(Flags.getVar(store4, ctx4, 0x4042), 2, "multi-mon: store 0x4042 tint idx is 2")
local want = { 1, 4, 7, 25, 133, 152 }
for i = 1, 6 do
checkEq(Flags.getVar(store4, ctx4, 0x4042 + i), want[i],
string.format("multi-mon: store 0x%04X icon %d snapshot order", 0x4042 + i, i))
end
local c4 = TrainerCard.cardData(session4)
checkEq(c4.monIconTint, 2, "multi-mon: gather monIconTint=2")
for i = 1, 6 do
checkEq(c4.monSpecies[i], want[i], string.format("multi-mon: gather monSpecies[%d]", i))
end
end
teardown()
print(string.format("Total: %d passed, %d failed", passed, failed))
os.exit(failed == 0 and 0 or 1)