Files
gen1recomp/tests/modkit/catalog.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

118 lines
3.3 KiB
Lua

-- The live extension-point catalog: every registry, every event name the
-- engine emits, every hook name it calls.
--
-- Registries come from Schemas.REGISTRIES. Events and hooks are read back
-- out of the source rather than from a hand-kept list, because a hand-kept
-- list is exactly the thing that drifts -- adding a Runtime.emit and
-- forgetting the catalog entry is how a seam ships untested. The
-- parity-gate meta-test (tests/engine/gate_meta_coverage.lua) walks these
-- three sets, so a new seam is in the coverage requirement the moment its
-- call site exists.
local Schemas = require("src.mods.Schemas")
local FsIo = require("tests.fs_io")
local Catalog = {}
local function luaFilesUnder(dir)
return FsIo.luaFilesUnder(dir)
end
local function scan(dirs, patterns)
local found = {}
for _, dir in ipairs(dirs) do
for _, path in ipairs(luaFilesUnder(dir)) do
local handle = io.open(path, "r")
if handle then
local body = handle:read("*a")
handle:close()
for _, pattern in ipairs(patterns) do
for name in body:gmatch(pattern) do
local list = found[name] or {}
found[name] = list
list[#list + 1] = path
end
end
end
end
end
return found
end
local function sortedKeys(map)
local keys = {}
for key in pairs(map) do keys[#keys + 1] = key end
table.sort(keys)
return keys
end
local registries, events, hooks, eventSites, hookSites
function Catalog.registries()
if not registries then registries = sortedKeys(Schemas.REGISTRIES) end
return registries
end
-- Runtime.emit is the engine's channel; a bare bus:emit inside src counts
-- too (the loader emits mods.loaded straight off its own bus)
function Catalog.events()
if not events then
eventSites = scan({ "src" }, {
'Runtime%.emit%("([%w%._]+)"',
'events:emit%("([%w%._]+)"',
})
events = sortedKeys(eventSites)
end
return events
end
-- A hook whose Runtime.call lives in a shared raiser rather than at the site
-- that decides the value. Gold's trainer art is not in field.playerPics, so
-- its screens resolve their own path and hand it to Sprites.playerPic
-- (src/pokemon/Sprites.lua); those callers are player.sprite sites and the
-- generation gate has to see them as such.
local INDIRECT_HOOKS = {
["player.sprite"] = "playerPic%(",
}
function Catalog.hooks()
if not hooks then
hookSites = scan({ "src" }, {
'Runtime%.call%("([%w%._]+)"',
'hooks:call%("([%w%._]+)"',
})
for name, pattern in pairs(INDIRECT_HOOKS) do
local list = hookSites[name] or {}
hookSites[name] = list
for _, path in ipairs(luaFilesUnder("src")) do
local handle = io.open(path, "r")
if handle then
local body = handle:read("*a")
handle:close()
if body:match(pattern) then list[#list + 1] = path end
end
end
end
hooks = sortedKeys(hookSites)
end
return hooks
end
function Catalog.eventSites(name)
Catalog.events()
return eventSites[name] or {}
end
function Catalog.hookSites(name)
Catalog.hooks()
return hookSites[name] or {}
end
-- mods may only emit under "mod.<id>."; those are not engine seams and
-- carry no coverage requirement
function Catalog.isModEvent(name)
return name:sub(1, 4) == "mod."
end
return Catalog