mirror of
https://github.com/MightyPirates/OpenComputers.git
synced 2025-09-08 14:50:51 -04:00
resumable computers; signals appear to work; fixed bug in allocator in jnlua
This commit is contained in:
parent
4e820da1f2
commit
0327cec937
Binary file not shown.
Binary file not shown.
210
assets/opencomputers/lua/boot.lua
Normal file
210
assets/opencomputers/lua/boot.lua
Normal file
@ -0,0 +1,210 @@
|
||||
--[[ Low level boot logic. ]]
|
||||
|
||||
--[[ Argument checking for functions. ]]
|
||||
function checkType(n, have, ...)
|
||||
have = type(have)
|
||||
for _, want in pairs({...}) do
|
||||
if have == want then return end
|
||||
end
|
||||
error("bad argument #" .. n .. " (" .. table.concat({...}, " or ") ..
|
||||
" expected, got " .. have .. ")", 3)
|
||||
end
|
||||
|
||||
--[[ The following code is used to allow making tables read-only. It is, by and
|
||||
large, inspired by http://lua-users.org/wiki/RecursiveReadOnlyTables
|
||||
We override some library functions in the sandbox we create to enforce
|
||||
honoring the fact that tables are readonly, such as rawset.
|
||||
--]]
|
||||
|
||||
-- Store metatables that have been made read-only. This allows us to uniquely
|
||||
-- identify such tables, without users being able to fake it.
|
||||
local roproxies = setmetatable({}, {__mode="kv"}) -- real table -> proxy
|
||||
local rometatables = setmetatable({}, {__mode="k"}) -- proxy -> metatable
|
||||
|
||||
--[[ Create a read-only proxy of a table. ]]
|
||||
function table.asreadonly(t)
|
||||
checkType(1, t, "table")
|
||||
local wrap
|
||||
local function roindex(t)
|
||||
return function(_, k)
|
||||
local value = t[k]
|
||||
if type(value) == "table" then
|
||||
value = wrap(value) -- wrap is cached
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
local function ronewindex(_, _, _)
|
||||
error("trying to modify read-only table", 2)
|
||||
end
|
||||
local function ropairs(t)
|
||||
local function ronext(_, k)
|
||||
local nk, nv = next(t, k)
|
||||
if type(nv) == "table" then
|
||||
nv = wrap(nv) -- wrap is cached
|
||||
end
|
||||
return nk, nv
|
||||
end
|
||||
return function(_)
|
||||
return ronext, nil, nil
|
||||
end
|
||||
end
|
||||
local function wrap(t)
|
||||
if not roproxies[t] then
|
||||
local metatable = { __index = roindex(t),
|
||||
__newindex = ronewindex,
|
||||
__pairs = ropairs(t),
|
||||
__metatable = "read only table" }
|
||||
rometatables[proxy] = metatable
|
||||
roproxies[t] = setmetatable({}, metatable)
|
||||
end
|
||||
return roproxies[t]
|
||||
end
|
||||
return wrap(t)
|
||||
end
|
||||
|
||||
--[[ Allow checking if a table is read-only. ]]
|
||||
function table.isreadonly(t)
|
||||
return rometatables[t] ~= nil
|
||||
end
|
||||
|
||||
--[[ Because I need this this every so often I decided to include it in the
|
||||
base API. This allows copying tables shallow or deep, to a new table or
|
||||
into an existing one. Usage:
|
||||
table.copy(t) -- new table, shallow copy
|
||||
table.copy(t, true) -- new table, deep copy
|
||||
table.copy(t1, t2) -- copy t1 to t2, shallow copy
|
||||
table.copy(t1, t2, true) -- copy t1 to t2, deep copy
|
||||
--]]
|
||||
function table.copy(from, to, deep)
|
||||
checkType(1, from, "table")
|
||||
checkType(2, to, "table", "boolean", "nil")
|
||||
checkType(3, deep, "boolean", "nil")
|
||||
|
||||
deep = deep or (to and type(to) ~= "table")
|
||||
local copied, shallowcopy, deepcopy = {}
|
||||
function shallowcopy(from, to)
|
||||
for k, v in pairs(from) do
|
||||
to[k] = (deep and type(v) == "table") and deepcopy(v) or v
|
||||
end
|
||||
return to
|
||||
end
|
||||
function deepcopy(t)
|
||||
if copied[t] then return copied[t] end
|
||||
copied[t] = {}
|
||||
return shallowcopy(t, copied[t])
|
||||
end
|
||||
return shallowcopy(from, type(to) == "table" and to or {})
|
||||
end
|
||||
|
||||
--[[ Wrap all driver callbacks.
|
||||
|
||||
For each driver we generate a wrapper that will yield a closure that
|
||||
will perform the actual call. This way the actual call can be performed
|
||||
in the server thread, meaning we don't have to worry about mutlithreading
|
||||
interaction with other components of Minecraft.
|
||||
--]]
|
||||
do
|
||||
-- OK, I admit this is a little crazy... here goes:
|
||||
local function wrap(f)
|
||||
-- This is the function that replaces the original API function. It is
|
||||
-- called from userland when it wants something from a driver.
|
||||
return function(...)
|
||||
local args = {...}
|
||||
-- What it does, is that it yields a function. That function is called
|
||||
-- from the server thread, to ensure synchronicity with the world.
|
||||
local result = coroutine.yield(function()
|
||||
-- It runs the actual API function protected mode. We return this as
|
||||
-- a table because a) we need it like this on the outside anyway and
|
||||
-- b) only the first item in the global stack is persisted.
|
||||
return {pcall(f, table.unpack(args))}
|
||||
end)
|
||||
-- The next time our executor runs it pushes that result and calls
|
||||
-- resume, so we get it via the yield. Thus: result = pcall(f, ...)
|
||||
if result[1] then
|
||||
-- API call was successful, return the results.
|
||||
return select(2, table.unpack(result))
|
||||
else
|
||||
-- API call failed, re-throw the error. We apply tostring to it
|
||||
-- because JNLua pushes the original Java exceptions.
|
||||
error(tostring(result[2]), 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- There really shouldn't be any cycles in the API table, but to be safe...
|
||||
local done = {}
|
||||
local function wrapRecursive(t)
|
||||
if done[t] then return end
|
||||
done[t] = true
|
||||
for k, v in pairs(t) do
|
||||
if type(v) == "function" then
|
||||
t[k] = wrap(v)
|
||||
elseif type(v) == "table" then
|
||||
wrapRecursive(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
wrapRecursive(drivers)
|
||||
end
|
||||
|
||||
--[[ Permanent value tables.
|
||||
|
||||
These tables must contain all java callbacks (i.e. C functions, since
|
||||
they are wrapped on the native side using a C function, of course).
|
||||
They are used when persisting/unpersisting the state so that the
|
||||
persistence library knows which values it doesn't have to serialize
|
||||
(since it cannot persist C functions).
|
||||
These tables may change after loading a game, for example due to a new
|
||||
mod being installed or an old one being removed. In that case, the
|
||||
persistence library will throw an error while unpersisting, leading
|
||||
to what will essentially be a computer crash; which is pretty much
|
||||
the best way to tackle this, I think.
|
||||
--]]
|
||||
do
|
||||
local perms, uperms = {}, {}
|
||||
|
||||
--[[ Used by the Java side to persist the state when the world is saved. ]]
|
||||
function persist(kernel)
|
||||
return eris.persist(perms, kernel)
|
||||
end
|
||||
|
||||
--[[ Used by the Java side unpersist the state when the world is loaded. ]]
|
||||
function unpersist(value)
|
||||
if value and type(value) == "string" and value:len() > 0 then
|
||||
return eris.unpersist(uperms, value)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Flattens nested tables to concatenate field names with points. This is
|
||||
done to ensure we don't have any duplicates and to get the perm "names".
|
||||
--]]
|
||||
local function flattenAndStore(k, v)
|
||||
-- We only care for tables and functions, any value types are safe.
|
||||
if type(v) == "table" or type(v) == "function" then
|
||||
assert(uperms[k] == nil, "duplicate permanent value named " .. k)
|
||||
-- If we have aliases its enough to store the value once.
|
||||
if perms[v] then return end
|
||||
perms[v] = k
|
||||
uperms[k] = v
|
||||
-- Recurse into tables.
|
||||
if type(v) == "table" then
|
||||
-- Enforce a deterministic order when determining the keys, to ensure
|
||||
-- the keys are the same when unpersisting again.
|
||||
local keys = {}
|
||||
for ck, _ in pairs(v) do
|
||||
table.insert(keys, ck)
|
||||
end
|
||||
table.sort(keys)
|
||||
for _, ck in ipairs(keys) do
|
||||
flattenAndStore(k .. "." .. ck, v[ck])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Mark everything that's globally reachable at this point as permanent.
|
||||
flattenAndStore("_ENV", _ENV)
|
||||
end
|
@ -1,155 +1,339 @@
|
||||
--[[
|
||||
Basic OS functionality, such as launching new programs and loading drivers.
|
||||
--[[ Basic functionality, drives userland and enforces timeouts.
|
||||
|
||||
This is called as the main coroutine by the computer. If this throws, the
|
||||
computer crashes. If this returns, the computer is considered powered off.
|
||||
]]
|
||||
This is called as the main coroutine by the computer. If this throws, the
|
||||
computer crashes. If this returns, the computer is considered powered off.
|
||||
--]]
|
||||
|
||||
--[[ Will be adjusted by the kernel when running, represents how long we can
|
||||
continue running without yielding. Used in the debug hook that enforces
|
||||
this timeout by throwing an error if it's exceeded. ]]
|
||||
local deadline = 0
|
||||
|
||||
--[[ The hook installed for process coroutines enforcing the timeout. ]]
|
||||
local function timeoutHook()
|
||||
local now = os.clock()
|
||||
if now > deadline then
|
||||
error({timeout=debug.traceback(2)}, 0)
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Set up the global environment we make available to userland programs. ]]
|
||||
local function buildSandbox()
|
||||
local sandbox = {
|
||||
-- Top level values. The selection of kept methods rougly follows the list
|
||||
-- as available on the Lua wiki here: http://lua-users.org/wiki/SandBoxes
|
||||
assert = assert,
|
||||
error = error,
|
||||
pcall = pcall,
|
||||
xpcall = xpcall,
|
||||
|
||||
ipairs = ipairs,
|
||||
next = next,
|
||||
pairs = pairs,
|
||||
|
||||
rawequal = rawequal,
|
||||
rawget = rawget,
|
||||
rawlen = rawlen,
|
||||
rawset = rawset,
|
||||
|
||||
select = select,
|
||||
type = type,
|
||||
tonumber = tonumber,
|
||||
tostring = tostring,
|
||||
|
||||
-- We don't care what users do with metatables. The only raised concern was
|
||||
-- about breaking an environment, and we don't care about that.
|
||||
getmetatable = getmetatable,
|
||||
setmetatable = setmetatable,
|
||||
|
||||
-- Custom print that actually writes to the screen buffer.
|
||||
print = print,
|
||||
|
||||
bit32 = {
|
||||
arshift = bit32.arshift,
|
||||
band = bit32.band,
|
||||
bnot = bit32.bnot,
|
||||
bor = bit32.bor,
|
||||
btest = bit32.btest,
|
||||
bxor = bit32.bxor,
|
||||
extract = bit32.extract,
|
||||
replace = bit32.replace,
|
||||
lrotate = bit32.lrotate,
|
||||
lshift = bit32.lshift,
|
||||
rrotate = bit32.rrotate,
|
||||
rshift = bit32.rshift
|
||||
},
|
||||
|
||||
coroutine = {
|
||||
create = coroutine.create,
|
||||
resume = coroutine.resume,
|
||||
running = coroutine.running,
|
||||
status = coroutine.status,
|
||||
wrap = coroutine.wrap,
|
||||
yield = coroutine.yield
|
||||
},
|
||||
|
||||
math = {
|
||||
abs = math.abs,
|
||||
acos = math.acos,
|
||||
asin = math.asin,
|
||||
atan = math.atan,
|
||||
atan2 = math.atan2,
|
||||
ceil = math.ceil,
|
||||
cos = math.cos,
|
||||
cosh = math.cosh,
|
||||
deg = math.deg,
|
||||
exp = math.exp,
|
||||
floor = math.floor,
|
||||
fmod = math.fmod,
|
||||
frexp = math.frexp,
|
||||
huge = math.huge,
|
||||
ldexp = math.ldexp,
|
||||
log = math.log,
|
||||
max = math.max,
|
||||
min = math.min,
|
||||
modf = math.modf,
|
||||
pi = math.pi,
|
||||
pow = math.pow,
|
||||
rad = math.rad,
|
||||
random = math.random,
|
||||
randomseed = math.randomseed,
|
||||
sin = math.sin,
|
||||
sinh = math.sinh,
|
||||
sqrt = math.sqrt,
|
||||
tan = math.tan,
|
||||
tanh = math.tanh
|
||||
},
|
||||
|
||||
os = {
|
||||
clock = os.clock,
|
||||
date = os.date,
|
||||
difftime = os.difftime,
|
||||
time = os.time,
|
||||
freeMemory = os.freeMemory,
|
||||
totalMemory = os.totalMemory
|
||||
},
|
||||
|
||||
string = {
|
||||
byte = string.byte,
|
||||
char = string.char,
|
||||
dump = string.dump,
|
||||
find = string.find,
|
||||
format = string.format,
|
||||
gmatch = string.gmatch,
|
||||
gsub = string.gsub,
|
||||
len = string.len,
|
||||
lower = string.lower,
|
||||
match = string.match,
|
||||
rep = string.rep,
|
||||
reverse = string.reverse,
|
||||
sub = string.sub,
|
||||
upper = string.upper
|
||||
},
|
||||
|
||||
table = {
|
||||
concat = table.concat,
|
||||
insert = table.insert,
|
||||
pack = table.pack,
|
||||
remove = table.remove,
|
||||
sort = table.sort,
|
||||
unpack = unpack,
|
||||
-- Custom functions.
|
||||
copy = table.copy,
|
||||
asreadonly = table.asreadonly,
|
||||
isreadonly = table.isreadonly
|
||||
}
|
||||
}
|
||||
sandbox._G = sandbox
|
||||
|
||||
-- Allow sandboxes to load code, but only in text form, and in the sandbox.
|
||||
-- Note that we allow passing a custom environment, because if this is called
|
||||
-- from inside the sandbox, env must already be in the sandbox.
|
||||
function sandbox.load(code, env)
|
||||
return load(code, nil, "t", env or sandbox)
|
||||
end
|
||||
|
||||
-- Make methods respect the read-only aspect of tables.
|
||||
do
|
||||
local function checkreadonly(t)
|
||||
if table.isreadonly(t) then
|
||||
error("trying to modify read-only table", 3)
|
||||
end
|
||||
end
|
||||
function sandbox.rawset(t, k, v)
|
||||
checkreadonly(t)
|
||||
rawset(t, k, v)
|
||||
end
|
||||
function sandbox.table.insert(t, k, v)
|
||||
checkreadonly(t)
|
||||
table.insert(t, k, v)
|
||||
end
|
||||
function sandbox.table.remove(t, k)
|
||||
checkreadonly(t)
|
||||
table.remove(t, k)
|
||||
end
|
||||
function sandbox.table.sort(t, f)
|
||||
checkreadonly(t)
|
||||
table.sort(t, f)
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Error thrower available to the userland. Used to differentiate system
|
||||
errors from user errors, such as timeouts (we rethrow system errors).
|
||||
--]]
|
||||
function sandbox.error(message, level)
|
||||
level = math.max(0, level or 1)
|
||||
error({message=message}, level > 0 and level + 1 or 0)
|
||||
end
|
||||
|
||||
local function checkResult(success, result, ...)
|
||||
if success then
|
||||
return success, result, ...
|
||||
end
|
||||
if result.timeout then
|
||||
error({timeout=result.timeout .. "\n" .. debug.traceback(2)}, 0)
|
||||
end
|
||||
return success, result.message
|
||||
end
|
||||
|
||||
function sandbox.pcall(f, ...)
|
||||
return checkResult(pcall(f, ...))
|
||||
end
|
||||
|
||||
function sandbox.xpcall(f, msgh, ...)
|
||||
function handler(msg)
|
||||
return msg.message and {message=msgh(msg.message)} or msg
|
||||
end
|
||||
return checkResult(xpcall(f, handler, ...))
|
||||
end
|
||||
|
||||
--[[ Install wrappers for coroutine management that reserves the first value
|
||||
returned by yields for internal stuff.
|
||||
--]]
|
||||
function sandbox.coroutine.yield(...)
|
||||
return coroutine.yield(nil, ...)
|
||||
end
|
||||
function sandbox.coroutine.resume(co, ...)
|
||||
if not debug.gethook(co) then -- Don't reset counter.
|
||||
debug.sethook(co, timeoutHook, "", 10000)
|
||||
end
|
||||
local result = {checkResult(coroutine.resume(co, ...))}
|
||||
if result[1] and result[2] then
|
||||
-- Internal yield, bubble up to the top and resume where we left off.
|
||||
return coroutine.resume(co, coroutine.yield(result[2]))
|
||||
end
|
||||
-- Userland yield or error, just pass it on.
|
||||
return result[1], select(3, table.unpack(result))
|
||||
end
|
||||
|
||||
--[[ Suspends the computer for the specified amount of time. Note that
|
||||
signal handlers will still be called if a signal arrives.
|
||||
--]]
|
||||
function sandbox.os.sleep(seconds)
|
||||
checkType(1, seconds, "number")
|
||||
local target = os.clock() + seconds
|
||||
while os.clock() < target do
|
||||
-- Yielding a number here will tell the host it can wait with running us
|
||||
-- again for that long. Note that this is *not* a sleep! We may be resumed
|
||||
-- way sooner, e.g. because of signals or a state load (after an unload).
|
||||
-- That's why we put a loop around the thing.
|
||||
coroutine.yield(seconds)
|
||||
end
|
||||
end
|
||||
|
||||
return sandbox
|
||||
end
|
||||
|
||||
local function main()
|
||||
--[[
|
||||
A copy the globals table to avoid user-space programs messing with us. The
|
||||
actual copy is created below, because we first need to declare the table copy
|
||||
function... which in turn uses this variable to avoid being tampered with.
|
||||
]]
|
||||
local g = _G
|
||||
--[[ Create the sandbox as a thread-local variable so it is persisted. ]]
|
||||
local sandbox = buildSandbox()
|
||||
|
||||
-- List of all active processes.
|
||||
local processes = {}
|
||||
|
||||
-- The ID of the process currently running.
|
||||
local currentProcess = 0
|
||||
|
||||
--[[
|
||||
Returns the process ID of the currently executing process.
|
||||
]]
|
||||
function _G.os.pid()
|
||||
return currentProcess
|
||||
end
|
||||
|
||||
-- Starts a new process using the specified callback.
|
||||
function _G.os.execute(task)
|
||||
local callback = task
|
||||
if g.type(task) == "string" then
|
||||
-- Check if we have a file system, load script and set callback.
|
||||
-- TODO ...
|
||||
g.setfenv(callback, g.setmetatable({}, {__index = _G}))
|
||||
end
|
||||
g.table.insert(processes, {
|
||||
pid = #processes,
|
||||
thread = g.coroutine.create(callback),
|
||||
parent = currentProcess,
|
||||
sleep = 0,
|
||||
signals = {}
|
||||
})
|
||||
end
|
||||
|
||||
--[[ Stops the process currently being executed. ]]
|
||||
function _G.os.exit()
|
||||
g.coroutine.yield("terminate")
|
||||
end
|
||||
|
||||
--[[ Makes the current process sleep for the specified amount of time. ]]
|
||||
function _G.os.sleep(seconds)
|
||||
assert(g.type(seconds) == "number",
|
||||
g.string.format("'number' expected, got '%s'", g.type(seconds)))
|
||||
processes[currentProcess].sleep = g.os.clock() + seconds
|
||||
while processes[currentProcess].sleep > g.os.clock() do
|
||||
local signal = {g.coroutine.yield()}
|
||||
if signal[1] then
|
||||
processes[currentProcess][signal[1]](g.unpack(signal))
|
||||
end
|
||||
end
|
||||
end
|
||||
--[[ List of signal handlers, by name. ]]
|
||||
local signals = setmetatable({}, {__mode = "v"})
|
||||
|
||||
--[[ Registers or unregisters a callback for a signal. ]]
|
||||
function _G.os.signal(name, callback)
|
||||
assert(g.type(name) == "string" and g.type(callback) == "function",
|
||||
g.string.format("'string', 'function' expected, got '%s', '%s'",
|
||||
g.type(name), g.type(callback)))
|
||||
processes[currentProcess][name] = callback
|
||||
function sandbox.os.signal(name, callback)
|
||||
checkType(1, name, "string")
|
||||
checkType(2, callback, "function", "nil")
|
||||
|
||||
local oldCallback = signals[name]
|
||||
signals[name] = callback
|
||||
return oldCallback
|
||||
end
|
||||
|
||||
-- We replace the default yield function so that be can differentiate between
|
||||
-- process level yields and coroutine level yields - in case a process spawns
|
||||
-- new coroutines.
|
||||
--[[
|
||||
function _G.coroutine.yield(...)
|
||||
while true do
|
||||
local result = {g.coroutine.yield(nil, ...)}
|
||||
if result[1] == "signal" then
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _G.coroutine.resume(...)
|
||||
while true do
|
||||
local result = {g.coroutine.resume(...)}
|
||||
if result[1] and result[2] == "signal" then
|
||||
|
||||
--[[ Error handler for signal callbacks. ]]
|
||||
local function onSignalError(msg)
|
||||
if type(msg) == "table" then
|
||||
if msg.timeout then
|
||||
msg = "too long without yielding"
|
||||
else
|
||||
return result[1], g.select(3, g.unpack(result))
|
||||
msg = msg.message
|
||||
end
|
||||
end
|
||||
print(msg)
|
||||
return msg
|
||||
end
|
||||
]]
|
||||
|
||||
-- Create the actual copy now that we have our copying function.
|
||||
g = g.table.copy(g, true)
|
||||
--[[ Set up the shell, which is really just a Lua interpreter. ]]
|
||||
local shellThread
|
||||
local function startShell(safeMode)
|
||||
-- Set sandbox environment and create the shell runner.
|
||||
local _ENV = sandbox
|
||||
local function shell()
|
||||
function test(arg)
|
||||
print(string.format("%d SIGNAL! Available RAM: %d/%d",
|
||||
os.time(), os.freeMemory(), os.totalMemory()))
|
||||
end
|
||||
os.signal("test", test)
|
||||
|
||||
-- Spawn the init process, which is basically a Lua interpreter.
|
||||
g.os.execute(function() print("hi") os.sleep(5) end)
|
||||
local i = 0
|
||||
while true do
|
||||
i = i + 1
|
||||
print("ping " .. i)
|
||||
os.sleep(1)
|
||||
end
|
||||
end
|
||||
shellThread = coroutine.create(shell)
|
||||
end
|
||||
startShell()
|
||||
|
||||
print("Running kernel...")
|
||||
|
||||
while true do
|
||||
print("ping")
|
||||
g.coroutine.yield(5)
|
||||
end
|
||||
|
||||
-- Begin running our processes. We run all processes consecutively if they are
|
||||
-- currently "awake", meaning not waiting for a call to os.sleep to return. If
|
||||
-- a signal arrives and the process has a callback for it, it is still resumed,
|
||||
-- though, to call the callback.
|
||||
-- If all processes are asleep, we yield the minimum sleep time, so that we can
|
||||
-- avoid busy waiting (resuming the main coroutine over and over again).
|
||||
local sleep = 0
|
||||
while #processes > 0 do
|
||||
local signal = {g.coroutine.yield(sleep)}
|
||||
local signalPid = signal[1]
|
||||
local signalName = signal[2]
|
||||
local signalArgs = g.select(3, g.unpack(signal))
|
||||
|
||||
for _, process in ipairs(processes) do
|
||||
local awake = g.os.clock() >= process.sleep
|
||||
local target = process.signals[signalName] and
|
||||
(signalPid < 1 or signalPid == process.pid)
|
||||
if awake or target then
|
||||
currentProcess = process.pid
|
||||
local result, cause = g.coroutine.resume(process.thread, "signal", signalName, g.unpack(signalArgs))
|
||||
if not result or g.coroutine.status(process.thread) == "dead" then
|
||||
process.thread = nil
|
||||
elseif cause == "terminate" then
|
||||
process.thread = nil
|
||||
-- Pending signal to be processed. We only either process a signal *or* run
|
||||
-- the shell, to keep the chance of long blocks low (and "accidentally" going
|
||||
-- over the timeout).
|
||||
local signal
|
||||
while coroutine.status(shellThread) ~= "dead" do
|
||||
deadline = os.clock() + 5
|
||||
local result
|
||||
if signal and signals[signal[1]] then
|
||||
xpcall(signals[signal[1]], onSignalError, select(2, table.unpack(signal)))
|
||||
else
|
||||
if not debug.gethook(shellThread) then
|
||||
debug.sethook(shellThread, timeoutHook, "", 10000)
|
||||
end
|
||||
local status = {coroutine.resume(shellThread)}
|
||||
if status[1] then
|
||||
-- All is well, in case we had a yield return the yielded value.
|
||||
if coroutine.status(shellThread) ~= "dead" then
|
||||
result = status[2]
|
||||
end
|
||||
end
|
||||
end
|
||||
sleep = g.math.huge
|
||||
for i = #processes, 1, -1 do
|
||||
if processes[i].thread == nil then
|
||||
g.table.remove(processes, i)
|
||||
elseif status[2].timeout then
|
||||
-- Timeout, restart the shell but don't start user scripts this time.
|
||||
startShell(true)
|
||||
else
|
||||
sleep = g.math.min(processes[i].sleep, sleep)
|
||||
-- Some other error, go kill ourselves.
|
||||
error(result[2], 0)
|
||||
end
|
||||
end
|
||||
sleep = g.math.max(0, sleep - g.os.clock())
|
||||
signal = {coroutine.yield(result)}
|
||||
end
|
||||
end
|
||||
|
||||
-- local result, message = pcall1(function()
|
||||
-- JNLua (or possibly Lua itself) sucks at propagating error messages across
|
||||
-- resumes that were triggered from the native side, so we do a pcall here.
|
||||
local result, message = pcall(main)
|
||||
-- if not result then error(message) end end)
|
||||
|
||||
if not result then
|
||||
print(message)
|
||||
end
|
||||
--if not result then
|
||||
print(result, message)
|
||||
--end
|
||||
return result, message
|
@ -1,172 +0,0 @@
|
||||
--[[
|
||||
This script sets up the environment to properly allow persisting the state
|
||||
after this point, regardless of what the following code does.
|
||||
]]
|
||||
|
||||
-- Keep a backup of globals that we use for direct access.
|
||||
local g = {
|
||||
assert = assert,
|
||||
error = error,
|
||||
next = next,
|
||||
pcall = pcall,
|
||||
type = type,
|
||||
unpack = unpack,
|
||||
coroutine = {
|
||||
create = coroutine.create,
|
||||
resume = coroutine.resume,
|
||||
status = coroutine.status,
|
||||
yield = coroutine.yield
|
||||
},
|
||||
debug = {
|
||||
traceback = debug.traceback
|
||||
}
|
||||
}
|
||||
_G.debug = nil
|
||||
|
||||
--[[
|
||||
Replace functions known to call back to Lua with ones writtin in Lua. This
|
||||
is necessary for yielding to work in Lua 5.1, where it's not possible to
|
||||
yield across C stack frames. Note that even though this works in 5.2, it's
|
||||
probably nigh impossible to serialize such yielded coroutines, exactly
|
||||
because of the C stack frames.
|
||||
]]
|
||||
|
||||
-- Wrap all native functions with a wrapper that generates a traceback.
|
||||
local function wrapper(f, name)
|
||||
return function(...)
|
||||
local result = {g.pcall(f, ...)}
|
||||
if result[1] then
|
||||
return g.unpack(result, 2)
|
||||
else
|
||||
g.error(g.debug.traceback(result[2], 2), 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
local function wrap(t)
|
||||
local walked = {}
|
||||
local towrap = {}
|
||||
local function walk(t)
|
||||
for k, v in pairs(t) do
|
||||
if not walked[v] then
|
||||
walked[v] = true
|
||||
if type(v) == "function" then
|
||||
table.insert(towrap, {t, k, v})
|
||||
elseif type(v) == "table" then
|
||||
walk(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
walk(t)
|
||||
for _, v in ipairs(towrap) do
|
||||
local t, k, v = unpack(v)
|
||||
t[k] = wrapper(v, k)
|
||||
end
|
||||
end
|
||||
wrap(_G)
|
||||
|
||||
function _G.xpcall(...)
|
||||
local args = {...}
|
||||
g.assert(#args > 1, "bad argument #2 to 'xpcall' (value expected)")
|
||||
local f = args[1]
|
||||
local msgh = args[2]
|
||||
local result
|
||||
if g.type(f) == "function" then
|
||||
local co = g.coroutine.create(f)
|
||||
result = {g.coroutine.resume(co, unpack(args, 3))}
|
||||
while g.coroutine.status(co) ~= "dead" do
|
||||
result = {g.coroutine.resume(co, g.coroutine.yield(g.unpack(result, 2)))}
|
||||
end
|
||||
else
|
||||
result = {false, "attempt to call a " .. g.type(f) .. " value"}
|
||||
end
|
||||
if result[1] then
|
||||
return g.unpack(result)
|
||||
end
|
||||
if g.type(msgh) == "function" then
|
||||
local ok, message = g.pcall(msgh, g.unpack(result, 2))
|
||||
if ok then
|
||||
return false, message
|
||||
end
|
||||
end
|
||||
return false, "error in error handling"
|
||||
end
|
||||
|
||||
g.xpcall = xpcall
|
||||
local function passthrough(msg) return msg end
|
||||
function _G.pcall(f, ...)
|
||||
return g.xpcall(f, passthrough, ...)
|
||||
end
|
||||
|
||||
function _G.ipairs(...)
|
||||
local args = {...}
|
||||
g.assert (#args > 0, "bad argument #1 to 'ipairs' (table expected, got no value)")
|
||||
local t = args[1]
|
||||
g.assert(g.type(t) == "table", "bad argument #1 to 'ipairs' (table expected, got" .. g.type(t) .. ")")
|
||||
return function(t, idx)
|
||||
idx = idx + 1
|
||||
local value = t[idx]
|
||||
if value then
|
||||
return idx, value
|
||||
end
|
||||
end, t, 0
|
||||
end
|
||||
|
||||
function _G.pairs(...)
|
||||
local args = {...}
|
||||
g.assert (#args > 0, "bad argument #1 to 'pairs' (table expected, got no value)")
|
||||
local t = args[1]
|
||||
g.assert(g.type(t) == "table", "bad argument #1 to 'pairs' (table expected, got" .. g.type(t) .. ")")
|
||||
return g.next, t, nil
|
||||
end
|
||||
|
||||
function _G.coroutine.wrap(...)
|
||||
local args = {...}
|
||||
g.assert (#args > 0, "bad argument #1 to 'wrap' (function expected, got no value)")
|
||||
local f = args[1]
|
||||
g.assert(g.type(f) == "function", "bad argument #1 to 'wrap' (function expected, got" .. g.type(f) .. ")")
|
||||
local co = g.coroutine.create(f)
|
||||
return function(...)
|
||||
local result = {g.coroutine.resume(co, ...)}
|
||||
if result[1] then
|
||||
return g.unpack(result, 2)
|
||||
else
|
||||
g.error(result[2], 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Build Pluto's permanent value tables. ]]
|
||||
local perms, uperms = {[_ENV] = "_ENV"}, {["_ENV"] = _ENV}
|
||||
|
||||
-- Flattens nested tables to concatenate field names with points. This is done
|
||||
-- to ensure we don't have any duplicates and to get the perm "names".
|
||||
local function store(t)
|
||||
if not t then return end
|
||||
local done = {}
|
||||
local function flattenAndStore(k, v)
|
||||
if type(v) == "table" then
|
||||
if not done[v] then
|
||||
done[v] = true
|
||||
local prefix = k .. "."
|
||||
for k, v in pairs(v) do
|
||||
flattenAndStore(prefix .. k, v)
|
||||
end
|
||||
end
|
||||
elseif type(v) == "function" then
|
||||
assert(uperms[k] == nil, "duplicate permanent value named " .. k)
|
||||
-- If we have aliases its enough to store the value once.
|
||||
if not perms[v] then
|
||||
perms[v] = k
|
||||
uperms[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
for k, v in pairs(t) do
|
||||
flattenAndStore(k, v)
|
||||
end
|
||||
end
|
||||
store(_G)
|
||||
store(...)
|
||||
|
||||
return perms, uperms
|
@ -1,235 +0,0 @@
|
||||
-- List of whitelisted globals, declared in advance because it's used by the
|
||||
-- functions we declare in here, too, to avoid tampering.
|
||||
local g
|
||||
|
||||
-- Until we get to ingame screens we log to Java's stdout. This is only used to
|
||||
-- report internal failures during startup. Once we reach the kernel print is
|
||||
-- replaced with a function that writes to the internal screen buffer.
|
||||
do
|
||||
local System, iprs, ts = java.require("java.lang.System"), ipairs, tostring
|
||||
_G.print = function(...)
|
||||
for _, value in iprs({...}) do
|
||||
System.out:print(ts(value))
|
||||
end
|
||||
System.out:println()
|
||||
end
|
||||
end
|
||||
|
||||
print("test")
|
||||
|
||||
--[[
|
||||
Install custom coroutine logic that forces coroutines to yield.
|
||||
|
||||
We replace the core functions: create, resume and yield.
|
||||
- create is replaced with a function that periodically forces the created
|
||||
coroutine to yield a string with the value "timeout".
|
||||
- yield is replaced with a function that prepends any yielded values with a
|
||||
nil value. This is purely to allow differentiating voluntary (normal)
|
||||
yields from timeouts.
|
||||
- resume is replaced with a function that checks the first value returned
|
||||
from a yielding function. If we had a timeout we bubble upward, by also
|
||||
yielding with a timeout. Otherwise normal yield functionality applies.
|
||||
]]
|
||||
--[[
|
||||
do
|
||||
-- Keep a backup of this function because it will be removed from our sandbox.
|
||||
local create, resume, yield, unpack, sethook =
|
||||
coroutine.create, coroutine.resume, coroutine.yield,
|
||||
unpack, debug.sethook
|
||||
-- This is the function we install as the hook.
|
||||
local function check()
|
||||
-- TODO check if there's a C stack frame? (i.e. we missed something)
|
||||
yield("timeout")
|
||||
end
|
||||
-- Install our coroutine factory replacement which takes care of forcing the
|
||||
-- created coroutines to yield once in a while. This is primarily used to to
|
||||
-- avoid coroutines from blocking completely.
|
||||
function _G.coroutine.create(f)
|
||||
local co = create(f)
|
||||
sethook(co, check, "", 100000)
|
||||
return co
|
||||
end
|
||||
-- Replace yield function used from now on to be able to distinguish between
|
||||
-- voluntary and forced yields.
|
||||
function _G.coroutine.yield(...)
|
||||
return yield(nil, ...)
|
||||
end
|
||||
-- Replace the resume function with one that automatically forwards timeouts.
|
||||
function _G.coroutine.resume(...)
|
||||
while true do
|
||||
local result = {resume(...)}
|
||||
if result[1] and result[2] == "timeout" then
|
||||
return yield("timeout")
|
||||
else
|
||||
return result[1], unpack(result, 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
]]
|
||||
|
||||
--[[ Set up the global environment we make available to userspace programs. ]]
|
||||
g = {
|
||||
-- Top level values. The selection of kept methods rougly follows the list
|
||||
-- as available on the Lua wiki here: http://lua-users.org/wiki/SandBoxes
|
||||
-- Some entries have been kept although they are marked as unsafe on the
|
||||
-- wiki, due to how we set up our environment: we clear the globals table,
|
||||
-- so it does not matter if user-space functions gain access to the global
|
||||
-- environment. We pretty much give all user-space code full control to
|
||||
-- mess up the VM on the Lua side, we just want to make sure they can never
|
||||
-- reach out to the Java side in an unintended way.
|
||||
assert = assert,
|
||||
error = error,
|
||||
pcall = pcall,
|
||||
xpcall = xpcall,
|
||||
|
||||
ipairs = ipairs,
|
||||
next = next,
|
||||
pairs = pairs,
|
||||
|
||||
rawequal = rawequal,
|
||||
rawget = rawget,
|
||||
rawset = rawset,
|
||||
|
||||
select = select,
|
||||
unpack = unpack,
|
||||
type = type,
|
||||
tonumber = tonumber,
|
||||
tostring = tostring,
|
||||
|
||||
-- Loadstring is OK because it's OK that the loaded chunk is in the global
|
||||
-- environment as mentioned in the comment above.
|
||||
loadstring = loadstring,
|
||||
|
||||
-- We don't care what users do with metatables. The only raised concern was
|
||||
-- about breaking an environment, and we don't care about that.
|
||||
getmetatable = getmetatable,
|
||||
setmetatable = setmetatable,
|
||||
|
||||
-- Same goes for environment setters themselves. We do use local environments
|
||||
-- for loaded scripts, but that's more for convenience than for control.
|
||||
getfenv = getfenv,
|
||||
setfenv = setfenv,
|
||||
|
||||
-- Custom print that actually writes to the screen buffer.
|
||||
print = print,
|
||||
|
||||
coroutine = {
|
||||
create = coroutine.create,
|
||||
resume = coroutine.resume,
|
||||
running = coroutine.running,
|
||||
status = coroutine.status,
|
||||
wrap = coroutine.wrap,
|
||||
yield = coroutine.yield
|
||||
},
|
||||
|
||||
string = {
|
||||
byte = string.byte,
|
||||
char = string.char,
|
||||
dump = string.dump,
|
||||
find = string.find,
|
||||
format = string.format,
|
||||
gmatch = string.gmatch,
|
||||
gsub = string.gsub,
|
||||
len = string.len,
|
||||
lower = string.lower,
|
||||
match = string.match,
|
||||
rep = string.rep,
|
||||
reverse = string.reverse,
|
||||
sub = string.sub,
|
||||
upper = string.upper
|
||||
},
|
||||
|
||||
table = {
|
||||
concat = table.concat,
|
||||
insert = table.insert,
|
||||
maxn = table.maxn,
|
||||
remove = table.remove,
|
||||
sort = table.sort,
|
||||
--[[
|
||||
Because I need this this every so often I decided to include it in the
|
||||
base API. This allows copying tables shallow or deep, to a new table or
|
||||
into an existing one. Usage:
|
||||
table.copy(t) -- new table, shallow copy
|
||||
table.copy(t, true) -- new table, deep copy
|
||||
table.copy(t1, t2) -- copy t1 to t2, shallow copy
|
||||
table.copy(t1, t2, true) -- copy t1 to t2, deep copy
|
||||
]]
|
||||
copy =
|
||||
function(from, to, deep)
|
||||
g.assert(g.type(from) == "table",
|
||||
"bad argument #1 (table expected, got " .. g.type(from) .. ")")
|
||||
deep = deep or (other and g.type(other) ~= "table")
|
||||
local copied, shallowcopy, deepcopy = {}
|
||||
function shallowcopy(from, to)
|
||||
for k, v in g.pairs(from) do
|
||||
to[k] = (deep and g.type(v) == "table") and deepcopy(v) or v
|
||||
end
|
||||
return to
|
||||
end
|
||||
function deepcopy(t)
|
||||
if copied[t] then return copied[t] end
|
||||
copied[t] = {}
|
||||
return shallowcopy(t, copied[t])
|
||||
end
|
||||
return shallowcopy(from, g.type(to) == "table" and to or {})
|
||||
end
|
||||
},
|
||||
|
||||
math = {
|
||||
abs = math.abs,
|
||||
acos = math.acos,
|
||||
asin = math.asin,
|
||||
atan = math.atan,
|
||||
atan2 = math.atan2,
|
||||
ceil = math.ceil,
|
||||
cos = math.cos,
|
||||
cosh = math.cosh,
|
||||
deg = math.deg,
|
||||
exp = math.exp,
|
||||
floor = math.floor,
|
||||
fmod = math.fmod,
|
||||
frexp = math.frexp,
|
||||
huge = math.huge,
|
||||
ldexp = math.ldexp,
|
||||
log = math.log,
|
||||
log10 = math.log10,
|
||||
max = math.max,
|
||||
min = math.min,
|
||||
modf = math.modf,
|
||||
pi = math.pi,
|
||||
pow = math.pow,
|
||||
rad = math.rad,
|
||||
-- TODO Check if different Java LuaState's interfere via this. If so we
|
||||
-- may have to create a custom random instance to replace the built
|
||||
-- in random functionality of Lua.
|
||||
random = math.random,
|
||||
randomseed = math.randomseed,
|
||||
sin = math.sin,
|
||||
sinh = math.sinh,
|
||||
sqrt = math.sqrt,
|
||||
tan = math.tan,
|
||||
tanh = math.tanh
|
||||
},
|
||||
|
||||
os = {
|
||||
clock = os.clock,
|
||||
date = os.date,
|
||||
difftime = os.difftime,
|
||||
time = os.time,
|
||||
-- TODO Evaluate whether this can actually get dangerous.
|
||||
traceback = debug.traceback,
|
||||
},
|
||||
|
||||
debug = debug
|
||||
}
|
||||
|
||||
_G.table.copy = g.table.copy
|
||||
do return end
|
||||
|
||||
-- Clear the globals table (except for its self-reference) and copy sandbox.
|
||||
local copy = g.table.copy
|
||||
for k, _ in pairs(_G) do
|
||||
if k ~= "_G" then _G[k] = nil end
|
||||
end
|
||||
copy(g, _G)
|
@ -31,20 +31,4 @@ public @interface Callback {
|
||||
* The name under which the method will be available in Lua.
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Whether this API function is called synchronized with the server thread.
|
||||
*
|
||||
* Each computer runs its Lua state using a separate thread, to avoid slowing
|
||||
* down the game or lock it up completely. This means that when a state calls
|
||||
* a driver's API function this code is run in such a worker thread that is
|
||||
* not synchronized with the server thread (i.e. the main game thread). The
|
||||
* driver must therefore take care to avoid threading issues. For convenience
|
||||
* a driver can request that an API function is only called in sync with the
|
||||
* server thread. In that case the computer's thread will stop and wait for
|
||||
* the server thread to come along. The server thread will see that the
|
||||
* computer thread is waiting for it and perform it's API call for it, then
|
||||
* let it loose again and continue with what it was doing.
|
||||
*/
|
||||
boolean synchronize() default false;
|
||||
}
|
@ -7,14 +7,12 @@ class Computer(val owner: AnyRef) extends IInternalComputerContext {
|
||||
def luaState = null
|
||||
|
||||
def start() = false
|
||||
|
||||
def stop() {}
|
||||
|
||||
def update() {}
|
||||
|
||||
def lock() {}
|
||||
|
||||
def unlock() {}
|
||||
|
||||
def signal(pid: Int, name: String, args: Any*) {}
|
||||
def signal(name: String, args: Any*) {}
|
||||
|
||||
def readFromNBT(nbt: NBTTagCompound) {}
|
||||
|
||||
|
@ -59,19 +59,14 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) {
|
||||
override def createTileEntity(world: World, metadata: Int) = new TileEntityComputer(world.isRemote)
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// Block rotation
|
||||
// Destruction / Interaction
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
override def onBlockPlacedBy(world: World, x: Int, y: Int, z: Int, entity: EntityLivingBase, itemStack: ItemStack) {
|
||||
if (!world.isRemote) {
|
||||
val facing = MathHelper.floor_double(entity.rotationYaw * 4 / 360 + 0.5) & 3
|
||||
setRotation(world, x, y, z, facing)
|
||||
}
|
||||
override def breakBlock(world: World, x: Int, y: Int, z: Int, `side?`: Int, metadata: Int) = {
|
||||
world.getBlockTileEntity(x, y, z).asInstanceOf[TileEntityComputer].turnOff()
|
||||
super.breakBlock(world, x, y, z, `side?`, metadata)
|
||||
}
|
||||
|
||||
override def getValidRotations(world: World, x: Int, y: Int, z: Int) =
|
||||
Array(ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.NORTH, ForgeDirection.EAST)
|
||||
|
||||
|
||||
override def onBlockActivated(world: World, x: Int, y: Int, z: Int, player: EntityPlayer,
|
||||
side: Int, hitX: Float, hitY: Float, hitZ: Float) = {
|
||||
if (player.isSneaking())
|
||||
@ -85,6 +80,20 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) {
|
||||
true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// Block rotation
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
override def onBlockPlacedBy(world: World, x: Int, y: Int, z: Int, entity: EntityLivingBase, itemStack: ItemStack) {
|
||||
if (!world.isRemote) {
|
||||
val facing = MathHelper.floor_double(entity.rotationYaw * 4 / 360 + 0.5) & 3
|
||||
setRotation(world, x, y, z, facing)
|
||||
}
|
||||
}
|
||||
|
||||
override def getValidRotations(world: World, x: Int, y: Int, z: Int) =
|
||||
Array(ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.NORTH, ForgeDirection.EAST)
|
||||
|
||||
def rotation(world: IBlockAccess, x: Int, y: Int, z: Int) =
|
||||
// Renderer(down, up, north, south, west, east) -> Facing(south, west, north, east) inverted.
|
||||
Array(0, 0, 0, 2, 3, 1)(world.getBlockMetadata(x, y, z))
|
||||
|
@ -10,12 +10,10 @@ trait IInternalComputerContext extends IComputerContext {
|
||||
|
||||
def start(): Boolean
|
||||
|
||||
def stop(): Unit
|
||||
|
||||
def update()
|
||||
|
||||
def lock()
|
||||
|
||||
def unlock()
|
||||
|
||||
def readFromNBT(nbt: NBTTagCompound)
|
||||
|
||||
def writeToNBT(nbt: NBTTagCompound)
|
||||
|
@ -1,11 +1,21 @@
|
||||
package li.cil.oc.common.tileentity
|
||||
import li.cil.oc.server.computer.IComputerEnvironment
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import li.cil.oc.server.computer.IComputerEnvironment
|
||||
import net.minecraft.nbt.NBTTagCompound
|
||||
import net.minecraft.tileentity.TileEntity
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.event.ForgeSubscribe
|
||||
import net.minecraftforge.event.world.ChunkEvent
|
||||
import net.minecraftforge.event.world.WorldEvent
|
||||
|
||||
class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnvironment {
|
||||
def this() = this(false)
|
||||
MinecraftForge.EVENT_BUS.register(this)
|
||||
|
||||
private val hasChanged = new AtomicBoolean()
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// General
|
||||
// ----------------------------------------------------------------------- //
|
||||
@ -16,17 +26,49 @@ class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnv
|
||||
|
||||
def turnOn() = computer.start()
|
||||
|
||||
def turnOff() = computer.stop()
|
||||
|
||||
override def readFromNBT(nbt: NBTTagCompound) = {
|
||||
super.readFromNBT(nbt)
|
||||
computer.readFromNBT(nbt)
|
||||
}
|
||||
|
||||
override def writeToNBT(nbt: NBTTagCompound) = {
|
||||
println("SAVING")
|
||||
super.writeToNBT(nbt)
|
||||
computer.writeToNBT(nbt)
|
||||
}
|
||||
|
||||
override def updateEntity() = computer.update()
|
||||
override def updateEntity() = {
|
||||
computer.update()
|
||||
if (hasChanged.get())
|
||||
worldObj.updateTileEntityChunkAndDoNothing(
|
||||
this.xCoord, this.yCoord, this.zCoord, this)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// Event Bus
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
@ForgeSubscribe
|
||||
def onChunkUnload(e: ChunkEvent.Unload) = {
|
||||
println("CHUNK UNLOADING")
|
||||
MinecraftForge.EVENT_BUS.unregister(this)
|
||||
computer.stop()
|
||||
}
|
||||
|
||||
@ForgeSubscribe
|
||||
def onWorldUnload(e: WorldEvent.Unload) = {
|
||||
println("WORLD UNLOADING")
|
||||
MinecraftForge.EVENT_BUS.unregister(this)
|
||||
computer.stop()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// IComputerEnvironment
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
def world = worldObj
|
||||
|
||||
def markAsChanged() = hasChanged.set(true)
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
package li.cil.oc.server.computer
|
||||
|
||||
import java.util.concurrent._
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
import scala.Array.canBuildFrom
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.util.Random
|
||||
|
||||
import com.naef.jnlua._
|
||||
|
||||
@ -19,24 +18,6 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
// General
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
/** The internal Lua state. Only set while the computer is running. */
|
||||
private var lua: LuaState = null
|
||||
|
||||
/**
|
||||
* The base memory consumption of the kernel. Used to permit a fixed base
|
||||
* memory for user-space programs even if the amount of memory the kernel
|
||||
* uses changes over time (i.e. with future releases of the mod). This is set
|
||||
* when starting up the computer.
|
||||
*/
|
||||
private var baseMemory = 0
|
||||
|
||||
/**
|
||||
* The time when the computer was started. This is used for our custom
|
||||
* implementation of os.clock(), which returns the amount of the time the
|
||||
* computer has been running.
|
||||
*/
|
||||
private var timeStarted = 0.0
|
||||
|
||||
/**
|
||||
* The current execution state of the computer. This is used to track how to
|
||||
* resume the computers main thread, if at all, and whether to accept new
|
||||
@ -44,6 +25,17 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
*/
|
||||
private var state = State.Stopped
|
||||
|
||||
/** The internal Lua state. Only set while the computer is running. */
|
||||
private var lua: LuaState = null
|
||||
|
||||
/**
|
||||
* The base memory consumption of the kernel. Used to permit a fixed base
|
||||
* memory for userland even if the amount of memory the kernel uses changes
|
||||
* over time (i.e. with future releases of the mod). This is set when
|
||||
* starting up the computer.
|
||||
*/
|
||||
private var kernelMemory = 0
|
||||
|
||||
/**
|
||||
* The queue of signals the Lua state should process. Signals are queued from
|
||||
* the Java side and processed one by one in the Lua VM. They are the only
|
||||
@ -52,22 +44,14 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
*/
|
||||
private val signals = new LinkedBlockingQueue[Signal](256)
|
||||
|
||||
/**
|
||||
* This is used to keep track of the current executor of the Lua state, for
|
||||
* example to wait for the computer to finish running a task. This is used to
|
||||
* cancel scheduled execution when a new signal arrives and to wait for the
|
||||
* computer to shut down.
|
||||
*/
|
||||
private var future: Future[_] = null
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
/**
|
||||
* This lock is used by the thread executing the Lua state when it performs
|
||||
* a synchronized API call. In that case it acquires this lock and waits for
|
||||
* the server thread. The server thread will try to acquire the lock after
|
||||
* notifying the state thread, to make sure the call was complete before
|
||||
* resuming.
|
||||
* The time (world time) when the computer was started. This is used for our
|
||||
* custom implementation of os.clock(), which returns the amount of the time
|
||||
* the computer has been running.
|
||||
*/
|
||||
private val driverLock = new ReentrantLock()
|
||||
private var timeStarted = 0L
|
||||
|
||||
/**
|
||||
* The last time (system time) the update function was called by the server
|
||||
@ -76,42 +60,67 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
*/
|
||||
private var lastUpdate = 0L
|
||||
|
||||
/**
|
||||
* The current world time. This is used for our custom implementation of
|
||||
* os.time(). This is updated by the server thread and read by the computer
|
||||
* thread, to avoid computer threads directly accessing the world state.
|
||||
*/
|
||||
private var worldTime = 0L
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
/**
|
||||
* This is used to keep track of the current executor of the Lua state, for
|
||||
* example to wait for the computer to finish running a task.
|
||||
*/
|
||||
private var future: Future[_] = null
|
||||
|
||||
/**
|
||||
* The object our executor thread waits on if the last update has been a
|
||||
* while, and the update function calls notify on each time it is run.
|
||||
*/
|
||||
private val updateMonitor = new Object()
|
||||
private val pauseMonitor = new Object()
|
||||
|
||||
/** This is used to synchronize access to the state field. */
|
||||
private val stateMonitor = new Object()
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// State
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
/** Starts asynchronous execution of this computer if it isn't running. */
|
||||
def start(): Boolean = state match {
|
||||
case State.Stopped => {
|
||||
if (init()) {
|
||||
state = State.Running
|
||||
future = Executor.pool.submit(this)
|
||||
true
|
||||
def start(): Boolean = stateMonitor.synchronized(
|
||||
state == State.Stopped && init() && {
|
||||
state = State.Suspended
|
||||
// Inject a dummy signal so that real one don't get swallowed. This way
|
||||
// we can just ignore the parameters the first time the kernel is run.
|
||||
signal("dummy")
|
||||
future = Executor.pool.submit(this)
|
||||
true
|
||||
})
|
||||
|
||||
/** Stops a computer, possibly asynchronously. */
|
||||
def stop(): Unit = stateMonitor.synchronized {
|
||||
if (state != State.Stopped) {
|
||||
if (state != State.Running) {
|
||||
// If the computer is not currently running we can simply close it,
|
||||
// and cancel any pending future - which may already be running and
|
||||
// waiting for the stateMonitor, so we do a hard abort.
|
||||
if (future != null) {
|
||||
future.cancel(true)
|
||||
}
|
||||
close()
|
||||
}
|
||||
else {
|
||||
// Otherwise we enter an intermediate state to ensure the executor
|
||||
// truly stopped, before switching back to stopped to allow starting
|
||||
// the computer again. The executor will check for this state and
|
||||
// call close.
|
||||
state = State.Stopping
|
||||
// Make sure the thread isn't waiting for an update.
|
||||
pauseMonitor.synchronized(pauseMonitor.notify())
|
||||
}
|
||||
else false
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
|
||||
/** Stops a computer asynchronously. */
|
||||
def stop(): Unit = if (state != State.Stopped) {
|
||||
signals.clear()
|
||||
signal(0, "terminate")
|
||||
}
|
||||
|
||||
/** Stops a computer synchronously. */
|
||||
def stopAndWait(): Unit = {
|
||||
stop()
|
||||
// Get a local copy to avoid having to synchronize it between the null
|
||||
// check and the actual wait.
|
||||
val future = this.future
|
||||
if (future != null) future.get()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
@ -121,84 +130,102 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
def luaState = lua
|
||||
|
||||
def update() {
|
||||
updateMonitor.synchronized {
|
||||
if (state == State.Stopped) return
|
||||
|
||||
// Check if executor is waiting for a lock to interact with a driver.
|
||||
future.synchronized {
|
||||
if (state == State.Synchronizing) {
|
||||
// Thread is waiting to perform synchronized API call, notify it.
|
||||
future.notify()
|
||||
// Wait until the API call completed, which is when the driver lock
|
||||
// becomes available again (we lock it in the executor thread before
|
||||
// waiting to be notified). We need an extra lock for that because the
|
||||
// driver will release the lock on 'future' to do so (see lock()).
|
||||
driverLock.lock()
|
||||
driverLock.unlock()
|
||||
}
|
||||
stateMonitor.synchronized(state match {
|
||||
case State.Stopped | State.Stopping => return
|
||||
case State.DriverCall => {
|
||||
assert(lua.getTop() == 2)
|
||||
assert(lua.`type`(1) == LuaType.THREAD)
|
||||
assert(lua.`type`(2) == LuaType.FUNCTION)
|
||||
lua.resume(1, 1)
|
||||
assert(lua.getTop() == 2)
|
||||
assert(lua.`type`(2) == LuaType.TABLE)
|
||||
state = State.DriverReturn
|
||||
future = Executor.pool.submit(this)
|
||||
}
|
||||
case _ => /* nothing special to do */
|
||||
})
|
||||
|
||||
// Update last time run to let our executor thread know it doesn't have to
|
||||
// pause, and wake it up if it did pause (because the game was paused).
|
||||
lastUpdate = System.currentTimeMillis()
|
||||
updateMonitor.notify()
|
||||
// Remember when we started the computer for os.clock(). We do this in the
|
||||
// update because only then can we be sure the world is available.
|
||||
if (timeStarted == 0)
|
||||
timeStarted = owner.world.getWorldInfo().getWorldTotalTime()
|
||||
|
||||
// Update world time for computer threads.
|
||||
worldTime = owner.world.getWorldInfo().getWorldTotalTime()
|
||||
|
||||
if (worldTime % 40 == 0) {
|
||||
signal("test", "ha!")
|
||||
}
|
||||
|
||||
// Update last time run to let our executor thread know it doesn't have to
|
||||
// pause, and wake it up if it did pause (because the game was paused).
|
||||
lastUpdate = System.currentTimeMillis
|
||||
|
||||
// Tell the executor thread it may continue if it's waiting.
|
||||
pauseMonitor.synchronized(pauseMonitor.notify())
|
||||
}
|
||||
|
||||
def signal(pid: Int, name: String, args: Any*) = {
|
||||
def signal(name: String, args: Any*) = {
|
||||
args.foreach {
|
||||
case _: Byte | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit
|
||||
case _ => throw new IllegalArgumentException()
|
||||
}
|
||||
if (state != State.Stopped) {
|
||||
signals.offer(new Signal(pid, name, Array(args)))
|
||||
// TODO cancel delayed future and schedule for immediate execution
|
||||
// if (this.synchronized(!signals.isEmpty() && state == State.Stopped)) {
|
||||
// state = State.Running
|
||||
// Executor.pool.execute(this)
|
||||
// }
|
||||
stateMonitor.synchronized(state match {
|
||||
// We don't push new signals when stopped or shutting down.
|
||||
case State.Stopped | State.Stopping =>
|
||||
// Currently sleeping. Cancel that and start immediately.
|
||||
case State.Sleeping =>
|
||||
future.cancel(true)
|
||||
state = State.Suspended
|
||||
signals.offer(new Signal(name, args.toArray))
|
||||
future = Executor.pool.submit(this)
|
||||
// Running or in driver call or only a short yield, just push the signal.
|
||||
case _ =>
|
||||
signals.offer(new Signal(name, args.toArray))
|
||||
})
|
||||
}
|
||||
|
||||
def readFromNBT(nbt: NBTTagCompound): Unit = this.synchronized {
|
||||
// Clear out what we currently have, if anything.
|
||||
stateMonitor.synchronized {
|
||||
assert(state != State.Running) // Lock on 'this' should guarantee this.
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
def lock() {
|
||||
driverLock.lock()
|
||||
future.synchronized {
|
||||
state = State.Synchronizing
|
||||
future.wait()
|
||||
}
|
||||
}
|
||||
state = State(nbt.getInteger("state"))
|
||||
|
||||
def unlock() {
|
||||
driverLock.unlock()
|
||||
}
|
||||
if (state != State.Stopped && init()) {
|
||||
// Unlimit memory use while unpersisting.
|
||||
val memory = lua.getTotalMemory()
|
||||
lua.setTotalMemory(Integer.MAX_VALUE)
|
||||
try {
|
||||
// Try unpersisting Lua, because that's what all of the rest depends on.
|
||||
// Clear the stack (meaning the current kernel).
|
||||
lua.setTop(0)
|
||||
|
||||
def readFromNBT(nbt: NBTTagCompound): Unit = {
|
||||
// If we're running we wait for the executor to yield, to get the Lua state
|
||||
// into a valid, suspended state before trying to unpersist into it.
|
||||
this.synchronized {
|
||||
state = State(nbt.getInteger("state"))
|
||||
if (state != State.Stopped && (lua != null || init())) {
|
||||
baseMemory = nbt.getInteger("baseMemory")
|
||||
timeStarted = nbt.getDouble("timeStarted")
|
||||
if (!unpersist(nbt.getByteArray("kernel")) || !lua.isThread(1)) {
|
||||
// This shouldn't really happen, but there's a chance it does if
|
||||
// the save was corrupt (maybe someone modified the Lua files).
|
||||
throw new IllegalStateException("Could not restore kernel.")
|
||||
}
|
||||
if (state == State.DriverCall || state == State.DriverReturn) {
|
||||
if (!unpersist(nbt.getByteArray("stack")) ||
|
||||
(state == State.DriverCall && !lua.isFunction(2)) ||
|
||||
(state == State.DriverReturn && !lua.isTable(2))) {
|
||||
// Same as with the above, should not really happen normally, but
|
||||
// could for the same reasons.
|
||||
throw new IllegalStateException("Could not restore driver call.")
|
||||
}
|
||||
}
|
||||
|
||||
val memory = lua.getTotalMemory()
|
||||
lua.setTotalMemory(Integer.MAX_VALUE)
|
||||
val kernel = nbt.getString("kernel")
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersist)
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable)
|
||||
lua.pushString(kernel)
|
||||
lua.call(2, 1)
|
||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
||||
lua.setTotalMemory(memory)
|
||||
|
||||
signals.clear()
|
||||
assert(signals.size() == 0)
|
||||
val signalsTag = nbt.getTagList("signals")
|
||||
signals.addAll((0 until signalsTag.tagCount()).
|
||||
map(signalsTag.tagAt(_).asInstanceOf[NBTTagCompound]).
|
||||
map(signal => {
|
||||
val argsTag = signal.getCompoundTag("args")
|
||||
val argsLength = argsTag.getInteger("length")
|
||||
new Signal(signal.getInteger("pid"), signal.getString("name"),
|
||||
new Signal(signal.getString("name"),
|
||||
(0 until argsLength).map("arg" + _).map(argsTag.getTag(_)).map {
|
||||
case tag: NBTTagByte => tag.data
|
||||
case tag: NBTTagShort => tag.data
|
||||
@ -210,40 +237,59 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
}.toArray)
|
||||
}))
|
||||
|
||||
lua.gc(LuaState.GcAction.COLLECT, 0)
|
||||
timeStarted = nbt.getLong("timeStarted")
|
||||
|
||||
// Start running our worker thread if we don't already have one.
|
||||
if (future == null) future = Executor.pool.submit(this)
|
||||
// Start running our worker thread.
|
||||
assert(future == null)
|
||||
future = Executor.pool.submit(this)
|
||||
}
|
||||
catch {
|
||||
case t: Throwable => {
|
||||
t.printStackTrace()
|
||||
// TODO display error in-game on monitor or something
|
||||
//signal("crash", "memory corruption")
|
||||
close()
|
||||
}
|
||||
}
|
||||
finally if (lua != null) {
|
||||
// Clean up some after we're done and limit memory again.
|
||||
lua.gc(LuaState.GcAction.COLLECT, 0)
|
||||
lua.setTotalMemory(memory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def writeToNBT(nbt: NBTTagCompound): Unit = {
|
||||
// If we're running we wait for the executor to yield, to get the Lua state
|
||||
// into a valid, suspended state before trying to persist it.
|
||||
this.synchronized {
|
||||
nbt.setInteger("state", state.id)
|
||||
if (state == State.Stopped) return
|
||||
def writeToNBT(nbt: NBTTagCompound): Unit = this.synchronized {
|
||||
stateMonitor.synchronized {
|
||||
assert(state != State.Running) // Lock on 'this' should guarantee this.
|
||||
assert(state != State.Stopping) // Only set while executor is running.
|
||||
}
|
||||
|
||||
nbt.setInteger("baseMemory", baseMemory)
|
||||
nbt.setDouble("timeStarted", timeStarted)
|
||||
nbt.setInteger("state", state.id)
|
||||
if (state == State.Stopped) {
|
||||
return
|
||||
}
|
||||
|
||||
// Call pluto.persist(persistTable, _G) and store the string result.
|
||||
val memory = lua.getTotalMemory()
|
||||
lua.setTotalMemory(Integer.MAX_VALUE)
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persist)
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable)
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
||||
lua.call(2, 1)
|
||||
val kernel = lua.toString(-1)
|
||||
lua.pop(1)
|
||||
nbt.setString("kernel", kernel)
|
||||
lua.setTotalMemory(memory)
|
||||
// Unlimit memory while persisting.
|
||||
val memory = lua.getTotalMemory()
|
||||
lua.setTotalMemory(Integer.MAX_VALUE)
|
||||
try {
|
||||
// Try persisting Lua, because that's what all of the rest depends on.
|
||||
// While in a driver call we have one object on the global stack: either
|
||||
// the function to call the driver with, or the result of the call.
|
||||
if (state == State.DriverCall || state == State.DriverReturn) {
|
||||
assert(
|
||||
if (state == State.DriverCall) lua.`type`(2) == LuaType.FUNCTION
|
||||
else lua.`type`(2) == LuaType.TABLE)
|
||||
nbt.setByteArray("stack", persist())
|
||||
}
|
||||
// Save the kernel state (which is always at stack index one).
|
||||
assert(lua.`type`(1) == LuaType.THREAD)
|
||||
nbt.setByteArray("kernel", persist())
|
||||
|
||||
val list = new NBTTagList()
|
||||
for (s <- signals.iterator()) {
|
||||
val signal = new NBTTagCompound()
|
||||
signal.setInteger("pid", s.pid)
|
||||
signal.setString("name", s.name)
|
||||
// TODO Test with NBTTagList, but supposedly it only allows entries
|
||||
// with the same type, so I went with this for now...
|
||||
@ -263,35 +309,66 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
}
|
||||
nbt.setTag("signals", list)
|
||||
|
||||
nbt.setLong("timeStarted", timeStarted)
|
||||
}
|
||||
catch {
|
||||
case t: Throwable => {
|
||||
t.printStackTrace()
|
||||
nbt.setInteger("state", State.Stopped.id)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Clean up some after we're done and limit memory again.
|
||||
lua.gc(LuaState.GcAction.COLLECT, 0)
|
||||
lua.setTotalMemory(memory)
|
||||
}
|
||||
}
|
||||
|
||||
private def persist(): Array[Byte] = {
|
||||
lua.getGlobal("persist") // ... obj persist?
|
||||
if (lua.`type`(-1) == LuaType.FUNCTION) { // ... obj persist
|
||||
lua.pushValue(-2) // ... obj persist obj
|
||||
lua.call(1, 1) // ... obj str?
|
||||
if (lua.`type`(-1) == LuaType.STRING) { // ... obj str
|
||||
val result = lua.toByteArray(-1)
|
||||
lua.pop(1) // ... obj
|
||||
return result
|
||||
} // ... obj :(
|
||||
} // ... obj :(
|
||||
lua.pop(1) // ... obj
|
||||
return Array[Byte]()
|
||||
}
|
||||
|
||||
private def unpersist(value: Array[Byte]): Boolean = {
|
||||
lua.getGlobal("unpersist") // ... unpersist?
|
||||
if (lua.`type`(-1) == LuaType.FUNCTION) { // ... unpersist
|
||||
lua.pushByteArray(value) // ... unpersist str
|
||||
lua.call(1, 1) // ... obj
|
||||
return true
|
||||
} // ... :(
|
||||
return false
|
||||
}
|
||||
|
||||
def init(): Boolean = {
|
||||
// Creates a new state with all base libraries as well as the Pluto
|
||||
// library loaded into it. This means the state has much more power than
|
||||
// it rightfully should have, so we sandbox it a bit in the following.
|
||||
// Creates a new state with all base libraries and the persistence library
|
||||
// loaded into it. This means the state has much more power than it
|
||||
// rightfully should have, so we sandbox it a bit in the following.
|
||||
lua = LuaStateFactory.createState()
|
||||
|
||||
try {
|
||||
// Before doing the actual sandboxing we save the Pluto library into the
|
||||
// registry, since it'll be removed from the globals table.
|
||||
lua.getGlobal("eris")
|
||||
lua.getField(-1, "persist")
|
||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.persist)
|
||||
lua.getField(-1, "unpersist")
|
||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersist)
|
||||
lua.pop(1)
|
||||
// If something went wrong while creating the state there's nothing else
|
||||
// we can do here...
|
||||
if (lua == null) return false
|
||||
|
||||
try {
|
||||
// Push a couple of functions that override original Lua API functions or
|
||||
// that add new functionality to it.
|
||||
// that add new functionality to it.1)
|
||||
lua.getGlobal("os")
|
||||
|
||||
// Return ingame time for os.time().
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
// Minecraft starts days at 6 o'clock, so we add six hours.
|
||||
lua.pushNumber((owner.world.getTotalWorldTime() + 6000.0) / 1000.0)
|
||||
// Minecraft starts days at 6 o'clock, so we add those six hours.
|
||||
lua.pushNumber(worldTime + 6000)
|
||||
return 1
|
||||
}
|
||||
})
|
||||
@ -301,72 +378,146 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
// been running, instead of the native library...
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
lua.pushNumber(owner.world.getTotalWorldTime() - timeStarted)
|
||||
// World time is in ticks, and each second has 20 ticks. Since we
|
||||
// want os.clock() to return real seconds, though, we'll divide it
|
||||
// accordingly.
|
||||
lua.pushNumber((owner.world.getTotalWorldTime() - timeStarted) / 20.0)
|
||||
return 1
|
||||
}
|
||||
})
|
||||
lua.setField(-2, "clock")
|
||||
|
||||
// TODO Other overrides?
|
||||
// Custom os.difftime(). For most Lua implementations this would be the
|
||||
// same anyway, but just to be on the safe side.
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
val t2 = lua.checkNumber(1)
|
||||
val t1 = lua.checkNumber(2)
|
||||
lua.pushNumber(t2 - t1)
|
||||
return 1
|
||||
}
|
||||
})
|
||||
lua.setField(-2, "difftime")
|
||||
|
||||
// Allow the system to read how much memory it uses and has available.
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
lua.pushInteger(lua.getTotalMemory() - kernelMemory)
|
||||
return 1
|
||||
}
|
||||
})
|
||||
lua.setField(-2, "totalMemory")
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
lua.pushInteger(lua.getFreeMemory())
|
||||
return 1
|
||||
}
|
||||
})
|
||||
lua.setField(-2, "freeMemory")
|
||||
|
||||
// Pop the os table.
|
||||
lua.pop(1)
|
||||
|
||||
// Run the sandboxing script. This script is presumed to be under our
|
||||
// control. We do the sandboxing in Lua because it'd be a pain to write
|
||||
// using only stack operations...
|
||||
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/sandbox.lua"), "sandbox", "t")
|
||||
lua.call(0, 0)
|
||||
lua.getGlobal("math")
|
||||
|
||||
// Install all driver callbacks into the registry. This is done once in
|
||||
// the beginning so that we can take the memory the callbacks use into
|
||||
// account when computing the kernel's memory use, as well as for building
|
||||
// a table of permanent values used when persisting/unpersisting the state.
|
||||
Drivers.injectInto(this)
|
||||
|
||||
// Run the script that builds the tables with permanent values. These
|
||||
// tables must contain all java callbacks (i.e. C functions, since they
|
||||
// are wrapped on the native side using a C function, of course). They
|
||||
// are used when persisting/unpersisting the state so that Pluto knows
|
||||
// which values it doesn't have to serialize (since it cannot persist C
|
||||
// functions). We store the two tables in the registry.
|
||||
// TODO These tables may change after loading a game, for example due to
|
||||
// a new mod being installed or an old one being removed. In that case,
|
||||
// previously existing values will "suddenly" become nil. We may want to
|
||||
// consider detecting such changes and rebooting computers in that case.
|
||||
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/persistence.lua"), "persistence", "t")
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
|
||||
// We give each Lua state it's own randomizer, since otherwise they'd
|
||||
// use the good old rand() from C. Which can be terrible, and isn't
|
||||
// necessarily thread-safe.
|
||||
val random = new Random
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
println(lua.toString(1))
|
||||
lua.getTop() match {
|
||||
case 0 => lua.pushNumber(random.nextDouble)
|
||||
case 1 => {
|
||||
val u = lua.checkInteger(1)
|
||||
lua.checkArg(1, 1 < u, "interval is empty")
|
||||
lua.pushInteger(1 + random.nextInt(u))
|
||||
}
|
||||
case 2 => {
|
||||
val l = lua.checkInteger(1)
|
||||
val u = lua.checkInteger(2)
|
||||
lua.checkArg(1, l < u, "interval is empty")
|
||||
lua.pushInteger(l + random.nextInt(u - l))
|
||||
}
|
||||
case _ => throw new IllegalArgumentException("wrong number of arguments")
|
||||
}
|
||||
return 1
|
||||
}
|
||||
})
|
||||
lua.setField(-2, "random")
|
||||
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
val seed = lua.checkInteger(1)
|
||||
random.setSeed(seed)
|
||||
return 0
|
||||
}
|
||||
})
|
||||
lua.call(2, 2)
|
||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable)
|
||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable)
|
||||
lua.setField(-2, "randomseed")
|
||||
|
||||
// Pop the math table.
|
||||
lua.pop(1)
|
||||
|
||||
// Until we get to ingame screens we log to Java's stdout.
|
||||
lua.pushJavaFunction(new JavaFunction() {
|
||||
def invoke(lua: LuaState): Int = {
|
||||
for (i <- 1 to lua.getTop()) {
|
||||
lua.`type`(i) match {
|
||||
case LuaType.NIL => print("nil")
|
||||
case LuaType.BOOLEAN => print(lua.toBoolean(i))
|
||||
case LuaType.NUMBER => print(lua.toNumber(i))
|
||||
case LuaType.STRING => print(lua.toString(i))
|
||||
case LuaType.TABLE => print("table")
|
||||
case LuaType.FUNCTION => print("function")
|
||||
case LuaType.THREAD => print("thread")
|
||||
case LuaType.LIGHTUSERDATA | LuaType.USERDATA => print("userdata")
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
})
|
||||
lua.setGlobal("print")
|
||||
|
||||
// TODO Other overrides?
|
||||
|
||||
// Install all driver callbacks into the state. This is done once in
|
||||
// the beginning so that we can take the memory the callbacks use into
|
||||
// account when computing the kernel's memory use, as well as for
|
||||
// building a table of permanent values used when persisting/unpersisting
|
||||
// the state.
|
||||
lua.newTable()
|
||||
lua.setGlobal("drivers")
|
||||
Drivers.injectInto(this)
|
||||
|
||||
// Run the boot script. This creates the global sandbox variable that is
|
||||
// used as the environment for any processes the kernel spawns, adds a
|
||||
// couple of library functions and sets up the permanent value tables as
|
||||
// well as making the functions used for persisting/unpersisting
|
||||
// available as globals.
|
||||
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/boot.lua"), "boot", "t")
|
||||
lua.call(0, 0)
|
||||
|
||||
// Load the basic kernel which takes care of handling signals by managing
|
||||
// the list of active processes. Whatever functionality we can we implement
|
||||
// in Lua, so we also implement most of the kernel's functionality in Lua.
|
||||
// Why? Because like this it's automatically persisted for us without
|
||||
// having to write more additional NBT stuff.
|
||||
// the list of active processes. Whatever functionality we can we
|
||||
// implement in Lua, so we also implement most of the kernel's
|
||||
// functionality in Lua. Why? Because like this it's automatically
|
||||
// persisted for us without having to write more additional NBT stuff.
|
||||
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/kernel.lua"), "kernel", "t")
|
||||
lua.newThread()
|
||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
||||
lua.newThread() // Leave it as the first value on the stack.
|
||||
|
||||
// Run the garbage collector to get rid of stuff left behind after the
|
||||
// initialization phase to get a good estimate of the base memory usage
|
||||
// the kernel has. We remember that size to grant user-space programs a
|
||||
// fixed base amount of memory.
|
||||
// fixed base amount of memory, regardless of the memory need of the
|
||||
// underlying system (which may change across releases).
|
||||
lua.gc(LuaState.GcAction.COLLECT, 0)
|
||||
baseMemory = lua.getTotalMemory() - lua.getFreeMemory()
|
||||
lua.setTotalMemory(baseMemory + 128 * 1024)
|
||||
kernelMemory = lua.getTotalMemory() - lua.getFreeMemory()
|
||||
lua.setTotalMemory(kernelMemory + 64 * 1024)
|
||||
|
||||
// Remember when we started the computer.
|
||||
timeStarted = System.currentTimeMillis()
|
||||
println("Kernel uses " + (kernelMemory / 1024) + "KB of memory.")
|
||||
|
||||
println("Kernel uses " + baseMemory + " bytes of memory.")
|
||||
// Clear any left-over signals from a previous run.
|
||||
signals.clear()
|
||||
|
||||
return true
|
||||
}
|
||||
@ -379,145 +530,151 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
return false
|
||||
}
|
||||
|
||||
def close() {
|
||||
lua.setTotalMemory(Integer.MAX_VALUE);
|
||||
lua.close()
|
||||
lua = null
|
||||
baseMemory = 0
|
||||
timeStarted = 0
|
||||
state = State.Stopped
|
||||
future = null
|
||||
signals.clear()
|
||||
}
|
||||
def close(): Unit = stateMonitor.synchronized(
|
||||
if (state != State.Stopped) {
|
||||
state = State.Stopped
|
||||
lua.setTotalMemory(Integer.MAX_VALUE);
|
||||
lua.close()
|
||||
lua = null
|
||||
kernelMemory = 0
|
||||
signals.clear()
|
||||
timeStarted = 0
|
||||
future = null
|
||||
})
|
||||
|
||||
// This is a really high level lock that we only use for saving and loading.
|
||||
def run(): Unit = this.synchronized {
|
||||
println(" > executor enter")
|
||||
|
||||
val driverReturn = State.DriverReturn == stateMonitor.synchronized {
|
||||
val oldState = state
|
||||
state = State.Running
|
||||
oldState
|
||||
}
|
||||
|
||||
def run() {
|
||||
try {
|
||||
println("start running computer")
|
||||
|
||||
// See if the game appears to be paused, in which case we also pause.
|
||||
if (System.currentTimeMillis() - lastUpdate > 500)
|
||||
updateMonitor.synchronized {
|
||||
updateMonitor.wait()
|
||||
}
|
||||
|
||||
println("running computer")
|
||||
if (System.currentTimeMillis - lastUpdate > 500)
|
||||
pauseMonitor.synchronized(pauseMonitor.wait())
|
||||
|
||||
// This is synchronized so that we don't run a Lua state while saving or
|
||||
// loading the computer to or from an NBTTagCompound.
|
||||
this.synchronized {
|
||||
// Push the kernel coroutine onto the stack so that we can resume it.
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
||||
// Get a copy to check the coroutine's status after it ran.
|
||||
lua.pushValue(-1)
|
||||
// loading the computer to or from an NBTTagCompound or other stuff
|
||||
// corrupting our Lua state.
|
||||
|
||||
try {
|
||||
// Resume the Lua state and remember the number of results we get.
|
||||
val results = state match {
|
||||
// Current coroutine was forced to yield. Resume without injecting any
|
||||
// signals. Any passed arguments would simply be ignored.
|
||||
case State.Yielding => {
|
||||
println("resuming forced yield")
|
||||
lua.resume(-1, 0)
|
||||
}
|
||||
// The kernel thread will always be at stack index one.
|
||||
assert(lua.`type`(1) == LuaType.THREAD)
|
||||
|
||||
// We're running normally, i.e. all coroutines yielded voluntarily and
|
||||
// this yield comes directly out of the main kernel coroutine.
|
||||
case _ => {
|
||||
// Try to get a signal to run the state with.
|
||||
signals.poll() match {
|
||||
// No signal, just run any non-sleeping processes.
|
||||
case null => {
|
||||
println("resuming without signal")
|
||||
lua.resume(-1, 0)
|
||||
}
|
||||
// Resume the Lua state and remember the number of results we get.
|
||||
val results = if (driverReturn) {
|
||||
// If we were doing a driver call, continue where we left off.
|
||||
assert(lua.getTop() == 2)
|
||||
lua.resume(1, 1)
|
||||
}
|
||||
else signals.poll() match {
|
||||
// No signal, just run any non-sleeping processes.
|
||||
case null => lua.resume(1, 0)
|
||||
|
||||
// Got a signal, inject it and call any handlers (if any).
|
||||
case signal => {
|
||||
println("injecting signal")
|
||||
lua.pushInteger(signal.pid)
|
||||
lua.pushString(signal.name)
|
||||
signal.args.foreach {
|
||||
case arg: Byte => lua.pushInteger(arg)
|
||||
case arg: Short => lua.pushInteger(arg)
|
||||
case arg: Integer => lua.pushInteger(arg)
|
||||
case arg: Long => lua.pushNumber(arg)
|
||||
case arg: Float => lua.pushNumber(arg)
|
||||
case arg: Double => lua.pushNumber(arg)
|
||||
case arg: String => lua.pushString(arg)
|
||||
}
|
||||
lua.resume(-1, 2 + signal.args.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Got a signal, inject it and call any handlers (if any).
|
||||
case signal => {
|
||||
lua.pushString(signal.name)
|
||||
signal.args.foreach {
|
||||
case arg: Byte => lua.pushInteger(arg)
|
||||
case arg: Short => lua.pushInteger(arg)
|
||||
case arg: Int => lua.pushInteger(arg)
|
||||
case arg: Long => lua.pushNumber(arg)
|
||||
case arg: Float => lua.pushNumber(arg)
|
||||
case arg: Double => lua.pushNumber(arg)
|
||||
case arg: String => lua.pushString(arg)
|
||||
}
|
||||
|
||||
println("lua yielded")
|
||||
|
||||
// Only queue for next execution step if the kernel is still alive.
|
||||
if (lua.status(-(results + 1)) != 0) {
|
||||
// See what we have. The convention is that if the first result is a
|
||||
// string with the value "timeout" the currently running coroutines was
|
||||
// forced to yield by the execution limit (i.e. the yield comes from the
|
||||
// debug hook we installed as seen in the sandbox.lua script). Otherwise
|
||||
// it's a normal yield, and we get the time to wait before we should try
|
||||
// to execute the state again in seconds.
|
||||
if (lua.isString(-results) && "timeout".equals(lua.toString(-results))) {
|
||||
// Forced yield due to long execution time. Remember this for the next
|
||||
// time we run, so we don't try to insert a signal which would get
|
||||
// ignored.
|
||||
state = State.Yielding
|
||||
future = Executor.pool.submit(this)
|
||||
}
|
||||
else {
|
||||
// Lua state yielded normally, see how long we should wait before
|
||||
// resuming the state again.
|
||||
val sleep = (lua.toNumber(-1) * 1000).toLong
|
||||
state = State.Running
|
||||
future = Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
lua.pop(results)
|
||||
}
|
||||
catch {
|
||||
// The kernel should never throw. If it does, the computer crashed
|
||||
// hard, so we just close the state.
|
||||
// TODO Print something to an in-game screen, a la kernel panic.
|
||||
case ex: LuaRuntimeException => ex.printLuaStackTrace()
|
||||
case ex: Throwable => ex.printStackTrace()
|
||||
}
|
||||
println("free memory: " + lua.getFreeMemory())
|
||||
|
||||
// If the kernel is no longer running the computer has stopped.
|
||||
lua.status(-1) match {
|
||||
case LuaState.YIELD => lua.pop(1)
|
||||
case _ => updateMonitor.synchronized(close())
|
||||
lua.resume(1, 1 + signal.args.length)
|
||||
}
|
||||
}
|
||||
|
||||
println("end running computer")
|
||||
// State has inevitably changed, mark as changed to save again.
|
||||
owner.markAsChanged()
|
||||
|
||||
// Only queue for next execution step if the kernel is still alive.
|
||||
if (lua.status(1) == LuaState.YIELD) {
|
||||
// Lua state yielded normally, see what we have.
|
||||
stateMonitor.synchronized {
|
||||
if (state == State.Stopping) {
|
||||
// Someone called stop() in the meantime.
|
||||
close()
|
||||
}
|
||||
else if (results == 1 && lua.isNumber(2)) {
|
||||
// We got a number. This tells us how long we should wait before
|
||||
// resuming the state again.
|
||||
val sleep = (lua.toNumber(2) * 1000).toLong
|
||||
lua.pop(results)
|
||||
state = State.Sleeping
|
||||
future = Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
else if (results == 1 && lua.isFunction(2)) {
|
||||
// If we get one function it's a wrapper for a driver call.
|
||||
state = State.DriverCall
|
||||
future = null
|
||||
}
|
||||
else {
|
||||
// Something else, just pop the results and try again.
|
||||
lua.pop(results)
|
||||
state = State.Suspended
|
||||
future = Executor.pool.submit(this)
|
||||
}
|
||||
}
|
||||
|
||||
println(" < executor leave")
|
||||
|
||||
// Avoid getting to the closing part after the exception handling.
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// The kernel should never throw. If it does, the computer crashed
|
||||
// hard, so we just close the state.
|
||||
// TODO Print something to an in-game screen, a la kernel panic.
|
||||
case ex: LuaRuntimeException => ex.printLuaStackTrace()
|
||||
case er: LuaMemoryAllocationException => {
|
||||
// This is pretty likely to happen for non-upgraded computers.
|
||||
// TODO Print an error message to an in-game screen.
|
||||
println("Out of memory!")
|
||||
er.printStackTrace()
|
||||
}
|
||||
// Top-level catch-anything, because otherwise those exceptions get
|
||||
// gobbled up by the executor unless we call the future's get().
|
||||
case t: Throwable => t.printStackTrace()
|
||||
}
|
||||
|
||||
// If we come here there was an error or we stopped, kill off the state.
|
||||
close()
|
||||
|
||||
println(" < executor leave")
|
||||
}
|
||||
|
||||
/** Signals are messages sent to the Lua state's processes from Java. */
|
||||
private class Signal(val pid: Int, val name: String, val args: Array[Any]) {
|
||||
}
|
||||
/** Signals are messages sent to the Lua state from Java asynchronously. */
|
||||
private class Signal(val name: String, val args: Array[Any])
|
||||
|
||||
/** Possible states of the computer, and in particular its executor. */
|
||||
private object State extends Enumeration {
|
||||
/** Self explanatory: the computer is not running right now. */
|
||||
/** The computer is not running right now and there is no Lua state. */
|
||||
val Stopped = Value("Stopped")
|
||||
|
||||
/** The computer is running but yielded for a moment. */
|
||||
val Suspended = Value("Suspended")
|
||||
|
||||
/** The computer is running but yielding for a longer amount of time. */
|
||||
val Sleeping = Value("Sleeping")
|
||||
|
||||
/** The computer is up and running, executing Lua code. */
|
||||
val Running = Value("Running")
|
||||
|
||||
/** The computer is yielding because of its execution limit. */
|
||||
val Yielding = Value("Yielding")
|
||||
/** The computer is currently shutting down (waiting for executor). */
|
||||
val Stopping = Value("Stopping")
|
||||
|
||||
/** The computer executor is waiting for the server thread. */
|
||||
val Synchronizing = Value("Synchronizing")
|
||||
/** The computer executor is waiting for a driver call to be made. */
|
||||
val DriverCall = Value("DriverCall")
|
||||
|
||||
/** The computer should resume with the result of a driver call. */
|
||||
val DriverReturn = Value("DriverReturn")
|
||||
}
|
||||
|
||||
/** Singleton for requesting executors that run our Lua states. */
|
||||
@ -532,9 +689,9 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
|
||||
def newThread(r: Runnable): Thread = {
|
||||
val name = "OpenComputers-" + threadNumber.getAndIncrement()
|
||||
val thread = new Thread(group, r, name, 0)
|
||||
if (thread.isDaemon())
|
||||
thread.setDaemon(false)
|
||||
val thread = new Thread(group, r, name)
|
||||
if (!thread.isDaemon())
|
||||
thread.setDaemon(true)
|
||||
if (thread.getPriority() != Thread.MIN_PRIORITY)
|
||||
thread.setPriority(Thread.MIN_PRIORITY)
|
||||
return thread
|
||||
@ -542,13 +699,3 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** Names of entries in the registries of the Lua states of computers. */
|
||||
private[computer] object ComputerRegistry {
|
||||
val kernel = "oc_kernel"
|
||||
val driverApis = "oc_apis"
|
||||
val persist = "oc_persist"
|
||||
val unpersist = "oc_unpersist"
|
||||
val unpersistTable = "oc_unpersistTable"
|
||||
val persistTable = "oc_persistTable"
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ package li.cil.oc.server.computer
|
||||
|
||||
import java.lang.reflect.Method
|
||||
|
||||
import scala.Array.canBuildFrom
|
||||
|
||||
import com.naef.jnlua.JavaFunction
|
||||
import com.naef.jnlua.LuaState
|
||||
|
||||
@ -18,115 +16,49 @@ private[oc] class Driver(val driver: IDriver) {
|
||||
if (api == null || api.isEmpty()) return
|
||||
val lua = context.luaState
|
||||
|
||||
// Get or create registry entry holding API tables.
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
|
||||
if (lua.isNil(-1)) {
|
||||
lua.pop(1)
|
||||
lua.newTable()
|
||||
lua.pushValue(-1)
|
||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
|
||||
}
|
||||
// Get or create table holding API tables.
|
||||
lua.getGlobal("drivers") // ... drivers?
|
||||
assert(!lua.isNil(-1)) // ... drivers
|
||||
|
||||
// Get or create API table.
|
||||
lua.getField(-1, api)
|
||||
if (lua.isNil(-1)) {
|
||||
lua.pop(1)
|
||||
lua.newTable()
|
||||
lua.pushValue(-1)
|
||||
lua.setField(-3, api)
|
||||
}
|
||||
lua.getField(-1, api) // ... drivers api?
|
||||
if (lua.isNil(-1)) { // ... drivers nil
|
||||
lua.pop(1) // ... drivers
|
||||
lua.newTable() // ... drivers api
|
||||
lua.pushValue(-1) // ... drivers api api
|
||||
lua.setField(-3, api) // ... drivers api
|
||||
} // ... drivers api
|
||||
|
||||
for (method <- driver.getClass().getMethods()) {
|
||||
val annotation = method.getAnnotation(classOf[Callback])
|
||||
if (annotation != null) {
|
||||
val name = annotation.name
|
||||
lua.getField(-1, name)
|
||||
if (lua.isNil(-1)) {
|
||||
// Entry already exists, skip it.
|
||||
lua.pop(1)
|
||||
// TODO Log warning properly via a logger.
|
||||
println("WARNING: Duplicate API entry, ignoring: " + api + "." + name)
|
||||
}
|
||||
else {
|
||||
// No such entry yet. Pop the nil and build our callback wrapper.
|
||||
lua.pop(1)
|
||||
if (annotation.synchronize) {
|
||||
lua.pushJavaFunction(new SynchronizedMethodWrapper(context, method))
|
||||
for (method <- driver.getClass().getMethods())
|
||||
method.getAnnotation(classOf[Callback]) match {
|
||||
case null => Unit // No annotation.
|
||||
case annotation => {
|
||||
val name = annotation.name
|
||||
lua.getField(-1, name) // ... drivers api func?
|
||||
if (lua.isNil(-1)) { // ... drivers api nil
|
||||
// No such entry yet.
|
||||
lua.pop(1) // ... drivers api
|
||||
lua.pushJavaFunction(new MethodWrapper(context, method)) // ... drivers api func
|
||||
lua.setField(-2, name) // ... drivers api
|
||||
}
|
||||
else {
|
||||
lua.pushJavaFunction(new MethodWrapper(context, method))
|
||||
else { // ... drivers api func
|
||||
// Entry already exists, skip it.
|
||||
lua.pop(1) // ... drivers api
|
||||
// TODO Log warning properly via a logger.
|
||||
println("WARNING: Duplicate API entry, ignoring: " + api + "." + name)
|
||||
}
|
||||
lua.setField(-2, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pop the API table and the table holding all APIs.
|
||||
lua.pop(2)
|
||||
} // ... drivers api
|
||||
lua.pop(2) // ...
|
||||
}
|
||||
|
||||
/**
|
||||
* This installs the driver on the computer, providing an API to interact
|
||||
* with the device.
|
||||
*
|
||||
* This copies an existing API table from the registry and executes any
|
||||
* initialization code provided by the driver.
|
||||
*/
|
||||
def install(context: IInternalComputerContext) {
|
||||
copyAPI(context)
|
||||
|
||||
// Do we have custom initialization code?
|
||||
val code = driver.apiCode
|
||||
if (code != null && !code.isEmpty()) {
|
||||
val lua = context.luaState
|
||||
lua.load(code, driver.componentName)
|
||||
// TODO Set environment so that variables not explicitly added to globals
|
||||
// table won't accidentally clutter it.
|
||||
lua.call(0, 0)
|
||||
private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
|
||||
def invoke(state: LuaState): Int = {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private def copyAPI(context: IInternalComputerContext) {
|
||||
// Check if the component actually provides an API.
|
||||
val api = driver.apiName
|
||||
if (api == null && api.isEmpty()) return
|
||||
|
||||
// Get the Lua state and check if the API table already exists.
|
||||
val lua = context.luaState
|
||||
|
||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
|
||||
if (lua.isNil(-1)) {
|
||||
// We don't have any APIs at all.
|
||||
lua.pop(1)
|
||||
return
|
||||
}
|
||||
|
||||
lua.getField(-1, api)
|
||||
if (lua.isNil(-1)) {
|
||||
// No such API. Which is kind of weird, but hey.
|
||||
lua.pop(2)
|
||||
return
|
||||
}
|
||||
|
||||
// OK, we have our registry table. Create a new table to copy into.
|
||||
val registryTable = lua.getTop()
|
||||
lua.newTable()
|
||||
val globalTable = lua.getTop()
|
||||
|
||||
// Copy all keys (which are the API functions).
|
||||
lua.pushNil()
|
||||
while (lua.next(registryTable)) {
|
||||
val key = lua.toString(-2)
|
||||
lua.setField(globalTable, key)
|
||||
}
|
||||
|
||||
// Push our globals table into the global name space.
|
||||
lua.setGlobal(api)
|
||||
|
||||
// Pop the registry API table and registry table holding all API tables.
|
||||
lua.pop(2)
|
||||
}
|
||||
|
||||
/*
|
||||
private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
|
||||
private val classOfBoolean = classOf[Boolean]
|
||||
private val classOfByte = classOf[Byte]
|
||||
@ -187,12 +119,5 @@ private[oc] class Driver(val driver: IDriver) {
|
||||
method.invoke(driver, args)
|
||||
}
|
||||
}
|
||||
|
||||
private class SynchronizedMethodWrapper(context: IInternalComputerContext, method: Method) extends MethodWrapper(context, method) {
|
||||
override def call(args: AnyRef*) = {
|
||||
context.lock()
|
||||
try super.call(args)
|
||||
finally context.unlock()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
@ -5,5 +5,5 @@ import com.naef.jnlua.LuaState
|
||||
import net.minecraft.nbt.NBTTagCompound
|
||||
|
||||
trait IComputerContext {
|
||||
def signal(pid: Int, name: String, args: Any*)
|
||||
def signal(name: String, args: Any*)
|
||||
}
|
@ -8,4 +8,7 @@ import net.minecraft.world.World
|
||||
*/
|
||||
trait IComputerEnvironment {
|
||||
def world: World
|
||||
|
||||
/** Called when the computer state changed, so it should be saved again. */
|
||||
def markAsChanged(): Unit
|
||||
}
|
@ -88,7 +88,29 @@ private[computer] object LuaStateFactory {
|
||||
val state = new LuaState(Integer.MAX_VALUE)
|
||||
try {
|
||||
// Load all libraries.
|
||||
state.openLibs()
|
||||
state.openLib(LuaState.Library.BASE)
|
||||
state.openLib(LuaState.Library.BIT32)
|
||||
state.openLib(LuaState.Library.COROUTINE)
|
||||
state.openLib(LuaState.Library.DEBUG)
|
||||
state.openLib(LuaState.Library.ERIS)
|
||||
state.openLib(LuaState.Library.MATH)
|
||||
state.openLib(LuaState.Library.STRING)
|
||||
state.openLib(LuaState.Library.TABLE)
|
||||
state.pop(8)
|
||||
|
||||
// Prepare table for os stuff.
|
||||
state.newTable()
|
||||
state.setGlobal("os")
|
||||
|
||||
// Remove some other functions we don't need and are dangerous.
|
||||
state.pushNil()
|
||||
state.setGlobal("dofile")
|
||||
state.pushNil()
|
||||
state.setGlobal("loadfile")
|
||||
state.pushNil()
|
||||
state.setGlobal("module")
|
||||
state.pushNil()
|
||||
state.setGlobal("require")
|
||||
}
|
||||
catch {
|
||||
case ex: Throwable => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user