From f197b517e5ced58eee13b3f9aef89156575d0540 Mon Sep 17 00:00:00 2001 From: CalMWolfs <94038482+CalMWolfs@users.noreply.github.com> Date: Sun, 5 Nov 2023 00:13:36 +1100 Subject: Feature: Sacks page in pv (#891) * sacks in pv * fix formatting * a bit more formatting * change location to make more sense * suggested changes * change colour * add coloured tooltips * remove some more magic numbers --- .../miscfeatures/profileviewer/SacksPage.kt | 449 +++++++++++++++++++++ .../notenoughupdates/util/KotlinJsonUtils.kt | 16 + .../notenoughupdates/util/KotlinStringUtils.kt | 5 + 3 files changed, 470 insertions(+) create mode 100644 src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/SacksPage.kt (limited to 'src/main/kotlin') diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/SacksPage.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/SacksPage.kt new file mode 100644 index 00000000..ebd53c4d --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/SacksPage.kt @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2023 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 net.minecraft.client.renderer.GlStateManager +import net.minecraft.util.ResourceLocation +import org.lwjgl.input.Mouse +import org.lwjgl.opengl.GL11 + + +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 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 + + private val 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 sackTypes = sacksJson.getAsJsonObject("sacks") + + 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 + } + + 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 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 + ) + + val itemInfo = sackItems[itemName] ?: SackItem(0, 0.0) + 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) + } + } + + 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() + } + } + + 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 sackTypes = sacksJson.getAsJsonObject("sacks") + + 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 + } + } + } + } + + 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) return + val sackTypes = sacksJson.getAsJsonObject("sacks") + val selectedProfile = selectedProfile?.profileJson ?: return + + if (!selectedProfile.has("sacks_counts") || !selectedProfile.get("sacks_counts").isJsonObject) return + val sacksInfo = selectedProfile.get("sacks_counts").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) + } + + for ((itemName, _) in sacksInfo.entrySet()) { + val adjustedName = itemName.replace(":", "-") + if (adjustedName.contains(Regex("(RUNE|PERFECT_|MUSHROOM_COLLECTION)"))) continue + if (adjustedName in sackItems) continue + println("$adjustedName missing from repo sacks file!") + } + + sackContents["All"] = SackInfo(totalItems, totalValue) + } + + private fun getPrice(itemName: String): Double { + val npcPrice = HypixelItemAPI.getNPCSellPrice(itemName) ?: 0.0 + val bazaarInfo = manager.auctionManager.getBazaarInfo(itemName) ?: return npcPrice + val buyPrice = bazaarInfo.getDoubleOrValue("curr_buy", 0.0) + val sellPrice = bazaarInfo.getDoubleOrValue("curr_sell", 0.0) + return maxOf(npcPrice, 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) +} diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinJsonUtils.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinJsonUtils.kt index 95b6f1ac..ef2eaedf 100644 --- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinJsonUtils.kt +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinJsonUtils.kt @@ -21,6 +21,7 @@ package io.github.moulberry.notenoughupdates.util import com.google.gson.JsonArray import com.google.gson.JsonElement +import com.google.gson.JsonObject fun Iterable.toJsonArray(): JsonArray = JsonArray().also { @@ -29,3 +30,18 @@ fun Iterable.toJsonArray(): JsonArray = JsonArray().also { } } +fun JsonObject.getIntOrValue(key: String, alternative: Int): Int { + return if (has(key) && get(key).isJsonPrimitive && get(key).asJsonPrimitive.isNumber) { + get(key).asInt + } else { + alternative + } +} + +fun JsonObject.getDoubleOrValue(key: String, alternative: Double): Double { + return if (has(key) && get(key).isJsonPrimitive && get(key).asJsonPrimitive.isNumber) { + get(key).asDouble + } else { + alternative + } +} diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt index d8491167..68b262ee 100644 --- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt @@ -22,7 +22,12 @@ package io.github.moulberry.notenoughupdates.util import net.minecraft.util.StringUtils import java.awt.Toolkit import java.awt.datatransfer.StringSelection +import java.util.regex.Matcher +import java.util.regex.Pattern fun String.stripControlCodes(): String = StringUtils.stripControlCodes(this) fun String.copyToClipboard() = Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(this), null) + +inline fun Pattern.matchMatcher(text: String, consumer: Matcher.() -> T) = + matcher(text).let { if (it.matches()) consumer(it) else null } -- cgit