Files
gen1recomp/tests/modkit/cases/dataset_views.lua
T
2026-08-24 09:33:51 +02:00

289 lines
12 KiB
Lua

-- Sandboxed public-API coverage for imported semantic dataset views.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Fixture = require("tests.modkit.dataset_view_fixture")
local GameVersion = require("src.core.GameVersion")
local CacheFs = require("src.import.CacheFs")
local Schemas = require("src.mods.Schemas")
local originalVersion = GameVersion.get()
local originalPrefix = CacheFs.prefix
GameVersion.set("red")
local files = {}
for _, version in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
Fixture.cache(files, version)
end
local probe = Fixture.addMod(files, "dataset_probe", [[
local mod = ...
local out = {}
for _, version in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
local view, reason = mod.datasets:open(version)
local mon = view and view.content.pokemon:get("FIXMON")
if mon then mon.name = "MUTATED" end
local ids = {}
if view then
for id, value in view.content.pokemon:each() do
ids[#ids + 1] = id
if value then value.name = "EACH_MUTATED" end
end
end
out[version] = {
reason = reason, generation = view and view.generation,
name = view and view.content.pokemon:get("FIXMON").name,
has = view and view.content.pokemon:has("FIXMON"), ids = ids,
aliasScripts = view and view.content.scripts == view.content.map_scripts,
aliasUi = view and view.content.ui == view.content.screens,
readOnly = view and view.content.pokemon.register == nil
and view.content.pokemon.patch == nil and view.content.pokemon.remove == nil,
}
end
local yellow = assert(mod.datasets:open("yellow"))
out.yellowOldMan = yellow.content.field:get("oldManBattle")
out.yellowBoot = yellow.content.field:get("boot")
local red = assert(mod.datasets:open("red"))
out.redBoot = red.content.field:get("boot")
out.redConstants = red.content.constants:get("dexSize")
local gold = assert(mod.datasets:open("gold"))
out.registryCount = 0
for _, registry in pairs(gold.content) do
if type(registry) == "table" then out.registryCount = out.registryCount + 1 end
end
out.foresight = gold.content.type_chart:get("NORMAL>GHOST")
out.held = gold.content.held_items:get("LEFTOVERS")
out.continuations, out.moves, out.assets = {}, {}, {}
for _, id in ipairs({ "CROBAT", "BELLOSSOM", "POLITOED", "SLOWKING",
"STEELIX", "SCIZOR", "KINGDRA", "PORYGON2", "BLISSEY" }) do
local row = gold.content.pokemon:get(id)
out.continuations[id] = row and row.dex
out.assets[id] = row and {
gold.assets:info(row.spriteFront), gold.assets:info(row.spriteBack) }
end
for _, id in ipairs({ "IRON_TAIL", "METAL_CLAW", "STEEL_WING", "RAIN_DANCE",
"SUNNY_DAY", "SANDSTORM", "SLUDGE_BOMB", "SHADOW_BALL" }) do
out.moves[id] = gold.content.moves:has(id)
end
out.steel = gold.content.type_chart:has("STEEL")
out.executableBuiltinHidden = gold.content.statuses:get("sleep") == nil
out.assetDirectory = gold.assets:info("assets/generated/battle/front")
out.assetRejects = {}
for _, path in ipairs({ "/assets/generated/x", "assets\\generated\\x",
"assets/generated/../x", "assets/generated/./x", "other/x",
"assets/generated/x\1" }) do
out.assetRejects[#out.assetRejects + 1] = pcall(gold.assets.path, gold.assets, path)
end
local unknown, unknownReason = mod.datasets:open("crystal")
out.unknown = { unknown ~= nil, unknownReason }
mod.exports.result = out
]])
local active = { pokemon = { ACTIVE = { id = "ACTIVE", nested = { n = 1 } } } }
local run = T.sdk.loadMods({ probe }, {
fs = T.sdk.memfs(files), data = active, generation = 1,
})
T.eq(#run.errors, 0, "dataset public probe loads clean")
local out = run.loader.exports.dataset_probe.result
for _, version in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
T.eq(out[version].reason, nil, version .. " opens")
T.eq(out[version].generation,
(version == "gold" or version == "silver") and 2 or 1,
version .. " reports selected generation")
T.eq(out[version].name, "FIXMON", version .. " get/each values are detached")
T.eq(out[version].has, true, version .. " has reads without exposing a copy")
T.eq(out[version].aliasScripts, true, version .. " scripts alias is canonical")
T.eq(out[version].aliasUi, true, version .. " ui alias is canonical")
T.eq(out[version].readOnly, true, version .. " registry has no write verbs")
end
T.eq(out.redConstants, 1, "Red derives dexSize through canonical defaults")
T.eq(out.redBoot and out.redBoot.screens and out.redBoot.screens.splash,
"IntroMovie", "Red uses canonical boot defaults")
T.eq(out.yellowOldMan and out.yellowOldMan.species, "RATTATA",
"Yellow applies canonical correction")
T.eq(out.yellowBoot and out.yellowBoot.screens and out.yellowBoot.screens.splash,
"YellowIntro",
"Yellow uses its canonical splash")
T.eq(out.foresight and out.foresight.multiplier, 0,
"Gold appends Foresight matchup rows")
T.eq(out.held and out.held.heldEffect, "HELD_LEFTOVERS", "Gold derives held_items")
for _, id in ipairs(Fixture.CONTINUATIONS) do
T.check(out.continuations[id] ~= nil, "Gold exposes " .. id)
T.same(out.assets[id], { { type = "file" }, { type = "file" } },
"Gold exposes namespaced front/back assets for " .. id)
end
for _, id in ipairs(Fixture.MOVES) do T.eq(out.moves[id], true, "Gold exposes " .. id) end
T.eq(out.steel, true, "Gold exposes Steel")
local registryCount = 0
for _ in pairs(Schemas.REGISTRIES) do registryCount = registryCount + 1 end
for _ in pairs(Schemas.ALIASES) do registryCount = registryCount + 1 end
T.eq(out.registryCount, registryCount,
"view exposes every canonical registry and alias")
T.eq(out.executableBuiltinHidden, true,
"engine records containing closures do not cross the data-only facade")
T.eq(out.assetDirectory, nil, "asset info does not expose directories")
for index, accepted in ipairs(out.assetRejects) do
T.eq(accepted, false, "asset rejection " .. index)
end
T.same(out.unknown, { false, "unknown_version" }, "unknown version fails stably")
T.eq(active.pokemon.ACTIVE.nested.n, 1, "active Data stays unchanged")
T.eq(active.pokemon.FIXMON, nil, "inactive records do not merge into active Data")
T.eq(GameVersion.get(), "red", "active GameVersion stays unchanged")
T.eq(CacheFs.prefix, originalPrefix, "CacheFs prefix stays unchanged")
run.release()
-- Facade replacement and nested record mutation do not cross mod boundaries.
local isolation = {}
Fixture.cache(isolation, "red")
local mutator = Fixture.addMod(isolation, "dataset_mutator", [[
local mod = ...
local view = assert(mod.datasets:open("red"))
view.content.pokemon.get = function() return { name = "POISONED" } end
local row = view.content.pokemon:get("FIXMON")
if row then row.name = "CHANGED" end
]])
local observer = Fixture.addMod(isolation, "dataset_observer", [[
local mod = ...
local view = assert(mod.datasets:open("red"))
mod.exports.name = view.content.pokemon:get("FIXMON").name
]])
isolation["mods/dataset_observer/manifest.json"] = [[{
"id": "dataset_observer", "name": "dataset_observer", "version": "1.0.0",
"entry": "main.lua", "api": 2, "games": ["all"],
"dependencies": ["dataset_mutator"]
}]]
local isolationRun = T.sdk.loadMods({ mutator, observer }, {
fs = T.sdk.memfs(isolation), data = { pokemon = {} }, generation = 1,
})
T.eq(isolationRun.loader.exports.dataset_observer.name, "FIXMON",
"facade and record mutation cannot cross mods")
isolationRun.release()
-- Marker-only, empty, partial, and stale-after-first-open caches fail closed.
local readiness = {}
local emptyPrefix = GameVersion.cachePrefix("red")
readiness[emptyPrefix .. "rom-cache.complete"] =
"rom-cache-v10:" .. GameVersion.info("red").sha1
Fixture.cache(readiness, "blue")
readiness[GameVersion.cachePrefix("blue") .. "data/generated/moves.lua"] = nil
Fixture.cache(readiness, "gold")
local staleReads = 0
local readinessFs = T.sdk.memfs(readiness)
local rawRead = readinessFs.read
readinessFs.read = function(path)
if path == GameVersion.cachePrefix("gold") .. "rom-cache.complete" then
staleReads = staleReads + 1
if staleReads > 1 then return "rom-cache-v9:stale" end
end
return rawRead(path)
end
local readinessMod = Fixture.addMod(readiness, "readiness_probe", [[
local mod = ...
local out = {}
for _, version in ipairs({ "red", "blue" }) do
local view, reason = mod.datasets:open(version)
out[version] = { view ~= nil, reason }
end
local first, firstReason = mod.datasets:open("gold")
local second, secondReason = mod.datasets:open("gold")
out.gold = { first ~= nil, firstReason, second ~= nil, secondReason }
mod.exports.result = out
]])
local readinessRun = T.sdk.loadMods({ readinessMod }, {
fs = readinessFs, data = { pokemon = {} }, generation = 1,
})
local readyOut = readinessRun.loader.exports.readiness_probe.result
T.same(readyOut.red, { false, "not_imported" }, "marker-only empty cache fails")
T.same(readyOut.blue, { false, "not_imported" }, "partial cache fails")
T.same(readyOut.gold, { true, nil, false, "not_imported" },
"cached view is invalidated when marker becomes stale")
readinessRun.release()
-- Removal followed by a fresh import cannot resurrect the pre-removal view.
local transition = {}
Fixture.cache(transition, "red")
local transitionFs = T.sdk.memfs(transition)
local transitionRead = transitionFs.read
local transitionMarkers = 0
local reimportedPokemon = require("src.import.LuaWriter").encode({
FIXMON = { id = "FIXMON", name = "REIMPORTED", dex = 1,
spriteFront = "assets/generated/battle/front/fixmon.png",
spriteBack = "assets/generated/battle/back/fixmon.png", frontSize = 5 },
})
transitionFs.read = function(path)
if path == GameVersion.cachePrefix("red") .. "rom-cache.complete" then
transitionMarkers = transitionMarkers + 1
if transitionMarkers == 2 then return nil end
end
local body = transitionRead(path)
if transitionMarkers >= 3
and path == GameVersion.cachePrefix("red") .. "data/generated/pokemon.lua" then
return reimportedPokemon
end
return body
end
local transitionMod = Fixture.addMod(transition, "transition_probe", [[
local mod = ...
local out = {}
for index = 1, 3 do
local view, reason = mod.datasets:open("red")
out[index] = { view and view.content.pokemon:get("FIXMON").name, reason }
end
mod.exports.result = out
]])
local transitionRun = T.sdk.loadMods({ transitionMod }, {
fs = transitionFs, data = { pokemon = {} }, generation = 1,
})
T.same(transitionRun.loader.exports.transition_probe.result, {
{ "FIXMON" }, { nil, "not_imported" }, { "REIMPORTED" },
}, "remove and reimport transition rebuilds the semantic view")
transitionRun.release()
-- Every hostile source is rejected as data, with one stable public reason.
local hostile = {
{ "red", "return { BAD = function() return 1 end }" },
{ "blue", "owned = true; return {}" },
{ "yellow", "return 7" },
{ "gold", string.char(27) .. "Lua" },
{ "silver", "return {}; while true do end" },
}
local hostileFiles, hostilePaths = {}, {}
for _, row in ipairs(hostile) do
Fixture.cache(hostileFiles, row[1], { pokemon = row[2] })
end
local hostileMod = Fixture.addMod(hostileFiles, "hostile_probe", [[
local mod = ...
local out = {}
for _, version in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
local view, reason = mod.datasets:open(version)
out[version] = { view ~= nil, reason }
end
mod.exports.result = out
]])
hostilePaths[1] = hostileMod
local previousHook, previousMask, previousCount
local beforeHook
if debug.gethook and debug.sethook then
previousHook, previousMask, previousCount = debug.gethook()
beforeHook = function() end
debug.sethook(beforeHook, "", 1000)
end
local hostileRun = T.sdk.loadMods(hostilePaths, {
fs = T.sdk.memfs(hostileFiles), data = { pokemon = {} }, generation = 1,
})
local hostileOut = hostileRun.loader.exports.hostile_probe.result
for _, row in ipairs(hostile) do
T.same(hostileOut[row[1]], { false, "invalid_cache" },
row[1] .. " hostile generated source fails closed")
end
T.eq(debug.gethook and debug.gethook(), beforeHook,
"dataset decoding preserves the caller debug hook")
if debug.sethook then
if previousHook then debug.sethook(previousHook, previousMask, previousCount)
else debug.sethook() end
end
hostileRun.release()
GameVersion.set(originalVersion)
CacheFs.prefix = originalPrefix
T.finish("dataset_views")