-- 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")