diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt index b1eb6560a..1e4a600d9 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt @@ -222,7 +222,7 @@ fun tickServer(ticker: IConditionalTickable) { postServerTick.add(ticker, SERVER_IS_LIVE, "Server is stopping") } -fun Level.timer(time: Int, ticker: Runnable): TickList.Timer? { +fun Level.once(time: Int, ticker: Runnable): TickList.Timer? { if (this.isClientSide) return null if (!SERVER_IS_LIVE) { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableBlockEntity.kt index fee9415e8..86a0703c2 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableBlockEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/cable/EnergyCableBlockEntity.kt @@ -15,9 +15,13 @@ import ru.dbotthepony.mc.otm.capability.FlowDirection import ru.dbotthepony.mc.otm.capability.MatteryCapability import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage import ru.dbotthepony.mc.otm.config.CablesConfig +import ru.dbotthepony.mc.otm.core.get import ru.dbotthepony.mc.otm.core.math.BlockRotation import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.core.math.RelativeSide +import ru.dbotthepony.mc.otm.core.set +import ru.dbotthepony.mc.otm.core.util.TickList +import ru.dbotthepony.mc.otm.core.util.countingLazy import ru.dbotthepony.mc.otm.graph.GraphNode import ru.dbotthepony.mc.otm.once import ru.dbotthepony.mc.otm.onceServer @@ -37,7 +41,7 @@ abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockP field = value if (value) { - node.graph.livelyNodes.add(node) + node.graph.addLivelyNode(node) } } @@ -55,7 +59,7 @@ abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockP if (isEnabled) { if (neighbour.isPresent) { if (neighbour.get() !is CableSide) { - node.graph.livelyNodes.add(node) + node.graph.addLivelyNode(node) } } @@ -90,7 +94,27 @@ abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockP get() = Decimal.POSITIVE_INFINITY } + private val isPoweredState by countingLazy(blockStateChangesCounter) { + this.blockState[BlockStateProperties.POWERED] + } + inner class Node : GraphNode(::EnergyCableGraph) { + private var _segment = EnergyCableGraph.Segment(this) + + var segment: EnergyCableGraph.Segment + get() { return _segment } + set(value) { + if (_segment === value) return + _segment.remove(this) + value.add(this) + _segment = value + } + + fun onInvalidate() { + _segment = EnergyCableGraph.Segment(this) + updatePoweredState(false) + } + val sides get() = energySides override fun onNeighbour(link: Link) { @@ -105,6 +129,35 @@ abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockP } } + private var updateTimer: TickList.Timer? = null + private var ongoingNewState: Boolean? = null + + fun updatePoweredState(newState: Boolean) { + if (isRemoved) return + val level = level ?: return + + if (isPoweredState != newState || ongoingNewState != newState) { + updateTimer?.cancel() + + if (isPoweredState == newState) { + ongoingNewState = null + updateTimer?.cancel() + } else { + ongoingNewState = newState + + // reduces flickering and block updates pressure + updateTimer = level.once(4) { + ongoingNewState = null + updateTimer = null + + if (!isRemoved) { + level.setBlock(blockPos, blockState.set(BlockStateProperties.POWERED, newState), Block.UPDATE_CLIENTS) + } + } + } + } + } + val blockEntity get() = this@EnergyCableBlockEntity val canTraverse get() = energyThroughput > Decimal.ZERO val energyThroughput get() = this@EnergyCableBlockEntity.energyThroughput @@ -115,30 +168,14 @@ abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockP if (!SERVER_IS_LIVE) return val level = level - val powered = node.graph.livelyNodes.filter { - it.blockEntity.energySides.filter { - side -> side.value.neighbour.isPresent && side.value.neighbour.get() !is CableSide - }.isNotEmpty() - }.size >= 2 - level?.once { if (!node.isValid) return@once val newState = blockState .setValue(CableBlock.MAPPING_CONNECTION_PROP[side]!!, status) - .setValue(BlockStateProperties.POWERED, powered) if (newState !== blockState && SERVER_IS_LIVE) level.setBlock(blockPos, newState, Block.UPDATE_CLIENTS) - - node.graph.livelyNodes.forEach { - if (it.isValid) { - val newState = it.blockEntity.blockState.setValue(BlockStateProperties.POWERED, powered) - - if (newState !== it.blockEntity.blockState && SERVER_IS_LIVE) - level.setBlock(it.blockEntity.blockPos, newState, Block.UPDATE_CLIENTS) - } - } } } @@ -154,7 +191,9 @@ abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockP override fun setLevel(level: Level) { super.setLevel(level) - node.discover(this, MatteryCapability.ENERGY_CABLE_NODE) + + if (!level.isClientSide) + node.discover(this, MatteryCapability.ENERGY_CABLE_NODE) } override fun setRemoved() { 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 7e391ec77..7b90e6472 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 @@ -1,37 +1,245 @@ package ru.dbotthepony.mc.otm.block.entity.cable +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap +import net.minecraft.core.BlockPos +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.mc.otm.UNIVERSE_TICKS import ru.dbotthepony.mc.otm.capability.receiveEnergy import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.core.math.RelativeSide +import ru.dbotthepony.mc.otm.core.shuffle import ru.dbotthepony.mc.otm.graph.GraphNodeList +import ru.dbotthepony.mc.otm.onceServer import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashSet import kotlin.math.ln class EnergyCableGraph : GraphNodeList() { - val livelyNodes = HashSet() + private val livelyNodes = HashSet() + private val livelyNodesList = ArrayList() - private val pathCache = HashMap, Decimal?>() + fun addLivelyNode(node: EnergyCableBlockEntity.Node) { + require(node in nodesSet) { "$node does not belong to $this" } + + if (livelyNodes.add(node)) { + livelyNodesList.add(node) + } + } + + // TODO: LRU cache? + private val pathCache = HashMap, ArrayList>() private class SearchNode(val node: EnergyCableBlockEntity.Node, target: EnergyCableBlockEntity.Node, var parent: SearchNode? = null) : Comparable { - val heuristics: Double = node.position.distSqr(target.position) * 0.0001 - ln(node.energyThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble()) + val heuristics: Double = node.position.distSqr(target.position) * 0.0001 - ln(node.segment.availableThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble()) override fun compareTo(other: SearchNode): Int { return heuristics.compareTo(other.heuristics) } } - fun invalidatePathCache() { - pathCache.clear() + class SegmentPath(private val a: BlockPos, private val b: BlockPos) { + val segments = HashSet() + private var shortCircuit = false + private var lastTickTransfers = 0 + private var lastTick = 0 + + val availableThroughput: Decimal + get() = segments.minOfOrNull { it.availableThroughput } ?: Decimal.ZERO + + fun transfer(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap): Decimal { + if (!amount.isPositive || shortCircuit) { + return Decimal.ZERO + } + + if (lastTickTransfers++ >= 40000) { + if (lastTick == UNIVERSE_TICKS) { + shortCircuit = true + LOGGER.warn("Cable path from $a to $b appears to be a short circuit, disabling.") + } else { + lastTick = UNIVERSE_TICKS + lastTickTransfers = 0 + } + } + + var min = amount + + for (segment in segments) { + min = minOf(segment.transfer(amount, instantSnapshot), min) + if (!min.isPositive) return Decimal.ZERO + } + + if (!simulate) + segments.forEach { it.transfer(min) } + + return min + } + + fun remove() { + segments.toTypedArray().forEach { it.remove(this) } + } } - private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node): Decimal? { + // TODO: Actual segment logic (merging segments) + class Segment() { + constructor(node: EnergyCableBlockEntity.Node) : this() { + nodes.add(node) + } + + private val nodes = HashSet() + private val paths = HashSet() + + var throughput = Decimal.ZERO + private set + + var transferredLastTick = Decimal.ZERO + private set + + val availableThroughput: Decimal + get() = throughput - transferredLastTick + + private var throughputKnown = false + private var lastTick = 0 + private var lastPoweredStatus = 0 + + private fun updateBlockstates(): Boolean { + if (lastTick + 2 < UNIVERSE_TICKS) { + transferredLastTick = Decimal.ZERO + lastTick = UNIVERSE_TICKS + } + + val newState = transferredLastTick.isPositive + val numState = if (newState) 1 else -1 + + if (numState != lastPoweredStatus) { + lastPoweredStatus = numState + nodes.forEach { if (it.segment === this) it.updatePoweredState(newState) } + } + + return newState + } + + private var hasBlockstateTimer = false + + private fun blockstateUpdateStep() { + hasBlockstateTimer = updateBlockstates() + + if (hasBlockstateTimer) { + onceServer(20) { + blockstateUpdateStep() + } + } + } + + fun transfer(amount: Decimal, instantSnapshot: MutableMap): Decimal { + if (lastTick != UNIVERSE_TICKS) { + transferredLastTick = Decimal.ZERO + lastTick = UNIVERSE_TICKS + } + + val currentTransferred = instantSnapshot[this] ?: transferredLastTick + + if (currentTransferred >= throughput || !amount.isPositive) { + return Decimal.ZERO + } + + val new = minOf(currentTransferred + amount, throughput) + val diff = new - currentTransferred + instantSnapshot[this] = new + return diff + } + + fun transfer(amount: Decimal): Decimal { + if (lastTick != UNIVERSE_TICKS) { + transferredLastTick = Decimal.ZERO + lastTick = UNIVERSE_TICKS + } + + if (transferredLastTick >= throughput || !amount.isPositive) { + return Decimal.ZERO + } + + val new = minOf(transferredLastTick + amount, throughput) + val diff = new - transferredLastTick + transferredLastTick = new + + if (!hasBlockstateTimer) { + blockstateUpdateStep() + } + + return diff + } + + 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" } + + if (nodes.isEmpty()) + throughput = Decimal.ZERO + else + throughput = nodes.maxOf { it.energyThroughput } + } + + fun add(node: EnergyCableBlockEntity.Node) { + check(nodes.add(node)) { "Tried to add node $node to segment $this, but we already have that node added" } + throughput = maxOf(throughput, node.energyThroughput) + } + + fun add(path: SegmentPath) { + 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 + } + } + + fun remove(path: SegmentPath) { + check(paths.remove(path)) { "Path $path shouldn't contain $this" } + check(path.segments.remove(this)) { "Path set and Segment disagree whenever $this is present in $path" } + } + + // breaks "instant snapshots" of segments atm + // shouldn't cause major gameplay issues though + fun split(): List { + if (nodes.isEmpty()) { + throw IllegalStateException("Empty segment somehow?") + } else if (nodes.size == 1) { + return listOf(this) + } else { + lastPoweredStatus = 0 + val list = ArrayList(nodes) + val itr = list.iterator() + itr.next() + val result = ArrayList() + result.add(this) + + for (v in itr) { + v.segment = Segment() + v.segment.paths.addAll(paths) + paths.forEach { it.segments.add(v.segment) } + result.add(v.segment) + } + + return result + } + } + } + + fun invalidatePathCache() { + pathCache.clear() + nodes.forEach { it.onInvalidate() } + } + + // TODO: Multiple paths, so energy can be delivered to receiver through different paths if previously found path is congested + private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): SegmentPath? { if (!a.canTraverse || !b.canTraverse) return null - val key = a to b + val list = pathCache.computeIfAbsent(a to b) { ArrayList(1) } - if (key in pathCache) - return pathCache[key] + 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), @@ -57,9 +265,13 @@ class EnergyCableGraph : GraphNodeList() + solution.forEach { touchedSegments.addAll(it.segment.split()) } + val path = SegmentPath(a.blockEntity.blockPos, b.blockEntity.blockPos) + touchedSegments.forEach { it.add(path) } + + list.add(path) + return path } else { for (neighbour in first.node.neighboursView.values) { if (!seenNodes.add(neighbour) || !neighbour.canTraverse) continue @@ -69,24 +281,31 @@ class EnergyCableGraph : GraphNodeList() for (node in itr) { var hit = false @@ -100,11 +319,11 @@ class EnergyCableGraph : GraphNodeList>(Direction::class.java) init { - for (value in values()) { + for (value in entries) { val (front, top) = value mapped.computeIfAbsent(front) { EnumMap(Direction::class.java) }.put(top, value) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/graph/GraphNodeList.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/graph/GraphNodeList.kt index de223337d..0da2e7a1d 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/graph/GraphNodeList.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/graph/GraphNodeList.kt @@ -6,18 +6,20 @@ import java.lang.ref.WeakReference import java.util.* import kotlin.collections.ArrayDeque import kotlin.collections.ArrayList +import kotlin.collections.HashSet open class GraphNodeList, G : GraphNodeList> : IConditionalTickable { private val queuedAdd = ArrayDeque() private val queuedRemove = ArrayDeque() private val nodesInternal = ArrayList() + private val nodesSetInternal = HashSet() private val conditional = ArrayList() private val always = ArrayList() private var isTicking = false private var shouldQueueChanges = false - protected val nodes: List = Collections.unmodifiableList(nodesInternal) - + val nodes: List = Collections.unmodifiableList(nodesInternal) + val nodesSet: Set = Collections.unmodifiableSet(nodesSetInternal) val size get() = nodesInternal.size var isValid = true @@ -36,7 +38,7 @@ open class GraphNodeList, G : GraphNodeList> : ICondit } fun beginTicking(node: N) { - require(node in nodesInternal || node in queuedAdd) { "Node $node does not belong to $this" } + require(node in nodesSetInternal || node in queuedAdd) { "Node $node does not belong to $this" } if (node in queuedRemove) return if (node is IConditionalTickable) { @@ -53,6 +55,7 @@ open class GraphNodeList, G : GraphNodeList> : ICondit private fun addNow(node: N) { node.graph = this as G nodesInternal.add(node) + nodesSetInternal.add(node) if (node is IConditionalTickable) { conditional.add(node) @@ -69,6 +72,8 @@ open class GraphNodeList, G : GraphNodeList> : ICondit if (removeFromList) nodesInternal.remove(node) + nodesSetInternal.remove(node) + if (node is IConditionalTickable) conditional.remove(node) else if (node is ITickable) @@ -108,7 +113,7 @@ open class GraphNodeList, G : GraphNodeList> : ICondit fun addNodeQueued(node: N): Boolean { check(isValid) { "$this is no longer valid" } - if (node in nodesInternal || node in queuedAdd) + if (node in nodesSetInternal || node in queuedAdd) return false queuedAdd.add(node) @@ -119,7 +124,7 @@ open class GraphNodeList, G : GraphNodeList> : ICondit fun addNode(node: N): Boolean { check(isValid) { "$this is no longer valid" } - if (node in nodesInternal) + if (node in nodesSetInternal) return false if (shouldQueueChanges) { @@ -137,7 +142,7 @@ open class GraphNodeList, G : GraphNodeList> : ICondit fun removeNode(node: N): Boolean { check(isValid) { "$this is no longer valid" } - if (node !in nodesInternal) + if (node !in nodesSetInternal) return false if (shouldQueueChanges) { @@ -155,6 +160,7 @@ open class GraphNodeList, G : GraphNodeList> : ICondit fun retain(nodes: Set) { check(isValid) { "$this is no longer valid" } queuedAdd.retainAll(nodes) + nodesInternal.removeIf { if (it !in nodes) { removeNow(it, false) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/item/tool/RedstoneInteractorItem.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/item/tool/RedstoneInteractorItem.kt index 76b313dd3..b289eac4d 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/item/tool/RedstoneInteractorItem.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/item/tool/RedstoneInteractorItem.kt @@ -26,7 +26,7 @@ import ru.dbotthepony.mc.otm.core.math.plus import ru.dbotthepony.mc.otm.core.util.TickList import ru.dbotthepony.mc.otm.item.MatteryItem import ru.dbotthepony.mc.otm.registry.MDataComponentTypes -import ru.dbotthepony.mc.otm.timer +import ru.dbotthepony.mc.otm.once import java.util.* class RedstoneInteractorItem : MatteryItem(Properties().stacksTo(1)) { @@ -79,7 +79,7 @@ class RedstoneInteractorItem : MatteryItem(Properties().stacksTo(1)) { for (pos in positions) { val shouldSendUpdate = pos !in map - val timer = context.level.timer(context.itemInHand.getOrDefault(MDataComponentTypes.TICK_TIMER, TickTimer.TICK_30).ticks) { + val timer = context.level.once(context.itemInHand.getOrDefault(MDataComponentTypes.TICK_TIMER, TickTimer.TICK_30).ticks) { map.remove(pos) context.level.updateNeighborsAt(pos, context.level.getBlockState(pos).block) } ?: throw RuntimeException("Timer unexpectedly ended up being null")