blob: 68227aa505bac91328d914c1b63c1004e45d2fd1 [file] [log] [blame] [raw]
package li.cil.oc.server.computer
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 nodes: 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))
}))
}
/** Do not allow modification of the network while it's updating. */
private var locked = false
nodes.values.flatten.foreach(_.data.network = this)
def connect(nodeA: INetworkNode, nodeB: INetworkNode) = try {
if (locked) throw new IllegalStateException(
"Cannot modify network while it is already updating its structure.")
locked = true
val containsA = nodes.get(nodeA.address).exists(_.exists(_.data == nodeA))
val containsB = nodes.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.")
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).
val oldNodeA = nodes(nodeA.address).find(_.data == nodeA).get
val oldNodeB = nodes(nodeB.address).find(_.data == nodeB).get
if (!oldNodeA.edges.exists(edge => edge.other(oldNodeA) == oldNodeB)) {
val edge = new Network.Edge(oldNodeA, oldNodeB)
oldNodeA.edges += edge
oldNodeB.edges += edge
true
}
// That connection already exists.
else false
}
// New node for this network, order the nodes and add the new one.
else if (containsA) add(nodes(nodeA.address).find(_.data == nodeA).get, nodeB)
else add(nodes(nodeB.address).find(_.data == nodeB).get, nodeA)
}
finally {
locked = false
}
def remove(node: INetworkNode) = nodes.get(node.address) match {
case None => false
case Some(list) => list.find(_.data == node) match {
case None => false
case Some(entry) => if (list.contains(entry)) {
if ((list -= entry).isEmpty)
nodes -= node.address
node.network = null
entry.remove().foreach(_.sendToAll(node, "network.disconnect"))
sendToAll(node, "network.disconnect")
true
}
else false
}
}
def node(address: Int) = nodes.get(address) match {
case None => None
case Some(list) => Some(list.last.data)
}
def sendToNode(source: INetworkNode, target: Int, name: String, data: Any*) =
nodes.get(target) match {
case None => None
case Some(list) => send(new Network.Message(source, name, Array(data: _*)), list.map(_.data).iterator)
}
def sendToAll(source: INetworkNode, name: String, data: Any*) =
send(new Network.Message(source, name, Array(data: _*)), nodes.values.flatten.map(_.data).iterator)
private def send(message: Network.Message, nodes: Iterator[INetworkNode]) = {
var result = None: Option[Array[Any]]
while (!message.isCanceled && nodes.hasNext) {
nodes.next().receive(message) match {
case None => // Ignore.
case r => result = r
}
}
result
}
private def add(oldNode: Network.Node, node: INetworkNode) = {
// The node is new to this network, check if we have to merge networks.
val newNode = if (node.network == null) {
// Other node is not yet in a network, create internal node and add it
// to our lookup table of internal nodes.
val newNode = new Network.Node(node)
node.address = findId()
nodes.getOrElseUpdate(node.address, new ArrayBuffer[Network.Node]) += newNode
node.network = this
sendToAll(node, "network.connect")
newNode
}
else {
val otherNetwork = node.network.asInstanceOf[Network]
// We have to merge. First create a copy of the old nodes to have the
// list of nodes to which to send "network.connect" messages.
val oldNodes = nodes.values.flatten.map(_.data).toArray
// Then get the list of nodes in the other network. This is, among the
// iteration to merge into this network, used to send "network.reconnect"
// messages to old nodes in case we have to change a node's address to
// ensure unique addresses in the merged network.
val otherNodes = otherNetwork.nodes.values.flatten.map(_.data)
// Pre-merge step: ensure addresses are unique.
for (node <- otherNodes if nodes.contains(node.address)) {
val oldAddress = node.address
node.address = findId(otherNetwork)
if (node.address != oldAddress) {
// If we successfully changed the address send message.
send(new Network.Message(node, "network.reconnect", Array(int2Integer(oldAddress))), otherNodes.iterator)
}
}
// Merge step: add nodes from other network into this network.
for (node <- otherNetwork.nodes.values.flatten) {
nodes.getOrElseUpdate(node.data.address, new ArrayBuffer[Network.Node]) += node
node.data.network = this
send(new Network.Message(node.data, "network.connect"), oldNodes.iterator)
}
// Return the node object of the newly connected node for the next step.
nodes(node.address).find(_.data == node).get
}
// Either way, add the connection between the two nodes.
val edge = new Network.Edge(oldNode, newNode)
oldNode.edges += edge
newNode.edges += edge
true
}
private def findId() = Range(1, Int.MaxValue).find(!nodes.contains(_)).get
private def findId(other: Network) = Range(1, Int.MaxValue).find(
address => !nodes.contains(address) && !other.nodes.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 class Node(val data: INetworkNode) {
val edges = ArrayBuffer.empty[Edge]
def remove() = {
// Remove self from neighbors.
for (edge <- edges) {
edge.other(this).edges -= edge
}
// Build neighbor graphs to see if our removal resulted in a split.
val subGraphs = mutable.MutableList.empty[(mutable.Map[Int, ArrayBuffer[Node]], mutable.Queue[Node])]
for (edge <- edges) {
val other = edge.other(this)
subGraphs += ((mutable.Map(other.data.address -> ArrayBuffer(other)), mutable.Queue(other.edges.map(_.other(other)): _*)))
}
// 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(otherSubGraph => {
val (otherNodes, _) = otherSubGraph
otherNodes.get(node.data.address) match {
case Some(list) if list.contains(node) => {
// Merge.
otherNodes ++= nodes
nodes.clear()
queue.clear()
true
}
case _ => false
}
})) {
// Not in any other graph yet.
nodes.getOrElseUpdate(node.data.address, new ArrayBuffer[Network.Node]) += node
// Add nodes this node is connected to to the queue if they're not
// already in this graph.
queue ++= node.edges.map(_.other(node)).
filter(node => !nodes.get(node.data.address).exists(_.contains(node)))
}
}
}
// Create new sub networks for separated sub-networks. Skip the first one
// to re-use the originating network and avoid re-creation if there is no
// split at all.
subGraphs map (_._1) filter (!_.isEmpty) drop 1 map (new Network(_))
}
}
private class Edge(val left: Node, val right: Node) {
def other(side: Node) = if (side == left) right else left
}
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
}
}