mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 03:02:39 +02:00
big ass modding update
This commit is contained in:
+251
-15
@@ -1,38 +1,274 @@
|
||||
-- Ordered, namespaced registries used by the native mod API.
|
||||
-- Mods register definitions here; the loader merges them into the live data
|
||||
-- only after every enabled mod has initialized successfully.
|
||||
-- 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
|
||||
|
||||
function Registry.new(name)
|
||||
return setmetatable({ name = name, values = {}, owners = {} }, 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
|
||||
|
||||
function Registry:register(id, value, owner, replace)
|
||||
assert(type(id) == "string" and id ~= "", self.name .. " id is required")
|
||||
assert(value ~= nil, self.name .. " value is required for " .. id)
|
||||
if self.values[id] ~= nil and not replace then
|
||||
error(("%s already registered: %s"):format(self.name, id))
|
||||
local function append(self, id, op, value, owner)
|
||||
if self.frozen then
|
||||
error(self.name .. ": content is frozen after load")
|
||||
end
|
||||
self.values[id] = value
|
||||
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)
|
||||
return self:register(id, value, owner, true)
|
||||
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)
|
||||
return self.values[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.values[id] ~= nil
|
||||
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()
|
||||
return self.values
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user