Fix Gen 1/2 battle, menu, PC and audio parity gaps; wire luacheck into CI

CLOSES #1484, CLOSES #1486, CLOSES #1513, CLOSES #1517, CLOSES #1556, CLOSES #1564, CLOSES #1567
This commit is contained in:
bryanthaboi
2026-08-21 19:22:34 -04:00
parent 087a275189
commit c0f4315cd8
45 changed files with 2487 additions and 168 deletions
@@ -0,0 +1,144 @@
-- T4: a mod may not reach a Gen 2 engine module from a Gen 1 game.
--
-- src/battle/gen2/Mon.lua returns { hp, attack, defense, speed, specialAttack,
-- specialDefense }; a Gen 1 party mon carries `special` instead. A Yellow mod
-- that required that module and ran refreshStats over save.party wrote the Gen
-- 2 block onto Gen 1 mons, and every reader of stats.special raised from then
-- on (#1517). The require shim refuses it now, whatever the manifest declares:
-- engine_internals is a disclosure, not a generation gate.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local PROBE = [[
local mod = ...
local out = mod.exports
local function attempt(name)
local ok, result = pcall(require, name)
if ok then return nil, type(result) end
return tostring(result), nil
end
out.attempt = attempt
out.monErr, out.monType = attempt("src.battle.gen2.Mon")
out.saveErr = attempt("src.core.gen2.Save")
out.worldErr = attempt("src.ui.gen2.Chrome")
out.game2Err = attempt("src.core.Game2")
out.semverErr, out.semverType = attempt("src.mods.Semver")
out.statsErr, out.statsType = attempt("src.pokemon.Stats")
out.compatErr, out.compatType = attempt("src.mods.Gen2Compat")
out.loggerErr, out.loggerType = attempt("src.core.Logger")
]]
local function manifest(id, permissions)
return ('{"id":"%s","name":"%s","version":"1.0.0","entry":"main.lua",'
.. '"api":2,"games":["all"]%s}'):format(id, id, permissions or "")
end
-- ------- Gen 1 boot: refused, and the permission does not unlock it
local FILES = {
["mods/gen2_declared/manifest.json"] =
manifest("gen2_declared", ',"permissions":["engine_internals"]'),
["mods/gen2_declared/main.lua"] = PROBE,
["mods/gen2_undeclared/manifest.json"] = manifest("gen2_undeclared"),
["mods/gen2_undeclared/main.lua"] = PROBE,
}
T.eq(package.loaded["src.battle.gen2.Mon"], nil,
"the Gen 2 mon module is not loaded before the Gen 1 boot")
local run = T.sdk.loadMods({ "mods/gen2_declared", "mods/gen2_undeclared" },
{ fs = T.sdk.memfs(FILES), data = {}, generation = 1 })
T.eq(#run.errors, 0,
"a mod that only probes still loads clean (" .. tostring(run.errors[1]) .. ")")
local declared = run.loader.exports.gen2_declared or {}
local undeclared = run.loader.exports.gen2_undeclared or {}
T.check(declared.monErr, "src.battle.gen2.Mon is refused on a Gen 1 game")
T.check(declared.monErr and declared.monErr:find("src.battle.gen2.Mon", 1, true),
"the refusal names the module: " .. tostring(declared.monErr))
T.check(declared.monErr and declared.monErr:find("gen2_declared", 1, true),
"and the mod that asked for it")
T.check(declared.monErr and declared.monErr:find("mod.game", 1, true),
"and the surface to use instead")
T.eq(declared.monType, nil, "nothing came back for the mod to call")
T.eq(package.loaded["src.battle.gen2.Mon"], nil,
"and the module was never loaded at all")
T.check(declared.saveErr, "src.core.gen2.Save is refused the same way")
T.check(declared.worldErr, "so is src.ui.gen2.Chrome")
T.check(declared.game2Err and declared.game2Err:find("src.core.Game2", 1, true),
"and src.core.Game2, the Gen 2 service owner: " .. tostring(declared.game2Err))
T.check(undeclared.monErr,
"a mod with no permissions is refused for the same reason")
T.eq(declared.monErr and declared.monErr:gsub("gen2_declared", "X"),
undeclared.monErr and undeclared.monErr:gsub("gen2_undeclared", "X"),
"engine_internals is a disclosure, not a generation gate: same refusal")
-- ------- what the gate does NOT touch
T.eq(declared.semverErr, nil, "the supported requires still resolve")
T.eq(declared.semverType, "table", "and hand back the module")
T.eq(declared.statsErr, nil, "src.pokemon.Stats is still published to mods")
T.eq(declared.compatErr, nil,
"src.mods.Gen2Compat is not a Gen 2 engine module: it is the adapter table")
T.eq(declared.compatType, "table", "so it still answers")
T.eq(declared.loggerErr, nil,
"an undeclared engine require is still a warning, not a block")
T.eq(declared.loggerType, "table", "and still resolves")
T.eq(undeclared.loggerErr, nil,
"including for a mod that declared no permissions at all")
-- a lazy require made long after the entry chunk ran is gated too: the mod's
-- own require is what the shim identifies, not the load phase
do
local err = declared.attempt and declared.attempt("src.world.gen2.World")
T.check(err and err:find("src.world.gen2.World", 1, true),
"a require made after load is refused too: " .. tostring(err))
end
-- the shim only judges a mod's own require; engine and harness callers keep
-- the module they asked for
do
local ok, module = pcall(require, "src.world.gen2.WorldAPI")
T.check(ok, "a non-mod caller still reaches a Gen 2 module: " .. tostring(module))
T.eq(type(module), "table", "and gets the real one")
end
run.release()
-- ------- an unguarded reach fails the whole mod, on the boot error feed
local HARD = {
["mods/gen2_hard/manifest.json"] = manifest("gen2_hard",
',"permissions":["engine_internals"]'),
["mods/gen2_hard/main.lua"] = [[
local mod = ...
local Mon = require("src.battle.gen2.Mon")
mod.exports.reached = Mon ~= nil
]],
}
local hard = T.sdk.loadMods({ "mods/gen2_hard" },
{ fs = T.sdk.memfs(HARD), data = {}, generation = 1 })
T.eq(hard.loader.mods.gen2_hard.state, "failed",
"a mod that reaches across the generation does not load")
T.eq(hard.loader.exports.gen2_hard, nil, "and publishes nothing")
T.check(hard.errors[1] and hard.errors[1]:find("src.battle.gen2.Mon", 1, true),
"the boot error feed names the module: " .. tostring(hard.errors[1]))
hard.release()
-- ------- Gen 2 boot: the same require is the mod's own generation
local gen2 = T.sdk.loadMods({ "mods/gen2_declared" },
{ fs = T.sdk.memfs(FILES), data = {}, generation = 2 })
local onGold = gen2.loader.exports.gen2_declared or {}
T.eq(onGold.monErr, nil,
"on a Gen 2 game the same require is answered: " .. tostring(onGold.monErr))
T.eq(onGold.monType, "table", "with the real Gen 2 mon module")
T.eq(onGold.game2Err, nil, "and src.core.Game2 is this game's service owner")
gen2.release()
T.finish("cross_generation_require")