aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/eu
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu')
-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
12 files changed, 740 insertions, 27 deletions
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);