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 net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory import net.minecraft.block.BlockState import net.minecraft.entity.ItemEntity 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.nbt.ListTag import net.minecraft.network.PacketByteBuf import net.minecraft.screen.ScreenHandler import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.text.LiteralText import net.minecraft.util.Tickable import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.api.util.MACAddress import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.network.DeviceBlockEntity import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlockEntity import net.shadowfacts.phycon.network.packet.* import net.shadowfacts.phycon.util.fromTag import net.shadowfacts.phycon.util.toTag import java.util.* import kotlin.math.min /** * @author shadowfacts */ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), InventoryChangedListener, BlockEntityClientSerializable, Tickable { companion object { val LOCATE_REQUEST_TIMEOUT = 40 // ticks } private val inventoryCache = mutableMapOf() val internalBuffer = TerminalBufferInventory(18) private val pendingRequests = LinkedList() private var observers = 0 val cachedNetItems = ItemStackCollections.intMap() var cachedSortedNetItems = listOf() var counter = 0 init { internalBuffer.addListener(this) } override fun handlePacket(packet: Packet) { when (packet) { is ReadInventoryPacket -> handleReadInventory(packet) is DeviceRemovedPacket -> handleDeviceRemoved(packet) is StackLocationPacket -> handleStackLocation(packet) is ItemStackPacket -> handleItemStack(packet) } } private fun handleReadInventory(packet: ReadInventoryPacket) { inventoryCache[packet.source] = packet.inventory updateNetItems() sync() } private fun handleDeviceRemoved(packet: DeviceRemovedPacket) { inventoryCache.remove(packet.source) updateNetItems() sync() } 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.sourceInterface) if (request.totalResultAmount >= request.amount || counter - request.timestamp >= LOCATE_REQUEST_TIMEOUT || request.results.size >= inventoryCache.size) { pendingRequests.remove(request) stackLocateRequestCompleted(request) } } } private fun handleItemStack(packet: ItemStackPacket) { val remaining = internalBuffer.insertFromNetwork(packet.stack) if (!remaining.isEmpty) { // 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) } } 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 } } } // todo: is the map necessary or is just the sorted list enough? cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map { val stack = it.key.copy() stack.count = it.intValue stack } } fun addObserver() { observers++ } fun removeObserver() { observers-- } override fun tick() { if (observers > 0 && (++counter % 20) == 0) { if (world!!.isClient) { println(cachedNetItems) } else { updateNetItems() sync() } } } fun onActivate(player: PlayerEntity) { if (!world!!.isClient) { updateNetItems() sync() inventoryCache.clear() sendToSingle(RequestInventoryPacket(macAddress)) val factory = object: ExtendedScreenHandlerFactory { override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? { return TerminalScreenHandler(syncId, playerInv, this@TerminalBlockEntity) } override fun getDisplayName() = LiteralText("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) { pendingRequests.add(StackLocateRequest(stack, amount, counter)) sendToSingle(LocateStackPacket(stack, macAddress)) } private fun stackLocateRequestCompleted(request: StackLocateRequest) { // 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 sendToSingle(ExtractStackPacket(request.stack, amountToRequest, macAddress, sourceInterface.macAddress)) } } 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.getList("InternalBuffer", 10)) } override fun toClientTag(tag: CompoundTag): CompoundTag { tag.put("InternalBuffer", internalBuffer.toTag()) val list = ListTag() tag.put("CachedNetItems", list) for ((stack, amount) in cachedNetItems) { val entryTag = stack.toTag(CompoundTag()) entryTag.putInt("NetAmount", amount) list.add(entryTag) } return tag } override fun fromClientTag(tag: CompoundTag) { tag.getList("InternalBuffer", 10)?.also { list -> if (list.isNotEmpty()) { internalBuffer.fromTag(list) } else { // todo: should this clear or just do nothing? internalBuffer.clear() } } val list = tag.getList("CachedNetItems", 10) cachedNetItems.clear() for (entryTag in list) { val stack = ItemStack.fromTag(entryTag as CompoundTag) val netAmount = entryTag.getInt("NetAmount") cachedNetItems[stack] = netAmount } cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map { val stack = it.key.copy() stack.count = it.intValue stack } } } data class StackLocateRequest( val stack: ItemStack, val amount: Int, val timestamp: Int, var results: MutableSet> = mutableSetOf() ) { val totalResultAmount: Int get() = results.fold(0) { acc, (amount, _) -> acc + amount } }