From 59febb056fbde1b882c96410e650c558ae1266fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Tue, 1 Oct 2013 22:10:54 +0200 Subject: [PATCH] network rewrite to use uuids as node addresses. this way we won't have to expect addresses to change on network merges, which make a lot of things a much easier (in particular on the Lua side) --- assets/opencomputers/lua/drivers/gpu.lua | 15 +- assets/opencomputers/lua/drivers/keyboard.lua | 1 - assets/opencomputers/lua/drivers/redstone.lua | 3 +- assets/opencomputers/lua/init.lua | 186 +++---- li/cil/oc/api/Network.scala | 128 ++--- li/cil/oc/api/driver/Item.scala | 2 + li/cil/oc/api/network/Node.scala | 72 +-- .../common/component/ScreenEnvironment.scala | 4 +- li/cil/oc/common/tileentity/Computer.scala | 21 +- li/cil/oc/common/tileentity/Keyboard.scala | 6 +- li/cil/oc/server/component/Computer.scala | 25 +- li/cil/oc/server/component/Filesystem.scala | 15 +- li/cil/oc/server/component/GraphicsCard.scala | 24 +- li/cil/oc/server/component/RedstoneCard.scala | 20 +- li/cil/oc/server/network/Network.scala | 513 ++++++++---------- 15 files changed, 466 insertions(+), 569 deletions(-) diff --git a/assets/opencomputers/lua/drivers/gpu.lua b/assets/opencomputers/lua/drivers/gpu.lua index 5215543d7..8913e568c 100644 --- a/assets/opencomputers/lua/drivers/gpu.lua +++ b/assets/opencomputers/lua/drivers/gpu.lua @@ -1,4 +1,3 @@ ---[[ API for graphics cards. ]] driver.gpu = {} function driver.gpu.setResolution(gpu, screen, w, h) @@ -25,25 +24,25 @@ function driver.gpu.copy(gpu, screen, col, row, w, h, tx, ty) sendToNode(gpu, "gpu.copy", screen, col, row, w, h, tx, ty) end -function driver.gpu.bind(gpuId, screenId) +function driver.gpu.bind(gpu, screen) return { setResolution = function(w, h) - driver.gpu.setResolution(component.address(gpuId), component.address(screenId), w, h) + driver.gpu.setResolution(gpu, screen, w, h) end, getResolution = function() - return driver.gpu.getResolution(component.address(gpuId), component.address(screenId)) + return driver.gpu.getResolution(gpu, screen) end, getResolutions = function() - return driver.gpu.getResolutions(component.address(gpuId), component.address(screenId)) + return driver.gpu.getResolutions(gpu, screen) end, set = function(col, row, value) - driver.gpu.set(component.address(gpuId), component.address(screenId), col, row, value) + driver.gpu.set(gpu, screen, col, row, value) end, fill = function(col, ro, w, h, value) - driver.gpu.fill(component.address(gpuId), component.address(screenId), col, ro, w, h, value) + driver.gpu.fill(gpu, screen, col, ro, w, h, value) end, copy = function(col, row, w, h, tx, ty) - driver.gpu.copy(component.address(gpuId), component.address(screenId), col, row, w, h, tx, ty) + driver.gpu.copy(gpu, screen, col, row, w, h, tx, ty) end } end \ No newline at end of file diff --git a/assets/opencomputers/lua/drivers/keyboard.lua b/assets/opencomputers/lua/drivers/keyboard.lua index bf9dfda81..c600882ad 100644 --- a/assets/opencomputers/lua/drivers/keyboard.lua +++ b/assets/opencomputers/lua/drivers/keyboard.lua @@ -1,4 +1,3 @@ ---[[ API for keyboards. ]] driver.keyboard = {} driver.keyboard.keys = { diff --git a/assets/opencomputers/lua/drivers/redstone.lua b/assets/opencomputers/lua/drivers/redstone.lua index 25f458282..799ec880c 100644 --- a/assets/opencomputers/lua/drivers/redstone.lua +++ b/assets/opencomputers/lua/drivers/redstone.lua @@ -1,6 +1,5 @@ ---[[ API for Redstone Cards. ]] - driver.redstone = {} +driver.rs = driver.redstone driver.redstone.sides = {"top", "bottom", "left", "right", "front", "back"} diff --git a/assets/opencomputers/lua/init.lua b/assets/opencomputers/lua/init.lua index e33b1ef90..7a62a4278 100644 --- a/assets/opencomputers/lua/init.lua +++ b/assets/opencomputers/lua/init.lua @@ -37,7 +37,7 @@ local weakListeners = {} local function listenersFor(name, weak) checkArg(1, name, "string") if weak then - weakListeners[name] = weakListeners[name] or setmetatable({}, {__mode = "k"}) + weakListeners[name] = weakListeners[name] or setmetatable({}, {__mode = "v"}) return weakListeners[name] else listeners[name] = listeners[name] or {} @@ -52,13 +52,21 @@ local timers = {} --[[ Register a new event listener for the specified event. ]] function event.listen(name, callback, weak) checkArg(2, callback, "function") - listenersFor(name, weak)[callback] = true + table.insert(listenersFor(name, weak), callback) end --[[ Remove an event listener. ]] function event.ignore(name, callback) - listenersFor(name, false)[callback] = nil - listenersFor(name, true)[callback] = nil + local function remove(list) + for k, v in ipairs(list) do + if v == callback then + table.remove(k) + return + end + end + end + remove(listenersFor(name, false)) + remove(listenersFor(name, true)) end --[[ Dispatch an event with the specified parameter. ]] @@ -67,13 +75,13 @@ function event.fire(name, ...) -- 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 + for _, callback in ipairs(listenersFor(name, false)) do local result, message = xpcall(callback, event.error, name, ...) if not result and message then error(message, 0) end end - for callback, _ in pairs(listenersFor(name, true)) do + for _, callback in ipairs(listenersFor(name, true)) do local result, message = xpcall(callback, event.error, name, ...) if not result and message then error(message, 0) @@ -133,100 +141,64 @@ end ------------------------------------------------------------------------------- ---[[ Keep track of connected components across address changes. ]] +--[[ Keep track of connected components. ]] local components = {} component = {} -function component.address(id) - local component = components[id] +function component.type(address) + local component = components[address] if component then - return component.address + return component end end -function component.type(id) - local component = components[id] - if component then - return component.name - end -end - -function component.id(address) - for id, component in pairs(components) do - if component.address == address then - return id - end - end -end - -function component.ids() - local id = nil +function component.list() + local address = nil return function() - id = next(components, id) - return id + address = next(components, address) + return address end end event.listen("component_added", function(_, address) - local id = #components + 1 - components[id] = {address = address, name = driver.componentType(address)} - event.fire("component_installed", id) + components[address] = driver.componentType(address) end) event.listen("component_removed", function(_, address) - local id = component.id(address) - 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) - 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 + components[address] = nil end) ------------------------------------------------------------------------------- --[[ Setup terminal API. ]] -local gpuId, screenId = 0, 0 +local gpuAddress, screenAddress = false, false 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 gpuId < 1 then - term.gpuId(id) - elseif type == "screen" and screenId < 1 then - term.screenId(id) +event.listen("component_added", function(_, address) + local type = component.type(address) + if type == "gpu" and not gpuAddress then + term.gpu(address) + elseif type == "screen" and not screenAddress then + term.screen(address) end end) -event.listen("component_uninstalled", function(_, id) - if gpuId == id then - term.gpuId(0) - for id in component.ids() do - if component.type(id) == "gpu" then - term.gpuId(id) +event.listen("component_removed", function(_, address) + if gpuAddress == address then + term.gpu(false) + for address in component.list() do + if component.type(address) == "gpu" then + term.gpu(address) return end end - elseif screenId == id then - term.screenId(0) - for id in component.ids() do - if component.type(id) == "screen" then - term.screenId(id) + elseif screenAddress == address then + term.screen(false) + for address in component.list() do + if component.type(address) == "screen" then + term.screen(address) return end end @@ -234,17 +206,16 @@ event.listen("component_uninstalled", function(_, id) end) event.listen("screen_resized", function(_, address, w, h) - local id = component.id(address) - if id == screenId then + if address == screenAddress then screenWidth = w screenHeight = h end end) local function bindIfPossible() - if gpuId > 0 and screenId > 0 then + if gpuAddress and screenAddress then if not boundGpu then - boundGpu = driver.gpu.bind(gpuId, screenId) + boundGpu = driver.gpu.bind(gpuAddress, screenAddress) screenWidth, screenHeight = boundGpu.getResolution() event.fire("term_available") end @@ -257,30 +228,26 @@ end term = {} -function term.gpu() - return boundGpu -end - function term.screenSize() return screenWidth, screenHeight end -function term.gpuId(id) - if id then - checkArg(1, id, "number") - gpuId = id +function term.gpu(address) + if address ~= nil then + checkArg(1, address, "string", "boolean") + gpuAddress = address bindIfPossible() end - return gpuId + return gpuAddress end -function term.screenId(id) - if id then - checkArg(1, id, "number") - screenId = id +function term.screen(address) + if address ~= nil then + checkArg(1, address, "string", "boolean") + screenAddress = address bindIfPossible() end - return screenId + return screenAddress end function term.getCursor() @@ -359,23 +326,23 @@ end ------------------------------------------------------------------------------- --[[ Primitive command line. ]] -local keyboardId = 0 +local keyboardAddress = false local lastCommand, command = "", "" local isRunning = false -event.listen("component_installed", function(_, id) - local type = component.type(id) - if type == "keyboard" and keyboardId < 1 then - keyboardId = id +event.listen("component_added", function(_, address) + local type = component.type(address) + if type == "keyboard" and not keyboardAddress then + term.keyboardAddress(address) 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 +event.listen("component_uninstalled", function(_, address) + if keyboardAddress == address then + term.keyboardAddress(false) + for address in component.list() do + if component.type(address) == "keyboard" then + term.keyboardAddress(address) return end end @@ -383,17 +350,17 @@ event.listen("component_uninstalled", function(_, id) 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 +function term.keyboardAddress(address) + if address ~= nil then + checkArg(1, address, "string", "boolean") + keyboardAddress = address end - return keyboardId + return keyboardAddress end local function onKeyDown(_, address, char, code) if isRunning then return end -- ignore events while running a command - if component.id(address) ~= keyboardId then return end + if address ~= keyboardAddress then return end if not boundGpu then return end local x, y = term.getCursor() local keys = driver.keyboard.keys @@ -438,7 +405,7 @@ end local function onClipboard(_, address, value) if isRunning then return end -- ignore events while running a command - if component.id(address) ~= keyboardId then return end + if address ~= keyboardAddress then return end value = value:match("([^\r\n]+)") if value and value:len() > 0 then command = command .. value @@ -466,13 +433,12 @@ end) local blinkState = false while true do coroutine.sleep(0.5) - local gpu = term.gpu() - if gpu then + if boundGpu then local x, y = term.getCursor() if blinkState then - term.gpu().set(x, y, string.char(0x2588)) -- Solid block. + boundGpu.set(x, y, string.char(0x2588)) -- Solid block. else - term.gpu().set(x, y, " ") + boundGpu.set(x, y, " ") end end blinkState = not blinkState diff --git a/li/cil/oc/api/Network.scala b/li/cil/oc/api/Network.scala index 6765ca2b5..efad63726 100644 --- a/li/cil/oc/api/Network.scala +++ b/li/cil/oc/api/Network.scala @@ -18,24 +18,23 @@ import net.minecraft.world.IBlockAccess * then merge it with the other(s). If no networks exist, it should create a new * one. All this logic is provided by `Network.joinOrCreateNetwork`. *

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

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

* There are a couple of system messages to be aware of. These are all sent by * the network manager itself: *

*

* IMPORTANT: do not implement this interface yourself and create @@ -51,7 +50,7 @@ trait Network { * This is used by nodes to join an existing network. At least one of the two * nodes must already be in the network. If one of the nodes is not yet in the * network, it will be added to the network. If both nodes are already in the - * network only the connection between the two nodes is stored. If one of the + * network only the connection between the two nodes is added. If one of the * nodes is not in this network but in another network, the networks will be * merged. *

@@ -66,26 +65,10 @@ trait Network { */ def connect(nodeA: Node, nodeB: Node): Boolean - /** - * Changes the address of a node. - *

- * If another node with the specified address already exists in the network - * it will be forced to change its address to an arbitrarily assigned, not - * taken one. - *

- * This is mainly used to restore a nodes address after it was loaded from - * an old state (chunk load). - * - * @param node the node to change the address of. - * @param address the new address of the node. - * @return whether the node's address changed. - */ - def reconnect(node: Node, address: Int): Boolean - /** * Removes a node connection in the network. *

- * Both nodes must be part of the same network. + * Both nodes must be part of this network. *

* This can be useful for cutting connections that depend on some condition * that does not involve the nodes' actual existence in the network, such as @@ -102,36 +85,30 @@ trait Network { /** * Removes a node from the network. *

- * This should be called by nodes when they are destroyed (e.g. onBreakBlock) - * or unloaded. If removing the node leads to two graphs (it was the a bridge - * node) the network will be split up. + * This should be called by nodes when they are destroyed (e.g. `breakBlock`) + * or unloaded. Removing the node can lead to one or more new networks if it + * was the a bridge node, i.e. the only node connecting the resulting + * networks. * * @param node the node to remove from the network. - * @return whether the node was removed. + * @return true if the node was removed; false if it wasn't in the network. */ def remove(node: Node): Boolean // ----------------------------------------------------------------------- // /** - * 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. + * Get the network node with the specified address. * * @param address the address of the node to get. * @return the node with that address. */ - def node(address: Int): Option[Node] + def node(address: String): Option[Node] /** - * The list of all valid nodes in this network. + * The list of all nodes in this network. *

- * This does not include nodes with an address less or equal to zero or with - * a visibility of `Visibility.None`. + * This does *not* include nodes with a visibility of `Visibility.None`. * * @return the list of nodes in this network. */ @@ -140,12 +117,15 @@ trait Network { /** * 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 does *not* include nodes with a visibility of `Visibility.None` or + * a visibility of `Visibility.Neighbors` when there is no direct connection + * between that node and the reference node. This will always also contain + * the reference node itself, unless the reference node's visibility is + * `Visibility.None`. *

- * 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 can be useful when performing a delayed initialization of a node. + * For example, computers will use this when starting up to generate + * `component_added` signals for all visible nodes in the network. * * @param reference the node to get the visible other nodes for. * @return the nodes visible to the specified node. @@ -153,10 +133,9 @@ trait Network { def nodes(reference: Node): Iterable[Node] /** - * The list of valid nodes the specified node is directly connected to. + * The list of 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 does *not* include nodes 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. @@ -170,17 +149,19 @@ trait Network { // ----------------------------------------------------------------------- // /** - * Sends a message to a specific address, which may mean multiple nodes. + * Sends a message to a specific address. *

- * 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 + * If the target node with that address has a visibility of `Visibility.None` + * the message will not be delivered to that node. If the target node with + * that address has a visibility of `Visibility.Neighbors` and the source + * node is not directly connected to the target 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 - * messages named "computer.signal" by converting the message data to a + * 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". + * this purpose should be named `computer.signal`. *

* 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 @@ -193,53 +174,52 @@ trait Network { * @param name the name of the message. * @param data the message to send. * @return the result of the message being handled, if any. + * @throws IllegalArgumentException if the source node is not in this network. */ - def sendToAddress(source: Node, target: Int, name: String, data: Any*): Option[Array[Any]] + def sendToAddress(source: Node, target: String, name: String, data: Any*): Option[Array[Any]] /** - * Sends a message to all direct valid neighbors of the source node. + * Sends a message to all direct 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`. + * Targets are determined using `neighbors`. *

* 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 + * 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". + * 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 + * @throws IllegalArgumentException if the source node is not in this network. + * @see `neighbors` */ def sendToNeighbors(source: Node, name: String, data: Any*) /** - * Sends a message to all valid nodes in the network. + * Sends a message to all nodes in the network visible to the source node. *

- * 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. + * Targets are determined using `nodes(source)`. *

* 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 + * 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". + * this purpose should be named `computer.signal`. * * @param source the node that sends the message. * @param data the message to send. + * @throws IllegalArgumentException if the source node is not in this network. + * @see `nodes` */ - def sendToAll(source: Node, name: String, data: Any*) + def sendToVisible(source: Node, name: String, data: Any*) } object Network extends NetworkAPI { /** - * Tries to add a tile entity network node at the specified coordinates to adjacent networks. + * Tries to add a tile entity network node at the specified coordinates to + * adjacent networks. * * @param world the world the tile entity lives in. * @param x the X coordinate of the tile entity. diff --git a/li/cil/oc/api/driver/Item.scala b/li/cil/oc/api/driver/Item.scala index 600ffe5fd..862196ac8 100644 --- a/li/cil/oc/api/driver/Item.scala +++ b/li/cil/oc/api/driver/Item.scala @@ -75,6 +75,8 @@ trait Item extends Driver { * @return the tag to use for saving and loading. */ def nbt(item: ItemStack) = { + if (!item.hasTagCompound) + item.setTagCompound(new NBTTagCompound()) val nbt = item.getTagCompound if (!nbt.hasKey("oc.node")) { nbt.setCompoundTag("oc.node", new NBTTagCompound()) diff --git a/li/cil/oc/api/network/Node.scala b/li/cil/oc/api/network/Node.scala index a7fd7f9a3..628e92c2e 100644 --- a/li/cil/oc/api/network/Node.scala +++ b/li/cil/oc/api/network/Node.scala @@ -1,16 +1,14 @@ package li.cil.oc.api.network import li.cil.oc.api.{Persistable, Network} -import net.minecraft.nbt.NBTTagCompound +import net.minecraft.nbt.{NBTTagString, NBTTagCompound} /** * A single node in a `INetwork`. *

- * All nodes in a network should have a unique address; the network - * will try to generate a unique address and assign it to new nodes. A node must - * never ever change its address while in a network (because the lookup-table in - * the network manager would not be notified of this change). If you must change - * the address, use `Network.reconnect`. + * All nodes in a network must have a unique address; the network will + * generate a unique address and assign it to new nodes. A node must never ever + * change its address on its own accord. *

* Per default there are two kinds of nodes: tile entities and item components. * If a `TileEntity` implements this interface adding/removal from its @@ -20,8 +18,8 @@ import net.minecraft.nbt.NBTTagCompound * All other kinds of nodes you may come up with will also have to be handled * manually. *

- * Items have to be handled by a corresponding `IItemDriver`. Existing - * blocks may be interfaced with a proxy block if a `IBlockDriver` exists + * Items have to be handled by a corresponding `ItemDriver`. Existing + * blocks may be interfaced with a proxy block if a `BlockDriver` exists * that supports the block. */ trait Node extends Persistable { @@ -55,28 +53,24 @@ trait Node extends Persistable { * The address of the node, so that it can be found in the network. *

* This is used by the network manager when a node is added to a network to - * assign it a unique address in that network. Nodes should avoid using custom - * addresses since that could lead to multiple nodes with the same address in - * a network. Note that that in and by itself is supported however, it is just - * harder to work with. - *

- * Some nodes may however wish to simply ignore this and always provide the - * same address (e.g. zero), when they are never expected to be used by other - * nodes (cables, for example). + * assign it a unique address, if it doesn't already have one. Nodes must not + * use custom addresses, only those assigned by the network. The only option + * they have is to *not* have an address, which can be useful for "dummy" + * nodes, such as cables. In that case they may ignore the address being set. * * @return the id of this node. */ - var address = 0 + var address: Option[String] = None /** * The network this node is currently in. *

- * Note that valid nodes should never return `null` here. When created a node + * Note that valid nodes should never return `None` here. When created a node * should immediately be added to a network, after being removed from its - * network it should be considered invalid. + * network a node should be considered invalid. *

- * This is used by the `NetworkAPI` and the network itself when merging with - * another network. You should never have to set this yourself. + * This will always be set automatically by the network manager. Do not + * change this value and do not return anything that it wasn't set to. * * @return the network the node is in. */ @@ -101,7 +95,6 @@ trait Node extends Persistable { if (message.source == this) message.name match { case "network.connect" => onConnect() case "network.disconnect" => onDisconnect() - case "network.reconnect" => onReconnect() case _ => // Ignore. } None @@ -109,31 +102,44 @@ trait Node extends Persistable { /** * Reads a previously stored address value from the specified tag. - * - * This should be called when implementing class is loaded. + *

+ * This should be called when associated object is loaded. For items, this + * should be called when their container is loaded. For blocks this should + * be called when their tile entity is loaded. * * @param nbt the tag to read from. */ def load(nbt: NBTTagCompound) = { - network match { - case None => address = nbt.getInteger("address") - case Some(net) => net.reconnect(this, nbt.getInteger("address")) - } + if (nbt.hasKey("address") && nbt.getTag("address").isInstanceOf[NBTTagString]) + address = Option(nbt.getString("address")) } /** * Stores the node's address in the specified NBT tag, to keep addresses the * same across unloading/loading. - * - * This should be called when the implementing class is saved. + *

+ * This should be called when the implementing class is saved. For items, + * this should be called when their container is saved. For blocks this + * should be called when their tile entity is saved. * * @param nbt the tag to write to. */ - def save(nbt: NBTTagCompound) = nbt.setInteger("address", address) + def save(nbt: NBTTagCompound) = { + address.foreach(nbt.setString("address", _)) + } + /** + * Called when this node is added to a network. + *

+ * Use this for custom initialization logic. + */ protected def onConnect() {} + /** + * Called when this node is removed from a network. + *

+ * Use this for custom tear-down logic. A node should be considered invalid + * and non-reusable after this has happened. + */ protected def onDisconnect() {} - - protected def onReconnect() {} } \ No newline at end of file diff --git a/li/cil/oc/common/component/ScreenEnvironment.scala b/li/cil/oc/common/component/ScreenEnvironment.scala index 082ef94fa..02fa05b25 100644 --- a/li/cil/oc/common/component/ScreenEnvironment.scala +++ b/li/cil/oc/common/component/ScreenEnvironment.scala @@ -15,7 +15,7 @@ trait ScreenEnvironment extends Node { override def name = "screen" - override def visibility = Visibility.Neighbors + override def visibility = Visibility.Network override def receive(message: Message): Option[Array[Any]] = { super.receive(message) @@ -53,7 +53,7 @@ trait ScreenEnvironment extends Node { } def onScreenResolutionChange(w: Int, h: Int) = - network.foreach(_.sendToAll(this, "computer.signal", "screen_resized", w, h)) + network.foreach(_.sendToVisible(this, "computer.signal", "screen_resized", w, h)) def onScreenSet(col: Int, row: Int, s: String) {} diff --git a/li/cil/oc/common/tileentity/Computer.scala b/li/cil/oc/common/tileentity/Computer.scala index f4087be2c..eccfad371 100644 --- a/li/cil/oc/common/tileentity/Computer.scala +++ b/li/cil/oc/common/tileentity/Computer.scala @@ -32,14 +32,14 @@ class Computer(isClient: Boolean) extends Rotatable with ComputerEnvironment wit message.data match { // 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" && message.source.address != 0 && isRunning => - computer.signal("component_removed", message.source.address); None + case Array() if message.name == "network.connect" && isRunning => + computer.signal("component_added", message.source.address.get); None + case Array() if message.name == "network.disconnect" && isRunning => + computer.signal("component_removed", message.source.address.get); None case Array(oldAddress: Integer) if message.name == "network.reconnect" && isRunning => - computer.signal("component_changed", message.source.address, oldAddress); None + computer.signal("component_changed", message.source.address.get, oldAddress); None case Array(name: String, args@_*) if message.name == "computer.signal" => - computer.signal(name, List(message.source.address) ++ args: _*); None + computer.signal(name, List(message.source.address.get) ++ args: _*); None case Array() if message.name == "computer.start" => Some(Array(turnOn().asInstanceOf[Any])) case Array() if message.name == "computer.stop" => @@ -50,11 +50,6 @@ class Computer(isClient: Boolean) extends Rotatable with ComputerEnvironment wit } } - override protected def onReconnect() = { - super.onReconnect() - computer.signal("address_change", address) - } - // ----------------------------------------------------------------------- // // General // ----------------------------------------------------------------------- // @@ -97,9 +92,9 @@ class Computer(isClient: Boolean) extends Rotatable with ComputerEnvironment wit if (isRunning != computer.isRunning) { isRunning = computer.isRunning if (isRunning) - network.foreach(_.sendToAll(this, "computer.started")) + network.foreach(_.sendToVisible(this, "computer.started")) else - network.foreach(_.sendToAll(this, "computer.stopped")) + network.foreach(_.sendToVisible(this, "computer.stopped")) ServerPacketSender.sendComputerState(this, isRunning) } } diff --git a/li/cil/oc/common/tileentity/Keyboard.scala b/li/cil/oc/common/tileentity/Keyboard.scala index adba1b30d..c39011055 100644 --- a/li/cil/oc/common/tileentity/Keyboard.scala +++ b/li/cil/oc/common/tileentity/Keyboard.scala @@ -15,13 +15,13 @@ class Keyboard extends Rotatable with Node { message.data match { case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyDown" => if (isUseableByPlayer(p)) - network.foreach(_.sendToAll(this, "computer.signal", "key_down", char, code)) + network.foreach(_.sendToVisible(this, "computer.signal", "key_down", char, code)) case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyUp" => if (isUseableByPlayer(p)) - network.foreach(_.sendToAll(this, "computer.signal", "key_up", char, code)) + network.foreach(_.sendToVisible(this, "computer.signal", "key_up", char, code)) case Array(p: Player, value: String) if message.name == "keyboard.clipboard" => if (isUseableByPlayer(p)) - network.foreach(_.sendToAll(this, "computer.signal", "clipboard", value)) + network.foreach(_.sendToVisible(this, "computer.signal", "clipboard", value)) case _ => // Ignore. } None diff --git a/li/cil/oc/server/component/Computer.scala b/li/cil/oc/server/component/Computer.scala index debf07249..032f01f23 100644 --- a/li/cil/oc/server/component/Computer.scala +++ b/li/cil/oc/server/component/Computer.scala @@ -159,7 +159,7 @@ class Computer(val owner: Computer.Environment) extends component.Computer with signal("") // Inject component added signals for all nodes in the network. - owner.network.foreach(_.nodes(owner).foreach(node => signal("component_added", node.address))) + owner.network.foreach(_.nodes(owner).foreach(node => signal("component_added", node.address.get))) // All green, computer started successfully. true @@ -242,12 +242,12 @@ class Computer(val owner: Computer.Environment) extends component.Computer with // 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.foreach(_.sendToAll(owner, "computer.crashed", "not enough memory")) + //owner.network.foreach(_.sendToVisible(owner, "computer.crashed", "not enough memory")) close() case e: java.lang.Error if e.getMessage == "not enough memory" => OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages // TODO get this to the world as a computer.crashed message. problem: synchronizing it. - owner.network.foreach(_.sendToAll(owner, "computer.crashed", "not enough memory")) + //owner.network.foreach(_.sendToVisible(owner, "computer.crashed", "not enough memory")) close() case e: Throwable => { OpenComputers.log.log(Level.WARNING, "Faulty Lua implementation for synchronized calls.", e) @@ -488,7 +488,10 @@ class Computer(val owner: Computer.Environment) extends component.Computer with // Allow the computer to figure out its own id in the component network. lua.pushScalaFunction(lua => { - lua.pushInteger(owner.address) + owner.address match { + case None => lua.pushNil() + case Some(address) => lua.pushString(address) + } 1 }) lua.setField(-2, "address") @@ -555,7 +558,7 @@ class Computer(val owner: Computer.Environment) extends component.Computer with lua.pushScalaFunction(lua => owner.network.fold(None: Option[Array[Any]])(_. - sendToAddress(owner, lua.checkInteger(1), lua.checkString(2), parseArguments(lua, 3): _*)) match { + sendToAddress(owner, lua.checkString(1), lua.checkString(2), parseArguments(lua, 3): _*)) match { case Some(Array(results@_*)) => results.foreach(pushResult(lua, _)) results.length @@ -563,14 +566,14 @@ class Computer(val owner: Computer.Environment) extends component.Computer with }) lua.setGlobal("sendToNode") - lua.pushScalaFunction(lua => { - owner.network.foreach(_.sendToAll(owner, lua.checkString(1), parseArguments(lua, 2): _*)) - 0 - }) - lua.setGlobal("sendToAll") +// lua.pushScalaFunction(lua => { +// owner.network.foreach(_.sendToVisible(owner, lua.checkString(1), parseArguments(lua, 2): _*)) +// 0 +// }) +// lua.setGlobal("sendToAll") lua.pushScalaFunction(lua => { - owner.network.fold(None: Option[Node])(_.node(lua.checkInteger(1))) match { + owner.network.fold(None: Option[Node])(_.node(lua.checkString(1))) match { case None => 0 case Some(node) => lua.pushString(node.name); 1 } diff --git a/li/cil/oc/server/component/Filesystem.scala b/li/cil/oc/server/component/Filesystem.scala index f4623cf98..d5a2a531d 100644 --- a/li/cil/oc/server/component/Filesystem.scala +++ b/li/cil/oc/server/component/Filesystem.scala @@ -10,24 +10,19 @@ import scala.collection.mutable class FileSystem(val fileSystem: api.FileSystem) extends ItemComponent { - private val handles = mutable.Map.empty[Int, mutable.Set[Long]] + private val handles = mutable.Map.empty[String, mutable.Set[Long]] override def name = "fs" override def receive(message: Message) = { message.data match { - case Array() if message.name == "network.disconnect" && handles.contains(message.source.address) => - for (handle <- handles(message.source.address)) { + case Array() if message.name == "network.disconnect" && handles.contains(message.source.address.get) => + for (handle <- handles(message.source.address.get)) { fileSystem.file(handle) match { case None => // Maybe file system was accessed from somewhere else. case Some(file) => file.close() } } - case Array(oldAddress: Int) if message.name == "network.reconnect" => - handles.remove(oldAddress) match { - case None => // Node held no handles. - case Some(set) => handles += message.source.address -> set - } case _ => // Ignore. } super.receive(message) @@ -54,7 +49,7 @@ class FileSystem(val fileSystem: api.FileSystem) extends ItemComponent { case Array(path: Array[Byte], mode: Array[Byte]) if message.name == "fs.open" => val handle = fileSystem.open(clean(path), Mode.parse(new String(mode, "UTF-8"))) if (handle > 0) { - handles.getOrElseUpdate(message.source.address, mutable.Set.empty[Long]) += handle + handles.getOrElseUpdate(message.source.address.get, mutable.Set.empty[Long]) += handle } Some(Array(handle.asInstanceOf[Any])) @@ -62,7 +57,7 @@ class FileSystem(val fileSystem: api.FileSystem) extends ItemComponent { fileSystem.file(handle.toLong) match { case None => // Ignore. case Some(file) => - handles.get(message.source.address) match { + handles.get(message.source.address.get) match { case None => // Not the owner of this handle. case Some(set) => if (set.remove(handle.toLong)) file.close() } diff --git a/li/cil/oc/server/component/GraphicsCard.scala b/li/cil/oc/server/component/GraphicsCard.scala index 80c8a410a..9f597aca3 100644 --- a/li/cil/oc/server/component/GraphicsCard.scala +++ b/li/cil/oc/server/component/GraphicsCard.scala @@ -9,29 +9,29 @@ class GraphicsCard extends ItemComponent { override def name = "gpu" override protected def receiveFromNeighbor(network: Network, message: Message) = message.data match { - case Array(screen: Double, w: Double, h: Double) if message.name == "gpu.resolution=" => + case Array(screen: Array[Byte], w: Double, h: Double) if message.name == "gpu.resolution=" => if (supportedResolutions.contains((w.toInt, h.toInt))) - network.sendToAddress(this, screen.toInt, "screen.resolution=", w.toInt, h.toInt) + network.sendToAddress(this, new String(screen, "UTF-8"), "screen.resolution=", w.toInt, h.toInt) else Some(Array(Unit, "unsupported resolution")) - case Array(screen: Double) if message.name == "gpu.resolution" => - network.sendToAddress(this, screen.toInt, "screen.resolution") - case Array(screen: Double) if message.name == "gpu.resolutions" => - network.sendToAddress(this, screen.toInt, "screen.resolutions") match { + case Array(screen: Array[Byte]) if message.name == "gpu.resolution" => + network.sendToAddress(this, new String(screen, "UTF-8"), "screen.resolution") + case Array(screen: Array[Byte]) if message.name == "gpu.resolutions" => + network.sendToAddress(this, new String(screen, "UTF-8"), "screen.resolutions") match { case Some(Array(resolutions@_*)) => Some(Array(supportedResolutions.intersect(resolutions): _*)) case _ => None } - case Array(screen: Double, x: Double, y: Double, value: Array[Byte]) if message.name == "gpu.set" => - network.sendToAddress(this, screen.toInt, "screen.set", x.toInt - 1, y.toInt - 1, new String(value, "UTF-8")) - case Array(screen: Double, x: Double, y: Double, w: Double, h: Double, value: Array[Byte]) if message.name == "gpu.fill" => + case Array(screen: Array[Byte], x: Double, y: Double, value: Array[Byte]) if message.name == "gpu.set" => + network.sendToAddress(this, new String(screen, "UTF-8"), "screen.set", x.toInt - 1, y.toInt - 1, new String(value, "UTF-8")) + case Array(screen: Array[Byte], x: Double, y: Double, w: Double, h: Double, value: Array[Byte]) if message.name == "gpu.fill" => val s = new String(value, "UTF-8") if (s.length == 1) - network.sendToAddress(this, screen.toInt, "screen.fill", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, s.charAt(0)) + network.sendToAddress(this, new String(screen, "UTF-8"), "screen.fill", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, s.charAt(0)) else Some(Array(Unit, "invalid fill value")) - case Array(screen: Double, x: Double, y: Double, w: Double, h: Double, tx: Double, ty: Double) if message.name == "gpu.copy" => - network.sendToAddress(this, screen.toInt, "screen.copy", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, tx.toInt, ty.toInt) + case Array(screen: Array[Byte], x: Double, y: Double, w: Double, h: Double, tx: Double, ty: Double) if message.name == "gpu.copy" => + network.sendToAddress(this, new String(screen, "UTF-8"), "screen.copy", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, tx.toInt, ty.toInt) case _ => None } } \ No newline at end of file diff --git a/li/cil/oc/server/component/RedstoneCard.scala b/li/cil/oc/server/component/RedstoneCard.scala index 9a9a778a6..d5023f916 100644 --- a/li/cil/oc/server/component/RedstoneCard.scala +++ b/li/cil/oc/server/component/RedstoneCard.scala @@ -10,18 +10,18 @@ class RedstoneCard extends ItemComponent { override def name = "redstone" override protected def receiveFromNeighbor(network: Network, message: Message) = message.data match { - case Array(target: Double, side: Double) if message.name == "redstone.input" => - input(target.toInt, side.toInt) - case Array(target: Double, side: Double) if message.name == "redstone.output" => - output(target.toInt, side.toInt) - case Array(target: Double, side: Double, value: Double) if message.name == "redstone.output=" => - output(target.toInt, side.toInt, value.toInt); None + case Array(target: Array[Byte], side: Double) if message.name == "redstone.input" => + input(new String(target, "UTF-8"), side.toInt) + case Array(target: Array[Byte], side: Double) if message.name == "redstone.output" => + output(new String(target, "UTF-8"), side.toInt) + case Array(target: Array[Byte], side: Double, value: Double) if message.name == "redstone.output=" => + output(new String(target, "UTF-8"), side.toInt, value.toInt); None case _ => None // Ignore. } - private def tryGet(target: Int) = network.fold(None: Option[Node])(_.node(target)) + private def tryGet(target: String) = network.fold(None: Option[Node])(_.node(target)) - private def input(target: Int, side: Int) = if (side >= 0 && side < 6) tryGet(target) match { + private def input(target: String, side: Int) = if (side >= 0 && side < 6) tryGet(target) match { case Some(r: RedstoneEnabled) => Some(Array(r.input(ForgeDirection.getOrientation(side)).asInstanceOf[Any])) case Some(t: TileEntity) => val face = ForgeDirection.getOrientation(side.toInt) @@ -31,7 +31,7 @@ class RedstoneCard extends ItemComponent { case _ => None // Can't work with this node. } else None - private def output(target: Int, side: Int) = if (side >= 0 && side < 6) tryGet(target) match { + private def output(target: String, side: Int) = if (side >= 0 && side < 6) tryGet(target) match { case Some(r: RedstoneEnabled) => Some(Array(r.output(ForgeDirection.getOrientation(side)).asInstanceOf[Any])) case Some(t: TileEntity) => val power = t.worldObj.isBlockProvidingPowerTo(t.xCoord, t.yCoord, t.zCoord, side.toInt) @@ -39,7 +39,7 @@ class RedstoneCard extends ItemComponent { case _ => None // Can't work with this node. } else None - private def output(target: Int, side: Int, value: Int) = if (side >= 0 && side < 6) tryGet(target) match { + private def output(target: String, side: Int, value: Int) = if (side >= 0 && side < 6) tryGet(target) match { case Some(r: RedstoneEnabled) => r.output(ForgeDirection.getOrientation(side)) = value case _ => // Can't work with this node. } diff --git a/li/cil/oc/server/network/Network.scala b/li/cil/oc/server/network/Network.scala index 7d29e6373..841349b71 100644 --- a/li/cil/oc/server/network/Network.scala +++ b/li/cil/oc/server/network/Network.scala @@ -1,52 +1,39 @@ package li.cil.oc.server.network -import _root_.net.minecraft.block.Block -import _root_.net.minecraft.tileentity.TileEntity -import _root_.net.minecraft.world.{IBlockAccess, World} -import _root_.net.minecraftforge.common.ForgeDirection -import _root_.net.minecraftforge.event.ForgeSubscribe -import _root_.net.minecraftforge.event.world.ChunkEvent import java.util.logging.Level import li.cil.oc.api.network.Visibility -import li.cil.oc.api.{network => net} -import li.cil.oc.server.network.Network.Node import li.cil.oc.{api, OpenComputers} +import net.minecraft.block.Block +import net.minecraft.tileentity.TileEntity +import net.minecraft.world.{IBlockAccess, World} +import net.minecraftforge.common.ForgeDirection +import net.minecraftforge.event.ForgeSubscribe +import net.minecraftforge.event.world.ChunkEvent import scala.beans.BeanProperty import scala.collection.JavaConverters._ import scala.collection.mutable import scala.collection.mutable.ArrayBuffer -/** - * Network implementation for component networks. - * - * This network interconnects components in a geometry-agnostic fashion. It - * builds an internal graph of network nodes and the connections between them, - * and takes care of merges when adding connections, as well as net splits on - * node removal. - * - * It keeps the list of nodes as a lookup table for fast id->node resolving. - * Note that it is possible for multiple nodes to have the same ID, though. - */ -class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network.Node]]) extends api.Network { - def this(node: net.Node) = { - this(mutable.Map({ - if (node.address < 1) - node.address = 1 - node.address -> ArrayBuffer(new Network.Node(node)) - })) - send(new Network.ConnectMessage(node), List(node)) +class Network private(private val addressedNodes: mutable.Map[String, Network.Node] = mutable.Map.empty, + private val unaddressedNodes: mutable.ArrayBuffer[Network.Node] = mutable.ArrayBuffer.empty) extends api.Network { + def this(node: api.network.Node) = { + this() + addNew(node) + send(Network.ConnectMessage(node), Iterable(node)) } nodes.foreach(_.network = Some(this)) - def connect(nodeA: net.Node, nodeB: net.Node) = { - val containsA = nodeMap.get(nodeA.address).exists(_.exists(_.data == nodeA)) - val containsB = nodeMap.get(nodeB.address).exists(_.exists(_.data == nodeB)) + def connect(nodeA: api.network.Node, nodeB: api.network.Node) = { + val containsA = contains(nodeA) + val containsB = contains(nodeB) + if (!containsA && !containsB) throw new IllegalArgumentException( "At least one of the nodes must already be in this network.") - def oldNodeA = nodeMap(nodeA.address).find(_.data == nodeA).get - def oldNodeB = nodeMap(nodeB.address).find(_.data == nodeB).get + def oldNodeA = node(nodeA) + def oldNodeB = node(nodeB) + if (containsA && containsB) { // Both nodes already exist in the network but there is a new connection. // This can happen if a new node sequentially connects to multiple nodes @@ -58,68 +45,169 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. Network.Edge(oldNodeA, oldNodeB) true } - // That connection already exists. - else false + else false // That connection already exists. } - // New node for this network, order the nodes and add the new one. else if (containsA) add(oldNodeA, nodeB) else add(oldNodeB, nodeA) } - private def add(oldNode: Network.Node, addedNode: net.Node) = { + def disconnect(nodeA: api.network.Node, nodeB: api.network.Node) = { + val containsA = contains(nodeA) + val containsB = contains(nodeB) + + if (!containsA || !containsB) throw new IllegalArgumentException( + "Both of the nodes must be in this network.") + + def oldNodeA = node(nodeA) + def oldNodeB = node(nodeB) + + oldNodeA.edges.find(_.isBetween(oldNodeA, oldNodeB)) match { + case None => false // That connection doesn't exists. + case Some(edge) => { + handleSplit(edge.remove()) + if (edge.left.data.visibility == Visibility.Neighbors) + send(Network.DisconnectMessage(edge.left.data), Iterable(edge.right.data)) + if (edge.right.data.visibility == Visibility.Neighbors) + send(Network.DisconnectMessage(edge.right.data), Iterable(edge.left.data)) + true + } + } + } + + def remove(node: api.network.Node) = (node.address match { + case None => unaddressedNodes.indexWhere(_.data == node) match { + case -1 => None + case index => Some(unaddressedNodes.remove(index)) + } + case Some(address) => addressedNodes.remove(address) + }) match { + case None => false + case Some(entry) => { + node.network = None + val subGraphs = entry.remove() + val targets = Iterable(node) ++ (entry.data.visibility match { + case Visibility.None => Iterable.empty[api.network.Node] + case Visibility.Neighbors => entry.edges.map(_.other(entry).data) + case Visibility.Network => subGraphs.map { + case (addressed, unaddressed) => addressed.values.map(_.data) + }.flatten + }).filter(_.visibility != Visibility.None) + handleSplit(subGraphs) + send(Network.DisconnectMessage(node), targets) + true + } + } + + def node(address: String) = addressedNodes.get(address) match { + case Some(node) => Some(node.data) + case _ => None + } + + def nodes = addressedNodes.values.map(_.data).filter(_.visibility != Visibility.None) + + def nodes(reference: api.network.Node) = { + val referenceNeighbors = neighbors(reference).toSet + reference + nodes.filter(node => node.visibility == Visibility.Network || referenceNeighbors.contains(node)) + } + + def neighbors(node: api.network.Node) = (node.address match { + case None => + unaddressedNodes.find(_.data == node) match { + case None => throw new IllegalArgumentException("Node must be in this network.") + case Some(n) => n.edges.map(_.other(n).data) + } + case Some(address) => + addressedNodes.get(address) match { + case None => throw new IllegalArgumentException("Node must be in this network.") + case Some(n) => n.edges.map(_.other(n).data) + } + }).filter(_.visibility != Visibility.None) + + def sendToAddress(source: api.network.Node, target: String, name: String, data: Any*) = { + if (source.network.isEmpty || source.network.get != this) + throw new IllegalArgumentException("Source node must be in this network.") + if (source.address.isDefined) addressedNodes.get(target) match { + case Some(node) if node.data.visibility == Visibility.Network || + (node.data.visibility == Visibility.Neighbors && neighbors(node.data).exists(_ == source)) => + send(new Network.Message(source, name, Array(data: _*)), Iterable(node.data)) + case _ => None + } else None + } + + def sendToNeighbors(source: api.network.Node, name: String, data: Any*) = { + if (source.network.isEmpty || source.network.get != this) + throw new IllegalArgumentException("Source node must be in this network.") + if (source.address.isDefined) + send(new Network.Message(source, name, Array(data: _*)), neighbors(source)) + } + + def sendToVisible(source: api.network.Node, name: String, data: Any*) = { + if (source.network.isEmpty || source.network.get != this) + throw new IllegalArgumentException("Source node must be in this network.") + if (source.address.isDefined) + send(new Network.Message(source, name, Array(data: _*)), nodes(source)) + } + + private def contains(node: api.network.Node) = (node.address match { + case None => unaddressedNodes.find(_.data == node) + case Some(address) => addressedNodes.get(address) + }).exists(_.data == node) + + private def node(node: api.network.Node) = (node.address match { + case None => unaddressedNodes.find(_.data == node) + case Some(address) => addressedNodes.get(address) + }).get + + private def addNew(node: api.network.Node) = { + val newNode = new Network.Node(node) + if (node.address.isEmpty) + node.address = Some(java.util.UUID.randomUUID().toString) + if (node.address.isDefined) + addressedNodes += node.address.get -> newNode + else + unaddressedNodes += newNode + node.network = Some(this) + newNode + } + + private def add(oldNode: Network.Node, addedNode: api.network.Node) = { // Check if the other node is new or if we have to merge networks. - val (newNode, sendQueue) = if (addedNode.network.isEmpty) { - val newNode = new Network.Node(addedNode) - if (nodeMap.contains(addedNode.address) || addedNode.address < 1) - addedNode.address = findId() - // Store everything with an invalid address in slot zero. - val address = addedNode.address match { - case a if a > 0 => a - case _ => 0 + val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[api.network.Node])] + val (newNode) = if (addedNode.network.isEmpty) { + val newNode = addNew(addedNode) + if (addedNode.address.isDefined) addedNode.visibility match { + case Visibility.None => // Nothing to do. + case Visibility.Neighbors => + sendQueue += ((Network.ConnectMessage(addedNode), Iterable(addedNode) ++ neighbors(addedNode))) + nodes(addedNode).foreach(node => sendQueue += ((new Network.ConnectMessage(node), Iterable(addedNode)))) + case Visibility.Network => + sendQueue += ((Network.ConnectMessage(addedNode), nodes)) + nodes(addedNode).foreach(node => if (node != addedNode) + sendQueue += ((new Network.ConnectMessage(node), Iterable(addedNode)))) } - // 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[net.Node])] - 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 = Some(this) - (newNode, sendQueue) + newNode } else { // Queue any messages to avoid side effects from receivers. - val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[net.Node])] val thisNodes = nodes.toBuffer val otherNetwork = addedNode.network.get.asInstanceOf[Network] val otherNodes = otherNetwork.nodes.toBuffer otherNodes.foreach(node => sendQueue += ((Network.ConnectMessage(node), thisNodes))) thisNodes.foreach(node => sendQueue += ((Network.ConnectMessage(node), otherNodes))) - // 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 - node.address = findId(reserved) - if (node.address != oldAddress) { - reserved += node.address - // Prepend to notify old nodes of address changes first. - sendQueue.+=:((Network.ReconnectMessage(node, oldAddress), otherNodes)) - } - }) - - // 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 + // Add nodes from other network into this network. + otherNetwork.addressedNodes.values.foreach(node => { + addressedNodes += node.data.address.get -> node node.data.network = Some(this) }) + unaddressedNodes ++= otherNetwork.unaddressedNodes + otherNetwork.unaddressedNodes.foreach(_.data.network = Some(this)) // Return the node object of the newly connected node for the next step. - (nodeMap(addedNode.address).find(_.data == addedNode).get, sendQueue) + addedNode.address match { + case None => unaddressedNodes.find(_.data == addedNode).get + case Some(address) => addressedNodes(address) + } } // Add the connection between the two nodes. @@ -131,195 +219,66 @@ class Network private(private val nodeMap: mutable.Map[Int, ArrayBuffer[Network. true } - def reconnect(node: net.Node, address: Int): Boolean = { - if (!nodeMap.get(node.address).exists(_.exists(_.data == node))) throw new IllegalArgumentException( - "The node must already be in this network.") - - val oldAddress = node.address - if (address == oldAddress) false - else { - val otherMessage = if (address < 1) { - node.address = 0 - None + private def handleSplit(subGraphs: Seq[(mutable.Map[String, Network.Node], mutable.ArrayBuffer[Network.Node])]) = + if (subGraphs.size > 1) { + val subNodes = subGraphs.map { + case (addressed, unaddressed) => + (addressed.values ++ unaddressed).map(_.data).filter(_.visibility != Visibility.None) + } + val visibleNodes = subGraphs.map { + case (addressed, unaddressed) => + addressed.values.map(_.data).filter(_.visibility == Visibility.Network) } - else { - // Check if there's a simple collision, if so resolve it. - nodeMap.get(address) match { - case None => - // No collision. - node.address = address - None - case Some(otherList) => - if (otherList.size > 1) - return false // Already multiple nodes with that address... - else { - // Simple collision. - val other = otherList.head - otherList -= other - other.data.address = findId() - nodeMap.getOrElseUpdate(other.data.address, new mutable.ArrayBuffer[Node]) += other - Some((Network.ReconnectMessage(other.data, address), nodes)) - } + addressedNodes.clear() + unaddressedNodes.clear() + + subGraphs.head match { + case (addressed, unaddressed) => + addressedNodes ++= addressed + unaddressedNodes ++= unaddressed + } + + subGraphs.tail.foreach { + case (addressed, unaddressed) => + new Network(addressed, unaddressed) + } + + for (indexA <- 0 until visibleNodes.length) { + val nodesA = subNodes(indexA) + val visibleNodesA = visibleNodes(indexA) + for (indexB <- (indexA + 1) until visibleNodes.length) { + val nodesB = subNodes(indexB) + val visibleNodesB = visibleNodes(indexB) + visibleNodesA.foreach(nodeA => send(new Network.DisconnectMessage(nodeA), nodesB)) + visibleNodesB.foreach(nodeB => send(new Network.DisconnectMessage(nodeB), nodesA)) } } - - val oldList = nodeMap(oldAddress) - val innerNode = oldList.remove(oldList.indexWhere(_.data == node)) - if (oldList.isEmpty) - nodeMap -= oldAddress - nodeMap.getOrElseUpdate(node.address, new mutable.ArrayBuffer[Node]) += innerNode - - otherMessage.foreach { - case (message, targets) => send(message, targets) - } - send(Network.ReconnectMessage(node, oldAddress), nodes) - - true - } - } - - def disconnect(nodeA: net.Node, nodeB: net.Node) = { - val containsA = nodeMap.get(nodeA.address).exists(_.exists(_.data == nodeA)) - val containsB = nodeMap.get(nodeB.address).exists(_.exists(_.data == nodeB)) - if (!containsA || !containsB) throw new IllegalArgumentException( - "Both of the nodes must be in this network.") - - def oldNodeA = nodeMap(nodeA.address).find(_.data == nodeA).get - def oldNodeB = nodeMap(nodeB.address).find(_.data == nodeB).get - oldNodeA.edges.find(_.isBetween(oldNodeA, oldNodeB)) match { - case None => false // That connection doesn't exists. - case Some(edge) => { - handleSplit(edge.remove()) - true - } - } - } - - def remove(node: net.Node) = nodeMap.get(node.address) match { - case None => false - case Some(list) => list.find(_.data == node) match { - case None => false - case Some(entry) => { - node.network = None - - // Removing a node may result in a net split, leaving us with multiple - // networks. The remove function returns all resulting networks, one - // 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 => send(new Network.DisconnectMessage(n), List(node))) - send(new Network.DisconnectMessage(node), nodes) - }) - send(new Network.DisconnectMessage(node), List(node)) - true - } - } - } - - private def handleSplit(subGraphs: Seq[mutable.Map[Int, ArrayBuffer[Network.Node]]], - messageCallback: Iterable[net.Node] => Unit = _ => {}) = { - // Sending the removal messages can have side effects, so we'll keep a - // copy of the original list of nodes in each sub network. - val subNodes = subGraphs.map(_.values.flatten.map(_.data).toBuffer).toBuffer - - // We re-use this network by assigning the first sub graph to it. For - // all additional sub graphs (if any) we'll have to create new ones. - nodeMap.clear() - // Empty for the last node removed from a network. - if (!subGraphs.isEmpty) { - nodeMap ++= subGraphs.head - subGraphs.tail.foreach(new Network(_)) } - // Send removal messages. First, to the removed node itself (for its - // onDisconnect handler), then one for the removed node to all sub - // networks, for each node in the sub networks back to the removed node - // and if there was a net split (we have multiple networks) also for - // each node now longer belonging to one of the resulting sub networks. - for (indexA <- 0 until subNodes.length) { - val nodesA = subNodes(indexA) - for (indexB <- (indexA + 1) until subNodes.length) { - val nodesB = subNodes(indexB) - 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 Some(list) if address > 0 => list.map(_.data).filter(_.visibility != Visibility.None).lastOption - case _ => None - } - - def nodes(reference: net.Node) = { - 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: net.Node) = nodeMap.get(node.address) match { - case None => throw new IllegalArgumentException("Node must be in this network.") - case Some(list) => list.find(_.data == node) match { - case None => throw new IllegalArgumentException("Node must be in this network.") - case Some(n) => n.edges.map(_.other(n).data) - } - } - - def sendToAddress(source: net.Node, target: Int, name: String, data: Any*) = - nodeMap.get(target) match { - case None => None - case Some(list) => send(new Network.Message(source, name, Array(data: _*)), list.map(_.data)) + private def send(message: Network.Message, targets: Iterable[api.network.Node]) = { + def protectedSend(target: api.network.Node) = try { + //println("receive(" + message.name + "(" + message.data.mkString(", ") + "): " + message.source.address.get + ":" + message.source.name + " -> " + target.address.get + ":" + target.name + ")") + target.receive(message) + } catch { + case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e); None } - def sendToNeighbors(source: net.Node, name: String, data: Any*) = - send(new Network.Message(source, name, Array(data: _*)), neighbors(source)) - - def sendToAll(source: net.Node, name: String, data: Any*) = - send(new Network.Message(source, name, Array(data: _*)), nodes) - - private def send(message: Network.Message, targets: Iterable[net.Node]) = - if (message.source.address > 0 && message.source.visibility != Visibility.None) { - def protectedSend(target: net.Node) = try { - //println("receive(" + message.name + "(" + message.data.mkString(", ") + "): " + message.source.address + ":" + message.source.name + " -> " + target.address + ":" + target.name + ")") - target.receive(message) - } catch { - case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e); None - } - - message match { - case _@(Network.ConnectMessage(_) | Network.ReconnectMessage(_, _)) => - // Cannot be canceled but respects visibility. - (message.source.visibility match { - case Visibility.Neighbors => - // Note: the neighbors() call already filters out invalid nodes. - val neighborSet = neighbors(message.source).toSet - targets.filter(target => target == message.source || neighborSet.contains(target)) - case Visibility.Network => - targets.filter(_.address > 0).filter(_.visibility == Visibility.Network) - }).foreach(protectedSend) - None - case _@Network.DisconnectMessage(_) => - // Cannot be canceled but ignores visibility (because it'd be a pain to implement this otherwise). - targets.filter(_.address > 0).foreach(protectedSend) - 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) - protectedSend(iterator.next()) match { - case None => // Ignore. - case r => result = r - } - 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 + message match { + case _@(Network.ConnectMessage(_) | Network.DisconnectMessage(_)) => + targets.foreach(protectedSend) + None + case _ => + var result = None: Option[Array[Any]] + val iterator = targets.iterator + while (!message.isCanceled && iterator.hasNext) + protectedSend(iterator.next()) match { + case None => // Ignore. + case r => result = r + } + result + } + } } object Network extends api.detail.NetworkAPI { @@ -334,8 +293,8 @@ object Network extends api.detail.NetworkAPI { private def onUnload(w: World, tileEntities: Iterable[TileEntity]) = if (!w.isRemote) { // TODO add a more efficient batch remove operation? something along the lines of if #remove > #nodes*factor remove all, re-add remaining? tileEntities. - filter(_.isInstanceOf[net.Node]). - map(_.asInstanceOf[net.Node]). + filter(_.isInstanceOf[api.network.Node]). + map(_.asInstanceOf[api.network.Node]). foreach(t => t.network.foreach(_.remove(t))) } @@ -360,17 +319,17 @@ object Network extends api.detail.NetworkAPI { } } - private def getNetworkNode(world: IBlockAccess, x: Int, y: Int, z: Int): Option[TileEntity with net.Node] = + private def getNetworkNode(world: IBlockAccess, x: Int, y: Int, z: Int): Option[TileEntity with api.network.Node] = Option(Block.blocksList(world.getBlockId(x, y, z))) match { case Some(block) if block.hasTileEntity(world.getBlockMetadata(x, y, z)) => world.getBlockTileEntity(x, y, z) match { - case tileEntity: TileEntity with net.Node => Some(tileEntity) + case tileEntity: TileEntity with api.network.Node => Some(tileEntity) case _ => None } case _ => None } - private class Node(val data: net.Node) { + private class Node(val data: api.network.Node) { val edges = ArrayBuffer.empty[Edge] def remove() = { @@ -397,41 +356,35 @@ object Network extends api.detail.NetworkAPI { private def searchGraphs(seeds: Seq[Node]) = { val seen = mutable.Set.empty[Node] seeds.map(seed => { - if (seen.contains(seed)) { - // If our seed node is contained in another sub graph we have nothing - // to do, since we're a sub graph of that sub graph. - mutable.Map.empty[Int, mutable.ArrayBuffer[Node]] - } + if (seen.contains(seed)) None else { - // Not yet processed, start growing a network from here. We're - // guaranteed to not find previously processed nodes, since edges - // are bidirectional, and we'd be in the other branch otherwise. - seen += seed - val subGraph = mutable.Map(seed.data.address -> mutable.ArrayBuffer(seed)) - val queue = mutable.Queue(seed.edges.map(_.other(seed)): _*) + val addressed = mutable.Map.empty[String, Node] + val unaddressed = mutable.ArrayBuffer.empty[Node] + val queue = mutable.Queue(seed) while (queue.nonEmpty) { val node = queue.dequeue() seen += node - subGraph.getOrElseUpdate(node.data.address, new ArrayBuffer[Node]) += node + node.data.address match { + case None => unaddressed += node + case Some(address) => addressed += address -> node + } queue ++= node.edges.map(_.other(node)).filter(n => !seen.contains(n) && !queue.contains(n)) } - subGraph + Some((addressed, unaddressed)) } - }) filter (_.nonEmpty) + }) filter (_.nonEmpty) map (_.get) } - private class Message(@BeanProperty val source: net.Node, + private class Message(@BeanProperty val source: api.network.Node, @BeanProperty val name: String, - @BeanProperty val data: Array[Any] = Array()) extends net.Message { + @BeanProperty val data: Array[Any] = Array()) extends api.network.Message { var isCanceled = false def cancel() = isCanceled = true } - private case class ConnectMessage(override val source: net.Node) extends Message(source, "network.connect") + private case class ConnectMessage(override val source: api.network.Node) extends Message(source, "network.connect") - private case class DisconnectMessage(override val source: net.Node) extends Message(source, "network.disconnect") - - private case class ReconnectMessage(override val source: net.Node, oldAddress: Int) extends Message(source, "network.reconnect", Array(oldAddress.asInstanceOf[Any])) + private case class DisconnectMessage(override val source: api.network.Node) extends Message(source, "network.disconnect") } \ No newline at end of file