mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 00:48:32 +02:00
Merge pull request #1767 from MaxTomahawk/adaptive-trainers/dataset-view-api
feat(mod-api): expose imported dataset views
This commit is contained in:
@@ -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")
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user