package net.shadowfacts.phycon.block.terminal import alexiil.mc.lib.attributes.item.GroupedItemInvView import alexiil.mc.lib.attributes.item.ItemStackCollections import alexiil.mc.lib.attributes.item.ItemStackUtil import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory import net.minecraft.block.BlockState import net.minecraft.entity.player.PlayerEntity import net.minecraft.entity.player.PlayerInventory import net.minecraft.inventory.Inventory import net.minecraft.inventory.InventoryChangedListener import net.minecraft.item.ItemStack import net.minecraft.nbt.CompoundTag import net.minecraft.network.PacketByteBuf import net.minecraft.screen.ScreenHandler import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.text.TranslatableText import net.minecraft.util.Tickable import net.minecraft.util.math.Direction import net.shadowfacts.phycon.api.Interface import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.api.util.IPAddress import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.block.DeviceBlockEntity import net.shadowfacts.phycon.util.NetworkUtil import net.shadowfacts.phycon.component.* import net.shadowfacts.phycon.packet.* import java.lang.ref.WeakReference import java.util.* import kotlin.math.min import kotlin.properties.Delegates /** * @author shadowfacts */ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), InventoryChangedListener, BlockEntityClientSerializable, Tickable, ItemStackPacketHandler, NetworkStackDispatcher { companion object { val LOCATE_REQUEST_TIMEOUT: Long = 40 // ticks val INSERTION_TIMEOUT: Long = 40 } private val inventoryCache = mutableMapOf() val internalBuffer = TerminalBufferInventory(18) private val pendingRequests = LinkedList() override val pendingInsertions = mutableListOf() override val dispatchStackTimeout = INSERTION_TIMEOUT private var observers = 0 val cachedNetItems = ItemStackCollections.intMap() var netItemObserver: WeakReference? = null init { internalBuffer.addListener(this) } override fun findDestination(): Interface? { for (dir in Direction.values()) { val itf = NetworkUtil.findConnectedInterface(world!!, pos, dir) if (itf != null) { return itf } } return null } override fun handle(packet: Packet) { when (packet) { is ReadInventoryPacket -> handleReadInventory(packet) is DeviceRemovedPacket -> handleDeviceRemoved(packet) is StackLocationPacket -> handleStackLocation(packet) is ItemStackPacket -> handleItemStack(packet) is CapacityPacket -> handleCapacity(packet) } } private fun handleReadInventory(packet: ReadInventoryPacket) { inventoryCache[packet.source] = packet.inventory updateAndSync() } private fun handleDeviceRemoved(packet: DeviceRemovedPacket) { inventoryCache.remove(packet.source) updateAndSync() } private fun handleStackLocation(packet: StackLocationPacket) { val request = pendingRequests.firstOrNull { ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack) } if (request != null) { request.results.add(packet.amount to packet.stackProvider) if (request.isFinishable(counter)) { stackLocateRequestCompleted(request) } } } override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { val remaining = internalBuffer.insertFromNetwork(packet.stack) // 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 updateAndSync() return remaining } private fun updateAndSync() { updateNetItems() sync() netItemObserver?.get()?.netItemsChanged() } private fun updateNetItems() { cachedNetItems.clear() for (inventory in inventoryCache.values) { for (stack in inventory.storedStacks) { val amount = inventory.getAmount(stack) cachedNetItems.mergeInt(stack, amount) { a, b -> a + b } } } } private fun beginInsertions() { if (world!!.isClient) return for (slot in 0 until internalBuffer.size()) { if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue if (pendingInsertions.any { it.bufferSlot == slot }) continue val stack = internalBuffer.getStack(slot) dispatchItemStack(stack) { insertion -> insertion.bufferSlot = slot } } } private fun finishPendingRequests() { if (world!!.isClient) return if (pendingRequests.isEmpty()) return val finishable = pendingRequests.filter { it.isFinishable(counter) } // stackLocateRequestCompleted removes the object from pendingRequests finishable.forEach(::stackLocateRequestCompleted) } fun addObserver() { observers++ } fun removeObserver() { observers-- } override fun tick() { super.tick() if (counter % 20 == 0L) { if (!world!!.isClient) { finishPendingRequests() beginInsertions() finishTimedOutPendingInsertions() } if (observers > 0 && !world!!.isClient) { updateAndSync() } } } fun onActivate(player: PlayerEntity) { if (!world!!.isClient) { updateAndSync() inventoryCache.clear() sendPacket(RequestInventoryPacket(ipAddress)) val factory = object: ExtendedScreenHandlerFactory { override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? { return TerminalScreenHandler(syncId, playerInv, this@TerminalBlockEntity) } override fun getDisplayName() = TranslatableText("block.phycon.terminal") override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) { buf.writeBlockPos(this@TerminalBlockEntity.pos) } } player.openHandledScreen(factory) } addObserver() } fun requestItem(stack: ItemStack, amount: Int = stack.count) { val request = StackLocateRequest(stack, amount, counter) pendingRequests.add(request) // locate packets are sent immediately instead of being added to a queue // otherwise the terminal UI feels sluggish and unresponsive sendPacket(LocateStackPacket(stack, ipAddress)) } private fun stackLocateRequestCompleted(request: StackLocateRequest) { pendingRequests.remove(request) // todo: also sort results by interface priority val sortedResults = request.results.sortedByDescending { it.first }.toMutableList() var amountRequested = 0 while (amountRequested < request.amount && sortedResults.isNotEmpty()) { val (sourceAmount, sourceInterface) = sortedResults.removeAt(0) val amountToRequest = min(sourceAmount, request.amount - amountRequested) amountRequested += amountToRequest sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, sourceInterface.ipAddress)) } } override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter) override fun finishInsertion(insertion: PendingInsertion): ItemStack { val remaining = super.finishInsertion(insertion) internalBuffer.setStack(insertion.bufferSlot, remaining) // as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets updateAndSync() return remaining } override fun onInventoryChanged(inv: Inventory) { if (inv == internalBuffer && world != null && !world!!.isClient) { markDirty() sync() } } override fun toTag(tag: CompoundTag): CompoundTag { tag.put("InternalBuffer", internalBuffer.toTag()) return super.toTag(tag) } override fun fromTag(state: BlockState, tag: CompoundTag) { super.fromTag(state, tag) internalBuffer.fromTag(tag.getCompound("InternalBuffer")) } override fun toClientTag(tag: CompoundTag): CompoundTag { tag.put("InternalBuffer", internalBuffer.toTag()) return tag } override fun fromClientTag(tag: CompoundTag) { internalBuffer.fromTag(tag.getCompound("InternalBuffer")) } interface NetItemObserver { fun netItemsChanged() } class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion(stack, timestamp) { var bufferSlot by Delegates.notNull() } } data class StackLocateRequest( val stack: ItemStack, val amount: Int, val timestamp: Long, var results: MutableSet> = mutableSetOf() ) { val totalResultAmount: Int get() = results.fold(0) { acc, (amount, _) -> acc + amount } fun isFinishable(currentTimestamp: Long): Boolean { return totalResultAmount >= amount || currentTimestamp - timestamp >= TerminalBlockEntity.LOCATE_REQUEST_TIMEOUT } }