diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt index a08c1870e..2665e4b7a 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/GlobalEventHandler.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.mc.otm +import com.google.common.util.concurrent.ThreadFactoryBuilder import net.minecraft.client.server.IntegratedServer import net.minecraft.core.HolderLookup import net.minecraft.server.MinecraftServer @@ -27,6 +28,7 @@ import ru.dbotthepony.mc.otm.core.util.IConditionalTickable import ru.dbotthepony.mc.otm.core.util.ITickable import ru.dbotthepony.mc.otm.core.util.TickList import ru.dbotthepony.mc.otm.graph.GraphNodeList +import java.lang.ref.Cleaner import java.util.* import java.util.concurrent.atomic.AtomicInteger @@ -38,6 +40,8 @@ private val postWorldTick = WeakHashMap() private val clientThreads = WeakHashSet() private val serverThreads = WeakHashSet() +val OTM_CLEANER: Cleaner = Cleaner.create(ThreadFactoryBuilder().setNameFormat("OverdriveThatMatters-Cleaner-%d").build()) + private val serverCounter = AtomicInteger() private var _server: MinecraftServer? = null val isClient: Boolean by lazy { FMLLoader.getDist() == Dist.CLIENT } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidFeature.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidFeature.kt index 97700ceba..b143da612 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidFeature.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidFeature.kt @@ -2,10 +2,9 @@ package ru.dbotthepony.mc.otm.android import net.minecraft.core.HolderLookup import net.minecraft.nbt.CompoundTag -import net.minecraft.network.RegistryFriendlyByteBuf import net.neoforged.neoforge.common.util.INBTSerializable import net.neoforged.neoforge.event.entity.living.LivingIncomingDamageEvent -import ru.dbotthepony.mc.otm.network.DelegateSyncher +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.mc.otm.capability.MatteryPlayer @@ -13,7 +12,7 @@ import ru.dbotthepony.mc.otm.core.nbt.set abstract class AndroidFeature(val type: AndroidFeatureType<*>, val android: MatteryPlayer) : INBTSerializable { val ply get() = android.ply - val syncher = DelegateSyncher() + val syncher = Syncher() val syncherRemote = syncher.Remote() open var level by syncher.int(setter = setter@{ field, value -> diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt index 8b64f5553..b1f3a113f 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt @@ -10,7 +10,7 @@ import net.minecraft.world.entity.player.Player import net.neoforged.bus.api.Event import net.neoforged.neoforge.common.NeoForge import net.neoforged.neoforge.common.util.INBTSerializable -import ru.dbotthepony.mc.otm.network.DelegateSyncher +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.mc.otm.OverdriveThatMatters @@ -48,7 +48,7 @@ class AndroidResearch(val type: AndroidResearchType, val capability: MatteryPlay val ply: Player get() = capability.ply - val syncher = DelegateSyncher() + val syncher = Syncher() val syncherRemote = syncher.Remote() var isResearched by syncher.boolean() diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/MatteryBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/MatteryBlockEntity.kt index dbd75d456..3a901d945 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/MatteryBlockEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/MatteryBlockEntity.kt @@ -37,7 +37,6 @@ import net.neoforged.neoforge.event.server.ServerStoppingEvent import net.neoforged.neoforge.event.tick.LevelTickEvent import net.neoforged.neoforge.network.PacketDistributor import org.apache.logging.log4j.LogManager -import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.Listenable import ru.dbotthepony.mc.otm.SERVER_IS_LIVE import ru.dbotthepony.mc.otm.block.INeighbourChangeListener @@ -54,6 +53,7 @@ import ru.dbotthepony.mc.otm.core.util.Savetables import ru.dbotthepony.mc.otm.core.util.TickList import ru.dbotthepony.mc.otm.core.util.countingLazy import ru.dbotthepony.mc.otm.network.BlockEntitySyncPacket +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.mc.otm.onceServer import ru.dbotthepony.mc.otm.registry.MBlocks import ru.dbotthepony.mc.otm.sometimeServer @@ -369,8 +369,8 @@ abstract class MatteryBlockEntity(p_155228_: BlockEntityType<*>, p_155229_: Bloc } } - val syncher = DelegateSyncher() - private val synchers = Object2ObjectArrayMap() + val syncher = Syncher() + private val synchers = Object2ObjectArrayMap() override fun setLevel(level: Level) { val old = this.level diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/RedstoneControl.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/RedstoneControl.kt index 869049e7d..179294c93 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/RedstoneControl.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/RedstoneControl.kt @@ -3,7 +3,7 @@ package ru.dbotthepony.mc.otm.block.entity import net.minecraft.core.HolderLookup import net.minecraft.nbt.CompoundTag import net.neoforged.neoforge.common.util.INBTSerializable -import ru.dbotthepony.mc.otm.network.DelegateSyncher +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.kommons.util.Listenable import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue @@ -79,7 +79,7 @@ class RedstoneControl(private val valueChanges: (new: Boolean, old: Boolean) -> } class SynchronizedRedstoneControl( - synchronizer: DelegateSyncher, + synchronizer: Syncher, private val valueChanges: (new: Boolean, old: Boolean) -> Unit, ) : AbstractRedstoneControl() { override var redstoneSetting: RedstoneSetting by synchronizer.enum(RedstoneSetting.LOW, setter = { access, value -> diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/FluidTankBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/FluidTankBlockEntity.kt index a03cd1219..17585936e 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/FluidTankBlockEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/FluidTankBlockEntity.kt @@ -27,7 +27,7 @@ import ru.dbotthepony.mc.otm.network.wrap import ru.dbotthepony.mc.otm.registry.MBlockEntities class FluidTankBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryDeviceBlockEntity(MBlockEntities.FLUID_TANK, blockPos, blockState) { - val fluid = BlockMatteryFluidHandler(ItemsConfig::FLUID_TANK_CAPACITY, syncher.Slot(ListenableDelegate.SmartBox(FluidStack.EMPTY, setter = { access, value -> + val fluid = BlockMatteryFluidHandler(ItemsConfig::FLUID_TANK_CAPACITY, syncher.add(ListenableDelegate.SmartBox(FluidStack.EMPTY, setter = { access, value -> access.accept(value) level?.lightEngine?.checkBlock(blockPos) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/AbstractProfiledStorage.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/AbstractProfiledStorage.kt index 1bc882e33..c6edd132a 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/AbstractProfiledStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/AbstractProfiledStorage.kt @@ -1,23 +1,22 @@ package ru.dbotthepony.mc.otm.capability -import com.google.common.collect.ImmutableList import it.unimi.dsi.fastutil.objects.ObjectArrayList import net.minecraft.core.HolderLookup import net.minecraft.nbt.CompoundTag -import net.minecraft.nbt.ListTag -import net.minecraft.nbt.NumericTag import net.neoforged.neoforge.common.util.INBTSerializable -import ru.dbotthepony.mc.otm.core.forValidRefs +import ru.dbotthepony.mc.otm.core.DecimalHistoryGraph import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.core.nbt.map import ru.dbotthepony.mc.otm.core.nbt.set -import java.lang.ref.WeakReference -import java.util.* -import kotlin.collections.ArrayList abstract class AbstractProfiledStorage(val parent: P) : INBTSerializable { - private val historyReceiveInternal = ArrayList(HISTORY_SIZE) - private val historyTransferInternal = ArrayList(HISTORY_SIZE) + val received = DecimalHistoryGraph(ticks = HISTORY_SIZE) + val transferred = DecimalHistoryGraph(ticks = HISTORY_SIZE) + + var receivedThisTick = Decimal.ZERO + private set + var transferredThisTick = Decimal.ZERO + private set private var isTicking = false @@ -28,31 +27,9 @@ abstract class AbstractProfiledStorage(val parent: P) : INBTSerializable< } } - val historyReceive: List = Collections.unmodifiableList(historyReceiveInternal) - val historyTransfer: List = Collections.unmodifiableList(historyTransferInternal) - - val lastTickReceive - get() = historyReceiveInternal[tick] - - val lastTickTransfer - get() = historyTransferInternal[tick] - - private var thisTickReceive = Decimal.ZERO - private var thisTickTransfer = Decimal.ZERO - - var tick = 0 - private set - - init { - for (i in 0 until HISTORY_SIZE) { - historyReceiveInternal.add(Decimal.ZERO) - historyTransferInternal.add(Decimal.ZERO) - } - } - protected fun recordTransfer(value: Decimal, simulate: Boolean): Decimal { if (!simulate) { - thisTickTransfer += value + transferredThisTick += value if (!value.isZero) startTicking() } @@ -61,7 +38,7 @@ abstract class AbstractProfiledStorage(val parent: P) : INBTSerializable< protected fun recordReceive(value: Decimal, simulate: Boolean): Decimal { if (!simulate) { - thisTickReceive += value + receivedThisTick += value if (!value.isZero) startTicking() } @@ -69,14 +46,13 @@ abstract class AbstractProfiledStorage(val parent: P) : INBTSerializable< } private fun tick(): Boolean { - tick = (tick + 1) % HISTORY_SIZE + received.add(receivedThisTick) + transferred.add(transferredThisTick) - historyReceiveInternal[tick] = thisTickReceive - historyTransferInternal[tick] = thisTickTransfer - isTicking = !thisTickReceive.isZero || !thisTickTransfer.isZero || historyReceiveInternal.any { !it.isZero } || historyTransferInternal.any { !it.isZero } + isTicking = !receivedThisTick.isZero || !transferredThisTick.isZero || received.any { !it.isZero } || transferred.any { !it.isZero } - thisTickReceive = Decimal.ZERO - thisTickTransfer = Decimal.ZERO + transferredThisTick = Decimal.ZERO + receivedThisTick = Decimal.ZERO return isTicking } @@ -84,48 +60,26 @@ abstract class AbstractProfiledStorage(val parent: P) : INBTSerializable< val savedata: INBTSerializable = object : INBTSerializable { override fun serializeNBT(registry: HolderLookup.Provider): CompoundTag { return CompoundTag().also { tag -> - tag["historyReceive"] = ListTag().also { - for (value in historyReceiveInternal) { - it.add(value.serializeNBT()) - } - } - - tag["historyTransfer"] = ListTag().also { - for (value in historyTransferInternal) { - it.add(value.serializeNBT()) - } - } - - tag["historyTick"] = tick + tag["historyReceive"] = received.serializeNBT(registry) + tag["historyTransfer"] = transferred.serializeNBT(registry) } } override fun deserializeNBT(registry: HolderLookup.Provider, nbt: CompoundTag?) { - tick = 0 - thisTickReceive = Decimal.ZERO - thisTickTransfer = Decimal.ZERO + transferredThisTick = Decimal.ZERO + receivedThisTick = Decimal.ZERO - for (i in 0 until HISTORY_SIZE) { - historyReceiveInternal[i] = Decimal.ZERO - historyTransferInternal[i] = Decimal.ZERO - } + received.clear() + transferred.clear() nbt ?: return - nbt.map("historyTick") { it: NumericTag -> - tick = it.asInt + nbt.map("historyReceive") { it: CompoundTag -> + received.deserializeNBT(registry, it) } - nbt.map("historyReceive") { it: ListTag -> - for (i in 0 until HISTORY_SIZE.coerceAtMost(it.size)) { - historyReceiveInternal[i] = Decimal.deserializeNBT(it[i]) - } - } - - nbt.map("historyTransfer") { it: ListTag -> - for (i in 0 until HISTORY_SIZE.coerceAtMost(it.size)) { - historyTransferInternal[i] = Decimal.deserializeNBT(it[i]) - } + nbt.map("historyTransfer") { it: CompoundTag -> + transferred.deserializeNBT(registry, it) } startTicking() @@ -150,14 +104,6 @@ abstract class AbstractProfiledStorage(val parent: P) : INBTSerializable< return tag } - val weightedTransfer: Decimal get() { - return calcWeightedAverage(historyTransfer, tick) - } - - val weightedReceive: Decimal get() { - return calcWeightedAverage(historyReceive, tick) - } - override fun deserializeNBT(registry: HolderLookup.Provider, nbt: CompoundTag?) { if (parent is INBTSerializable<*>) { (parent as INBTSerializable).deserializeNBT(registry, nbt) @@ -167,48 +113,9 @@ abstract class AbstractProfiledStorage(val parent: P) : INBTSerializable< } companion object { - const val HISTORY_SIZE = 20 + const val HISTORY_SIZE = 20 * 5 private val storages = ObjectArrayList>() - val HISTORY_WEIGHTERS: ImmutableList = ImmutableList.of( - Decimal("0.313335967"), - Decimal("0.146176397"), - Decimal("0.09357867"), - Decimal("0.068193701"), - Decimal("0.053351084"), - Decimal("0.043655993"), - Decimal("0.036847023"), - Decimal("0.031813486"), - Decimal("0.027947534"), - Decimal("0.024889161"), - Decimal("0.02241188"), - Decimal("0.020366241"), - Decimal("0.018649731"), - Decimal("0.017189744"), - Decimal("0.015933452"), - Decimal("0.014841516"), - Decimal("0.013884059"), - Decimal("0.013037985"), - Decimal("0.012285173"), - Decimal("0.011611204"), - ) - - fun calcWeightedAverage(inputs: List, pointer: Int): Decimal { - require(inputs.size == HISTORY_WEIGHTERS.size) { "Expected list of size ${HISTORY_WEIGHTERS.size}, got ${inputs.size}" } - require(pointer in 0 until HISTORY_WEIGHTERS.size) { "Invalid pointer position: $pointer" } - - var result = Decimal.ZERO - var i = 0 - - for (i2 in pointer downTo 0) - result += inputs[i2] * HISTORY_WEIGHTERS[i++] - - for (i2 in HISTORY_WEIGHTERS.size - 1 downTo pointer + 1) - result += inputs[i2] * HISTORY_WEIGHTERS[i++] - - return result - } - // после раздумий, наиболее корректное место для обновления состояния профилированных // хранилищ энергии и материи будет после всех остальных хуков на тики после тика сервера // разумеется, такое решение может несколько снизить производительность сервера, diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayer.kt index 2faef441a..53e3846c3 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayer.kt @@ -59,7 +59,7 @@ import org.apache.logging.log4j.LogManager import org.joml.Vector4f import ru.dbotthepony.kommons.collect.ListenableMap import ru.dbotthepony.kommons.collect.ListenableSet -import ru.dbotthepony.mc.otm.network.DelegateSyncher +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.kommons.util.ListenableDelegate import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue @@ -80,6 +80,7 @@ import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage import ru.dbotthepony.mc.otm.capability.energy.extractEnergyExact import ru.dbotthepony.mc.otm.capability.energy.receiveEnergyExact import ru.dbotthepony.mc.otm.client.minecraft +import ru.dbotthepony.mc.otm.client.onceClient import ru.dbotthepony.mc.otm.config.AndroidConfig import ru.dbotthepony.mc.otm.config.ExopackConfig import ru.dbotthepony.mc.otm.container.CombinedContainer @@ -122,6 +123,7 @@ import ru.dbotthepony.mc.otm.triggers.ExopackGainedSmeltingTrigger import ru.dbotthepony.mc.otm.triggers.ExopackObtainedTrigger import ru.dbotthepony.mc.otm.triggers.ExopackSlotsExpandedTrigger import ru.dbotthepony.mc.otm.triggers.MatteryInventoryChangeTrigger +import java.io.Closeable import java.util.* import java.util.stream.Stream import kotlin.collections.ArrayDeque @@ -182,7 +184,7 @@ class MatteryPlayer(val ply: Player) { * any differences in field order/types/etc between client and server * will break *everything* */ - val syncher = DelegateSyncher() + val syncher = Syncher() /** * Remote for "this" player @@ -196,10 +198,10 @@ class MatteryPlayer(val ply: Player) { * any differences in field order/types/etc between client and server * will break *everything* */ - val publicSyncher = DelegateSyncher() + val publicSyncher = Syncher() val publicSyncherRemote = publicSyncher.Remote() - val remoteSynchers = Object2ObjectArrayMap() + val remoteSynchers = Object2ObjectArrayMap() /** * For data to be stored to and loaded from NBT automatically @@ -242,8 +244,8 @@ class MatteryPlayer(val ply: Player) { exopackSlotModifier.recompute() } - private val exopackSlotModifierMap = syncher.MapSlot( - ListenableMap().also { it.addListener(Runnable { _recomputeModifiers() }) }, + private val exopackSlotModifierMap = syncher.map( + backing = ListenableMap().also { it.addListener(Runnable { _recomputeModifiers() }) }, keyCodec = StreamCodecs.UUID, valueCodec = StreamCodecs.VAR_INT, ) @@ -265,9 +267,9 @@ class MatteryPlayer(val ply: Player) { syncher.add(null, StreamCodecs.ITEM_TYPE.nullable()) } - val slotsChargeFlag = syncher.SetSlot( - ListenableSet(IntAVLTreeSet()), - StreamCodecs.VAR_INT, + val slotsChargeFlag = syncher.set( + backing = ListenableSet(IntAVLTreeSet()), + codec = StreamCodecs.VAR_INT, ).delegate private fun slotChargeToDefault() { @@ -282,13 +284,15 @@ class MatteryPlayer(val ply: Player) { slotChargeToDefault() } + private var exopackContainerSynchedFilters: List = listOf() + /** * Exopack container, which actually store items inside Exopack */ var exopackContainer: MatteryContainer = PlayerMatteryContainer(0) private set(value) { _exoPackMenu = null - field.removeFilterSynchronizer() + exopackContainerSynchedFilters.forEach { it.close() } @Suppress("SENSELESS_COMPARISON") // false positive - fields of player can easily be nulls, despite annotations saying otherwise if (ply.containerMenu != null && (ply !is ServerPlayer || ply.connection != null)) { @@ -303,7 +307,7 @@ class MatteryPlayer(val ply: Player) { } value.deserializeNBT(ply.level().registryAccess(), field.serializeNBT(ply.level().registryAccess())) - value.addFilterSynchronizer(syncher) + exopackContainerSynchedFilters = value.synchableFilters.map { syncher.add0(it) } field = value _combinedInventory = null diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/energy/BatteryBackedEnergyStorage.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/energy/BatteryBackedEnergyStorage.kt index 54dd5abfb..58f409500 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/energy/BatteryBackedEnergyStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/energy/BatteryBackedEnergyStorage.kt @@ -8,7 +8,7 @@ import net.minecraft.world.item.ItemStack import net.minecraft.world.ticks.ContainerSingleItem import net.neoforged.neoforge.capabilities.Capabilities import net.neoforged.neoforge.common.util.INBTSerializable -import ru.dbotthepony.mc.otm.network.DelegateSyncher +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.mc.otm.capability.FlowDirection @@ -23,7 +23,7 @@ import ru.dbotthepony.mc.otm.triggers.ExopackBatterySlotTrigger class BatteryBackedEnergyStorage( private val ply: Player, - synchronizer: DelegateSyncher, + synchronizer: Syncher, initialCharge: Decimal, maxCharge: Decimal, val isAndroid: Boolean, diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jade/providers/MatterStorageProvider.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jade/providers/MatterStorageProvider.kt index 5fc4e52ec..97f6859d6 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jade/providers/MatterStorageProvider.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jade/providers/MatterStorageProvider.kt @@ -43,8 +43,8 @@ object MatterStorageProvider : IBlockComponentProvider, IServerDataProvider = Array(size) { ItemStack.EMPTY } private val filters: Array = arrayOfNulls(size) - private var filterSynchronizer: DelegateSyncher.MapSlot? = null var changeset = 0 private set @@ -111,60 +116,18 @@ open class MatteryContainer(var listener: ContainerListener, private val size: I fun clearSlotFilters() { Arrays.fill(filters, null) - filterSynchronizer?.delegate?.clear() } - fun removeFilterSynchronizer() { - filterSynchronizer?.remove() - filterSynchronizer = null - } - - fun addFilterSynchronizer(synchronizer: DelegateSyncher): DelegateSyncher.MapSlot { - check(filterSynchronizer?.delegate == null) { "Already has filter synchronizer" } - - val field = synchronizer.MapSlot( - ListenableMap(Int2ObjectOpenHashMap()), - keyCodec = StreamCodecs.VAR_INT, - valueCodec = StreamCodecs.ITEM_TYPE, - ) - - for ((i, filter) in filters.withIndex()) { - if (filter != null) { - field.delegate[i] = filter + val synchableFilters by lazy { + immutableList { + for (i in 0 until size) { + accept(SynchableObservedDelegate(Delegate.Of({ filters[i] }, { filters[i] = it }), StreamCodecs.ITEM_TYPE_NULLABLE)) } } - - field.addListener(object : ListenableMap.MapListener { - override fun onClear() { - Arrays.fill(filters, null) - } - - override fun onValueAdded(key: Int, value: Item) { - filters[key] = value - } - - override fun onValueRemoved(key: Int, value: Item) { - filters[key] = null - } - }) - - filterSynchronizer = field - return field } final override fun setSlotFilter(slot: Int, filter: Item?): Boolean { - if (filters[slot] !== filter) { - filters[slot] = filter - - filterSynchronizer?.delegate?.let { - if (filter == null) { - it.remove(slot) - } else { - it[slot] = filter - } - } - } - + filters[slot] = filter return true } @@ -230,8 +193,6 @@ open class MatteryContainer(var listener: ContainerListener, private val size: I nonEmptyFlags.clear() nonEmptyIndices = IntArrayList() - filterSynchronizer?.delegate?.clear() - if (tag != null) { SerializedState.CODEC.parse(registries.createSerializationContext(NbtOps.INSTANCE), tag) .resultOrPartial { LOGGER.error("Error deserializing container: $it") } @@ -258,11 +219,9 @@ open class MatteryContainer(var listener: ContainerListener, private val size: I } } - val map = filterSynchronizer?.delegate - for ((item, index) in it.filters) { - filters[index] = item - map?.put(index, item) + if (index in 0 until size) + filters[index] = item } setChanged() diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/AbstractHistoryGraph.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/AbstractHistoryGraph.kt new file mode 100644 index 000000000..e4978664e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/AbstractHistoryGraph.kt @@ -0,0 +1,200 @@ +package ru.dbotthepony.mc.otm.core + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.minecraft.core.HolderLookup +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.NbtOps +import net.minecraft.network.RegistryFriendlyByteBuf +import net.neoforged.neoforge.common.util.INBTSerializable +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.mc.otm.network.MatteryStreamCodec +import ru.dbotthepony.mc.otm.network.syncher.IRemoteState +import ru.dbotthepony.mc.otm.network.syncher.ISynchable +import java.util.concurrent.CopyOnWriteArrayList + +abstract class AbstractHistoryGraph( + /** + * How many measurements one graph value contains + */ + val resolution: Int, + + /** + * Limit on how many graph values to store + */ + val width: Int = 100, +) : Iterable, INBTSerializable, ISynchable { + constructor(ticks: Int) : this(1, ticks) + + init { + require(resolution >= 1) { "Invalid resolution: $resolution" } + require(width >= 1) { "Invalid width: $width" } + } + + // can't believe i've become lazy enough to use codecs for this + private data class SData(val accumulated: List, val values: List) + + protected abstract fun calculateAverage(input: List): V + protected abstract fun sum(a: V, b: V): V + protected abstract fun identity(): V + + abstract val codec: Codec + abstract val streamCodec: MatteryStreamCodec + + private val remotes = CopyOnWriteArrayList() + + private inner class RemoteState(val listener: Runnable) : IRemoteState { + init { + remotes.add(this) + } + + private var shouldFullyNetwork = false + private val networkValues = ArrayList() + + override fun write(stream: RegistryFriendlyByteBuf) { + stream.writeBoolean(shouldFullyNetwork) + + if (shouldFullyNetwork) { + shouldFullyNetwork = false + stream.writeVarInt(values.size) + values.forEach { streamCodec.encode(stream, it) } + } else { + stream.writeVarInt(networkValues.size) + networkValues.forEach { streamCodec.encode(stream, it) } + networkValues.clear() + } + } + + fun add(value: V) { + if (shouldFullyNetwork) { + return + } else if (networkValues.size + 1 >= (width / 2).coerceIn(20, 100)) { + shouldFullyNetwork = true + networkValues.clear() + listener.run() + } else { + networkValues.add(value) + listener.run() + } + } + + override fun invalidate() { + networkValues.clear() + shouldFullyNetwork = true + } + + override fun close() { + remotes.remove(this) + } + } + + override fun read(stream: RegistryFriendlyByteBuf) { + if (stream.readBoolean()) { + values.clear() + + for (i in 0 until stream.readVarInt()) { + values.add(streamCodec.decode(stream)) + } + } else { + for (i in 0 until stream.readVarInt()) { + values.addFirst(streamCodec.decode(stream)) + } + } + } + + override fun createRemoteState(listener: Runnable): IRemoteState { + return RemoteState(listener) + } + + fun calculateAverage(): V { + if (values.isEmpty()) { + return identity() + } + + return calculateAverage(values) + } + + private val accumulator = ArrayList(this.resolution) + private val values = ArrayDeque() + + init { + values.fill(identity()) + } + + /** + * Calculates sum of values provided by calls to [add] and writes it to graph history or graph value accumulator + */ + fun add(value: V) { + accumulator.add(value) + + if (accumulator.size >= resolution) { + val calc = calculateAverage(accumulator) + values.addFirst(calc) + remotes.forEach { it.add(calc) } + accumulator.clear() + while (values.size >= width) values.removeLast() + } + } + + val size: Int + get() = values.size + + operator fun get(index: Int): V { + return values.getOrNull(index) ?: identity() + } + + fun getRaw(index: Int): V { + return values[index] + } + + private fun codec(): Codec> { + val list = Codec.list(codec) + + return RecordCodecBuilder.create { + it.group( + list.fieldOf("accumulator").forGetter { it.accumulated }, + list.fieldOf("values").forGetter { it.values }, + ).apply(it, ::SData) + } + } + + override fun serializeNBT(registry: HolderLookup.Provider): CompoundTag { + return codec().encodeStart(registry.createSerializationContext(NbtOps.INSTANCE), SData(accumulator, values)).orThrow as CompoundTag + } + + override fun deserializeNBT(registry: HolderLookup.Provider, nbt: CompoundTag) { + this.accumulator.clear() + this.values.clear() + + val (accumulator, values) = codec().decode(registry.createSerializationContext(NbtOps.INSTANCE), nbt) + .map { it.first } + .resultOrPartial { LOGGER.error("Errors during deserializing history graph: $it") } + .orElseGet { SData(listOf(), listOf()) } + + this.accumulator.addAll(accumulator) + this.values.addAll(values) + } + + fun clear() { + accumulator.clear() + values.fill(identity()) + } + + final override fun iterator(): Iterator { + return object : Iterator { + private var index = 0 + + override fun hasNext(): Boolean { + return index < width + } + + override fun next(): V { + return this@AbstractHistoryGraph[index++] + } + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/DecimalHistoryGraph.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/DecimalHistoryGraph.kt new file mode 100644 index 000000000..55e511684 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/DecimalHistoryGraph.kt @@ -0,0 +1,60 @@ +package ru.dbotthepony.mc.otm.core + +import com.google.common.collect.ImmutableList +import com.mojang.serialization.Codec +import net.minecraft.network.RegistryFriendlyByteBuf +import ru.dbotthepony.mc.otm.core.collect.reduce +import ru.dbotthepony.mc.otm.core.math.Decimal +import ru.dbotthepony.mc.otm.data.DecimalCodec +import ru.dbotthepony.mc.otm.network.MatteryStreamCodec +import kotlin.math.pow + +class DecimalHistoryGraph : AbstractHistoryGraph { + constructor(resolution: Int, width: Int) : super(resolution, width) + constructor(ticks: Int) : this(1, ticks) + + override fun calculateAverage(input: List): Decimal { + return input.iterator().reduce(Decimal.ZERO, Decimal::plus) / Decimal(input.size) + } + + override fun sum(a: Decimal, b: Decimal): Decimal { + return a + b + } + + override fun identity(): Decimal { + return Decimal.ZERO + } + + override val codec: Codec + get() = DecimalCodec + override val streamCodec: MatteryStreamCodec + get() = DecimalCodec.NETWORK + + fun calcWeightedAverage(): Decimal { + return calcWeightedAverage(this::get) + } + + companion object { + private val HISTORY_WEIGHTERS: ImmutableList = immutableList { + /*for (i in 0 until 20) { + accept(Decimal(0.5.pow(i + 1))) + }*/ + + val average = Decimal(1) / Decimal(20) + + for (i in 0 until 20) { + accept(average) + } + } + + fun calcWeightedAverage(getter: (Int) -> Decimal): Decimal { + var result = Decimal.ZERO + + for (i in 0 until 20) { + result += getter(i) * HISTORY_WEIGHTERS[i] + } + + return result + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/collect/ListTagCollector.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/collect/ListTagCollector.kt new file mode 100644 index 000000000..c5c52a2a0 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/collect/ListTagCollector.kt @@ -0,0 +1,42 @@ +package ru.dbotthepony.mc.otm.core.collect + +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.Tag +import java.util.function.BiConsumer +import java.util.function.BinaryOperator +import java.util.function.Function +import java.util.function.Supplier +import java.util.stream.Collector +import java.util.stream.Collector.Characteristics + +object ListTagCollector : Collector { + override fun supplier(): Supplier { + return Supplier { ListTag() } + } + + override fun accumulator(): BiConsumer { + return BiConsumer { t, u -> t.add(u) } + } + + override fun combiner(): BinaryOperator { + return BinaryOperator { t, u -> + if (t.size >= u.size) { + t.addAll(u) + t + } else { + u.addAll(t) + u + } + } + } + + override fun finisher(): Function { + return Function.identity() + } + + private val chars = setOf(Characteristics.IDENTITY_FINISH) + + override fun characteristics(): Set { + return chars + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/util/Formatting.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/util/Formatting.kt index e66de1df1..0f21fe07a 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/util/Formatting.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/util/Formatting.kt @@ -1,12 +1,9 @@ package ru.dbotthepony.mc.otm.core.util import it.unimi.dsi.fastutil.chars.CharArrayList -import it.unimi.dsi.fastutil.ints.IntArrayList import net.minecraft.ChatFormatting import net.minecraft.network.chat.Component import net.minecraft.network.chat.MutableComponent -import ru.dbotthepony.kommons.util.value -import ru.dbotthepony.mc.otm.capability.AbstractProfiledStorage import ru.dbotthepony.mc.otm.core.TextComponent import ru.dbotthepony.mc.otm.core.TranslatableComponent import ru.dbotthepony.mc.otm.core.math.Decimal @@ -283,22 +280,20 @@ fun formatHistory( } if (verbose.asBoolean) { - addLine(widget.lastTickReceive, widget.lastTickTransfer) - - val values = IntArrayList() - values.addAll(widget.tick downTo 0) - values.addAll(AbstractProfiledStorage.HISTORY_SIZE - 1 downTo widget.tick + 1) - - for (i in values.intIterator()) { - addLine(widget.historyReceive[i].value, widget.historyTransfer[i].value) - } + addLine(widget.receivedThisTick, widget.transferredThisTick) } addLine( - widget.weightedReceive, - widget.weightedTransfer + widget.received.calcWeightedAverage(), + widget.transferred.calcWeightedAverage() ) + if (verbose.asBoolean) { + for (i in 0 until 20) { + addLine(widget.received[i], widget.transferred[i]) + } + } + val maxWidthDeltaNumber = lines.maxOf { it.delta.formatted.length } val maxWidthInNumber = lines.maxOf { it.incoming.formatted.length } val maxWidthOutNumber = lines.maxOf { it.outgoing.formatted.length } @@ -310,7 +305,7 @@ fun formatHistory( fun Part.format(widthNumbers: Int, widthSi: Int): MutableComponent { return TranslatableComponent( prefix.formatLocaleKey, - resplice("-".repeat(max(widthNumbers - formatted.length, 0)) + formatted, decimals) + "-".repeat(max(widthSi - prefix.length, 0)), + resplice("_".repeat(max(widthNumbers - formatted.length, 0)) + formatted, decimals) + "_".repeat(max(widthSi - prefix.length, 0)), suffix ) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/DecimalCodec.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/DecimalCodec.kt index 353a4578a..35477d652 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/data/DecimalCodec.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/DecimalCodec.kt @@ -10,6 +10,7 @@ import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.codec.StreamCodec import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.core.math.Decimal.Companion.fromByteArray +import ru.dbotthepony.mc.otm.network.wrap import java.nio.ByteBuffer object DecimalCodec : Codec { @@ -21,7 +22,7 @@ object DecimalCodec : Codec { override fun encode(buf: FriendlyByteBuf, p_320396_: Decimal) { buf.writeByteArray(p_320396_.toByteArray()) } - } + }.wrap() override fun encode(input: Decimal, ops: DynamicOps, prefix: T): DataResult { if (ops === NbtOps.INSTANCE) { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt index 5a6f8f484..5a82ef272 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt @@ -14,7 +14,6 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload import net.minecraft.resources.ResourceLocation import net.minecraft.server.level.ServerPlayer import net.minecraft.world.Container -import net.minecraft.world.SimpleContainer import net.minecraft.world.entity.player.Inventory import net.minecraft.world.entity.player.Player import net.minecraft.world.inventory.AbstractContainerMenu @@ -32,26 +31,18 @@ import ru.dbotthepony.kommons.util.Delegate import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.mc.otm.OverdriveThatMatters -import ru.dbotthepony.mc.otm.capability.IMatteryUpgrade -import ru.dbotthepony.mc.otm.capability.MatteryCapability -import ru.dbotthepony.mc.otm.capability.UpgradeType import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage import ru.dbotthepony.mc.otm.capability.matteryPlayer import ru.dbotthepony.mc.otm.compat.cos.cosmeticArmorSlots import ru.dbotthepony.mc.otm.compat.curios.curiosSlots import ru.dbotthepony.mc.otm.compat.curios.isCurioSlot import ru.dbotthepony.mc.otm.container.IMatteryContainer -import ru.dbotthepony.mc.otm.container.UpgradeContainer import ru.dbotthepony.mc.otm.container.computeSortedIndices import ru.dbotthepony.mc.otm.container.sortWithIndices import ru.dbotthepony.mc.otm.core.ResourceLocation -import ru.dbotthepony.mc.otm.core.collect.ConditionalEnumSet import ru.dbotthepony.mc.otm.core.collect.ConditionalSet -import ru.dbotthepony.mc.otm.core.immutableList import ru.dbotthepony.mc.otm.core.math.Decimal -import ru.dbotthepony.mc.otm.menu.input.InstantBooleanInput import ru.dbotthepony.mc.otm.menu.widget.ProfiledLevelGaugeWidget -import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.mc.otm.network.MatteryStreamCodec import ru.dbotthepony.mc.otm.network.MenuDataPacket import ru.dbotthepony.mc.otm.network.SetCarriedPacket @@ -60,13 +51,12 @@ import ru.dbotthepony.mc.otm.network.decode import ru.dbotthepony.mc.otm.network.encode import ru.dbotthepony.mc.otm.network.nullable import ru.dbotthepony.mc.otm.network.readByteListUnbounded +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.mc.otm.network.wrap import ru.dbotthepony.mc.otm.network.writeByteListUnbounded import java.util.* import java.util.function.BooleanSupplier import java.util.function.Consumer -import java.util.function.DoubleSupplier -import java.util.function.IntSupplier import java.util.function.Predicate data class PlayerSlot(val functional: A, val cosmetic: B? = null) @@ -85,7 +75,7 @@ abstract class MatteryMenu( /** * Server->Client synchronizer */ - val mSynchronizer = DelegateSyncher() + val mSynchronizer = Syncher() val synchronizerRemote = mSynchronizer.Remote() val player: Player get() = inventory.player @@ -133,7 +123,7 @@ abstract class MatteryMenu( /** * Client->Server input */ - inner class PlayerInput(val codec: MatteryStreamCodec, allowSpectators: Boolean = false, val handler: (V) -> Unit) : Consumer, Predicate { + inner class PlayerInput(val codec: MatteryStreamCodec, allowSpectators: Boolean = false, val handler: (V) -> Unit) : Consumer, Predicate { val id = playerInputs.size var allowSpectators by mSynchronizer.boolean(allowSpectators) @@ -209,7 +199,7 @@ abstract class MatteryMenu( val exopackChargeSlots: List = Collections.unmodifiableList(_exopackChargeSlots) - val exopackPowerLevel = ProfiledLevelGaugeWidget>(mSynchronizer) + val exopackPowerLevel = ProfiledLevelGaugeWidget>(mSynchronizer, player.matteryPlayer.exopackEnergy) var sortInventoryInput: SortInput? = null private set @@ -326,8 +316,6 @@ abstract class MatteryMenu( for (i in 0 until mattery.exopackChargeSlots.containerSize) _exopackChargeSlots.add(ChargeSlot(mattery.exopackChargeSlots, i).also { mapQuickMoveToExternal(it); mapQuickMoveToInventory(it); addSlot(it) }) - - exopackPowerLevel.with(mattery.exopackEnergy) } sortInventoryInput = SortInput(mattery.inventoryAndExopackNoHotbar, playerSortSettings) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/PainterMenu.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/PainterMenu.kt index c7964e42b..701608634 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/PainterMenu.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/PainterMenu.kt @@ -48,7 +48,7 @@ class PainterMenu( val inputContainer = MatteryContainer(::rescan, 1) val outputContainer = MatteryContainer(1) private var lastRecipe: RecipeHolder? = null - var selectedRecipe by mSynchronizer.Slot(ListenableDelegate.Box(null), StreamCodecs.RESOURCE_LOCATION.nullable()).also { it.addListener(Runnable { rescan() }) } + var selectedRecipe by mSynchronizer.add(ListenableDelegate.Box(null), StreamCodecs.RESOURCE_LOCATION.nullable()).also { it.addListener(Runnable { rescan() }) } val isBulk = BooleanInputWithFeedback(this, tile?.let { it::isBulk }) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/storage/DriveViewerMenu.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/storage/DriveViewerMenu.kt index 5861fd82d..0382a5ab1 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/storage/DriveViewerMenu.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/storage/DriveViewerMenu.kt @@ -44,7 +44,7 @@ class DriveViewerMenu( private val powered: PoweredVirtualComponent? private var lastDrive: IMatteryDrive? = null - val profiledEnergy = ProfiledLevelGaugeWidget>(this, energyWidget) + val profiledEnergy = ProfiledLevelGaugeWidget>(this, tile?.energy, energyWidget) val energyConfig = EnergyConfigPlayerInput(this, tile?.energyConfig) val settings = object : DriveViewerBlockEntity.ISettings { @@ -62,7 +62,6 @@ class DriveViewerMenu( init { if (tile != null) { - profiledEnergy.with(tile.energy) powered = PoweredVirtualComponent(StorageStack.ITEMS, tile::energy) this.networkedItemView.component = powered } else { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/FluidGaugeWidget.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/FluidGaugeWidget.kt index b4229775a..8704ad54b 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/FluidGaugeWidget.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/FluidGaugeWidget.kt @@ -2,7 +2,7 @@ package ru.dbotthepony.mc.otm.menu.widget import net.neoforged.neoforge.fluids.FluidStack import net.neoforged.neoforge.fluids.capability.IFluidHandler -import ru.dbotthepony.mc.otm.network.DelegateSyncher +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.mc.otm.container.get import ru.dbotthepony.mc.otm.menu.MatteryMenu @@ -10,7 +10,7 @@ import ru.dbotthepony.mc.otm.network.wrap import java.util.function.IntSupplier import java.util.function.Supplier -class FluidGaugeWidget(synchronizer: DelegateSyncher) { +class FluidGaugeWidget(synchronizer: Syncher) { constructor(menu: MatteryMenu) : this(menu.mSynchronizer) var maxCapacitySupplier = IntSupplier { 0 } @@ -27,7 +27,7 @@ class FluidGaugeWidget(synchronizer: DelegateSyncher) { return (fluid.amount.toFloat() / maxCapacity.toFloat()).coerceIn(0f, 1f) } - constructor(synchronizer: DelegateSyncher, fluid: IFluidHandler?, tank: Int = 0) : this(synchronizer) { + constructor(synchronizer: Syncher, fluid: IFluidHandler?, tank: Int = 0) : this(synchronizer) { if (fluid != null) { with(fluid, tank) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/LevelGaugeWidget.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/LevelGaugeWidget.kt index 7fd0bdb86..b7f61ffba 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/LevelGaugeWidget.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/LevelGaugeWidget.kt @@ -1,6 +1,5 @@ package ru.dbotthepony.mc.otm.menu.widget -import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage import ru.dbotthepony.mc.otm.capability.matter.IMatterStorage @@ -8,9 +7,10 @@ import ru.dbotthepony.mc.otm.capability.matter.IPatternStorage import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.menu.MatteryMenu import ru.dbotthepony.mc.otm.network.StreamCodecs +import ru.dbotthepony.mc.otm.network.syncher.Syncher @Suppress("unused") -class LevelGaugeWidget(synchronizer: DelegateSyncher) { +class LevelGaugeWidget(synchronizer: Syncher) { constructor(menu: MatteryMenu) : this(menu.mSynchronizer) var levelProvider = { Decimal.ONE } @@ -56,7 +56,7 @@ class LevelGaugeWidget(synchronizer: DelegateSyncher) { } constructor( - synchronizer: DelegateSyncher, + synchronizer: Syncher, power: IMatteryEnergyStorage? ) : this(synchronizer) { if (power != null) { @@ -65,7 +65,7 @@ class LevelGaugeWidget(synchronizer: DelegateSyncher) { } constructor( - synchronizer: DelegateSyncher, + synchronizer: Syncher, matter: IMatterStorage? ) : this(synchronizer) { if (matter != null) { @@ -74,7 +74,7 @@ class LevelGaugeWidget(synchronizer: DelegateSyncher) { } constructor( - synchronizer: DelegateSyncher, + synchronizer: Syncher, patterns: IPatternStorage? ) : this(synchronizer) { if (patterns != null) { @@ -83,7 +83,7 @@ class LevelGaugeWidget(synchronizer: DelegateSyncher) { } constructor( - synchronizer: DelegateSyncher, + synchronizer: Syncher, level: () -> Decimal, maxLevel: () -> Decimal, ) : this(synchronizer) { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProfiledLevelGaugeWidget.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProfiledLevelGaugeWidget.kt index dfbf3afd6..8540e4fba 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProfiledLevelGaugeWidget.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProfiledLevelGaugeWidget.kt @@ -1,64 +1,40 @@ package ru.dbotthepony.mc.otm.menu.widget -import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.mc.otm.capability.AbstractProfiledStorage +import ru.dbotthepony.mc.otm.capability.AbstractProfiledStorage.Companion.HISTORY_SIZE import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage import ru.dbotthepony.mc.otm.capability.matter.IMatterStorage -import ru.dbotthepony.mc.otm.core.collect.SupplierList -import ru.dbotthepony.mc.otm.core.immutableList +import ru.dbotthepony.mc.otm.core.DecimalHistoryGraph import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.menu.MatteryMenu -import ru.dbotthepony.mc.otm.menu.MatteryPoweredMenu import ru.dbotthepony.mc.otm.network.StreamCodecs -import java.util.function.IntSupplier +import ru.dbotthepony.mc.otm.network.syncher.Syncher -class ProfiledLevelGaugeWidget

>(synchronizer: DelegateSyncher, val gauge: LevelGaugeWidget = LevelGaugeWidget(synchronizer)) { - var parent: P? = null - private set +class ProfiledLevelGaugeWidget

>( + synchronizer: Syncher, + val storage: P?, + val gauge: LevelGaugeWidget = LevelGaugeWidget(synchronizer) +) { + constructor( + menu: MatteryMenu, + storage: P?, + gauge: LevelGaugeWidget = LevelGaugeWidget(menu.mSynchronizer) + ) : this(menu.mSynchronizer, storage, gauge) - val historyReceive = immutableList(AbstractProfiledStorage.HISTORY_SIZE) { - synchronizer.computed({ parent?.historyReceive?.get(it) ?: Decimal.ZERO }, StreamCodecs.DECIMAL) - } + val received = storage?.received ?: DecimalHistoryGraph(ticks = HISTORY_SIZE) + val transferred = storage?.transferred ?: DecimalHistoryGraph(ticks = HISTORY_SIZE) - val historyTransfer = immutableList(AbstractProfiledStorage.HISTORY_SIZE) { - synchronizer.computed({ parent?.historyTransfer?.get(it) ?: Decimal.ZERO }, StreamCodecs.DECIMAL) - } + val receivedThisTick by synchronizer.computed({ storage?.receivedThisTick ?: Decimal.ZERO }, StreamCodecs.DECIMAL) + val transferredThisTick by synchronizer.computed({ storage?.transferredThisTick ?: Decimal.ZERO }, StreamCodecs.DECIMAL) - val directHistoryReceive = SupplierList(historyReceive) - val directHistoryTransfer = SupplierList(historyTransfer) - - val tick by synchronizer.computedInt(IntSupplier { parent?.tick ?: 0 }) - val lastTickReceive by synchronizer.computed({ parent?.lastTickReceive ?: Decimal.ZERO }, StreamCodecs.DECIMAL) - val lastTickTransfer by synchronizer.computed({ parent?.lastTickTransfer ?: Decimal.ZERO }, StreamCodecs.DECIMAL) - - constructor(synchronizer: DelegateSyncher, storage: P?, gauge: LevelGaugeWidget = LevelGaugeWidget(synchronizer)) : this(synchronizer, gauge = gauge) { - if (storage != null) { - with(storage) - } - } - - constructor(menu: MatteryMenu, storage: P?, gauge: LevelGaugeWidget = LevelGaugeWidget(menu)) : this(menu.mSynchronizer, storage, gauge = gauge) - constructor(menu: MatteryMenu, gauge: LevelGaugeWidget = LevelGaugeWidget(menu)) : this(menu.mSynchronizer, gauge = gauge) - - constructor(menu: MatteryPoweredMenu, storage: P?) : this(menu.mSynchronizer, storage, menu.energyWidget) - constructor(menu: MatteryPoweredMenu) : this(menu.mSynchronizer, menu.energyWidget) - - fun with(storage: P): ProfiledLevelGaugeWidget

{ + init { if (storage is IMatterStorage) gauge.with(storage) else if (storage is IMatteryEnergyStorage) gauge.with(storage) - parent = storage - return this - } - - val weightedTransfer: Decimal get() { - return AbstractProfiledStorage.calcWeightedAverage(directHistoryTransfer, tick) - } - - val weightedReceive: Decimal get() { - return AbstractProfiledStorage.calcWeightedAverage(directHistoryReceive, tick) + synchronizer.add(received) + synchronizer.add(transferred) } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProgressGaugeWidget.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProgressGaugeWidget.kt index ae0034a0d..99014f864 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProgressGaugeWidget.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/widget/ProgressGaugeWidget.kt @@ -1,6 +1,6 @@ package ru.dbotthepony.mc.otm.menu.widget -import ru.dbotthepony.mc.otm.network.DelegateSyncher +import ru.dbotthepony.mc.otm.network.syncher.Syncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.mc.otm.block.entity.MachineJobEventLoop import ru.dbotthepony.mc.otm.block.entity.MatteryWorkerBlockEntity @@ -9,7 +9,7 @@ import ru.dbotthepony.mc.otm.menu.MatteryMenu import java.util.function.BooleanSupplier @Suppress("unused") -class ProgressGaugeWidget(synchronizer: DelegateSyncher) { +class ProgressGaugeWidget(synchronizer: Syncher) { constructor(menu: MatteryMenu) : this(menu.mSynchronizer) var progressSupplier: FloatSupplier = FloatSupplier { 0f } @@ -44,14 +44,14 @@ class ProgressGaugeWidget(synchronizer: DelegateSyncher) { } constructor( - synchronizer: DelegateSyncher, + synchronizer: Syncher, progress: FloatSupplier ) : this(synchronizer) { this.progressSupplier = progress } constructor( - synchronizer: DelegateSyncher, + synchronizer: Syncher, blockEntity: MatteryWorkerBlockEntity<*>? ) : this(synchronizer) { if (blockEntity != null) { @@ -60,7 +60,7 @@ class ProgressGaugeWidget(synchronizer: DelegateSyncher) { } constructor( - synchronizer: DelegateSyncher, + synchronizer: Syncher, job: MachineJobEventLoop<*>? ) : this(synchronizer) { if (job != null) { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/DelegateSyncher.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/DelegateSyncher.kt deleted file mode 100644 index e5ece6295..000000000 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/DelegateSyncher.kt +++ /dev/null @@ -1,857 +0,0 @@ -package ru.dbotthepony.mc.otm.network - -import io.netty.buffer.ByteBufAllocator -import it.unimi.dsi.fastutil.bytes.ByteArrayList -import it.unimi.dsi.fastutil.ints.IntAVLTreeSet -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap -import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet -import net.minecraft.core.RegistryAccess -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.world.item.ItemStack -import net.neoforged.neoforge.network.connection.ConnectionType -import ru.dbotthepony.kommons.collect.ListenableMap -import ru.dbotthepony.kommons.collect.ListenableSet -import ru.dbotthepony.kommons.util.Delegate -import ru.dbotthepony.kommons.util.DelegateGetter -import ru.dbotthepony.kommons.util.DelegateSetter -import ru.dbotthepony.kommons.util.KOptional -import ru.dbotthepony.kommons.util.Listenable -import ru.dbotthepony.kommons.util.ListenableDelegate -import ru.dbotthepony.kommons.util.Observer -import ru.dbotthepony.kommons.util.ValueObserver -import ru.dbotthepony.mc.otm.core.math.Decimal -import java.io.Closeable -import java.util.* -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.locks.ReentrantLock -import java.util.function.BooleanSupplier -import java.util.function.Consumer -import java.util.function.DoubleSupplier -import java.util.function.IntSupplier -import java.util.function.LongSupplier -import java.util.function.Supplier -import kotlin.concurrent.withLock - -/** - * Universal, one-to-many delegate/value synchronizer. - * - * Values and delegates can be attached using [add], or by constructing subclassed directly. - * Delta changes are tracked by [DelegateSyncher.Remote] instances. - * - * In general, this class is not meant to be _structurally_ concurrently mutated by different threads, - * to avoid structure corruption a lock is employed. - * - * Attached delegates can be safely mutated by multiple threads concurrently - * (attached [ListenableDelegate] can call [Listenable.addListener] callback from different threads). - */ -@Suppress("UNCHECKED_CAST", "UNUSED") -class DelegateSyncher : Observer { - private val lock = ReentrantLock() - private val slots = ArrayList() - private val gaps = IntAVLTreeSet() - private val observers = ArrayList() - private val remotes = ArrayList() - private var isRemote = false - - override fun observe(): Boolean { - var any = false - observers.forEach { any = it.observe() || any } - return any - } - - fun read(stream: RegistryFriendlyByteBuf) { - isRemote = true - - var readID = stream.readVarInt() - - while (readID > 0) { - val slot = slots.getOrNull(readID - 1) ?: throw IndexOutOfBoundsException("Unknown networked slot ${readID - 1}!") - slot.read(stream) - readID = stream.readVarInt() - } - } - - fun read(registry: RegistryAccess, bytes: ByteArrayList) { - return decodePayload(registry, bytes) { - read(it) - } - } - - abstract inner class AbstractSlot : Closeable, Observer, Comparable { - val id: Int - - protected val isRemoved = AtomicBoolean() - - fun remove() { - if (isRemoved.compareAndSet(false, true)) { - lock.withLock { - slots[id] = null - gaps.add(id) - - remoteSlots.forEach { - it.remove() - } - - remoteSlots.clear() - remove0() - } - } - } - - final override fun close() { - return remove() - } - - final override fun compareTo(other: AbstractSlot): Int { - return id.compareTo(other.id) - } - - protected abstract fun remove0() - internal abstract fun read(stream: RegistryFriendlyByteBuf) - internal abstract fun write(stream: RegistryFriendlyByteBuf) - - internal val remoteSlots = CopyOnWriteArrayList() - - protected fun markDirty() { - if (!isRemote && !isRemoved.get()) { - remoteSlots.forEach { - it.markDirty() - } - } - } - - init { - lock.withLock { - if (gaps.isNotEmpty()) { - val gap = gaps.firstInt() - gaps.remove(gap) - id = gap - check(slots[id] == null) { "Expected slot $id to be empty" } - slots[id] = this - } else { - id = slots.size - slots.add(this) - } - - remotes.forEach { - it.addSlot(this) - } - } - } - } - - inner class Slot(val delegate: ListenableDelegate, val codec: MatteryStreamCodec) : AbstractSlot(), ListenableDelegate { - constructor(delegate: Supplier, codec: MatteryStreamCodec) : this(ListenableDelegate.CustomShadow(delegate, codec::compare, codec::copy), codec) - - private val l = delegate.addListener(Consumer { - markDirty() - listeners.accept(it) - }) - - init { - if (delegate is ListenableDelegate.AbstractShadow) { - observers.add(this) - } - } - - private val listeners = Listenable.Impl() - - override fun remove0() { - l.remove() - listeners.clear() - } - - override fun read(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - delegate.accept(codec.decode(stream)) - } - - override fun write(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - codec.encode(stream, delegate.get()) - } - - override fun get(): V { - check(!isRemoved.get()) { "This network slot was removed" } - return delegate.get() - } - - override fun accept(t: V) { - check(!isRemoved.get()) { "This network slot was removed" } - delegate.accept(t) - } - - override fun addListener(listener: Consumer): Listenable.L { - check(!isRemoved.get()) { "This network slot was removed" } - return listeners.addListener(listener) - } - - override fun observe(): Boolean { - check(!isRemoved.get()) { "This network slot was removed" } - - if (delegate is ListenableDelegate.AbstractShadow) { - return delegate.observe() - } - - return false - } - } - - inner class ObservedSlot(val delegate: Delegate, val codec: MatteryStreamCodec) : AbstractSlot(), ListenableDelegate, ValueObserver { - private val listeners = Listenable.Impl() - private var observed: Any? = Mark - private val observeLock = ReentrantLock() - - init { - observers.add(this) - } - - override fun remove0() { - listeners.clear() - } - - override fun read(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - val read = codec.decode(stream) - observed = codec.copy(read) - delegate.accept(read) - } - - override fun write(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - codec.encode(stream, delegate.get()) - } - - override fun observe(): Boolean { - val get = delegate.get() - - if (observed === Mark || !codec.compare(get, observed as V)) { - observeLock.withLock { - if (observed === Mark || !codec.compare(get, observed as V)) { - observed = codec.copy(get) - markDirty() - listeners.accept(get) - } - - return true - } - } - - return false - } - - override fun getAndObserve(): Pair { - val get = delegate.get() - - if (observed === Mark || !codec.compare(get, observed as V)) { - observeLock.withLock { - if (observed === Mark || !codec.compare(get, observed as V)) { - observed = codec.copy(get) - markDirty() - listeners.accept(get) - } - - return get to true - } - } - - return get to false - } - - override fun get(): V { - check(!isRemoved.get()) { "This network slot was removed" } - return delegate.get() - } - - override fun accept(t: V) { - check(!isRemoved.get()) { "This network slot was removed" } - delegate.accept(t) - } - - override fun addListener(listener: Consumer): Listenable.L { - check(!isRemoved.get()) { "This network slot was removed" } - return listeners.addListener(listener) - } - } - - private object Mark - - internal data class SetAction(val value: KOptional, val action: Int) - - inner class SetSlot(val delegate: ListenableSet, val codec: MatteryStreamCodec) : AbstractSlot() { - private val listener = object : ListenableSet.SetListener { - override fun onClear() { - remoteSlots.forEach { - (it as Remote.RemoteSetSlot).changelist.clear() - - synchronized(it.changelistLock) { - it.changelist.add(SetAction(KOptional(), CLEAR)) - } - - listeners.forEach { - it.listener.onClear() - } - - it.markDirty() - } - } - - override fun onValueAdded(element: V) { - remoteSlots.forEach { - it as Remote.RemoteSetSlot - - synchronized(it.changelistLock) { - it.changelist.removeIf { it.value.valueEquals(element) } - it.changelist.add(SetAction(KOptional(codec.copy(element)), ADD)) - } - - listeners.forEach { - it.listener.onValueAdded(element) - } - - it.markDirty() - } - } - - override fun onValueRemoved(element: V) { - remoteSlots.forEach { - it as Remote.RemoteSetSlot - - synchronized(it.changelistLock) { - it.changelist.removeIf { it.value.valueEquals(element) } - it.changelist.add(SetAction(KOptional(codec.copy(element)), REMOVE)) - } - - listeners.forEach { - it.listener.onValueRemoved(element) - } - - it.markDirty() - } - } - } - - private val l = delegate.addListener(listener) - private val listeners = CopyOnWriteArrayList() - - override fun remove0() { - l.remove() - listeners.clear() - } - - private inner class Listener(val listener: ListenableSet.SetListener): Listenable.L { - private var isRemoved = false - - init { - listeners.add(this) - } - - override fun remove() { - if (!isRemoved) { - isRemoved = true - listeners.remove(this) - } - } - } - - fun addListener(listener: ListenableSet.SetListener): Listenable.L { - check(!isRemoved.get()) { "This network slot was removed" } - return Listener(listener) - } - - fun addListener(listener: Runnable): Listenable.L { - check(!isRemoved.get()) { "This network slot was removed" } - return Listener(ListenableSet.RunnableAdapter(listener)) - } - - override fun read(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - var action = stream.readByte().toInt() - - while (true) { - when (action) { - ADD -> delegate.add(codec.decode(stream)) - REMOVE -> delegate.remove(codec.decode(stream)) - CLEAR -> delegate.clear() - else -> break - } - - action = stream.readByte().toInt() - } - } - - override fun write(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - throw RuntimeException("unreachable code") - } - - override fun observe(): Boolean { - check(!isRemoved.get()) { "This network slot was removed" } - return false - } - } - - internal data class MapAction(val key: KOptional, val value: KOptional, val action: Int) - - inner class MapSlot(val delegate: ListenableMap, val keyCodec: MatteryStreamCodec, val valueCodec: MatteryStreamCodec) : AbstractSlot() { - private val listener = object : ListenableMap.MapListener { - override fun onClear() { - remoteSlots.forEach { - it as Remote.RemoteMapSlot - - synchronized(it.changelistLock) { - it.changelist.clear() - it.changelist.add(MapAction(KOptional(), KOptional(), CLEAR)) - } - - listeners.forEach { - it.listener.onClear() - } - - it.markDirty() - } - } - - override fun onValueAdded(key: K, value: V) { - remoteSlots.forEach { - it as Remote.RemoteMapSlot - - synchronized(it.changelistLock) { - it.changelist.removeIf { it.key.valueEquals(key) } - it.changelist.add(MapAction(KOptional(keyCodec.copy(key)), KOptional(valueCodec.copy(value)), ADD)) - } - - listeners.forEach { - it.listener.onValueAdded(key, value) - } - - it.markDirty() - } - } - - override fun onValueRemoved(key: K, value: V) { - remoteSlots.forEach { - it as Remote.RemoteMapSlot - - synchronized(it.changelistLock) { - it.changelist.removeIf { it.key.valueEquals(key) } - it.changelist.add(MapAction(KOptional(keyCodec.copy(key)), KOptional(), REMOVE)) - } - - listeners.forEach { - it.listener.onValueRemoved(key, value) - } - - it.markDirty() - } - } - } - - private val l = delegate.addListener(listener) - private val listeners = CopyOnWriteArrayList() - - override fun remove0() { - l.remove() - listeners.clear() - } - - private inner class Listener(val listener: ListenableMap.MapListener): Listenable.L { - private var isRemoved = false - - init { - listeners.add(this) - } - - override fun remove() { - if (!isRemoved) { - isRemoved = true - listeners.remove(this) - } - } - } - - fun addListener(listener: ListenableMap.MapListener): Listenable.L { - check(!isRemoved.get()) { "This network slot was removed" } - return Listener(listener) - } - - fun addListener(listener: Runnable): Listenable.L { - check(!isRemoved.get()) { "This network slot was removed" } - return Listener(ListenableMap.RunnableAdapter(listener)) - } - - override fun read(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - var action = stream.readByte().toInt() - - while (true) { - when (action) { - ADD -> delegate.put(keyCodec.decode(stream), valueCodec.decode(stream)) - REMOVE -> delegate.remove(keyCodec.decode(stream)) - CLEAR -> delegate.clear() - else -> break - } - - action = stream.readByte().toInt() - } - } - - override fun write(stream: RegistryFriendlyByteBuf) { - check(!isRemoved.get()) { "This network slot was removed" } - throw RuntimeException("unreachable code") - } - - override fun observe(): Boolean { - check(!isRemoved.get()) { "This network slot was removed" } - return false - } - } - - fun add(value: V, codec: MatteryStreamCodec, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return Slot(ListenableDelegate.maskSmart(value, getter, setter), codec) - } - - fun add(delegate: ListenableDelegate, codec: MatteryStreamCodec): Slot { - return Slot(delegate, codec) - } - - fun add(delegate: Delegate, codec: MatteryStreamCodec): ObservedSlot { - return ObservedSlot(delegate, codec) - } - - fun add(delegate: Supplier, codec: MatteryStreamCodec): Slot { - return computed(delegate, codec) - } - - fun add(delegate: ListenableSet, codec: MatteryStreamCodec): SetSlot { - return SetSlot(delegate, codec) - } - - fun add(delegate: ListenableMap, keyCodec: MatteryStreamCodec, valueCodec: MatteryStreamCodec): MapSlot { - return MapSlot(delegate, keyCodec, valueCodec) - } - - fun computedByte(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.BYTE) - fun computedShort(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.SHORT) - fun computedInt(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.INT) - fun computedLong(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.VAR_LONG) - fun computedFloat(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.FLOAT) - fun computedDouble(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.DOUBLE) - fun computedBoolean(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.BOOLEAN) - fun computedString(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.STRING) - fun computedUUID(delegate: Supplier) = Slot(ListenableDelegate.Shadow(delegate), StreamCodecs.UUID) - fun computed(delegate: Supplier, codec: MatteryStreamCodec) = Slot(ListenableDelegate.CustomShadow(delegate, codec::compare, codec::copy), codec) - - fun computedInt(delegate: IntSupplier) = Slot(ListenableDelegate.Shadow(delegate::getAsInt), StreamCodecs.VAR_INT) - fun computedLong(delegate: LongSupplier) = Slot(ListenableDelegate.Shadow(delegate::getAsLong), StreamCodecs.VAR_LONG) - fun computedDouble(delegate: DoubleSupplier) = Slot(ListenableDelegate.Shadow(delegate::getAsDouble), StreamCodecs.DOUBLE) - fun computedBoolean(delegate: BooleanSupplier) = Slot(ListenableDelegate.Shadow(delegate::getAsBoolean), StreamCodecs.BOOLEAN) - - @JvmName("vbyte") - @JvmOverloads - fun byte(value: Byte = 0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.BYTE) - } - - @JvmName("vshort") - @JvmOverloads - fun short(value: Short = 0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.SHORT) - } - - @JvmName("vint") - @JvmOverloads - fun int(value: Int = 0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.VAR_INT) - } - - @JvmName("vlong") - @JvmOverloads - fun long(value: Long = 0L, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.VAR_LONG) - } - - @JvmName("vfloat") - @JvmOverloads - fun float(value: Float = 0f, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.FLOAT) - } - - @JvmName("vdouble") - @JvmOverloads - fun double(value: Double = 0.0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.DOUBLE) - } - - @JvmName("vboolean") - @JvmOverloads - fun boolean(value: Boolean = false, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.BOOLEAN) - } - - @JvmOverloads - fun string(value: String, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.STRING) - } - - @JvmOverloads - fun uuid(value: UUID, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.UUID) - } - - @JvmOverloads - fun set(codec: MatteryStreamCodec, backing: MutableSet = ObjectOpenHashSet()): SetSlot { - return add(ListenableSet(backing), codec) - } - - @JvmOverloads - fun map(keyCodec: MatteryStreamCodec, valueCodec: MatteryStreamCodec, backing: MutableMap = Object2ObjectOpenHashMap()): MapSlot { - return add(ListenableMap(backing), keyCodec, valueCodec) - } - - @JvmOverloads - fun > enum(value: E, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), MatteryStreamCodec.Enum(value::class.java)) - } - - fun decimal(value: Decimal = Decimal.ZERO, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): DelegateSyncher.Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.DECIMAL) - } - - fun computedDecimal(delegate: Supplier): DelegateSyncher.Slot { - return add(delegate, StreamCodecs.DECIMAL) - } - - fun item(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): DelegateSyncher.Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), ItemStack.OPTIONAL_STREAM_CODEC.wrap()) - } - - fun computedItem(delegate: Supplier): DelegateSyncher.Slot { - return computed(delegate, ItemStack.OPTIONAL_STREAM_CODEC.wrap()) - } - - fun observedItem(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): DelegateSyncher.ObservedSlot { - return add(Delegate.maskSmart(value, getter, setter), ItemStack.OPTIONAL_STREAM_CODEC.wrap()) - } - - /** - * Remotes must be [close]d when no longer in use, otherwise they will - * leak memory and decrease performance of syncher. - * - * To get changes to be networked to remote client, [write] should be called. - */ - inner class Remote : Closeable { - @Volatile - private var isRemoved = false - internal val dirty = ConcurrentLinkedQueue() - private val remoteSlots = CopyOnWriteArrayList() - - internal open inner class RemoteSlot(val parent: AbstractSlot) : Comparable { - private val isDirty = AtomicBoolean() - - final override fun compareTo(other: RemoteSlot): Int { - return parent.compareTo(other.parent) - } - - init { - remoteSlots.add(this) - dirty.add(this) - } - - fun markDirty() { - if (!isRemoved && isDirty.compareAndSet(false, true)) { - dirty.add(this) - } - } - - fun markClean() { - isDirty.set(false) - } - - open fun invalidate() { - markDirty() - } - - open fun remove() { - isDirty.set(true) - remoteSlots.remove(this) - dirty.remove(this) - } - - open fun write(stream: RegistryFriendlyByteBuf) { - parent.write(stream) - } - - override fun hashCode(): Int { - return parent.hashCode() - } - - override fun equals(other: Any?): Boolean { - return this === other || other is Remote.RemoteSlot && other.parent == parent - } - } - - internal inner class RemoteSetSlot(parent: SetSlot) : RemoteSlot(parent) { - val changelist = ArrayList>() - val changelistLock = Any() - - override fun remove() { - super.remove() - changelist.clear() - } - - override fun invalidate() { - synchronized(changelistLock) { - changelist.clear() - changelist.add(SetAction(KOptional(), CLEAR)) - - parent as SetSlot - - for (v in parent.delegate) { - changelist.add(SetAction(KOptional(parent.codec.copy(v)), ADD)) - } - } - - super.invalidate() - } - - override fun write(stream: RegistryFriendlyByteBuf) { - synchronized(changelistLock) { - for (change in changelist) { - stream.writeByte(change.action) - change.value.ifPresent { (parent as SetSlot).codec.encode(stream, it) } - } - - changelist.clear() - } - - stream.writeByte(0) - } - } - - internal inner class RemoteMapSlot(parent: MapSlot) : RemoteSlot(parent) { - val changelist = ArrayList>() - val changelistLock = Any() - - override fun remove() { - super.remove() - changelist.clear() - } - - override fun invalidate() { - synchronized(changelistLock) { - changelist.clear() - changelist.add(MapAction(KOptional(), KOptional(), CLEAR)) - - parent as MapSlot - - for ((k, v) in parent.delegate) { - changelist.add(MapAction(KOptional(parent.keyCodec.copy(k)), KOptional(parent.valueCodec.copy(v)), ADD)) - } - } - - super.invalidate() - } - - override fun write(stream: RegistryFriendlyByteBuf) { - synchronized(changelistLock) { - for (change in changelist) { - stream.writeByte(change.action) - change.key.ifPresent { (parent as MapSlot).keyCodec.encode(stream, it) } - change.value.ifPresent { (parent as MapSlot).valueCodec.encode(stream, it) } - } - - changelist.clear() - } - - stream.writeByte(0) - } - } - - init { - lock.withLock { - slots.forEach { - if (it != null) { - addSlot(it) - } - } - - remotes.add(this) - } - } - - internal fun addSlot(slot: AbstractSlot) { - if (slot is SetSlot<*>) { - slot.remoteSlots.add(RemoteSetSlot(slot)) - } else if (slot is MapSlot<*, *>) { - slot.remoteSlots.add(RemoteMapSlot(slot)) - } else { - slot.remoteSlots.add(RemoteSlot(slot)) - } - } - - /** - * Returns null if this remote is clean. - * - * [DelegateSyncher.observe] is not called automatically for performance - * reasons, you must call it manually. - */ - fun write(registry: RegistryAccess): ByteArrayList? { - if (dirty.isNotEmpty()) { - val sorted = ObjectAVLTreeSet() - var next = dirty.poll() - - while (next != null) { - sorted.add(next) - next.markClean() - next = dirty.poll() - } - - return encodePayload(registry) { - for (slot in sorted) { - it.writeVarInt(slot.parent.id + 1) - slot.write(it) - } - - it.writeByte(0) - } - } - - return null - } - - /** - * Marks all networked slots dirty - */ - fun invalidate() { - remoteSlots.forEach { it.invalidate() } - } - - override fun close() { - if (!isRemoved) { - lock.withLock { - if (!isRemoved) { - remoteSlots.forEach { - it.remove() - it.parent.remoteSlots.remove(it) - } - - remotes.remove(this) - } - } - - dirty.clear() - } - } - } - - companion object { - private const val END = 0 - private const val CLEAR = 1 - private const val ADD = 2 - private const val REMOVE = 3 - } -} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt index 11e59ee7d..d01605af8 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt @@ -12,7 +12,7 @@ import java.io.DataInputStream import java.io.DataOutputStream import java.util.NoSuchElementException -interface MatteryStreamCodec : StreamCodec { +interface MatteryStreamCodec : StreamCodec<@UnsafeVariance S, V> { fun copy(value: V): V { return value } @@ -25,9 +25,9 @@ interface MatteryStreamCodec : StreamCodec { override fun decode(stream: S): V override fun encode(stream: S, value: V) - class Wrapper(parent: StreamCodec) : MatteryStreamCodec, StreamCodec by parent + class Wrapper(parent: StreamCodec) : MatteryStreamCodec, StreamCodec<@UnsafeVariance S, V> by parent - class Nullable(val parent: MatteryStreamCodec) : MatteryStreamCodec { + class Nullable(val parent: MatteryStreamCodec) : MatteryStreamCodec { override fun decode(stream: S): V? { return if (!stream.readBoolean()) null else parent.decode(stream) } @@ -52,7 +52,7 @@ interface MatteryStreamCodec : StreamCodec { } } - class Optional(val parent: MatteryStreamCodec) : MatteryStreamCodec> { + class Optional(val parent: MatteryStreamCodec) : MatteryStreamCodec> { override fun decode(stream: S): java.util.Optional { return if (!stream.readBoolean()) java.util.Optional.empty() else java.util.Optional.of(parent.decode(stream)) } @@ -77,7 +77,7 @@ interface MatteryStreamCodec : StreamCodec { } } - class Collection>(val elementCodec: MatteryStreamCodec, val collectionFactory: (Int) -> C) : MatteryStreamCodec { + class Collection>(val elementCodec: MatteryStreamCodec, val collectionFactory: (Int) -> C) : MatteryStreamCodec { override fun decode(stream: S): C { val size = stream.readVarInt() @@ -106,7 +106,7 @@ interface MatteryStreamCodec : StreamCodec { } } - class Enum>(clazz: Class) : MatteryStreamCodec { + class Enum>(clazz: Class) : MatteryStreamCodec { val clazz = searchClass(clazz) val values: List = listOf(*this.clazz.enumConstants!!) val valuesMap = values.associateBy { it.name } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/IRemoteState.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/IRemoteState.kt new file mode 100644 index 000000000..6ec12381f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/IRemoteState.kt @@ -0,0 +1,10 @@ +package ru.dbotthepony.mc.otm.network.syncher + +import net.minecraft.network.RegistryFriendlyByteBuf +import java.io.Closeable + +interface IRemoteState : Closeable { + fun write(stream: RegistryFriendlyByteBuf) + + fun invalidate() +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/ISynchable.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/ISynchable.kt new file mode 100644 index 000000000..4b6cf43eb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/ISynchable.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.mc.otm.network.syncher + +import net.minecraft.network.RegistryFriendlyByteBuf +import ru.dbotthepony.kommons.util.Observer + +interface ISynchable : Observer { + fun read(stream: RegistryFriendlyByteBuf) + + /** + * Provided [listener] is to be considered thread-safe, and can be called at any time + * + * Created [IRemoteState] is implied to have [IRemoteState.invalidate] called implicitly internally + */ + fun createRemoteState(listener: Runnable): IRemoteState + + val shouldBeObserved: Boolean + get() = false + + override fun observe(): Boolean { + // no op + return false + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableDelegate.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableDelegate.kt new file mode 100644 index 000000000..b4e2a1de7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableDelegate.kt @@ -0,0 +1,63 @@ +package ru.dbotthepony.mc.otm.network.syncher + +import net.minecraft.network.RegistryFriendlyByteBuf +import ru.dbotthepony.kommons.util.Listenable +import ru.dbotthepony.kommons.util.ListenableDelegate +import ru.dbotthepony.kommons.util.Observer +import ru.dbotthepony.mc.otm.network.MatteryStreamCodec +import java.util.function.Consumer +import java.util.function.Supplier + +class SynchableDelegate(val delegate: ListenableDelegate, val codec: MatteryStreamCodec) : ISynchable, ListenableDelegate, Observer { + constructor(delegate: Supplier, codec: MatteryStreamCodec) : this(ListenableDelegate.CustomShadow(delegate, codec::compare, codec::copy), codec) + + private val listeners = Listenable.Impl() + private val l = delegate.addListener(listeners) + + override fun read(stream: RegistryFriendlyByteBuf) { + delegate.accept(codec.decode(stream)) + } + + inner class Remote(listener: Runnable) : IRemoteState { + private val l = listeners.addListener(listener) + + override fun write(stream: RegistryFriendlyByteBuf) { + codec.encode(stream, delegate.get()) + } + + override fun invalidate() { + // do nothing + } + + override fun close() { + l.remove() + } + } + + override fun createRemoteState(listener: Runnable): IRemoteState { + return Remote(listener) + } + + override fun accept(t: V) { + delegate.accept(t) + } + + override fun addListener(listener: Consumer): Listenable.L { + return listeners.addListener(listener) + } + + override fun get(): V { + return delegate.get() + } + + override val shouldBeObserved: Boolean + get() = delegate is ListenableDelegate.AbstractShadow + + override fun observe(): Boolean { + if (delegate is ListenableDelegate.AbstractShadow) { + return delegate.observe() + } + + return false + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableMap.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableMap.kt new file mode 100644 index 000000000..fc905fe6e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableMap.kt @@ -0,0 +1,122 @@ +package ru.dbotthepony.mc.otm.network.syncher + +import net.minecraft.network.RegistryFriendlyByteBuf +import ru.dbotthepony.kommons.collect.ListenableMap +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.mc.otm.network.MatteryStreamCodec +import java.util.ArrayList +import java.util.concurrent.CopyOnWriteArrayList + +class SynchableMap( + val delegate: ListenableMap, + val keyCodec: MatteryStreamCodec, + val valueCodec: MatteryStreamCodec +) : ISynchable { + override fun read(stream: RegistryFriendlyByteBuf) { + var action = stream.readByte().toInt() + + while (true) { + when (action) { + ADD -> delegate.put(keyCodec.decode(stream), valueCodec.decode(stream)) + REMOVE -> delegate.remove(keyCodec.decode(stream)) + CLEAR -> delegate.clear() + else -> break + } + + action = stream.readByte().toInt() + } + } + + private val remoteSlots = CopyOnWriteArrayList() + private data class MapAction(val key: KOptional, val value: KOptional, val action: Int) + + private inner class RemoteState(val listener: Runnable) : IRemoteState, ListenableMap.MapListener { + private val changelist = ArrayList>() + private val changelistLock = Any() + private val l = delegate.addListener(this) + + init { + remoteSlots.add(this) + } + + override fun write(stream: RegistryFriendlyByteBuf) { + synchronized(changelistLock) { + for (change in changelist) { + stream.writeByte(change.action) + change.key.ifPresent { keyCodec.encode(stream, it) } + change.value.ifPresent { valueCodec.encode(stream, it) } + } + + changelist.clear() + } + + stream.writeByte(END) + } + + override fun invalidate() { + synchronized(changelistLock) { + changelist.clear() + changelist.add(MapAction(KOptional(), KOptional(), CLEAR)) + + for ((k, v) in delegate) { + changelist.add(MapAction(KOptional(keyCodec.copy(k)), KOptional(valueCodec.copy(v)), ADD)) + } + } + } + + override fun close() { + l.remove() + remoteSlots.remove(this) + + synchronized(changelistLock) { + changelist.clear() + } + } + + override fun onClear() { + synchronized(changelistLock) { + changelist.clear() + changelist.add(MapAction(KOptional(), KOptional(), CLEAR)) + } + + listener.run() + } + + override fun onValueAdded(key: K, value: V) { + synchronized(changelistLock) { + changelist.removeIf { it.key.valueEquals(key) } + changelist.add(MapAction(KOptional(keyCodec.copy(key)), KOptional(valueCodec.copy(value)), ADD)) + } + + listener.run() + } + + override fun onValueRemoved(key: K, value: V) { + synchronized(changelistLock) { + changelist.removeIf { it.key.valueEquals(key) } + changelist.add(MapAction(KOptional(keyCodec.copy(key)), KOptional(), REMOVE)) + } + + listener.run() + } + + init { + invalidate() + } + } + + override fun createRemoteState(listener: Runnable): IRemoteState { + return RemoteState(listener) + } + + override fun observe(): Boolean { + return false + } + + companion object { + private const val END = 0 + private const val CLEAR = 1 + private const val ADD = 2 + private const val REMOVE = 3 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableObservedDelegate.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableObservedDelegate.kt new file mode 100644 index 000000000..5753f7175 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableObservedDelegate.kt @@ -0,0 +1,94 @@ +package ru.dbotthepony.mc.otm.network.syncher + +import net.minecraft.network.RegistryFriendlyByteBuf +import ru.dbotthepony.kommons.util.Delegate +import ru.dbotthepony.kommons.util.Listenable +import ru.dbotthepony.kommons.util.ListenableDelegate +import ru.dbotthepony.kommons.util.ValueObserver +import ru.dbotthepony.mc.otm.network.MatteryStreamCodec +import java.util.concurrent.locks.ReentrantLock +import java.util.function.Consumer +import kotlin.concurrent.withLock + +class SynchableObservedDelegate(val delegate: Delegate, val codec: MatteryStreamCodec) : ISynchable, ListenableDelegate, ValueObserver { + private object Mark + + private val listeners = Listenable.Impl() + private var observed: Any? = Mark + private val observeLock = ReentrantLock() + + override fun read(stream: RegistryFriendlyByteBuf) { + val read = codec.decode(stream) + observed = codec.copy(read) + delegate.accept(read) + } + + override fun observe(): Boolean { + val get = delegate.get() + + if (observed === Mark || !codec.compare(get, observed as V)) { + observeLock.withLock { + if (observed === Mark || !codec.compare(get, observed as V)) { + observed = codec.copy(get) + listeners.accept(get) + } + + return true + } + } + + return false + } + + override fun getAndObserve(): Pair { + val get = delegate.get() + + if (observed === Mark || !codec.compare(get, observed as V)) { + observeLock.withLock { + if (observed === Mark || !codec.compare(get, observed as V)) { + observed = codec.copy(get) + listeners.accept(get) + } + + return get to true + } + } + + return get to false + } + + override fun get(): V { + return delegate.get() + } + + override fun accept(t: V) { + delegate.accept(t) + } + + override val shouldBeObserved: Boolean + get() = true + + override fun addListener(listener: Consumer): Listenable.L { + return listeners.addListener(listener) + } + + private inner class RemoteState(listener: Runnable) : IRemoteState { + private val l = listeners.addListener(listener) + + override fun write(stream: RegistryFriendlyByteBuf) { + codec.encode(stream, delegate.get()) + } + + override fun invalidate() { + // do nothing + } + + override fun close() { + l.remove() + } + } + + override fun createRemoteState(listener: Runnable): IRemoteState { + return RemoteState(listener) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableSet.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableSet.kt new file mode 100644 index 000000000..f5dd7f1d1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/SynchableSet.kt @@ -0,0 +1,119 @@ +package ru.dbotthepony.mc.otm.network.syncher + +import net.minecraft.network.RegistryFriendlyByteBuf +import ru.dbotthepony.kommons.collect.ListenableSet +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.Listenable +import ru.dbotthepony.mc.otm.network.MatteryStreamCodec +import java.util.ArrayList +import java.util.concurrent.CopyOnWriteArrayList + +class SynchableSet(val delegate: ListenableSet, val codec: MatteryStreamCodec) : ISynchable { + private data class SetAction(val value: KOptional, val action: Int) + private val remoteSlots = CopyOnWriteArrayList() + + private inner class RemoteState(val listener: Runnable) : IRemoteState, ListenableSet.SetListener { + private val changelist = ArrayList>() + private val changelistLock = Any() + private val l = delegate.addListener(this) + + init { + remoteSlots.add(this) + } + + override fun write(stream: RegistryFriendlyByteBuf) { + synchronized(changelistLock) { + for (change in changelist) { + stream.writeByte(change.action) + change.value.ifPresent { codec.encode(stream, it) } + } + + changelist.clear() + } + + stream.writeByte(END) + } + + override fun invalidate() { + synchronized(changelistLock) { + changelist.clear() + changelist.add(SetAction(KOptional(), CLEAR)) + + for (v in delegate) { + changelist.add(SetAction(KOptional(codec.copy(v)), ADD)) + } + } + } + + override fun close() { + remoteSlots.remove(this) + l.remove() + + synchronized(changelistLock) { + changelist.clear() + } + } + + override fun onClear() { + changelist.clear() + + synchronized(changelistLock) { + changelist.add(SetAction(KOptional(), CLEAR)) + } + + listener.run() + } + + override fun onValueAdded(element: V) { + synchronized(changelistLock) { + changelist.removeIf { it.value.valueEquals(element) } + changelist.add(SetAction(KOptional(codec.copy(element)), ADD)) + } + + listener.run() + } + + override fun onValueRemoved(element: V) { + synchronized(changelistLock) { + changelist.removeIf { it.value.valueEquals(element) } + changelist.add(SetAction(KOptional(codec.copy(element)), REMOVE)) + } + + listener.run() + } + + init { + invalidate() + } + } + + override fun createRemoteState(listener: Runnable): IRemoteState { + return RemoteState(listener) + } + + override fun observe(): Boolean { + return false + } + + override fun read(stream: RegistryFriendlyByteBuf) { + var action = stream.readByte().toInt() + + while (true) { + when (action) { + ADD -> delegate.add(codec.decode(stream)) + REMOVE -> delegate.remove(codec.decode(stream)) + CLEAR -> delegate.clear() + else -> break + } + + action = stream.readByte().toInt() + } + } + + companion object { + private const val END = 0 + private const val CLEAR = 1 + private const val ADD = 2 + private const val REMOVE = 3 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/Syncher.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/Syncher.kt new file mode 100644 index 000000000..1fa0f7600 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/syncher/Syncher.kt @@ -0,0 +1,443 @@ +package ru.dbotthepony.mc.otm.network.syncher + +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet +import net.minecraft.core.RegistryAccess +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.world.item.ItemStack +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.collect.ListenableMap +import ru.dbotthepony.kommons.collect.ListenableSet +import ru.dbotthepony.kommons.util.Delegate +import ru.dbotthepony.kommons.util.DelegateGetter +import ru.dbotthepony.kommons.util.DelegateSetter +import ru.dbotthepony.kommons.util.Listenable +import ru.dbotthepony.kommons.util.ListenableDelegate +import ru.dbotthepony.kommons.util.Observer +import ru.dbotthepony.mc.otm.OTM_CLEANER +import ru.dbotthepony.mc.otm.core.math.Decimal +import ru.dbotthepony.mc.otm.network.MatteryStreamCodec +import ru.dbotthepony.mc.otm.network.StreamCodecs +import ru.dbotthepony.mc.otm.network.decodePayload +import ru.dbotthepony.mc.otm.network.encodePayload +import ru.dbotthepony.mc.otm.network.wrap +import java.io.Closeable +import java.lang.ref.Cleaner.Cleanable +import java.lang.ref.WeakReference +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReentrantLock +import java.util.function.BooleanSupplier +import java.util.function.DoubleSupplier +import java.util.function.IntSupplier +import java.util.function.LongSupplier +import java.util.function.Supplier +import kotlin.collections.ArrayList +import kotlin.concurrent.withLock + +/** + * Universal, one-to-many delegate/value synchronizer. + * + * Values and delegates can be attached using [add], or by constructing subclassed directly. + * Delta changes are tracked by [Syncher.Remote] instances. + * + * In general, this class is not meant to be _structurally_ concurrently mutated by different threads, + * to avoid structure corruption a lock is employed. + * + * Attached delegates can be safely mutated by multiple threads concurrently + * (attached [ListenableDelegate] can call [Listenable.addListener] callback from different threads). + */ +@Suppress("UNUSED") +class Syncher : Observer, ISynchable { + private val lock = ReentrantLock() + private val slots = ArrayList() + private val gaps = IntAVLTreeSet() + private val observers = ArrayList() + private val remotes = ArrayList() + private var isRemote = false + + override fun observe(): Boolean { + var any = false + observers.forEach { any = it.observe() || any } + return any + } + + override val shouldBeObserved: Boolean + get() = true + + override fun createRemoteState(listener: Runnable): IRemoteState { + return Remote(listener) + } + + override fun read(stream: RegistryFriendlyByteBuf) { + isRemote = true + + var readID = stream.readVarInt() + + while (readID > 0) { + val slot = lock.withLock { slots.getOrNull(readID - 1) ?: throw IndexOutOfBoundsException("Unknown networked slot ${readID - 1}!") } + slot.synchable.read(stream) + readID = stream.readVarInt() + } + } + + fun read(registry: RegistryAccess, bytes: ByteArrayList) { + return decodePayload(registry, bytes) { + read(it) + } + } + + internal inner class Slot(val synchable: ISynchable) : Closeable, Observer { + val id: Int + + private val isRemoved = AtomicBoolean() + val remoteSlots = CopyOnWriteArrayList() + + override fun close() { + if (isRemoved.compareAndSet(false, true)) { + lock.withLock { + slots[id] = null + gaps.add(id) + + observers.remove(this) + remoteSlots.forEach { it.remove() } + } + } + } + + override fun observe(): Boolean { + return synchable.observe() + } + + private fun markDirty() { + if (!isRemote && !isRemoved.get()) { + remoteSlots.forEach { + it.markDirty() + } + } + } + + init { + lock.withLock { + if (synchable.shouldBeObserved) { + observers.add(this) + } + + if (gaps.isNotEmpty()) { + val gap = gaps.firstInt() + gaps.remove(gap) + id = gap + check(slots[id] == null) { "Expected slot $id to be empty" } + slots[id] = this + } else { + id = slots.size + slots.add(this) + } + + remotes.forEach { + it.addSlot(this) + } + } + } + } + + fun add0(synchable: ISynchable): Closeable { + return Slot(synchable) + } + + fun add(synchable: T): T { + Slot(synchable) + return synchable + } + + fun add(value: V, codec: MatteryStreamCodec, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(SynchableDelegate(ListenableDelegate.maskSmart(value, getter, setter), codec)) + } + + fun add(delegate: ListenableDelegate, codec: MatteryStreamCodec): SynchableDelegate { + return add(SynchableDelegate(delegate, codec)) + } + + fun add(delegate: Delegate, codec: MatteryStreamCodec): SynchableObservedDelegate { + return add(SynchableObservedDelegate(delegate, codec)) + } + + fun add(delegate: Supplier, codec: MatteryStreamCodec): SynchableDelegate { + return computed(delegate, codec) + } + + fun add(delegate: ListenableSet, codec: MatteryStreamCodec): SynchableSet { + return add(SynchableSet(delegate, codec)) + } + + fun add(delegate: ListenableMap, keyCodec: MatteryStreamCodec, valueCodec: MatteryStreamCodec): SynchableMap { + return add(SynchableMap(delegate, keyCodec, valueCodec)) + } + + fun computedByte(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.BYTE)) + fun computedShort(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.SHORT)) + fun computedInt(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.INT)) + fun computedLong(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.VAR_LONG)) + fun computedFloat(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.FLOAT)) + fun computedDouble(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.DOUBLE)) + fun computedBoolean(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.BOOLEAN)) + fun computedString(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.STRING)) + fun computedUUID(delegate: Supplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.UUID)) + fun computed(delegate: Supplier, codec: MatteryStreamCodec) = add(SynchableDelegate(ListenableDelegate.CustomShadow(delegate, codec::compare, codec::copy), codec)) + + fun computedInt(delegate: IntSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsInt), StreamCodecs.VAR_INT)) + fun computedLong(delegate: LongSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsLong), StreamCodecs.VAR_LONG)) + fun computedDouble(delegate: DoubleSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsDouble), StreamCodecs.DOUBLE)) + fun computedBoolean(delegate: BooleanSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsBoolean), StreamCodecs.BOOLEAN)) + + @JvmName("vbyte") + @JvmOverloads + fun byte(value: Byte = 0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.BYTE) + } + + @JvmName("vshort") + @JvmOverloads + fun short(value: Short = 0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.SHORT) + } + + @JvmName("vint") + @JvmOverloads + fun int(value: Int = 0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.VAR_INT) + } + + @JvmName("vlong") + @JvmOverloads + fun long(value: Long = 0L, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.VAR_LONG) + } + + @JvmName("vfloat") + @JvmOverloads + fun float(value: Float = 0f, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.FLOAT) + } + + @JvmName("vdouble") + @JvmOverloads + fun double(value: Double = 0.0, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.DOUBLE) + } + + @JvmName("vboolean") + @JvmOverloads + fun boolean(value: Boolean = false, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.BOOLEAN) + } + + @JvmOverloads + fun string(value: String, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.STRING) + } + + @JvmOverloads + fun uuid(value: UUID, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.UUID) + } + + @JvmOverloads + fun set(codec: MatteryStreamCodec, backing: MutableSet = ObjectOpenHashSet()): SynchableSet { + return add(ListenableSet(backing), codec) + } + + @JvmOverloads + fun map(keyCodec: MatteryStreamCodec, valueCodec: MatteryStreamCodec, backing: MutableMap = Object2ObjectOpenHashMap()): SynchableMap { + return add(ListenableMap(backing), keyCodec, valueCodec) + } + + @JvmOverloads + fun > enum(value: E, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), MatteryStreamCodec.Enum(value::class.java)) + } + + fun decimal(value: Decimal = Decimal.ZERO, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.DECIMAL) + } + + fun computedDecimal(delegate: Supplier): SynchableDelegate { + return add(delegate, StreamCodecs.DECIMAL) + } + + fun item(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableDelegate { + return add(ListenableDelegate.maskSmart(value, getter, setter), ItemStack.OPTIONAL_STREAM_CODEC.wrap()) + } + + fun computedItem(delegate: Supplier): SynchableDelegate { + return computed(delegate, ItemStack.OPTIONAL_STREAM_CODEC.wrap()) + } + + fun observedItem(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): SynchableObservedDelegate { + return add(Delegate.maskSmart(value, getter, setter), ItemStack.OPTIONAL_STREAM_CODEC.wrap()) + } + + /** + * Remotes must be [close]d when no longer in use, otherwise they will + * leak memory and decrease performance of syncher. + * + * To get changes to be networked to remote client, [write] should be called. + */ + inner class Remote(private val listener: Runnable) : Closeable, IRemoteState { + constructor() : this(Runnable { }) + + @Volatile + private var isRemoved = false + internal val dirty = ConcurrentLinkedQueue() + private val remoteSlots = CopyOnWriteArrayList() + + internal inner class RemoteSlot(val slot: Slot) { + private val isDirty = AtomicBoolean() + + init { + remoteSlots.add(this) + dirty.add(this) + } + + fun markDirty() { + if (!isRemoved && isDirty.compareAndSet(false, true)) { + dirty.add(this) + listener.run() + } + } + + private val state: IRemoteState + private val cleanable: Cleanable + + init { + val weakSelf = WeakReference(this) + val state = slot.synchable.createRemoteState(Runnable { + weakSelf.get()?.markDirty() + }) + + // !!! + // hOLY SHIT + val weakState = WeakReference(state) + cleanable = OTM_CLEANER.register(this) { weakState.get()?.close() } + this.state = state + } + + fun markClean() { + isDirty.set(false) + } + + fun invalidate() { + state.invalidate() + markDirty() + } + + fun write(stream: RegistryFriendlyByteBuf) { + state.write(stream) + } + + fun remove() { + isDirty.set(true) + remoteSlots.remove(this) + dirty.remove(this) + cleanable.clean() + } + + override fun hashCode(): Int { + return slot.hashCode() + } + + override fun equals(other: Any?): Boolean { + return this === other || other is RemoteSlot && other.slot == slot + } + } + + init { + lock.withLock { + slots.forEach { + if (it != null) { + addSlot(it) + } + } + + remotes.add(this) + } + } + + internal fun addSlot(slot: Slot) { + slot.remoteSlots.add(RemoteSlot(slot)) + } + + /** + * Returns null if this remote is clean. + * + * [Syncher.observe] is not called automatically for performance + * reasons, you must call it manually. + */ + fun write(registry: RegistryAccess): ByteArrayList? { + if (dirty.isNotEmpty()) { + return encodePayload(registry) { + write(it) + } + } + + return null + } + + /** + * Marks all networked slots dirty + */ + override fun invalidate() { + remoteSlots.forEach { it.invalidate() } + } + + override fun write(stream: RegistryFriendlyByteBuf) { + val seen = ReferenceOpenHashSet() + val sorted = ArrayList() + var next = dirty.poll() + + while (next != null) { + val status = seen.add(next) + next.markClean() + + if (status) { + sorted.add(next) + } + + next = dirty.poll() + } + + for (slot in sorted) { + stream.writeVarInt(slot.slot.id + 1) + slot.write(stream) + } + + stream.writeByte(0) + } + + override fun close() { + if (!isRemoved) { + lock.withLock { + if (!isRemoved) { + remoteSlots.forEach { + it.remove() + it.slot.remoteSlots.remove(it) + } + + remotes.remove(this) + } + } + + dirty.clear() + } + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +}