diff --git a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt index 2717a2a..4575fbb 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt @@ -15,6 +15,6 @@ object PhysicalConnectivityClient: ClientModInitializer { override fun onInitializeClient() { BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent()) - ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen) + ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt index 913092b..dfe3172 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt @@ -7,6 +7,6 @@ import net.shadowfacts.phycon.network.block.terminal.TerminalScreenHandler object PhyScreens { - val TERMINAL_SCREEN_HANDLER = ScreenHandlerRegistry.registerExtended(Identifier(PhysicalConnectivity.MODID, "terminal"), ::TerminalScreenHandler) + val TERMINAL_SCREEN_HANDLER = ScreenHandlerRegistry.registerExtended(Identifier(PhysicalConnectivity.MODID, "terminal"), ::TerminalScreenHandler) } \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt index 1830c8d..5a60491 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt @@ -1,8 +1,10 @@ package net.shadowfacts.phycon.network.block.netinterface import alexiil.mc.lib.attributes.SearchOptions +import alexiil.mc.lib.attributes.Simulation import alexiil.mc.lib.attributes.item.GroupedItemInv import alexiil.mc.lib.attributes.item.ItemAttributes +import net.minecraft.entity.ItemEntity import net.minecraft.util.math.Direction import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.init.PhyBlockEntities @@ -38,6 +40,8 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE) { is RequestInventoryPacket -> handleRequestInventory(packet) is LocateStackPacket -> handleLocateStack(packet) is ExtractStackPacket -> handleExtractStack(packet) + is CheckCapacityPacket -> handleCheckCapacity(packet) + is ItemStackPacket -> handleItemStack(packet) } } @@ -61,4 +65,30 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE) { } } + private fun handleCheckCapacity(packet: CheckCapacityPacket) { + getInventory()?.also { inv -> + val remaining = inv.attemptInsertion(packet.stack, Simulation.SIMULATE) + val couldAccept = packet.stack.count - remaining.count + sendToSingle(CapacityPacket(packet.stack, couldAccept, this, macAddress, packet.source)) + } + } + + private fun handleItemStack(packet: ItemStackPacket) { + val inventory = getInventory() + if (inventory != null) { + val remaining = inventory.insert(packet.stack) + if (!remaining.isEmpty) { + // todo: should this send whatever was left back to the terminal instead of dropping it? + // todo: calculate entity spawn point by finding non-obstructed location + val entity = ItemEntity(world!!, pos.x.toDouble(), pos.y + 1.0, pos.z.toDouble(), remaining) + world!!.spawnEntity(entity) + } + } else { + // todo: should the stack back to the terminal instead of dropping it? + // todo: calculate entity spawn point by finding non-obstructed location + val entity = ItemEntity(world!!, pos.x.toDouble(), pos.y + 1.0, pos.z.toDouble(), packet.stack) + world!!.spawnEntity(entity) + } + } + } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt index ba84c52..8f5318f 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt @@ -3,6 +3,7 @@ package net.shadowfacts.phycon.network.block.terminal import alexiil.mc.lib.attributes.item.GroupedItemInv import alexiil.mc.lib.attributes.item.ItemStackCollections import alexiil.mc.lib.attributes.item.ItemStackUtil +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory import net.minecraft.block.BlockState @@ -37,12 +38,14 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), Invento companion object { val LOCATE_REQUEST_TIMEOUT = 40 // ticks + val INSERTION_TIMEOUT = 40 } private val inventoryCache = mutableMapOf() val internalBuffer = TerminalBufferInventory(18) private val pendingRequests = LinkedList() + private val pendingInsertions = Int2ObjectArrayMap() private var observers = 0 val cachedNetItems = ItemStackCollections.intMap() @@ -59,6 +62,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), Invento is DeviceRemovedPacket -> handleDeviceRemoved(packet) is StackLocationPacket -> handleStackLocation(packet) is ItemStackPacket -> handleItemStack(packet) + is CapacityPacket -> handleCapacity(packet) } } @@ -94,6 +98,24 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), Invento val entity = ItemEntity(world!!, pos.x.toDouble(), pos.y + 1.0, pos.z.toDouble(), remaining) world!!.spawnEntity(entity) } + + // this happens outside the normal update loop because by receiving the item stack packet + // we "know" how much the count in the source inventory has changed + updateNetItems() + sync() + } + + private fun handleCapacity(packet: CapacityPacket) { + val insertion = pendingInsertions.values.firstOrNull { + ItemStackUtil.areEqualIgnoreAmounts(packet.stack, it.stack) + } + if (insertion != null) { + insertion.results.add(packet.capacity to packet.receivingInterface) + if (insertion.totalCapacity >= insertion.stack.count || counter - insertion.timestamp >= INSERTION_TIMEOUT || insertion.results.size >= inventoryCache.size) { + pendingInsertions.remove(insertion.bufferSlot) + completeInsertion(insertion) + } + } } private fun updateNetItems() { @@ -112,6 +134,18 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), Invento } } + private fun beginInsertions() { + if (world!!.isClient) return + + for (slot in 0 until internalBuffer.size()) { + if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue + if (slot in pendingInsertions) continue + val stack = internalBuffer.getStack(slot) + pendingInsertions[slot] = PendingStackInsertion(slot, stack, counter) + sendToSingle(CheckCapacityPacket(stack, macAddress, MACAddress.BROADCAST)) + } + } + fun addObserver() { observers++ } @@ -126,6 +160,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), Invento println(cachedNetItems) } else { updateNetItems() + beginInsertions() sync() } } @@ -138,7 +173,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), Invento inventoryCache.clear() sendToSingle(RequestInventoryPacket(macAddress)) - val factory = object: ExtendedScreenHandlerFactory { + val factory = object: ExtendedScreenHandlerFactory { override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? { return TerminalScreenHandler(syncId, playerInv, this@TerminalBlockEntity) } @@ -171,6 +206,24 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), Invento } } + private fun completeInsertion(insertion: PendingStackInsertion) { + // todo: also sort results by interface priority + val sortedResults = insertion.results.sortedBy { it.first }.toMutableList() + val remaining = insertion.stack + while (!remaining.isEmpty && sortedResults.isNotEmpty()) { + val (capacity, receivingInterface) = sortedResults.removeAt(0) + if (capacity <= 0) continue + sendToSingle(ItemStackPacket(remaining.copy(), macAddress, receivingInterface.macAddress)) + // todo: the interface should confirm how much was actually inserted, in case of race condition + remaining.count -= capacity + } + internalBuffer.setStack(insertion.bufferSlot, remaining) + + // as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets + updateNetItems() + sync() + } + override fun onInventoryChanged(inv: Inventory) { if (inv == internalBuffer && world != null && !world!!.isClient) { markDirty() @@ -234,3 +287,13 @@ data class StackLocateRequest( val totalResultAmount: Int get() = results.fold(0) { acc, (amount, _) -> acc + amount } } + +data class PendingStackInsertion( + val bufferSlot: Int, + val stack: ItemStack, + val timestamp: Int, + val results: MutableSet> = mutableSetOf(), +) { + val totalCapacity: Int + get() = results.fold(0) { acc, (amount, _) -> acc + amount } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBufferInventory.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBufferInventory.kt index ce32355..5d0e673 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBufferInventory.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBufferInventory.kt @@ -14,6 +14,7 @@ class TerminalBufferInventory(size: Int): SimpleInventory(size) { TO_NETWORK, FROM_NETWORK, UNASSIGNED } + // todo: modes should be saved to NBT private val modes = Array(size) { Mode.UNASSIGNED } fun insertFromNetwork(stack: ItemStack): ItemStack { @@ -56,4 +57,12 @@ class TerminalBufferInventory(size: Int): SimpleInventory(size) { super.setStack(slot, stack) } + fun getMode(slot: Int): Mode { + return modes[slot] + } + + fun markSlot(slot: Int, mode: Mode) { + modes[slot] = mode + } + } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt index 950bea3..dc357b7 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt @@ -83,6 +83,13 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina } } return ItemStack.EMPTY + } else if (slotId in 54 until 72) { + // internal buffer + if (actionType == SlotActionType.PICKUP && !player.inventory.cursorStack.isEmpty) { + // placing cursor stack into buffer + val bufferSlot = slotId - 54 // subtract 54 to convert the handler slot ID to a valid buffer index + terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK) + } } return super.onSlotClick(slotId, clickData, actionType, player) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/packet/CapacityPacket.kt b/src/main/kotlin/net/shadowfacts/phycon/network/packet/CapacityPacket.kt new file mode 100644 index 0000000..f190743 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/network/packet/CapacityPacket.kt @@ -0,0 +1,11 @@ +package net.shadowfacts.phycon.network.packet + +import net.minecraft.item.ItemStack +import net.shadowfacts.phycon.api.util.MACAddress +import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlockEntity + +/** + * @author shadowfacts + */ +class CapacityPacket(val stack: ItemStack, val capacity: Int, val receivingInterface: InterfaceBlockEntity, source: MACAddress, destination: MACAddress): BasePacket(source, destination) { +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/packet/CheckCapacityPacket.kt b/src/main/kotlin/net/shadowfacts/phycon/network/packet/CheckCapacityPacket.kt new file mode 100644 index 0000000..3c14bd0 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/network/packet/CheckCapacityPacket.kt @@ -0,0 +1,10 @@ +package net.shadowfacts.phycon.network.packet + +import net.minecraft.item.ItemStack +import net.shadowfacts.phycon.api.util.MACAddress + +/** + * @author shadowfacts + */ +class CheckCapacityPacket(val stack: ItemStack, source: MACAddress, destination: MACAddress): BasePacket(source, destination) { +} \ No newline at end of file