If multiple paths exist to target, and they differ in throughput, now cables will combine their throughput
This commit is contained in:
parent
61eab5fb37
commit
655a9c541e
@ -75,6 +75,14 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
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 {
|
||||
if (!amount.isPositive || shortCircuit) {
|
||||
return Decimal.ZERO
|
||||
@ -90,16 +98,15 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
}
|
||||
}
|
||||
|
||||
var min = amount
|
||||
val min = if (simulate) segments.minOf { it.throughput(instantSnapshot) } else segments.minOf { it.availableThroughput }
|
||||
|
||||
for (segment in segments) {
|
||||
min = minOf(segment.transfer(amount, instantSnapshot), min)
|
||||
if (!min.isPositive) return Decimal.ZERO
|
||||
if (min.isPositive) {
|
||||
if (simulate)
|
||||
segments.forEach { it.transfer(min, instantSnapshot) }
|
||||
else
|
||||
segments.forEach { it.transfer(min) }
|
||||
}
|
||||
|
||||
if (!simulate)
|
||||
segments.forEach { it.transfer(min) }
|
||||
|
||||
return min
|
||||
}
|
||||
|
||||
@ -128,15 +135,18 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
var throughput = Decimal.ZERO
|
||||
private set
|
||||
|
||||
var transferredLastTick = Decimal.ZERO
|
||||
private set
|
||||
|
||||
val availableThroughput: Decimal get() {
|
||||
private fun checkThroughput() {
|
||||
if (!throughputKnown) {
|
||||
throughput = nodes.maxOf { it.energyThroughput }
|
||||
throughputKnown = true
|
||||
}
|
||||
}
|
||||
|
||||
var transferredLastTick = Decimal.ZERO
|
||||
private set
|
||||
|
||||
val availableThroughput: Decimal get() {
|
||||
checkThroughput()
|
||||
return throughput - transferredLastTick
|
||||
}
|
||||
|
||||
@ -173,6 +183,17 @@ 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 {
|
||||
if (lastTick != UNIVERSE_TICKS) {
|
||||
transferredLastTick = Decimal.ZERO
|
||||
@ -240,10 +261,7 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
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" }
|
||||
|
||||
if (!throughputKnown) {
|
||||
throughput = nodes.maxOf { it.energyThroughput }
|
||||
throughputKnown = true
|
||||
}
|
||||
checkThroughput()
|
||||
}
|
||||
|
||||
fun remove(path: SegmentPath) {
|
||||
@ -331,9 +349,9 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): SegmentPath? {
|
||||
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): List<SegmentPath> {
|
||||
if (!a.canTraverse || !b.canTraverse)
|
||||
return null
|
||||
return listOf()
|
||||
|
||||
val list = pathCache.computeIfAbsent(a to b) { CacheEntry() }
|
||||
|
||||
@ -343,8 +361,8 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
while (maxThroughput < energyToTransfer && !list.saturated) {
|
||||
val find = findPath(a, b, list.paths, maxThroughput)
|
||||
|
||||
if (find == null) {
|
||||
list.saturated
|
||||
if (find == null || find in list.paths) {
|
||||
list.saturated = true
|
||||
} else {
|
||||
list.paths.add(find)
|
||||
maxThroughput = maxOf(maxThroughput, find.availableThroughput)
|
||||
@ -352,9 +370,8 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Immediate load selection is bad because it can easily lead to oscillations and non optimal throughput
|
||||
// without user having any clue why their shit work suboptimally
|
||||
return list.paths.maxByOrNull { it.availableThroughput }
|
||||
list.paths.sortByDescending { it.availableThroughput }
|
||||
return list.paths
|
||||
}
|
||||
|
||||
override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) {
|
||||
@ -383,31 +400,79 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
var hit = false
|
||||
|
||||
for (side in node.sides.values) {
|
||||
if (side.isEnabled) {
|
||||
if (fromNode === node && side.side === fromSide) {
|
||||
hit = true
|
||||
if (!side.isEnabled)
|
||||
continue
|
||||
else if (fromNode === node && side.side === fromSide) {
|
||||
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
|
||||
}
|
||||
|
||||
side.neighbour.get()?.let {
|
||||
if (it !is EnergyCableBlockEntity.CableSide) {
|
||||
val path = getPath(fromNode, node, residue)
|
||||
hit = true
|
||||
var thisReceived = it.receiveEnergy(potentiallyAccepted - calcResidue, simulate)
|
||||
received += thisReceived
|
||||
residue -= thisReceived
|
||||
|
||||
if (path != null) {
|
||||
val thisReceived = it.receiveEnergy(path.transfer(residue, simulate, snapshot), simulate)
|
||||
received += thisReceived
|
||||
residue -= thisReceived
|
||||
if (!residue.isPositive) return received
|
||||
}
|
||||
}
|
||||
for (path in paths) {
|
||||
val passed = path.transfer(thisReceived, simulate, snapshot)
|
||||
thisReceived -= passed
|
||||
if (thisReceived <= Decimal.ZERO) break
|
||||
}
|
||||
|
||||
if (!residue.isPositive) return received
|
||||
check(thisReceived <= Decimal.ZERO) { "Путом, алло, Путом, какого чёрта Путом? Путом почему ты заблокировал логику, а Путом?" }
|
||||
}
|
||||
}
|
||||
|
||||
if (!hit) {
|
||||
itr.remove()
|
||||
check(livelyNodes.remove(node))
|
||||
check(livelyNodes.remove(node)) { "Lively nodes Set does not contain $node" }
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user