-- 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.."; those are not engine seams and -- carry no coverage requirement function Catalog.isModEvent(name) return name:sub(1, 4) == "mod." end return Catalog