From 953153625f8586201a85d8b983ef2d9dd0e98807 Mon Sep 17 00:00:00 2001 From: Cow Date: Sun, 25 Apr 2021 23:36:52 +0200 Subject: Bumped version to 1.8.9-0.13.0 - also minor text fixes (package name, readme etc) --- CHANGELOG.md | 6 +- README.md | 4 +- gradle.properties | 2 +- .../java/de/cowtipper/cowlection/Cowlection.java | 2 +- .../chestTracker/ChestInteractionListener.java | 277 -------------- .../cowlection/chestTracker/ChestOverviewGui.java | 418 --------------------- .../cowlection/chestTracker/ChestTracker.java | 170 --------- .../cowlection/chestTracker/HyBazaarData.java | 61 --- .../cowlection/chestTracker/ItemData.java | 70 ---- .../chesttracker/ChestInteractionListener.java | 277 ++++++++++++++ .../cowlection/chesttracker/ChestOverviewGui.java | 418 +++++++++++++++++++++ .../cowlection/chesttracker/ChestTracker.java | 170 +++++++++ .../cowlection/chesttracker/HyBazaarData.java | 61 +++ .../cowlection/chesttracker/ItemData.java | 70 ++++ .../cowtipper/cowlection/command/MooCommand.java | 4 +- .../de/cowtipper/cowlection/util/ApiUtils.java | 2 +- update.json | 6 +- 17 files changed, 1010 insertions(+), 1008 deletions(-) delete mode 100644 src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java delete mode 100644 src/main/java/de/cowtipper/cowlection/chestTracker/ChestOverviewGui.java delete mode 100644 src/main/java/de/cowtipper/cowlection/chestTracker/ChestTracker.java delete mode 100644 src/main/java/de/cowtipper/cowlection/chestTracker/HyBazaarData.java delete mode 100644 src/main/java/de/cowtipper/cowlection/chestTracker/ItemData.java create mode 100644 src/main/java/de/cowtipper/cowlection/chesttracker/ChestInteractionListener.java create mode 100644 src/main/java/de/cowtipper/cowlection/chesttracker/ChestOverviewGui.java create mode 100644 src/main/java/de/cowtipper/cowlection/chesttracker/ChestTracker.java create mode 100644 src/main/java/de/cowtipper/cowlection/chesttracker/HyBazaarData.java create mode 100644 src/main/java/de/cowtipper/cowlection/chesttracker/ItemData.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f81699..a7c7719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [1.8.9-0.13.0] - unreleased +## [1.8.9-0.13.0] - 25.04.2021 ### Added - Bestiary Overview: enhances tooltips of `/bestiary` ⬌ `/be` - hover over one of the area/location-items in a *sub*-category of the Bestiary to see an overview of the tiers upgrades you are closest to @@ -46,7 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fixed issue with 'no dung class selected' - Unexpected API-related exceptions no longer void all chat output -- Greatly increased speed of the Log Search (`/moo search`) +- Greatly increased speed of the Log Search (`/moo search`) ## [1.8.9-0.12.0] - 03.01.2021 ### Added @@ -316,7 +316,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). *Note:* The 'best friends' list is currently available via ESC > Mod Options > Cowlection > Config > bestFriends. -[1.8.9-0.13.0]: https://github.com/cow-mc/Cowlection/compare/v1.8.9-0.12.0...master +[1.8.9-0.13.0]: https://github.com/cow-mc/Cowlection/compare/v1.8.9-0.12.0...v1.8.9-0.13.0 [1.8.9-0.12.0]: https://github.com/cow-mc/Cowlection/compare/v1.8.9-0.11.0...v1.8.9-0.12.0 [1.8.9-0.11.0]: https://github.com/cow-mc/Cowlection/compare/v1.8.9-0.10.2...v1.8.9-0.11.0 [1.8.9-0.10.2]: https://github.com/cow-mc/Cowlection/compare/v1.8.9-0.10.1...v1.8.9-0.10.2 diff --git a/README.md b/README.md index 5f8aa27..e07d828 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,9 @@ It is a collection of different features mainly focused on Hypixel SkyBlock. | 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 shift (configurable) while viewing a dungeon item tooltip | +| Improved Dungeon item tooltips (item quality + obtained floor; normalize dungeon item stats) | To normalize stats: Hold shift (configurable) while viewing a dungeon item tooltip | +| Improved Dungeon Party Finder | configure with `/moo config party` | +| Dungeon Party inspector (Who is in my party again? What class are we missing?) | `/moo dungeon party` or `/moo dp` | | Dungeon performance tracker and overlay: Skill score calculation, class milestone tracker, destroyed crypts tracker, and elapsed time indicator | automatically; or with `/moo dungeon` | | Check how long current world has been loaded (≈ when the server was last restarted) | `/moo worldage` + `/moo config` → SkyBlock | | Additional info in various tooltips (e.g. show item age, display pet exp, price per item in an auction, Bestiary overview) | `/moo config` → SkyBlock → Tooltip enhancements | diff --git a/gradle.properties b/gradle.properties index a8e993b..8881b56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ mod_name=Cowlection group_package=de.cowtipper.cowlection -mod_version=0.12.0 +mod_version=0.13.0 git_url=https://github.com/cow-mc/Cowlection/ mc_version=1.8.9 diff --git a/src/main/java/de/cowtipper/cowlection/Cowlection.java b/src/main/java/de/cowtipper/cowlection/Cowlection.java index 855e7c4..23ff2a8 100644 --- a/src/main/java/de/cowtipper/cowlection/Cowlection.java +++ b/src/main/java/de/cowtipper/cowlection/Cowlection.java @@ -1,6 +1,6 @@ package de.cowtipper.cowlection; -import de.cowtipper.cowlection.chestTracker.ChestTracker; +import de.cowtipper.cowlection.chesttracker.ChestTracker; import de.cowtipper.cowlection.command.MooCommand; import de.cowtipper.cowlection.command.ReplyCommand; import de.cowtipper.cowlection.command.ShrugCommand; diff --git a/src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java b/src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java deleted file mode 100644 index f4278fb..0000000 --- a/src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java +++ /dev/null @@ -1,277 +0,0 @@ -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 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 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 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 deleted file mode 100644 index 970eb93..0000000 --- a/src/main/java/de/cowtipper/cowlection/chestTracker/ChestOverviewGui.java +++ /dev/null @@ -1,418 +0,0 @@ -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 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 void addTooltip(T field, List 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 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 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 deleted file mode 100644 index cbad97b..0000000 --- a/src/main/java/de/cowtipper/cowlection/chestTracker/ChestTracker.java +++ /dev/null @@ -1,170 +0,0 @@ -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> chestCache = new HashMap<>(); - private final Map doubleChestCache = new HashMap<>(); - private Map 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 itemCounts = new HashMap<>(); - for (List 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 getAnalysisResult(ChestOverviewGui.Column orderBy, boolean orderDesc, boolean useInstantSellPrices) { - List orderedAnalysisResult = new ArrayList<>(); - // sort by bazaar value (most value first) - for (Map.Entry 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 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 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 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 deleted file mode 100644 index 6480244..0000000 --- a/src/main/java/de/cowtipper/cowlection/chestTracker/HyBazaarData.java +++ /dev/null @@ -1,61 +0,0 @@ -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 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 deleted file mode 100644 index 062da2b..0000000 --- a/src/main/java/de/cowtipper/cowlection/chestTracker/ItemData.java +++ /dev/null @@ -1,70 +0,0 @@ -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/chesttracker/ChestInteractionListener.java b/src/main/java/de/cowtipper/cowlection/chesttracker/ChestInteractionListener.java new file mode 100644 index 0000000..eea6988 --- /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 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 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 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..2c20727 --- /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 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 void addTooltip(T field, List 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 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 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..64e4067 --- /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> chestCache = new HashMap<>(); + private final Map doubleChestCache = new HashMap<>(); + private Map 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 itemCounts = new HashMap<>(); + for (List 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 getAnalysisResult(ChestOverviewGui.Column orderBy, boolean orderDesc, boolean useInstantSellPrices) { + List orderedAnalysisResult = new ArrayList<>(); + // sort by bazaar value (most value first) + for (Map.Entry 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 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 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 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..38aba6f --- /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 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..05f52bf --- /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 745ea15..7deec61 100644 --- a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java +++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java @@ -4,7 +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.chesttracker.ChestOverviewGui; import de.cowtipper.cowlection.command.exception.ApiContactException; import de.cowtipper.cowlection.command.exception.InvalidPlayerNameException; import de.cowtipper.cowlection.command.exception.MooCommandException; @@ -935,7 +935,7 @@ public class MooCommand extends CommandBase { GuiScreen.setClipboardString(GsonUtils.toJson(entities, true)); main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied " + nearbyEntities.size() + " nearby entities to clipboard."); } else { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "You stare into the void... and see nothing of interest."); + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "You stare into the void... and see nothing of interest. " + EnumChatFormatting.GRAY + "Try looking at: NPCs, mobs, armor stands, placed skulls, banners, signs, dropped items, item in item frames, or maps on a wall."); } } diff --git a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java index 70e4328..35d9542 100644 --- a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java +++ b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java @@ -5,7 +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.chesttracker.HyBazaarData; import de.cowtipper.cowlection.command.exception.ThrowingConsumer; import de.cowtipper.cowlection.config.CredentialStorage; import de.cowtipper.cowlection.data.*; diff --git a/update.json b/update.json index 39c6f8c..12f1ba2 100644 --- a/update.json +++ b/update.json @@ -1,10 +1,10 @@ { "homepage": "https://github.com/cow-mc/Cowlection/", "1.8.9": { - "1.8.9-0.12.0": "https://github.com/cow-mc/Cowlection/blob/master/CHANGELOG.md" + "1.8.9-0.13.0": "https://github.com/cow-mc/Cowlection/blob/master/CHANGELOG.md" }, "promos": { - "1.8.9-latest": "1.8.9-0.12.0", - "1.8.9-recommended": "1.8.9-0.12.0" + "1.8.9-latest": "1.8.9-0.13.0", + "1.8.9-recommended": "1.8.9-0.13.0" } } -- cgit