/*
* 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.inventory
import io.github.moulberry.notenoughupdates.NotEnoughUpdates
import io.github.moulberry.notenoughupdates.core.util.ArrowPagesUtils
import io.github.moulberry.notenoughupdates.core.util.render.TextRenderUtils
import io.github.moulberry.notenoughupdates.events.ButtonExclusionZoneEvent
import io.github.moulberry.notenoughupdates.mixins.AccessorGuiContainer
import io.github.moulberry.notenoughupdates.options.separatesections.Museum
import io.github.moulberry.notenoughupdates.util.*
import io.github.moulberry.notenoughupdates.util.MuseumUtil.DonationState.MISSING
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiScreen
import net.minecraft.client.gui.ScaledResolution
import net.minecraft.client.gui.inventory.GuiChest
import net.minecraft.client.renderer.GlStateManager
import net.minecraft.client.renderer.RenderHelper
import net.minecraft.init.Blocks
import net.minecraft.init.Items
import net.minecraft.inventory.Slot
import net.minecraft.item.ItemStack
import net.minecraft.util.EnumChatFormatting
import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.event.GuiScreenEvent
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import org.lwjgl.input.Mouse
import org.lwjgl.opengl.GL11
import kotlin.math.ceil
object MuseumCheapestItemOverlay {
enum class Category {
WEAPONS,
ARMOUR_SETS,
RARITIES,
NOT_APPLICABLE; // Either not a valid category or inside the "Special Items" category, which is not useful;
/**
* Convert to readable String to be displayed to the user
*/
override fun toString(): String {
return when (this) {
WEAPONS -> "Weapons"
ARMOUR_SETS -> "Armour Sets"
RARITIES -> "Rarities"
NOT_APPLICABLE -> "Everything"
}
}
}
data class MuseumItem(
var name: String,
var internalNames: List,
var value: Double,
var priceRefreshedAt: Long,
var category: Category
)
private const val ITEMS_PER_PAGE = 10
private val backgroundResource: ResourceLocation = ResourceLocation("notenoughupdates:minion_overlay.png")
val config: Museum get() = NotEnoughUpdates.INSTANCE.config.museum
/**
* The top left position of the arrows to be drawn, used by [ArrowPagesUtils]
*/
private var topLeft = intArrayOf(237, 110)
private var currentPage: Int = 0
private var previousSlots: List = emptyList()
private var itemsToDonate: MutableList = emptyList().toMutableList()
private var leftButtonRect = Rectangle(0, 0, 0, 0)
private var rightButtonRect = Rectangle(0, 0, 0, 0)
private var selectedCategory = Category.NOT_APPLICABLE
private var totalPages = 0
/**
*category -> was the highest page visited?
*/
private var checkedPages: HashMap = hashMapOf(
//this page only shows items when you have already donated them -> there is no useful information to gather
Category.WEAPONS to false,
Category.ARMOUR_SETS to false,
Category.RARITIES to false
)
/**
* Draw the overlay and parse items, if applicable
*/
@SubscribeEvent
fun onDrawBackground(event: GuiScreenEvent.BackgroundDrawnEvent) {
if (!shouldRender(event.gui)) return
val chest = event.gui as GuiChest
val slots = chest.inventorySlots.inventorySlots
//check if there is any info to gather only when a category is currently open
if (!slots.equals(previousSlots) && Utils.getOpenChestName().startsWith("Museum ➜")) {
checkIfHighestPageWasVisited(slots)
parseItems(slots)
updateOutdatedValues()
}
previousSlots = slots
val xSize = (event.gui as AccessorGuiContainer).xSize
val guiLeft = (event.gui as AccessorGuiContainer).guiLeft
val guiTop = (event.gui as AccessorGuiContainer).guiTop
drawBackground(guiLeft, xSize, guiTop)
drawLines(guiLeft, guiTop)
drawButtons(guiLeft, xSize, guiTop)
}
/**
* Pass on mouse clicks to [ArrowPagesUtils], if applicable
*/
@SubscribeEvent
fun onMouseClick(event: GuiScreenEvent.MouseInputEvent.Pre) {
if (!shouldRender(event.gui)) return
if (!Mouse.getEventButtonState()) return
val guiLeft = (event.gui as AccessorGuiContainer).guiLeft
val guiTop = (event.gui as AccessorGuiContainer).guiTop
ArrowPagesUtils.onPageSwitchMouse(
guiLeft, guiTop, topLeft, currentPage, totalPages
) { pageChange: Int -> currentPage = pageChange }
}
@SubscribeEvent
fun onButtonExclusionZones(event: ButtonExclusionZoneEvent) {
if (shouldRender(event.gui)) {
event.blockArea(
Rectangle(
event.guiBaseRect.right,
event.guiBaseRect.top,
175, 130
), ButtonExclusionZoneEvent.PushDirection.TOWARDS_RIGHT
)
}
}
@SubscribeEvent
fun onMouseInput(event: GuiScreenEvent.MouseInputEvent.Pre) {
if (!shouldRender(event.gui)) return
val mouseX = Utils.getMouseX()
val mouseY = Utils.getMouseY()
if (Mouse.getEventButtonState() && leftButtonRect.contains(mouseX, mouseY)) {
config.museumCheapestItemOverlayValueSource = 1 - config.museumCheapestItemOverlayValueSource
updateAllValues()
} else if (Mouse.getEventButtonState() && rightButtonRect.contains(mouseX, mouseY)) {
advanceSelectedCategory()
}
}
/**
* Move the selected category one index forward, or back to the start when already at the end
*/
private fun advanceSelectedCategory() {
val nextValueIndex = (selectedCategory.ordinal + 1) % 4
selectedCategory = enumValues()[nextValueIndex]
}
/**
* Draw the two clickable buttons on the bottom right and display a tooltip if needed
*/
private fun drawButtons(guiLeft: Int, xSize: Int, guiTop: Int) {
RenderHelper.enableGUIStandardItemLighting()
val useBIN = config.museumCheapestItemOverlayValueSource == 0
val mouseX = Utils.getMouseX()
val mouseY = Utils.getMouseY()
val scaledResolution = ScaledResolution(Minecraft.getMinecraft())
val width = scaledResolution.scaledWidth
val height = scaledResolution.scaledHeight
// Left button
val leftItemStack = if (useBIN) {
ItemUtils.getCoinItemStack(100000.0)
} else {
ItemStack(Blocks.crafting_table)
}
leftButtonRect = Rectangle(
guiLeft + xSize + 131,
guiTop + 106,
16,
16
)
Minecraft.getMinecraft().renderItem.renderItemIntoGUI(
leftItemStack,
leftButtonRect.x,
leftButtonRect.y
)
if (leftButtonRect.contains(mouseX, mouseY)) {
val tooltip = if (useBIN) {
listOf(
"${EnumChatFormatting.GREEN}Using ${EnumChatFormatting.BLUE}lowest BIN ${EnumChatFormatting.GREEN}as price source!",
"",
"${EnumChatFormatting.YELLOW}Click to switch to craft cost!"
)
} else {
listOf(
"${EnumChatFormatting.GREEN}Using ${EnumChatFormatting.AQUA}craft cost ${EnumChatFormatting.GREEN}as price source!",
"",
"${EnumChatFormatting.YELLOW}Click to switch to lowest BIN!"
)
}
Utils.drawHoveringText(
tooltip,
mouseX,
mouseY,
width,
height,
-1,
Minecraft.getMinecraft().fontRendererObj
)
}
// Right button
val rightItemStack = when (selectedCategory) {
Category.WEAPONS -> ItemStack(Items.diamond_sword)
Category.ARMOUR_SETS -> ItemStack(Items.diamond_chestplate)
Category.RARITIES -> ItemStack(Items.emerald)
Category.NOT_APPLICABLE -> ItemStack(Items.filled_map)
}
rightButtonRect = Rectangle(
guiLeft + xSize + 150,
guiTop + 106,
16,
16
)
Minecraft.getMinecraft().renderItem.renderItemIntoGUI(
rightItemStack,
rightButtonRect.x,
rightButtonRect.y
)
if (rightButtonRect.contains(mouseX, mouseY)) {
val tooltip = mutableListOf(
"${EnumChatFormatting.GREEN}Category Filter",
"",
)
for (category in Category.values()) {
tooltip.add(
if (category == selectedCategory) {
"${EnumChatFormatting.BLUE}>$category"
} else {
category.toString()
}
)
}
tooltip.add("")
tooltip.add("${EnumChatFormatting.YELLOW}Click to advance!")
Utils.drawHoveringText(
tooltip,
mouseX,
mouseY,
width,
height,
-1,
Minecraft.getMinecraft().fontRendererObj
)
}
RenderHelper.disableStandardItemLighting()
}
/**
* Sort the collected items by their calculated value
*/
private fun sortByValue() {
itemsToDonate.sortBy { it.value }
}
/**
* Update all values that have not been updated for the last minute
*/
private fun updateOutdatedValues() {
val time = System.currentTimeMillis()
itemsToDonate.filter { time - it.priceRefreshedAt >= 60000 }
.forEach {
it.value = calculateValue(it.internalNames)
it.priceRefreshedAt = time
}
}
/**
* Update all values regardless of the time of the last update
*/
private fun updateAllValues() {
val time = System.currentTimeMillis()
itemsToDonate.forEach {
it.value = calculateValue(it.internalNames)
it.priceRefreshedAt = time
}
sortByValue()
}
/**
* Calculate the value of an item as displayed in the museum, which may consist of multiple pieces
*/
private fun calculateValue(internalNames: List): Double {
var totalValue = 0.0
internalNames.forEach {
val itemValue: Double =
when (config.museumCheapestItemOverlayValueSource) {
0 -> NotEnoughUpdates.INSTANCE.manager.auctionManager.getBazaarOrBin(it, false)
1 -> NotEnoughUpdates.INSTANCE.manager.auctionManager.getCraftCost(it)?.craftCost ?: return@forEach
else -> -1.0 //unreachable
}
if (itemValue == -1.0 || itemValue == 0.0) {
totalValue = Double.MAX_VALUE
return@forEach
} else {
totalValue += itemValue
}
}
if (totalValue == 0.0) {
totalValue = Double.MAX_VALUE
}
return totalValue
}
/**
* Draw the lines containing the displayname and value over the background
*/
private fun drawLines(guiLeft: Int, guiTop: Int) {
val mouseX = Utils.getMouseX()
val mouseY = Utils.getMouseY()
val scaledResolution = ScaledResolution(Minecraft.getMinecraft())
val width = scaledResolution.scaledWidth
val height = scaledResolution.scaledHeight
val applicableItems = if (selectedCategory == Category.NOT_APPLICABLE) {
itemsToDonate
} else {
itemsToDonate.toList().filter { it.category == selectedCategory }
}
val lines = buildLines(applicableItems)
totalPages = ceil(applicableItems.size.toFloat() / ITEMS_PER_PAGE.toFloat()).toInt()
lines.forEachIndexed { index, line ->
if (!visitedAllPages() && (index == ITEMS_PER_PAGE || index == lines.size - 1)) {
TextRenderUtils.drawStringScaledMaxWidth(
"${EnumChatFormatting.RED}Visit all pages for accurate info!",
Minecraft.getMinecraft().fontRendererObj,
(guiLeft + 185).toFloat(),
(guiTop + 95).toFloat(),
true,
155,
0
)
return@forEachIndexed
} else {
val x = (guiLeft + 187).toFloat()
val y = (guiTop + 5 + (index * 10)).toFloat()
Utils.renderAlignedString(
line.name,
if (line.value == Double.MAX_VALUE) "${EnumChatFormatting.RED}Unknown ${if (config.museumCheapestItemOverlayValueSource == 0) "BIN" else "Craft Cost"}" else "${EnumChatFormatting.AQUA}${
Utils.shortNumberFormat(
line.value,
0
)
}",
x,
y,
156
)
if (Utils.isWithinRect(mouseX, mouseY, x.toInt(), y.toInt(), 170, 10)) {
val tooltip = mutableListOf(line.name, "")
//armor set
if (line.internalNames.size > 1) {
tooltip.add("${EnumChatFormatting.AQUA}Consists of:")
line.internalNames.forEach {
val displayname =
NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery().withKnownInternalName(it)
.resolveToItemListJson()
?.get("displayname")?.asString ?: "ERROR"
val value = calculateValue(listOf(it))
// Creates:" - displayname (price)" OR " - displayname (No BIN found!)"
tooltip.add(
" ${EnumChatFormatting.DARK_GRAY}-${EnumChatFormatting.RESET} $displayname${EnumChatFormatting.DARK_GRAY} (${EnumChatFormatting.GOLD}${
if (value == Double.MAX_VALUE) {
"${EnumChatFormatting.RED}No BIN found!"
} else {
Utils.shortNumberFormat(
value,
0
)
}
}${EnumChatFormatting.DARK_GRAY})"
)
}
tooltip.add("")
}
if (line.internalNames.isEmpty()) {
tooltip.add("${EnumChatFormatting.RED}Could not determine item!")
}
else if (NotEnoughUpdates.INSTANCE.manager.getRecipesFor(line.internalNames[0]).isNotEmpty()) {
tooltip.add("${EnumChatFormatting.YELLOW}${EnumChatFormatting.BOLD}Click to open recipe!")
} else {
tooltip.add("${EnumChatFormatting.RED}${EnumChatFormatting.BOLD}No recipe available!")
}
if (Mouse.getEventButtonState()) {
//TODO? this only opens the recipe for one of the armor pieces
NotEnoughUpdates.INSTANCE.manager.showRecipe(line.internalNames[0])
}
Utils.drawHoveringText(
tooltip,
mouseX,
mouseY,
width,
height,
-1,
Minecraft.getMinecraft().fontRendererObj
)
}
}
}
//no page has been visited yet
if (lines.isEmpty()) {
TextRenderUtils.drawStringScaledMaxWidth(
"${EnumChatFormatting.RED}No items matching filter!",
Minecraft.getMinecraft().fontRendererObj,
(guiLeft + 200).toFloat(),
(guiTop + 128 / 2).toFloat(),
true,
155,
0
)
}
ArrowPagesUtils.onDraw(guiLeft, guiTop, topLeft, currentPage, totalPages)
return
}
/**
* Create the list of [MuseumItem]s that should be displayed on the current page
*/
private fun buildLines(applicableItems: List): List {
val list = emptyList().toMutableList()
for (i in (ITEMS_PER_PAGE * currentPage) until ((ITEMS_PER_PAGE * currentPage) + ITEMS_PER_PAGE)) {
if (i >= applicableItems.size) {
break
}
list.add(applicableItems[i])
}
return list
}
/**
* Parse the not already donated items present in the currently open Museum page
*/
private fun parseItems(slots: List) {
Thread {
val time = System.currentTimeMillis()
val category = getCategory()
if (category == Category.NOT_APPLICABLE) {
return@Thread
}
val armor = category == Category.ARMOUR_SETS
for (i in 0..53) {
val stack = slots[i].stack ?: continue
val parsedItems = MuseumUtil.findMuseumItem(stack, armor) ?: continue
when (parsedItems.state) {
MISSING -> {
val displayName = if (armor) {
// Use the provided displayname for armor sets but change the color to blue (from red)
"${EnumChatFormatting.BLUE}${stack.displayName.stripControlCodes()}"
} else {
// Find out the real displayname and use it for normal items, if possible
NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery()
.withKnownInternalName(parsedItems.skyblockItemIds.first())
.resolveToItemListJson()
?.get("displayname")?.asString ?: "${EnumChatFormatting.RED}ERROR"
}
//if the list does not already contain it, insert this MuseumItem
if (itemsToDonate.none { it.internalNames == parsedItems.skyblockItemIds }) {
itemsToDonate.add(
MuseumItem(
displayName,
parsedItems.skyblockItemIds,
calculateValue(parsedItems.skyblockItemIds),
time,
category
)
)
}
}
else -> itemsToDonate.retainAll { it.internalNames != parsedItems.skyblockItemIds }
}
}
sortByValue()
}.start()
}
/**
* Check if the highest page for the current category is currently open and update [checkedPages] accordingly
*/
private fun checkIfHighestPageWasVisited(slots: List) {
val category = getCategory()
val nextPageSlot = slots[53]
// If the "Next Page" arrow is missing, we are at the highest page
if ((nextPageSlot.stack ?: return).item != Items.arrow) {
checkedPages[category] = true
}
}
/**
* Draw the background texture to the right side of the open Museum Page
*/
private fun drawBackground(guiLeft: Int, xSize: Int, guiTop: Int) {
Minecraft.getMinecraft().textureManager.bindTexture(backgroundResource)
GL11.glColor4f(1F, 1F, 1F, 1F)
GlStateManager.disableLighting()
Utils.drawTexturedRect(
(guiLeft + xSize + 4).toFloat(),
guiTop.toFloat(),
168f,
128f,
0f,
1f,
0f,
1f,
GL11.GL_NEAREST
)
}
/**
* Determine if the overlay should be active based on the config option and the currently open GuiChest, if applicable
*/
private fun shouldRender(gui: GuiScreen): Boolean =
config.museumCheapestItemOverlay && NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard() && (gui is GuiChest && Utils.getOpenChestName()
.startsWith("Museum ➜") || Utils.getOpenChestName() == "Your Museum")
/**
* Determine the currently open Museum Category
*/
private fun getCategory(): Category =
when (Utils.getOpenChestName().substring(9, Utils.getOpenChestName().length)) {
"Weapons" -> Category.WEAPONS
"Armor Sets" -> Category.ARMOUR_SETS
"Rarities" -> Category.RARITIES
else -> Category.NOT_APPLICABLE
}
/**
* Determine if all useful pages have been visited
*/
private fun visitedAllPages(): Boolean = !checkedPages.containsValue(false)
}