mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 03:02:39 +02:00
initial commit
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
local Events = {}
|
||||
Events.__index = Events
|
||||
|
||||
function Events.new()
|
||||
return setmetatable({ listeners = {}, sealed = false }, Events)
|
||||
end
|
||||
|
||||
function Events:on(name, callback, priority)
|
||||
assert(not self.sealed, "mod events are sealed")
|
||||
assert(type(name) == "string" and name ~= "", "event name is required")
|
||||
assert(type(callback) == "function", "event callback must be a function")
|
||||
local list = self.listeners[name] or {}
|
||||
self.listeners[name] = list
|
||||
local entry = { callback = callback, priority = priority or 0 }
|
||||
list[#list + 1] = entry
|
||||
table.sort(list, function(a, b) return a.priority > b.priority end)
|
||||
return function()
|
||||
for i, candidate in ipairs(list) do
|
||||
if candidate == entry then table.remove(list, i) break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Events:emit(name, payload)
|
||||
local list = self.listeners[name] or {}
|
||||
for _, entry in ipairs(list) do
|
||||
entry.callback(payload)
|
||||
end
|
||||
end
|
||||
|
||||
function Events:seal()
|
||||
self.sealed = true
|
||||
end
|
||||
|
||||
return Events
|
||||
@@ -0,0 +1,47 @@
|
||||
local Hooks = {}
|
||||
Hooks.__index = Hooks
|
||||
local unpack = table.unpack or unpack
|
||||
|
||||
function Hooks.new()
|
||||
return setmetatable({ chains = {}, sealed = false }, Hooks)
|
||||
end
|
||||
|
||||
function Hooks:wrap(name, callback, priority)
|
||||
assert(not self.sealed, "mod hooks are sealed")
|
||||
assert(type(name) == "string" and name ~= "", "hook name is required")
|
||||
assert(type(callback) == "function", "hook callback must be a function")
|
||||
local chain = self.chains[name] or {}
|
||||
self.chains[name] = chain
|
||||
local entry = { callback = callback, priority = priority or 0 }
|
||||
chain[#chain + 1] = entry
|
||||
table.sort(chain, function(a, b) return a.priority > b.priority end)
|
||||
return function()
|
||||
for i, candidate in ipairs(chain) do
|
||||
if candidate == entry then table.remove(chain, i) break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Hooks:call(name, vanilla, ...)
|
||||
local chain = self.chains[name] or {}
|
||||
local args = { ... }
|
||||
local function run(index, current)
|
||||
if index > #chain then return current(unpack(args)) end
|
||||
return chain[index].callback(function(...)
|
||||
local nextArgs = { ... }
|
||||
if #nextArgs == 0 then return run(index + 1, current) end
|
||||
local old = args
|
||||
args = nextArgs
|
||||
local result = run(index + 1, current)
|
||||
args = old
|
||||
return result
|
||||
end, unpack(args))
|
||||
end
|
||||
return run(1, vanilla)
|
||||
end
|
||||
|
||||
function Hooks:seal()
|
||||
self.sealed = true
|
||||
end
|
||||
|
||||
return Hooks
|
||||
@@ -0,0 +1,237 @@
|
||||
local Json = require("src.link.Json")
|
||||
local Logger = require("src.core.Logger")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Manifest = require("src.mods.Manifest")
|
||||
local Registry = require("src.mods.Registry")
|
||||
local Events = require("src.mods.Events")
|
||||
local Hooks = require("src.mods.Hooks")
|
||||
|
||||
local Loader = {}
|
||||
Loader.__index = Loader
|
||||
|
||||
local REGISTRY_NAMES = {
|
||||
"pokemon", "moves", "items", "maps", "tilesets", "encounters",
|
||||
"trainers", "sprites", "music", "audio", "text", "scripts", "ui",
|
||||
}
|
||||
|
||||
local MOD_STATE_FILE = "mod_state.lua" -- legacy migration only
|
||||
|
||||
local function readManifest(root)
|
||||
local raw, err = love.filesystem.read(root .. "/manifest.json")
|
||||
if not raw then return nil, err end
|
||||
local data, decodeErr = Json.decode(raw)
|
||||
if not data then return nil, decodeErr end
|
||||
local ok, manifest = pcall(Manifest.validate, data, root)
|
||||
if not ok then return nil, manifest end
|
||||
return manifest
|
||||
end
|
||||
|
||||
local function topoSort(mods)
|
||||
local ordered, visiting, visited = {}, {}, {}
|
||||
local function visit(id)
|
||||
if visited[id] then return end
|
||||
if visiting[id] then error("circular mod dependency involving " .. id) end
|
||||
local mod = mods[id]
|
||||
if not mod then error("missing required mod dependency: " .. id) end
|
||||
visiting[id] = true
|
||||
for _, dependency in ipairs(mod.manifest.dependencies) do visit(dependency) end
|
||||
visiting[id], visited[id] = nil, true
|
||||
ordered[#ordered + 1] = mod
|
||||
end
|
||||
local ids = {}
|
||||
for id in pairs(mods) do ids[#ids + 1] = id end
|
||||
table.sort(ids, function(a, b)
|
||||
local pa, pb = mods[a].manifest.priority, mods[b].manifest.priority
|
||||
if pa == pb then return a < b end
|
||||
return pa < pb
|
||||
end)
|
||||
for _, id in ipairs(ids) do visit(id) end
|
||||
return ordered
|
||||
end
|
||||
|
||||
function Loader.new()
|
||||
local self = setmetatable({
|
||||
mods = {}, loaded = {}, errors = {}, initialized = false,
|
||||
events = Events.new(), hooks = Hooks.new(), content = {}, assets = {},
|
||||
}, Loader)
|
||||
for _, name in ipairs(REGISTRY_NAMES) do
|
||||
self.content[name] = Registry.new(name)
|
||||
end
|
||||
self.disabled = {}
|
||||
return self
|
||||
end
|
||||
|
||||
function Loader:_loadState()
|
||||
self.disabled = {}
|
||||
local options = SaveData.loadOptions()
|
||||
for id, enabled in pairs(options.mods or {}) do
|
||||
if enabled == false then self.disabled[id] = true end
|
||||
end
|
||||
-- Migrate the original prototype manager's separate state file into the
|
||||
-- normal persistent options file once. New Game never resets options.
|
||||
if next(options.mods or {}) == nil and love.filesystem.getInfo
|
||||
and love.filesystem.getInfo(MOD_STATE_FILE) then
|
||||
local chunk = love.filesystem.load(MOD_STATE_FILE)
|
||||
local ok, state = chunk and pcall(chunk)
|
||||
if ok and type(state) == "table" then
|
||||
for id, disabled in pairs(state) do
|
||||
if disabled then
|
||||
options.mods[id] = false
|
||||
self.disabled[id] = true
|
||||
end
|
||||
end
|
||||
SaveData.saveOptions(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Loader:_saveState()
|
||||
local options = SaveData.loadOptions()
|
||||
options.mods = options.mods or {}
|
||||
for id in pairs(self.mods) do
|
||||
options.mods[id] = not self.disabled[id]
|
||||
end
|
||||
SaveData.saveOptions(options)
|
||||
end
|
||||
|
||||
function Loader:setEnabled(id, enabled)
|
||||
if not self.mods[id] then return false end
|
||||
self.disabled[id] = not enabled
|
||||
self.mods[id].enabled = enabled
|
||||
self:_saveState()
|
||||
return true
|
||||
end
|
||||
|
||||
function Loader:_discover()
|
||||
if not love.filesystem.getDirectoryItems then return end
|
||||
local roots = { "mods" }
|
||||
for _, root in ipairs(roots) do
|
||||
if love.filesystem.getInfo(root) then
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems(root)) do
|
||||
local path = root .. "/" .. name
|
||||
local info = love.filesystem.getInfo(path)
|
||||
if info and info.type == "directory" then
|
||||
local manifest, err = readManifest(path)
|
||||
if manifest then
|
||||
if self.mods[manifest.id] then
|
||||
self.errors[#self.errors + 1] = manifest.id .. ": duplicate mod id"
|
||||
else
|
||||
self.mods[manifest.id] = { manifest = manifest, path = path }
|
||||
end
|
||||
else
|
||||
Logger.warn("mod %s ignored: %s", path, tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Loader:_api(mod)
|
||||
local loader = self
|
||||
local api = {
|
||||
id = mod.manifest.id,
|
||||
version = mod.manifest.version,
|
||||
path = mod.path,
|
||||
content = {},
|
||||
events = { on = function(_, name, callback, priority)
|
||||
return loader.events:on(name, callback, priority)
|
||||
end },
|
||||
hooks = { wrap = function(_, name, callback, priority)
|
||||
return loader.hooks:wrap(name, callback, priority)
|
||||
end },
|
||||
log = {
|
||||
info = function(_, fmt, ...) Logger.info("[%s] " .. fmt, mod.manifest.id, ...) end,
|
||||
warn = function(_, fmt, ...) Logger.warn("[%s] " .. fmt, mod.manifest.id, ...) end,
|
||||
error = function(_, fmt, ...) Logger.error("[%s] " .. fmt, mod.manifest.id, ...) end,
|
||||
},
|
||||
}
|
||||
for _, name in ipairs(REGISTRY_NAMES) do
|
||||
api.content[name] = {
|
||||
register = function(_, id, value)
|
||||
return loader.content[name]:register(id, value, mod.manifest.id)
|
||||
end,
|
||||
override = function(_, id, value)
|
||||
return loader.content[name]:override(id, value, mod.manifest.id)
|
||||
end,
|
||||
get = function(_, id)
|
||||
return loader.content[name]:get(id)
|
||||
or (loader.baseData and loader.baseData[name]
|
||||
and loader.baseData[name][id])
|
||||
end,
|
||||
}
|
||||
end
|
||||
api.assets = api.content
|
||||
function api:read(relative)
|
||||
local path = self.path .. "/" .. relative
|
||||
return love.filesystem.read(path)
|
||||
end
|
||||
return api
|
||||
end
|
||||
|
||||
function Loader:_loadMod(mod)
|
||||
local path = mod.path .. "/" .. mod.manifest.entry
|
||||
local chunk, err = love.filesystem.load(path)
|
||||
if not chunk then error(err or ("unable to load " .. path)) end
|
||||
local api = self:_api(mod)
|
||||
local result = chunk(api)
|
||||
if type(result) == "function" then result(api) end
|
||||
end
|
||||
|
||||
function Loader:load(data)
|
||||
self.baseData = data
|
||||
self:_loadState()
|
||||
self:_discover()
|
||||
local ok, ordered = pcall(topoSort, self.mods)
|
||||
if not ok then
|
||||
self.errors[#self.errors + 1] = ordered
|
||||
Logger.error("mod dependency resolution failed: %s", tostring(ordered))
|
||||
return false
|
||||
end
|
||||
for _, mod in ipairs(ordered) do
|
||||
mod.enabled = not self.disabled[mod.manifest.id]
|
||||
local success, err = true, nil
|
||||
if mod.enabled then
|
||||
success, err = pcall(self._loadMod, self, mod)
|
||||
end
|
||||
if success and mod.enabled then
|
||||
self.loaded[#self.loaded + 1] = mod
|
||||
Logger.info("loaded mod %s %s", mod.manifest.id, mod.manifest.version)
|
||||
else
|
||||
self.errors[#self.errors + 1] = mod.manifest.id .. ": " .. tostring(err)
|
||||
Logger.error("mod %s failed: %s", mod.manifest.id, tostring(err))
|
||||
end
|
||||
end
|
||||
-- Native content registrations override the imported base definitions.
|
||||
for name, registry in pairs(self.content) do
|
||||
local target = data and data[name]
|
||||
if name == "music" and data and data.audio then
|
||||
data.audio.songs = data.audio.songs or {}
|
||||
target = data.audio.songs
|
||||
end
|
||||
if type(target) == "table" then
|
||||
for id, value in pairs(registry.values) do target[id] = value end
|
||||
end
|
||||
end
|
||||
self.events:emit("mods.loaded", { loader = self, data = data })
|
||||
self.events:seal()
|
||||
self.hooks:seal()
|
||||
self.initialized = true
|
||||
return #self.errors == 0
|
||||
end
|
||||
|
||||
function Loader:status()
|
||||
local available, loaded = {}, {}
|
||||
for _, mod in pairs(self.mods) do
|
||||
local manifest = {}
|
||||
for key, value in pairs(mod.manifest) do manifest[key] = value end
|
||||
manifest.enabled = mod.enabled ~= false
|
||||
available[#available + 1] = manifest
|
||||
if manifest.enabled then loaded[#loaded + 1] = manifest end
|
||||
end
|
||||
table.sort(available, function(a, b) return a.id < b.id end)
|
||||
table.sort(loaded, function(a, b) return a.id < b.id end)
|
||||
return { available = available, loaded = loaded, errors = self.errors }
|
||||
end
|
||||
|
||||
return Loader
|
||||
@@ -0,0 +1,242 @@
|
||||
-- Built-in mod manager using the same tile boxes, cursor, spacing, and
|
||||
-- navigation language as the game's START menu.
|
||||
local Font = require("src.render.Font")
|
||||
|
||||
local ManagerState = {}
|
||||
ManagerState.__index = ManagerState
|
||||
ManagerState.isOpaque = true
|
||||
|
||||
local CURSOR = 0xED
|
||||
local DOWN_ARROW = 0xEE
|
||||
|
||||
local function wrap(text, width)
|
||||
local lines = {}
|
||||
for paragraph in tostring(text or ""):gmatch("[^\n]+") do
|
||||
local line = ""
|
||||
for word in paragraph:gmatch("%S+") do
|
||||
while #word > width do
|
||||
if line ~= "" then
|
||||
lines[#lines + 1] = line
|
||||
line = ""
|
||||
end
|
||||
lines[#lines + 1] = word:sub(1, width)
|
||||
word = word:sub(width + 1)
|
||||
end
|
||||
if word ~= "" then
|
||||
local candidate = line == "" and word or line .. " " .. word
|
||||
if #candidate > width and line ~= "" then
|
||||
lines[#lines + 1] = line
|
||||
line = word
|
||||
else
|
||||
line = candidate
|
||||
end
|
||||
end
|
||||
end
|
||||
if line ~= "" then lines[#lines + 1] = line end
|
||||
end
|
||||
if #lines == 0 then lines[1] = "" end
|
||||
return lines
|
||||
end
|
||||
|
||||
function ManagerState.new(game)
|
||||
return setmetatable({
|
||||
game = game,
|
||||
mode = "categories",
|
||||
categoryIndex = 1,
|
||||
modIndex = 1,
|
||||
scroll = 1,
|
||||
restartPending = false,
|
||||
}, ManagerState)
|
||||
end
|
||||
|
||||
function ManagerState:enter()
|
||||
self:rebuildCategories()
|
||||
end
|
||||
|
||||
function ManagerState:rebuildCategories()
|
||||
local status = self.game.modStatus or { available = {} }
|
||||
self.categories = {}
|
||||
self.byCategory = {}
|
||||
for _, manifest in ipairs(status.available or {}) do
|
||||
local category = manifest.category or "OTHER"
|
||||
self.byCategory[category] = self.byCategory[category] or {}
|
||||
self.byCategory[category][#self.byCategory[category] + 1] = manifest
|
||||
end
|
||||
for category in pairs(self.byCategory) do
|
||||
self.categories[#self.categories + 1] = category
|
||||
end
|
||||
table.sort(self.categories)
|
||||
self.categoryIndex = math.min(self.categoryIndex, math.max(1, #self.categories))
|
||||
end
|
||||
|
||||
function ManagerState:currentMods()
|
||||
return self.byCategory[self.categories[self.categoryIndex]] or {}
|
||||
end
|
||||
|
||||
function ManagerState:currentMod()
|
||||
return self:currentMods()[self.modIndex]
|
||||
end
|
||||
|
||||
function ManagerState:openCategory()
|
||||
self.mode = "mods"
|
||||
self.modIndex = 1
|
||||
end
|
||||
|
||||
function ManagerState:openMod()
|
||||
self.mode = "detail"
|
||||
self.scroll = 1
|
||||
end
|
||||
|
||||
function ManagerState:toggleCurrent()
|
||||
local manifest = self:currentMod()
|
||||
if not manifest then return end
|
||||
self.game.mods:setEnabled(manifest.id, not manifest.enabled)
|
||||
self.game.modStatus = self.game.mods:status()
|
||||
self.restartPending = true
|
||||
self:rebuildCategories()
|
||||
for _, candidate in ipairs(self:currentMods()) do
|
||||
if candidate.id == manifest.id then
|
||||
self.modIndex = _
|
||||
break
|
||||
end
|
||||
end
|
||||
self.mode = "detail"
|
||||
end
|
||||
|
||||
function ManagerState:restartGame()
|
||||
if self.game.restartWithMods then
|
||||
self.game:restartWithMods()
|
||||
elseif love.event and love.event.quit then
|
||||
love.event.quit("restart")
|
||||
end
|
||||
end
|
||||
|
||||
function ManagerState:back()
|
||||
if self.mode == "detail" then
|
||||
self.mode = "mods"
|
||||
elseif self.mode == "mods" then
|
||||
self.mode = "categories"
|
||||
else
|
||||
self.game.stack:pop()
|
||||
end
|
||||
end
|
||||
|
||||
function ManagerState:onKeyPressed(key)
|
||||
local activate = key == "return" or key == "kpenter" or key == "z"
|
||||
or key == "space"
|
||||
if key == "escape" or key == "f10" or key == "x" or key == "backspace" then
|
||||
self:back()
|
||||
return
|
||||
end
|
||||
if self.mode == "categories" then
|
||||
if key == "up" and #self.categories > 0 then
|
||||
self.categoryIndex = self.categoryIndex > 1 and self.categoryIndex - 1 or #self.categories
|
||||
elseif key == "down" and #self.categories > 0 then
|
||||
self.categoryIndex = self.categoryIndex < #self.categories and self.categoryIndex + 1 or 1
|
||||
elseif activate and #self.categories > 0 then
|
||||
self:openCategory()
|
||||
end
|
||||
elseif self.mode == "mods" then
|
||||
local mods = self:currentMods()
|
||||
if key == "up" and #mods > 0 then
|
||||
self.modIndex = self.modIndex > 1 and self.modIndex - 1 or #mods
|
||||
elseif key == "down" and #mods > 0 then
|
||||
self.modIndex = self.modIndex < #mods and self.modIndex + 1 or 1
|
||||
elseif activate and #mods > 0 then
|
||||
self:openMod()
|
||||
end
|
||||
else
|
||||
if key == "up" then self.scroll = math.max(1, self.scroll - 1)
|
||||
elseif key == "down" then self.scroll = self.scroll + 1
|
||||
elseif activate then
|
||||
if self.restartPending then self:restartGame()
|
||||
else self:toggleCurrent() end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ManagerState:update() end
|
||||
|
||||
local function drawList(items, index, tx, ty, tw, th)
|
||||
local visible = math.max(1, math.floor((th - 2) / 2))
|
||||
local first = math.max(1, index - visible + 1)
|
||||
local y = ty + 1
|
||||
for itemIndex = first, math.min(#items, first + visible - 1) do
|
||||
local itemLines = wrap(items[itemIndex], tw - 2)
|
||||
if itemIndex == index then
|
||||
Font.drawCode(CURSOR, (tx + 1) * 8, y * 8)
|
||||
end
|
||||
for lineIndex = 1, math.min(2, #itemLines) do
|
||||
Font.draw(itemLines[lineIndex], (tx + 2) * 8,
|
||||
(y + lineIndex - 1) * 8)
|
||||
end
|
||||
y = y + 2
|
||||
end
|
||||
if #items > first + visible - 1 then
|
||||
Font.drawCode(DOWN_ARROW, (tx + tw - 2) * 8, (ty + th - 1) * 8)
|
||||
end
|
||||
end
|
||||
|
||||
function ManagerState:drawDetail(manifest)
|
||||
local title = wrap(manifest.name, 16)
|
||||
Font.draw(title[1], 2 * 8, 4 * 8)
|
||||
Font.draw(manifest.enabled and "ENABLED" or "DISABLED", 3 * 8, 6 * 8)
|
||||
local lines = wrap(manifest.description, 16)
|
||||
-- Rows 8-12 are description, row 13 is deliberately blank, and row 14
|
||||
-- is the option/restart action.
|
||||
local visible = 5
|
||||
for row = 1, visible do
|
||||
local line = lines[self.scroll + row - 1]
|
||||
if not line then break end
|
||||
Font.draw(line, 2 * 8, (7 + row) * 8)
|
||||
end
|
||||
if self.scroll + visible <= #lines then
|
||||
Font.drawCode(DOWN_ARROW, 17 * 8, 12 * 8)
|
||||
end
|
||||
if self.restartPending then
|
||||
Font.draw("RESTART REQUIRED", 2 * 8, 14 * 8)
|
||||
Font.draw("A:RESTART", 11 * 8, 15 * 8)
|
||||
else
|
||||
Font.draw(manifest.enabled and "DISABLE" or "ENABLE", 2 * 8, 14 * 8)
|
||||
Font.draw("A:CHANGE", 11 * 8, 15 * 8)
|
||||
end
|
||||
end
|
||||
|
||||
function ManagerState:draw()
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
Font.drawBox(0, 0, 20, 18)
|
||||
Font.draw("MOD MENU", 2 * 8, 1 * 8)
|
||||
|
||||
if self.mode == "detail" then
|
||||
self:drawDetail(self:currentMod())
|
||||
return
|
||||
end
|
||||
|
||||
local categoryItems = {}
|
||||
for _, category in ipairs(self.categories) do
|
||||
categoryItems[#categoryItems + 1] = category
|
||||
end
|
||||
if #categoryItems == 0 then categoryItems[1] = "NO MODS" end
|
||||
if self.mode == "categories" then
|
||||
drawList(categoryItems, self.categoryIndex, 1, 4, 18, 11)
|
||||
Font.draw("A:OPEN", 2 * 8, 16 * 8)
|
||||
Font.draw("B:BACK", 12 * 8, 16 * 8)
|
||||
return
|
||||
end
|
||||
|
||||
if self.mode == "mods" then
|
||||
local mods = self:currentMods()
|
||||
local labels = {}
|
||||
for _, manifest in ipairs(mods) do
|
||||
labels[#labels + 1] = (manifest.enabled and "" or "*") .. manifest.name
|
||||
end
|
||||
Font.draw(self.categories[self.categoryIndex] or "MODS", 2 * 8, 4 * 8)
|
||||
drawList(labels, self.modIndex, 1, 6, 18, 9)
|
||||
Font.draw("A:OPEN", 2 * 8, 16 * 8)
|
||||
Font.draw("B:BACK", 12 * 8, 16 * 8)
|
||||
end
|
||||
end
|
||||
|
||||
return ManagerState
|
||||
@@ -0,0 +1,33 @@
|
||||
local Manifest = {}
|
||||
|
||||
local function array(value)
|
||||
if value == nil then return {} end
|
||||
assert(type(value) == "table", "manifest arrays must be tables")
|
||||
return value
|
||||
end
|
||||
|
||||
function Manifest.validate(raw, path)
|
||||
assert(type(raw) == "table", "manifest must be an object")
|
||||
assert(type(raw.id) == "string" and raw.id:match("^[%w_%-]+$"),
|
||||
"manifest id must contain only letters, numbers, _ or -")
|
||||
assert(type(raw.name) == "string" and raw.name ~= "", "manifest name is required")
|
||||
assert(type(raw.version) == "string" and raw.version ~= "", "manifest version is required")
|
||||
assert(type(raw.entry) == "string" and raw.entry ~= "", "manifest entry is required")
|
||||
return {
|
||||
id = raw.id,
|
||||
name = raw.name,
|
||||
version = raw.version,
|
||||
entry = raw.entry,
|
||||
priority = tonumber(raw.priority) or 0,
|
||||
dependencies = array(raw.dependencies),
|
||||
optional_dependencies = array(raw.optional_dependencies),
|
||||
conflicts = array(raw.conflicts),
|
||||
category = raw.category or "OTHER",
|
||||
game_version = raw.game_version,
|
||||
description = raw.description or "",
|
||||
path = path,
|
||||
raw = raw,
|
||||
}
|
||||
end
|
||||
|
||||
return Manifest
|
||||
@@ -0,0 +1,38 @@
|
||||
-- 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.
|
||||
local Registry = {}
|
||||
Registry.__index = Registry
|
||||
|
||||
function Registry.new(name)
|
||||
return setmetatable({ name = name, values = {}, owners = {} }, 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))
|
||||
end
|
||||
self.values[id] = value
|
||||
self.owners[id] = owner
|
||||
return value
|
||||
end
|
||||
|
||||
function Registry:override(id, value, owner)
|
||||
return self:register(id, value, owner, true)
|
||||
end
|
||||
|
||||
function Registry:get(id)
|
||||
return self.values[id]
|
||||
end
|
||||
|
||||
function Registry:has(id)
|
||||
return self.values[id] ~= nil
|
||||
end
|
||||
|
||||
function Registry:items()
|
||||
return self.values
|
||||
end
|
||||
|
||||
return Registry
|
||||
Reference in New Issue
Block a user