blob: 5dcbd5be3610753de44f3536175fc0c7ad81d132 [file] [log] [blame] [raw]
package li.cil.oc.server.computer
import java.util.logging.Level
import li.cil.oc.OpenComputers
import li.cil.oc.api.INetwork
import li.cil.oc.api.INetworkMessage
import li.cil.oc.api.INetworkNode
import net.minecraft.block.Block
import net.minecraft.tileentity.TileEntity
import net.minecraft.world.IBlockAccess
import net.minecraftforge.common.ForgeDirection
import scala.beans.BeanProperty
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 INetwork {
def this(node: INetworkNode) = {
this(mutable.Map({
node.address = 1
node.address -> ArrayBuffer(new Network.Node(node))
}))
Network.send(new Network.ConnectMessage(node), List(node))
}
nodes.foreach(_.network = this)
def connect(nodeA: INetworkNode, nodeB: INetworkNode) = {
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(
"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
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
// in an existing network, e.g. in a setup like so:
// O O Where O is an old node, and N is the new Node. It would connect
// O N to the node above and left to it (in no particular order).
if (!oldNodeA.edges.exists(_.isBetween(oldNodeA, oldNodeB))) {
assert(!oldNodeB.edges.exists(_.isBetween(oldNodeA, oldNodeB)))
Network.Edge(oldNodeA, oldNodeB)
true
}
// That connection already exists.
else false
}
// 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: INetworkNode) = {
// Check if the other node is new or if we have to merge networks.
val (newNode, sendQueue) = if (addedNode.network == null) {
val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[INetworkNode])]
sendQueue += ((new Network.ConnectMessage(addedNode), List(addedNode) ++ nodes))
nodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), List(addedNode))))
val newNode = new Network.Node(addedNode)
if (nodeMap.contains(addedNode.address) || addedNode.address < 1)
addedNode.address = findId() // Assign address first since it may be ignored.
nodeMap.getOrElseUpdate(addedNode.address, new ArrayBuffer[Network.Node]) += newNode
addedNode.network = this
(newNode, sendQueue)
}
else {
// Queue any messages to avoid side effects from receivers.
val sendQueue = mutable.Buffer.empty[(Network.Message, Iterable[INetworkNode])]
val thisNodes = nodes.toBuffer
val otherNetwork = addedNode.network.asInstanceOf[Network]
val otherNodes = otherNetwork.nodes.toBuffer
otherNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), thisNodes)))
thisNodes.foreach(node => sendQueue += ((new Network.ConnectMessage(node), otherNodes)))
// Change addresses for conflicting nodes in other network.
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.+=:((new Network.ReconnectMessage(node, oldAddress), otherNodes))
}
})
// Add nodes from other network into this network.
otherNetwork.nodeMap.values.flatten.foreach(node => {
nodeMap.getOrElseUpdate(node.data.address, new ArrayBuffer[Network.Node]) += node
node.data.network = this
})
// Return the node object of the newly connected node for the next step.
(nodeMap(addedNode.address).find(_.data == addedNode).get, sendQueue)
}
// Add the connection between the two nodes.
Network.Edge(oldNode, newNode)
// Send all generated messages.
for ((message, nodes) <- sendQueue) Network.send(message, nodes)
true
}
def disconnect(nodeA: INetworkNode, nodeB: INetworkNode) = {
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: INetworkNode) = nodeMap.get(node.address) match {
case None => false
case Some(list) => list.find(_.data == node) match {
case None => false
case Some(entry) => {
node.network = null
// 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 => Network.send(new Network.DisconnectMessage(n), List(node)))
Network.send(new Network.DisconnectMessage(node), nodes)
})
Network.send(new Network.DisconnectMessage(node), List(node))
true
}
}
}
private def handleSplit(subGraphs: Seq[mutable.Map[Int, ArrayBuffer[Network.Node]]],
messageCallback: Iterable[INetworkNode] => 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 => Network.send(new Network.DisconnectMessage(nodeA), nodesB))
nodesB.foreach(nodeB => Network.send(new Network.DisconnectMessage(nodeB), nodesA))
}
messageCallback(nodesA)
}
}
def node(address: Int) = nodeMap.get(address) match {
case None => None
case Some(list) => Some(list.last.data)
}
def nodes = nodeMap.values.flatten.map(_.data)
def sendToNode(source: INetworkNode, target: Int, name: String, data: Any*) =
nodeMap.get(target) match {
case None => None
case Some(list) => Network.send(new Network.Message(source, name, Array(data: _*)), list.map(_.data))
}
def sendToAll(source: INetworkNode, name: String, data: Any*) =
Network.send(new Network.Message(source, name, Array(data: _*)), nodes)
private def findId() = Range(1, Int.MaxValue).find(!nodeMap.contains(_)).get
private def findId(reserved: collection.Set[Int]) = Range(1, Int.MaxValue).find(
address => !nodeMap.contains(address) && !reserved.contains(address)).get
}
object Network {
def joinOrCreateNetwork(world: IBlockAccess, x: Int, y: Int, z: Int): Unit =
getNetworkNode(world, x, y, z) match {
case None => // Invalid block.
case Some(node) => {
for (side <- ForgeDirection.VALID_DIRECTIONS) {
getNetworkNode(world, x + side.offsetX, y + side.offsetY, z + side.offsetZ) match {
case None => // Ignore.
case Some(neighborNode) =>
if (neighborNode != null && neighborNode.network != null) {
neighborNode.network.connect(neighborNode, node)
}
}
}
if (node.network == null) new Network(node)
}
}
private def getNetworkNode(world: IBlockAccess, x: Int, y: Int, z: Int): Option[TileEntity with INetworkNode] =
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 INetworkNode => Some(tileEntity)
case _ => None
}
case _ => None
}
private def send(message: Network.Message, nodes: Iterable[INetworkNode]) = {
//println("send(" + message.name + "(" + message.data.mkString(", ") + "): " + message.source.address + " -> [" + nodes.map(_.address).mkString(", ") + "])")
val iterator = nodes.iterator
var result = None: Option[Array[Any]]
while (!message.isCanceled && iterator.hasNext) {
try {
iterator.next().receive(message) match {
case None => // Ignore.
case r => result = r
}
} catch {
case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e)
}
}
result
}
private class Node(val data: INetworkNode) {
val edges = ArrayBuffer.empty[Edge]
def remove() = {
val edgesCopy = edges.toBuffer
edges.foreach(edge => edge.other(this).edges -= edge)
edges.clear()
edgesCopy.map(_.remove().filter(_.values.head.head != this)).flatten
}
}
private case class Edge(left: Node, right: Node) {
left.edges += this
right.edges += this
def other(side: Node) = if (side == left) right else left
def isBetween(a: Node, b: Node) = (a == left && b == right) || (b == left && a == right)
def remove() = {
left.edges -= this
right.edges -= this
// Build neighbor graphs to see if our removal resulted in a split.
val subGraphs = List(
(mutable.Map(left.data.address -> ArrayBuffer(left)), mutable.Queue(left.edges.map(_.other(left)): _*)),
(mutable.Map(right.data.address -> ArrayBuffer(right)), mutable.Queue(right.edges.map(_.other(right)): _*)))
// Breadth-first search to make early merges more likely.
while (!subGraphs.forall {
case (_, queue) => queue.isEmpty
}) for (subGraph <- subGraphs.filter {
case (_, queue) => !queue.isEmpty
}) {
val (nodes, queue) = subGraph
val node = queue.dequeue()
// See if the node is already in some other graph, in which case we
// merge this graph into the other graph.
if (!subGraphs.filter(_ != subGraph).exists {
case (otherNodes, otherQueue) => otherNodes.get(node.data.address) match {
case Some(list) if list.contains(node) => {
otherNodes ++= nodes
otherQueue ++= queue
nodes.clear()
queue.clear()
true
}
case _ => false
}
}) {
nodes.getOrElseUpdate(node.data.address, new ArrayBuffer[Network.Node]) += node
queue ++= node.edges.map(_.other(node)).filter(n => !nodes.get(n.data.address).exists(_.contains(n)))
}
}
subGraphs map (_._1) filter (!_.isEmpty)
}
}
private class Message(@BeanProperty val source: INetworkNode,
@BeanProperty val name: String,
@BeanProperty val data: Array[Any] = Array()) extends INetworkMessage {
var isCanceled = false
def cancel() = isCanceled = true
}
private class ConnectMessage(source: INetworkNode) extends Message(source, "network.connect")
private class DisconnectMessage(source: INetworkNode) extends Message(source, "network.disconnect")
private class ReconnectMessage(source: INetworkNode, oldAddress: Int) extends Message(source, "network.reconnect", Array(oldAddress.asInstanceOf[Any]))
}