From 37f4b7799419f10b7b1f959f59497bc227247396 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 9 Jul 2023 14:45:13 +0700 Subject: [PATCH] Exopack arbitrary inventory slots charging --- .../mc/otm/datagen/lang/English.kt | 1 + .../mc/otm/datagen/lang/Russian.kt | 1 + .../otm/capability/MatteryPlayerCapability.kt | 107 ++++++- .../ru/dbotthepony/mc/otm/client/Ext.kt | 1 + .../client/screen/ExoPackInventoryScreen.kt | 6 +- .../mc/otm/client/screen/MatteryScreen.kt | 27 +- .../screen/panels/PlayerEquipmentPanel.kt | 5 +- .../screen/panels/slot/InventorySlotPanel.kt | 50 +++ .../ru/dbotthepony/mc/otm/menu/MatteryMenu.kt | 30 +- .../{MapAction.kt => ChangesetAction.kt} | 2 +- .../network/synchronizer/FieldSynchronizer.kt | 301 ++++++++++++++++-- .../otm/network/synchronizer/MapChangeset.kt | 12 +- .../otm/network/synchronizer/SetChangeset.kt | 22 ++ 13 files changed, 510 insertions(+), 55 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt rename src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/{MapAction.kt => ChangesetAction.kt} (71%) create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/SetChangeset.kt diff --git a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/English.kt b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/English.kt index bd6ba264a..08a7414d8 100644 --- a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/English.kt +++ b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/English.kt @@ -118,6 +118,7 @@ private fun sounds(provider: MatteryLanguageProvider) { private fun misc(provider: MatteryLanguageProvider) { with(provider.english) { gui("help.slot_filters", "Hold CTRL to setup slot filters") + gui("help.slot_charging", "Hold ALT to switch slot charging") misc("needs_no_power", "Requires no power to operate") diff --git a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/Russian.kt b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/Russian.kt index ee2336182..1d6dac6ea 100644 --- a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/Russian.kt +++ b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/lang/Russian.kt @@ -126,6 +126,7 @@ private fun sounds(provider: MatteryLanguageProvider) { private fun misc(provider: MatteryLanguageProvider) { with(provider.russian) { gui("help.slot_filters", "Удерживайте CTRL для настройки фильтрации слотов") + gui("help.slot_charging", "Удерживайте ALT для переключения зарядки слотов") misc("needs_no_power", "Не требует энергии для работы") diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayerCapability.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayerCapability.kt index 691c4eea9..3a03ed84f 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayerCapability.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/capability/MatteryPlayerCapability.kt @@ -1,11 +1,14 @@ package ru.dbotthepony.mc.otm.capability +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import net.minecraft.ChatFormatting import net.minecraft.commands.Commands import net.minecraft.commands.arguments.EntityArgument import net.minecraft.core.Direction +import net.minecraft.nbt.ByteTag import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.IntTag import net.minecraft.nbt.ListTag import net.minecraft.nbt.StringTag import net.minecraft.network.chat.Component @@ -72,6 +75,7 @@ import ru.dbotthepony.mc.otm.client.minecraft import ru.dbotthepony.mc.otm.config.AndroidConfig import ru.dbotthepony.mc.otm.config.ExopackConfig import ru.dbotthepony.mc.otm.container.MatteryContainer +import ru.dbotthepony.mc.otm.container.get import ru.dbotthepony.mc.otm.container.iterator import ru.dbotthepony.mc.otm.container.stream import ru.dbotthepony.mc.otm.container.vanishCursedItems @@ -80,14 +84,18 @@ import ru.dbotthepony.mc.otm.core.collect.UUIDIntModifiersMap import ru.dbotthepony.mc.otm.core.collect.filter import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.core.math.minus +import ru.dbotthepony.mc.otm.core.nbt.getByteList 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.BooleanValueCodec import ru.dbotthepony.mc.otm.core.util.IntValueCodec 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.core.util.UUIDValueCodec +import ru.dbotthepony.mc.otm.core.util.VarIntValueCodec import ru.dbotthepony.mc.otm.menu.ExoPackInventoryMenu import ru.dbotthepony.mc.otm.network.* import ru.dbotthepony.mc.otm.network.synchronizer.FieldSynchronizer @@ -224,6 +232,27 @@ class MatteryPlayerCapability(val ply: Player) : ICapabilityProvider, INBTSerial synchronizer.Field(null, ItemValueCodec.nullable) } + val regularSlotChargeFlag = immutableList(Inventory.INVENTORY_SIZE + 4) { + synchronizer.bool() + } + + private fun slotChargeToDefault() { + // броня + regularSlotChargeFlag[36].boolean = true + regularSlotChargeFlag[37].boolean = true + regularSlotChargeFlag[38].boolean = true + regularSlotChargeFlag[39].boolean = true + } + + init { + slotChargeToDefault() + } + + val exoPackSlotsChargeFlag by synchronizer.Set( + codec = VarIntValueCodec, + backingSet = IntAVLTreeSet(), + ) + /** * Exopack container, which actually store items inside Exopack */ @@ -808,6 +837,18 @@ class MatteryPlayerCapability(val ply: Player) : ICapabilityProvider, INBTSerial } } + tag["regularSlotChargeFlag"] = ListTag().also { + for (flag in regularSlotChargeFlag) { + it.add(ByteTag.valueOf(flag.boolean)) + } + } + + tag["exoPackSlotsChargeFlag"] = ListTag().also { + for (value in exoPackSlotsChargeFlag) { + it.add(IntTag.valueOf(value)) + } + } + return tag } @@ -818,6 +859,14 @@ class MatteryPlayerCapability(val ply: Player) : ICapabilityProvider, INBTSerial filter.value = null } + for (flag in regularSlotChargeFlag) { + flag.boolean = false + } + + exoPackSlotsChargeFlag.clear() + + slotChargeToDefault() + val regularSlotFilters = tag.getStringList("regularSlotFilters") for (i in 0 until regularSlotFilters.size.coerceAtMost(this.regularSlotFilters.size)) { @@ -826,6 +875,16 @@ class MatteryPlayerCapability(val ply: Player) : ICapabilityProvider, INBTSerial this.regularSlotFilters[i].value = ForgeRegistries.ITEMS.getValue(ResourceLocation.tryParse(path) ?: continue) ?: Items.AIR } + val regularSlotChargeFlag = tag.getByteList("regularSlotChargeFlag") + + for (i in 0 until regularSlotChargeFlag.size.coerceAtMost(this.regularSlotChargeFlag.size)) { + this.regularSlotChargeFlag[i].boolean = regularSlotChargeFlag[i].asInt > 0 + } + + for (v in tag.getIntList("exoPackSlotsChargeFlag")) { + this.exoPackSlotsChargeFlag.add(v.asInt) + } + // iterations deathLog.clear() @@ -965,16 +1024,50 @@ class MatteryPlayerCapability(val ply: Player) : ICapabilityProvider, INBTSerial if (isExoPackSmeltingInstalled) smelters.forEach { it.think() } - if (!exoPackChargeSlots.isEmpty && exoPackEnergy.batteryLevel.isPositive) { + if (exoPackEnergy.batteryLevel.isPositive) { var available = exoPackEnergy.extractEnergy(exoPackEnergy.batteryLevel, true) - for (item in exoPackChargeSlots) { - if (item.isNotEmpty) { - item.energy?.let { - available -= exoPackEnergy.extractEnergy(it.receiveEnergy(available, false), false) - } + if (!exoPackChargeSlots.isEmpty) { + for (item in exoPackChargeSlots) { + if (item.isNotEmpty) { + item.energy?.let { + available -= exoPackEnergy.extractEnergy(it.receiveEnergy(available, false), false) + } - if (!available.isPositive) break + if (!available.isPositive) break + } + } + } + + if (available.isPositive) { + for ((i, flag) in regularSlotChargeFlag.withIndex()) { + if (flag.boolean) { + val item = ply.inventory[i] + + if (item.isNotEmpty) { + item.energy?.let { + available -= exoPackEnergy.extractEnergy(it.receiveEnergy(available, false), false) + } + + if (!available.isPositive) break + } + } + } + } + + if (available.isPositive) { + for (slot in exoPackSlotsChargeFlag) { + if (slot in 0 until exoPackContainer.containerSize) { + val item = exoPackContainer[slot] + + if (item.isNotEmpty) { + item.energy?.let { + available -= exoPackEnergy.extractEnergy(it.receiveEnergy(available, false), false) + } + + if (!available.isPositive) break + } + } } } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/Ext.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/Ext.kt index c62ca4bd2..55f05461a 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/Ext.kt @@ -21,6 +21,7 @@ fun Window.isKeyDown(key: Int) = InputConstants.isKeyDown(window, key) val Window.isShiftDown get() = isKeyDown(InputConstants.KEY_LSHIFT) || isKeyDown(InputConstants.KEY_RSHIFT) val Window.isCtrlDown get() = isKeyDown(InputConstants.KEY_RCONTROL) || isKeyDown(InputConstants.KEY_LCONTROL) +val Window.isAltDown get() = isKeyDown(InputConstants.KEY_RALT) || isKeyDown(InputConstants.KEY_LALT) object ShiftPressedCond : BooleanSupplier { override fun getAsBoolean(): Boolean { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoPackInventoryScreen.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoPackInventoryScreen.kt index 7f4b2051c..31aaa3131 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoPackInventoryScreen.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoPackInventoryScreen.kt @@ -13,7 +13,7 @@ import ru.dbotthepony.mc.otm.client.screen.panels.* import ru.dbotthepony.mc.otm.client.screen.panels.button.LargeRectangleButtonPanel import ru.dbotthepony.mc.otm.client.screen.panels.slot.AbstractSlotPanel import ru.dbotthepony.mc.otm.client.screen.panels.slot.BatterySlotPanel -import ru.dbotthepony.mc.otm.client.screen.panels.slot.UserFilteredSlotPanel +import ru.dbotthepony.mc.otm.client.screen.panels.slot.InventorySlotPanel import ru.dbotthepony.mc.otm.client.screen.panels.slot.SlotPanel import ru.dbotthepony.mc.otm.client.screen.panels.util.BackgroundPanel import ru.dbotthepony.mc.otm.client.screen.panels.util.DiscreteScrollBarPanel @@ -55,7 +55,7 @@ class ExoPackInventoryScreen(menu: ExoPackInventoryMenu) : MatteryScreen> { val frame = FramePanel(this, width = 200f, height = FRAME_BASE_HEIGHT + inventoryRows * AbstractSlotPanel.SIZE, title = this.title) - frame.makeHelpButton().addSlotFiltersHelp() + frame.makeHelpButton().addSlotFiltersHelp().tooltips.add(TranslatableComponent("otm.gui.help.slot_charging")) val hotbarStrip = EditablePanel(this, frame, height = 18f) hotbarStrip.dock = Dock.BOTTOM @@ -92,7 +92,7 @@ class ExoPackInventoryScreen(menu: ExoPackInventoryMenu) : MatteryScreen(menu: T, inventory: Inventory, tit if (menu.playerInventorySlots.isNotEmpty() && menu.autoCreateInventoryFrame) { if (menu.playerExoSuitSlots.isEmpty()) { inventoryFrame = FramePanel>(this, null, 0f, 0f, INVENTORY_FRAME_WIDTH, INVENTORY_FRAME_HEIGHT, inventory.displayName).also(this::addPanel) + inventoryFrame!!.makeHelpButton().addSlotFiltersHelp().tooltips.add(TranslatableComponent("otm.gui.help.slot_charging")) val hotbarStrip = EditablePanel(this, inventoryFrame, height = AbstractSlotPanel.SIZE) hotbarStrip.dock = Dock.BOTTOM for (slot in menu.playerHotbarSlots) { - UserFilteredSlotPanel.of(this, hotbarStrip, slot).also { + InventorySlotPanel(this, hotbarStrip, slot).also { it.dock = Dock.LEFT } } @@ -174,6 +176,7 @@ abstract class MatteryScreen(menu: T, inventory: Inventory, tit } } else { inventoryFrame = FramePanel>(this, null, 0f, 0f, INVENTORY_FRAME_WIDTH_EXTENDED, BASE_INVENTORY_FRAME_HEIGHT + AbstractSlotPanel.SIZE * inventoryRows, inventory.displayName).also(this::addPanel) + inventoryFrame!!.makeHelpButton().addSlotFiltersHelp().tooltips.add(TranslatableComponent("otm.gui.help.slot_charging")) inventoryScrollbar = DiscreteScrollBarPanel(this, inventoryFrame, { integerDivisionDown(menu.playerCombinedInventorySlots.size, 9) }, { _, old, new -> @@ -206,7 +209,7 @@ abstract class MatteryScreen(menu: T, inventory: Inventory, tit hotbarStrip.dock = Dock.BOTTOM for (slot in menu.playerHotbarSlots) { - UserFilteredSlotPanel.of(this, hotbarStrip, slot).also { + InventorySlotPanel(this, hotbarStrip, slot).also { it.dock = Dock.LEFT } } @@ -249,7 +252,7 @@ abstract class MatteryScreen(menu: T, inventory: Inventory, tit for (i in 0 .. (8).coerceAtMost(menu.playerCombinedInventorySlots.size - offset - 1)) { val slot = menu.playerCombinedInventorySlots[offset + i] - object : UserFilteredSlotPanel, Slot>(this@MatteryScreen, canvas, slot) { + object : InventorySlotPanel, MatteryMenu.InventorySlot>(this@MatteryScreen, canvas, slot) { init { dock = Dock.LEFT } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/PlayerEquipmentPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/PlayerEquipmentPanel.kt index 2560ad982..b0e2781cb 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/PlayerEquipmentPanel.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/PlayerEquipmentPanel.kt @@ -5,6 +5,7 @@ import ru.dbotthepony.mc.otm.client.minecraft import ru.dbotthepony.mc.otm.client.screen.MatteryScreen import ru.dbotthepony.mc.otm.client.screen.panels.slot.AbstractSlotPanel import ru.dbotthepony.mc.otm.client.screen.panels.slot.FoldableSlotPanel +import ru.dbotthepony.mc.otm.client.screen.panels.slot.InventorySlotPanel import ru.dbotthepony.mc.otm.client.screen.panels.slot.SlotPanel import ru.dbotthepony.mc.otm.client.screen.panels.util.BackgroundPanel import ru.dbotthepony.mc.otm.compat.cos.CosmeticToggleButton @@ -64,10 +65,10 @@ open class PlayerEquipmentPanel>( for ((slot, cosmeticSlot) in armorSlots) { if (cosmeticSlot == null) { - SlotPanel(screen, armorSlotsStrip, slot).dock = Dock.TOP + InventorySlotPanel(screen, armorSlotsStrip, slot).dock = Dock.TOP } else { FoldableSlotPanel(screen, armorSlotsStrip, - SlotPanel(screen, null, slot).also { CosmeticToggleButton(screen, it, slot.type) }, + InventorySlotPanel(screen, null, slot).also { CosmeticToggleButton(screen, it, slot.type) }, listOf(SlotPanel(screen, null, cosmeticSlot))).dock = Dock.TOP } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt new file mode 100644 index 000000000..eefd83760 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt @@ -0,0 +1,50 @@ +package ru.dbotthepony.mc.otm.client.screen.panels.slot + +import com.mojang.blaze3d.platform.InputConstants +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.item.Item +import ru.dbotthepony.mc.otm.client.isAltDown +import ru.dbotthepony.mc.otm.client.minecraft +import ru.dbotthepony.mc.otm.client.playGuiClickSound +import ru.dbotthepony.mc.otm.client.render.MatterySprite +import ru.dbotthepony.mc.otm.client.render.Widgets18 +import ru.dbotthepony.mc.otm.client.screen.MatteryScreen +import ru.dbotthepony.mc.otm.client.screen.panels.EditablePanel +import ru.dbotthepony.mc.otm.menu.MatteryMenu + +open class InventorySlotPanel, out T : MatteryMenu.InventorySlot>( + screen: S, + parent: EditablePanel<*>?, + slot: T, + x: Float = 0f, + y: Float = 0f, + noItemIcon: MatterySprite? = null +) : UserFilteredSlotPanel(screen, parent, slot, x, y, SIZE, SIZE, noItemIcon) { + override var slotFilter: Item? + get() = slot.filter?.get() + set(value) { slot.filter?.accept(value) } + + override fun renderSlotBackground(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float) { + super.renderSlotBackground(graphics, mouseX, mouseY, partialTick) + + if (slot.chargeFlag?.get() == true) { + Widgets18.BATTERY_SLOT_BACKGROUND.render(graphics, 0f, 0f, width, height) + } + } + + override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean { + if (button == InputConstants.MOUSE_BUTTON_LEFT && minecraft.window.isAltDown) { + val chargeFlag = slot.chargeFlag + + if (chargeFlag == null) { + return super.mouseClickedInner(x, y, button) + } else { + chargeFlag.accept(!chargeFlag.get()) + playGuiClickSound() + return true + } + } + + return super.mouseClickedInner(x, y, button) + } +} 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 97b480caf..a4049eacf 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt @@ -262,20 +262,40 @@ abstract class MatteryMenu @JvmOverloads protected constructor( return super.isSameInventory(other) } + var chargeFlag: GetterSetter? = null + private set + init { val mattery = ply.matteryPlayer if (mattery != null) { - if (container === inventory && slotIndex in mattery.regularSlotFilters.indices) { - filter = GetterSetter.of( - getter = { mattery.regularSlotFilters[slotIndex].value }, - setter = { MatteryPlayerNetworkChannel.sendToServer(SetInventoryFilterPacket(SetInventoryFilterPacket.Type.INVENTORY, slotIndex, it)) } - ) + if (container === inventory) { + if (slotIndex in mattery.regularSlotFilters.indices) + filter = GetterSetter.of( + getter = { mattery.regularSlotFilters[slotIndex].value }, + setter = { MatteryPlayerNetworkChannel.sendToServer(SetInventoryFilterPacket(SetInventoryFilterPacket.Type.INVENTORY, slotIndex, it)) } + ) + + if (slotIndex in mattery.regularSlotChargeFlag.indices) { + val input = booleanInput(true) { mattery.regularSlotChargeFlag[slotIndex].boolean = it } + + chargeFlag = GetterSetter.of( + getter = { mattery.regularSlotChargeFlag[slotIndex].boolean }, + setter = input::input + ) + } } else if (container === mattery.exoPackContainer) { filter = GetterSetter.of( getter = { mattery.exoPackContainer.getSlotFilter(slotIndex) }, setter = { MatteryPlayerNetworkChannel.sendToServer(SetInventoryFilterPacket(SetInventoryFilterPacket.Type.EXOPACK, slotIndex, it)) } ) + + val input = booleanInput(true) { if (it) mattery.exoPackSlotsChargeFlag.add(slotIndex) else mattery.exoPackSlotsChargeFlag.remove(slotIndex) } + + chargeFlag = GetterSetter.of( + getter = { slotIndex in mattery.exoPackSlotsChargeFlag }, + setter = input::input + ) } else { filter = null } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/MapAction.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/ChangesetAction.kt similarity index 71% rename from src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/MapAction.kt rename to src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/ChangesetAction.kt index 2065e8c65..870e44ea7 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/MapAction.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/ChangesetAction.kt @@ -1,5 +1,5 @@ package ru.dbotthepony.mc.otm.network.synchronizer -enum class MapAction { +enum class ChangesetAction { CLEAR, ADD, REMOVE } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/FieldSynchronizer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/FieldSynchronizer.kt index a86c4e5f4..76a7c5ae5 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/FieldSynchronizer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/FieldSynchronizer.kt @@ -349,6 +349,7 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa // use LinkedList because it is ensured memory is freed on LinkedList#clear private val mapBacklogs = Reference2ObjectOpenHashMap, LinkedList Unit>>>() + private val setBacklogs = Reference2ObjectOpenHashMap, LinkedList Unit>>>() var unused: Boolean = false private set @@ -401,6 +402,24 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa }) } + internal fun removeMapBacklog(map: Map) { + mapBacklogs.remove(map) + } + + internal fun getSetBacklog(set: Set): LinkedList Unit>> { + if (unused) { + return LinkedList() + } + + return setBacklogs.computeIfAbsent(set, Reference2ObjectFunction { + LinkedList() + }) + } + + internal fun removeSetBacklog(set: Set) { + setBacklogs.remove(set) + } + fun collectNetworkPayload(): FastByteArrayOutputStream? { if (unused || dirtyFields.isEmpty()) { return null @@ -1527,6 +1546,246 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa } } + inner class Set( + private val codec: IStreamCodec, + private val backingSet: MutableSet, + private val callback: ((changes: Collection>) -> Unit)? = null, + ) : AbstractField>() { + private var isRemote = false + private var isDirty = false + + private fun pushBacklog(element: E, action: (DataOutputStream) -> Unit) { + check(!isRemote) { "Field marked as remote" } + + val pair = element to action + + forEachEndpoint { + val list = it.getSetBacklog(this) + val iterator = list.listIterator() + + for (value in iterator) { + if (value.first == element) { + iterator.remove() + } + } + + list.addLast(pair) + } + } + + override fun observe(): Boolean { + return false + } + + override fun remove() { + if (!isRemoved) { + forEachEndpoint { it.removeSetBacklog(this) } + } + + super.remove() + } + + override fun markDirty() { + check(!isRemoved) { "Field was removed" } + notifyEndpoints(this) + isDirty = true + } + + override fun markDirty(endpoint: Endpoint) { + super.markDirty(endpoint) + + endpoint.getSetBacklog(this).let { + it.clear() + it.add(null to ClearBacklogEntry) + + for (value in backingSet) { + it.add(value to { it.write(ChangesetAction.ADD.ordinal + 1); codec.write(it, value) }) + } + } + } + + override val value: MutableSet = object : MutableSet { + override fun add(element: E): Boolean { + if (backingSet.add(element)) { + if (!isRemote) { + markDirty() + + pushBacklog(element) { + it.write(ChangesetAction.ADD.ordinal + 1) + codec.write(it, element) + } + } + + return true + } + + return false + } + + override fun addAll(elements: Collection): Boolean { + var any = false + elements.forEach { any = add(it) || any } + return any + } + + override fun clear() { + if (backingSet.isNotEmpty()) { + backingSet.clear() + + if (!isRemote) { + markDirty() + + forEachEndpoint { + it.getSetBacklog(this@Set).let { + it.clear() + it.add(null to ClearBacklogEntry) + } + } + } + } + } + + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val parent = backingSet.iterator() + private var lastElement: Any? = Mark + + override fun hasNext(): Boolean { + return parent.hasNext() + } + + override fun next(): E { + return parent.next().also { lastElement = it } + } + + override fun remove() { + parent.remove() + val lastElement = lastElement + + if (lastElement !== Mark) { + this.lastElement = Mark + + if (!isRemote) { + markDirty() + + pushBacklog(lastElement as E) { + it.write(ChangesetAction.REMOVE.ordinal + 1) + codec.write(it, lastElement as E) + } + } + } + } + } + } + + override fun remove(element: E): Boolean { + if (backingSet.remove(element)) { + if (!isRemote) { + markDirty() + + pushBacklog(element) { + it.write(ChangesetAction.REMOVE.ordinal + 1) + codec.write(it, element) + } + } + + return true + } + + return false + } + + override fun removeAll(elements: Collection): Boolean { + var any = false + elements.forEach { any = remove(it) || any } + return any + } + + override fun retainAll(elements: Collection): Boolean { + var any = false + + val iterator = iterator() + + for (value in iterator) { + if (value !in elements) { + any = true + iterator.remove() + } + } + + return any + } + + override val size: Int + get() = backingSet.size + + override fun contains(element: E): Boolean { + return element in backingSet + } + + override fun containsAll(elements: Collection): Boolean { + return backingSet.containsAll(elements) + } + + override fun isEmpty(): Boolean { + return backingSet.isEmpty() + } + } + + override fun write(stream: DataOutputStream, endpoint: Endpoint) { + val list = endpoint.getSetBacklog(this) + + for (value in list) { + value.second.invoke(stream) + } + + stream.write(0) + list.clear() + isDirty = false + } + + override fun read(stream: DataInputStream) { + if (!isRemote) { + isRemote = true + forEachEndpoint { it.removeSetBacklog(this) } + } + + isDirty = false + + var action = stream.read() + val changeset = LinkedList>() + + while (action != 0) { + if ((action - 1) !in ChangesetActionList.indices) { + throw IllegalArgumentException("Unknown changeset action with index ${action - 1}") + } + + when (ChangesetActionList[action - 1]) { + ChangesetAction.CLEAR -> { + changeset.add(SetChangeset(ChangesetAction.CLEAR, null)) + backingSet.clear() + } + + ChangesetAction.ADD -> { + val read = codec.read(stream) + changeset.add(SetChangeset(ChangesetAction.ADD, read)) + backingSet.add(read) + } + + ChangesetAction.REMOVE -> { + val read = codec.read(stream) + changeset.add(SetChangeset(ChangesetAction.REMOVE, read)) + backingSet.remove(read) + } + } + + action = stream.read() + } + + callback?.invoke(changeset) + } + } + inner class Map( private val keyCodec: IStreamCodec, private val valueCodec: IStreamCodec, @@ -1545,17 +1804,19 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa } private fun pushBacklog(key: Any?, value: (DataOutputStream) -> Unit) { + val pair = key to value + forEachEndpoint { val list = it.getMapBacklog(this) val iterator = list.listIterator() - for (pair in iterator) { - if (pair.first == key) { + for (e in iterator) { + if (e.first == key) { iterator.remove() } } - list.addLast(key to value) + list.addLast(pair) } } @@ -1582,7 +1843,7 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa val valueCopy = valueCodec.copy(value) pushBacklog(key) { - it.write(MapAction.ADD.ordinal + 1) + it.write(ChangesetAction.ADD.ordinal + 1) keyCodec.write(it, key) valueCodec.write(it, valueCopy) } @@ -1631,7 +1892,7 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa val valueCopy = valueCodec.copy(value) val action = { it: DataOutputStream -> - it.write(MapAction.ADD.ordinal + 1) + it.write(ChangesetAction.ADD.ordinal + 1) keyCodec.write(it, key) valueCodec.write(it, valueCopy) } @@ -1660,7 +1921,7 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa val valueCopy = valueCodec.copy(value) backlog.add(key to { - it.write(MapAction.ADD.ordinal + 1) + it.write(ChangesetAction.ADD.ordinal + 1) keyCodec.write(it, key) valueCodec.write(it, valueCopy) }) @@ -1707,7 +1968,7 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa pushBacklog(key) { @Suppress("BlockingMethodInNonBlockingContext") // false positive - it.write(MapAction.ADD.ordinal + 1) + it.write(ChangesetAction.ADD.ordinal + 1) keyCodec.write(it, key) valueCodec.write(it, valueCopy) } @@ -1735,7 +1996,7 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa pushBacklog(key) { @Suppress("BlockingMethodInNonBlockingContext") // false positive - it.write(MapAction.REMOVE.ordinal + 1) + it.write(ChangesetAction.REMOVE.ordinal + 1) keyCodec.write(it, keyCopy) } @@ -1767,7 +2028,7 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa override fun read(stream: DataInputStream) { if (!isRemote) { isRemote = true - clearBacklog() + forEachEndpoint { it.removeMapBacklog(this) } observingBackingMap?.clear() } @@ -1777,27 +2038,27 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa var readAction = stream.read() - 1 while (readAction != -1) { - if (readAction >= MapActionList.size) { + if (readAction >= ChangesetActionList.size) { throw IndexOutOfBoundsException("Unknown map action with ID $readAction") } - when (MapActionList[readAction]) { - MapAction.CLEAR -> { + when (ChangesetActionList[readAction]) { + ChangesetAction.CLEAR -> { backingMap.clear() changeset.add(ClearMapChangeset) } - MapAction.ADD -> { + ChangesetAction.ADD -> { val key = keyCodec.read(stream) val value = valueCodec.read(stream) backingMap[key] = value - changeset.add(MapChangeset(MapAction.ADD, key, value)) + changeset.add(MapChangeset(ChangesetAction.ADD, key, value)) } - MapAction.REMOVE -> { + ChangesetAction.REMOVE -> { val key = keyCodec.read(stream) backingMap.remove(key) - changeset.add(MapChangeset(MapAction.REMOVE, key, null)) + changeset.add(MapChangeset(ChangesetAction.REMOVE, key, null)) } } @@ -1860,9 +2121,11 @@ class FieldSynchronizer(private val callback: Runnable, private val alwaysCallCa return i } + private object Mark + companion object { - private val ClearBacklogEntry = { stream: DataOutputStream -> stream.write(MapAction.CLEAR.ordinal + 1) } - private val MapActionList = MapAction.values() - private val ClearMapChangeset = MapChangeset(MapAction.CLEAR, null, null) + private val ClearBacklogEntry = { stream: DataOutputStream -> stream.write(ChangesetAction.CLEAR.ordinal + 1) } + private val ChangesetActionList = ChangesetAction.values() + private val ClearMapChangeset = MapChangeset(ChangesetAction.CLEAR, null, null) } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/MapChangeset.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/MapChangeset.kt index f7cf986e4..47b1f5105 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/MapChangeset.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/MapChangeset.kt @@ -1,23 +1,23 @@ package ru.dbotthepony.mc.otm.network.synchronizer data class MapChangeset( - val action: MapAction, + val action: ChangesetAction, val key: K?, val value: V? ) { inline fun map(add: (K, V) -> Unit, remove: (K) -> Unit) { when (action) { - MapAction.ADD -> add.invoke(key!!, value!!) - MapAction.REMOVE -> remove.invoke(key!!) + ChangesetAction.ADD -> add.invoke(key!!, value!!) + ChangesetAction.REMOVE -> remove.invoke(key!!) else -> {} } } inline fun map(add: (K, V) -> Unit, remove: (K) -> Unit, clear: () -> Unit) { when (action) { - MapAction.CLEAR -> clear.invoke() - MapAction.ADD -> add.invoke(key!!, value!!) - MapAction.REMOVE -> remove.invoke(key!!) + ChangesetAction.CLEAR -> clear.invoke() + ChangesetAction.ADD -> add.invoke(key!!, value!!) + ChangesetAction.REMOVE -> remove.invoke(key!!) } } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/SetChangeset.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/SetChangeset.kt new file mode 100644 index 000000000..8c8894616 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/synchronizer/SetChangeset.kt @@ -0,0 +1,22 @@ +package ru.dbotthepony.mc.otm.network.synchronizer + +data class SetChangeset( + val action: ChangesetAction, + val value: V? +) { + inline fun map(add: (V) -> Unit, remove: (V) -> Unit) { + when (action) { + ChangesetAction.ADD -> add.invoke(value!!) + ChangesetAction.REMOVE -> remove.invoke(value!!) + else -> {} + } + } + + inline fun map(add: (V) -> Unit, remove: (V) -> Unit, clear: () -> Unit) { + when (action) { + ChangesetAction.CLEAR -> clear.invoke() + ChangesetAction.ADD -> add.invoke(value!!) + ChangesetAction.REMOVE -> remove.invoke(value!!) + } + } +}