feat(mod-api): add imported dataset views

This commit is contained in:
MaxTomahawk
2026-08-21 15:00:45 +02:00
parent e88f2ef060
commit b286e6584f
7 changed files with 661 additions and 2 deletions
+36
View File
@@ -200,6 +200,42 @@ the adapter's own coverage table:
python3 tools/modkit.py gen2check mods/my_mod python3 tools/modkit.py gen2check mods/my_mod
``` ```
## Imported version datasets
A mod can inspect semantic content from another game the player has already
imported without switching the active game or reaching into engine cache
internals:
```lua
local gold, reason = mod.datasets:open("gold")
if not gold then
-- reason is "unknown_version" or "not_imported"
return
end
local chikorita = gold.content.pokemon:get("CHIKORITA")
local normalVsGhost = gold.content.type_chart:get("NORMAL>GHOST")
local spritePath = gold.assets:path(chikorita.spriteFront)
for id, record in gold.content.pokemon:each() do
-- ids are returned in deterministic lexical order
end
```
`view.version` and `view.generation` identify the selected dataset.
`view.content` exposes the same registry names and generation-specific record
shapes as `mod.content`, but only `get`, `has`, and `each`; returned
records are detached copies and cannot mutate either dataset. Each open call
receives an independent facade, so one mod cannot replace another mod view method. Engine-provided
semantic records, such as `NORMAL>GHOST` type matchups, are included.
Only a cache carrying the current engine completion marker is opened. Missing
and stale imports both return `nil, "not_imported"`; no raw ROM bytes or
generated Lua modules are exposed. `view.assets:path(relative)` and
`view.assets:info(relative)` accept only `assets/generated/...` paths and
keep them under the selected version cache prefix. The API never changes
`mod.game`, the active `Data` table, `GameVersion`, or cache mount state.
## Editing maps in Tiled ## Editing maps in Tiled
Maps are data, not assets, so they can be authored in a real map editor and Maps are data, not assets, so they can be authored in a real map editor and
+113
View File
@@ -0,0 +1,113 @@
# RFC 0012: Read-only imported dataset views
## Status
Proposed.
## Motivation
A cross-version content mod can read only the active merged dataset through
`mod.content`. Even when the player has already imported another supported
game, a mod cannot inspect that version's semantic species, moves, items, type
chart, or generated asset namespace. The available alternatives are private:
mutating `CacheFs.prefix`, mounting another cache over the active one, loading
generated Lua directly, or asking for a second raw-ROM import. They compose
poorly with the active game, expose unstable import layout, and make safe
read-only use impossible from the sandbox.
The immediate example is an optional content pack derived from an already
verified Gold import while Red, Blue, or Yellow remains active. The capability
is generic and useful to randomizers, compatibility inspectors, dex tools, and
other cross-version content mods.
## Decision and plan extended
This implements **D-AT-004: optional Kanto+ content consumes an
active-independent semantic dataset view**. The consuming design is tracked in
the Adaptive Trainers implementation plan,
[`docs/superpowers/plans/2026-08-14-adaptive-trainers.md`](https://github.com/MaxTomahawk/gen1recomp-adaptive-trainers/blob/main/docs/superpowers/plans/2026-08-14-adaptive-trainers.md),
Task 8. The delta is a generic, additive public API and contains no trainer
pools, scaling, boss identities, version-mixing rules, or Adaptive Trainers
policy.
## Exact API delta
Every sandboxed mod receives:
```lua
local view, reason = mod.datasets:open("gold")
```
A known version with the current completed import marker returns a read-only view:
```lua
view = {
version = "gold",
generation = 2,
content = {
pokemon = {
get = function(self, id) end,
has = function(self, id) end,
each = function(self) end,
},
-- every public registry name and alias
},
assets = {
path = function(self, generatedPath) end,
info = function(self, generatedPath) end,
},
}
```
`get` returns a detached copy or nil. `has` reports semantic presence.
`each` returns ids in lexical order and detached values. The registries use
the selected version's generation routing and the engine's existing
`Schemas`, `Registry`, and `Builtins` normalization, so structured sources
such as type matchups retain the same public ids used by the active
`mod.content` facade. No register, patch, override, or remove verb is exposed. Each call returns an
independent facade over the cached internal dataset, so facade mutation cannot cross
mod boundaries.
`assets:path` returns the selected cache-prefixed virtual path.
`assets:info` returns sanitized `type` and optional `size` metadata. Both
accept only relative paths below `assets/generated/`, reject control
characters, absolute paths, backslashes, and traversal, and expose no byte
reader.
An unknown version returns `nil, "unknown_version"`. A missing or stale
completion marker returns `nil, "not_imported"`. Generated tables are parsed
under an empty environment and cached per version; the API never exposes raw
ROM bytes, generated source, host paths, or a cache mount.
## Migration and compatibility
Existing mods change nothing. `mod.datasets` is additive and requires no
permission. The service is allocated lazily on the first explicit
`mod.datasets:open` call. A boot with no mods, or with mods that do not call
it, performs no cross-version cache reads.
Opening a view does not change `GameVersion`, `CacheFs.prefix`, the active
`Data` table, PhysFS mounts, save state, or the selected game's behavior.
Red, Blue, Yellow, Gold, and Silver keep their existing active data paths.
The completion-marker format moves to the pure `CacheFormat` helper shared by
the importer and dataset service. The literal marker and import readiness
behavior are unchanged.
## Verification
- `tests/modkit/cases/dataset_views.lua` loads a sandboxed fixture mod through
the public API and covers Red, Blue, Yellow, and Gold independently.
- The test proves semantic registry normalization, deterministic iteration,
detached records, read-only facades, cross-mod facade isolation,
version-prefixed generated assets,
traversal rejection, stable failure reasons, and stale-marker rejection.
- The same test proves that cross-version reads do not change the active data,
active game, or cache prefix, and that a no-mod boot performs zero
cross-version cache reads.
- The existing importer and engine/modkit suites prove unchanged marker
readiness and no-mod behavior.
## Deprecation etiquette
Nothing is removed, renamed, superseded, or deprecated.
+22
View File
@@ -0,0 +1,22 @@
-- Current completion-marker contract for generated ROM caches.
-- Pure and version-aware so read-only consumers can verify an import without
-- loading the launcher or changing CacheFs.prefix.
local GameVersion = require("src.core.GameVersion")
local CacheFormat = {}
CacheFormat.PREFIX = "rom-cache-v10:"
function CacheFormat.markerFor(version)
local info = GameVersion.VERSIONS[version]
if not info then return nil end
return CacheFormat.PREFIX .. info.sha1
end
function CacheFormat.matches(version, marker)
local expected = CacheFormat.markerFor(version)
return expected ~= nil and marker == expected
end
return CacheFormat
+2 -2
View File
@@ -1,4 +1,5 @@
local GameVersion = require("src.core.GameVersion") local GameVersion = require("src.core.GameVersion")
local CacheFormat = require("src.import.CacheFormat")
local GamepadMap = require("src.core.GamepadMap") local GamepadMap = require("src.core.GamepadMap")
local Logger = require("src.core.Logger") local Logger = require("src.core.Logger")
local Strings = require("src.core.Strings") local Strings = require("src.core.Strings")
@@ -41,7 +42,6 @@ end
-- the stage, which is what the Yellow markers below already do for #439/#557. -- the stage, which is what the Yellow markers below already do for #439/#557.
-- Reach for a bump when the change spans versions or has no single file to -- Reach for a bump when the change spans versions or has no single file to
-- point at. -- point at.
local CACHE_FORMAT = "rom-cache-v10:"
-- The completion marker is written under each version's cache prefix -- The completion marker is written under each version's cache prefix
-- (red/rom-cache.complete, blue/rom-cache.complete, ...). -- (red/rom-cache.complete, blue/rom-cache.complete, ...).
local MARKER_PATH = "rom-cache.complete" local MARKER_PATH = "rom-cache.complete"
@@ -49,7 +49,7 @@ local MARKER_PATH = "rom-cache.complete"
-- The marker a finished import writes for a version: the generation tag plus -- The marker a finished import writes for a version: the generation tag plus
-- that version's ROM hash, so both a format bump and a swapped ROM invalidate. -- that version's ROM hash, so both a format bump and a swapped ROM invalidate.
local function markerFor(version) local function markerFor(version)
return CACHE_FORMAT .. GameVersion.info(version).sha1 return CacheFormat.markerFor(version)
end end
local COMMUNITY_URL = "https://bois.icu" local COMMUNITY_URL = "https://bois.icu"
local TRUST_WARNING = "if you did not get this from bryanthaboi's github " .. local TRUST_WARNING = "if you did not get this from bryanthaboi's github " ..
+228
View File
@@ -0,0 +1,228 @@
-- Read-only views over verified, version-scoped generated datasets.
--
-- A view reads semantic registry records from another imported version without
-- changing GameVersion, CacheFs.prefix, the active Data table, or any PhysFS
-- mount. Records are detached copies and generated assets stay under the
-- selected version's virtual cache prefix.
local CacheFormat = require("src.import.CacheFormat")
local GameVersion = require("src.core.GameVersion")
local Builtins = require("src.mods.Builtins")
local Merge = require("src.mods.Merge")
local Registry = require("src.mods.Registry")
local Schemas = require("src.mods.Schemas")
local DatasetViews = {}
DatasetViews.__index = DatasetViews
local GEN1_MODULES = {
constants = "constants", maps = "maps", tilesets = "tilesets",
text = "text", text_pointers = "text_pointers",
trainer_headers = "trainer_headers", font = "font", sprites = "sprites",
pokemon = "pokemon", moves = "moves", items = "items",
type_chart = "type_chart", trainers = "trainers",
encounters = "encounters", field = "field",
battle_anims = "battle_anims", audio = "audio",
palettes = "palettes", icons = "icons",
}
local GEN2_MODULES = {
pokemon = "pokemon", moves = "moves", items = "items",
type_chart = "type_chart", audio = "audio", font = "font",
gen2Maps = "maps", gen2Tilesets = "tilesets", gen2Text = "text",
gen2Trainers = "trainers", gen2Encounters = "encounters",
gen2Sprites = "sprites", gen2Palettes = "palettes",
gen2Icons = "icons", gen2BattleAnims = "battle_anims",
gen2Constants = "constants", gen2Landmarks = "landmarks",
}
local function compileTable(source, chunkname)
if type(source) ~= "string" or source:byte(1) == 27 then return nil end
local env = {}
local chunk, err
if setfenv then
chunk, err = loadstring(source, chunkname)
if chunk then setfenv(chunk, env) end
else
chunk, err = load(source, chunkname, "t", env)
end
if not chunk then return nil, err end
local ok, value = pcall(chunk)
if not ok or type(value) ~= "table" then
return nil, ok and "generated module did not return a table" or value
end
return value
end
local function resolvePath(root, suffix)
local node = root
for key in suffix:gmatch("[^.]+") do
if type(node) ~= "table" then return nil end
node = node[key]
end
return node
end
local function assetRelative(path)
if type(path) ~= "string" or path == "" then
error("dataset asset path is required", 3)
end
if path:find("\\", 1, true) or path:sub(1, 1) == "/"
or path:find("[%z\1-\31]") then
error("dataset asset path must be a generated relative path", 3)
end
if path:sub(1, 17) ~= "assets/generated/" then
error("dataset assets are limited to assets/generated/", 3)
end
for segment in path:gmatch("[^/]+") do
if segment == "." or segment == ".." then
error("dataset asset path may not traverse directories", 3)
end
end
return path
end
function DatasetViews.new(fs)
assert(fs and fs.read, "DatasetViews.new requires a readable filesystem")
return setmetatable({ fs = fs, datasets = {} }, DatasetViews)
end
function DatasetViews:_module(view, root)
local moduleName = view.modules[root]
if not moduleName then return nil end
local cached = view.moduleCache[moduleName]
if cached ~= nil then return cached or nil end
local path = view.prefix .. "data/generated/" .. moduleName .. ".lua"
local source = self.fs.read(path)
local value = compileTable(source, "@" .. path)
view.moduleCache[moduleName] = value or false
return value
end
function DatasetViews:_data(view)
if view.data then return view.data end
local data = {}
for root in pairs(view.modules) do
local value = self:_module(view, root)
if value ~= nil then data[root] = value end
end
view.data = data
return data
end
local function registryBase(data, target)
return function()
return resolvePath(data, target)
end
end
function DatasetViews:_registries(view)
if view.registries then return view.registries end
local data = self:_data(view)
local registries = {}
for name, catalogSpec in pairs(Schemas.REGISTRIES) do
local spec = Schemas.shapeFor(name, catalogSpec, view.generation)
local registry = Registry.new(name, spec)
if spec.target then registry.base = registryBase(data, spec.target) end
registries[name] = registry
end
Builtins.install(registries, data, view.generation)
for _, registry in pairs(registries) do registry:freeze() end
view.registries = registries
return registries
end
function DatasetViews:_registry(view, name)
local registry = self:_registries(view)[name]
local function valueAt(id)
local value = registry and registry:get(id)
if value == nil then return nil end
return Merge.deepCopy(value)
end
return {
get = function(_, id)
if type(id) ~= "string" or id == "" then return nil end
return valueAt(id)
end,
has = function(_, id)
return valueAt(id) ~= nil
end,
each = function()
local ids = {}
if registry then
for id in registry:each() do
if type(id) == "string" then ids[#ids + 1] = id end
end
end
table.sort(ids)
local index = 0
return function()
index = index + 1
local id = ids[index]
if id == nil then return nil end
return id, valueAt(id)
end
end,
}
end
function DatasetViews:_assets(view)
local service = self
local assets = {}
function assets:path(path)
return view.prefix .. assetRelative(path)
end
function assets:info(path)
local full = self:path(path)
local info = service.fs.getInfo and service.fs.getInfo(full, "file")
if not info then return nil end
local out = { type = info.type }
if info.size ~= nil then out.size = info.size end
return out
end
return assets
end
function DatasetViews:open(version)
if type(version) ~= "string" or not GameVersion.VERSIONS[version] then
return nil, "unknown_version"
end
local internal = self.datasets[version]
if not internal then
local prefix = GameVersion.cachePrefix(version)
local marker = self.fs.read(prefix .. "rom-cache.complete")
if not CacheFormat.matches(version, marker) then
return nil, "not_imported"
end
local generation = GameVersion.generation(version)
internal = {
version = version, generation = generation, prefix = prefix,
modules = generation == 2 and GEN2_MODULES or GEN1_MODULES,
moduleCache = {},
}
internal.registries = self:_registries(internal)
self.datasets[version] = internal
end
local view = {
version = version,
generation = internal.generation,
content = {},
}
for name in pairs(Schemas.REGISTRIES) do
view.content[name] = self:_registry(internal, name)
end
for alias, canonical in pairs(Schemas.ALIASES) do
view.content[alias] = view.content[canonical]
end
view.assets = self:_assets(internal)
return view
end
return DatasetViews
+9
View File
@@ -983,6 +983,15 @@ function Loader:_api(mod)
-- a deep copy: what a mod does to its own view never reaches the loader -- a deep copy: what a mod does to its own view never reaches the loader
manifest = Merge.deepCopy(mod.manifest), manifest = Merge.deepCopy(mod.manifest),
content = {}, content = {},
datasets = {
open = function(_, version)
if not loader.datasetViews then
local DatasetViews = engineRequire("src.mods.DatasetViews")
loader.datasetViews = DatasetViews.new(loader.fs)
end
return loader.datasetViews:open(version)
end,
},
exports = {}, exports = {},
DELETE = Registry.DELETE, DELETE = Registry.DELETE,
events = { events = {
+251
View File
@@ -0,0 +1,251 @@
-- Cross-version imported datasets through the public sandboxed mod API.
-- The view is semantic and read-only: registry-shaped records in, detached
-- copies out, with generated assets kept inside the selected version namespace.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local GameVersion = require("src.core.GameVersion")
local CacheFs = require("src.import.CacheFs")
local CacheFormat = require("src.import.CacheFormat")
local function serialized(value)
local function encode(v)
if type(v) == "string" then return string.format("%q", v) end
if type(v) == "number" or type(v) == "boolean" then return tostring(v) end
local keys = {}
for key in pairs(v) do keys[#keys + 1] = key end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
local out = { "{" }
for _, key in ipairs(keys) do
out[#out + 1] = "[" .. encode(key) .. "]=" .. encode(v[key]) .. ","
end
out[#out + 1] = "}"
return table.concat(out)
end
return "return " .. encode(value)
end
local files = {
["mods/dataset_probe/manifest.json"] = [[{
"id": "dataset_probe",
"name": "Dataset Probe",
"version": "1.0.0",
"entry": "main.lua",
"api": 2,
"games": ["all"]
}]],
["mods/dataset_probe/main.lua"] = [[
local mod = ...
local out = {}
for _, version in ipairs({ "red", "blue", "yellow", "gold" }) do
local view, reason = mod.datasets:open(version)
if not view then
out[version] = { reason = reason }
else
local registry = view.content.pokemon
local first = registry:get("FIXMON")
first.name = "MUTATED"
local ids = {}
for id in registry:each() do ids[#ids + 1] = id end
local immune = view.content.type_chart:get("NORMAL>GHOST")
local steel = view.content.type_chart:get("STEEL")
out[version] = {
version = view.version,
generation = view.generation,
name = registry:get("FIXMON").name,
has = registry:has("FIXMON"),
ids = ids,
writable = registry.register ~= nil or registry.patch ~= nil
or registry.override ~= nil or registry.remove ~= nil,
sprite = view.assets:path(registry:get("FIXMON").spriteFront),
spriteInfo = view.assets:info(registry:get("FIXMON").spriteFront),
normalGhost = immune and immune.multiplier,
steelCategory = steel and steel.category,
}
end
end
local missing, missingReason = mod.datasets:open("silver")
local unknown, unknownReason = mod.datasets:open("crystal")
out.silver = { present = missing ~= nil, reason = missingReason }
out.crystal = { present = unknown ~= nil, reason = unknownReason }
local gold = mod.datasets:open("gold")
out.assetEscape = pcall(function() gold.assets:path("../save.lua") end)
out.rawAssetRead = gold.assets.read ~= nil
mod.exports.result = out
]],
}
local isolationMods = {
["mods/dataset_mutator/manifest.json"] = [[{
"id": "dataset_mutator",
"name": "Dataset Mutator",
"version": "1.0.0",
"entry": "main.lua",
"api": 2,
"games": ["all"]
}]],
["mods/dataset_mutator/main.lua"] = [[
local mod = ...
local view = assert(mod.datasets:open("red"))
view.content.pokemon.get = function() return { name = "POISONED" } end
]],
["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"]
}]],
["mods/dataset_observer/main.lua"] = [[
local mod = ...
local view = assert(mod.datasets:open("red"))
mod.exports.name = view.content.pokemon:get("FIXMON").name
]],
}
local labels = {
red = "RED SOURCE", blue = "BLUE SOURCE",
yellow = "YELLOW SOURCE", gold = "GOLD SOURCE",
}
for _, version in ipairs({ "red", "blue", "yellow", "gold" }) do
local prefix = GameVersion.cachePrefix(version)
files[prefix .. "rom-cache.complete"] =
"rom-cache-v10:" .. GameVersion.info(version).sha1
files[prefix .. "data/generated/pokemon.lua"] = serialized({
FIXMON = {
id = "FIXMON", name = labels[version], dex = version == "gold" and 252 or 152,
spriteFront = "assets/generated/battle/front/fixmon.png",
},
ALPHA = { id = "ALPHA", name = "ALPHA", dex = 1,
spriteFront = "assets/generated/battle/front/alpha.png" },
})
files[prefix .. "data/generated/type_chart.lua"] = serialized({
matchups = { { attacker = "NORMAL", defender = "GHOST", multiplier = 0 } },
types = version == "gold" and {
STEEL = { name = "STEEL", category = "physical", index = 9 },
} or {},
})
files[prefix .. "assets/generated/battle/front/fixmon.png"] = "png-" .. version
end
-- An otherwise plausible Silver tree has no completion marker. A view must
-- not execute or expose it.
files[GameVersion.cachePrefix("silver") .. "data/generated/pokemon.lua"] =
serialized({ FIXMON = { id = "FIXMON", name = "UNVERIFIED" } })
T.eq(CacheFormat.markerFor("red"),
"rom-cache-v10:" .. GameVersion.info("red").sha1,
"cache marker follows the importer format and version hash")
T.eq(CacheFormat.matches("silver", "rom-cache-v9:stale"), false,
"stale cache formats are rejected")
T.eq(CacheFormat.markerFor("crystal"), nil,
"unknown versions have no valid cache marker")
local originalVersion = GameVersion.get()
local originalPrefix = CacheFs.prefix
GameVersion.set("red")
local function isVersionCachePath(path)
for _, version in ipairs(GameVersion.ORDER) do
local prefix = GameVersion.cachePrefix(version)
if path:sub(1, #prefix) == prefix then return true end
end
return false
end
-- No mod means the new service is completely cold. Count only cross-version
-- cache reads so the loader's ordinary discovery/state reads do not matter.
local vanillaFiles = {}
for path, body in pairs(files) do
if not path:match("^mods/") then
vanillaFiles[path] = body
end
end
local vanillaFs = T.sdk.memfs(vanillaFiles)
local vanillaReads = 0
local vanillaRead = vanillaFs.read
vanillaFs.read = function(path)
if isVersionCachePath(path) then
vanillaReads = vanillaReads + 1
end
return vanillaRead(path)
end
local vanillaData = { pokemon = { ACTIVE = { id = "ACTIVE", name = "ACTIVE" } } }
local vanilla = T.sdk.loadNone({ fs = vanillaFs, data = vanillaData })
T.eq(#vanilla.errors, 0, "no-mod loader remains clean")
T.eq(vanillaReads, 0, "no mod performs no imported-dataset reads")
T.eq(vanillaData.pokemon.ACTIVE.name, "ACTIVE",
"no mod leaves the active dataset unchanged")
vanilla.release()
local activeData = { pokemon = { ACTIVE = { id = "ACTIVE", name = "ACTIVE" } } }
local run = T.sdk.loadMods({ "mods/dataset_probe" }, {
fs = T.sdk.memfs(files), data = activeData, generation = 1,
})
T.eq(#run.errors, 0,
"sandboxed dataset probe loads clean: " .. tostring(run.errors[1]))
local out = run.loader.exports.dataset_probe
and run.loader.exports.dataset_probe.result or {}
for _, version in ipairs({ "red", "blue", "yellow", "gold" }) do
local got = out[version] or {}
T.eq(got.version, version, version .. " view reports its selected version")
T.eq(got.generation, version == "gold" and 2 or 1,
version .. " view reports the selected generation")
T.eq(got.name, labels[version],
version .. " returns its own semantic species record")
T.eq(got.has, true, version .. " registry has() reads the imported view")
T.same(got.ids, { "ALPHA", "FIXMON" },
version .. " registry each() is deterministic")
T.eq(got.writable, false, version .. " registry exposes no write verbs")
T.eq(got.sprite,
GameVersion.cachePrefix(version) .. "assets/generated/battle/front/fixmon.png",
version .. " asset path stays in its cache namespace")
T.same(got.spriteInfo, { type = "file" },
version .. " asset info is sanitized metadata")
T.eq(got.normalGhost, 0,
version .. " exposes structured matchup ids semantically")
end
T.eq(out.gold and out.gold.steelCategory, "physical",
"Gold exposes imported Steel as a semantic type record")
T.same(out.silver, { present = false, reason = "not_imported" },
"an unverified cache is unavailable")
T.same(out.crystal, { present = false, reason = "unknown_version" },
"an unknown version fails with a stable reason")
T.eq(out.assetEscape, false, "dataset asset paths reject traversal")
T.eq(out.rawAssetRead, false, "dataset assets expose no raw byte reader")
T.eq(activeData.pokemon.ACTIVE.name, "ACTIVE",
"cross-version reads do not mutate the active game dataset")
T.eq(activeData.pokemon.FIXMON, nil,
"cross-version records are not merged into active game data")
T.eq(GameVersion.get(), "red", "dataset reads do not switch the active game")
T.eq(CacheFs.prefix, originalPrefix, "dataset reads do not change cache prefix state")
run.release()
local isolationFiles = {}
for path, body in pairs(files) do
if not path:match("^mods/") then
isolationFiles[path] = body
end
end
for path, body in pairs(isolationMods) do isolationFiles[path] = body end
local isolationRun = T.sdk.loadMods({
"mods/dataset_mutator", "mods/dataset_observer",
}, { fs = T.sdk.memfs(isolationFiles),
data = { pokemon = {} }, generation = 1 })
T.eq(#isolationRun.errors, 0, "cross-mod isolation fixture loads cleanly")
T.eq(isolationRun.loader.exports.dataset_observer
and isolationRun.loader.exports.dataset_observer.name,
labels.red, "one mod cannot mutate another mod dataset facade")
isolationRun.release()
GameVersion.set(originalVersion)
CacheFs.prefix = originalPrefix
T.finish("dataset_views")