aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de
diff options
context:
space:
mode:
authorCow <cow@volloeko.de>2020-07-28 00:12:36 +0200
committerCow <cow@volloeko.de>2020-07-28 00:12:36 +0200
commitb393636cb3f7e05ef8b34804eeb06357f1b9cfbe (patch)
treed754561fd2e2f09ac66f41b2645ac5f351c1cace /src/main/java/de
parent023589c75ae72ddc5ff75fa7235bce4d102b2ad1 (diff)
downloadCowlection-b393636cb3f7e05ef8b34804eeb06357f1b9cfbe.tar.gz
Cowlection-b393636cb3f7e05ef8b34804eeb06357f1b9cfbe.tar.bz2
Cowlection-b393636cb3f7e05ef8b34804eeb06357f1b9cfbe.zip
Renamed package to match cowtipper.de
Diffstat (limited to 'src/main/java/de')
-rw-r--r--src/main/java/de/cowtipper/cowlection/Cowlection.java127
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/MooCommand.java641
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/ReplyCommand.java35
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/ShrugCommand.java34
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/TabCompletableCommand.java53
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/exception/ApiContactException.java7
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/exception/InvalidPlayerNameException.java10
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/exception/MooCommandException.java9
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/exception/ThrowingConsumer.java25
-rw-r--r--src/main/java/de/cowtipper/cowlection/config/MooConfig.java306
-rw-r--r--src/main/java/de/cowtipper/cowlection/config/MooGuiConfig.java86
-rw-r--r--src/main/java/de/cowtipper/cowlection/config/MooGuiFactory.java29
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/DataHelper.java723
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/Friend.java65
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/HyPlayerData.java103
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java239
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/HyStalkingData.java120
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/LogEntry.java82
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/XpTables.java253
-rw-r--r--src/main/java/de/cowtipper/cowlection/handler/DungeonCache.java52
-rw-r--r--src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java176
-rw-r--r--src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java47
-rw-r--r--src/main/java/de/cowtipper/cowlection/listener/ChatListener.java194
-rw-r--r--src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java128
-rw-r--r--src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java399
-rw-r--r--src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java162
-rw-r--r--src/main/java/de/cowtipper/cowlection/search/GuiDateField.java37
-rw-r--r--src/main/java/de/cowtipper/cowlection/search/GuiSearch.java603
-rw-r--r--src/main/java/de/cowtipper/cowlection/search/GuiTooltip.java50
-rw-r--r--src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java181
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/ApiUtils.java139
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/ChatHelper.java82
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/GsonUtils.java94
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/ImageUtils.java64
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java186
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/TickDelay.java29
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/Utils.java253
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/VersionChecker.java146
38 files changed, 5969 insertions, 0 deletions
diff --git a/src/main/java/de/cowtipper/cowlection/Cowlection.java b/src/main/java/de/cowtipper/cowlection/Cowlection.java
new file mode 100644
index 0000000..de92ab9
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/Cowlection.java
@@ -0,0 +1,127 @@
+package de.cowtipper.cowlection;
+
+import de.cowtipper.cowlection.command.MooCommand;
+import de.cowtipper.cowlection.command.ReplyCommand;
+import de.cowtipper.cowlection.command.ShrugCommand;
+import de.cowtipper.cowlection.command.TabCompletableCommand;
+import de.cowtipper.cowlection.config.MooConfig;
+import de.cowtipper.cowlection.handler.DungeonCache;
+import de.cowtipper.cowlection.handler.FriendsHandler;
+import de.cowtipper.cowlection.handler.PlayerCache;
+import de.cowtipper.cowlection.listener.ChatListener;
+import de.cowtipper.cowlection.listener.PlayerListener;
+import de.cowtipper.cowlection.util.ChatHelper;
+import de.cowtipper.cowlection.util.VersionChecker;
+import net.minecraftforge.client.ClientCommandHandler;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.Mod.EventHandler;
+import net.minecraftforge.fml.common.event.FMLInitializationEvent;
+import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
+import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+
+@Mod(modid = Cowlection.MODID, name = Cowlection.MODNAME, version = Cowlection.VERSION,
+ clientSideOnly = true,
+ guiFactory = "@PACKAGE@.config.MooGuiFactory",
+ updateJSON = "https://raw.githubusercontent.com/cow-mc/Cowlection/master/update.json")
+public class Cowlection {
+ public static final String MODID = "@MODID@";
+ public static final String VERSION = "@VERSION@";
+ public static final String MODNAME = "@MODNAME@";
+ public static final String GITURL = "@GITURL@";
+ private static Cowlection instance;
+ private File configDir;
+ private File modsDir;
+ private MooConfig config;
+ private FriendsHandler friendsHandler;
+ private VersionChecker versionChecker;
+ private ChatHelper chatHelper;
+ private PlayerCache playerCache;
+ private DungeonCache dungeonCache;
+ private Logger logger;
+
+ @Mod.EventHandler
+ public void preInit(FMLPreInitializationEvent e) {
+ instance = this;
+ logger = e.getModLog();
+ modsDir = e.getSourceFile().getParentFile();
+
+ this.configDir = new File(e.getModConfigurationDirectory(), MODID + File.separatorChar);
+ if (!configDir.exists()) {
+ configDir.mkdirs();
+ }
+
+ friendsHandler = new FriendsHandler(this, new File(configDir, "friends.json"));
+ config = new MooConfig(this, new Configuration(new File(configDir, MODID + ".cfg")));
+
+ chatHelper = new ChatHelper();
+ }
+
+ @EventHandler
+ public void init(FMLInitializationEvent e) {
+ MinecraftForge.EVENT_BUS.register(new ChatListener(this));
+ MinecraftForge.EVENT_BUS.register(new PlayerListener(this));
+ ClientCommandHandler.instance.registerCommand(new MooCommand(this));
+ ClientCommandHandler.instance.registerCommand(new ReplyCommand(this));
+ ClientCommandHandler.instance.registerCommand(new ShrugCommand(this));
+ for (String tabCompletableNamesCommand : MooConfig.tabCompletableNamesCommands) {
+ ClientCommandHandler.instance.registerCommand(new TabCompletableCommand(this, tabCompletableNamesCommand));
+ }
+ }
+
+ @EventHandler
+ public void postInit(FMLPostInitializationEvent e) {
+ versionChecker = new VersionChecker(this);
+ playerCache = new PlayerCache(this);
+ }
+
+ public MooConfig getConfig() {
+ return config;
+ }
+
+ public FriendsHandler getFriendsHandler() {
+ return friendsHandler;
+ }
+
+ public VersionChecker getVersionChecker() {
+ return versionChecker;
+ }
+
+ public ChatHelper getChatHelper() {
+ return chatHelper;
+ }
+
+ public PlayerCache getPlayerCache() {
+ return playerCache;
+ }
+
+ public DungeonCache getDungeonCache() {
+ if (dungeonCache == null) {
+ dungeonCache = new DungeonCache(this);
+ }
+ return dungeonCache;
+ }
+
+ public File getConfigDirectory() {
+ return configDir;
+ }
+
+ public File getModsDirectory() {
+ return modsDir;
+ }
+
+ public Logger getLogger() {
+ return logger;
+ }
+
+ /**
+ * Get mod's instance; instead of this method use dependency injection where possible
+ */
+ public static Cowlection getInstance() {
+ return instance;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java
new file mode 100644
index 0000000..219ec01
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java
@@ -0,0 +1,641 @@
+package de.cowtipper.cowlection.command;
+
+import com.mojang.realmsclient.util.Pair;
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.command.exception.ApiContactException;
+import de.cowtipper.cowlection.command.exception.InvalidPlayerNameException;
+import de.cowtipper.cowlection.command.exception.MooCommandException;
+import de.cowtipper.cowlection.config.MooConfig;
+import de.cowtipper.cowlection.config.MooGuiConfig;
+import de.cowtipper.cowlection.data.*;
+import de.cowtipper.cowlection.data.HySkyBlockStats.Profile.Pet;
+import de.cowtipper.cowlection.handler.DungeonCache;
+import de.cowtipper.cowlection.search.GuiSearch;
+import de.cowtipper.cowlection.util.*;
+import net.minecraft.client.Minecraft;
+import net.minecraft.command.*;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.item.EntityArmorStand;
+import net.minecraft.event.ClickEvent;
+import net.minecraft.event.HoverEvent;
+import net.minecraft.item.ItemSkull;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.*;
+import net.minecraftforge.common.util.Constants;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.List;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+public class MooCommand extends CommandBase {
+ private final Cowlection main;
+
+ public MooCommand(Cowlection main) {
+ this.main = main;
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ if (args.length == 0) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "Tried to say " + EnumChatFormatting.YELLOW + getCommandName() + EnumChatFormatting.GOLD + "? Use " + EnumChatFormatting.YELLOW + getCommandName() + " say [optional text]" + EnumChatFormatting.GOLD + " instead.\n"
+ + "Tried to use the command " + EnumChatFormatting.YELLOW + "/" + getCommandName() + EnumChatFormatting.GOLD + "? Use " + EnumChatFormatting.YELLOW + "/" + getCommandName() + " help" + EnumChatFormatting.GOLD + " for a list of available commands");
+ return;
+ }
+ // sub commands: friends & other players
+ if (args[0].equalsIgnoreCase("say")) {
+ // work-around so you can still say 'moo' in chat without triggering the client-side command
+ String msg = CommandBase.buildString(args, 1);
+ Minecraft.getMinecraft().thePlayer.sendChatMessage(getCommandName() + (!msg.isEmpty() ? " " + msg : ""));
+ } else if (args[0].equalsIgnoreCase("stalk")
+ || args[0].equalsIgnoreCase("s")
+ || args[0].equalsIgnoreCase("askPolitelyWhereTheyAre")) {
+ if (args.length != 2) {
+ throw new WrongUsageException("/" + getCommandName() + " stalk <playerName>");
+ } else if (!Utils.isValidMcName(args[1])) {
+ throw new InvalidPlayerNameException(args[1]);
+ } else {
+ handleStalking(args[1]);
+ }
+ } else if (args[0].equalsIgnoreCase("stalkskyblock") || args[0].equalsIgnoreCase("skyblockstalk")
+ || args[0].equalsIgnoreCase("ss")
+ || args[0].equalsIgnoreCase("stalksb") || args[0].equalsIgnoreCase("sbstalk")
+ || args[0].equalsIgnoreCase("askPolitelyAboutTheirSkyBlockProgress")) {
+ if (args.length != 2) {
+ throw new WrongUsageException("/" + getCommandName() + " skyblockstalk <playerName>");
+ } else if (!Utils.isValidMcName(args[1])) {
+ throw new InvalidPlayerNameException(args[1]);
+ } else {
+ handleStalkingSkyBlock(args[1]);
+ }
+ } else if (args[0].equalsIgnoreCase("analyzeIsland")) {
+ Map<String, String> minions = DataHelper.getMinions();
+
+ Map<String, Integer> detectedMinions = new HashMap<>();
+ Map<Integer, Integer> detectedMinionsWithSkin = new HashMap<>();
+ int detectedMinionCount = 0;
+ int minionsWithSkinCount = 0;
+ entityLoop:
+ for (Entity entity : sender.getEntityWorld().loadedEntityList) {
+ if (entity instanceof EntityArmorStand) {
+ EntityArmorStand minion = (EntityArmorStand) entity;
+
+ if (minion.isInvisible() || !minion.isSmall() || minion.getHeldItem() == null) {
+ // not a minion: invisible, or not small armor stand, or no item in hand (= minion in a minion chair)
+ continue;
+ }
+ for (int slot = 0; slot < 4; slot++) {
+ if (minion.getCurrentArmor(slot) == null) {
+ // not a minion: missing equipment
+ continue entityLoop;
+ }
+ }
+ ItemStack skullItem = minion.getCurrentArmor(3); // head slot
+ if (skullItem.getItem() instanceof ItemSkull && skullItem.getMetadata() == 3 && skullItem.hasTagCompound()) {
+ // is a player head!
+ if (skullItem.getTagCompound().hasKey("SkullOwner", Constants.NBT.TAG_COMPOUND)) {
+ NBTTagCompound skullOwner = skullItem.getTagCompound().getCompoundTag("SkullOwner");
+ String skullDataBase64 = skullOwner.getCompoundTag("Properties").getTagList("textures", Constants.NBT.TAG_COMPOUND).getCompoundTagAt(0).getString("Value");
+ String skullData = new String(Base64.decodeBase64(skullDataBase64));
+ String minionSkinId = StringUtils.substringBetween(skullData, "http://textures.minecraft.net/texture/", "\"");
+ String detectedMinion = minions.get(minionSkinId);
+ if (detectedMinion != null) {
+ // minion head matches one know minion tier
+ detectedMinions.put(detectedMinion, detectedMinions.getOrDefault(detectedMinion, 0) + 1);
+ detectedMinionCount++;
+ } else {
+ int minionTier = ImageUtils.getTierFromTexture(minionSkinId);
+ if (minionTier > 0) {
+ detectedMinionsWithSkin.put(minionTier, detectedMinionsWithSkin.getOrDefault(minionTier, 0) + 1);
+ minionsWithSkinCount++;
+ } else {
+ // looked like a minion but has no matching tier badge
+ main.getLogger().info("[/moo analyzeIsland] Found an armor stand that could be a minion but it is missing a tier badge: " + minionSkinId + "\t\t\t" + minion.serializeNBT());
+ }
+ }
+ }
+ }
+ }
+ }
+ StringBuilder analysisResults = new StringBuilder("Found ").append(EnumChatFormatting.GOLD).append(detectedMinionCount).append(EnumChatFormatting.YELLOW).append(" minions");
+ if (minionsWithSkinCount > 0) {
+ analysisResults.append(" + ").append(EnumChatFormatting.GOLD).append(minionsWithSkinCount).append(EnumChatFormatting.YELLOW).append(" unknown minions with skins");
+ }
+ analysisResults.append(" on this island");
+ detectedMinions.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey()) // sort alphabetically by minion name and tier
+ .forEach(minion -> {
+ String minionWithTier = minion.getKey();
+ int lastSpace = minionWithTier.lastIndexOf(' ');
+
+ String tierRoman = minionWithTier.substring(lastSpace + 1);
+
+ int tierArabic = Utils.convertRomanToArabic(tierRoman);
+ EnumChatFormatting tierColor = Utils.getMinionTierColor(tierArabic);
+
+ minionWithTier = minionWithTier.substring(0, lastSpace) + " " + tierColor + (MooConfig.useRomanNumerals() ? tierRoman : tierArabic);
+ analysisResults.append("\n ").append(EnumChatFormatting.GOLD).append(minion.getValue()).append(minion.getValue() > 1 ? "✕ " : "⨉ ")
+ .append(EnumChatFormatting.YELLOW).append(minionWithTier);
+ });
+ detectedMinionsWithSkin.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey()) // sort by tier
+ .forEach(minionWithSkin -> {
+ EnumChatFormatting tierColor = Utils.getMinionTierColor(minionWithSkin.getKey());
+ String minionTier = MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(minionWithSkin.getKey()) : String.valueOf(minionWithSkin.getKey());
+ analysisResults.append("\n ").append(EnumChatFormatting.GOLD).append(minionWithSkin.getValue()).append(minionWithSkin.getValue() > 1 ? "✕ " : "⨉ ")
+ .append(EnumChatFormatting.RED).append("Unknown minion ").append(EnumChatFormatting.YELLOW).append("(new or with minion skin) ").append(tierColor).append(minionTier);
+ });
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, analysisResults.toString());
+ } else if (args[0].equalsIgnoreCase("deaths")) {
+ DungeonCache dungeonCache = main.getDungeonCache();
+ if (dungeonCache.isInDungeon()) {
+ dungeonCache.sendDeathCounts();
+ } else {
+ throw new MooCommandException(EnumChatFormatting.DARK_RED + "Looks like you're not in a dungeon...");
+ }
+ } else if (args[0].equalsIgnoreCase("add")) {
+ handleBestFriendAdd(args);
+ } else if (args[0].equalsIgnoreCase("remove")) {
+ handleBestFriendRemove(args);
+ } else if (args[0].equalsIgnoreCase("list")) {
+ handleListBestFriends();
+ } else if (args[0].equalsIgnoreCase("nameChangeCheck")) {
+ handleNameChangeCheck(args);
+ }
+ // sub-commands: miscellaneous
+ else if (args[0].equalsIgnoreCase("config") || args[0].equalsIgnoreCase("toggle")) {
+ new TickDelay(() -> Minecraft.getMinecraft().displayGuiScreen(new MooGuiConfig(null)), 1); // delay by 1 tick, because the chat closing would close the new gui instantly as well.
+ } else if (args[0].equalsIgnoreCase("search")) {
+ new TickDelay(() -> Minecraft.getMinecraft().displayGuiScreen(new GuiSearch(main.getConfigDirectory())), 1); // delay by 1 tick, because the chat closing would close the new gui instantly as well.
+ } else if (args[0].equalsIgnoreCase("guiscale")) {
+ int currentGuiScale = (Minecraft.getMinecraft()).gameSettings.guiScale;
+ if (args.length == 1) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u279C Current GUI scale: " + EnumChatFormatting.DARK_GREEN + currentGuiScale);
+ } else {
+ int scale = MathHelper.parseIntWithDefault(args[1], -1);
+ if (scale == -1 || scale > 10) {
+ throw new NumberInvalidException(EnumChatFormatting.DARK_RED + args[1] + EnumChatFormatting.RED + " is an invalid GUI scale value. Valid values are integers below 10");
+ }
+ Minecraft.getMinecraft().gameSettings.guiScale = scale;
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u2714 New GUI scale: " + EnumChatFormatting.DARK_GREEN + scale + EnumChatFormatting.GREEN + " (previous: " + EnumChatFormatting.DARK_GREEN + currentGuiScale + EnumChatFormatting.GREEN + ")");
+ }
+ } else if (args[0].equalsIgnoreCase("rr")) {
+ Minecraft.getMinecraft().thePlayer.sendChatMessage("/r " + CommandBase.buildString(args, 1));
+ } else if (args[0].equalsIgnoreCase("shrug")) {
+ main.getChatHelper().sendShrug(buildString(args, 1));
+ } else if (args[0].equalsIgnoreCase("apikey")) {
+ handleApiKey(args);
+ }
+ // sub-commands: update mod
+ else if (args[0].equalsIgnoreCase("update")) {
+ boolean updateCheckStarted = main.getVersionChecker().runUpdateCheck(true);
+
+ if (updateCheckStarted) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u279C Checking for a newer mod version...");
+ // VersionChecker#handleVersionStatus will run with a 5 seconds delay
+ } else {
+ long nextUpdate = main.getVersionChecker().getNextCheck();
+ String waitingTime = String.format("%02d:%02d",
+ TimeUnit.MILLISECONDS.toMinutes(nextUpdate),
+ TimeUnit.MILLISECONDS.toSeconds(nextUpdate) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(nextUpdate)));
+ throw new MooCommandException("\u26A0 Update checker is on cooldown. Please wait " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + waitingTime + EnumChatFormatting.RESET + EnumChatFormatting.RED + " more minutes before checking again.");
+ }
+ } else if (args[0].equalsIgnoreCase("updateHelp")) {
+ main.getChatHelper().sendMessage(new ChatComponentText("\u279C Update instructions:").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true))
+ .appendSibling(new ChatComponentText("\n\u278A" + EnumChatFormatting.YELLOW + " download latest mod version").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(false)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, main.getVersionChecker().getDownloadUrl()))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Download the latest version of " + Cowlection.MODNAME + "\n\u279C Click to download latest mod file")))))
+ .appendSibling(new ChatComponentText("\n\u278B" + EnumChatFormatting.YELLOW + " exit Minecraft").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(false)
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.GOLD + "\u278B" + EnumChatFormatting.YELLOW + " Without closing Minecraft first,\n" + EnumChatFormatting.YELLOW + "you can't delete the old .jar file!")))))
+ .appendSibling(new ChatComponentText("\n\u278C" + EnumChatFormatting.YELLOW + " copy " + EnumChatFormatting.GOLD + Cowlection.MODNAME.replace(" ", "") + "-" + main.getVersionChecker().getNewVersion() + ".jar" + EnumChatFormatting.YELLOW + " into mods directory").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(false)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo directory"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Open mods directory with command " + EnumChatFormatting.GOLD + "/moo directory\n\u279C Click to open mods directory")))))
+ .appendSibling(new ChatComponentText("\n\u278D" + EnumChatFormatting.YELLOW + " delete old mod file " + EnumChatFormatting.GOLD + Cowlection.MODNAME.replace(" ", "") + "-" + Cowlection.VERSION + ".jar ").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(false)))
+ .appendSibling(new ChatComponentText("\n\u278E" + EnumChatFormatting.YELLOW + " start Minecraft again").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(false))));
+ } else if (args[0].equalsIgnoreCase("version")) {
+ main.getVersionChecker().handleVersionStatus(true);
+ } else if (args[0].equalsIgnoreCase("directory") || args[0].equalsIgnoreCase("folder")) {
+ try {
+ Desktop.getDesktop().open(main.getModsDirectory());
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new MooCommandException("\u2716 An error occurred trying to open the mod's directory. I guess you have to open it manually \u00af\\_(\u30c4)_/\u00af");
+ }
+ } else if (args[0].equalsIgnoreCase("help")) {
+ sendCommandUsage(sender);
+ }
+ // "catch-all" remaining sub-commands
+ else {
+ main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Command " + EnumChatFormatting.DARK_RED + "/" + getCommandName() + " " + args[0] + EnumChatFormatting.RED + " doesn't exist. Use " + EnumChatFormatting.DARK_RED + "/" + getCommandName() + " help " + EnumChatFormatting.RED + "to show command usage.");
+ }
+ }
+
+ private void handleApiKey(String[] args) throws CommandException {
+ if (args.length == 1) {
+ String firstSentence;
+ EnumChatFormatting color;
+ EnumChatFormatting colorSecondary;
+ if (Utils.isValidUuid(MooConfig.moo)) {
+ firstSentence = "You already set your Hypixel API key.";
+ color = EnumChatFormatting.GREEN;
+ colorSecondary = EnumChatFormatting.DARK_GREEN;
+ } else {
+ firstSentence = "You haven't set your Hypixel API key yet.";
+ color = EnumChatFormatting.RED;
+ colorSecondary = EnumChatFormatting.DARK_RED;
+ }
+ main.getChatHelper().sendMessage(color, firstSentence + " Use " + colorSecondary + "/api new" + color + " to request a new API key from Hypixel or use " + colorSecondary + "/" + this.getCommandName() + " apikey <key>" + color + " to manually set your existing API key.");
+ } else {
+ String key = args[1];
+ if (Utils.isValidUuid(key)) {
+ MooConfig.moo = key;
+ main.getConfig().syncFromFields();
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Updated API key!");
+ } else {
+ throw new SyntaxErrorException("That doesn't look like a valid API key...");
+ }
+ }
+ }
+
+ private void handleStalking(String playerName) throws CommandException {
+ if (!Utils.isValidUuid(MooConfig.moo)) {
+ throw new MooCommandException("You haven't set your Hypixel API key yet. Use " + EnumChatFormatting.DARK_RED + "/api new" + EnumChatFormatting.RED + " to request a new API key from Hypixel or use " + EnumChatFormatting.DARK_RED + "/" + this.getCommandName() + " apikey <key>" + EnumChatFormatting.RED + " to manually set your existing API key.");
+ }
+ main.getChatHelper().sendMessage(EnumChatFormatting.GRAY, "Stalking " + EnumChatFormatting.WHITE + playerName + EnumChatFormatting.GRAY + ". This may take a few seconds.");
+ boolean isBestFriend = main.getFriendsHandler().isBestFriend(playerName, true);
+ if (isBestFriend) {
+ Friend stalkedPlayer = main.getFriendsHandler().getBestFriend(playerName);
+ // we have the uuid already, so stalk the player
+ stalkPlayer(stalkedPlayer);
+ } else {
+ // fetch player uuid
+ ApiUtils.fetchFriendData(playerName, stalkedPlayer -> {
+ if (stalkedPlayer == null) {
+ throw new ApiContactException("Mojang", "couldn't stalk " + EnumChatFormatting.DARK_RED + playerName);
+ } else if (stalkedPlayer.equals(Friend.FRIEND_NOT_FOUND)) {
+ throw new PlayerNotFoundException("There is no player with the name " + EnumChatFormatting.DARK_RED + playerName + EnumChatFormatting.RED + ".");
+ } else {
+ // ... then stalk the player
+ stalkPlayer(stalkedPlayer);
+ }
+ });
+ }
+ }
+
+ private void stalkPlayer(Friend stalkedPlayer) {
+ ApiUtils.fetchPlayerStatus(stalkedPlayer, hyStalking -> {
+ if (hyStalking != null && hyStalking.isSuccess()) {
+ HyStalkingData.HySession session = hyStalking.getSession();
+ if (session.isOnline()) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, EnumChatFormatting.GOLD + stalkedPlayer.getName() + EnumChatFormatting.YELLOW + " is currently playing " + EnumChatFormatting.GOLD + session.getGameType() + EnumChatFormatting.YELLOW
+ + (session.getMode() != null ? ": " + EnumChatFormatting.GOLD + session.getMode() : "")
+ + (session.getMap() != null ? EnumChatFormatting.YELLOW + " (Map: " + EnumChatFormatting.GOLD + session.getMap() + EnumChatFormatting.YELLOW + ")" : ""));
+ } else {
+ ApiUtils.fetchPlayerOfflineStatus(stalkedPlayer, hyPlayerData -> {
+ if (hyPlayerData == null) {
+ throw new ApiContactException("Hypixel", "couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + " but they appear to be offline currently.");
+ } else if (hyPlayerData.hasNeverJoinedHypixel()) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, EnumChatFormatting.GOLD + stalkedPlayer.getName() + EnumChatFormatting.YELLOW + " has " + EnumChatFormatting.GOLD + "never " + EnumChatFormatting.YELLOW + "been on Hypixel (or might be nicked).");
+ } else if (hyPlayerData.isHidingOnlineStatus()) {
+ main.getChatHelper().sendMessage(new ChatComponentText(hyPlayerData.getPlayerNameFormatted()).appendSibling(new ChatComponentText(" is hiding their online status from the Hypixel API. You can see their online status with ").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.YELLOW)))
+ .appendSibling(new ChatComponentText("/profile " + hyPlayerData.getPlayerName()).setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GOLD)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/profile " + hyPlayerData.getPlayerName()))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/profile " + hyPlayerData.getPlayerName())))))
+ .appendSibling(new ChatComponentText(" while you're in a lobby (tooltip of the player head on the top left).").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.YELLOW))));
+ } else if (hyPlayerData.hasNeverLoggedOut()) {
+ Pair<String, String> lastOnline = Utils.getDurationAsWords(hyPlayerData.getLastLogin());
+
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, hyPlayerData.getPlayerNameFormatted() + EnumChatFormatting.YELLOW + " was last online " + EnumChatFormatting.GOLD + lastOnline.first() + EnumChatFormatting.YELLOW + " ago"
+ + (lastOnline.second() != null ? " (" + EnumChatFormatting.GOLD + lastOnline.second() + EnumChatFormatting.YELLOW + ")" : "") + ".");
+ } else if (hyPlayerData.getLastLogin() > hyPlayerData.getLastLogout()) {
+ // player is logged in but is hiding their session details from API (My Profile > API settings > Online Status)
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, EnumChatFormatting.GOLD + hyPlayerData.getPlayerNameFormatted() + EnumChatFormatting.YELLOW + " is currently playing " + EnumChatFormatting.GOLD + hyPlayerData.getLastGame() + "\n" + EnumChatFormatting.DARK_GRAY + "(" + hyPlayerData.getPlayerName() + " hides their session details from the API so that only their current game mode is visible)");
+ } else {
+ Pair<String, String> lastOnline = Utils.getDurationAsWords(hyPlayerData.getLastLogout());
+
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, hyPlayerData.getPlayerNameFormatted() + EnumChatFormatting.YELLOW + " is " + EnumChatFormatting.GOLD + "offline" + EnumChatFormatting.YELLOW + " for " + EnumChatFormatting.GOLD + lastOnline.first() + EnumChatFormatting.YELLOW
+ + ((lastOnline.second() != null || hyPlayerData.getLastGame() != null) ? (" ("
+ + (lastOnline.second() != null ? EnumChatFormatting.GOLD + lastOnline.second() + EnumChatFormatting.YELLOW : "") // = last online date
+ + (lastOnline.second() != null && hyPlayerData.getLastGame() != null ? "; " : "") // = delimiter
+ + (hyPlayerData.getLastGame() != null ? "last played gamemode: " + EnumChatFormatting.GOLD + hyPlayerData.getLastGame() + EnumChatFormatting.YELLOW : "") // = last gamemode
+ + ")") : "") + ".");
+ }
+ });
+ }
+ } else {
+ String cause = (hyStalking != null) ? hyStalking.getCause() : null;
+ throw new ApiContactException("Hypixel", "couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + (cause != null ? " (Reason: " + EnumChatFormatting.DARK_RED + cause + EnumChatFormatting.RED + ")" : "") + ".");
+ }
+ });
+ }
+
+ private void handleStalkingSkyBlock(String playerName) throws CommandException {
+ if (!Utils.isValidUuid(MooConfig.moo)) {
+ throw new MooCommandException("You haven't set your Hypixel API key yet. Use " + EnumChatFormatting.DARK_RED + "/api new" + EnumChatFormatting.RED + " to request a new API key from Hypixel or use " + EnumChatFormatting.DARK_RED + "/" + this.getCommandName() + " apikey <key>" + EnumChatFormatting.RED + " to manually set your existing API key.");
+ }
+ main.getChatHelper().sendMessage(EnumChatFormatting.GRAY, "Stalking " + EnumChatFormatting.WHITE + playerName + EnumChatFormatting.GRAY + "'s SkyBlock stats. This may take a few seconds.");
+ boolean isBestFriend = main.getFriendsHandler().isBestFriend(playerName, true);
+ if (isBestFriend) {
+ Friend stalkedPlayer = main.getFriendsHandler().getBestFriend(playerName);
+ // we have the uuid already, so stalk the player
+ stalkSkyBlockStats(stalkedPlayer);
+ } else {
+ // fetch player uuid
+ ApiUtils.fetchFriendData(playerName, stalkedPlayer -> {
+ if (stalkedPlayer == null) {
+ throw new ApiContactException("Mojang", "couldn't stalk " + EnumChatFormatting.DARK_RED + playerName);
+ } else if (stalkedPlayer.equals(Friend.FRIEND_NOT_FOUND)) {
+ throw new PlayerNotFoundException("There is no player with the name " + EnumChatFormatting.DARK_RED + playerName + EnumChatFormatting.RED + ".");
+ } else {
+ // ... then stalk the player
+ stalkSkyBlockStats(stalkedPlayer);
+ }
+ });
+ }
+ }
+
+ private void stalkSkyBlockStats(Friend stalkedPlayer) {
+ ApiUtils.fetchSkyBlockStats(stalkedPlayer, hySBStalking -> {
+ if (hySBStalking != null && hySBStalking.isSuccess()) {
+ HySkyBlockStats.Profile activeProfile = hySBStalking.getActiveProfile(stalkedPlayer.getUuid());
+
+ if (activeProfile == null) {
+ throw new MooCommandException("Looks like " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + " hasn't played SkyBlock yet.");
+ }
+
+ String highestSkill = null;
+ int highestLevel = -1;
+
+ MooChatComponent skillLevels = new MooChatComponent("Skill levels:").gold();
+ HySkyBlockStats.Profile.Member member = activeProfile.getMember(stalkedPlayer.getUuid());
+ int skillLevelsSum = 0;
+ for (Map.Entry<XpTables.Skill, Integer> entry : member.getSkills().entrySet()) {
+ String skill = Utils.fancyCase(entry.getKey().name());
+ int level = entry.getValue();
+ String skillLevel = MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(level) : String.valueOf(level);
+ skillLevels.appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent(skill, skillLevel));
+
+ if (level > highestLevel) {
+ highestSkill = skill;
+ highestLevel = level;
+ }
+ if (!skill.equals("Carpentry") && !skill.equals("Runecrafting")) {
+ skillLevelsSum += level;
+ }
+ }
+
+ // output inspired by /profiles hover
+
+ // coins:
+ String coinsBankAndPurse = (activeProfile.getCoinBank() >= 0) ? Utils.formatNumberWithAbbreviations(activeProfile.getCoinBank() + member.getCoinPurse()) : "API access disabled";
+ Pair<String, String> fancyFirstJoined = member.getFancyFirstJoined();
+
+ MooChatComponent wealthHover = new MooChatComponent("Accessible coins:").gold()
+ .appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Purse", Utils.formatNumberWithAbbreviations(member.getCoinPurse())))
+ .appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Bank", (activeProfile.getCoinBank() != -1) ? Utils.formatNumberWithAbbreviations(activeProfile.getCoinBank()) : "API access disabled"));
+ if (activeProfile.coopCount() > 0) {
+ wealthHover.appendFreshSibling(new ChatComponentText(" "));
+ wealthHover.appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Co-op members", String.valueOf(activeProfile.coopCount())));
+ wealthHover.appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Co-ops' purses sum", Utils.formatNumberWithAbbreviations(activeProfile.getCoopCoinPurses(stalkedPlayer.getUuid()))));
+ }
+
+ MooChatComponent sbStats = new MooChatComponent("SkyBlock stats of " + stalkedPlayer.getName() + " (" + activeProfile.getCuteName() + ")").gold().bold().setUrl("https://sky.lea.moe/stats/" + stalkedPlayer.getName() + "/" + activeProfile.getCuteName(), "Click to view SkyBlock stats on sky.lea.moe")
+ .appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Coins", coinsBankAndPurse).setHover(wealthHover));
+ // highest skill + skill average:
+ if (highestSkill != null) {
+ if (highestLevel == 0) {
+ sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Highest Skill", "All skills level 0"));
+ } else {
+ String highestSkillLevel = MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(highestLevel) : String.valueOf(highestLevel);
+ sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Highest Skill", highestSkill + " " + highestSkillLevel).setHover(skillLevels));
+ }
+ double skillAverage = XpTables.Skill.getSkillAverage(skillLevelsSum);
+ sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Skill average", String.format("%.1f", skillAverage))
+ .setHover(new MooChatComponent("Average skill level over all non-cosmetic skills\n(all except Carpentry and Runecrafting)").gray()));
+ } else {
+ sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Highest Skill", "API access disabled"));
+ }
+
+ // slayer levels:
+ StringBuilder slayerLevels = new StringBuilder();
+ StringBuilder slayerLevelsTooltip = new StringBuilder();
+ MooChatComponent slayerLevelsTooltipComponent = new MooChatComponent("Slayer bosses:").gold();
+ for (Map.Entry<XpTables.Slayer, Integer> entry : member.getSlayerLevels().entrySet()) {
+ String slayerBoss = Utils.fancyCase(entry.getKey().name());
+ if (slayerLevels.length() > 0) {
+ slayerLevels.append(EnumChatFormatting.GRAY).append(" | ").append(EnumChatFormatting.YELLOW);
+ slayerLevelsTooltip.append(EnumChatFormatting.DARK_GRAY).append(" | ").append(EnumChatFormatting.WHITE);
+ }
+ slayerLevelsTooltip.append(slayerBoss);
+ int level = entry.getValue();
+
+ String slayerLevel = (level > 0) ? (MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(level) : String.valueOf(level)) : "0";
+ slayerLevels.append(slayerLevel);
+ }
+ MooChatComponent slayerLevelsComponent = new MooChatComponent.KeyValueChatComponent("Slayer levels", slayerLevels.toString());
+ slayerLevelsComponent.setHover(slayerLevelsTooltipComponent.appendFreshSibling(new MooChatComponent(slayerLevelsTooltip.toString()).white()));
+ sbStats.appendFreshSibling(slayerLevelsComponent);
+
+ // pets:
+ Pet activePet = null;
+ Pet bestPet = null;
+ StringBuilder pets = new StringBuilder();
+ List<Pet> memberPets = member.getPets();
+ int showPetsLimit = Math.min(16, memberPets.size());
+ for (int i = 0; i < showPetsLimit; i++) {
+ Pet pet = memberPets.get(i);
+ if (pet.isActive()) {
+ activePet = pet;
+ } else {
+ if (activePet == null && bestPet == null && pets.length() == 0) {
+ // no active pet, display highest pet instead
+ bestPet = pet;
+ continue;
+ } else if (pets.length() > 0) {
+ pets.append("\n");
+ }
+ pets.append(pet.toFancyString());
+ }
+ }
+ int remainingPets = memberPets.size() - showPetsLimit;
+ if (remainingPets > 0 && pets.length() > 0) {
+ pets.append("\n").append(EnumChatFormatting.GRAY).append(" + ").append(remainingPets).append(" other pets");
+ }
+ MooChatComponent petsComponent = null;
+ if (activePet != null) {
+ petsComponent = new MooChatComponent.KeyValueChatComponent("Active Pet", activePet.toFancyString());
+ } else if (bestPet != null) {
+ petsComponent = new MooChatComponent.KeyValueChatComponent("Best Pet", bestPet.toFancyString());
+ }
+ if (pets.length() > 0 && petsComponent != null) {
+ petsComponent.setHover(new MooChatComponent("Other pets:").gold().bold().appendFreshSibling(new MooChatComponent(pets.toString())));
+ }
+ if (petsComponent == null) {
+ petsComponent = new MooChatComponent.KeyValueChatComponent("Pet", "none");
+ }
+ sbStats.appendFreshSibling(petsComponent);
+
+ // minions:
+ Pair<Integer, Integer> uniqueMinionsData = activeProfile.getUniqueMinions();
+ String uniqueMinions = String.valueOf(uniqueMinionsData.first());
+ String uniqueMinionsHoverText = null;
+ if (uniqueMinionsData.second() > activeProfile.coopCount()) {
+ // all players have their unique minions api access disabled
+ uniqueMinions = "API access disabled";
+ } else if (uniqueMinionsData.second() > 0) {
+ // at least one player has their unique minions api access disabled
+ uniqueMinions += EnumChatFormatting.GRAY + " or more";
+ uniqueMinionsHoverText = "" + EnumChatFormatting.WHITE + uniqueMinionsData.second() + " out of " + (activeProfile.coopCount() + 1) + EnumChatFormatting.GRAY + " Co-op members have disabled API access, so some unique minions may be missing";
+ }
+
+ MooChatComponent.KeyValueChatComponent uniqueMinionsComponent = new MooChatComponent.KeyValueChatComponent("Unique Minions", uniqueMinions);
+ if (uniqueMinionsHoverText != null) {
+ uniqueMinionsComponent.setHover(new MooChatComponent(uniqueMinionsHoverText).gray());
+ }
+ sbStats.appendFreshSibling(uniqueMinionsComponent);
+ // fairy souls:
+ sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Fairy Souls", (member.getFairySoulsCollected() >= 0) ? String.valueOf(member.getFairySoulsCollected()) : "API access disabled"));
+ // profile age:
+ sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Profile age", fancyFirstJoined.first()).setHover(new MooChatComponent.KeyValueTooltipComponent("Join date", fancyFirstJoined.second())));
+
+ main.getChatHelper().sendMessage(sbStats);
+ } else {
+ String cause = (hySBStalking != null) ? hySBStalking.getCause() : null;
+ throw new ApiContactException("Hypixel", "couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + (cause != null ? " (Reason: " + EnumChatFormatting.DARK_RED + cause + EnumChatFormatting.RED + ")" : "") + ".");
+ }
+ });
+ }
+
+ private void handleBestFriendAdd(String[] args) throws CommandException {
+ if (args.length != 2) {
+ throw new WrongUsageException("/" + getCommandName() + " add <playerName>");
+ } else if (!Utils.isValidMcName(args[1])) {
+ throw new InvalidPlayerNameException(args[1]);
+ } else if (main.getFriendsHandler().isBestFriend(args[1], true)) {
+ throw new MooCommandException(EnumChatFormatting.DARK_RED + args[1] + EnumChatFormatting.RED + " is a best friend already.");
+ } else {
+ // TODO Add check if 'best friend' is on normal friend list
+ main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "Fetching " + EnumChatFormatting.YELLOW + args[1] + EnumChatFormatting.GOLD + "'s unique user id. This may take a few seconds...");
+ // add friend async
+ main.getFriendsHandler().addBestFriend(args[1]);
+ }
+ }
+
+ private void handleBestFriendRemove(String[] args) throws CommandException {
+ if (args.length != 2) {
+ throw new WrongUsageException("/" + getCommandName() + " remove <playerName>");
+ } else if (!Utils.isValidMcName(args[1])) {
+ throw new InvalidPlayerNameException(args[1]);
+ }
+ String username = args[1];
+ boolean removed = main.getFriendsHandler().removeBestFriend(username);
+ if (removed) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Removed " + EnumChatFormatting.DARK_GREEN + username + EnumChatFormatting.GREEN + " from best friends list.");
+ } else {
+ throw new MooCommandException(EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " isn't a best friend.");
+ }
+ }
+
+ private void handleListBestFriends() {
+ Set<String> bestFriends = main.getFriendsHandler().getBestFriends();
+
+ // TODO show fancy gui with list of best friends; maybe with buttons to delete them
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u279C Best friends: " + ((bestFriends.isEmpty())
+ ? EnumChatFormatting.ITALIC + "none :c"
+ : EnumChatFormatting.DARK_GREEN + String.join(EnumChatFormatting.GREEN + ", " + EnumChatFormatting.DARK_GREEN, bestFriends)));
+ }
+
+ private void handleNameChangeCheck(String[] args) throws CommandException {
+ if (args.length != 2) {
+ throw new WrongUsageException("/" + getCommandName() + " nameChangeCheck <playerName>");
+ } else if (!Utils.isValidMcName(args[1])) {
+ throw new InvalidPlayerNameException(args[1]);
+ }
+ Friend bestFriend = main.getFriendsHandler().getBestFriend(args[1]);
+ if (bestFriend.equals(Friend.FRIEND_NOT_FOUND)) {
+ throw new MooCommandException(EnumChatFormatting.DARK_RED + args[1] + EnumChatFormatting.RED + " isn't a best friend.");
+ } else {
+ main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "Checking if " + bestFriend.getName() + " changed their name... This will take a few seconds...");
+ // check for name change async
+ main.getFriendsHandler().updateBestFriend(bestFriend, true);
+ }
+ }
+
+ @Override
+ public String getCommandName() {
+ return "moo";
+ }
+
+ @Override
+ public List<String> getCommandAliases() {
+ return Collections.singletonList("m");
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return "/" + getCommandName() + " help";
+ }
+
+ private void sendCommandUsage(ICommandSender sender) {
+ IChatComponent usage = new MooChatComponent("\u279C " + Cowlection.MODNAME + " commands:").gold().bold()
+ .appendSibling(createCmdHelpSection(1, "Friends & other players"))
+ .appendSibling(createCmdHelpEntry("stalk", "Get info of player's status"))
+ .appendSibling(createCmdHelpEntry("stalkskyblock", "Get info of player's SkyBlock stats"))
+ .appendSibling(createCmdHelpEntry("analyzeIsland", "Analyze a SkyBlock private island"))
+ .appendSibling(createCmdHelpEntry("deaths", "SkyBlock Dungeons: death counts"))
+ .appendSibling(createCmdHelpEntry("add", "Add best friends"))
+ .appendSibling(createCmdHelpEntry("remove", "Remove best friends"))
+ .appendSibling(createCmdHelpEntry("list", "View list of best friends"))
+ .appendSibling(createCmdHelpEntry("nameChangeCheck", "Force a scan for a changed name of a best friend"))
+ .appendSibling(createCmdHelpEntry("toggle", "Toggle join/leave notifications"))
+ .appendSibling(createCmdHelpSection(2, "Miscellaneous"))
+ .appendSibling(createCmdHelpEntry("config", "Open mod's configuration"))
+ .appendSibling(createCmdHelpEntry("search", "Open Minecraft log search"))
+ .appendSibling(createCmdHelpEntry("guiScale", "Change GUI scale"))
+ .appendSibling(createCmdHelpEntry("rr", "Alias for /r without auto-replacement to /msg"))
+ .appendSibling(createCmdHelpEntry("shrug", "\u00AF\\_(\u30C4)_/\u00AF")) // ¯\_(ツ)_/¯
+ .appendSibling(createCmdHelpSection(3, "Update mod"))
+ .appendSibling(createCmdHelpEntry("update", "Check for new mod updates"))
+ .appendSibling(createCmdHelpEntry("updateHelp", "Show mod update instructions"))
+ .appendSibling(createCmdHelpEntry("version", "View results of last mod update check"))
+ .appendSibling(createCmdHelpEntry("directory", "Open Minecraft's mods directory"));
+ sender.addChatMessage(usage);
+ }
+
+ private IChatComponent createCmdHelpSection(int nr, String title) {
+ String prefix = Character.toString((char) (0x2789 + nr));
+ return new ChatComponentText("\n").appendSibling(new ChatComponentText(prefix + " " + title).setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true)));
+ }
+
+ private IChatComponent createCmdHelpEntry(String cmd, String usage) {
+ String command = "/" + this.getCommandName() + " " + cmd;
+
+ return new MooChatComponent("\n").reset().appendSibling(new MooChatComponent.KeyValueChatComponent(command, usage, " \u27A1 ").setSuggestCommand(command));
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+
+ @Override
+ public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) {
+ if (args.length == 1) {
+ return getListOfStringsMatchingLastWord(args,
+ /* friends & other players */ "stalk", "askPolitelyWhereTheyAre", "stalkskyblock", "skyblockstalk", "askPolitelyAboutTheirSkyBlockProgress", "analyzeIsland", "deaths", "add", "remove", "list", "nameChangeCheck", "toggle",
+ /* miscellaneous */ "config", "search", "guiscale", "rr", "shrug", "apikey",
+ /* update mod */ "update", "updateHelp", "version", "directory",
+ /* help */ "help");
+ } else if (args.length == 2 && args[0].equalsIgnoreCase("remove")) {
+ return getListOfStringsMatchingLastWord(args, main.getFriendsHandler().getBestFriends());
+ } else if (args.length == 2 && args[0].toLowerCase().contains("stalk")) { // stalk & stalkskyblock
+ return getListOfStringsMatchingLastWord(args, main.getPlayerCache().getAllNamesSorted());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/ReplyCommand.java b/src/main/java/de/cowtipper/cowlection/command/ReplyCommand.java
new file mode 100644
index 0000000..e37ce85
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/ReplyCommand.java
@@ -0,0 +1,35 @@
+package de.cowtipper.cowlection.command;
+
+import de.cowtipper.cowlection.Cowlection;
+import net.minecraft.client.Minecraft;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.CommandException;
+import net.minecraft.command.ICommandSender;
+
+public class ReplyCommand extends CommandBase {
+ private final Cowlection main;
+
+ public ReplyCommand(Cowlection main) {
+ this.main = main;
+ }
+
+ @Override
+ public String getCommandName() {
+ return "rr";
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return "/rr <message>";
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ Minecraft.getMinecraft().thePlayer.sendChatMessage("/r " + CommandBase.buildString(args, 0));
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/ShrugCommand.java b/src/main/java/de/cowtipper/cowlection/command/ShrugCommand.java
new file mode 100644
index 0000000..264e19d
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/ShrugCommand.java
@@ -0,0 +1,34 @@
+package de.cowtipper.cowlection.command;
+
+import de.cowtipper.cowlection.Cowlection;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.CommandException;
+import net.minecraft.command.ICommandSender;
+
+public class ShrugCommand extends CommandBase {
+ private final Cowlection main;
+
+ public ShrugCommand(Cowlection main) {
+ this.main = main;
+ }
+
+ @Override
+ public String getCommandName() {
+ return "shrug";
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return "/shrug [message]";
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ main.getChatHelper().sendShrug(args);
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/TabCompletableCommand.java b/src/main/java/de/cowtipper/cowlection/command/TabCompletableCommand.java
new file mode 100644
index 0000000..5edabe8
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/TabCompletableCommand.java
@@ -0,0 +1,53 @@
+package de.cowtipper.cowlection.command;
+
+import de.cowtipper.cowlection.Cowlection;
+import net.minecraft.client.Minecraft;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.CommandException;
+import net.minecraft.command.ICommandSender;
+import net.minecraft.util.BlockPos;
+
+import java.util.List;
+
+/**
+ * This is not a real command. Its sole purpose is to add tab completion for usernames to server-side commands that do not provide tab completion for usernames by default.
+ */
+public class TabCompletableCommand extends CommandBase {
+ private final Cowlection main;
+ private final String cmdName;
+
+ public TabCompletableCommand(Cowlection main, String cmdName) {
+ this.main = main;
+ this.cmdName = cmdName;
+ }
+
+ @Override
+ public String getCommandName() {
+ return cmdName;
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return null;
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ // send client-command to server
+ Minecraft.getMinecraft().thePlayer.sendChatMessage("/" + getCommandName() + " " + CommandBase.buildString(args, 0));
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+
+ @Override
+ public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) {
+ if (args.length == 1 || args.length == 2) {
+ // suggest recently 'seen' usernames as tab-completion options.
+ return getListOfStringsMatchingLastWord(args, main.getPlayerCache().getAllNamesSorted());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/exception/ApiContactException.java b/src/main/java/de/cowtipper/cowlection/command/exception/ApiContactException.java
new file mode 100644
index 0000000..09a04a0
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/exception/ApiContactException.java
@@ -0,0 +1,7 @@
+package de.cowtipper.cowlection.command.exception;
+
+public class ApiContactException extends MooCommandException {
+ public ApiContactException(String api, String failedAction) {
+ super("Sorry, couldn't contact the " + api + " API and thus " + failedAction);
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/exception/InvalidPlayerNameException.java b/src/main/java/de/cowtipper/cowlection/command/exception/InvalidPlayerNameException.java
new file mode 100644
index 0000000..cfaa54b
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/exception/InvalidPlayerNameException.java
@@ -0,0 +1,10 @@
+package de.cowtipper.cowlection.command.exception;
+
+import net.minecraft.command.SyntaxErrorException;
+import net.minecraft.util.EnumChatFormatting;
+
+public class InvalidPlayerNameException extends SyntaxErrorException {
+ public InvalidPlayerNameException(String playerName) {
+ super(EnumChatFormatting.DARK_RED + playerName + EnumChatFormatting.RED + "? This... doesn't look like a valid username.");
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/exception/MooCommandException.java b/src/main/java/de/cowtipper/cowlection/command/exception/MooCommandException.java
new file mode 100644
index 0000000..f16e488
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/exception/MooCommandException.java
@@ -0,0 +1,9 @@
+package de.cowtipper.cowlection.command.exception;
+
+import net.minecraft.command.CommandException;
+
+public class MooCommandException extends CommandException {
+ public MooCommandException(String msg) {
+ super("cowlection.commands.generic.exception", msg);
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/command/exception/ThrowingConsumer.java b/src/main/java/de/cowtipper/cowlection/command/exception/ThrowingConsumer.java
new file mode 100644
index 0000000..0efd9c5
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/command/exception/ThrowingConsumer.java
@@ -0,0 +1,25 @@
+package de.cowtipper.cowlection.command.exception;
+
+import de.cowtipper.cowlection.Cowlection;
+import net.minecraft.command.CommandException;
+import net.minecraft.util.ChatComponentTranslation;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+
+import java.util.function.Consumer;
+
+@FunctionalInterface
+public interface ThrowingConsumer<T> extends Consumer<T> {
+ @Override
+ default void accept(T t) {
+ try {
+ acceptThrows(t);
+ } catch (CommandException e) {
+ IChatComponent errorMsg = new ChatComponentTranslation(e.getMessage(), e.getErrorObjects());
+ errorMsg.getChatStyle().setColor(EnumChatFormatting.RED);
+ Cowlection.getInstance().getChatHelper().sendMessage(errorMsg);
+ }
+ }
+
+ void acceptThrows(T t) throws CommandException;
+}
diff --git a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java
new file mode 100644
index 0000000..84cbc1f
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java
@@ -0,0 +1,306 @@
+package de.cowtipper.cowlection.config;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.util.Utils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.Util;
+import net.minecraftforge.common.ForgeModContainer;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.common.config.Property;
+import net.minecraftforge.fml.client.FMLConfigGuiFactory;
+import net.minecraftforge.fml.client.event.ConfigChangedEvent;
+import net.minecraftforge.fml.common.eventhandler.EventPriority;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+
+import java.io.File;
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Mod configuration via ingame gui
+ * <p>
+ * Based on <a href="https://github.com/TheGreyGhost/MinecraftByExample/blob/1-8-9final/src/main/java/minecraftbyexample/mbe70_configuration/MBEConfiguration.java">TheGreyGhost's MinecraftByExample</a>
+ *
+ * @see ForgeModContainer
+ * @see FMLConfigGuiFactory
+ */
+public class MooConfig {
+ static final String CATEGORY_LOGS_SEARCH = "logssearch";
+ // main config
+ public static boolean doUpdateCheck;
+ public static boolean showBestFriendNotifications;
+ public static boolean showFriendNotifications;
+ public static boolean showGuildNotifications;
+ public static boolean showAdvancedTooltips;
+ public static String[] tabCompletableNamesCommands;
+ private static String numeralSystem;
+ // SkyBlock dungeon
+ public static int[] dungClassRange;
+ public static boolean dungFilterPartiesWithDupes;
+ public static String dungItemQualityPos;
+ // logs search config
+ public static String[] logsDirs;
+ private static String defaultStartDate;
+ // other stuff
+ public static String moo;
+ private static Configuration cfg = null;
+ private final Cowlection main;
+ private List<String> propOrderGeneral;
+ private List<String> propOrderLogsSearch;
+
+ public MooConfig(Cowlection main, Configuration configuration) {
+ this.main = main;
+ cfg = configuration;
+ initConfig();
+ }
+
+ static Configuration getConfig() {
+ return cfg;
+ }
+
+ private void initConfig() {
+ syncFromFile();
+ MinecraftForge.EVENT_BUS.register(new ConfigEventHandler());
+ }
+
+ /**
+ * Load the configuration values from the configuration file
+ */
+ private void syncFromFile() {
+ syncConfig(true, true);
+ }
+
+ /**
+ * Save the GUI-altered values to disk
+ */
+ private void syncFromGui() {
+ syncConfig(false, true);
+ }
+
+ /**
+ * Save the Configuration variables (fields) to disk
+ */
+ public void syncFromFields() {
+ syncConfig(false, false);
+ }
+
+ public static LocalDate calculateStartDate() {
+ try {
+ // date format: yyyy-mm-dd
+ return LocalDate.parse(defaultStartDate);
+ } catch (DateTimeParseException e) {
+ // fallthrough
+ }
+ try {
+ int months = Integer.parseInt(defaultStartDate);
+ return LocalDate.now().minus(months, ChronoUnit.MONTHS);
+ } catch (NumberFormatException e) {
+ // default: 1 month
+ return LocalDate.now().minus(1, ChronoUnit.MONTHS);
+ }
+ }
+
+ /**
+ * Synchronise the three copies of the data
+ * 1) loadConfigFromFile && readFieldsFromConfig -> initialise everything from the disk file
+ * 2) !loadConfigFromFile && readFieldsFromConfig -> copy everything from the config file (altered by GUI)
+ * 3) !loadConfigFromFile && !readFieldsFromConfig -> copy everything from the native fields
+ *
+ * @param loadConfigFromFile if true, load the config field from the configuration file on disk
+ * @param readFieldsFromConfig if true, reload the member variables from the config field
+ */
+ private void syncConfig(boolean loadConfigFromFile, boolean readFieldsFromConfig) {
+ if (loadConfigFromFile) {
+ cfg.load();
+ }
+
+ // config section: main configuration
+ propOrderGeneral = new ArrayList<>();
+
+ Property propDoUpdateCheck = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "doUpdateCheck", true, "Check for mod updates?"), true);
+ Property propShowBestFriendNotifications = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "showBestFriendNotifications", true, "Set to true to receive best friends' login/logout messages, set to false hide them."), true);
+ Property propShowFriendNotifications = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "showFriendNotifications", true, "Set to true to receive friends' login/logout messages, set to false hide them."), true);
+ Property propShowGuildNotifications = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "showGuildNotifications", true, "Set to true to receive guild members' login/logout messages, set to false hide them."), true);
+ Property propShowAdvancedTooltips = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "showAdvancedTooltips", true, "Set to true to show advanced tooltips, set to false show default tooltips."), true);
+ Property propNumeralSystem = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "numeralSystem", "Arabic numerals: 1, 4, 10", "Use Roman or Arabic numeral system?", new String[]{"Arabic numerals: 1, 4, 10", "Roman numerals: I, IV, X"}), true);
+ Property propTabCompletableNamesCommands = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "tabCompletableNamesCommands", new String[]{"party", "p", "invite", "visit", "ah", "ignore", "msg", "tell", "w", "boop", "profile", "friend", "friends", "f"}, "List of commands with a Tab-completable username argument."), true)
+ .setValidationPattern(Pattern.compile("^[A-Za-z]+$"));
+ Property propMoo = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "moo", "", "The answer to life the universe and everything. Don't edit this entry manually!", Utils.VALID_UUID_PATTERN), false);
+
+ // SkyBlock dungeon
+ Property propDungClassRange = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "dungClassRange", new int[]{-1, -1}, "Accepted level range for the dungeon party finder. Set to -1 to disable"), true)
+ .setMinValue(-1).setIsListLengthFixed(true);
+ Property propDungFilterPartiesWithDupes = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "dungFilterPartiesWithDupes", false, "Mark parties with duplicated classes?"), true);
+ Property propDungItemQualityPos = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT,
+ "dungItemQualityPos", "top", "Position of item quality in tooltip", new String[]{"top", "bottom"}), true);
+
+ cfg.setCategoryPropertyOrder(Configuration.CATEGORY_CLIENT, propOrderGeneral);
+
+ // config section: log files search
+ propOrderLogsSearch = new ArrayList<>();
+
+ Property propLogsDirs = addConfigEntry(cfg.get(CATEGORY_LOGS_SEARCH,
+ "logsDirs", resolveDefaultLogsDirs(),
+ "Directories with Minecraft log files"), true, CATEGORY_LOGS_SEARCH);
+ Property propDefaultStartDate = addConfigEntry(cfg.get(CATEGORY_LOGS_SEARCH,
+ "defaultStartDate", "3", "Default start date (a number means X months ago, alternatively a fixed date à la yyyy-mm-dd can be used)"), true)
+ .setValidationPattern(Pattern.compile("^[1-9][0-9]{0,2}|(2[0-9]{3}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))$"));
+
+ cfg.setCategoryPropertyOrder(CATEGORY_LOGS_SEARCH, propOrderLogsSearch);
+
+ // 'manual' replacement for propTabCompletableNamesCommands.hasChanged()
+ boolean modifiedTabCompletableCommandsList = false;
+ String[] tabCompletableCommandsPreChange = tabCompletableNamesCommands != null ? tabCompletableNamesCommands.clone() : null;
+ if (readFieldsFromConfig) {
+ // main config
+ doUpdateCheck = propDoUpdateCheck.getBoolean();
+ showBestFriendNotifications = propShowBestFriendNotifications.getBoolean();
+ showFriendNotifications = propShowFriendNotifications.getBoolean();
+ showGuildNotifications = propShowGuildNotifications.getBoolean();
+ showAdvancedTooltips = propShowAdvancedTooltips.getBoolean();
+ numeralSystem = propNumeralSystem.getString();
+ tabCompletableNamesCommands = propTabCompletableNamesCommands.getStringList();
+ moo = propMoo.getString();
+
+ // SkyBlock dungeon
+ dungClassRange = propDungClassRange.getIntList();
+ dungFilterPartiesWithDupes = propDungFilterPartiesWithDupes.getBoolean();
+ dungItemQualityPos = propDungItemQualityPos.getString();
+
+ // logs search config
+ logsDirs = propLogsDirs.getStringList();
+ defaultStartDate = propDefaultStartDate.getString().trim();
+
+ if (!Arrays.equals(tabCompletableCommandsPreChange, tabCompletableNamesCommands)) {
+ modifiedTabCompletableCommandsList = true;
+ }
+ }
+
+ // main config
+ propDoUpdateCheck.set(doUpdateCheck);
+ propShowBestFriendNotifications.set(showBestFriendNotifications);
+ propShowFriendNotifications.set(showFriendNotifications);
+ propShowGuildNotifications.set(showGuildNotifications);
+ propShowAdvancedTooltips.set(showAdvancedTooltips);
+ propNumeralSystem.set(numeralSystem);
+ propTabCompletableNamesCommands.set(tabCompletableNamesCommands);
+ propMoo.set(moo);
+
+ // SkyBlock dungeon
+ propDungClassRange.set(dungClassRange);
+ propDungFilterPartiesWithDupes.set(dungFilterPartiesWithDupes);
+ propDungItemQualityPos.set(dungItemQualityPos);
+
+ // logs search config
+ propLogsDirs.set(logsDirs);
+ propDefaultStartDate.set(defaultStartDate);
+
+ if (cfg.hasChanged()) {
+ if (Minecraft.getMinecraft().thePlayer != null) {
+ if (modifiedTabCompletableCommandsList) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Added or removed commands with tab-completable usernames take effect after a game restart!");
+ }
+ if (dungClassRange[0] > -1 && dungClassRange[1] > -1 && dungClassRange[0] > dungClassRange[1]) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Dungeon class range minimum value cannot be higher than the maximum value.");
+ }
+ }
+ cfg.save();
+ }
+ }
+
+ private Property addConfigEntry(Property property, boolean showInGui, String category) {
+ if (showInGui) {
+ property.setLanguageKey(Cowlection.MODID + ".config." + property.getName());
+ } else {
+ property.setShowInGui(false);
+ }
+
+ if (CATEGORY_LOGS_SEARCH.equals(category)) {
+ propOrderLogsSearch.add(property.getName());
+ } else {
+ // == Configuration.CATEGORY_CLIENT:
+ propOrderGeneral.add(property.getName());
+ }
+ return property;
+ }
+
+ private Property addConfigEntry(Property property, boolean showInGui) {
+ return addConfigEntry(property, showInGui, Configuration.CATEGORY_CLIENT);
+ }
+
+ /**
+ * Tries to find/resolve default directories containing minecraft logfiles (in .log.gz format)
+ *
+ * @return list of /logs/ directories
+ */
+ private String[] resolveDefaultLogsDirs() {
+ List<String> logsDirs = new ArrayList<>();
+ File currentMcLogsDirFile = new File(Minecraft.getMinecraft().mcDataDir, "logs");
+ if (currentMcLogsDirFile.exists() && currentMcLogsDirFile.isDirectory()) {
+ String currentMcLogsDir = Utils.toRealPath(currentMcLogsDirFile);
+ logsDirs.add(currentMcLogsDir);
+ }
+
+ String defaultMcLogsDir = System.getProperty("user.home");
+ Util.EnumOS osType = Util.getOSType();
+ // default directories for .minecraft: https://minecraft.gamepedia.com/.minecraft
+ switch (osType) {
+ case WINDOWS:
+ defaultMcLogsDir += "\\AppData\\Roaming\\.minecraft\\logs";
+ break;
+ case OSX:
+ defaultMcLogsDir += "/Library/Application Support/minecraft/logs";
+ break;
+ default:
+ defaultMcLogsDir += "/.minecraft/logs";
+ }
+ File defaultMcLogsDirFile = new File(defaultMcLogsDir);
+ if (defaultMcLogsDirFile.exists() && defaultMcLogsDirFile.isDirectory() && !currentMcLogsDirFile.equals(defaultMcLogsDirFile)) {
+ logsDirs.add(Utils.toRealPath(defaultMcLogsDirFile));
+ }
+ return logsDirs.toArray(new String[]{});
+ }
+
+ /**
+ * Should login/logout notifications be modified and thus monitored?
+ *
+ * @return true if notifications should be monitored
+ */
+ public static boolean doMonitorNotifications() {
+ return showBestFriendNotifications || !showFriendNotifications || !showGuildNotifications;
+ }
+
+ public static boolean useRomanNumerals() {
+ return numeralSystem.startsWith("Roman");
+ }
+
+ public static boolean isDungItemQualityAtTop() {
+ return dungItemQualityPos.equals("top");
+ }
+
+ public class ConfigEventHandler {
+ @SubscribeEvent(priority = EventPriority.NORMAL)
+ public void onEvent(ConfigChangedEvent.OnConfigChangedEvent e) {
+ if (Cowlection.MODID.equals(e.modID)) {
+ syncFromGui();
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/config/MooGuiConfig.java b/src/main/java/de/cowtipper/cowlection/config/MooGuiConfig.java
new file mode 100644
index 0000000..3c650b8
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/config/MooGuiConfig.java
@@ -0,0 +1,86 @@
+package de.cowtipper.cowlection.config;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.search.GuiTooltip;
+import de.cowtipper.cowlection.util.Utils;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraft.client.gui.GuiTextField;
+import net.minecraft.client.resources.I18n;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraftforge.common.config.ConfigElement;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.fml.client.config.GuiConfig;
+import net.minecraftforge.fml.client.config.GuiConfigEntries;
+import net.minecraftforge.fml.client.config.IConfigElement;
+import org.apache.commons.lang3.reflect.FieldUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MooGuiConfig extends GuiConfig {
+ private GuiTooltip defaultStartDateTooltip;
+ private GuiTextField textFieldDefaultStartDate;
+ private String defaultStartDateTooltipText;
+
+ public MooGuiConfig(GuiScreen parent) {
+ super(parent,
+ getConfigElements(),
+ Cowlection.MODID,
+ false,
+ false,
+ EnumChatFormatting.BOLD + "Configuration for " + Cowlection.MODNAME);
+ titleLine2 = EnumChatFormatting.GRAY + Utils.toRealPath(MooConfig.getConfig().getConfigFile());
+ }
+
+ private static List<IConfigElement> getConfigElements() {
+ List<IConfigElement> list = new ArrayList<>(new ConfigElement(MooConfig.getConfig().getCategory(Configuration.CATEGORY_CLIENT)).getChildElements());
+ list.addAll(new ConfigElement(MooConfig.getConfig().getCategory(MooConfig.CATEGORY_LOGS_SEARCH)).getChildElements());
+ return list;
+ }
+
+ @Override
+ public void initGui() {
+ super.initGui();
+ // optional: add buttons and initialize fields
+ for (GuiConfigEntries.IConfigEntry configEntry : entryList.listEntries) {
+ if ("defaultStartDate".equals(configEntry.getName()) && configEntry instanceof GuiConfigEntries.StringEntry) {
+ GuiConfigEntries.StringEntry entry = (GuiConfigEntries.StringEntry) configEntry;
+ defaultStartDateTooltipText = I18n.format(configEntry.getConfigElement().getLanguageKey() + ".tooltip");
+ try {
+ textFieldDefaultStartDate = (GuiTextField) FieldUtils.readField(entry, "textFieldValue", true);
+ defaultStartDateTooltip = null;
+ } catch (IllegalAccessException e) {
+ // wasn't able to access textField, abort drawing tooltip
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
+ super.drawScreen(mouseX, mouseY, partialTicks);
+ // optional: create animations, draw additional elements, etc.
+
+ // add tooltip to defaultStartDate textField
+ if (textFieldDefaultStartDate != null) {
+ if (defaultStartDateTooltip == null) {
+ if (textFieldDefaultStartDate.yPosition == 0) {
+ return;
+ }
+ // create GuiTooltip here instead in initGui because y-position of textField is 0 inside initGui
+ defaultStartDateTooltip = new GuiTooltip(textFieldDefaultStartDate, Arrays.asList(defaultStartDateTooltipText.split("\\\\n")));
+ } else if (defaultStartDateTooltip.checkHover(mouseX, mouseY)) {
+ drawHoveringText(defaultStartDateTooltip.getText(), mouseX, mouseY, fontRendererObj);
+ }
+ }
+ }
+
+ @Override
+ protected void actionPerformed(GuiButton button) {
+ super.actionPerformed(button);
+ // optional: process any additional buttons added in initGui
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/config/MooGuiFactory.java b/src/main/java/de/cowtipper/cowlection/config/MooGuiFactory.java
new file mode 100644
index 0000000..eedd676
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/config/MooGuiFactory.java
@@ -0,0 +1,29 @@
+package de.cowtipper.cowlection.config;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraftforge.fml.client.IModGuiFactory;
+
+import java.util.Set;
+
+public class MooGuiFactory implements IModGuiFactory {
+ @Override
+ public void initialize(Minecraft minecraftInstance) {
+
+ }
+
+ @Override
+ public Class<? extends GuiScreen> mainConfigGuiClass() {
+ return MooGuiConfig.class;
+ }
+
+ @Override
+ public Set<RuntimeOptionCategoryElement> runtimeGuiCategories() {
+ return null;
+ }
+
+ @Override
+ public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) {
+ return null;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/data/DataHelper.java b/src/main/java/de/cowtipper/cowlection/data/DataHelper.java
new file mode 100644
index 0000000..9af613f
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/data/DataHelper.java
@@ -0,0 +1,723 @@
+package de.cowtipper.cowlection.data;
+
+import de.cowtipper.cowlection.util.Utils;
+import net.minecraft.util.EnumChatFormatting;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class DataHelper {
+ private DataHelper() {
+ }
+
+ public enum SkyBlockRarity {
+ COMMON(EnumChatFormatting.WHITE),
+ UNCOMMON(EnumChatFormatting.GREEN),
+ RARE(EnumChatFormatting.BLUE),
+ EPIC(EnumChatFormatting.DARK_PURPLE),
+ LEGENDARY(EnumChatFormatting.GOLD),
+ MYTHIC(EnumChatFormatting.LIGHT_PURPLE),
+ SPECIAL(EnumChatFormatting.RED),
+ VERY_SPECIAL(EnumChatFormatting.RED);
+
+ public final EnumChatFormatting rarityColor;
+
+ SkyBlockRarity(EnumChatFormatting color) {
+ this.rarityColor = color;
+ }
+
+ public static SkyBlockRarity[] getPetRarities() {
+ return Arrays.stream(values(), 0, 5).toArray(SkyBlockRarity[]::new);
+ }
+
+ public EnumChatFormatting getColor() {
+ return rarityColor;
+ }
+ }
+
+ // TODO replace with api request: https://github.com/HypixelDev/PublicAPI/blob/master/Documentation/misc/GameType.md
+ public enum GameType {
+ QUAKECRAFT("Quakecraft"),
+ WALLS("Walls"),
+ PAINTBALL("Paintball"),
+ SURVIVAL_GAMES("Blitz Survival Games"),
+ TNTGAMES("The TNT Games"),
+ VAMPIREZ("VampireZ"),
+ WALLS3("Mega Walls"),
+ ARCADE("Arcade"),
+ ARENA("Arena Brawl"),
+ UHC("UHC Champions"),
+ MCGO("Cops and Crims"),
+ BATTLEGROUND("Warlords"),
+ SUPER_SMASH("Smash Heroes"),
+ GINGERBREAD("Turbo Kart Racers"),
+ HOUSING("Housing"),
+ SKYWARS("SkyWars"),
+ TRUE_COMBAT("Crazy Walls"),
+ SPEED_UHC("Speed UHC"),
+ SKYCLASH("SkyClash"),
+ LEGACY("Classic Games"),
+ PROTOTYPE("Prototype"),
+ BEDWARS("Bed Wars"),
+ MURDER_MYSTERY("Murder Mystery"),
+ BUILD_BATTLE("Build Battle"),
+ DUELS("Duels"),
+ SKYBLOCK("SkyBlock"),
+ PIT("Pit");
+
+ private final String cleanName;
+
+ GameType(String cleanName) {
+ this.cleanName = cleanName;
+ }
+
+ public static String getFancyName(String gameName) {
+ if (gameName == null) {
+ return null;
+ }
+ String cleanGameType;
+ try {
+ cleanGameType = valueOf(gameName).getCleanName();
+ } catch (IllegalArgumentException e) {
+ // no matching game type found
+ cleanGameType = Utils.fancyCase(gameName);
+ }
+ return cleanGameType;
+ }
+
+ public String getCleanName() {
+ return cleanName;
+ }
+ }
+
+ public static Map<String, String> getMinions() {
+ // key = skin id, value = minion type and tier
+ Map<String, String> minions = new HashMap<>();
+ // TODO currently Fishing VI + VII and Creeper V + VI each use the same skull texture (server-side) - thus can't be distinguished
+ minions.put("2f93289a82bd2a06cbbe61b733cfdc1f1bd93c4340f7a90abd9bdda774109071", "Cobblestone I");
+ minions.put("3fd87486dc94cb8cd04a3d7d06f191f027f38dad7b4ed34c6681fb4d08834c06", "Cobblestone II");
+ minions.put("cc088ed6bb8763af4eb7d006e00fda7dc11d7681e97c983b7011c3e872f6aab9", "Cobblestone III");
+ minions.put("39514fee95d702625b974f1730fd62e567c5934997f73bae7e07ab52ddf9066e", "Cobblestone IV");
+ minions.put("3e2467b8ccaf007d03a9bb7c22d6a61397ca1bb284f128d5ccd138ad09124e68", "Cobblestone V");
+ minions.put("f4e01f552549037ae8887570700e74db20c6f026a650aeec5d9c8ec51ba3f515", "Cobblestone VI");
+ minions.put("51616e63be0ff341f70862e0049812fa0c27b39a2e77058dd8bfc386375e1d16", "Cobblestone VII");
+ minions.put("ea53e3c9f446a77e8c59df305a410a8accb751c002a41e55a1018ce1b3114690", "Cobblestone VIII");
+ minions.put("ccf546584428b5385bc0c1a0031aa87e98e85875e4d6104e1be06cef8bd74fe4", "Cobblestone IX");
+ minions.put("989db0a9c97f0e0b5bb9ec7b3e32f8d63c648d4608cfd5be9adbe8825d4e6a94", "Cobblestone X");
+ minions.put("ebcc099f3a00ece0e5c4b31d31c828e52b06348d0a4eac11f3fcbef3c05cb407", "Cobblestone XI");
+
+ minions.put("320c29ab966637cb9aecc34ee76d5a0130461e0c4fdb08cdaf80939fa1209102", "Obsidian I");
+ minions.put("58348315724fb1409142dda1cab2e45be34ead373d4a1ecdae6cb4143cd2bd25", "Obsidian II");
+ minions.put("c5c30c4800b25625ab51d4569437ad7f3e5f6465b51575512388b4c96ecbac90", "Obsidian III");
+ minions.put("1f417418f6df6efc7515ef31f4db570353d36ee87d46c6f87a7f9678b1f3ac57", "Obsidian IV");
+ minions.put("44d4ae42f0d6e82c7ebf9877303f9a84c96ce1978a8ac33681143f4b55a447ce", "Obsidian V");
+ minions.put("7c124351bd2da2312d261574fb578594c18720ac9c9d9edfdb57754b7340bd27", "Obsidian VI");
+ minions.put("db80b743fa6a8537c495ba7786ebefb3325e6013dc87d8c144ab902bbdb20f86", "Obsidian VII");
+ minions.put("745c8fc5ccb0bdbc19278c7e91ad6ac33d44f11fae46e1bfbfd1737ec1e420d4", "Obsidian VIII");
+ minions.put("15a45b66c8e21b515ea25abf47c9c27d995fe79b128844a0c8bf7777f3badee5", "Obsidian IX");
+ minions.put("1731be266b727b49ad135b4ea7b94843f7b322f873888da9fe037edea2984324", "Obsidian X");
+ minions.put("4d36910bcbb3fc0b7dedaae85ff052967ad74f3f4c2fb6f7dd2bed5bcfd0992b", "Obsidian XI");
+
+ minions.put("20f4d7c26b0310990a7d3a3b45948b95dd4ab407a16a4b6d3b7cb4fba031aeed", "Glowstone I");
+ minions.put("c0418cf84d91171f2cd67cbaf827c5b99ce4c1eeba76e77eab241e61e865a89f", "Glowstone II");
+ minions.put("7b21d7757c8ae382432b606b26ce7854f6c1555e668444ed0eecc2faab55a37d", "Glowstone III");
+ minions.put("3cc302e56b0474d5e428978704cd4a85de2f6c3e885a70f781e2838b551d5bfc", "Glowstone IV");
+ minions.put("ba8879a5be2d2cc75fcf468054046bc1eb9c61204a66f93991c9ff840a7c57cb", "Glowstone V");
+ minions.put("cd965a713f2e553c4c3ec047237b600b5ba0de9321a9c7dfe3d47b71d6afda41", "Glowstone VI");
+ minions.put("7f07e68f9985db6c905fe8f4f079137a6deef493413206d4ec90756245b4765e", "Glowstone VII");
+ minions.put("a8507f495bf89912dd2a317ae86faf8ce3631d62ca3d062e9fe5bf8d9d00fd70", "Glowstone VIII");
+ minions.put("b30d071e8c97a9c065b307d8a845ef8be6f6db85b71a2299f1bea0be062873e7", "Glowstone IX");
+ minions.put("8eeb870670e9408a78b386db6c2106e93f7c8cf03344b2cb3128ae0a4ea19674", "Glowstone X");
+ minions.put("8bc66c5eb7a197d959fcc5d45a7aff938e07ddcd42e3f3993bde00f56fe58dd1", "Glowstone XI");
+
+ minions.put("7458507ed31cf9a38986ac8795173c609637f03da653f30483a721d3fbe602d", "Gravel I");
+ minions.put("fb48c89157ae36038bbd9c88054ef8797f5b6f38631c1b57e58dcb8d701fa61d", "Gravel II");
+ minions.put("aae230c0ded51aa97c7964db885786f0c77f6244539b185ef4a5f2554199c785", "Gravel III");
+ minions.put("ef5b6973f41305d2b41aa82b94ef3b95e05e943e4cd4f793ca59278c46cbb985", "Gravel IV");
+ minions.put("c5961d126cda263759e43940c5665e9f1487ac2c7e26f903e5086affb3785714", "Gravel V");
+ minions.put("69c5f0583967589650b0de2c5108811ff01c32ac9861a820bba650f0412126d6", "Gravel VI");
+ minions.put("d092f7535b5d091cc3d3f0a343be5d46f16466ae9344b0cac452f3435f00996a", "Gravel VII");
+ minions.put("7117a2f4cf83c41a8dfb9c7a8238ca06bbdb5540a1e91e8721df5476b70f6e74", "Gravel VIII");
+ minions.put("14463534f9fbf4590d9e2dcc1067231ccb8d7f641ee56f4652a17f5027f62c63", "Gravel IX");
+ minions.put("5c6e62f2366d42596c752925c7799c63edbfc226fffd9327ce7780b24c3abd11", "Gravel X");
+ minions.put("3945c30d258d68576f061c162b7d50ca8a1f07e41d557e42723dbd4fcce5d594", "Gravel XI");
+
+ minions.put("81f8e2ad021eefd1217e650e848b57622144d2bf8a39fbd50dab937a7eac10de", "Sand I");
+ minions.put("2ab4e2bdb878de70505120203b4481f63611c7feac98db194f864be10b07b87e", "Sand II");
+ minions.put("a53d8f2c1449bc5b89e485182633b26970538e74410ac9e6e4f5eb1195c36887", "Sand III");
+ minions.put("847709a9f5bae2c5e727aee4be706a359c51acb842aafa1a4d23fb62f73e9aa6", "Sand IV");
+ minions.put("52b94ddeedecce5f90f9d227015dd6071c314cf0234433329e53f5b26b8cf890", "Sand V");
+ minions.put("7a756b6a9735b74031b284be6064898f649e5bb4d1300aafc3c0b280dad04b69", "Sand VI");
+ minions.put("13a1a8b92d83d2200d172d4bbda8d69e37afeb676d214b83af00f246c267dcd2", "Sand VII");
+ minions.put("765db90f1e3dab4df3a5a42cd80f7e71a92ea4739395df56f1750c73c27cdc4f", "Sand VIII");
+ minions.put("281ccdfe00a7843bce0c109676c1b59dd156389f730f00d3987c10aef64a7f96", "Sand IX");
+ minions.put("fdceae5bc34dee02b31a68b0015d0ca808844e491cf926c6763d52b26191993f", "Sand X");
+ minions.put("c0e9118bcebf481394132a5111fcbcd9981b9a99504923b04794912660e22cea", "Sand XI");
+
+ minions.put("af9b312c8f53da289060e6452855072e07971458abbf338ddec351e16c171ff8", "Clay I");
+ minions.put("7411bd08421fccfea5077320a5cd2e4eecd285c86fc9d2687abb795ef119097f", "Clay II");
+ minions.put("fd4ffcb5df4ef82fc07bc7585474c97fc0f2bf59022ffd6c2606b4675f8aaa42", "Clay III");
+ minions.put("fb2cfdad77fb027ede854bcd14ee5c0b4133aa25bf4c444789782c89acd00593", "Clay IV");
+ minions.put("393452da603462cce47dda35da160316291d8d8e6db8f377f5df71971242f3d1", "Clay V");
+ minions.put("23974725dd17729fc5f751a6749e02c8fa3d9299d890c9164225b1fbb7280329", "Clay VI");
+ minions.put("94a6fbf682862d7f0b68c192521e455122bb8f3a9b7ba876294027b7a35cd1a7", "Clay VII");
+ minions.put("f0ec6c510e8c72627efd3011bb3dcf5ad33d6b6162fa7fcbd46d661db02b2e68", "Clay VIII");
+ minions.put("c7de1140a2d1ce558dffb2f69666dc9145aa8166f1528a259013d2aa49c949a8", "Clay IX");
+ minions.put("b1655ad07817ef1e71c9b82024648340f0e46a3254857e1c7fee2a1eb2eaab41", "Clay X");
+ minions.put("8428bb198b27ac6656698cb3081c2ba94c8cee2f33d16e8e9e11e82a4c1763c6", "Clay XI");
+
+ minions.put("e500064321b12972f8e5750793ec1c823da4627535e9d12feaee78394b86dabe", "Ice I");
+ minions.put("de333a96dc994277adedb2c79d37605e45442bc97ff8c9138b62e90231008d08", "Ice II");
+ minions.put("c2846bd72a4b9ac548f6b69f21004f4d9a0f2a1aee66044fb9388ca06ecb0b0d", "Ice III");
+ minions.put("79579614fdaa24d6b2136a164c23e7ef082d3dee751c2e37e096d48bef028272", "Ice IV");
+ minions.put("60bcda03d6b3b91170818dd5d91fc718e6084ca06a2fa1e841bd1db2cb0859f4", "Ice V");
+ minions.put("38bdef08b0cd6378e9a7b9c4438f7324c65d2c2afdfb699ef14305b668b44700", "Ice VI");
+ minions.put("93a0b0c2794dda82986934e95fb5a08e30a174ef6120b70c58f573683088e27e", "Ice VII");
+ minions.put("d381912c9337a459a28f66e2a3edcdacbddc296dd69b3c820942ba1f4969d936", "Ice VIII");
+ minions.put("21cb422b9e633e0700692ae573c5f63a838ebc771a209a5e0cc3cba4c56f746f", "Ice IX");
+ minions.put("6406ca9dcd26cc148e05917ae1524066824a4f59f5865c47214ba8771e9b924b", "Ice X");
+ minions.put("5ef40b76cca1e4bcd2cbda5bc61bc982a519a2df5170662ea889bf0d95aa2c1b", "Ice XI");
+
+ minions.put("f6d180684c3521c9fc89478ba4405ae9ce497da8124fa0da5a0126431c4b78c3", "Snow I");
+ minions.put("69921bab54af140481c016a59a819b369667a4e4fb2f2449ceebf7c897ed588e", "Snow II");
+ minions.put("4e13862d1d0c52d272ece109e923af62aedebb13b56c47085f41752a5d4d59e2", "Snow III");
+ minions.put("44485d90a129ff672d9287af7bf47f8ece94abeb496bda38366330893aa69464", "Snow IV");
+ minions.put("9da9d3bfa431206ab33e62f8815e4092dae6e8fc9f04b9a005a205061ea895a8", "Snow V");
+ minions.put("7c53e9ef4aba3a41fe8e03c43e6a310eec022d1089fd9a92f3af8ed8eed4ec03", "Snow VI");
+ minions.put("e1fd2b30f2ef93785404cf4ca42e6f28755e2935cd3cae910121bfa4327345c1", "Snow VII");
+ minions.put("9f53221b1b2e40a97a7a10fb47710e61bdd84e15052dd817da2f89783248375e", "Snow VIII");
+ minions.put("caa370beebe77ced5ba4d106591d523640f57e7c46a4cecec60a4fe0ebac4a4c", "Snow IX");
+ minions.put("f2c498b33325cce5668a3395a262046412cfd4844b8d86ddaeb9c84e940e2af", "Snow X");
+ minions.put("bce70b1b8e30e90a5ad951f42ff469c19dd416cedf98d5aa4178ec953c584796", "Snow XI");
+
+ minions.put("425b8d2ea965c780652d29c26b1572686fd74f6fe6403b5a3800959feb2ad935", "Coal I");
+ minions.put("f262e1dad0b220f41581dbe272963fff60be55a85c0d587e460a447b33f797c6", "Coal II");
+ minions.put("b84f042872bfc4cc1381caab90a7bbe2c053cca1dae4238a861ac3f4139d7464", "Coal III");
+ minions.put("8c87968d19102ed75d95a04389f3759667cc48a2ecacee8b404f7c1681626748", "Coal IV");
+ minions.put("c5ebd621512c22d013aab7f443862a2d81856ce037afe80fcd6841d0d539136b", "Coal V");
+ minions.put("f4757020d157443e591b28c4661064d9a6a44dafe177c9bc133300b176fc40e", "Coal VI");
+ minions.put("2d2f9afcfada866a2918335509b5401d5c56d6902658090ec4ced91fea6bf53a", "Coal VII");
+ minions.put("1292ecec3b09fbbdffc07fbe7e17fa10b1ff82a6956744e3fa35c7eb75124a98", "Coal VIII");
+ minions.put("29f5b3c25dd013c4b1630746b7f6ee88c73c0bacf22a970a2331818c225a0620", "Coal IX");
+ minions.put("46bb54a946802ebce2f00760639b2bf484faed76cbb284eb2efaa5796d771e6", "Coal X");
+ minions.put("641ffadeaa22c8d97a72036cbd5d934ca454032a81957052e85f3f95b79d3169", "Coal XI");
+
+ minions.put("af435022cb3809a68db0fccfa8993fc1954dc697a7181494905b03fdda035e4a", "Iron I");
+ minions.put("5573adc1a442b2bad0bafd4415603e821d56a20201c1e7abd259cc0790baa7bf", "Iron II");
+ minions.put("b21f639707fbe87e55ca18424384b9ef5deb896fe16a6f23a7197b096f038ad9", "Iron III");
+ minions.put("a7e83a03615a41d7a7c967fd40c0583ad273d37d0a19bde3e4a562a0f680c920", "Iron IV");
+ minions.put("c6177919c0849aa737c07b13fd5019be5572d6c9e4be59c33645a99057f25014", "Iron V");
+ minions.put("546591b819ea8a33cf993bb433fa5bbbbb200af8e6eb1103a7d1ee11da1db5f1", "Iron VI");
+ minions.put("f58e6617cefe52eeacb9d3f7f424d52e4ae1b8e8f775c0bfd6045c99387851ab", "Iron VII");
+ minions.put("7e5ea57f0fb1f58a68746b5a6e93b8491528d09ba07b586b73a43ae05638ad0c", "Iron VIII");
+ minions.put("3679a748d40754d4e5bc418d152064c7aeee4121a2b1d8e058d0b0326f8bff3", "Iron IX");
+ minions.put("ac48b4758211fd0e4713ad0b6ea9d13a29eb9f523d3911dcff945197b8bc1a56", "Iron X");
+ minions.put("1b22b582b750dbdb0b7599c786d39f2efedfe64b5ae32656020295b57f2fcf7", "Iron XI");
+
+ minions.put("f6da04ed8c810be29bba53c62e712d65cfb25238117b94d7e85a4615775bf14f", "Gold I");
+ minions.put("761bcdb7251638f757cd70fd0fd21a2a05f41cbdc78ad3e50f64ecf38aa3220c", "Gold II");
+ minions.put("8bd48b17a82d5e395034d01db61c76f13f88da6e5a0b1c1d198fbdb0805a7739", "Gold III");
+ minions.put("2b46c4f5f574748cf82777ceba40a4fa96572f9e3cb871eb9258330df960f72e", "Gold IV");
+ minions.put("f6e438a6d1ef319c42d73a00db2b2981fdbb0e437a1c02399b8daf8d9e37a05b", "Gold V");
+ minions.put("1317a0b66ed2dc274b3b0d77b6859d4b9942ccc942d9e0bc26f7fa15b89842b", "Gold VI");
+ minions.put("9d4411f11d6c0c0eb43a139e16e5b7db3fdca94f3c1a2443ff6e9b06d576a6d4", "Gold VII");
+ minions.put("6439b889f62ff1f2cc5e00db5892655c591194b9294725a366efa3e4b2a022fa", "Gold VIII");
+ minions.put("5bf721e5d5403e11d176dd33d54458332195667e89d88bbf402c6d0307ec442c", "Gold IX");
+ minions.put("3d236e1e3ef0dea06e3be72f0980767c5aea41b882f8b9b58838298ace64dc9d", "Gold X");
+ minions.put("936a5712543dde0a3c7dc54063178cb69179dfd5cd75cb1de4fe8771b53dd03f", "Gold XI");
+
+ minions.put("2354bbe604dfe58bf92e7729730d0c8e37844e831ee3816d7e8427c27a1824a2", "Diamond I");
+ minions.put("5138692abccf0cebd0d99b4a59a26d41f779acfd35a07b2b527593356bc8ece6", "Diamond II");
+ minions.put("4e7ed1638aeaab8a87d5a1193080d46262e2d6d3aa22ad3222c6af0170b3ee17", "Diamond III");
+ minions.put("51fb872f35ad1536d2d5709797f8429043dc2f4cd5a4d8e91b9422dc5c51db95", "Diamond IV");
+ minions.put("32812ae070fca358216ad627859e2d69c67c8ac7b631ff4d5c57ebc394357095", "Diamond V");
+ minions.put("ec01c11ca159a5cd18ab45c03b1c56ae3a228711b8369daf828f85c5c1a4bb2c", "Diamond VI");
+ minions.put("a6a28d4cba6907c107421e90b90c274b5ac6ce937def9e28521b5ef81f461493", "Diamond VII");
+ minions.put("6406f0ba05e99cabb166f8982ac6eff8de42b5f884b26c2ffd4ad70ec7d21151", "Diamond VIII");
+ minions.put("d6cc4e3241c518aeb8e64e32a0b6f536d7684c8b5fe6bc9699db9ee6332cf70f", "Diamond IX");
+ minions.put("f024c9b17c2b4a84fe503726df57b7cd7a77827f530c6b93d2e94e7cc0b515f9", "Diamond X");
+ minions.put("537643ca84a56411f676a11eceab6d96fc7877ae3402c6083b69ee97ab95e700", "Diamond XI");
+
+ minions.put("64fd97b9346c1208c1db3957530cdfc5789e3e65943786b0071cf2b2904a6b5c", "Lapis I");
+ minions.put("65be0e9684b28a2531bec6186f75171c1111c3133b8ea944f32c34f247ea6923", "Lapis II");
+ minions.put("2a3915a78c2397f2cef96002391f54c544889c5ced4089eb723d14b0a6f02b08", "Lapis III");
+ minions.put("97df8ae6e1436504a6f08137313e5e47e17aa078827f3a636336668a4188e6fc", "Lapis IV");
+ minions.put("aa5d796b9687cc358ea59b06fdd9a0a519d2c7a2928de10d37848b91fbbc648f", "Lapis V");
+ minions.put("6e5db0956181c801b21e53cd7eb7335941801a0f335b535a7c9afd26022e9e70", "Lapis VI");
+ minions.put("1a49908cf8c407860512997f8256f0b831bd8fc4f41d0bf21cd23dbc0bdebb0f", "Lapis VII");
+ minions.put("c08a219f5cf568c9e03711518fcf18631a1866b407c1315017e3bf57f44ef563", "Lapis VIII");
+ minions.put("e5a93254f20364b7117f606fd6745769994acd3b5c057d3382e5dd828f9ebfd4", "Lapis IX");
+ minions.put("6fe5c4ceb6e66e7e0c357014be3d58f052a38c040be62f26af5fb9bed437541", "Lapis X");
+ minions.put("736cd50c9e8cf786646960734b5e23e4d2e3112f4494d5ddb3c1e45033324a0e", "Lapis XI");
+
+ minions.put("1edefcf1a89d687a0a4ecf1589977af1e520fc673c48a0434be426612e8faa67", "Redstone I");
+ minions.put("4ebdbf0aca7d245f6d54c91c37ec7102a55dd0f3b0cfe3c2485f3a99b3e53aa0", "Redstone II");
+ minions.put("c1a5175a1caf7a88a82b88b4737159132a68dc9fc99936696b1573ea5a7bb76d", "Redstone III");
+ minions.put("bbf83cb38bd6861b33665c1c6f56e29cbc4a87a2f494581999d51d309d58d0aa", "Redstone IV");
+ minions.put("d96fa75edd9bc6e1d89789e58a489c4594d406dd93d7c566ed4534971b52c118", "Redstone V");
+ minions.put("9cfd7010be9a08edd1e91c4203fccff6ddf71e680e4dfb4d32c38dee99d4a389", "Redstone VI");
+ minions.put("18db0ef0af4853603a3f663de24381159e9faaa1cdf93b026719dab050ea9954", "Redstone VII");
+ minions.put("a40b85c00f824f61beefd651c9588698e49d01902e84a098f79ee09941d8e4ac", "Redstone VIII");
+ minions.put("85d61b9d0b8ad786e8e1ff1dbbde1221a8691fda1daf93c8605cbc2e4fdea63", "Redstone IX");
+ minions.put("6588bed4136c95dd961b54a06307b2489726bbfe4fda41cee8ab2c57fa36f291", "Redstone X");
+ minions.put("6670498256b1cbae7c8463bc2d65036cf07447b146f7d3f69bfa2dc07e9fd8cf", "Redstone XI");
+
+ minions.put("9bf57f3401b130c6b53808f2b1e119cc7b984622dac7077bbd53454e1f65bbf0", "Emerald I");
+ minions.put("5e2d440d6c2300d94e7d5c44906d73a5cde521dfe516698513dd8c02ffdd5a82", "Emerald II");
+ minions.put("6b94475f5c54147c27cbba0434ca0f5e4a501c7bae44c73d36ea36016fa47fec", "Emerald III");
+ minions.put("65cbd71747c835a09af211e821d65c4facef7fb6824973bc3ca8c4aba4a98e30", "Emerald IV");
+ minions.put("dc7c36d02b65e871cc696db42b3ae0cc98670205c8bc90cf96e5f53424cd681e", "Emerald V");
+ minions.put("67136258da9fa4143c058d4d8a6758dcb6f615e6d98d019fdafe526e5f900b1f", "Emerald VI");
+ minions.put("a6762402cbd0127351fd1aa37423f463b309379eff4a2ad28b5766d51407f288", "Emerald VII");
+ minions.put("34e546bb3f8930b0a1a204daaec9ed76f85eab3d57a61e62c3133d11f65c15b", "Emerald VIII");
+ minions.put("db81f7229ad141b925ad3ff0f121c14686478a35f4777c73d3416505343d3811", "Emerald IX");
+ minions.put("c2ba3b81576024cbde5dade239ad7603e1fe5f9d8b1fe4716cfc68a9bcb2d324", "Emerald X");
+ minions.put("67a5b0b9839081488ffde6d0cd74b540cf23a505c95e521f77e337775c49b431", "Emerald XI");
+
+ minions.put("d270093be62dfd3019f908043db570b5dfd366fd5345fccf9da340e75c701a60", "Quartz I");
+ minions.put("c305506b47609d71488a793c12479ad8b990f7f39fd7de53a45f4c50874d1051", "Quartz II");
+ minions.put("83f023160a3289b9c21431194940c8e5f45c7e43687cf1834755151d7c2250f7", "Quartz III");
+ minions.put("c2bc6c98d4cbab68af7d8434116a92a351011165f73a3f6356fb88df8af40a49", "Quartz IV");
+ minions.put("5c0e10de9331da29e0a15e73475a351b8337cd4725b8b24880fb728eb9d679dd", "Quartz V");
+ minions.put("300120cabf0ae77a143adca34b9d7187ca1ef6d724269b256d5e3663c7f19bd9", "Quartz VI");
+ minions.put("bde647431a27149bf3f462a22515863af6c36532c1f66668688131ca11453fd1", "Quartz VII");
+ minions.put("9899278d0464397dd076408812eef40758f75b1cdb82c04c08c81503453e07e6", "Quartz VIII");
+ minions.put("2974bc0b9771a4af994ea571638adf1e98cd896acf95cc27b890915669bcedfd", "Quartz IX");
+ minions.put("3ae41345d675f4ed4dc5145662303123cb828b6e1a3e72d8278174488562dfa9", "Quartz X");
+ minions.put("7aeec9ef192e733bfcb723afd489cbf4735e7cfdd2ec45cae924009a8f093708", "Quartz XI");
+
+ minions.put("7994be3dcfbb4ed0a5a7495b7335af1a3ced0b5888b5007286a790767c3b57e6", "End Stone I");
+ minions.put("eb0f8a2752e733e8d9152b1cf0a385961fa1ba77daed8d2e4e02348691610529", "End Stone II");
+ minions.put("63f211a5f8aca7607a02df177145bea0c6edc032bc82807fb1daeaa5d95b447d", "End Stone III");
+ minions.put("fd40b308f1ca5b1188618c45a3de05b068d9cafba7039aa06d2bc5b9c6751cea", "End Stone IV");
+ minions.put("627218013240d63354b7fe931f1cbea1321535e28e292937f0c8f6f776088723", "End Stone V");
+ minions.put("70ca8dbb37647bb0b51b41799d619615af04976b8111dd22d3b23316562c6c30", "End Stone VI");
+ minions.put("1260560ce465dcd39fcb0c3ce365a88ff5c8e123cdc4d0e95e682e70b9283392", "End Stone VII");
+ minions.put("2b1755ccf1e0bba50ab41007aaec977286b7d7fcf3953a9c018aaf62967f3474", "End Stone VIII");
+ minions.put("13f379728f8cbb88300e8de137d9b576205cde2c5e36607ad8d3bb558f533d68", "End Stone IX");
+ minions.put("1fbd102a90e2a19a9d2f5fcc98da00b4addbfb02593b34367fe4c2339c37eff0", "End Stone X");
+ minions.put("35fc7b11e32ead79f7d110d3efb6447a66b387fce79f70fa4ceaaa0e0fe717f5", "End Stone XI");
+
+ minions.put("bbc571c5527336352e2fee2b40a9edfa2e809f64230779aa01253c6aa535881b", "Wheat I");
+ minions.put("c62b0508b3fef406833d519da8b08ee078604c78b8ca6e9c138760041fa861bf", "Wheat II");
+ minions.put("e61773628ed7555e0a63add21166ad34227d10d21e34c3c7e5a0fa8532dd3f6", "Wheat III");
+ minions.put("a8751403935a5c637ff225cddb739c6a960a48256bc88e6ead0a728d70981267", "Wheat IV");
+ minions.put("c32fa98a6c398ca75c9480711158a60a0f37e9f93bdc8fe4156191bd88a888b3", "Wheat V");
+ minions.put("662d04c94d385f6ecddaa4bc51371baf54061ab6f21c8a030df639d87ac6be2d", "Wheat VI");
+ minions.put("5c6f9f3a7b55ee7093ee8f5bf5888b16def6bfde157556df219a1c0b94b0458f", "Wheat VII");
+ minions.put("efe4b6553a3d0f764a20624f4ab256792167e5fc3b75b31b59732325a316f162", "Wheat VIII");
+ minions.put("2e3cb30a26293b7fc67519249bb07efd9c9a72229811e902e1627027938edcce", "Wheat IX");
+ minions.put("d6ab7a3d5438c101c71b727643d08d3b3fedf321d3853ade72b1ae83feb5df70", "Wheat X");
+ minions.put("2f6d62602c22e630153be8b764f45827585521bd82bb5307df1168f250f17f6b", "Wheat XI");
+
+ minions.put("95d54539ac8d3fba9696c91f4dcc7f15c320ab86029d5c92f12359abd4df811e", "Melon I");
+ minions.put("93dbb7b41ddd998842719915179a6b5a82d0c223e4c313c9eb081b52c84a764f", "Melon II");
+ minions.put("9ed762a1b1bf0c811a6cc62742526840e8eb3c01fc86cc5afed89ec9beeb530e", "Melon III");
+ minions.put("9e2654f305b788b9345bef6be076d8f36e7c946d6eae26d3c0803ddc4843b596", "Melon IV");
+ minions.put("1234f7f7ad67acd50b30932781c21a0b1cc22530a4980f688549123e10d9c474", "Melon V");
+ minions.put("8fea2e4ddf7314f21b87fc8d8634c60cab23320112002932d6c12c2e92d5549b", "Melon VI");
+ minions.put("213637a2898fd04f0ced904c0293f136238057c33b16983e0262ff9ae0047dd2", "Melon VII");
+ minions.put("3b7e6694866967e222664641461c0a1b08b9aa0c390944e6142e769d473987a5", "Melon VIII");
+ minions.put("8ebac9c3ddbb76ea862b91a6992bae358c0eaed7b2ca73fb28b8115be019e32b", "Melon IX");
+ minions.put("dca93d1d2dc3f7ee1ac66ab5280c619fbe37e1e338a6484808a5433b1a3ee911", "Melon X");
+ minions.put("166a1693eb7764d0c78d683bb53787db6836518de0b5087869df692dc6be942", "Melon XI");
+
+ minions.put("f3fb663e843a7da787e290f23c8af2f97f7b6f572fa59a0d4d02186db6eaabb7", "Pumpkin I");
+ minions.put("95bcb44bbeaec7c903d4f37273ff6a20bd40f240dfbefc4aaf25cb4b0a25f3c4", "Pumpkin II");
+ minions.put("6832fd793f38e20265cfef3d979289493b951e0b5fb53511984bf500b6ad64ca", "Pumpkin III");
+ minions.put("6656b05400537c47b3e986697e5af027ed36adf7c80d9edaec6b48cb1af9f99b", "Pumpkin IV");
+ minions.put("16685cf51ab0e08e842a822ca416225df3c583b32110bf4d778ad69f3f604b43", "Pumpkin V");
+ minions.put("ee1807903ca846a732ea46e9490b752a2803f75017a3008808c48437cfd8827f", "Pumpkin VI");
+ minions.put("cda682c874c482e9e659e37fe3e8399c5f3c4f6237f0656071af5ffaf418ea9a", "Pumpkin VII");
+ minions.put("bb72233f28cb814aadb63f0688033e7317907cf43f015097e493a578a3f50222", "Pumpkin VIII");
+ minions.put("2cc1a47302e055e561b06daede35f84a04829bc899af03d5603b78e55269c402", "Pumpkin IX");
+ minions.put("7e246ead094174d265eb03222417dd4ed1d1a6a5ad33d77ed2578ab55eed3a37", "Pumpkin X");
+ minions.put("4c6a48f079ef70d84df10332bb0f2bf038d8e0e82ac36734823fb4b4a50705e4", "Pumpkin XI");
+
+ minions.put("4baea990b45d330998cb0c1f8515c27b24f93bff1df0db056e647f8200d03b9d", "Carrot I");
+ minions.put("32a0a1695d50e0a9ced4b91edfd42afd41b4e737aa5d174c74b13963fb022556", "Carrot II");
+ minions.put("149dbae380e85f93d4c86f5097ec2ac3dec28389fb528a0d6a719fc6139626a8", "Carrot III");
+ minions.put("7399c0373eed7e12d5c212e13c51422e21d4ec7fa301f5a5c684f816a2eb2aab", "Carrot IV");
+ minions.put("c56711e5002d0a7003f85cc2f59137da467648332baa630d939684580c5bbddb", "Carrot V");
+ minions.put("52bc4e06ec80d34fb5c419d743e7ccf313866a98dbd15d53a83db98a7bff8ff5", "Carrot VI");
+ minions.put("c8c0dbbc8cc4bdc5d8483c732a61404ad12ab0ab7c49ff81cba2b709ae547923", "Carrot VII");
+ minions.put("f3e5e690f2f78fd39efb4b0bf212bf68eb02d76936891238c8db2b4940d49313", "Carrot VIII");
+ minions.put("42c2ab452b92102b7ba030b81084d000e155beb616a026f95a11c654e96f4e28", "Carrot IX");
+ minions.put("bdf031730f2f6bd8aaebfbc6e160723d294e03cc9545d98d9bdb84cfdf853266", "Carrot X");
+ minions.put("62858c422e0963f1b1da6196e9d47936acea449bea9f90e2dbf32f921f2522e", "Carrot XI");
+
+ minions.put("7dda35a044cb0374b516015d991a0f65bf7d0fb6566e350496642cf2059ff1d9", "Potato I");
+ minions.put("6ce06fb5d857f1b821b4f6f4481464b2471650733bf7baa3e1f6b41555aab561", "Potato II");
+ minions.put("29e3d309d56d37b51f4a356cba55fec4ac8e174bf2b72a03fb8361a2b41da17d", "Potato III");
+ minions.put("72fd7129e7831c043447a8355e78109431e7ca19959ef79dcc7a4c8f0a4ccf77", "Potato IV");
+ minions.put("2033a6e541525d523fe25da2c68a885ba1c2449362d0b35a68c95b69e8a28c87", "Potato V");
+ minions.put("2aea4e3ef5782f4cb6e0d38e8d871221d29197cb186aca0e144922e7cd2e1224", "Potato VI");
+ minions.put("6a1812e4f58f1ec46c608521fc5f51eb2e653bbc4e43cd9e89dff88e8c777e", "Potato VII");
+ minions.put("9788f69ebdb3030054feff365e689bb5b10a867f52f9873bc952fc26b54d48ff", "Potato VIII");
+ minions.put("b84792c8674b964f708b880df7d175631b9b6d9b5362353362ca997e727e1189", "Potato IX");
+ minions.put("e05c2ab7f41ca1f3f221b949edc7b20b800ed3bbaeb36514eb003887338f960", "Potato X");
+ minions.put("57441fa19d89d5df902c07586c084f1b00c4ca06ca4cc2ec5b6230d1a5199811", "Potato XI");
+
+ minions.put("4a3b58341d196a9841ef1526b367209cbc9f96767c24f5f587cf413d42b74a93", "Mushroom I");
+ minions.put("645c0b050d7223cce699f6cdc8649b865349ebc22001c067bf41151d6e5c1060", "Mushroom II");
+ minions.put("a8e5b335b018b36c2d259711bee83da5b42fcc55ec234514ae2c23b1e98d7e77", "Mushroom III");
+ minions.put("80970ebf76d0aa52a6abb7458a2e3917d967d553def9174a8b83697a10f4e339", "Mushroom IV");
+ minions.put("268b2d44457a92988400687d43e1562e0cb2ed1667ef0e62ed033a2881723eb4", "Mushroom V");
+ minions.put("ac8772dfd110ef66d5eb3957046834313adbf035f36352f2426be2802c1a21d8", "Mushroom VI");
+ minions.put("9fd3d54f0eb2570ffd254bcbff3b3c076521ed896118d984fb66db154d4a5466", "Mushroom VII");
+ minions.put("dd360de8da7f050cedad36e91f595577622a2ae9db32f622b745c47f35dc012e", "Mushroom VIII");
+ minions.put("e785f2fb94555998b380d19deffe120eb8dbd1191b0927312221e1f4f762a87d", "Mushroom IX");
+ minions.put("9ff2be74b7aad963d3f7bad59d2f9cda1337c3d00af0bcd6e314ba1fe348dfae", "Mushroom X");
+ minions.put("74e69059cb27b7b7c65b5543db19aa153a2509a0090719e5199f1082acc1b051", "Mushroom XI");
+
+ minions.put("ef93ec6e67a6cd272c9a9684b67df62584cb084a265eee3cde141d20e70d7d72", "Cactus I");
+ minions.put("d133a6e56ac05d1cfa027a564c7392b04c2cffda3e57c70b06ed5ae1d73ca6fe", "Cactus II");
+ minions.put("1b35a27732a2cb5ee36a52653b2e7d98bdd9d3d799499035c9f918344570c9e8", "Cactus III");
+ minions.put("6569e2f7104a423362844fdd645c3a9d2b8f8c1b8979d208ec20487ac2a5c783", "Cactus IV");
+ minions.put("c4fe9efb395689a9254fea06d929c3c408a5b314084399b386c009ca83a062a9", "Cactus V");
+ minions.put("ac8a964da5cc050812171dce6e9937191e4e7b65b7eb5f27e1846f868c023f58", "Cactus VI");
+ minions.put("2c1b5f3b3ffb6a8983f5110d4fd347df6086205bcedd3e065cbdf9ee47f957fe", "Cactus VII");
+ minions.put("2ddea64e86688c84d9edae63cf94765b9a5fac004e4babb3bfff081b30198327", "Cactus VIII");
+ minions.put("5a03ed5566128ca6d7911e2e1614450e28372e2b0513327689e183168edc5711", "Cactus IX");
+ minions.put("f4272b51f991a088d3aae579b93d253efc2e6d9657f0299191e3a18ee89a22c0", "Cactus X");
+ minions.put("b1cabca262d9f98ccabf5546c033614f664173c6f206e626ca8b316d7962f8c8", "Cactus XI");
+
+ minions.put("acb680e96f6177cd8ffaf27e9625d8b544d720afc50738801818d0e745c0e5f7", "Cocoa Beans I");
+ minions.put("475cb9dcc1b3c33aca8220834588d457f9f771235f37d62050544be2f2825d1b", "Cocoa Beans II");
+ minions.put("1d569fdd54d61e55c84960271950ce755d60ea6dc03c427773098649e8b7136d", "Cocoa Beans III");
+ minions.put("5ed37c0b33043212ad9527df957be53f0e0fb08c184648cf0d2a64775fb6b4ec", "Cocoa Beans IV");
+ minions.put("4ea5d503ed03184a906ad29a8b1809f20ba95b99bb889a8e6d04c2cc586c6412", "Cocoa Beans V");
+ minions.put("b1db22b8f0a12492c2c7cf2784025c6cad2afc66998c4f47c0f02e6100454851", "Cocoa Beans VI");
+ minions.put("afdfa53bdd3937be5305a2ef17b3f80860d12b85000dd51a80a4f3f9b744998b", "Cocoa Beans VII");
+ minions.put("fa73332b8e1e64e172f4e8ccb58f93e78d06185db298b409eccedf6d6f6ebde3", "Cocoa Beans VIII");
+ minions.put("db215abd78aced038772b6f73d828dbfc33369d7e9e00a58539e989508da6911", "Cocoa Beans IX");
+ minions.put("80c4434c532a0e1a41dad610989f8a01432ea47adc39d64ec81fef81284d581", "Cocoa Beans X");
+ minions.put("d71be56d6fbfec9e2602737dc3df8409368e23fb854b353b2451c30daa8c425b", "Cocoa Beans XI");
+
+ minions.put("2fced0e80f0d7a5d1f45a1a7217e6a99ea9720156c63f6efc84916d4837fabde", "Sugar Cane I");
+ minions.put("30863e2c1fdce44bc35856c25c039164845456ff1525729d993f0f40ede0f257", "Sugar Cane II");
+ minions.put("f77e3fe28ddc55f385733175e3cf7866a696c0e8ffca4c7de5873cd6cc9fe840", "Sugar Cane III");
+ minions.put("fbeac9c599b7d794a79a7879f86b10fb7743ad42c9937954d8ffeedc3ce55122", "Sugar Cane IV");
+ minions.put("802e05d0a041aaf3a7fa04d6c97e67a66987c7617ae45311ae2bb6f2005f59c1", "Sugar Cane V");
+ minions.put("ca9351b61e93840264f5cc6c6b5a882111ae58a404f3bbbe455e07bf868d3975", "Sugar Cane VI");
+ minions.put("f983804d6b4afdcda8050670f51bb3890945fa4fa8a9c3cfa143a3d7912036a3", "Sugar Cane VII");
+ minions.put("f2212f3b630af6b32b77905e4b45fc3f11046ccc9a7dd83b15d429944c4e2102", "Sugar Cane VIII");
+ minions.put("49b33dad5234dc354a84f9217daf22684b58d80058de05c785f992f0b226590a", "Sugar Cane IX");
+ minions.put("f1a94db5ee94ffdf4f3e87e5c12c0f112122fa52dc7c15f3881b6190aed4db92", "Sugar Cane X");
+ minions.put("237514eb4e09053002f242a04997cfb3584928185acf99fa9a1d998bd987e1d7", "Sugar Cane XI");
+
+ minions.put("71a4620bb3459c1c2fa74b210b1c07b4a02254351f75173e643a0e009a63f558", "Nether Wart I");
+ minions.put("153a8dc9cf122c9d045e540d0624ccc348a85b8829074593d9262543671dc213", "Nether Wart II");
+ minions.put("d7820332f21afe31d88f42111364cb8aa33746b6f1e7581fb5f50bbfe870f0ad", "Nether Wart III");
+ minions.put("4b07451870cbd1804654e5b5db62e700efad8e7bcc7bf113a54ef6f5a5ab47e6", "Nether Wart IV");
+ minions.put("8ef39a8e6958dafc2b5dbc55993d63e065f3d88e62e94ac6d28c865d85b9432", "Nether Wart V");
+ minions.put("3245cbc9d11455ad30f9b7860604a372cc6bdba64ebe13babf6815de9ac5ab89", "Nether Wart VI");
+ minions.put("d45298dcfb39274d0eed5df91ad744d7161d75da155f52955c44767231e88584", "Nether Wart VII");
+ minions.put("b5c14d391dd776ebd5d0245bb762495371f666f5772022e82aebfdffc9b9447", "Nether Wart VIII");
+ minions.put("3d8780f780548eb3d7fce773c09e89307a090b175570271808953eb81b5a9d72", "Nether Wart IX");
+ minions.put("3e5291d28b362a5d8c17b521a317b3a66d68f0ed9e8f322b65db0c32c42e10a2", "Nether Wart X");
+ minions.put("79d99c73e1f9a5376bd697c7ccbe3844f762a2b196fa72a5831988747aaacfa", "Nether Wart XI");
+
+ minions.put("baa7c59b2f792d8d091aecacf47a19f8ab93f3fd3c48f6930b1c2baeb09e0f9b", "Flower I");
+ minions.put("ddb0a9581e7d5f989d4fb6350fc7c51d65b3e49e4a0be35c3f3523287a0ff979", "Flower II");
+ minions.put("de5c24a8bcd21e4f0a37551a2ad197a798be986ef08371ab11e95c2044bb1bc0", "Flower III");
+ minions.put("5d473a99697430f82786b331c9657adef370655492b6763de9ea24066168ab41", "Flower IV");
+ minions.put("6367c303c2c4f6ef4e0feeb528f37bcd71c1c0765621259ff00530c5d99b584b", "Flower V");
+ minions.put("b8ffa832227440cbd8f218fb20f2cca7f9778024814b4e92bd5112d0a3f4b7f9", "Flower VI");
+ minions.put("3bc942565909d05cb447945726411a3da83dcaa0a5c9b04fa041c3c5ca84e955", "Flower VII");
+ minions.put("8959d9c639b20b294cf6cd0726422092682e762d3991ddac39d92cdc60334103", "Flower VIII");
+ minions.put("4beed0b166465261f07399fe97304b9913f522e0d42e78d86849ec72be3d7fa9", "Flower IX");
+ minions.put("d719f6041aaaf6c7b55042a550d51e17af727f6b8e41af09a1aded49c9ff9e31", "Flower X");
+ minions.put("1142fe535855dd6b06f4f0817dbc8bf98da31265ae918b854cd11fcacd6fab4c", "Flower XI");
+
+ minions.put("53ea0fd89524db3d7a3544904933830b4fc8899ef60c113d948bb3c4fe7aabb1", "Fishing I");
+ minions.put("8798c0d7b65bfa5f56b084c1f51767a4276ad9f2c60bcb284dc6eccb7281e2ab", "Fishing II");
+ minions.put("c8cefef2d7268a5170dd86fd782d6fee06f063b0a223e86378dde3d766c19929", "Fishing III");
+ minions.put("cdfb98800f7d01c56744fa116d2275090a337334f6f884230522f8ea3964c9e0", "Fishing IV");
+ minions.put("5eb079ce77840f08fb170aad0a89827695d92a6ccca5977f48c43fe931fd22f7", "Fishing V");
+ minions.put("db557d80642ccd12c417a9190c8d24b9df2e797eb79b9b63e55c4b0716584222", "Fishing VI");
+ minions.put("db557d80642ccd12c417a9190c8d24b9df2e797eb79b9b63e55c4b0716584222", "Fishing VII");
+ minions.put("a5ee01b414c8e7fb1f55d8143d63b9dfed0c0428f7de043b721424c4a84eded3", "Fishing VIII");
+ minions.put("204b03b60b99d675da18c4238d3031b6139c3763dcb59ba09129e6b3367d9f59", "Fishing IX");
+ minions.put("593aa3e4eaa3911456d25aab27ce63908fe7a57d880a55884498c3c6a67549b0", "Fishing X");
+ minions.put("46efc2d1ebb53ed1242081f22614a7e3ac983b9f6159814e6bcbc73ce7e3132a", "Fishing XI");
+
+ minions.put("196063a884d3901c41f35b69a8c9f401c61ac9f6330f964f80c35352c3e8bfb0", "Zombie I");
+ minions.put("c01613ba2e99ee8326b5ceae77efb1e9afa6ae541f38b4ed63e79ecb01e725f0", "Zombie II");
+ minions.put("d6fdd8d54bc3a109b7e06baaf1b0ac97fb22989aa93069b63cca817ff7fd7463", "Zombie III");
+ minions.put("bfbec1bd0fe3b71b9da9d7666fd6bbde341b4c481e8563fddf61f4ee52f7cd1b", "Zombie IV");
+ minions.put("67a1945b52761443d1a7de233a4e4aea40c9abad92ae9ac35e385478971956ae", "Zombie V");
+ minions.put("a8c3ab42d327fa01271f9f19958c77e0dee9fde57415f873783737a1e83f4e86", "Zombie VI");
+ minions.put("5058f08910b39c30644f33fd71f81a412f6e05fe7c703a87fd4f3d5e4b2b6509", "Zombie VII");
+ minions.put("e40b20aba5b3c279dee42b39d8e03de25cbead3421655f0cf1bea43ed0b4272e", "Zombie VIII");
+ minions.put("fcbf17681e579f00d65f978c0b50915aaf2d5f609da7d9ab156cb6f092b88840", "Zombie IX");
+ minions.put("6b4a9dc6d0fdbd1bad3613dcb3ab5c54c5ea5e0b498ee35b2fd30951cc2e9fcd", "Zombie X");
+ minions.put("6699ff5ce9a0f5032340596f6b2dd6ac7028fc7cc5b943d4c1fc2d3749fedcd6", "Zombie XI");
+
+ minions.put("a3dce8555923558d8d74c2a2b261b2b2d630559db54ef97ed3f9c30e9a20aba", "Revenant I");
+ minions.put("c5aff1b4f533bb1e1cf5ea96caea7349d5efe9e9a982ec8051ac32910e3ae68c", "Revenant II");
+ minions.put("d3865482377fb54bc07dc7633a5a25bbaaecc3e9978c04bf608776da1f8a154a", "Revenant III");
+ minions.put("e138071b15709fd98c89597abddeefb70bee370fdcce7da9cbfa7275f2421557", "Revenant IV");
+ minions.put("73de056cedbd88c61cff93c1f97e4cc69f0dafcdac3ca62013e3c0527fb5245", "Revenant V");
+ minions.put("d51616207cb10c6414aa7812e56e3f4b408eac7c5ddc9011fe794b41b7ae7c24", "Revenant VI");
+ minions.put("4097b94aecc2b187fcc251b2d2273554f66853436fa8ecc8e5156b148004c804", "Revenant VII");
+ minions.put("57228dedbff9e114d5e4ce7f8e39c3bfb07f2c2f121545a8dc7803dfc0484786", "Revenant VIII");
+ minions.put("e7eb574b6ab8b394c6b4a112ae18d5a672ffa414ec4dff3d65c9950523c19e0a", "Revenant IX");
+ minions.put("d0197c8a4eaca2e5cc1b287ac84c62ef8c9f63068218105292dd89c3f7e64596", "Revenant X");
+ minions.put("9cf6f95308bedb182b434aa73058aa8d69818b48900396cebc127c1bf7df6790", "Revenant XI");
+
+ minions.put("2fe009c5cfa44c05c88e5df070ae2533bd682a728e0b33bfc93fd92a6e5f3f64", "Skeleton I");
+ minions.put("3ab6f9c3c911879181dbf2468783348abc671346d5e8c34d118b2b7ece7c47c2", "Skeleton II");
+ minions.put("ccd9559dc31e4700aaf001e0e2f0bd3517f238af25decd8395f4621404ca4568", "Skeleton III");
+ minions.put("5b2df127315a583e767c6116f9c6ccdb887dc71fbe36ff30e0c4533db2c8514e", "Skeleton IV");
+ minions.put("1605c73264a27d5c9339b8a55c830d288450345df37329023c13cdc4e4b46ccc", "Skeleton V");
+ minions.put("b51e887ab5c0966bb4622882e4417037c3eee8a2d0162e2e82bf295f0d1e1db2", "Skeleton VI");
+ minions.put("40ad48abf6ae82b8bad2c8a1f1a0c40dea748c05922b7ff00f705b313329e1f1", "Skeleton VII");
+ minions.put("1a81a52e837daa71fd05c9e4c37a9cad2e722f96779b574127072d30a98af582", "Skeleton VIII");
+ minions.put("e0a8fae40ff866e3fb7d9131f50efb8bd870da92cdf11051af48fa394bfa19e2", "Skeleton IX");
+ minions.put("ed666149a1967b13df3341690c4c9a9f409b0f3b4f9ca8725d1969102ad420e0", "Skeleton X");
+ minions.put("576255c781ebfb719d28f904813f69e20541d697f88bc6d96a6d4aa05b0fbc22", "Skeleton XI");
+
+ minions.put("54a92c2f8c1b3774e80492200d0b2218d7b019314a73c9cb5b9f04cfcacec471", "Creeper I");
+ minions.put("3fcf99ab9b31c2f0b7a7378c6936b63ac4a78857831729f08cca603925a5873b", "Creeper II");
+ minions.put("488b4089a835e276838bba45c79d1146f0c2341971170a6163e6493890fd1b83", "Creeper III");
+ minions.put("ac2d5f8dcfc9f35897f8b0a42ff0c19e483bdc745e7e64bf0aaf1054a6e67dd", "Creeper IV");
+ minions.put("654bde9a26e35094e3438540c225cffa7690c1d4456251da30cc990ff921cc36", "Creeper V");
+ minions.put("654bde9a26e35094e3438540c225cffa7690c1d4456251da30cc990ff921cc36", "Creeper VI");
+ minions.put("f6f95998dd76a3bd9ffe949e7a4fe993b4baa2e981f49bf7113417f51003b193", "Creeper VII");
+ minions.put("8c0abba2be5c9a93362a7da3231aeea824c5c590bfaaaec78888f1b3d9d32adc", "Creeper VIII");
+ minions.put("21abd529c1898f6ec7e01d9943419c6358de93e0d6cdd2d90c8d63e7036db60d", "Creeper IX");
+ minions.put("5699c6b6bc8adfa79e22ae51cc049fab2c7a51b686ca968df222cfa98faf92a", "Creeper X");
+ minions.put("70850cccb3dfb7fe4bb0f7a008d5b4c10c08f9e36998f6f44ae8c9bc1b1b8e01", "Creeper XI");
+
+ minions.put("e77c4c284e10dea038f004d7eb43ac493de69f348d46b5c1f8ef8154ec2afdd0", "Spider I");
+ minions.put("c9a88db53bdf854c29d91b09027904684a6ba638d8007b7ad142a7321b9a212", "Spider II");
+ minions.put("6be5128d61371acc4eabd3590013a5b8bfc678366e61c5363bf41a8b0154efdc", "Spider III");
+ minions.put("4ef774366eef0ae26c9da09f52d101fd3a6181f62c059d579900d33098968058", "Spider IV");
+ minions.put("eeb537b6d278623a110b4d31784ae415789972fad78bec236aa36f3a5f43a856", "Spider V");
+ minions.put("dc785f2b1cca983928b0fe8ceb700660365b757e93a42a6651a937df773c70af", "Spider VI");
+ minions.put("887abd32a5aae5870821fe0883002cdad26a58f9fee052c7ab9b800ee6e9ac52", "Spider VII");
+ minions.put("4781e95aeb0e31be71e64093a084602de754f3e50443d5a4f685aac00d7a662f", "Spider VIII");
+ minions.put("fe3869503b7fdeaa063351fd3579dbaf6fd3592bd4c30bac1058c9367c6a3823", "Spider IX");
+ minions.put("5a4209e45b623b8338bcd184f15d279558c8a9d756d538e1584911780a60697a", "Spider X");
+ minions.put("62d0262788369b6734e65d0240185affc2ead224a07efbcd70e4c7125d2c5330", "Spider XI");
+
+ minions.put("97e86007064c9ce26eb4bad8ac9aa30aac309e70a9e0b615936318dea40a721", "Tarantula I");
+ minions.put("578fea239bb3881ae53d6c735b8af69d8c6b477c0f5c34bc7cbed5792869ca67", "Tarantula II");
+ minions.put("fc398914acb7fce5d93c2002a258f23b795d2f20d8e5fc555acd5070662efa0b", "Tarantula III");
+ minions.put("bafde429ffcd5141f42b3d754d75a0ad3528594509c09be0083bc2c98d38fdce", "Tarantula IV");
+ minions.put("f78b57faf9b4935932b749e10a2fff66532fecdede5a4e58f80d6f6ace2ed7ed", "Tarantula V");
+ minions.put("ba4c2f24b79f98133f9fd66760685a18d4c29e415cef0b62e67e957085b3875b", "Tarantula VI");
+ minions.put("7affad96dbfb4d5bfd4dc73d4dc1295db0062cbbd0c967b7da39bcc6e051b2e", "Tarantula VII");
+ minions.put("cb0bdd9de5c6d56f3f3341ed7bc07d17f372d5f21b32fbb9cdc67ec7096a7cf0", "Tarantula VIII");
+ minions.put("13cb3afa7d81b71751a246278d4f8f3a406a80a1302291ac620fc42c6cf2c179", "Tarantula IX");
+ minions.put("535cc5773ffb461bc491270af45aa14cda6d7d92a4cc8c12b2b188620a2a44e4", "Tarantula X");
+ minions.put("9c4d0dfb09516a79b286a9e8c67c4e981f245ff6221f470f6452fdafc0a92749", "Tarantula XI");
+
+ minions.put("5d815df973bcd01ee8dfdb3bd74f0b7cb8fef2a70559e4faa5905127bbb4a435", "Cave Spider I");
+ minions.put("677fb9a717ec81d45f89b73f0acd4ee03c9e396601a2de60f2719e4458c7325b", "Cave Spider II");
+ minions.put("7f4912b76e599d12e73e4b03ee51a105999ad1306709fbffcfbaed556a9d7eb0", "Cave Spider III");
+ minions.put("3d90f56d6e1632c00c14d568036aa536073c6a4a7e5759e012bd46d9f3809086", "Cave Spider IV");
+ minions.put("c682c74ba44a5221a70f98188e76a4e88e41f633363a54af1d26247423130636", "Cave Spider V");
+ minions.put("b54735acf9c010f2d25d7af70d600d8bc2633729a4fde7b4ac248c211135f3ab", "Cave Spider VI");
+ minions.put("729095202ca3cd63556e3549f71c39aae4b6718170de19067d6819be4ddecd6e", "Cave Spider VII");
+ minions.put("5c4ec7d3c5084a5c91bdf3fba196a1d12d5bf71049b61b97dd1c5854617a41cf", "Cave Spider VIII");
+ minions.put("42654f0248464e23cf70811a1b1665cad19aa207857f05452967f860458a4c64", "Cave Spider IX");
+ minions.put("4cded81400f3ced561bed776bd44b48e784f7a810ba6cd6340d26f4c00a0c50f", "Cave Spider X");
+ minions.put("36303fc7e2046822ec79a95ce5c7350e58dabd8d776e5c36669f5404581d0459", "Cave Spider XI");
+
+ minions.put("3208fbd64e97c6e00853d36b3a201e4803cae43dcbd6936a3cece050912e1f20", "Blaze I");
+ minions.put("ffcc301b04b1537f040d53fd88a5c16e9e1fde5ea32cd38758059a531b75cb46", "Blaze II");
+ minions.put("da5e196586d751ba7063bcf58d3dc84121e746288cb3c364b4b6f216a6492a27", "Blaze III");
+ minions.put("6ddae5fcdd5ede764f8fe9397b07893ccf3761496f8e2895625581ce54225b00", "Blaze IV");
+ minions.put("f5e3a84c9d6609964b5be8f5f4c96800194677d0f8f43d53a4d2db93dbb66fad", "Blaze V");
+ minions.put("e9d7db90d3118ef56c166418a2232100fb4eb0ab5403548cfa63e985d5e0152c", "Blaze VI");
+ minions.put("a9bdeb530d09ee73479db19b357597318eac92ee7855740e46a1b97ae682b27", "Blaze VII");
+ minions.put("d7fc92fa962d0944ce46b71bc7dcb73a5f51f9d8a7e2bcccf666f2da05a0152d", "Blaze VIII");
+ minions.put("a2a246dbcc45be4a936a19b44fcb61725c0fe2372a0ce0676fb08fd54d4d899b", "Blaze IX");
+ minions.put("ea357aeaf75a8cfed2b3c1c8f3ccf54f907ae2b64fa871cf201baeef53528e19", "Blaze X");
+ minions.put("e791eb26b39f162f552d539a4d22c4bee8aa9c571d9acf82a012593bb945c360", "Blaze XI");
+
+ minions.put("18c9a7a24da7e3182e4f62fa62762e21e1680962197c7424144ae1d2c42174f7", "Magma Cube I");
+ minions.put("212ff47f5c8b38e96e940b9957958e37d610918df9b664b0c11bd6246799f4af", "Magma Cube II");
+ minions.put("376d0b9eb9e5633d21424f6eaade8bd4124b9c91f3fa1f6be512fe0b51d6a013", "Magma Cube III");
+ minions.put("69890974664089d1d08a34d5febead4bb34508f902aa624e0be02b61d0178b7f", "Magma Cube IV");
+ minions.put("5a74333ed5c54aef95aead60c21e541131d797d3f0d7a647915d7a03bbe4a5fe", "Magma Cube V");
+ minions.put("5de0153aa18d34939b7d297c110e7a207779908cee070e3278a3d4dc9e97b122", "Magma Cube VI");
+ minions.put("bf77572393b4b420559f17a56cb55f9ec47c3e9958403184699dba27d12f3ef2", "Magma Cube VII");
+ minions.put("365c702393988e0312f56c00c6e73c8cf510b89df05ad766a65b36a1f281b604", "Magma Cube VIII");
+ minions.put("76101f4bb000518bbedc4b1147a920a99f141b8a679f2984fb94741a33eed69f", "Magma Cube IX");
+ minions.put("e9e67c3860cc1d36cb4930e0ae0488c64abc4e910b4224dc9160d273c3af0bba", "Magma Cube X");
+ minions.put("6ab2af6b08c3acedd2328e152ef7177f6bbb617dc985dfbfecdc982e04939b04", "Magma Cube XI");
+
+ minions.put("e460d20ba1e9cd1d4cfd6d5fb0179ff41597ac6d2461bd7ccdb58b20291ec46e", "Enderman I");
+ minions.put("e38b1bacbce1c6fa1928a89d443868a40a98da7b4507801993b1ab9bb9115458", "Enderman II");
+ minions.put("2f2e4d0850b0d87c0b6a2d361b630960ff9165a47893c287eddf3eda2caa101b", "Enderman III");
+ minions.put("2b37ae94f463c642d7c0caf3da5b95b4b7568c47daad99337ecefdeb25be5d9d", "Enderman IV");
+ minions.put("9dd3f4532c428d0589bac809463b76e15e6fa31bccd2d5e350aa7d506b792904", "Enderman V");
+ minions.put("89f50d3955bec550def51df0e4e143cda3d71314f9a7288dd92e0079605b5363", "Enderman VI");
+ minions.put("368c2e2d9827cb25bf4add695f668180bb2b52d41342f175bdfeb142f960d712", "Enderman VII");
+ minions.put("84c91f6c71b6f75b7540134cb4d36b7e3c5ff8f26b6919a7410fe3427663b7dd", "Enderman VIII");
+ minions.put("c70a920c4940a1ffaebcc20b87afaaf0b17ebc4d3b1c34dfd0374a0a583de32d", "Enderman IX");
+ minions.put("ecaf73a2cd819331d8096caf2f83f65db119692f0600c02d48081ceacf0c864c", "Enderman X");
+ minions.put("86906d7f34af69a797ddf5b5a5b1c428f77284451c67e788caf08070e3008ad", "Enderman XI");
+
+ minions.put("2478547d122ec83a818b46f3b13c5230429559e40c7d144d4ec225f92c1494b3", "Ghast I");
+ minions.put("cd35bd7c4dd1792eeb85ee0a54645cd4e466c8b7b35d71dde4a4d51dfbbdb13f", "Ghast II");
+ minions.put("e1fb348c7c14e174b19d14c8c77d282f1abe4c792519b376cd0622a777b68200", "Ghast III");
+ minions.put("1b0c2e0852f7369ea7d3fe04eb17eff41bb35a1a8a034834369e1e624c79c03", "Ghast IV");
+ minions.put("a3c5c52a4c945825e4c959c5cb6aa607a0e3a1bffd5cb6a0577e172d0f356a2b", "Ghast V");
+ minions.put("ef97eff2721dc201b23373afc3111eda22a325c08de6a14f03dcfcb98d3c9507", "Ghast VI");
+ minions.put("5836df340405415ad7d8b84bbe0e806d0cfed990796c3ade38934169a48ebd25", "Ghast VII");
+ minions.put("7f2e537ca12c9d8bd0ec6bd56ac8bdae86521e960b5852b0bbb31b2cc83dfc7e", "Ghast VIII");
+ minions.put("af4d8d82f4d86569c70d265f5cf62e46ee8dc0a5a6d97ef1901c793d0b127545", "Ghast IX");
+ minions.put("4013d128e7116812388b789ec641d31d48bf10aa862f7d63d2b4fc0a03d147a2", "Ghast X");
+ minions.put("5840896c78884ebb35103b31ffd7276c941ea862b8b6b0e0810a66b4ed66cbc2", "Ghast XI");
+
+ minions.put("c95eced85db62c922724efca804ea0060c4a87fcdedf2fd5c4f9ac1130a6eb26", "Slime I");
+ minions.put("4a3ea6b0c297c5156249353ff7fcf57b1175e1b90b56a815aa039009ff0ea04f", "Slime II");
+ minions.put("b6b35286eb19278b65c61a98ed28e04ca56a58386161b1ae6a347c7181cda73b", "Slime III");
+ minions.put("7afc7e081dcc29042129e1b17a10baa05c8e74432600465bf75b31c99bab3fae", "Slime IV");
+ minions.put("f0d0c0365bc692b560d8e33e9ef6e232c65907957f6bec4733e3efa4ed03ef58", "Slime V");
+ minions.put("a0356eda9d7227d59ad1c8616bad1bed33831670867755e1bc71a240013de867", "Slime VI");
+ minions.put("7266c128064e202143402ac7caee52392e3b003274c25ad8ac5c6773bf863ca2", "Slime VII");
+ minions.put("b967e05936b33c2819d32f3aecbecdd478130fccbe877275e131235968ffb6b2", "Slime VIII");
+ minions.put("827b73cde1cdf73e4393f5177626c681bfaaeaf5c93f9237b6cce4f2f6a74ee8", "Slime IX");
+ minions.put("7a2d1ca7dc1a6d9b3b2ee4cf5641bf4add7419f6ac97060898bd98924ab91589", "Slime X");
+ minions.put("c04c9cb411cfd504c3bc7972fc74acd5045c55e1a76379d40e37f5d73c92e453", "Slime XI");
+
+ minions.put("c2fd8976e1b64aebfd38afbe62aa1429914253df3417ace1f589e5cf45fbd717", "Cow I");
+ minions.put("e4273e1870f9fc54358f7193b7fa3f27fb7bac1d68c9941f63f3c588337b70", "Cow II");
+ minions.put("9c12694906b281c988312cf0575d93274c178a0449b71eff047de1eeb01e3b64", "Cow III");
+ minions.put("e7b32af9f116a425c7394d23dd851f3bff53f05ec413fb2fce3839533d925a86", "Cow IV");
+ minions.put("7b412e13e1eba6d84336aee778115f183b88cbbe546b83ea64c5b6295145355a", "Cow V");
+ minions.put("a63ba85ccc57534108199cb2034826d9853e40df3a8edaf6452326b73748e22a", "Cow VI");
+ minions.put("fd9cb1a9c54e00d1030a101c961f1f516c145b719f4ec8e7d4ab3c9759ae10f3", "Cow VII");
+ minions.put("9e28cd7376398c57887bc326c14c04c9c5796f613d7de9565d5e66c5b12c4d41", "Cow VIII");
+ minions.put("cfa251097580c0d8d26e93e446f28469ae7b5f1208e559626683b4a5ecf5e0e2", "Cow IX");
+ minions.put("3e3f56f3924106eb91414a8859e76b0962395dffaeb91ebda538332fd9774cea", "Cow X");
+ minions.put("cbe1ed84b41681fff45a60cb57b884e6bf4ecc23df2aa6cb112f74d3cb52e315", "Cow XI");
+
+ minions.put("a9bb5f0c56408c73cfa412345c8fc51f75b6c7311ae60e7099c4781c48760562", "Pig I");
+ minions.put("13d136654297e744ccb3ba71bb85bd7653267db4b9b940b621be587d52a51310", "Pig II");
+ minions.put("d0215bbadccec19fc11b04d10958eedea0cb2957479d60d092fcb7339e0d3a3d", "Pig III");
+ minions.put("8a591979d1f27c834b837482ff077dd6ae60603af1d42efd54fd0fe423f473b2", "Pig IV");
+ minions.put("c6dcf14cfaee6c9a5aef79f7cfe7f0a05f6d1d51c0ae9f93e44945a99d7b67e9", "Pig V");
+ minions.put("d3054be358caefe2b9c049159144dbd94de0bdefab4fa07472d8d8f3b22a1edc", "Pig VI");
+ minions.put("73c5582b39fc6c08d4adc8c27bd7b9fc1340073ace1d5571276f57bfc852d864", "Pig VII");
+ minions.put("6be861ec200f4741fb5a202c31b94c345417b7b85bc3e5dd595fdedf387a5559", "Pig VIII");
+ minions.put("caa9f8b050d5f71bb638398af11fe0c6f523251b4d8ff262979248933e2ac7b1", "Pig IX");
+ minions.put("4a466ca591bfe16022be2a6f8aeb2c6321913fd6ad5cb9f40f5e0058521b0d3a", "Pig X");
+ minions.put("9281a6db6bec7d3d5f05f3bbec4eca94ba2073863b0ec2fa853c0c8f28c97629", "Pig XI");
+
+ minions.put("a04b7da13b0a97839846aa5648f5ac6736ba0ca9fbf38cd366916e417153fd7f", "Chicken I");
+ minions.put("7ae39f29a0cc4d8ac8277e7a4e6d56b0e42f04267a9f9033fcba109751ebfff5", "Chicken II");
+ minions.put("2fdacd78fce2c6c70cd020dd0cf69481582d97796abcda0a282e1f7e1a9ab6f3", "Chicken III");
+ minions.put("c968476a306df54c26053b639de69e1473b5b453a4f84cf371f675ba794314da", "Chicken IV");
+ minions.put("597ca4daa25ad8a48eb0a34a23000971f87fe42319c32375c21dea940ffffd5e", "Chicken V");
+ minions.put("7a6ed3e94cc354164f759c448f39cc0ac0ee50feae2e4008e26c890a8387f7e", "Chicken VI");
+ minions.put("c1c9ed510850622947e215dbd9b018939a7908595c645c8415fc8e4e5ce714d", "Chicken VII");
+ minions.put("c3812cb86fe22971d0ae58789f18a1d208116cb204329aff7905aa3993b0d0d8", "Chicken VIII");
+ minions.put("9f24c0d1e3aa3c2999a1268fcc0f933591a9910437f082b1d5dc9bed7ee1a753", "Chicken IX");
+ minions.put("4212ce883dfd2bec43e6cd9b7a7f86be1cca8ebceb33b83e3e70ad873717be18", "Chicken X");
+ minions.put("d5c12fd3968d389f6d053b1a7a85dc1cfb90a39385c379e3afee6295aaafcd37", "Chicken XI");
+
+ minions.put("fd15d4b8bce708f77f963f1b4e87b1b969fef1766a3e9b67b249c59d5e80e8c5", "Sheep I");
+ minions.put("deaee0de135a24a27b8920ddc1c7b58314ffaba3ef3f4cf0d77195936d471c20", "Sheep II");
+ minions.put("c33da48269f28698c4548c1dbb8773f8e49888afd93af5f5b420e0f43c39f2eb", "Sheep III");
+ minions.put("3bd2c5fe2fe9be577c9034d3abfdbd3e90c697deebf9cd35107786bd4dd0555b", "Sheep IV");
+ minions.put("f7b64375097693c11215acd72c429d2770e746178aa4014066285974fdacdaa6", "Sheep V");
+ minions.put("aea49ac4e8f88bbf0321b55ed264df0d527952ce49c387fdebdedc5d6447376", "Sheep VI");
+ minions.put("d23301e0358c2c33e55011cec3a848c6cb4f3c8a016968ffc55190ff2d813c85", "Sheep VII");
+ minions.put("f04de71765e46be9bcdb6c499f954c9bc831563d52776c593c16c99113bcb2d9", "Sheep VIII");
+ minions.put("eea074e9e53cb179da2ebd625de042b70cb0d8cc7280fc101c7cafb9abe07680", "Sheep IX");
+ minions.put("3f92d454109855656d16061c8947760ce84a9561863481292ce8aa60b981911c", "Sheep X");
+ minions.put("6abba939e3a292203108d09da6a867dcf77cef01a5e6e77bcf9cfac5360b0e88", "Sheep XI");
+
+ minions.put("ef59c052d339bb6305cad370fd8c52f58269a957dfaf433a255597d95e68a373", "Rabbit I");
+ minions.put("95beb50764cd6b3bd9cdad5c8788762dde5b8aca1cd47b9ebdeaf4ab62046022", "Rabbit II");
+ minions.put("4caf38c59c689f162a1fedba239a6e44fd6c65c103038c91c0d32e5713a0694c", "Rabbit III");
+ minions.put("a5253184a1665ef0e1ab9da27dcfff2bdbde45836e5b26fc860cee9c2eccf741", "Rabbit IV");
+ minions.put("cd465a0e504286b0dcea425e599e8296c442138cefcea98c76cd966fe53d0639", "Rabbit V");
+ minions.put("1fe6e315e28258a79ec55c5a12f2ec58fe3fa3b735517779eaa888db219f305b", "Rabbit VI");
+ minions.put("c110ae6f601c71a6a779a2943a33546dc08adaac4fdfd54cfc4a98aa90ca12fb", "Rabbit VII");
+ minions.put("e553b809b5164816aa61d5e39f8998d59fac4a35ff01c54d8a16b16627b06403", "Rabbit VIII");
+ minions.put("26e6ecd9f7dfd5ee99a7964e0e404953a29907acca4d6b165aa2ef9807119fe0", "Rabbit IX");
+ minions.put("3ccfa391def65b86e90f1938c98f1dc5874e9cc94e3eefce91ba40a202de4e69", "Rabbit X");
+ minions.put("7f3fdd04826405dec5c17d0f688e874e7ba9bfbdead28b7ed5a0463335629697", "Rabbit XI");
+
+ minions.put("57e4a30f361204ea9cded3fbff850160731a0081cc452cfe26aed48e97f6364b", "Oak I");
+ minions.put("bb4eccf762baf18f2d5b5b0c8fa9ca2ce1150f8beb1ce66756a4884c68253d9a", "Oak II");
+ minions.put("a306123edb86a30535267a12ba6ab13558d93abad973793dba6c82c929dfb430", "Oak III");
+ minions.put("c643dd831a5d5e409b22f721bd4a6d1e1109b1b24e1fbafeeb0d2aba8c626ce9", "Oak IV");
+ minions.put("553cbf53549d02cd342aafa13534617514a363ae74db94834fced3a8dd3801b8", "Oak V");
+ minions.put("3497c3ff3cf509495bbf59884f8ecae2148ee391a589d4e20bbcb7872d55373f", "Oak VI");
+ minions.put("c22238ee3f8a38acb4bd05a68479b9b478967eecd51547631c553733c20f6bd9", "Oak VII");
+ minions.put("fcf9f335bc5c68cf1bb1590d421e8564b942ed94d3c2b4025c1b30168981214e", "Oak VIII");
+ minions.put("93b2cb6e9ec862139600e83505e6b56e07838abb1b6faf4649db9a7098096d20", "Oak IX");
+ minions.put("546f4040054a097956bf7e135656ea8f52c53acaebddbddbff8d123231c82e93", "Oak X");
+ minions.put("e613f991f92bd0cf700cfee9a1440ff4dfe89999792e1eb9698b406549761180", "Oak XI");
+
+ minions.put("7ba04bfe516955fd43932dcb33bd5eac20b38a231d9fa8415b3fb301f60f7363", "Spruce I");
+ minions.put("3cc4e6fa46cd52a6480dc2eac053e9ac8a7d6ee0ee9c9cf74e176b289a43eb3a", "Spruce II");
+ minions.put("b2d2366357a435a230fbbdd55929c23dd4985a8978020102255b7a007476fa56", "Spruce III");
+ minions.put("2c188216e275281e49e64a32b787463dff849e3f6f05ae307f4b21f68be28232", "Spruce IV");
+ minions.put("bdb2fcbf4be4a110b814d93fe8093ba66badabb6d65c58846a731935fa0228f0", "Spruce V");
+ minions.put("5b2efe8fe599598326b4941c2ff55c284ce26b0948b520c0490de8b0d9aeff4a", "Spruce VI");
+ minions.put("e1b1af499ef6a63dc5b111e955c3ad7b4647841135df7953c1d441955540a6a4", "Spruce VII");
+ minions.put("ed3f7f42298490fcf71e27a7b4c5ed5f2c556c58c97fd0f2e3460488d32938c7", "Spruce VIII");
+ minions.put("999cbe069cd5fc2368e41c9dd073d1aedaa8e5465276d4b8852ac5a917bbdda8", "Spruce IX");
+ minions.put("74ba98e2b81e9426e5f1f44b63559633b3b2ab416a72cbc3b6cb4d527aaad8cd", "Spruce X");
+ minions.put("da54f11da358d14fa11e2c32eb1b93d9444eabcd600e32cc0ab462172a1f12c", "Spruce XI");
+
+ minions.put("eb74109dbb88178afb7a9874afc682904cedb3df75978a51f7beeb28f924251", "Birch I");
+ minions.put("6dd53989833505625fa9fc5ce5d4c8a745f25201e58d56cc6f94125c78606a91", "Birch II");
+ minions.put("6ed87a6d743d9e036b169b03973c5772b611db48f5c6844f1f427ffa702c12ef", "Birch III");
+ minions.put("ac49f5616584ddb09b46e2d9eba91228c5c55d81dd557c8bf84f7ead7e74578a", "Birch IV");
+ minions.put("1a1fb86ed5a7d5bddcee9593eed7142f68b4fb55a8b812d0bfaa765e2162138d", "Birch V");
+ minions.put("7b79821acb2d8dd8bc54ac77ee6486d6bd21f5e20c828f84973325d6b3f2eb41", "Birch VI");
+ minions.put("292863ce28af7319e7181be85be55c43be21d3efba789f4768cffaefd488206f", "Birch VII");
+ minions.put("8f85e3656474430d5cca86f73c474aa647d78594791fcd5acb8d637f60133164", "Birch VIII");
+ minions.put("5e07676b749e912c6299bdb05904aba8fc6df91eb9494376957fcf0f745be295", "Birch IX");
+ minions.put("d0d6563ad8a3f57870674b7ed87069401016be21cc43625850197db8d299482d", "Birch X");
+ minions.put("c7461229df076f8137a4560b38365ae48430b01070b90221aa5846284c17b876", "Birch XI");
+
+ minions.put("5ecdc8d6b2b7e081ed9c36609052c91879b89730b9953adbc987e25bf16c5581", "Dark Oak I");
+ minions.put("b25860cc1423ab010cf17697b288fdd3f5cb725ea9ab3e88a499dc1938104b02", "Dark Oak II");
+ minions.put("2ecb65fceae74d76106b02eaa31bd80cc26b3f88d32372b645658d337352b42", "Dark Oak III");
+ minions.put("358db48413f01eb669ac98a4cb0884021307886e29048a072d27e4f73e1ea6fe", "Dark Oak IV");
+ minions.put("cf0969d586970c7ed5fef0c44d2899cfc97780488a36d725d55a6569dd02fa3c", "Dark Oak V");
+ minions.put("299b2d8c62b17108023c57e2bc40873446e1b96f11674a2bb2a27f915cf9d519", "Dark Oak VI");
+ minions.put("3ee074f5bb1680686d0794506c6c26e8f6acf1117b015ad3441aa938c9dcc8d", "Dark Oak VII");
+ minions.put("fd20485516e15e9c7ade2529848ebee04a9242fea2e2eefa4b336e7bd9177af1", "Dark Oak VIII");
+ minions.put("c0cde69130063d80dcd974d96ac02af355deeb1a5391fa14cbabecb530924ad3", "Dark Oak IX");
+ minions.put("9fc5b2ee7d07de80538e77d651c9190eeafea9ef3dfe094589f70117c4d4ed07", "Dark Oak X");
+ minions.put("23c650b69189a1da2a0a9e9d0a235cb89df0f32ab421ad059e012be59638057f", "Dark Oak XI");
+
+ minions.put("42183eaf5b133b838db13d145247e389ab4b4f33c67846363792dc3d82b524c0", "Acacia I");
+ minions.put("9609bcfecad73c84dd957673439a7f56426269fc569b6e8405d1a1c05ced8557", "Acacia II");
+ minions.put("85c6492e5b0e3315fbdfd2314ee98073abdcdcbec36b4915b40e0943a95d726", "Acacia III");
+ minions.put("4afae3d06cb1510d931c3b213854549614983e6d8e2440ce76b683960aab69f6", "Acacia IV");
+ minions.put("f06b64b7743a20fc36f2aaa0908d64346540af97e38d8263acf5b53e4e4a16fe", "Acacia V");
+ minions.put("836bc401455a23aed7f83b6ae46f2bcd52809a153bb5888b04a7dca3a702f531", "Acacia VI");
+ minions.put("572b1b70882093a9d19c96e9dd7db8bd51aa117f5b5bbbc27e3bafb9e1c1167", "Acacia VII");
+ minions.put("10a919b3efd2521fc823b2da1246568d5e83dc1f6908ac128d19cde5d326d469", "Acacia VIII");
+ minions.put("2f0b33a2ab3e165a193d33e148f61384d01ed45d9edabbf1e55a3016ccd991f5", "Acacia IX");
+ minions.put("9b4826120105ca75f208c3b97225245033e156a61fb53ecebc3fa6e1baaba919", "Acacia X");
+ minions.put("4f6e34656f238ed0d6823fc31cb16455f79aa9756884225d6ce4ef681c8240eb", "Acacia XI");
+
+ minions.put("2fe73d981690c1be346a16331819c4e8800859fcdc3e5153718c6ad45861924c", "Jungle I");
+ minions.put("61a133a359788b12655cfb9abd3eb71532d231052f5bb213fd05930d2ee4937", "Jungle II");
+ minions.put("9829fa43121066bc01344745f889c67f8e80a75ba30a38e017d2393e17cfef21", "Jungle III");
+ minions.put("95ca25a3b4fc31454da307a4e98c09455efaaa9f2c074b066a98300764e2690b", "Jungle IV");
+ minions.put("20d26c2e29b2205c620b8b60fbaa056942d5417b75a2acc7f4c581b0e9bc6d", "Jungle V");
+ minions.put("b8619464d104822d9937344d11ee5c037169a13b2473f59b24836fca4cf214c5", "Jungle VI");
+ minions.put("d7113a0d8e635447ef7b1908cab69d6fd68c010f1fc08b9db4d2612a35e65646", "Jungle VII");
+ minions.put("24606b1daf8e60363fc8db71ef204262ee800fa7b6496fb2e05f57d0674ef51f", "Jungle VIII");
+ minions.put("a4bbeb118757923d36871c835779aa71f8790931f64e64f2942ad3306aee59ad", "Jungle IX");
+ minions.put("3ee34e1469da11fe6c44f2ca90dc9b2861a1e7b98594cb344d86824eeeabcb60", "Jungle X");
+ minions.put("dbefc4e8d5c73d9a9e3fe5b1009f568c5d3cb071fa869b54d2604cadef474505", "Jungle XI");
+ return minions;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/cowtipper/cowlection/data/Friend.java b/src/main/java/de/cowtipper/cowlection/data/Friend.java
new file mode 100644
index 0000000..81d93d7
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/data/Friend.java
@@ -0,0 +1,65 @@
+package de.cowtipper.cowlection.data;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public class Friend {
+ public static final Friend FRIEND_NOT_FOUND = new Friend();
+ private UUID id;
+ private String name;
+ private long lastChecked;
+
+ static {
+ // uuid & name are null
+ FRIEND_NOT_FOUND.setLastChecked(0);
+ }
+
+ /**
+ * No-args constructor for GSON
+ */
+ private Friend() {
+ this.lastChecked = System.currentTimeMillis();
+ }
+
+ public UUID getUuid() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public long getLastChecked() {
+ return lastChecked;
+ }
+
+ public void setLastChecked(long lastChecked) {
+ this.lastChecked = lastChecked;
+ }
+
+ @Override
+ public String toString() {
+ return "Friend{" +
+ "uuid=" + id +
+ ", name='" + name + '\'' +
+ ", lastChecked=" + lastChecked +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Friend friend = (Friend) o;
+ return Objects.equals(id, friend.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/data/HyPlayerData.java b/src/main/java/de/cowtipper/cowlection/data/HyPlayerData.java
new file mode 100644
index 0000000..22e1eaa
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/data/HyPlayerData.java
@@ -0,0 +1,103 @@
+package de.cowtipper.cowlection.data;
+
+import net.minecraft.util.EnumChatFormatting;
+
+public class HyPlayerData {
+ private String displayname;
+ private String rank;
+ private String prefix;
+ private String newPackageRank;
+ private String rankPlusColor;
+ private String monthlyPackageRank;
+ private String monthlyRankColor;
+ private long lastLogin;
+ private long lastLogout;
+ private String mostRecentGameType;
+
+ /**
+ * No-args constructor for GSON
+ */
+ public HyPlayerData() {
+ }
+
+ public String getPlayerName() {
+ return displayname;
+ }
+
+ public String getPlayerNameFormatted() {
+ return getRankFormatted() + " " + displayname;
+ }
+
+ public long getLastLogin() {
+ return lastLogin;
+ }
+
+ public long getLastLogout() {
+ return lastLogout;
+ }
+
+ public String getLastGame() {
+ return DataHelper.GameType.getFancyName(mostRecentGameType);
+ }
+
+ public boolean hasNeverJoinedHypixel() {
+ // example player that has never joined Hypixel (as of April 2020): Joe
+ return rank == null && lastLogin == 0;
+ }
+
+ public boolean hasNeverLoggedOut() {
+ // example player that has no logout value (as of April 2020): Pig (in general accounts that haven't logged in for a few years)
+ return lastLogin != 0 && lastLogout == 0;
+ }
+
+ public boolean isHidingOnlineStatus() {
+ // example players: any higher ranked player (mods, admins, ...)
+ return lastLogin == 0 && lastLogout == 0;
+ }
+
+ /**
+ * Player's Rank prefix: https://github.com/HypixelDev/PublicAPI/wiki/Common-Questions#how-do-i-get-a-players-rank-prefix
+ *
+ * @return formatted rank
+ */
+ private String getRankFormatted() {
+ if (prefix != null) {
+ return prefix;
+ }
+ if (rank != null) {
+ switch (rank) {
+ case "HELPER":
+ return EnumChatFormatting.BLUE + "[HELPER]";
+ case "MODERATOR":
+ return EnumChatFormatting.DARK_GREEN + "[MOD]";
+ case "ADMIN":
+ return EnumChatFormatting.RED + "[ADMIN]";
+ case "YOUTUBER":
+ return EnumChatFormatting.RED + "[" + EnumChatFormatting.WHITE + "YOUTUBE" + EnumChatFormatting.RED + "]";
+ }
+ }
+ if (rankPlusColor == null) {
+ rankPlusColor = "RED";
+ }
+ if (monthlyPackageRank != null && monthlyPackageRank.equals("SUPERSTAR")) {
+ // MVP++
+ EnumChatFormatting rankPlusPlusColor = monthlyRankColor != null ? EnumChatFormatting.getValueByName(monthlyRankColor) : EnumChatFormatting.GOLD;
+ return rankPlusPlusColor + "[MVP" + EnumChatFormatting.getValueByName(rankPlusColor) + "++" + rankPlusPlusColor + "]";
+ }
+ if (newPackageRank != null) {
+ switch (newPackageRank) {
+ case "VIP":
+ return EnumChatFormatting.GREEN + "[VIP]";
+ case "VIP_PLUS":
+ return EnumChatFormatting.GREEN + "[VIP" + EnumChatFormatting.GOLD + "+" + EnumChatFormatting.GREEN + "]";
+ case "MVP":
+ return EnumChatFormatting.AQUA + "[MVP]";
+ case "MVP_PLUS":
+ return EnumChatFormatting.AQUA + "[MVP" + EnumChatFormatting.getValueByName(rankPlusColor) + "+" + EnumChatFormatting.AQUA + "]";
+ default:
+ return EnumChatFormatting.GRAY.toString();
+ }
+ }
+ return EnumChatFormatting.GRAY.toString();
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java b/src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java
new file mode 100644
index 0000000..86fb0a6
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java
@@ -0,0 +1,239 @@
+package de.cowtipper.cowlection.data;
+
+import com.google.common.collect.ComparisonChain;
+import com.mojang.realmsclient.util.Pair;
+import com.mojang.util.UUIDTypeAdapter;
+import de.cowtipper.cowlection.util.Utils;
+
+import java.util.*;
+
+public class HySkyBlockStats {
+ private boolean success;
+ private String cause;
+ private List<Profile> profiles;
+
+ /**
+ * No-args constructor for GSON
+ */
+ private HySkyBlockStats() {
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public String getCause() {
+ return cause;
+ }
+
+ public Profile getActiveProfile(UUID uuid) {
+ if (profiles == null) {
+ return null;
+ }
+ Profile lastSavedProfile = null;
+ long latestSave = -1;
+ for (Profile profile : profiles) {
+ long lastProfileSave = profile.getMember(uuid).last_save;
+ if (latestSave < lastProfileSave) {
+ lastSavedProfile = profile;
+ latestSave = lastProfileSave;
+ }
+ }
+ return lastSavedProfile;
+ }
+
+ public static class Profile {
+ private String cute_name;
+ private Map<String, Member> members;
+ private Banking banking;
+
+ /**
+ * No-args constructor for GSON
+ */
+ private Profile() {
+ }
+
+ public String getCuteName() {
+ return cute_name;
+ }
+
+ public Member getMember(UUID uuid) {
+ return members.get(UUIDTypeAdapter.fromUUID(uuid));
+ }
+
+ public double getCoinBank() {
+ return (banking != null) ? banking.balance : -1;
+ }
+
+ public int coopCount() {
+ return members.size() - 1;
+ }
+
+ public double getCoopCoinPurses(UUID stalkedUuid) {
+ double coopCoinPurses = 0;
+ for (Map.Entry<String, Member> memberEntry : members.entrySet()) {
+ if (memberEntry.getKey().equals(UUIDTypeAdapter.fromUUID(stalkedUuid))) {
+ // don't include stalked player's purse again, only coops' purse
+ continue;
+ }
+ coopCoinPurses += memberEntry.getValue().getCoinPurse();
+ }
+ return coopCoinPurses;
+ }
+
+ public Pair<Integer, Integer> getUniqueMinions() {
+ int uniqueMinions = 0;
+ int membersWithDisabledApi = 0;
+ for (Member member : members.values()) {
+ if (member.crafted_generators != null) {
+ if (uniqueMinions > 0) {
+ --uniqueMinions; // subtract duplicate COBBLESTONE_1 minion
+ }
+ uniqueMinions += member.crafted_generators.size();
+ } else {
+ ++membersWithDisabledApi;
+ }
+ }
+ return Pair.of(uniqueMinions, membersWithDisabledApi);
+ }
+
+ public static class Member {
+ private long last_save;
+ private long first_join;
+ private double coin_purse;
+ private List<String> crafted_generators;
+ private int fairy_souls_collected = -1;
+ private double experience_skill_farming = -1;
+ private double experience_skill_mining = -1;
+ private double experience_skill_combat = -1;
+ private double experience_skill_foraging = -1;
+ private double experience_skill_fishing = -1;
+ private double experience_skill_enchanting = -1;
+ private double experience_skill_alchemy = -1;
+ private double experience_skill_carpentry = -1;
+ private double experience_skill_runecrafting = -1;
+ private double experience_skill_taming = -1;
+ private Map<String, SlayerBossDetails> slayer_bosses;
+ private List<Pet> pets;
+
+ /**
+ * No-args constructor for GSON
+ */
+ private Member() {
+ }
+
+ public Pair<String, String> getFancyFirstJoined() {
+ return Utils.getDurationAsWords(first_join);
+ }
+
+ public double getCoinPurse() {
+ return coin_purse;
+ }
+
+ public int getFairySoulsCollected() {
+ return fairy_souls_collected;
+ }
+
+ public Map<XpTables.Skill, Integer> getSkills() {
+ Map<XpTables.Skill, Integer> skills = new TreeMap<>();
+ if (experience_skill_farming >= 0) {
+ skills.put(XpTables.Skill.FARMING, XpTables.Skill.FARMING.getLevel(experience_skill_farming));
+ }
+ if (experience_skill_mining >= 0) {
+ skills.put(XpTables.Skill.MINING, XpTables.Skill.MINING.getLevel(experience_skill_mining));
+ }
+ if (experience_skill_combat >= 0) {
+ skills.put(XpTables.Skill.COMBAT, XpTables.Skill.COMBAT.getLevel(experience_skill_combat));
+ }
+ if (experience_skill_foraging >= 0) {
+ skills.put(XpTables.Skill.FORAGING, XpTables.Skill.FORAGING.getLevel(experience_skill_foraging));
+ }
+ if (experience_skill_fishing >= 0) {
+ skills.put(XpTables.Skill.FISHING, XpTables.Skill.FISHING.getLevel(experience_skill_fishing));
+ }
+ if (experience_skill_enchanting >= 0) {
+ skills.put(XpTables.Skill.ENCHANTING, XpTables.Skill.ENCHANTING.getLevel(experience_skill_enchanting));
+ }
+ if (experience_skill_alchemy >= 0) {
+ skills.put(XpTables.Skill.ALCHEMY, XpTables.Skill.ALCHEMY.getLevel(experience_skill_alchemy));
+ }
+ if (experience_skill_carpentry >= 0) {
+ skills.put(XpTables.Skill.CARPENTRY, XpTables.Skill.CARPENTRY.getLevel(experience_skill_carpentry));
+ }
+ if (experience_skill_runecrafting >= 0) {
+ skills.put(XpTables.Skill.RUNECRAFTING, XpTables.Skill.RUNECRAFTING.getLevel(experience_skill_runecrafting));
+ }
+ if (experience_skill_taming >= 0) {
+ skills.put(XpTables.Skill.TAMING, XpTables.Skill.TAMING.getLevel(experience_skill_taming));
+ }
+ return skills;
+ }
+
+ public Map<XpTables.Slayer, Integer> getSlayerLevels() {
+ Map<XpTables.Slayer, Integer> slayerLevels = new EnumMap<>(XpTables.Slayer.class);
+ for (XpTables.Slayer slayerBoss : XpTables.Slayer.values()) {
+ SlayerBossDetails bossDetails = slayer_bosses.get(slayerBoss.name().toLowerCase());
+ int slayerLevel = slayerBoss.getLevel(bossDetails.xp);
+ slayerLevels.put(slayerBoss, slayerLevel);
+ }
+ return slayerLevels;
+ }
+
+ public List<Pet> getPets() {
+ pets.sort((p1, p2) -> ComparisonChain.start().compare(p2.active, p1.active).compare(p2.getRarity(), p1.getRarity()).compare(p2.exp, p1.exp).result());
+ return pets;
+ }
+ }
+
+ private static class SlayerBossDetails {
+ private int xp;
+ }
+
+ public static class Pet {
+ private String type;
+ private double exp;
+ private String tier;
+ private boolean active;
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public DataHelper.SkyBlockRarity getRarity() {
+ return DataHelper.SkyBlockRarity.valueOf(tier);
+ }
+
+ public String toFancyString() {
+ return getRarity().getColor() + Utils.fancyCase(type) + " " + getLevel();
+ }
+
+ private int getLevel() {
+ return XpTables.Pet.getLevel(tier, exp);
+ }
+ }
+
+ public static class Banking {
+ private double balance;
+ // private List<Transaction> transactions;
+
+ /**
+ * No-args constructor for GSON
+ */
+ private Banking() {
+ }
+
+ // private class Transaction {
+ // private int amount;
+ // private long timestamp;
+ // private Transaction.Action action;
+ // private String initiator_name;
+ //
+ // /**
+ // * No-args constructor for GSON
+ // */
+ // private Transaction() {
+ // }
+ // }
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/data/HyStalkingData.java b/src/main/java/de/cowtipper/cowlection/data/HyStalkingData.java
new file mode 100644
index 0000000..45f3344
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/data/HyStalkingData.java
@@ -0,0 +1,120 @@
+package de.cowtipper.cowlection.data;
+
+import de.cowtipper.cowlection.util.Utils;
+import org.apache.commons.lang3.StringUtils;
+
+public class HyStalkingData {
+ private boolean success;
+ private String cause;
+ private HySession session;
+
+ /**
+ * No-args constructor for GSON
+ */
+ private HyStalkingData() {
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public String getCause() {
+ return cause;
+ }
+
+ public HySession getSession() {
+ return session;
+ }
+
+ public static class HySession {
+ private boolean online;
+ private String gameType;
+ private String mode;
+ private String map;
+
+ /**
+ * No-args constructor for GSON
+ */
+ private HySession() {
+ }
+
+ public boolean isOnline() {
+ return online;
+ }
+
+ public String getGameType() {
+ return DataHelper.GameType.getFancyName(gameType);
+ }
+
+ public String getMode() {
+ // modes partially taken from https://api.hypixel.net/gameCounts?key=MOO
+ if (mode == null) {
+ return null;
+ }
+ String gameType = getGameType();
+ if (DataHelper.GameType.BEDWARS.getCleanName().equals(gameType)) {
+ // BedWars related
+ String playerMode;
+ String specialMode;
+ int specialModeStart = StringUtils.ordinalIndexOf(mode, "_", 2);
+ if (specialModeStart > -1) {
+ playerMode = mode.substring(0, specialModeStart);
+ specialMode = mode.substring(specialModeStart + 1) + " ";
+ } else {
+ playerMode = mode;
+ specialMode = "";
+ }
+ String playerModeClean;
+ switch (playerMode) {
+ case "EIGHT_ONE":
+ playerModeClean = "Solo";
+ break;
+ case "EIGHT_TWO":
+ playerModeClean = "Doubles";
+ break;
+ case "FOUR_THREE":
+ playerModeClean = "3v3v3v3";
+ break;
+ case "FOUR_FOUR":
+ playerModeClean = "4v4v4v4";
+ break;
+ case "TWO_FOUR":
+ playerModeClean = "4v4";
+ break;
+ default:
+ playerModeClean = playerMode;
+ }
+ return Utils.fancyCase(specialMode + playerModeClean);
+ } else if (DataHelper.GameType.SKYBLOCK.getCleanName().equals(gameType)) {
+ // SkyBlock related
+ switch (mode) {
+ case "dynamic":
+ return "Private Island";
+ case "hub":
+ return "Hub";
+ case "combat_1":
+ return "Spider's Den";
+ case "combat_2":
+ return "Blazing Fortress";
+ case "combat_3":
+ return "The End";
+ case "farming_1":
+ return "The Barn";
+ case "farming_2":
+ return "Mushroom Desert";
+ case "foraging_1":
+ return "The Park";
+ case "mining_1":
+ return "Gold Mine";
+ case "mining_2":
+ return "Deep Caverns";
+ }
+ }
+ return Utils.fancyCase(mode);
+ }
+
+ public String getMap() {
+ return map;
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/data/LogEntry.java b/src/main/java/de/cowtipper/cowlection/data/LogEntry.java
new file mode 100644
index 0000000..aa59483
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/data/LogEntry.java
@@ -0,0 +1,82 @@
+package de.cowtipper.cowlection.data;
+
+import net.minecraft.util.EnumChatFormatting;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.util.regex.Pattern;
+
+public class LogEntry {
+ private static final Pattern UTF_PARAGRAPH_SYMBOL = Pattern.compile("§");
+ private LocalDateTime time;
+ private Path filePath;
+ private String message;
+
+ public LogEntry(LocalDateTime time, Path filePath, String logEntry) {
+ this.time = time;
+ this.filePath = filePath;
+ this.message = logEntry;
+ }
+
+ public LogEntry(String message) {
+ this.message = message;
+ }
+
+ public LocalDateTime getTime() {
+ return time;
+ }
+
+ public Path getFilePath() {
+ return filePath;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void addLogLine(String logLine) {
+ message += "\n" + logLine;
+ }
+
+ public void removeFormatting() {
+ this.message = EnumChatFormatting.getTextWithoutFormattingCodes(message);
+ }
+
+ public void fixWeirdCharacters() {
+ if (message.contains("§")) {
+ message = UTF_PARAGRAPH_SYMBOL.matcher(message).replaceAll("§");
+ }
+ }
+
+ /**
+ * Is this log entry a 'real' log entry or just an error message from the search process?
+ *
+ * @return true if error message, otherwise false
+ */
+ public boolean isError() {
+ return time == null && filePath == null;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LogEntry logEntry = (LogEntry) o;
+ return new EqualsBuilder()
+ .append(time, logEntry.time)
+ .append(filePath, logEntry.filePath)
+ .append(message, logEntry.message)
+ .isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(time)
+ .append(filePath)
+ .append(message)
+ .toHashCode();
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/data/XpTables.java b/src/main/java/de/cowtipper/cowlection/data/XpTables.java
new file mode 100644
index 0000000..260983e
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/data/XpTables.java
@@ -0,0 +1,253 @@
+package de.cowtipper.cowlection.data;
+
+import java.util.*;
+
+public class XpTables {
+ public enum Skill {
+ FARMING, MINING, COMBAT, FORAGING, FISHING, ENCHANTING, ALCHEMY, CARPENTRY, RUNECRAFTING(true), TAMING;
+ private final boolean alternativeXpFormula;
+ private static final TreeMap<Integer, Integer> XP_TO_LEVEL = new TreeMap<>();
+ private static final TreeMap<Integer, Integer> XP_TO_LEVEL_ALTERNATIVE = new TreeMap<>();
+
+ static {
+ // exp data taken from https://api.hypixel.net/resources/skyblock/skills
+ XP_TO_LEVEL.put(0, 0);
+ XP_TO_LEVEL.put(50, 1);
+ XP_TO_LEVEL.put(175, 2);
+ XP_TO_LEVEL.put(375, 3);
+ XP_TO_LEVEL.put(675, 4);
+ XP_TO_LEVEL.put(1175, 5);
+ XP_TO_LEVEL.put(1925, 6);
+ XP_TO_LEVEL.put(2925, 7);
+ XP_TO_LEVEL.put(4425, 8);
+ XP_TO_LEVEL.put(6425, 9);
+ XP_TO_LEVEL.put(9925, 10);
+ XP_TO_LEVEL.put(14925, 11);
+ XP_TO_LEVEL.put(22425, 12);
+ XP_TO_LEVEL.put(32425, 13);
+ XP_TO_LEVEL.put(47425, 14);
+ XP_TO_LEVEL.put(67425, 15);
+ XP_TO_LEVEL.put(97425, 16);
+ XP_TO_LEVEL.put(147425, 17);
+ XP_TO_LEVEL.put(222425, 18);
+ XP_TO_LEVEL.put(322425, 19);
+ XP_TO_LEVEL.put(522425, 20);
+ XP_TO_LEVEL.put(822425, 21);
+ XP_TO_LEVEL.put(1222425, 22);
+ XP_TO_LEVEL.put(1722425, 23);
+ XP_TO_LEVEL.put(2322425, 24);
+ XP_TO_LEVEL.put(3022425, 25);
+ XP_TO_LEVEL.put(3822425, 26);
+ XP_TO_LEVEL.put(4722425, 27);
+ XP_TO_LEVEL.put(5722425, 28);
+ XP_TO_LEVEL.put(6822425, 29);
+ XP_TO_LEVEL.put(8022425, 30);
+ XP_TO_LEVEL.put(9322425, 31);
+ XP_TO_LEVEL.put(10722425, 32);
+ XP_TO_LEVEL.put(12222425, 33);
+ XP_TO_LEVEL.put(13822425, 34);
+ XP_TO_LEVEL.put(15522425, 35);
+ XP_TO_LEVEL.put(17322425, 36);
+ XP_TO_LEVEL.put(19222425, 37);
+ XP_TO_LEVEL.put(21222425, 38);
+ XP_TO_LEVEL.put(23322425, 39);
+ XP_TO_LEVEL.put(25522425, 40);
+ XP_TO_LEVEL.put(27822425, 41);
+ XP_TO_LEVEL.put(30222425, 42);
+ XP_TO_LEVEL.put(32722425, 43);
+ XP_TO_LEVEL.put(35322425, 44);
+ XP_TO_LEVEL.put(38072425, 45);
+ XP_TO_LEVEL.put(40972425, 46);
+ XP_TO_LEVEL.put(44072425, 47);
+ XP_TO_LEVEL.put(47472425, 48);
+ XP_TO_LEVEL.put(51172425, 49);
+ XP_TO_LEVEL.put(55172425, 50);
+
+ XP_TO_LEVEL_ALTERNATIVE.put(0, 0);
+ XP_TO_LEVEL_ALTERNATIVE.put(50, 1);
+ XP_TO_LEVEL_ALTERNATIVE.put(150, 2);
+ XP_TO_LEVEL_ALTERNATIVE.put(275, 3);
+ XP_TO_LEVEL_ALTERNATIVE.put(435, 4);
+ XP_TO_LEVEL_ALTERNATIVE.put(635, 5);
+ XP_TO_LEVEL_ALTERNATIVE.put(885, 6);
+ XP_TO_LEVEL_ALTERNATIVE.put(1200, 7);
+ XP_TO_LEVEL_ALTERNATIVE.put(1600, 8);
+ XP_TO_LEVEL_ALTERNATIVE.put(2100, 9);
+ XP_TO_LEVEL_ALTERNATIVE.put(2725, 10);
+ XP_TO_LEVEL_ALTERNATIVE.put(3510, 11);
+ XP_TO_LEVEL_ALTERNATIVE.put(4510, 12);
+ XP_TO_LEVEL_ALTERNATIVE.put(5760, 13);
+ XP_TO_LEVEL_ALTERNATIVE.put(7325, 14);
+ XP_TO_LEVEL_ALTERNATIVE.put(9325, 15);
+ XP_TO_LEVEL_ALTERNATIVE.put(11825, 16);
+ XP_TO_LEVEL_ALTERNATIVE.put(14950, 17);
+ XP_TO_LEVEL_ALTERNATIVE.put(18950, 18);
+ XP_TO_LEVEL_ALTERNATIVE.put(23950, 19);
+ XP_TO_LEVEL_ALTERNATIVE.put(30200, 20);
+ XP_TO_LEVEL_ALTERNATIVE.put(38050, 21);
+ XP_TO_LEVEL_ALTERNATIVE.put(47850, 22);
+ XP_TO_LEVEL_ALTERNATIVE.put(60100, 23);
+ XP_TO_LEVEL_ALTERNATIVE.put(75400, 24);
+
+ }
+
+ Skill() {
+ this(false);
+ }
+
+ Skill(boolean alternativeXpFormula) {
+ this.alternativeXpFormula = alternativeXpFormula;
+ }
+
+ public int getLevel(double exp) {
+ if (alternativeXpFormula) {
+ return XP_TO_LEVEL_ALTERNATIVE.floorEntry((int) exp).getValue();
+ } else {
+ return XP_TO_LEVEL.floorEntry((int) exp).getValue();
+ }
+ }
+
+ public static double getSkillAverage(int skillLevelsSum) {
+ return skillLevelsSum / (getSkillCount() * 1d);
+ }
+
+ /**
+ * Amount of skills without cosmetic skills (Carpentry, Runecrafting)
+ *
+ * @return amount of existing skills
+ */
+ private static int getSkillCount() {
+ return values().length - 2;
+ }
+ }
+
+ public enum Slayer {
+ ZOMBIE, SPIDER, WOLF(true);
+ private final boolean alternativeXpFormula;
+ /**
+ * Valid for Zombie + Spider
+ */
+ private static final TreeMap<Integer, Integer> XP_TO_LEVEL = new TreeMap<>();
+ /**
+ * Valid for Wolf
+ */
+ private static final TreeMap<Integer, Integer> XP_TO_LEVEL_ALTERNATIVE = new TreeMap<>();
+
+ static {
+ XP_TO_LEVEL.put(0, 0);
+ XP_TO_LEVEL.put(5, 1);
+ XP_TO_LEVEL.put(15, 2);
+ XP_TO_LEVEL.put(200, 3);
+ XP_TO_LEVEL.put(1000, 4);
+ XP_TO_LEVEL.put(5000, 5);
+ XP_TO_LEVEL.put(20000, 6);
+ XP_TO_LEVEL.put(100000, 7);
+ XP_TO_LEVEL.put(400000, 8);
+ XP_TO_LEVEL.put(1000000, 9);
+
+ XP_TO_LEVEL_ALTERNATIVE.put(0, 0);
+ XP_TO_LEVEL_ALTERNATIVE.put(5, 1);
+ XP_TO_LEVEL_ALTERNATIVE.put(15, 2);
+ XP_TO_LEVEL_ALTERNATIVE.put(200, 3);
+ XP_TO_LEVEL_ALTERNATIVE.put(1500, 4);
+ XP_TO_LEVEL_ALTERNATIVE.put(5000, 5);
+ XP_TO_LEVEL_ALTERNATIVE.put(20000, 6);
+ XP_TO_LEVEL_ALTERNATIVE.put(100000, 7);
+ XP_TO_LEVEL_ALTERNATIVE.put(400000, 8);
+ XP_TO_LEVEL_ALTERNATIVE.put(1000000, 9);
+ }
+
+ Slayer() {
+ this(false);
+ }
+
+ Slayer(boolean alternativeXpFormula) {
+ this.alternativeXpFormula = alternativeXpFormula;
+ }
+
+ public int getLevel(double exp) {
+ if (alternativeXpFormula) {
+ return XP_TO_LEVEL_ALTERNATIVE.floorEntry((int) exp).getValue();
+ } else {
+ return XP_TO_LEVEL.floorEntry((int) exp).getValue();
+ }
+ }
+ }
+
+ public static final class Pet {
+ private static final Map<DataHelper.SkyBlockRarity, TreeSet<Integer>> PET_XP = new HashMap<>();
+
+ private Pet() {
+ }
+
+ static {
+ for (DataHelper.SkyBlockRarity rarity : DataHelper.SkyBlockRarity.getPetRarities()) {
+ PET_XP.put(rarity, new TreeSet<>());
+ }
+ Collections.addAll(PET_XP.get(DataHelper.SkyBlockRarity.COMMON),
+ 0, 100, 210, 330, 460, 605, 765, 940, 1130, 1340, // 1-10
+ 1570, 1820, 2095, 2395, 2725, 3085, 3485, 3925, 4415, 4955, // 11-20
+ 5555, 6215, 6945, 7745, 8625, 9585, 10635, 11785, 13045, 14425, // 21-30
+ 15935, 17585, 19385, 21345, 23475, 25785, 28285, 30985, 33905, 37065, // 31-40
+ 40485, 44185, 48185, 52535, 57285, 62485, 68185, 74485, 81485, 89285, // 41-50
+ 97985, 107685, 118485, 130485, 143785, 158485, 174685, 192485, 211985, 233285, // 51-60
+ 256485, 281685, 309085, 338885, 371285, 406485, 444685, 486085, 530885, 579285, // 61-70
+ 631485, 687685, 748085, 812885, 882285, 956485, 1035685, 1120385, 1211085, 1308285, // 71-80
+ 1412485, 1524185, 1643885, 1772085, 1909285, 2055985, 2212685, 2380385, 2560085, 2752785, // 81-90
+ 2959485, 3181185, 3418885, 3673585, 3946285, 4237985, 4549685, 4883385, 5241085, 5624785); // 91-100
+ Collections.addAll(PET_XP.get(DataHelper.SkyBlockRarity.UNCOMMON),
+ 0, 175, 365, 575, 805, 1055, 1330, 1630, 1960, 2320, // 1-10
+ 2720, 3160, 3650, 4190, 4790, 5450, 6180, 6980, 7860, 8820, // 11-20
+ 9870, 11020, 12280, 13660, 15170, 16820, 18620, 20580, 22710, 25020, // 21-30
+ 27520, 30220, 33140, 36300, 39720, 43420, 47420, 51770, 56520, 61720, // 31-40
+ 67420, 73720, 80720, 88520, 97220, 106920, 117720, 129720, 143020, 157720, // 41-50
+ 173920, 191720, 211220, 232520, 255720, 280920, 308320, 338120, 370520, 405720, // 51-60
+ 443920, 485320, 530120, 578520, 630720, 686920, 747320, 812120, 881520, 955720, // 61-70
+ 1034920, 1119620, 1210320, 1307520, 1411720, 1523420, 1643120, 1771320, 1908520, 2055220, // 71-80
+ 2211920, 2379620, 2559320, 2752020, 2958720, 3180420, 3418120, 3672820, 3945520, 4237220, // 81-90
+ 4548920, 4882620, 5240320, 5624020, 6035720, 6477420, 6954120, 7470820, 8032520, 8644220); // 91-100
+ Collections.addAll(PET_XP.get(DataHelper.SkyBlockRarity.RARE),
+ 0, 275, 575, 905, 1265, 1665, 2105, 2595, 3135, 3735, // 1-10
+ 4395, 5125, 5925, 6805, 7765, 8815, 9965, 11225, 12605, 14115, // 11-20
+ 15765, 17565, 19525, 21655, 23965, 26465, 29165, 32085, 35245, 38665, // 21-30
+ 42365, 46365, 50715, 55465, 60665, 66365, 72665, 79665, 87465, 96165, // 31-40
+ 105865, 116665, 128665, 141965, 156665, 172865, 190665, 210165, 231465, 254665, // 41-50
+ 279865, 307265, 337065, 369465, 404665, 442865, 484265, 529065, 577465, 629665, // 51-60
+ 685865, 746265, 811065, 880465, 954665, 1033865, 1118565, 1209265, 1306465, 1410665, // 61-70
+ 1522365, 1642065, 1770265, 1907465, 2054165, 2210865, 2378565, 2558265, 2750965, 2957665, // 71-80
+ 3179365, 3417065, 3671765, 3944465, 4236165, 4547865, 4881565, 5239265, 5622965, 6034665, // 81-90
+ 6476365, 6953065, 7469765, 8031465, 8643165, 9309865, 10036565, 10828265, 11689965, 12626665); // 91-100
+ Collections.addAll(PET_XP.get(DataHelper.SkyBlockRarity.EPIC),
+ 0, 440, 930, 1470, 2070, 2730, 3460, 4260, 5140, 6100, // 1-10
+ 7150, 8300, 9560, 10940, 12450, 14100, 15900, 17860, 19990, 22300, // 11-20
+ 24800, 27500, 30420, 33580, 37000, 40700, 44700, 49050, 53800, 59000, // 21-30
+ 64700, 71000, 78000, 85800, 94500, 104200, 115000, 127000, 140300, 155000, // 31-40
+ 171200, 189000, 208500, 229800, 253000, 278200, 305600, 335400, 367800, 403000, // 41-50
+ 441200, 482600, 527400, 575800, 628000, 684200, 744600, 809400, 878800, 953000, // 51-60
+ 1032200, 1116900, 1207600, 1304800, 1409000, 1520700, 1640400, 1768600, 1905800, 2052500, // 61-70
+ 2209200, 2376900, 2556600, 2749300, 2956000, 3177700, 3415400, 3670100, 3942800, 4234500, // 71-80
+ 4546200, 4879900, 5237600, 5621300, 6033000, 6474700, 6951400, 7468100, 8029800, 8641500, // 81-90
+ 9308200, 10034900, 10826600, 11688300, 12625000, 13641700, 14743400, 15935100, 17221800, 18608500); // 91-100
+ Collections.addAll(PET_XP.get(DataHelper.SkyBlockRarity.LEGENDARY),
+ 0, 660, 1390, 2190, 3070, 4030, 5080, 6230, 7490, 8870, // 1-10
+ 10380, 12030, 13830, 15790, 17920, 20230, 22730, 25430, 28350, 31510, // 11-20
+ 34930, 38630, 42630, 46980, 51730, 56930, 62630, 68930, 75930, 83730, // 21-30
+ 92430, 102130, 112930, 124930, 138230, 152930, 169130, 186930, 206430, 227730, // 31-40
+ 250930, 276130, 303530, 333330, 365730, 400930, 439130, 480530, 525330, 573730, // 41-50
+ 625930, 682130, 742530, 807330, 876730, 950930, 1030130, 1114830, 1205530, 1302730, // 51-60
+ 1406930, 1518630, 1638330, 1766530, 1903730, 2050430, 2207130, 2374830, 2554530, 2747230, // 61-70
+ 2953930, 3175630, 3413330, 3668030, 3940730, 4232430, 4544130, 4877830, 5235530, 5619230, // 71-80
+ 6030930, 6472630, 6949330, 7466030, 8027730, 8639430, 9306130, 10032830, 10824530, 11686230, // 81-90
+ 12622930, 13639630, 14741330, 15933030, 17219730, 18606430, 20103130, 21719830, 23466530, 25353230); // 91-100
+ }
+
+ public static int getLevel(String rarity, double exp) {
+ TreeSet<Integer> xpToLevels = PET_XP.get(DataHelper.SkyBlockRarity.valueOf(rarity));
+ if (xpToLevels != null) {
+ return xpToLevels.headSet((int) exp, true).size();
+ } else {
+ return -1;
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/handler/DungeonCache.java b/src/main/java/de/cowtipper/cowlection/handler/DungeonCache.java
new file mode 100644
index 0000000..7cef4cd
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/handler/DungeonCache.java
@@ -0,0 +1,52 @@
+package de.cowtipper.cowlection.handler;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.util.TickDelay;
+import net.minecraft.util.EnumChatFormatting;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class DungeonCache {
+ private boolean isInDungeon;
+ private final Map<String, Integer> deathCounter;
+ private final Cowlection main;
+
+ public DungeonCache(Cowlection main) {
+ this.main = main;
+ deathCounter = new HashMap<>();
+ }
+
+ public void onDungeonEntered() {
+ isInDungeon = true;
+ deathCounter.clear();
+ }
+
+ public void onDungeonLeft() {
+ isInDungeon = false;
+ deathCounter.clear();
+ }
+
+ public void addDeath(String playerName) {
+ int previousPlayerDeaths = deathCounter.getOrDefault(playerName, 0);
+ deathCounter.put(playerName, previousPlayerDeaths + 1);
+
+ new TickDelay(this::sendDeathCounts, 1);
+ }
+
+ public boolean isInDungeon() {
+ return isInDungeon;
+ }
+
+ public void sendDeathCounts() {
+ if (deathCounter.isEmpty()) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "☠ Deaths: " + EnumChatFormatting.WHITE + "none \\o/");
+ } else {
+ String deaths = deathCounter.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).map(deathEntry -> " " + EnumChatFormatting.WHITE + deathEntry.getKey() + ": " + EnumChatFormatting.LIGHT_PURPLE + deathEntry.getValue())
+ .collect(Collectors.joining("\n"));
+ main.getChatHelper().sendMessage(EnumChatFormatting.RED, "☠ " + EnumChatFormatting.BOLD + "Deaths:\n" + deaths);
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java b/src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java
new file mode 100644
index 0000000..ae1467a
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java
@@ -0,0 +1,176 @@
+package de.cowtipper.cowlection.handler;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.reflect.TypeToken;
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.command.exception.ApiContactException;
+import de.cowtipper.cowlection.command.exception.MooCommandException;
+import de.cowtipper.cowlection.data.Friend;
+import de.cowtipper.cowlection.util.ApiUtils;
+import de.cowtipper.cowlection.util.GsonUtils;
+import io.netty.util.internal.ConcurrentSet;
+import net.minecraft.command.PlayerNotFoundException;
+import net.minecraft.event.ClickEvent;
+import net.minecraft.event.HoverEvent;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.ChatStyle;
+import net.minecraft.util.EnumChatFormatting;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+public class FriendsHandler {
+ private static final long UPDATE_FREQUENCY_DEFAULT = TimeUnit.HOURS.toMillis(10);
+ private final Cowlection main;
+ private final Set<Friend> bestFriends = new ConcurrentSet<>();
+ private final File bestFriendsFile;
+ private final AtomicInteger bestFriendQueue = new AtomicInteger();
+
+ public FriendsHandler(Cowlection main, File friendsFile) {
+ this.main = main;
+ this.bestFriendsFile = friendsFile;
+ loadBestFriends();
+ updateBestFriends();
+ }
+
+ public boolean isBestFriend(String playerName, boolean ignoreCase) {
+ if (ignoreCase) {
+ return bestFriends.stream().map(Friend::getName).anyMatch(playerName::equalsIgnoreCase);
+ } else {
+ return bestFriends.stream().map(Friend::getName).anyMatch(playerName::equals);
+ }
+ }
+
+ public void addBestFriend(String name) {
+ if (name.isEmpty()) {
+ return;
+ }
+
+ ApiUtils.fetchFriendData(name, friend -> {
+ if (friend == null) {
+ throw new ApiContactException("Mojang", "didn't add " + name + " as a best friend.");
+ } else if (friend.equals(Friend.FRIEND_NOT_FOUND)) {
+ throw new PlayerNotFoundException("There is no player with the name " + EnumChatFormatting.DARK_RED + name + EnumChatFormatting.RED + ".");
+ } else {
+ boolean added = bestFriends.add(friend);
+ if (added) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Added " + EnumChatFormatting.DARK_GREEN + friend.getName() + EnumChatFormatting.GREEN + " as best friend.");
+ saveBestFriends();
+ }
+ }
+ });
+ }
+
+ public boolean removeBestFriend(String name) {
+ boolean removed = bestFriends.removeIf(friend -> friend.getName().equalsIgnoreCase(name));
+ if (removed) {
+ saveBestFriends();
+ }
+ return removed;
+ }
+
+ public Set<String> getBestFriends() {
+ return bestFriends.stream().map(Friend::getName).collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ public Friend getBestFriend(String name) {
+ return bestFriends.stream().filter(friend -> friend.getName().equalsIgnoreCase(name)).findFirst().orElse(Friend.FRIEND_NOT_FOUND);
+ }
+
+ private Friend getBestFriend(UUID uuid) {
+ return bestFriends.stream().filter(friend -> friend.getUuid().equals(uuid)).findFirst().orElse(Friend.FRIEND_NOT_FOUND);
+ }
+
+ public void updateBestFriends() {
+ bestFriends.stream().filter(friend -> System.currentTimeMillis() - friend.getLastChecked() > UPDATE_FREQUENCY_DEFAULT)
+ .forEach(friend1 -> {
+ bestFriendQueue.incrementAndGet();
+ updateBestFriend(friend1, false);
+ });
+ }
+
+ public void updateBestFriend(Friend friend, boolean isCommandTriggered) {
+ ApiUtils.fetchCurrentName(friend, newName -> {
+ if (newName == null) {
+ // skipping friend, something went wrong with API request
+ if (isCommandTriggered) {
+ throw new ApiContactException("Mojang", "couldn't check " + EnumChatFormatting.DARK_RED + friend.getName() + EnumChatFormatting.RED + " (possible) new player name");
+ }
+ } else if (newName.equals(ApiUtils.UUID_NOT_FOUND)) {
+ throw new PlayerNotFoundException("How did you manage to get a unique id on your best friends list that has no name attached to it?");
+ } else if (newName.equals(friend.getName())) {
+ // name hasn't changed, only updating lastChecked timestamp
+ Friend bestFriend = getBestFriend(friend.getUuid());
+ if (!bestFriend.equals(Friend.FRIEND_NOT_FOUND)) {
+ bestFriend.setLastChecked(System.currentTimeMillis());
+ if (isCommandTriggered) {
+ throw new MooCommandException(friend.getName() + " hasn't changed his name");
+ }
+ }
+ } else {
+ // name has changed
+ main.getChatHelper().sendMessage(new ChatComponentText("Your best friend " + EnumChatFormatting.DARK_GREEN + friend.getName() + EnumChatFormatting.GREEN + " changed the name to " + EnumChatFormatting.DARK_GREEN + newName + EnumChatFormatting.GREEN + ".").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GREEN)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://namemc.com/search?q=" + newName))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "View " + EnumChatFormatting.GOLD + newName + EnumChatFormatting.YELLOW + "'s name history on namemc.com")))));
+
+ Friend bestFriend = getBestFriend(friend.getUuid());
+ if (!bestFriend.equals(Friend.FRIEND_NOT_FOUND)) {
+ bestFriend.setName(newName);
+ bestFriend.setLastChecked(System.currentTimeMillis());
+ }
+ }
+ if (isCommandTriggered) {
+ saveBestFriends();
+ } else {
+ int remainingFriendsToCheck = bestFriendQueue.decrementAndGet();
+ if (remainingFriendsToCheck == 0) {
+ // we're done with checking for name changes, save updates to file!
+ saveBestFriends();
+ }
+ }
+ });
+ }
+
+ public synchronized void saveBestFriends() {
+ try {
+ String bestFriendsJsonZoned = GsonUtils.toJson(this.bestFriends);
+ FileUtils.writeStringToFile(this.bestFriendsFile, bestFriendsJsonZoned, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ main.getLogger().error("Couldn't save best friends", e);
+ }
+ }
+
+ private void loadBestFriends() {
+ try {
+ boolean createdNewFile = this.bestFriendsFile.createNewFile();
+
+ this.bestFriends.clear();
+ if (!createdNewFile) {
+ String bestFriendsData = FileUtils.readFileToString(this.bestFriendsFile, StandardCharsets.UTF_8);
+ if (bestFriendsData.length() > 0) {
+ this.bestFriends.addAll(parseJson(bestFriendsData));
+ }
+ }
+ } catch (IOException e) {
+ main.getLogger().error("Couldn't read best friends file " + this.bestFriendsFile, e);
+ } catch (JsonParseException e) {
+ main.getLogger().error("Couldn't parse best friends file " + this.bestFriendsFile, e);
+ }
+ }
+
+ private Set<Friend> parseJson(String bestFriendsData) {
+ Type collectionType = new TypeToken<Set<Friend>>() {
+ }.getType();
+ return GsonUtils.fromJson(bestFriendsData, collectionType);
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java b/src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java
new file mode 100644
index 0000000..2206473
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java
@@ -0,0 +1,47 @@
+package de.cowtipper.cowlection.handler;
+
+import com.google.common.collect.EvictingQueue;
+import de.cowtipper.cowlection.Cowlection;
+
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class PlayerCache {
+ @SuppressWarnings("UnstableApiUsage")
+ private final EvictingQueue<String> nameCache = EvictingQueue.create(50);
+ @SuppressWarnings("UnstableApiUsage")
+ private final EvictingQueue<String> bestFriendCache = EvictingQueue.create(50);
+ private final Cowlection main;
+
+ public PlayerCache(Cowlection main) {
+ this.main = main;
+ }
+
+ public void add(String name) {
+ // remove old entry (if exists) to 'push' name to the end of the queue
+ nameCache.remove(name);
+ nameCache.add(name);
+ }
+
+ public void addBestFriend(String name) {
+ // remove old entry (if exists) to 'push' name to the end of the queue
+ bestFriendCache.remove(name);
+ bestFriendCache.add(name);
+ }
+
+ public void removeBestFriend(String name) {
+ bestFriendCache.remove(name);
+ }
+
+ public SortedSet<String> getAllNamesSorted() {
+ SortedSet<String> nameList = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ nameList.addAll(bestFriendCache);
+ nameList.addAll(nameCache);
+ return nameList;
+ }
+
+ public void clearAllCaches() {
+ nameCache.clear();
+ bestFriendCache.clear();
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java b/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java
new file mode 100644
index 0000000..0b53166
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java
@@ -0,0 +1,194 @@
+package de.cowtipper.cowlection.listener;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.config.MooConfig;
+import de.cowtipper.cowlection.util.Utils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiChat;
+import net.minecraft.client.gui.GuiControls;
+import net.minecraft.client.gui.GuiNewChat;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraft.util.StringUtils;
+import net.minecraftforge.client.event.ClientChatReceivedEvent;
+import net.minecraftforge.client.event.GuiOpenEvent;
+import net.minecraftforge.client.event.GuiScreenEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import org.apache.commons.lang3.CharUtils;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.input.Mouse;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ChatListener {
+ /**
+ * Examples:
+ * - §aFriend > §r§aNAME §r§eleft.§r
+ * - §2Guild > §r§aNAME §r§eleft.§r
+ */
+ private static final Pattern LOGIN_LOGOUT_NOTIFICATION = Pattern.compile("^(?<type>§aFriend|§2Guild) > §r(?<rank>§[0-9a-f])(?<playerName>[\\w]+)(?<joinLeaveSuffix> §r§e(?<joinedLeft>joined|left)\\.)§r$");
+ private static final Pattern CHAT_MESSAGE_RECEIVED_PATTERN = Pattern.compile("^(?:Party|Guild) > (?:\\[.*?] )?(\\w+)(?: \\[.*?])?: ");
+ private static final Pattern PRIVATE_MESSAGE_RECEIVED_PATTERN = Pattern.compile("^From (?:\\[.*?] )?(\\w+): ");
+ private static final Pattern PARTY_OR_GAME_INVITE_PATTERN = Pattern.compile("^[-]+\\s+(?:\\[.*?] )?(\\w+) has invited you ");
+ private static final Pattern DUNGEON_FINDER_JOINED_PATTERN = Pattern.compile("^Dungeon Finder > (\\w+) joined the dungeon group! \\(([A-Z][a-z]+) Level (\\d+)\\)$");
+ private final Cowlection main;
+ private String lastTypedChars = "";
+ private String lastPMSender;
+
+ public ChatListener(Cowlection main) {
+ this.main = main;
+ }
+
+ @SubscribeEvent
+ public void onLogInOutMessage(ClientChatReceivedEvent e) {
+ if (e.type != 2) { // normal chat or system msg (not above action bar)
+ String text = e.message.getUnformattedText();
+ Matcher notificationMatcher = LOGIN_LOGOUT_NOTIFICATION.matcher(e.message.getFormattedText());
+
+ if (MooConfig.doMonitorNotifications() && text.length() < 42 && notificationMatcher.matches()) {
+ // we got a login or logout notification!
+ main.getLogger().info(text);
+
+ String type = notificationMatcher.group("type");
+ String rank = notificationMatcher.group("rank");
+ String playerName = notificationMatcher.group("playerName");
+ String joinLeaveSuffix = notificationMatcher.group("joinLeaveSuffix");
+ String joinedLeft = notificationMatcher.group("joinedLeft");
+
+ boolean isBestFriend = main.getFriendsHandler().isBestFriend(playerName, false);
+ if (isBestFriend) {
+ switch (joinedLeft) {
+ case "joined":
+ main.getPlayerCache().addBestFriend(playerName);
+ break;
+ case "left":
+ main.getPlayerCache().removeBestFriend(playerName);
+ break;
+ }
+ if (MooConfig.showBestFriendNotifications) {
+ // replace default (friend/guild) notification with best friend notification
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, "" + EnumChatFormatting.DARK_GREEN + EnumChatFormatting.BOLD + "Best friend" + EnumChatFormatting.DARK_GREEN + " > " + EnumChatFormatting.RESET + rank + playerName + joinLeaveSuffix);
+ e.setCanceled(true);
+ return;
+ }
+ }
+ if (!MooConfig.showFriendNotifications && "§aFriend".equals(type)) {
+ e.setCanceled(true);
+ } else if (!MooConfig.showGuildNotifications && "§2Guild".equals(type)) {
+ e.setCanceled(true);
+ }
+ } else if (text.length() == 56 && text.startsWith("Your new API key is ")) {
+ // Your new API key is 00000000-0000-0000-0000-000000000000
+ String moo = text.substring(20, 56);
+ if (Utils.isValidUuid(moo)) {
+ MooConfig.moo = moo;
+ main.getConfig().syncFromFields();
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Added updated API key in " + Cowlection.MODNAME + " config!");
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onClickOnChat(GuiScreenEvent.MouseInputEvent.Pre e) {
+ if (Mouse.getEventButton() < 0) {
+ // no button press, just mouse-hover
+ return;
+ }
+ if (e.gui instanceof GuiChat) {
+ if (!Mouse.getEventButtonState() && Mouse.getEventButton() == 1 && Keyboard.isKeyDown(Keyboard.KEY_LMENU)) { // alt key pressed and right mouse button being released
+ IChatComponent chatComponent = Minecraft.getMinecraft().ingameGUI.getChatGUI().getChatComponent(Mouse.getX(), Mouse.getY());
+ if (chatComponent != null) {
+ boolean copyWithFormatting = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT);
+ String chatData;
+ if (copyWithFormatting) {
+ chatData = main.getChatHelper().cleanChatComponent(chatComponent);
+ } else {
+ chatData = StringUtils.stripControlCodes(chatComponent.getUnformattedText());
+ if (chatData.startsWith(": ")) {
+ chatData = chatData.substring(2);
+ }
+ }
+ GuiControls.setClipboardString(chatData);
+ main.getChatHelper().sendAboveChatMessage(EnumChatFormatting.YELLOW + "Copied chat component to clipboard:", "" + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276E" + EnumChatFormatting.RESET + (copyWithFormatting ? chatComponent.getUnformattedText() : chatData) + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276F");
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onReplyToMsg(GuiScreenEvent.KeyboardInputEvent.Pre e) {
+ // TODO Switch to more reliable way: GuiTextField#writeText on GuiChat#inputField (protected field) via reflections [using "Open Command"-key isn't detected currently]
+ if (lastPMSender != null && e.gui instanceof GuiChat && lastTypedChars.length() < 3 && Keyboard.getEventKeyState()) {
+ char eventCharacter = Keyboard.getEventCharacter();
+ if (!CharUtils.isAsciiControl(eventCharacter)) {
+ lastTypedChars += eventCharacter;
+ if (lastTypedChars.equalsIgnoreCase("/r ")) {
+ // replace /r with /msg <last user>
+ main.getChatHelper().sendAboveChatMessage("Sending message to " + lastPMSender + "!");
+ Minecraft.getMinecraft().displayGuiScreen(new GuiChat("/w " + lastPMSender + " "));
+ }
+ } else if (Keyboard.getEventKey() == Keyboard.KEY_BACK) { // Backspace
+ lastTypedChars = lastTypedChars.substring(0, Math.max(lastTypedChars.length() - 1, 0));
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onChatOpen(GuiOpenEvent e) {
+ if (e.gui instanceof GuiChat) {
+ lastTypedChars = "";
+ }
+ }
+
+ @SubscribeEvent
+ public void onChatMsgReceive(ClientChatReceivedEvent e) {
+ if (e.type != 2) {
+ String messageSender = null;
+
+ String message = EnumChatFormatting.getTextWithoutFormattingCodes(e.message.getUnformattedText());
+
+ Matcher privateMessageMatcher = PRIVATE_MESSAGE_RECEIVED_PATTERN.matcher(message);
+ Matcher chatMessageMatcher = CHAT_MESSAGE_RECEIVED_PATTERN.matcher(message);
+ Matcher partyOrGameInviteMatcher = PARTY_OR_GAME_INVITE_PATTERN.matcher(message);
+ Matcher dungeonPartyFinderJoinedMatcher = DUNGEON_FINDER_JOINED_PATTERN.matcher(message);
+ if (privateMessageMatcher.find()) {
+ messageSender = privateMessageMatcher.group(1);
+ this.lastPMSender = messageSender;
+ } else if (chatMessageMatcher.find()) {
+ messageSender = chatMessageMatcher.group(1);
+ } else if (partyOrGameInviteMatcher.find()) {
+ messageSender = partyOrGameInviteMatcher.group(1);
+ } else if (dungeonPartyFinderJoinedMatcher.find()) {
+ messageSender = dungeonPartyFinderJoinedMatcher.group(1);
+ }
+
+ if (messageSender != null) {
+ main.getPlayerCache().add(messageSender);
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onRenderChatGui(RenderGameOverlayEvent.Chat e) {
+ if (e.type == RenderGameOverlayEvent.ElementType.CHAT) {
+ // render message above chat box
+ String[] aboveChatMessage = main.getChatHelper().getAboveChatMessage();
+ if (aboveChatMessage != null) {
+ float chatHeightFocused = Minecraft.getMinecraft().gameSettings.chatHeightFocused;
+ float chatScale = Minecraft.getMinecraft().gameSettings.chatScale;
+ int chatBoxHeight = (int) (GuiNewChat.calculateChatboxHeight(chatHeightFocused) * chatScale);
+
+ int defaultTextY = e.resolution.getScaledHeight() - chatBoxHeight - 30;
+
+ for (int i = 0; i < aboveChatMessage.length; i++) {
+ String msg = aboveChatMessage[i];
+ int textY = defaultTextY - (aboveChatMessage.length - i) * (Minecraft.getMinecraft().fontRendererObj.FONT_HEIGHT + 1);
+ Minecraft.getMinecraft().fontRendererObj.drawStringWithShadow(msg, 2, textY, 0xffffff);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java b/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java
new file mode 100644
index 0000000..228a10b
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java
@@ -0,0 +1,128 @@
+package de.cowtipper.cowlection.listener;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.listener.skyblock.DungeonsListener;
+import de.cowtipper.cowlection.listener.skyblock.SkyBlockListener;
+import de.cowtipper.cowlection.util.GsonUtils;
+import de.cowtipper.cowlection.util.TickDelay;
+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.inventory.ContainerChest;
+import net.minecraft.inventory.IInventory;
+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.minecraftforge.client.event.GuiScreenEvent;
+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.lwjgl.input.Keyboard;
+
+public class PlayerListener {
+ private final Cowlection main;
+ private DungeonsListener dungeonsListener;
+ private SkyBlockListener skyBlockListener;
+ private boolean isOnSkyBlock;
+
+ public PlayerListener(Cowlection main) {
+ this.main = main;
+ }
+
+ @SubscribeEvent
+ public void onKeyboardInput(GuiScreenEvent.KeyboardInputEvent.Pre e) {
+ if (Keyboard.getEventKeyState() && Keyboard.getEventKey() == Keyboard.KEY_C && GuiScreen.isCtrlKeyDown()) {
+ // ctrl + C
+ IInventory inventory;
+ String inventoryName;
+ if (e.gui instanceof GuiChest) {
+ // some kind of chest
+ ContainerChest chestContainer = (ContainerChest) ((GuiChest) e.gui).inventorySlots;
+ inventory = chestContainer.getLowerChestInventory();
+ inventoryName = (inventory.hasCustomName() ? EnumChatFormatting.getTextWithoutFormattingCodes(inventory.getDisplayName().getUnformattedTextForChat()) : inventory.getName());
+ } else if (e.gui instanceof GuiInventory) {
+ // player inventory
+ inventory = Minecraft.getMinecraft().thePlayer.inventory;
+ inventoryName = "Player inventory";
+ } else {
+ // another gui, abort!
+ return;
+ }
+ NBTTagList items = new NBTTagList();
+ for (int slot = 0; slot < inventory.getSizeInventory(); slot++) {
+ ItemStack item = inventory.getStackInSlot(slot);
+ if (item != null) {
+ // slot + item
+ NBTTagCompound tag = new NBTTagCompound();
+ tag.setByte("Slot", (byte) slot);
+ item.writeToNBT(tag);
+ items.appendTag(tag);
+ }
+ }
+ GuiScreen.setClipboardString(GsonUtils.toJson(items));
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied " + items.tagCount() + " items from '" + inventoryName + "' to clipboard!");
+ }
+ }
+
+ @SubscribeEvent
+ public void onServerJoin(FMLNetworkEvent.ClientConnectedToServerEvent e) {
+ main.getVersionChecker().runUpdateCheck(false);
+ new TickDelay(() -> main.getChatHelper().sendOfflineMessages(), 6 * 20);
+ isOnSkyBlock = false;
+ main.getLogger().info("Joined the server");
+ }
+
+ @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 = isOnSkyBlock;
+ isOnSkyBlock = (scoreboardSidebar != null && EnumChatFormatting.getTextWithoutFormattingCodes(scoreboardSidebar.getDisplayName()).startsWith("SKYBLOCK"));
+
+ if (!wasOnSkyBlock && isOnSkyBlock) {
+ // player wasn't on SkyBlock before but now is on SkyBlock
+ main.getLogger().info("Entered SkyBlock! Registering SkyBlock listeners");
+ registerSkyBlockListeners();
+ } else if (wasOnSkyBlock && !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() {
+ main.getDungeonCache().onDungeonLeft();
+ if (dungeonsListener != null) {
+ MinecraftForge.EVENT_BUS.unregister(dungeonsListener);
+ dungeonsListener = null;
+ }
+ if (skyBlockListener != null) {
+ MinecraftForge.EVENT_BUS.unregister(skyBlockListener);
+ skyBlockListener = null;
+ main.getLogger().info("Left SkyBlock");
+ }
+ }
+
+ @SubscribeEvent
+ public void onServerLeave(FMLNetworkEvent.ClientDisconnectionFromServerEvent e) {
+ main.getFriendsHandler().saveBestFriends();
+ main.getPlayerCache().clearAllCaches();
+ unregisterSkyBlockListeners();
+ main.getLogger().info("Left the server");
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java b/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java
new file mode 100644
index 0000000..02d221e
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsListener.java
@@ -0,0 +1,399 @@
+package de.cowtipper.cowlection.listener.skyblock;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.config.MooConfig;
+import de.cowtipper.cowlection.util.TickDelay;
+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.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;
+import net.minecraftforge.client.event.GuiScreenEvent;
+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 org.apache.commons.lang3.StringUtils;
+import org.lwjgl.input.Keyboard;
+
+import java.awt.*;
+import java.util.List;
+import java.util.*;
+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:
+ * <ul>
+ * <li>§7Crit Damage: §c+23% §8(Heavy -3%) §8(+28.75%)</li>
+ * <li>§7Health: §a+107 HP §8(+133.75 HP)</li>
+ * <li>§7Defense: §a+130 §8(Heavy +65) §8(+162.5)</li>
+ * <li>§7Speed: §a-1 §8(Heavy -1)</li>
+ * </ul>
+ * <pre>
+ * | 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 |
+ * </pre>
+ */
+ private final Pattern TOOLTIP_LINE_PATTERN = Pattern.compile("^(?<prefix>(?:" + FORMATTING_CODE + ")+[A-Za-z ]+: " + FORMATTING_CODE + ")(?<statNonDungeon>[+-]?[0-9]+)(?<statNonDungeonUnit>%| HP|)(?: (?<colorReforge>" + FORMATTING_CODE + ")\\((?<reforge>[A-Za-z]+) (?<statReforge>[+-]?[0-9]+)(?<statReforgeUnit>%| HP|)\\))?(?: (?<colorDungeon>" + FORMATTING_CODE + ")\\((?<statDungeon>[+-]?[.0-9]+)(?<statDungeonUnit>%| HP|)\\))?$");
+ /**
+ * Player deaths in dungeon:
+ * <ul>
+ * <li> ☠ [player] disconnected from the Dungeon and became a ghost.</li>
+ * <li> ☠ [player/You] died and became a ghost.</li>
+ * <li> ☠ [player] fell to their death with help from [mob] and became a ghost.</li>
+ * <li> ☠ [player] was killed by [mob] and became a ghost.</li>
+ * <li> ☠ You were killed by [mob] and became a ghost.</li>
+ * </ul>
+ */
+ private final Pattern DUNGEON_DEATH_PATTERN = Pattern.compile("^ ☠ (\\w+) (?:.*?) and became a ghost\\.$");
+
+ 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<String> tooltipIterator = e.toolTip.listIterator();
+
+ String itemQualityBottom = null;
+ while (tooltipIterator.hasNext()) {
+ String line = tooltipIterator.next();
+ Matcher lineMatcher = TOOLTIP_LINE_PATTERN.matcher(line);
+ String lineWithoutFormatting = EnumChatFormatting.getTextWithoutFormattingCodes(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) {
+ }
+ } else if (lineWithoutFormatting.startsWith("Item Ability: ") || lineWithoutFormatting.startsWith("Full Set Bonus: ")) {
+ // stop replacing tooltip entries once we reach item ability or full set bonus
+ break;
+ }
+ }
+ 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<String> toolTip) {
+ ListIterator<String> 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, 280);
+ 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 parties
+ 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<String> 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<String> 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());
+ }
+
+ // Events inside dungeons
+ @SubscribeEvent
+ public void onDungeonsEnterOrLeave(PlayerSetSpawnEvent e) {
+ // check if player has entered or left a SkyBlock dungeon
+ new TickDelay(() -> {
+ Scoreboard scoreboard = e.entityPlayer.worldObj.getScoreboard();
+ ScoreObjective scoreboardSidebar = scoreboard.getObjectiveInDisplaySlot(1);
+ if (scoreboardSidebar == null) {
+ return;
+ }
+ boolean wasInDungeon = main.getDungeonCache().isInDungeon();
+
+ 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(" ⏣")) {
+ boolean isInDungeonNow = lineWithoutFormatting.startsWith(" ⏣ The Catacombs");
+
+ if (!wasInDungeon && isInDungeonNow) {
+ main.getLogger().info("Entered SkyBlock Dungeon!");
+ main.getDungeonCache().onDungeonEntered();
+ } else if (wasInDungeon && !isInDungeonNow) {
+ main.getLogger().info("Leaving SkyBlock Dungeon!");
+ main.getDungeonCache().onDungeonLeft();
+ }
+ return;
+ }
+ }
+ }
+ }, 20); // 1 second delay, making sure scoreboard got sent
+ }
+
+ @SubscribeEvent
+ public void onMessageReceived(ClientChatReceivedEvent e) {
+ if (main.getDungeonCache().isInDungeon() && e.type != 2) { // normal chat or system msg (not above action bar)
+ String text = EnumChatFormatting.getTextWithoutFormattingCodes(e.message.getUnformattedText());
+ Matcher dungeonDeathMatcher = DUNGEON_DEATH_PATTERN.matcher(text);
+ if (dungeonDeathMatcher.matches()) {
+ String playerName = dungeonDeathMatcher.group(1);
+ if (playerName.equals("You")) {
+ playerName = Minecraft.getMinecraft().thePlayer.getName();
+ }
+ main.getDungeonCache().addDeath(playerName);
+ } else if (text.trim().equals("> EXTRA STATS <")) {
+ // dungeon "end screen"
+ new TickDelay(() -> main.getDungeonCache().sendDeathCounts(), 5);
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java b/src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java
new file mode 100644
index 0000000..d6b94fc
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/listener/skyblock/SkyBlockListener.java
@@ -0,0 +1,162 @@
+package de.cowtipper.cowlection.listener.skyblock;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.config.MooConfig;
+import de.cowtipper.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<String> 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")));
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/search/GuiDateField.java b/src/main/java/de/cowtipper/cowlection/search/GuiDateField.java
new file mode 100644
index 0000000..5289841
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/search/GuiDateField.java
@@ -0,0 +1,37 @@
+package de.cowtipper.cowlection.search;
+
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.GuiTextField;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+
+class GuiDateField extends GuiTextField {
+ GuiDateField(int componentId, FontRenderer fontrendererObj, int x, int y, int width, int height) {
+ super(componentId, fontrendererObj, x, y, width, height);
+ }
+
+ LocalDate getDate() {
+ try {
+ return LocalDate.parse(this.getText());
+ } catch (DateTimeParseException e) {
+ return LocalDate.now();
+ }
+ }
+
+ boolean validateDate() {
+ try {
+ LocalDate localDate = LocalDate.parse(this.getText());
+ if (localDate.isAfter(LocalDate.now()) || localDate.isBefore(LocalDate.ofYearDay(2009, 1))) {
+ // searching for things written in the future isn't possible (yet). It is also not possible to perform a search before the existence of mc.
+ setTextColor(0xFFFF3333);
+ return false;
+ }
+ } catch (DateTimeParseException e) {
+ setTextColor(0xFFFF3333);
+ return false;
+ }
+ setTextColor(0xFFFFFF);
+ return true;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/search/GuiSearch.java b/src/main/java/de/cowtipper/cowlection/search/GuiSearch.java
new file mode 100644
index 0000000..48c9212
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/search/GuiSearch.java
@@ -0,0 +1,603 @@
+package de.cowtipper.cowlection.search;
+
+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.Utils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.*;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraftforge.common.ForgeVersion;
+import net.minecraftforge.fml.client.GuiScrollingList;
+import net.minecraftforge.fml.client.config.GuiButtonExt;
+import net.minecraftforge.fml.client.config.GuiCheckBox;
+import net.minecraftforge.fml.client.config.GuiUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.tuple.ImmutableTriple;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.opengl.GL11;
+
+import java.awt.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+import java.util.zip.GZIPInputStream;
+
+public class GuiSearch extends GuiScreen {
+ private static final String SEARCH_QUERY_PLACE_HOLDER = "Search for...";
+ private final File mcLogOutputFile;
+ /**
+ * @see Executors#newCachedThreadPool()
+ */
+ private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
+ 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().setNameFormat(Cowlection.MODID + "-logfilesearcher-%d").build());
+ // data
+ private String searchQuery;
+ private boolean chatOnly;
+ private boolean matchCase;
+ private boolean removeFormatting;
+ /**
+ * Cached results are required after resizing the client
+ */
+ private List<LogEntry> searchResults;
+ private LocalDate dateStart;
+ private LocalDate dateEnd;
+
+ // gui elements
+ private GuiButton buttonSearch;
+ private GuiButton buttonClose;
+ private GuiButton buttonHelp;
+ private GuiCheckBox checkboxChatOnly;
+ private GuiCheckBox checkboxMatchCase;
+ private GuiCheckBox checkboxRemoveFormatting;
+ private GuiTextField fieldSearchQuery;
+ private GuiDateField fieldDateStart;
+ private GuiDateField fieldDateEnd;
+ private SearchResults guiSearchResults;
+ private List<GuiTooltip> guiTooltips;
+ private boolean isSearchInProgress;
+ private String analyzedFiles;
+ private String analyzedFilesWithHits;
+ private boolean areEntriesSearchResults;
+
+ public GuiSearch(File configDirectory) {
+ this.mcLogOutputFile = new File(configDirectory, "mc-log.txt");
+ try {
+ mcLogOutputFile.createNewFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ this.searchQuery = SEARCH_QUERY_PLACE_HOLDER;
+ this.searchResults = new ArrayList<>();
+ this.dateStart = MooConfig.calculateStartDate();
+ this.dateEnd = LocalDate.now();
+ this.chatOnly = true;
+ }
+
+ /**
+ * Adds the buttons (and other controls) to the screen in question. Called when the GUI is displayed and when the
+ * window resizes, the buttonList is cleared beforehand.
+ */
+ @Override
+ public void initGui() {
+ this.guiTooltips = new ArrayList<>();
+
+ this.fieldSearchQuery = new GuiTextField(42, this.fontRendererObj, this.width / 2 - 100, 13, 200, 20);
+ this.fieldSearchQuery.setMaxStringLength(255);
+ this.fieldSearchQuery.setText(searchQuery);
+ if (SEARCH_QUERY_PLACE_HOLDER.equals(searchQuery)) {
+ this.fieldSearchQuery.setFocused(true);
+ this.fieldSearchQuery.setSelectionPos(0);
+ }
+
+ // date field: start
+ this.fieldDateStart = new GuiDateField(50, this.fontRendererObj, this.width / 2 + 110, 15, 70, 15);
+ this.fieldDateStart.setText(dateStart.toString());
+ addTooltip(fieldDateStart, Arrays.asList(EnumChatFormatting.YELLOW + "Start date", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Format: " + EnumChatFormatting.RESET + "year-month-day"));
+ // date field: end
+ this.fieldDateEnd = new GuiDateField(51, this.fontRendererObj, this.width / 2 + 110, 35, 70, 15);
+ this.fieldDateEnd.setText(dateEnd.toString());
+ addTooltip(fieldDateEnd, Arrays.asList(EnumChatFormatting.YELLOW + "End date", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Format: " + EnumChatFormatting.RESET + "year-month-day"));
+
+ // close
+ this.buttonList.add(this.buttonClose = new GuiButtonExt(0, this.width - 25, 3, 22, 20, EnumChatFormatting.RED + "X"));
+ addTooltip(buttonClose, Arrays.asList(EnumChatFormatting.RED + "Close search interface", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Hint:" + EnumChatFormatting.RESET + " alternatively press ESC"));
+ // help
+ this.buttonList.add(this.buttonHelp = new GuiButtonExt(1, this.width - 25 - 25, 3, 22, 20, "?"));
+ addTooltip(buttonHelp, Collections.singletonList(EnumChatFormatting.YELLOW + "Show help"));
+
+ // chatOnly
+ this.buttonList.add(this.checkboxChatOnly = new GuiCheckBox(21, this.width / 2 - 100, 35, " Chatbox only", chatOnly));
+ addTooltip(checkboxChatOnly, Collections.singletonList(EnumChatFormatting.YELLOW + "Should " + EnumChatFormatting.GOLD + "only " + EnumChatFormatting.YELLOW + "results that have " + EnumChatFormatting.GOLD + "appeared in the chat box " + EnumChatFormatting.YELLOW + "be displayed?\n"
+ + EnumChatFormatting.GRAY + "For example, this " + EnumChatFormatting.WHITE + "excludes error messages" + EnumChatFormatting.GRAY + " but still " + EnumChatFormatting.WHITE + "includes messages sent by a server" + EnumChatFormatting.GRAY + "."));
+ // matchCase
+ this.buttonList.add(this.checkboxMatchCase = new GuiCheckBox(20, this.width / 2 - 100, 45, " Match case", matchCase));
+ addTooltip(checkboxMatchCase, Collections.singletonList(EnumChatFormatting.YELLOW + "Should the search be " + EnumChatFormatting.GOLD + "case-sensitive" + EnumChatFormatting.YELLOW + "?"));
+ // removeFormatting
+ this.buttonList.add(this.checkboxRemoveFormatting = new GuiCheckBox(22, this.width / 2 - 100, 55, " Remove formatting", removeFormatting));
+ addTooltip(checkboxRemoveFormatting, Collections.singletonList(EnumChatFormatting.YELLOW + "Should " + EnumChatFormatting.GOLD + "formatting " + EnumChatFormatting.YELLOW + "and " + EnumChatFormatting.GOLD + "color codes " + EnumChatFormatting.YELLOW + "be " + EnumChatFormatting.GOLD + "removed " + EnumChatFormatting.YELLOW + "from the search results?"));
+ // search
+ this.buttonList.add(this.buttonSearch = new GuiButtonExt(100, this.width / 2 + 40, 40, 60, 20, "Search"));
+
+ this.guiSearchResults = new SearchResults(70);
+ this.guiSearchResults.setResults(searchResults);
+
+ this.setIsSearchInProgress(isSearchInProgress);
+
+ boolean isStartDateValid = fieldDateStart.validateDate();
+ boolean isEndDateValid = fieldDateEnd.validateDate();
+ this.buttonSearch.enabled = !isSearchInProgress && this.fieldSearchQuery.getText().trim().length() > 1 && !this.fieldSearchQuery.getText().startsWith(SEARCH_QUERY_PLACE_HOLDER) && isStartDateValid && isEndDateValid && !dateStart.isAfter(dateEnd);
+
+ if (isStartDateValid && isEndDateValid && dateStart.isAfter(dateEnd)) {
+ fieldDateStart.setTextColor(0xFFDD3333);
+ fieldDateEnd.setTextColor(0xFFCC3333);
+ }
+ }
+
+ private <T extends Gui> void addTooltip(T field, List<String> tooltip) {
+ GuiTooltip guiTooltip = new GuiTooltip(field, tooltip);
+ this.guiTooltips.add(guiTooltip);
+ }
+
+ @Override
+ public void updateScreen() {
+ fieldSearchQuery.updateCursorCounter();
+ fieldDateStart.updateCursorCounter();
+ fieldDateEnd.updateCursorCounter();
+ }
+
+ @Override
+ protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException {
+ // allow clicks on 'close' button even while a search is in progress
+ super.mouseClicked(mouseX, mouseY, mouseButton);
+ if (isSearchInProgress) {
+ // search in progress, abort
+ return;
+ }
+ fieldSearchQuery.mouseClicked(mouseX, mouseY, mouseButton);
+ fieldDateStart.mouseClicked(mouseX, mouseY, mouseButton);
+ fieldDateEnd.mouseClicked(mouseX, mouseY, mouseButton);
+ }
+
+ @Override
+ protected void keyTyped(char typedChar, int keyCode) throws IOException {
+ if (isSearchInProgress && keyCode != Keyboard.KEY_ESCAPE) {
+ // search in progress, don't process key typed - but allow escape to exit gui
+ return;
+ }
+ if (dateStart.isBefore(dateEnd)) {
+ fieldDateStart.setTextColor(0xFFFFFFFF);
+ fieldDateEnd.setTextColor(0xFFFFFFFF);
+ }
+ if (keyCode == Keyboard.KEY_RETURN && this.fieldSearchQuery.isFocused()) {
+ // perform search
+ actionPerformed(buttonSearch);
+ } else if (this.fieldSearchQuery.textboxKeyTyped(typedChar, keyCode)) {
+ searchQuery = this.fieldSearchQuery.getText();
+ } else if (this.fieldDateStart.textboxKeyTyped(typedChar, keyCode)) {
+ if (fieldDateStart.validateDate()) {
+ dateStart = fieldDateStart.getDate();
+ }
+ } else if (this.fieldDateEnd.textboxKeyTyped(typedChar, keyCode)) {
+ if (fieldDateEnd.validateDate()) {
+ dateEnd = fieldDateEnd.getDate();
+ }
+ } else if (GuiScreen.isKeyComboCtrlA(keyCode)) {
+ // copy all search results
+ String searchResults = guiSearchResults.getAllSearchResults();
+ if (!searchResults.isEmpty()) {
+ GuiScreen.setClipboardString(searchResults);
+ }
+ } else if (GuiScreen.isKeyComboCtrlC(keyCode)) {
+ // copy current selected entry
+ LogEntry selectedSearchResult = guiSearchResults.getSelectedSearchResult();
+ if (selectedSearchResult != null) {
+ GuiScreen.setClipboardString(EnumChatFormatting.getTextWithoutFormattingCodes(selectedSearchResult.getMessage()));
+ }
+ } else if (keyCode == Keyboard.KEY_C && isCtrlKeyDown() && isShiftKeyDown() && !isAltKeyDown()) {
+ // copy current selected entry with formatting codes
+ LogEntry selectedSearchResult = guiSearchResults.getSelectedSearchResult();
+ if (selectedSearchResult != null) {
+ GuiScreen.setClipboardString(selectedSearchResult.getMessage());
+ }
+ } else {
+ if (keyCode == Keyboard.KEY_ESCAPE) {
+ guiSearchResults = null;
+ }
+ super.keyTyped(typedChar, keyCode);
+ }
+
+ boolean isStartDateValid = fieldDateStart.validateDate();
+ boolean isEndDateValid = fieldDateEnd.validateDate();
+ this.buttonSearch.enabled = !isSearchInProgress && searchQuery.trim().length() > 1 && !searchQuery.startsWith(SEARCH_QUERY_PLACE_HOLDER) && isStartDateValid && isEndDateValid && !dateStart.isAfter(dateEnd);
+
+ if (isStartDateValid && isEndDateValid && dateStart.isAfter(dateEnd)) {
+ fieldDateStart.setTextColor(0xFFDD3333);
+ fieldDateEnd.setTextColor(0xFFCC3333);
+ }
+ }
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
+ this.drawDefaultBackground();
+ this.drawCenteredString(this.fontRendererObj, EnumChatFormatting.BOLD + "Minecraft Log Search", this.width / 2, 2, 0xFFFFFF);
+ this.fieldSearchQuery.drawTextBox();
+ this.fieldDateStart.drawTextBox();
+ this.fieldDateEnd.drawTextBox();
+ this.guiSearchResults.drawScreen(mouseX, mouseY, partialTicks);
+
+ super.drawScreen(mouseX, mouseY, partialTicks);
+
+ for (GuiTooltip guiTooltip : guiTooltips) {
+ if (guiTooltip.checkHover(mouseX, mouseY)) {
+ drawHoveringText(guiTooltip.getText(), mouseX, mouseY, 300);
+ // only one tooltip can be displayed at a time: break!
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void actionPerformed(GuiButton button) throws IOException {
+ if (button == this.buttonClose && button.enabled) {
+ guiSearchResults = null;
+ this.mc.setIngameFocus();
+ }
+ if (isSearchInProgress || !button.enabled) {
+ return;
+ }
+ if (button == this.buttonSearch) {
+ setIsSearchInProgress(true);
+
+ executorService.execute(() -> {
+ try {
+ ImmutableTriple<Integer, Integer, List<LogEntry>> searchResultsData = new LogFilesSearcher().searchFor(this.fieldSearchQuery.getText(), checkboxChatOnly.isChecked(), checkboxMatchCase.isChecked(), checkboxRemoveFormatting.isChecked(), dateStart, dateEnd);
+ this.searchResults = searchResultsData.right;
+ this.analyzedFiles = "Analyzed files: " + EnumChatFormatting.WHITE + searchResultsData.left;
+ this.analyzedFilesWithHits = "Files with hits: " + EnumChatFormatting.WHITE + searchResultsData.middle;
+ if (this.searchResults.isEmpty()) {
+ this.searchResults.add(new LogEntry(EnumChatFormatting.ITALIC + "No results"));
+ areEntriesSearchResults = false;
+ } else {
+ areEntriesSearchResults = true;
+ }
+ } catch (IOException e) {
+ System.err.println("Error reading/parsing file log files:");
+ e.printStackTrace();
+ if (e.getStackTrace().length > 0) {
+ searchResults.add(new LogEntry(StringUtils.replaceEach(ExceptionUtils.getStackTrace(e), new String[]{"\t", "\r\n"}, new String[]{" ", "\n"})));
+ }
+ }
+ Minecraft.getMinecraft().addScheduledTask(() -> {
+ this.guiSearchResults.setResults(this.searchResults);
+ setIsSearchInProgress(false);
+ });
+ });
+ } else if (button == checkboxChatOnly) {
+ chatOnly = checkboxChatOnly.isChecked();
+ } else if (button == checkboxMatchCase) {
+ matchCase = checkboxMatchCase.isChecked();
+ } else if (button == checkboxRemoveFormatting) {
+ removeFormatting = checkboxRemoveFormatting.isChecked();
+ } else if (button == buttonHelp) {
+ this.areEntriesSearchResults = false;
+ this.searchResults.clear();
+ this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Initial setup/Configuration " + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "/moo config"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 1) " + EnumChatFormatting.RESET + "Configure directories that should be scanned for log files (\"Directories with Minecraft log files\")"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 2) " + EnumChatFormatting.RESET + "Set default starting date (\"Start date for log file search\")"));
+ this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Performing a search " + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "/moo search"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 1) " + EnumChatFormatting.RESET + "Enter search term"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 2) " + EnumChatFormatting.RESET + "Adjust start and end date"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 3) " + EnumChatFormatting.RESET + "Select desired options (match case, ...)"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 4) " + EnumChatFormatting.RESET + "Click 'Search'"));
+ this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Search results"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + C " + EnumChatFormatting.RESET + "to copy selected search result"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + Shift + C " + EnumChatFormatting.RESET + "to copy selected search result " + EnumChatFormatting.ITALIC + "with" + EnumChatFormatting.RESET + " formatting codes"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + A " + EnumChatFormatting.RESET + "to copy all search results"));
+ this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "Double click search result " + EnumChatFormatting.RESET + "to open corresponding log file in default text editor"));
+ this.guiSearchResults.setResults(searchResults);
+ }
+ }
+
+ private void setIsSearchInProgress(boolean isSearchInProgress) {
+ this.isSearchInProgress = isSearchInProgress;
+ buttonSearch.enabled = !isSearchInProgress;
+ fieldSearchQuery.setEnabled(!isSearchInProgress);
+ fieldDateStart.setEnabled(!isSearchInProgress);
+ fieldDateEnd.setEnabled(!isSearchInProgress);
+ checkboxChatOnly.enabled = !isSearchInProgress;
+ checkboxMatchCase.enabled = !isSearchInProgress;
+ checkboxRemoveFormatting.enabled = !isSearchInProgress;
+ if (isSearchInProgress) {
+ fieldSearchQuery.setFocused(false);
+ fieldDateStart.setFocused(false);
+ fieldDateEnd.setFocused(false);
+ buttonSearch.displayString = EnumChatFormatting.ITALIC + "Searching";
+ searchResults.clear();
+ guiSearchResults.clearResults();
+ analyzedFiles = null;
+ analyzedFilesWithHits = null;
+ } else {
+ buttonSearch.displayString = "Search";
+ }
+ }
+
+ private void drawHoveringText(List<String> textLines, int mouseX, int mouseY, int maxTextWidth) {
+ if (ForgeVersion.getBuildVersion() < 1808) {
+ // we're running a forge version from before 24 March 2016 (http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.9.html for reference)
+ // using mc built-in method
+ drawHoveringText(textLines, mouseX, mouseY, fontRendererObj);
+ } else {
+ // we're on a newer forge version, so we can use the improved tooltip rendering 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, width, height, maxTextWidth, fontRendererObj);
+ }
+ }
+
+ /**
+ * List gui element similar to GuiModList.Info
+ */
+ class SearchResults extends GuiScrollingList {
+ private final String[] spinner = new String[]{"oooooo", "Oooooo", "oOoooo", "ooOooo", "oooOoo", "ooooOo", "oooooO"};
+ private final DateTimeFormatter coloredDateFormatter = DateTimeFormatter.ofPattern(EnumChatFormatting.GRAY + "HH" + EnumChatFormatting.DARK_GRAY + ":" + EnumChatFormatting.GRAY + "mm" + EnumChatFormatting.DARK_GRAY + ":" + EnumChatFormatting.GRAY + "ss");
+ private List<LogEntry> rawResults;
+ private List<IChatComponent> slotsData;
+ /**
+ * key: slot id of 1st line of a search result (if multi-line-result), value: search result id
+ */
+ private NavigableMap<Integer, Integer> searchResultEntries;
+ private Pair<Long, String> errorMessage;
+ private String resultsCount;
+
+ SearchResults(int marginTop) {
+ super(GuiSearch.this.mc,
+ GuiSearch.this.width - 10, // 5 pixel margin each
+ GuiSearch.this.height - marginTop - 5,
+ marginTop, GuiSearch.this.height - 5,
+ 5, 12,
+ GuiSearch.this.width,
+ GuiSearch.this.height);
+ this.rawResults = Collections.emptyList();
+ this.slotsData = Collections.emptyList();
+ this.searchResultEntries = Collections.emptyNavigableMap();
+ }
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
+ super.drawScreen(mouseX, mouseY, partialTicks);
+ if (isSearchInProgress) {
+ // spinner taken from IProgressMeter and GuiAchievements#drawScreen
+ GuiSearch.this.drawCenteredString(GuiSearch.this.fontRendererObj, "Searching for '" + GuiSearch.this.searchQuery + "'", GuiSearch.this.width / 2, GuiSearch.this.height / 2, 16777215);
+ GuiSearch.this.drawCenteredString(GuiSearch.this.fontRendererObj, spinner[(int) (Minecraft.getSystemTime() / 150L % (long) spinner.length)], GuiSearch.this.width / 2, GuiSearch.this.height / 2 + GuiSearch.this.fontRendererObj.FONT_HEIGHT * 2, 16777215);
+ }
+ int hoveredSlotId = this.func_27256_c(mouseX, mouseY);
+ if (hoveredSlotId >= 0 && mouseY > top && mouseY < bottom) {
+ float scrollDistance = getScrollDistance();
+ if (scrollDistance != Float.MIN_VALUE) {
+ // draw hovered entry details
+
+ int hoveredSearchResultId = getSearchResultIdBySlotId(hoveredSlotId);
+ LogEntry hoveredEntry = getSearchResultByResultId(hoveredSearchResultId);
+ if (hoveredEntry != null && !hoveredEntry.isError()) {
+ // draw 'tooltips' in the top left corner
+ drawString(fontRendererObj, "Log file: ", 2, 2, 0xff888888);
+ GlStateManager.pushMatrix();
+ float scaleFactor = 0.75f;
+ GL11.glScalef(scaleFactor, scaleFactor, scaleFactor);
+ fontRendererObj.drawSplitString(EnumChatFormatting.GRAY + Utils.toRealPath(hoveredEntry.getFilePath()), 5, (int) ((4 + fontRendererObj.FONT_HEIGHT) * (1 / scaleFactor)), (int) ((GuiSearch.this.fieldSearchQuery.xPosition - 8) * (1 / scaleFactor)), 0xff888888);
+ GlStateManager.popMatrix();
+ drawString(fontRendererObj, "Result: " + EnumChatFormatting.WHITE + (hoveredSearchResultId + 1) + EnumChatFormatting.RESET + "/" + EnumChatFormatting.WHITE + this.rawResults.size(), 8, 48, 0xff888888);
+ drawString(fontRendererObj, "Time: " + hoveredEntry.getTime().format(coloredDateFormatter), 8, 58, 0xff888888);
+ }
+
+ // formula from GuiScrollingList#drawScreen slotTop
+ int baseY = this.top + /* border: */4 - (int) scrollDistance;
+
+ // highlight multiline search results
+ Integer resultIndexStart = searchResultEntries.floorKey(hoveredSlotId);
+ Integer resultIndexEnd = searchResultEntries.higherKey(hoveredSlotId);
+
+ if (resultIndexStart == null) {
+ return;
+ } else if (resultIndexEnd == null) {
+ // last result entry
+ resultIndexEnd = getSize();
+ }
+
+ int slotTop = baseY + resultIndexStart * this.slotHeight - 2;
+ int slotBottom = baseY + resultIndexEnd * this.slotHeight - 2;
+ drawRect(this.left, Math.max(slotTop, top), right - /* scrollBar: */7, Math.min(slotBottom, bottom), 0x22ffffff);
+ }
+ } else if (areEntriesSearchResults) {
+ if (analyzedFiles != null) {
+ drawString(fontRendererObj, analyzedFiles, 8, 22, 0xff888888);
+ }
+ if (analyzedFilesWithHits != null) {
+ drawString(fontRendererObj, analyzedFilesWithHits, 8, 32, 0xff888888);
+ }
+ if (resultsCount != null) {
+ drawString(fontRendererObj, resultsCount, 8, 48, 0xff888888);
+ }
+ }
+ if (errorMessage != null) {
+ if (errorMessage.first().compareTo(System.currentTimeMillis()) > 0) {
+ String errorText = "Error: " + EnumChatFormatting.RED + errorMessage.second();
+ int stringWidth = fontRendererObj.getStringWidth(errorText);
+ int margin = 5;
+ int left = width / 2 - stringWidth / 2 - margin;
+ int top = height / 2 - margin;
+ drawRect(left, top, left + stringWidth + 2 * margin, top + fontRendererObj.FONT_HEIGHT + 2 * margin, 0xff000000);
+ drawCenteredString(fontRendererObj, errorText,/* 2, 30*/width / 2, height / 2, 0xffDD1111);
+ } else {
+ errorMessage = null;
+ }
+ }
+ }
+
+ private float getScrollDistance() {
+ Field scrollDistanceField = FieldUtils.getField(GuiScrollingList.class, "scrollDistance", true);
+ if (scrollDistanceField == null) {
+ // scrollDistance field not found in class GuiScrollingList
+ return Float.MIN_VALUE;
+ }
+ try {
+ return (float) scrollDistanceField.get(this);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ return Float.MIN_VALUE;
+ }
+ }
+
+ @Override
+ protected int getSize() {
+ return slotsData.size();
+ }
+
+ @Override
+ protected void elementClicked(int index, boolean doubleClick) {
+ if (doubleClick) {
+ int searchResultIdBySlotId = getSearchResultIdBySlotId(index);
+ LogEntry searchResult = rawResults.get(searchResultIdBySlotId);
+ if (searchResult.getFilePath() == null) {
+ setErrorMessage("This log entry is not from a file");
+ return;
+ }
+ byte[] buffer = new byte[1024];
+ String logFileName = Utils.toRealPath(searchResult.getFilePath());
+ if (logFileName.endsWith("latest.log")) {
+ try {
+ Files.copy(searchResult.getFilePath(), mcLogOutputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else { // .log.gz
+ String newLine = System.getProperty("line.separator");
+ String fileHeader = "# Original filename: " + logFileName + newLine + "# Use CTRL + F to search for specific words" + newLine + newLine;
+ try (GZIPInputStream logFileGzipped = new GZIPInputStream(new FileInputStream(logFileName));
+ FileOutputStream logFileUnGzipped = new FileOutputStream(mcLogOutputFile)) {
+ logFileUnGzipped.write(fileHeader.getBytes());
+ int len;
+ while ((len = logFileGzipped.read(buffer)) > 0) {
+ logFileUnGzipped.write(buffer, 0, len);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ try {
+ Desktop.getDesktop().open(mcLogOutputFile);
+ } catch (IOException e) {
+ setErrorMessage("File extension .txt has no associated default editor");
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ setErrorMessage(e.getMessage()); // The file: <path> doesn't exist.
+ e.printStackTrace();
+ } catch (UnsupportedOperationException e) {
+ setErrorMessage("Can't open files on this OS");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void setErrorMessage(String errorMessage) {
+ int showDuration = 10000; // ms
+ this.errorMessage = Pair.of(System.currentTimeMillis() + showDuration, errorMessage);
+ }
+
+ @Override
+ protected boolean isSelected(int index) {
+ return false;
+ }
+
+ @Override
+ protected void drawBackground() {
+ }
+
+ @Override
+ protected void drawSlot(int slotIdx, int entryRight, int slotTop, int slotBuffer, Tessellator tess) {
+ int drawnResultIndex = searchResultEntries.floorKey(slotIdx);
+ if (Objects.equals(searchResultEntries.floorKey(selectedIndex), drawnResultIndex)) {
+ // highlight all lines of selected entry
+ drawRect(this.left, slotTop - 2, entryRight, slotTop + slotHeight - 2, 0x99000000);
+ }
+ IChatComponent slotData = slotsData.get(slotIdx);
+ if (slotData != null) {
+ GlStateManager.enableBlend();
+ GuiSearch.this.fontRendererObj.drawStringWithShadow(slotData.getFormattedText(), this.left + 4, slotTop, 0xFFFFFF);
+ GlStateManager.disableAlpha();
+ GlStateManager.disableBlend();
+ }
+ }
+
+ private void setResults(List<LogEntry> searchResult) {
+ this.rawResults = searchResult;
+ this.slotsData = resizeContent(searchResult);
+ if (GuiSearch.this.areEntriesSearchResults) {
+ this.resultsCount = "Results: " + EnumChatFormatting.WHITE + this.rawResults.size();
+ }
+ }
+
+ private void clearResults() {
+ this.rawResults = Collections.emptyList();
+ this.resultsCount = null;
+ this.slotsData = resizeContent(Collections.emptyList());
+ }
+
+ private List<IChatComponent> resizeContent(List<LogEntry> searchResults) {
+ this.searchResultEntries = new TreeMap<>();
+ List<IChatComponent> slotsData = new ArrayList<>();
+ for (int searchResultIndex = 0; searchResultIndex < searchResults.size(); searchResultIndex++) {
+ LogEntry searchResult = searchResults.get(searchResultIndex);
+
+ String searchResultEntry;
+ if (searchResult.isError()) {
+ searchResultEntry = searchResult.getMessage();
+ } else {
+ searchResultEntry = EnumChatFormatting.DARK_GRAY + searchResult.getTime().format(DateTimeFormatter.ISO_LOCAL_DATE) + " " + EnumChatFormatting.RESET + searchResult.getMessage();
+ }
+ searchResultEntries.put(slotsData.size(), searchResultIndex);
+ List<IChatComponent> multilineResult = GuiUtilRenderComponents.splitText(new ChatComponentText(searchResultEntry), this.listWidth - 8, GuiSearch.this.fontRendererObj, false, true);
+ slotsData.addAll(multilineResult);
+ }
+ return slotsData;
+ }
+
+ LogEntry getSelectedSearchResult() {
+ int searchResultId = getSearchResultIdBySlotId(selectedIndex);
+ return getSearchResultByResultId(searchResultId);
+ }
+
+ private LogEntry getSearchResultByResultId(int searchResultId) {
+ return (searchResultId >= 0 && searchResultId < rawResults.size()) ? rawResults.get(searchResultId) : null;
+ }
+
+ private int getSearchResultIdBySlotId(int slotId) {
+ Map.Entry<Integer, Integer> searchResultIds = searchResultEntries.floorEntry(slotId);
+ return searchResultIds != null ? searchResultIds.getValue() : -1;
+ }
+
+ String getAllSearchResults() {
+ return rawResults.stream().map(logEntry -> EnumChatFormatting.getTextWithoutFormattingCodes(logEntry.getMessage()))
+ .collect(Collectors.joining("\n"));
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/search/GuiTooltip.java b/src/main/java/de/cowtipper/cowlection/search/GuiTooltip.java
new file mode 100644
index 0000000..41b3b89
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/search/GuiTooltip.java
@@ -0,0 +1,50 @@
+package de.cowtipper.cowlection.search;
+
+import net.minecraft.client.gui.Gui;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.gui.GuiTextField;
+import net.minecraftforge.fml.client.config.GuiCheckBox;
+import net.minecraftforge.fml.client.config.HoverChecker;
+
+import java.util.List;
+
+public class GuiTooltip {
+ private final HoverChecker hoverChecker;
+ private final List<String> tooltip;
+
+ public <T extends Gui> GuiTooltip(T field, List<String> tooltip) {
+ if (field instanceof GuiCheckBox) {
+ // checkbox
+ GuiCheckBox guiCheckBox = (GuiCheckBox) field;
+ int top = guiCheckBox.yPosition;
+ int bottom = guiCheckBox.yPosition + guiCheckBox.height;
+ int left = guiCheckBox.xPosition;
+ int right = guiCheckBox.xPosition + guiCheckBox.width;
+
+ this.hoverChecker = new HoverChecker(top, bottom, left, right, 300);
+ } else if (field instanceof GuiTextField) {
+ // text field
+ GuiTextField guiTextField = (GuiTextField) field;
+ int top = guiTextField.yPosition;
+ int bottom = guiTextField.yPosition + guiTextField.height;
+ int left = guiTextField.xPosition;
+ int right = guiTextField.xPosition + guiTextField.width;
+
+ this.hoverChecker = new HoverChecker(top, bottom, left, right, 300);
+ } else if (field instanceof GuiButton) {
+ // button
+ this.hoverChecker = new HoverChecker((GuiButton) field, 300);
+ } else {
+ throw new IllegalArgumentException("Tried to add a tooltip to an illegal field type: " + field.getClass());
+ }
+ this.tooltip = tooltip;
+ }
+
+ public List<String> getText() {
+ return tooltip;
+ }
+
+ public boolean checkHover(int mouseX, int mouseY) {
+ return hoverChecker.checkHover(mouseX, mouseY);
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java b/src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java
new file mode 100644
index 0000000..636a46d
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/search/LogFilesSearcher.java
@@ -0,0 +1,181 @@
+package de.cowtipper.cowlection.search;
+
+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;
+
+import java.io.*;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.zip.GZIPInputStream;
+
+class LogFilesSearcher {
+ /**
+ * Log4j.xml PatternLayout: [%d{HH:mm:ss}] [%t/%level]: %msg%n
+ * Log line: [TIME] [THREAD/LEVEL]: [CHAT] msg
+ * examples:
+ * - [13:33:37] [Client thread/INFO]: [CHAT] Hello World
+ * - [08:15:42] [Client thread/ERROR]: Item entity 9001 has no item?!
+ */
+ private static final Pattern LOG4J_PATTERN = Pattern.compile("^\\[(?<timeHours>[\\d]{2}):(?<timeMinutes>[\\d]{2}):(?<timeSeconds>[\\d]{2})] \\[(?<thread>[^/]+)/(?<logLevel>[A-Z]+)]:(?<isChat> \\[CHAT])? (?<message>.*)$");
+ private int analyzedFilesWithHits = 0;
+
+ ImmutableTriple<Integer, Integer, List<LogEntry>> searchFor(String searchQuery, boolean chatOnly, boolean matchCase, boolean removeFormatting, LocalDate dateStart, LocalDate dateEnd) throws IOException {
+ List<Path> files = new ArrayList<>();
+ for (String logsDirPath : MooConfig.logsDirs) {
+ File logsDir = new File(logsDirPath);
+ if (logsDir.exists() && logsDir.isDirectory()) {
+ try {
+ files.addAll(fileList(logsDir, dateStart, dateEnd));
+ } catch (IOException e) {
+ throw throwIoException(logsDirPath, e);
+ }
+ }
+ }
+
+ if (files.isEmpty()) {
+ throw new FileNotFoundException(EnumChatFormatting.DARK_RED + "ERROR: Couldn't find any Minecraft log files. Please check if the log file directories are set correctly (/moo config).");
+ } else {
+ List<LogEntry> searchResults = analyzeFiles(files, searchQuery, chatOnly, matchCase, removeFormatting)
+ .stream().sorted(Comparator.comparing(LogEntry::getTime)).collect(Collectors.toList());
+ return new ImmutableTriple<>(files.size(), analyzedFilesWithHits, searchResults);
+ }
+ }
+
+ private List<LogEntry> analyzeFiles(List<Path> paths, String searchTerm, boolean chatOnly, boolean matchCase, boolean removeFormatting) throws IOException {
+ List<LogEntry> searchResults = new ArrayList<>();
+ for (Path path : paths) {
+ boolean foundSearchTermInFile = false;
+ try (BufferedReader in = (path.endsWith("latest.log")
+ ? new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile()))) // latest.log
+ : new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(path.toFile())))))) { // ....log.gz
+ String fileName = path.getFileName().toString(); // 2020-04-20-3.log.gz
+ String date = fileName.equals("latest.log")
+ ? LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)
+ : fileName.substring(0, fileName.lastIndexOf('-'));
+ String content;
+ LogEntry logEntry = null;
+ while ((content = in.readLine()) != null) {
+ Matcher logLineMatcher = LOG4J_PATTERN.matcher(content);
+ if (logLineMatcher.matches()) { // current line is a new log entry
+ if (logEntry != null) {
+ // we had a previous log entry; analyze it!
+ LogEntry result = analyzeLogEntry(logEntry, searchTerm, matchCase, removeFormatting);
+ if (result != null) {
+ searchResults.add(result);
+ foundSearchTermInFile = true;
+ }
+ logEntry = null;
+ }
+ // handle first line of new log entry
+ if (chatOnly && logLineMatcher.group("isChat") == null) {
+ // not a chat log entry, although we're only searching for chat messages, abort!
+ continue;
+ }
+ LocalDateTime dateTime = getDate(date, logLineMatcher);
+ logEntry = new LogEntry(dateTime, path, logLineMatcher.group("message"));
+ } else if (logEntry != null) {
+ // multiline log entry
+ logEntry.addLogLine(content);
+ }
+ }
+ if (logEntry != null) {
+ // end of file! analyze last log entry in file
+ LogEntry result = analyzeLogEntry(logEntry, searchTerm, matchCase, removeFormatting);
+ if (result != null) {
+ searchResults.add(result);
+ foundSearchTermInFile = true;
+ }
+ }
+ if (foundSearchTermInFile) {
+ analyzedFilesWithHits++;
+ }
+ } catch (IOException e) {
+ throw throwIoException(path.toString(), e);
+ }
+ }
+ return searchResults;
+ }
+
+ private LocalDateTime getDate(String date, Matcher logLineMatcher) {
+ int year = Integer.parseInt(date.substring(0, 4));
+ int month = Integer.parseInt(date.substring(5, 7));
+ int day = Integer.parseInt(date.substring(8, 10));
+ int hour = Integer.parseInt(logLineMatcher.group(1));
+ int minute = Integer.parseInt(logLineMatcher.group(2));
+ int sec = Integer.parseInt(logLineMatcher.group(3));
+
+ return LocalDateTime.of(year, month, day, hour, minute, sec);
+ }
+
+ private LogEntry analyzeLogEntry(LogEntry logEntry, String searchTerms, boolean matchCase, boolean removeFormatting) {
+ if (logEntry.getMessage().length() > 5000) {
+ // avoid ultra long log entries
+ return null;
+ }
+ logEntry.fixWeirdCharacters();
+
+ if (removeFormatting) {
+ logEntry.removeFormatting();
+ }
+ String logMessage = logEntry.getMessage();
+ if (!matchCase) {
+ if (!StringUtils.containsIgnoreCase(logMessage, searchTerms)) {
+ // no result, abort
+ return null;
+ }
+ } else if (!logMessage.contains(searchTerms)) {
+ // no result, abort
+ return null;
+ }
+
+ return logEntry;
+ }
+
+ private List<Path> fileList(File directory, LocalDate startDate, LocalDate endDate) throws IOException {
+ List<Path> fileNames = new ArrayList<>();
+ try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory.toPath())) {
+ for (Path path : directoryStream) {
+ if (path.toString().endsWith(".log.gz")) {
+ String[] fileDate = path.getFileName().toString().split("-");
+ if (fileDate.length == 4) {
+ LocalDate fileLocalDate = LocalDate.of(Integer.parseInt(fileDate[0]),
+ Integer.parseInt(fileDate[1]), Integer.parseInt(fileDate[2]));
+
+ if (fileLocalDate.compareTo(startDate) >= 0 && fileLocalDate.compareTo(endDate) <= 0) {
+ fileNames.add(path);
+ }
+ } else {
+ System.err.println("Error with " + path.toString());
+ }
+ } else if (path.getFileName().toString().equals("latest.log")) {
+ LocalDate lastModified = Instant.ofEpochMilli(path.toFile().lastModified()).atZone(ZoneId.systemDefault()).toLocalDate();
+ if (!lastModified.isBefore(startDate) && !lastModified.isAfter(endDate)) {
+ fileNames.add(path);
+ }
+ }
+ }
+ }
+ return fileNames;
+ }
+
+ private IOException throwIoException(String file, IOException e) throws IOException {
+ IOException ioException = new IOException(EnumChatFormatting.DARK_RED + "ERROR: An error occurred trying to read/parse '" + EnumChatFormatting.RED + file + EnumChatFormatting.DARK_RED + "'");
+ ioException.setStackTrace(e.getStackTrace());
+ throw ioException;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java
new file mode 100644
index 0000000..763084d
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java
@@ -0,0 +1,139 @@
+package de.cowtipper.cowlection.util;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import com.mojang.util.UUIDTypeAdapter;
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.command.exception.ThrowingConsumer;
+import de.cowtipper.cowlection.config.MooConfig;
+import de.cowtipper.cowlection.data.Friend;
+import de.cowtipper.cowlection.data.HyPlayerData;
+import de.cowtipper.cowlection.data.HySkyBlockStats;
+import de.cowtipper.cowlection.data.HyStalkingData;
+import org.apache.http.HttpStatus;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ApiUtils {
+ public static final String UUID_NOT_FOUND = "UUID-NOT-FOUND";
+ private static final String NAME_TO_UUID_URL = "https://api.mojang.com/users/profiles/minecraft/";
+ 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 PLAYER_URL = "https://api.hypixel.net/player?key=%s&uuid=%s";
+ private static final ExecutorService pool = Executors.newCachedThreadPool();
+
+ private ApiUtils() {
+ }
+
+ public static void fetchFriendData(String name, ThrowingConsumer<Friend> action) {
+ pool.execute(() -> action.accept(getFriend(name)));
+ }
+
+ private static Friend getFriend(String name) {
+ try (BufferedReader reader = makeApiCall(NAME_TO_UUID_URL + name)) {
+ if (reader == null) {
+ return Friend.FRIEND_NOT_FOUND;
+ } else {
+ return GsonUtils.fromJson(reader, Friend.class);
+ }
+ } catch (IOException | JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchCurrentName(Friend friend, ThrowingConsumer<String> action) {
+ pool.execute(() -> action.accept(getCurrentName(friend)));
+ }
+
+ private static String getCurrentName(Friend friend) {
+ try (BufferedReader reader = makeApiCall(String.format(UUID_TO_NAME_URL, UUIDTypeAdapter.fromUUID(friend.getUuid())))) {
+ if (reader == null) {
+ return UUID_NOT_FOUND;
+ } else {
+ JsonArray nameHistoryData = new JsonParser().parse(reader).getAsJsonArray();
+ if (nameHistoryData.size() > 0) {
+ return nameHistoryData.get(nameHistoryData.size() - 1).getAsJsonObject().get("name").getAsString();
+ }
+ }
+ } catch (IOException | JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchPlayerStatus(Friend friend, ThrowingConsumer<HyStalkingData> action) {
+ pool.execute(() -> action.accept(stalkPlayer(friend)));
+ }
+
+ private static HyStalkingData stalkPlayer(Friend friend) {
+ try (BufferedReader reader = makeApiCall(String.format(ONLINE_STATUS_URL, MooConfig.moo, UUIDTypeAdapter.fromUUID(friend.getUuid())))) {
+ if (reader != null) {
+ return GsonUtils.fromJson(reader, HyStalkingData.class);
+ }
+ } catch (IOException | JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchSkyBlockStats(Friend friend, ThrowingConsumer<HySkyBlockStats> action) {
+ pool.execute(() -> action.accept(stalkSkyBlockStats(friend)));
+ }
+
+ private static HySkyBlockStats stalkSkyBlockStats(Friend friend) {
+ try (BufferedReader reader = makeApiCall(String.format(SKYBLOCK_STATS_URL, MooConfig.moo, UUIDTypeAdapter.fromUUID(friend.getUuid())))) {
+ if (reader != null) {
+ return GsonUtils.fromJson(reader, HySkyBlockStats.class);
+ }
+ } catch (IOException | JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchPlayerOfflineStatus(Friend stalkedPlayer, ThrowingConsumer<HyPlayerData> action) {
+ pool.execute(() -> action.accept(stalkOfflinePlayer(stalkedPlayer)));
+ }
+
+ private static HyPlayerData stalkOfflinePlayer(Friend stalkedPlayer) {
+ try (BufferedReader reader = makeApiCall(String.format(PLAYER_URL, MooConfig.moo, UUIDTypeAdapter.fromUUID(stalkedPlayer.getUuid())))) {
+ if (reader != null) {
+ return GsonUtils.fromJson(reader, HyPlayerData.class);
+ }
+ } catch (IOException | JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private static BufferedReader makeApiCall(String url) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setConnectTimeout(5000);
+ connection.setReadTimeout(10000);
+ connection.addRequestProperty("User-Agent", "Forge Mod " + Cowlection.MODNAME + "/" + Cowlection.VERSION + " (" + Cowlection.GITURL + ")");
+
+ connection.getResponseCode();
+ if (connection.getResponseCode() == HttpStatus.SC_NO_CONTENT) { // http status 204
+ return null;
+ } else {
+ BufferedReader reader;
+ InputStream errorStream = connection.getErrorStream();
+ if (errorStream != null) {
+ reader = new BufferedReader(new InputStreamReader(errorStream));
+ } else {
+ reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ }
+ return reader;
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/ChatHelper.java b/src/main/java/de/cowtipper/cowlection/util/ChatHelper.java
new file mode 100644
index 0000000..28d20ab
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/ChatHelper.java
@@ -0,0 +1,82 @@
+package de.cowtipper.cowlection.util;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.ChatStyle;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraftforge.client.event.ClientChatReceivedEvent;
+import net.minecraftforge.common.MinecraftForge;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ChatHelper {
+ private static final Pattern USELESS_JSON_CONTENT_PATTERN = Pattern.compile("\"[A-Za-z]+\":false,?");
+ private static final int DISPLAY_DURATION = 5000;
+ private final List<IChatComponent> offlineMessages = new ArrayList<>();
+ private String[] aboveChatMessage;
+ private long aboveChatMessageExpiration;
+
+ public ChatHelper() {
+ }
+
+ public void sendMessage(EnumChatFormatting color, String text) {
+ sendMessage(new ChatComponentText(text).setChatStyle(new ChatStyle().setColor(color)));
+ }
+
+ public void sendMessage(IChatComponent chatComponent) {
+ ClientChatReceivedEvent event = new ClientChatReceivedEvent((byte) 1, chatComponent);
+ MinecraftForge.EVENT_BUS.post(event);
+ if (!event.isCanceled()) {
+ if (Minecraft.getMinecraft().thePlayer == null) {
+ offlineMessages.add(event.message);
+ } else {
+ Minecraft.getMinecraft().thePlayer.addChatMessage(event.message);
+ }
+ }
+ }
+
+ public void sendOfflineMessages() {
+ if (Minecraft.getMinecraft().thePlayer != null) {
+ Iterator<IChatComponent> offlineMessages = this.offlineMessages.iterator();
+ if (offlineMessages.hasNext()) {
+ Minecraft.getMinecraft().thePlayer.playSound("random.levelup", 0.4F, 0.8F);
+ }
+ while (offlineMessages.hasNext()) {
+ Minecraft.getMinecraft().thePlayer.addChatMessage(offlineMessages.next());
+ offlineMessages.remove();
+ }
+ }
+ }
+
+ public void sendAboveChatMessage(String... text) {
+ aboveChatMessage = text;
+ aboveChatMessageExpiration = Minecraft.getSystemTime() + DISPLAY_DURATION;
+ }
+
+ public String[] getAboveChatMessage() {
+ if (aboveChatMessageExpiration < Minecraft.getSystemTime()) {
+ // message expired
+ aboveChatMessage = null;
+ }
+ return aboveChatMessage;
+ }
+
+ public String cleanChatComponent(IChatComponent chatComponent) {
+ String component = IChatComponent.Serializer.componentToJson(chatComponent);
+ Matcher jsonMatcher = USELESS_JSON_CONTENT_PATTERN.matcher(component);
+ return jsonMatcher.replaceAll("");
+ }
+
+ public void sendShrug(String... args) {
+ String chatMsg = "\u00AF\\_(\u30C4)_/\u00AF"; // ¯\\_(ツ)_/¯"
+ if (args.length > 0) {
+ chatMsg = String.join(" ", args) + " " + chatMsg;
+ }
+ Minecraft.getMinecraft().thePlayer.sendChatMessage(chatMsg);
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/GsonUtils.java b/src/main/java/de/cowtipper/cowlection/util/GsonUtils.java
new file mode 100644
index 0000000..c0b2735
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/GsonUtils.java
@@ -0,0 +1,94 @@
+package de.cowtipper.cowlection.util;
+
+import com.google.gson.*;
+import com.mojang.util.UUIDTypeAdapter;
+import de.cowtipper.cowlection.data.HyPlayerData;
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTTagString;
+import net.minecraftforge.common.util.Constants;
+
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.UUID;
+
+public final class GsonUtils {
+ private static final Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).registerTypeAdapter(HyPlayerData.class, new HyPlayerDataDeserializer()).create();
+ private static final Gson gsonPrettyPrinter = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).setPrettyPrinting().create();
+
+ private GsonUtils() {
+ }
+
+ public static <T> T fromJson(String json, Type clazz) {
+ return gson.fromJson(json, clazz);
+ }
+
+ public static <T> T fromJson(Reader json, Class<T> clazz) {
+ return gson.fromJson(json, clazz);
+ }
+
+ public static String toJson(Object object) {
+ if (object instanceof NBTBase) {
+ return gsonPrettyPrinter.toJson(nbtToJson((NBTBase) object));
+ } else {
+ return gson.toJson(object);
+ }
+ }
+
+ private static JsonElement nbtToJson(NBTBase nbtElement) {
+ if (nbtElement instanceof NBTBase.NBTPrimitive) {
+ NBTBase.NBTPrimitive nbtNumber = (NBTBase.NBTPrimitive) nbtElement;
+ switch (nbtNumber.getId()) {
+ case Constants.NBT.TAG_BYTE:
+ return new JsonPrimitive(nbtNumber.getByte());
+ case Constants.NBT.TAG_SHORT:
+ return new JsonPrimitive(nbtNumber.getShort());
+ case Constants.NBT.TAG_INT:
+ return new JsonPrimitive(nbtNumber.getInt());
+ case Constants.NBT.TAG_LONG:
+ return new JsonPrimitive(nbtNumber.getLong());
+ case Constants.NBT.TAG_FLOAT:
+ return new JsonPrimitive(nbtNumber.getFloat());
+ case Constants.NBT.TAG_DOUBLE:
+ return new JsonPrimitive(nbtNumber.getDouble());
+ default:
+ return new JsonObject();
+ }
+ } else if (nbtElement instanceof NBTTagString) {
+ return new JsonPrimitive(((NBTTagString) nbtElement).getString());
+ } else if (nbtElement instanceof NBTTagList) {
+ NBTTagList nbtList = (NBTTagList) nbtElement;
+ JsonArray jsonArray = new JsonArray();
+ for (int tagId = 0; tagId < nbtList.tagCount(); tagId++) {
+ jsonArray.add(nbtToJson(nbtList.get(tagId)));
+ }
+ return jsonArray;
+ } else if (nbtElement instanceof NBTTagCompound) {
+ NBTTagCompound nbtCompound = (NBTTagCompound) nbtElement;
+ JsonObject jsonObject = new JsonObject();
+ for (String nbtEntry : nbtCompound.getKeySet()) {
+ jsonObject.add(nbtEntry, nbtToJson(nbtCompound.getTag(nbtEntry)));
+ }
+ return jsonObject;
+ }
+ return new JsonObject();
+ }
+
+ public static class HyPlayerDataDeserializer implements JsonDeserializer<HyPlayerData> {
+ @Override
+ public HyPlayerData deserialize(JsonElement json, Type type, JsonDeserializationContext jdc) throws JsonParseException {
+ if (!json.getAsJsonObject().get("success").getAsBoolean()) {
+ // status: failed
+ return null;
+ }
+ JsonElement player = json.getAsJsonObject().get("player");
+ HyPlayerData hyPlayerData = gsonPrettyPrinter.fromJson(player, HyPlayerData.class);
+ if (hyPlayerData == null) {
+ // player hasn't played Hypixel before
+ return new HyPlayerData();
+ }
+ return hyPlayerData;
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/ImageUtils.java b/src/main/java/de/cowtipper/cowlection/util/ImageUtils.java
new file mode 100644
index 0000000..6280e65
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/ImageUtils.java
@@ -0,0 +1,64 @@
+package de.cowtipper.cowlection.util;
+
+import com.mojang.authlib.minecraft.MinecraftProfileTexture;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.ThreadDownloadImageData;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.fml.relauncher.ReflectionHelper;
+
+import java.awt.image.BufferedImage;
+
+public class ImageUtils {
+ public static int getTierFromTexture(String minionSkinId) {
+ String textureUrl = "http://textures.minecraft.net/texture/" + minionSkinId;
+ MinecraftProfileTexture minionSkinTextureDetails = new MinecraftProfileTexture(textureUrl, null);
+
+ ResourceLocation minionSkinLocation = Minecraft.getMinecraft().getSkinManager().loadSkin(minionSkinTextureDetails, MinecraftProfileTexture.Type.SKIN);
+
+ ThreadDownloadImageData minionSkinTexture = (ThreadDownloadImageData) Minecraft.getMinecraft().getTextureManager().getTexture(minionSkinLocation);
+ BufferedImage minionSkinImage = ReflectionHelper.getPrivateValue(ThreadDownloadImageData.class, minionSkinTexture, "bufferedImage", "field_110560_d");
+
+ // extract relevant part of the minion tier badge (center 2x1 pixel)
+ BufferedImage minionSkinTierBadge = minionSkinImage.getSubimage(43, 3, 2, 1);
+
+ return MinionTier.getByColors(minionSkinTierBadge.getRGB(0, 0), minionSkinTierBadge.getRGB(1, 0)).getTier();
+ }
+
+ private enum MinionTier {
+ UNKNOWN(-1, -1),
+ I(0, 0),
+ II(-2949295, -10566655),
+ III(-1245259, -10566655),
+ IV(-8922850, -983608),
+ V(-8110849, -11790679),
+ VI(-4681729, -11790679),
+ VII(-9486653, -3033345),
+ VIII(-907953, -7208930),
+ IX(-31330, -7208930),
+ X(-5046235, -20031),
+ XI(-15426142, -1769477);
+
+ private final int color1;
+ private final int color2;
+
+ MinionTier(int color1, int color2) {
+ this.color1 = color1;
+ this.color2 = color2;
+ }
+
+ private static MinionTier getByColors(int color1, int color2) {
+ MinionTier[] tiers = values();
+ for (int i = 1; i < tiers.length; i++) {
+ MinionTier minionTier = tiers[i];
+ if (minionTier.color1 == color1 && minionTier.color2 == color2) {
+ return minionTier;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ private int getTier() {
+ return ordinal();
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java b/src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java
new file mode 100644
index 0000000..062d488
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java
@@ -0,0 +1,186 @@
+package de.cowtipper.cowlection.util;
+
+import net.minecraft.event.ClickEvent;
+import net.minecraft.event.HoverEvent;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+
+public class MooChatComponent extends ChatComponentText {
+ public MooChatComponent(String msg) {
+ super(msg);
+ }
+
+ public MooChatComponent black() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.BLACK));
+ return this;
+ }
+
+ public MooChatComponent darkBlue() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_BLUE));
+ return this;
+ }
+
+ public MooChatComponent darkGreen() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_GREEN));
+ return this;
+ }
+
+ public MooChatComponent darkAqua() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_AQUA));
+ return this;
+ }
+
+ public MooChatComponent darkRed() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_RED));
+ return this;
+ }
+
+ public MooChatComponent darkPurple() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_PURPLE));
+ return this;
+ }
+
+ public MooChatComponent gold() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.GOLD));
+ return this;
+ }
+
+ public MooChatComponent gray() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.GRAY));
+ return this;
+ }
+
+ public MooChatComponent darkGray() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_GRAY));
+ return this;
+ }
+
+ public MooChatComponent blue() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.BLUE));
+ return this;
+ }
+
+ public MooChatComponent green() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.GREEN));
+ return this;
+ }
+
+ public MooChatComponent aqua() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.AQUA));
+ return this;
+ }
+
+ public MooChatComponent red() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.RED));
+ return this;
+ }
+
+ public MooChatComponent lightPurple() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.LIGHT_PURPLE));
+ return this;
+ }
+
+ public MooChatComponent yellow() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.YELLOW));
+ return this;
+ }
+
+ public MooChatComponent white() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.WHITE));
+ return this;
+ }
+
+ public MooChatComponent obfuscated() {
+ setChatStyle(getChatStyle().setObfuscated(true));
+ return this;
+ }
+
+ public MooChatComponent bold() {
+ setChatStyle(getChatStyle().setBold(true));
+ return this;
+ }
+
+ public MooChatComponent strikethrough() {
+ setChatStyle(getChatStyle().setStrikethrough(true));
+ return this;
+ }
+
+ public MooChatComponent underline() {
+ setChatStyle(getChatStyle().setUnderlined(true));
+ return this;
+ }
+
+ public MooChatComponent italic() {
+ setChatStyle(getChatStyle().setItalic(true));
+ return this;
+ }
+
+ public MooChatComponent reset() {
+ setChatStyle(getChatStyle().setParentStyle(null).setBold(false).setItalic(false).setObfuscated(false).setUnderlined(false).setStrikethrough(false));
+ return this;
+ }
+
+ public MooChatComponent setHover(IChatComponent hover) {
+ setChatStyle(getChatStyle().setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover)));
+ return this;
+ }
+
+ public MooChatComponent setUrl(String url) {
+ setUrl(url, new KeyValueTooltipComponent("Click to visit", url));
+ return this;
+ }
+
+ public MooChatComponent setUrl(String url, String hover) {
+ setUrl(url, new MooChatComponent(hover).yellow());
+ return this;
+ }
+
+ public MooChatComponent setUrl(String url, IChatComponent hover) {
+ setChatStyle(getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)));
+ setHover(hover);
+ return this;
+ }
+
+ public MooChatComponent setSuggestCommand(String command) {
+ setChatStyle(getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command)));
+ setHover(new KeyValueChatComponent("Run", command, " "));
+ return this;
+ }
+
+ /**
+ * Appends the given component in a new line, without inheriting formatting of previous siblings.
+ *
+ * @see ChatComponentText#appendSibling appendSibling
+ */
+ public MooChatComponent appendFreshSibling(IChatComponent sibling) {
+ this.siblings.add(new ChatComponentText("\n").appendSibling(sibling));
+ return this;
+ }
+
+ @Deprecated
+ public MooChatComponent appendKeyValue(String key, String value) {
+ appendSibling(new MooChatComponent("\n").appendFreshSibling(new KeyValueChatComponent(key, value)));
+ return this;
+ }
+
+ public static class KeyValueChatComponent extends MooChatComponent {
+ public KeyValueChatComponent(String key, String value) {
+ this(key, value, ": ");
+ }
+
+ public KeyValueChatComponent(String key, String value, String separator) {
+ super(key);
+ appendText(separator);
+ gold().appendSibling(new MooChatComponent(value).yellow());
+ }
+ }
+
+ public static class KeyValueTooltipComponent extends MooChatComponent {
+ public KeyValueTooltipComponent(String key, String value) {
+ super(key);
+ appendText(": ");
+ gray().appendSibling(new MooChatComponent(value).yellow());
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/TickDelay.java b/src/main/java/de/cowtipper/cowlection/util/TickDelay.java
new file mode 100644
index 0000000..cbb21c5
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/TickDelay.java
@@ -0,0 +1,29 @@
+package de.cowtipper.cowlection.util;
+
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+
+public class TickDelay {
+ private Runnable task;
+ private int waitingTicks;
+
+ public TickDelay(Runnable task, int ticks) {
+ this.task = task;
+ this.waitingTicks = ticks;
+
+ MinecraftForge.EVENT_BUS.register(this);
+ }
+
+ @SubscribeEvent
+ public void onTick(TickEvent.ClientTickEvent e) {
+ if (e.phase == TickEvent.Phase.START) {
+ if (waitingTicks < 1) {
+ // we're done waiting! Do stuff and exit.
+ task.run();
+ MinecraftForge.EVENT_BUS.unregister(this);
+ }
+ waitingTicks--;
+ }
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/Utils.java b/src/main/java/de/cowtipper/cowlection/util/Utils.java
new file mode 100644
index 0000000..e833cf8
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/Utils.java
@@ -0,0 +1,253 @@
+package de.cowtipper.cowlection.util;
+
+import com.mojang.realmsclient.util.Pair;
+import net.minecraft.util.EnumChatFormatting;
+import org.apache.commons.lang3.text.WordUtils;
+import org.apache.commons.lang3.time.DateFormatUtils;
+import org.apache.commons.lang3.time.DurationFormatUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.text.DecimalFormat;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+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 char[] LARGE_NUMBERS = new char[]{'k', 'm', 'b', 't'};
+
+ private Utils() {
+ }
+
+ public static boolean isValidUuid(String uuid) {
+ return VALID_UUID_PATTERN.matcher(uuid).matches();
+ }
+
+ public static boolean isValidMcName(String username) {
+ return VALID_USERNAME.matcher(username).matches();
+ }
+
+ public static String fancyCase(String string) {
+ return WordUtils.capitalizeFully(string.replace('_', ' '));
+ }
+
+ /**
+ * Turn timestamp into pretty-formatted duration and date details.
+ *
+ * @param timestamp last login/logout
+ * @return 1st: duration between timestamp and now in words; 2nd: formatted date if time differences is >24h, otherwise null
+ */
+ public static Pair<String, String> getDurationAsWords(long timestamp) {
+ long duration = System.currentTimeMillis() - timestamp;
+ long daysPast = TimeUnit.MILLISECONDS.toDays(duration);
+
+ String dateFormatted = null;
+ if (daysPast > 1) {
+ dateFormatted = DateFormatUtils.format(timestamp, "dd-MMM-yyyy");
+ }
+
+ if (daysPast > 31) {
+ return Pair.of(
+ DurationFormatUtils.formatPeriod(timestamp, System.currentTimeMillis(), (daysPast > 365 ? "y 'years' " : "") + "M 'months' d 'days'"),
+ dateFormatted);
+ } else {
+ return Pair.of(
+ DurationFormatUtils.formatDurationWords(duration, true, true),
+ dateFormatted);
+ }
+ }
+
+ public static String getDurationAsWord(long timestamp) {
+ long duration = System.currentTimeMillis() - timestamp;
+ long secondsPast = TimeUnit.MILLISECONDS.toSeconds(duration);
+ if (secondsPast < 60) {
+ return secondsPast + " second" + (secondsPast > 1 ? "s" : "");
+ }
+ long minutesPast = TimeUnit.SECONDS.toMinutes(secondsPast);
+ if (minutesPast < 60) {
+ return minutesPast + " minute" + (minutesPast > 1 ? "s" : "");
+ }
+ long hoursPast = TimeUnit.MINUTES.toHours(minutesPast);
+ if (hoursPast < 24) {
+ return hoursPast + " hour" + (hoursPast > 1 ? "s" : "");
+ }
+ long daysPast = TimeUnit.HOURS.toDays(hoursPast);
+ if (daysPast < 31) {
+ return daysPast + " day" + (daysPast > 1 ? "s" : "");
+ }
+ double monthsPast = daysPast / 30.5d;
+ if (monthsPast < 12) {
+ return new DecimalFormat("0.#").format(monthsPast) + " month" + (monthsPast >= 2 ? "s" : "");
+ }
+ double yearsPast = monthsPast / 12d;
+ return new DecimalFormat("0.#").format(yearsPast) + " year" + (yearsPast >= 2 ? "s" : "");
+ }
+
+ public static String toRealPath(Path path) {
+ try {
+ return path.toRealPath().toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return "file not found";
+ }
+ }
+
+ public static String toRealPath(File path) {
+ return toRealPath(path.toPath());
+ }
+
+ /**
+ * Formats a large number with abbreviations for each factor of a thousand (k, m, ...)
+ *
+ * @param number the number to format
+ * @return a String representing the number n formatted in a cool looking way.
+ * @see <a href="https://stackoverflow.com/a/4753866">Source</a>
+ */
+ public static String formatNumberWithAbbreviations(double number) {
+ return formatNumberWithAbbreviations(number, 0);
+ }
+
+ private static String formatNumberWithAbbreviations(double number, int iteration) {
+ @SuppressWarnings("IntegerDivisionInFloatingPointContext") double d = ((long) number / 100) / 10.0;
+ boolean isRound = (d * 10) % 10 == 0; //true if the decimal part is equal to 0 (then it's trimmed anyway)
+ // this determines the class, i.e. 'k', 'm' etc
+ // this decides whether to trim the decimals
+ // (int) d * 10 / 10 drops the decimal
+ return d < 1000 ? // this determines the class, i.e. 'k', 'm' etc
+ (d > 99.9 || isRound || d > 9.99 ? // this decides whether to trim the decimals
+ (int) d * 10 / 10 : d + "" // (int) d * 10 / 10 drops the decimal
+ ) + "" + LARGE_NUMBERS[iteration]
+ : formatNumberWithAbbreviations(d, iteration + 1);
+ }
+
+ /**
+ * Convert Roman numerals to their corresponding Arabic numeral
+ *
+ * @param roman Roman numeral
+ * @return Arabic numeral
+ * @see <a href="https://www.w3resource.com/javascript-exercises/javascript-math-exercise-22.php">Source</a>
+ */
+ public static int convertRomanToArabic(String roman) {
+ if (roman == null) return -1;
+ int number = romanCharToArabic(roman.charAt(0));
+
+ for (int i = 1; i < roman.length(); i++) {
+ int current = romanCharToArabic(roman.charAt(i));
+ int previous = romanCharToArabic(roman.charAt(i - 1));
+ if (current <= previous) {
+ number += current;
+ } else {
+ number = number - previous * 2 + current;
+ }
+ }
+ return number;
+ }
+
+ private static int romanCharToArabic(char c) {
+ switch (c) {
+ case 'I':
+ return 1;
+ case 'V':
+ return 5;
+ case 'X':
+ return 10;
+ case 'L':
+ return 50;
+ case 'C':
+ return 100;
+ case 'D':
+ return 500;
+ case 'M':
+ return 1000;
+ default:
+ return -1;
+ }
+ }
+
+ /**
+ * Convert Arabic numerals to their corresponding Roman numerals
+ *
+ * @param number Arabic numerals
+ * @return Roman numerals
+ * @see <a href="https://stackoverflow.com/a/48357180">Source</a>
+ */
+ public static String convertArabicToRoman(int number) {
+ if (number == 0) {
+ return "0";
+ }
+ String romanOnes = arabicToRomanChars(number % 10, "I", "V", "X");
+ number /= 10;
+
+ String romanTens = arabicToRomanChars(number % 10, "X", "L", "C");
+ number /= 10;
+
+ String romanHundreds = arabicToRomanChars(number % 10, "C", "D", "M");
+ number /= 10;
+
+ String romanThousands = arabicToRomanChars(number % 10, "M", "", "");
+
+ return romanThousands + romanHundreds + romanTens + romanOnes;
+ }
+
+ private static String arabicToRomanChars(int n, String one, String five, String ten) {
+ switch (n) {
+ case 1:
+ return one;
+ case 2:
+ return one + one;
+ case 3:
+ return one + one + one;
+ case 4:
+ return one + five;
+ case 5:
+ return five;
+ case 6:
+ return five + one;
+ case 7:
+ return five + one + one;
+ case 8:
+ return five + one + one + one;
+ case 9:
+ return one + ten;
+ }
+ return "";
+ }
+
+ /**
+ * Get the minion tier's color for chat formatting
+ *
+ * @param tier minion tier
+ * @return color code corresponding to the tier
+ */
+ public static EnumChatFormatting getMinionTierColor(int tier) {
+ EnumChatFormatting tierColor;
+ switch (tier) {
+ case 1:
+ tierColor = EnumChatFormatting.WHITE;
+ break;
+ case 2:
+ case 3:
+ case 4:
+ tierColor = EnumChatFormatting.GREEN;
+ break;
+ case 5:
+ case 6:
+ case 7:
+ tierColor = EnumChatFormatting.DARK_PURPLE;
+ break;
+ case 8:
+ case 9:
+ case 10:
+ tierColor = EnumChatFormatting.RED;
+ break;
+ case 11:
+ tierColor = EnumChatFormatting.AQUA;
+ break;
+ default:
+ tierColor = EnumChatFormatting.OBFUSCATED;
+ }
+ return tierColor;
+ }
+}
diff --git a/src/main/java/de/cowtipper/cowlection/util/VersionChecker.java b/src/main/java/de/cowtipper/cowlection/util/VersionChecker.java
new file mode 100644
index 0000000..d463d04
--- /dev/null
+++ b/src/main/java/de/cowtipper/cowlection/util/VersionChecker.java
@@ -0,0 +1,146 @@
+package de.cowtipper.cowlection.util;
+
+import de.cowtipper.cowlection.Cowlection;
+import de.cowtipper.cowlection.config.MooConfig;
+import net.minecraft.client.Minecraft;
+import net.minecraft.event.ClickEvent;
+import net.minecraft.event.HoverEvent;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.ChatStyle;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraftforge.common.ForgeModContainer;
+import net.minecraftforge.common.ForgeVersion;
+import net.minecraftforge.fml.common.Loader;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @see ForgeVersion
+ */
+public class VersionChecker {
+ /**
+ * Cooldown between to update checks in minutes
+ */
+ private static final int CHECK_COOLDOWN = 15;
+ private static final String CHANGELOG_URL = Cowlection.GITURL + "blob/master/CHANGELOG.md";
+ private final Cowlection main;
+ private long lastCheck;
+ private String newVersion;
+ private String downloadUrl;
+
+ public VersionChecker(Cowlection main) {
+ this.main = main;
+ this.lastCheck = Minecraft.getSystemTime();
+ newVersion = "[newVersion]";
+ downloadUrl = Cowlection.GITURL + "releases";
+ }
+
+ public boolean runUpdateCheck(boolean isCommandTriggered) {
+ if (isCommandTriggered || (!ForgeModContainer.disableVersionCheck && MooConfig.doUpdateCheck)) {
+ Runnable handleResults = () -> main.getVersionChecker().handleVersionStatus(isCommandTriggered);
+
+ long now = Minecraft.getSystemTime();
+
+ // only re-run if last check was >CHECK_COOLDOWN minutes ago
+ if (getNextCheck() < 0) { // next allowed check is "in the past", so we're good to go
+ lastCheck = now;
+ ForgeVersion.startVersionCheck();
+
+ // check status after 5 seconds - hopefully that's enough to check
+ new TickDelay(handleResults, 5 * 20);
+ return true;
+ } else {
+ new TickDelay(handleResults, 1);
+ }
+ }
+ return false;
+ }
+
+ public void handleVersionStatus(boolean isCommandTriggered) {
+ ForgeVersion.CheckResult versionResult = ForgeVersion.getResult(Loader.instance().activeModContainer());
+ if (versionResult.target != null) {
+ newVersion = versionResult.target.toString();
+ downloadUrl = Cowlection.GITURL + "releases/download/v" + newVersion + "/" + Cowlection.MODNAME.replace(" ", "") + "-" + newVersion + ".jar";
+ }
+
+ IChatComponent statusMsg = null;
+
+ if (isCommandTriggered) {
+ if (versionResult.status == ForgeVersion.Status.UP_TO_DATE) {
+ // up to date
+ statusMsg = new ChatComponentText("\u2714 You're running the latest version (" + Cowlection.VERSION + ").").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GREEN));
+ } else if (versionResult.status == ForgeVersion.Status.PENDING) {
+ // pending
+ statusMsg = new ChatComponentText("\u279C " + "Version check either failed or is still running.").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.YELLOW))
+ .appendSibling(new ChatComponentText("\n \u278A Check for results again in a few seconds with " + EnumChatFormatting.GOLD + "/moo version").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.YELLOW)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo version"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo version")))))
+ .appendSibling(new ChatComponentText("\n \u278B Re-run update check with " + EnumChatFormatting.GOLD + "/moo update").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.YELLOW)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo update"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo update")))));
+ } else if (versionResult.status == ForgeVersion.Status.FAILED) {
+ // check failed
+ statusMsg = new ChatComponentText("\u2716 Version check failed for an unknown reason. Check again in a few seconds with ").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.RED))
+ .appendSibling(new ChatComponentText("/moo update").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GOLD)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo update"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo update")))));
+ }
+ }
+ if (versionResult.status == ForgeVersion.Status.OUTDATED || versionResult.status == ForgeVersion.Status.BETA_OUTDATED) {
+ // outdated
+ IChatComponent spacer = new ChatComponentText(" ").setChatStyle(new ChatStyle().setParentStyle(null));
+
+ IChatComponent text = new ChatComponentText("\u279C New version of " + EnumChatFormatting.DARK_GREEN + Cowlection.MODNAME + " " + EnumChatFormatting.GREEN + "available (" + Cowlection.VERSION + " \u27A1 " + newVersion + ")\n").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GREEN));
+
+ IChatComponent download = new ChatComponentText("[Download]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.DARK_GREEN).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, downloadUrl))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Download the latest version of " + Cowlection.MODNAME))));
+
+ IChatComponent changelog = new ChatComponentText("[Changelog]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.DARK_AQUA).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, CHANGELOG_URL))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "View changelog"))));
+
+ IChatComponent updateInstructions = new ChatComponentText("[Update instructions]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GOLD).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo updateHelp"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo updateHelp"))));
+
+ IChatComponent openModsDirectory = new ChatComponentText("\n[Open Mods directory]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GREEN).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo directory"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Open mods directory with command " + EnumChatFormatting.GOLD + "/moo directory\n\u279C Click to open mods directory"))));
+
+ statusMsg = text.appendSibling(download).appendSibling(spacer).appendSibling(changelog).appendSibling(spacer).appendSibling(updateInstructions).appendSibling(spacer).appendSibling(openModsDirectory);
+ }
+
+ if (statusMsg != null) {
+ if (isCommandTriggered) {
+ main.getChatHelper().sendMessage(statusMsg);
+ } else {
+ IChatComponent finalStatusMsg = statusMsg;
+ new TickDelay(() -> main.getChatHelper().sendMessage(finalStatusMsg)
+ , 6 * 20);
+ }
+ }
+ }
+
+ public long getNextCheck() {
+ long cooldown = TimeUnit.MINUTES.toMillis(CHECK_COOLDOWN);
+ long systemTime = Minecraft.getSystemTime();
+ return cooldown - (systemTime - lastCheck);
+ }
+
+ public String getNewVersion() {
+ return newVersion;
+ }
+
+ public String getDownloadUrl() {
+ return downloadUrl;
+ }
+}