aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/de/cowtipper/cowlection/Cowlection.java25
-rw-r--r--src/main/java/de/cowtipper/cowlection/chestTracker/ChestInteractionListener.java277
-rw-r--r--src/main/java/de/cowtipper/cowlection/chestTracker/ChestOverviewGui.java418
-rw-r--r--src/main/java/de/cowtipper/cowlection/chestTracker/ChestTracker.java170
-rw-r--r--src/main/java/de/cowtipper/cowlection/chestTracker/HyBazaarData.java61
-rw-r--r--src/main/java/de/cowtipper/cowlection/chestTracker/ItemData.java70
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/MooCommand.java52
-rw-r--r--src/main/java/de/cowtipper/cowlection/config/MooConfig.java27
-rw-r--r--src/main/java/de/cowtipper/cowlection/search/GuiSearch.java1
-rw-r--r--src/main/java/de/cowtipper/cowlection/search/LogEntry.java (renamed from src/main/java/de/cowtipper/cowlection/data/LogEntry.java)2
-rw-r--r--src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java1
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/ApiUtils.java17
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/Utils.java17
-rw-r--r--src/main/resources/assets/cowlection/lang/en_US.lang6
14 files changed, 1132 insertions, 12 deletions
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