aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCow <cow@volloeko.de>2020-06-16 02:32:15 +0200
committerCow <cow@volloeko.de>2020-06-16 02:32:15 +0200
commitc2c644d9b0f8a66e757b67145935b0e3c447db97 (patch)
tree627c087f6b8c165e4fb3506ff7b559afd2318ddc
parentb9d6b75423ea24c4947b3a655f199c3b34aa167a (diff)
downloadCowlection-c2c644d9b0f8a66e757b67145935b0e3c447db97.tar.gz
Cowlection-c2c644d9b0f8a66e757b67145935b0e3c447db97.tar.bz2
Cowlection-c2c644d9b0f8a66e757b67145935b0e3c447db97.zip
Added `/moo search` to search through mc log files
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md5
-rw-r--r--src/main/java/eu/olli/cowmoonication/Cowmoonication.java4
-rw-r--r--src/main/java/eu/olli/cowmoonication/command/MooCommand.java24
-rw-r--r--src/main/java/eu/olli/cowmoonication/config/MooConfig.java113
-rw-r--r--src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java53
-rw-r--r--src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java8
-rw-r--r--src/main/java/eu/olli/cowmoonication/search/GuiDateField.java37
-rw-r--r--src/main/java/eu/olli/cowmoonication/search/GuiSearch.java359
-rw-r--r--src/main/java/eu/olli/cowmoonication/search/GuiTooltip.java50
-rw-r--r--src/main/java/eu/olli/cowmoonication/search/LogFilesSearcher.java107
-rw-r--r--src/main/java/eu/olli/cowmoonication/util/ApiUtils.java2
-rw-r--r--src/main/java/eu/olli/cowmoonication/util/GsonUtils.java2
-rw-r--r--src/main/java/eu/olli/cowmoonication/util/VersionChecker.java8
-rw-r--r--src/main/resources/assets/cowmoonication/lang/en_US.lang6
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
diff --git a/README.md b/README.md
index 9954d89..f99a729 100644
--- a/README.md
+++ b/README.md
@@ -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` &rarr; `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