blob: b11ab0353a1319bd2e691f91f0c1b969a37c3863 [file] [log] [blame] [raw]
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, OpenComputers}
import li.cil.oc.api.network.Visibility
import li.cil.oc.api.{network => net}
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))
}
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))
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: net.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
}
// 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)
}
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 += ((new Network.ConnectMessage(node), thisNodes)))
thisNodes.foreach(node => sendQueue += ((new 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.+=:((new 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
node.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)
}
// Add the connection between the two nodes.
Network.Edge(oldNode, newNode)
// Send all generated messages.
for ((message, nodes) <- sendQueue) send(message, 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))
}
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 debug(target: net.Node) = {} // println("receive(" + message.name + "(" + message.data.mkString(", ") + "): " + message.source.address + ":" + message.source.name + " -> " + target.address + ":" + target.name + ")")
message match {
case _@(Network.ConnectMessage(_) | Network.ReconnectMessage(_, _)) =>
// Cannot be canceled but respects visibility.
message.source.visibility match {
case Visibility.Neighbors =>
val neighborSet = neighbors(message.source).toSet
val iterator = targets.filter(target => target == message.source || neighborSet.contains(target)).iterator
while (iterator.hasNext) try {
val target = iterator.next()
debug(target)
target.receive(message)
} catch {
case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e)
}
case Visibility.Network =>
val iterator = targets.filter(_.address > 0).filter(_.visibility == Visibility.Network).iterator
while (iterator.hasNext) try {
val target = iterator.next()
debug(target)
target.receive(message)
} catch {
case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e)
}
}
None
case _@Network.DisconnectMessage(_) =>
// Cannot be canceled but ignores visibility (because it'd be a pain to implement this otherwise).
val iterator = targets.filter(_.address > 0).iterator
while (iterator.hasNext) try {
val target = iterator.next()
debug(target)
target.receive(message)
} catch {
case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e)
}
None
case _ =>
// Can be canceled but ignores visibility.
var result = None: Option[Array[Any]]
val iterator = targets.filter(_.address > 0).iterator
while (!message.isCanceled && iterator.hasNext) try {
val target = iterator.next()
debug(target)
target.receive(message) match {
case None => // Ignore.
case r => result = r
}
} catch {
case e: Throwable => OpenComputers.log.log(Level.WARNING, "Error in message handler", e)
}
result
}
} else None
private def findId(reserved: collection.Set[Int] = collection.Set.empty[Int]) = Range(1, Int.MaxValue).find(
address => !nodeMap.contains(address) && !reserved.contains(address)).get
}
object Network {
@ForgeSubscribe
def onChunkUnload(e: ChunkEvent.Unload) =
onUnload(e.world, e.getChunk.chunkTileEntityMap.values.asScala.map(_.asInstanceOf[TileEntity]))
@ForgeSubscribe
def onChunkLoad(e: ChunkEvent.Load) =
onLoad(e.world, e.getChunk.chunkTileEntityMap.values.asScala.map(_.asInstanceOf[TileEntity]))
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]).
foreach(t => t.network.foreach(_.remove(t)))
}
private def onLoad(w: World, tileEntities: Iterable[TileEntity]) = if (!w.isRemote) {
tileEntities.foreach(t => joinOrCreateNetwork(w, t.xCoord, t.yCoord, t.zCoord))
}
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.network.isDefined) {
neighborNode.network.foreach(_.connect(neighborNode, node))
}
}
}
if (node.network.isEmpty) new Network(node)
}
}
private def getNetworkNode(world: IBlockAccess, x: Int, y: Int, z: Int): Option[TileEntity with net.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 _ => None
}
case _ => None
}
private class Node(val data: net.Node) {
val edges = ArrayBuffer.empty[Edge]
def remove() = {
edges.foreach(edge => edge.other(this).edges -= edge)
searchGraphs(edges.map(_.other(this)))
}
}
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
searchGraphs(List(left, right))
}
}
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]]
}
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)): _*)
while (queue.nonEmpty) {
val node = queue.dequeue()
seen += node
subGraph.getOrElseUpdate(node.data.address, new ArrayBuffer[Node]) += node
queue ++= node.edges.map(_.other(node)).filter(n => !seen.contains(n) && !queue.contains(n))
}
subGraph
}
}) filter (_.nonEmpty)
}
private class Message(@BeanProperty val source: net.Node,
@BeanProperty val name: String,
@BeanProperty val data: Array[Any] = Array()) extends net.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 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]))
}