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.entity.BlockEntityType 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.ItemScatterer 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 */ abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>): DeviceBlockEntity(type), InventoryChangedListener, BlockEntityClientSerializable, Tickable, ItemStackPacketHandler, NetworkStackDispatcher { companion object { // the locate/insertion timeouts are only 1 tick because that's long enough to hear from every device on the network // in a degraded state (when there's latency in the network), not handling interface priorities correctly is acceptable val LOCATE_REQUEST_TIMEOUT: Long = 1 // ticks val INSERTION_TIMEOUT: Long = 1 } protected 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() // todo: multiple players could have the terminal open simultaneously 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.insert(packet.stack, TerminalBufferInventory.Mode.FROM_NETWORK) // 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 } protected fun updateAndSync() { updateNetItems() // syncs the internal buffer to the client sync() // syncs the open container (if any) to the client 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) } override fun tick() { super.tick() if (!world!!.isClient) { finishPendingRequests() finishTimedOutPendingInsertions() } if (counter % 20 == 0L && !world!!.isClient) { beginInsertions() } } open fun onActivate(player: PlayerEntity) { } 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) val sortedResults = request.results.toMutableList() sortedResults.sortWith { a, b -> // sort results first by provider priority, and then by the count that it can provide if (a.second.providerPriority == b.second.providerPriority) { b.first - a.first } else { b.second.providerPriority - a.second.providerPriority } } 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() } } open fun dropItems() { ItemScatterer.spawn(world, pos, internalBuffer) } override fun toCommonTag(tag: CompoundTag) { super.toCommonTag(tag) tag.put("InternalBuffer", internalBuffer.toTag()) } override fun fromCommonTag(tag: CompoundTag) { super.fromCommonTag(tag) 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 { // we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to // correctly sort by priority return currentTimestamp - timestamp >= AbstractTerminalBlockEntity.LOCATE_REQUEST_TIMEOUT } } }