mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 16:31:05 +02:00
f0a88ea473
* audio timing stuff * bug fixes and translation additions * translation stuff * Update modkit.py * better asset resolution
287 lines
9.8 KiB
Lua
287 lines
9.8 KiB
Lua
-- The map_scripts compose store (09 §4.4). The engine's hand-ported
|
|
-- data/scripts/* modules are the base contribution -- attachBase folds
|
|
-- them with the v1 merge rules (talk per TEXT constant, other keys
|
|
-- replaced by later files), so a mod-free boot dispatches the exact
|
|
-- table it always did. Mod contributions arrive through the merged
|
|
-- registry (Data.map_scripts, one ordered chain per map id) and compose:
|
|
--
|
|
-- talk / scripts / legacy keys single winner, false suppresses
|
|
-- onEnter / onVictory / onBoulderMoved all-run, pcall-guarded
|
|
-- onStep / onInteract first truthy return consumes
|
|
--
|
|
-- Precedence is priority descending, later registration first at equal
|
|
-- priority; base sits at priority 0 behind every default-priority mod.
|
|
|
|
local Data = require("src.core.Data")
|
|
local Logger = require("src.core.Logger")
|
|
local Runtime = require("src.mods.Runtime")
|
|
local Strings = require("src.core.Strings")
|
|
|
|
local MapScripts = {}
|
|
|
|
local base = {} -- mapId -> merged engine contribution
|
|
local views = {} -- mapId -> { chain = chainRef, value = merged view, sources }
|
|
|
|
local HOOK_RULES = {
|
|
onEnter = "all", onVictory = "all", onBoulderMoved = "all",
|
|
onStep = "first", onInteract = "first",
|
|
}
|
|
|
|
-- the v1 merge, verbatim: later base files override earlier ones
|
|
function MapScripts.attachBase(mapId, contribution)
|
|
local existing = base[mapId]
|
|
if not existing then
|
|
base[mapId] = contribution
|
|
else
|
|
for k, v in pairs(contribution) do
|
|
if k == "talk" and existing.talk then
|
|
for textConst, script in pairs(v) do
|
|
existing.talk[textConst] = script
|
|
end
|
|
else
|
|
existing[k] = v
|
|
end
|
|
end
|
|
end
|
|
views[mapId] = nil
|
|
end
|
|
|
|
function MapScripts.invalidate(mapId)
|
|
if mapId then
|
|
views[mapId] = nil
|
|
else
|
|
views = {}
|
|
end
|
|
end
|
|
|
|
-- Registry:chain hands the mod contributions priority-descending with
|
|
-- earlier registrations first; re-rank each equal-priority run so a
|
|
-- later registration outranks an earlier one, then slot base in at
|
|
-- priority 0 behind the mods that tie it. chain.owners -- stamped by the
|
|
-- loader merge, index-aligned with the values -- rides along so every
|
|
-- contribution keeps its attribution.
|
|
local function contributions(chain, baseEntry)
|
|
local owners = chain.owners or {}
|
|
local ordered, run, runPriority = {}, {}, nil
|
|
local function flush()
|
|
for i = #run, 1, -1 do ordered[#ordered + 1] = run[i] end
|
|
run = {}
|
|
end
|
|
for i, entry in ipairs(chain) do
|
|
local p = type(entry) == "table" and entry.priority or 0
|
|
if p ~= runPriority then
|
|
flush()
|
|
runPriority = p
|
|
end
|
|
run[#run + 1] = { value = entry, owner = owners[i] }
|
|
end
|
|
flush()
|
|
if baseEntry then
|
|
local at = #ordered + 1
|
|
for i, entry in ipairs(ordered) do
|
|
local p = type(entry.value) == "table" and entry.value.priority or 0
|
|
if (p or 0) < 0 then
|
|
at = i
|
|
break
|
|
end
|
|
end
|
|
table.insert(ordered, at, { value = baseEntry })
|
|
end
|
|
return ordered
|
|
end
|
|
|
|
-- the runner's ctx.source for one contribution's rows (09 §4.4): errors,
|
|
-- mod: field routing and the script events name the owning mod. Base
|
|
-- contributions stay unattributed, so a mod-free dispatch hands the runner
|
|
-- the exact extra it always did.
|
|
local function sourceFor(owner, mapId, hook)
|
|
if not (owner and owner.modId) then return nil end
|
|
return { modId = owner.modId, strict = owner.strict,
|
|
mapId = mapId, hook = hook }
|
|
end
|
|
|
|
local function blame(mapId, hookName, owner, err)
|
|
local modId = owner and owner.modId
|
|
Logger.error("map script %s.%s [%s]: %s", mapId, hookName,
|
|
tostring(modId or "engine"), tostring(err))
|
|
if modId then Runtime.reportError(modId, tostring(err)) end
|
|
end
|
|
|
|
local function chainAll(mapId, hookName, handlers)
|
|
return function(...)
|
|
for _, handler in ipairs(handlers) do
|
|
local ok, err = pcall(handler.fn, ...)
|
|
if not ok then blame(mapId, hookName, handler.owner, err) end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function chainFirst(mapId, hookName, handlers)
|
|
return function(...)
|
|
local result
|
|
for _, handler in ipairs(handlers) do
|
|
local ok, value = pcall(handler.fn, ...)
|
|
if not ok then
|
|
blame(mapId, hookName, handler.owner, value)
|
|
elseif value then
|
|
return value
|
|
else
|
|
result = value
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
end
|
|
|
|
local function buildView(mapId, ordered)
|
|
local view = { talk = {} }
|
|
local sources = { talk = {}, scripts = {} }
|
|
local talkDefined, scriptsDefined, otherDefined = {}, {}, {}
|
|
local hooks = {}
|
|
for _, entry in ipairs(ordered) do
|
|
local contribution, owner = entry.value, entry.owner
|
|
for key, value in pairs(contribution) do
|
|
if key == "talk" then
|
|
for textConst, script in pairs(value) do
|
|
if not talkDefined[textConst] then
|
|
talkDefined[textConst] = true
|
|
-- false is explicit suppression: talkTo falls through to its
|
|
-- item-ball/trainer/mart branches
|
|
if script ~= false then
|
|
view.talk[textConst] = script
|
|
sources.talk[textConst] = sourceFor(owner, mapId, "talk")
|
|
end
|
|
end
|
|
end
|
|
elseif key == "scripts" then
|
|
view.scripts = view.scripts or {}
|
|
for name, rows in pairs(value) do
|
|
if not scriptsDefined[name] then
|
|
scriptsDefined[name] = true
|
|
if rows ~= false then
|
|
view.scripts[name] = rows
|
|
sources.scripts[name] = sourceFor(owner, mapId, "scripts." .. name)
|
|
end
|
|
end
|
|
end
|
|
elseif HOOK_RULES[key] then
|
|
local list = hooks[key]
|
|
if not list then
|
|
list = {}
|
|
hooks[key] = list
|
|
end
|
|
list[#list + 1] = { fn = value, owner = owner }
|
|
elseif key ~= "priority" then
|
|
-- legacy ad-hoc keys (snorlaxWake, escort, ...): talk's rule
|
|
if not otherDefined[key] then
|
|
otherDefined[key] = true
|
|
if value ~= false then view[key] = value end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for hookName, handlers in pairs(hooks) do
|
|
if #handlers == 1 and not (handlers[1].owner and handlers[1].owner.modId) then
|
|
-- a lone base handler dispatches bare, exactly as the v1 merge did
|
|
view[hookName] = handlers[1].fn
|
|
elseif HOOK_RULES[hookName] == "all" then
|
|
view[hookName] = chainAll(mapId, hookName, handlers)
|
|
else
|
|
view[hookName] = chainFirst(mapId, hookName, handlers)
|
|
end
|
|
end
|
|
return view, sources
|
|
end
|
|
|
|
function MapScripts.get(mapId)
|
|
local chains = Data.map_scripts
|
|
local chain = chains and chains[mapId]
|
|
local baseEntry = base[mapId]
|
|
-- map_scripts:override or :remove cleared the chain for this map
|
|
-- (09 4.4), so the engine's own contribution is excluded too -- otherwise
|
|
-- a total conversion still gets the vanilla onEnter and every TEXT
|
|
-- constant it did not redefine, and a removed map is not actually gone.
|
|
-- A tombstone arrives as an empty chain and falls through to nil below
|
|
if chain and chain.replacesBase then baseEntry = nil end
|
|
if not chain or #chain == 0 then return baseEntry end
|
|
local hit = views[mapId]
|
|
if hit and hit.chain == chain then return hit.value end
|
|
local view, sources = buildView(mapId, contributions(chain, baseEntry))
|
|
views[mapId] = { chain = chain, value = view, sources = sources }
|
|
return view
|
|
end
|
|
|
|
-- the cached sources beside a map's merged view; nil when the map has no
|
|
-- chain (base fast path) and for base-owned winners
|
|
local function viewSources(mapId)
|
|
local chains = Data.map_scripts
|
|
local chain = chains and chains[mapId]
|
|
if not chain or #chain == 0 then return nil end
|
|
MapScripts.get(mapId)
|
|
local hit = views[mapId]
|
|
return hit and hit.sources
|
|
end
|
|
|
|
-- ctx.source for a talk dispatch, handed to ScriptRunner:run by
|
|
-- showMapText so the winning contribution's rows run as their owner
|
|
function MapScripts.talkSource(mapId, textConst)
|
|
local sources = viewSources(mapId)
|
|
return sources and sources.talk[textConst] or nil
|
|
end
|
|
|
|
-- ctx.source for a named `scripts` entry (run_parallel / queueScript refs)
|
|
function MapScripts.namedSource(mapId, name)
|
|
local sources = viewSources(mapId)
|
|
return sources and sources.scripts[name] or nil
|
|
end
|
|
|
|
-- script to run when the player talks to an object with this TEXT_ constant
|
|
function MapScripts.talkScript(mapId, textConst)
|
|
local view = MapScripts.get(mapId)
|
|
return view and view.talk and view.talk[textConst] or nil
|
|
end
|
|
|
|
-- the base (engine) talk handler behind any mod override -- the supported
|
|
-- replacement for the old re-wrap idiom
|
|
function MapScripts.baseTalk(mapId, textConst)
|
|
local entry = base[mapId]
|
|
return entry and entry.talk and entry.talk[textConst] or nil
|
|
end
|
|
|
|
-- "MAP_ID/name" refs used by run_parallel and queueScript
|
|
function MapScripts.namedScript(mapId, name)
|
|
local view = MapScripts.get(mapId)
|
|
return view and view.scripts and view.scripts[name] or nil
|
|
end
|
|
|
|
-- ------- load-time validation (09 §4.9)
|
|
|
|
-- every row list reachable from one contribution; findings name the key
|
|
-- they were found under. lookup is the verb resolver handed to
|
|
-- ScriptRunner.validate; the loader and modkit share this pass.
|
|
function MapScripts.validateContribution(contribution, lookup)
|
|
local ScriptRunner = require("src.script.ScriptRunner")
|
|
local problems = {}
|
|
local function collect(where, rows)
|
|
if type(rows) ~= "table" then return end
|
|
for _, finding in ipairs(ScriptRunner.validate(rows, lookup)) do
|
|
problems[#problems + 1] = where .. ": " .. finding
|
|
end
|
|
end
|
|
if type(contribution) ~= "table" then
|
|
return { Strings("contribution is not a table") }
|
|
end
|
|
for textConst, script in pairs(contribution.talk or {}) do
|
|
if type(script) == "table" then collect("talk." .. textConst, script) end
|
|
end
|
|
for name, rows in pairs(contribution.scripts or {}) do
|
|
if type(rows) == "table" then collect("scripts." .. name, rows) end
|
|
end
|
|
if type(contribution.snorlaxWake) == "table" then
|
|
collect("snorlaxWake", contribution.snorlaxWake.script)
|
|
end
|
|
return problems
|
|
end
|
|
|
|
return MapScripts
|