diff options
15 files changed, 749 insertions, 30 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 79260f4..df34081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [1.8.9-0.6.0] - unreleased ### Added +- Minecraft log file search `/moo search` - List SkyBlock info of a player `/moo stalkskyblock <playerName>` ### Changed @@ -4,9 +4,10 @@ A client-side only Forge mod by [Cow](https://namemc.com/profile/Cow) providing ## Current features | Feature | Command/Usage | |-------------------------------------------------------------------------|-----------------------------------------| -| 'Best friends' list to limit the amount of join and leave notifications (always up-to-date names even after player name changes) | `/moo add/remove/list` | +| 'Best friends' list to limit the amount of join and leave notifications (always up-to-date names even after player name changes) | `/moo add/remove/list` | +| Search through your Minecraft log files | `/moo search` | | Stalk a player (check online status, current game, ...) | `/moo stalk` | -| Stalk SkyBlock stats of a player | `/moo stalkskyblock` | +| Stalk SkyBlock stats of a player | `/moo stalkskyblock` | | Toggle join/leave notifications for friends, guild members or best friends separately | `/moo toggle` | | Copy chat component | <kbd>ALT</kbd> + <kbd>right click</kbd><br>Hold <kbd>shift</kbd> to copy full component | | Tab-completable usernames for several commands (e.g. `/party`, `/invite`, ...) | `/moo config` → `Commands with Tab-completable usernames` for full list of commands | diff --git a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java index 2460058..45fb791 100644 --- a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java +++ b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java @@ -44,6 +44,7 @@ public class Cowmoonication { public void preInit(FMLPreInitializationEvent e) { instance = this; logger = e.getModLog(); + modsDir = e.getSourceFile().getParentFile(); File modDir = new File(e.getModConfigurationDirectory(), MODID + File.separatorChar); if (!modDir.exists()) { @@ -54,7 +55,6 @@ public class Cowmoonication { config = new MooConfig(this, new Configuration(new File(modDir, MODID + ".cfg"))); chatHelper = new ChatHelper(); - modsDir = e.getSourceFile().getParentFile(); } @EventHandler @@ -94,7 +94,7 @@ public class Cowmoonication { return playerCache; } - public File getModsFolder() { + public File getModsDirectory() { return modsDir; } diff --git a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java index 4ad4e2c..b4d357d 100644 --- a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java +++ b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java @@ -10,6 +10,7 @@ import eu.olli.cowmoonication.config.MooGuiConfig; import eu.olli.cowmoonication.data.Friend; import eu.olli.cowmoonication.data.HySkyBlockStats; import eu.olli.cowmoonication.data.HyStalkingData; +import eu.olli.cowmoonication.search.GuiSearch; import eu.olli.cowmoonication.util.ApiUtils; import eu.olli.cowmoonication.util.MooChatComponent; import eu.olli.cowmoonication.util.TickDelay; @@ -70,6 +71,8 @@ public class MooCommand extends CommandBase { // 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()), 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) { @@ -105,19 +108,19 @@ public class MooCommand extends CommandBase { .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Download the latest version of " + Cowmoonication.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 + Cowmoonication.MODNAME.replace(" ", "") + "-" + main.getVersionChecker().getNewVersion() + ".jar" + EnumChatFormatting.YELLOW + " into mods folder").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(false) - .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo folder")) - .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Open mods folder with command " + EnumChatFormatting.GOLD + "/moo folder\n\u279C Click to open mods folder"))))) + .appendSibling(new ChatComponentText("\n\u278C" + EnumChatFormatting.YELLOW + " copy " + EnumChatFormatting.GOLD + Cowmoonication.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 + Cowmoonication.MODNAME.replace(" ", "") + "-" + Cowmoonication.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("folder")) { + } else if (args[0].equalsIgnoreCase("directory") || args[0].equalsIgnoreCase("folder")) { try { - Desktop.getDesktop().open(main.getModsFolder()); + Desktop.getDesktop().open(main.getModsDirectory()); } catch (IOException e) { e.printStackTrace(); - throw new MooCommandException("\u2716 An error occurred trying to open the mod's folder. I guess you have to open it manually \u00af\\_(\u30c4)_/\u00af"); + 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"); } } // "catch-all" remaining sub-commands @@ -379,13 +382,14 @@ public class MooCommand extends CommandBase { .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("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("folder", "Open Minecraft's mods folder")); + .appendSibling(createCmdHelpEntry("directory", "Open Minecraft's mods directory")); sender.addChatMessage(usage); } @@ -410,12 +414,12 @@ public class MooCommand extends CommandBase { if (args.length == 1) { return getListOfStringsMatchingLastWord(args, /* friends */ "stalk", "stalkskyblock", "skyblockstalk", "add", "remove", "list", "nameChangeCheck", "toggle", - /* miscellaneous */ "config", "guiscale", "shrug", "apikey", - /* update mod */ "update", "updateHelp", "version", "folder", + /* miscellaneous */ "config", "search", "guiscale", "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].equalsIgnoreCase("stalk")) { + } 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/eu/olli/cowmoonication/config/MooConfig.java b/src/main/java/eu/olli/cowmoonication/config/MooConfig.java index cb852a7..4500e34 100644 --- a/src/main/java/eu/olli/cowmoonication/config/MooConfig.java +++ b/src/main/java/eu/olli/cowmoonication/config/MooConfig.java @@ -4,28 +4,50 @@ import eu.olli.cowmoonication.Cowmoonication; import eu.olli.cowmoonication.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 { + public 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 String[] tabCompletableNamesCommands; + // logs search config + public static String[] logsDirs; + private static String defaultStartDate; + // other stuff public static String moo; private static Configuration cfg = null; private final Cowmoonication main; private List<String> propOrderGeneral; + private List<String> propOrderLogsSearch; public MooConfig(Cowmoonication main, Configuration configuration) { this.main = main; @@ -52,7 +74,7 @@ public class MooConfig { /** * Save the GUI-altered values to disk */ - private void syncFromGUI() { + private void syncFromGui() { syncConfig(false, true); } @@ -63,6 +85,22 @@ public class MooConfig { 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 @@ -76,6 +114,8 @@ public class MooConfig { if (loadConfigFromFile) { cfg.load(); } + + // config section: main configuration propOrderGeneral = new ArrayList<>(); Property propDoUpdateCheck = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT, @@ -94,11 +134,24 @@ public class MooConfig { 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(); @@ -106,11 +159,16 @@ public class MooConfig { tabCompletableNamesCommands = propTabCompletableNamesCommands.getStringList(); moo = propMoo.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); @@ -118,6 +176,10 @@ public class MooConfig { propTabCompletableNamesCommands.set(tabCompletableNamesCommands); propMoo.set(moo); + // logs search config + propLogsDirs.set(logsDirs); + propDefaultStartDate.set(defaultStartDate); + if (cfg.hasChanged()) { if (modifiedTabCompletableCommandsList && Minecraft.getMinecraft().thePlayer != null) { main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Added or removed commands with tab-completable usernames take effect after a game restart!"); @@ -126,16 +188,59 @@ public class MooConfig { } } - private Property addConfigEntry(Property property, boolean showInGui) { + private Property addConfigEntry(Property property, boolean showInGui, String category) { if (showInGui) { property.setLanguageKey(Cowmoonication.MODID + ".config." + property.getName()); } else { property.setShowInGui(false); } - propOrderGeneral.add(property.getName()); + + 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 = currentMcLogsDirFile.getAbsolutePath(); + 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(defaultMcLogsDirFile.getAbsolutePath()); + } + return logsDirs.toArray(new String[]{}); + } + /** * Should login/logout notifications be modified and thus monitored? * @@ -149,7 +254,7 @@ public class MooConfig { @SubscribeEvent(priority = EventPriority.NORMAL) public void onEvent(ConfigChangedEvent.OnConfigChangedEvent e) { if (Cowmoonication.MODID.equals(e.modID)) { - syncFromGUI(); + syncFromGui(); } } } diff --git a/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java b/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java index 4583dc9..7807e03 100644 --- a/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java +++ b/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java @@ -1,33 +1,80 @@ package eu.olli.cowmoonication.config; import eu.olli.cowmoonication.Cowmoonication; +import eu.olli.cowmoonication.search.GuiTooltip; 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, - new ConfigElement(MooConfig.getConfig().getCategory(Configuration.CATEGORY_CLIENT)).getChildElements(), + getConfigElements(), Cowmoonication.MODID, false, false, - "Configuration for " + Cowmoonication.MODNAME); - titleLine2 = MooConfig.getConfig().getConfigFile().getAbsolutePath(); + EnumChatFormatting.BOLD + "Configuration for " + Cowmoonication.MODNAME); + titleLine2 = EnumChatFormatting.GRAY + MooConfig.getConfig().getConfigFile().getAbsolutePath(); + } + + 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 diff --git a/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java b/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java index 9ec469e..8d714a1 100644 --- a/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java +++ b/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java @@ -148,9 +148,13 @@ public class FriendsHandler { private void loadBestFriends() { try { + boolean createdNewFile = this.bestFriendsFile.createNewFile(); + this.bestFriends.clear(); - String bestFriendsData = FileUtils.readFileToString(this.bestFriendsFile, StandardCharsets.UTF_8); - this.bestFriends.addAll(parseJson(bestFriendsData)); + if (!createdNewFile) { + String bestFriendsData = FileUtils.readFileToString(this.bestFriendsFile, StandardCharsets.UTF_8); + this.bestFriends.addAll(parseJson(bestFriendsData)); + } } catch (IOException e) { main.getLogger().error("Couldn't read best friends file " + this.bestFriendsFile, e); } catch (JsonParseException e) { diff --git a/src/main/java/eu/olli/cowmoonication/search/GuiDateField.java b/src/main/java/eu/olli/cowmoonication/search/GuiDateField.java new file mode 100644 index 0000000..136e4ac --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/search/GuiDateField.java @@ -0,0 +1,37 @@ +package eu.olli.cowmoonication.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/eu/olli/cowmoonication/search/GuiSearch.java b/src/main/java/eu/olli/cowmoonication/search/GuiSearch.java new file mode 100644 index 0000000..b53aa8c --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/search/GuiSearch.java @@ -0,0 +1,359 @@ +package eu.olli.cowmoonication.search; + +import com.google.common.base.Joiner; +import eu.olli.cowmoonication.config.MooConfig; +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.EnumChatFormatting; +import net.minecraft.util.IChatComponent; +import net.minecraftforge.common.ForgeHooks; +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.lwjgl.input.Keyboard; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.*; +import java.util.concurrent.Executors; + +public class GuiSearch extends GuiScreen { + private static final String SEARCH_QUERY_PLACE_HOLDER = "Search for..."; + // data + private String searchQuery; + private boolean matchCase; + private boolean removeFormatting; + /** + * Cached results are required after resizing the client + */ + private List<String> searchResults; + private LocalDate dateStart; + private LocalDate dateEnd; + + // gui elements + private GuiButton buttonSearch; + private GuiButton buttonClose; + 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; + + public GuiSearch() { + this.searchQuery = SEARCH_QUERY_PLACE_HOLDER; + this.matchCase = false; + this.searchResults = new ArrayList<>(); + this.dateStart = MooConfig.calculateStartDate(); + this.dateEnd = LocalDate.now(); + } + + /** + * 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, 15, 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 fields + 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")); + + 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")); + + // buttons + 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")); + + this.buttonList.add(this.checkboxMatchCase = new GuiCheckBox(1, this.width / 2 - 100, 40, " Match case", matchCase)); + addTooltip(checkboxMatchCase, Collections.singletonList(EnumChatFormatting.YELLOW + "Should the search be " + EnumChatFormatting.GOLD + "case-sensitive" + EnumChatFormatting.YELLOW + "?")); + this.buttonList.add(this.checkboxRemoveFormatting = new GuiCheckBox(1, this.width / 2 - 100, 50, " 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?")); + 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(EnumChatFormatting.getTextWithoutFormattingCodes(searchResults)); + } + } else if (GuiScreen.isKeyComboCtrlC(keyCode)) { + // copy current selected entry + String selectedSearchResult = guiSearchResults.getSelectedSearchResult(); + if (selectedSearchResult != null) { + GuiScreen.setClipboardString(EnumChatFormatting.getTextWithoutFormattingCodes(selectedSearchResult)); + } + } else { + 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, 3, 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) { + this.mc.setIngameFocus(); + } + if (isSearchInProgress || !button.enabled) { + return; + } + if (button == this.buttonSearch) { + setIsSearchInProgress(true); + + Executors.newSingleThreadExecutor().execute(() -> { + searchResults = new LogFilesSearcher().searchFor(this.fieldSearchQuery.getText(), checkboxMatchCase.isChecked(), checkboxRemoveFormatting.isChecked(), dateStart, dateEnd); + if (searchResults.isEmpty()) { + searchResults.add(EnumChatFormatting.ITALIC + "No results"); + } + Minecraft.getMinecraft().addScheduledTask(() -> { + this.guiSearchResults.setResults(searchResults); + setIsSearchInProgress(false); + }); + }); + } else if (button == checkboxMatchCase) { + matchCase = checkboxMatchCase.isChecked(); + } else if (button == checkboxRemoveFormatting) { + removeFormatting = checkboxRemoveFormatting.isChecked(); + } + } + + private void setIsSearchInProgress(boolean isSearchInProgress) { + this.isSearchInProgress = isSearchInProgress; + buttonSearch.enabled = !isSearchInProgress; + fieldSearchQuery.setEnabled(!isSearchInProgress); + fieldDateStart.setEnabled(!isSearchInProgress); + fieldDateEnd.setEnabled(!isSearchInProgress); + checkboxRemoveFormatting.enabled = !isSearchInProgress; + checkboxMatchCase.enabled = !isSearchInProgress; + if (isSearchInProgress) { + fieldSearchQuery.setFocused(false); + fieldDateStart.setFocused(false); + fieldDateEnd.setFocused(false); + buttonSearch.displayString = EnumChatFormatting.ITALIC + "Searching"; + searchResults.clear(); + this.guiSearchResults.clearResults(); + } 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 List<String> rawResults; + private List<IChatComponent> slotsData; + private NavigableMap<Integer, Integer> searchResultEntries; + + 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); + } + } + + @Override + protected int getSize() { + return slotsData.size(); + } + + @Override + protected void elementClicked(int index, boolean doubleClick) { + } + + @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) { + if (Objects.equals(searchResultEntries.floorKey(selectedIndex), searchResultEntries.floorKey(slotIdx))) { + // 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<String> searchResult) { + this.rawResults = searchResult; + this.slotsData = resizeContent(searchResult); + } + + private void clearResults() { + this.rawResults = Collections.emptyList(); + this.slotsData = resizeContent(Collections.emptyList()); + } + + private List<IChatComponent> resizeContent(List<String> searchResults) { + this.searchResultEntries = new TreeMap<>(); + List<IChatComponent> slotsData = new ArrayList<>(); + for (int searchResultIndex = 0; searchResultIndex < searchResults.size(); searchResultIndex++) { + String searchResult = searchResults.get(searchResultIndex); + + searchResultEntries.put(slotsData.size(), searchResultIndex); + IChatComponent chat = ForgeHooks.newChatWithLinks(searchResult, false); + List<IChatComponent> multilineResult = GuiUtilRenderComponents.splitText(chat, this.listWidth - 8, GuiSearch.this.fontRendererObj, false, true); + slotsData.addAll(multilineResult); + } + return slotsData; + } + + String getSelectedSearchResult() { + Map.Entry<Integer, Integer> selectedResultIndex = searchResultEntries.floorEntry(selectedIndex); + return (selectedResultIndex != null && selectedResultIndex.getValue() < rawResults.size()) ? rawResults.get(selectedResultIndex.getValue()) : null; + } + + String getAllSearchResults() { + return Joiner.on('\n').join(rawResults); + } + } +} diff --git a/src/main/java/eu/olli/cowmoonication/search/GuiTooltip.java b/src/main/java/eu/olli/cowmoonication/search/GuiTooltip.java new file mode 100644 index 0000000..9fed357 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/search/GuiTooltip.java @@ -0,0 +1,50 @@ +package eu.olli.cowmoonication.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/eu/olli/cowmoonication/search/LogFilesSearcher.java b/src/main/java/eu/olli/cowmoonication/search/LogFilesSearcher.java new file mode 100644 index 0000000..50d7c85 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/search/LogFilesSearcher.java @@ -0,0 +1,107 @@ +package eu.olli.cowmoonication.search; + +import eu.olli.cowmoonication.config.MooConfig; +import net.minecraft.util.EnumChatFormatting; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.io.*; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; + +class LogFilesSearcher { + private static final Pattern UTF_PARAGRAPH_SYMBOL = Pattern.compile("§"); + + List<String> searchFor(String searchQuery, boolean matchCase, boolean removeFormatting, LocalDate dateStart, LocalDate dateEnd) { + List<String> 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) { + return printErrors(logsDirPath, e); + } + } + } + + if (files.isEmpty()) { + List<String> errors = new ArrayList<>(); + errors.add(EnumChatFormatting.DARK_RED + "ERROR: Couldn't find any Minecraft log files. Please check if the log file directories are set correctly (/moo config)."); + return errors; + } else { + return analyseFiles(files, searchQuery, matchCase, removeFormatting); + } + } + + private List<String> analyseFiles(List<String> files, String searchTerm, boolean matchCase, boolean removeFormatting) { + List<String> searchResults = new ArrayList<>(); + for (String file : files) { + try (BufferedReader in = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(file))))) { + String content; + while ((content = in.readLine()) != null) { + String result = analyseLine(content, searchTerm, matchCase); + if (result != null) { + if (result.contains("§")) { + result = UTF_PARAGRAPH_SYMBOL.matcher(result).replaceAll("§"); + } + if (removeFormatting) { + result = EnumChatFormatting.getTextWithoutFormattingCodes(result); + } + String date = file.substring(file.lastIndexOf('\\') + 1, file.lastIndexOf('-')); + searchResults.add(EnumChatFormatting.DARK_GRAY + date + " " + EnumChatFormatting.RESET + result); + } + } + } catch (IOException e) { + return printErrors(file, e); + } + } + return searchResults; + } + + private String analyseLine(String logLine, String searchTerms, boolean matchCase) { + String result = logLine; + if (!matchCase) { + logLine = logLine.toLowerCase(); + searchTerms = searchTerms.toLowerCase(); + } + return logLine.contains(searchTerms) ? (result.contains("[Client thread/INFO]: [CHAT]") ? result.substring(40) : result) : null; + } + + private List<String> fileList(File directory, LocalDate startDate, LocalDate endDate) throws IOException { + List<String> 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.toString()); + } + } else { + System.err.println("Error with " + path.toString()); + } + } + } + } + return fileNames; + } + + private List<String> printErrors(String file, IOException e) { + System.err.println("Error reading/parsing file: " + file); + e.printStackTrace(); + List<String> errorMessage = new ArrayList<>(); + errorMessage.add(EnumChatFormatting.DARK_RED + "ERROR: An error occurred trying to read/parse '" + EnumChatFormatting.RED + file + EnumChatFormatting.DARK_RED + "'"); + errorMessage.add(StringUtils.replaceEach(ExceptionUtils.getStackTrace(e), new String[]{"\t", "\r\n"}, new String[]{" ", "\n"})); + return errorMessage; + } +} diff --git a/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java b/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java index 8515593..317ed80 100644 --- a/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java +++ b/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java @@ -28,7 +28,7 @@ public class ApiUtils { private static final String STALKING_URL_OFFICIAL = "https://api.hypixel.net/status?key=%s&uuid=%s"; private static final String SKYBLOCK_STATS_URL_OFFICIAL = "https://api.hypixel.net/skyblock/profiles?key=%s&uuid=%s"; private static final String STALKING_URL_UNOFFICIAL = "https://api.slothpixel.me/api/players/%s"; - private static ExecutorService pool = Executors.newCachedThreadPool(); + private static final ExecutorService pool = Executors.newCachedThreadPool(); private ApiUtils() { } diff --git a/src/main/java/eu/olli/cowmoonication/util/GsonUtils.java b/src/main/java/eu/olli/cowmoonication/util/GsonUtils.java index 4a723ec..c448e0e 100644 --- a/src/main/java/eu/olli/cowmoonication/util/GsonUtils.java +++ b/src/main/java/eu/olli/cowmoonication/util/GsonUtils.java @@ -9,7 +9,7 @@ import java.lang.reflect.Type; import java.util.UUID; public final class GsonUtils { - private static Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + private static final Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); private GsonUtils() { } diff --git a/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java b/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java index 1b7503e..902d1e0 100644 --- a/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java +++ b/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java @@ -111,12 +111,12 @@ public class VersionChecker { .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 openModsFolder = new ChatComponentText("\n[Open Mods folder]").setChatStyle(new ChatStyle() + IChatComponent openModsDirectory = new ChatComponentText("\n[Open Mods directory]").setChatStyle(new ChatStyle() .setColor(EnumChatFormatting.GREEN).setBold(true) - .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo folder")) - .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Open mods folder with command " + EnumChatFormatting.GOLD + "/moo folder\n\u279C Click to open mods folder")))); + .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(openModsFolder); + statusMsg = text.appendSibling(download).appendSibling(spacer).appendSibling(changelog).appendSibling(spacer).appendSibling(updateInstructions).appendSibling(spacer).appendSibling(openModsDirectory); } if (statusMsg != null) { diff --git a/src/main/resources/assets/cowmoonication/lang/en_US.lang b/src/main/resources/assets/cowmoonication/lang/en_US.lang index f66517c..6b09da2 100644 --- a/src/main/resources/assets/cowmoonication/lang/en_US.lang +++ b/src/main/resources/assets/cowmoonication/lang/en_US.lang @@ -8,4 +8,8 @@ cowmoonication.config.showGuildNotifications=Show guild notifications cowmoonication.config.showGuildNotifications.tooltip=Set to true to receive guild members' login/logout messages, set to false hide them. cowmoonication.config.tabCompletableNamesCommands=Commands with Tab-completable usernames cowmoonication.config.tabCompletableNamesCommands.tooltip=List of commands with a username argument that should be Tab-completable.\nRequires game restart to take effect! -cowmoonication.commands.generic.exception=%s
\ No newline at end of file +cowmoonication.config.logsDirs=Directories with Minecraft log files +cowmoonication.config.logsDirs.tooltip=List of directories containing Minecraft log files +cowmoonication.config.defaultStartDate=Start date for log file search +cowmoonication.config.defaultStartDate.tooltip=§eCan be either a §6number§e (e.g. "§63§e" means "§6start searching 3 months ago§e"),\n§eor alternatively a §6fixed date §e(§6yyyy-mm-dd§e) +cowmoonication.commands.generic.exception=%s |