Files
gen1recomp/tests/engine/game3_font_provider_test.lua
T
Shane McGovern e2454bf568 fix(game3): review v3 fixes (battle, field, script, save) + gated T6 tier
Second wave of fixes from the Gen 3 codebase review (210 findings), the
regressions the full-suite sweep surfaced, and a T6 tier so the top-level
tests/game3_*.lua suites run inside ./scripts/test.sh instead of outside it.

Battle: Knock Off and Thief/Trick item persistence follow the ROM, not the
review - the party slot is never written through during battle, and the
knocked-off mask suppresses the item on later send-outs
(pokefirered/src/battle_script_commands.c:2731 MOVE_EFFECT_KNOCK_OFF with the
STICKY_HOLD guard at :2732, opponent-steal guard at :2610-2622, mask use at
:4489). Rapid Spin keeps its one-per-use chain order (:8435-8474); Growl
keeps pret's target (src/data/battle_moves.h, include/battle.h:63).

Scripting: operand layouts match pret/asm/macros/event.inc - givemon 15 bytes
(:989-997), comparestat {B,W} (:1573-1576), setptr/loadbytefromptr/setptrbyte
take a word pointer (:118-137). 18 of the 20 E10 ops are wired per
src/scrcmd.c (per-op citations in docs/game3/e10-opcode-spec.md), money and
random and the warp family read their operands as VarGets
(src/scrcmd.c:1798-1830, :455-461, :719-731), and the day-care party-full
guard is in place (src/daycare.c:525,:1081).

Field and UI: fishing counts rounds as the ROM does
(src/field_player_avatar.c:1740-1765), an unresolved map section no longer
reports Pallet Town (src/region_map.c:3782), and a font provider sits behind
a FireRed capability/profile seam.

Save data: profile-driven Options.block, a save version round-trip, and slot
id validation that blocks path traversal (slotDiskPath("firered", "../evil")
returns nil).

Importer: the parallel import path now writes both completion markers,
object kind/clone bytes decode as pret defines them
(include/global.fieldmap.h:110-130), and the HM table drops Whirlpool
(include/constants/items.h:411-418).

Tests: six new scenario suites (battle_ai, capture, event, menu, move,
overworld) and eight engine suites (profile, capabilities, cache paths, font
provider, options block, save version round-trip, version dispatch,
versions_game). T6 runs every top-level game3 suite at GAME3_JOBS default 8
with per-suite logs, failure cause classification and a KNOWN_GAME3_FAILURES
ceiling of 23 as a shrinking guard.

Docs: the working notes under docs/game3 (triage ledger, pret citation audit,
e10 opcode spec, RSE seams, sweep v3/v113, test baseline, artifact
conversions, merge trial).

Full gate at this snapshot: exit 0, all tiers passed - engine 624/624,
T6 279/279 (0 known, 0 fresh failures), gen2 146/146, modkit 37/37, luacheck
clean, privacy gate 3/3 over a 9,711-file publication set.
2026-09-22 12:22:50 +01:00

96 lines
4.5 KiB
Lua

-- T5.1: the Game3 font provider resolves the active game's implementation
-- through the profile and forwards its API, so a Ruby/Sapphire/Emerald font
-- bank is a profile row plus (optionally) a registration, never a fork of
-- frlg_font.lua.
--
-- FireRed's profile names src.ui.game3.frlg_font, so every forwarded value
-- must be identical to requiring that module directly.
-- luajit tests/engine/game3_font_provider_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local GameVersion = require("src.core.GameVersion")
local Profile = require("src.core.game3.profile")
local Font = require("src.ui.game3.font")
local FrlgFont = require("src.ui.game3.frlg_font")
local prevVersion = GameVersion.get()
-- --------------------------------------------------- FireRed resolution
GameVersion.set("firered")
Profile.reset()
Font.reset()
check(Font.impl("firered") == FrlgFont, "firered resolves the profile-named frlg_font")
check(Font.impl(nil) == FrlgFont, "nil resolves the active game's implementation")
check(Font.active() == FrlgFont, "active() is the active game's implementation")
check(Font.impl("ruby") == FrlgFont, "an overlay-less RSE id falls back to FireRed's font")
-- The provider forwards, live and by identity.
eq(Font.CELL, FrlgFont.CELL, "CELL forwards")
eq(Font.GLYPH_HEIGHT, FrlgFont.GLYPH_HEIGHT, "GLYPH_HEIGHT forwards")
eq(Font.LINE_PITCH, FrlgFont.LINE_PITCH, "LINE_PITCH forwards")
check(Font.COLOR == FrlgFont.COLOR, "COLOR forwards by identity")
check(Font.STDPAL == FrlgFont.STDPAL, "STDPAL forwards by identity")
check(Font.COLOR_IDS == FrlgFont.COLOR_IDS, "COLOR_IDS forwards by identity")
check(Font.draw == FrlgFont.draw, "draw forwards the raw function")
check(Font.measure == FrlgFont.measure, "measure forwards the raw function")
check(Font.wrap == FrlgFont.wrap, "wrap forwards the raw function")
check(Font.drawGlyph == FrlgFont.drawGlyph, "drawGlyph forwards")
check(Font.scanTokens == FrlgFont.scanTokens, "scanTokens forwards")
check(Font.invalidate == FrlgFont.invalidate, "invalidate forwards")
check(Font.countChars == FrlgFont.countChars, "countChars forwards")
eq(Font.MAX_LETTER_WIDTH, FrlgFont.MAX_LETTER_WIDTH, "MAX_LETTER_WIDTH forwards")
-- Forwarding reads the implementation now, not a load-time snapshot: a
-- registration for the ACTIVE game is visible immediately.
local replacement = {
CELL = 99,
measure = function() return 42 end,
}
check(Font.register("testgame", replacement) == true, "register accepts a version + table")
check(Font.register("", replacement) == false, "register rejects an empty version")
check(Font.register("testgame", 7) == false, "register rejects a non-table")
check(Font.impl("testgame") == replacement, "a registration wins over the profile module")
eq(Font.impl("testgame").CELL, 99, "a registered table answers directly")
-- Active forwarding follows a registration for the game that is running.
check(Font.register("firered", replacement) == true, "register the active game")
eq(Font.CELL, 99, "a forwarded constant follows the active registration")
check(Font.measure == replacement.measure, "a forwarded function follows the registration")
-- reset() drops resolutions and keeps registrations.
Font.reset()
check(Font.impl("firered") == replacement, "registrations survive reset")
check(Font.impl("ruby") == FrlgFont, "the FireRed fallback still answers for an overlay-less id")
-- unregister restores the profile's module.
check(Font.unregister("firered") == true, "unregister returns true")
Font.reset()
check(Font.impl("firered") == FrlgFont, "unregister falls back to the profile's module")
check(Font.unregister("testgame") == true, "clean up the fixture registration")
-- ------------------------------------------------ lazy, profile-driven load
-- The provider must not require an implementation until asked, and it must
-- load the module the profile names.
local saved = package.loaded["src.ui.game3.frlg_font"]
package.loaded["src.ui.game3.frlg_font"] = nil
Font.reset()
local loaded = Font.impl("firered")
check(loaded ~= nil, "the provider loads the profile-named module")
check(package.loaded["src.ui.game3.frlg_font"] == loaded,
"the loaded module is the one frlg_font's path names")
package.loaded["src.ui.game3.frlg_font"] = saved or loaded
-- A key the implementation does not carry reads as nil instead of raising, so
-- a headless tool can probe the provider without a graphics context.
check(Font.NO_SUCH_KEY == nil, "an unknown forwarded key reads as nil")
GameVersion.set(prevVersion)
T.finish("game3_font_provider_test")