diff options
-rw-r--r-- | CHANGELOG.md | 9 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/Cowmoonication.java | 21 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/Friends.java | 28 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/command/MooCommand.java | 89 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/config/MooConfig.java | 19 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/friends/Friend.java | 78 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/friends/Friends.java | 167 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/listener/ChatListener.java | 10 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/listener/PlayerListener.java | 7 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/util/ApiUtils.java | 70 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/util/ChatHelper.java | 74 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/util/Utils.java | 59 | ||||
-rw-r--r-- | src/main/java/eu/olli/cowmoonication/util/VersionChecker.java | 2 |
14 files changed, 502 insertions, 133 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ac786..9132a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 1.8.9-0.3.0 - unreleased +### Added +- `/moo nameChangeCheck`: Force a scan for changed names of best friends + +### Changed +- Moved best friends add/remove functionality from config GUI back to commands (`/moo <add|remove> <name>`) +- Saving best friends' UUIDs now (instead of just the name), also checking for name changes periodically to keep best friends list up to date + + ## [1.8.9-0.2.0] - 08.03.2020 ### Added - Mod update notification (opt-out via config) @@ -5,7 +5,7 @@ A client-side only Forge mod by [Cow](https://namemc.com/profile/Cow) providing | Feature | Command/Usage | |-------------------------------------------------------------------------|-----------------------------------------| | Toggle to hide all join/leave notifications | `/moo toggle` | -| 'Best friends' list to limit the amount of join and leave notifications | `/moo friends` | +| 'Best friends' list to limit the amount of join and leave notifications | `/moo add/remove/list` | | Change guiScale to any value | `/moo guiscale [newValue]` | | Auto-replace `/r` with `/w <latest username>` | `/r ` | | Copy chat components | <kbd>ALT</kbd> + <kbd>right click</kbd> | diff --git a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java index 78f1905..2b46e71 100644 --- a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java +++ b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java @@ -2,9 +2,10 @@ package eu.olli.cowmoonication; import eu.olli.cowmoonication.command.MooCommand; import eu.olli.cowmoonication.config.MooConfig; +import eu.olli.cowmoonication.friends.Friends; import eu.olli.cowmoonication.listener.ChatListener; import eu.olli.cowmoonication.listener.PlayerListener; -import eu.olli.cowmoonication.util.Utils; +import eu.olli.cowmoonication.util.ChatHelper; import eu.olli.cowmoonication.util.VersionChecker; import net.minecraftforge.client.ClientCommandHandler; import net.minecraftforge.common.MinecraftForge; @@ -26,10 +27,11 @@ public class Cowmoonication { public static final String MODID = "cowmoonication"; public static final String VERSION = "1.8.9-0.2.0"; public static final String MODNAME = "Cowmoonication"; + private File modsDir; private MooConfig config; private Friends friends; private VersionChecker versionChecker; - private Utils utils; + private ChatHelper chatHelper; private Logger logger; @Mod.EventHandler @@ -41,10 +43,11 @@ public class Cowmoonication { modDir.mkdirs(); } - friends = new Friends(this); - config = new MooConfig(new Configuration(new File(modDir, MODID + ".cfg")), this); + friends = new Friends(this, new File(modDir, "friends.json")); + config = new MooConfig(new Configuration(new File(modDir, MODID + ".cfg"))); - utils = new Utils(this, e.getSourceFile()); + chatHelper = new ChatHelper(); + modsDir = e.getSourceFile().getParentFile(); } @EventHandler @@ -72,8 +75,12 @@ public class Cowmoonication { return versionChecker; } - public Utils getUtils() { - return utils; + public ChatHelper getChatHelper() { + return chatHelper; + } + + public File getModsFolder() { + return modsDir; } public Logger getLogger() { diff --git a/src/main/java/eu/olli/cowmoonication/Friends.java b/src/main/java/eu/olli/cowmoonication/Friends.java deleted file mode 100644 index 2becc74..0000000 --- a/src/main/java/eu/olli/cowmoonication/Friends.java +++ /dev/null @@ -1,28 +0,0 @@ -package eu.olli.cowmoonication; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.TreeSet; - -public class Friends { - private final Cowmoonication main; - private Set<String> bestFriends = new HashSet<>(); - - public Friends(Cowmoonication main) { - this.main = main; - } - - public boolean isBestFriend(String playerName) { - return bestFriends.contains(playerName); - } - - public Set<String> getBestFriends() { - return new TreeSet<>(bestFriends); - } - - public void syncFriends(String[] bestFriends) { - this.bestFriends = new HashSet<>(); - Collections.addAll(this.bestFriends, bestFriends); - } -} diff --git a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java index 08c377d..22d510a 100644 --- a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java +++ b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java @@ -4,6 +4,7 @@ import eu.olli.cowmoonication.Cowmoonication; import eu.olli.cowmoonication.config.MooConfig; import eu.olli.cowmoonication.config.MooGuiConfig; import eu.olli.cowmoonication.util.TickDelay; +import eu.olli.cowmoonication.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.command.CommandBase; import net.minecraft.command.CommandException; @@ -28,40 +29,52 @@ public class MooCommand extends CommandBase { @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { if (args.length == 0) { - main.getUtils().sendMessage(new ChatComponentTranslation(getCommandUsage(sender))); + main.getChatHelper().sendMessage(new ChatComponentTranslation(getCommandUsage(sender))); return; } - if (args[0].equalsIgnoreCase("friends") || args[0].equalsIgnoreCase("config")) { - 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. + // sub commands: friends + if (args.length == 2 && args[0].equalsIgnoreCase("add")) { + handleBestFriendAdd(args[1]); + } else if (args.length == 2 && args[0].equalsIgnoreCase("remove")) { + handleBestFriendRemove(args[1]); } else if (args[0].equalsIgnoreCase("list")) { handleListBestFriends(); + } else if (args[0].equalsIgnoreCase("nameChangeCheck")) { + main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "Looking for best friends that have changed their name... This will take a few seconds..."); + main.getFriends().updateBestFriends(true); } else if (args[0].equalsIgnoreCase("toggle")) { main.getConfig().toggleNotifications(); - main.getUtils().sendMessage(EnumChatFormatting.GREEN + "\u2714 Switched all non-best friend login/logout notifications " + (MooConfig.filterFriendNotifications ? EnumChatFormatting.DARK_GREEN + "off" : EnumChatFormatting.DARK_RED + "on")); + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u2714 Switched all non-best friend login/logout notifications " + (MooConfig.filterFriendNotifications ? EnumChatFormatting.DARK_GREEN + "off" : EnumChatFormatting.DARK_RED + "on")); + } + // sub-commands: miscellaneous + else if (args[0].equalsIgnoreCase("config")) { + 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("guiscale")) { int currentGuiScale = (Minecraft.getMinecraft()).gameSettings.guiScale; if (args.length == 1) { - main.getUtils().sendMessage(EnumChatFormatting.GREEN + "\u279C Current GUI scale: " + EnumChatFormatting.DARK_GREEN + currentGuiScale); + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u279C Current GUI scale: " + EnumChatFormatting.DARK_GREEN + currentGuiScale); } else { int scale = Math.min(10, MathHelper.parseIntWithDefault(args[1], 6)); Minecraft.getMinecraft().gameSettings.guiScale = scale; - main.getUtils().sendMessage(EnumChatFormatting.GREEN + "\u2714 New GUI scale: " + EnumChatFormatting.DARK_GREEN + scale + EnumChatFormatting.GREEN + " (previous: " + EnumChatFormatting.DARK_GREEN + currentGuiScale + EnumChatFormatting.GREEN + ")"); + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u2714 New GUI scale: " + EnumChatFormatting.DARK_GREEN + scale + EnumChatFormatting.GREEN + " (previous: " + EnumChatFormatting.DARK_GREEN + currentGuiScale + EnumChatFormatting.GREEN + ")"); } - } else if (args[0].equalsIgnoreCase("update")) { + } + // sub-commands: update mod + else if (args[0].equalsIgnoreCase("update")) { boolean updateCheckStarted = main.getVersionChecker().runUpdateCheck(true); if (updateCheckStarted) { - main.getUtils().sendMessage(EnumChatFormatting.GREEN + "\u279C Checking for a newer mod version..."); + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u279C Checking for a newer mod version..."); // VersionChecker#handleVersionStatus will run with a 5 seconds delay } else { long nextUpdate = main.getVersionChecker().getNextCheck(); String waitingTime = String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(nextUpdate), TimeUnit.MILLISECONDS.toSeconds(nextUpdate) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(nextUpdate))); - main.getUtils().sendMessage(new ChatComponentText("\u26A0 Update checker is on cooldown. Please wait " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + waitingTime + EnumChatFormatting.RESET + EnumChatFormatting.RED + " more minutes before checking again.").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.RED))); + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "\u26A0 Update checker is on cooldown. Please wait " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + waitingTime + EnumChatFormatting.RESET + EnumChatFormatting.RED + " more minutes before checking again."); } } else if (args[0].equalsIgnoreCase("updateHelp")) { - main.getUtils().sendMessage(new ChatComponentText("\u279C Update instructions:").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true)) + main.getChatHelper().sendMessage(new ChatComponentText("\u279C Update instructions:").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true)) .appendSibling(new ChatComponentText("\n\u278A" + EnumChatFormatting.YELLOW + " download latest mod version").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(false) .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, main.getVersionChecker().getDownloadUrl())) .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Download the latest version of Cowmoonication\n\u279C Click to download latest mod file"))))) @@ -76,21 +89,53 @@ public class MooCommand extends CommandBase { main.getVersionChecker().handleVersionStatus(true); } else if (args[0].equalsIgnoreCase("folder")) { try { - Desktop.getDesktop().open(main.getUtils().getModsFolder()); + Desktop.getDesktop().open(main.getModsFolder()); } catch (IOException e) { - main.getUtils().sendMessage(new ChatComponentText("\u2716 An error occurred trying to open the mod's folder. I guess you have to open it manually \u00af\\_(\u30c4)_/\u00af").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.RED))); + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "\u2716 An error occurred trying to open the mod's folder. I guess you have to open it manually \u00af\\_(\u30c4)_/\u00af"); e.printStackTrace(); } + } + // "catch-all" remaining sub-commands + else { + main.getChatHelper().sendMessage(new ChatComponentTranslation(getCommandUsage(sender))); + } + } + + private void handleBestFriendAdd(String username) { + if (!Utils.isValidMcName(username)) { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + "? This... doesn't look like a valid username."); + return; + } + + // TODO Add check if 'best friend' is on normal friend list + if (main.getFriends().isBestFriend(username, true)) { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " is a best friend already."); + } else { + main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "Fetching " + EnumChatFormatting.YELLOW + username + EnumChatFormatting.GOLD + "'s unique user id. This may take a few seconds..."); + // add friend async + main.getFriends().addBestFriend(username); + } + } + + private void handleBestFriendRemove(String username) { + if (!Utils.isValidMcName(username)) { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + "? This... doesn't look like a valid username."); + return; + } + + boolean removed = main.getFriends().removeBestFriend(username); + if (removed) { + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Removed " + EnumChatFormatting.DARK_GREEN + username + EnumChatFormatting.GREEN + " from best friends list."); } else { - main.getUtils().sendMessage(new ChatComponentTranslation(getCommandUsage(sender))); + main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " isn't a best friend."); } } private void handleListBestFriends() { Set<String> bestFriends = main.getFriends().getBestFriends(); - // TODO show fancy gui with list of best friends (maybe just the mod's settings?) - main.getUtils().sendMessage(EnumChatFormatting.GREEN + "\u279C Best friends: " + EnumChatFormatting.DARK_GREEN + String.join(EnumChatFormatting.GREEN + ", " + EnumChatFormatting.DARK_GREEN, bestFriends)); + // TODO show fancy gui with list of best friends; maybe with buttons to delete them + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u279C Best friends: " + EnumChatFormatting.DARK_GREEN + String.join(EnumChatFormatting.GREEN + ", " + EnumChatFormatting.DARK_GREEN, bestFriends)); } @Override @@ -101,12 +146,14 @@ public class MooCommand extends CommandBase { @Override public String getCommandUsage(ICommandSender sender) { IChatComponent usage = new ChatComponentText("\u279C Cowmoonication commands:").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true)) - .appendSibling(createCmdHelpSection(1, "Login/Logout Notifications")) - .appendSibling(createCmdHelpEntry("friends", "Add/remove best friends")) + .appendSibling(createCmdHelpSection(1, "Friends")) + .appendSibling(createCmdHelpEntry("add", "Add best friends")) + .appendSibling(createCmdHelpEntry("remove", "Remove best friends")) .appendSibling(createCmdHelpEntry("list", "View list of best friends")) + .appendSibling(createCmdHelpEntry("nameChangeCheck", "Force a scan for changed names of best friends")) .appendSibling(createCmdHelpEntry("toggle", "Toggle show/hide all join/leave notifications")) .appendSibling(createCmdHelpSection(2, "Miscellaneous")) - .appendSibling(createCmdHelpEntry("config", "Open configuration GUI")) + .appendSibling(createCmdHelpEntry("config", "Open mod's configuration")) .appendSibling(createCmdHelpEntry("guiScale", "Change GUI scale")) .appendSibling(createCmdHelpSection(3, "Update mod")) .appendSibling(createCmdHelpEntry("update", "Check for new mod updates")) @@ -139,7 +186,11 @@ public class MooCommand extends CommandBase { @Override public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) { if (args.length == 1) { - return getListOfStringsMatchingLastWord(args, "config", "friends", "list", "toggle", "guiscale", "update", "updateHelp", "version", "folder", "help"); + return getListOfStringsMatchingLastWord(args, + /* friends */ "add", "remove", "list", "nameChangeCheck", "toggle", + /* miscellaneous */ "guiscale", "config", + /* update mod */ "update", "updateHelp", "version", "folder", + /* help */ "help"); } 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 e9b2146..d13758c 100644 --- a/src/main/java/eu/olli/cowmoonication/config/MooConfig.java +++ b/src/main/java/eu/olli/cowmoonication/config/MooConfig.java @@ -1,7 +1,6 @@ package eu.olli.cowmoonication.config; import eu.olli.cowmoonication.Cowmoonication; -import eu.olli.cowmoonication.util.Utils; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Property; @@ -15,23 +14,19 @@ import java.util.List; public class MooConfig { public static boolean doUpdateCheck; public static boolean filterFriendNotifications; - private static String[] bestFriends; private static Configuration cfg = null; - private final Cowmoonication main; - public MooConfig(Configuration configuration, Cowmoonication main) { - this.main = main; + public MooConfig(Configuration configuration) { cfg = configuration; initConfig(); } - public static Configuration getConfig() { + static Configuration getConfig() { return cfg; } private void initConfig() { syncFromFile(); - main.getFriends().syncFriends(bestFriends); MinecraftForge.EVENT_BUS.register(new ConfigEventHandler()); } @@ -76,32 +71,22 @@ public class MooConfig { final boolean FILTER_FRIEND_NOTIFICATIONS = true; Property propFilterFriendNotify = cfg.get(Configuration.CATEGORY_CLIENT, "filterFriendNotifications", FILTER_FRIEND_NOTIFICATIONS, "Set to false to receive all login/logout messages, set to true to only get notifications of 'best friends' joining/leaving"); - final String[] BEST_FRIENDS_DEFAULT_VALUE = new String[]{"Cow"}; - Property propBestFriends = cfg.get(Configuration.CATEGORY_CLIENT, "bestFriends", BEST_FRIENDS_DEFAULT_VALUE, "List of best friends: receive login/logout notifications from them"); - propBestFriends.setValidationPattern(Utils.VALID_USERNAME); - List<String> propOrderGeneral = new ArrayList<>(); propOrderGeneral.add(propDoUpdateCheck.getName()); propOrderGeneral.add(propFilterFriendNotify.getName()); - propOrderGeneral.add(propBestFriends.getName()); cfg.setCategoryPropertyOrder(Configuration.CATEGORY_CLIENT, propOrderGeneral); if (readFieldsFromConfig) { doUpdateCheck = propDoUpdateCheck.getBoolean(DO_UPDATE_CHECK); filterFriendNotifications = propFilterFriendNotify.getBoolean(FILTER_FRIEND_NOTIFICATIONS); - bestFriends = propBestFriends.getStringList(); } propDoUpdateCheck.set(doUpdateCheck); propFilterFriendNotify.set(filterFriendNotifications); - propBestFriends.set(bestFriends); if (cfg.hasChanged()) { cfg.save(); } - if (propBestFriends.hasChanged()) { - main.getFriends().syncFriends(bestFriends); - } } public void toggleNotifications() { diff --git a/src/main/java/eu/olli/cowmoonication/friends/Friend.java b/src/main/java/eu/olli/cowmoonication/friends/Friend.java new file mode 100644 index 0000000..889e087 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/friends/Friend.java @@ -0,0 +1,78 @@ +package eu.olli.cowmoonication.friends; + +import com.google.gson.InstanceCreator; + +import java.lang.reflect.Type; +import java.util.Objects; +import java.util.UUID; + +public class Friend { + public static final Friend FRIEND_NOT_FOUND = new Friend(null, null, -1); + private UUID id; + private String name; + private long lastChecked; + + /** + * No-args constructor for GSON + */ + private Friend() { + this.lastChecked = System.currentTimeMillis(); + } + + private Friend(UUID uuid, String name, long lastChecked) { + this.id = uuid; + this.name = name; + if (lastChecked > 0) { + this.lastChecked = lastChecked; + } + } + + public UUID getUuid() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getLastChecked() { + return lastChecked; + } + + public void setLastChecked(long lastChecked) { + this.lastChecked = lastChecked; + } + + @Override + public String toString() { + return "Friend{" + + "uuid=" + id + + ", name='" + name + '\'' + + ", lastChecked=" + lastChecked + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Friend friend = (Friend) o; + return Objects.equals(id, friend.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public static class FriendCreator implements InstanceCreator { + @Override + public Friend createInstance(Type type) { + return new Friend(); + } + } +} diff --git a/src/main/java/eu/olli/cowmoonication/friends/Friends.java b/src/main/java/eu/olli/cowmoonication/friends/Friends.java new file mode 100644 index 0000000..e37b4a4 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/friends/Friends.java @@ -0,0 +1,167 @@ +package eu.olli.cowmoonication.friends; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import eu.olli.cowmoonication.Cowmoonication; +import eu.olli.cowmoonication.util.ApiUtils; +import eu.olli.cowmoonication.util.TickDelay; +import io.netty.util.internal.ConcurrentSet; +import net.minecraft.event.ClickEvent; +import net.minecraft.event.HoverEvent; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatStyle; +import net.minecraft.util.EnumChatFormatting; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class Friends { + private static final long UPDATE_FREQUENCY_DEFAULT = TimeUnit.HOURS.toMillis(15); + private static final long UPDATE_FREQUENCY_MINIMUM = TimeUnit.MINUTES.toMillis(5); + private final Cowmoonication main; + private Set<Friend> bestFriends = new ConcurrentSet<>(); + private File bestFriendsFile; + private UpdateStatus updateStatus; + + public Friends(Cowmoonication main, File friendsFile) { + this.main = main; + this.bestFriendsFile = friendsFile; + this.updateStatus = UpdateStatus.IDLE; + loadBestFriends(); + updateBestFriends(false); + } + + public boolean isBestFriend(String playerName, boolean ignoreCase) { + if (ignoreCase) { + return bestFriends.stream().map(Friend::getName).anyMatch(playerName::equalsIgnoreCase); + } else { + return bestFriends.stream().map(Friend::getName).anyMatch(playerName::equals); + } + } + + public void addBestFriend(String name) { + if (name.isEmpty()) { + return; + } + + ApiUtils.fetchFriendData(name, friend -> { + if (friend == null) { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Sorry, could contact Mojang's API and thus didn't add " + name + " as a best friend."); + } else if (friend.equals(Friend.FRIEND_NOT_FOUND)) { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "There is no player with the name " + EnumChatFormatting.DARK_RED + name + EnumChatFormatting.RED + "."); + } else { + boolean added = bestFriends.add(friend); + if (added) { + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Added " + EnumChatFormatting.DARK_GREEN + friend.getName() + EnumChatFormatting.GREEN + " as best friend."); + saveBestFriends(); + } + } + }); + } + + public boolean removeBestFriend(String name) { + boolean removed = bestFriends.removeIf(friend -> friend.getName().equalsIgnoreCase(name)); + if (removed) { + saveBestFriends(); + } + return removed; + } + + public Set<String> getBestFriends() { + return bestFriends.stream().map(Friend::getName).collect(Collectors.toCollection(TreeSet::new)); + } + + private Friend getBestFriend(UUID uuid) { + return bestFriends.stream().filter(friend -> friend.getUuid().equals(uuid)).findFirst().orElse(Friend.FRIEND_NOT_FOUND); + } + + public void updateBestFriends(boolean isCommandTriggered) { + bestFriends.stream().filter(friend -> System.currentTimeMillis() - friend.getLastChecked() > (isCommandTriggered ? UPDATE_FREQUENCY_MINIMUM : UPDATE_FREQUENCY_DEFAULT)).forEach(this::updateBestFriend); + + new TickDelay(() -> { + if (this.updateStatus != UpdateStatus.IDLE) { + if (isCommandTriggered && updateStatus == UpdateStatus.NO_NAME_CHANGES) { + main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "No name changes detected."); + } + saveBestFriends(); + this.updateStatus = UpdateStatus.IDLE; + } + }, 10 * 20); + } + + private void updateBestFriend(Friend friend) { + ApiUtils.fetchCurrentName(friend, newName -> { + if (newName == null) { + // skipping friend, something went wrong with API request + } else if (newName.equals(ApiUtils.UUID_NOT_FOUND)) { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "How did you manage to get a unique id on your best friends list that has no name attached to it?"); + } else if (newName.equals(friend.getName())) { + // name hasn't changed, only updating lastChecked timestamp + Friend bestFriend = getBestFriend(friend.getUuid()); + if (!bestFriend.equals(Friend.FRIEND_NOT_FOUND)) { + bestFriend.setLastChecked(System.currentTimeMillis()); + if (this.updateStatus == UpdateStatus.IDLE) { + this.updateStatus = UpdateStatus.NO_NAME_CHANGES; + } + } + } else { + // name has changed + main.getChatHelper().sendMessage(new ChatComponentText("Your best friend " + EnumChatFormatting.DARK_GREEN + friend.getName() + EnumChatFormatting.GREEN + " changed the name to " + EnumChatFormatting.DARK_GREEN + newName + EnumChatFormatting.GREEN + ".").setChatStyle(new ChatStyle() + .setColor(EnumChatFormatting.GREEN) + .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://namemc.com/search?q=" + newName)) + .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "View " + EnumChatFormatting.GOLD + newName + EnumChatFormatting.YELLOW + "'s name history on namemc.com"))))); + + Friend bestFriend = getBestFriend(friend.getUuid()); + if (!bestFriend.equals(Friend.FRIEND_NOT_FOUND)) { + bestFriend.setName(newName); + bestFriend.setLastChecked(System.currentTimeMillis()); + this.updateStatus = UpdateStatus.NAME_CHANGED; + } + } + }); + } + + public synchronized void saveBestFriends() { + try { + String bestFriendsJsonZoned = dumpJson(this.bestFriends); + FileUtils.writeStringToFile(this.bestFriendsFile, bestFriendsJsonZoned, StandardCharsets.UTF_8); + } catch (IOException e) { + main.getLogger().error("Couldn't save best friends", e); + } + } + + private void loadBestFriends() { + try { + this.bestFriends.clear(); + 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) { + main.getLogger().error("Couldn't parse best friends file " + this.bestFriendsFile, e); + } + } + + private Set<Friend> parseJson(String bestFriendsData) { + Type collectionType = new TypeToken<Set<Friend>>() { + }.getType(); + return new Gson().fromJson(bestFriendsData, collectionType); + } + + private String dumpJson(Set<Friend> bestFriends) { + return new Gson().toJson(bestFriends); + } + + private enum UpdateStatus { + IDLE, NAME_CHANGED, NO_NAME_CHANGES + } +} diff --git a/src/main/java/eu/olli/cowmoonication/listener/ChatListener.java b/src/main/java/eu/olli/cowmoonication/listener/ChatListener.java index 54f8025..42a45e0 100644 --- a/src/main/java/eu/olli/cowmoonication/listener/ChatListener.java +++ b/src/main/java/eu/olli/cowmoonication/listener/ChatListener.java @@ -44,7 +44,7 @@ public class ChatListener { if (nameEnd == -1) { nameEnd = text.indexOf(" left"); } - boolean isBestFriend = main.getFriends().isBestFriend(text.substring(0, nameEnd)); + boolean isBestFriend = main.getFriends().isBestFriend(text.substring(0, nameEnd), false); if (!isBestFriend) { e.setCanceled(true); } @@ -58,10 +58,10 @@ public class ChatListener { if (!Mouse.getEventButtonState() && Mouse.getEventButton() == 1 && Keyboard.isKeyDown(Keyboard.KEY_LMENU)) { // alt key pressed and right mouse button being released IChatComponent chatComponent = Minecraft.getMinecraft().ingameGUI.getChatGUI().getChatComponent(Mouse.getX(), Mouse.getY()); if (chatComponent != null) { - String chatData = main.getUtils().cleanChatComponent(chatComponent); + String chatData = main.getChatHelper().cleanChatComponent(chatComponent); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(new StringSelection(chatData), null); - main.getUtils().sendAboveChatMessage(EnumChatFormatting.YELLOW + "Copied chat component to clipboard:", "" + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276E" + EnumChatFormatting.RESET + chatComponent.getUnformattedText() + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276F"); + main.getChatHelper().sendAboveChatMessage(EnumChatFormatting.YELLOW + "Copied chat component to clipboard:", "" + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276E" + EnumChatFormatting.RESET + chatComponent.getUnformattedText() + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276F"); } } } @@ -76,7 +76,7 @@ public class ChatListener { lastTypedChars += eventCharacter; if (lastTypedChars.equalsIgnoreCase("/r ")) { // replace /r with /msg <last user> - main.getUtils().sendAboveChatMessage("Sending message to " + lastPMSender + "!"); + main.getChatHelper().sendAboveChatMessage("Sending message to " + lastPMSender + "!"); Minecraft.getMinecraft().displayGuiScreen(new GuiChat("/w " + lastPMSender + " ")); } } else if (Keyboard.getEventKey() == Keyboard.KEY_BACK) { // Backspace @@ -106,7 +106,7 @@ public class ChatListener { public void onRenderChatGui(RenderGameOverlayEvent.Chat e) { if (e.type == RenderGameOverlayEvent.ElementType.CHAT) { // render message above chat box - String[] aboveChatMessage = main.getUtils().getAboveChatMessage(); + String[] aboveChatMessage = main.getChatHelper().getAboveChatMessage(); if (aboveChatMessage != null) { float chatHeightFocused = Minecraft.getMinecraft().gameSettings.chatHeightFocused; float chatScale = Minecraft.getMinecraft().gameSettings.chatScale; diff --git a/src/main/java/eu/olli/cowmoonication/listener/PlayerListener.java b/src/main/java/eu/olli/cowmoonication/listener/PlayerListener.java index b817ed8..304806d 100644 --- a/src/main/java/eu/olli/cowmoonication/listener/PlayerListener.java +++ b/src/main/java/eu/olli/cowmoonication/listener/PlayerListener.java @@ -1,6 +1,7 @@ package eu.olli.cowmoonication.listener; import eu.olli.cowmoonication.Cowmoonication; +import eu.olli.cowmoonication.util.TickDelay; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.network.FMLNetworkEvent; @@ -14,5 +15,11 @@ public class PlayerListener { @SubscribeEvent public void onServerJoin(FMLNetworkEvent.ClientConnectedToServerEvent e) { main.getVersionChecker().runUpdateCheck(false); + new TickDelay(() -> main.getChatHelper().sendOfflineMessages(), 6 * 20); + } + + @SubscribeEvent + public void onServerLeave(FMLNetworkEvent.ClientDisconnectionFromServerEvent e) { + main.getFriends().saveBestFriends(); } } diff --git a/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java b/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java new file mode 100644 index 0000000..5ff7d5d --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java @@ -0,0 +1,70 @@ +package eu.olli.cowmoonication.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; +import com.mojang.util.UUIDTypeAdapter; +import eu.olli.cowmoonication.friends.Friend; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +public class ApiUtils { + public static final String UUID_NOT_FOUND = "UUID-NOT-FOUND"; + private static final String NAME_TO_UUID_URL = "https://api.mojang.com/users/profiles/minecraft/"; + private static final String UUID_TO_NAME_URL = "https://api.mojang.com/user/profiles/%s/names"; + private static ExecutorService pool = Executors.newCachedThreadPool(); + private static Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).registerTypeAdapter(Friend.class, new Friend.FriendCreator()).create(); + + private ApiUtils() { + } + + public static void fetchFriendData(String name, Consumer<Friend> action) { + pool.execute(() -> action.accept(getFriend(name))); + } + + private static Friend getFriend(String name) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(NAME_TO_UUID_URL + name).openConnection(); + connection.setReadTimeout(5000); + if (connection.getResponseCode() == 204) { + return Friend.FRIEND_NOT_FOUND; + } else if (connection.getResponseCode() == 200) { + return gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), Friend.class); + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public static void fetchCurrentName(Friend friend, Consumer<String> action) { + pool.execute(() -> action.accept(getCurrentName(friend))); + } + + private static String getCurrentName(Friend friend) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(UUID_TO_NAME_URL, UUIDTypeAdapter.fromUUID(friend.getUuid()))).openConnection(); + connection.setReadTimeout(5000); + if (connection.getResponseCode() == 204) { + return UUID_NOT_FOUND; + } else if (connection.getResponseCode() == 200) { + JsonArray nameHistoryData = new JsonParser().parse(new BufferedReader(new InputStreamReader(connection.getInputStream()))).getAsJsonArray(); + if (nameHistoryData.size() > 0) { + return nameHistoryData.get(nameHistoryData.size() - 1).getAsJsonObject().get("name").getAsString(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/eu/olli/cowmoonication/util/ChatHelper.java b/src/main/java/eu/olli/cowmoonication/util/ChatHelper.java new file mode 100644 index 0000000..a0820c0 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/util/ChatHelper.java @@ -0,0 +1,74 @@ +package eu.olli.cowmoonication.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatStyle; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.common.MinecraftForge; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ChatHelper { + private static final Pattern USELESS_JSON_CONTENT_PATTERN = Pattern.compile("\"[A-Za-z]+\":false,?"); + private static final int DISPLAY_DURATION = 5000; + private List<IChatComponent> offlineMessages = new ArrayList<>(); + private String[] aboveChatMessage; + private long aboveChatMessageExpiration; + + public ChatHelper() { + } + + public void sendMessage(EnumChatFormatting color, String text) { + sendMessage(new ChatComponentText(text).setChatStyle(new ChatStyle().setColor(color))); + } + + public void sendMessage(IChatComponent chatComponent) { + ClientChatReceivedEvent event = new ClientChatReceivedEvent((byte) 1, chatComponent); + MinecraftForge.EVENT_BUS.post(event); + if (!event.isCanceled()) { + if (Minecraft.getMinecraft().thePlayer == null) { + offlineMessages.add(event.message); + } else { + Minecraft.getMinecraft().thePlayer.addChatMessage(event.message); + } + } + } + + public void sendOfflineMessages() { + if (Minecraft.getMinecraft().thePlayer != null) { + Iterator<IChatComponent> offlineMessages = this.offlineMessages.iterator(); + if (offlineMessages.hasNext()) { + Minecraft.getMinecraft().thePlayer.playSound("random.levelup", 0.4F, 0.8F); + } + while (offlineMessages.hasNext()) { + Minecraft.getMinecraft().thePlayer.addChatMessage(offlineMessages.next()); + offlineMessages.remove(); + } + } + } + + public void sendAboveChatMessage(String... text) { + aboveChatMessage = text; + aboveChatMessageExpiration = Minecraft.getSystemTime() + DISPLAY_DURATION; + } + + public String[] getAboveChatMessage() { + if (aboveChatMessageExpiration < Minecraft.getSystemTime()) { + // message expired + aboveChatMessage = null; + } + return aboveChatMessage; + } + + public String cleanChatComponent(IChatComponent chatComponent) { + String component = IChatComponent.Serializer.componentToJson(chatComponent); + Matcher jsonMatcher = USELESS_JSON_CONTENT_PATTERN.matcher(component); + return jsonMatcher.replaceAll(""); + } +} diff --git a/src/main/java/eu/olli/cowmoonication/util/Utils.java b/src/main/java/eu/olli/cowmoonication/util/Utils.java index 60e2ea3..0b890f0 100644 --- a/src/main/java/eu/olli/cowmoonication/util/Utils.java +++ b/src/main/java/eu/olli/cowmoonication/util/Utils.java @@ -1,65 +1,14 @@ package eu.olli.cowmoonication.util; -import eu.olli.cowmoonication.Cowmoonication; -import net.minecraft.client.Minecraft; -import net.minecraft.util.ChatComponentText; -import net.minecraft.util.IChatComponent; -import net.minecraftforge.client.event.ClientChatReceivedEvent; -import net.minecraftforge.common.MinecraftForge; - -import java.io.File; -import java.util.regex.Matcher; import java.util.regex.Pattern; -public class Utils { - public static final Pattern VALID_USERNAME = Pattern.compile("^[\\w]{1,16}$"); - private static final Pattern USELESS_JSON_CONTENT_PATTERN = Pattern.compile("\"[A-Za-z]+\":false,?"); - private final Cowmoonication main; - private final File modsDir; - private String[] aboveChatMessage; - private long aboveChatMessageExpiration; - - public Utils(Cowmoonication main, File sourceFile) { - this.main = main; - modsDir = sourceFile.getParentFile(); - } - - public void sendMessage(String text) { - sendMessage(new ChatComponentText(text)); - } - - public void sendMessage(IChatComponent chatComponent) { - ClientChatReceivedEvent event = new ClientChatReceivedEvent((byte) 1, chatComponent); - MinecraftForge.EVENT_BUS.post(event); - if (!event.isCanceled()) { - Minecraft.getMinecraft().thePlayer.addChatMessage(event.message); - } - } +public final class Utils { + private static final Pattern VALID_USERNAME = Pattern.compile("^[\\w]{1,16}$"); - public void sendAboveChatMessage(String... text) { - aboveChatMessage = text; - aboveChatMessageExpiration = Minecraft.getSystemTime() + 5000; + private Utils() { } - public String[] getAboveChatMessage() { - if (aboveChatMessageExpiration < Minecraft.getSystemTime()) { - // message expired - aboveChatMessage = null; - } - return aboveChatMessage; - } - - public boolean isValidMcName(String username) { + public static boolean isValidMcName(String username) { return VALID_USERNAME.matcher(username).matches(); } - - public String cleanChatComponent(IChatComponent chatComponent) { - String component = IChatComponent.Serializer.componentToJson(chatComponent); - Matcher jsonMatcher = USELESS_JSON_CONTENT_PATTERN.matcher(component); - return jsonMatcher.replaceAll(""); - } - - public File getModsFolder() { - return modsDir; - } } diff --git a/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java b/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java index 5c29ae5..fa04c38 100644 --- a/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java +++ b/src/main/java/eu/olli/cowmoonication/util/VersionChecker.java @@ -120,7 +120,7 @@ public class VersionChecker { } if (statusMsg != null) { - main.getUtils().sendMessage(statusMsg); + main.getChatHelper().sendMessage(statusMsg); } } |