From b8e76a0fd43169d36420bce1f8ee7fc141200e2b Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 7 Mar 2025 10:45:07 +0700 Subject: [PATCH] Make energy cable graph track node + side in lively nodes list, considerably improving performance --- .../block/entity/cable/EnergyCableGraph.kt | 203 +++++++++--------- 1 file changed, 107 insertions(+), 96 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableGraph.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableGraph.kt index 49b77cbe7..8434ce627 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableGraph.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableGraph.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.mc.otm.block.entity.cable import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ReferenceArraySet import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet +import net.minecraft.core.Direction import org.apache.logging.log4j.LogManager import ru.dbotthepony.mc.otm.SERVER_IS_LIVE import ru.dbotthepony.mc.otm.UNIVERSE_TICKS @@ -74,16 +75,20 @@ private class LinkedPriorityQueue> { } class EnergyCableGraph : GraphNodeList() { - private val livelyNodes = HashSet() - private val livelyNodesList = ArrayList() + private val livelyNodes = HashSet>() + private val livelyNodesList = ArrayList>() fun addLivelyNode(node: EnergyCableBlockEntity.Node) { when (contains(node)) { ContainsStatus.ABOUT_TO_BE_REMOVED, ContainsStatus.ABOUT_TO_BE_ADDED -> { } // do nothing ContainsStatus.DOES_NOT_BELONG -> throw IllegalArgumentException("$node does not belong to $this") ContainsStatus.CONTAINS -> { - if (livelyNodes.add(node)) { - livelyNodesList.add(node) + for (dir in RelativeSide.entries) { + val pair = node to dir + + if (livelyNodes.add(pair)) { + livelyNodesList.add(pair) + } } } } @@ -584,8 +589,12 @@ class EnergyCableGraph : GraphNodeList() @@ -608,8 +617,11 @@ class EnergyCableGraph : GraphNodeList() - for (node in itr) { - var hit = false + for (pair in itr) { + val (node, relSide) = pair + val side = node.sides[relSide]!! - for (side in node.sides.values) { - 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, it.receiveEnergy(residue, true), !simulate) - hit = true - - if (paths.size == 1) { - // Single path, fast scenario - val path = paths[0] - val pathTransferred = path.transfer(residue, simulate, snapshot) - if (pathTransferred <= Decimal.ZERO) continue - 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) - - if (!simulate && thisReceived != Decimal.ZERO) { - path.triggerBlockstateUpdates() - } - } else if (!simulate) { - path.triggerBlockstateUpdates() - } - - 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 - } - - var thisReceived = it.receiveEnergy(potentiallyAccepted - calcResidue, simulate) - received += thisReceived - residue -= thisReceived - - for (path in paths) { - val passed = path.transfer(thisReceived, simulate, snapshot) - - if (!simulate) - path.triggerBlockstateUpdates() - - thisReceived -= passed - if (thisReceived <= Decimal.ZERO) break - } - - if (!residue.isPositive) return received - //check(thisReceived <= Decimal.ZERO) { "Путом, алло, Путом, какого чёрта Путом? Путом почему ты заблокировал логику, а Путом?" } - - if (thisReceived > Decimal.ZERO) { - LOGGER.warn("Cable path from $fromNode to $node doesn't follow common sense, disabling.") - paths.forEach { it.shortCircuit = true } - } - } + if (!side.isEnabled) { + itr.remove() + check(livelyNodes.remove(pair)) { "Lively nodes Set does not contain $pair" } + continue + } else if (fromNode === node && side.side === fromSide) { + continue } - if (!hit) { + val it = side.neighbour.get() + + if (it == null || it is EnergyCableBlockEntity.CableSide) { itr.remove() - check(livelyNodes.remove(node)) { "Lively nodes Set does not contain $node" } + check(livelyNodes.remove(pair)) { "Lively nodes Set does not contain $pair" } + continue + } + + val paths = getPath(fromNode, node, it.receiveEnergy(residue, true), !simulate) + + if (paths.size == 1) { + // Single path, fast scenario + val path = paths[0] + val pathTransferred = path.transfer(residue, simulate, snapshot) + if (pathTransferred <= Decimal.ZERO) continue + 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) + + if (!simulate && thisReceived != Decimal.ZERO) { + path.triggerBlockstateUpdates() + } + } else if (!simulate) { + path.triggerBlockstateUpdates() + } + + 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 + } + + var thisReceived = it.receiveEnergy(potentiallyAccepted - calcResidue, simulate) + received += thisReceived + residue -= thisReceived + + for (path in paths) { + val passed = path.transfer(thisReceived, simulate, snapshot) + + if (!simulate) + path.triggerBlockstateUpdates() + + thisReceived -= passed + if (thisReceived <= Decimal.ZERO) break + } + + if (!residue.isPositive) return received + //check(thisReceived <= Decimal.ZERO) { "Путом, алло, Путом, какого чёрта Путом? Путом почему ты заблокировал логику, а Путом?" } + + if (thisReceived > Decimal.ZERO) { + LOGGER.warn("Cable path from $fromNode to $node doesn't follow common sense, disabling.") + paths.forEach { it.shortCircuit = true } + } } }