172 lines
5.1 KiB
Lua
172 lines
5.1 KiB
Lua
-- mod must either not use event filters, or be prepared to receive unfiltered events
|
|
-- mod must handle having an empty `global` in on_load or have it transplanted to `level.__modloader[modname]`
|
|
|
|
-- you can't *call* remote at file load time, but you can check if an interface exists
|
|
-- but there's not really anything useful to call for later either
|
|
local loaded = function()
|
|
return true
|
|
end
|
|
local modloader = {
|
|
env = { _ENV=_ENV },
|
|
remote = {},
|
|
}
|
|
remote.add_interface("modloader",modloader.remote)
|
|
|
|
function modloader.load(modname)
|
|
if modname == "level" then
|
|
error("Cannot wrap `level`")
|
|
end
|
|
local modevents = {
|
|
events = {},
|
|
on_nth_tick = {},
|
|
}
|
|
local env = _ENV
|
|
local package = env.package
|
|
|
|
local modpackages = {}
|
|
local sandbox
|
|
local function sandbox_require(require_name,withpackage)
|
|
|
|
-- just skip all the hooking and return cached results directly:
|
|
do
|
|
local cached = modpackages[require_name]
|
|
if cached then
|
|
return cached
|
|
end
|
|
end
|
|
|
|
-- check for an existing hook:
|
|
local oldhook,oldmask,oldcount = debug.gethook()
|
|
-- this hook is only a call hook, which means losing a few line events,
|
|
-- but only between here and the start of the main chunk of the required file
|
|
-- there are no return hooks in that time, and the two call hooks are
|
|
-- passed on to the original hook handler via tailcall
|
|
local function hook(event)
|
|
local info = debug.getinfo(2,"fu")
|
|
-- skip the call to require itself...
|
|
if info.func == require then
|
|
if oldhook and oldmask and oldmask:match("c") then
|
|
-- tailcall the original hook to preserve call event
|
|
return oldhook(event)
|
|
end
|
|
return
|
|
end
|
|
-- on the main chunk, replace it's _ENV upval with sandbox
|
|
local f = info.func
|
|
for i = 1,info.nups do -- this *should* always be upval 1 but just to be sure...
|
|
local name = debug.getupvalue(f,i)
|
|
if name == "_ENV" then
|
|
-- replace its _ENV with the sandbox
|
|
debug.upvaluejoin(f,i,function() return sandbox end,1)
|
|
break
|
|
end
|
|
end
|
|
|
|
-- then restore previous hook and pass along the event
|
|
debug.sethook(oldhook,oldmask,oldcount)
|
|
if oldhook and oldmask and oldmask:match("c") then
|
|
-- and tailcall the original hook to preserve call event
|
|
return oldhook(event)
|
|
end
|
|
end
|
|
|
|
local realpackage = package.loaded
|
|
if withpackage then
|
|
package.loaded = modpackages
|
|
end
|
|
debug.sethook(hook,"c")
|
|
local result = require(require_name)
|
|
if withpackage then
|
|
package.loaded = realpackage
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- on_event needs to be redirected to the events table, print warning when ignoring filters
|
|
local function on_event(event,f,filters)
|
|
local etype = type(event)
|
|
if etype == "number" then
|
|
if filters then
|
|
log("ignored filters") --TODO: print something more useful
|
|
end
|
|
modevents.events[event] = f
|
|
elseif etype == "string" then
|
|
modevents.events[event] = f
|
|
elseif etype == "table" then
|
|
for _,e in pairs(event) do
|
|
on_event(e,f)
|
|
end
|
|
else
|
|
error({"","Invalid Event type ",etype},2)
|
|
end
|
|
end
|
|
sandbox = setmetatable({
|
|
script = setmetatable({
|
|
-- on_init/on_load need redirect, mod needs to handle possibly being added by on_load the first time if added in an update!
|
|
on_init = function(f)
|
|
modevents.on_init = f
|
|
end,
|
|
on_load = function(f)
|
|
modevents.on_load = f
|
|
end,
|
|
on_event = on_event,
|
|
-- on_nth_tick needs to be redirected to events table
|
|
on_nth_tick = function(n,f)
|
|
modevents.on_nth_tick[n] = f
|
|
end,
|
|
on_configuration_changed = function(f)
|
|
modevents.on_configuration_changed = f
|
|
end,
|
|
get_event_handler = function (event)
|
|
return modevents.events[event]
|
|
end,
|
|
},{
|
|
__debugline = "<modloader script proxy for "..modname..">",
|
|
__debugtype = "modloader.LuaBootstrap",
|
|
__index = script,
|
|
}),
|
|
-- reduce arg list and tailcall sandbox_require with only the name
|
|
require = function(name) return sandbox_require(name) end,
|
|
package = setmetatable({loaded = modpackages},{__index = package})
|
|
},{
|
|
__debugline = "<modloader _ENV for "..modname..">",
|
|
__index = function(t,k)
|
|
if k == "global" then
|
|
local global = env.global
|
|
local mods = global.__modloader
|
|
if not mods then
|
|
mods = {}
|
|
global.__modloader = mods
|
|
end
|
|
local mod = mods[modname]
|
|
if not mod then
|
|
mod = {}
|
|
mods[modname] = mod
|
|
end
|
|
return mod
|
|
else
|
|
return env[k]
|
|
end
|
|
end,
|
|
__newindex = function(t,k,v)
|
|
if k == "global" then
|
|
local global = env.global
|
|
local mods = global.__modloader
|
|
if not mods then
|
|
mods = {}
|
|
global.__modloader = mods
|
|
end
|
|
mods[modname] = v
|
|
else
|
|
rawset(t,k,v)
|
|
end
|
|
end,
|
|
})
|
|
modloader.env[modname] = sandbox
|
|
|
|
sandbox_require("__"..modname.."__/control.lua",true)
|
|
modloader.remote[modname] = loaded
|
|
return modevents
|
|
end
|
|
|
|
return modloader |