diff options
author | Cow <cow@volloeko.de> | 2020-12-28 15:43:37 +0100 |
---|---|---|
committer | Cow <cow@volloeko.de> | 2020-12-28 15:43:37 +0100 |
commit | e1c46e5fbad592241dc21b7297f8a982ea4b347f (patch) | |
tree | 636cac23d8d4b98482d2413ad6b4405b03b1c43f /src | |
parent | 50fe638726f9f684004365bb99c3117acead8cd0 (diff) | |
download | Cowlection-e1c46e5fbad592241dc21b7297f8a982ea4b347f.tar.gz Cowlection-e1c46e5fbad592241dc21b7297f8a982ea4b347f.tar.bz2 Cowlection-e1c46e5fbad592241dc21b7297f8a982ea4b347f.zip |
SkyBlock Bazaar graphs improvements
Diffstat (limited to 'src')
6 files changed, 186 insertions, 33 deletions
diff --git a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java index cf6b388..b5768fb 100644 --- a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java +++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java @@ -52,11 +52,12 @@ public class MooCommand extends CommandBase { @Override public List<String> getCommandAliases() { - if (StringUtils.isEmpty(MooConfig.mooCmdAlias)) { - return Collections.emptyList(); - } else { - return Collections.singletonList(MooConfig.mooCmdAlias); + List<String> aliases = new ArrayList<>(); + aliases.add(Cowlection.MODID); + if (StringUtils.isNotEmpty(MooConfig.mooCmdAlias)) { + aliases.add(MooConfig.mooCmdAlias); } + return aliases; } @Override diff --git a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java index 5266438..f8674ed 100644 --- a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java +++ b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java @@ -1,5 +1,6 @@ package de.cowtipper.cowlection.config; +import com.google.common.collect.Maps; import de.cowtipper.cowlection.Cowlection; import de.cowtipper.cowlection.command.MooCommand; import de.cowtipper.cowlection.command.TabCompletableCommand; @@ -68,6 +69,7 @@ public class MooConfig { public static int notifyOldServer; public static int tooltipToggleKeyBinding; private static String tooltipAuctionHousePriceEach; + private static String bazaarConnectGraphsNodes; private static String tooltipItemAge; public static boolean tooltipItemAgeShortened; private static String tooltipItemTimestamp; @@ -316,6 +318,18 @@ public class MooConfig { Property propTooltipAuctionHousePriceEach = subCat.addConfigEntry(cfg.get(configCat.getConfigName(), "tooltipAuctionHousePriceEach", "always", "Add price per item if multiple items are bought or sold", new String[]{"always", "key press", "never"})); + MooConfigPreview bazaarGraphPreview = new MooConfigPreview(MooConfigPreview.createDemoItem("paper", "§aBuy Price §731d §77d §e24h", new String[]{ + "§7The price at which buy orders have been filled.", "", + "§r┌----------------------------------------------┐", "§r│§66. 1k§r+§bxxxxxx§8·································§bxx§r│", + "§r│§8····§r│§8······································§bx§8··§r│", "§r│§66. 1k§r+§8·····§bx§8···················§bx§8·······§bxxxxx§8···§r│", + "§r│§8····§r│§8···············§bx§8········§bxxxxxxxxx§8········§r│", "§r│§8··§66k§r+§8··············§bx§8····§bxx§8··§bx§8·················§r│", + "§r│§8····§r│§8············§bx§8··§bxxxx§8·§bxxx§8··················§r│", "§r│§8··§66k§r+§8······§bx§8·§bxxxx§8·§bx§8···························§r│", + "§r│§8····§r│§8·······§bx§8·································§r│", "§r│§8··§66k§r+---------+----------+---------+---------+│", + "§r│§8····§r24h§8······§r18h§8········§r12h§8·······§r6h§8·······§rnow│", "§r└----------------------------------------------┘"}, Maps.newHashMap())); + Property propBazaarConnectGraphsNodes = subCat.addConfigEntry(cfg.get(configCat.getConfigName(), + "bazaarConnectGraphsNodes", "always", "Bazaar: connect the graph nodes", new String[]{"always", "key press", "never"}), + bazaarGraphPreview); + Map<String, NBTBase> demoItemExtraAttributes = new HashMap<>(); demoItemExtraAttributes.put("new_years_cake", new NBTTagInt(1)); demoItemExtraAttributes.put("originTag", new NBTTagString("REWARD_NEW_YEARS_CAKE_NPC")); @@ -470,6 +484,7 @@ public class MooConfig { notifyOldServer = propNotifyOldServer.getInt(); tooltipToggleKeyBinding = propTooltipToggleKeyBinding.getInt(); tooltipAuctionHousePriceEach = propTooltipAuctionHousePriceEach.getString(); + bazaarConnectGraphsNodes = propBazaarConnectGraphsNodes.getString(); tooltipItemAge = propTooltipItemAge.getString(); tooltipItemAgeShortened = propTooltipItemAgeShortened.getBoolean(); tooltipItemTimestamp = propTooltipItemTimestamp.getString(); @@ -521,6 +536,7 @@ public class MooConfig { propNotifyOldServer.set(notifyOldServer); propTooltipToggleKeyBinding.set(tooltipToggleKeyBinding); propTooltipAuctionHousePriceEach.set(tooltipAuctionHousePriceEach); + propBazaarConnectGraphsNodes.set(bazaarConnectGraphsNodes); propTooltipItemAge.set(tooltipItemAge); propTooltipItemAgeShortened.set(tooltipItemAgeShortened); propTooltipItemTimestamp.set(tooltipItemTimestamp); @@ -646,6 +662,10 @@ public class MooConfig { return Setting.get(tooltipAuctionHousePriceEach); } + public static Setting getBazaarConnectGraphsNodes() { + return Setting.get(bazaarConnectGraphsNodes); + } + public static Setting getTooltipItemAgeDisplay() { return Setting.get(tooltipItemAge); } diff --git a/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java b/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java index f1fdf74..15a902d 100644 --- a/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java +++ b/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java @@ -5,13 +5,13 @@ import de.cowtipper.cowlection.config.MooConfig; import de.cowtipper.cowlection.config.gui.MooConfigGui; import de.cowtipper.cowlection.data.DataHelper.DungeonClass; import de.cowtipper.cowlection.handler.DungeonCache; +import de.cowtipper.cowlection.util.GuiHelper; import de.cowtipper.cowlection.util.TickDelay; import de.cowtipper.cowlection.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.client.audio.SoundCategory; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.inventory.GuiChest; -import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.init.Blocks; @@ -23,10 +23,6 @@ import net.minecraft.item.EnumDyeColor; import net.minecraft.item.ItemSkull; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.scoreboard.Score; -import net.minecraft.scoreboard.ScoreObjective; -import net.minecraft.scoreboard.ScorePlayerTeam; -import net.minecraft.scoreboard.Scoreboard; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.MathHelper; import net.minecraftforge.client.event.ClientChatReceivedEvent; @@ -34,11 +30,9 @@ import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.common.util.Constants; import net.minecraftforge.event.entity.player.ItemTooltipEvent; -import net.minecraftforge.event.entity.player.PlayerSetSpawnEvent; import net.minecraftforge.fml.common.eventhandler.EventPriority; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; -import net.minecraftforge.fml.relauncher.ReflectionHelper; import net.minecraftforge.fml.relauncher.Side; import org.apache.commons.lang3.StringUtils; import org.lwjgl.input.Mouse; @@ -504,7 +498,7 @@ public class DungeonsListener { IInventory inventory = guiChest.inventorySlots.getSlot(0).inventory; if (inventory.getName().equals("Party Finder")) { // get dungeon floor nr when joining a dungeon party via party finder - Slot hoveredSlot = getSlotUnderMouse(guiChest); + Slot hoveredSlot = GuiHelper.getSlotUnderMouse(guiChest); if (hoveredSlot != null && hoveredSlot.getHasStack()) { // clicked on an item List<String> itemToolTip = hoveredSlot.getStack().getTooltip(Minecraft.getMinecraft().thePlayer, false); @@ -516,7 +510,7 @@ public class DungeonsListener { } } else if (inventory.getName().equals("Group Builder")) { // get dungeon floor nr when creating a dungeon party for party finder - Slot hoveredSlot = getSlotUnderMouse(guiChest); + Slot hoveredSlot = GuiHelper.getSlotUnderMouse(guiChest); if (hoveredSlot != null && hoveredSlot.getHasStack() && hoveredSlot.getStack().hasDisplayName()) { // clicked on an item String clickedItemName = EnumChatFormatting.getTextWithoutFormattingCodes(hoveredSlot.getStack().getDisplayName()); @@ -561,15 +555,6 @@ public class DungeonsListener { } } - private Slot getSlotUnderMouse(GuiChest guiChest) { - try { - return ReflectionHelper.getPrivateValue(GuiContainer.class, guiChest, "theSlot", "field_147006_u"); - } catch (ReflectionHelper.UnableToAccessFieldException e) { - e.printStackTrace(); - return null; - } - } - @SubscribeEvent public void onPlayerTick(TickEvent.PlayerTickEvent e) { if (e.phase != TickEvent.Phase.END && e.side != Side.CLIENT && e.type != TickEvent.Type.PLAYER) { diff --git a/src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java b/src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java index 4702ede..3f79154 100644 --- a/src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java +++ b/src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java @@ -1,12 +1,17 @@ package de.cowtipper.cowlection.listener.skyblock; import de.cowtipper.cowlection.config.MooConfig; +import de.cowtipper.cowlection.config.gui.MooConfigGui; +import de.cowtipper.cowlection.util.GuiHelper; import de.cowtipper.cowlection.util.Utils; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.inventory.GuiChest; 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; @@ -24,15 +29,11 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.regex.Pattern; public class SkyBlockListener { - /** - * 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() { @@ -45,6 +46,34 @@ public class SkyBlockListener { if (e.itemStack == null || e.toolTip == null) { return; } + + // bazaar graphs enhancements + if ((MooConfig.getBazaarConnectGraphsNodes() == MooConfig.Setting.ALWAYS + || MooConfig.getBazaarConnectGraphsNodes() == MooConfig.Setting.SPECIAL && MooConfig.isTooltipToggleKeyBindingPressed()) + && e.itemStack.getItem() == Items.paper) { + boolean drawGraph = false; + GuiScreen currentScreen = Minecraft.getMinecraft().currentScreen; + if (currentScreen instanceof GuiChest) { + // some kind of chest + ContainerChest chestContainer = (ContainerChest) ((GuiChest) currentScreen).inventorySlots; + IInventory inventory = chestContainer.getLowerChestInventory(); + String inventoryName = (inventory.hasCustomName() ? EnumChatFormatting.getTextWithoutFormattingCodes(inventory.getDisplayName().getUnformattedTextForChat()) : inventory.getName()); + + if (inventoryName.endsWith("➜ Graphs")) { + // bazaar interface with graphs + drawGraph = true; + } + } else if (currentScreen instanceof MooConfigGui) { + // preview in config gui + drawGraph = true; + } + if (drawGraph) { + GuiHelper.drawHoveringTextWithGraph(new ArrayList<>(e.toolTip)); + e.toolTip.clear(); + 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)) { diff --git a/src/main/java/de/cowtipper/cowlection/util/GuiHelper.java b/src/main/java/de/cowtipper/cowlection/util/GuiHelper.java index b904670..44bfe13 100644 --- a/src/main/java/de/cowtipper/cowlection/util/GuiHelper.java +++ b/src/main/java/de/cowtipper/cowlection/util/GuiHelper.java @@ -4,23 +4,41 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.gui.inventory.GuiChest; +import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.WorldRenderer; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.inventory.Slot; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.ForgeVersion; import net.minecraftforge.fml.client.config.GuiUtils; +import net.minecraftforge.fml.relauncher.ReflectionHelper; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public final class GuiHelper extends GuiScreen { private static GuiHelper instance; + private FontRenderer fontRendererAscii; private GuiHelper() { this.mc = Minecraft.getMinecraft(); this.fontRendererObj = mc.fontRendererObj; + + if (this.fontRendererObj.getCharWidth('x') != this.fontRendererObj.getCharWidth('·')) { + // we're not using default font (x and · should be the same width) - could be unicode font or a custom font + this.fontRendererAscii = new FontRenderer(mc.gameSettings, new ResourceLocation("textures/font/ascii.png"), mc.renderEngine, false); + this.fontRendererAscii.onResourceManagerReload(null); // load font widths + + } this.itemRender = mc.getRenderItem(); } @@ -31,6 +49,15 @@ public final class GuiHelper extends GuiScreen { return instance; } + public static Slot getSlotUnderMouse(GuiChest guiChest) { + try { + return ReflectionHelper.getPrivateValue(GuiContainer.class, guiChest, "theSlot", "field_147006_u"); + } catch (ReflectionHelper.UnableToAccessFieldException e) { + e.printStackTrace(); + return null; + } + } + /** * Draw a 1 pixel wide horizontal line. Args: startX, endX, y, color */ @@ -73,20 +100,33 @@ public final class GuiHelper extends GuiScreen { // using mc built-in method getInstance().width = screenWidth; getInstance().height = screenHeight; - drawHoveringText(textLines, mouseX, mouseY, screenWidth, screenHeight, maxTextWidth, getInstance().fontRendererObj); + getInstance().drawHoveringText(textLines, mouseX, mouseY, screenWidth, screenHeight, maxTextWidth, false); } else { // we're on a newer forge version, so we can use the improved tooltip rendering directly added in 1.8.9-11.15.1.1808 (released 03/24/16 09:25 PM) in this pull request: https://github.com/MinecraftForge/MinecraftForge/pull/2649 GuiUtils.drawHoveringText(textLines, mouseX, mouseY, screenWidth, screenHeight, maxTextWidth, getInstance().fontRendererObj); } } + public static void drawHoveringTextWithGraph(List<String> toolTip) { + int mouseX = Mouse.getX() * getInstance().width / getInstance().mc.displayWidth; + int mouseY = getInstance().height - Mouse.getY() * getInstance().height / getInstance().mc.displayHeight - 1; + ScaledResolution scaledResolution = new ScaledResolution(Minecraft.getMinecraft()); + + getInstance().width = scaledResolution.getScaledWidth(); + getInstance().height = scaledResolution.getScaledHeight(); + + getInstance().drawHoveringText(toolTip, mouseX, mouseY, scaledResolution.getScaledWidth(), scaledResolution.getScaledHeight(), -1, true); + } + /** * Fixed method for forge versions older than 1.8.9-11.15.1.1808 * * @see GuiUtils#drawHoveringText */ - public static void drawHoveringText(List<String> textLines, final int mouseX, final int mouseY, final int screenWidth, final int screenHeight, final int maxTextWidth, FontRenderer font) { + private void drawHoveringText(List<String> textLines, final int mouseX, final int mouseY, final int screenWidth, final int screenHeight, final int maxTextWidth, boolean drawGraph) { if (!textLines.isEmpty()) { + FontRenderer font = fontRendererAscii != null ? fontRendererAscii : fontRendererObj; + GlStateManager.disableRescaleNormal(); RenderHelper.disableStandardItemLighting(); GlStateManager.disableLighting(); @@ -107,7 +147,7 @@ public final class GuiHelper extends GuiScreen { int tooltipX = mouseX + 12; if (tooltipX + tooltipTextWidth + 4 > screenWidth) { tooltipX = mouseX - 16 - tooltipTextWidth; - if (tooltipX < 4) { // if the tooltip doesn't fit on the screen + if (tooltipX < 4 && !drawGraph) { // if the tooltip doesn't fit on the screen if (mouseX > screenWidth / 2) { tooltipTextWidth = mouseX - 12 - 8; } else { @@ -122,7 +162,7 @@ public final class GuiHelper extends GuiScreen { needsWrap = true; } - if (needsWrap) { + if (needsWrap && !drawGraph) { int wrappedTooltipWidth = 0; List<String> wrappedTextLines = new ArrayList<>(); for (int i = 0; i < textLines.size(); i++) { @@ -177,9 +217,41 @@ public final class GuiHelper extends GuiScreen { Gui.drawRect(tooltipX - 3, tooltipY - 3, tooltipX + tooltipTextWidth + 3, tooltipY - 3 + 1, borderColorStart); Gui.drawRect(tooltipX - 3, tooltipY + tooltipHeight + 2, tooltipX + tooltipTextWidth + 3, tooltipY + tooltipHeight + 3, borderColorEnd); + List<GraphNode> graphNodes = new ArrayList<>(); + if (drawGraph) { + graphNodes.addAll(Collections.nCopies(100, null)); + } + int widthCharX = font.getCharWidth('x') / 2; for (int lineNumber = 0; lineNumber < textLines.size(); ++lineNumber) { String line = textLines.get(lineNumber); - font.drawStringWithShadow(line, (float) tooltipX, (float) tooltipY, -1); + String lineWithoutFormattingCodes = EnumChatFormatting.getTextWithoutFormattingCodes(line); + + if (drawGraph && (lineWithoutFormattingCodes.startsWith("│") || lineWithoutFormattingCodes.startsWith("┌") || lineWithoutFormattingCodes.startsWith("└"))) { + // use default mc font + int substrWidth = 0; + int spaceOffset = 0; + for (int c = 0; c < Math.min(100, lineWithoutFormattingCodes.length()); c++) { + char xOrDot = lineWithoutFormattingCodes.charAt(c); + substrWidth += font.getCharWidth(xOrDot); + if (xOrDot == 'x') { + int index = c - spaceOffset; + int yPos = tooltipY + font.FONT_HEIGHT / 2; + GraphNode graphNode = graphNodes.get(index); + if (graphNode == null) { + graphNode = new GraphNode(tooltipX + substrWidth - widthCharX, yPos); + graphNodes.set(index, graphNode); + } else { + graphNode.addY(yPos); + } + } else if (xOrDot == ' ') { + spaceOffset = 1; + } + } + font.drawStringWithShadow(line, (float) tooltipX, (float) tooltipY, -1); + } else { + // use client's font + fontRendererObj.drawStringWithShadow(line, (float) tooltipX, (float) tooltipY, -1); + } if (lineNumber + 1 == titleLinesCount) { tooltipY += 2; @@ -192,6 +264,50 @@ public final class GuiHelper extends GuiScreen { GlStateManager.enableDepth(); RenderHelper.enableStandardItemLighting(); GlStateManager.enableRescaleNormal(); + + if (drawGraph) { + GlStateManager.pushMatrix(); + GlStateManager.clear(GL11.GL_DEPTH_BUFFER_BIT); + GL11.glLineWidth(6F); + GlStateManager.disableTexture2D(); + GlStateManager.color(255 / 255F, 170 / 255F, 0 / 255F); + WorldRenderer wr = Tessellator.getInstance().getWorldRenderer(); + wr.begin(GL11.GL_LINE_STRIP, DefaultVertexFormats.POSITION); + for (GraphNode graphNode : graphNodes) { + if (graphNode == null) { + continue; + } + wr.pos(graphNode.getX(), graphNode.getY(), 0).endVertex(); + } + Tessellator.getInstance().draw(); + GlStateManager.popMatrix(); + } + } + } + + private static class GraphNode { + private final int x; + private final List<Integer> y = new ArrayList<>(); + + public GraphNode(int x, int y) { + this.x = x; + this.y.add(y); + } + + public int getX() { + return x; + } + + public int getY() { + int sum = 0; + for (Integer y : this.y) { + sum += y; + } + return sum / this.y.size(); + } + + public void addY(int y) { + this.y.add(y); } } } diff --git a/src/main/resources/assets/cowlection/lang/en_US.lang b/src/main/resources/assets/cowlection/lang/en_US.lang index c1c7be5..f4f189a 100644 --- a/src/main/resources/assets/cowlection/lang/en_US.lang +++ b/src/main/resources/assets/cowlection/lang/en_US.lang @@ -38,8 +38,10 @@ 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.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' -cowlection.config.tooltipAuctionHousePriceEach=Auction house: price per item +cowlection.config.tooltipAuctionHousePriceEach=§7Auction house: §rprice per item cowlection.config.tooltipAuctionHousePriceEach.tooltip=Add price per item if multiple items are bought or sold? +cowlection.config.bazaarConnectGraphsNodes=§7Bazaar: §rconnect the graph nodes +cowlection.config.bazaarConnectGraphsNodes.tooltip=Draw a line through the bazaar graphs' nodes?\n§7§oThis also (tries to) fix the graphs when using the MC unicode font. cowlection.config.tooltipItemAge=Show item age cowlection.config.tooltipItemAge.tooltip=Show item age? Only works for non-stackable items cowlection.config.tooltipItemAgeShortened=Shorten item age? |