package net.shadowfacts.phycon.block.terminal import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType import net.minecraft.entity.player.PlayerEntity import net.minecraft.entity.player.PlayerInventory import net.minecraft.item.ItemStack import net.minecraft.network.PacketByteBuf import net.minecraft.screen.ScreenHandler import net.minecraft.screen.ScreenHandlerType import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.util.Identifier import net.minecraft.util.registry.Registry import net.shadowfacts.phycon.DefaultPlugin import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems import net.shadowfacts.phycon.util.SortMode import net.shadowfacts.phycon.util.TerminalSettings import net.shadowfacts.phycon.util.copyWithCount import java.lang.ref.WeakReference import kotlin.math.ceil import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt /** * @author shadowfacts */ abstract class AbstractTerminalScreenHandler( handlerType: ScreenHandlerType<*>, syncId: Int, val playerInv: PlayerInventory, val terminal: T, ): ScreenHandler(handlerType, syncId), AbstractTerminalBlockEntity.NetItemObserver { private val rowsDisplayed = 6 private val fakeInv = FakeInventory(this) private var searchQuery: String = "" private var settings = TerminalSettings() var totalEntries = 0 private set var scrollPosition = 0f private var itemEntries = listOf() set(value) { field = value if (terminal.world!!.isClient) { itemsForDisplay = value.map { it.stack.copyWithCount(it.amount) } } } var itemsForDisplay = listOf() private set open val xOffset: Int = 0 init { if (!terminal.world!!.isClient) { assert(terminal.netItemObserver?.get() === null) terminal.netItemObserver = WeakReference(this) // intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings } val xOffset = xOffset // network for (y in 0 until 6) { for (x in 0 until 9) { addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, xOffset + 66 + x * 18, 18 + y * 18)) } } // internal buffer for (y in 0 until 6) { for (x in 0 until 3) { addSlot(Slot(terminal.internalBuffer, y * 3 + x, xOffset + 8 + x * 18, 18 + y * 18)) } } // player inv for (y in 0 until 3) { for (x in 0 until 9) { addSlot(Slot(playerInv, x + y * 9 + 9, xOffset + 66 + x * 18, 140 + y * 18)) } } // hotbar for (x in 0 until 9) { addSlot(Slot(playerInv, x, xOffset + 66 + x * 18, 198)) } } override fun netItemsChanged() { val player = playerInv.player assert(player is ServerPlayerEntity) val filtered = terminal.cachedNetItems.object2IntEntrySet().filter { if (searchQuery.isBlank()) return@filter true if (searchQuery.startsWith('@')) { val unprefixed = searchQuery.drop(1) val key = Registry.ITEM.getKey(it.key.item) if (key.isPresent && key.get().value.namespace.startsWith(unprefixed, true)) { return@filter true } } it.key.name.string.contains(searchQuery, true) } totalEntries = filtered.size val sorted = when (settings[DefaultPlugin.SORT_MODE]) { SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue } SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue } SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string } } val offsetInItems = currentScrollOffsetInItems() val end = min(offsetInItems + rowsDisplayed * 9, sorted.size) itemEntries = sorted.subList(offsetInItems, end).map { Entry(it.key, it.intValue) } // itemEntries = sorted.map { Entry(it.key, it.intValue) } (player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, settings, scrollPosition, totalEntries)) } fun totalRows(): Int { return ceil(totalEntries / 9f).toInt() } fun maxScrollOffsetInRows(): Int { return totalRows() - rowsDisplayed } fun currentScrollOffsetInRows(): Int { return max(0, (scrollPosition * maxScrollOffsetInRows()).roundToInt()) } fun currentScrollOffsetInItems(): Int { return currentScrollOffsetInRows() * 9 } fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, settings: TerminalSettings, scrollPosition: Float) { this.searchQuery = query this.settings = settings this.scrollPosition = scrollPosition netItemsChanged() } fun receivedUpdatedItemsFromServer(entries: List, query: String, scrollPosition: Float, totalEntries: Int) { assert(playerInv.player.world.isClient) this.searchQuery = query this.scrollPosition = scrollPosition this.totalEntries = totalEntries itemEntries = entries } override fun canUse(player: PlayerEntity): Boolean { return true } override fun close(player: PlayerEntity) { super.close(player) terminal.netItemObserver = null } override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity) { if (isBufferSlot(slotId)) { // todo: why does this think it's quick_craft sometimes? if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !cursorStack.isEmpty) { // placing cursor stack into buffer val bufferSlot = slotId - bufferSlotsStart // subtract 54 to convert the handler slot ID to a valid buffer index terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK) } } super.onSlotClick(slotId, clickData, actionType, player) } override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack { if (isNetworkSlot(slotId)) { return ItemStack.EMPTY; } val slot = slots[slotId] if (!slot.hasStack()) { return ItemStack.EMPTY } val result = slot.stack.copy() if (isBufferSlot(slotId)) { // last boolean param is fromLast if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) { return ItemStack.EMPTY } if (slot.stack.isEmpty) { terminal.internalBuffer.markSlot(slotId - bufferSlotsStart, TerminalBufferInventory.Mode.UNASSIGNED) } } else if (isPlayerSlot(slotId)) { val slotsInsertedInto = tryInsertItem(slot.stack, bufferSlotsStart until playerSlotsStart) { terminal.internalBuffer.getMode(it - bufferSlotsStart) != TerminalBufferInventory.Mode.FROM_NETWORK } slotsInsertedInto.forEach { terminal.internalBuffer.markSlot(it - bufferSlotsStart, TerminalBufferInventory.Mode.TO_NETWORK) } if (slotsInsertedInto.isEmpty()) { return ItemStack.EMPTY } } return result } private fun tryInsertItem(stack: ItemStack, slots: IntRange, slotPredicate: (Int) -> Boolean): Collection { val slotsInsertedInto = mutableListOf() for (index in slots) { if (stack.isEmpty) break if (!slotPredicate(index)) continue val slot = this.slots[index] val slotStack = slot.stack if (slotStack.isEmpty) { slot.stack = stack.copy() stack.count = 0 slot.markDirty() slotsInsertedInto.add(index) } else if (ItemStack.canCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) { val maxToMove = slotStack.maxCount - slotStack.count val toMove = min(maxToMove, stack.count) slotStack.increment(toMove) stack.decrement(toMove) slot.markDirty() slotsInsertedInto.add(index) } } return slotsInsertedInto } val networkSlotsStart = 0 val networkSlotsEnd = 54 val bufferSlotsStart = 54 val bufferSlotsEnd = 72 val playerSlotsStart = 72 val playerSlotsEnd = 72 + 36 fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart fun isPlayerSlot(id: Int) = id >= playerSlotsStart data class Entry(val stack: ItemStack, val amount: Int) }