os.reboot() and os.shutdown(); term.keyboardId to control which keyboard's input to listen to; added network "visibility" flag for nodes, controls which other nodes receive connect/reconnect messages from them; some more validation of network node validation (i.e. not sending to / receiving from nodes with address < 1 or visibility == none); fixed margin rendering for screen gui; always injecting the address of a sender for a signal received via the network; fixed item localization; made computers start/stop/queryable via network; major overhaul of multi-threading in computer class, not relying on future.cancel anymore (which apparently didn't interrupt running threads even though it should have) and centralized the state switching a bit (no longer any in signal() for example). removed the saveMonitor (if someone calls us from something other than the server thread its their fault).

This commit is contained in:
Florian Nücke 2013-09-28 17:32:23 +02:00
parent 90f4b2d96f
commit d24f726632
19 changed files with 705 additions and 457 deletions

View File

@ -1,3 +1,4 @@
oc.block.Computer.name=Computer
oc.block.Screen.name=Screen
oc.container.computer=Computer
oc.container.computer=Computer
oc.item.GraphicsCard.name=Graphics Card

View File

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

View File

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

View File

@ -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.
* <p/>
* This does not include nodes with an address less or equal to zero or with
* a visibility of `Visibility.None`.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* The same base filters as for `nodes` apply, with additional visibility
* checks applied, based on the specified node as a point of reference.
* <p/>
* 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.
* <p/>
* This does not include nodes with an address less or equal to zero or with
* a visibility of `Visibility.None`.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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 {
* <p/>
* 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.
* <p/>
* This does not include nodes with an address less or equal to zero or with
* a visibility of `Visibility.None`.
* <p/>
* 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.
* <p/>
* This does not include nodes with an address less or equal to zero or with
* a visibility of `Visibility.None`.
* <p/>
* 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.
* <p/>
* Messages should have a unique name to allow differentiating them when
* handling them in a network node. For example, computers will try to parse

View File

@ -35,6 +35,21 @@ trait INetworkNode {
*/
def name: String
/**
* The visibility of this node.
* <p/>
* 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.
* <p/>

View File

@ -0,0 +1,19 @@
package li.cil.oc.api
/**
* Possible reachability values foe nodes.
* <p/>
* 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")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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).
* <p/>
* 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
}
})

View File

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

View File

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

View File

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