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.server.network.ServerPlayerEntity import net.minecraft.util.Identifier import net.minecraft.util.registry.Registry 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.copyWithCount import java.lang.ref.WeakReference import kotlin.math.ceil import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt /** * @author shadowfacts */ class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL, syncId), TerminalBlockEntity.NetItemObserver { companion object { val ID = Identifier(PhysicalConnectivity.MODID, "terminal") } private val rowsDisplayed = 6 private val fakeInv = FakeInventory(this) private var searchQuery: String = "" var sortMode = SortMode.COUNT_HIGH_FIRST private set var totalEntries = 0 private set private 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 constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): this(syncId, playerInv, PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!) init { if (!terminal.world!!.isClient) { terminal.netItemObserver = WeakReference(this) netItemsChanged() } // network for (y in 0 until 6) { for (x in 0 until 9) { addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, 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, 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, 66 + x * 18, 140 + y * 18)) } } // hotbar for (x in 0 until 9) { addSlot(Slot(playerInv, x, 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 (sortMode) { 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, sortMode, 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, sortMode: SortMode, scrollPosition: Float) { this.searchQuery = query this.sortMode = sortMode this.scrollPosition = scrollPosition netItemsChanged() } fun receivedUpdatedItemsFromServer(entries: List, query: String, sortMode: SortMode, scrollPosition: Float, totalEntries: Int) { assert(playerInv.player.world.isClient) this.searchQuery = query this.sortMode = sortMode 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.removeObserver() } override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack { if (isBufferSlot(slotId)) { // todo: why does this think it's quick_craft sometimes? if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.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) } } return 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 (slot.stack.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 (canStacksCombine(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 bufferSlotsStart = 54 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) }