/* * 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 . */ package io.github.moulberry.notenoughupdates.miscgui.minionhelper.render; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; 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.events.ButtonExclusionZoneEvent; import io.github.moulberry.notenoughupdates.miscgui.TrophyRewardOverlay; import io.github.moulberry.notenoughupdates.miscgui.minionhelper.Minion; import io.github.moulberry.notenoughupdates.miscgui.minionhelper.MinionHelperManager; import io.github.moulberry.notenoughupdates.miscgui.minionhelper.render.renderables.OverviewLine; import io.github.moulberry.notenoughupdates.miscgui.minionhelper.render.renderables.OverviewText; import io.github.moulberry.notenoughupdates.miscgui.minionhelper.sources.MinionSource; import io.github.moulberry.notenoughupdates.miscgui.minionhelper.sources.NpcSource; import io.github.moulberry.notenoughupdates.mixins.AccessorGuiContainer; import io.github.moulberry.notenoughupdates.util.ItemUtils; import io.github.moulberry.notenoughupdates.util.NotificationHandler; import io.github.moulberry.notenoughupdates.util.Rectangle; import io.github.moulberry.notenoughupdates.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; 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.item.ItemStack; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.GuiOpenEvent; import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class MinionHelperOverlay { private final ResourceLocation minionOverlayImage = new ResourceLocation("notenoughupdates:minion_overlay.png"); private final ResourceLocation greenCheckImage = new ResourceLocation("notenoughupdates:dungeon_map/green_check.png"); private final ResourceLocation whiteCheckImage = new ResourceLocation("notenoughupdates:dungeon_map/white_check.png"); private final MinionHelperManager manager; private final MinionHelperOverlayHover hover; private int[] topLeft = new int[]{237, 110}; private LinkedHashMap cacheRenderMap = null; private int cacheTotalPages = -1; private boolean filterEnabled = true; private boolean useInstantBuyPrice = true; private int maxPerPage = 7; private int currentPage = 0; public MinionHelperOverlay(MinionHelperManager manager) { this.manager = manager; hover = new MinionHelperOverlayHover(this, manager); } @SubscribeEvent public void onGuiOpen(GuiOpenEvent event) { resetCache(); } public void resetCache() { cacheRenderMap = null; cacheTotalPages = -1; } @SubscribeEvent public void onButtonExclusionZones(ButtonExclusionZoneEvent event) { if (manager.inCraftedMinionsInventory() && NotEnoughUpdates.INSTANCE.config.minionHelper.gui) { event.blockArea( new Rectangle( event.getGuiBaseRect().getRight(), event.getGuiBaseRect().getTop(), 168 /*width*/ + 4 /*space*/, 128 ), ButtonExclusionZoneEvent.PushDirection.TOWARDS_RIGHT ); } } @SubscribeEvent public void onDrawBackground(GuiScreenEvent.BackgroundDrawnEvent event) { if (!manager.inCraftedMinionsInventory()) return; if (!NotEnoughUpdates.INSTANCE.config.minionHelper.gui) return; if (manager.isInvalidApiKey()) { LinkedHashMap map = new LinkedHashMap<>(); map.put("§cNo data found, try running /pv", new OverviewText(Collections.emptyList(), () -> {})); render(map); return; } if (manager.notReady()) { LinkedHashMap map = new LinkedHashMap<>(); map.put("§cLoading...", new OverviewText(Collections.emptyList(), () -> {})); render(map); return; } if (manager.getApi().isNotifyNoCollectionApi()) { NotificationHandler.displayNotification(Lists.newArrayList( "", "§cCollection API is disabled!", "§cMinion Helper will not filter minions that", "§cdo not meet the collection requirements!" ), false, true); manager.getApi().setNotifyNoCollectionApi(false); } LinkedHashMap renderMap = getRenderMap(); hover.renderHover(renderMap); render(renderMap); renderArrows(); } private void renderArrows() { GuiScreen gui = Minecraft.getMinecraft().currentScreen; if (gui instanceof AccessorGuiContainer) { AccessorGuiContainer container = (AccessorGuiContainer) gui; int guiLeft = container.getGuiLeft(); int guiTop = container.getGuiTop(); int totalPages = getTotalPages(); ArrowPagesUtils.onDraw(guiLeft, guiTop, topLeft, currentPage, totalPages); } } @SubscribeEvent public void onMouseClick(GuiScreenEvent.MouseInputEvent.Pre event) { if (!manager.inCraftedMinionsInventory()) return; if (!NotEnoughUpdates.INSTANCE.config.minionHelper.gui) return; if (manager.notReady()) return; if (!Mouse.getEventButtonState()) return; OverviewLine overviewLine = getObjectOverMouse(getRenderMap()); if (overviewLine != null) { overviewLine.onClick(); event.setCanceled(true); } int totalPages = getTotalPages(); if (event.gui instanceof AccessorGuiContainer) { int guiLeft = ((AccessorGuiContainer) event.gui).getGuiLeft(); int guiTop = ((AccessorGuiContainer) event.gui).getGuiTop(); if (ArrowPagesUtils.onPageSwitchMouse(guiLeft, guiTop, topLeft, currentPage, totalPages, pageChange -> { currentPage = pageChange; resetCache(); })) { event.setCanceled(true); } } checkButtonClick(); } private void checkButtonClick() { GuiScreen gui = Minecraft.getMinecraft().currentScreen; if (!(gui instanceof GuiChest)) return; int xSize = ((AccessorGuiContainer) gui).getXSize(); int guiLeft = ((AccessorGuiContainer) gui).getGuiLeft(); int guiTop = ((AccessorGuiContainer) gui).getGuiTop(); final ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft()); final int scaledWidth = scaledresolution.getScaledWidth(); final int scaledHeight = scaledresolution.getScaledHeight(); int mouseX = Mouse.getX() * scaledWidth / Minecraft.getMinecraft().displayWidth; int mouseY = scaledHeight - Mouse.getY() * scaledHeight / Minecraft.getMinecraft().displayHeight - 1; int x = guiLeft + xSize + 4 + 149 - 3; int y = guiTop + 109 - 3; if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { toggleShowAvailable(); } x = guiLeft + xSize + 4 + 149 - 3 - 16 - 3; y = guiTop + 109 - 3; if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { toggleUseInstantBuyPrice(); } } @SubscribeEvent public void onMouseClick(GuiScreenEvent.KeyboardInputEvent.Pre event) { if (!manager.inCraftedMinionsInventory()) return; if (!NotEnoughUpdates.INSTANCE.config.minionHelper.gui) return; if (manager.notReady()) return; int totalPages = getTotalPages(); if (ArrowPagesUtils.onPageSwitchKey(currentPage, totalPages, pageChange -> { currentPage = pageChange; resetCache(); })) { event.setCanceled(true); } } private Map getMissing() { Map prices = new HashMap<>(); for (Minion minion : manager.getAllMinions().values()) { if (!minion.doesMeetRequirements() && filterEnabled) continue; if (!minion.isCrafted()) { double price = manager.getPriceCalculation().calculateUpgradeCosts(minion, true); prices.put(minion, price); } } return prices; } private void render(Map renderMap) { Minecraft minecraft = Minecraft.getMinecraft(); Gui gui = Minecraft.getMinecraft().currentScreen; if (!(gui instanceof GuiChest)) return; int xSize = ((AccessorGuiContainer) gui).getXSize(); int guiLeft = ((AccessorGuiContainer) gui).getGuiLeft(); int guiTop = ((AccessorGuiContainer) gui).getGuiTop(); minecraft.getTextureManager().bindTexture(minionOverlayImage); GL11.glColor4f(1, 1, 1, 1); GlStateManager.disableLighting(); Utils.drawTexturedRect(guiLeft + xSize + 4, guiTop, 168, 128, 0, 1f, 0, 1f, GL11.GL_NEAREST); if (filterEnabled) { minecraft.getTextureManager().bindTexture(greenCheckImage); } else { minecraft.getTextureManager().bindTexture(whiteCheckImage); } Utils.drawTexturedRect(guiLeft + xSize + 4 + 149, guiTop + 109, 10, 10, 0, 1f, 0, 1f, GL11.GL_NEAREST); GlStateManager.disableLighting(); RenderHelper.enableGUIStandardItemLighting(); ItemStack itemStack; if (useInstantBuyPrice) { itemStack = ItemUtils.getCoinItemStack(10_000_000); } else { itemStack = ItemUtils.getCoinItemStack(100_000); } Minecraft.getMinecraft().getRenderItem().renderItemIntoGUI( itemStack, guiLeft + xSize + 4 + 149 - 3 - 16 - 3, guiTop + 109 - 3 ); RenderHelper.disableStandardItemLighting(); int x = guiLeft + xSize + 10; int i = 0; int y = guiTop + 6; FontRenderer fontRendererObj = minecraft.fontRendererObj; for (Map.Entry entry : renderMap.entrySet()) { String line = entry.getKey(); /* * Renders the part of the string after '§6' and before '§7' with shadows. * * I don't know how to tell mixin to "only capture part x if part y is present" * Therefore I use these bad splits. I'm Sorry! */ if (line.contains("§6")) { String[] split = line.split("§6"); line = split[0]; String price = "§6§l" + split[1]; if (price.contains("§8")) { split = price.split("§8"); String newPrice = split[0]; String stuffBehindPricePart = "§8" + price.substring(newPrice.length() + 2); price = newPrice; int lineLen = Minecraft.getMinecraft().fontRendererObj.getStringWidth(line + price); fontRendererObj.drawString(stuffBehindPricePart, x + lineLen, y, -1, false); } int lineLen = Minecraft.getMinecraft().fontRendererObj.getStringWidth(line); fontRendererObj.drawString(price, x + lineLen, y, -1, true); } fontRendererObj.drawString(line, x, y, -1, false); i++; if (i == 3) { y += 13; } else { y += 10; } } } private LinkedHashMap getRenderMap() { if (cacheRenderMap != null) return cacheRenderMap; Map prices = getMissing(); LinkedHashMap renderMap = new LinkedHashMap<>(); addTitle(prices, renderMap); addNeedToNextSlot(prices, renderMap); if (!prices.isEmpty()) { addMinions(prices, renderMap); } cacheRenderMap = renderMap; return renderMap; } private void addNeedToNextSlot( Map prices, LinkedHashMap renderMap ) { int neededForNextSlot = manager.getNeedForNextSlot(); if (neededForNextSlot == -1) { renderMap.put("§8Next slot: ?", new OverviewText(Collections.emptyList(), () -> {})); return; } double priceNeeded = 0; int peltsNeeded = 0; int northStarsNeeded = 0; int xpGain = 0; int index = 0; for (Minion minion : TrophyRewardOverlay.sortByValue(prices).keySet()) { Double price = prices.get(minion); priceNeeded += price; xpGain += minion.getXpGain(); index++; peltsNeeded += getSpecialItemNeeds(minion, "SKYBLOCK_PELT"); northStarsNeeded += getSpecialItemNeeds(minion, "SKYBLOCK_NORTH_STAR"); if (index == neededForNextSlot) break; } String costFormat = manager.getPriceCalculation().formatCoins(priceNeeded); costFormat = costFormat.replace(" coins", ""); if (peltsNeeded > 0) { costFormat = costFormat + " §8+ §5" + peltsNeeded + " Pelts"; } if (northStarsNeeded > 0) { costFormat = costFormat + " §8+ §d" + northStarsNeeded + " North Stars"; } List lore; if (xpGain == 0) { if (index == 0) { lore = Arrays.asList("§aAll minions bought!", "§cNo more SkyBlock XP to gain!"); } else { lore = Collections.singletonList("§cCould not load SkyBlock XP for next slot!"); } } else { lore = Arrays.asList(EnumChatFormatting.DARK_AQUA.toString() + xpGain + " Skyblock XP §efor next slot", "§8DISCLAIMER: This only works if", "§8you follow the helper." ); } OverviewText overviewText = new OverviewText(lore, () -> {}); renderMap.put("§8Next slot: §3" + neededForNextSlot + " minions", overviewText); renderMap.put("§8Cost: " + costFormat, overviewText); } private static int getSpecialItemNeeds(Minion minion, String specialItem) { int count = 0; MinionSource minionSource = minion.getMinionSource(); if (minionSource instanceof NpcSource) { NpcSource source = (NpcSource) minionSource; ArrayListMultimap items = source.getItems(); if (items.containsKey(specialItem)) { for (Integer amount : items.get(specialItem)) { count += amount; } } } return count; } private void addTitle(Map prices, LinkedHashMap renderMap) { String name = "§8" + prices.size() + " " + (filterEnabled ? "craftable" : "total") + " minions"; renderMap.put(name, new OverviewText(Collections.emptyList(), () -> {})); } private void addMinions(Map prices, LinkedHashMap renderMap) { int skipPreviousPages = currentPage * maxPerPage; int i = 0; Map sort = TrophyRewardOverlay.sortByValue(prices); for (Minion minion : sort.keySet()) { if (i >= skipPreviousPages) { String displayName = minion.getDisplayName(); if (displayName == null) { if (NotEnoughUpdates.INSTANCE.config.hidden.dev) { Utils.addChatMessage("§cDisplayname is null for " + minion.getInternalName()); } continue; } displayName = displayName.replace(" Minion", ""); String format = manager.getPriceCalculation().calculateUpgradeCostsFormat(minion, true); format = format.replace(" coins", ""); String requirementFormat = minion.doesMeetRequirements() ? "§9" : "§c"; renderMap.put( requirementFormat + displayName + " " + minion.getTier() + " §8- " + format, minion ); } i++; if (i == ((currentPage + 1) * maxPerPage)) break; } } private int getTotalPages() { if (cacheTotalPages != -1) return cacheTotalPages; Map prices = getMissing(); int totalPages = (int) ((double) prices.size() / maxPerPage); if (prices.size() % maxPerPage != 0) { totalPages++; } cacheTotalPages = totalPages; return totalPages; } private void toggleShowAvailable() { filterEnabled = !filterEnabled; currentPage = 0; resetCache(); } private void toggleUseInstantBuyPrice() { useInstantBuyPrice = !useInstantBuyPrice; currentPage = 0; resetCache(); manager.getPriceCalculation().resetCache(); } OverviewLine getObjectOverMouse(LinkedHashMap renderMap) { GuiScreen gui = Minecraft.getMinecraft().currentScreen; if (!(gui instanceof GuiChest)) return null; int xSize = ((AccessorGuiContainer) gui).getXSize(); int guiLeft = ((AccessorGuiContainer) gui).getGuiLeft(); int guiTop = ((AccessorGuiContainer) gui).getGuiTop(); int x = guiLeft + xSize + 9; int y = guiTop + 5; final ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft()); final int scaledWidth = scaledresolution.getScaledWidth(); final int scaledHeight = scaledresolution.getScaledHeight(); int mouseX = Mouse.getX() * scaledWidth / Minecraft.getMinecraft().displayWidth; int mouseY = scaledHeight - Mouse.getY() * scaledHeight / Minecraft.getMinecraft().displayHeight - 1; int i = 0; FontRenderer fontRenderer = Minecraft.getMinecraft().fontRendererObj; for (Map.Entry entry : renderMap.entrySet()) { String text = entry.getKey(); int width = fontRenderer.getStringWidth(StringUtils.cleanColour(text)); if (mouseX > x && mouseX < x + width + 4 && mouseY > y && mouseY < y + 11) { return entry.getValue(); } i++; if (i == 3) { y += 13; } else { y += 10; } } return null; } public void onProfileSwitch() { currentPage = 0; filterEnabled = true; useInstantBuyPrice = true; } public void setMaxPerPage(int maxPerPage) { this.maxPerPage = maxPerPage; } public void setTopLeft(int[] topLeft) { this.topLeft = topLeft; } public boolean isFilterEnabled() { return filterEnabled; } public boolean isUseInstantBuyPrice() { return useInstantBuyPrice; } }