Files
gen1recomp/src/mods/job_worker.lua
T
bryanthaboi 5198b35945 bazinga
2026-08-15 10:44:20 -04:00

74 lines
3.1 KiB
Lua

-- Worker state behind src/mods/Job.lua. One per job, not a pool: a reused
-- state would carry one mod's globals into another mod's job.
--
-- This is the file that makes running mod Lua off the main thread safe. The
-- mod's chunk is loaded into the SAME sandbox environment the main thread
-- builds (Sandbox.envFor), so love.filesystem, io, os, debug, ffi and package
-- are as absent here as they are there -- even though this state required
-- love.filesystem to bootstrap itself.
--
-- A job is pure compute: plain data in, plain data out, no engine API, no
-- game state, no storage. require is refused outright rather than reaching
-- src.* -- an engine module loaded in a second state would be a second
-- instance writing the same files as the main thread's.
require("love.thread")
require("love.filesystem")
require("love.timer")
local modId, scriptPath, argChannel, resultChannel, permissionsJson = ...
-- Fresh love threads have no "src.*" searcher (see src/net/fetch_worker.lua),
-- so install one before Sandbox's own requires run.
table.insert(package.loaders or package.searchers, function(name)
local path = name:gsub("%.", "/") .. ".lua"
if not love.filesystem.getInfo(path) then return nil end
return love.filesystem.load(path)
end)
local resCh = love.thread.getChannel(resultChannel)
local function fail(err)
resCh:push({ ok = false, err = tostring(err) })
end
local ok, err = pcall(function()
local Sandbox = require("src.mods.Sandbox")
local Json = require("src.link.Json")
local permissions = {}
if type(permissionsJson) == "string" and permissionsJson ~= "" then
local decoded = select(2, pcall(Json.decode, permissionsJson))
if type(decoded) == "table" then permissions = decoded end
end
local env = Sandbox.envFor({ modId = modId, permissions = permissions })
-- A job cannot reach the engine. Anything it needs comes in through its
-- argument and goes back through its return value.
env.require = function(name)
error(("[%s] require(%q) is not available inside a background job; a job "
.. "takes plain data and returns plain data"):format(modId,
tostring(name)), 2)
end
local chunk, loadErr = Sandbox.loadFile(love.filesystem, scriptPath, env)
if not chunk then error(loadErr or ("could not load " .. scriptPath), 0) end
local arg = love.thread.getChannel(argChannel):pop()
-- NO in-worker time budget, deliberately. A debug count hook was the
-- obvious way to stop a runaway, and it does not work: LuaJIT swallows an
-- error raised from a hook (measured: ~5000 raises a second, the loop
-- running straight through them), and the raising itself wedged the whole
-- process -- the main thread stopped being scheduled at all. Without the
-- hook a runaway job simply spins on its own core, the game stays
-- responsive, and it quits normally. Job.poll enforces maxSeconds on the
-- main thread so the MOD is never left waiting; the work itself runs to its
-- own end.
local ranOk, result = pcall(chunk, arg)
if not ranOk then error(result, 0) end
resCh:push({ ok = true, result = result })
end)
if not ok then fail(err) end