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)

This commit is contained in:
Florian Nücke 2013-09-29 11:57:14 +02:00
parent d931b6c92f
commit abddfae2ea
28 changed files with 401 additions and 511 deletions

View File

@ -111,9 +111,4 @@ end
sendToNode = wrap(sendToNode) sendToNode = wrap(sendToNode)
sendToAll = wrap(sendToAll) sendToAll = wrap(sendToAll)
nodeName = wrap(nodeName) nodeName = wrap(nodeName)
driver = {}
function driver.componentType(id)
return nodeName(id)
end

View File

@ -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

View File

@ -2,17 +2,57 @@
The Disk API, provided by disk components. The Disk API, provided by disk components.
]] ]]
mount = function() end driver.disk = {}
umount = function() end
listdir = function(dirname) end -- We track mounted disks in this table.
remove = _G.os.remove local fstab = {}
rename = _G.os.rename
tmpname = function() end
open = function(filename, mode) end function driver.disk.mount()
read = function() end end
write = function(value) end
flush = function(file) end function driver.disk.umount()
close = function(file) end end
type = function(file) 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

View File

@ -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) sendToNode(gpu, "gpu.copy", screen, col, row, w, h, tx, ty)
end end
function driver.gpu.bind(gpuAccess, screenAccess) function driver.gpu.bind(gpuId, screenId)
local gpu = type(gpuAccess) == "function" and gpuAccess or function() return gpuAccess end
local screen = type(screenAccess) == "function" and screenAccess or function() return screenAccess end
return { return {
setResolution = function(w, h) setResolution = function(w, h)
driver.gpu.setResolution(gpu(), screen(), w, h) driver.gpu.setResolution(component.address(gpuId), component.address(screenId), w, h)
end, end,
getResolution = function() getResolution = function()
return driver.gpu.getResolution(gpu(), screen()) return driver.gpu.getResolution(component.address(gpuId), component.address(screenId))
end, end,
getResolutions = function() getResolutions = function()
return driver.gpu.getResolutions(gpu(), screen()) return driver.gpu.getResolutions(component.address(gpuId), component.address(screenId))
end, end,
set = function(col, row, value) 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, end,
fill = function(col, ro, w, h, value) 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, end,
copy = function(col, row, w, h, tx, ty) 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
} }
end end

View File

@ -235,9 +235,7 @@ end)
local function bindIfPossible() local function bindIfPossible()
if gpuId > 0 and screenId > 0 then if gpuId > 0 and screenId > 0 then
if not boundGpu then if not boundGpu then
local function gpu() return component.address(gpuId) end boundGpu = driver.gpu.bind(gpuId, screenId)
local function screen() return component.address(screenId) end
boundGpu = driver.gpu.bind(gpu, screen)
screenWidth, screenHeight = boundGpu.getResolution() screenWidth, screenHeight = boundGpu.getResolution()
event.fire("term_available") event.fire("term_available")
end end
@ -340,12 +338,12 @@ end
write = function(...) write = function(...)
local args = {...} local args = {...}
local first = true local first = true
for _, value in ipairs(args) do for i = 1, #args do
if not first then if not first then
term.write(", ") term.write(", ")
end end
first = false first = false
term.write(value, true) term.write(args[i], true)
end end
end end

View File

@ -1,20 +1,6 @@
--[[ Basic functionality, drives userland and enforces timeouts. --[[ 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
This is called as the main coroutine by the computer. If this throws, the error occurred. Shutdown / reboot are signalled via special yields. ]]
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
--[[ Set up the global environment we make available to userland programs. ]] --[[ Set up the global environment we make available to userland programs. ]]
local sandbox = { local sandbox = {
@ -39,17 +25,9 @@ local sandbox = {
tonumber = tonumber, tonumber = tonumber,
tostring = tostring, 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, getmetatable = getmetatable,
setmetatable = setmetatable, setmetatable = setmetatable,
write = function() end,
checkArg = checkArg,
component = component,
driver = driver,
bit32 = { bit32 = {
arshift = bit32.arshift, arshift = bit32.arshift,
band = bit32.band, band = bit32.band,
@ -142,114 +120,130 @@ local sandbox = {
unpack = table.unpack unpack = table.unpack
} }
} }
sandbox.checkArg = checkArg
sandbox._G = sandbox
-- Note: 'write' will be replaced by init script.
function sandbox.write(...) end
function sandbox.print(...) function sandbox.print(...)
sandbox.write(...) sandbox.write(...)
sandbox.write("\n") sandbox.write("\n")
end 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) function sandbox.load(code, source, env)
return load(code, source, "t", env or sandbox) return load(code, source, "t", env or sandbox)
end end
--[[ Install wrappers for coroutine management that reserves the first value --[[ Install wrappers for coroutine management that reserves the first value
returned by yields for internal stuff. For now this is used for driver returned by yields for internal stuff. Used for sleeping and message
calls, in which case the first function is the function performing the calls (sendToNode and its ilk) that happen synchronized (Server thread).
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.
--]] --]]
function sandbox.coroutine.yield(...) local deadline = 0
return coroutine.yield(nil, ...)
end local function checkDeadline()
function sandbox.coroutine.resume(co, ...) if os.realTime() > deadline then
local function checkDeadline() error("too long without yielding", 0)
if os.realTime() > deadline then
error("too long without yielding", 0)
end
end end
local args = {...} end
local function main(co)
local args = {}
while true do while true do
-- Don't reset counter when resuming, to avoid circumventing the timeout. deadline = os.realTime() + 3
if not debug.gethook(co) then if not debug.gethook(co) then
debug.sethook(co, checkDeadline, "", 10000) debug.sethook(co, checkDeadline, "", 10000)
end end
-- Run the coroutine.
local result = {coroutine.resume(co, table.unpack(args))} 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 if result[1] then
-- Coroutine returned normally, if we yielded it may be a system yield. args = {coroutine.yield(result[2])} -- system yielded value
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
else 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) return table.unpack(result)
end end
end 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) function sandbox.os.signal(name, timeout)
checkArg(1, name, "string", "nil") checkArg(1, name, "string", "nil")
checkArg(2, timeout, "number", "nil") checkArg(2, timeout, "number", "nil")
local deadline = os.clock() + (timeout or math.huge) local waitUntil = os.clock() + (timeout or math.huge)
while os.clock() < deadline do while os.clock() < waitUntil do
local signal = {coroutine.yield(deadline - os.clock())} local signal = {coroutine.yield(waitUntil - os.clock())}
if signal and (signal[1] == name or name == nil) then if signal and (name == signal[1] or name == nil) then
return table.unpack(signal) return table.unpack(signal)
end end
end end
end end
--[[ Shutdown the computer. ]]
function sandbox.os.shutdown() function sandbox.os.shutdown()
coroutine.yield(false) coroutine.yield(false)
end end
--[[ Reboot the computer. ]]
function sandbox.os.reboot() function sandbox.os.reboot()
coroutine.yield(true) coroutine.yield(true)
end 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 -- 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. -- traceback later. Because of that we have to do the error handling here.
return pcall(function() return pcall(main, coinit)
-- 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)

View File

@ -5,34 +5,34 @@ driver.redstone = {}
driver.redstone.sides = {"top", "bottom", "left", "right", "front", "back"} driver.redstone.sides = {"top", "bottom", "left", "right", "front", "back"}
-- Add inverse mapping and aliases. -- Add inverse mapping and aliases.
for k, v in pairs(sides) do sides[v] = k end for k, v in pairs(driver.redstone.sides) do
sides.up = sides.top driver.redstone.sides[v] = k
sides.down = sides.bottom end
driver.redstone.sides.up = driver.redstone.sides.top
driver.redstone.sides.down = driver.redstone.sides.bottom
function driver.redstone.getAnalogInput(card, side) local safeOsAddress = os.address
checkArg(1, side, "number")
sendToNode(card, os.address(), "redstone.input", side) function driver.redstone.analogInput(card, side)
sendToNode(card, safeOsAddress(), "redstone.input", side)
end end
function driver.redstone.getAnalogOutput(card, side) function driver.redstone.analogOutput(card, side, value)
checkArg(1, side, "number") if value then
sendToNode(card, os.address(), "redstone.output", side) sendToNode(card, safeOsAddress(), "redstone.output=", side, value)
else
return sendToNode(card, safeOsAddress(), "redstone.output", side)
end
end end
function driver.redstone.setAnalogOutput(card, side, value) function driver.redstone.input(card, side)
checkArg(1, side, "number") return driver.redstone.analogInput(card, side) > 0
checkArg(2, side, "number")
sendToNode(card, os.address(), "redstone.output=", side, value)
end end
function getInput(side) function output(card, side, value)
return getAnalogInput(side) > 0 if value then
end driver.redstone.analogOutput(side, value and 15 or 0)
else
function getOutput(side) return driver.redstone.analogOutput(card, side) > 0
return getAnalogOutput(side) > 0 end
end
function setOutput(side, value)
rs.setAnalogOutput(side, value and 15 or 0)
end end

View File

@ -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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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).
* <p/>
* This can be `None` to do nothing. Otherwise this is expected to be valid
* Lua code (it is loaded via <code>load()</code> and then run).
* <p/>
* 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.
* <p/>
* 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))
}

View File

@ -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. * 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 * 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 * 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 * 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`.
* <p/> * <p/>
* Note that for network nodes implemented in <tt>TileEntities</tt> adding and * Note that for network nodes implemented in <tt>TileEntities</tt> adding and
* removal is automatically provided on chunk load and unload. When a block is * 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 * placed or broken you will have to implement this logic yourself (i.e. call
* <tt>NetworkAPI.joinOrCreateNetwork</tt> in <tt>onBlockAdded</tt> and * <tt>Network.joinOrCreateNetwork</tt> in <tt>onBlockAdded</tt> and
* <tt>getNetwork.remove</tt> in <tt>breakBlock</tt>. * <tt>Network.remove</tt> in <tt>breakBlock</tt>.
* <p/> * <p/>
* All other kinds of nodes have to be managed manually. See `INetworkNode`. * All other kinds of nodes have to be managed manually. See `Node`.
* <p/> * <p/>
* There are a couple of system messages to be aware of. These are all sent by * There are a couple of system messages to be aware of. These are all sent by
* the network manager itself: * the network manager itself:
@ -38,7 +41,7 @@ package li.cil.oc.api.network
* instances of your own network implementation; this will lead to * instances of your own network implementation; this will lead to
* incompatibilities with the built-in network implementation (which can only * incompatibilities with the built-in network implementation (which can only
* merge with other networks of its own type). Always use the methods provided * merge with other networks of its own type). Always use the methods provided
* in <tt>NetworkAPI</tt> to create and join networks. * in <tt>Network</tt> to create and join networks.
*/ */
trait Network { trait Network {
/** /**
@ -59,10 +62,6 @@ trait Network {
* @return true if a new connection between the two nodes was added; false if * @return true if a new connection between the two nodes was added; false if
* the connection already existed. * the connection already existed.
* @throws IllegalArgumentException if neither node is in this network. * @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 def connect(nodeA: Node, nodeB: Node): Boolean
@ -215,4 +214,20 @@ trait Network {
* @param data the message to send. * @param data the message to send.
*/ */
def sendToAll(source: Node, name: String, data: Any*) 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))
} }

View File

@ -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)
}
}

View File

@ -2,13 +2,14 @@ package li.cil.oc.api.driver
import li.cil.oc.api.network.Node import li.cil.oc.api.network.Node
import net.minecraft.world.World import net.minecraft.world.World
import li.cil.oc.api.Driver
/** /**
* Interface for block component drivers. * Interface for block component drivers.
* <p/> * <p/>
* This driver type is used for components that are blocks, i.e. that can be * 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 * placed in the world, but cannot be modified to or don't want to have their
* `TileEntities` implement `INetworkNode`. * `TileEntities` implement `network.Node`.
* <p/> * <p/>
* A block driver is used by proxy blocks to check its neighbors and whether * A block driver is used by proxy blocks to check its neighbors and whether
* those neighbors should be treated as components or not. * those neighbors should be treated as components or not.

View File

@ -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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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`).
* <p/>
* 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.
* <p/>
* This can be `None` to do nothing. Otherwise this is expected to be valid
* Lua code (it is simply loaded via <code>load()</code> and then executed).
* <p/>
* 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.
* <p/>
* 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
}

View File

@ -2,6 +2,7 @@ package li.cil.oc.api.driver
import li.cil.oc.api.network.Node import li.cil.oc.api.network.Node
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import li.cil.oc.api.Driver
/** /**
* Interface for item component drivers. * 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 * 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 * queried using the drivers' `worksWith` functions. The first driver that
* replies positively and whose check against the slot type is successful, i.e. * 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 * for which the `slot` matches the slot it should be inserted into, will be
* component's driver and the component will be added. If no driver is found * used as the component's driver and the component will be added. If no driver
* the item will be rejected and cannot be installed. * is found the item will be rejected and cannot be installed.
* <p/> * <p/>
* Note that it is possible to write one driver that supports as many different * 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 * items as you wish. I'd recommend writing one per device (type), though, to

View File

@ -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)
}

View File

@ -1,6 +1,7 @@
package li.cil.oc.api.network package li.cil.oc.api.network
import net.minecraft.nbt.NBTTagCompound import net.minecraft.nbt.NBTTagCompound
import li.cil.oc.api.Network
/** /**
* A single node in a `INetwork`. * A single node in a `INetwork`.
@ -83,7 +84,7 @@ trait Node {
* *
* @return the network the node is in. * @return the network the node is in.
*/ */
var network: Network = null var network: Option[Network] = None
/** /**
* Makes the node handle a message. * Makes the node handle a message.

View File

@ -4,10 +4,12 @@ import cpw.mods.fml.common.event._
import cpw.mods.fml.common.network.NetworkRegistry import cpw.mods.fml.common.network.NetworkRegistry
import cpw.mods.fml.common.registry.LanguageRegistry import cpw.mods.fml.common.registry.LanguageRegistry
import li.cil.oc._ import li.cil.oc._
import li.cil.oc.api.driver.API import li.cil.oc.api.Driver
import li.cil.oc.server.computer.{Computer, Drivers} import li.cil.oc.api.Network
import li.cil.oc.server.computer.Computer
import li.cil.oc.server.driver 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 import net.minecraftforge.common.MinecraftForge
class Proxy { class Proxy {
@ -16,6 +18,9 @@ class Proxy {
LanguageRegistry.instance.loadLocalization( LanguageRegistry.instance.loadLocalization(
"/assets/opencomputers/lang/en_US.lang", "en_US", false) "/assets/opencomputers/lang/en_US.lang", "en_US", false)
Driver.registry = Some(driver.Registry)
Network.network = Some(network.Network)
} }
def init(e: FMLInitializationEvent): Unit = { def init(e: FMLInitializationEvent): Unit = {
@ -24,17 +29,17 @@ class Proxy {
NetworkRegistry.instance.registerGuiHandler(OpenComputers, GuiHandler) NetworkRegistry.instance.registerGuiHandler(OpenComputers, GuiHandler)
API.addDriver(driver.GraphicsCard) Driver.add(driver.GraphicsCard)
API.addDriver(driver.Keyboard) Driver.add(driver.Keyboard)
MinecraftForge.EVENT_BUS.register(Computer) MinecraftForge.EVENT_BUS.register(Computer)
MinecraftForge.EVENT_BUS.register(Network) MinecraftForge.EVENT_BUS.register(network.Network)
} }
def postInit(e: FMLPostInitializationEvent): Unit = { def postInit(e: FMLPostInitializationEvent): Unit = {
// Lock the driver registry to avoid drivers being added after computers // 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 // 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. // over the course of a game, since that could lead to weird effects.
Drivers.locked = true Registry.locked = true
} }
} }

View File

@ -3,7 +3,8 @@ package li.cil.oc.common.block
import cpw.mods.fml.common.registry.GameRegistry import cpw.mods.fml.common.registry.GameRegistry
import li.cil.oc.Config import li.cil.oc.Config
import li.cil.oc.CreativeTab 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 li.cil.oc.common.tileentity.TileEntityRotatable
import net.minecraft.block.Block import net.minecraft.block.Block
import net.minecraft.block.material.Material 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 None => // Invalid but avoid match error.
case Some(subBlock) => { case Some(subBlock) => {
world.getBlockTileEntity(x, y, z) match { 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) 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 None => // Invalid but avoid match error.
case Some(subBlock) => { case Some(subBlock) => {
world.getBlockTileEntity(x, y, z) match { 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) subBlock.onBlockAdded(world, x, y, z)
} }

View File

@ -52,9 +52,8 @@ trait ScreenEnvironment extends Node {
} }
def onScreenResolutionChange(w: Int, h: Int) = if (network != null) { def onScreenResolutionChange(w: Int, h: Int) =
network.sendToAll(this, "computer.signal", "screen_resized", w, h) network.foreach(_.sendToAll(this, "computer.signal", "screen_resized", w, h))
}
def onScreenSet(col: Int, row: Int, s: String) {} def onScreenSet(col: Int, row: Int, s: String) {}

View File

@ -1,7 +1,6 @@
package li.cil.oc.common.tileentity package li.cil.oc.common.tileentity
import li.cil.oc.api.network.Node import li.cil.oc.api.network.Node
import li.cil.oc.server.computer.Drivers
import net.minecraft.inventory.IInventory import net.minecraft.inventory.IInventory
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.nbt.NBTTagCompound import net.minecraft.nbt.NBTTagCompound
@ -9,6 +8,7 @@ import net.minecraft.nbt.NBTTagList
import net.minecraft.world.World import net.minecraft.world.World
import li.cil.oc.api.driver.Slot import li.cil.oc.api.driver.Slot
import li.cil.oc.common.component.Computer import li.cil.oc.common.component.Computer
import li.cil.oc.server.driver.Registry
trait ItemComponentProxy extends IInventory with Node { trait ItemComponentProxy extends IInventory with Node {
protected val inventory = new Array[ItemStack](8) protected val inventory = new Array[ItemStack](8)
@ -52,29 +52,29 @@ trait ItemComponentProxy extends IInventory with Node {
// NetworkNode // NetworkNode
// ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- //
override protected def onConnect() = { override protected def onConnect() {
super.onConnect() super.onConnect()
for (slot <- 0 until inventory.length) { for (slot <- 0 until inventory.length) {
itemNode(slot) match { itemNode(slot) match {
case None => // Ignore. case None => // Ignore.
case Some(node) => case Some(node) =>
network.connect(this, node) network.foreach(_.connect(this, node))
} }
} }
} }
override protected def onDisconnect() = { override protected def onDisconnect() {
super.onDisconnect() super.onDisconnect()
for (slot <- 0 until inventory.length) { for (slot <- 0 until inventory.length) {
itemNode(slot) match { itemNode(slot) match {
case None => // Ignore. case None => // Ignore.
case Some(node) => 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 None => None
case Some(driver) => driver.node(inventory(slot)) case Some(driver) => driver.node(inventory(slot))
} }
@ -115,7 +115,7 @@ trait ItemComponentProxy extends IInventory with Node {
if (!world.isRemote) itemNode(slot) match { if (!world.isRemote) itemNode(slot) match {
case None => // Nothing to do. case None => // Nothing to do.
case Some(node) => case Some(node) =>
node.network.remove(node) node.network.foreach(_.remove(node))
} }
inventory(slot) = item inventory(slot) = item
@ -125,11 +125,11 @@ trait ItemComponentProxy extends IInventory with Node {
if (!world.isRemote) itemNode(slot) match { if (!world.isRemote) itemNode(slot) match {
case None => // Nothing to do. case None => // Nothing to do.
case Some(node) => 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 (_, None) => false // Invalid item.
case (0, Some(driver)) => driver.slot(item) == Slot.PSU case (0, Some(driver)) => driver.slot(item) == Slot.PSU
case (1 | 2 | 3, Some(driver)) => driver.slot(item) == Slot.PCI case (1 | 2 | 3, Some(driver)) => driver.slot(item) == Slot.PCI

View File

@ -96,11 +96,10 @@ class TileEntityComputer(isClient: Boolean) extends TileEntityRotatable with com
} }
if (isRunning != computer.isRunning) { if (isRunning != computer.isRunning) {
isRunning = computer.isRunning isRunning = computer.isRunning
if (network != null) if (isRunning)
if (isRunning) network.foreach(_.sendToAll(this, "computer.started"))
network.sendToAll(this, "computer.started") else
else network.foreach(_.sendToAll(this, "computer.stopped"))
network.sendToAll(this, "computer.stopped")
ServerPacketSender.sendComputerState(this, isRunning) ServerPacketSender.sendComputerState(this, isRunning)
} }
} }

View File

@ -15,13 +15,13 @@ class TileEntityKeyboard extends TileEntityRotatable with Node {
message.data match { message.data match {
case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyDown" => case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyDown" =>
if (isUseableByPlayer(p)) 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" => case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyUp" =>
if (isUseableByPlayer(p)) 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" => case Array(p: Player, value: String) if message.name == "keyboard.clipboard" =>
if (isUseableByPlayer(p)) if (isUseableByPlayer(p))
network.sendToAll(this, "computer.signal", "clipboard", value) network.foreach(_.sendToAll(this, "computer.signal", "clipboard", value))
case _ => // Ignore. case _ => // Ignore.
} }
None None

View File

@ -68,18 +68,18 @@ class PacketHandler extends CommonPacketHandler {
def onKeyDown(p: PacketParser) = def onKeyDown(p: PacketParser) =
p.readTileEntity[Node]() match { p.readTileEntity[Node]() match {
case None => // Invalid packet. 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) = def onKeyUp(p: PacketParser) =
p.readTileEntity[Node]() match { p.readTileEntity[Node]() match {
case None => // Invalid packet. 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) = def onClipboard(p: PacketParser) =
p.readTileEntity[Node]() match { p.readTileEntity[Node]() match {
case None => // Invalid packet. 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()))
} }
} }

View File

@ -1,5 +1,6 @@
package li.cil.oc.server.component package li.cil.oc.server.component
import li.cil.oc.api.Network
import li.cil.oc.api.network.Message import li.cil.oc.api.network.Message
import net.minecraft.nbt.NBTTagCompound import net.minecraft.nbt.NBTTagCompound
@ -10,27 +11,28 @@ class GraphicsCard(nbt: NBTTagCompound) extends ItemComponent(nbt) {
override def receive(message: Message) = { override def receive(message: Message) = {
super.receive(message) super.receive(message)
def trySend(f: Network => Option[Array[Any]]) = network.fold(None: Option[Array[Any]])(f)
message.data match { message.data match {
case Array(screen: Double, w: Double, h: Double) if message.name == "gpu.resolution=" => case Array(screen: Double, w: Double, h: Double) if message.name == "gpu.resolution=" =>
if (supportedResolutions.contains((w.toInt, h.toInt))) 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")) else Some(Array(None, "unsupported resolution"))
case Array(screen: Double) if message.name == "gpu.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" => 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@_*)) => case Some(Array(resolutions@_*)) =>
Some(Array(supportedResolutions.intersect(resolutions): _*)) Some(Array(supportedResolutions.intersect(resolutions): _*))
case _ => None case _ => None
} }
case Array(screen: Double, x: Double, y: Double, value: String) if message.name == "gpu.set" => 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" => 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) 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")) 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" => 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 case _ => None
} }
} }

View File

@ -1,6 +1,7 @@
package li.cil.oc.server.component package li.cil.oc.server.component
import li.cil.oc.api.network.Message import li.cil.oc.api.network.Message
import li.cil.oc.api.network.Node
import net.minecraft.nbt.NBTTagCompound import net.minecraft.nbt.NBTTagCompound
import net.minecraft.tileentity.TileEntity import net.minecraft.tileentity.TileEntity
import net.minecraftforge.common.ForgeDirection 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(r: RedstoneEnabled) => Some(Array(r.input(ForgeDirection.getOrientation(side)).asInstanceOf[Any]))
case Some(t: TileEntity) => case Some(t: TileEntity) =>
val face = ForgeDirection.getOrientation(side.toInt) 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. case _ => None // Can't work with this node.
} else None } 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(r: RedstoneEnabled) => Some(Array(r.output(ForgeDirection.getOrientation(side)).asInstanceOf[Any]))
case Some(t: TileEntity) => case Some(t: TileEntity) =>
val power = t.worldObj.isBlockProvidingPowerTo(t.xCoord, t.yCoord, t.zCoord, side.toInt) 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. case _ => None // Can't work with this node.
} else None } 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 Some(r: RedstoneEnabled) => r.output(ForgeDirection.getOrientation(side)) = value
case _ => // Can't work with this node. case _ => // Can't work with this node.
} }

View File

@ -5,8 +5,10 @@ import java.lang.Thread.UncaughtExceptionHandler
import java.util.concurrent._ import java.util.concurrent._
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level import java.util.logging.Level
import li.cil.oc.api.network.Node
import li.cil.oc.common.component import li.cil.oc.common.component
import li.cil.oc.common.tileentity.TileEntityComputer import li.cil.oc.common.tileentity.TileEntityComputer
import li.cil.oc.server.driver
import li.cil.oc.{OpenComputers, Config} import li.cil.oc.{OpenComputers, Config}
import net.minecraft.nbt._ import net.minecraft.nbt._
import net.minecraft.tileentity.TileEntity 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 private var state = Computer.State.Stopped
/** The internal Lua state. Only set while the computer is running. */ /** 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 * 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("") signal("")
// Inject component added signals for all nodes in the network. // 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. // All green, computer started successfully.
true true
@ -220,27 +222,27 @@ class Computer(val owner: Environment) extends component.Computer with Runnable
// to the coroutine.yield() that triggered the call. // to the coroutine.yield() that triggered the call.
lua.call(0, 1) lua.call(0, 1)
lua.checkType(2, LuaType.TABLE) 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 { } catch {
case _: LuaMemoryAllocationException => case _: LuaMemoryAllocationException =>
// This can happen if we run out of memory while converting a Java // 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 // exception to a string (which we have to do to avoid keeping
// userdata on the stack, which cannot be persisted). // 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 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() close()
case e: Throwable => { case e: Throwable => {
OpenComputers.log.log(Level.WARNING, "Faulty Lua implementation for synchronized calls.", e) OpenComputers.log.log(Level.WARNING, "Faulty Lua implementation for synchronized calls.", e)
close() 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. 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()) { if (state != Computer.State.Stopped && init()) {
// Unlimit memory use while unpersisting. // Unlimit memory use while unpersisting.
val memory = lua.getTotalMemory
lua.setTotalMemory(Integer.MAX_VALUE) lua.setTotalMemory(Integer.MAX_VALUE)
try { try {
// Try unpersisting Lua, because that's what all of the rest depends // 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) }.toArray)
}).asJava) }).asJava)
kernelMemory = nbt.getInteger("kernelMemory")
timeStarted = nbt.getLong("timeStarted") timeStarted = nbt.getLong("timeStarted")
// Clean up some after we're done and limit memory again. // Clean up some after we're done and limit memory again.
lua.gc(LuaState.GcAction.COLLECT, 0) 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 // 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 // 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.setTag("signals", list)
nbt.setInteger("kernelMemory", kernelMemory)
nbt.setLong("timeStarted", timeStarted) nbt.setLong("timeStarted", timeStarted)
} }
catch { catch {
@ -523,7 +526,8 @@ class Computer(val owner: Environment) extends component.Computer with Runnable
} }
lua.pushJavaFunction(ScalaFunction(lua => 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@_*)) => case Some(Array(results@_*)) =>
results.foreach(pushResult(lua, _)) results.foreach(pushResult(lua, _))
results.length results.length
@ -532,19 +536,44 @@ class Computer(val owner: Environment) extends component.Computer with Runnable
lua.setGlobal("sendToNode") lua.setGlobal("sendToNode")
lua.pushJavaFunction(ScalaFunction(lua => { 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 0
})) }))
lua.setGlobal("sendToAll") lua.setGlobal("sendToAll")
lua.pushJavaFunction(ScalaFunction(lua => { 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 None => 0
case Some(node) => lua.pushString(node.name); 1 case Some(node) => lua.pushString(node.name); 1
} }
})) }))
lua.setGlobal("nodeName") 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 // Run the boot script. This sets up the permanent value tables as
// well as making the functions used for persisting/unpersisting // well as making the functions used for persisting/unpersisting
// available as globals. It also wraps the message sending functions // 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") "/assets/opencomputers/lua/boot.lua"), "=boot", "t")
lua.call(0, 0) lua.call(0, 0)
// Install all driver callbacks into the state. This is done once in // Load the basic kernel which sets up the sandbox, loads the init script
// the beginning so that we can take the memory the callbacks use into // and then runs it in a coroutine with a debug hook checking for
// account when computing the kernel's memory use. // timeouts.
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.
lua.load(classOf[Computer].getResourceAsStream( lua.load(classOf[Computer].getResourceAsStream(
"/assets/opencomputers/lua/kernel.lua"), "=kernel", "t") "/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 // 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 // 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). // underlying system (which may change across releases).
lua.gc(LuaState.GcAction.COLLECT, 0) lua.gc(LuaState.GcAction.COLLECT, 0)
kernelMemory = lua.getTotalMemory - lua.getFreeMemory kernelMemory = lua.getTotalMemory - lua.getFreeMemory
lua.setTotalMemory(kernelMemory + 64 * 1024) lua.setTotalMemory(kernelMemory + 16 * 1024)
// Clear any left-over signals from a previous run. // Clear any left-over signals from a previous run.
signals.clear() signals.clear()
@ -731,6 +745,8 @@ class Computer(val owner: Environment) extends component.Computer with Runnable
OpenComputers.log.warning("Kernel stopped unexpectedly.") OpenComputers.log.warning("Kernel stopped unexpectedly.")
} }
else { 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 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. // TODO get this to the world as a computer.crashed message. problem: synchronizing it.
//owner.network.sendToAll(owner, "computer.crashed", lua.toString(3)) //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 => case e: LuaRuntimeException =>
OpenComputers.log.warning("Kernel crashed. This is a bug!\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) OpenComputers.log.warning("Kernel crashed. This is a bug!\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat "))
close() 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 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. // TODO get this to the world as a computer.crashed message. problem: synchronizing it.
//owner.network.sendToAll(owner, "computer.crashed", "not enough memory") //owner.network.sendToAll(owner, "computer.crashed", "not enough memory")
close() close()
}
} }
// State has inevitably changed, mark as changed to save again. // State has inevitably changed, mark as changed to save again.

View File

@ -5,7 +5,7 @@ import com.naef.jnlua.{JavaFunction, LuaState, NativeSupport}
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.nio.channels.Channels import java.nio.channels.Channels
import java.util.{Locale, Calendar} import java.util.{Formatter, Locale, Calendar}
import scala.util.Random import scala.util.Random
case class ScalaFunction(f: (LuaState) => Int) extends JavaFunction { case class ScalaFunction(f: (LuaState) => Int) extends JavaFunction {
@ -217,13 +217,13 @@ private[computer] object LuaStateFactory {
})) }))
state.setField(-2, "char") 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 => { state.pushJavaFunction(ScalaFunction(lua => {
lua.pushInteger(lua.checkString(1).length) lua.pushInteger(lua.checkString(1).length)
@ -237,7 +237,7 @@ private[computer] object LuaStateFactory {
})) }))
state.setField(-2, "lower") state.setField(-2, "lower")
// TODO match // TODO match (probably not necessary?)
state.pushJavaFunction(ScalaFunction(lua => { state.pushJavaFunction(ScalaFunction(lua => {
lua.pushString(lua.checkString(1).reverse) lua.pushString(lua.checkString(1).reverse)

View File

@ -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 li.cil.oc.api.driver.{Block, Item}
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.world.World import net.minecraft.world.World
import scala.Some import scala.Some
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
import scala.compat.Platform._
/** /**
* This class keeps track of registered drivers and provides installation logic * 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 * usually require a component of the type the driver wraps to be installed in
* the computer, but may also provide context-free functions. * the computer, but may also provide context-free functions.
*/ */
private[oc] object Drivers { private[oc] object Registry {
/** The list of registered block drivers. */ /** The list of registered block drivers. */
private val blocks = ArrayBuffer.empty[Block] private val blocks = ArrayBuffer.empty[Block]
@ -90,30 +87,11 @@ private[oc] object Drivers {
else None else None
/** /**
* Used by the computer to initialize its Lua state, injecting the APIs of * Gets a list of all driver APIs.
* all known drivers. *
* @return the apis of all known drivers.
*/ */
private[computer] def installOn(computer: Computer) = def apis = (blocks ++ items) map (driver => (driver.getClass.getSimpleName, driver.api)) collect {
(blocks ++ items).foreach(driver => { case (name, Some(code)) => (name, code)
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))
}
}
})
} }

View File

@ -7,7 +7,7 @@ import _root_.net.minecraftforge.common.ForgeDirection
import _root_.net.minecraftforge.event.ForgeSubscribe import _root_.net.minecraftforge.event.ForgeSubscribe
import _root_.net.minecraftforge.event.world.ChunkEvent import _root_.net.minecraftforge.event.world.ChunkEvent
import java.util.logging.Level 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.Visibility
import li.cil.oc.api.{network => net} import li.cil.oc.api.{network => net}
import scala.beans.BeanProperty 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. * 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. * 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) = { def this(node: net.Node) = {
this(mutable.Map({ this(mutable.Map({
if (node.address < 1) 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)) send(new Network.ConnectMessage(node), List(node))
} }
nodes.foreach(_.network = this) nodes.foreach(_.network = Some(this))
def connect(nodeA: net.Node, nodeB: net.Node) = { def connect(nodeA: net.Node, nodeB: net.Node) = {
val containsA = nodeMap.get(nodeA.address).exists(_.exists(_.data == nodeA)) 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) = { private def add(oldNode: Network.Node, addedNode: net.Node) = {
// Check if the other node is new or if we have to merge networks. // 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) val newNode = new Network.Node(addedNode)
if (nodeMap.contains(addedNode.address) || addedNode.address < 1) if (nodeMap.contains(addedNode.address) || addedNode.address < 1)
addedNode.address = findId() 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)))) nodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), List(addedNode))))
} }
nodeMap.getOrElseUpdate(address, new ArrayBuffer[Network.Node]) += newNode nodeMap.getOrElseUpdate(address, new ArrayBuffer[Network.Node]) += newNode
addedNode.network = this addedNode.network = Some(this)
(newNode, sendQueue) (newNode, sendQueue)
} }
else { else {
// Queue any messages to avoid side effects from receivers. // Queue any messages to avoid side effects from receivers.
val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[net.Node])] val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[net.Node])]
val thisNodes = nodes.toBuffer val thisNodes = nodes.toBuffer
val otherNetwork = addedNode.network.asInstanceOf[Network] val otherNetwork = addedNode.network.get.asInstanceOf[Network]
val otherNodes = otherNetwork.nodes.toBuffer val otherNodes = otherNetwork.nodes.toBuffer
otherNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), thisNodes))) otherNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), thisNodes)))
thisNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), otherNodes))) 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. // Add nodes from other network into this network, including invalid nodes.
otherNetwork.nodeMap.values.flatten.foreach(node => { otherNetwork.nodeMap.values.flatten.foreach(node => {
nodeMap.getOrElseUpdate(node.data.address, new ArrayBuffer[Network.Node]) += 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. // 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 Some(list) => list.find(_.data == node) match {
case None => false case None => false
case Some(entry) => { case Some(entry) => {
node.network = null node.network = None
// Removing a node may result in a net split, leaving us with multiple // Removing a node may result in a net split, leaving us with multiple
// networks. The remove function returns all resulting networks, one // networks. The remove function returns all resulting networks, one
@ -306,7 +306,7 @@ object Network {
tileEntities. tileEntities.
filter(_.isInstanceOf[net.Node]). filter(_.isInstanceOf[net.Node]).
map(_.asInstanceOf[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) { 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 { getNetworkNode(world, x + side.offsetX, y + side.offsetY, z + side.offsetZ) match {
case None => // Ignore. case None => // Ignore.
case Some(neighborNode) => case Some(neighborNode) =>
if (neighborNode != null && neighborNode.network != null) { if (neighborNode.network.isDefined) {
neighborNode.network.connect(neighborNode, node) neighborNode.network.foreach(_.connect(neighborNode, node))
} }
} }
} }
if (node.network == null) new Network(node) if (node.network.isEmpty) new Network(node)
} }
} }