mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
fix(mod-api): reserve generated metadata ids
This commit is contained in:
+3
-1
@@ -228,7 +228,9 @@ and data-only record shapes as `mod.content`, but only `get`, `has`, and
|
||||
`each`. Returned records are detached copies and cannot mutate either dataset.
|
||||
Every generated base record passes the selected generation's existing public
|
||||
schema before it is returned; extractor metadata beside record maps stays out
|
||||
of the registry id space. A malformed record makes `get` return nil, `has`
|
||||
of the registry id space and is reserved against `register`, `override`,
|
||||
`patch`, and `remove` writes through the active registry. A malformed record
|
||||
makes `get` return nil, `has`
|
||||
return false, and `each` return no rows, and invalidates that dataset view.
|
||||
Records containing functions, userdata, threads, metatables, or cycles are not
|
||||
exposed. Each open call receives an independent facade, so one mod cannot
|
||||
|
||||
@@ -71,8 +71,9 @@ 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 and extractor metadata beside record maps is not exposed
|
||||
as a record id. Every generated base record is checked with that selected
|
||||
generation's existing public schema before it can cross `get`, `has`, or
|
||||
as a record id or writable through the active registry's mutation verbs. Every
|
||||
generated base record is checked with that selected generation's existing
|
||||
public schema before it can cross `get`, `has`, or
|
||||
`each`; there is no dataset-specific duplicate schema. 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
|
||||
|
||||
@@ -12,6 +12,12 @@ Registry.__index = Registry
|
||||
-- exposed to mods as mod.DELETE: a patch value that unsets a field
|
||||
Registry.DELETE = Merge.DELETE
|
||||
|
||||
local function assertWritable(self, id)
|
||||
if self.spec.reservedIds and self.spec.reservedIds[id] then
|
||||
error(("%s id is reserved engine metadata: %s"):format(self.name, id))
|
||||
end
|
||||
end
|
||||
|
||||
-- spec comes from Schemas.REGISTRIES[name]; bare Registry.new(name) keeps
|
||||
-- the v1 record behavior for standalone use in tests and tools
|
||||
function Registry.new(name, spec)
|
||||
@@ -33,6 +39,7 @@ local function append(self, id, op, value, owner)
|
||||
error(self.name .. ": content is frozen after load")
|
||||
end
|
||||
assert(type(id) == "string" and id ~= "", self.name .. " id is required")
|
||||
assertWritable(self, id)
|
||||
local list = self.ops[id]
|
||||
if not list then
|
||||
list = {}
|
||||
|
||||
+12
-7
@@ -676,9 +676,10 @@ end
|
||||
--
|
||||
-- So beside `value` / `fields` / `keys` / `keyValue` a spec may carry
|
||||
-- `gen2Value` / `gen2Fields` / `gen2Keys` / `gen2KeyValue`, and beside
|
||||
-- `semantics` / `extra` / `write` / `baseAt` / `baseIds` / `example` /
|
||||
-- `notes` the matching `gen2*`. Absent means "the Gen 1 shape is right here
|
||||
-- too", which is the common case and why most registries carry none of this.
|
||||
-- `semantics` / `extra` / `write` / `baseAt` / `baseIds` / `reservedIds` /
|
||||
-- `example` / `notes` the matching `gen2*`. Absent means "the Gen 1 shape is
|
||||
-- right here too", which is the common case and why most registries carry
|
||||
-- none of this.
|
||||
-- The registry NAME, the verbs and (wherever the id space allows it) the ids
|
||||
-- stay shared, exactly as the routing table keeps them shared.
|
||||
--
|
||||
@@ -700,6 +701,7 @@ local GEN2_SHAPE = {
|
||||
gen2KeyValue = "keyValue", gen2Extra = "extra",
|
||||
gen2Semantics = "semantics", gen2Write = "write",
|
||||
gen2BaseAt = "baseAt", gen2BaseIds = "baseIds",
|
||||
gen2ReservedIds = "reservedIds",
|
||||
gen2Example = "example", gen2Notes = "notes",
|
||||
}
|
||||
|
||||
@@ -773,14 +775,14 @@ local function recordMapExcept(...)
|
||||
if not excluded[id] then ids[#ids + 1] = id end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
end, excluded
|
||||
end
|
||||
|
||||
local pokemonGen2BaseAt, pokemonGen2BaseIds =
|
||||
local pokemonGen2BaseAt, pokemonGen2BaseIds, pokemonGen2ReservedIds =
|
||||
recordMapExcept("growthRates", "tmhmMoves")
|
||||
local movesGen2BaseAt, movesGen2BaseIds =
|
||||
local movesGen2BaseAt, movesGen2BaseIds, movesGen2ReservedIds =
|
||||
recordMapExcept("generation", "source")
|
||||
local itemsGen2BaseAt, itemsGen2BaseIds =
|
||||
local itemsGen2BaseAt, itemsGen2BaseIds, itemsGen2ReservedIds =
|
||||
recordMapExcept("generation", "source", "pockets")
|
||||
|
||||
-- ------- shared Gen 2 leaves
|
||||
@@ -806,6 +808,7 @@ local gen2PaletteRow = f.list(gen2Color)
|
||||
R.pokemon = {
|
||||
semantics = "record", target = "pokemon",
|
||||
gen2BaseAt = pokemonGen2BaseAt, gen2BaseIds = pokemonGen2BaseIds,
|
||||
gen2ReservedIds = pokemonGen2ReservedIds,
|
||||
fields = {
|
||||
id = f.str, name = f.str, dex = f.int(1),
|
||||
index = f.opt(f.int(0, 255)),
|
||||
@@ -898,6 +901,7 @@ R.pokemon = {
|
||||
R.moves = {
|
||||
semantics = "record", target = "moves",
|
||||
gen2BaseAt = movesGen2BaseAt, gen2BaseIds = movesGen2BaseIds,
|
||||
gen2ReservedIds = movesGen2ReservedIds,
|
||||
fields = {
|
||||
id = f.str, name = f.str,
|
||||
index = f.opt(f.int(0, 255)),
|
||||
@@ -923,6 +927,7 @@ R.moves = {
|
||||
R.items = {
|
||||
semantics = "record", target = "items",
|
||||
gen2BaseAt = itemsGen2BaseAt, gen2BaseIds = itemsGen2BaseIds,
|
||||
gen2ReservedIds = itemsGen2ReservedIds,
|
||||
fields = {
|
||||
id = f.str, name = f.str,
|
||||
index = f.opt(f.int(0, 255)),
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
-- 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,
|
||||
})
|
||||
]],
|
||||
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")
|
||||
Reference in New Issue
Block a user