diff options
16 files changed, 1136 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 210ed5d..af6eb34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `/moo whatAmILookingAt` (or: `/m waila`) - copy info of "the thing" you're looking at (NPC or mob + nearby "text-only" armor stands; armor stand, placed skull, banner, sign, dropped item, item in item frame, map on wall) - automatically decodes base64 data (e.g. skin details) and unix timestamps +- Chest Tracker & Analyzer: Evaluate Bazaar value of your chests + - Select chests on your island, then get an overview of all items in the selected chests and the Bazaar value of the items + - command: `/moo analyzeChests` - Auction house: Mark sold/ended/expired auctions - either one letter (S, E, E) or the full word - Auction house: show price for each lvl 1 enchantment book required to craft a higher tier book @@ -26,6 +26,7 @@ It is a collection of different features mainly focused on Hypixel SkyBlock. š | Feature | Command/Usage | |-------------------------------------------------------------------------|-----------------------------------------| | Stalk SkyBlock stats of a player | `/moo stalkskyblock` | +| Analyze chests and their Bazaar value on your private island | `/moo analyzeChests` | | Analyze minions on a private island | `/moo analyzeIsland` | | Dungeon interfaces enhancements (normalize dungeon item stats, improved party finder) | Hold <kbd>shift</kbd> (configurable) while viewing a dungeon item tooltip | | Dungeon performance tracker and overlay: Skill score calculation, class milestone tracker, destroyed crypts tracker, and elapsed time indicator | automatically; or with `/moo dungeon` | diff --git a/src/main/java/de/cowtipper/cowlection/Cowlection.java b/src/main/java/de/cowtipper/cowlection/Cowlection.java index 5e0e7bb..855e7c4 100644 --- a/src/main/java/de/cowtipper/cowlection/Cowlection.java +++ b/src/main/java/de/cowtipper/cowlection/Cowlection.java @@ -1,5 +1,6 @@ package de.cowtipper.cowlection; +import de.cowtipper.cowlection.chestTracker.ChestTracker; import de.cowtipper.cowlection.command.MooCommand; import de.cowtipper.cowlection.command.ReplyCommand; import de.cowtipper.cowlection.command.ShrugCommand; @@ -47,9 +48,10 @@ public class Cowlection { private ChatHelper chatHelper; private PlayerCache playerCache; private DungeonCache dungeonCache; + private ChestTracker chestTracker; private Logger logger; - @Mod.EventHandler + @EventHandler public void preInit(FMLPreInitializationEvent e) { instance = this; logger = e.getModLog(); @@ -123,6 +125,27 @@ public class Cowlection { return dungeonCache; } + public boolean enableChestTracker() { + if (chestTracker == null) { + chestTracker = new ChestTracker(this); + return true; + } + return false; + } + + public boolean disableChestTracker() { + if (chestTracker != null) { + chestTracker.clear(); + chestTracker = null; + return true; + } + return false; + } + + public ChestTracker getChestTracker() { + return chestTracker; + } + public File getConfigDirectory() { return configDir; } diff --git a/src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java b/src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java new file mode 100644 index 0000000..f4278fb --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java @@ -0,0 +1,277 @@ +package de.cowtipper.cowlection.chestTracker; + +import de.cowtipper.cowlection.Cowlection; +import de.cowtipper.cowlection.util.AbortableRunnable; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.inventory.GuiChest; +import net.minecraft.client.player.inventory.ContainerLocalMenu; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.inventory.ContainerChest; +import net.minecraft.item.ItemStack; +import net.minecraft.scoreboard.Score; +import net.minecraft.scoreboard.ScoreObjective; +import net.minecraft.scoreboard.ScorePlayerTeam; +import net.minecraft.scoreboard.Scoreboard; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.tileentity.TileEntityChest; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.Vec3; +import net.minecraftforge.client.event.DrawBlockHighlightEvent; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.event.entity.player.PlayerSetSpawnEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class ChestInteractionListener { + private TileEntityChest lastInteractedChest = null; + private boolean interactedWhileSneaking; + private boolean isOnOwnIsland; + private AbortableRunnable checkScoreboard; + private final Cowlection main; + + public ChestInteractionListener(Cowlection main) { + this.main = main; + checkIfOnPrivateIsland(); + } + + @SubscribeEvent + public void onWorldEnter(PlayerSetSpawnEvent e) { + checkIfOnPrivateIsland(); + } + + private void checkIfOnPrivateIsland() { + stopScoreboardChecker(); + isOnOwnIsland = false; + + // check if player has entered or left their private island + checkScoreboard = new AbortableRunnable() { + private int retries = 20 * 20; // retry for up to 20 seconds + + @SubscribeEvent + public void onTickCheckScoreboard(TickEvent.ClientTickEvent e) { + if (!stopped && e.phase == TickEvent.Phase.END) { + if (Minecraft.getMinecraft().theWorld == null || retries <= 0) { + // already stopped; or world gone, probably disconnected; or no retries left (took too long [20 seconds not enough?] or is not on SkyBlock): stop! + stop(); + return; + } + retries--; + Scoreboard scoreboard = Minecraft.getMinecraft().theWorld.getScoreboard(); + ScoreObjective scoreboardSidebar = scoreboard.getObjectiveInDisplaySlot(1); + if (scoreboardSidebar == null && retries >= 0) { + // scoreboard hasn't loaded yet, retry next tick + return; + } else if (scoreboardSidebar != null) { + // scoreboard loaded! + Collection<Score> scoreboardLines = scoreboard.getSortedScores(scoreboardSidebar); + for (Score line : scoreboardLines) { + ScorePlayerTeam scorePlayerTeam = scoreboard.getPlayersTeam(line.getPlayerName()); + if (scorePlayerTeam != null) { + String lineWithoutFormatting = EnumChatFormatting.getTextWithoutFormattingCodes(scorePlayerTeam.getColorPrefix() + scorePlayerTeam.getColorSuffix()); + if (lineWithoutFormatting.startsWith(" ā£")) { + // current location: own private island or somewhere else? + isOnOwnIsland = lineWithoutFormatting.startsWith(" ā£ Your Island"); + break; + } + } + } + } + stop(); + } + } + + @Override + public void stop() { + if (!stopped) { + stopped = true; + retries = -1; + MinecraftForge.EVENT_BUS.unregister(this); + stopScoreboardChecker(); + } + } + + @Override + public void run() { + MinecraftForge.EVENT_BUS.register(this); + } + }; + checkScoreboard.run(); + } + + private void stopScoreboardChecker() { + if (checkScoreboard != null) { + // there is still a scoreboard-checker running, stop it + checkScoreboard.stop(); + checkScoreboard = null; + } + } + + @SubscribeEvent + public void onRightClickChest(PlayerInteractEvent e) { + if (isOnOwnIsland && e.action == PlayerInteractEvent.Action.RIGHT_CLICK_BLOCK) { + TileEntity tileEntity = Minecraft.getMinecraft().theWorld.getTileEntity(e.pos); + if (tileEntity instanceof TileEntityChest) { + // Interacted with a chest at position e.pos + lastInteractedChest = ((TileEntityChest) tileEntity); + interactedWhileSneaking = Minecraft.getMinecraft().thePlayer.isSneaking(); + } + } + } + + @SubscribeEvent + public void onGuiClose(GuiScreenEvent.KeyboardInputEvent.Pre e) { + if (isOnOwnIsland && e.gui instanceof GuiChest && Keyboard.getEventKeyState() && + // closing chest via ESC or key bind to open (and close) inventory (Default: E) + (Keyboard.getEventKey() == Keyboard.KEY_ESCAPE || Keyboard.getEventKey() == Minecraft.getMinecraft().gameSettings.keyBindInventory.getKeyCode())) { + if (lastInteractedChest == null || lastInteractedChest.isInvalid()) { + // gui wasn't a chest gui or chest got removed + return; + } + BlockPos chestPos = lastInteractedChest.getPos(); + EnumFacing otherChestFacing = getOtherChestFacing(lastInteractedChest); + + if (interactedWhileSneaking) { + // remove chest from cache + main.getChestTracker().removeChest(chestPos, otherChestFacing); + } else { + // add chest to cache + ContainerChest chestContainer = (ContainerChest) ((GuiChest) e.gui).inventorySlots; + ContainerLocalMenu chestInventory = (ContainerLocalMenu) chestContainer.getLowerChestInventory(); + + List<ItemStack> chestContents = new ArrayList<>(); + for (int slot = 0; slot < chestInventory.getSizeInventory(); slot++) { + ItemStack item = chestInventory.getStackInSlot(slot); + if (item != null) { + chestContents.add(item); + } + } + main.getChestTracker().addChest(chestPos, chestContents, otherChestFacing); + } + lastInteractedChest = null; + } + } + + /** + * Get the facing of the other chest of a double chest. + * + * @param chest clicked chest + * @return facing of the other chest of a double chest, EnumFacing.UP means no double chest + */ + private EnumFacing getOtherChestFacing(TileEntityChest chest) { + EnumFacing otherChestFacing = EnumFacing.UP; + if (chest.adjacentChestXNeg != null) { + otherChestFacing = EnumFacing.WEST; + } else if (chest.adjacentChestXPos != null) { + otherChestFacing = EnumFacing.EAST; + } else if (chest.adjacentChestZNeg != null) { + otherChestFacing = EnumFacing.NORTH; + } else if (chest.adjacentChestZPos != null) { + otherChestFacing = EnumFacing.SOUTH; + } + return otherChestFacing; + } + + /** + * Renders a bounding box around all cached chests. + * Partially taken from RenderManager#renderDebugBoundingBox + */ + @SubscribeEvent + public void highlightChests(DrawBlockHighlightEvent e) { + if (isOnOwnIsland && Minecraft.getMinecraft().theWorld != null && Minecraft.getMinecraft().thePlayer != null) { + Set<BlockPos> cachedChestPositions = main.getChestTracker().getCachedPositions(); + if (cachedChestPositions.isEmpty()) { + return; + } + // highlight chests whose contents have already been cached + Vec3 playerPos = e.player.getPositionEyes(e.partialTicks); + double xMinOffset = playerPos.xCoord - 0.06; + double xMaxOffset = playerPos.xCoord + 0.06; + double yMinOffset = playerPos.yCoord - Minecraft.getMinecraft().thePlayer.getEyeHeight() + /* to avoid z-fighting: */ 0.009999999776482582d; + double yMaxOffset = playerPos.yCoord + 0.12 - Minecraft.getMinecraft().thePlayer.getEyeHeight(); + double zMinOffset = playerPos.zCoord - 0.06; + double zMaxOffset = playerPos.zCoord + 0.06; + + GlStateManager.pushMatrix(); + GlStateManager.depthMask(false); + GlStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, 1, 0); + GlStateManager.disableTexture2D(); + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldRenderer = tessellator.getWorldRenderer(); + + GlStateManager.color(55 / 255f, 155 / 255f, 55 / 255f, 100 / 255f); + + for (BlockPos chestPos : cachedChestPositions) { + EnumFacing otherChestFacing = main.getChestTracker().getOtherChestFacing(chestPos); + double chestPosXMin = chestPos.getX() - xMinOffset - (otherChestFacing == EnumFacing.WEST ? 1 : 0); + double chestPosXMax = chestPos.getX() - xMaxOffset + 1 + (otherChestFacing == EnumFacing.EAST ? 1 : 0); + double chestPosYMin = chestPos.getY() - yMinOffset; + double chestPosYMax = chestPos.getY() - yMaxOffset + 1; + double chestPosZMin = chestPos.getZ() - zMinOffset - (otherChestFacing == EnumFacing.NORTH ? 1 : 0); + double chestPosZMax = chestPos.getZ() - zMaxOffset + 1 + (otherChestFacing == EnumFacing.SOUTH ? 1 : 0); + + // one coordinate is always either min or max; the other two coords are: min,min > min,max > max,max > max,min + + // down + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION); + worldRenderer.pos(chestPosXMin, chestPosYMin, chestPosZMin).endVertex(); + worldRenderer.pos(chestPosXMin, chestPosYMin, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMin, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMin, chestPosZMin).endVertex(); + tessellator.draw(); + // up + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION); + worldRenderer.pos(chestPosXMin, chestPosYMax, chestPosZMin).endVertex(); + worldRenderer.pos(chestPosXMin, chestPosYMax, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMax, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMax, chestPosZMin).endVertex(); + tessellator.draw(); + // north + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION); + worldRenderer.pos(chestPosXMin, chestPosYMin, chestPosZMin).endVertex(); + worldRenderer.pos(chestPosXMin, chestPosYMax, chestPosZMin).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMax, chestPosZMin).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMin, chestPosZMin).endVertex(); + tessellator.draw(); + // south + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION); + worldRenderer.pos(chestPosXMin, chestPosYMin, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMin, chestPosYMax, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMax, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMin, chestPosZMax).endVertex(); + tessellator.draw(); + // west + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION); + worldRenderer.pos(chestPosXMin, chestPosYMin, chestPosZMin).endVertex(); + worldRenderer.pos(chestPosXMin, chestPosYMin, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMin, chestPosYMax, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMin, chestPosYMax, chestPosZMin).endVertex(); + tessellator.draw(); + // east + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION); + worldRenderer.pos(chestPosXMax, chestPosYMin, chestPosZMin).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMin, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMax, chestPosZMax).endVertex(); + worldRenderer.pos(chestPosXMax, chestPosYMax, chestPosZMin).endVertex(); + tessellator.draw(); + } + GlStateManager.enableTexture2D(); + GlStateManager.depthMask(true); + GlStateManager.disableBlend(); + GlStateManager.popMatrix(); + } + } +} diff --git a/src/main/java/de/cowtipper/cowlection/chestTracker/ChestOverviewGui.java b/src/main/java/de/cowtipper/cowlection/chestTracker/ChestOverviewGui.java new file mode 100644 index 0000000..970eb93 --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/chestTracker/ChestOverviewGui.java @@ -0,0 +1,418 @@ +package de.cowtipper.cowlection.chestTracker; + +import de.cowtipper.cowlection.Cowlection; +import de.cowtipper.cowlection.config.MooConfig; +import de.cowtipper.cowlection.search.GuiTooltip; +import de.cowtipper.cowlection.util.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.*; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.client.config.GuiButtonExt; +import net.minecraftforge.fml.client.config.GuiCheckBox; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; + +import java.io.IOException; +import java.util.*; + +public class ChestOverviewGui extends GuiScreen { + private ItemOverview itemOverview; + private List<GuiTooltip> guiTooltips; + private GuiButton btnClose; + private GuiButton btnUpdateBazaar; + private GuiButton btnCopy; + private GuiCheckBox showNonBazaarItems; + private GuiButton btnBazaarInstantOrOffer; + private AbortableRunnable updateBazaar; + private final String screenTitle; + private final Cowlection main; + + public ChestOverviewGui(Cowlection main) { + this.screenTitle = Cowlection.MODNAME + " Chest Analyzer"; + this.main = main; + } + + @Override + public void initGui() { + this.guiTooltips = new ArrayList<>(); + // close + this.buttonList.add(this.btnClose = new GuiButtonExt(1, this.width - 25, 3, 22, 20, EnumChatFormatting.RED + "X")); + addTooltip(btnClose, Arrays.asList(EnumChatFormatting.RED + "Close interface", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Hint:" + EnumChatFormatting.RESET + " alternatively press ESC")); + // update bazaar prices + this.buttonList.add(this.btnUpdateBazaar = new GuiButton(20, this.width - 165, 5, 130, 16, "ā³ Update Bazaar prices")); + addTooltip(btnUpdateBazaar, Arrays.asList(EnumChatFormatting.YELLOW + "Get latest Bazaar prices from Hypixel API", EnumChatFormatting.WHITE + "(only once per minute)")); + // copy to clipboard + this.buttonList.add(this.btnCopy = new GuiButton(21, this.width - 280, 5, 110, 16, "ā Copy to clipboard")); + addTooltip(btnCopy, Collections.singletonList(EnumChatFormatting.YELLOW + "Copied data can be pasted into e.g. Google Spreadsheets")); + // checkbox: show/hide non-bazaar items + this.buttonList.add(this.showNonBazaarItems = new GuiCheckBox(10, this.width - 162, this.height - 28, " Show non-Bazaar items", MooConfig.chestAnalyzerShowNonBazaarItems)); + addTooltip(showNonBazaarItems, Collections.singletonList(EnumChatFormatting.YELLOW + "Should items that are " + EnumChatFormatting.GOLD + "not " + EnumChatFormatting.YELLOW + "on the Bazaar be displayed?")); + // toggle: use insta-sell or sell offer prices + this.buttonList.add(this.btnBazaarInstantOrOffer = new GuiButton(15, this.width - 165, this.height - 16, 130, 14, MooConfig.useInstantSellBazaarPrices() ? "Use Instant-Sell prices" : "Use Sell Offer prices")); + addTooltip(btnBazaarInstantOrOffer, Collections.singletonList(EnumChatFormatting.YELLOW + "Use " + EnumChatFormatting.GOLD + "Instant-Sell " + EnumChatFormatting.YELLOW + "or " + EnumChatFormatting.GOLD + "Sell Offer" + EnumChatFormatting.YELLOW + " prices?")); + // main item gui + this.itemOverview = new ItemOverview(); + } + + private <T extends Gui> void addTooltip(T field, List<String> tooltip) { + GuiTooltip guiTooltip = new GuiTooltip(field, tooltip); + this.guiTooltips.add(guiTooltip); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + btnUpdateBazaar.enabled = updateBazaar == null && main.getChestTracker().allowUpdateBazaar(); + itemOverview.drawScreen(mouseX, mouseY, partialTicks); + this.drawString(this.fontRendererObj, this.screenTitle, itemOverview.getLeftX(), 10, 0xFFCC00); + super.drawScreen(mouseX, mouseY, partialTicks); + itemOverview.drawScreenPost(mouseX, mouseY); + for (GuiTooltip guiTooltip : guiTooltips) { + if (guiTooltip.checkHover(mouseX, mouseY)) { + GuiHelper.drawHoveringText(guiTooltip.getText(), mouseX, mouseY, width, height, 300); + // only one tooltip can be displayed at a time: break! + break; + } + } + } + + @Override + public void drawDefaultBackground() { + } + + @Override + public void drawWorldBackground(int tint) { + } + + @Override + public void drawBackground(int tint) { + // = dirt background + } + + @Override + protected void actionPerformed(GuiButton button) { + if (button.enabled) { + if (button == btnClose) { + this.mc.displayGuiScreen(null); + } else if (button == btnUpdateBazaar) { + btnUpdateBazaar.enabled = false; + this.main.getChestTracker().refreshBazaarCache(); + updateBazaar = new AbortableRunnable() { + private int retries = 20 * 20; // retry for up to 20 seconds + private final long previousBazaarUpdate = main.getChestTracker().getLastBazaarUpdate(); + + @SubscribeEvent + public void onTickCheckBazaarDataUpdated(TickEvent.ClientTickEvent e) { + if (!stopped && e.phase == TickEvent.Phase.END) { + if (Minecraft.getMinecraft().theWorld == null || retries <= 0) { + // already stopped; or world gone, probably disconnected; or no retries left (took too long [20 seconds not enough?] or is not on SkyBlock): stop! + stop(); + return; + } + retries--; + if (previousBazaarUpdate == main.getChestTracker().getLastBazaarUpdate()) { + // bazaar data wasn't updated yet, retry next tick + return; + } + // refresh item overview + Minecraft.getMinecraft().addScheduledTask(() -> ChestOverviewGui.this.itemOverview.reloadItemData()); + stop(); + } + } + + @Override + public void stop() { + if (!stopped) { + stopped = true; + retries = -1; + MinecraftForge.EVENT_BUS.unregister(this); + stopScoreboardChecker(); + } + } + + @Override + public void run() { + MinecraftForge.EVENT_BUS.register(this); + } + }; + new TickDelay(updateBazaar, 20); // 2 second delay + retrying for 20 seconds, making sure bazaar data got updated + } else if (button == showNonBazaarItems) { + this.itemOverview.reloadItemData(); + } else if (button == btnCopy) { + StringBuilder allItemData = new StringBuilder("Item\tItem (formatted)\tAmount\tPrice (instant-sell)\tValue (instant-sell)\tPrice (sell offer)\tValue (sell offer)"); + for (ItemData itemData : itemOverview.itemDataHolder) { + allItemData.append(itemData.toCopyableFormat()); + } + allItemData.append("\n\n").append("Bazaar value (instant-sell):\t").append(itemOverview.summedValueInstaSell) + .append("\n").append("Bazaar value (sell offer):\t").append(itemOverview.summedValueSellOffer); + GuiScreen.setClipboardString(allItemData.toString()); + } else if (button == btnBazaarInstantOrOffer) { + if ("Use Instant-Sell prices".equals(btnBazaarInstantOrOffer.displayString)) { + btnBazaarInstantOrOffer.displayString = "Use Sell Offer prices"; + } else { + btnBazaarInstantOrOffer.displayString = "Use Instant-Sell prices"; + } + this.itemOverview.reloadItemData(); + } + } + } + + private void stopScoreboardChecker() { + if (updateBazaar != null) { + // there is still a bazaar update-checker running, stop it + updateBazaar.stop(); + updateBazaar = null; + } + } + + @Override + public void onGuiClosed() { + if (MooConfig.chestAnalyzerShowCommandUsage) { + main.getChatHelper().sendMessage(new MooChatComponent(EnumChatFormatting.GRAY + "Use " + EnumChatFormatting.WHITE + "/moo analyzeChests stop " + EnumChatFormatting.GRAY + "to stop the Chest Analyzer.").setSuggestCommand("/moo analyzeChests stop") + .appendFreshSibling(new MooChatComponent(EnumChatFormatting.GRAY + "Or continue adding more chests and run " + EnumChatFormatting.WHITE + "/moo analyzeChests " + EnumChatFormatting.GRAY + " again to re-run the analysis.").setSuggestCommand("/moo analyzeChests"))); + } + } + + @Override + public boolean doesGuiPauseGame() { + return true; + } + + @Override + public void handleMouseInput() throws IOException { + super.handleMouseInput(); + + if (this.itemOverview != null) { + this.itemOverview.handleMouseInput(); + } + } + + /** + * Inspired by {@link net.minecraft.client.gui.achievement.GuiStats} + */ + class ItemOverview extends GuiSlot { + private Column orderBy = Column.PRICE_SUM; + private boolean orderDesc = true; + private long lastOrderChange; + private List<ItemData> itemDataHolder; + private int summedValueInstaSell; + private int summedValueSellOffer; + + ItemOverview() { + super(ChestOverviewGui.this.mc, ChestOverviewGui.this.width, ChestOverviewGui.this.height, 32, ChestOverviewGui.this.height - 32, 16); + this.setShowSelectionBox(false); + // space above first entry for control buttons + int headerPadding = 20; + this.setHasListHeader(true, headerPadding); + + reloadItemData(); + } + + private void reloadItemData() { + boolean useInstantSellPrices = "Use Instant-Sell prices".equals(btnBazaarInstantOrOffer.displayString); + itemDataHolder = main.getChestTracker().getAnalysisResult(orderBy, orderDesc, useInstantSellPrices); + summedValueInstaSell = 0; + summedValueSellOffer = 0; + boolean showNonBazaarItems = ChestOverviewGui.this.showNonBazaarItems.isChecked(); + + for (Iterator<ItemData> iterator = itemDataHolder.iterator(); iterator.hasNext(); ) { + ItemData itemData = iterator.next(); + boolean hasBazaarPrice = false; + if (itemData.getBazaarInstantSellPrice() > 0) { + summedValueInstaSell += itemData.getBazaarInstantSellValue(); + hasBazaarPrice = true; + } + if (itemData.getBazaarSellOfferPrice() > 0) { + summedValueSellOffer += itemData.getBazaarSellOfferValue(); + hasBazaarPrice = true; + } + if (!showNonBazaarItems && !hasBazaarPrice) { + iterator.remove(); + } + } + } + + @Override + protected void drawListHeader(int x, int y, Tessellator tessellator) { + if (y < 0) { + // header not on screen + return; + } + int arrowX = -50; + // draw column titles + for (Column column : Column.values()) { + int columnX = x + column.getXOffset() - (column != Column.ITEM_NAME ? ChestOverviewGui.this.fontRendererObj.getStringWidth(column.getName()) : /* item name is aligned left, rest right */ 0); + ChestOverviewGui.this.drawString(ChestOverviewGui.this.fontRendererObj, column.getName(), columnX, y + 2, 0xFFFFFF); + if (column == orderBy) { + arrowX = columnX; + } + } + // draw arrow down/up + GuiHelper.drawSprite(arrowX - 18, y - 3, 18 + (orderDesc ? 0 : 18), 0, 500); + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + } + + @Override + protected int getSize() { + return this.itemDataHolder.size(); + } + + /** + * GuiSlot#clickedHeader + */ + @Override + protected void func_148132_a(int x, int y) { + long now = System.currentTimeMillis(); + if (now - lastOrderChange < 50) { + // detected two clicks + return; + } + lastOrderChange = now; + + int allowedMargin = 1; // tolerance to the left and right of a word to still be considered a click + for (Column column : Column.values()) { + int xOffset = getLeftX() + column.getXOffset(); + int columnTitleWidth = ChestOverviewGui.this.fontRendererObj.getStringWidth(column.getName()); + int columnXMin; + int columnXMax; + if (column == Column.ITEM_NAME) { + // aligned left + columnXMin = xOffset - /* up/down arrow */ 16; + columnXMax = xOffset + columnTitleWidth; + } else { + // aligned right + columnXMin = xOffset - columnTitleWidth - /* up/down arrow */ 16; + columnXMax = xOffset; + } + + if (mouseX + allowedMargin >= columnXMin && mouseX - allowedMargin <= columnXMax) { + // clicked on column title! + orderDesc = orderBy != column || !orderDesc; + orderBy = column; + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + break; + } + } + reloadItemData(); + } + + @Override + protected void elementClicked(int slotIndex, boolean isDoubleClick, int mouseX, int mouseY) { + } + + @Override + protected boolean isSelected(int slotIndex) { + return false; + } + + @Override + protected void drawBackground() { + } + + @Override + public int getListWidth() { + return this.width - 30; + } + + @Override + protected void drawSlot(int entryID, int x, int y, int z, int mouseXIn, int mouseYIn) { + if (!isMouseYWithinSlotBounds(y + 5)) { + // slot isn't visible anyways... + return; + } + ItemData itemData = itemDataHolder.get(entryID); + + // render item icon without shadows + GlStateManager.enableRescaleNormal(); + RenderHelper.enableGUIStandardItemLighting(); + ChestOverviewGui.this.itemRender.renderItemIntoGUI(itemData.getItemStack(), x, y - 3); + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableRescaleNormal(); + + FontRenderer fontRenderer = ChestOverviewGui.this.fontRendererObj; + String itemAmount = Utils.formatNumber(itemData.getAmount()); + int amountXPos = x + Column.ITEM_AMOUNT.getXOffset() - fontRenderer.getStringWidth(itemAmount); + int itemNameXPos = x + Column.ITEM_NAME.getXOffset(); + String itemName = fontRenderer.trimStringToWidth(itemData.getName(), amountXPos - itemNameXPos - 5); + if (itemName.length() != itemData.getName().length()) { + itemName += "ā¦"; + } + ChestOverviewGui.this.drawString(fontRenderer, itemName, itemNameXPos, y + 1, entryID % 2 == 0 ? 0xFFFFFF : 0x909090); + ChestOverviewGui.this.drawString(fontRenderer, itemAmount, amountXPos, y + 1, entryID % 2 == 0 ? 0xFFFFFF : 0x909090); + + boolean useInstantSellPrices = "Use Instant-Sell prices".equals(btnBazaarInstantOrOffer.displayString); + double itemPrice = useInstantSellPrices ? itemData.getBazaarInstantSellPrice() : itemData.getBazaarSellOfferPrice(); + String bazaarPrice = itemPrice > 0 ? Utils.formatDecimal(itemPrice) : EnumChatFormatting.DARK_GRAY + "?"; + ChestOverviewGui.this.drawString(fontRenderer, bazaarPrice, x + Column.PRICE_EACH.getXOffset() - fontRenderer.getStringWidth(bazaarPrice), y + 1, entryID % 2 == 0 ? 0xFFFFFF : 0x909090); + + double itemValue = useInstantSellPrices ? itemData.getBazaarInstantSellValue() : itemData.getBazaarSellOfferValue(); + String bazaarValue = itemPrice > 0 ? Utils.formatNumber(itemValue) : EnumChatFormatting.DARK_GRAY + "?"; + ChestOverviewGui.this.drawString(fontRenderer, bazaarValue, x + Column.PRICE_SUM.getXOffset() - fontRenderer.getStringWidth(bazaarValue), y + 1, entryID % 2 == 0 ? 0xFFFFFF : 0x909090); + } + + public void drawScreenPost(int mouseX, int mouseY) { + int xMin = getLeftX(); + String bazaarValueInstantSell = "ā Bazaar value (instant-sell prices): " + (summedValueInstaSell > 0 ? EnumChatFormatting.WHITE + Utils.formatNumber(summedValueInstaSell) : EnumChatFormatting.DARK_GRAY + "?"); // sum + ChestOverviewGui.this.drawString(ChestOverviewGui.this.fontRendererObj, bazaarValueInstantSell, xMin + 175 - ChestOverviewGui.this.fontRendererObj.getStringWidth("ā Bazaar value (instant-sell prices): "), ChestOverviewGui.this.height - 28, 0xdddddd); + String bazaarValueSellOffer = "ā Bazaar value (sell offer prices): " + (summedValueSellOffer > 0 ? EnumChatFormatting.WHITE + Utils.formatNumber(summedValueSellOffer) : EnumChatFormatting.DARK_GRAY + "?"); // sum + ChestOverviewGui.this.drawString(ChestOverviewGui.this.fontRendererObj, bazaarValueSellOffer, xMin + 175 - ChestOverviewGui.this.fontRendererObj.getStringWidth("ā Bazaar value (sell offer prices): "), ChestOverviewGui.this.height - 17, 0xdddddd); + + if (isMouseYWithinSlotBounds(mouseY)) { + int slotIndex = this.getSlotIndexFromScreenCoords(mouseX, mouseY); + + if (slotIndex >= 0) { + // mouse is over a slot: maybe draw item tooltip + int xMax = xMin + 16; // 16 = item icon width + if (mouseX < xMin || mouseX > xMax) { + // mouseX outside of valid item x values + return; + } + ItemData itemData = itemDataHolder.get(slotIndex); + FontRenderer font = itemData.getItemStack().getItem().getFontRenderer(itemData.getItemStack()); + GlStateManager.pushMatrix(); + ChestOverviewGui.this.drawHoveringText(itemData.getItemStack().getTooltip(mc.thePlayer, false), mouseX, mouseY, (font == null ? fontRendererObj : font)); + GlStateManager.popMatrix(); + } + } + } + + /** + * GuiSlot#drawScreen: x of slot + */ + private int getLeftX() { + return this.left + this.width / 2 - this.getListWidth() / 2 + 2; + } + } + + public enum Column { + ITEM_NAME("Item", 22), + ITEM_AMOUNT("Amount", 200), + PRICE_EACH("Price", 260), + PRICE_SUM("Value", 330); + + private final int xOffset; + private final String name; + + Column(String name, int xOffset) { + this.name = "" + EnumChatFormatting.BOLD + EnumChatFormatting.UNDERLINE + name; + this.xOffset = xOffset; + } + + public String getName() { + return name; + } + + public int getXOffset() { + return xOffset; + } + } +} diff --git a/src/main/java/de/cowtipper/cowlection/chestTracker/ChestTracker.java b/src/main/java/de/cowtipper/cowlection/chestTracker/ChestTracker.java new file mode 100644 index 0000000..cbad97b --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/chestTracker/ChestTracker.java @@ -0,0 +1,170 @@ +package de.cowtipper.cowlection.chestTracker; + +import de.cowtipper.cowlection.Cowlection; +import de.cowtipper.cowlection.util.ApiUtils; +import de.cowtipper.cowlection.util.MooChatComponent; +import net.minecraft.init.Blocks; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumFacing; +import net.minecraftforge.common.MinecraftForge; + +import java.util.*; + +public class ChestTracker { + private final Map<BlockPos, List<ItemStack>> chestCache = new HashMap<>(); + private final Map<BlockPos, EnumFacing> doubleChestCache = new HashMap<>(); + private Map<String, ItemData> analysisResult = new HashMap<>(); + private ChestInteractionListener chestInteractionListener; + private HyBazaarData bazaarCache; + private long lastBazaarUpdate; + private final Cowlection main; + + public ChestTracker(Cowlection main) { + this.main = main; + refreshBazaarCache(); + chestInteractionListener = new ChestInteractionListener(main); + MinecraftForge.EVENT_BUS.register(chestInteractionListener); + } + + public void analyzeResults() { + Map<String, ItemData> itemCounts = new HashMap<>(); + for (List<ItemStack> chestContents : chestCache.values()) { + for (ItemStack item : chestContents) { + String key = item.hasDisplayName() ? item.getDisplayName() : item.getUnlocalizedName(); + + if (item.hasTagCompound()) { + key = item.getTagCompound().getCompoundTag("ExtraAttributes").getString("id"); + } + + ItemData itemData = itemCounts.get(key); + if (itemData == null) { + itemData = new ItemData(key, item.copy()); + } + itemCounts.put(key, itemData.addAmount(item.stackSize)); + } + } + this.analysisResult = itemCounts; + } + + /** + * Returns ordered analysis result with prices + */ + public List<ItemData> getAnalysisResult(ChestOverviewGui.Column orderBy, boolean orderDesc, boolean useInstantSellPrices) { + List<ItemData> orderedAnalysisResult = new ArrayList<>(); + // sort by bazaar value (most value first) + for (Map.Entry<String, ItemData> itemEntry : analysisResult.entrySet()) { + if (bazaarCache != null && bazaarCache.isSuccess()) { + String productKey = itemEntry.getKey(); + HyBazaarData.Product product = bazaarCache.getProduct(productKey); + if (product != null) { + // item is sold on bazaar! + itemEntry.getValue().setBazaarInstantSellPrice(product.getInstantSellPrice()); + itemEntry.getValue().setBazaarSellOfferPrice(product.getSellOfferPrice()); + } + } + orderedAnalysisResult.add(itemEntry.getValue()); + } + Comparator<ItemData> comparator; + switch (orderBy) { + case ITEM_NAME: + comparator = Comparator.comparing(ItemData::getName); + break; + case ITEM_AMOUNT: + comparator = Comparator.comparing(ItemData::getAmount); + break; + case PRICE_EACH: + comparator = useInstantSellPrices ? Comparator.comparing(ItemData::getBazaarInstantSellPrice) : Comparator.comparing(ItemData::getBazaarSellOfferPrice); + break; + default: // case PRICE_SUM: + comparator = useInstantSellPrices ? Comparator.comparing(ItemData::getBazaarInstantSellValue) : Comparator.comparing(ItemData::getBazaarSellOfferValue); + break; + } + orderedAnalysisResult.sort((orderDesc ? comparator.reversed() : comparator).thenComparing(ItemData::getName)); + return orderedAnalysisResult; + } + + public Set<BlockPos> getCachedPositions() { + return chestCache.keySet(); + } + + public void clear() { + MinecraftForge.EVENT_BUS.unregister(chestInteractionListener); + chestInteractionListener = null; + bazaarCache = null; + chestCache.clear(); + doubleChestCache.clear(); + analysisResult.clear(); + } + + public void addChest(BlockPos chestPos, List<ItemStack> chestContents, EnumFacing otherChestFacing) { + if (chestContents.size() > 0) { // check if the chest is a chest we want to cache/analyze + ItemStack firstItem = chestContents.get(0); + if (firstItem != null && firstItem.hasDisplayName() && firstItem.getDisplayName().equals(" ") && firstItem.getItem() == Item.getItemFromBlock(Blocks.stained_glass_pane)) { + // item in first slot of chest is a glass pane with the display name " ", indicating e.g. a minion chest which we don't want to track + return; + } + } + BlockPos mainChestPos = chestPos; + + if (otherChestFacing != EnumFacing.UP) { // we have a double chest! + if (isOtherChestCached(chestPos, otherChestFacing)) { // other chest is cached already, update that one instead + mainChestPos = chestPos.offset(otherChestFacing); + } + + if (chestPos.equals(mainChestPos)) { + doubleChestCache.put(chestPos, otherChestFacing); + } else { + doubleChestCache.put(mainChestPos, otherChestFacing.getOpposite()); + } + } + chestCache.put(mainChestPos, chestContents); + } + + public void removeChest(BlockPos chestPos, EnumFacing otherChestFacing) { + BlockPos mainChestPos = chestPos; + + if (otherChestFacing != EnumFacing.UP) { // we have a double chest! + if (isOtherChestCached(chestPos, otherChestFacing)) { // other chest is cached already, update that one instead + mainChestPos = chestPos.offset(otherChestFacing); + } + + if (chestPos.equals(mainChestPos)) { + doubleChestCache.remove(chestPos); + } else { + doubleChestCache.remove(mainChestPos); + } + } + chestCache.remove(mainChestPos); + } + + private boolean isOtherChestCached(BlockPos chestPos, EnumFacing otherChestFacing) { + BlockPos otherChestPos = chestPos.offset(otherChestFacing); + return chestCache.containsKey(otherChestPos); + } + + public EnumFacing getOtherChestFacing(BlockPos pos) { + return doubleChestCache.getOrDefault(pos, EnumFacing.UP); + } + + public void refreshBazaarCache() { + if (allowUpdateBazaar()) { + ApiUtils.fetchBazaarData(bazaarData -> { + if (bazaarData == null || !bazaarData.isSuccess()) { + main.getChatHelper().sendMessage(new MooChatComponent("Error: Couldn't get Bazaar data from Hypixel API! API might be down: check status.hypixel.net").red().setUrl("https://status.hypixel.net/")); + } + this.bazaarCache = bazaarData; + this.lastBazaarUpdate = System.currentTimeMillis(); + }); + } + } + + public boolean allowUpdateBazaar() { + return bazaarCache == null || bazaarCache.allowRefreshData(); + } + + public long getLastBazaarUpdate() { + return this.lastBazaarUpdate; + } +} diff --git a/src/main/java/de/cowtipper/cowlection/chestTracker/HyBazaarData.java b/src/main/java/de/cowtipper/cowlection/chestTracker/HyBazaarData.java new file mode 100644 index 0000000..6480244 --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/chestTracker/HyBazaarData.java @@ -0,0 +1,61 @@ +package de.cowtipper.cowlection.chestTracker; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +@SuppressWarnings("unused") +public class HyBazaarData { + private boolean success; + private long lastUpdated; + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private Map<String, Product> products; + + public boolean isSuccess() { + return success; + } + + /** + * Returns {@link Product} from bazaar reply. + * Returns null if product does not exist + * + * @param productId product in bazaar + * @return instance of Product + */ + public Product getProduct(String productId) { + return products.get(productId); + } + + /** + * Refresh only allowed once per minute + */ + public boolean allowRefreshData() { + return (System.currentTimeMillis() - lastUpdated) > 60000; + } + + public static class Product { + @SerializedName("quick_status") + private Status quickStatus; + + public double getInstantSellPrice() { + return quickStatus.getSellPrice(); + } + + public double getSellOfferPrice() { + return quickStatus.getBuyPrice(); + } + + public static class Status { + private double sellPrice; + private double buyPrice; + + public double getSellPrice() { + return sellPrice; + } + + public double getBuyPrice() { + return buyPrice; + } + } + } +} diff --git a/src/main/java/de/cowtipper/cowlection/chestTracker/ItemData.java b/src/main/java/de/cowtipper/cowlection/chestTracker/ItemData.java new file mode 100644 index 0000000..062da2b --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/chestTracker/ItemData.java @@ -0,0 +1,70 @@ +package de.cowtipper.cowlection.chestTracker; + +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; + +public class ItemData { + private final String key; + private final ItemStack itemStack; + private final String name; + private int amount; + private double bazaarInstantSellPrice = -1; + private double bazaarSellOfferPrice = -1; + + public ItemData(String key, ItemStack itemStack) { + this.key = key; + this.itemStack = itemStack; + this.itemStack.stackSize = 1; + this.name = itemStack.getDisplayName(); + this.amount = 0; + } + + public String getKey() { + return key; + } + + public ItemStack getItemStack() { + return itemStack; + } + + public String getName() { + return name; + } + + public int getAmount() { + return amount; + } + + public double getBazaarInstantSellPrice() { + return bazaarInstantSellPrice; + } + + public void setBazaarInstantSellPrice(double bazaarInstantSellPrice) { + this.bazaarInstantSellPrice = bazaarInstantSellPrice; + } + + public double getBazaarSellOfferPrice() { + return bazaarSellOfferPrice; + } + + public void setBazaarSellOfferPrice(double bazaarSellOfferPrice) { + this.bazaarSellOfferPrice = bazaarSellOfferPrice; + } + + public ItemData addAmount(int stackSize) { + this.amount += stackSize; + return this; + } + + public double getBazaarInstantSellValue() { + return bazaarInstantSellPrice >= 0 ? amount * bazaarInstantSellPrice : -1; + } + + public double getBazaarSellOfferValue() { + return bazaarSellOfferPrice >= 0 ? amount * bazaarSellOfferPrice : -1; + } + + public String toCopyableFormat() { + return "\n" + EnumChatFormatting.getTextWithoutFormattingCodes(name) + "\t" + name + "\t" + amount + "\t" + Math.round(getBazaarInstantSellPrice()) + "\t" + Math.round(getBazaarInstantSellValue()) + "\t" + Math.round(getBazaarSellOfferPrice()) + "\t" + Math.round(getBazaarSellOfferValue()); + } +} diff --git a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java index 26ef279..745ea15 100644 --- a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java +++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java @@ -4,6 +4,7 @@ import com.mojang.authlib.GameProfile; import com.mojang.authlib.properties.Property; import com.mojang.realmsclient.util.Pair; import de.cowtipper.cowlection.Cowlection; +import de.cowtipper.cowlection.chestTracker.ChestOverviewGui; import de.cowtipper.cowlection.command.exception.ApiContactException; import de.cowtipper.cowlection.command.exception.InvalidPlayerNameException; import de.cowtipper.cowlection.command.exception.MooCommandException; @@ -113,7 +114,9 @@ public class MooCommand extends CommandBase { || args[0].equalsIgnoreCase("stalksb") || args[0].equalsIgnoreCase("sbstalk") || args[0].equalsIgnoreCase("askPolitelyAboutTheirSkyBlockProgress")) { handleStalkingSkyBlock(args); - } else if (args[0].equalsIgnoreCase("analyzeIsland")) { + } else if (args[0].equalsIgnoreCase("chestAnalyzer") || args[0].equalsIgnoreCase("chestAnalyser") || args[0].equalsIgnoreCase("analyzeChests") || args[0].equalsIgnoreCase("analyseChests")) { + handleAnalyzeChests(args); + } else if (args[0].equalsIgnoreCase("analyzeIsland") || args[0].equalsIgnoreCase("analyseIsland")) { handleAnalyzeIsland(sender); } else if (args[0].equalsIgnoreCase("waila") || args[0].equalsIgnoreCase("whatAmILookingAt")) { boolean showAllInfo = MooConfig.keepFullWailaInfo(); @@ -682,6 +685,42 @@ public class MooCommand extends CommandBase { }); } + private void handleAnalyzeChests(String[] args) { + if (args.length == 1) { + boolean enabledChestTracker = main.enableChestTracker(); + if (enabledChestTracker) { + // chest tracker wasn't enabled before, now it is + String analyzeCommand = "/" + getCommandName() + " analyzeChests"; + if (MooConfig.chestAnalyzerShowCommandUsage) { + main.getChatHelper().sendMessage(new MooChatComponent("Enabled chest tracker! You can now...").green() + .appendFreshSibling(new MooChatComponent(EnumChatFormatting.GREEN + " ā¶ " + EnumChatFormatting.YELLOW + "add chests on your island by opening them; deselect chests by Sneaking + Right Click.").yellow()) + .appendFreshSibling(new MooChatComponent(EnumChatFormatting.GREEN + " ā· " + EnumChatFormatting.YELLOW + "use " + EnumChatFormatting.GOLD + analyzeCommand + EnumChatFormatting.YELLOW + " again to run the chest analysis.").yellow().setSuggestCommand(analyzeCommand)) + .appendFreshSibling(new MooChatComponent(EnumChatFormatting.GREEN + " āø " + EnumChatFormatting.YELLOW + "use " + EnumChatFormatting.GOLD + analyzeCommand + " stop" + EnumChatFormatting.YELLOW + " to stop the chest tracker and clear current results.").yellow().setSuggestCommand(analyzeCommand + " stop"))); + } else { + main.getChatHelper().sendMessage(new MooChatComponent("Enabled chest tracker! " + EnumChatFormatting.GRAY + "Run " + analyzeCommand + " again to run the chest analysis.").green().setSuggestCommand(analyzeCommand)); + } + } else { + // chest tracker was already enabled, open analysis GUI + main.getChestTracker().analyzeResults(); + new TickDelay(() -> Minecraft.getMinecraft().displayGuiScreen(new ChestOverviewGui(main)), 1); + } + } else if (args.length == 2 && args[1].equalsIgnoreCase("stop")) { + boolean disabledChestTracker = main.disableChestTracker(); + if (disabledChestTracker) { + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Disabled chest tracker and cleared chest cache!"); + } else { + main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, "Chest tracker wasn't even enabled..."); + } + } else { + String analyzeCommand = "/" + getCommandName() + " analyzeChests"; + main.getChatHelper().sendMessage(new MooChatComponent(Cowlection.MODNAME + " chest tracker & analyzer").gold().bold() + .appendFreshSibling(new MooChatComponent("Use " + EnumChatFormatting.GOLD + analyzeCommand + EnumChatFormatting.YELLOW + " to start tracking chests on your island! " + EnumChatFormatting.GREEN + "Then you can...").yellow().setSuggestCommand(analyzeCommand)) + .appendFreshSibling(new MooChatComponent(EnumChatFormatting.GREEN + " ā¶ " + EnumChatFormatting.YELLOW + "add chests by opening them; deselect chests by Sneaking + Right Click.").yellow()) + .appendFreshSibling(new MooChatComponent(EnumChatFormatting.GREEN + " ā· " + EnumChatFormatting.YELLOW + "use " + EnumChatFormatting.GOLD + analyzeCommand + EnumChatFormatting.YELLOW + " again to run the chest analysis.").yellow().setSuggestCommand(analyzeCommand)) + .appendFreshSibling(new MooChatComponent(EnumChatFormatting.GREEN + " āø " + EnumChatFormatting.YELLOW + "use " + EnumChatFormatting.GOLD + analyzeCommand + " stop" + EnumChatFormatting.YELLOW + " to stop the chest tracker and clear current results.").yellow().setSuggestCommand(analyzeCommand + " stop"))); + } + } + private void handleAnalyzeIsland(ICommandSender sender) { Map<String, String> minions = DataHelper.getMinions(); @@ -1100,7 +1139,8 @@ public class MooCommand extends CommandBase { .appendSibling(createCmdHelpEntry("nameChangeCheck", "Force a scan for a changed name of a best friend (is done automatically as well)")) .appendSibling(createCmdHelpSection(2, "SkyBlock")) .appendSibling(createCmdHelpEntry("stalkskyblock", "Get info of player's SkyBlock stats Ā§dĀ§lā·")) - .appendSibling(createCmdHelpEntry("analyzeIsland", "Analyze a SkyBlock private island")) + .appendSibling(createCmdHelpEntry("analyzeChests", "Analyze chests' contents and evaluate potential Bazaar value")) + .appendSibling(createCmdHelpEntry("analyzeIsland", "Analyze a SkyBlock private island (inspect minions)")) .appendSibling(createCmdHelpEntry("waila", "Copy the 'thing' you're looking at")) .appendSibling(createCmdHelpEntry("dungeon", "SkyBlock Dungeons: display current dungeon performance")) .appendSibling(createCmdHelpEntry("dungeon party", "SkyBlock Dungeons: Shows armor and dungeon info about current party members " + EnumChatFormatting.GRAY + "(alias: " + EnumChatFormatting.WHITE + "/" + getCommandName() + " dp" + EnumChatFormatting.GRAY + ") Ā§dĀ§lā·")) @@ -1138,10 +1178,10 @@ public class MooCommand extends CommandBase { public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) { if (args.length == 1) { return getListOfStringsMatchingLastWord(args, - /* help */ "help", + /* main */ "help", "config", /* Best friends, friends & other players */ "stalk", "add", "remove", "list", "online", "nameChangeCheck", - /* SkyBlock */ "stalkskyblock", "skyblockstalk", "analyzeIsland", "waila", "whatAmILookingAt", "dungeon", - /* miscellaneous */ "config", "search", "worldage", "serverage", "guiscale", "rr", "shrug", "apikey", + /* SkyBlock */ "stalkskyblock", "skyblockstalk", "chestAnalyzer", "analyzeChests", "analyzeIsland", "waila", "whatAmILookingAt", "dungeon", + /* miscellaneous */ "search", "worldage", "serverage", "guiscale", "rr", "shrug", "apikey", /* update mod */ "update", "updateHelp", "version", "directory", /* rarely used aliases */ "askPolitelyWhereTheyAre", "askPolitelyAboutTheirSkyBlockProgress", "year", "whatyearisit"); } else if (args.length == 2 && (args[0].equalsIgnoreCase("waila") || args[0].equalsIgnoreCase("whatAmILookingAt"))) { @@ -1152,6 +1192,8 @@ public class MooCommand extends CommandBase { return getListOfStringsMatchingLastWord(args, "party", "enter", "leave"); } else if (args.length == 2 && (args[0].equalsIgnoreCase("worldage") || args[0].equalsIgnoreCase("serverage"))) { return getListOfStringsMatchingLastWord(args, "off", "on", "disable", "enable"); + } else if (args.length == 2 && (args[0].equalsIgnoreCase("chestAnalyzer") || args[0].equalsIgnoreCase("chestAnalyser") || args[0].equalsIgnoreCase("analyzeChests") || args[0].equalsIgnoreCase("analyseChests"))) { + return getListOfStringsMatchingLastWord(args, "stop"); } String commandArg = args[0].toLowerCase(); if (args.length == 2 && (commandArg.equals("s") || commandArg.equals("ss") || commandArg.equals("namechangecheck") || commandArg.contains("stalk") || commandArg.contains("askpolitely"))) { // stalk & stalkskyblock + namechangecheck diff --git a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java index 74d9876..27f12e7 100644 --- a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java +++ b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java @@ -71,6 +71,9 @@ public class MooConfig { public static int notifyFreshServer; public static int notifyOldServer; public static boolean notifyServerAge; + public static boolean chestAnalyzerShowNonBazaarItems; + private static String chestAnalyzerUseBazaarPrices; + public static boolean chestAnalyzerShowCommandUsage; public static int tooltipToggleKeyBinding; private static String tooltipItemAge; public static boolean tooltipItemAgeShortened; @@ -374,6 +377,20 @@ public class MooConfig { Property propNotifyServerAge = subCat.addConfigEntry(cfg.get(configCat.getConfigName(), "notifyServerAge", true, "Show server age notifications?")); + // Sub-Category: Chest Analyzer (Bazaar prices) + subCat = configCat.addSubCategory("Chest Tracker & Analyzer (Bazaar prices)"); + String analyzeCommand = "/moo analyzeChests"; + subCat.addExplanations("Use " + EnumChatFormatting.YELLOW + analyzeCommand + EnumChatFormatting.RESET + " to start tracking chests on your island! " + EnumChatFormatting.GREEN + "Then you can...", + EnumChatFormatting.GREEN + " ā¶ " + EnumChatFormatting.RESET + "add chests by opening them; deselect chests by Sneaking + Right Click.", + EnumChatFormatting.GREEN + " ā· " + EnumChatFormatting.RESET + "use " + EnumChatFormatting.YELLOW + analyzeCommand + EnumChatFormatting.RESET + " again to run the chest analysis.", + EnumChatFormatting.GREEN + " āø " + EnumChatFormatting.RESET + "use " + EnumChatFormatting.YELLOW + analyzeCommand + " stop" + EnumChatFormatting.RESET + " to stop the chest tracker and clear current results."); + Property propChestAnalyzerShowNonBazaarItems = subCat.addConfigEntry(cfg.get(configCat.getConfigName(), + "chestAnalyzerShowNonBazaarItems", false, "Show non-Bazaar items in Chest Tracker?")); + Property propChestAnalyzerUseBazaarPrices = subCat.addConfigEntry(cfg.get(configCat.getConfigName(), + "chestAnalyzerUseBazaarPrices", "Instant-Sell", "Use Bazaar prices?", new String[]{"Instant-Sell", "Sell Offer"})); + Property propChestAnalyzerShowCommandUsage = subCat.addConfigEntry(cfg.get(configCat.getConfigName(), + "chestAnalyzerShowCommandUsage", true, "Show command usage?")); + // Sub-Category: Tooltip enhancements subCat = configCat.addSubCategory("Tooltip & GUI enhancements"); @@ -630,6 +647,9 @@ public class MooConfig { notifyFreshServer = propNotifyFreshServer.getInt(); notifyOldServer = propNotifyOldServer.getInt(); notifyServerAge = propNotifyServerAge.getBoolean(); + chestAnalyzerShowNonBazaarItems = propChestAnalyzerShowNonBazaarItems.getBoolean(); + chestAnalyzerUseBazaarPrices = propChestAnalyzerUseBazaarPrices.getString(); + chestAnalyzerShowCommandUsage = propChestAnalyzerShowCommandUsage.getBoolean(); tooltipToggleKeyBinding = propTooltipToggleKeyBinding.getInt(); tooltipItemAge = propTooltipItemAge.getString(); tooltipItemAgeShortened = propTooltipItemAgeShortened.getBoolean(); @@ -708,6 +728,9 @@ public class MooConfig { propNotifyFreshServer.set(notifyFreshServer); propNotifyOldServer.set(notifyOldServer); propNotifyServerAge.set(notifyServerAge); + propChestAnalyzerShowNonBazaarItems.set(chestAnalyzerShowNonBazaarItems); + propChestAnalyzerUseBazaarPrices.set(chestAnalyzerUseBazaarPrices); + propChestAnalyzerShowCommandUsage.set(chestAnalyzerShowCommandUsage); propTooltipToggleKeyBinding.set(tooltipToggleKeyBinding); propTooltipItemAge.set(tooltipItemAge); propTooltipItemAgeShortened.set(tooltipItemAgeShortened); @@ -873,6 +896,10 @@ public class MooConfig { return Setting.get(enableSkyBlockOnlyFeatures); } + public static boolean useInstantSellBazaarPrices() { + return "Instant-Sell".equals(chestAnalyzerUseBazaarPrices); + } + public static Setting getTooltipAuctionHousePriceEachDisplay() { return Setting.get(tooltipAuctionHousePriceEach); } diff --git a/src/main/java/de/cowtipper/cowlection/search/GuiSearch.java b/src/main/java/de/cowtipper/cowlection/search/GuiSearch.java index ae17d32..21059ca 100644 --- a/src/main/java/de/cowtipper/cowlection/search/GuiSearch.java +++ b/src/main/java/de/cowtipper/cowlection/search/GuiSearch.java @@ -4,7 +4,6 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.realmsclient.util.Pair; import de.cowtipper.cowlection.Cowlection; import de.cowtipper.cowlection.config.MooConfig; -import de.cowtipper.cowlection.data.LogEntry; import de.cowtipper.cowlection.util.GuiHelper; import de.cowtipper.cowlection.util.Utils; import net.minecraft.client.Minecraft; diff --git a/src/main/java/de/cowtipper/cowlection/data/LogEntry.java b/src/main/java/de/cowtipper/cowlection/search/LogEntry.java index aa59483..cd86a78 100644 --- a/src/main/java/de/cowtipper/cowlection/data/LogEntry.java +++ b/src/main/java/de/cowtipper/cowlection/search/LogEntry.java @@ -1,4 +1,4 @@ -package de.cowtipper.cowlection.data; +package de.cowtipper.cowlection.search; import net.minecraft.util.EnumChatFormatting; import org.apache.commons.lang3.builder.EqualsBuilder; diff --git a/src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java b/src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java index 0d292ae..a3ed781 100644 --- a/src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java +++ b/src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java @@ -2,7 +2,6 @@ package de.cowtipper.cowlection.search; import com.mojang.realmsclient.util.Pair; import de.cowtipper.cowlection.config.MooConfig; -import de.cowtipper.cowlection.data.LogEntry; import net.minecraft.util.EnumChatFormatting; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutableTriple; diff --git a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java index 03e4a14..584e818 100644 --- a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java +++ b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java @@ -5,6 +5,7 @@ import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import com.mojang.util.UUIDTypeAdapter; import de.cowtipper.cowlection.Cowlection; +import de.cowtipper.cowlection.chestTracker.HyBazaarData; import de.cowtipper.cowlection.command.exception.ThrowingConsumer; import de.cowtipper.cowlection.config.CredentialStorage; import de.cowtipper.cowlection.data.*; @@ -27,6 +28,7 @@ public class ApiUtils { private static final String UUID_TO_NAME_URL = "https://api.mojang.com/user/profiles/%s/names"; private static final String ONLINE_STATUS_URL = "https://api.hypixel.net/status?key=%s&uuid=%s"; private static final String SKYBLOCK_STATS_URL = "https://api.hypixel.net/skyblock/profiles?key=%s&uuid=%s"; + private static final String BAZAAR_URL = "https://api.hypixel.net/skyblock/bazaar"; private static final String PLAYER_URL = "https://api.hypixel.net/player?key=%s&uuid=%s"; private static final String API_KEY_URL = "https://api.hypixel.net/key?key=%s"; private static final ExecutorService pool = Executors.newCachedThreadPool(); @@ -101,6 +103,21 @@ public class ApiUtils { return null; } + public static void fetchBazaarData(ThrowingConsumer<HyBazaarData> action) { + pool.execute(() -> action.accept(getBazaarData())); + } + + private static HyBazaarData getBazaarData() { + try (BufferedReader reader = makeApiCall(BAZAAR_URL)) { + if (reader != null) { + return GsonUtils.fromJson(reader, HyBazaarData.class); + } + } catch (IOException | JsonSyntaxException e) { + e.printStackTrace(); + } + return null; + } + public static void fetchHyPlayerDetails(Friend stalkedPlayer, ThrowingConsumer<HyPlayerData> action) { pool.execute(() -> action.accept(stalkHyPlayer(stalkedPlayer))); } diff --git a/src/main/java/de/cowtipper/cowlection/util/Utils.java b/src/main/java/de/cowtipper/cowlection/util/Utils.java index 47eb356..f15b095 100644 --- a/src/main/java/de/cowtipper/cowlection/util/Utils.java +++ b/src/main/java/de/cowtipper/cowlection/util/Utils.java @@ -12,6 +12,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -21,6 +22,8 @@ public final class Utils { public static final Pattern VALID_UUID_PATTERN = Pattern.compile("^(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$"); private static final Pattern VALID_USERNAME = Pattern.compile("^[\\w]{1,16}$"); private static final NavigableMap<Double, Character> NUMBER_SUFFIXES = new TreeMap<>(); + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.#", new DecimalFormatSymbols(Locale.ENGLISH)); + private static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("#,##0", new DecimalFormatSymbols(Locale.ENGLISH)); static { NUMBER_SUFFIXES.put(1_000d, 'k'); @@ -90,10 +93,10 @@ public final class Utils { } double monthsPast = daysPast / 30.5d; if (monthsPast < 12) { - return new DecimalFormat("0.#").format(monthsPast) + " month" + (monthsPast >= 2 ? "s" : ""); + return DECIMAL_FORMAT.format(monthsPast) + " month" + (monthsPast >= 2 ? "s" : ""); } double yearsPast = monthsPast / 12d; - return new DecimalFormat("0.#").format(yearsPast) + " year" + (yearsPast >= 2 ? "s" : ""); + return DECIMAL_FORMAT.format(yearsPast) + " year" + (yearsPast >= 2 ? "s" : ""); } public static String toRealPath(Path path) { @@ -109,6 +112,14 @@ public final class Utils { return toRealPath(path.toPath()); } + public static String formatDecimal(double number) { + return DECIMAL_FORMAT.format(number); + } + + public static String formatNumber(double number) { + return NUMBER_FORMAT.format(number); + } + /** * Formats a large number with abbreviations for each factor of a thousand (k, m, b, t) */ @@ -132,7 +143,7 @@ public final class Utils { default: amountOfDecimals = "###"; } - DecimalFormat df = new DecimalFormat("#,##0." + amountOfDecimals); + DecimalFormat df = new DecimalFormat("#,##0." + amountOfDecimals, new DecimalFormatSymbols(Locale.ENGLISH)); return df.format(number / divideBy) + suffix; } diff --git a/src/main/resources/assets/cowlection/lang/en_US.lang b/src/main/resources/assets/cowlection/lang/en_US.lang index 4328dbb..195d148 100644 --- a/src/main/resources/assets/cowlection/lang/en_US.lang +++ b/src/main/resources/assets/cowlection/lang/en_US.lang @@ -40,6 +40,12 @@ cowlection.config.notifyOldServer=Notify when server restarted ā„X days ago cowlection.config.notifyOldServer.tooltip=Notify when joining a server that hasn't restarted for X ingame days.\nĀ§eSet to 0 to disable notifications! cowlection.config.notifyServerAge=Show server age notifications? cowlection.config.notifyServerAge.tooltip=Overrides the two settings above.\nĀ§7This setting can also be changed with Ā§e/moo worldage <on|off> +cowlection.config.chestAnalyzerShowNonBazaarItems=Show non-Bazaar items? +cowlection.config.chestAnalyzerShowNonBazaarItems.tooltip=Should items that are Ā§enot Ā§ron the Bazaar be displayed by default? +cowlection.config.chestAnalyzerUseBazaarPrices=Use Bazaar prices +cowlection.config.chestAnalyzerUseBazaarPrices.tooltip=Should Ā§eInstant-Sell Ā§ror Ā§eSell Offer Ā§rprices be used by default? +cowlection.config.chestAnalyzerShowCommandUsage=Show command usage? +cowlection.config.chestAnalyzerShowCommandUsage.tooltip=Should the command usage of Ā§e/moo analyzeChests Ā§rbe displayed again each time? cowlection.config.tooltipToggleKeyBinding=Key binding: toggle tooltip cowlection.config.tooltipToggleKeyBinding.tooltip=Hold down this key to toggle tooltip if one of the following settings is set to 'key press'\n\nĀ§7Ā§odisable key binding: Ā§eĀ§oset key binding to Ā§lESC cowlection.config.tooltipItemAge=Ā§7Items: Ā§rShow item age |