From aa5d36c48806bbd1a83b2e4b9a611ca87483706a Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 10 Aug 2024 23:26:17 +0700 Subject: [PATCH] Re-implement DelegateSyncher using Mojang's StreamCodec --- .../mc/otm/android/AndroidFeature.kt | 14 +- .../mc/otm/android/AndroidResearch.kt | 11 +- .../mc/otm/android/AndroidResearchManager.kt | 4 +- .../mc/otm/block/entity/MatteryBlockEntity.kt | 9 +- .../mc/otm/block/entity/RedstoneControl.kt | 2 +- .../mc/otm/capability/MatteryPlayer.kt | 49 +- .../energy/BatteryBackedEnergyStorage.kt | 2 +- .../mc/otm/container/MatteryContainer.kt | 2 +- .../mc/otm/core/util/StreamCodecs.kt | 46 - .../ru/dbotthepony/mc/otm/menu/MatteryMenu.kt | 86 +- .../mc/otm/menu/widget/FluidGaugeWidget.kt | 2 +- .../mc/otm/menu/widget/LevelGaugeWidget.kt | 2 +- .../menu/widget/ProfiledLevelGaugeWidget.kt | 2 +- .../mc/otm/menu/widget/ProgressGaugeWidget.kt | 2 +- .../mc/otm/network/AndroidPackets.kt | 27 +- .../mc/otm/network/DelegateSyncher.kt | 857 ++++++++++++++++++ .../ru/dbotthepony/mc/otm/network/Ext.kt | 66 ++ .../mc/otm/network/MatteryPlayerPackets.kt | 17 +- .../mc/otm/network/MatteryStreamCodec.kt | 127 +++ .../mc/otm/network/MenuDataPacket.kt | 16 +- .../mc/otm/network/StreamCodecs.kt | 34 +- 21 files changed, 1177 insertions(+), 200 deletions(-) delete mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/core/util/StreamCodecs.kt create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/network/DelegateSyncher.kt create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt 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 b7cdbbb44..97700ceba 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidFeature.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidFeature.kt @@ -1,16 +1,15 @@ package ru.dbotthepony.mc.otm.android -import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream 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.kommons.io.DelegateSyncher +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.mc.otm.capability.MatteryPlayer import ru.dbotthepony.mc.otm.core.nbt.set -import java.io.InputStream abstract class AndroidFeature(val type: AndroidFeatureType<*>, val android: MatteryPlayer) : INBTSerializable { val ply get() = android.ply @@ -39,15 +38,6 @@ abstract class AndroidFeature(val type: AndroidFeatureType<*>, val android: Matt open fun onHurt(event: LivingIncomingDamageEvent) {} - open fun collectNetworkPayload(): FastByteArrayOutputStream? { - syncher.observe() - return syncherRemote.write() - } - - open fun applyNetworkPayload(stream: InputStream) { - syncher.read(stream) - } - override fun serializeNBT(registry: HolderLookup.Provider): CompoundTag { return CompoundTag().also { it["level"] = level 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 aeb204908..8b64f5553 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.kommons.io.DelegateSyncher +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.mc.otm.OverdriveThatMatters @@ -173,15 +173,6 @@ class AndroidResearch(val type: AndroidResearchType, val capability: MatteryPlay return true } - fun collectNetworkPayload(): FastByteArrayOutputStream? { - syncher.observe() - return syncherRemote.write() - } - - fun applyNetworkPayload(stream: InputStream) { - syncher.read(stream) - } - val canResearch: Boolean get() { if (!consumeResearchCost(simulate = true)) { return false diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt index 5190b4dd0..00a1570ab 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt @@ -88,7 +88,7 @@ object AndroidResearchManager : SimpleJsonResourceReloadListener(GsonBuilder().s if (SERVER_IS_LIVE) { onceServer { for (ply in MINECRAFT_SERVER.playerList.players) { - ply.matteryPlayer.reloadResearch() + ply.matteryPlayer.reloadResearch(MINECRAFT_SERVER.registryAccess()) } } } @@ -134,7 +134,7 @@ object AndroidResearchManager : SimpleJsonResourceReloadListener(GsonBuilder().s } context.enqueueWork { - minecraft.player?.matteryPlayer?.reloadResearch() + minecraft.player?.matteryPlayer?.reloadResearch(MINECRAFT_SERVER.registryAccess()) } } 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 9251fb91a..218b5babd 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 @@ -1,6 +1,5 @@ package ru.dbotthepony.mc.otm.block.entity -import com.google.common.collect.ImmutableList import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap @@ -37,9 +36,8 @@ 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.kommons.io.DelegateSyncher +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 import ru.dbotthepony.mc.otm.block.RotatableMatteryBlock import ru.dbotthepony.mc.otm.capability.MatteryCapability @@ -48,25 +46,20 @@ import ru.dbotthepony.mc.otm.core.collect.WeakHashSet import ru.dbotthepony.mc.otm.core.get import ru.dbotthepony.mc.otm.core.math.BlockRotation import ru.dbotthepony.mc.otm.core.math.RelativeSide -import ru.dbotthepony.mc.otm.core.math.plus import ru.dbotthepony.mc.otm.core.util.IntCounter 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.once import ru.dbotthepony.mc.otm.onceServer import ru.dbotthepony.mc.otm.sometimeServer import java.lang.ref.WeakReference import java.util.* -import java.util.function.BooleanSupplier import java.util.function.Consumer import java.util.function.Predicate import java.util.function.Supplier import java.util.stream.Stream import kotlin.collections.ArrayList -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty /** * Absolute barebone (lol) block entity class in Overdrive that Matters, providing bare minimum (lulmao, minecraft engine) functionality 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 615c2382a..869049e7d 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.kommons.io.DelegateSyncher +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.Listenable import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue 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 c529aa144..c418d3d34 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayer.kt @@ -16,6 +16,7 @@ import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.IntTag import net.minecraft.nbt.ListTag import net.minecraft.nbt.StringTag +import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.chat.Component import net.minecraft.network.protocol.common.custom.CustomPacketPayload import net.minecraft.resources.ResourceLocation @@ -57,12 +58,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.kommons.io.DelegateSyncher -import ru.dbotthepony.kommons.io.IntValueCodec -import ru.dbotthepony.kommons.io.RGBCodec -import ru.dbotthepony.kommons.io.UUIDValueCodec -import ru.dbotthepony.kommons.io.VarIntValueCodec -import ru.dbotthepony.kommons.io.nullable +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.ListenableDelegate import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue @@ -103,7 +99,6 @@ import ru.dbotthepony.mc.otm.core.nbt.getCompoundList import ru.dbotthepony.mc.otm.core.nbt.getIntList import ru.dbotthepony.mc.otm.core.nbt.getStringList import ru.dbotthepony.mc.otm.core.nbt.set -import ru.dbotthepony.mc.otm.core.util.ItemValueCodec import ru.dbotthepony.mc.otm.core.util.Savetables import ru.dbotthepony.mc.otm.core.util.TickList import ru.dbotthepony.mc.otm.menu.ExopackInventoryMenu @@ -235,7 +230,7 @@ class MatteryPlayer(val ply: Player) { */ var exopackGlows by publicSyncher.boolean(true) - var exopackColor by publicSyncher.add(ListenableDelegate.Box(null), RGBCodec.nullable()) + var exopackColor by publicSyncher.add(ListenableDelegate.Box(null), StreamCodecs.RGBA.nullable()) /** * Tick event schedulers @@ -249,8 +244,8 @@ class MatteryPlayer(val ply: Player) { private val exopackSlotModifierMap = syncher.MapSlot( ListenableMap().also { it.addListener(Runnable { _recomputeModifiers() }) }, - keyCodec = UUIDValueCodec, - valueCodec = IntValueCodec, + keyCodec = StreamCodecs.UUID, + valueCodec = StreamCodecs.VAR_INT, ) /** @@ -267,12 +262,12 @@ class MatteryPlayer(val ply: Player) { }, backingMap = this.exopackSlotModifierMap.delegate) val regularSlotFilters = immutableList(Inventory.INVENTORY_SIZE) { - syncher.add(null, ItemValueCodec.nullable()) + syncher.add(null, StreamCodecs.ITEM_TYPE.nullable()) } val slotsChargeFlag = syncher.SetSlot( ListenableSet(IntAVLTreeSet()), - VarIntValueCodec, + StreamCodecs.VAR_INT, ).delegate private fun slotChargeToDefault() { @@ -779,7 +774,7 @@ class MatteryPlayer(val ply: Player) { } } - internal fun reloadResearch() { + internal fun reloadResearch(registry: HolderLookup.Provider) { val old = ArrayList(research.size) old.addAll(research.values) research.clear() @@ -788,7 +783,7 @@ class MatteryPlayer(val ply: Player) { val new = AndroidResearchManager.get(v.type.id) ?: continue research[new] = AndroidResearch(new, this).also { - it.deserializeNBT(v.serializeNBT()) + it.deserializeNBT(registry, v.serializeNBT(registry)) } } } @@ -808,10 +803,10 @@ class MatteryPlayer(val ply: Player) { if (ply is ServerPlayer) { feature.invalidateNetwork() - val payload = feature.collectNetworkPayload() + val payload = feature.syncherRemote.write(ply.registryAccess()) if (payload != null) { - sendNetwork(AndroidFeatureSyncPacket(feature.type, payload, null)) + sendNetwork(AndroidFeatureSyncPacket(feature.type, payload)) } } @@ -836,10 +831,10 @@ class MatteryPlayer(val ply: Player) { if (ply is ServerPlayer) { factory.invalidateNetwork() - val payload = factory.collectNetworkPayload() + val payload = factory.syncherRemote.write(ply.registryAccess()) if (payload != null) { - sendNetwork(AndroidFeatureSyncPacket(feature, payload, null)) + sendNetwork(AndroidFeatureSyncPacket(feature, payload)) } } @@ -942,7 +937,7 @@ class MatteryPlayer(val ply: Player) { tag["research"] = ListTag().also { for ((type, instance) in research) { - it.add(instance.serializeNBT().also { + it.add(instance.serializeNBT(registry).also { it["id"] = type.id.toString() }) } @@ -1031,7 +1026,7 @@ class MatteryPlayer(val ply: Player) { if (research != null) { val instance = AndroidResearch(research, this) - instance.deserializeNBT(researchTag) + instance.deserializeNBT(registry, researchTag) this.research[research] = instance if (instance.isResearched && ply is ServerPlayer) { @@ -1301,7 +1296,7 @@ class MatteryPlayer(val ply: Player) { syncher.observe() publicSyncher.observe() - val payload = privateSyncherRemote.write() + val payload = privateSyncherRemote.write(ply.registryAccess()) if (payload != null) { PacketDistributor.sendToPlayer(ply as ServerPlayer, MatteryPlayerDataPacket(payload, false)) @@ -1315,14 +1310,14 @@ class MatteryPlayer(val ply: Player) { continue } - val payload2 = remote.write() + val payload2 = remote.write(ply.registryAccess()) if (payload2 != null) { PacketDistributor.sendToPlayer(ply as ServerPlayer, MatteryPlayerDataPacket(payload2, true, this.ply.uuid)) } } - val payload3 = publicSyncherRemote.write() + val payload3 = publicSyncherRemote.write(ply.registryAccess()) if (payload3 != null) { PacketDistributor.sendToPlayer(ply as ServerPlayer, MatteryPlayerDataPacket(payload3, true)) @@ -1337,18 +1332,18 @@ class MatteryPlayer(val ply: Player) { } for (instance in research.values) { - val researchPayload = instance.collectNetworkPayload() + val researchPayload = instance.syncherRemote.write(ply.registryAccess()) if (researchPayload != null) { - sendNetwork(AndroidResearchSyncPacket(instance.type, researchPayload, null)) + sendNetwork(AndroidResearchSyncPacket(instance.type, researchPayload)) } } for (instance in featureMap.values) { - val featurePayload = instance.collectNetworkPayload() + val featurePayload = instance.syncherRemote.write(ply.registryAccess()) if (featurePayload != null) { - sendNetwork(AndroidFeatureSyncPacket(instance.type, featurePayload, null)) + sendNetwork(AndroidFeatureSyncPacket(instance.type, featurePayload)) } } 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 9bb35f2f7..94f1fae8c 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.kommons.io.DelegateSyncher +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.mc.otm.capability.FlowDirection diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/MatteryContainer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/MatteryContainer.kt index 8984bcf4f..5a0ffa29a 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/MatteryContainer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/MatteryContainer.kt @@ -24,7 +24,7 @@ import net.minecraft.world.item.Items import net.neoforged.neoforge.common.util.INBTSerializable import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.collect.ListenableMap -import ru.dbotthepony.kommons.io.DelegateSyncher +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.io.VarIntValueCodec import ru.dbotthepony.mc.otm.container.util.IContainerSlot import ru.dbotthepony.mc.otm.core.addSorted diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/util/StreamCodecs.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/util/StreamCodecs.kt deleted file mode 100644 index 7d99dd590..000000000 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/util/StreamCodecs.kt +++ /dev/null @@ -1,46 +0,0 @@ -package ru.dbotthepony.mc.otm.core.util - -import net.minecraft.world.item.ItemStack -import net.neoforged.neoforge.fluids.FluidStack -import ru.dbotthepony.kommons.io.DelegateSyncher -import ru.dbotthepony.kommons.io.StreamCodec -import ru.dbotthepony.kommons.util.Delegate -import ru.dbotthepony.kommons.util.DelegateGetter -import ru.dbotthepony.kommons.util.DelegateSetter -import ru.dbotthepony.kommons.util.ListenableDelegate -import ru.dbotthepony.mc.otm.core.math.Decimal -import ru.dbotthepony.mc.otm.core.math.readDecimal -import ru.dbotthepony.mc.otm.core.math.writeDecimal -import ru.dbotthepony.mc.otm.core.readItemType -import ru.dbotthepony.mc.otm.core.writeItemType -import java.io.DataInputStream -import java.io.DataOutputStream -import java.util.* -import java.util.function.Supplier - -val ItemStackValueCodec = StreamCodec.Impl(DataInputStream::readItem, DataOutputStream::writeItem, ItemStack::copy) { a, b -> a.equals(b, true) } -val FluidStackValueCodec = StreamCodec.Impl(DataInputStream::readFluidStack, DataOutputStream::writeFluidStack, FluidStack::copy) { a, b -> a == b && a.amount == b.amount } -val ItemValueCodec = StreamCodec.Impl(DataInputStream::readItemType, DataOutputStream::writeItemType) { a, b -> a === b } -val DecimalValueCodec = StreamCodec.Impl(DataInputStream::readDecimal, DataOutputStream::writeDecimal) -val BigDecimalValueCodec = StreamCodec.Impl(DataInputStream::readBigDecimal, DataOutputStream::writeBigDecimal) -val ResourceLocationValueCodec = StreamCodec.Impl(DataInputStream::readResourceLocation, DataOutputStream::writeResourceLocation) - -fun DelegateSyncher.decimal(value: Decimal = Decimal.ZERO, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): DelegateSyncher.Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), DecimalValueCodec) -} - -fun DelegateSyncher.computedDecimal(delegate: Supplier): DelegateSyncher.Slot { - return add(delegate, DecimalValueCodec) -} - -fun DelegateSyncher.item(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): DelegateSyncher.Slot { - return add(ListenableDelegate.maskSmart(value, getter, setter), ItemStackValueCodec) -} - -fun DelegateSyncher.computedItem(delegate: Supplier): DelegateSyncher.Slot { - return computed(delegate, ItemStackValueCodec) -} - -fun DelegateSyncher.observedItem(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = DelegateGetter.passthrough()): DelegateSyncher.ObservedSlot { - return add(Delegate.maskSmart(value, getter, setter), ItemStackValueCodec) -} 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 f19b76614..96e342ac8 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt @@ -2,14 +2,14 @@ package ru.dbotthepony.mc.otm.menu import com.google.common.collect.ImmutableList import com.mojang.datafixers.util.Pair +import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntCollection -import it.unimi.dsi.fastutil.io.FastByteArrayInputStream -import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.objects.Reference2ObjectFunction import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ReferenceArrayList import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.custom.CustomPacketPayload import net.minecraft.resources.ResourceLocation import net.minecraft.server.level.ServerPlayer @@ -23,17 +23,12 @@ import net.minecraft.world.inventory.MenuType import net.minecraft.world.inventory.Slot import net.minecraft.world.item.Item import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.enchantment.EnchantmentEffectComponents +import net.minecraft.world.item.enchantment.EnchantmentHelper +import net.minecraft.world.item.enchantment.Enchantments import net.minecraft.world.level.block.entity.BlockEntity import net.neoforged.neoforge.network.PacketDistributor import net.neoforged.neoforge.network.handling.IPayloadContext -import ru.dbotthepony.kommons.io.BinaryStringCodec -import ru.dbotthepony.kommons.io.BooleanValueCodec -import ru.dbotthepony.kommons.io.DelegateSyncher -import ru.dbotthepony.kommons.io.FloatValueCodec -import ru.dbotthepony.kommons.io.NullValueCodec -import ru.dbotthepony.kommons.io.StreamCodec -import ru.dbotthepony.kommons.io.VarIntValueCodec -import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.util.Delegate import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue @@ -43,7 +38,6 @@ 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.client.minecraft import ru.dbotthepony.mc.otm.compat.cos.cosmeticArmorSlots import ru.dbotthepony.mc.otm.compat.curios.curiosSlots import ru.dbotthepony.mc.otm.compat.curios.isCurioSlot @@ -57,19 +51,19 @@ 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.core.util.BigDecimalValueCodec -import ru.dbotthepony.mc.otm.core.util.DecimalValueCodec -import ru.dbotthepony.mc.otm.core.util.ItemStackValueCodec -import ru.dbotthepony.mc.otm.core.util.ItemValueCodec -import ru.dbotthepony.mc.otm.core.util.computedDecimal -import ru.dbotthepony.mc.otm.core.util.computedItem 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 -import java.io.DataInputStream -import java.io.DataOutputStream -import java.math.BigDecimal +import ru.dbotthepony.mc.otm.network.StreamCodecs +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.wrap +import ru.dbotthepony.mc.otm.network.writeByteListUnbounded import java.util.* import java.util.function.BooleanSupplier import java.util.function.Consumer @@ -115,13 +109,13 @@ abstract class MatteryMenu( private val playerInputs = ArrayList>() - class PlayerInputPacket(val containerId: Int, val inputId: Int, val payload: ByteArray) : CustomPacketPayload { - constructor(buff: FriendlyByteBuf) : this(buff.readVarInt(), buff.readVarInt(), ByteArray(buff.readableBytes()).also { buff.readBytes(it) }) + class PlayerInputPacket(val containerId: Int, val inputId: Int, val payload: ByteArrayList) : CustomPacketPayload { + constructor(buff: FriendlyByteBuf) : this(buff.readVarInt(), buff.readVarInt(), buff.readByteListUnbounded()) fun write(buff: FriendlyByteBuf) { buff.writeVarInt(containerId) buff.writeVarInt(inputId) - buff.writeBytes(payload) + buff.writeByteListUnbounded(payload) } fun play(context: IPayloadContext) { @@ -129,7 +123,7 @@ abstract class MatteryMenu( if (menu.containerId != containerId || !menu.stillValid(context.player())) return val input = menu.playerInputs.getOrNull(inputId) ?: return if (!input.test(context.player())) return - input.invoke(input.codec.read(DataInputStream(FastByteArrayInputStream(payload)))) + input.invoke(input.codec.decode(context.player().registryAccess(), payload)) } override fun type(): CustomPacketPayload.Type { @@ -152,7 +146,7 @@ abstract class MatteryMenu( /** * Client->Server input */ - inner class PlayerInput(val codec: StreamCodec, 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) @@ -171,16 +165,13 @@ abstract class MatteryMenu( return this } - override fun test(player: Player?): Boolean { - if (player == null) return false + override fun test(player: Player): Boolean { return filters.all { it.test(player) } } override fun accept(value: V) { - if (test(minecraft.player as Player?)) { - val stream = FastByteArrayOutputStream() - codec.write(DataOutputStream(stream), value) - PacketDistributor.sendToServer(PlayerInputPacket(containerId, id, stream.array.copyOfRange(0, stream.length))) + if (test(player)) { + PacketDistributor.sendToServer(PlayerInputPacket(containerId, id, codec.encode(player.registryAccess(), value))) } } @@ -190,7 +181,7 @@ abstract class MatteryMenu( } inner class SortInput(val container: Container, val settings: IItemStackSortingSettings) { - val input = PlayerInput(StreamCodec.Collection(VarIntValueCodec, ::IntArrayList)) { + val input = PlayerInput(MatteryStreamCodec.Collection(StreamCodecs.VAR_INT, ::IntArrayList)) { container.sortWithIndices(it) } @@ -200,20 +191,19 @@ abstract class MatteryMenu( } fun oneWayInput(allowSpectators: Boolean = false, handler: () -> Unit): PlayerInput { - return PlayerInput(NullValueCodec, allowSpectators) { + return PlayerInput(StreamCodecs.NOTHING, allowSpectators) { handler.invoke() } } - fun bigDecimalInput(allowSpectators: Boolean = false, handler: (BigDecimal) -> Unit) = PlayerInput(BigDecimalValueCodec, allowSpectators, handler) - fun decimalInput(allowSpectators: Boolean = false, handler: (Decimal) -> Unit) = PlayerInput(DecimalValueCodec, allowSpectators, handler) - fun booleanInput(allowSpectators: Boolean = false, handler: (Boolean) -> Unit) = PlayerInput(BooleanValueCodec, allowSpectators, handler) - fun itemInput(allowSpectators: Boolean = false, handler: (Item) -> Unit) = PlayerInput(ItemValueCodec, allowSpectators, handler) - fun itemStackInput(allowSpectators: Boolean = false, handler: (ItemStack) -> Unit) = PlayerInput(ItemStackValueCodec, allowSpectators, handler) - fun nullableItemInput(allowSpectators: Boolean = false, handler: (Item?) -> Unit) = PlayerInput(ItemValueCodec.nullable(), allowSpectators, handler) - fun stringInput(allowSpectators: Boolean = false, handler: (String) -> Unit) = PlayerInput(BinaryStringCodec, allowSpectators, handler) - fun floatInput(allowSpectators: Boolean = false, handler: (Float) -> Unit) = PlayerInput(FloatValueCodec, allowSpectators, handler) - fun intInput(allowSpectators: Boolean = false, handler: (Int) -> Unit) = PlayerInput(VarIntValueCodec, allowSpectators, handler) + fun decimalInput(allowSpectators: Boolean = false, handler: (Decimal) -> Unit) = PlayerInput(StreamCodecs.DECIMAL, allowSpectators, handler) + fun booleanInput(allowSpectators: Boolean = false, handler: (Boolean) -> Unit) = PlayerInput(StreamCodecs.BOOLEAN, allowSpectators, handler) + fun itemInput(allowSpectators: Boolean = false, handler: (Item) -> Unit) = PlayerInput(StreamCodecs.ITEM_TYPE, allowSpectators, handler) + fun itemStackInput(allowSpectators: Boolean = false, handler: (ItemStack) -> Unit) = PlayerInput(ItemStack.STREAM_CODEC.wrap(), allowSpectators, handler) + fun nullableItemInput(allowSpectators: Boolean = false, handler: (Item?) -> Unit) = PlayerInput(StreamCodecs.ITEM_TYPE.nullable(), allowSpectators, handler) + fun stringInput(allowSpectators: Boolean = false, handler: (String) -> Unit) = PlayerInput(StreamCodecs.STRING, allowSpectators, handler) + fun floatInput(allowSpectators: Boolean = false, handler: (Float) -> Unit) = PlayerInput(StreamCodecs.FLOAT, allowSpectators, handler) + fun intInput(allowSpectators: Boolean = false, handler: (Int) -> Unit) = PlayerInput(StreamCodecs.INT, allowSpectators, handler) /** * hotbar + inventory + Exopack (in this order) @@ -331,7 +321,7 @@ abstract class MatteryMenu( } override fun mayPickup(player: Player): Boolean { - return super.mayPickup(player) && !item.isEmpty && (player.isCreative || !hasBindingCurse(item)) + return super.mayPickup(player) && !item.isEmpty && (player.isCreative || !EnchantmentHelper.has(item, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) } override fun getMaxStackSize(): Int { @@ -402,7 +392,7 @@ abstract class MatteryMenu( beforeBroadcast() mSynchronizer.observe() - val payload = synchronizerRemote.write() + val payload = synchronizerRemote.write(player.registryAccess()) if (payload != null) { PacketDistributor.sendToPlayer(player as ServerPlayer, MenuDataPacket(containerId, payload)) @@ -459,11 +449,11 @@ abstract class MatteryMenu( val field: Delegate if (container is IMatteryContainer) { - input = PlayerInput(ItemValueCodec.nullable(), handler = { container.setSlotFilter(pSlot.slotIndex, it) }) - field = mSynchronizer.add(delegate = { container.getSlotFilter(pSlot.slotIndex) }, ItemValueCodec.nullable()) + input = PlayerInput(StreamCodecs.ITEM_TYPE_NULLABLE, handler = { container.setSlotFilter(pSlot.slotIndex, it) }) + field = mSynchronizer.add(delegate = { container.getSlotFilter(pSlot.slotIndex) }, StreamCodecs.ITEM_TYPE_NULLABLE) } else { - input = PlayerInput(ItemValueCodec.nullable(), handler = { throw UnsupportedOperationException() }) - field = mSynchronizer.add(delegate = { null }, ItemValueCodec.nullable()) + input = PlayerInput(StreamCodecs.ITEM_TYPE_NULLABLE, handler = { throw UnsupportedOperationException() }) + field = mSynchronizer.add(delegate = { null }, StreamCodecs.ITEM_TYPE_NULLABLE) } pSlot.filter = Delegate.Of(getter = field::get, setter = input::accept) 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 1b9391bdd..d54991f9b 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.kommons.io.DelegateSyncher +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.mc.otm.container.get import ru.dbotthepony.mc.otm.core.util.FluidStackValueCodec 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 3439e0d03..19ea5aca5 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,6 @@ package ru.dbotthepony.mc.otm.menu.widget -import ru.dbotthepony.kommons.io.DelegateSyncher +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 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 8261cea21..754342f86 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,6 +1,6 @@ package ru.dbotthepony.mc.otm.menu.widget -import ru.dbotthepony.kommons.io.DelegateSyncher +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.energy.IMatteryEnergyStorage 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 91ebc2df0..ae0034a0d 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.kommons.io.DelegateSyncher +import ru.dbotthepony.mc.otm.network.DelegateSyncher import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.mc.otm.block.entity.MachineJobEventLoop import ru.dbotthepony.mc.otm.block.entity.MatteryWorkerBlockEntity diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/AndroidPackets.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/AndroidPackets.kt index 136c4fc2d..8e5737585 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/AndroidPackets.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/AndroidPackets.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.mc.otm.network +import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf @@ -32,17 +33,15 @@ import ru.dbotthepony.mc.otm.registry.MRegistry import ru.dbotthepony.mc.otm.registry.MSoundEvents import java.io.ByteArrayInputStream -class AndroidFeatureSyncPacket(val type: AndroidFeatureType<*>, val dataList: FastByteArrayOutputStream?, val dataBytes: ByteArray?) : CustomPacketPayload { - fun write(buff: FriendlyByteBuf) { - dataList ?: throw NullPointerException("No byte list is present") +class AndroidFeatureSyncPacket(val type: AndroidFeatureType<*>, val data: ByteArrayList) : CustomPacketPayload { + fun write(buff: RegistryFriendlyByteBuf) { buff.writeInt(MRegistry.ANDROID_FEATURES.getId(type)) - buff.writeBytes(dataList.array, 0, dataList.length) + buff.writeBytes(data.elements(), 0, data.size) } fun play(context: IPayloadContext) { - dataBytes ?: throw NullPointerException("No data bytes array is present") val android = minecraft.player?.matteryPlayer ?: return - android.computeIfAbsent(type).applyNetworkPayload(ByteArrayInputStream(dataBytes)) + android.computeIfAbsent(type).syncher.read(data, context.player().registryAccess()) } override fun type(): CustomPacketPayload.Type { @@ -57,30 +56,28 @@ class AndroidFeatureSyncPacket(val type: AndroidFeatureType<*>, val dataList: Fa ) ) - val CODEC: StreamCodec = + val CODEC: StreamCodec = StreamCodec.ofMember(AndroidFeatureSyncPacket::write, ::read) - fun read(buff: FriendlyByteBuf): AndroidFeatureSyncPacket { + fun read(buff: RegistryFriendlyByteBuf): AndroidFeatureSyncPacket { return AndroidFeatureSyncPacket( MRegistry.ANDROID_FEATURES.byIdOrThrow(buff.readInt()), - null, ByteArray(buff.readableBytes()).also { buff.readBytes(it) } + ByteArrayList.wrap(ByteArray(buff.readableBytes()).also { buff.readBytes(it) }) ) } } } -class AndroidResearchSyncPacket(val type: AndroidResearchType, val dataList: FastByteArrayOutputStream?, val dataBytes: ByteArray?) : +class AndroidResearchSyncPacket(val type: AndroidResearchType, val dataList: ByteArrayList) : CustomPacketPayload { fun write(buff: FriendlyByteBuf) { - dataList ?: throw NullPointerException("No byte list is present") buff.writeUtf(type.id.toString()) - buff.writeBytes(dataList.array, 0, dataList.length) + buff.writeBytes(dataList.elements(), 0, dataList.size) } fun play(context: IPayloadContext) { - dataBytes ?: throw NullPointerException("No data bytes array is present") val android = minecraft.player?.matteryPlayer ?: return - android.getResearch(type).applyNetworkPayload(ByteArrayInputStream(dataBytes)) + android.getResearch(type).syncher.read(dataList, context.player().registryAccess()) } override fun type(): CustomPacketPayload.Type { @@ -101,7 +98,7 @@ class AndroidResearchSyncPacket(val type: AndroidResearchType, val dataList: Fas fun read(buff: FriendlyByteBuf): AndroidResearchSyncPacket { return AndroidResearchSyncPacket( AndroidResearchManager[net.minecraft.resources.ResourceLocation.parse(buff.readUtf())] ?: throw NoSuchElementException(), - null, ByteArray(buff.readableBytes()).also { buff.readBytes(it) } + ByteArrayList.wrap(ByteArray(buff.readableBytes()).also { buff.readBytes(it) }) ) } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/DelegateSyncher.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/DelegateSyncher.kt new file mode 100644 index 000000000..1c4dd1d90 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/DelegateSyncher.kt @@ -0,0 +1,857 @@ +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(RegistryFriendlyByteBuf(it, registry, ConnectionType.NEOFORGE)) + } + } + + 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.STREAM_CODEC.wrap()) + } + + fun computedItem(delegate: Supplier): DelegateSyncher.Slot { + return computed(delegate, ItemStack.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.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/Ext.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/Ext.kt index 649826d53..4a2200964 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/Ext.kt @@ -1,8 +1,14 @@ package ru.dbotthepony.mc.otm.network +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import net.minecraft.core.RegistryAccess import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.codec.StreamCodec import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.neoforged.neoforge.network.connection.ConnectionType import net.neoforged.neoforge.network.handling.IPayloadContext import net.neoforged.neoforge.network.registration.PayloadRegistrar import kotlin.reflect.KFunction1 @@ -18,3 +24,63 @@ fun PayloadRegistrar.playToServer( codec: StreamCodec, handler: KFunction1 ): PayloadRegistrar = playToServer(type, codec) { _, context -> handler(context) } + +inline fun encodePayload(registry: RegistryAccess, block: (RegistryFriendlyByteBuf) -> Unit): ByteArrayList { + val underlying = ByteBufAllocator.DEFAULT.buffer() + val buf = RegistryFriendlyByteBuf(underlying, registry, ConnectionType.NEOFORGE) + + try { + block.invoke(buf) + + val bytes = ByteArrayList(buf.readableBytes()) + check(bytes.size == buf.readableBytes()) + buf.readBytes(bytes.elements()) + return bytes + } finally { + underlying.release() + } +} + +inline fun decodePayload(registry: RegistryAccess, data: ByteArrayList, block: (RegistryFriendlyByteBuf) -> T): T { + val underlying = ByteBufAllocator.DEFAULT.buffer(data.size) + val buf = RegistryFriendlyByteBuf(underlying, registry, ConnectionType.NEOFORGE) + + try { + underlying.writeBytes(data.elements(), 0, data.size) + return block.invoke(buf) + } finally { + underlying.release() + } +} + +fun FriendlyByteBuf.writeByteListUnbounded(list: ByteArrayList) { + writeBytes(list.elements(), 0, list.size) +} + +fun FriendlyByteBuf.readByteListUnbounded(): ByteArrayList { + return ByteArrayList.wrap(ByteArray(readableBytes()).also(::readBytes)) +} + +fun FriendlyByteBuf.writeByteList(list: ByteArrayList) { + writeVarInt(list.size) + writeBytes(list.elements(), 0, list.size) +} + +fun FriendlyByteBuf.readByteList(): ByteArrayList { + return ByteArrayList.wrap(ByteArray(readVarInt()).also(::readBytes)) +} + +fun StreamCodec.wrap(): MatteryStreamCodec = MatteryStreamCodec.Wrapper(this) +fun MatteryStreamCodec.nullable(): MatteryStreamCodec = MatteryStreamCodec.Nullable(this) + +fun StreamCodec.encode(registry: RegistryAccess, value: V): ByteArrayList { + return encodePayload(registry) { + encode(it, value) + } +} + +fun StreamCodec.decode(registry: RegistryAccess, value: ByteArrayList): V { + return decodePayload(registry, value) { + decode(it) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryPlayerPackets.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryPlayerPackets.kt index 77c5b6d85..3d55bec93 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryPlayerPackets.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryPlayerPackets.kt @@ -1,6 +1,6 @@ package ru.dbotthepony.mc.otm.network -import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import it.unimi.dsi.fastutil.bytes.ByteArrayList import net.minecraft.core.particles.ParticleTypes import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf @@ -28,14 +28,9 @@ import ru.dbotthepony.mc.otm.core.position import ru.dbotthepony.mc.otm.core.readItem import ru.dbotthepony.mc.otm.core.writeItem import ru.dbotthepony.mc.otm.menu.ExopackInventoryMenu -import java.io.ByteArrayInputStream import java.util.* - -class MatteryPlayerDataPacket(val bytes: ByteArray, val length: Int, val isPublic: Boolean, val target: UUID? = null) : - CustomPacketPayload { - constructor(stream: FastByteArrayOutputStream, isPublic: Boolean, target: UUID? = null) : this(stream.array, stream.length, isPublic, target) - +class MatteryPlayerDataPacket(val bytes: ByteArrayList, val isPublic: Boolean, val target: UUID? = null) : CustomPacketPayload { fun write(buff: FriendlyByteBuf) { buff.writeBoolean(target != null) @@ -43,7 +38,7 @@ class MatteryPlayerDataPacket(val bytes: ByteArray, val length: Int, val isPubli buff.writeUUID(target) buff.writeBoolean(isPublic) - buff.writeBytes(bytes, 0, length) + buff.writeBytes(bytes.elements(), 0, bytes.size) } fun play(context: IPayloadContext) { @@ -56,9 +51,9 @@ class MatteryPlayerDataPacket(val bytes: ByteArray, val length: Int, val isPubli } if (isPublic) { - player.publicSyncher.read(ByteArrayInputStream(bytes, 0, length)) + player.publicSyncher.read(bytes, context.player().registryAccess()) } else { - player.syncher.read(ByteArrayInputStream(bytes, 0, length)) + player.syncher.read(bytes, context.player().registryAccess()) } } @@ -84,7 +79,7 @@ class MatteryPlayerDataPacket(val bytes: ByteArray, val length: Int, val isPubli val isPublic = buff.readBoolean() val readable = buff.readableBytes() - return MatteryPlayerDataPacket(ByteArray(readable).also(buff::readBytes), readable, isPublic, target) + return MatteryPlayerDataPacket(ByteArrayList.wrap(ByteArray(readable).also(buff::readBytes)), isPublic, target) } } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt new file mode 100644 index 000000000..2fd8afa6e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MatteryStreamCodec.kt @@ -0,0 +1,127 @@ +package ru.dbotthepony.mc.otm.network + +import io.netty.buffer.ByteBuf +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import net.minecraft.core.RegistryAccess +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeVarInt +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.NoSuchElementException + +interface MatteryStreamCodec : StreamCodec { + fun copy(value: V): V { + return value + } + + fun compare(a: V, b: V): Boolean { + return a == b + } + + // ignore kotlin compiler warning; StreamCodec<> has wrong package-wide nullability annotation + override fun decode(stream: S): V + override fun encode(stream: S, value: V) + + class Wrapper(parent: StreamCodec) : MatteryStreamCodec, StreamCodec by parent + + class Nullable(val parent: MatteryStreamCodec) : MatteryStreamCodec { + override fun decode(stream: S): V? { + return if (!stream.readBoolean()) null else parent.decode(stream) + } + + override fun encode(stream: S, value: V?) { + if (value === null) + stream.writeBoolean(false) + else { + stream.writeBoolean(true) + parent.encode(stream, value) + } + } + + override fun copy(value: V?): V? { + return if (value === null) null else parent.copy(value) + } + + override fun compare(a: V?, b: V?): Boolean { + if (a === null && b === null) return true + if (a === null || b === null) return false + return parent.compare(a, b) + } + } + + class Collection>(val elementCodec: MatteryStreamCodec, val collectionFactory: (Int) -> C) : MatteryStreamCodec { + override fun decode(stream: S): C { + val size = stream.readVarInt() + + if (size <= 0) { + return collectionFactory.invoke(0) + } + + val collection = collectionFactory.invoke(size) + + for (i in 0 until size) { + collection.add(elementCodec.decode(stream)) + } + + return collection + } + + override fun encode(stream: S, value: C) { + stream.writeVarInt(value.size) + value.forEach { elementCodec.encode(stream, it) } + } + + override fun copy(value: C): C { + val new = collectionFactory.invoke(value.size) + value.forEach { new.add(elementCodec.copy(it)) } + return new + } + } + + class Enum>(clazz: Class) : MatteryStreamCodec { + val clazz = searchClass(clazz) + val values: List = listOf(*this.clazz.enumConstants!!) + val valuesMap = values.associateBy { it.name } + + override fun decode(stream: S): V { + val id = stream.readVarInt() + return values.getOrNull(id) ?: throw NoSuchElementException("No such enum with index $id") + } + + override fun encode(stream: S, value: V) { + stream.writeVarInt(value.ordinal) + } + + override fun copy(value: V): V { + return value + } + + override fun compare(a: V, b: V): Boolean { + return a === b + } + + companion object { + /** + * FIXME: enums with abstract methods which get compiled to subclasses, whose DO NOT expose "parent's" enum constants array + * + * is there an already existing solution? + */ + fun > searchClass(clazz: Class): Class { + var search: Class<*> = clazz + + while (search.enumConstants == null && search.superclass != null) { + search = search.superclass + } + + if (search.enumConstants == null) { + throw ClassCastException("$clazz does not represent an enum or enum subclass") + } + + return search as Class + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/MenuDataPacket.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MenuDataPacket.kt index be8e83470..f981e2442 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/MenuDataPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/MenuDataPacket.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.mc.otm.network +import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.codec.StreamCodec @@ -13,22 +14,20 @@ import ru.dbotthepony.mc.otm.menu.ExopackInventoryMenu import ru.dbotthepony.mc.otm.menu.MatteryMenu import java.io.ByteArrayInputStream -class MenuDataPacket(val containerId: Int, val bytes: ByteArray, val length: Int) : CustomPacketPayload { - constructor(containerId: Int, stream: FastByteArrayOutputStream) : this(containerId, stream.array, stream.length) - +class MenuDataPacket(val containerId: Int, val bytes: ByteArrayList) : CustomPacketPayload { fun write(buff: FriendlyByteBuf) { buff.writeVarInt(containerId) - buff.writeBytes(bytes, 0, length) + buff.writeByteListUnbounded(bytes) } fun play(context: IPayloadContext) { if (containerId == ExopackInventoryMenu.CONTAINER_ID) { - minecraft.player?.matteryPlayer?.exoPackMenu?.mSynchronizer?.read(ByteArrayInputStream(bytes, 0, length)) + context.player().matteryPlayer.exoPackMenu.mSynchronizer.read(context.player().registryAccess(), bytes) } else { - val menu = minecraft.player?.containerMenu as? MatteryMenu ?: return + val menu = context.player().containerMenu as? MatteryMenu ?: return if (menu.containerId == containerId) - menu.mSynchronizer.read(ByteArrayInputStream(bytes, 0, length)) + menu.mSynchronizer.read(context.player().registryAccess(), bytes) } } @@ -49,8 +48,7 @@ class MenuDataPacket(val containerId: Int, val bytes: ByteArray, val length: Int fun read(buff: FriendlyByteBuf): MenuDataPacket { val containerId = buff.readVarInt() - val readable = buff.readableBytes() - return MenuDataPacket(containerId, ByteArray(readable).also(buff::readBytes), readable) + return MenuDataPacket(containerId, buff.readByteListUnbounded()) } } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/StreamCodecs.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/StreamCodecs.kt index a9803ffac..010d6dcc1 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/StreamCodecs.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/StreamCodecs.kt @@ -1,13 +1,37 @@ package ru.dbotthepony.mc.otm.network +import io.netty.buffer.ByteBuf +import net.minecraft.core.UUIDUtil import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.codec.StreamCodec +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.mc.otm.core.math.readDecimal +import ru.dbotthepony.mc.otm.core.math.writeDecimal +import ru.dbotthepony.mc.otm.core.readItemType +import ru.dbotthepony.mc.otm.core.writeItemType // because mojang didn't bother object StreamCodecs { - val INT: StreamCodec = StreamCodec.of(FriendlyByteBuf::writeInt, FriendlyByteBuf::readInt) - val LONG: StreamCodec = StreamCodec.of(FriendlyByteBuf::writeLong, FriendlyByteBuf::readLong) - val DOUBLE: StreamCodec = StreamCodec.of(FriendlyByteBuf::writeDouble, FriendlyByteBuf::readDouble) - val FLOAT: StreamCodec = StreamCodec.of(FriendlyByteBuf::writeFloat, FriendlyByteBuf::readFloat) - val BOOLEAN: StreamCodec = StreamCodec.of(FriendlyByteBuf::writeBoolean, FriendlyByteBuf::readBoolean) + val NOTHING: MatteryStreamCodec = StreamCodec.of({ _, _ -> }, { null }).wrap() + + val BYTE: MatteryStreamCodec = StreamCodec.of({ s, v -> s.writeByte(v.toInt()) }, ByteBuf::readByte).wrap() + val SHORT = StreamCodec.of({ s, v -> s.writeShort(v.toInt()) }, ByteBuf::readShort).wrap() + val INT = StreamCodec.of(ByteBuf::writeInt, ByteBuf::readInt).wrap() + val VAR_INT = StreamCodec.of(FriendlyByteBuf::writeVarInt, FriendlyByteBuf::readVarInt).wrap() + val LONG = StreamCodec.of(ByteBuf::writeLong, ByteBuf::readLong).wrap() + val VAR_LONG = StreamCodec.of(FriendlyByteBuf::writeVarLong, FriendlyByteBuf::readVarLong).wrap() + val DOUBLE = StreamCodec.of(ByteBuf::writeDouble, ByteBuf::readDouble).wrap() + val FLOAT = StreamCodec.of(ByteBuf::writeFloat, ByteBuf::readFloat).wrap() + val BOOLEAN = StreamCodec.of(ByteBuf::writeBoolean, ByteBuf::readBoolean).wrap() + val STRING = StreamCodec.of(FriendlyByteBuf::writeUtf, FriendlyByteBuf::readUtf).wrap() + val UUID = UUIDUtil.STREAM_CODEC.wrap() + + val RGBA: MatteryStreamCodec = StreamCodec.of( + { s, v -> s.writeFloat(v.red); s.writeFloat(v.green); s.writeFloat(v.blue); s.writeFloat(v.alpha) }, + { s -> RGBAColor(s.readFloat(), s.readFloat(), s.readFloat(), s.readFloat()) }).wrap() + + val ITEM_TYPE = StreamCodec.of(FriendlyByteBuf::writeItemType, FriendlyByteBuf::readItemType).wrap() + val ITEM_TYPE_NULLABLE = ITEM_TYPE.nullable() + val DECIMAL = StreamCodec.of(FriendlyByteBuf::writeDecimal, FriendlyByteBuf::readDecimal).wrap() }