mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 15:51:17 +02:00
275 lines
9.5 KiB
Lua
275 lines
9.5 KiB
Lua
-- Ordered, namespaced content registries used by the native mod API.
|
|
-- Each registry stores an op log per id (register/override/patch/remove)
|
|
-- folded over the base record at read/merge time, so patches stack across
|
|
-- mods in load order and undoing a failed mod is just dropping its ops.
|
|
-- The loader merges effective values into the live data only after every
|
|
-- enabled mod has initialized successfully.
|
|
local Merge = require("src.mods.Merge")
|
|
|
|
local Registry = {}
|
|
Registry.__index = Registry
|
|
|
|
-- exposed to mods as mod.DELETE: a patch value that unsets a field
|
|
Registry.DELETE = Merge.DELETE
|
|
|
|
-- 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)
|
|
return setmetatable({
|
|
name = name,
|
|
spec = spec or { semantics = "record" },
|
|
ops = {}, -- id -> ordered { op, value, owner }
|
|
owners = {}, -- id -> last-writing owner (provenance for errors)
|
|
order = {}, -- ids in first-touch order, for array-rebuilding targets
|
|
seen = {}, -- id -> true, keeps order free of duplicates
|
|
cache = {}, -- id -> { value } memoized fold
|
|
base = nil, -- installed by the loader: fn() -> base table or nil
|
|
frozen = false,
|
|
}, Registry)
|
|
end
|
|
|
|
local function append(self, id, op, value, owner)
|
|
if self.frozen then
|
|
error(self.name .. ": content is frozen after load")
|
|
end
|
|
assert(type(id) == "string" and id ~= "", self.name .. " id is required")
|
|
local list = self.ops[id]
|
|
if not list then
|
|
list = {}
|
|
self.ops[id] = list
|
|
end
|
|
-- a rolled-back id keeps its slot: order is registration history, not a
|
|
-- live key set, so a resurrected id stays where it first appeared
|
|
if not self.seen[id] then
|
|
self.seen[id] = true
|
|
self.order[#self.order + 1] = id
|
|
end
|
|
list[#list + 1] = { op = op, value = value, owner = owner }
|
|
self.owners[id] = owner
|
|
self.cache[id] = nil
|
|
return value
|
|
end
|
|
|
|
-- spec.baseAt lets a registry whose ids do not map one-to-one onto target
|
|
-- keys (battle_anims routes by id prefix) resolve its own pristine value
|
|
local function baseValue(self, id)
|
|
local base = self.base and self.base()
|
|
if base == nil then return nil end
|
|
if self.spec.baseAt then return self.spec.baseAt(base, id) end
|
|
return base[id]
|
|
end
|
|
|
|
-- effective value = base plus the op list; a tombstone folds to nil and a
|
|
-- later register may resurrect the id
|
|
local function fold(self, value, opList)
|
|
local deep = self.spec.semantics == "deep"
|
|
for _, entry in ipairs(opList or {}) do
|
|
local op = entry.op
|
|
-- a payload that IS the sentinel folds as a delete, never a value;
|
|
-- without this the bare DELETE table would leak into Data as a record
|
|
if entry.value == Merge.DELETE then
|
|
value = nil
|
|
elseif op == "override" or (op == "register" and not deep) then
|
|
value = entry.value
|
|
elseif op == "register" or op == "patch" then
|
|
-- deep registries treat register and patch alike; scalar payloads
|
|
-- (a lone top-level value) replace outright
|
|
if type(entry.value) == "table" then
|
|
value = Merge.deepMerge(Merge.deepCopy(value == nil and {} or value),
|
|
entry.value, self.spec.semantics)
|
|
else
|
|
value = entry.value
|
|
end
|
|
elseif op == "remove" then
|
|
value = nil
|
|
end
|
|
end
|
|
return value
|
|
end
|
|
|
|
function Registry:register(id, value, owner, replace)
|
|
if replace then return self:override(id, value, owner) end -- v1 signature
|
|
assert(value ~= nil, self.name .. " value is required for " .. tostring(id))
|
|
-- duplicates collide against the base table too, forcing an explicit
|
|
-- override; compose chains accumulate and deep keys merge instead
|
|
if self.spec.semantics == "record" and self:get(id) ~= nil then
|
|
error(("%s already registered: %s"):format(self.name, id))
|
|
end
|
|
return append(self, id, "register", value, owner)
|
|
end
|
|
|
|
function Registry:override(id, value, owner)
|
|
assert(value ~= nil, self.name .. " value is required for " .. tostring(id))
|
|
return append(self, id, "override", value, owner)
|
|
end
|
|
|
|
function Registry:patch(id, partial, owner)
|
|
assert(partial ~= nil, self.name .. " patch value is required for " .. tostring(id))
|
|
if self.spec.semantics == "compose" then
|
|
error(self.name .. ": patch is not supported on compose registries")
|
|
end
|
|
return append(self, id, "patch", partial, owner)
|
|
end
|
|
|
|
-- tombstone: consumers treat the id as absent after the merge
|
|
function Registry:remove(id, owner)
|
|
return append(self, id, "remove", nil, owner)
|
|
end
|
|
|
|
function Registry:get(id)
|
|
if self.spec.semantics == "compose" then
|
|
-- chain() sorts top priority first, so the head is the effective value
|
|
local chain = self:chain(id)
|
|
return chain[1]
|
|
end
|
|
local hit = self.cache[id]
|
|
if hit then return hit.value end
|
|
local value = fold(self, baseValue(self, id), self.ops[id])
|
|
self.cache[id] = { value = value }
|
|
return value
|
|
end
|
|
|
|
function Registry:has(id)
|
|
return self:get(id) ~= nil
|
|
end
|
|
|
|
-- compose fold: the ordered entry list for an id. Override is the
|
|
-- total-conversion escape hatch (09 4.4) -- it clears the whole chain, every
|
|
-- owner's entries alike, and installs itself as the only contribution;
|
|
-- remove tombstones the whole entry the same way but installs nothing.
|
|
-- Order is priority (higher first) then registration order. The second
|
|
-- return says the chain was cleared, which is how a consumer holding an
|
|
-- out-of-band base contribution (MapScripts) knows to leave it out.
|
|
local function composed(self, id)
|
|
local entries, replacesBase = {}, false
|
|
for seq, entry in ipairs(self.ops[id] or {}) do
|
|
if entry.op == "register" then
|
|
entries[#entries + 1] = { value = entry.value, owner = entry.owner, seq = seq }
|
|
elseif entry.op == "override" then
|
|
for i = #entries, 1, -1 do entries[i] = nil end
|
|
entries[1] = { value = entry.value, owner = entry.owner, seq = seq }
|
|
replacesBase = true
|
|
elseif entry.op == "remove" then
|
|
-- owner-scoped removal would leave the consumer's own base
|
|
-- contribution standing, so the map would still dispatch; 09 4.4
|
|
-- makes remove a whole-entry tombstone. A later register still
|
|
-- resurrects the id, ops after this one survive the clear
|
|
for i = #entries, 1, -1 do entries[i] = nil end
|
|
replacesBase = true
|
|
end
|
|
end
|
|
table.sort(entries, function(a, b)
|
|
local pa = type(a.value) == "table" and a.value.priority or 0
|
|
local pb = type(b.value) == "table" and b.value.priority or 0
|
|
if pa ~= pb then return pa > pb end
|
|
return a.seq < b.seq
|
|
end)
|
|
return entries, replacesBase
|
|
end
|
|
|
|
-- compose only: the ordered value list for an id
|
|
function Registry:chain(id)
|
|
assert(self.spec.semantics == "compose",
|
|
self.name .. ": chain is compose-only")
|
|
local entries = composed(self, id)
|
|
local values = {}
|
|
for i = 1, #entries do values[i] = entries[i].value end
|
|
return values
|
|
end
|
|
|
|
-- chain()'s owners, index-aligned with its values: consumers that
|
|
-- attribute dispatch (map_scripts runner sources) read both sides of the
|
|
-- same fold
|
|
function Registry:chainOwners(id)
|
|
assert(self.spec.semantics == "compose",
|
|
self.name .. ": chainOwners is compose-only")
|
|
local entries = composed(self, id)
|
|
local owners = {}
|
|
for i = 1, #entries do owners[i] = entries[i].owner end
|
|
return owners
|
|
end
|
|
|
|
-- compose only: true once an override has cleared this id's chain, so a
|
|
-- consumer that keeps its own base contribution outside the registry
|
|
-- (MapScripts' engine scripts) knows the total conversion excluded it
|
|
function Registry:chainReplacesBase(id)
|
|
assert(self.spec.semantics == "compose",
|
|
self.name .. ": chainReplacesBase is compose-only")
|
|
local _, replacesBase = composed(self, id)
|
|
return replacesBase
|
|
end
|
|
|
|
-- iterator over the merged view: base ids first, then op-only ids;
|
|
-- tombstoned ids are skipped. No ordering guarantee.
|
|
function Registry:each()
|
|
local ids, seen = {}, {}
|
|
local base = self.base and self.base()
|
|
if base then
|
|
-- spec.baseIds names the ids hiding inside a structured target; without
|
|
-- it the target's own keys are the id space
|
|
if self.spec.baseIds then
|
|
for _, id in ipairs(self.spec.baseIds(base)) do
|
|
seen[id] = true
|
|
ids[#ids + 1] = id
|
|
end
|
|
else
|
|
for id in pairs(base) do
|
|
seen[id] = true
|
|
ids[#ids + 1] = id
|
|
end
|
|
end
|
|
end
|
|
for id in pairs(self.ops) do
|
|
if not seen[id] then ids[#ids + 1] = id end
|
|
end
|
|
local i = 0
|
|
return function()
|
|
while true do
|
|
i = i + 1
|
|
local id = ids[i]
|
|
if id == nil then return nil end
|
|
local value = self:get(id)
|
|
if value ~= nil then return id, value end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- v1 compat: the values mods contributed, folded to their effective form
|
|
function Registry:items()
|
|
local out = {}
|
|
for id in pairs(self.ops) do out[id] = self:get(id) end
|
|
return out
|
|
end
|
|
|
|
-- deletes every op an owner appended, in one pass; the loader calls this
|
|
-- before the merge so a failed mod leaves zero trace in Data
|
|
function Registry:rollback(owner)
|
|
if owner == nil then return end
|
|
for id, list in pairs(self.ops) do
|
|
local touched = false
|
|
for i = #list, 1, -1 do
|
|
if list[i].owner == owner then
|
|
table.remove(list, i)
|
|
touched = true
|
|
end
|
|
end
|
|
if touched then
|
|
if #list == 0 then
|
|
self.ops[id] = nil
|
|
self.owners[id] = nil
|
|
else
|
|
self.owners[id] = list[#list].owner
|
|
end
|
|
self.cache[id] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- set once the boot merge has run; unlike the event/hook buses, content
|
|
-- stays deterministic by refusing registration after that point
|
|
function Registry:freeze()
|
|
self.frozen = true
|
|
end
|
|
|
|
return Registry
|