From 70dba3149a04105488c33fdc6ea99be93eb616aa Mon Sep 17 00:00:00 2001 From: Cow Date: Wed, 22 Jul 2020 23:07:45 +0200 Subject: Added detection whether player is on SkyBlock or in other gamemodes - Fixes e.g. removal of enchantments in non-SkyBlock gamemodes --- src/main/java/eu/olli/cowlection/Cowlection.java | 11 +- .../olli/cowlection/listener/DungeonsListener.java | 326 --------------------- .../olli/cowlection/listener/PlayerListener.java | 193 ++++-------- .../listener/skyblock/DungeonsListener.java | 326 +++++++++++++++++++++ .../listener/skyblock/SkyBlockListener.java | 162 ++++++++++ 5 files changed, 547 insertions(+), 471 deletions(-) delete mode 100644 src/main/java/eu/olli/cowlection/listener/DungeonsListener.java create mode 100644 src/main/java/eu/olli/cowlection/listener/skyblock/DungeonsListener.java create mode 100644 src/main/java/eu/olli/cowlection/listener/skyblock/SkyBlockListener.java (limited to 'src') diff --git a/src/main/java/eu/olli/cowlection/Cowlection.java b/src/main/java/eu/olli/cowlection/Cowlection.java index ab538ee..9ce14c3 100644 --- a/src/main/java/eu/olli/cowlection/Cowlection.java +++ b/src/main/java/eu/olli/cowlection/Cowlection.java @@ -7,7 +7,6 @@ import eu.olli.cowlection.config.MooConfig; import eu.olli.cowlection.handler.FriendsHandler; import eu.olli.cowlection.handler.PlayerCache; import eu.olli.cowlection.listener.ChatListener; -import eu.olli.cowlection.listener.DungeonsListener; import eu.olli.cowlection.listener.PlayerListener; import eu.olli.cowlection.util.ChatHelper; import eu.olli.cowlection.util.VersionChecker; @@ -40,6 +39,7 @@ public class Cowlection { private VersionChecker versionChecker; private ChatHelper chatHelper; private PlayerCache playerCache; + private boolean isOnSkyBlock; private Logger logger; @Mod.EventHandler @@ -62,7 +62,6 @@ public class Cowlection { @EventHandler public void init(FMLInitializationEvent e) { MinecraftForge.EVENT_BUS.register(new ChatListener(this)); - MinecraftForge.EVENT_BUS.register(new DungeonsListener(this)); MinecraftForge.EVENT_BUS.register(new PlayerListener(this)); ClientCommandHandler.instance.registerCommand(new MooCommand(this)); ClientCommandHandler.instance.registerCommand(new ShrugCommand(this)); @@ -109,6 +108,14 @@ public class Cowlection { return logger; } + public boolean isOnSkyBlock() { + return isOnSkyBlock; + } + + public void setIsOnSkyBlock(boolean isOnSkyBlock) { + this.isOnSkyBlock = isOnSkyBlock; + } + /** * Get mod's instance; instead of this method use dependency injection where possible */ diff --git a/src/main/java/eu/olli/cowlection/listener/DungeonsListener.java b/src/main/java/eu/olli/cowlection/listener/DungeonsListener.java deleted file mode 100644 index cbc45c9..0000000 --- a/src/main/java/eu/olli/cowlection/listener/DungeonsListener.java +++ /dev/null @@ -1,326 +0,0 @@ -package eu.olli.cowlection.listener; - -import eu.olli.cowlection.Cowlection; -import eu.olli.cowlection.config.MooConfig; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.inventory.GuiChest; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemSkull; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.EnumChatFormatting; -import net.minecraft.util.MathHelper; -import net.minecraftforge.client.event.GuiScreenEvent; -import net.minecraftforge.common.util.Constants; -import net.minecraftforge.event.entity.player.ItemTooltipEvent; -import net.minecraftforge.fml.common.eventhandler.EventPriority; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import org.apache.commons.lang3.StringUtils; -import org.lwjgl.input.Keyboard; - -import java.awt.*; -import java.util.HashSet; -import java.util.List; -import java.util.ListIterator; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class DungeonsListener { - private static final String FORMATTING_CODE = "§[0-9a-fl-or]"; - private final Cowlection main; - /** - * example: (space)Robin_Hood: Archer (42) - */ - private final Pattern DUNGEON_PARTY_FINDER_PLAYER = Pattern.compile("^ (?:\\w+): ([A-Za-z]+) \\((\\d+)\\)$"); - /** - * Example tooltip lines: - * - *
-     * | Groups                     | Example matches   |
-     * |----------------------------|-------------------|
-     * | Group `prefix`             | §7Crit Damage: §c |
-     * | Group `statNonDungeon`     | +23               |
-     * | Group `statNonDungeonUnit` | %                 |
-     * | Group `colorReforge`       | §8                |
-     * | Group `reforge`            | Heavy             |
-     * | Group `statReforge`        | -3                |
-     * | Group `statReforgeUnit`    | %                 |
-     * | Group `colorDungeon`       | §8                |
-     * 
- */ - private final Pattern TOOLTIP_LINE_PATTERN = Pattern.compile("^(?(?:" + FORMATTING_CODE + ")+[A-Za-z ]+: " + FORMATTING_CODE + ")(?[+-]?[0-9]+)(?%| HP|)(?: (?" + FORMATTING_CODE + ")\\((?[A-Za-z]+) (?[+-]?[0-9]+)(?%| HP|)\\))?(?: (?" + FORMATTING_CODE + ")\\((?[+-]?[.0-9]+)(?%| HP|)\\))?$"); - - private String activeDungeonClass; - - public DungeonsListener(Cowlection main) { - this.main = main; - activeDungeonClass = "unknown"; - } - - @SubscribeEvent(priority = EventPriority.HIGH) - public void onItemTooltip(ItemTooltipEvent e) { - if (e.itemStack == null || e.toolTip == null) { - return; - } - if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && isDungeonItem(e.toolTip)) { - // simplify dungeon armor stats - String originalItemName = e.itemStack.getDisplayName(); - NBTTagCompound extraAttributes = e.itemStack.getSubCompound("ExtraAttributes", false); - if (extraAttributes != null) { - StringBuilder modifiedItemName = new StringBuilder(originalItemName); - String reforge = ""; - String grayedOutFormatting = "" + EnumChatFormatting.GRAY + EnumChatFormatting.STRIKETHROUGH; - - if (extraAttributes.hasKey("modifier")) { - // item has been reforged; re-format item name to exclude reforges - reforge = StringUtils.capitalize(extraAttributes.getString("modifier")); - int modifierSuffix = Math.max(reforge.indexOf("_sword"), reforge.indexOf("_bow")); - if (modifierSuffix != -1) { - reforge = reforge.substring(0, modifierSuffix); - } - int reforgeInItemName = originalItemName.indexOf(reforge); - if (reforgeInItemName == -1 && reforge.equals("Light") && extraAttributes.getString("id").startsWith("HEAVY_")) { - // special case: heavy armor with light reforge - reforgeInItemName = originalItemName.indexOf("Heavy"); - } - - if (reforgeInItemName > 0 && !originalItemName.contains(EnumChatFormatting.STRIKETHROUGH.toString())) { - // we have a reforged item! strike through reforge in item name and remove any essence upgrades (✪) - - int reforgeLength = reforge.length(); - String reforgePrefix = null; - // special cases for reforge + item name - if (reforge.equals("Heavy") && extraAttributes.getString("id").startsWith("HEAVY_")) { - reforgePrefix = "Extremely "; - } else if (reforge.equals("Light") && extraAttributes.getString("id").startsWith("HEAVY_")) { - reforgePrefix = "Not So "; - } else if ((reforge.equals("Wise") && extraAttributes.getString("id").startsWith("WISE_DRAGON_")) - || (reforge.equals("Strong") && extraAttributes.getString("id").startsWith("STRONG_DRAGON_"))) { - reforgePrefix = "Very "; - } else if (reforge.equals("Superior") && extraAttributes.getString("id").startsWith("SUPERIOR_DRAGON_")) { - reforgePrefix = "Highly "; - } else if (reforge.equals("Perfect") && extraAttributes.getString("id").startsWith("PERFECT_")) { - reforgePrefix = "Absolutely "; - } - if (reforgePrefix != null) { - reforgeInItemName -= reforgePrefix.length(); - reforgeLength = reforgePrefix.length() - 1; - } - - modifiedItemName.insert(reforgeInItemName, grayedOutFormatting) - .insert(reforgeInItemName + reforgeLength + grayedOutFormatting.length(), originalItemName.substring(0, reforgeInItemName)); - } - } - // remove essence upgrade indicators (✪) - String essenceUpgradeIndicator = EnumChatFormatting.GOLD + "✪"; - int essenceModifier = modifiedItemName.indexOf(essenceUpgradeIndicator); - while (essenceModifier > 0) { - modifiedItemName.replace(essenceModifier, essenceModifier + essenceUpgradeIndicator.length(), grayedOutFormatting + "✪"); - essenceModifier = modifiedItemName.indexOf(essenceUpgradeIndicator); - } - e.toolTip.set(0, modifiedItemName.toString()); // replace item name - - // subtract stat boosts from reforge and update stats for dungeons - ListIterator tooltipIterator = e.toolTip.listIterator(); - - String itemQualityBottom = null; - while (tooltipIterator.hasNext()) { - String line = tooltipIterator.next(); - Matcher lineMatcher = TOOLTIP_LINE_PATTERN.matcher(line); - if (lineMatcher.matches()) { - if (EnumChatFormatting.getTextWithoutFormattingCodes(lineMatcher.group("prefix")).equals("Gear Score: ")) { - // replace meaningless gear score with item quality (gear score includes reforges etc) - StringBuilder customGearScore = new StringBuilder(EnumChatFormatting.GRAY.toString()).append("Item Quality: "); - boolean hasCustomGearScore = false; - if (extraAttributes.hasKey("baseStatBoostPercentage")) { - int itemQuality = extraAttributes.getInteger("baseStatBoostPercentage") * 2; // value between 0 and 50 => *2 == in % - customGearScore.append(EnumChatFormatting.LIGHT_PURPLE).append(itemQuality).append("%"); - hasCustomGearScore = true; - } - if (extraAttributes.hasKey("item_tier", Constants.NBT.TAG_INT)) { - int obtainedFromFloor = extraAttributes.getInteger("item_tier"); - customGearScore.append(EnumChatFormatting.GRAY).append(" (Floor ").append(EnumChatFormatting.LIGHT_PURPLE).append(obtainedFromFloor).append(EnumChatFormatting.GRAY).append(")"); - hasCustomGearScore = true; - } - if (!hasCustomGearScore) { - customGearScore.append("―"); - } - if (MooConfig.isDungItemQualityAtTop()) { - // replace 'Gear Score' line - tooltipIterator.set(customGearScore.toString()); - } else { - // delete 'Gear Score' line and add item quality to bottom - tooltipIterator.remove(); - itemQualityBottom = customGearScore.toString(); - } - continue; - } - try { - int statNonDungeon = Integer.parseInt(lineMatcher.group("statNonDungeon")); - - int statBase = statNonDungeon; - if (reforge.equalsIgnoreCase(lineMatcher.group("reforge"))) { - // tooltip line has reforge stats; subtract them from base stats - statBase -= Integer.parseInt(lineMatcher.group("statReforge")); - } - - if (statBase == 0) { - // don't redraw 0 stats - tooltipIterator.remove(); - continue; - } - String newToolTipLine = String.format("%s%+d%s", lineMatcher.group("prefix"), statBase, lineMatcher.group("statNonDungeonUnit")); - if (lineMatcher.group("statDungeon") != null) { - // tooltip line has dungeon stats; update them! - double statDungeon = Double.parseDouble(lineMatcher.group("statDungeon")); - - double dungeonStatModifier = statDungeon / statNonDungeon; // modified through skill level or gear essence upgrades - if (extraAttributes.hasKey("dungeon_item_level")) { - // with essences upgraded item => calculate base (level based) dungeon modifier - dungeonStatModifier -= extraAttributes.getInteger("dungeon_item_level") / 10d; - } - - double statBaseDungeon = statBase * dungeonStatModifier; - double statDungeonWithMaxEssenceUpgrades = statBase * (dungeonStatModifier + /*5x essence à +10% each => +50% stats */0.5d); - newToolTipLine += String.format(" %s(₀ₓ✪ %+.1f%s) %s(₅ₓ✪ %+.1f%s)", lineMatcher.group("colorDungeon"), statBaseDungeon, lineMatcher.group("statDungeonUnit"), - lineMatcher.group("colorDungeon"), statDungeonWithMaxEssenceUpgrades, lineMatcher.group("statDungeonUnit")); - } - - tooltipIterator.set(newToolTipLine); - } catch (NumberFormatException ignored) { - } - } - } - if (itemQualityBottom != null) { - int index = Math.max(0, e.toolTip.size() - (e.showAdvancedItemTooltips ? /* item name & nbt info */ 2 : 0)); - e.toolTip.add(index, itemQualityBottom); - } - } - } - } - - private boolean isDungeonItem(List toolTip) { - ListIterator toolTipIterator = toolTip.listIterator(toolTip.size()); - while (toolTipIterator.hasPrevious()) { - if (toolTipIterator.previous().contains(" DUNGEON ")) { - return true; - } - } - return false; - } - - @SubscribeEvent - public void onRenderGuiBackground(GuiScreenEvent.DrawScreenEvent.Pre e) { - if (e.gui instanceof GuiChest) { - GuiChest guiChest = (GuiChest) e.gui; - - Container inventorySlots = guiChest.inventorySlots; - IInventory inventory = inventorySlots.getSlot(0).inventory; - if (inventory.getName().equals("Catacombs Gate")) { - // update active selected class - ItemStack dungeonClassIndicator = inventory.getStackInSlot(47); - if (dungeonClassIndicator == null) { - // couldn't detect dungeon class indicator - return; - } - for (String toolTipLine : dungeonClassIndicator.getTooltip(Minecraft.getMinecraft().thePlayer, false)) { - String line = EnumChatFormatting.getTextWithoutFormattingCodes(toolTipLine); - if (line.startsWith("Currently Selected: ")) { - String selectedClass = line.substring(line.lastIndexOf(' ') + 1); - if (!selectedClass.equals(activeDungeonClass)) { - activeDungeonClass = selectedClass; - } - } - } - } else if (inventory.getName().equals("Party Finder")) { - // enhance party finder - - // formulas from GuiContainer#initGui (guiLeft, guiTop) and GuiChest (ySize) - int guiLeft = (guiChest.width - 176) / 2; - int inventoryRows = inventory.getSizeInventory() / 9; - int ySize = 222 - 108 + inventoryRows * 18; - int guiTop = (guiChest.height - ySize) / 2; - GlStateManager.pushMatrix(); - - GlStateManager.translate(0, 0, 1); - float scaleFactor = 0.8f; - GlStateManager.scale(scaleFactor, scaleFactor, 0); - for (Slot inventorySlot : inventorySlots.inventorySlots) { - if (inventorySlot.getHasStack()) { - int slotRow = inventorySlot.slotNumber / 9; - int slotColumn = inventorySlot.slotNumber % 9; - // check if slot is one of the middle slots with actual minions - int maxRow = inventoryRows - 2; - if (slotRow > 0 && slotRow < maxRow && slotColumn > 0 && slotColumn < 8) { - int slotX = (int) ((guiLeft + inventorySlot.xDisplayPosition) / scaleFactor); - int slotY = (int) ((guiTop + inventorySlot.yDisplayPosition) / scaleFactor); - renderPartyStatus(inventorySlot.getStack(), slotX, slotY); - } - } - } - GlStateManager.popMatrix(); - } - } - } - - private void renderPartyStatus(ItemStack item, int x, int y) { - if (!(item.getItem() instanceof ItemSkull && item.getMetadata() == 3 && item.hasTagCompound())) { - // not a player skull, don't draw party status indicator - return; - } - String status = "⬛"; // ok - Color color = new Color(20, 200, 20, 255); - - List itemTooltip = item.getTooltip(Minecraft.getMinecraft().thePlayer, false); - if (itemTooltip.size() < 5) { - // not a valid dungeon party tooltip - return; - } - if (itemTooltip.get(itemTooltip.size() - 1).endsWith("Complete previous floor first!")) { - // cannot enter dungeon - status = "✗"; - color = new Color(220, 20, 20, 255); - } else if (itemTooltip.get(itemTooltip.size() - 1).endsWith("You are in this party!")) { - status = EnumChatFormatting.OBFUSCATED + "#"; - } else { - int dungClassMin = MooConfig.dungClassRange[0]; - int dungClassMax = MooConfig.dungClassRange[1]; - Set dungClassesInParty = new HashSet<>(); - dungClassesInParty.add(activeDungeonClass); // add our own class - - for (String toolTipLine : itemTooltip) { - Matcher playerDetailMatcher = DUNGEON_PARTY_FINDER_PLAYER.matcher(EnumChatFormatting.getTextWithoutFormattingCodes(toolTipLine)); - if (playerDetailMatcher.matches()) { - String clazz = playerDetailMatcher.group(1); - int classLevel = MathHelper.parseIntWithDefault(playerDetailMatcher.group(2), -1); - if (MooConfig.dungFilterPartiesWithDupes && !dungClassesInParty.add(clazz)) { - // duped class! - status = "²⁺"; // 2+ - color = new Color(220, 120, 20, 255); - break; - } else if (dungClassMin > -1 && classLevel < dungClassMin) { - // party member too low level - status = EnumChatFormatting.BOLD + "ᐯ"; - color = new Color(200, 20, 20, 255); - break; - } else if (dungClassMax > -1 && classLevel > dungClassMax) { - // party member too high level - status = EnumChatFormatting.BOLD + "ᐱ"; - color = new Color(20, 120, 230, 255); - break; - } - } - } - } - Minecraft.getMinecraft().fontRendererObj.drawStringWithShadow(status, x, y, color.getRGB()); - } -} diff --git a/src/main/java/eu/olli/cowlection/listener/PlayerListener.java b/src/main/java/eu/olli/cowlection/listener/PlayerListener.java index 1c7710d..fe15a4f 100644 --- a/src/main/java/eu/olli/cowlection/listener/PlayerListener.java +++ b/src/main/java/eu/olli/cowlection/listener/PlayerListener.java @@ -1,171 +1,35 @@ package eu.olli.cowlection.listener; import eu.olli.cowlection.Cowlection; -import eu.olli.cowlection.config.MooConfig; +import eu.olli.cowlection.listener.skyblock.DungeonsListener; +import eu.olli.cowlection.listener.skyblock.SkyBlockListener; import eu.olli.cowlection.util.GsonUtils; import eu.olli.cowlection.util.TickDelay; -import eu.olli.cowlection.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.inventory.GuiChest; import net.minecraft.client.gui.inventory.GuiInventory; -import net.minecraft.enchantment.Enchantment; -import net.minecraft.init.Blocks; -import net.minecraft.init.Items; import net.minecraft.inventory.ContainerChest; import net.minecraft.inventory.IInventory; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.scoreboard.ScoreObjective; import net.minecraft.util.EnumChatFormatting; -import net.minecraft.util.StatCollector; import net.minecraftforge.client.event.GuiScreenEvent; -import net.minecraftforge.common.util.Constants; -import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.PlayerSetSpawnEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.network.FMLNetworkEvent; -import org.apache.commons.lang3.StringUtils; import org.lwjgl.input.Keyboard; -import java.text.NumberFormat; -import java.text.ParseException; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - public class PlayerListener { private final Cowlection main; - private final NumberFormat numberFormatter; - /** - * timestamp example: 4/20/20 4:20 AM - */ - private final Pattern SB_TIMESTAMP_PATTERN = Pattern.compile("^(\\d{1,2})/(\\d{1,2})/(\\d{2}) (\\d{1,2}):(\\d{2}) (AM|PM)$"); + private DungeonsListener dungeonsListener; + private SkyBlockListener skyBlockListener; public PlayerListener(Cowlection main) { this.main = main; - numberFormatter = NumberFormat.getNumberInstance(Locale.US); - numberFormatter.setMaximumFractionDigits(0); - } - - @SubscribeEvent - public void onItemTooltip(ItemTooltipEvent e) { - if (e.itemStack == null || e.toolTip == null) { - return; - } - // remove unnecessary tooltip entries: dyed leather armor - NBTTagCompound nbtDisplay = e.itemStack.getSubCompound("display", false); - if (nbtDisplay != null && nbtDisplay.hasKey("color", Constants.NBT.TAG_INT)) { - if (Minecraft.getMinecraft().gameSettings.advancedItemTooltips) { - e.toolTip.removeIf(line -> line.startsWith("Color: #")); - } else { - e.toolTip.removeIf(line -> line.equals(EnumChatFormatting.ITALIC + StatCollector.translateToLocal("item.dyed"))); - } - } - - // remove unnecessary tooltip entries: enchantments (already added via lore) - NBTTagList enchantments = e.itemStack.getEnchantmentTagList(); - if (enchantments != null) { - for (int enchantmentNr = 0; enchantmentNr < enchantments.tagCount(); ++enchantmentNr) { - int enchantmentId = enchantments.getCompoundTagAt(enchantmentNr).getShort("id"); - int enchantmentLevel = enchantments.getCompoundTagAt(enchantmentNr).getShort("lvl"); - - if (Enchantment.getEnchantmentById(enchantmentId) != null) { - e.toolTip.remove(Enchantment.getEnchantmentById(enchantmentId).getTranslatedName(enchantmentLevel)); - } - } - } - - if (!MooConfig.showAdvancedTooltips && !Keyboard.isKeyDown(Keyboard.KEY_LMENU)) { - return; - } - // add item age to tooltip - NBTTagCompound extraAttributes = e.itemStack.getSubCompound("ExtraAttributes", false); - if (extraAttributes != null && extraAttributes.hasKey("timestamp")) { - String rawTimestamp = extraAttributes.getString("timestamp"); - Matcher sbTimestampMatcher = SB_TIMESTAMP_PATTERN.matcher(rawTimestamp); - if (sbTimestampMatcher.matches()) { - // Timezone = America/Toronto! headquarter is in Val-des-Monts, Quebec, Canada; timezone can also be confirmed by looking at the timestamps of New Year Cakes - ZonedDateTime dateTime = getDateTimeWithZone(sbTimestampMatcher, ZoneId.of("America/Toronto")); // EDT/EST - String dateTimeFormatted = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm zzz")); - - int index = Math.max(0, e.toolTip.size() - (e.showAdvancedItemTooltips ? /* item name & nbt info */ 2 : 0)); - - if (Keyboard.isKeyDown(Keyboard.KEY_LMENU)) { - // full tooltip - e.toolTip.add(index, "Timestamp: " + EnumChatFormatting.DARK_GRAY + dateTimeFormatted); - e.toolTip.add(index, "Item age: " + EnumChatFormatting.DARK_GRAY + Utils.getDurationAsWords(dateTime.toEpochSecond() * 1000).first()); - } else { - // abbreviated tooltip - e.toolTip.add(index, "Item age: " + EnumChatFormatting.DARK_GRAY + Utils.getDurationAsWord(dateTime.toEpochSecond() * 1000)); - } - } - } - - // for auction house: show price for each item if multiple items are sold at once - if (e.entityPlayer != null && e.entityPlayer.openContainer instanceof ContainerChest) { - int stackSize = e.itemStack.stackSize; - if ((stackSize == 1 && !isSubmitBidItem(e.itemStack)) || e.toolTip.size() < 4) { - // only 1 item or irrelevant tooltip - nothing to do here, abort! - return; - } - - if (isSubmitBidItem(e.itemStack)) { - // special case: "place bid on an item" interface ("Auction View") - ItemStack auctionedItem = e.entityPlayer.openContainer.getInventory().get(13); - stackSize = auctionedItem.stackSize; - if (stackSize == 1) { - // still only 1 item, abort! - return; - } - } - - List toolTip = e.toolTip; - - // starting with i=1 because first line is never the one we're looking for - for (int i = 1; i < toolTip.size(); i++) { - String toolTipLineUnformatted = EnumChatFormatting.getTextWithoutFormattingCodes(toolTip.get(i)); - if (toolTipLineUnformatted.startsWith("Top bid: ") - || toolTipLineUnformatted.startsWith("Starting bid: ") - || toolTipLineUnformatted.startsWith("Buy it now: ") - || toolTipLineUnformatted.startsWith("Sold for: ") - || toolTipLineUnformatted.startsWith("New bid: ") /* special case: 'Submit Bid' item */) { - - try { - long price = numberFormatter.parse(StringUtils.substringBetween(toolTipLineUnformatted, ": ", " coins")).longValue(); - double priceEach = price / (double) stackSize; - String formattedPriceEach = priceEach < 5000 ? numberFormatter.format(priceEach) : Utils.formatNumberWithAbbreviations(priceEach); - String pricePerItem = EnumChatFormatting.YELLOW + " (" + formattedPriceEach + " each)"; - toolTip.set(i, toolTip.get(i) + pricePerItem); - return; - } catch (ParseException ex) { - return; - } - } - } - } - } - - private ZonedDateTime getDateTimeWithZone(Matcher sbTimestampMatcher, ZoneId zoneId) { - int year = 2000 + Integer.parseInt(sbTimestampMatcher.group(3)); - int month = Integer.parseInt(sbTimestampMatcher.group(1)); - int day = Integer.parseInt(sbTimestampMatcher.group(2)); - int hour = (Integer.parseInt(sbTimestampMatcher.group(4)) + (sbTimestampMatcher.group(6).equals("PM") ? 12 : 0)) % 24; - int minute = Integer.parseInt(sbTimestampMatcher.group(5)); - - LocalDateTime localDateTime = LocalDateTime.of(year, month, day, hour, minute); - - return ZonedDateTime.of(localDateTime, zoneId); - } - - private boolean isSubmitBidItem(ItemStack itemStack) { - return (itemStack.getItem().equals(Items.gold_nugget) || itemStack.getItem().equals(Item.getItemFromBlock(Blocks.gold_block))) - && (itemStack.hasDisplayName() && (itemStack.getDisplayName().endsWith("Submit Bid") || itemStack.getDisplayName().endsWith("Collect Auction"))); } @SubscribeEvent @@ -207,11 +71,54 @@ public class PlayerListener { public void onServerJoin(FMLNetworkEvent.ClientConnectedToServerEvent e) { main.getVersionChecker().runUpdateCheck(false); new TickDelay(() -> main.getChatHelper().sendOfflineMessages(), 6 * 20); + main.setIsOnSkyBlock(false); + } + + @SubscribeEvent + public void onWorldEnter(PlayerSetSpawnEvent e) { + // check if player is on SkyBlock or on another gamemode + new TickDelay(() -> { + ScoreObjective scoreboardSidebar = e.entityPlayer.worldObj.getScoreboard().getObjectiveInDisplaySlot(1); + boolean wasOnSkyBlock = main.isOnSkyBlock(); + main.setIsOnSkyBlock(scoreboardSidebar != null && EnumChatFormatting.getTextWithoutFormattingCodes(scoreboardSidebar.getDisplayName()).startsWith("SKYBLOCK")); + + if (!wasOnSkyBlock && main.isOnSkyBlock()) { + // player wasn't on SkyBlock before but now is on SkyBlock + main.getLogger().info("Entered SkyBlock! Registering SkyBlock listeners"); + registerSkyBlockListeners(); + } else if (wasOnSkyBlock && !main.isOnSkyBlock()) { + // player was on SkyBlock before and is now in another gamemode + unregisterSkyBlockListeners(); + main.getLogger().info("Leaving SkyBlock! Un-registering SkyBlock listeners"); + } + }, 20); // 1 second delay, making sure scoreboard got sent + } + + private void registerSkyBlockListeners() { + if (dungeonsListener == null) { + MinecraftForge.EVENT_BUS.register(dungeonsListener = new DungeonsListener(main)); + } + if (skyBlockListener == null) { + MinecraftForge.EVENT_BUS.register(skyBlockListener = new SkyBlockListener(main)); + } + } + + private void unregisterSkyBlockListeners() { + if (dungeonsListener != null) { + MinecraftForge.EVENT_BUS.unregister(dungeonsListener); + dungeonsListener = null; + } + if (skyBlockListener != null) { + MinecraftForge.EVENT_BUS.unregister(skyBlockListener); + skyBlockListener = null; + } } @SubscribeEvent public void onServerLeave(FMLNetworkEvent.ClientDisconnectionFromServerEvent e) { + main.setIsOnSkyBlock(false); main.getFriendsHandler().saveBestFriends(); main.getPlayerCache().clearAllCaches(); + unregisterSkyBlockListeners(); } } diff --git a/src/main/java/eu/olli/cowlection/listener/skyblock/DungeonsListener.java b/src/main/java/eu/olli/cowlection/listener/skyblock/DungeonsListener.java new file mode 100644 index 0000000..eedd0b7 --- /dev/null +++ b/src/main/java/eu/olli/cowlection/listener/skyblock/DungeonsListener.java @@ -0,0 +1,326 @@ +package eu.olli.cowlection.listener.skyblock; + +import eu.olli.cowlection.Cowlection; +import eu.olli.cowlection.config.MooConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.inventory.GuiChest; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.inventory.Container; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemSkull; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.MathHelper; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.util.Constants; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.fml.common.eventhandler.EventPriority; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.apache.commons.lang3.StringUtils; +import org.lwjgl.input.Keyboard; + +import java.awt.*; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DungeonsListener { + private static final String FORMATTING_CODE = "§[0-9a-fl-or]"; + private final Cowlection main; + /** + * example: (space)Robin_Hood: Archer (42) + */ + private final Pattern DUNGEON_PARTY_FINDER_PLAYER = Pattern.compile("^ (?:\\w+): ([A-Za-z]+) \\((\\d+)\\)$"); + /** + * Example tooltip lines: + *
    + *
  • §7Crit Damage: §c+23% §8(Heavy -3%) §8(+28.75%)
  • + *
  • §7Health: §a+107 HP §8(+133.75 HP)
  • + *
  • §7Defense: §a+130 §8(Heavy +65) §8(+162.5)
  • + *
  • §7Speed: §a-1 §8(Heavy -1)
  • + *
+ *
+     * | Groups                     | Example matches   |
+     * |----------------------------|-------------------|
+     * | Group `prefix`             | §7Crit Damage: §c |
+     * | Group `statNonDungeon`     | +23               |
+     * | Group `statNonDungeonUnit` | %                 |
+     * | Group `colorReforge`       | §8                |
+     * | Group `reforge`            | Heavy             |
+     * | Group `statReforge`        | -3                |
+     * | Group `statReforgeUnit`    | %                 |
+     * | Group `colorDungeon`       | §8                |
+     * 
+ */ + private final Pattern TOOLTIP_LINE_PATTERN = Pattern.compile("^(?(?:" + FORMATTING_CODE + ")+[A-Za-z ]+: " + FORMATTING_CODE + ")(?[+-]?[0-9]+)(?%| HP|)(?: (?" + FORMATTING_CODE + ")\\((?[A-Za-z]+) (?[+-]?[0-9]+)(?%| HP|)\\))?(?: (?" + FORMATTING_CODE + ")\\((?[+-]?[.0-9]+)(?%| HP|)\\))?$"); + + private String activeDungeonClass; + + public DungeonsListener(Cowlection main) { + this.main = main; + activeDungeonClass = "unknown"; + } + + @SubscribeEvent(priority = EventPriority.HIGH) + public void onItemTooltip(ItemTooltipEvent e) { + if (e.itemStack == null || e.toolTip == null) { + return; + } + if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && isDungeonItem(e.toolTip)) { + // simplify dungeon armor stats + String originalItemName = e.itemStack.getDisplayName(); + NBTTagCompound extraAttributes = e.itemStack.getSubCompound("ExtraAttributes", false); + if (extraAttributes != null) { + StringBuilder modifiedItemName = new StringBuilder(originalItemName); + String reforge = ""; + String grayedOutFormatting = "" + EnumChatFormatting.GRAY + EnumChatFormatting.STRIKETHROUGH; + + if (extraAttributes.hasKey("modifier")) { + // item has been reforged; re-format item name to exclude reforges + reforge = StringUtils.capitalize(extraAttributes.getString("modifier")); + int modifierSuffix = Math.max(reforge.indexOf("_sword"), reforge.indexOf("_bow")); + if (modifierSuffix != -1) { + reforge = reforge.substring(0, modifierSuffix); + } + int reforgeInItemName = originalItemName.indexOf(reforge); + if (reforgeInItemName == -1 && reforge.equals("Light") && extraAttributes.getString("id").startsWith("HEAVY_")) { + // special case: heavy armor with light reforge + reforgeInItemName = originalItemName.indexOf("Heavy"); + } + + if (reforgeInItemName > 0 && !originalItemName.contains(EnumChatFormatting.STRIKETHROUGH.toString())) { + // we have a reforged item! strike through reforge in item name and remove any essence upgrades (✪) + + int reforgeLength = reforge.length(); + String reforgePrefix = null; + // special cases for reforge + item name + if (reforge.equals("Heavy") && extraAttributes.getString("id").startsWith("HEAVY_")) { + reforgePrefix = "Extremely "; + } else if (reforge.equals("Light") && extraAttributes.getString("id").startsWith("HEAVY_")) { + reforgePrefix = "Not So "; + } else if ((reforge.equals("Wise") && extraAttributes.getString("id").startsWith("WISE_DRAGON_")) + || (reforge.equals("Strong") && extraAttributes.getString("id").startsWith("STRONG_DRAGON_"))) { + reforgePrefix = "Very "; + } else if (reforge.equals("Superior") && extraAttributes.getString("id").startsWith("SUPERIOR_DRAGON_")) { + reforgePrefix = "Highly "; + } else if (reforge.equals("Perfect") && extraAttributes.getString("id").startsWith("PERFECT_")) { + reforgePrefix = "Absolutely "; + } + if (reforgePrefix != null) { + reforgeInItemName -= reforgePrefix.length(); + reforgeLength = reforgePrefix.length() - 1; + } + + modifiedItemName.insert(reforgeInItemName, grayedOutFormatting) + .insert(reforgeInItemName + reforgeLength + grayedOutFormatting.length(), originalItemName.substring(0, reforgeInItemName)); + } + } + // remove essence upgrade indicators (✪) + String essenceUpgradeIndicator = EnumChatFormatting.GOLD + "✪"; + int essenceModifier = modifiedItemName.indexOf(essenceUpgradeIndicator); + while (essenceModifier > 0) { + modifiedItemName.replace(essenceModifier, essenceModifier + essenceUpgradeIndicator.length(), grayedOutFormatting + "✪"); + essenceModifier = modifiedItemName.indexOf(essenceUpgradeIndicator); + } + e.toolTip.set(0, modifiedItemName.toString()); // replace item name + + // subtract stat boosts from reforge and update stats for dungeons + ListIterator tooltipIterator = e.toolTip.listIterator(); + + String itemQualityBottom = null; + while (tooltipIterator.hasNext()) { + String line = tooltipIterator.next(); + Matcher lineMatcher = TOOLTIP_LINE_PATTERN.matcher(line); + if (lineMatcher.matches()) { + if (EnumChatFormatting.getTextWithoutFormattingCodes(lineMatcher.group("prefix")).equals("Gear Score: ")) { + // replace meaningless gear score with item quality (gear score includes reforges etc) + StringBuilder customGearScore = new StringBuilder(EnumChatFormatting.GRAY.toString()).append("Item Quality: "); + boolean hasCustomGearScore = false; + if (extraAttributes.hasKey("baseStatBoostPercentage")) { + int itemQuality = extraAttributes.getInteger("baseStatBoostPercentage") * 2; // value between 0 and 50 => *2 == in % + customGearScore.append(EnumChatFormatting.LIGHT_PURPLE).append(itemQuality).append("%"); + hasCustomGearScore = true; + } + if (extraAttributes.hasKey("item_tier", Constants.NBT.TAG_INT)) { + int obtainedFromFloor = extraAttributes.getInteger("item_tier"); + customGearScore.append(EnumChatFormatting.GRAY).append(" (Floor ").append(EnumChatFormatting.LIGHT_PURPLE).append(obtainedFromFloor).append(EnumChatFormatting.GRAY).append(")"); + hasCustomGearScore = true; + } + if (!hasCustomGearScore) { + customGearScore.append("―"); + } + if (MooConfig.isDungItemQualityAtTop()) { + // replace 'Gear Score' line + tooltipIterator.set(customGearScore.toString()); + } else { + // delete 'Gear Score' line and add item quality to bottom + tooltipIterator.remove(); + itemQualityBottom = customGearScore.toString(); + } + continue; + } + try { + int statNonDungeon = Integer.parseInt(lineMatcher.group("statNonDungeon")); + + int statBase = statNonDungeon; + if (reforge.equalsIgnoreCase(lineMatcher.group("reforge"))) { + // tooltip line has reforge stats; subtract them from base stats + statBase -= Integer.parseInt(lineMatcher.group("statReforge")); + } + + if (statBase == 0) { + // don't redraw 0 stats + tooltipIterator.remove(); + continue; + } + String newToolTipLine = String.format("%s%+d%s", lineMatcher.group("prefix"), statBase, lineMatcher.group("statNonDungeonUnit")); + if (lineMatcher.group("statDungeon") != null) { + // tooltip line has dungeon stats; update them! + double statDungeon = Double.parseDouble(lineMatcher.group("statDungeon")); + + double dungeonStatModifier = statDungeon / statNonDungeon; // modified through skill level or gear essence upgrades + if (extraAttributes.hasKey("dungeon_item_level")) { + // with essences upgraded item => calculate base (level based) dungeon modifier + dungeonStatModifier -= extraAttributes.getInteger("dungeon_item_level") / 10d; + } + + double statBaseDungeon = statBase * dungeonStatModifier; + double statDungeonWithMaxEssenceUpgrades = statBase * (dungeonStatModifier + /*5x essence à +10% each => +50% stats */0.5d); + newToolTipLine += String.format(" %s(₀ₓ✪ %+.1f%s) %s(₅ₓ✪ %+.1f%s)", lineMatcher.group("colorDungeon"), statBaseDungeon, lineMatcher.group("statDungeonUnit"), + lineMatcher.group("colorDungeon"), statDungeonWithMaxEssenceUpgrades, lineMatcher.group("statDungeonUnit")); + } + + tooltipIterator.set(newToolTipLine); + } catch (NumberFormatException ignored) { + } + } + } + if (itemQualityBottom != null) { + int index = Math.max(0, e.toolTip.size() - (e.showAdvancedItemTooltips ? /* item name & nbt info */ 2 : 0)); + e.toolTip.add(index, itemQualityBottom); + } + } + } + } + + private boolean isDungeonItem(List toolTip) { + ListIterator toolTipIterator = toolTip.listIterator(toolTip.size()); + while (toolTipIterator.hasPrevious()) { + if (toolTipIterator.previous().contains(" DUNGEON ")) { + return true; + } + } + return false; + } + + @SubscribeEvent + public void onRenderGuiBackground(GuiScreenEvent.DrawScreenEvent.Pre e) { + if (e.gui instanceof GuiChest) { + GuiChest guiChest = (GuiChest) e.gui; + + Container inventorySlots = guiChest.inventorySlots; + IInventory inventory = inventorySlots.getSlot(0).inventory; + if (inventory.getName().equals("Catacombs Gate")) { + // update active selected class + ItemStack dungeonClassIndicator = inventory.getStackInSlot(47); + if (dungeonClassIndicator == null) { + // couldn't detect dungeon class indicator + return; + } + for (String toolTipLine : dungeonClassIndicator.getTooltip(Minecraft.getMinecraft().thePlayer, false)) { + String line = EnumChatFormatting.getTextWithoutFormattingCodes(toolTipLine); + if (line.startsWith("Currently Selected: ")) { + String selectedClass = line.substring(line.lastIndexOf(' ') + 1); + if (!selectedClass.equals(activeDungeonClass)) { + activeDungeonClass = selectedClass; + } + } + } + } else if (inventory.getName().equals("Party Finder")) { + // enhance party finder + + // formulas from GuiContainer#initGui (guiLeft, guiTop) and GuiChest (ySize) + int guiLeft = (guiChest.width - 176) / 2; + int inventoryRows = inventory.getSizeInventory() / 9; + int ySize = 222 - 108 + inventoryRows * 18; + int guiTop = (guiChest.height - ySize) / 2; + GlStateManager.pushMatrix(); + + GlStateManager.translate(0, 0, 1); + float scaleFactor = 0.8f; + GlStateManager.scale(scaleFactor, scaleFactor, 0); + for (Slot inventorySlot : inventorySlots.inventorySlots) { + if (inventorySlot.getHasStack()) { + int slotRow = inventorySlot.slotNumber / 9; + int slotColumn = inventorySlot.slotNumber % 9; + // check if slot is one of the middle slots with actual minions + int maxRow = inventoryRows - 2; + if (slotRow > 0 && slotRow < maxRow && slotColumn > 0 && slotColumn < 8) { + int slotX = (int) ((guiLeft + inventorySlot.xDisplayPosition) / scaleFactor); + int slotY = (int) ((guiTop + inventorySlot.yDisplayPosition) / scaleFactor); + renderPartyStatus(inventorySlot.getStack(), slotX, slotY); + } + } + } + GlStateManager.popMatrix(); + } + } + } + + private void renderPartyStatus(ItemStack item, int x, int y) { + if (!(item.getItem() instanceof ItemSkull && item.getMetadata() == 3 && item.hasTagCompound())) { + // not a player skull, don't draw party status indicator + return; + } + String status = "⬛"; // ok + Color color = new Color(20, 200, 20, 255); + + List itemTooltip = item.getTooltip(Minecraft.getMinecraft().thePlayer, false); + if (itemTooltip.size() < 5) { + // not a valid dungeon party tooltip + return; + } + if (itemTooltip.get(itemTooltip.size() - 1).endsWith("Complete previous floor first!")) { + // cannot enter dungeon + status = "✗"; + color = new Color(220, 20, 20, 255); + } else if (itemTooltip.get(itemTooltip.size() - 1).endsWith("You are in this party!")) { + status = EnumChatFormatting.OBFUSCATED + "#"; + } else { + int dungClassMin = MooConfig.dungClassRange[0]; + int dungClassMax = MooConfig.dungClassRange[1]; + Set dungClassesInParty = new HashSet<>(); + dungClassesInParty.add(activeDungeonClass); // add our own class + + for (String toolTipLine : itemTooltip) { + Matcher playerDetailMatcher = DUNGEON_PARTY_FINDER_PLAYER.matcher(EnumChatFormatting.getTextWithoutFormattingCodes(toolTipLine)); + if (playerDetailMatcher.matches()) { + String clazz = playerDetailMatcher.group(1); + int classLevel = MathHelper.parseIntWithDefault(playerDetailMatcher.group(2), -1); + if (MooConfig.dungFilterPartiesWithDupes && !dungClassesInParty.add(clazz)) { + // duped class! + status = "²⁺"; // 2+ + color = new Color(220, 120, 20, 255); + break; + } else if (dungClassMin > -1 && classLevel < dungClassMin) { + // party member too low level + status = EnumChatFormatting.BOLD + "ᐯ"; + color = new Color(200, 20, 20, 255); + break; + } else if (dungClassMax > -1 && classLevel > dungClassMax) { + // party member too high level + status = EnumChatFormatting.BOLD + "ᐱ"; + color = new Color(20, 120, 230, 255); + break; + } + } + } + } + Minecraft.getMinecraft().fontRendererObj.drawStringWithShadow(status, x, y, color.getRGB()); + } +} diff --git a/src/main/java/eu/olli/cowlection/listener/skyblock/SkyBlockListener.java b/src/main/java/eu/olli/cowlection/listener/skyblock/SkyBlockListener.java new file mode 100644 index 0000000..9722fb5 --- /dev/null +++ b/src/main/java/eu/olli/cowlection/listener/skyblock/SkyBlockListener.java @@ -0,0 +1,162 @@ +package eu.olli.cowlection.listener.skyblock; + +import eu.olli.cowlection.Cowlection; +import eu.olli.cowlection.config.MooConfig; +import eu.olli.cowlection.util.Utils; +import net.minecraft.client.Minecraft; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.inventory.ContainerChest; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import net.minecraftforge.common.util.Constants; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.apache.commons.lang3.StringUtils; +import org.lwjgl.input.Keyboard; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SkyBlockListener { + private final Cowlection main; + /** + * timestamp example: 4/20/20 4:20 AM + */ + private final Pattern SB_TIMESTAMP_PATTERN = Pattern.compile("^(\\d{1,2})/(\\d{1,2})/(\\d{2}) (\\d{1,2}):(\\d{2}) (AM|PM)$"); + private final NumberFormat numberFormatter; + + public SkyBlockListener(Cowlection main) { + this.main = main; + numberFormatter = NumberFormat.getNumberInstance(Locale.US); + numberFormatter.setMaximumFractionDigits(0); + } + + @SubscribeEvent + public void onItemTooltip(ItemTooltipEvent e) { + if (e.itemStack == null || e.toolTip == null) { + return; + } + // remove unnecessary tooltip entries: dyed leather armor + NBTTagCompound nbtDisplay = e.itemStack.getSubCompound("display", false); + if (nbtDisplay != null && nbtDisplay.hasKey("color", Constants.NBT.TAG_INT)) { + if (Minecraft.getMinecraft().gameSettings.advancedItemTooltips) { + e.toolTip.removeIf(line -> line.startsWith("Color: #")); + } else { + e.toolTip.removeIf(line -> line.equals(EnumChatFormatting.ITALIC + StatCollector.translateToLocal("item.dyed"))); + } + } + + // remove unnecessary tooltip entries: enchantments (already added via lore) + NBTTagList enchantments = e.itemStack.getEnchantmentTagList(); + if (enchantments != null) { + for (int enchantmentNr = 0; enchantmentNr < enchantments.tagCount(); ++enchantmentNr) { + int enchantmentId = enchantments.getCompoundTagAt(enchantmentNr).getShort("id"); + int enchantmentLevel = enchantments.getCompoundTagAt(enchantmentNr).getShort("lvl"); + + if (Enchantment.getEnchantmentById(enchantmentId) != null) { + e.toolTip.remove(Enchantment.getEnchantmentById(enchantmentId).getTranslatedName(enchantmentLevel)); + } + } + } + + if (!MooConfig.showAdvancedTooltips && !Keyboard.isKeyDown(Keyboard.KEY_LMENU)) { + return; + } + // add item age to tooltip + NBTTagCompound extraAttributes = e.itemStack.getSubCompound("ExtraAttributes", false); + if (extraAttributes != null && extraAttributes.hasKey("timestamp")) { + String rawTimestamp = extraAttributes.getString("timestamp"); + Matcher sbTimestampMatcher = SB_TIMESTAMP_PATTERN.matcher(rawTimestamp); + if (sbTimestampMatcher.matches()) { + // Timezone = America/Toronto! headquarter is in Val-des-Monts, Quebec, Canada; timezone can also be confirmed by looking at the timestamps of New Year Cakes + ZonedDateTime dateTime = getDateTimeWithZone(sbTimestampMatcher, ZoneId.of("America/Toronto")); // EDT/EST + String dateTimeFormatted = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm zzz")); + + int index = Math.max(0, e.toolTip.size() - (e.showAdvancedItemTooltips ? /* item name & nbt info */ 2 : 0)); + + if (Keyboard.isKeyDown(Keyboard.KEY_LMENU)) { + // full tooltip + e.toolTip.add(index, "Timestamp: " + EnumChatFormatting.DARK_GRAY + dateTimeFormatted); + e.toolTip.add(index, "Item age: " + EnumChatFormatting.DARK_GRAY + Utils.getDurationAsWords(dateTime.toEpochSecond() * 1000).first()); + } else { + // abbreviated tooltip + e.toolTip.add(index, "Item age: " + EnumChatFormatting.DARK_GRAY + Utils.getDurationAsWord(dateTime.toEpochSecond() * 1000)); + } + } + } + + // for auction house: show price for each item if multiple items are sold at once + if (e.entityPlayer != null && e.entityPlayer.openContainer instanceof ContainerChest) { + int stackSize = e.itemStack.stackSize; + if ((stackSize == 1 && !isSubmitBidItem(e.itemStack)) || e.toolTip.size() < 4) { + // only 1 item or irrelevant tooltip - nothing to do here, abort! + return; + } + + if (isSubmitBidItem(e.itemStack)) { + // special case: "place bid on an item" interface ("Auction View") + ItemStack auctionedItem = e.entityPlayer.openContainer.getInventory().get(13); + stackSize = auctionedItem.stackSize; + if (stackSize == 1) { + // still only 1 item, abort! + return; + } + } + + List toolTip = e.toolTip; + + // starting with i=1 because first line is never the one we're looking for + for (int i = 1; i < toolTip.size(); i++) { + String toolTipLineUnformatted = EnumChatFormatting.getTextWithoutFormattingCodes(toolTip.get(i)); + if (toolTipLineUnformatted.startsWith("Top bid: ") + || toolTipLineUnformatted.startsWith("Starting bid: ") + || toolTipLineUnformatted.startsWith("Buy it now: ") + || toolTipLineUnformatted.startsWith("Sold for: ") + || toolTipLineUnformatted.startsWith("New bid: ") /* special case: 'Submit Bid' item */) { + + try { + long price = numberFormatter.parse(StringUtils.substringBetween(toolTipLineUnformatted, ": ", " coins")).longValue(); + double priceEach = price / (double) stackSize; + String formattedPriceEach = priceEach < 5000 ? numberFormatter.format(priceEach) : Utils.formatNumberWithAbbreviations(priceEach); + String pricePerItem = EnumChatFormatting.YELLOW + " (" + formattedPriceEach + " each)"; + toolTip.set(i, toolTip.get(i) + pricePerItem); + return; + } catch (ParseException ex) { + return; + } + } + } + } + } + + private ZonedDateTime getDateTimeWithZone(Matcher sbTimestampMatcher, ZoneId zoneId) { + int year = 2000 + Integer.parseInt(sbTimestampMatcher.group(3)); + int month = Integer.parseInt(sbTimestampMatcher.group(1)); + int day = Integer.parseInt(sbTimestampMatcher.group(2)); + int hour = (Integer.parseInt(sbTimestampMatcher.group(4)) + (sbTimestampMatcher.group(6).equals("PM") ? 12 : 0)) % 24; + int minute = Integer.parseInt(sbTimestampMatcher.group(5)); + + LocalDateTime localDateTime = LocalDateTime.of(year, month, day, hour, minute); + + return ZonedDateTime.of(localDateTime, zoneId); + } + + private boolean isSubmitBidItem(ItemStack itemStack) { + return (itemStack.getItem().equals(Items.gold_nugget) || itemStack.getItem().equals(Item.getItemFromBlock(Blocks.gold_block))) + && (itemStack.hasDisplayName() && (itemStack.getDisplayName().endsWith("Submit Bid") || itemStack.getDisplayName().endsWith("Collect Auction"))); + } +} -- cgit