Move everything to BESubscribeList (and eliminate old, bugged code)

This commit is contained in:
DBotThePony 2023-01-14 22:06:45 +07:00
parent 3c73f809dc
commit 95a9aa72aa
Signed by: DBot
GPG Key ID: DCC23B5715498507
6 changed files with 244 additions and 157 deletions

View File

@ -19,6 +19,7 @@ import net.minecraft.world.level.block.Block
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.VoxelShape
import ru.dbotthepony.mc.otm.once
import ru.dbotthepony.mc.otm.oncePre
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import ru.dbotthepony.mc.otm.shapes.BlockShapes
@ -77,7 +78,7 @@ class BatteryBankBlock : RotatableMatteryBlock(), EntityBlock {
) {
super.neighborChanged(state, level, pos, neighbour, neighbourPos, movedByPiston)
val blockEntity = level.getBlockEntity(pos) as? BatteryBankBlockEntity ?: return
level.once { blockEntity.checkSurroundings(level) }
level.oncePre { blockEntity.checkSurroundings() }
}
companion object {

View File

@ -31,6 +31,7 @@ import ru.dbotthepony.mc.otm.core.*
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.unaryMinus
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.BESubscribeList
import ru.dbotthepony.mc.otm.menu.BatteryBankMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities
@ -263,7 +264,7 @@ class BatteryBankBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Matte
override fun setLevel(p_155231_: Level) {
super.setLevel(p_155231_)
tickOnceServer(this::checkSurroundings)
checkSurroundings()
}
override fun <T> getCapability(cap: Capability<T>, side: Direction?): LazyOptional<T> {
@ -294,41 +295,23 @@ class BatteryBankBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Matte
return super.getCapability(cap,side)
}
private var consumingCapability = LazyOptional.empty<IEnergyStorage>()
fun checkSurroundings(level: Level) {
if (isRemoved) return
val tile = level.getBlockEntity(blockPos.offset(blockState.getValue(RotatableMatteryBlock.FACING).normal))
if (tile == null) {
consumingCapability = LazyOptional.empty()
return
}
consumingCapability = getAndBind(
old = consumingCapability,
provider = tile,
capability = ForgeCapabilities.ENERGY,
side = -blockState.getValue(RotatableMatteryBlock.FACING)
) {
@Suppress("name_shadowing")
val level = this.level
if (level is ServerLevel && !SERVER_IS_LIVE)
checkSurroundings(level)
private val consumers = object : BESubscribeList<IEnergyStorage>(this@BatteryBankBlockEntity, ForgeCapabilities.ENERGY) {
override fun test(t: Direction): Boolean {
return -blockState.getValue(RotatableMatteryBlock.FACING) == t
}
}
fun checkSurroundings() = consumers.update((-blockState.getValue(RotatableMatteryBlock.FACING))::equals)
fun tick() {
if (isBlockedByRedstone)
return
consumingCapability.ifPresentK {
for (it in consumers) {
val (_, maxThroughput) = energy.getDistribution(false)
if (maxThroughput.isZero)
return@ifPresentK
continue
val diff = it.receiveEnergy(maxThroughput, true)

View File

@ -29,6 +29,7 @@ import ru.dbotthepony.mc.otm.core.nbt.getByteArrayList
import ru.dbotthepony.mc.otm.core.nbt.ifHas
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.BESubscribeList
import ru.dbotthepony.mc.otm.menu.EnergyCounterMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import java.lang.ref.WeakReference
@ -125,8 +126,17 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
private val energyInput = EnergyCounterCap(true)
private val energyOutput = EnergyCounterCap(false)
private var inputCapability: LazyOptional<out IEnergyStorage> = LazyOptional.empty()
private var outputCapability: LazyOptional<out IEnergyStorage> = LazyOptional.empty()
private val inputCapability = object : BESubscribeList<IEnergyStorage>(this@EnergyCounterBlockEntity, ForgeCapabilities.ENERGY) {
override fun test(t: Direction): Boolean {
return t == blockState.getValue(EnergyCounterBlock.INPUT_DIRECTION)
}
}
private val outputCapability = object : BESubscribeList<IEnergyStorage>(this@EnergyCounterBlockEntity, ForgeCapabilities.ENERGY) {
override fun test(t: Direction): Boolean {
return t == -blockState.getValue(EnergyCounterBlock.INPUT_DIRECTION)
}
}
override fun setLevel(p_155231_: Level) {
super.setLevel(p_155231_)
@ -137,9 +147,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
override val energyFlow = FlowDirection.input(isInput)
override fun extractEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
if (inputCapability.isPresent) {
val it = inputCapability.resolve().get()
val it = inputCapability.first
if (it != null) {
val diff: Decimal
val ioLimit = ioLimit
@ -162,9 +172,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
}
override fun receiveEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
if (outputCapability.isPresent) {
val it = outputCapability.resolve().get()
val it = outputCapability.first
if (it != null) {
val diff: Decimal
val ioLimit = ioLimit
@ -192,9 +202,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
override var batteryLevel: Decimal
get() {
if (energyFlow.input) {
if (outputCapability.isPresent) {
val it = outputCapability.resolve().get()
val it = outputCapability.first
if (it != null) {
if (it is IMatteryEnergyStorage) {
return it.batteryLevel
}
@ -202,9 +212,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
return Decimal(it.energyStored)
}
} else {
if (inputCapability.isPresent) {
val it = inputCapability.resolve().get()
val it = inputCapability.first
if (it != null) {
if (it is IMatteryEnergyStorage) {
return it.batteryLevel
}
@ -222,9 +232,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
override val maxBatteryLevel: Decimal
get() {
if (energyFlow.input) {
if (outputCapability.isPresent) {
val it = outputCapability.resolve().get()
val it = outputCapability.first
if (it != null) {
if (it is IMatteryEnergyStorage) {
return it.maxBatteryLevel
}
@ -232,9 +242,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
return Decimal(it.maxEnergyStored)
}
} else {
if (inputCapability.isPresent) {
val it = inputCapability.resolve().get()
val it = inputCapability.first
if (it != null) {
if (it is IMatteryEnergyStorage) {
return it.maxBatteryLevel
}
@ -249,9 +259,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
override val missingPower: Decimal
get() {
if (energyFlow.input) {
if (outputCapability.isPresent) {
val it = outputCapability.resolve().get()
val it = outputCapability.first
if (it != null) {
if (it is IMatteryEnergyStorage) {
return it.missingPower
}
@ -259,9 +269,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
return Decimal((it.maxEnergyStored - it.energyStored).coerceAtLeast(0))
}
} else {
if (inputCapability.isPresent) {
val it = inputCapability.resolve().get()
val it = inputCapability.first
if (it != null) {
if (it is IMatteryEnergyStorage) {
return it.missingPower
}
@ -326,45 +336,9 @@ class EnergyCounterBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : Mat
}
}
private fun getAndBind(
level: Level,
old: LazyOptional<out IEnergyStorage>,
side: Direction
): LazyOptional<out IEnergyStorage> {
val ent = level.getBlockEntity(blockPos.offset(side.normal)) ?: return LazyOptional.empty()
val resolve = ent.getEnergySided(-side)
if (resolve !== old) {
if (resolve.isPresent) {
val weak = WeakReference(this)
resolve.addListener {
if (SERVER_IS_LIVE)
weak.get()?.checkSurroundings()
}
}
return resolve
}
return old
}
fun checkSurroundings() {
val level = level
if (isRemoved || level !is ServerLevel) return
inputCapability = getAndBind(
level,
inputCapability,
blockState.getValue(EnergyCounterBlock.INPUT_DIRECTION)
)
outputCapability = getAndBind(
level,
outputCapability,
-blockState.getValue(EnergyCounterBlock.INPUT_DIRECTION)
)
inputCapability.update((blockState.getValue(EnergyCounterBlock.INPUT_DIRECTION))::equals)
outputCapability.update((-blockState.getValue(EnergyCounterBlock.INPUT_DIRECTION))::equals)
}
override fun <T> getCapability(cap: Capability<T>, side: Direction?): LazyOptional<T> {

View File

@ -98,29 +98,6 @@ abstract class MatteryBlockEntity(p_155228_: BlockEntityType<*>, p_155229_: Bloc
level.oncePre { if (!isRemoved) func.invoke(level) }
}
protected fun <T> getAndBind(
old: LazyOptional<T>,
provider: ICapabilityProvider?,
capability: Capability<T>,
side: Direction,
invalidate: Runnable
): LazyOptional<T> {
val get = provider?.getCapability(capability, side) ?: LazyOptional.empty()
if (old !== get) {
if (get.isPresent) {
val ref = WeakReference(invalidate)
get.addListener {
ref.get()?.run()
}
}
return get.cast()
}
return get.cast()
}
override fun getDisplayName(): Component {
return customDisplayName ?: defaultDisplayName
}

View File

@ -27,11 +27,11 @@ import ru.dbotthepony.mc.otm.capability.energy.extractEnergyExact
import ru.dbotthepony.mc.otm.container.ItemFilter
import ru.dbotthepony.mc.otm.core.*
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.math.toIntSafe
import ru.dbotthepony.mc.otm.core.math.unaryMinus
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.BESubscribeList
import ru.dbotthepony.mc.otm.graph.Graph6Node
import ru.dbotthepony.mc.otm.graph.GraphNodeListener
import ru.dbotthepony.mc.otm.graph.storage.BasicStorageGraphNode
@ -81,12 +81,14 @@ abstract class AbstractStorageImportExport<T>(
override fun invalidateCaps() {
super.invalidateCaps()
cell.invalidate()
target.invalidate()
valid = false
}
override fun reviveCaps() {
super.reviveCaps()
cell.revive()
target.revive()
valid = true
}
@ -101,21 +103,22 @@ abstract class AbstractStorageImportExport<T>(
if (p_155231_ is ServerLevel) {
StorageNetworkGraph.discoverFull(this, cell.storageNode)
}
tickOnceServer(this::checkSurroundings)
}
}
protected var target: LazyOptional<T> = LazyOptional.empty()
protected abstract val targetCapability: Capability<T>
protected val target by lazy {
object : BESubscribeList<T>(this@AbstractStorageImportExport, targetCapability) {
override fun test(t: Direction): Boolean {
return t == -this@AbstractStorageImportExport.blockState.getValue(RotatableMatteryBlock.FACING_FULL)
}
}
}
fun checkSurroundings() {
target = getAndBind(
target,
level?.getBlockEntity(blockPos + blockState.getValue(RotatableMatteryBlock.FACING_FULL).normal),
targetCapability,
-blockState.getValue(RotatableMatteryBlock.FACING_FULL),
) { tickOnceServer(this::checkSurroundings) }
target.update()
}
abstract val filter: ItemFilter
@ -226,18 +229,18 @@ class StorageImporterBlockEntity(blockPos: BlockPos, blockState: BlockState)
nextTick--
if (nextTick <= 0 && target.isPresent && enoughEnergy) {
val target = target.firstOrNull()
if (nextTick <= 0 && target != null && enoughEnergy) {
val graph = cell.storageGraph ?: return
val items = graph.getVirtualComponent(ITEM_STORAGE)
val resolved = target.orThrow()
if (lastSlot >= resolved.slots) {
if (lastSlot >= target.slots) {
lastSlot = 0
}
val maxMove = energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, MAX_MOVE_PER_OPERATION, true)
var extracted = resolved.extractItem(lastSlot, maxMove, true)
var extracted = target.extractItem(lastSlot, maxMove, true)
if (extracted.isEmpty || !filter.match(extracted)) {
lastSlot++
@ -245,7 +248,7 @@ class StorageImporterBlockEntity(blockPos: BlockPos, blockState: BlockState)
val leftOver = items.insertStack(ItemStackWrapper(extracted), true)
if (leftOver.count.toInt() != extracted.count) {
extracted = resolved.extractItem(lastSlot, extracted.count - leftOver.count.toInt(), false)
extracted = target.extractItem(lastSlot, extracted.count - leftOver.count.toInt(), false)
energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, extracted.count, false)
items.insertStack(ItemStackWrapper(extracted), false)
} else {
@ -335,43 +338,50 @@ class StorageExporterBlockEntity(blockPos: BlockPos, blockState: BlockState) :
nextTick--
if (nextTick <= 0 && target.isPresent && enoughEnergy) {
val target = target.firstOrNull()
if (nextTick <= 0 && target != null && enoughEnergy) {
val graph = cell.storageGraph ?: return
val items = graph.getVirtualComponent(ITEM_STORAGE)
val resolved = target.orThrow()
if (lastSlot >= resolved.slots) {
if (lastSlot >= target.slots) {
lastSlot = 0
}
var hit = false
for (stack in exportStacks) {
if (!resolved.isItemValid(lastSlot, stack.second.item)) {
if (!target.isItemValid(lastSlot, stack.second.item)) {
continue
}
val exportAmountA = items.extractStack(stack.first, stack.second.count.coerceAtMost(
val exportAmountA = items.extractStack(
stack.first, stack.second.count.coerceAtMost(
MAX_MOVE_PER_OPERATION
), true).count
), true
).count
if (exportAmountA == BigInteger.ZERO) {
continue
}
var exportAmount = energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, exportAmountA, true).toIntSafe()
var exportAmount =
energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, exportAmountA, true).toIntSafe()
if (exportAmount == 0) {
break
}
val leftover = resolved.insertItem(lastSlot, stack.second.stack.also { it.count = exportAmount }, true)
val leftover = target.insertItem(lastSlot, stack.second.stack.also { it.count = exportAmount }, true)
if (leftover.count != exportAmount) {
hit = true
exportAmount = items.extractStack(stack.first, (exportAmount - leftover.count).toBigInteger(), false).count.toInt()
resolved.insertItem(lastSlot, stack.second.stack.also { it.count = exportAmount }, false)
exportAmount = items.extractStack(
stack.first,
(exportAmount - leftover.count).toBigInteger(),
false
).count.toInt()
target.insertItem(lastSlot, stack.second.stack.also { it.count = exportAmount }, false)
energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, exportAmount, false)
break
}

View File

@ -2,23 +2,84 @@ package ru.dbotthepony.mc.otm.core.util
import net.minecraft.core.Direction
import net.minecraft.core.SectionPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraftforge.common.capabilities.Capability
import net.minecraftforge.common.util.LazyOptional
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.core.collect.WeakHashSet
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.math.unaryMinus
import ru.dbotthepony.mc.otm.core.orNull
import ru.dbotthepony.mc.otm.onceServer
import java.lang.ref.WeakReference
import java.util.EnumMap
import java.util.function.Predicate
private inline val Direction.flag: Int get() = 1 shl (ordinal + 1)
/**
* This class allows block entities to track [capability] per block side.
*
* Allowed sides are governed by [test] method, which is open for override.
*
* **IMPORTANT:** Once rules behind [test] are changed, you MUST call [validate] or [update] right away, or expect hard to debug bugs.
* If rule changes can't be tracked in practical way, then flip on [alwaysValidate], although it will come with performance
* penalty (this will cause [iterator] to invoke [validate] on each its invocation).
*
* Subclasses can also override [subscribe] and [unsubscribe] to more finely track changes in neighbours.
*
* **THIS CLASS IS COMPLETELY THREAD UNSAFE**, it is assumed you only ever invoke it on server thread,
* or, at very least, thread where [ServerLevel] resides.
*/
open class BESubscribeList<T>(
val block: BlockEntity,
val capability: Capability<T>,
val alwaysValidate: Boolean = false
) : Predicate<Direction>, Iterable<T> {
private val knownCaps = WeakHashSet<LazyOptional<T>>()
private val trackedCaps = EnumMap<Direction, WeakReference<LazyOptional<T>>>(Direction::class.java)
private var updateQueued = false
private var firstKey: Direction? = null
private var lastDirectionSet = 0
private var isWaitingOnChunk = false
/**
* First valid capability, this is faster than invoking [iterator] and getting first value from it.
*/
val first: T? get() {
return firstLO.orNull()
}
/**
* First valid capability as [LazyOptional], this is faster than invoking [iterator] and getting first value from it.
*
* If required, calls [update]
*/
val firstLO: LazyOptional<T> get() {
if (updateQueued) {
update()
} else if (alwaysValidate) {
validate()
}
for (i in 0 .. 2) {
if (firstKey == null) {
return LazyOptional.empty()
}
val get = trackedCaps[firstKey]?.get()
if (get == null || !get.isPresent) {
update()
continue
}
return get
}
return LazyOptional.empty()
}
var valid = true
set(value) {
@ -48,17 +109,13 @@ open class BESubscribeList<T>(
return true
}
fun validate() {
val iterator = trackedCaps.iterator()
for ((k, v) in iterator) {
if (v.get() == null || !test(k)) {
unsubscribe(k, null)
}
}
}
override fun iterator(): Iterator<T> {
if (updateQueued) {
update()
} else if (alwaysValidate) {
validate()
}
return object : Iterator<T> {
val iterator = trackedCaps.iterator()
var value: T? = null
@ -68,9 +125,13 @@ open class BESubscribeList<T>(
val (k, v) = iterator.next()
value = v.get()?.orNull()
if (value == null || !test(k)) {
if (value == null) {
iterator.remove()
unsubscribe(k, null)
} else if (!test(k)) {
iterator.remove()
unsubscribe(k, null)
updateQueued = true
}
}
}
@ -93,29 +154,99 @@ open class BESubscribeList<T>(
return update(direction::equals)
}
private var updateTick = 0
private fun calculateDirectionSet(): Int {
var set = 0
for (value in VALUES)
if (test(value))
set = set or value.flag
return set
}
/**
* Updates subscription list, searching for new capabilities or "removing" outdated ones
* Checks for one of these conditions to be true:
* * If [test] was determined to be changed during last time [iterator] was invoked AND tracked capability was filtered out due to [test] returning false
* * Performs a fast bitflag intersection test, by calling [test] on all directions and seeking difference between built set and previous set checked during last [update]
* * Tracked capabilities are checked for validity (if references are still valid)
*
* [predicate] predicate narrows sides-to-check list, not replaces list generated by this class' [test]
* Once *any* of conditions above end up true, [update] is called and nothing else down the list is checked further.
*/
fun validate() {
check(valid) { "Subscription list is marked as invalid" }
if (updateQueued) {
update()
return
}
val new = calculateDirectionSet()
if (new != lastDirectionSet) {
val intersect = new and lastDirectionSet
val removed = lastDirectionSet and (intersect.inv())
val added = new and (intersect.inv())
for (dir in VALUES) {
if (dir.flag and removed != 0) {
val value1 = trackedCaps.remove(dir)
val value = value1?.get()
if (value1 != null) {
unsubscribe(dir, value)
}
}
}
if (added != 0) {
update { it.flag and added != 0 }
}
lastDirectionSet = new
return
}
val iterator = trackedCaps.iterator()
var any = 0
for ((k, v) in iterator) {
if (v.get() == null) {
unsubscribe(k, null)
any = any or k.flag
}
}
if (any != 0) {
update { it.flag and any != 0 }
}
}
/**
* Updates subscription list, searching for new capabilities and "removing" outdated ones.
*
* [predicate] narrows sides-to-check list, not replaces the set generated by this class' [test]
*/
@JvmOverloads
fun update(predicate: Predicate<Direction>? = null) {
if (!valid)
if (!valid || block.isRemoved)
return
val sorse = block.level?.chunkSource ?: return
val level = block.level as? ServerLevel ?: return // don't run on client
val sorse = level.chunkSource
var waitChunkLoad = false
updateTick++
val updateTick = updateTick
var stream = Direction.stream().filter(this)
var stream = Direction.stream()
if (predicate != null) {
stream = stream.filter(predicate)
} else {
updateQueued = false
lastDirectionSet = calculateDirectionSet()
}
stream = stream.filter(this)
firstKey = null
for (dir in stream) {
val pos = block.blockPos + dir
val getChunk = sorse.getChunkNow(SectionPos.blockToSectionCoord(pos.x), SectionPos.blockToSectionCoord(pos.z))
@ -123,7 +254,7 @@ open class BESubscribeList<T>(
if (getChunk == null) {
waitChunkLoad = true
} else {
val cap = getChunk.getBlockEntity(pos)?.getCapability(capability)
val cap = getChunk.getBlockEntity(pos)?.getCapability(capability, -dir)
val knownCap = trackedCaps[dir]?.get()
if (cap != null && cap.isPresent) {
@ -147,6 +278,10 @@ open class BESubscribeList<T>(
trackedCaps[dir] = WeakReference(cap)
subscribe(dir, cap)
}
if (firstKey == null) {
firstKey = dir
}
} else {
trackedCaps.remove(dir)
unsubscribe(dir, knownCap)
@ -154,8 +289,15 @@ open class BESubscribeList<T>(
}
}
if (waitChunkLoad) {
onceServer { if (updateTick == this.updateTick) update(predicate) }
if (waitChunkLoad && SERVER_IS_LIVE) {
onceServer { if (isWaitingOnChunk) update() }
isWaitingOnChunk = true
} else if (predicate == null) {
isWaitingOnChunk = false
}
}
companion object {
private val VALUES = Direction.values()
}
}