/* * Copyright (C) 2023-2024 NotEnoughUpdates contributors * * This file is part of NotEnoughUpdates. * * NotEnoughUpdates is free software: you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * NotEnoughUpdates is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with NotEnoughUpdates. If not, see . */ package io.github.moulberry.notenoughupdates.miscfeatures.profileviewer import com.google.gson.JsonObject import io.github.moulberry.notenoughupdates.NotEnoughUpdates import io.github.moulberry.notenoughupdates.core.util.ArrowPagesUtils import io.github.moulberry.notenoughupdates.core.util.StringUtils import io.github.moulberry.notenoughupdates.core.util.render.RenderUtils import io.github.moulberry.notenoughupdates.profileviewer.GuiProfileViewer import io.github.moulberry.notenoughupdates.profileviewer.GuiProfileViewerPage import io.github.moulberry.notenoughupdates.profileviewer.SkyblockProfiles import io.github.moulberry.notenoughupdates.util.* import io.github.moulberry.notenoughupdates.util.hypixelapi.HypixelItemAPI import io.github.moulberry.notenoughupdates.util.kotlin.set import net.minecraft.client.renderer.GlStateManager import net.minecraft.init.Blocks import net.minecraft.init.Items import net.minecraft.item.ItemStack import net.minecraft.util.ResourceLocation import org.lwjgl.input.Mouse import org.lwjgl.opengl.GL11 import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set import kotlin.math.max class SacksPage(pvInstance: GuiProfileViewer) : GuiProfileViewerPage(pvInstance) { private val manager get() = NotEnoughUpdates.INSTANCE.manager private val pv_sacks = ResourceLocation("notenoughupdates:pv_sacks.png") private var sacksJson = Constants.SACKS private var sackTypes = if (sacksJson != null && sacksJson.isJsonObject) { sacksJson.getAsJsonObject("sacks") ?: JsonObject() } else { Utils.showOutdatedRepoNotification("sacks.json") JsonObject() } private var tooltipToDisplay = listOf() private var currentProfile: SkyblockProfiles.SkyblockProfile? = null private var currentSack = "All" private var page = 0 private var maxPage = 0 private val arrowsHeight = 180 private val arrowsXPos = 110 private val columns = 7 private val rows = 4 private val pageSize = columns * rows private var guiLeft = GuiProfileViewer.getGuiLeft() private var guiTop = GuiProfileViewer.getGuiTop() private val sackArrayLeft = 168 private val sackArrayTop = 20 private val sackGridXSize = 37 private val sackGridYSize = 41 private val itemIconSize = 20 // Lazy initialisation to allow for guiLeft and guiTop to be initialized first private val priceSourceButtonRect by lazy { Rectangle(guiLeft + 54, guiTop + 155, 20, 20) } private enum class PriceSource { Bazaar, NPC } private var currentPriceSource = PriceSource.Bazaar private val sortButtonRect by lazy { Rectangle(guiLeft + 76, guiTop + 155, 20, 20) } private enum class SortMode { Value, Quantity } private var currentSortMode = SortMode.Value private var sackContents = mutableMapOf() private val sackItems = mutableMapOf() private val playerRunes = mutableListOf() private val sackPattern = "^RUNE_(?\\w+)_(?\\d)\$".toPattern() override fun drawPage(mouseX: Int, mouseY: Int, partialTicks: Float) { guiLeft = GuiProfileViewer.getGuiLeft() guiTop = GuiProfileViewer.getGuiTop() MC.textureManager.bindTexture(pv_sacks) Utils.drawTexturedRect( guiLeft.toFloat(), guiTop.toFloat(), instance.sizeX.toFloat(), instance.sizeY.toFloat(), GL11.GL_NEAREST ) val newProfile = selectedProfile if (newProfile == null) { Utils.drawStringCentered("§cMissing Profile Data", guiLeft + 250, guiTop + 101, true, 0) return } if (sacksJson == null) { Utils.drawStringCentered("§cMissing Repo Data", guiLeft + 250, guiTop + 101, true, 0) return } if (newProfile != currentProfile) { getData() currentProfile = selectedProfile } val currentSackData = sackContents[currentSack] ?: run { Utils.drawStringCentered("§cApi Info Missing", guiLeft + 250, guiTop + 101, true, 0) return } // after this point everything in the constants json exists and does not need to be checked again except "item" val name = if (currentSack == "All") "§2All Sacks" else "§2$currentSack Sack" Utils.renderShadowedString(name, (guiLeft + 78).toFloat(), (guiTop + 74).toFloat(), 105) Utils.renderAlignedString( "§6Value", "§f${StringUtils.formatNumber(currentSackData.sackValue.toLong())}", (guiLeft + 27).toFloat(), (guiTop + 91).toFloat(), 102 ) Utils.renderAlignedString( "§2Items", "§f${StringUtils.formatNumber(currentSackData.itemCount)}", (guiLeft + 27).toFloat(), (guiTop + 108).toFloat(), 102 ) GlStateManager.enableDepth() val startIndex = page * pageSize val endIndex = (page + 1) * pageSize if (currentSack == "All") { for ((index, entrySet) in sackTypes.entrySet().withIndex()) { val (sackName, sackData) = entrySet if (index < startIndex || index >= endIndex) continue val adjustedIndex = index - startIndex val xIndex = adjustedIndex % columns val yIndex = adjustedIndex / columns if (yIndex >= rows) continue val data = sackData.asJsonObject if (!data.has("item") || !data.get("item").isJsonPrimitive || !data.get("item").asJsonPrimitive.isString) continue val sackItemName = data.get("item").asString val itemStack = manager.createItem(sackItemName) val x = guiLeft + sackArrayLeft + xIndex * sackGridXSize val y = guiTop + sackArrayTop + yIndex * sackGridYSize MC.textureManager.bindTexture(GuiProfileViewer.pv_elements) Utils.drawTexturedRect( (x).toFloat(), (y).toFloat(), 20f, 20f, 0f, 20 / 256f, 0f, 20 / 256f, GL11.GL_NEAREST ) val sackInfo = sackContents[sackName] ?: SackInfo(0, 0.0) Utils.drawStringCentered( "§6${StringUtils.shortNumberFormat(sackInfo.sackValue.roundToDecimals(0))}", x + itemIconSize / 2, y - 4, true, 0 ) Utils.drawStringCentered( "§7${StringUtils.shortNumberFormat(sackInfo.itemCount)}", x + itemIconSize / 2, y + 26, true, 0 ) GlStateManager.color(0f, 0f, 0f, 0f) if (itemStack != null) { Utils.drawItemStack(itemStack, x + 2, y + 2) if (mouseX > x && mouseX < x + itemIconSize) { if (mouseY > y && mouseY < y + itemIconSize) { tooltipToDisplay = createTooltip( "$sackName Sack", sackInfo.sackValue, sackInfo.itemCount, true ) } } } else { println("$sackItemName missing in neu repo") } } } else { val sackData = sackTypes.get(currentSack).asJsonObject val sackContents = sackData.getAsJsonArray("contents") var sackItemNames = sackContents.map { it.asString }.toList() if (currentSack == "Rune") { sackItemNames = playerRunes } sackItemNames = sackItemNames.sortedByDescending { val sackInfo = sackItems[it] ?: SackItem(0, 0.0) when (currentSortMode) { SortMode.Value -> sackInfo.value SortMode.Quantity -> sackInfo.amount.toDouble() } } for ((index, itemName) in sackItemNames.withIndex()) { if (index < startIndex || index >= endIndex) continue val adjustedIndex = index - startIndex val xIndex = adjustedIndex % columns val yIndex = adjustedIndex / columns if (yIndex >= rows) continue val itemInfo = sackItems[itemName] ?: SackItem(0, 0.0) val itemStack = manager.createItem(itemName) val x = guiLeft + sackArrayLeft + xIndex * sackGridXSize val y = guiTop + sackArrayTop + yIndex * sackGridYSize MC.textureManager.bindTexture(GuiProfileViewer.pv_elements) Utils.drawTexturedRect( (x).toFloat(), (y).toFloat(), 20f, 20f, 0f, 20 / 256f, 0f, 20 / 256f, GL11.GL_NEAREST ) Utils.drawStringCentered( "§6${StringUtils.shortNumberFormat(itemInfo.value.roundToDecimals(0))}", x + itemIconSize / 2, y - 4, true, 0 ) Utils.drawStringCentered("§7${StringUtils.shortNumberFormat(itemInfo.amount)}", x + 10, y + 26, true, 0) GlStateManager.color(1f, 1f, 1f, 1f) if (itemStack != null) { val stackName = itemStack.displayName Utils.drawItemStack(itemStack, x + 2, y + 2) if (mouseX > x && mouseX < x + itemIconSize) { if (mouseY > y && mouseY < y + itemIconSize) { tooltipToDisplay = createTooltip(stackName, itemInfo.value, itemInfo.amount, false) } } } else { println("$itemName missing in neu repo") } } val buttonRect = Rectangle(guiLeft + sackArrayLeft, guiTop + arrowsHeight, 80, 15) RenderUtils.drawFloatingRectWithAlpha( buttonRect.x, buttonRect.y, buttonRect.width, buttonRect.height, 100, true ) Utils.renderShadowedString( "§2Back", (guiLeft + sackArrayLeft + 40).toFloat(), (guiTop + arrowsHeight + 3).toFloat(), 79 ) if (Mouse.getEventButtonState() && Utils.isWithinRect(mouseX, mouseY, buttonRect)) { currentSack = "All" Utils.playPressSound() page = 0 maxPage = getPages(currentSack, sackTypes) } } renderPriceSourceAndSortButtons(mouseX, mouseY) GlStateManager.color(1f, 1f, 1f, 1f) ArrowPagesUtils.onDraw(guiLeft, guiTop, intArrayOf(sackArrayLeft + arrowsXPos, arrowsHeight), page, maxPage + 1) if (tooltipToDisplay.isNotEmpty()) { tooltipToDisplay = tooltipToDisplay.map { "§7$it" } Utils.drawHoveringText(tooltipToDisplay, mouseX, mouseY, instance.width, instance.height, -1) tooltipToDisplay = listOf() } } private fun renderPriceSourceAndSortButtons(mouseX: Int, mouseY: Int) { KotlinRenderUtils.renderItemStackButton( priceSourceButtonRect, when (currentPriceSource) { PriceSource.Bazaar -> { // Bazaar NPC val uuid = "c232e3820897429157619b0ee099fec0628f602fff12b695de54aef11d923ad7" ItemUtils.createSkullItemStack( uuid, uuid, "https://textures.minecraft.net/texture/$uuid" ) } PriceSource.NPC -> ItemUtils.getCoinItemStack(100000.0) }, GuiProfileViewer.pv_elements ) if (priceSourceButtonRect.contains(mouseX, mouseY)) { val tooltip = mutableListOf( "§6Select price source", ) tooltip.addAll(generateTooltipFromEnum(currentPriceSource)) tooltipToDisplay = tooltip } KotlinRenderUtils.renderItemStackButton( sortButtonRect, when (currentSortMode) { SortMode.Value -> ItemStack(Items.gold_ingot) SortMode.Quantity -> ItemStack(Blocks.hopper) }, GuiProfileViewer.pv_elements ) if (sortButtonRect.contains(mouseX, mouseY)) { val tooltip = mutableListOf( "§6Select sorting mode", ) tooltip.addAll(generateTooltipFromEnum(currentSortMode)) tooltipToDisplay = tooltip } } private inline fun > generateTooltipFromEnum(currentlySelected: T): List { val tooltip = mutableListOf() for (enumValue in enumValues()) { var line = " " line += if (enumValue == currentlySelected) { "§2> ${enumValue.name}" } else { enumValue.name } tooltip.add(line) } tooltip.add("") tooltip.add("§eClick to switch!") return tooltip } fun mouseClick(mouseX: Int, mouseY: Int, mouseButton: Int): Boolean { super.mouseClicked(mouseX, mouseY, mouseButton) if (sacksJson == null || sackContents.isEmpty()) { return false } // after this point everything in the constants json exists and does not need to be checked again if (currentSack == "All") { val startIndex = page * pageSize val endIndex = (page + 1) * pageSize for ((index, sackData) in sackTypes.entrySet().withIndex()) { val sackName = sackData.key if (index < startIndex || index >= endIndex) continue val adjustedIndex = index - startIndex val xIndex = adjustedIndex % columns val yIndex = adjustedIndex / columns if (yIndex >= rows) continue val x = guiLeft + sackArrayLeft + xIndex * sackGridXSize val y = guiTop + sackArrayTop + yIndex * sackGridYSize if (mouseX > x && mouseX < x + itemIconSize) { if (mouseY > y && mouseY < y + itemIconSize) { currentSack = sackName Utils.playPressSound() page = 0 maxPage = getPages(currentSack, sackTypes) return true } } } } if (priceSourceButtonRect.contains(mouseX, mouseY)) { currentPriceSource = PriceSource.values()[(currentPriceSource.ordinal + 1) % PriceSource.values().size] Utils.playPressSound() getData() } if (sortButtonRect.contains(mouseX, mouseY)) { currentSortMode = SortMode.values()[(currentSortMode.ordinal + 1) % SortMode.values().size] Utils.playPressSound() getData() } ArrowPagesUtils.onPageSwitchMouse( guiLeft, guiTop, intArrayOf(sackArrayLeft + arrowsXPos, arrowsHeight), page, maxPage + 1 ) { pageChange -> page = pageChange } return false } private fun createTooltip(name: String, value: Double, amount: Int, isSack: Boolean): List { val baseList = mutableListOf( "§2$name", "Items Stored: §a${StringUtils.formatNumber(amount)}", "Total Value: §6${StringUtils.formatNumber(value.toLong())}" ) if (isSack) baseList.add("§eClick for more details") return baseList } private fun getPages(pageName: String, sackTypes: JsonObject): Int { return when (pageName) { "All" -> { sackTypes.entrySet().size / pageSize } "Rune" -> { playerRunes.size / pageSize } else -> { val sackData = sackTypes.get(currentSack).asJsonObject val sackContents = sackData.getAsJsonArray("contents") sackContents.size() / pageSize } } } private fun getData() { sackContents.clear() sackItems.clear() playerRunes.clear() if (!sacksJson.has("sacks") || !sacksJson.get("sacks").isJsonObject) { Utils.showOutdatedRepoNotification("sacks.json") return } val selectedProfile = selectedProfile?.profileJson ?: return val sacksInfo = Utils.getElementOrDefault(selectedProfile, "inventory.sacks_counts", JsonObject()).asJsonObject var totalValue = 0.0 var totalItems = 0 for ((sackName, sackData) in sackTypes.entrySet()) { if (!sackData.isJsonObject) return val data = sackData.asJsonObject var sackValue = 0.0 var sackItemCount = 0 if (sackName == "Rune") { totalItems += getRuneData(sacksInfo) continue } if (!data.has("contents") || !data.get("contents").isJsonArray) return val contents = data.getAsJsonArray("contents") for (item in contents) { if (!item.isJsonPrimitive || !item.asJsonPrimitive.isString) return val sackItem = item.asString val adjustedName = sackItem.replace("-", ":") val itemCount = sacksInfo.getIntOrValue(adjustedName, 0) val itemValue = itemCount * getPrice(sackItem) if (sackItem !in sackItems) { totalValue += itemValue totalItems += itemCount } sackItems[sackItem] = SackItem(itemCount, itemValue) sackValue += itemValue sackItemCount += itemCount } sackContents[sackName] = SackInfo(sackItemCount, sackValue) } sackTypes = sortSackTypesList() for ((itemName, _) in sacksInfo.entrySet()) { val adjustedName = itemName.replace(":", "-") if ((adjustedName in sackItems) || adjustedName.contains(Regex("(RUNE|PERFECT_|FLAWLESS_|MUSHROOM_COLLECTION)"))) continue println("$adjustedName missing from repo sacks file!") } sackContents["All"] = SackInfo(totalItems, totalValue) } /** * Sort the sackTypes list via the chosen sorting mode. * * This will control the order in which the list will be rendered later * * @see SortMode */ private fun sortSackTypesList(): JsonObject { val sortedTypes = JsonObject() Constants.SACKS.getAsJsonObject("sacks").entrySet() .sortedByDescending { (key, _) -> val sack = sackContents[key] ?: SackInfo(0, 0.0) when (currentSortMode) { SortMode.Value -> sack.sackValue SortMode.Quantity -> sack.itemCount.toDouble() } } .forEach { (key, value) -> sortedTypes[key] = value } return sortedTypes } private fun getPrice(itemName: String): Double { val npcPrice = HypixelItemAPI.getNPCSellPrice(itemName) ?: 0.0 return when (currentPriceSource) { PriceSource.NPC -> npcPrice PriceSource.Bazaar -> { val bazaarInfo = manager.auctionManager.getBazaarInfo(itemName) ?: return npcPrice val buyPrice = bazaarInfo.getDoubleOrValue("curr_buy", 0.0) val sellPrice = bazaarInfo.getDoubleOrValue("curr_sell", 0.0) max(buyPrice, sellPrice) } } } private fun getRuneData(sacksInfo: JsonObject): Int { var sackItemCount = 0 for ((itemName, amount) in sacksInfo.entrySet()) { if (!amount.isJsonPrimitive || !amount.asJsonPrimitive.isNumber) continue sackPattern.matchMatcher(itemName) { val itemAmount = amount.asInt val name = group("name") val tier = group("tier") val neuInternalName = "${name}_RUNE;$tier" sackItemCount += itemAmount sackItems[neuInternalName] = SackItem(itemAmount, 1.0 * itemAmount) playerRunes.add(neuInternalName) } } sackContents["Rune"] = SackInfo(sackItemCount, 1.0 * sackItemCount) return sackItemCount } data class SackInfo(val itemCount: Int, val sackValue: Double) data class SackItem(val amount: Int, val value: Double) }