mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
e2454bf568
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.
125 lines
6.0 KiB
Lua
125 lines
6.0 KiB
Lua
-- J10: the mod API dispatches Gen 3 on (engine, versionId), with a graceful
|
|
-- fallback to the generation's default row so FireRed behaviour is unchanged.
|
|
--
|
|
-- The seam is three tables: Loader.apiModule (facade module paths),
|
|
-- Schemas.GEN3_ROUTING (per-game registry routing) and
|
|
-- Schemas.GEN3_LIVE_MODULES (per-game live-module bindings). This suite pins
|
|
-- the defaults and proves an overlay can override one registry/key without
|
|
-- forking the shared tables.
|
|
-- luajit tests/engine/game3_version_dispatch_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 Schemas = require("src.mods.Schemas")
|
|
local Loader = require("src.mods.Loader")
|
|
|
|
local prevVersion = GameVersion.get()
|
|
|
|
-- ------------------------------------------------------- facade dispatch
|
|
|
|
eq(Loader.apiModule("battle", 3, nil), "src.battle.game3.BattleAPI",
|
|
"gen3 battle facade default is the FireRed-backed module")
|
|
eq(Loader.apiModule("world", 3, nil), "src.world.game3.WorldAPI",
|
|
"gen3 world facade default is the FireRed-backed module")
|
|
eq(Loader.apiModule("battle", 3, "firered"), "src.battle.game3.BattleAPI",
|
|
"firered resolves the FireRed-backed facade")
|
|
eq(Loader.apiModule("battle", 3, "leafgreen"), "src.battle.game3.BattleAPI",
|
|
"leafgreen shares the FireRed-backed facade")
|
|
eq(Loader.apiModule("world", 3, "leafgreen"), "src.world.game3.WorldAPI",
|
|
"leafgreen world facade")
|
|
eq(Loader.apiModule("battle", 3, "ruby"), "src.battle.game3.BattleAPI",
|
|
"an RSE id with no row falls back to the FireRed-backed facade")
|
|
eq(Loader.apiModule("world", 3, "not-a-game"), "src.world.game3.WorldAPI",
|
|
"an unknown id falls back to the FireRed-backed facade")
|
|
|
|
-- other generations keep their existing arms
|
|
eq(Loader.apiModule("battle", 2, nil), "src.battle.gen2.BattleAPI", "gen2 battle facade")
|
|
eq(Loader.apiModule("world", 2, nil), "src.world.gen2.WorldAPI", "gen2 world facade")
|
|
eq(Loader.apiModule("battle", 1, nil), "src.battle.BattleAPI", "gen1 battle facade")
|
|
eq(Loader.apiModule("world", 1, nil), "src.world.WorldAPI", "gen1 world facade")
|
|
|
|
-- ------------------------------------------------------- routing dispatch
|
|
|
|
check(Schemas.routing(3, nil) == Schemas.GEN3, "gen3 routing without a version is GEN3")
|
|
check(Schemas.routing(3, "firered") == Schemas.GEN3, "firered has no overlay, so GEN3")
|
|
check(Schemas.routing(3, "ruby") == Schemas.GEN3, "an overlay-less RSE id reads GEN3")
|
|
check(Schemas.routing(2, nil) == Schemas.GEN2, "gen2 routing unchanged")
|
|
check(Schemas.routing(1, nil) == Schemas.GEN1, "gen1 routing unchanged")
|
|
|
|
local spec = Schemas.REGISTRIES.pokemon
|
|
check(spec ~= nil, "the pokemon registry spec exists to test against")
|
|
eq(Schemas.targetFor("pokemon", spec, 3, "firered"), "gen3Pokemon",
|
|
"firered routes pokemon to the shared gen3Pokemon root")
|
|
eq(Schemas.targetFor("pokemon", spec, 3, "ruby"), "gen3Pokemon",
|
|
"an overlay-less RSE id routes pokemon to the shared root")
|
|
check(Schemas.gatedFor("pokemon", 3, "firered") == false, "pokemon is not gated under FireRed")
|
|
|
|
-- a sparse overlay changes one registry and inherits the rest
|
|
Schemas.GEN3_ROUTING["testgame"] = { pokemon = "gen3PokemonTest", moves = false }
|
|
local routed = Schemas.routing(3, "testgame")
|
|
check(routed ~= Schemas.GEN3, "an overlay produces a distinct merged view")
|
|
eq(routed.pokemon, "gen3PokemonTest", "the overlay's row wins")
|
|
eq(routed.moves, false, "the overlay can gate a registry")
|
|
eq(routed.items, Schemas.GEN3.items, "registries the overlay omits are inherited")
|
|
eq(Schemas.targetFor("pokemon", spec, 3, "testgame"), "gen3PokemonTest",
|
|
"targetFor consults the overlay")
|
|
check(Schemas.gatedFor("moves", 3, "testgame") == true,
|
|
"the overlay can gate a registry the default leaves open")
|
|
check(Schemas.gatedFor("moves", 3, "firered") == false,
|
|
"the default view is untouched by the overlay")
|
|
check(Schemas.routing(3, "firered") == Schemas.GEN3, "the merged view is never written back")
|
|
Schemas.GEN3_ROUTING["testgame"] = nil
|
|
|
|
-- ---------------------------------------------------- live module dispatch
|
|
|
|
local probe = { measure = function() return 1 end }
|
|
package.loaded["tests.dispatch_probe"] = probe
|
|
Schemas.GEN3_LIVE_MODULES["testgame"] = { gen3Pokemon = "tests.dispatch_probe" }
|
|
check(Schemas.liveModuleFor("gen3Pokemon", "testgame") == probe,
|
|
"a version overlay selects its own live module")
|
|
check(Schemas.liveModuleFor("gen3Pokemon", "firered") ~= probe,
|
|
"the FireRed live module is unaffected")
|
|
Schemas.GEN3_LIVE_MODULES["testgame"] = nil
|
|
package.loaded["tests.dispatch_probe"] = nil
|
|
|
|
-- bindGen3 records the game so live-module lookups can narrow by it
|
|
local data = { gen3Pokemon = { names = {} } }
|
|
Schemas.bindGen3(data, "testgame")
|
|
eq(Schemas.boundVersion(data), "testgame", "bindGen3 records the version id")
|
|
local legacy = { gen3Pokemon = { names = {} } }
|
|
Schemas.bindGen3(legacy)
|
|
eq(Schemas.boundVersion(legacy), nil, "a version-less bind reports nil")
|
|
|
|
-- ------------------------------------------------------- loader plumbing
|
|
|
|
local function memfs(files)
|
|
return {
|
|
read = function(_, path) return files[path] end,
|
|
write = function(_, path, data) files[path] = data; return true end,
|
|
getInfo = function(_, path) return files[path] and { type = "file" } or nil end,
|
|
getDirectoryItems = function()
|
|
local items = {}
|
|
for key in pairs(files) do items[#items + 1] = key end
|
|
table.sort(items)
|
|
return items
|
|
end,
|
|
}
|
|
end
|
|
|
|
GameVersion.set("firered")
|
|
local loader = Loader.new({ fs = memfs({}) })
|
|
eq(loader.generation, 3, "a Gen 3 boot builds a Gen 3 loader")
|
|
eq(loader.version, "firered", "the loader carries the active game id")
|
|
|
|
local injected = Loader.new({ fs = memfs({}), generation = 3, version = "testgame" })
|
|
eq(injected.version, "testgame", "opts.version is the test seam")
|
|
eq(Loader.apiModule("world", injected.generation, injected.version), "src.world.game3.WorldAPI",
|
|
"an injected game with no row still resolves the default facade")
|
|
|
|
GameVersion.set(prevVersion)
|
|
T.finish("game3_version_dispatch_test")
|