mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 13:36:14 +02:00
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:
@@ -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")
|
||||
Reference in New Issue
Block a user