From abddfae2eaf3cfb11b2e65c94bf157f0cae5fc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 29 Sep 2013 11:57:14 +0200 Subject: [PATCH] some more refactoring; switched up driver loading a bit: they're now loaded from the kernel, by it requesting the list of drivers. this way they're loaded into the sandbox (with the network sending function available, in addition to what the normal sandbox can do); doing primary init of kernel in computer init to get a better idea of how much memory our os stuff needs, in particular the driver table; saving the kernel size now, since the baseline is primarily dependent on the kernel, not what we provide aside from that. this way the "user" memory that comes free is now limitable to 16k with much less variance (init takes around 9k, apparently, the rest is free) --- assets/opencomputers/lua/boot.lua | 7 +- assets/opencomputers/lua/buffer.lua | 137 ------------- assets/opencomputers/lua/disk.lua | 64 ++++-- assets/opencomputers/lua/gpu.lua | 16 +- assets/opencomputers/lua/init.lua | 8 +- assets/opencomputers/lua/kernel.lua | 182 +++++++++--------- assets/opencomputers/lua/redstone.lua | 46 ++--- li/cil/oc/api/Driver.scala | 56 ++++++ li/cil/oc/api/{network => }/Network.scala | 35 +++- li/cil/oc/api/driver/API.scala | 13 -- li/cil/oc/api/driver/Block.scala | 3 +- li/cil/oc/api/driver/Driver.scala | 45 ----- li/cil/oc/api/driver/Item.scala | 7 +- li/cil/oc/api/network/API.scala | 21 -- li/cil/oc/api/network/Node.scala | 3 +- li/cil/oc/common/Proxy.scala | 19 +- li/cil/oc/common/block/Multi.scala | 7 +- .../common/component/ScreenEnvironment.scala | 5 +- .../tileentity/ItemComponentProxy.scala | 18 +- .../tileentity/TileEntityComputer.scala | 9 +- .../tileentity/TileEntityKeyboard.scala | 6 +- li/cil/oc/server/PacketHandler.scala | 6 +- li/cil/oc/server/component/GraphicsCard.scala | 14 +- li/cil/oc/server/component/RedstoneCard.scala | 9 +- li/cil/oc/server/computer/Computer.scala | 102 ++++++---- .../oc/server/computer/LuaStateFactory.scala | 12 +- .../Drivers.scala => driver/Registry.scala} | 38 +--- li/cil/oc/server/network/Network.scala | 24 +-- 28 files changed, 401 insertions(+), 511 deletions(-) delete mode 100644 assets/opencomputers/lua/buffer.lua create mode 100644 li/cil/oc/api/Driver.scala rename li/cil/oc/api/{network => }/Network.scala (89%) delete mode 100644 li/cil/oc/api/driver/API.scala delete mode 100644 li/cil/oc/api/driver/Driver.scala delete mode 100644 li/cil/oc/api/network/API.scala rename li/cil/oc/server/{computer/Drivers.scala => driver/Registry.scala} (72%) diff --git a/assets/opencomputers/lua/boot.lua b/assets/opencomputers/lua/boot.lua index 7d3350dd7..7c55fead1 100644 --- a/assets/opencomputers/lua/boot.lua +++ b/assets/opencomputers/lua/boot.lua @@ -111,9 +111,4 @@ end sendToNode = wrap(sendToNode) sendToAll = wrap(sendToAll) -nodeName = wrap(nodeName) - -driver = {} -function driver.componentType(id) - return nodeName(id) -end \ No newline at end of file +nodeName = wrap(nodeName) \ No newline at end of file diff --git a/assets/opencomputers/lua/buffer.lua b/assets/opencomputers/lua/buffer.lua deleted file mode 100644 index a6ca2abb7..000000000 --- a/assets/opencomputers/lua/buffer.lua +++ /dev/null @@ -1,137 +0,0 @@ ---[[ - This is the terminal implementation. - - It provides means to interact with a screen that is provided by the actual - game in some way. -]] - -function write(term, ...) - local w, h = term.getSize() - local x, y = term.getCursorPosition() - local foreground = term.getForeground() - local background = term.getBackground() - for _, value in ipairs({...}) do - local value = tostring(value) - for i in 1, value:len() do - term.setCharAt(x, y) = value.sub(i, i) - end - end -end - ---[[ Creates a new terminal with an internal display buffer. ]] -function new(width, height) - -- This is the current display buffer of the terminal: an array of lines, which - -- are in turn arrays of the displayed characters on that line, as well as - -- foreground and background colors (which may or may not be ignored in the - -- actual presentation layer). - local buffer = {} - - -- This is the size of the buffer, i.e. its width and height. - local size = {x = 0, y = 0} - - -- Cursor position and whether to blink or not. - local cursor = {x = 1, y = 1, blink = false} - - -- The current foreground and background color, applied when writing. - local colors = {foreground = 0xFFFFFF, background = 0x000000} - - --[[ Resizes the internal buffer and trims / expands it as necessary. ]] - local function resize(w, h) - -- Trim vertical area. - for line = size.y, h + 1, -1 do - buffer[line] = nil - end - -- Trim horizontal area. - for line = 1, h do - local buffer = buffer[line] - for column = size.x, w + 1, -1 do - buffer[column] = nil - end - end - -- Expand vertical area. - for line = size.y + 1, h do - buffer[line] = {} - end - -- Expand horizontal area. - for line = 1, h do - local buffer = buffer[line] - for column = line <= h and size.x + 1 or 1, w do - buffer[column] = {" ", 0xFFFFFF, 0x000000} - end - end - -- Store new sizes. - size.x = x - size.y = y - end - - -- Pre-fill the buffer. - resize(80, 24) - - -- Checked buffer access for abstraction layer below. - local function get(x, y) - if x < 1 or x > size.x then - error("column out of bounds") - elseif y < 1 or y > size.y then - error("line out of bounds") - else - return buffer[x][y] - end - end - - -- Abstraction layer to avoid direct access to internals while re-using - -- function instances. - local term = { - getSize = function() return size.x, size.y end, - getCharAt = function(x, y) return get(x, y)[1] end, - setCharAt = function(x, y, char) - assert(type(char) == "string" and char:len() == 1, - "'char' must be a single character") - get(x, y)[1] = char - end, - getForegroundAt = function(x, y) return get(x, y)[2] end, - setForegroundAt = function(x, y, color) - assert(type(color) == "number" and color == math.floor(color), - "'color' must be an integral number") - get(x, y)[2] = color - end, - getBackgroundAt = function(x, y) return get(x, y)[3] end, - setBackgroundAt = function(x, y, color) - assert(type(color) == "number" and color == math.floor(color), - "'color' must be an integral number") - get(x, y)[3] = color - end, - getCursorPosition = function() return cursor.x, cursor.y end, - setCursorPosition = function(x, y) - assert(type(x) == "number" and x == math.floor(x), - "'x' must be an integral number") - assert(x >= 1 and x <= size.x, "column out of bounds") - assert(type(y) == "number" and y == math.floor(y), - "'y' must be an integral number") - assert(y >= 1 and y <= size.y, "line out of bounds") - cursor.x = x - cursor.y = y - end, - getCursorBlink = function() return cursor.blink end, - setCursorBlink = function(blink) - assert(type(blink) == "boolean", "'blink' must be a boolean") - cursor.blink = blink - end, - getForeground = function() return colors.foreground end, - setForeground = function(color) - assert(type(color) == "number" and color == math.floor(color), - "'color' must be an integral number") - colors.foreground = color - end, - getBackground = function() return colors.background end, - setBackground = function(color) - assert(type(color) == "number" and color == math.floor(color), - "'color' must be an integral number") - colors.background = color - end - } - return setmetatable({}, {__index = term}) -end - - -local function clear() -end \ No newline at end of file diff --git a/assets/opencomputers/lua/disk.lua b/assets/opencomputers/lua/disk.lua index 3a085bb19..2b1843483 100644 --- a/assets/opencomputers/lua/disk.lua +++ b/assets/opencomputers/lua/disk.lua @@ -2,17 +2,57 @@ The Disk API, provided by disk components. ]] -mount = function() end -umount = function() end +driver.disk = {} -listdir = function(dirname) end -remove = _G.os.remove -rename = _G.os.rename -tmpname = function() end +-- We track mounted disks in this table. +local fstab = {} -open = function(filename, mode) end -read = function() end -write = function(value) end -flush = function(file) end -close = function(file) end -type = function(file) end \ No newline at end of file +function driver.disk.mount() +end + +function driver.disk.umount() +end + +function driver.disk.listdir(dirname) +end + +function driver.disk.remove(path) +end + +function driver.disk.rename(path) +end + +function driver.disk.tmpname() +end + +function driver.disk.open(filename, mode) +end + +function driver.disk.read() +end + +function driver.disk.write(value) +end + +function driver.disk.flush(file) +end + +function driver.disk.close(file) +end + +function driver.disk.type(file) +end + +-- Aliases for vanilla Lua. +os.remove = driver.disk.remove +os.rename = driver.disk.rename +os.tmpname = driver.disk.tmpname + +io = {} +io.flush = function() end -- does nothing +-- TODO io.lines = function(filename) end +io.open = driver.disk.open +-- TODO io.popen = function(prog, mode) end +io.read = driver.disk.read +-- TODO io.tmpfile = function() end +io.type = driver.disk.type diff --git a/assets/opencomputers/lua/gpu.lua b/assets/opencomputers/lua/gpu.lua index 1ac469e6b..5215543d7 100644 --- a/assets/opencomputers/lua/gpu.lua +++ b/assets/opencomputers/lua/gpu.lua @@ -25,27 +25,25 @@ function driver.gpu.copy(gpu, screen, col, row, w, h, tx, ty) sendToNode(gpu, "gpu.copy", screen, col, row, w, h, tx, ty) end -function driver.gpu.bind(gpuAccess, screenAccess) - local gpu = type(gpuAccess) == "function" and gpuAccess or function() return gpuAccess end - local screen = type(screenAccess) == "function" and screenAccess or function() return screenAccess end +function driver.gpu.bind(gpuId, screenId) return { setResolution = function(w, h) - driver.gpu.setResolution(gpu(), screen(), w, h) + driver.gpu.setResolution(component.address(gpuId), component.address(screenId), w, h) end, getResolution = function() - return driver.gpu.getResolution(gpu(), screen()) + return driver.gpu.getResolution(component.address(gpuId), component.address(screenId)) end, getResolutions = function() - return driver.gpu.getResolutions(gpu(), screen()) + return driver.gpu.getResolutions(component.address(gpuId), component.address(screenId)) end, set = function(col, row, value) - driver.gpu.set(gpu(), screen(), col, row, value) + driver.gpu.set(component.address(gpuId), component.address(screenId), col, row, value) end, fill = function(col, ro, w, h, value) - driver.gpu.fill(gpu(), screen(), col, ro, w, h, value) + driver.gpu.fill(component.address(gpuId), component.address(screenId), col, ro, w, h, value) end, copy = function(col, row, w, h, tx, ty) - driver.gpu.copy(gpu(), screen(), col, row, w, h, tx, ty) + driver.gpu.copy(component.address(gpuId), component.address(screenId), col, row, w, h, tx, ty) end } end \ No newline at end of file diff --git a/assets/opencomputers/lua/init.lua b/assets/opencomputers/lua/init.lua index 94ec2c5f9..f94d8e096 100644 --- a/assets/opencomputers/lua/init.lua +++ b/assets/opencomputers/lua/init.lua @@ -235,9 +235,7 @@ end) local function bindIfPossible() if gpuId > 0 and screenId > 0 then if not boundGpu then - local function gpu() return component.address(gpuId) end - local function screen() return component.address(screenId) end - boundGpu = driver.gpu.bind(gpu, screen) + boundGpu = driver.gpu.bind(gpuId, screenId) screenWidth, screenHeight = boundGpu.getResolution() event.fire("term_available") end @@ -340,12 +338,12 @@ end write = function(...) local args = {...} local first = true - for _, value in ipairs(args) do + for i = 1, #args do if not first then term.write(", ") end first = false - term.write(value, true) + term.write(args[i], true) end end diff --git a/assets/opencomputers/lua/kernel.lua b/assets/opencomputers/lua/kernel.lua index 6e72fe7d7..34a015d79 100644 --- a/assets/opencomputers/lua/kernel.lua +++ b/assets/opencomputers/lua/kernel.lua @@ -1,20 +1,6 @@ ---[[ 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. ---]] - ---[[ 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() - if os.realTime() > deadline then - error({timeout = debug.traceback(2)}, 0) - end -end +--[[ This is called as the main coroutine by the host. If this returns the + computer crashes. It should never ever return "normally", only when an + error occurred. Shutdown / reboot are signalled via special yields. ]] --[[ Set up the global environment we make available to userland programs. ]] local sandbox = { @@ -39,17 +25,9 @@ local sandbox = { tonumber = tonumber, tostring = tostring, - -- We don't care what users do with metatables. The only raised concern was - -- about breaking an environment, which is pretty much void in Lua 5.2. getmetatable = getmetatable, setmetatable = setmetatable, - write = function() end, - - checkArg = checkArg, - component = component, - driver = driver, - bit32 = { arshift = bit32.arshift, band = bit32.band, @@ -142,114 +120,130 @@ local sandbox = { unpack = table.unpack } } +sandbox.checkArg = checkArg +sandbox._G = sandbox + +-- Note: 'write' will be replaced by init script. +function sandbox.write(...) end function sandbox.print(...) sandbox.write(...) sandbox.write("\n") end --- Make the sandbox its own globals table. -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, source, env) return load(code, source, "t", env or sandbox) end --[[ Install wrappers for coroutine management that reserves the first value - returned by yields for internal stuff. For now this is used for driver - calls, in which case the first function is the function performing the - actual driver call, but is called from the server thread, and for sleep - notifications, i.e. letting the host know we're in no hurry to be - called again any time soon. See os.sleep for more on the second. + returned by yields for internal stuff. Used for sleeping and message + calls (sendToNode and its ilk) that happen synchronized (Server thread). --]] -function sandbox.coroutine.yield(...) - return coroutine.yield(nil, ...) -end -function sandbox.coroutine.resume(co, ...) - local function checkDeadline() - if os.realTime() > deadline then - error("too long without yielding", 0) - end +local deadline = 0 + +local function checkDeadline() + if os.realTime() > deadline then + error("too long without yielding", 0) end - local args = {...} +end + +local function main(co) + local args = {} while true do - -- Don't reset counter when resuming, to avoid circumventing the timeout. + deadline = os.realTime() + 3 if not debug.gethook(co) then debug.sethook(co, checkDeadline, "", 10000) end - - -- Run the coroutine. local result = {coroutine.resume(co, table.unpack(args))} - - -- Check if we're over the deadline since I'm pretty sure the counter of - -- coroutines is separate. - checkDeadline() - - -- See what kind of yield we have. if result[1] then - -- Coroutine returned normally, if we yielded it may be a system yield. - if coroutine.status(co) ~= "dead" and result[2] then - -- Propagate system yields and repeat the retry. - args = coroutine.yield(table.unpack(result, 2)) - else - -- Normal yield or coroutine returned, return result. - return result[1], table.unpack(result, 3) - end + args = {coroutine.yield(result[2])} -- system yielded value else - -- Error while executing coroutine. + error(result[2]) + end + end +end + +function sandbox.coroutine.resume(co, ...) + local args = {...} + while true do + if not debug.gethook(co) then -- don't reset counter + debug.sethook(co, checkDeadline, "", 10000) + end + local result = {coroutine.resume(co, table.unpack(args))} + checkDeadline() + if result[1] then + local isSystemYield = coroutine.status(co) ~= "dead" and result[2] ~= nil + if isSystemYield then + args = coroutine.yield(result[2]) + else + return true, table.unpack(result, 3) + end + else -- error: result = (bool, string) return table.unpack(result) end end end ---[[ Pull a signal with an optional timeout. ]] +function sandbox.coroutine.yield(...) + return coroutine.yield(nil, ...) +end + function sandbox.os.signal(name, timeout) checkArg(1, name, "string", "nil") checkArg(2, timeout, "number", "nil") - local deadline = os.clock() + (timeout or math.huge) - while os.clock() < deadline do - local signal = {coroutine.yield(deadline - os.clock())} - if signal and (signal[1] == name or name == nil) then + local waitUntil = os.clock() + (timeout or math.huge) + while os.clock() < waitUntil do + local signal = {coroutine.yield(waitUntil - os.clock())} + if signal and (name == signal[1] or name == nil) then return table.unpack(signal) end end end ---[[ Shutdown the computer. ]] function sandbox.os.shutdown() coroutine.yield(false) end ---[[ Reboot the computer. ]] function sandbox.os.reboot() coroutine.yield(true) end +sandbox.driver = {} + +function sandbox.driver.componentType(id) + return nodeName(id) +end + +do + local env = setmetatable({ + sendToNode = sendToNode, + sendToAll = sendToAll + }, { __index = sandbox }) + for name, code in pairs(drivers()) do + local driver, reason = load(code, "=" .. name, "t", env) + if not driver then + print("Failed loading driver '" .. name .. "': " .. reason) + else + local result, reason = pcall(driver) + if not result then + print("Failed initializing driver '" .. name .. "': " .. reason) + end + end + end +end + +-- Load init script in sandboxed environment. +local coinit +do + local result, reason = load(init(), "=init", "t", sandbox) + if not result then + error(reason) + end + coinit = coroutine.create(result) +end + +-- Yield once to allow initializing up to here to get a memory baseline. +coroutine.yield() + -- JNLua converts the coroutine to a string immediately, so we can't get the -- traceback later. Because of that we have to do the error handling here. -return pcall(function() - -- Replace init script code with loaded, sandboxed and threaded script. - local init = (function() - local result, reason = load(init(), "=init", "t", sandbox) - if not result then error(reason, 0) end - return coroutine.create(result) - end)() - - -- Main kernel loop. - local data = {} - while true do - deadline = os.realTime() + 3 - local result = {coroutine.resume(init, table.unpack(data))} - if result[1] then - -- Init should never return, so we have a yield. The first yielded - -- value can only be set by system yields. - result = result[2] - else - -- Some other error, go kill ourselves. - error(result[2]) - end - data = {coroutine.yield(result)} - end -end) \ No newline at end of file +return pcall(main, coinit) \ No newline at end of file diff --git a/assets/opencomputers/lua/redstone.lua b/assets/opencomputers/lua/redstone.lua index 1a56f7176..25f458282 100644 --- a/assets/opencomputers/lua/redstone.lua +++ b/assets/opencomputers/lua/redstone.lua @@ -5,34 +5,34 @@ driver.redstone = {} driver.redstone.sides = {"top", "bottom", "left", "right", "front", "back"} -- Add inverse mapping and aliases. -for k, v in pairs(sides) do sides[v] = k end -sides.up = sides.top -sides.down = sides.bottom +for k, v in pairs(driver.redstone.sides) do + driver.redstone.sides[v] = k +end +driver.redstone.sides.up = driver.redstone.sides.top +driver.redstone.sides.down = driver.redstone.sides.bottom -function driver.redstone.getAnalogInput(card, side) - checkArg(1, side, "number") - sendToNode(card, os.address(), "redstone.input", side) +local safeOsAddress = os.address + +function driver.redstone.analogInput(card, side) + sendToNode(card, safeOsAddress(), "redstone.input", side) end -function driver.redstone.getAnalogOutput(card, side) - checkArg(1, side, "number") - sendToNode(card, os.address(), "redstone.output", side) +function driver.redstone.analogOutput(card, side, value) + if value then + sendToNode(card, safeOsAddress(), "redstone.output=", side, value) + else + return sendToNode(card, safeOsAddress(), "redstone.output", side) + end end -function driver.redstone.setAnalogOutput(card, side, value) - checkArg(1, side, "number") - checkArg(2, side, "number") - sendToNode(card, os.address(), "redstone.output=", side, value) +function driver.redstone.input(card, side) + return driver.redstone.analogInput(card, side) > 0 end -function getInput(side) - return getAnalogInput(side) > 0 -end - -function getOutput(side) - return getAnalogOutput(side) > 0 -end - -function setOutput(side, value) - rs.setAnalogOutput(side, value and 15 or 0) +function output(card, side, value) + if value then + driver.redstone.analogOutput(side, value and 15 or 0) + else + return driver.redstone.analogOutput(card, side) > 0 + end end diff --git a/li/cil/oc/api/Driver.scala b/li/cil/oc/api/Driver.scala new file mode 100644 index 000000000..c455787fc --- /dev/null +++ b/li/cil/oc/api/Driver.scala @@ -0,0 +1,56 @@ +package li.cil.oc.api + +import java.io.InputStream +import li.cil.oc.api.driver.{Item, Block} + +/** + * This interface specifies the structure of a driver for a component. + *

+ * A driver is essentially the glue code that allows arbitrary objects to be + * used as computer components. They specify an API that is injected into the + * Lua state when the driver is installed, and provide general information used + * by the computer. + *

+ * Do not implement this interface directly; use the `ItemDriver` and + * `BlockDriver` interfaces for the respective component types. + */ +trait Driver { + /** + * The code that is run when the driver is installed. + *

+ * The code will be run in a 'privileged' sandbox, i.e the basic environment + * is already the sandboxed environment (thus any changes made to the globals + * table will be noticeable - keep copies of security relevant functions). In + * addition to the sandbox it will also have access to two more functions, + * `sendToNode` and `sendToAll`, which will send a (synchronized) message to + * the component network the computer is attached to. + *

+ * Drivers must take all precautions to avoid these two functions "leaking" + * into user-space; at that point the user is pretty much in "creative mode", + * relatively speaking (i.e. the user could send arbitrary messages into the + * network, which is really not a good idea). + *

+ * This can be `None` to do nothing. Otherwise this is expected to be valid + * Lua code (it is loaded via load() and then run). + *

+ * The stream has to be recreated each time this is called. Normally you will + * return something along the lines of + * `Mod.class.getResourceAsStream("/assets/yourmod/ocapi.lua")` + * from this method. If you wish to hard-code the returned script, you can + * use `new ByteArrayInputStream(yourScript.getBytes())` instead. + *

+ * IMPORTANT: Note that the stream will automatically closed. + * + * @return the Lua code to run when a computer is started up. + */ + def api: Option[InputStream] = None +} + +object Driver { + /** Initialized in pre-init. */ + private[oc] var registry: Option[{def add(driver: Block); def add(driver: Item)}] = None + + def add(driver: Block) = registry.foreach(_.add(driver)) + + def add(driver: Item) = registry.foreach(_.add(driver)) +} \ No newline at end of file diff --git a/li/cil/oc/api/network/Network.scala b/li/cil/oc/api/Network.scala similarity index 89% rename from li/cil/oc/api/network/Network.scala rename to li/cil/oc/api/Network.scala index 49680372c..3979bab29 100644 --- a/li/cil/oc/api/network/Network.scala +++ b/li/cil/oc/api/Network.scala @@ -1,4 +1,7 @@ -package li.cil.oc.api.network +package li.cil.oc.api + +import li.cil.oc.api.network.Node +import net.minecraft.world.IBlockAccess /** * Interface for interacting with component networks. @@ -12,15 +15,15 @@ package li.cil.oc.api.network * neighbors to see if a network already exists. If so, it should join that * network. If multiple different networks are adjacent it should join one and * then merge it with the other(s). If no networks exist, it should create a new - * one. All this logic is provided by `NetworkAPI#joinOrCreateNetwork`. + * one. All this logic is provided by `Network.joinOrCreateNetwork`. *

* Note that for network nodes implemented in TileEntities adding and * removal is automatically provided on chunk load and unload. When a block is * placed or broken you will have to implement this logic yourself (i.e. call - * NetworkAPI.joinOrCreateNetwork in onBlockAdded and - * getNetwork.remove in breakBlock. + * Network.joinOrCreateNetwork in onBlockAdded and + * Network.remove in breakBlock. *

- * All other kinds of nodes have to be managed manually. See `INetworkNode`. + * All other kinds of nodes have to be managed manually. See `Node`. *

* There are a couple of system messages to be aware of. These are all sent by * the network manager itself: @@ -38,7 +41,7 @@ package li.cil.oc.api.network * instances of your own network implementation; this will lead to * incompatibilities with the built-in network implementation (which can only * merge with other networks of its own type). Always use the methods provided - * in NetworkAPI to create and join networks. + * in Network to create and join networks. */ trait Network { /** @@ -59,10 +62,6 @@ trait Network { * @return true if a new connection between the two nodes was added; false if * the connection already existed. * @throws IllegalArgumentException if neither node is in this network. - * @throws IllegalStateException if this is called while the network is - * already updating, for example from - * `INetworkNode#receive` (which may be - * called during the update). */ def connect(nodeA: Node, nodeB: Node): Boolean @@ -215,4 +214,20 @@ trait Network { * @param data the message to send. */ def sendToAll(source: Node, name: String, data: Any*) +} + +object Network { + /** Initialized in pre-init. */ + private[oc] var network: Option[{def joinOrCreateNetwork(world: IBlockAccess, x: Int, y: Int, z: Int)}] = None + + /** + * Tries to add a tile entity network node at the specified coordinates to adjacent networks. + * + * @param world the world the tile entity lives in. + * @param x the X coordinate of the tile entity. + * @param y the Y coordinate of the tile entity. + * @param z the Z coordinate of the tile entity. + */ + def joinOrCreateNetwork(world: IBlockAccess, x: Int, y: Int, z: Int) = + network.foreach(_.joinOrCreateNetwork(world, x, y, z)) } \ No newline at end of file diff --git a/li/cil/oc/api/driver/API.scala b/li/cil/oc/api/driver/API.scala deleted file mode 100644 index f8ce1b8fd..000000000 --- a/li/cil/oc/api/driver/API.scala +++ /dev/null @@ -1,13 +0,0 @@ -package li.cil.oc.api.driver - -import li.cil.oc.server.computer.Drivers - -object API { - def addDriver(driver: Block) { - Drivers.add(driver) - } - - def addDriver(driver: Item) { - Drivers.add(driver) - } -} \ No newline at end of file diff --git a/li/cil/oc/api/driver/Block.scala b/li/cil/oc/api/driver/Block.scala index fcaf16c80..e689a55c7 100644 --- a/li/cil/oc/api/driver/Block.scala +++ b/li/cil/oc/api/driver/Block.scala @@ -2,13 +2,14 @@ package li.cil.oc.api.driver import li.cil.oc.api.network.Node import net.minecraft.world.World +import li.cil.oc.api.Driver /** * Interface for block component drivers. *

* This driver type is used for components that are blocks, i.e. that can be * placed in the world, but cannot be modified to or don't want to have their - * `TileEntities` implement `INetworkNode`. + * `TileEntities` implement `network.Node`. *

* A block driver is used by proxy blocks to check its neighbors and whether * those neighbors should be treated as components or not. diff --git a/li/cil/oc/api/driver/Driver.scala b/li/cil/oc/api/driver/Driver.scala deleted file mode 100644 index c2a10d3dc..000000000 --- a/li/cil/oc/api/driver/Driver.scala +++ /dev/null @@ -1,45 +0,0 @@ -package li.cil.oc.api.driver - -import java.io.InputStream - -/** - * This interface specifies the structure of a driver for a component. - *

- * A driver is essentially the glue code that allows arbitrary objects to be - * used as computer components. They specify an API that is injected into the - * Lua state when the driver is installed, and provide general information used - * by the computer. - *

- * Do not implement this interface directly; use the `IItemDriver` and - * `IBlockDriver` interfaces for the respective component types. - */ -trait Driver { - /** - * Some initialization code that is run when the driver is installed. - *

- * These will usually be some functions that generate network messages of - * the particular signature the node of the driver handles, but may contain - * arbitrary other functions. However, whatever you do, keep in mind that - * only certain parts of the global namespace will be made available to the - * computer at runtime, so it's best to keep all you declare in the driver - * table (global variable `driver`). - *

- * This is loaded into the Lua state and run in the global, un-sandboxed - * environment. This means your scripts can mess things up bad, so make sure - * you know what you're doing and exposing. - *

- * This can be `None` to do nothing. Otherwise this is expected to be valid - * Lua code (it is simply loaded via load() and then executed). - *

- * The stream has to be recreated each time this is called. Normally you will - * return something along the lines of - * `Mod.class.getResourceAsStream("/assets/yourmod/lua/ocapi.lua")` - * from this method. If you wish to hard-code the returned script, you can use - * `new ByteArrayInputStream(yourScript.getBytes())` instead. - *

- * IMPORTANT: Note that the stream will automatically closed. - * - * @return the Lua code to run when a computer is started up. - */ - def api: Option[InputStream] = None -} \ No newline at end of file diff --git a/li/cil/oc/api/driver/Item.scala b/li/cil/oc/api/driver/Item.scala index 9c1a43f26..fefa8ffd3 100644 --- a/li/cil/oc/api/driver/Item.scala +++ b/li/cil/oc/api/driver/Item.scala @@ -2,6 +2,7 @@ package li.cil.oc.api.driver import li.cil.oc.api.network.Node import net.minecraft.item.ItemStack +import li.cil.oc.api.Driver /** * Interface for item component drivers. @@ -13,9 +14,9 @@ import net.minecraft.item.ItemStack * When trying to add an item to a computer the list of registered drivers is * queried using the drivers' `worksWith` functions. The first driver that * replies positively and whose check against the slot type is successful, i.e. - * for which the `componentType` matches the slot, will be used as the - * component's driver and the component will be added. If no driver is found - * the item will be rejected and cannot be installed. + * for which the `slot` matches the slot it should be inserted into, will be + * used as the component's driver and the component will be added. If no driver + * is found the item will be rejected and cannot be installed. *

* Note that it is possible to write one driver that supports as many different * items as you wish. I'd recommend writing one per device (type), though, to diff --git a/li/cil/oc/api/network/API.scala b/li/cil/oc/api/network/API.scala deleted file mode 100644 index eb5656f00..000000000 --- a/li/cil/oc/api/network/API.scala +++ /dev/null @@ -1,21 +0,0 @@ -package li.cil.oc.api.network - -import li.cil.oc.server -import net.minecraft.world.IBlockAccess - -/** - * Provides convenience methods for interacting with component networks. - */ -object API { - /** - * Tries to add a tile entity network node at the specified coordinates to adjacent networks. - * - * @param world the world the tile entity lives in. - * @param x the X coordinate of the tile entity. - * @param y the Y coordinate of the tile entity. - * @param z the Z coordinate of the tile entity. - */ - def joinOrCreateNetwork(world: IBlockAccess, x: Int, y: Int, z: Int) = - // TODO reflection - server.network.Network.joinOrCreateNetwork(world, x, y, z) -} \ No newline at end of file diff --git a/li/cil/oc/api/network/Node.scala b/li/cil/oc/api/network/Node.scala index 6a282fdbd..bfeea4189 100644 --- a/li/cil/oc/api/network/Node.scala +++ b/li/cil/oc/api/network/Node.scala @@ -1,6 +1,7 @@ package li.cil.oc.api.network import net.minecraft.nbt.NBTTagCompound +import li.cil.oc.api.Network /** * A single node in a `INetwork`. @@ -83,7 +84,7 @@ trait Node { * * @return the network the node is in. */ - var network: Network = null + var network: Option[Network] = None /** * Makes the node handle a message. diff --git a/li/cil/oc/common/Proxy.scala b/li/cil/oc/common/Proxy.scala index ce2ac4903..26f218971 100644 --- a/li/cil/oc/common/Proxy.scala +++ b/li/cil/oc/common/Proxy.scala @@ -4,10 +4,12 @@ import cpw.mods.fml.common.event._ import cpw.mods.fml.common.network.NetworkRegistry import cpw.mods.fml.common.registry.LanguageRegistry import li.cil.oc._ -import li.cil.oc.api.driver.API -import li.cil.oc.server.computer.{Computer, Drivers} +import li.cil.oc.api.Driver +import li.cil.oc.api.Network +import li.cil.oc.server.computer.Computer import li.cil.oc.server.driver -import li.cil.oc.server.network.Network +import li.cil.oc.server.driver.Registry +import li.cil.oc.server.network import net.minecraftforge.common.MinecraftForge class Proxy { @@ -16,6 +18,9 @@ class Proxy { LanguageRegistry.instance.loadLocalization( "/assets/opencomputers/lang/en_US.lang", "en_US", false) + + Driver.registry = Some(driver.Registry) + Network.network = Some(network.Network) } def init(e: FMLInitializationEvent): Unit = { @@ -24,17 +29,17 @@ class Proxy { NetworkRegistry.instance.registerGuiHandler(OpenComputers, GuiHandler) - API.addDriver(driver.GraphicsCard) - API.addDriver(driver.Keyboard) + Driver.add(driver.GraphicsCard) + Driver.add(driver.Keyboard) MinecraftForge.EVENT_BUS.register(Computer) - MinecraftForge.EVENT_BUS.register(Network) + MinecraftForge.EVENT_BUS.register(network.Network) } def postInit(e: FMLPostInitializationEvent): Unit = { // Lock the driver registry to avoid drivers being added after computers // may have already started up. This makes sure the driver API won't change // over the course of a game, since that could lead to weird effects. - Drivers.locked = true + Registry.locked = true } } \ No newline at end of file diff --git a/li/cil/oc/common/block/Multi.scala b/li/cil/oc/common/block/Multi.scala index 12e065196..b667457f6 100644 --- a/li/cil/oc/common/block/Multi.scala +++ b/li/cil/oc/common/block/Multi.scala @@ -3,7 +3,8 @@ package li.cil.oc.common.block import cpw.mods.fml.common.registry.GameRegistry import li.cil.oc.Config import li.cil.oc.CreativeTab -import li.cil.oc.api.network.{Node, API} +import li.cil.oc.api.Network +import li.cil.oc.api.network.Node import li.cil.oc.common.tileentity.TileEntityRotatable import net.minecraft.block.Block import net.minecraft.block.material.Material @@ -102,7 +103,7 @@ class Multi(id: Int) extends Block(id, Material.iron) { case None => // Invalid but avoid match error. case Some(subBlock) => { world.getBlockTileEntity(x, y, z) match { - case node: Node => node.network.remove(node) + case node: Node => node.network.foreach(_.remove(node)) } subBlock.breakBlock(world, x, y, z, blockId, metadata) } @@ -266,7 +267,7 @@ class Multi(id: Int) extends Block(id, Material.iron) { case None => // Invalid but avoid match error. case Some(subBlock) => { world.getBlockTileEntity(x, y, z) match { - case _: Node => API.joinOrCreateNetwork(world, x, y, z) + case _: Node => Network.joinOrCreateNetwork(world, x, y, z) } subBlock.onBlockAdded(world, x, y, z) } diff --git a/li/cil/oc/common/component/ScreenEnvironment.scala b/li/cil/oc/common/component/ScreenEnvironment.scala index 53bf0b224..082ef94fa 100644 --- a/li/cil/oc/common/component/ScreenEnvironment.scala +++ b/li/cil/oc/common/component/ScreenEnvironment.scala @@ -52,9 +52,8 @@ trait ScreenEnvironment extends Node { } - def onScreenResolutionChange(w: Int, h: Int) = if (network != null) { - network.sendToAll(this, "computer.signal", "screen_resized", w, h) - } + def onScreenResolutionChange(w: Int, h: Int) = + network.foreach(_.sendToAll(this, "computer.signal", "screen_resized", w, h)) def onScreenSet(col: Int, row: Int, s: String) {} diff --git a/li/cil/oc/common/tileentity/ItemComponentProxy.scala b/li/cil/oc/common/tileentity/ItemComponentProxy.scala index fd30ecfbd..354d44702 100644 --- a/li/cil/oc/common/tileentity/ItemComponentProxy.scala +++ b/li/cil/oc/common/tileentity/ItemComponentProxy.scala @@ -1,7 +1,6 @@ package li.cil.oc.common.tileentity import li.cil.oc.api.network.Node -import li.cil.oc.server.computer.Drivers import net.minecraft.inventory.IInventory import net.minecraft.item.ItemStack import net.minecraft.nbt.NBTTagCompound @@ -9,6 +8,7 @@ import net.minecraft.nbt.NBTTagList import net.minecraft.world.World import li.cil.oc.api.driver.Slot import li.cil.oc.common.component.Computer +import li.cil.oc.server.driver.Registry trait ItemComponentProxy extends IInventory with Node { protected val inventory = new Array[ItemStack](8) @@ -52,29 +52,29 @@ trait ItemComponentProxy extends IInventory with Node { // NetworkNode // ----------------------------------------------------------------------- // - override protected def onConnect() = { + override protected def onConnect() { super.onConnect() for (slot <- 0 until inventory.length) { itemNode(slot) match { case None => // Ignore. case Some(node) => - network.connect(this, node) + network.foreach(_.connect(this, node)) } } } - override protected def onDisconnect() = { + override protected def onDisconnect() { super.onDisconnect() for (slot <- 0 until inventory.length) { itemNode(slot) match { case None => // Ignore. case Some(node) => - node.network.remove(node) + node.network.foreach(_.remove(node)) } } } - private def itemNode(slot: Int) = Drivers.driverFor(inventory(slot)) match { + private def itemNode(slot: Int) = Registry.driverFor(inventory(slot)) match { case None => None case Some(driver) => driver.node(inventory(slot)) } @@ -115,7 +115,7 @@ trait ItemComponentProxy extends IInventory with Node { if (!world.isRemote) itemNode(slot) match { case None => // Nothing to do. case Some(node) => - node.network.remove(node) + node.network.foreach(_.remove(node)) } inventory(slot) = item @@ -125,11 +125,11 @@ trait ItemComponentProxy extends IInventory with Node { if (!world.isRemote) itemNode(slot) match { case None => // Nothing to do. case Some(node) => - network.connect(this, node) + network.foreach(_.connect(this, node)) } } - def isItemValidForSlot(slot: Int, item: ItemStack) = (slot, Drivers.driverFor(item)) match { + def isItemValidForSlot(slot: Int, item: ItemStack) = (slot, Registry.driverFor(item)) match { case (_, None) => false // Invalid item. case (0, Some(driver)) => driver.slot(item) == Slot.PSU case (1 | 2 | 3, Some(driver)) => driver.slot(item) == Slot.PCI diff --git a/li/cil/oc/common/tileentity/TileEntityComputer.scala b/li/cil/oc/common/tileentity/TileEntityComputer.scala index 04064e2cf..96da718f4 100644 --- a/li/cil/oc/common/tileentity/TileEntityComputer.scala +++ b/li/cil/oc/common/tileentity/TileEntityComputer.scala @@ -96,11 +96,10 @@ class TileEntityComputer(isClient: Boolean) extends TileEntityRotatable with com } if (isRunning != computer.isRunning) { isRunning = computer.isRunning - if (network != null) - if (isRunning) - network.sendToAll(this, "computer.started") - else - network.sendToAll(this, "computer.stopped") + if (isRunning) + network.foreach(_.sendToAll(this, "computer.started")) + else + network.foreach(_.sendToAll(this, "computer.stopped")) ServerPacketSender.sendComputerState(this, isRunning) } } diff --git a/li/cil/oc/common/tileentity/TileEntityKeyboard.scala b/li/cil/oc/common/tileentity/TileEntityKeyboard.scala index 3c81d1f4e..38a5b24e1 100644 --- a/li/cil/oc/common/tileentity/TileEntityKeyboard.scala +++ b/li/cil/oc/common/tileentity/TileEntityKeyboard.scala @@ -15,13 +15,13 @@ class TileEntityKeyboard extends TileEntityRotatable with Node { message.data match { case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyDown" => if (isUseableByPlayer(p)) - network.sendToAll(this, "computer.signal", "key_down", char, code) + network.foreach(_.sendToAll(this, "computer.signal", "key_down", char, code)) case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyUp" => if (isUseableByPlayer(p)) - network.sendToAll(this, "computer.signal", "key_up", char, code) + network.foreach(_.sendToAll(this, "computer.signal", "key_up", char, code)) case Array(p: Player, value: String) if message.name == "keyboard.clipboard" => if (isUseableByPlayer(p)) - network.sendToAll(this, "computer.signal", "clipboard", value) + network.foreach(_.sendToAll(this, "computer.signal", "clipboard", value)) case _ => // Ignore. } None diff --git a/li/cil/oc/server/PacketHandler.scala b/li/cil/oc/server/PacketHandler.scala index 50035bedd..1a8e4f001 100644 --- a/li/cil/oc/server/PacketHandler.scala +++ b/li/cil/oc/server/PacketHandler.scala @@ -68,18 +68,18 @@ class PacketHandler extends CommonPacketHandler { def onKeyDown(p: PacketParser) = p.readTileEntity[Node]() match { case None => // Invalid packet. - case Some(n) => n.network.sendToNeighbors(n, "keyboard.keyDown", p.player, p.readChar(), p.readInt()) + case Some(n) => n.network.foreach(_.sendToNeighbors(n, "keyboard.keyDown", p.player, p.readChar(), p.readInt())) } def onKeyUp(p: PacketParser) = p.readTileEntity[Node]() match { case None => // Invalid packet. - case Some(n) => n.network.sendToNeighbors(n, "keyboard.keyUp", p.player, p.readChar(), p.readInt()) + case Some(n) => n.network.foreach(_.sendToNeighbors(n, "keyboard.keyUp", p.player, p.readChar(), p.readInt())) } def onClipboard(p: PacketParser) = p.readTileEntity[Node]() match { case None => // Invalid packet. - case Some(n) => n.network.sendToNeighbors(n, "keyboard.clipboard", p.player, p.readUTF()) + case Some(n) => n.network.foreach(_.sendToNeighbors(n, "keyboard.clipboard", p.player, p.readUTF())) } } \ No newline at end of file diff --git a/li/cil/oc/server/component/GraphicsCard.scala b/li/cil/oc/server/component/GraphicsCard.scala index 47479b022..0a12d0d7d 100644 --- a/li/cil/oc/server/component/GraphicsCard.scala +++ b/li/cil/oc/server/component/GraphicsCard.scala @@ -1,5 +1,6 @@ package li.cil.oc.server.component +import li.cil.oc.api.Network import li.cil.oc.api.network.Message import net.minecraft.nbt.NBTTagCompound @@ -10,27 +11,28 @@ class GraphicsCard(nbt: NBTTagCompound) extends ItemComponent(nbt) { override def receive(message: Message) = { super.receive(message) + def trySend(f: Network => Option[Array[Any]]) = network.fold(None: Option[Array[Any]])(f) message.data match { case Array(screen: Double, w: Double, h: Double) if message.name == "gpu.resolution=" => if (supportedResolutions.contains((w.toInt, h.toInt))) - network.sendToAddress(this, screen.toInt, "screen.resolution=", w.toInt, h.toInt) + trySend(_.sendToAddress(this, screen.toInt, "screen.resolution=", w.toInt, h.toInt)) else Some(Array(None, "unsupported resolution")) case Array(screen: Double) if message.name == "gpu.resolution" => - network.sendToAddress(this, screen.toInt, "screen.resolution") + trySend(_.sendToAddress(this, screen.toInt, "screen.resolution")) case Array(screen: Double) if message.name == "gpu.resolutions" => - network.sendToAddress(this, screen.toInt, "screen.resolutions") match { + trySend(_.sendToAddress(this, screen.toInt, "screen.resolutions")) match { case Some(Array(resolutions@_*)) => Some(Array(supportedResolutions.intersect(resolutions): _*)) case _ => None } case Array(screen: Double, x: Double, y: Double, value: String) if message.name == "gpu.set" => - network.sendToAddress(this, screen.toInt, "screen.set", x.toInt - 1, y.toInt - 1, value) + trySend(_.sendToAddress(this, screen.toInt, "screen.set", x.toInt - 1, y.toInt - 1, value)) case Array(screen: Double, x: Double, y: Double, w: Double, h: Double, value: String) if message.name == "gpu.fill" => if (value != null && value.length == 1) - network.sendToAddress(this, screen.toInt, "screen.fill", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, value.charAt(0)) + trySend(_.sendToAddress(this, screen.toInt, "screen.fill", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, value.charAt(0))) else Some(Array(None, "invalid fill value")) case Array(screen: Double, x: Double, y: Double, w: Double, h: Double, tx: Double, ty: Double) if message.name == "gpu.copy" => - network.sendToAddress(this, screen.toInt, "screen.copy", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, tx.toInt, ty.toInt) + trySend(_.sendToAddress(this, screen.toInt, "screen.copy", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, tx.toInt, ty.toInt)) case _ => None } } diff --git a/li/cil/oc/server/component/RedstoneCard.scala b/li/cil/oc/server/component/RedstoneCard.scala index 23f455173..dfd5190a4 100644 --- a/li/cil/oc/server/component/RedstoneCard.scala +++ b/li/cil/oc/server/component/RedstoneCard.scala @@ -1,6 +1,7 @@ package li.cil.oc.server.component import li.cil.oc.api.network.Message +import li.cil.oc.api.network.Node import net.minecraft.nbt.NBTTagCompound import net.minecraft.tileentity.TileEntity import net.minecraftforge.common.ForgeDirection @@ -21,7 +22,9 @@ class RedstoneCard(nbt: NBTTagCompound) extends ItemComponent(nbt) { } } - private def input(target: Int, side: Int) = if (side >= 0 && side < 6) network.node(target) match { + private def tryGet(target: Int) = network.fold(None: Option[Node])(_.node(target)) + + private def input(target: Int, side: Int) = if (side >= 0 && side < 6) tryGet(target) match { case Some(r: RedstoneEnabled) => Some(Array(r.input(ForgeDirection.getOrientation(side)).asInstanceOf[Any])) case Some(t: TileEntity) => val face = ForgeDirection.getOrientation(side.toInt) @@ -31,7 +34,7 @@ class RedstoneCard(nbt: NBTTagCompound) extends ItemComponent(nbt) { case _ => None // Can't work with this node. } else None - private def output(target: Int, side: Int) = if (side >= 0 && side < 6) network.node(target) match { + private def output(target: Int, side: Int) = if (side >= 0 && side < 6) tryGet(target) match { case Some(r: RedstoneEnabled) => Some(Array(r.output(ForgeDirection.getOrientation(side)).asInstanceOf[Any])) case Some(t: TileEntity) => val power = t.worldObj.isBlockProvidingPowerTo(t.xCoord, t.yCoord, t.zCoord, side.toInt) @@ -39,7 +42,7 @@ class RedstoneCard(nbt: NBTTagCompound) extends ItemComponent(nbt) { case _ => None // Can't work with this node. } else None - private def output(target: Int, side: Int, value: Int) = if (side >= 0 && side < 6) network.node(target) match { + private def output(target: Int, side: Int, value: Int) = if (side >= 0 && side < 6) tryGet(target) match { case Some(r: RedstoneEnabled) => r.output(ForgeDirection.getOrientation(side)) = value case _ => // Can't work with this node. } diff --git a/li/cil/oc/server/computer/Computer.scala b/li/cil/oc/server/computer/Computer.scala index e76bd47bd..702a0ba27 100644 --- a/li/cil/oc/server/computer/Computer.scala +++ b/li/cil/oc/server/computer/Computer.scala @@ -5,8 +5,10 @@ import java.lang.Thread.UncaughtExceptionHandler import java.util.concurrent._ import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Level +import li.cil.oc.api.network.Node import li.cil.oc.common.component import li.cil.oc.common.tileentity.TileEntityComputer +import li.cil.oc.server.driver import li.cil.oc.{OpenComputers, Config} import net.minecraft.nbt._ import net.minecraft.tileentity.TileEntity @@ -47,7 +49,7 @@ class Computer(val owner: Environment) extends component.Computer with Runnable private var state = Computer.State.Stopped /** The internal Lua state. Only set while the computer is running. */ - private[computer] var lua: LuaState = null + private var lua: LuaState = null /** * The base memory consumption of the kernel. Used to permit a fixed base @@ -151,7 +153,7 @@ class Computer(val owner: Environment) extends component.Computer with Runnable signal("") // Inject component added signals for all nodes in the network. - owner.network.nodes(owner).foreach(node => signal("component_added", node.address)) + owner.network.foreach(_.nodes(owner).foreach(node => signal("component_added", node.address))) // All green, computer started successfully. true @@ -220,27 +222,27 @@ class Computer(val owner: Environment) extends component.Computer with Runnable // to the coroutine.yield() that triggered the call. lua.call(0, 1) lua.checkType(2, LuaType.TABLE) + // Nothing should have been able to trigger a future. + assert(future.isEmpty) + // If the call lead to stop() being called we stop right now, + // otherwise we return the result to our executor. + if (state == Computer.State.Stopping) + close() + else + execute(Computer.State.SynchronizedReturn) } catch { case _: LuaMemoryAllocationException => // This can happen if we run out of memory while converting a Java // exception to a string (which we have to do to avoid keeping // userdata on the stack, which cannot be persisted). OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages - owner.network.sendToAll(owner, "computer.crashed", "not enough memory") + owner.network.foreach(_.sendToAll(owner, "computer.crashed", "not enough memory")) close() case e: Throwable => { OpenComputers.log.log(Level.WARNING, "Faulty Lua implementation for synchronized calls.", e) close() } } - // Nothing should have been able to trigger a future. - assert(future.isEmpty) - // If the call lead to stop() being called we stop right now, - // otherwise we return the result to our executor. - if (state == Computer.State.Stopping) - close() - else - execute(Computer.State.SynchronizedReturn) } case _ => // Nothing special to do, just avoid match errors. }) @@ -256,7 +258,6 @@ class Computer(val owner: Environment) extends component.Computer with Runnable if (state != Computer.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 @@ -295,11 +296,12 @@ class Computer(val owner: Environment) extends component.Computer with Runnable }.toArray) }).asJava) + kernelMemory = nbt.getInteger("kernelMemory") timeStarted = nbt.getLong("timeStarted") // Clean up some after we're done and limit memory again. lua.gc(LuaState.GcAction.COLLECT, 0) - lua.setTotalMemory(memory) + lua.setTotalMemory(kernelMemory + 16 * 1024) // Start running our worker thread if we have to (for cases where it // would not be re-started automatically in update()). We start with a @@ -363,6 +365,7 @@ class Computer(val owner: Environment) extends component.Computer with Runnable } nbt.setTag("signals", list) + nbt.setInteger("kernelMemory", kernelMemory) nbt.setLong("timeStarted", timeStarted) } catch { @@ -523,7 +526,8 @@ class Computer(val owner: Environment) extends component.Computer with Runnable } lua.pushJavaFunction(ScalaFunction(lua => - owner.network.sendToAddress(owner, lua.checkInteger(1), lua.checkString(2), parseArguments(lua, 3): _*) match { + owner.network.fold(None: Option[Array[Any]])(_. + sendToAddress(owner, lua.checkInteger(1), lua.checkString(2), parseArguments(lua, 3): _*)) match { case Some(Array(results@_*)) => results.foreach(pushResult(lua, _)) results.length @@ -532,19 +536,44 @@ class Computer(val owner: Environment) extends component.Computer with Runnable lua.setGlobal("sendToNode") lua.pushJavaFunction(ScalaFunction(lua => { - owner.network.sendToAll(owner, lua.checkString(1), parseArguments(lua, 2): _*) + owner.network.foreach(_.sendToAll(owner, lua.checkString(1), parseArguments(lua, 2): _*)) 0 })) lua.setGlobal("sendToAll") lua.pushJavaFunction(ScalaFunction(lua => { - owner.network.node(lua.checkInteger(1)) match { + owner.network.fold(None: Option[Node])(_.node(lua.checkInteger(1))) match { case None => 0 case Some(node) => lua.pushString(node.name); 1 } })) lua.setGlobal("nodeName") + // Provide driver API code. + lua.pushJavaFunction(ScalaFunction(lua => { + val apis = driver.Registry.apis + lua.newTable(apis.length, 0) + for ((name, code) <- apis) { + lua.pushString(Source.fromInputStream(code).mkString) + code.close() + lua.setField(-2, name) + } + 1 + })) + lua.setGlobal("drivers") + + // Loads the init script. This is loaded and then run by the kernel as a + // separate coroutine to sandbox it and enforce timeouts and sandbox user + // scripts. + lua.pushJavaFunction(new JavaFunction() { + def invoke(lua: LuaState): Int = { + lua.pushString(Source.fromInputStream(classOf[Computer]. + getResourceAsStream("/assets/opencomputers/lua/init.lua")).mkString) + 1 + } + }) + lua.setGlobal("init") + // Run the boot script. This sets up the permanent value tables as // well as making the functions used for persisting/unpersisting // available as globals. It also wraps the message sending functions @@ -554,30 +583,15 @@ class Computer(val owner: Environment) extends component.Computer with Runnable "/assets/opencomputers/lua/boot.lua"), "=boot", "t") lua.call(0, 0) - // 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. - Drivers.installOn(this) - - // Loads the init script. This is run by the kernel as a separate - // coroutine to enforce timeouts and sandbox user scripts. - lua.pushJavaFunction(new JavaFunction() { - def invoke(lua: LuaState): Int = { - lua.pushString(Source.fromInputStream(classOf[Computer]. - getResourceAsStream("/assets/opencomputers/lua/init.lua")).mkString) - 1 - } - }) - lua.setGlobal("init") - - // 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. + // Load the basic kernel which sets up the sandbox, loads the init script + // and then runs it in a coroutine with a debug hook checking for + // timeouts. lua.load(classOf[Computer].getResourceAsStream( "/assets/opencomputers/lua/kernel.lua"), "=kernel", "t") - lua.newThread() // Leave it as the first value on the stack. + lua.newThread() // Left as the first value on the stack. + // Run to the first yield in kernel, to get a good idea of how much + // memory all the basic functionality we provide needs. + lua.pop(lua.resume(1, 0)) // 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 @@ -586,7 +600,7 @@ class Computer(val owner: Environment) extends component.Computer with Runnable // underlying system (which may change across releases). lua.gc(LuaState.GcAction.COLLECT, 0) kernelMemory = lua.getTotalMemory - lua.getFreeMemory - lua.setTotalMemory(kernelMemory + 64 * 1024) + lua.setTotalMemory(kernelMemory + 16 * 1024) // Clear any left-over signals from a previous run. signals.clear() @@ -731,6 +745,8 @@ class Computer(val owner: Environment) extends component.Computer with Runnable OpenComputers.log.warning("Kernel stopped unexpectedly.") } else { + // This can trigger another out of memory error if the original + // error was an out of memory error. OpenComputers.log.warning("Computer crashed.\n" + lua.toString(3)) // TODO remove this when we have a component that can display crash messages // TODO get this to the world as a computer.crashed message. problem: synchronizing it. //owner.network.sendToAll(owner, "computer.crashed", lua.toString(3)) @@ -742,12 +758,16 @@ class Computer(val owner: Environment) extends component.Computer with Runnable case e: LuaRuntimeException => OpenComputers.log.warning("Kernel crashed. This is a bug!\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) close() - case e: LuaMemoryAllocationException => { + case e: LuaMemoryAllocationException => + OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages + // TODO get this to the world as a computer.crashed message. problem: synchronizing it. + //owner.network.sendToAll(owner, "computer.crashed", "not enough memory") + close() + case e: java.lang.Error if e.getMessage == "not enough memory" => OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages // TODO get this to the world as a computer.crashed message. problem: synchronizing it. //owner.network.sendToAll(owner, "computer.crashed", "not enough memory") close() - } } // State has inevitably changed, mark as changed to save again. diff --git a/li/cil/oc/server/computer/LuaStateFactory.scala b/li/cil/oc/server/computer/LuaStateFactory.scala index 7f604e0fd..ffb2d50ef 100644 --- a/li/cil/oc/server/computer/LuaStateFactory.scala +++ b/li/cil/oc/server/computer/LuaStateFactory.scala @@ -5,7 +5,7 @@ import com.naef.jnlua.{JavaFunction, LuaState, NativeSupport} import java.io.File import java.io.FileOutputStream import java.nio.channels.Channels -import java.util.{Locale, Calendar} +import java.util.{Formatter, Locale, Calendar} import scala.util.Random case class ScalaFunction(f: (LuaState) => Int) extends JavaFunction { @@ -217,13 +217,13 @@ private[computer] object LuaStateFactory { })) state.setField(-2, "char") - // TODO find + // TODO find (probably not necessary?) - // TODO format + // TODO format (probably not necessary?) - // TODO gmatch + // TODO gmatch (probably not necessary?) - // TODO gsub + // TODO gsub (probably not necessary?) state.pushJavaFunction(ScalaFunction(lua => { lua.pushInteger(lua.checkString(1).length) @@ -237,7 +237,7 @@ private[computer] object LuaStateFactory { })) state.setField(-2, "lower") - // TODO match + // TODO match (probably not necessary?) state.pushJavaFunction(ScalaFunction(lua => { lua.pushString(lua.checkString(1).reverse) diff --git a/li/cil/oc/server/computer/Drivers.scala b/li/cil/oc/server/driver/Registry.scala similarity index 72% rename from li/cil/oc/server/computer/Drivers.scala rename to li/cil/oc/server/driver/Registry.scala index 08e304a8d..35c3179d8 100644 --- a/li/cil/oc/server/computer/Drivers.scala +++ b/li/cil/oc/server/driver/Registry.scala @@ -1,13 +1,10 @@ -package li.cil.oc.server.computer +package li.cil.oc.server.driver -import com.naef.jnlua.LuaRuntimeException -import li.cil.oc.OpenComputers import li.cil.oc.api.driver.{Block, Item} import net.minecraft.item.ItemStack import net.minecraft.world.World import scala.Some import scala.collection.mutable.ArrayBuffer -import scala.compat.Platform._ /** * This class keeps track of registered drivers and provides installation logic @@ -23,7 +20,7 @@ import scala.compat.Platform._ * usually require a component of the type the driver wraps to be installed in * the computer, but may also provide context-free functions. */ -private[oc] object Drivers { +private[oc] object Registry { /** The list of registered block drivers. */ private val blocks = ArrayBuffer.empty[Block] @@ -90,30 +87,11 @@ private[oc] object Drivers { else None /** - * Used by the computer to initialize its Lua state, injecting the APIs of - * all known drivers. + * Gets a list of all driver APIs. + * + * @return the apis of all known drivers. */ - private[computer] def installOn(computer: Computer) = - (blocks ++ items).foreach(driver => { - driver.api match { - case None => // Nothing to do. - case Some(code) => - val name = driver.getClass.getName - try { - computer.lua.load(code, "=" + name, "t") // ... func - code.close() - computer.lua.call(0, 0) // ... - } - catch { - case e: LuaRuntimeException => - OpenComputers.log.warning(String.format( - "Initialization code of driver %s threw an error: %s", - name, e.getLuaStackTrace.mkString("", EOL, EOL))) - case e: Throwable => - OpenComputers.log.warning(String.format( - "Initialization code of driver %s threw an error: %s", - name, e.getStackTraceString)) - } - } - }) + def apis = (blocks ++ items) map (driver => (driver.getClass.getSimpleName, driver.api)) collect { + case (name, Some(code)) => (name, code) + } } diff --git a/li/cil/oc/server/network/Network.scala b/li/cil/oc/server/network/Network.scala index 60ed741f4..b11ab0353 100644 --- a/li/cil/oc/server/network/Network.scala +++ b/li/cil/oc/server/network/Network.scala @@ -7,7 +7,7 @@ import _root_.net.minecraftforge.common.ForgeDirection import _root_.net.minecraftforge.event.ForgeSubscribe import _root_.net.minecraftforge.event.world.ChunkEvent import java.util.logging.Level -import li.cil.oc.OpenComputers +import li.cil.oc.{api, OpenComputers} import li.cil.oc.api.network.Visibility import li.cil.oc.api.{network => net} import scala.beans.BeanProperty @@ -26,7 +26,7 @@ import scala.collection.mutable.ArrayBuffer * It keeps the list of nodes as a lookup table for fast id->node resolving. * Note that it is possible for multiple nodes to have the same ID, though. */ -class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network.Node]]) extends net.Network { +class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network.Node]]) extends api.Network { def this(node: net.Node) = { this(mutable.Map({ if (node.address < 1) @@ -36,7 +36,7 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. send(new Network.ConnectMessage(node), List(node)) } - nodes.foreach(_.network = this) + nodes.foreach(_.network = Some(this)) def connect(nodeA: net.Node, nodeB: net.Node) = { val containsA = nodeMap.get(nodeA.address).exists(_.exists(_.data == nodeA)) @@ -67,7 +67,7 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. private def add(oldNode: Network.Node, addedNode: net.Node) = { // Check if the other node is new or if we have to merge networks. - val (newNode, sendQueue) = if (addedNode.network == null) { + val (newNode, sendQueue) = if (addedNode.network.isEmpty) { val newNode = new Network.Node(addedNode) if (nodeMap.contains(addedNode.address) || addedNode.address < 1) addedNode.address = findId() @@ -84,14 +84,14 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. nodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), List(addedNode)))) } nodeMap.getOrElseUpdate(address, new ArrayBuffer[Network.Node]) += newNode - addedNode.network = this + addedNode.network = Some(this) (newNode, sendQueue) } else { // Queue any messages to avoid side effects from receivers. val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[net.Node])] val thisNodes = nodes.toBuffer - val otherNetwork = addedNode.network.asInstanceOf[Network] + val otherNetwork = addedNode.network.get.asInstanceOf[Network] val otherNodes = otherNetwork.nodes.toBuffer otherNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), thisNodes))) thisNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), otherNodes))) @@ -114,7 +114,7 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. // Add nodes from other network into this network, including invalid nodes. otherNetwork.nodeMap.values.flatten.foreach(node => { nodeMap.getOrElseUpdate(node.data.address, new ArrayBuffer[Network.Node]) += node - node.data.network = this + node.data.network = Some(this) }) // Return the node object of the newly connected node for the next step. @@ -152,7 +152,7 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. case Some(list) => list.find(_.data == node) match { case None => false case Some(entry) => { - node.network = null + node.network = None // Removing a node may result in a net split, leaving us with multiple // networks. The remove function returns all resulting networks, one @@ -306,7 +306,7 @@ object Network { tileEntities. filter(_.isInstanceOf[net.Node]). map(_.asInstanceOf[net.Node]). - foreach(t => t.network.remove(t)) + foreach(t => t.network.foreach(_.remove(t))) } private def onLoad(w: World, tileEntities: Iterable[TileEntity]) = if (!w.isRemote) { @@ -321,12 +321,12 @@ object Network { getNetworkNode(world, x + side.offsetX, y + side.offsetY, z + side.offsetZ) match { case None => // Ignore. case Some(neighborNode) => - if (neighborNode != null && neighborNode.network != null) { - neighborNode.network.connect(neighborNode, node) + if (neighborNode.network.isDefined) { + neighborNode.network.foreach(_.connect(neighborNode, node)) } } } - if (node.network == null) new Network(node) + if (node.network.isEmpty) new Network(node) } }