Compare commits

...

5 Commits

3 changed files with 73 additions and 20 deletions

View File

@ -111,11 +111,6 @@ abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockP
_segment = value
}
fun onInvalidate() {
_segment = EnergyCableGraph.Segment(this)
updatePoweredState(false)
}
val sides get() = energySides
override fun onNeighbour(link: Link) {

View File

@ -8,6 +8,11 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.UNIVERSE_TICKS
import ru.dbotthepony.mc.otm.capability.receiveEnergy
import ru.dbotthepony.mc.otm.config.CablesConfig
import ru.dbotthepony.mc.otm.core.addAll
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.collect.max
import ru.dbotthepony.mc.otm.core.collect.reduce
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.RelativeSide
import ru.dbotthepony.mc.otm.core.shuffle
@ -66,8 +71,9 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
}
}
class SegmentPath(private val a: BlockPos, private val b: BlockPos) {
class SegmentPath(val a: EnergyCableBlockEntity.Node, val b: EnergyCableBlockEntity.Node) {
val segments = LinkedHashSet<Segment>()
val nodes = HashSet<EnergyCableBlockEntity.Node>()
var shortCircuit = false
private var lastTickTransfers = 0
private var lastTick = 0
@ -80,7 +86,7 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
}
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
return segments.any { node in it }
return node in nodes
}
override fun equals(other: Any?): Boolean {
@ -133,7 +139,7 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
}
private val nodes = LinkedHashSet<EnergyCableBlockEntity.Node>()
private var paths = ReferenceLinkedOpenHashSet<SegmentPath>()
var paths = ReferenceLinkedOpenHashSet<SegmentPath>()
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
return node in nodes
@ -165,6 +171,11 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
private set
val availableThroughput: Decimal get() {
if (lastTick != UNIVERSE_TICKS) {
transferredLastTick = Decimal.ZERO
lastTick = UNIVERSE_TICKS
}
checkThroughput()
return throughput - transferredLastTick
}
@ -283,11 +294,13 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
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" }
paths.forEach { it.nodes.remove(node) }
throughputKnown = false
}
fun add(node: EnergyCableBlockEntity.Node) {
check(nodes.add(node)) { "Tried to add node $node to segment $this, but we already have that node added" }
paths.forEach { it.nodes.add(node) }
if (throughputKnown)
throughput = maxOf(throughput, node.energyThroughput)
@ -296,12 +309,14 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
fun add(path: SegmentPath) {
if (paths.add(path)) {
check(path.segments.add(this)) { "Path set and Segment disagree whenever $this is absent from $path" }
path.nodes.addAll(nodes)
}
}
fun remove(path: SegmentPath) {
check(paths.remove(path)) { "Path $path should already contain $this" }
check(path.segments.remove(this)) { "Path set and Segment disagree whenever $this is present in $path" }
path.nodes.removeAll(nodes)
}
// breaks "instant snapshots" of segments atm
@ -331,6 +346,26 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
}
}
fun split(): List<Segment> {
val result = ArrayList<Segment>()
result.add(this)
for (v in nodes) {
val segment = Segment()
v._segment = segment
segment.nodes.add(v)
segment.throughput = v.energyThroughput
segment.throughputKnown = true
segment.transferredLastTick = transferredLastTick
result.add(segment)
v.updatePoweredState(false)
}
// mark this segment as dead
this.nodes.clear()
return result
}
private fun combineInto(segment: Segment) {
nodes.forEach { it._segment = segment }
segment.nodes.addAll(nodes)
@ -378,11 +413,6 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
}
}
fun invalidatePathCache() {
pathCache.clear()
nodes.forEach { it.onInvalidate() }
}
// isn't exactly A*, but greedy algorithm, which searched for locally optimal solutions because they lead to globally optimal ones
private fun findPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, existing: Collection<SegmentPath>, threshold: Decimal): SegmentPath? {
val seenTop = existing.any { a in it }
@ -428,7 +458,7 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
}
}
val path = SegmentPath(a.blockEntity.blockPos, b.blockEntity.blockPos)
val path = SegmentPath(a, b)
solution.forEach { it.segment.add(path) }
touchedSegments.forEach { it.tryCombine(touchedSegments) }
@ -446,23 +476,34 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
return null
}
private var lastTickSearches = 0
private var searchedOnTick = 0
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal, sort: Boolean): List<SegmentPath> {
if (!a.canTraverse || !b.canTraverse)
return listOf()
if (searchedOnTick != UNIVERSE_TICKS) {
searchedOnTick = UNIVERSE_TICKS
lastTickSearches = 0
}
val list = pathCache.computeIfAbsent(a to b) { CacheEntry() }
if (!list.saturated) {
var maxThroughput = list.paths.maxOfOrNull { it.availableThroughput } ?: Decimal.ZERO
val maxSearches = CablesConfig.SEARCHES_PER_TICK
while (maxThroughput < energyToTransfer && !list.saturated) {
if (!list.saturated && lastTickSearches <= maxSearches) {
var maxThroughput = list.paths.iterator().map { it.availableThroughput }.reduce(Decimal.ZERO, Decimal::plus)
while (maxThroughput < energyToTransfer && !list.saturated && ++lastTickSearches <= maxSearches) {
// TODO: Heuristics here is wrong
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)
maxThroughput += find.availableThroughput
}
}
}
@ -473,18 +514,29 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
return list.paths
}
fun notifyThroughputsChanged() {
pathCache.values.forEach { it.saturated = false }
}
override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) {
if (livelyNodes.remove(node)) {
check(livelyNodesList.remove(node))
}
invalidatePathCache()
node.segment.paths.forEach {
pathCache.remove(it.a to it.b)
pathCache.remove(it.b to it.a)
}
node.segment.paths = ReferenceLinkedOpenHashSet()
node.segment.split()
}
override fun onNodeAdded(node: EnergyCableBlockEntity.Node) {
check(livelyNodes.add(node))
livelyNodesList.add(node)
invalidatePathCache()
notifyThroughputsChanged()
}
fun receiveEnergy(howMuch: Decimal, simulate: Boolean, fromNode: EnergyCableBlockEntity.Node, fromSide: RelativeSide): Decimal {

View File

@ -27,4 +27,10 @@ object CablesConfig : AbstractConfig("cables") {
init {
E.SUPERCONDUCTOR
}
val SEARCHES_PER_TICK: Int by builder
.comment("How many paths are allowed to be searched for during single tick *per* cable network")
.comment("Too low value will make huge cable networks woozy when their cabling changes (e.g. cable added or removed, or valve switched)")
.comment("Too high value will create lag spikes depending on complexity of cable network at hand when cabling changes")
.defineInRange("SEARCHER_PER_TICK", 40, 1)
}