diff options
Diffstat (limited to 'src/main/kotlin')
4 files changed, 549 insertions, 0 deletions
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumCheapestItemOverlay.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumCheapestItemOverlay.kt new file mode 100644 index 00000000..ccaf5ad8 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumCheapestItemOverlay.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>. + */ + +package io.github.moulberry.notenoughupdates.miscfeatures.inventory + +import io.github.moulberry.notenoughupdates.NotEnoughUpdates +import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe +import io.github.moulberry.notenoughupdates.core.util.ArrowPagesUtils +import io.github.moulberry.notenoughupdates.mixins.AccessorGuiContainer +import io.github.moulberry.notenoughupdates.util.MuseumUtil +import io.github.moulberry.notenoughupdates.util.MuseumUtil.DonationState.MISSING +import io.github.moulberry.notenoughupdates.util.Utils +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiScreen +import net.minecraft.client.gui.inventory.GuiChest +import net.minecraft.client.renderer.GlStateManager +import net.minecraft.init.Items +import net.minecraft.inventory.Slot +import net.minecraft.util.EnumChatFormatting +import net.minecraft.util.ResourceLocation +import net.minecraftforge.client.event.GuiScreenEvent +import net.minecraftforge.client.event.GuiScreenEvent.BackgroundDrawnEvent +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import org.lwjgl.input.Mouse +import org.lwjgl.opengl.GL11 + +@NEUAutoSubscribe +object MuseumCheapestItemOverlay { + data class MuseumItem( + var name: String, + var internalNames: List<String>, + var value: Double, + var priceRefreshedAt: Long + ) + + private const val ITEMS_PER_PAGE = 8 + + private val backgroundResource: ResourceLocation = ResourceLocation("notenoughupdates:dungeon_chest_worth.png") + + val config 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<Slot> = emptyList() + private var itemsToDonate: MutableList<MuseumItem> = emptyList<MuseumItem>().toMutableList() + + /** + *category -> was the highest page visited? + */ + private var checkedPages: HashMap<String, Boolean> = hashMapOf( + //this page only shows items when you have already donated them -> there is no useful information to gather + "Special Items" to true, + "Weapons" to false, + "Armor Sets" to false, + "Rarities" to false + ) + + /** + * Draw the overlay and parse items, if applicable + */ + @SubscribeEvent + fun onDrawBackground(event: BackgroundDrawnEvent) { + if (!shouldRender(event.gui)) return + val chest = event.gui as GuiChest + + val slots = chest.inventorySlots.inventorySlots + if (!slots.equals(previousSlots)) { + checkIfHighestPageWasVisited(slots) + parseItems(slots) + updateOutdatedValues() + sortByValue() + } + 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) + } + + /** + * 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 } + } + + /** + * 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 + } + } + + /** + * Calculate the value of an item as displayed in the museum, which may consist of multiple pieces + */ + private fun calculateValue(internalNames: List<String>): 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 lines = buildLines() + lines.forEachIndexed { index, line -> + if (index == ITEMS_PER_PAGE && !visitedAllPages()) { + Minecraft.getMinecraft().fontRendererObj.drawStringWithShadow( + "${EnumChatFormatting.RED}Visit all pages for accurate info!", + (guiLeft + 185).toFloat(), + (guiTop + 85).toFloat(), + 0 + ) + } else { + Utils.renderAlignedString( + "${EnumChatFormatting.RESET}${line.name}", + if (line.value == Double.MAX_VALUE) "${EnumChatFormatting.RED}Unknown" else "${EnumChatFormatting.AQUA}${ + Utils.shortNumberFormat( + line.value, + 0 + ) + }", + (guiLeft + 187).toFloat(), + (guiTop + 5 + (index * 10)).toFloat(), + 160 + ) + } + } + + 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(): List<MuseumItem> { + val list = emptyList<MuseumItem>().toMutableList() + for (i in (if (currentPage == 0) ITEMS_PER_PAGE else ITEMS_PER_PAGE + 1) * (currentPage)..(if (currentPage == 0) ITEMS_PER_PAGE else ITEMS_PER_PAGE + 1) * currentPage + ITEMS_PER_PAGE) { + if (i >= itemsToDonate.size) { + break + } + list.add(itemsToDonate[i]) + } + return list + } + + /** + * Parse the not already donated items present in the currently open Museum page + */ + private fun parseItems(slots: List<Slot>) { + // Iterate upper chest with 56 slots + val time = System.currentTimeMillis() + val armor = Utils.getOpenChestName().endsWith("Armor Sets") + for (i in 0..53) { + val stack = slots[i].stack ?: continue + val parsedItems = MuseumUtil.findMuseumItem(stack, armor) ?: continue + when (parsedItems.state) { + MISSING -> + if (itemsToDonate.none { it.internalNames == parsedItems.skyblockItemIds }) + itemsToDonate.add( + MuseumItem( + stack.displayName, + parsedItems.skyblockItemIds, + calculateValue(parsedItems.skyblockItemIds), + time + ) + ) + + else -> itemsToDonate.retainAll { it.internalNames != parsedItems.skyblockItemIds } + } + } + } + + /** + * Check if the highest page for the current category is currently open and update [checkedPages] accordingly + */ + private fun checkIfHighestPageWasVisited(slots: List<Slot>) { + 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.toFloat() + xSize + 4, + guiTop.toFloat(), + 180F, + 101F, + 0F, + 180 / 256F, + 0F, + 101 / 256F, + 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 && gui is GuiChest && Utils.getOpenChestName() + .startsWith("Museum ➜") + + /** + * Determine the name of the currently open Museum Category + */ + private fun getCategory(): String = Utils.getOpenChestName().substring(9, Utils.getOpenChestName().length) + + /** + * Determine if all useful pages have been visited + */ + private fun visitedAllPages(): Boolean = !checkedPages.containsValue(false) + + /** + * Calculate the total amount of pages the overlay should have + */ + private fun totalPages(): Int = when (itemsToDonate.size % ITEMS_PER_PAGE) { + 0 -> itemsToDonate.size / ITEMS_PER_PAGE + else -> (itemsToDonate.size / ITEMS_PER_PAGE) + 1 + } +} diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumItemHighlighter.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumItemHighlighter.kt new file mode 100644 index 00000000..945449ba --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumItemHighlighter.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Linnea Gräf + * + * 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 <https://www.gnu.org/licenses/>. + */ + +package io.github.moulberry.notenoughupdates.miscfeatures.inventory + +import io.github.moulberry.notenoughupdates.NotEnoughUpdates +import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe +import io.github.moulberry.notenoughupdates.core.ChromaColour +import io.github.moulberry.notenoughupdates.core.util.StringUtils +import io.github.moulberry.notenoughupdates.events.GuiContainerBackgroundDrawnEvent +import io.github.moulberry.notenoughupdates.events.ReplaceItemEvent +import io.github.moulberry.notenoughupdates.events.RepositoryReloadEvent +import io.github.moulberry.notenoughupdates.util.ItemResolutionQuery +import io.github.moulberry.notenoughupdates.util.ItemUtils +import io.github.moulberry.notenoughupdates.util.LRUCache +import io.github.moulberry.notenoughupdates.util.MuseumUtil +import net.minecraft.client.gui.Gui +import net.minecraft.init.Items +import net.minecraft.inventory.ContainerChest +import net.minecraft.inventory.IInventory +import net.minecraft.item.EnumDyeColor +import net.minecraft.item.ItemStack +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +@NEUAutoSubscribe +object MuseumItemHighlighter { + + private val manager get() = NotEnoughUpdates.INSTANCE.manager + private val config get() = NotEnoughUpdates.INSTANCE.config.museum + + private fun getHighlightColor() = ChromaColour.specialToChromaRGB(config.museumItemColor) + + + private val findRawItemForName = LRUCache.memoize(::findRawItemForName0, 4 * 7 * 2) + + @SubscribeEvent + fun onRepositoryReload(event: RepositoryReloadEvent) { + findRawItemForName.clearCache() + } + + private fun findRawItemForName0(arg: Pair<String, Boolean>): ItemStack? { + val (name, armor) = arg + return MuseumUtil.findItemsByName(name, armor).firstOrNull()?.let { manager.createItem(it) } + } + + + @SubscribeEvent + fun onItemOverride(event: ReplaceItemEvent) { + if (!config.museumItemShow) return + if (!isMuseumInventory(event.inventory)) return + val original = event.original ?: return + if (!isCompletedRetrievedItem(original)) return + val armor = StringUtils.cleanColour(event.inventory.displayName.unformattedText).endsWith("Armor Sets") + val rawItem = findRawItemForName.apply(original.displayName to armor) ?: return + val hydratedItem = hydrateMuseumItem(rawItem, original) + event.replaceWith(hydratedItem) + } + + fun isCompletedRetrievedItem(itemStack: ItemStack): Boolean { + return itemStack.hasDisplayName() && itemStack.item == Items.dye && EnumDyeColor.byDyeDamage(itemStack.itemDamage) == EnumDyeColor.LIME + } + + fun isMuseumInventory(inventory: IInventory): Boolean { + return StringUtils.cleanColour(inventory.displayName.unformattedText).startsWith("Museum ➜") + } + + @SubscribeEvent + fun onBackgroundDrawn(event: GuiContainerBackgroundDrawnEvent) { + val egui = event.container ?: return + val chest = egui.inventorySlots as? ContainerChest ?: return + if (!config.museumItemShow) return + if (!isMuseumInventory(chest.lowerChestInventory)) return + val fixedHighlightColor = getHighlightColor() + for (slot in chest.inventorySlots) { + if (slot == null || slot.stack == null) continue + if (isHydratedMuseumItem(slot.stack) || isCompletedRetrievedItem(slot.stack)) { + val left = slot.xDisplayPosition + val top = slot.yDisplayPosition + Gui.drawRect( + left, top, + left + 16, top + 16, + fixedHighlightColor + ) + } + } + } + + fun hydrateMuseumItem(rawItem: ItemStack, original: ItemStack) = rawItem.copy().apply { + setStackDisplayName(original.displayName) + val originalLore = ItemUtils.getLore(original).toMutableList() + ItemUtils.setLore(this, originalLore) + val data = ItemUtils.getOrCreateTag(this) + val extraAttributes = data.getCompoundTag("ExtraAttributes") + extraAttributes.setByte("donated_museum", 1) + data.setTag("ExtraAttributes", extraAttributes) + data.setBoolean(MUSEUM_HYDRATED_ITEM_TAG, true) + } + + fun isHydratedMuseumItem(stack: ItemStack): Boolean { + return ItemUtils.getOrCreateTag(stack).getBoolean(MUSEUM_HYDRATED_ITEM_TAG) + } + + const val MUSEUM_HYDRATED_ITEM_TAG = "NEU_HYDRATED_MUSEUM_ITEM" + +} diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt new file mode 100644 index 00000000..dc1e800c --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>. + */ + +package io.github.moulberry.notenoughupdates.util + +import net.minecraft.util.StringUtils + +fun String.stripControlCodes(): String = StringUtils.stripControlCodes(this) diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MuseumUtil.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MuseumUtil.kt new file mode 100644 index 00000000..dd52d175 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MuseumUtil.kt @@ -0,0 +1,113 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +package io.github.moulberry.notenoughupdates.util + +import io.github.moulberry.notenoughupdates.NEUManager +import io.github.moulberry.notenoughupdates.NotEnoughUpdates +import net.minecraft.item.EnumDyeColor +import net.minecraft.item.ItemDye +import net.minecraft.item.ItemStack + +object MuseumUtil { + + data class MuseumItem( + /** + * A potentially non-exhaustive list of item ids that are required for this museum donation. + */ + val skyblockItemIds: List<String>, + val state: DonationState, + ) + + enum class DonationState { + /** + * Donated armor only shows one piece, so we use that for id resolution, which might result in incomplete + * results (hence the separate state). This still means that the entire set is donated, but it is guaranteed to + * be only a partial result. Other values of this enum do not guarantee a full result, but at least they do not + * guarantee a partial one. + */ + DONATED_PRESENT_PARTIAL, + DONATED_PRESENT, + DONATED_VACANT, + MISSING, + } + + fun findMuseumItem(stack: ItemStack, isOnArmorPage: Boolean): MuseumItem? { + val item = stack.item ?: return null + val items by lazy { findItemsByName(stack.displayName, isOnArmorPage)} + if (item is ItemDye) { + val dyeColor = EnumDyeColor.byDyeDamage(stack.itemDamage) + if (dyeColor == EnumDyeColor.LIME) { + // Item is donated, but not present in the museum + return MuseumItem(items, DonationState.DONATED_VACANT) + } else if (dyeColor == EnumDyeColor.GRAY) { + // Item is not donated + return MuseumItem(items, DonationState.MISSING) + } + // Otherwise unknown item, try to analyze as normal item. + } + val skyblockId = NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery().withItemStack(stack) + .resolveInternalName() + if (skyblockId != null) { + return MuseumItem( + listOf(skyblockId), + if (isOnArmorPage) DonationState.DONATED_PRESENT_PARTIAL else DonationState.DONATED_PRESENT + ) + } + return MuseumItem( + items, + DonationState.DONATED_PRESENT + ) + } + + fun findItemsByName(displayName: String, armor: Boolean): List<String> { + return (if (armor) + findMuseumArmorSetByName(displayName) + else + listOf(findMuseumItemByName(displayName))).filterNotNull() + + } + + fun findMuseumItemByName(displayName: String): String? = + ItemResolutionQuery.findInternalNameByDisplayName(displayName, true) + + + fun findMuseumArmorSetByName(displayName: String): List<String?> { + val armorSlots = arrayOf( + "HELMET", + "LEGGINGS", + "CHESTPLATE", + "BOOTS" + ) + val monochromeName = NEUManager.cleanForTitleMapSearch(displayName) + val results = ItemResolutionQuery.findInternalNameCandidatesForDisplayName(displayName) + .asSequence() + .filter { + val item = NotEnoughUpdates.INSTANCE.manager.createItem(it) + val name = NEUManager.cleanForTitleMapSearch(item.displayName) + monochromeName.replace("armor", "") in name + } + .toSet() + return armorSlots.map { armorSlot -> + results.singleOrNull { armorSlot in it } + } + } + + +} |