Merge pull request #1767 from MaxTomahawk/adaptive-trainers/dataset-view-api

feat(mod-api): expose imported dataset views
This commit is contained in:
bryanthaboi
2026-08-25 08:26:51 -04:00
committed by GitHub
21 changed files with 1808 additions and 36 deletions
@@ -0,0 +1,46 @@
-- Imported semantic modules are opened without eager reads and are decoded
-- once, on first use, through the same public facade mods receive.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Fixture = require("tests.modkit.dataset_view_fixture")
local DatasetViews = require("src.mods.DatasetViews")
local GameVersion = require("src.core.GameVersion")
local SaveSerializer = require("src.core.SaveSerializer")
local files = {}
Fixture.cache(files, "gold")
local fs = T.sdk.memfs(files)
local rawRead = fs.read
local reads = {}
fs.read = function(path)
reads[path] = (reads[path] or 0) + 1
return rawRead(path)
end
local decodes = 0
local service = DatasetViews.new(fs, require, function(source, limits)
decodes = decodes + 1
return SaveSerializer.decode(source, limits)
end)
local pokemonPath = GameVersion.cachePrefix("gold")
.. "data/generated/pokemon.lua"
local movesPath = GameVersion.cachePrefix("gold")
.. "data/generated/moves.lua"
local view, reason = service:open("gold")
T.eq(reason, nil, "marker-valid Gold dataset opens")
T.check(view ~= nil, "open returns the lazy view")
T.eq(reads[pokemonPath] or 0, 0, "open does not read an unused root")
T.eq(reads[movesPath] or 0, 0, "open does not read another unused root")
T.eq(decodes, 0, "open decodes no semantic root")
local first = view and view.content.pokemon:get("FIXMON")
T.eq(first and first.name, "FIXMON", "first access decodes a valid root")
T.check(decodes > 0, "first root access performs bounded decoding")
T.eq(reads[movesPath] or 0, 0, "pokemon access leaves moves unused")
local afterFirst = decodes
local second = view and view.content.pokemon:get("FIXMON")
T.eq(second and second.name, "FIXMON", "repeated access remains available")
T.eq(decodes, afterFirst, "repeated access does not re-decode cached roots")
T.finish("dataset_views_lazy_validation")
@@ -0,0 +1,22 @@
-- Dedicated Route B no-mod parity: the additive facade remains cold.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local reads = 0
local fs = T.sdk.memfs({})
local read = fs.read
fs.read = function(path)
if path:match("^[a-z]+/data/generated/")
or path:match("^[a-z]+/rom%-cache%.complete$") then
reads = reads + 1
end
return read(path)
end
local data = { pokemon = { ACTIVE = { id = "ACTIVE", nested = { n = 1 } } } }
local run = T.sdk.loadNone({ fs = fs, data = data, generation = 1 })
T.eq(#run.errors, 0, "no-mod load remains clean")
T.eq(run.loader.datasetViews, nil, "no-mod load does not allocate dataset service")
T.eq(reads, 0, "no-mod load performs no imported dataset reads")
T.eq(data.pokemon.ACTIVE.nested.n, 1, "no-mod data remains unchanged")
run.release()
T.finish("dataset_views_no_mod_parity")
@@ -0,0 +1,56 @@
-- Optional dataset absence is a handled API result, while a previously valid
-- view disappearing and malformed cache content remain actionable warnings.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Fixture = require("tests.modkit.dataset_view_fixture")
local DatasetViews = require("src.mods.DatasetViews")
local GameVersion = require("src.core.GameVersion")
local Logger = require("src.core.Logger")
local previousWarn = Logger.warn
local warnings = {}
Logger.warn = function(fmt, ...)
warnings[#warnings + 1] = select("#", ...) > 0
and string.format(fmt, ...) or fmt
end
local ok, err = xpcall(function()
local unavailable = DatasetViews.new(T.sdk.memfs({}))
local unknown, unknownReason = unavailable:open("missing-version")
T.eq(unknown, nil, "unknown version has no view")
T.eq(unknownReason, "unknown_version", "unknown version returns its reason")
local absent, absentReason = unavailable:open("gold")
T.eq(absent, nil, "missing optional Gold dataset has no view")
T.eq(absentReason, "not_imported", "missing optional Gold returns its reason")
T.eq(#warnings, 0, "unknown and initially absent datasets do not warn")
local files = {}
Fixture.cache(files, "gold")
local service = DatasetViews.new(T.sdk.memfs(files))
local view = assert(service:open("gold"))
files[GameVersion.cachePrefix("gold") .. "rom-cache.complete"] = nil
T.eq(view.assets:path("assets/generated/missing.png"), nil,
"a stale view closes when its imported dataset disappears")
T.eq(#warnings, 1, "a previously valid view disappearing still warns")
T.check(warnings[1]:find("dataset gold unavailable", 1, true) ~= nil,
"the stale-view warning identifies the unavailable dataset")
warnings = {}
local malformedFiles = {}
Fixture.cache(malformedFiles, "gold", { pokemon = "not generated data" })
local malformed = assert(DatasetViews.new(T.sdk.memfs(malformedFiles)):open("gold"))
T.eq(#warnings, 0, "lazy open does not warn before malformed data is read")
T.eq(malformed.content.pokemon:get("FIXMON"), nil,
"malformed generated data fails closed")
T.eq(#warnings, 1, "malformed generated data still warns")
T.check(warnings[1]:find("dataset gold cache rejected", 1, true) ~= nil,
"the malformed-cache warning identifies cache rejection")
T.check(warnings[1]:find("pokemon:", 1, true) ~= nil,
"the malformed-cache warning keeps actionable module detail")
end, debug.traceback)
Logger.warn = previousWarn
if not ok then error(err, 0) end
T.finish("dataset_views_warning_policy")
+125
View File
@@ -0,0 +1,125 @@
-- Extractor metadata hidden from record id spaces remains engine-owned: mods
-- cannot claim, update, or remove it through the public content registries.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Mon = require("src.battle.gen2.Mon")
local function goldData()
local data = T.fixtures.fresh()
data.pokemon.growthRates = {
GROWTH_MEDIUM_FAST = {
numerator = 1, denominator = 1, squared = 0, linear = 0, constant = 0,
},
}
data.pokemon.tmhmMoves = { "FIX_TACKLE", "FIX_CUT" }
data.moves.generation, data.moves.source = 2, "ROM:Moves"
data.items.generation, data.items.source = 2, "ROM:Items"
data.items.pockets = { "ITEM", "BALL", "KEY_ITEM", "TM_HM" }
return data
end
local function assertMetadata(data, refs, label)
T.eq(data.pokemon.growthRates, refs.growthRates,
label .. ": growth-rate table identity is preserved")
T.eq(data.pokemon.tmhmMoves, refs.tmhmMoves,
label .. ": TM/HM order identity is preserved")
T.eq(data.moves.generation, 2, label .. ": move generation is preserved")
T.eq(data.moves.source, "ROM:Moves", label .. ": move source is preserved")
T.eq(data.items.generation, 2, label .. ": item generation is preserved")
T.eq(data.items.source, "ROM:Items", label .. ": item source is preserved")
T.eq(data.items.pockets, refs.pockets,
label .. ": item pocket order identity is preserved")
T.eq(Mon.experienceForLevel(
Mon.growthFor(data, "GROWTH_MEDIUM_FAST"), 10), 1000,
label .. ": active Gold growth behavior is preserved")
end
-- A no-mod load characterizes the unchanged active-boot behavior.
do
local data = goldData()
local refs = {
growthRates = data.pokemon.growthRates,
tmhmMoves = data.pokemon.tmhmMoves,
pockets = data.items.pockets,
}
local run = T.sdk.loadNone({ data = data, generation = 2 })
T.eq(#run.errors, 0, "no-mod Gold load remains clean")
assertMetadata(run.data, refs, "no-mod Gold")
run.release()
end
local files, paths = {}, {}
local attempts = {
register_growth_rates = [[
local mod = ...
mod.content.pokemon:register("growthRates", {
id = "growthRates", name = "CLAIMED", dex = 999,
types = {},
baseStats = { hp = 1, attack = 1, defense = 1, speed = 1,
specialAttack = 1, specialDefense = 1 },
catchRate = 1, baseExp = 1, growthRate = "GROWTH_MEDIUM_FAST",
levelMoves = {}, evolutions = {},
spriteFront = "assets/generated/battle/front/claimed.png",
spriteBack = "assets/generated/battle/back/claimed.png", picSize = 5,
})
]],
replace_growth_rates = [[
local mod = ...
mod.content.pokemon:register("growthRates", {
id = "growthRates", name = "REPLACED", dex = 999,
types = {},
baseStats = { hp = 1, attack = 1, defense = 1, speed = 1,
specialAttack = 1, specialDefense = 1 },
catchRate = 1, baseExp = 1, growthRate = "GROWTH_MEDIUM_FAST",
levelMoves = {}, evolutions = {},
spriteFront = "assets/generated/battle/front/replaced.png",
spriteBack = "assets/generated/battle/back/replaced.png", picSize = 5,
}, { replace = true })
]],
override_move_generation = [[
local mod = ...
mod.content.moves:override("generation", {
id = "generation", name = "CLAIMED", type = "NORMAL",
power = 1, accuracy = 100, pp = 1, effect = "NO_ADDITIONAL_EFFECT",
})
]],
patch_item_pockets = [[
local mod = ...
mod.content.items:patch("pockets", { price = 999 })
]],
remove_tmhm_moves = [[
local mod = ...
mod.content.pokemon:remove("tmhmMoves")
]],
}
for id, body in pairs(attempts) do
files["mods/" .. id .. "/manifest.json"] = ([[{
"id": %q, "name": %q, "version": "1.0.0", "entry": "main.lua",
"api": 2, "gen2compat": true
}]]):format(id, id)
files["mods/" .. id .. "/main.lua"] = body
paths[#paths + 1] = "mods/" .. id
end
table.sort(paths)
local data = goldData()
local refs = {
growthRates = data.pokemon.growthRates,
tmhmMoves = data.pokemon.tmhmMoves,
pockets = data.items.pockets,
}
local run = T.sdk.loadMods(paths, {
fs = T.sdk.memfs(files), data = data, generation = 2,
})
for id in pairs(attempts) do
local mod = run.loader.mods[id]
T.eq(mod and mod.state, "failed", id .. " is rejected")
T.check(mod and tostring(mod.failure):match("reserved") ~= nil,
id .. " reports the reserved metadata boundary")
end
assertMetadata(run.data, refs, "rejected metadata writes")
run.release()
T.finish("gen2_reserved_metadata_ids")
@@ -0,0 +1,37 @@
-- Restricted generated-data grammar and resource limits.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local SaveSerializer = require("src.core.SaveSerializer")
local limits = {
allowArray = true, allowComments = true,
maxBytes = 256, maxDepth = 4, maxNodes = 7,
maxStringBytes = 8, maxTableEntries = 5, rootName = "generated data",
}
local value, err = SaveSerializer.decode(
'-- Generated by tools/build_data.py. DO NOT EDIT.\n'
.. 'return { rows = { "A", "B" }, [3] = true }\n', limits)
T.same(value, { rows = { "A", "B" }, [3] = true },
"LuaWriter array and keyed-table grammar decodes as data")
T.eq(err, nil, "valid generated data has no error")
local invalid = {
{ "return function() end", "function literal" },
{ "owned = true; return {}", "global assignment" },
{ "return {}; print('x')", "trailing executable syntax" },
{ string.char(27) .. "Lua", "binary chunk" },
{ "return " .. string.rep(" ", 260) .. "{}", "source-size limit" },
{ 'return { a = { b = { c = { d = { e = {} } } } } }', "depth limit" },
{ 'return { a = "123456789" }', "string limit" },
{ 'return { 1, 2, 3, 4, 5, 6 }', "table-entry limit" },
{ 'return { a = 1, b = 2, c = 3, d = 4, e = { f = 6, g = 7 } }',
"node limit" },
{ "return 7", "non-table root" },
}
for _, row in ipairs(invalid) do
local got = SaveSerializer.decode(row[1], limits)
T.eq(got, nil, row[2] .. " fails closed")
end
T.finish("generated_data_decoder")
+394
View File
@@ -0,0 +1,394 @@
-- 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", "crystal" }) 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", "crystal" }) 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.metadataHidden = not gold.content.pokemon:has("growthRates")
and not gold.content.pokemon:has("tmhmMoves")
and not gold.content.moves:has("generation")
and not gold.content.moves:has("source")
and not gold.content.items:has("generation")
and not gold.content.items:has("source")
and not gold.content.items:has("pockets")
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("missing-version")
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", "crystal" }) do
T.eq(out[version].reason, nil, version .. " opens")
T.eq(out[version].generation,
GameVersion.generation(version),
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")
T.eq(out.metadataHidden, true,
"Gold extractor metadata stays outside record registry id spaces")
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,
types = {}, catchRate = 45, baseExp = 64, growthRate = "MEDIUM_FAST",
baseStats = { hp = 45, attack = 49, defense = 49, speed = 45,
special = 65 },
level1Moves = {}, tmhm = {}, learnset = {}, evolutions = {},
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
-- open and the first facade read each recheck readiness; remove the cache
-- for the next open, then make the following open the reimport.
if transitionMarkers == 3 then return nil end
end
local body = transitionRead(path)
if transitionMarkers >= 4
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", "crystal" }) do
local view, reason = mod.datasets:open(version)
local value = view and view.content.pokemon:get("BAD")
local reopened, reopenedReason = mod.datasets:open(version)
out[version] = {
first = view ~= nil, firstReason = reason, value = value,
reopened = reopened ~= nil, reason = reopenedReason,
}
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]], {
first = true, reopened = false, reason = "invalid_cache",
}, row[1] .. " hostile generated source fails closed on first root access")
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()
-- A syntactically valid semantic root with a primitive record must fail closed
-- whichever public read verb encounters it first.
local malformedFiles = {}
for _, version in ipairs({ "red", "blue", "yellow" }) do
Fixture.cache(malformedFiles, version, {
pokemon = "return { FIXMON = 7 }",
})
end
local malformedMod = Fixture.addMod(malformedFiles, "malformed_probe", [[
local mod = ...
local out = {}
local getView = assert(mod.datasets:open("red"))
out.get = getView.content.pokemon:get("FIXMON")
local getAgain, getReason = mod.datasets:open("red")
out.getAgain = { getAgain ~= nil, getReason }
local hasView = assert(mod.datasets:open("blue"))
out.has = hasView.content.pokemon:has("FIXMON")
local hasAgain, hasReason = mod.datasets:open("blue")
out.hasAgain = { hasAgain ~= nil, hasReason }
local eachView = assert(mod.datasets:open("yellow"))
for id, value in eachView.content.pokemon:each() do
out.each = { id, value }
break
end
local eachAgain, eachReason = mod.datasets:open("yellow")
out.eachAgain = { eachAgain ~= nil, eachReason }
mod.exports.result = out
]])
local malformedRun = T.sdk.loadMods({ malformedMod }, {
fs = T.sdk.memfs(malformedFiles), data = { pokemon = {} }, generation = 1,
})
T.eq(#malformedRun.errors, 0, "malformed-record probe stays sandboxed")
local malformedOut = malformedRun.loader.exports.malformed_probe.result
T.eq(malformedOut.get, nil, "get never exposes a malformed semantic record")
T.eq(malformedOut.has, false, "has never affirms a malformed semantic record")
T.eq(malformedOut.each, nil, "each never exposes a malformed semantic record")
T.same(malformedOut.getAgain, { false, "invalid_cache" },
"get invalidates the malformed dataset")
T.same(malformedOut.hasAgain, { false, "invalid_cache" },
"has invalidates the malformed dataset")
T.same(malformedOut.eachAgain, { false, "invalid_cache" },
"each invalidates the malformed dataset")
malformedRun.release()
-- The same Gold view stays independent while each Gen 1 version is the
-- actual active runtime version and cache namespace, not just a fixture label.
for _, activeVersion in ipairs({ "red", "blue", "yellow" }) do
local matrixFiles = {}
Fixture.cache(matrixFiles, "gold")
local matrixMod = Fixture.addMod(matrixFiles,
"active_" .. activeVersion .. "_gold_probe", [[
local mod = ...
local gold = assert(mod.datasets:open("gold"))
mod.exports.result = {
species = gold.content.pokemon:get("FIXMON").name,
foresight = gold.content.type_chart:get("NORMAL>GHOST").multiplier,
held = gold.content.held_items:get("LEFTOVERS").heldEffect,
}
]])
GameVersion.set(activeVersion)
CacheFs.prefix = GameVersion.cachePrefix(activeVersion)
local activeData = {
pokemon = { ACTIVE = { id = "ACTIVE_" .. activeVersion,
nested = { version = activeVersion } } },
}
local matrixRun = T.sdk.loadMods({ matrixMod }, {
fs = T.sdk.memfs(matrixFiles), data = activeData, generation = 1,
})
T.eq(#matrixRun.errors, 0, activeVersion .. "-active Gold probe loads")
T.same(matrixRun.loader.exports["active_" .. activeVersion .. "_gold_probe"].result,
{ species = "FIXMON", foresight = 0, held = "HELD_LEFTOVERS" },
activeVersion .. "-active runtime sees canonical Gold semantics")
T.eq(GameVersion.get(), activeVersion,
activeVersion .. " remains the active GameVersion")
T.eq(CacheFs.prefix, GameVersion.cachePrefix(activeVersion),
activeVersion .. " remains the active cache namespace")
T.eq(activeData.pokemon.ACTIVE.nested.version, activeVersion,
activeVersion .. " active Data remains unchanged")
T.eq(activeData.pokemon.FIXMON, nil,
activeVersion .. " active Data receives no Gold record")
matrixRun.release()
end
GameVersion.set(originalVersion)
CacheFs.prefix = originalPrefix
T.finish("dataset_views")
@@ -0,0 +1,27 @@
-- A generated chunk that would loop if executed must be rejected as syntax.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Fixture = require("tests.modkit.dataset_view_fixture")
local files = {}
Fixture.cache(files, "red", {
pokemon = "while true do end; return {}",
})
local modPath = Fixture.addMod(files, "nontermination_probe", [[
local mod = ...
local view, reason = mod.datasets:open("red")
local value = view and view.content.pokemon:get("FIXMON")
local reopened, reopenedReason = mod.datasets:open("red")
mod.exports.result = {
first = view ~= nil, firstReason = reason, value = value,
reopened = reopened ~= nil, reason = reopenedReason,
}
]])
local run = T.sdk.loadMods({ modPath }, {
fs = T.sdk.memfs(files), data = { pokemon = {} }, generation = 1,
})
T.same(run.loader.exports.nontermination_probe.result,
{ first = true, reopened = false, reason = "invalid_cache" },
"generated code is never executed and invalidates on first root access")
run.release()
T.finish("dataset_views_nontermination")
+138
View File
@@ -0,0 +1,138 @@
local GameVersion = require("src.core.GameVersion")
local LuaWriter = require("src.import.LuaWriter")
local CacheContract = require("src.import.CacheContract")
local Fixture = {}
local GEN1_MODULES = {
"constants", "maps", "tilesets", "text", "text_pointers",
"trainer_headers", "font", "sprites", "pokemon", "moves", "items",
"type_chart", "trainers", "encounters", "field", "battle_anims",
}
local GEN1_OPTIONAL_MODULES = { "audio", "palettes", "icons" }
local GEN2_MODULES = {
"pokemon", "moves", "items", "type_chart", "audio", "font", "maps",
"tilesets", "text", "trainers", "encounters", "sprites", "palettes",
"icons", "battle_anims", "constants", "landmarks",
}
local CONTINUATIONS = {
"CROBAT", "BELLOSSOM", "POLITOED", "SLOWKING", "STEELIX",
"SCIZOR", "KINGDRA", "PORYGON2", "BLISSEY",
}
local MOVES = {
"IRON_TAIL", "METAL_CLAW", "STEEL_WING", "RAIN_DANCE", "SUNNY_DAY",
"SANDSTORM", "SLUDGE_BOMB", "SHADOW_BALL",
}
local function species(id, dex, generation)
local row = {
id = id, name = id, dex = dex,
types = {}, catchRate = 45, baseExp = 64, growthRate = "MEDIUM_FAST",
tmhm = {}, evolutions = {},
spriteFront = "assets/generated/battle/front/" .. id:lower() .. ".png",
spriteBack = "assets/generated/battle/back/" .. id:lower() .. ".png",
}
if generation == 2 then
row.baseStats = { hp = 45, attack = 49, defense = 49, speed = 45,
specialAttack = 65, specialDefense = 65 }
row.levelMoves = {}
row.picSize = 5
else
row.baseStats = { hp = 45, attack = 49, defense = 49, speed = 45,
special = 65 }
row.level1Moves = {}
row.learnset = {}
row.frontSize = 5
end
return row
end
local function defaults(version)
local generation = GameVersion.generation(version)
local pokemon = { FIXMON = species("FIXMON", 1, generation) }
local moves = {}
local items = {}
local typeChart = {
matchups = { { attacker = "NORMAL", defender = "ROCK", multiplier = 5 } },
types = {},
}
if generation == 2 then
pokemon.growthRates = {}
pokemon.tmhmMoves = {}
moves.generation, moves.source = 2, "fixture moves"
items.generation, items.source, items.pockets = 2, "fixture items", {}
for index, id in ipairs(CONTINUATIONS) do
pokemon[id] = species(id, 168 + index, generation)
end
for index, id in ipairs(MOVES) do
moves[id] = { id = id, name = id, index = index, type = "STEEL",
power = 50, accuracy = 100, pp = 15, effect = "NO_ADDITIONAL_EFFECT" }
end
typeChart.types.STEEL = { name = "STEEL", category = "physical", index = 9 }
typeChart.foresightMatchups = {
{ attacker = "NORMAL", defender = "GHOST", multiplier = 0 },
}
items.LEFTOVERS = { id = "LEFTOVERS", name = "LEFTOVERS", price = 0,
heldEffect = "HELD_LEFTOVERS", heldParameter = 0 }
end
return {
constants = {}, maps = {}, tilesets = {}, text = {}, text_pointers = {},
trainer_headers = {}, font = {}, sprites = {}, pokemon = pokemon,
moves = moves, items = items, type_chart = typeChart, trainers = {},
encounters = {}, field = { oakSpeech = {} }, battle_anims = {},
audio = {}, palettes = {}, icons = {}, landmarks = {},
}
end
function Fixture.cache(files, version, overrides)
local prefix = GameVersion.cachePrefix(version)
local generation = GameVersion.generation(version)
local values = defaults(version)
for name, value in pairs(overrides or {}) do values[name] = value end
files[prefix .. "rom-cache.complete"] =
CacheContract.markerFor(version)
for _, name in ipairs(generation == 2 and GEN2_MODULES or GEN1_MODULES) do
local value = values[name]
files[prefix .. "data/generated/" .. name .. ".lua"] =
type(value) == "string" and value or LuaWriter.encode(value or {})
end
if generation == 1 then
for _, name in ipairs(GEN1_OPTIONAL_MODULES) do
files[prefix .. "data/generated/" .. name .. ".lua"] =
LuaWriter.encode(values[name] or {})
end
end
for _, path in ipairs(CacheContract.requiredFiles(version)) do
if files[prefix .. path] == nil then
files[prefix .. path] = path:match("%.lua$") and LuaWriter.encode({}) or "fixture"
end
end
if type(values.pokemon) == "table" then
for _, row in pairs(values.pokemon) do
if type(row) == "table" and row.spriteFront then
files[prefix .. row.spriteFront] = "front"
files[prefix .. row.spriteBack] = "back"
end
end
end
return files
end
function Fixture.addMod(files, id, body)
files["mods/" .. id .. "/manifest.json"] = ([[{
"id": %q, "name": %q, "version": "1.0.0", "entry": "main.lua",
"api": 2, "games": ["all"]
}]]):format(id, id)
files["mods/" .. id .. "/main.lua"] = body
return "mods/" .. id
end
Fixture.GEN1_MODULES = GEN1_MODULES
Fixture.GEN2_MODULES = GEN2_MODULES
Fixture.CONTINUATIONS = CONTINUATIONS
Fixture.MOVES = MOVES
return Fixture