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)

This commit is contained in:
Florian Nücke 2013-10-01 22:10:54 +02:00
parent 06eac9254c
commit 59febb056f
15 changed files with 466 additions and 569 deletions

View File

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

View File

@ -1,4 +1,3 @@
--[[ API for keyboards. ]]
driver.keyboard = {}
driver.keyboard.keys = {

View File

@ -1,6 +1,5 @@
--[[ API for Redstone Cards. ]]
driver.redstone = {}
driver.rs = driver.redstone
driver.redstone.sides = {"top", "bottom", "left", "right", "front", "back"}

View File

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

View File

@ -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`.
* <p/>
* Note that for network nodes implemented in <tt>TileEntities</tt> 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
* <tt>Network.joinOrCreateNetwork</tt> in <tt>onBlockAdded</tt> and
* <tt>Network.remove</tt> in <tt>breakBlock</tt>.
* `Network.joinOrCreateNetwork` in `onBlockAdded` and `Network.remove` in
* `breakBlock`.
* <p/>
* All other kinds of nodes have to be managed manually. See `Node`.
* <p/>
* There are a couple of system messages to be aware of. These are all sent by
* the network manager itself:
* <ul>
* <li><tt>network.connect</tt> is generated when a node is added to the
* network, with the added node as the sender.</li>
* <li><tt>network.disconnect</tt> is generated when a node is removed from the
* network, with the removed node as the sender.</li>
* <li><tt>network.reconnect</tt> is generated when a node's address changes,
* usually due to a network merge, with the node whose address changed as the
* sender and the old address as the only parameter.</li>
* <li>`network.connect` is generated when a node is added to the network,
* with the added node as the sender. This will also be sent to the nodes of
* the other network, when a network merges with another one (both ways).</li>
* <li>`network.disconnect` is generated when a node is removed from the
* network, with the removed node as the sender. This will also be sent to the
* nodes of the other network(s), when a network is split (all pairs).</li>
* </ul>
* <p/>
* IMPORTANT: do <em>not</em> 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.
* <p/>
@ -66,26 +65,10 @@ trait Network {
*/
def connect(nodeA: Node, nodeB: Node): Boolean
/**
* Changes the address of a node.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* Both nodes must be part of the same network.
* Both nodes must be part of this network.
* <p/>
* 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.
* <p/>
* 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.
* <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.
* 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.
* <p/>
* 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.
* <p/>
* 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`.
* <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 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.
* <p/>
* 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`.
* <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.
@ -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.
* <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
* 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.
* <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
* 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`.
* <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
@ -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.
* <p/>
* 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`.
* <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
* 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.
* <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.
* Targets are determined using `nodes(source)`.
* <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
* 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.

View File

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

View File

@ -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`.
* <p/>
* All nodes in a network <em>should</em> 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 <em>must</em> 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* Use this for custom initialization logic.
*/
protected def onConnect() {}
/**
* Called when this node is removed from a network.
* <p/>
* 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() {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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