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.minecraft.block.BlockState import net.minecraft.block.entity.BlockEntityType import net.minecraft.entity.player.PlayerEntity import net.minecraft.inventory.Inventory import net.minecraft.inventory.InventoryChangedListener import net.minecraft.item.ItemStack import net.minecraft.nbt.NbtCompound import net.minecraft.util.ItemScatterer import net.minecraft.util.math.BlockPos 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.block.DeviceBlockEntity import net.shadowfacts.phycon.component.* import net.shadowfacts.phycon.packet.* import net.shadowfacts.phycon.util.NetworkUtil import java.lang.ref.WeakReference import java.util.* import java.util.function.IntBinaryOperator import kotlin.math.min import kotlin.properties.Delegates /** * @author shadowfacts */ abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState): DeviceBlockEntity(type, pos, state), InventoryChangedListener, 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) protected 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 mode = if (packet.bounceCount > 0) { // if this stack bounced from an inventory, that means we previously tried to send it to the network, so retry TerminalBufferInventory.Mode.TO_NETWORK } else { TerminalBufferInventory.Mode.FROM_NETWORK } val remaining = internalBuffer.insert(packet.stack, mode) // 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 markUpdate() // 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, IntBinaryOperator { 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)) } protected open 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) { markUpdate() } } open fun dropItems() { ItemScatterer.spawn(world, pos, internalBuffer) } override fun toCommonTag(tag: NbtCompound) { super.toCommonTag(tag) tag.put("InternalBuffer", internalBuffer.toTag()) } override fun fromCommonTag(tag: NbtCompound) { 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() } open 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 >= LOCATE_REQUEST_TIMEOUT } } }