Compare commits
No commits in common. "655a9c541ef7a0eabecf07f398f07fb6c0cbf2a4" and "436456dcc53bbc89e3fd4060e6b6b21fd85934cf" have entirely different histories.
655a9c541e
...
436456dcc5
@ -31,31 +31,11 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CacheEntry {
|
|
||||||
val paths = ArrayList<SegmentPath>(1)
|
|
||||||
var saturated = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: LRU cache?
|
// TODO: LRU cache?
|
||||||
private val pathCache = HashMap<Pair<EnergyCableBlockEntity.Node, EnergyCableBlockEntity.Node>, CacheEntry>()
|
private val pathCache = HashMap<Pair<EnergyCableBlockEntity.Node, EnergyCableBlockEntity.Node>, ArrayList<SegmentPath?>>()
|
||||||
|
|
||||||
private class SearchNode(
|
private class SearchNode(val node: EnergyCableBlockEntity.Node, target: EnergyCableBlockEntity.Node, var parent: SearchNode? = null) : Comparable<SearchNode> {
|
||||||
val node: EnergyCableBlockEntity.Node,
|
val heuristics: Double = node.position.distSqr(target.position) * 0.0001 - ln(node.segment.availableThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble())
|
||||||
target: EnergyCableBlockEntity.Node,
|
|
||||||
var parent: SearchNode? = null,
|
|
||||||
previouslySeen: Boolean
|
|
||||||
) : Comparable<SearchNode> {
|
|
||||||
val heuristics: Double
|
|
||||||
|
|
||||||
init {
|
|
||||||
var heuristics = node.position.distSqr(target.position) * 0.0001
|
|
||||||
|
|
||||||
if (!previouslySeen) {
|
|
||||||
heuristics -= ln(node.segment.availableThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble())
|
|
||||||
}
|
|
||||||
|
|
||||||
this.heuristics = heuristics
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun compareTo(other: SearchNode): Int {
|
override fun compareTo(other: SearchNode): Int {
|
||||||
return heuristics.compareTo(other.heuristics)
|
return heuristics.compareTo(other.heuristics)
|
||||||
@ -71,18 +51,6 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
val availableThroughput: Decimal
|
val availableThroughput: Decimal
|
||||||
get() = segments.minOfOrNull { it.availableThroughput } ?: Decimal.ZERO
|
get() = segments.minOfOrNull { it.availableThroughput } ?: Decimal.ZERO
|
||||||
|
|
||||||
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
|
|
||||||
return segments.any { node in it }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
return this === other || other is SegmentPath && segments == other.segments
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return segments.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun transfer(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>): Decimal {
|
fun transfer(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>): Decimal {
|
||||||
if (!amount.isPositive || shortCircuit) {
|
if (!amount.isPositive || shortCircuit) {
|
||||||
return Decimal.ZERO
|
return Decimal.ZERO
|
||||||
@ -98,20 +66,17 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val min = if (simulate) segments.minOf { it.throughput(instantSnapshot) } else segments.minOf { it.availableThroughput }
|
var min = amount
|
||||||
|
|
||||||
if (min.isPositive) {
|
for (segment in segments) {
|
||||||
if (simulate)
|
min = minOf(segment.transfer(amount, instantSnapshot), min)
|
||||||
segments.forEach { it.transfer(min, instantSnapshot) }
|
if (!min.isPositive) return Decimal.ZERO
|
||||||
else
|
|
||||||
segments.forEach { it.transfer(min) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return min
|
if (!simulate)
|
||||||
}
|
segments.forEach { it.transfer(min) }
|
||||||
|
|
||||||
fun refund(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>) {
|
return min
|
||||||
segments.forEach { it.refund(amount, simulate, instantSnapshot) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove() {
|
fun remove() {
|
||||||
@ -128,25 +93,18 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
private val nodes = HashSet<EnergyCableBlockEntity.Node>()
|
private val nodes = HashSet<EnergyCableBlockEntity.Node>()
|
||||||
private val paths = HashSet<SegmentPath>()
|
private val paths = HashSet<SegmentPath>()
|
||||||
|
|
||||||
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
|
|
||||||
return node in nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
var throughput = Decimal.ZERO
|
var throughput = Decimal.ZERO
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private fun checkThroughput() {
|
|
||||||
if (!throughputKnown) {
|
|
||||||
throughput = nodes.maxOf { it.energyThroughput }
|
|
||||||
throughputKnown = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var transferredLastTick = Decimal.ZERO
|
var transferredLastTick = Decimal.ZERO
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val availableThroughput: Decimal get() {
|
val availableThroughput: Decimal get() {
|
||||||
checkThroughput()
|
if (!throughputKnown) {
|
||||||
|
throughput = nodes.maxOf { it.energyThroughput }
|
||||||
|
throughputKnown = true
|
||||||
|
}
|
||||||
|
|
||||||
return throughput - transferredLastTick
|
return throughput - transferredLastTick
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,17 +141,6 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun throughput(instantSnapshot: Map<Segment, Decimal>): Decimal {
|
|
||||||
if (lastTick != UNIVERSE_TICKS) {
|
|
||||||
transferredLastTick = Decimal.ZERO
|
|
||||||
lastTick = UNIVERSE_TICKS
|
|
||||||
}
|
|
||||||
|
|
||||||
checkThroughput()
|
|
||||||
val currentTransferred = instantSnapshot[this] ?: transferredLastTick
|
|
||||||
return throughput - currentTransferred
|
|
||||||
}
|
|
||||||
|
|
||||||
fun transfer(amount: Decimal, instantSnapshot: MutableMap<Segment, Decimal>): Decimal {
|
fun transfer(amount: Decimal, instantSnapshot: MutableMap<Segment, Decimal>): Decimal {
|
||||||
if (lastTick != UNIVERSE_TICKS) {
|
if (lastTick != UNIVERSE_TICKS) {
|
||||||
transferredLastTick = Decimal.ZERO
|
transferredLastTick = Decimal.ZERO
|
||||||
@ -233,16 +180,6 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
return diff
|
return diff
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refund(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>) {
|
|
||||||
if (simulate) {
|
|
||||||
if (this in instantSnapshot) {
|
|
||||||
instantSnapshot[this] = maxOf(instantSnapshot[this]!! - amount, Decimal.ZERO)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
transferredLastTick = maxOf(transferredLastTick, Decimal.ZERO)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun remove(node: EnergyCableBlockEntity.Node) {
|
fun remove(node: EnergyCableBlockEntity.Node) {
|
||||||
check(nodes.remove(node)) { "Tried to remove node $node from segment $this, but that node does not belong to this segment" }
|
check(nodes.remove(node)) { "Tried to remove node $node from segment $this, but that node does not belong to this segment" }
|
||||||
|
|
||||||
@ -261,7 +198,10 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
check(paths.add(path)) { "Path $path should already contain $this" }
|
check(paths.add(path)) { "Path $path should already contain $this" }
|
||||||
check(path.segments.add(this)) { "Path set and Segment disagree whenever $this is absent from $path" }
|
check(path.segments.add(this)) { "Path set and Segment disagree whenever $this is absent from $path" }
|
||||||
|
|
||||||
checkThroughput()
|
if (!throughputKnown) {
|
||||||
|
throughput = nodes.maxOf { it.energyThroughput }
|
||||||
|
throughputKnown = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(path: SegmentPath) {
|
fun remove(path: SegmentPath) {
|
||||||
@ -301,18 +241,23 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
nodes.forEach { it.onInvalidate() }
|
nodes.forEach { it.onInvalidate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// isn't exactly A*, but greedy algorithm, which searched for locally optimal solutions because they lead to globally optimal ones
|
// TODO: Multiple paths, so energy can be delivered to receiver through different paths if previously found path is congested
|
||||||
private fun findPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, existing: Collection<SegmentPath>, threshold: Decimal): SegmentPath? {
|
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): SegmentPath? {
|
||||||
val seenTop = existing.any { a in it }
|
if (!a.canTraverse || !b.canTraverse)
|
||||||
|
|
||||||
if (seenTop && a.energyThroughput <= threshold) {
|
|
||||||
return null
|
return null
|
||||||
}
|
|
||||||
|
|
||||||
|
val list = pathCache.computeIfAbsent(a to b) { ArrayList(1) }
|
||||||
|
|
||||||
|
if (list.isNotEmpty())
|
||||||
|
return list.first
|
||||||
|
|
||||||
|
// no free paths available, try to find extra one
|
||||||
|
// while this use A* algorithm, this is done purely for biasing search towards end point (to speed up search),
|
||||||
|
// on small cable networks simple flooding will do just fine, if we consider overloaded cables as closed flood gates
|
||||||
val openNodes = PriorityQueue<SearchNode>()
|
val openNodes = PriorityQueue<SearchNode>()
|
||||||
val seenNodes = HashSet<EnergyCableBlockEntity.Node>()
|
val seenNodes = HashSet<EnergyCableBlockEntity.Node>()
|
||||||
|
|
||||||
openNodes.add(SearchNode(a, b, null, seenTop))
|
openNodes.add(SearchNode(a, b))
|
||||||
|
|
||||||
while (openNodes.isNotEmpty()) {
|
while (openNodes.isNotEmpty()) {
|
||||||
val first = openNodes.remove()
|
val first = openNodes.remove()
|
||||||
@ -335,45 +280,21 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
val path = SegmentPath(a.blockEntity.blockPos, b.blockEntity.blockPos)
|
val path = SegmentPath(a.blockEntity.blockPos, b.blockEntity.blockPos)
|
||||||
touchedSegments.forEach { it.add(path) }
|
touchedSegments.forEach { it.add(path) }
|
||||||
|
|
||||||
|
list.add(path)
|
||||||
return path
|
return path
|
||||||
} else {
|
} else {
|
||||||
for (neighbour in first.node.neighboursView.values) {
|
for (neighbour in first.node.neighboursView.values) {
|
||||||
if (!seenNodes.add(neighbour) || !neighbour.canTraverse) continue
|
if (!seenNodes.add(neighbour) || !neighbour.canTraverse) continue
|
||||||
val seen = existing.any { neighbour in it }
|
openNodes.add(SearchNode(neighbour, b, first))
|
||||||
if (seen && neighbour.energyThroughput <= threshold) continue
|
|
||||||
openNodes.add(SearchNode(neighbour, b, first, seen))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// solution does not exist
|
||||||
|
list.add(null)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): List<SegmentPath> {
|
|
||||||
if (!a.canTraverse || !b.canTraverse)
|
|
||||||
return listOf()
|
|
||||||
|
|
||||||
val list = pathCache.computeIfAbsent(a to b) { CacheEntry() }
|
|
||||||
|
|
||||||
if (!list.saturated) {
|
|
||||||
var maxThroughput = list.paths.maxOfOrNull { it.availableThroughput } ?: Decimal.ZERO
|
|
||||||
|
|
||||||
while (maxThroughput < energyToTransfer && !list.saturated) {
|
|
||||||
val find = findPath(a, b, list.paths, maxThroughput)
|
|
||||||
|
|
||||||
if (find == null || find in list.paths) {
|
|
||||||
list.saturated = true
|
|
||||||
} else {
|
|
||||||
list.paths.add(find)
|
|
||||||
maxThroughput = maxOf(maxThroughput, find.availableThroughput)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.paths.sortByDescending { it.availableThroughput }
|
|
||||||
return list.paths
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) {
|
override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) {
|
||||||
if (livelyNodes.remove(node)) {
|
if (livelyNodes.remove(node)) {
|
||||||
check(livelyNodesList.remove(node))
|
check(livelyNodesList.remove(node))
|
||||||
@ -400,79 +321,31 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
|||||||
var hit = false
|
var hit = false
|
||||||
|
|
||||||
for (side in node.sides.values) {
|
for (side in node.sides.values) {
|
||||||
if (!side.isEnabled)
|
if (side.isEnabled) {
|
||||||
continue
|
if (fromNode === node && side.side === fromSide) {
|
||||||
else if (fromNode === node && side.side === fromSide) {
|
hit = true
|
||||||
hit = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val it = side.neighbour.get() ?: continue
|
|
||||||
if (it is EnergyCableBlockEntity.CableSide) continue
|
|
||||||
|
|
||||||
val paths = getPath(fromNode, node, residue)
|
|
||||||
hit = true
|
|
||||||
|
|
||||||
if (paths.size == 1) {
|
|
||||||
// Single path, fast scenario
|
|
||||||
val path = paths[0]
|
|
||||||
val pathTransferred = path.transfer(residue, simulate, snapshot)
|
|
||||||
val thisReceived = it.receiveEnergy(pathTransferred, simulate)
|
|
||||||
|
|
||||||
// If cable transferred more than machine accepted, then "refund" energy
|
|
||||||
// so cables record actual value transferred through them
|
|
||||||
if (thisReceived != pathTransferred) {
|
|
||||||
path.refund(pathTransferred - thisReceived, simulate, snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
received += thisReceived
|
|
||||||
residue -= thisReceived
|
|
||||||
if (!residue.isPositive) return received
|
|
||||||
} else if (paths.size >= 2) {
|
|
||||||
// Multiple paths, a bit more complicated
|
|
||||||
// Determine how much machine is likely to accept
|
|
||||||
val potentiallyAccepted = it.receiveEnergy(residue, true)
|
|
||||||
|
|
||||||
// Won't accept anything
|
|
||||||
if (potentiallyAccepted <= Decimal.ZERO)
|
|
||||||
continue
|
|
||||||
|
|
||||||
// Now determine combined available throughput
|
|
||||||
// Make a copy of snapshot, so we can freely write into it
|
|
||||||
val copy = snapshot.clone()
|
|
||||||
var calcResidue = potentiallyAccepted
|
|
||||||
|
|
||||||
// TODO: Currently, all transfers cause Braess's paradox, because of greedy selection of "fastest" cable
|
|
||||||
// Need to implement heuristics to better distribute load across different paths/segments
|
|
||||||
for (path in paths) {
|
|
||||||
val passed = path.transfer(calcResidue, true, copy)
|
|
||||||
calcResidue -= passed
|
|
||||||
if (calcResidue <= Decimal.ZERO) break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calcResidue == potentiallyAccepted) {
|
|
||||||
// мда
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var thisReceived = it.receiveEnergy(potentiallyAccepted - calcResidue, simulate)
|
side.neighbour.get()?.let {
|
||||||
received += thisReceived
|
if (it !is EnergyCableBlockEntity.CableSide) {
|
||||||
residue -= thisReceived
|
val path = getPath(fromNode, node, residue)
|
||||||
|
hit = true
|
||||||
|
|
||||||
for (path in paths) {
|
if (path != null) {
|
||||||
val passed = path.transfer(thisReceived, simulate, snapshot)
|
val thisReceived = it.receiveEnergy(path.transfer(residue, simulate, snapshot), simulate)
|
||||||
thisReceived -= passed
|
received += thisReceived
|
||||||
if (thisReceived <= Decimal.ZERO) break
|
residue -= thisReceived
|
||||||
|
if (!residue.isPositive) return received
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!residue.isPositive) return received
|
|
||||||
check(thisReceived <= Decimal.ZERO) { "Путом, алло, Путом, какого чёрта Путом? Путом почему ты заблокировал логику, а Путом?" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
itr.remove()
|
itr.remove()
|
||||||
check(livelyNodes.remove(node)) { "Lively nodes Set does not contain $node" }
|
check(livelyNodes.remove(node))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user