diff --git a/assets/opencomputers/lang/en_US.lang b/assets/opencomputers/lang/en_US.lang index 21e4c3d8f..d507bc19c 100644 --- a/assets/opencomputers/lang/en_US.lang +++ b/assets/opencomputers/lang/en_US.lang @@ -1,3 +1,4 @@ oc.block.Computer.name=Computer oc.block.Screen.name=Screen -oc.container.computer=Computer \ No newline at end of file +oc.container.computer=Computer +oc.item.GraphicsCard.name=Graphics Card \ No newline at end of file diff --git a/assets/opencomputers/lua/init.lua b/assets/opencomputers/lua/init.lua index 9678b94ac..94ec2c5f9 100644 --- a/assets/opencomputers/lua/init.lua +++ b/assets/opencomputers/lua/init.lua @@ -54,7 +54,10 @@ end --[[ Dispatch an event with the specified parameter. ]] function event.fire(name, ...) + -- We may have no arguments at all if the call is just used to drive the + -- timer check (for example if we had no signal in coroutine.sleep()). if name then + checkArg(1, name, "string") for callback, _ in pairs(listenersFor(name, false)) do local result, message = xpcall(callback, event.error, name, ...) if not result and message then @@ -68,15 +71,18 @@ function event.fire(name, ...) end end end + -- Collect elapsed callbacks first, since calling them may in turn lead to + -- new timers being registered, which would add entries to the table we're + -- iterating, which is not supported. local elapsed = {} for id, info in pairs(timers) do if info.after < os.clock() then - table.insert(elapsed, info) + table.insert(elapsed, info.callback) timers[id] = nil end end - for _, info in ipairs(elapsed) do - local result, message = xpcall(info.callback, event.error) + for _, callback in ipairs(elapsed) do + local result, message = xpcall(callback, event.error) if not result and message then error(message, 0) end @@ -160,34 +166,46 @@ end) event.listen("component_removed", function(_, address) local id = component.id(address) - components[id] = nil - event.fire("component_uninstalled", id) + if id then + components[id] = nil + event.fire("component_uninstalled", id) + end end) event.listen("component_changed", function(_, newAddress, oldAddress) local id = component.id(oldAddress) - components[id].address = newAddress + if oldAddress > 0 and not id then return end + if oldAddress > 0 and newAddress == 0 then -- ~0 -> 0 + components[id] = nil + event.fire("component_uninstalled", id) + elseif oldAddress == 0 and newAddress > 0 then -- 0 -> ~0 + id = #components + 1 + components[id] = {address = newAddress, name = driver.componentType(newAddress)} + event.fire("component_installed", id) + elseif oldAddress > 0 and newAddress > 0 then -- ~0 -> ~0 + components[id].address = newAddress + end end) ------------------------------------------------------------------------------- --[[ Setup terminal API. ]] -local idGpu, idScreen = 0, 0 +local gpuId, screenId = 0, 0 local screenWidth, screenHeight = 0, 0 local boundGpu = nil local cursorX, cursorY = 1, 1 event.listen("component_installed", function(_, id) local type = component.type(id) - if type == "gpu" and idGpu < 1 then + if type == "gpu" and gpuId < 1 then term.gpuId(id) - elseif type == "screen" and idScreen < 1 then + elseif type == "screen" and screenId < 1 then term.screenId(id) end end) event.listen("component_uninstalled", function(_, id) - if idGpu == id then + if gpuId == id then term.gpuId(0) for id in component.ids() do if component.type(id) == "gpu" then @@ -195,7 +213,7 @@ event.listen("component_uninstalled", function(_, id) return end end - elseif idScreen == id then + elseif screenId == id then term.screenId(0) for id in component.ids() do if component.type(id) == "screen" then @@ -208,17 +226,17 @@ end) event.listen("screen_resized", function(_, address, w, h) local id = component.id(address) - if id == idScreen then + if id == screenId then screenWidth = w screenHeight = h end end) local function bindIfPossible() - if idGpu > 0 and idScreen > 0 then + if gpuId > 0 and screenId > 0 then if not boundGpu then - local function gpu() return component.address(idGpu) end - local function screen() return component.address(idScreen) end + local function gpu() return component.address(gpuId) end + local function screen() return component.address(screenId) end boundGpu = driver.gpu.bind(gpu, screen) screenWidth, screenHeight = boundGpu.getResolution() event.fire("term_available") @@ -243,19 +261,19 @@ end function term.gpuId(id) if id then checkArg(1, id, "number") - idGpu = id + gpuId = id bindIfPossible() end - return idGpu + return gpuId end function term.screenId(id) if id then checkArg(1, id, "number") - idScreen = id + screenId = id bindIfPossible() end - return idScreen + return screenId end function term.getCursor() @@ -312,10 +330,21 @@ function term.clear() cursorX, cursorY = 1, 1 end +function term.clearLine() + if not boundGpu then return end + boundGpu.fill(1, cursorY, screenWidth, 1, " ") + cursorX = 1 +end + -- Set custom write function to replace the dummy. write = function(...) local args = {...} + local first = true for _, value in ipairs(args) do + if not first then + term.write(", ") + end + first = false term.write(value, true) end end @@ -323,18 +352,49 @@ end ------------------------------------------------------------------------------- --[[ Primitive command line. ]] -local command = "" +local keyboardId = 0 +local lastCommand, command = "", "" local isRunning = false -local function commandLineKey(_, char, code) + +event.listen("component_installed", function(_, id) + local type = component.type(id) + if type == "keyboard" and keyboardId < 1 then + keyboardId = id + end +end) + +event.listen("component_uninstalled", function(_, id) + if keyboardId == id then + keyboardId = 0 + for id in component.ids() do + if component.type(id) == "keyboard" then + keyboardId = id + return + end + end + end +end) + +-- Put this into the term table since other programs may want to use it, too. +function term.keyboardId(id) + if id then + checkArg(1, id, "number") + keyboardId = id + end + return keyboardId +end + +local function onKeyDown(_, address, char, code) if isRunning then return end -- ignore events while running a command - local keys = driver.keyboard.keys - local gpu = term.gpu() + if component.id(address) ~= keyboardId then return end + if not boundGpu then return end local x, y = term.getCursor() + local keys = driver.keyboard.keys if code == keys.back then if command:len() == 0 then return end command = command:sub(1, -2) term.setCursor(command:len() + 3, y) -- from leading "> " - gpu.set(x - 1, y, " ") -- overwrite cursor blink + boundGpu.set(x - 1, y, " ") -- overwrite cursor blink elseif code == keys.enter then if command:len() == 0 then return end print(" ") -- overwrite cursor blink @@ -347,14 +407,19 @@ local function commandLineKey(_, char, code) local result = {pcall(code)} isRunning = false if not result[1] or result[2] ~= nil then - -- TODO handle multiple results? - print(result[2]) + print(table.unpack(result, 2)) end else print(result) end + lastCommand = command command = "" write("> ") + elseif code == keys.up then + command = lastCommand + term.clearLine() + term.write("> " .. command) + term.setCursor(command:len() + 3, y) elseif not keys.isControl(char) then -- Non-control character, add to command. char = string.char(char) @@ -362,8 +427,10 @@ local function commandLineKey(_, char, code) term.write(char) end end -local function commandLineClipboard(_, value) + +local function onClipboard(_, address, value) if isRunning then return end -- ignore events while running a command + if component.id(address) ~= keyboardId then return end value = value:match("([^\r\n]+)") if value and value:len() > 0 then command = command .. value @@ -376,12 +443,12 @@ event.listen("term_available", function() term.clear() command = "" write("> ") - event.listen("key_down", commandLineKey) - event.listen("clipboard", commandLineClipboard) + event.listen("key_down", onKeyDown) + event.listen("clipboard", onClipboard) end) event.listen("term_unavailable", function() - event.ignore("key_down", commandLineKey) - event.ignore("clipboard", commandLineClipboard) + event.ignore("key_down", onKeyDown) + event.ignore("clipboard", onClipboard) end) -- Serves as main event loop while keeping the cursor blinking. As soon as diff --git a/assets/opencomputers/lua/kernel.lua b/assets/opencomputers/lua/kernel.lua index 8d5f9c78c..6e72fe7d7 100644 --- a/assets/opencomputers/lua/kernel.lua +++ b/assets/opencomputers/lua/kernel.lua @@ -112,7 +112,8 @@ local sandbox = { difftime = os.difftime, time = os.time, freeMemory = os.freeMemory, - totalMemory = function() return os.totalMemory() - os.romSize() end + totalMemory = function() return os.totalMemory() - os.romSize() end, + address = os.address }, string = { @@ -216,6 +217,16 @@ function sandbox.os.signal(name, timeout) end end +--[[ Shutdown the computer. ]] +function sandbox.os.shutdown() + coroutine.yield(false) +end + +--[[ Reboot the computer. ]] +function sandbox.os.reboot() + coroutine.yield(true) +end + -- 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() diff --git a/li/cil/oc/api/INetwork.scala b/li/cil/oc/api/INetwork.scala index fb8d8d067..29e739b1d 100644 --- a/li/cil/oc/api/INetwork.scala +++ b/li/cil/oc/api/INetwork.scala @@ -96,7 +96,10 @@ trait INetwork { def remove(node: INetworkNode): Boolean /** - * Get the network node with the specified address. + * Get the valid network node with the specified address. + *

+ * This does not include nodes with an address less or equal to zero or with + * a visibility of `Visibility.None`. *

* If there are multiple nodes with the same address this will return the * node that most recently joined the network. @@ -107,18 +110,35 @@ trait INetwork { def node(address: Int): Option[INetworkNode] /** - * The list of nodes in this network. + * The list of all valid nodes in this network. *

- * This can be used to perform a delayed initialization of a node. For - * example, computers will use this when starting up to generate component - * added events for all nodes. + * This does not include nodes with an address less or equal to zero or with + * a visibility of `Visibility.None`. * * @return the list of nodes in this network. */ def nodes: Iterable[INetworkNode] /** - * The list of nodes the specified node is directly connected to. + * The list of nodes in the network visible to the specified node. + *

+ * The same base filters as for `nodes` apply, with additional visibility + * checks applied, based on the specified node as a point of reference. + *

+ * This can be used to perform a delayed initialization of a node. For + * example, computers will use this when starting up to generate component + * added events for all nodes. + * + * @param reference the node to get the visible other nodes for. + * @return the nodes visible to the specified node. + */ + def nodes(reference: INetworkNode): Iterable[INetworkNode] + + /** + * The list of valid nodes the specified node is directly connected to. + *

+ * This does not include nodes with an address less or equal to zero or with + * a visibility of `Visibility.None`. *

* This can be used to verify arguments for components that should only work * for other components that are directly connected to them, for example. @@ -130,7 +150,11 @@ trait INetwork { def neighbors(node: INetworkNode): Iterable[INetworkNode] /** - * Sends a message to a specific node. + * Sends a message to a specific address, which may mean multiple nodes. + *

+ * If the target is less or equal to zero no message is sent. If a node with + * that address has a visibility of `Visibility.None` the message will not be + * delivered to that node. *

* Messages should have a unique name to allow differentiating them when * handling them in a network node. For example, computers will try to parse @@ -140,8 +164,9 @@ trait INetwork { *

* Note that message handlers may also return results. In this case that * result will be returned from this function. In the case that there are - * more than one target node (shared addresses) the last result that was not - * `None` will be returned, or `None` if all were. + * more than one target node (shared addresses, should not happen, but may if + * a node implementation decides to ignore this rule) the last result that + * was not `None` will be returned, or `None` if all results were `None`. * * @param source the node that sends the message. * @param target the id of the node to send the message to. @@ -149,10 +174,36 @@ trait INetwork { * @param data the message to send. * @return the result of the message being handled, if any. */ - def sendToNode(source: INetworkNode, target: Int, name: String, data: Any*): Option[Array[Any]] + def sendToAddress(source: INetworkNode, target: Int, name: String, data: Any*): Option[Array[Any]] /** - * Sends a message to all nodes in the network. + * Sends a message to all direct valid neighbors of the source node. + *

+ * This does not include nodes with an address less or equal to zero or with + * a visibility of `Visibility.None`. + *

+ * Messages should have a unique name to allow differentiating them when + * handling them in a network node. For example, computers will try to parse + * messages named "computer.signal" by converting the message data to a + * signal and inject that signal into the Lua VM, so no message not used for + * this purpose should be named "computer.signal". + * + * @param source the node that sends the message. + * @param name the name of the message. + * @param data the message to send. + * @see neighbors + */ + def sendToNeighbors(source: INetworkNode, name: String, data: Any*) + + /** + * Sends a message to all valid nodes in the network. + *

+ * This does not include nodes with an address less or equal to zero or with + * a visibility of `Visibility.None`. + *

+ * This ignores any further visibility checks, i.e. even if a node is not + * visible to the source node it will still receive the message, as long as + * it is a valid node. *

* Messages should have a unique name to allow differentiating them when * handling them in a network node. For example, computers will try to parse diff --git a/li/cil/oc/api/INetworkNode.scala b/li/cil/oc/api/INetworkNode.scala index b67e807cb..993dcc181 100644 --- a/li/cil/oc/api/INetworkNode.scala +++ b/li/cil/oc/api/INetworkNode.scala @@ -35,6 +35,21 @@ trait INetworkNode { */ def name: String + /** + * The visibility of this node. + *

+ * This is used by the network to control which system messages to deliver to + * which nodes. This value should not change over the lifetime of a node. + * Note that this has no effect on the real reachability of a node; it is + * only used to filter to which nodes to send connect, disconnect and + * reconnect messages. If addressed directly or when a broadcast is sent, the + * node will still receive that message. Therefore nodes should still verify + * themselves that they want to accept a message from the message's source. + * + * @return visibility of the node. + */ + def visibility = Visibility.None + /** * The address of the node, so that it can be found in the network. *

diff --git a/li/cil/oc/api/Visibility.scala b/li/cil/oc/api/Visibility.scala new file mode 100644 index 000000000..b8e7e73ee --- /dev/null +++ b/li/cil/oc/api/Visibility.scala @@ -0,0 +1,19 @@ +package li.cil.oc.api + +/** + * Possible reachability values foe nodes. + *

+ * Since all components that are connected are packed into the same network, + * we want some way of controlling what's accessible from where on a low + * level (to avoid unnecessary messages and unauthorized access). + */ +object Visibility extends Enumeration { + /** The node neither receives nor sends messages. */ + val None = Value("None") + + /** The node only handles messages from its immediate neighbors. */ + val Neighbors = Value("Neighbors") + + /** The node can interact with the complete network. */ + val Network = Value("Network") +} diff --git a/li/cil/oc/client/gui/GuiScreen.scala b/li/cil/oc/client/gui/GuiScreen.scala index fcda6180a..4fd2923c5 100644 --- a/li/cil/oc/client/gui/GuiScreen.scala +++ b/li/cil/oc/client/gui/GuiScreen.scala @@ -29,16 +29,16 @@ class GuiScreen(val tileEntity: TileEntityScreen) extends MCGuiScreen { /** Must be called when the size of the underlying screen changes */ def setSize(w: Double, h: Double) = { // Re-compute sizes and positions. - val totalMargin = (GuiScreen.margin + GuiScreen.innerMargin) * 2 + val totalMargin = GuiScreen.margin + GuiScreen.innerMargin val bufferWidth = w * MonospaceFontRenderer.fontWidth val bufferHeight = h * MonospaceFontRenderer.fontHeight - val bufferScaleX = ((width - totalMargin) / bufferWidth) min 1 - val bufferScaleY = ((height - totalMargin) / bufferHeight) min 1 + val bufferScaleX = (width / (bufferWidth + totalMargin * 2.0)) min 1 + val bufferScaleY = (height / (bufferHeight + totalMargin * 2.0)) min 1 scale = bufferScaleX min bufferScaleY - innerWidth = (bufferWidth * scale + 1).ceil.toInt - innerHeight = (bufferHeight * scale + 1).ceil.toInt - x = (width - (innerWidth + totalMargin)) / 2 - y = (height - (innerHeight + totalMargin)) / 2 + innerWidth = (bufferWidth * scale).toInt + innerHeight = (bufferHeight * scale).toInt + x = (width - (innerWidth + totalMargin * 2)) / 2 + y = (height - (innerHeight + totalMargin * 2)) / 2 // Re-build display lists. GuiScreen.compileBackground(innerWidth, innerHeight) @@ -114,43 +114,46 @@ object GuiScreen { GL11.glCallLists(buffer.get) } - private[gui] def compileBackground(innerWidth: Int, innerHeight: Int) = + private[gui] def compileBackground(bufferWidth: Int, bufferHeight: Int) = if (textureManager.isDefined) { + val innerWidth = innerMargin * 2 + bufferWidth + val innerHeight = innerMargin * 2 + bufferHeight + GL11.glNewList(displayLists.get, GL11.GL_COMPILE) setTexture(borders) // Top border (left corner, middle bar, right corner). drawBorder( - 0, 0, 7, 7, + 0, 0, margin, margin, 0, 0, 7, 7) drawBorder( - margin, 0, innerWidth, 7, + margin, 0, innerWidth, margin, 7, 0, 8, 7) drawBorder( - margin + innerWidth, 0, 7, 7, + margin + innerWidth, 0, margin, margin, 8, 0, 15, 7) // Middle area (left bar, screen background, right bar). drawBorder( - 0, margin, 7, innerHeight, + 0, margin, margin, innerHeight, 0, 7, 7, 8) drawBorder( margin, margin, innerWidth, innerHeight, 7, 7, 8, 8) drawBorder( - margin + innerWidth, margin, 7, innerHeight, + margin + innerWidth, margin, margin, innerHeight, 8, 7, 15, 8) // Bottom border (left corner, middle bar, right corner). drawBorder( - 0, margin + innerHeight, 7, 7, + 0, margin + innerHeight, margin, margin, 0, 8, 7, 15) drawBorder( - margin, margin + innerHeight, innerWidth, 7, + margin, margin + innerHeight, innerWidth, margin, 7, 8, 8, 15) drawBorder( - margin + innerWidth, margin + innerHeight, 7, 7, + margin + innerWidth, margin + innerHeight, margin, margin, 8, 8, 15, 15) GL11.glEndList() diff --git a/li/cil/oc/common/block/BlockMulti.scala b/li/cil/oc/common/block/BlockMulti.scala index edd3c85c6..819dccb4b 100644 --- a/li/cil/oc/common/block/BlockMulti.scala +++ b/li/cil/oc/common/block/BlockMulti.scala @@ -55,10 +55,7 @@ class BlockMulti(id: Int) extends Block(id, Material.iron) { } def subBlock(world: IBlockAccess, x: Int, y: Int, z: Int): Option[SubBlock] = - subBlock(world.getBlockMetadata(x, y, z)) match { - case Some(subBlock) if world.getBlockId(x, y, z) == this.blockID => Some(subBlock) - case _ => None - } + subBlock(world.getBlockMetadata(x, y, z)) def subBlock(metadata: Int) = metadata match { diff --git a/li/cil/oc/common/components/IScreenEnvironment.scala b/li/cil/oc/common/components/IScreenEnvironment.scala index 2e7e040a3..5acb96166 100644 --- a/li/cil/oc/common/components/IScreenEnvironment.scala +++ b/li/cil/oc/common/components/IScreenEnvironment.scala @@ -1,6 +1,6 @@ package li.cil.oc.common.components -import li.cil.oc.api.{INetworkMessage, INetworkNode} +import li.cil.oc.api.{Visibility, INetworkMessage, INetworkNode} import net.minecraft.nbt.NBTTagCompound /** @@ -15,6 +15,8 @@ trait IScreenEnvironment extends INetworkNode { override def name = "screen" + override def visibility = Visibility.Neighbors + override def receive(message: INetworkMessage): Option[Array[Any]] = { super.receive(message) message.data match { @@ -51,7 +53,7 @@ trait IScreenEnvironment extends INetworkNode { } def onScreenResolutionChange(w: Int, h: Int) = if (network != null) { - network.sendToAll(this, "computer.signal", "screen_resized", this.address, w, h) + network.sendToAll(this, "computer.signal", "screen_resized", w, h) } def onScreenSet(col: Int, row: Int, s: String) {} diff --git a/li/cil/oc/common/items/ItemMulti.scala b/li/cil/oc/common/items/ItemMulti.scala index ed67d518f..6d38218f2 100644 --- a/li/cil/oc/common/items/ItemMulti.scala +++ b/li/cil/oc/common/items/ItemMulti.scala @@ -59,7 +59,7 @@ class ItemMulti(id: Int) extends Item(id) { override def getUnlocalizedName(item: ItemStack): String = subItem(item) match { case None => getUnlocalizedName - case Some(subItem) => subItem.unlocalizedName + case Some(subItem) => "oc.item." + subItem.unlocalizedName } override def getUnlocalizedName: String = "oc.item" diff --git a/li/cil/oc/common/tileentity/TileEntityComputer.scala b/li/cil/oc/common/tileentity/TileEntityComputer.scala index cad3569d3..fe64c9b2c 100644 --- a/li/cil/oc/common/tileentity/TileEntityComputer.scala +++ b/li/cil/oc/common/tileentity/TileEntityComputer.scala @@ -30,16 +30,22 @@ class TileEntityComputer(isClient: Boolean) extends TileEntityRotatable with ICo override def receive(message: INetworkMessage) = { super.receive(message) message.data match { - // The isRunning check is here to avoid network.connect messages being sent - // while loading a chunk (thus leading to "false" component_added signals). - case Array() if message.name == "network.connect" && isRunning => + // The isRunning check is here to avoid component_* signals being + // generated while loading a chunk. + case Array() if message.name == "network.connect" && message.source.address != 0 && isRunning => computer.signal("component_added", message.source.address); None - case Array() if message.name == "network.disconnect" && isRunning => + case Array() if message.name == "network.disconnect" && message.source.address != 0 && isRunning => computer.signal("component_removed", message.source.address); None case Array(oldAddress: Integer) if message.name == "network.reconnect" && isRunning => computer.signal("component_changed", message.source.address, oldAddress); None case Array(name: String, args@_*) if message.name == "computer.signal" => - computer.signal(name, args: _*); None + computer.signal(name, List(message.source.address) ++ args: _*); None + case Array() if message.name == "computer.start" => + Some(Array(turnOn().asInstanceOf[Any])) + case Array() if message.name == "computer.stop" => + Some(Array(turnOff().asInstanceOf[Any])) + case Array() if message.name == "computer.running" => + Some(Array(isOn.asInstanceOf[Any])) case _ => None } } @@ -92,9 +98,9 @@ class TileEntityComputer(isClient: Boolean) extends TileEntityRotatable with ICo isRunning = computer.isRunning if (network != null) if (isRunning) - network.sendToAll(this, "computer.start") + network.sendToAll(this, "computer.started") else - network.sendToAll(this, "computer.stop") + network.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 df8589246..b2144afcb 100644 --- a/li/cil/oc/common/tileentity/TileEntityKeyboard.scala +++ b/li/cil/oc/common/tileentity/TileEntityKeyboard.scala @@ -1,28 +1,27 @@ package li.cil.oc.common.tileentity import cpw.mods.fml.common.network.Player -import li.cil.oc.api.{INetworkNode, INetworkMessage} +import li.cil.oc.api.{Visibility, INetworkNode, INetworkMessage} import net.minecraft.entity.player.EntityPlayer import net.minecraft.nbt.NBTTagCompound class TileEntityKeyboard extends TileEntityRotatable with INetworkNode { override def name = "keyboard" + override def visibility = Visibility.Network + override def receive(message: INetworkMessage) = { super.receive(message) 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) - message.cancel() // One keyboard is enough. - } - 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) - message.cancel() // One keyboard is enough. - } - case Array(p: Player, value: String) if message.name == "keyboard.clipboard" => if (isUseableByPlayer(p)) { - network.sendToAll(this, "computer.signal", "clipboard", value) - message.cancel() - } + 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) + 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) + case Array(p: Player, value: String) if message.name == "keyboard.clipboard" => + if (isUseableByPlayer(p)) + network.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 572fae1ed..2569eb74d 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[INetworkNode]() match { case None => // Invalid packet. - case Some(n) => n.network.sendToAll(n, "keyboard.keyDown", p.player, p.readChar(), p.readInt()) + case Some(n) => n.network.sendToNeighbors(n, "keyboard.keyDown", p.player, p.readChar(), p.readInt()) } def onKeyUp(p: PacketParser) = p.readTileEntity[INetworkNode]() match { case None => // Invalid packet. - case Some(n) => n.network.sendToAll(n, "keyboard.keyUp", p.player, p.readChar(), p.readInt()) + case Some(n) => n.network.sendToNeighbors(n, "keyboard.keyUp", p.player, p.readChar(), p.readInt()) } def onClipboard(p: PacketParser) = - p.readTileEntity[INetworkNode]() match { - case None => // Invalid packet. - case Some(n) => n.network.sendToAll(n, "keyboard.clipboard", p.player, p.readUTF()) - } + p.readTileEntity[INetworkNode]() match { + case None => // Invalid packet. + case Some(n) => n.network.sendToNeighbors(n, "keyboard.clipboard", p.player, p.readUTF()) + } } \ No newline at end of file diff --git a/li/cil/oc/server/components/GraphicsCard.scala b/li/cil/oc/server/components/GraphicsCard.scala index 2927ed824..8704df8d4 100644 --- a/li/cil/oc/server/components/GraphicsCard.scala +++ b/li/cil/oc/server/components/GraphicsCard.scala @@ -1,6 +1,6 @@ package li.cil.oc.server.components -import li.cil.oc.api.{INetworkNode, INetworkMessage} +import li.cil.oc.api.INetworkMessage import net.minecraft.nbt.NBTTagCompound class GraphicsCard(nbt: NBTTagCompound) extends ItemComponent(nbt) { @@ -13,24 +13,24 @@ class GraphicsCard(nbt: NBTTagCompound) extends ItemComponent(nbt) { message.data match { case Array(screen: Double, w: Double, h: Double) if message.name == "gpu.resolution=" => if (supportedResolutions.contains((w.toInt, h.toInt))) - network.sendToNode(message.source, screen.toInt, "screen.resolution=", w.toInt, h.toInt) + network.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.sendToNode(message.source, screen.toInt, "screen.resolution") + network.sendToAddress(this, screen.toInt, "screen.resolution") case Array(screen: Double) if message.name == "gpu.resolutions" => - network.sendToNode(this, screen.toInt, "screen.resolutions") match { + network.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.sendToNode(this, screen.toInt, "screen.set", x.toInt - 1, y.toInt - 1, value) + network.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.sendToNode(this, screen.toInt, "screen.fill", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, value.charAt(0)) + network.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.sendToNode(this, screen.toInt, "screen.copy", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, tx.toInt, ty.toInt) + network.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/components/ItemComponent.scala b/li/cil/oc/server/components/ItemComponent.scala index d35153a64..e76169211 100644 --- a/li/cil/oc/server/components/ItemComponent.scala +++ b/li/cil/oc/server/components/ItemComponent.scala @@ -1,11 +1,13 @@ package li.cil.oc.server.components -import li.cil.oc.api.INetworkNode +import li.cil.oc.api.{Visibility, INetworkNode} import net.minecraft.nbt.NBTTagCompound abstract class ItemComponent(val nbt: NBTTagCompound) extends INetworkNode { address = nbt.getInteger("address") + override def visibility = Visibility.Neighbors + override def address_=(value: Int) = { super.address_=(value) nbt.setInteger("address", address) diff --git a/li/cil/oc/server/computer/Computer.scala b/li/cil/oc/server/computer/Computer.scala index 7a6fce53e..df64c3604 100644 --- a/li/cil/oc/server/computer/Computer.scala +++ b/li/cil/oc/server/computer/Computer.scala @@ -1,6 +1,7 @@ package li.cil.oc.server.computer import com.naef.jnlua._ +import java.lang.Thread.UncaughtExceptionHandler import java.util.concurrent._ import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Level @@ -61,8 +62,11 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable * the Java side and processed one by one in the Lua VM. They are the only * means to communicate actively with the computer (passively only message * handlers can interact with the computer by returning some result). + *

+ * The queue is intentionally pretty big, because we have to enqueue one + * signal for for each component in the network when the computer starts up. */ - private val signals = new LinkedBlockingQueue[Computer.Signal](100) + private val signals = new LinkedBlockingQueue[Computer.Signal](256) // ----------------------------------------------------------------------- // @@ -71,7 +75,7 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable * custom implementation of os.clock(), which returns the amount of the time * the computer has been running. */ - private var timeStarted = 0.0 + private var timeStarted = 0L /** * The last time (system time) the update function was called by the server @@ -95,18 +99,22 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable */ private var future: Option[Future[_]] = None + /** + * Timestamp until which to sleep, i.e. when we hit this time we will create + * a future to run the computer. Until then we have nothing to do. + */ + private var sleepUntil = Long.MaxValue + /** This is used to synchronize access to the state field. */ private val stateMonitor = new Object() - /** This is used to synchronize while saving, so we don't stop while we do. */ - private val saveMonitor = new Object() - // ----------------------------------------------------------------------- // // IComputerContext // ----------------------------------------------------------------------- // - override def signal(name: String, args: Any*) = { - def values = args.map { + override def signal(name: String, args: Any*) = stateMonitor.synchronized(state match { + case Computer.State.Stopped | Computer.State.Stopping => false + case _ => signals.offer(new Computer.Signal(name, args.map { case null | Unit => Unit case arg: Boolean => arg case arg: Byte => arg.toDouble @@ -118,263 +126,253 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable case arg: Double => arg case arg: String => arg case _ => throw new IllegalArgumentException() - }.toArray - stateMonitor.synchronized(state match { - // We don't push new signals when stopped or shutting down. - case Computer.State.Stopped | Computer.State.Stopping => false - // Currently sleeping. Cancel that and start immediately. - case Computer.State.Sleeping => - val v = values // Map first, may error. - future.get.cancel(true) - state = Computer.State.Suspended - signals.offer(new Computer.Signal(name, v)) - future = Some(Computer.Executor.pool.submit(this)) - true - // Basically running, but had nothing to do so we stopped. Resume. - case Computer.State.Suspended if !future.isDefined => - signals.offer(new Computer.Signal(name, values)) - future = Some(Computer.Executor.pool.submit(this)) - true - // Running or in synchronized call, just push the signal. - case _ => - signals.offer(new Computer.Signal(name, values)) - true - }) - } + }.toArray)) + }) // ----------------------------------------------------------------------- // // IComputer // ----------------------------------------------------------------------- // override def start() = stateMonitor.synchronized( - state == Computer.State.Stopped && init() && { + (state == Computer.State.Stopped) && init() && { + // Initial state. Will be switched to State.Yielded in the next update() + // due to the signals queue not being empty ( state = Computer.State.Suspended + // Remember when we started, for os.clock(). + timeStarted = owner.world.getWorldInfo.getWorldTotalTime + // Mark state change in owner, to send it to clients. owner.markAsChanged() // Inject a dummy signal so that real ones don't get swallowed. This way // we can just ignore the parameters the first time the kernel is run // and all actual signals will be read using coroutine.yield(). - // IMPORTANT: This will also create our worker thread for the first run. - signal("dummy") + signal("") // Inject component added signals for all nodes in the network. - owner.network.nodes.foreach(node => signal("component_added", node.address)) + owner.network.nodes(owner).foreach(node => signal("component_added", node.address)) + + // All green, computer started successfully. true }) - override def stop() = saveMonitor.synchronized(stateMonitor.synchronized { - if (state != Computer.State.Stopped) { - if (state != Computer.State.Running) { - // If the computer is not currently running we can simply close it, - // and cancel any pending future - which may already be running and - // waiting for the stateMonitor, so we do a hard abort. - future.foreach(_.cancel(true)) - close() - } - else { - // Otherwise we enter an intermediate state to ensure the executor - // truly stopped, before switching back to stopped to allow starting - // the computer again. The executor will check for this state and - // call close. - state = Computer.State.Stopping - } + override def stop() = stateMonitor.synchronized(state match { + case Computer.State.Stopped => false // Nothing to do. + case _ if future.isEmpty => close(); true // Not executing, kill it. + case _ => + // If the computer is currently executing something we enter an + // intermediate state to ensure the executor or synchronized call truly + // stopped, before switching back to stopped to allow starting the + // computer again. The executor and synchronized call will check for + // this state and call close(), thus switching the state to stopped. + state = Computer.State.Stopping true - } - else false }) - override def isRunning = stateMonitor.synchronized(state != Computer.State.Stopped) + override def isRunning = state != Computer.State.Stopped override def update() { - stateMonitor.synchronized(state match { - case Computer.State.Stopped | Computer.State.Stopping => return - case Computer.State.SynchronizedCall => { - assert(lua.getTop == 2) - assert(lua.isThread(1)) - assert(lua.isFunction(2)) - try { - lua.call(0, 1) - lua.checkType(2, LuaType.TABLE) - state = Computer.State.SynchronizedReturn - assert(!future.isDefined) - future = Some(Computer.Executor.pool.submit(this)) - } catch { - // This can happen if we run out of memory while converting a Java exception to a string. - case _: LuaMemoryAllocationException => - // TODO error message somewhere ingame - close() - // This should not happen. - case _: Throwable => { - OpenComputers.log.warning("Faulty Lua implementation for synchronized calls.") - close() - } - } - } - case Computer.State.Paused => { - state = Computer.State.Suspended - assert(!future.isDefined) - future = Some(Computer.Executor.pool.submit(this)) - } - case Computer.State.SynchronizedReturnPaused => { - state = Computer.State.SynchronizedReturn - assert(!future.isDefined) - future = Some(Computer.Executor.pool.submit(this)) - } - case _ => /* Nothing special to do. */ - }) + // Update last time run to let our executor thread know it doesn't have to + // pause. + lastUpdate = System.currentTimeMillis // Update world time for computer threads. worldTime = owner.world.getWorldInfo.getWorldTotalTime - // Remember when we started the computer for os.clock(). We do this in the - // update because only then can we be sure the world is available. - if (timeStarted == 0) - timeStarted = worldTime - - // Update last time run to let our executor thread know it doesn't have to - // pause. - lastUpdate = System.currentTimeMillis + // Check if we should switch states. + stateMonitor.synchronized(state match { + // Resume from pauses based on signal underflow. + case Computer.State.Suspended if signals.nonEmpty => { + assert(future.isEmpty) + execute(Computer.State.Yielded) + } + case Computer.State.Sleeping if lastUpdate >= sleepUntil || signals.nonEmpty => { + assert(future.isEmpty) + execute(Computer.State.Yielded) + } + // Resume in case we paused because the game was paused. + case Computer.State.Paused => { + assert(future.isEmpty) + execute(Computer.State.Yielded) + } + case Computer.State.SynchronizedReturnPaused => { + assert(future.isEmpty) + execute(Computer.State.SynchronizedReturn) + } + // Perform a synchronized call (message sending). + case Computer.State.SynchronizedCall => { + assert(future.isEmpty) + // These three asserts are all guaranteed by run(). + assert(lua.getTop == 2) + assert(lua.isThread(1)) + assert(lua.isFunction(2)) + // We switch into running state, since we'll behave as though the call + // were performed from our executor thread. + state = Computer.State.Running + try { + // Synchronized call protocol requires the called function to return + // a table, which holds the results of the call, to be passed back + // to the coroutine.yield() that triggered the call. + lua.call(0, 1) + lua.checkType(2, LuaType.TABLE) + } 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") + 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. + }) } // ----------------------------------------------------------------------- // - override def load(nbt: NBTTagCompound): Unit = - saveMonitor.synchronized(this.synchronized { - // Clear out what we currently have, if anything. - stateMonitor.synchronized { - assert(state != Computer.State.Running) // Lock on 'this' should guarantee this. - stop() - } + override def load(nbt: NBTTagCompound) { + state = nbt.getInteger("state") match { + case id if id >= 0 && id < Computer.State.maxId => Computer.State(id) + case _ => Computer.State.Stopped + } - state = Computer.State(nbt.getInteger("state")) - - 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 - // on. First, clear the stack, meaning the current kernel. - lua.setTop(0) - - if (!unpersist(nbt.getByteArray("kernel")) || !lua.isThread(1)) { - // This shouldn't really happen, but there's a chance it does if - // the save was corrupt (maybe someone modified the Lua files). - throw new IllegalStateException("Could not restore kernel.") - } - if (state == Computer.State.SynchronizedCall || state == Computer.State.SynchronizedReturn) { - if (!unpersist(nbt.getByteArray("stack")) || - (state == Computer.State.SynchronizedCall && !lua.isFunction(2)) || - (state == Computer.State.SynchronizedReturn && !lua.isTable(2))) { - // Same as with the above, should not really happen normally, but - // could for the same reasons. - throw new IllegalStateException("Could not restore stack.") - } - assert(lua.getTop == 2) - } - - assert(signals.size() == 0) - val signalsTag = nbt.getTagList("signals") - signals.addAll((0 until signalsTag.tagCount()). - map(signalsTag.tagAt(_).asInstanceOf[NBTTagCompound]). - map(signal => { - val argsTag = signal.getCompoundTag("args") - val argsLength = argsTag.getInteger("length") - new Computer.Signal(signal.getString("name"), - (0 until argsLength).map("arg" + _).map(argsTag.getTag).map { - case tag: NBTTagByte if tag.data == -1 => Unit - case tag: NBTTagByte => tag.data == 1 - case tag: NBTTagDouble => tag.data - case tag: NBTTagString => tag.data - }.toArray) - }).asJava) - - timeStarted = nbt.getDouble("timeStarted") - - // Clean up some after we're done and limit memory again. - lua.gc(LuaState.GcAction.COLLECT, 0) - lua.setTotalMemory(memory) - - // Start running our worker thread. - assert(!future.isDefined) - state match { - case Computer.State.Suspended | Computer.State.Sleeping | Computer.State.SynchronizedReturn => - future = Some(Computer.Executor.pool.submit(this)) - case _ => // Wasn't running before. - } - - } catch { - case t: Throwable => { - OpenComputers.log.log(Level.WARNING, "Could not restore computer.", t) - close() - } - } - } - else { - close() - } - }) - - override def save(nbt: NBTTagCompound): Unit = - saveMonitor.synchronized(this.synchronized { - stateMonitor.synchronized { - assert(state != Computer.State.Running) // Lock on 'this' should guarantee this. - assert(state != Computer.State.Stopping) // Only set while executor is running. - } - - nbt.setInteger("state", state.id) - if (state == Computer.State.Stopped) { - return - } - - // Unlimit memory while persisting. + if (state != Computer.State.Stopped && init()) { + // Unlimit memory use while unpersisting. val memory = lua.getTotalMemory lua.setTotalMemory(Integer.MAX_VALUE) try { - // Try persisting Lua, because that's what all of the rest depends on. - // While in a driver call we have one object on the global stack: either - // the function to call the driver with, or the result of the call. + // Try unpersisting Lua, because that's what all of the rest depends + // on. First, clear the stack, meaning the current kernel. + lua.setTop(0) + + if (!nbt.hasKey("kernel") || !unpersist(nbt.getByteArray("kernel")) || !lua.isThread(1)) { + // This shouldn't really happen, but there's a chance it does if + // the save was corrupt (maybe someone modified the Lua files). + throw new IllegalStateException("Invalid kernel.") + } if (state == Computer.State.SynchronizedCall || state == Computer.State.SynchronizedReturn) { - assert(if (state == Computer.State.SynchronizedCall) lua.isFunction(2) else lua.isTable(2)) - nbt.setByteArray("stack", persist(2)) - } - // Save the kernel state (which is always at stack index one). - assert(lua.isThread(1)) - nbt.setByteArray("kernel", persist(1)) - - val list = new NBTTagList - for (s <- signals.iterator) { - val signal = new NBTTagCompound - signal.setString("name", s.name) - val args = new NBTTagCompound - args.setInteger("length", s.args.length) - s.args.zipWithIndex.foreach { - case (Unit, i) => args.setByte("arg" + i, -1) - case (arg: Boolean, i) => args.setByte("arg" + i, if (arg) 1 else 0) - case (arg: Double, i) => args.setDouble("arg" + i, arg) - case (arg: String, i) => args.setString("arg" + i, arg) + if (!nbt.hasKey("stack") || !unpersist(nbt.getByteArray("stack")) || + (state == Computer.State.SynchronizedCall && !lua.isFunction(2)) || + (state == Computer.State.SynchronizedReturn && !lua.isTable(2))) { + // Same as with the above, should not really happen normally, but + // could for the same reasons. + throw new IllegalStateException("Invalid stack.") } - signal.setCompoundTag("args", args) - list.appendTag(signal) } - nbt.setTag("signals", list) - nbt.setDouble("timeStarted", timeStarted) - } - catch { - case t: Throwable => { - t.printStackTrace() - nbt.setInteger("state", Computer.State.Stopped.id) - } - } - finally { + assert(signals.size == 0) + val signalsNbt = nbt.getTagList("signals") + signals.addAll((0 until signalsNbt.tagCount()). + map(signalsNbt.tagAt(_).asInstanceOf[NBTTagCompound]). + map(signalNbt => { + val argsNbt = signalNbt.getCompoundTag("args") + val argsLength = argsNbt.getInteger("length") + new Computer.Signal(signalNbt.getString("name"), + (0 until argsLength).map("arg" + _).map(argsNbt.getTag).map { + case tag: NBTTagByte if tag.data == -1 => Unit + case tag: NBTTagByte => tag.data == 1 + case tag: NBTTagDouble => tag.data + case tag: NBTTagString => tag.data + case _ => throw new IllegalStateException("Invalid signal.") + }.toArray) + }).asJava) + + timeStarted = nbt.getLong("timeStarted") + // Clean up some after we're done and limit memory again. lua.gc(LuaState.GcAction.COLLECT, 0) lua.setTotalMemory(memory) + + // 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 + // slight delay, to allow the world to settle. + assert(future.isEmpty) + state match { + case Computer.State.Yielded | Computer.State.SynchronizedReturn => + future = Some(Computer.Executor.pool.schedule(this, 500, TimeUnit.MILLISECONDS)) + case Computer.State.Sleeping => sleepUntil = Long.MinValue + case _ => // Will be started by update() if necessary. + } + } catch { + case e: IllegalStateException => { + OpenComputers.log.log(Level.WARNING, "Could not restore computer.", e) + close() + } } - }) + } + // Init failed, or we were already stopped. + else state = Computer.State.Stopped + } + + override def save(nbt: NBTTagCompound): Unit = this.synchronized { + assert(state != Computer.State.Running) // Lock on 'this' should guarantee this. + assert(state != Computer.State.Stopping) // Only set while executor is running. + + nbt.setInteger("state", state.id) + if (state == Computer.State.Stopped) { + return + } + + // Unlimit memory while persisting. + val memory = lua.getTotalMemory + lua.setTotalMemory(Integer.MAX_VALUE) + try { + // Try persisting Lua, because that's what all of the rest depends on. + // While in a driver call we have one object on the global stack: either + // the function to call the driver with, or the result of the call. + if (state == Computer.State.SynchronizedCall || state == Computer.State.SynchronizedReturn) { + assert(if (state == Computer.State.SynchronizedCall) lua.isFunction(2) else lua.isTable(2)) + nbt.setByteArray("stack", persist(2)) + } + // Save the kernel state (which is always at stack index one). + assert(lua.isThread(1)) + nbt.setByteArray("kernel", persist(1)) + + val list = new NBTTagList + for (s <- signals.iterator) { + val signal = new NBTTagCompound + signal.setString("name", s.name) + val args = new NBTTagCompound + args.setInteger("length", s.args.length) + s.args.zipWithIndex.foreach { + case (Unit, i) => args.setByte("arg" + i, -1) + case (arg: Boolean, i) => args.setByte("arg" + i, if (arg) 1 else 0) + case (arg: Double, i) => args.setDouble("arg" + i, arg) + case (arg: String, i) => args.setString("arg" + i, arg) + } + signal.setCompoundTag("args", args) + list.appendTag(signal) + } + nbt.setTag("signals", list) + + nbt.setLong("timeStarted", timeStarted) + } + catch { + case e: Throwable => { + e.printStackTrace() + nbt.setInteger("state", Computer.State.Stopped.id) + } + } + finally { + // Clean up some after we're done and limit memory again. + lua.gc(LuaState.GcAction.COLLECT, 0) + lua.setTotalMemory(memory) + } + } private def persist(index: Int): Array[Byte] = { lua.getGlobal("persist") // ... obj persist? @@ -404,8 +402,6 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable false } - // ----------------------------------------------------------------------- // - // Internals // ----------------------------------------------------------------------- // private def init(): Boolean = { @@ -523,11 +519,11 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable } lua.pushJavaFunction(ScalaFunction(lua => - owner.network.sendToNode(owner, lua.checkInteger(1), lua.checkString(2), parseArguments(lua, 3): _*) match { + owner.network.sendToAddress(owner, lua.checkInteger(1), lua.checkString(2), parseArguments(lua, 3): _*) match { case Some(Array(results@_*)) => results.foreach(pushResult(lua, _)) results.length - case None => 0 + case _ => 0 })) lua.setGlobal("sendToNode") @@ -612,61 +608,59 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable signals.clear() timeStarted = 0 future = None + sleepUntil = Long.MaxValue // Mark state change in owner, to send it to clients. owner.markAsChanged() }) + private def execute(value: Computer.State.Value) { + assert(future.isEmpty) + sleepUntil = Long.MaxValue + state = value + future = Some(Computer.Executor.pool.submit(this)) + } + // This is a really high level lock that we only use for saving and loading. override def run(): Unit = this.synchronized { - // See if the game appears to be paused, in which case we also pause. - if (System.currentTimeMillis - lastUpdate > 200) - stateMonitor.synchronized { - state = - if (state == Computer.State.SynchronizedReturn) Computer.State.SynchronizedReturnPaused - else Computer.State.Paused + val callReturn = stateMonitor.synchronized { + val oldState = state + state = Computer.State.Running + + // See if the game appears to be paused, in which case we also pause. + if (System.currentTimeMillis - lastUpdate > 200) { + state = state match { + case Computer.State.SynchronizedReturn => Computer.State.SynchronizedReturnPaused + case _ => Computer.State.Paused + } future = None return } - val callReturn = stateMonitor.synchronized { - if (state == Computer.State.Stopped) return - val oldState = state - state = Computer.State.Running - future = None oldState } match { - case Computer.State.SynchronizedReturn | Computer.State.SynchronizedReturnPaused => true - case Computer.State.Stopped | Computer.State.Paused | Computer.State.Suspended | Computer.State.Sleeping => false + case Computer.State.SynchronizedReturn => true + case Computer.State.Yielded | Computer.State.Sleeping => false case s => - OpenComputers.log.warning("Running computer from invalid state " + s.toString + "!") - stateMonitor.synchronized { - state = s - future = None - } + OpenComputers.log.warning("Running computer from invalid state " + s.toString + ". This is a bug!") + close() return } + // The kernel thread will always be at stack index one. + assert(lua.isThread(1)) + try { - // This is synchronized so that we don't run a Lua state while saving or - // loading the computer to or from an NBTTagCompound or other stuff - // corrupting our Lua state. - - // The kernel thread will always be at stack index one. - assert(lua.isThread(1)) - // Resume the Lua state and remember the number of results we get. val results = if (callReturn) { - // If we were doing a driver call, continue where we left off. + // If we were doing a synchronized call, continue where we left off. assert(lua.getTop == 2) + assert(lua.isTable(2)) lua.resume(1, 1) } - else signals.poll() match { - // No signal, just run any non-sleeping processes. - case null => lua.resume(1, 0) - - // Got a signal, inject it and call any handlers (if any). - case signal => { + else Option(signals.poll()) match { + case None => lua.resume(1, 0) + case Some(signal) => { lua.pushString(signal.name) signal.args.foreach { case Unit => lua.pushNil() @@ -678,70 +672,82 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable } } - // State has inevitably changed, mark as changed to save again. - owner.markAsChanged() - - // Only queue for next execution step if the kernel is still alive. - if (lua.status(1) == LuaState.YIELD) { - // Lua state yielded normally, see what we have. - stateMonitor.synchronized { - if (state == Computer.State.Stopping) { - // Someone called stop() in the meantime. - close() - } - else if (results == 1 && lua.isNumber(2)) { - // We got a number. This tells us how long we should wait before - // resuming the state again. - val sleep = (lua.toNumber(2) * 1000).toLong - lua.pop(results) - if (signals.isEmpty) { - state = Computer.State.Sleeping - assert(!future.isDefined) - future = Some(Computer.Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS)) - } - else { - state = Computer.State.Suspended - assert(!future.isDefined) - future = Some(Computer.Executor.pool.submit(this)) - } - } - else if (results == 1 && lua.isFunction(2)) { - // If we get one function it's a wrapper for a synchronized call. - state = Computer.State.SynchronizedCall - assert(!future.isDefined) - } - else { - // Something else, just pop the results and try again. - lua.pop(results) - state = Computer.State.Suspended - assert(!future.isDefined) - if (!signals.isEmpty) future = Some(Computer.Executor.pool.submit(this)) + // Check if the kernel is still alive. + stateMonitor.synchronized(if (lua.status(1) == LuaState.YIELD) { + // Intermediate state in some cases. Satisfies the assert in execute(). + future = None + // Someone called stop() in the meantime. + if (state == Computer.State.Stopping) + close() + // If we have a single number that's how long we may wait before + // resuming the state again. + else if (results == 1 && lua.isNumber(2)) { + val sleep = (lua.toNumber(2) * 1000).toLong + lua.pop(results) + // But only sleep if we don't have more signals to process. + if (signals.isEmpty) { + state = Computer.State.Sleeping + sleepUntil = System.currentTimeMillis + sleep } + else execute(Computer.State.Yielded) + } + // If we get one function it must be a wrapper for a synchronized call. + // The protocol is that a closure is pushed that is then called from + // the main server thread, and returns a table, which is in turn passed + // to the originating coroutine.yield(). + else if (results == 1 && lua.isFunction(2)) + state = Computer.State.SynchronizedCall + // Check if we are shutting down, and if so if we're rebooting. This is + // signalled by boolean values, where `false` means shut down, `true` + // means reboot (i.e shutdown then start again). + else if (results == 1 && lua.isBoolean(2)) { + val reboot = lua.toBoolean(2) + close() + if (reboot) + start() + } + else { + // Something else, just pop the results and try again. + lua.pop(results) + if (signals.isEmpty) + state = Computer.State.Suspended + else + execute(Computer.State.Yielded) } - - // Avoid getting to the closing part after the exception handling. - return - } - // Error handling. - else if (lua.isBoolean(2) && !lua.toBoolean(2)) { - // TODO Print something to an in-game screen. - OpenComputers.log.warning(lua.toString(3)) } + // The kernel thread returned. If it threw we'd we in the catch below. + else { + assert(lua.isThread(1)) + // We're expecting the result of a pcall, if anything, so boolean + (result | string). + if (!lua.isBoolean(2) || !(lua.isString(3) || lua.isNil(3))) { + OpenComputers.log.warning("Kernel returned unexpected results.") + } + // The pcall *should* never return normally... but check for it nonetheless. + if (lua.toBoolean(2)) { + OpenComputers.log.warning("Kernel stopped unexpectedly.") + } + else { + 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)) + } + close() + }) } catch { - case er: LuaMemoryAllocationException => { - // This is pretty likely to happen for non-upgraded computers. - // TODO Print something to an in-game screen, a la kernel panic. - OpenComputers.log.warning("Out of memory!") + 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 => { + 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() } - // Top-level catch-anything, because otherwise those exceptions get - // gobbled up by the executor unless we call the future's get(). - case t: Throwable => - OpenComputers.log.log(Level.WARNING, "Faulty kernel implementation, it should never throw.", t) } - // If we come here there was an error or we stopped, kill off the state. - close() + // State has inevitably changed, mark as changed to save again. + owner.markAsChanged() } } @@ -765,9 +771,12 @@ object Computer { /** The computer is not running right now and there is no Lua state. */ val Stopped = Value("Stopped") - /** The computer is running but yielded for a moment. */ + /** The computer is running but yielded and there were no more signals to process. */ val Suspended = Value("Suspended") + /** The computer is running but yielded but will resume as soon as possible. */ + val Yielded = Value("Yielded") + /** The computer is running but yielding for a longer amount of time. */ val Sleeping = Value("Sleeping") @@ -808,6 +817,11 @@ object Computer { thread.setDaemon(true) if (thread.getPriority != Thread.MIN_PRIORITY) thread.setPriority(Thread.MIN_PRIORITY) + thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler { + def uncaughtException(t: Thread, e: Throwable) { + OpenComputers.log.log(Level.WARNING, "Unhandled exception in worker thread.", e) + } + }) thread } }) diff --git a/li/cil/oc/server/computer/Drivers.scala b/li/cil/oc/server/computer/Drivers.scala index 9abc89e7b..40c0a5686 100644 --- a/li/cil/oc/server/computer/Drivers.scala +++ b/li/cil/oc/server/computer/Drivers.scala @@ -100,7 +100,7 @@ private[oc] object Drivers { case Some(code) => val name = driver.getClass.getName try { - computer.lua.load(code, name, "t") // ... func + computer.lua.load(code, "=" + name, "t") // ... func code.close() computer.lua.call(0, 0) // ... } diff --git a/li/cil/oc/server/computer/IComputerEnvironment.scala b/li/cil/oc/server/computer/IComputerEnvironment.scala index 19d790c50..bf748084d 100644 --- a/li/cil/oc/server/computer/IComputerEnvironment.scala +++ b/li/cil/oc/server/computer/IComputerEnvironment.scala @@ -1,6 +1,6 @@ package li.cil.oc.server.computer -import li.cil.oc.api.INetworkNode +import li.cil.oc.api.{Visibility, INetworkNode} import net.minecraft.world.World /** @@ -10,6 +10,8 @@ import net.minecraft.world.World trait IComputerEnvironment extends INetworkNode { override def name = "computer" + override def visibility = Visibility.Network + def world: World /** diff --git a/li/cil/oc/server/computer/Network.scala b/li/cil/oc/server/computer/Network.scala index 18b8a2908..b88358968 100644 --- a/li/cil/oc/server/computer/Network.scala +++ b/li/cil/oc/server/computer/Network.scala @@ -2,9 +2,7 @@ package li.cil.oc.server.computer import java.util.logging.Level import li.cil.oc.OpenComputers -import li.cil.oc.api.INetwork -import li.cil.oc.api.INetworkMessage -import li.cil.oc.api.INetworkNode +import li.cil.oc.api.{Visibility, INetwork, INetworkMessage, INetworkNode} import net.minecraft.block.Block import net.minecraft.tileentity.TileEntity import net.minecraft.world.{World, IBlockAccess} @@ -34,7 +32,7 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. node.address = 1 node.address -> ArrayBuffer(new Network.Node(node)) })) - Network.send(new Network.ConnectMessage(node), List(node)) + send(new Network.ConnectMessage(node), List(node)) } nodes.foreach(_.network = this) @@ -69,13 +67,22 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. private def add(oldNode: Network.Node, addedNode: INetworkNode) = { // Check if the other node is new or if we have to merge networks. val (newNode, sendQueue) = if (addedNode.network == null) { - val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[INetworkNode])] - sendQueue += ((new Network.ConnectMessage(addedNode), List(addedNode) ++ nodes)) - nodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), List(addedNode)))) val newNode = new Network.Node(addedNode) if (nodeMap.contains(addedNode.address) || addedNode.address < 1) addedNode.address = findId() - nodeMap.getOrElseUpdate(addedNode.address, new ArrayBuffer[Network.Node]) += newNode + // Store everything with an invalid address in slot zero. + val address = addedNode.address match { + case a if a > 0 => a + case _ => 0 + } + // Create the message queue. The address check is purely for performance, + // since we can skip all that if the node is non-valid. + val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[INetworkNode])] + if (address > 0 && addedNode.visibility != Visibility.None) { + sendQueue += ((new Network.ConnectMessage(addedNode), List(addedNode) ++ nodes)) + nodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), List(addedNode)))) + } + nodeMap.getOrElseUpdate(address, new ArrayBuffer[Network.Node]) += newNode addedNode.network = this (newNode, sendQueue) } @@ -88,7 +95,10 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. otherNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), thisNodes))) thisNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), otherNodes))) - // Change addresses for conflicting nodes in other network. + // Change addresses for conflicting nodes in other network. We can queue + // these messages because we're storing references to the nodes, so they + // will send the change notification to the right node even if that node + // also changes its address. val reserved = mutable.Set(otherNetwork.nodeMap.keySet.toSeq: _*) otherNodes.filter(node => nodeMap.contains(node.address)).foreach(node => { val oldAddress = node.address @@ -100,7 +110,7 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. } }) - // Add nodes from other network into this 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 @@ -114,7 +124,7 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. Network.Edge(oldNode, newNode) // Send all generated messages. - for ((message, nodes) <- sendQueue) Network.send(message, nodes) + for ((message, nodes) <- sendQueue) send(message, nodes) true } @@ -148,10 +158,10 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. // of which we'll re-use for this network. For all additional ones we // create new network instances. handleSplit(entry.remove(), nodes => { - nodes.foreach(n => Network.send(new Network.DisconnectMessage(n), List(node))) - Network.send(new Network.DisconnectMessage(node), nodes) + nodes.foreach(n => send(new Network.DisconnectMessage(n), List(node))) + send(new Network.DisconnectMessage(node), nodes) }) - Network.send(new Network.DisconnectMessage(node), List(node)) + send(new Network.DisconnectMessage(node), List(node)) true } } @@ -181,19 +191,24 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. val nodesA = subNodes(indexA) for (indexB <- (indexA + 1) until subNodes.length) { val nodesB = subNodes(indexB) - nodesA.foreach(nodeA => Network.send(new Network.DisconnectMessage(nodeA), nodesB)) - nodesB.foreach(nodeB => Network.send(new Network.DisconnectMessage(nodeB), nodesA)) + nodesA.foreach(nodeA => send(new Network.DisconnectMessage(nodeA), nodesB)) + nodesB.foreach(nodeB => send(new Network.DisconnectMessage(nodeB), nodesA)) } messageCallback(nodesA) } } def node(address: Int) = nodeMap.get(address) match { - case None => None - case Some(list) => Some(list.last.data) + case Some(list) if address > 0 => list.map(_.data).filter(_.visibility != Visibility.None).lastOption + case _ => None } - def nodes = nodeMap.values.flatten.map(_.data) + def nodes(reference: INetworkNode) = { + val referenceNeighbors = neighbors(reference).toSet + nodes.filter(node => node.visibility == Visibility.Network || referenceNeighbors.contains(node)) + } + + def nodes = nodeMap.filter(_._1 > 0).values.flatten.map(_.data).filter(_.visibility != Visibility.None) def neighbors(node: INetworkNode) = nodeMap.get(node.address) match { case None => throw new IllegalArgumentException("Node must be in this network.") @@ -203,14 +218,74 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. } } - def sendToNode(source: INetworkNode, target: Int, name: String, data: Any*) = + def sendToAddress(source: INetworkNode, target: Int, name: String, data: Any*) = nodeMap.get(target) match { case None => None - case Some(list) => Network.send(new Network.Message(source, name, Array(data: _*)), list.map(_.data)) + case Some(list) => send(new Network.Message(source, name, Array(data: _*)), list.map(_.data)) } + def sendToNeighbors(source: INetworkNode, name: String, data: Any*) = + send(new Network.Message(source, name, Array(data: _*)), neighbors(source)) + def sendToAll(source: INetworkNode, name: String, data: Any*) = - Network.send(new Network.Message(source, name, Array(data: _*)), nodes) + send(new Network.Message(source, name, Array(data: _*)), nodes) + + private def send(message: Network.Message, targets: Iterable[INetworkNode]) = + if (message.source.address > 0 && message.source.visibility != Visibility.None) { + def debug(target: INetworkNode) = {} // println("receive(" + message.name + "(" + message.data.mkString(", ") + "): " + message.source.address + ":" + message.source.name + " -> " + target.address + ":" + target.name + ")") + message match { + case _@(Network.ConnectMessage(_) | Network.ReconnectMessage(_, _)) => + // Cannot be canceled but respects visibility. + message.source.visibility match { + case Visibility.Neighbors => + val neighborSet = neighbors(message.source).toSet + val iterator = targets.filter(target => target == message.source || neighborSet.contains(target)).iterator + while (iterator.hasNext) try { + val target = iterator.next() + debug(target) + target.receive(message) + } catch { + case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e) + } + case Visibility.Network => + val iterator = targets.filter(_.address > 0).filter(_.visibility == Visibility.Network).iterator + while (iterator.hasNext) try { + val target = iterator.next() + debug(target) + target.receive(message) + } catch { + case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e) + } + } + None + case _@Network.DisconnectMessage(_) => + // Cannot be canceled but ignores visibility (because it'd be a pain to implement this otherwise). + val iterator = targets.filter(_.address > 0).iterator + while (iterator.hasNext) try { + val target = iterator.next() + debug(target) + target.receive(message) + } catch { + case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e) + } + None + case _ => + // Can be canceled but ignores visibility. + var result = None: Option[Array[Any]] + val iterator = targets.filter(_.address > 0).iterator + while (!message.isCanceled && iterator.hasNext) try { + val target = iterator.next() + debug(target) + target.receive(message) match { + case None => // Ignore. + case r => result = r + } + } catch { + case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e) + } + result + } + } else None private def findId(reserved: collection.Set[Int] = collection.Set.empty[Int]) = Range(1, Int.MaxValue).find( address => !nodeMap.contains(address) && !reserved.contains(address)).get @@ -322,26 +397,10 @@ object Network { def cancel() = isCanceled = true } - private class ConnectMessage(source: INetworkNode) extends Message(source, "network.connect") + private case class ConnectMessage(override val source: INetworkNode) extends Message(source, "network.connect") - private class DisconnectMessage(source: INetworkNode) extends Message(source, "network.disconnect") + private case class DisconnectMessage(override val source: INetworkNode) extends Message(source, "network.disconnect") - private class ReconnectMessage(source: INetworkNode, oldAddress: Int) extends Message(source, "network.reconnect", Array(oldAddress.asInstanceOf[Any])) + private case class ReconnectMessage(override val source: INetworkNode, oldAddress: Int) extends Message(source, "network.reconnect", Array(oldAddress.asInstanceOf[Any])) - private def send(message: Network.Message, nodes: Iterable[INetworkNode]) = { - //println("send(" + message.name + "(" + message.data.mkString(", ") + "): " + message.source.address + ":" + message.source.name + " -> [" + nodes.map(node => node.address + ":" + node.name).mkString(", ") + "])") - val iterator = nodes.iterator - var result = None: Option[Array[Any]] - while (!message.isCanceled && iterator.hasNext) { - try { - iterator.next().receive(message) match { - case None => // Ignore. - case r => result = r - } - } catch { - case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e) - } - } - result - } } \ No newline at end of file