package net.shadowfacts.phycon.block.terminal import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawableHelper import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.client.gui.widget.AbstractButtonWidget import net.minecraft.client.gui.widget.AbstractPressableButtonWidget import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.gui.widget.TextFieldWidget import net.minecraft.client.render.Tessellator import net.minecraft.client.render.VertexConsumerProvider import net.minecraft.client.util.math.MatrixStack import net.minecraft.entity.player.PlayerInventory import net.minecraft.item.ItemStack import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType import net.minecraft.text.LiteralText import net.minecraft.text.Text import net.minecraft.text.TranslatableText import net.minecraft.util.Identifier import net.minecraft.util.math.MathHelper import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.networking.C2STerminalRequestItem import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems import net.shadowfacts.phycon.util.SortMode import net.shadowfacts.phycon.util.next import net.shadowfacts.phycon.util.prev import org.lwjgl.glfw.GLFW import java.lang.NumberFormatException import java.math.RoundingMode import java.text.DecimalFormat import kotlin.math.ceil import kotlin.math.floor import kotlin.math.min import kotlin.math.roundToInt /** * @author shadowfacts */ // todo: translate title class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, title: Text): HandledScreen(handler, playerInv, title) { companion object { private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png") private val DIALOG = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal_amount.png") } private lateinit var searchBox: TextFieldWidget private lateinit var sortButton: SortButton var sortButtonX: Int = 0 private set var sortButtonY = 0 private set private lateinit var amountBox: TextFieldWidget private var dialogStack = ItemStack.EMPTY private var showingAmountDialog = false set(value) { val oldValue = field field = value for (e in dialogChildren) { e.visible = value } amountBox.isVisible searchBox.setSelected(!value) amountBox.setSelected(value) if (value && !oldValue) { amountBox.text = "1" } updateFocusedElement() } private var dialogChildren = mutableListOf() private var scrollPosition = 0f private var isDraggingScrollThumb = false private val trackMinY = 18 private val trackHeight = 106 private val thumbHeight = 15 private val thumbWidth = 12 private val scrollThumbTop: Int get() = trackMinY + (scrollPosition * (trackHeight - thumbHeight)).roundToInt() private val dialogWidth = 158 private val dialogHeight = 62 init { backgroundWidth = 252 backgroundHeight = 222 } override fun init() { super.init() children.clear() dialogChildren.clear() client!!.keyboard.setRepeatEvents(true) searchBox = TextFieldWidget(textRenderer, x + 138, y + 6, 80, 9, LiteralText("Search")) searchBox.setMaxLength(50) // setHasBorder is actually setDrawsBackground searchBox.setHasBorder(false) searchBox.isVisible = true searchBox.setSelected(true) searchBox.setEditableColor(0xffffff) addChild(searchBox) sortButtonX = x + 256 sortButtonY = y sortButton = SortButton(sortButtonX, sortButtonY, handler.sortMode, { requestUpdatedItems() }, ::renderTooltip) addButton(sortButton) val dialogMinX = width / 2 - dialogWidth / 2 val dialogMinY = height / 2 - dialogHeight / 2 amountBox = TextFieldWidget(textRenderer, dialogMinX + 8, dialogMinY + 27, 80, 9, LiteralText("Amount")) amountBox.setHasBorder(false) amountBox.isVisible = false amountBox.setSelected(false) amountBox.setEditableColor(0xffffff) amountBox.setTextPredicate { if (it.isEmpty()) { true } else { try { Integer.parseInt(it) > 0 } catch (e: NumberFormatException) { false } } } dialogChildren.add(amountBox) val plusOne = SmallButton(dialogMinX + 7, dialogMinY + 7, 28, LiteralText("+1")) { amountBox.intValue += 1 } dialogChildren.add(plusOne) val plusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 7, 28, LiteralText("+10")) { amountBox.intValue = ceil((amountBox.intValue + 1) / 10.0).toInt() * 10 } dialogChildren.add(plusTen) val plusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 7, 28, LiteralText("+100")) { amountBox.intValue = ceil((amountBox.intValue + 1) / 100.0).toInt() * 100 } dialogChildren.add(plusHundred) val minusOne = SmallButton(dialogMinX + 7, dialogMinY + 39, 28, LiteralText("-1")) { amountBox.intValue -= 1 } dialogChildren.add(minusOne) val minusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 39, 28, LiteralText("-10")) { amountBox.intValue = floor((amountBox.intValue - 1) / 10.0).toInt() * 10 } dialogChildren.add(minusTen) val minusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 39, 28, LiteralText("-100")) { amountBox.intValue = floor((amountBox.intValue - 1) / 100.0).toInt() * 100 } dialogChildren.add(minusHundred) // 101,25 val request = ButtonWidget(dialogMinX + 101, dialogMinY + 21, 50, 20, LiteralText("Request")) { doDialogRequest() } dialogChildren.add(request) updateFocusedElement() requestUpdatedItems() } private fun updateFocusedElement() { focused = if (showingAmountDialog) { amountBox } else if (searchBox.isFocused) { searchBox } else { null } } private fun requestUpdatedItems() { val player = MinecraftClient.getInstance().player!! player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchBox.text, sortButton.mode, scrollPosition)) } override fun tick() { super.tick() searchBox.tick() amountBox.tick() } override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { textRenderer.draw(matrixStack, title, 65f, 6f, 0x404040) textRenderer.draw(matrixStack, playerInventory.displayName, 65f, backgroundHeight - 94f, 0x404040) textRenderer.draw(matrixStack, TranslatableText("gui.phycon.terminal_buffer"), 7f, 6f, 0x404040) } override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { // if the dialog is open, the background gradient will be drawn in front of the main terminal gui if (!showingAmountDialog) { renderBackground(matrixStack) } RenderSystem.color4f(1f, 1f, 1f, 1f) client!!.textureManager.bindTexture(BACKGROUND) val x = (width - backgroundWidth) / 2 val y = (height - backgroundHeight) / 2 drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight) // scroll thumb drawTexture(matrixStack, x + 232, y + scrollThumbTop, 52, 230, thumbWidth, thumbHeight) } @ExperimentalUnsignedTypes override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { if (showingAmountDialog) { RenderSystem.pushMatrix() // items are rendered at some stupidly high z offset. item amounts at an even higher one RenderSystem.translatef(0f, 0f, -350f) // fake the mouse x/y while showing a dialog so slot mouseover highlights aren't drawn super.render(matrixStack, -1, -1, delta) RenderSystem.popMatrix() } else { super.render(matrixStack, mouseX, mouseY, delta) } searchBox.render(matrixStack, mouseX, mouseY, delta) if (showingAmountDialog) { renderBackground(matrixStack) RenderSystem.color4f(1f, 1f, 1f, 1f) client!!.textureManager.bindTexture(DIALOG) val dialogMinX = width / 2 - dialogWidth / 2 val dialogMinY = height / 2 - dialogHeight / 2 drawTexture(matrixStack, dialogMinX, dialogMinY, 0, 0, dialogWidth, dialogHeight) for (e in dialogChildren) { e.render(matrixStack, mouseX, mouseY, delta) } } else { drawMouseoverTooltip(matrixStack, mouseX, mouseY) } } @ExperimentalUnsignedTypes fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) { if (!handler.isBufferSlot(slot.id)) { return } val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart) val color: UInt = when (mode) { TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u else -> return } DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt()) } private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP } private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP } fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int, slot: Slot, matrixStack: MatrixStack) { val amount = stack.count val s = when { amount < 1_000 -> amount.toString() amount < 1_000_000 -> { val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT format.format(amount / 1_000.0) + "K" } amount < 1_000_000_000 -> { val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT format.format(amount / 1_000_000.0) + "M" } else -> { DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B" } } // draw damage bar // empty string for label because vanilla renders the count behind the damage bar itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "") matrixStack.push() matrixStack.translate(x.toDouble(), y.toDouble(), itemRenderer.zOffset + 200.0) val scale = 2 / 3f matrixStack.scale(scale, scale, 1.0f) val immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().buffer) val textX = (1 / scale * 18) - textRenderer.getWidth(s).toFloat() - 3 val textY = (1 / scale * 18) - 11 textRenderer.draw(s, textX, textY, 0xffffff, true, matrixStack.peek().model, immediate, false, 0, 0xF000F0) immediate.draw() matrixStack.pop() } private fun isPointInsScrollThumb(mouseX: Double, mouseY: Double): Boolean { val x = (width - backgroundWidth) / 2 val y = (height - backgroundHeight) / 2 val thumbMinX = x + 232 val thumbMaxX = thumbMinX + thumbWidth val thumbMinY = y + scrollThumbTop val thumbMaxY = thumbMinY + thumbHeight return mouseX >= thumbMinX && mouseX < thumbMaxX && mouseY >= thumbMinY && mouseY < thumbMaxY } override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) { super.onMouseClick(slot, invSlot, clickData, type) updateFocusedElement() if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) { val stack = slot.stack if (type == SlotActionType.QUICK_MOVE) { // shift click, request full stack requestItem(stack, min(stack.count, stack.maxCount)) } else if (type == SlotActionType.PICKUP) { if (clickData == 1) { // right click, request half stack requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt()) } else { dialogStack = stack showingAmountDialog = true searchBox.setSelected(false) } } } } override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { if (showingAmountDialog) { for (e in dialogChildren) { if (e.mouseClicked(mouseX, mouseY, button)) { return true } } return false } else { if (isPointInsScrollThumb(mouseX, mouseY)) { isDraggingScrollThumb = true return true } return super.mouseClicked(mouseX, mouseY, button) } } override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { if (showingAmountDialog) { return false } else if (isDraggingScrollThumb) { scrollPosition = (mouseY.toFloat() - (y + trackMinY) - 7.5f) / (trackHeight - 15) scrollPosition = MathHelper.clamp(scrollPosition, 0f, 1f) requestUpdatedItems() return true } else { return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) } } override fun mouseMoved(d: Double, e: Double) { if (showingAmountDialog) { } else { super.mouseMoved(d, e) } } override fun mouseReleased(d: Double, e: Double, i: Int): Boolean { if (showingAmountDialog) { return false } else { isDraggingScrollThumb = false return super.mouseReleased(d, e, i) } } override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean { if (showingAmountDialog) { return false } else { var newOffsetInRows = handler.currentScrollOffsetInRows() - amount.toInt() newOffsetInRows = MathHelper.clamp(newOffsetInRows, 0, handler.maxScrollOffsetInRows()) val newScrollPosition = newOffsetInRows / handler.maxScrollOffsetInRows().toFloat() scrollPosition = newScrollPosition requestUpdatedItems() return super.mouseScrolled(mouseX, mouseY, amount) } } override fun charTyped(c: Char, i: Int): Boolean { if (showingAmountDialog) { return amountBox.charTyped(c, i) } else { val oldText = searchBox.text if (searchBox.charTyped(c, i)) { if (searchBox.text != oldText) { scrollPosition = 0f requestUpdatedItems() } return true } return super.charTyped(c, i) } } override fun keyPressed(key: Int, j: Int, k: Int): Boolean { if (showingAmountDialog) { return when (key) { GLFW.GLFW_KEY_ESCAPE -> { showingAmountDialog = false true } GLFW.GLFW_KEY_ENTER -> { doDialogRequest() true } else -> { amountBox.keyPressed(key, j, k) } } } else { val oldText = searchBox.text if (searchBox.keyPressed(key, j, k)) { if (searchBox.text != oldText) { scrollPosition = 0f requestUpdatedItems() } return true } return if (searchBox.isFocused && searchBox.isVisible && key != GLFW.GLFW_KEY_ESCAPE) { true } else { super.keyPressed(key, j, k) } } } private fun doDialogRequest() { showingAmountDialog = false requestItem(dialogStack, amountBox.intValue) } private fun requestItem(stack: ItemStack, amount: Int) { val netHandler = MinecraftClient.getInstance().player!!.networkHandler val packet = C2STerminalRequestItem(handler.terminal, stack, amount) netHandler.sendPacket(packet) } private var TextFieldWidget.intValue: Int get() = if (text.isEmpty()) 0 else Integer.parseInt(text) set(value) { text = value.toString() setSelected(true) } class SmallButton(x: Int, y: Int, width: Int, title: Text, action: PressAction): ButtonWidget(x, y, width, 14, title, action) { @ExperimentalUnsignedTypes override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { val client = MinecraftClient.getInstance() client.textureManager.bindTexture(DIALOG) RenderSystem.color4f(1f, 1f, 1f, 1f) val v = if (isHovered) 142 else 128 RenderSystem.enableBlend() RenderSystem.defaultBlendFunc() RenderSystem.enableDepthTest() drawTexture(matrixStack, x, y, 0, v, width / 2, height) drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, v, width / 2, height) drawCenteredText(matrixStack, client.textRenderer, message, x + width / 2, y + (height - 8) / 2, 0xffffffffu.toInt()) } } class SortButton( x: Int, y: Int, var mode: SortMode, val onChange: (SortMode) -> Unit, val doRenderTooltip: (MatrixStack, Text, Int, Int) -> Unit ): AbstractPressableButtonWidget(x, y, 20, 20, LiteralText("")) { override fun onPress() {} override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { if ((button == 0 || button == 1) && clicked(mouseX, mouseY)) { val newVal = if (button == 0) mode.next else mode.prev mode = newVal onChange(mode) playDownSound(MinecraftClient.getInstance().soundManager) return true } return false } override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { val client = MinecraftClient.getInstance() RenderSystem.color4f(1f, 1f, 1f, 1f) RenderSystem.enableBlend() RenderSystem.defaultBlendFunc() RenderSystem.enableDepthTest() client.textureManager.bindTexture(WIDGETS_LOCATION) val k = getYImage(isHovered) drawTexture(matrixStack, x, y, 0, 46 + k * 20, width / 2, height) drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, 46 + k * 20, width / 2, height) client.textureManager.bindTexture(BACKGROUND) val u: Int = when (mode) { SortMode.COUNT_HIGH_FIRST -> 0 SortMode.COUNT_LOW_FIRST -> 16 SortMode.ALPHABETICAL -> 32 } drawTexture(matrixStack, x + 2, y + 2, u, 230, 16, 16) if (isHovered) { renderToolTip(matrixStack, mouseX, mouseY) } } override fun renderToolTip(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { val text = LiteralText("") text.append("Sort by: ") text.append(mode.tooltip) doRenderTooltip(matrixStack, text, mouseX, mouseY) } } }