diff options
Diffstat (limited to 'src/main/java/eu/olli/cowmoonication')
12 files changed, 749 insertions, 42 deletions
diff --git a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java index ae61781..2460058 100644 --- a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java +++ b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java @@ -5,10 +5,10 @@ import eu.olli.cowmoonication.command.ShrugCommand; import eu.olli.cowmoonication.command.TabCompletableCommand; import eu.olli.cowmoonication.config.MooConfig; import eu.olli.cowmoonication.handler.FriendsHandler; +import eu.olli.cowmoonication.handler.PlayerCache; import eu.olli.cowmoonication.listener.ChatListener; import eu.olli.cowmoonication.listener.PlayerListener; import eu.olli.cowmoonication.util.ChatHelper; -import eu.olli.cowmoonication.handler.PlayerCache; import eu.olli.cowmoonication.util.VersionChecker; import net.minecraftforge.client.ClientCommandHandler; import net.minecraftforge.common.MinecraftForge; @@ -31,6 +31,7 @@ public class Cowmoonication { public static final String VERSION = "@VERSION@"; public static final String MODNAME = "@MODNAME@"; public static final String GITURL = "@GITURL@"; + private static Cowmoonication instance; private File modsDir; private MooConfig config; private FriendsHandler friendsHandler; @@ -41,6 +42,7 @@ public class Cowmoonication { @Mod.EventHandler public void preInit(FMLPreInitializationEvent e) { + instance = this; logger = e.getModLog(); File modDir = new File(e.getModConfigurationDirectory(), MODID + File.separatorChar); @@ -99,4 +101,11 @@ public class Cowmoonication { public Logger getLogger() { return logger; } + + /** + * Get mod's instance; instead of this method use dependency injection where possible + */ + public static Cowmoonication getInstance() { + return instance; + } } diff --git a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java index 8800ff2..4ad4e2c 100644 --- a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java +++ b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java @@ -2,17 +2,20 @@ package eu.olli.cowmoonication.command; import com.mojang.realmsclient.util.Pair; import eu.olli.cowmoonication.Cowmoonication; +import eu.olli.cowmoonication.command.exception.ApiContactException; +import eu.olli.cowmoonication.command.exception.InvalidPlayerNameException; +import eu.olli.cowmoonication.command.exception.MooCommandException; import eu.olli.cowmoonication.config.MooConfig; import eu.olli.cowmoonication.config.MooGuiConfig; import eu.olli.cowmoonication.data.Friend; -import eu.olli.cowmoonication.util.ApiUtils; +import eu.olli.cowmoonication.data.HySkyBlockStats; import eu.olli.cowmoonication.data.HyStalkingData; +import eu.olli.cowmoonication.util.ApiUtils; +import eu.olli.cowmoonication.util.MooChatComponent; 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; -import net.minecraft.command.ICommandSender; +import net.minecraft.command.*; import net.minecraft.event.ClickEvent; import net.minecraft.event.HoverEvent; import net.minecraft.util.*; @@ -20,6 +23,7 @@ import net.minecraft.util.*; import java.awt.*; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -39,12 +43,20 @@ public class MooCommand extends CommandBase { // sub commands: friends if (args[0].equalsIgnoreCase("stalk")) { if (args.length != 2) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Usage: /" + getCommandName() + " stalk <playerName>"); + throw new WrongUsageException("/" + getCommandName() + " stalk <playerName>"); } else if (!Utils.isValidMcName(args[1])) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "\"" + args[1] + "\" is not a valid player name."); + throw new InvalidPlayerNameException(args[1]); } else { handleStalking(args[1]); } + } else if (args[0].equalsIgnoreCase("stalksb") || args[0].equalsIgnoreCase("stalkskyblock") || args[0].equalsIgnoreCase("skyblockstalk")) { + if (args.length != 2) { + throw new WrongUsageException("/" + getCommandName() + " stalkskyblock <playerName>"); + } else if (!Utils.isValidMcName(args[1])) { + throw new InvalidPlayerNameException(args[1]); + } else { + handleStalkingSkyBlock(args[1]); + } } else if (args.length == 2 && args[0].equalsIgnoreCase("add")) { handleBestFriendAdd(args[1]); } else if (args.length == 2 && args[0].equalsIgnoreCase("remove")) { @@ -84,7 +96,7 @@ public class MooCommand extends CommandBase { String waitingTime = String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(nextUpdate), TimeUnit.MILLISECONDS.toSeconds(nextUpdate) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(nextUpdate))); - 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."); + throw new MooCommandException("\u26A0 Update checker is on cooldown. Please wait " + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + waitingTime + EnumChatFormatting.RESET + EnumChatFormatting.RED + " more minutes before checking again."); } } else if (args[0].equalsIgnoreCase("updateHelp")) { main.getChatHelper().sendMessage(new ChatComponentText("\u279C Update instructions:").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true)) @@ -104,8 +116,8 @@ public class MooCommand extends CommandBase { try { Desktop.getDesktop().open(main.getModsFolder()); } catch (IOException e) { - 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(); + 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"); } } // "catch-all" remaining sub-commands @@ -114,7 +126,7 @@ public class MooCommand extends CommandBase { } } - private void handleApiKey(String[] args) { + private void handleApiKey(String[] args) throws CommandException { if (args.length == 1) { String firstSentence; EnumChatFormatting color; @@ -136,15 +148,14 @@ public class MooCommand extends CommandBase { main.getConfig().syncFromFields(); main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Updated API key!"); } else { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "That doesn't look like a valid API key..."); + throw new SyntaxErrorException("That doesn't look like a valid API key..."); } } } - private void handleStalking(String playerName) { + private void handleStalking(String playerName) throws CommandException { if (!Utils.isValidUuid(MooConfig.moo)) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "You haven't set your Hypixel API key yet. Use " + EnumChatFormatting.DARK_RED + "/api new" + EnumChatFormatting.RED + " to request a new API key from Hypixel or use " + EnumChatFormatting.DARK_RED + "/" + this.getCommandName() + " apikey <key>" + EnumChatFormatting.RED + " to manually set your existing API key."); - return; + throw new MooCommandException("You haven't set your Hypixel API key yet. Use " + EnumChatFormatting.DARK_RED + "/api new" + EnumChatFormatting.RED + " to request a new API key from Hypixel or use " + EnumChatFormatting.DARK_RED + "/" + this.getCommandName() + " apikey <key>" + EnumChatFormatting.RED + " to manually set your existing API key."); } main.getChatHelper().sendMessage(EnumChatFormatting.GRAY, "Stalking " + EnumChatFormatting.WHITE + playerName + EnumChatFormatting.GRAY + ". This may take a few seconds."); boolean isBestFriend = main.getFriendsHandler().isBestFriend(playerName, true); @@ -156,9 +167,9 @@ public class MooCommand extends CommandBase { // fetch player uuid ApiUtils.fetchFriendData(playerName, stalkedPlayer -> { if (stalkedPlayer == null) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Sorry, could contact Mojang's API and thus couldn't stalk " + EnumChatFormatting.DARK_RED + playerName); + throw new ApiContactException("Mojang", "couldn't stalk " + EnumChatFormatting.DARK_RED + playerName); } else if (stalkedPlayer.equals(Friend.FRIEND_NOT_FOUND)) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "There is no player with the name " + EnumChatFormatting.DARK_RED + playerName + EnumChatFormatting.RED + "."); + throw new PlayerNotFoundException("There is no player with the name " + EnumChatFormatting.DARK_RED + playerName + EnumChatFormatting.RED + "."); } else { // ... then stalk the player stalkPlayer(stalkedPlayer); @@ -178,11 +189,16 @@ public class MooCommand extends CommandBase { } else { ApiUtils.fetchPlayerOfflineStatus(stalkedPlayer, slothStalking -> { if (slothStalking == null) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Something went wrong contacting the Slothpixel API. Couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + " but they appear to be offline currently."); + throw new ApiContactException("Slothpixel", "couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + " but they appear to be offline currently."); } else if (slothStalking.hasNeverJoinedHypixel()) { main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, EnumChatFormatting.GOLD + stalkedPlayer.getName() + EnumChatFormatting.YELLOW + " has " + EnumChatFormatting.GOLD + "never " + EnumChatFormatting.YELLOW + "been on Hypixel (or might be nicked)."); } else if (slothStalking.isHidingOnlineStatus()) { - main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, slothStalking.getPlayerNameFormatted() + EnumChatFormatting.YELLOW + " is hiding their online status."); + main.getChatHelper().sendMessage(new ChatComponentText(slothStalking.getPlayerNameFormatted()).appendSibling(new ChatComponentText(" is hiding their online status from the Hypixel API. You can see their online status with ").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.YELLOW))) + .appendSibling(new ChatComponentText("/profile " + slothStalking.getPlayerName()).setChatStyle(new ChatStyle() + .setColor(EnumChatFormatting.GOLD) + .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/profile " + slothStalking.getPlayerName())) + .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/profile " + slothStalking.getPlayerName()))))) + .appendSibling(new ChatComponentText(" while you're in a lobby (tooltip of the player head on the top left).").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.YELLOW)))); } else if (slothStalking.hasNeverLoggedOut()) { Pair<String, String> lastOnline = Utils.getLastOnlineWords(slothStalking.getLastLogin()); @@ -202,20 +218,118 @@ public class MooCommand extends CommandBase { } } else { String cause = (hyStalking != null) ? hyStalking.getCause() : null; - main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Something went wrong contacting the Hypixel API. Couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + (cause != null ? " (Reason: " + EnumChatFormatting.DARK_RED + cause + EnumChatFormatting.RED + ")" : "") + "."); + throw new ApiContactException("Hypixel", "couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + (cause != null ? " (Reason: " + EnumChatFormatting.DARK_RED + cause + EnumChatFormatting.RED + ")" : "") + "."); } }); } - private void handleBestFriendAdd(String username) { + private void handleStalkingSkyBlock(String playerName) throws CommandException { + if (!Utils.isValidUuid(MooConfig.moo)) { + throw new MooCommandException("You haven't set your Hypixel API key yet. Use " + EnumChatFormatting.DARK_RED + "/api new" + EnumChatFormatting.RED + " to request a new API key from Hypixel or use " + EnumChatFormatting.DARK_RED + "/" + this.getCommandName() + " apikey <key>" + EnumChatFormatting.RED + " to manually set your existing API key."); + } + main.getChatHelper().sendMessage(EnumChatFormatting.GRAY, "Stalking " + EnumChatFormatting.WHITE + playerName + EnumChatFormatting.GRAY + "'s SkyBlock stats. This may take a few seconds."); + boolean isBestFriend = main.getFriendsHandler().isBestFriend(playerName, true); + if (isBestFriend) { + Friend stalkedPlayer = main.getFriendsHandler().getBestFriend(playerName); + // we have the uuid already, so stalk the player + stalkSkyBlockStats(stalkedPlayer); + } else { + // fetch player uuid + ApiUtils.fetchFriendData(playerName, stalkedPlayer -> { + if (stalkedPlayer == null) { + throw new ApiContactException("Mojang", "couldn't stalk " + EnumChatFormatting.DARK_RED + playerName); + } else if (stalkedPlayer.equals(Friend.FRIEND_NOT_FOUND)) { + throw new PlayerNotFoundException("There is no player with the name " + EnumChatFormatting.DARK_RED + playerName + EnumChatFormatting.RED + "."); + } else { + // ... then stalk the player + stalkSkyBlockStats(stalkedPlayer); + } + }); + } + } + + private void stalkSkyBlockStats(Friend stalkedPlayer) { + ApiUtils.fetchSkyBlockStats(stalkedPlayer, hySBStalking -> { + if (hySBStalking != null && hySBStalking.isSuccess()) { + HySkyBlockStats.Profile activeProfile = hySBStalking.getActiveProfile(stalkedPlayer.getUuid()); + + if (activeProfile == null) { + throw new MooCommandException("Looks like " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + " hasn't played SkyBlock yet."); + } + + String highestSkill = null; + int highestLevel = -1; + + MooChatComponent skillLevels = new MooChatComponent("Skill levels:").gold(); + HySkyBlockStats.Profile.Member member = activeProfile.getMember(stalkedPlayer.getUuid()); + for (Map.Entry<HySkyBlockStats.SkillLevel, Double> entry : member.getSkills().entrySet()) { + String skill = Utils.fancyCase(entry.getKey().name()); + int level = entry.getKey().getLevel(entry.getValue()); + if (level > 0) { + skillLevels.appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent(skill, String.valueOf(level))); + } + + if (level > highestLevel) { + highestSkill = skill; + highestLevel = level; + } + } + + // output inspired by /profiles hover + String coinsBankAndPurse = (activeProfile.getCoinBank() >= 0) ? Utils.formatNumberWithAbbreviations(activeProfile.getCoinBank() + member.getCoinPurse()) : "API access disabled"; + Pair<String, String> fancyFirstJoined = member.getFancyFirstJoined(); + + MooChatComponent wealthHover = new MooChatComponent("Accessible coins:").gold() + .appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Purse", Utils.formatNumberWithAbbreviations(member.getCoinPurse()))) + .appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Bank", (activeProfile.getCoinBank() != -1) ? Utils.formatNumberWithAbbreviations(activeProfile.getCoinBank()) : "API access disabled")); + if (activeProfile.coopCount() > 0) { + wealthHover.appendFreshSibling(new ChatComponentText(" ")); + wealthHover.appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Co-op members", String.valueOf(activeProfile.coopCount()))); + wealthHover.appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent("Co-ops' purses sum", Utils.formatNumberWithAbbreviations(activeProfile.getCoopCoinPurses(stalkedPlayer.getUuid())))); + } + + MooChatComponent sbStats = new MooChatComponent("SkyBlock stats of " + stalkedPlayer.getName() + " (" + activeProfile.getCuteName() + ")").gold().bold().setUrl("https://sky.lea.moe/stats/" + stalkedPlayer.getName() + "/" + activeProfile.getCuteName(), "Click to view SkyBlock stats on sky.lea.moe") + .appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Coins", coinsBankAndPurse).setHover(wealthHover)); + if (highestSkill != null) { + if (highestLevel == 0) { + sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Highest Skill", "All skills level 0")); + } else { + sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Highest Skill", highestSkill + " " + highestLevel).setHover(skillLevels)); + } + } else { + sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Highest Skill", "API access disabled")); + } + + Pair<Integer, Integer> uniqueMinionsData = activeProfile.getUniqueMinions(); + String uniqueMinions = String.valueOf(uniqueMinionsData.first()); + if (uniqueMinionsData.second() > activeProfile.coopCount()) { + // all players have their unique minions api access disabled + uniqueMinions = "API access disabled"; + } else if (uniqueMinionsData.second() > 0) { + // at least one player has their unique minions api access disabled + uniqueMinions += " or more (" + uniqueMinionsData.second() + "/" + (activeProfile.coopCount() + 1) + " have their API access disabled)"; + } + + sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Unique Minions", uniqueMinions)); + sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Fairy Souls", (member.getFairySoulsCollected() >= 0) ? String.valueOf(member.getFairySoulsCollected()) : "API access disabled")); + sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Profile age", fancyFirstJoined.first()).setHover(new MooChatComponent.KeyValueTooltipComponent("Join date", fancyFirstJoined.second()))); + + main.getChatHelper().sendMessage(sbStats); + } else { + String cause = (hySBStalking != null) ? hySBStalking.getCause() : null; + throw new ApiContactException("Hypixel", "couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + (cause != null ? " (Reason: " + EnumChatFormatting.DARK_RED + cause + EnumChatFormatting.RED + ")" : "") + "."); + } + }); + } + + private void handleBestFriendAdd(String username) throws CommandException { if (!Utils.isValidMcName(username)) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + "? This... doesn't look like a valid username."); - return; + throw new InvalidPlayerNameException(username); } // TODO Add check if 'best friend' is on normal friend list if (main.getFriendsHandler().isBestFriend(username, true)) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " is a best friend already."); + throw new MooCommandException(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 @@ -223,17 +337,16 @@ public class MooCommand extends CommandBase { } } - private void handleBestFriendRemove(String username) { + private void handleBestFriendRemove(String username) throws CommandException { if (!Utils.isValidMcName(username)) { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + "? This... doesn't look like a valid username."); - return; + throw new InvalidPlayerNameException(username); } boolean removed = main.getFriendsHandler().removeBestFriend(username); if (removed) { main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Removed " + EnumChatFormatting.DARK_GREEN + username + EnumChatFormatting.GREEN + " from best friends list."); } else { - main.getChatHelper().sendMessage(EnumChatFormatting.RED, EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " isn't a best friend."); + throw new MooCommandException(EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " isn't a best friend."); } } @@ -255,9 +368,10 @@ public class MooCommand extends CommandBase { } private void sendCommandUsage(ICommandSender sender) { - IChatComponent usage = new ChatComponentText("\u279C " + Cowmoonication.MODNAME + " commands:").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true)) + IChatComponent usage = new MooChatComponent("\u279C " + Cowmoonication.MODNAME + " commands:").gold().bold() .appendSibling(createCmdHelpSection(1, "Friends")) .appendSibling(createCmdHelpEntry("stalk", "Get info of player's status")) + .appendSibling(createCmdHelpEntry("stalkskyblock", "Get info of player's SkyBlock stats")) .appendSibling(createCmdHelpEntry("add", "Add best friends")) .appendSibling(createCmdHelpEntry("remove", "Remove best friends")) .appendSibling(createCmdHelpEntry("list", "View list of best friends")) @@ -282,11 +396,8 @@ public class MooCommand extends CommandBase { private IChatComponent createCmdHelpEntry(String cmd, String usage) { String command = "/" + this.getCommandName() + " " + cmd; - ChatStyle clickableMsg = new ChatStyle() - .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + command))) - .setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command)); - return new ChatComponentText("\n").appendSibling(new ChatComponentText(command).setChatStyle(clickableMsg.createDeepCopy().setColor(EnumChatFormatting.GOLD))) - .appendSibling(new ChatComponentText(" \u27A1 " + usage).setChatStyle(clickableMsg.createDeepCopy().setColor(EnumChatFormatting.YELLOW))); + + return new MooChatComponent("\n").reset().appendSibling(new MooChatComponent.KeyValueChatComponent(command, usage, " \u27A1 ").setSuggestCommand(command)); } @Override @@ -298,7 +409,7 @@ public class MooCommand extends CommandBase { public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) { if (args.length == 1) { return getListOfStringsMatchingLastWord(args, - /* friends */ "stalk", "add", "remove", "list", "nameChangeCheck", "toggle", + /* friends */ "stalk", "stalkskyblock", "skyblockstalk", "add", "remove", "list", "nameChangeCheck", "toggle", /* miscellaneous */ "config", "guiscale", "shrug", "apikey", /* update mod */ "update", "updateHelp", "version", "folder", /* help */ "help"); diff --git a/src/main/java/eu/olli/cowmoonication/command/exception/ApiContactException.java b/src/main/java/eu/olli/cowmoonication/command/exception/ApiContactException.java new file mode 100644 index 0000000..9dd8d9e --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/command/exception/ApiContactException.java @@ -0,0 +1,7 @@ +package eu.olli.cowmoonication.command.exception; + +public class ApiContactException extends MooCommandException { + public ApiContactException(String api, String failedAction) { + super("Sorry, couldn't contact the " + api + " API and thus " + failedAction); + } +} diff --git a/src/main/java/eu/olli/cowmoonication/command/exception/InvalidPlayerNameException.java b/src/main/java/eu/olli/cowmoonication/command/exception/InvalidPlayerNameException.java new file mode 100644 index 0000000..7afdeaa --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/command/exception/InvalidPlayerNameException.java @@ -0,0 +1,10 @@ +package eu.olli.cowmoonication.command.exception; + +import net.minecraft.command.SyntaxErrorException; +import net.minecraft.util.EnumChatFormatting; + +public class InvalidPlayerNameException extends SyntaxErrorException { + public InvalidPlayerNameException(String playerName) { + super(EnumChatFormatting.DARK_RED + playerName + EnumChatFormatting.RED + "? This... doesn't look like a valid username."); + } +} diff --git a/src/main/java/eu/olli/cowmoonication/command/exception/MooCommandException.java b/src/main/java/eu/olli/cowmoonication/command/exception/MooCommandException.java new file mode 100644 index 0000000..6c3cb08 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/command/exception/MooCommandException.java @@ -0,0 +1,9 @@ +package eu.olli.cowmoonication.command.exception; + +import net.minecraft.command.CommandException; + +public class MooCommandException extends CommandException { + public MooCommandException(String msg) { + super("cowmoonication.commands.generic.exception", msg); + } +} diff --git a/src/main/java/eu/olli/cowmoonication/command/exception/ThrowingConsumer.java b/src/main/java/eu/olli/cowmoonication/command/exception/ThrowingConsumer.java new file mode 100644 index 0000000..589ada6 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/command/exception/ThrowingConsumer.java @@ -0,0 +1,25 @@ +package eu.olli.cowmoonication.command.exception; + +import eu.olli.cowmoonication.Cowmoonication; +import net.minecraft.command.CommandException; +import net.minecraft.util.ChatComponentTranslation; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; + +import java.util.function.Consumer; + +@FunctionalInterface +public interface ThrowingConsumer<T> extends Consumer<T> { + @Override + default void accept(T t) { + try { + acceptThrows(t); + } catch (CommandException e) { + IChatComponent errorMsg = new ChatComponentTranslation(e.getMessage(), e.getErrorObjects()); + errorMsg.getChatStyle().setColor(EnumChatFormatting.RED); + Cowmoonication.getInstance().getChatHelper().sendMessage(errorMsg); + } + } + + void acceptThrows(T t) throws CommandException; +} diff --git a/src/main/java/eu/olli/cowmoonication/data/HySkyBlockStats.java b/src/main/java/eu/olli/cowmoonication/data/HySkyBlockStats.java new file mode 100644 index 0000000..a9735c3 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/data/HySkyBlockStats.java @@ -0,0 +1,302 @@ +package eu.olli.cowmoonication.data; + +import com.mojang.realmsclient.util.Pair; +import com.mojang.util.UUIDTypeAdapter; +import eu.olli.cowmoonication.util.Utils; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; + +public class HySkyBlockStats { + private boolean success; + private String cause; + private List<Profile> profiles; + + /** + * No-args constructor for GSON + */ + private HySkyBlockStats() { + } + + public boolean isSuccess() { + return success; + } + + public String getCause() { + return cause; + } + + public Profile getActiveProfile(UUID uuid) { + if (profiles == null) { + return null; + } + Profile lastSavedProfile = null; + long latestSave = -1; + for (Profile profile : profiles) { + long lastProfileSave = profile.getMember(uuid).last_save; + if (latestSave < lastProfileSave) { + lastSavedProfile = profile; + latestSave = lastProfileSave; + } + } + return lastSavedProfile; + } + + public static class Profile { + private String cute_name; + private Map<String, Member> members; + private Banking banking; + + /** + * No-args constructor for GSON + */ + private Profile() { + } + + public String getCuteName() { + return cute_name; + } + + public Member getMember(UUID uuid) { + return members.get(UUIDTypeAdapter.fromUUID(uuid)); + } + + public double getCoinBank() { + return (banking != null) ? banking.balance : -1; + } + + public int coopCount() { + return members.size() - 1; + } + + public double getCoopCoinPurses(UUID stalkedUuid) { + double coopCoinPurses = 0; + for (Map.Entry<String, Member> memberEntry : members.entrySet()) { + if (memberEntry.getKey().equals(UUIDTypeAdapter.fromUUID(stalkedUuid))) { + // don't include stalked player's purse again, only coops' purse + continue; + } + coopCoinPurses += memberEntry.getValue().getCoinPurse(); + } + return coopCoinPurses; + } + + public Pair<Integer, Integer> getUniqueMinions() { + int uniqueMinions = 0; + int membersWithDisabledApi = 0; + for (Member member : members.values()) { + if (member.crafted_generators != null) { + if (uniqueMinions > 0) { + --uniqueMinions; // subtract duplicate COBBLESTONE_1 minion + } + uniqueMinions += member.crafted_generators.size(); + } else { + ++membersWithDisabledApi; + } + } + return Pair.of(uniqueMinions, membersWithDisabledApi); + } + + public static class Member { + private long last_save; + private long first_join; + private double coin_purse; + private List<String> crafted_generators; + private int fairy_souls_collected = -1; + private double experience_skill_farming = -1; + private double experience_skill_mining = -1; + private double experience_skill_combat = -1; + private double experience_skill_foraging = -1; + private double experience_skill_fishing = -1; + private double experience_skill_enchanting = -1; + private double experience_skill_alchemy = -1; + private double experience_skill_carpentry = -1; + private double experience_skill_runecrafting = -1; + private double experience_skill_taming = -1; + + /** + * No-args constructor for GSON + */ + private Member() { + } + + public Pair<String, String> getFancyFirstJoined() { + return Utils.getLastOnlineWords(first_join); + } + + public double getCoinPurse() { + return coin_purse; + } + + public int getFairySoulsCollected() { + return fairy_souls_collected; + } + + public Map<SkillLevel, Double> getSkills() { + Map<SkillLevel, Double> skills = new TreeMap<>(); + if (experience_skill_farming >= 0) { + skills.put(SkillLevel.FARMING, experience_skill_farming); + } + if (experience_skill_mining >= 0) { + skills.put(SkillLevel.MINING, experience_skill_mining); + } + if (experience_skill_combat >= 0) { + skills.put(SkillLevel.COMBAT, experience_skill_combat); + } + if (experience_skill_foraging >= 0) { + skills.put(SkillLevel.FORAGING, experience_skill_foraging); + } + if (experience_skill_fishing >= 0) { + skills.put(SkillLevel.FISHING, experience_skill_fishing); + } + if (experience_skill_enchanting >= 0) { + skills.put(SkillLevel.ENCHANTING, experience_skill_enchanting); + } + if (experience_skill_alchemy >= 0) { + skills.put(SkillLevel.ALCHEMY, experience_skill_alchemy); + } + if (experience_skill_carpentry >= 0) { + skills.put(SkillLevel.CARPENTRY, experience_skill_carpentry); + } + if (experience_skill_runecrafting >= 0) { + skills.put(SkillLevel.RUNECRAFTING, experience_skill_runecrafting); + } + if (experience_skill_taming >= 0) { + skills.put(SkillLevel.TAMING, experience_skill_taming); + } + return skills; + } + } + + public static class Banking { + private double balance; + // private List<Transaction> transactions; + + /** + * No-args constructor for GSON + */ + private Banking() { + } + + // private class Transaction { + // private int amount; + // private long timestamp; + // private Transaction.Action action; + // private String initiator_name; + // + // /** + // * No-args constructor for GSON + // */ + // private Transaction() { + // } + // } + } + } + + public enum SkillLevel { + FARMING, MINING, COMBAT, FORAGING, FISHING, ENCHANTING, ALCHEMY, CARPENTRY, RUNECRAFTING(true), TAMING; + private final boolean alternativeXpFormula; + private static final TreeMap<Integer, Integer> XP_TO_LEVEL = new TreeMap<>(); + private static final TreeMap<Integer, Integer> XP_TO_LEVEL_ALTERNATIVE = new TreeMap<>(); + + static { + // exp data taken from https://api.hypixel.net/resources/skyblock/skills + XP_TO_LEVEL.put(0, 0); + XP_TO_LEVEL.put(50, 1); + XP_TO_LEVEL.put(175, 2); + XP_TO_LEVEL.put(375, 3); + XP_TO_LEVEL.put(675, 4); + XP_TO_LEVEL.put(1175, 5); + XP_TO_LEVEL.put(1925, 6); + XP_TO_LEVEL.put(2925, 7); + XP_TO_LEVEL.put(4425, 8); + XP_TO_LEVEL.put(6425, 9); + XP_TO_LEVEL.put(9925, 10); + XP_TO_LEVEL.put(14925, 11); + XP_TO_LEVEL.put(22425, 12); + XP_TO_LEVEL.put(32425, 13); + XP_TO_LEVEL.put(47425, 14); + XP_TO_LEVEL.put(67425, 15); + XP_TO_LEVEL.put(97425, 16); + XP_TO_LEVEL.put(147425, 17); + XP_TO_LEVEL.put(222425, 18); + XP_TO_LEVEL.put(322425, 19); + XP_TO_LEVEL.put(522425, 20); + XP_TO_LEVEL.put(822425, 21); + XP_TO_LEVEL.put(1222425, 22); + XP_TO_LEVEL.put(1722425, 23); + XP_TO_LEVEL.put(2322425, 24); + XP_TO_LEVEL.put(3022425, 25); + XP_TO_LEVEL.put(3822425, 26); + XP_TO_LEVEL.put(4722425, 27); + XP_TO_LEVEL.put(5722425, 28); + XP_TO_LEVEL.put(6822425, 29); + XP_TO_LEVEL.put(8022425, 30); + XP_TO_LEVEL.put(9322425, 31); + XP_TO_LEVEL.put(10722425, 32); + XP_TO_LEVEL.put(12222425, 33); + XP_TO_LEVEL.put(13822425, 34); + XP_TO_LEVEL.put(15522425, 35); + XP_TO_LEVEL.put(17322425, 36); + XP_TO_LEVEL.put(19222425, 37); + XP_TO_LEVEL.put(21222425, 38); + XP_TO_LEVEL.put(23322425, 39); + XP_TO_LEVEL.put(25522425, 40); + XP_TO_LEVEL.put(27822425, 41); + XP_TO_LEVEL.put(30222425, 42); + XP_TO_LEVEL.put(32722425, 43); + XP_TO_LEVEL.put(35322425, 44); + XP_TO_LEVEL.put(38072425, 45); + XP_TO_LEVEL.put(40972425, 46); + XP_TO_LEVEL.put(44072425, 47); + XP_TO_LEVEL.put(47472425, 48); + XP_TO_LEVEL.put(51172425, 49); + XP_TO_LEVEL.put(55172425, 50); + + XP_TO_LEVEL_ALTERNATIVE.put(0, 0); + XP_TO_LEVEL_ALTERNATIVE.put(50, 1); + XP_TO_LEVEL_ALTERNATIVE.put(150, 2); + XP_TO_LEVEL_ALTERNATIVE.put(275, 3); + XP_TO_LEVEL_ALTERNATIVE.put(435, 4); + XP_TO_LEVEL_ALTERNATIVE.put(635, 5); + XP_TO_LEVEL_ALTERNATIVE.put(885, 6); + XP_TO_LEVEL_ALTERNATIVE.put(1200, 7); + XP_TO_LEVEL_ALTERNATIVE.put(1600, 8); + XP_TO_LEVEL_ALTERNATIVE.put(2100, 9); + XP_TO_LEVEL_ALTERNATIVE.put(2725, 10); + XP_TO_LEVEL_ALTERNATIVE.put(3510, 11); + XP_TO_LEVEL_ALTERNATIVE.put(4510, 12); + XP_TO_LEVEL_ALTERNATIVE.put(5760, 13); + XP_TO_LEVEL_ALTERNATIVE.put(7325, 14); + XP_TO_LEVEL_ALTERNATIVE.put(9325, 15); + XP_TO_LEVEL_ALTERNATIVE.put(11825, 16); + XP_TO_LEVEL_ALTERNATIVE.put(14950, 17); + XP_TO_LEVEL_ALTERNATIVE.put(18950, 18); + XP_TO_LEVEL_ALTERNATIVE.put(23950, 19); + XP_TO_LEVEL_ALTERNATIVE.put(30200, 20); + XP_TO_LEVEL_ALTERNATIVE.put(38050, 21); + XP_TO_LEVEL_ALTERNATIVE.put(47850, 22); + XP_TO_LEVEL_ALTERNATIVE.put(60100, 23); + XP_TO_LEVEL_ALTERNATIVE.put(75400, 24); + + } + + SkillLevel() { + this(false); + } + + SkillLevel(boolean alternativeXpFormula) { + this.alternativeXpFormula = alternativeXpFormula; + } + + public int getLevel(double exp) { + if (alternativeXpFormula) { + return XP_TO_LEVEL_ALTERNATIVE.floorEntry((int) exp).getValue(); + } else { + return XP_TO_LEVEL.floorEntry((int) exp).getValue(); + } + } + } +} diff --git a/src/main/java/eu/olli/cowmoonication/data/SlothStalkingData.java b/src/main/java/eu/olli/cowmoonication/data/SlothStalkingData.java index 1072634..6fc7639 100644 --- a/src/main/java/eu/olli/cowmoonication/data/SlothStalkingData.java +++ b/src/main/java/eu/olli/cowmoonication/data/SlothStalkingData.java @@ -15,6 +15,10 @@ public class SlothStalkingData { public SlothStalkingData() { } + public String getPlayerName() { + return username; + } + public String getPlayerNameFormatted() { return rank_formatted.replace('&', 'ยง') + " " + username; } diff --git a/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java b/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java index 119e289..9ec469e 100644 --- a/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java +++ b/src/main/java/eu/olli/cowmoonication/handler/FriendsHandler.java @@ -3,11 +3,13 @@ package eu.olli.cowmoonication.handler; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; import eu.olli.cowmoonication.Cowmoonication; +import eu.olli.cowmoonication.command.exception.ApiContactException; import eu.olli.cowmoonication.data.Friend; import eu.olli.cowmoonication.util.ApiUtils; import eu.olli.cowmoonication.util.GsonUtils; import eu.olli.cowmoonication.util.TickDelay; import io.netty.util.internal.ConcurrentSet; +import net.minecraft.command.PlayerNotFoundException; import net.minecraft.event.ClickEvent; import net.minecraft.event.HoverEvent; import net.minecraft.util.ChatComponentText; @@ -56,9 +58,9 @@ public class FriendsHandler { 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."); + throw new ApiContactException("Mojang", "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 + "."); + throw new PlayerNotFoundException("There is no player with the name " + EnumChatFormatting.DARK_RED + name + EnumChatFormatting.RED + "."); } else { boolean added = bestFriends.add(friend); if (added) { @@ -108,7 +110,7 @@ public class FriendsHandler { 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?"); + throw new PlayerNotFoundException("How did you manage to get a unique id on your best friends list that has no name attached to it?"); } else if (newName.equals(friend.getName())) { // name hasn't changed, only updating lastChecked timestamp Friend bestFriend = getBestFriend(friend.getUuid()); diff --git a/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java b/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java index aca1819..8515593 100644 --- a/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java +++ b/src/main/java/eu/olli/cowmoonication/util/ApiUtils.java @@ -4,8 +4,10 @@ import com.google.gson.JsonArray; import com.google.gson.JsonParser; import com.mojang.util.UUIDTypeAdapter; import eu.olli.cowmoonication.Cowmoonication; +import eu.olli.cowmoonication.command.exception.ThrowingConsumer; import eu.olli.cowmoonication.config.MooConfig; import eu.olli.cowmoonication.data.Friend; +import eu.olli.cowmoonication.data.HySkyBlockStats; import eu.olli.cowmoonication.data.HyStalkingData; import eu.olli.cowmoonication.data.SlothStalkingData; import org.apache.http.HttpStatus; @@ -18,20 +20,20 @@ import java.net.HttpURLConnection; import java.net.URL; 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 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 ApiUtils() { } - public static void fetchFriendData(String name, Consumer<Friend> action) { + public static void fetchFriendData(String name, ThrowingConsumer<Friend> action) { pool.execute(() -> action.accept(getFriend(name))); } @@ -48,7 +50,7 @@ public class ApiUtils { return null; } - public static void fetchCurrentName(Friend friend, Consumer<String> action) { + public static void fetchCurrentName(Friend friend, ThrowingConsumer<String> action) { pool.execute(() -> action.accept(getCurrentName(friend))); } @@ -68,7 +70,7 @@ public class ApiUtils { return null; } - public static void fetchPlayerStatus(Friend friend, Consumer<HyStalkingData> action) { + public static void fetchPlayerStatus(Friend friend, ThrowingConsumer<HyStalkingData> action) { pool.execute(() -> action.accept(stalkPlayer(friend))); } @@ -83,7 +85,22 @@ public class ApiUtils { return null; } - public static void fetchPlayerOfflineStatus(Friend stalkedPlayer, Consumer<SlothStalkingData> action) { + public static void fetchSkyBlockStats(Friend friend, ThrowingConsumer<HySkyBlockStats> action) { + pool.execute(() -> action.accept(stalkSkyBlockStats(friend))); + } + + private static HySkyBlockStats stalkSkyBlockStats(Friend friend) { + try (BufferedReader reader = makeApiCall(String.format(SKYBLOCK_STATS_URL_OFFICIAL, MooConfig.moo, UUIDTypeAdapter.fromUUID(friend.getUuid())))) { + if (reader != null) { + return GsonUtils.fromJson(reader, HySkyBlockStats.class); + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public static void fetchPlayerOfflineStatus(Friend stalkedPlayer, ThrowingConsumer<SlothStalkingData> action) { pool.execute(() -> action.accept(stalkOfflinePlayer(stalkedPlayer))); } diff --git a/src/main/java/eu/olli/cowmoonication/util/MooChatComponent.java b/src/main/java/eu/olli/cowmoonication/util/MooChatComponent.java new file mode 100644 index 0000000..a489391 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/util/MooChatComponent.java @@ -0,0 +1,186 @@ +package eu.olli.cowmoonication.util; + +import net.minecraft.event.ClickEvent; +import net.minecraft.event.HoverEvent; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; + +public class MooChatComponent extends ChatComponentText { + public MooChatComponent(String msg) { + super(msg); + } + + public MooChatComponent black() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.BLACK)); + return this; + } + + public MooChatComponent darkBlue() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_BLUE)); + return this; + } + + public MooChatComponent darkGreen() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_GREEN)); + return this; + } + + public MooChatComponent darkAqua() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_AQUA)); + return this; + } + + public MooChatComponent darkRed() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_RED)); + return this; + } + + public MooChatComponent darkPurple() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_PURPLE)); + return this; + } + + public MooChatComponent gold() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.GOLD)); + return this; + } + + public MooChatComponent gray() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.GRAY)); + return this; + } + + public MooChatComponent darkGray() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_GRAY)); + return this; + } + + public MooChatComponent blue() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.BLUE)); + return this; + } + + public MooChatComponent green() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.GREEN)); + return this; + } + + public MooChatComponent aqua() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.AQUA)); + return this; + } + + public MooChatComponent red() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.RED)); + return this; + } + + public MooChatComponent lightPurple() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.LIGHT_PURPLE)); + return this; + } + + public MooChatComponent yellow() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.YELLOW)); + return this; + } + + public MooChatComponent white() { + setChatStyle(getChatStyle().setColor(EnumChatFormatting.WHITE)); + return this; + } + + public MooChatComponent obfuscated() { + setChatStyle(getChatStyle().setObfuscated(true)); + return this; + } + + public MooChatComponent bold() { + setChatStyle(getChatStyle().setBold(true)); + return this; + } + + public MooChatComponent strikethrough() { + setChatStyle(getChatStyle().setStrikethrough(true)); + return this; + } + + public MooChatComponent underline() { + setChatStyle(getChatStyle().setUnderlined(true)); + return this; + } + + public MooChatComponent italic() { + setChatStyle(getChatStyle().setItalic(true)); + return this; + } + + public MooChatComponent reset() { + setChatStyle(getChatStyle().setParentStyle(null).setBold(false).setItalic(false).setObfuscated(false).setUnderlined(false).setStrikethrough(false)); + return this; + } + + public MooChatComponent setHover(IChatComponent hover) { + setChatStyle(getChatStyle().setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover))); + return this; + } + + public MooChatComponent setUrl(String url) { + setUrl(url, new KeyValueTooltipComponent("Click to visit", url)); + return this; + } + + public MooChatComponent setUrl(String url, String hover) { + setUrl(url, new MooChatComponent(hover).yellow()); + return this; + } + + public MooChatComponent setUrl(String url, IChatComponent hover) { + setChatStyle(getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url))); + setHover(hover); + return this; + } + + public MooChatComponent setSuggestCommand(String command) { + setChatStyle(getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command))); + setHover(new KeyValueChatComponent("Run", command, " ")); + return this; + } + + /** + * Appends the given component in a new line, without inheriting formatting of previous siblings. + * + * @see ChatComponentText#appendSibling appendSibling + */ + public MooChatComponent appendFreshSibling(IChatComponent sibling) { + this.siblings.add(new ChatComponentText("\n").appendSibling(sibling)); + return this; + } + + @Deprecated + public MooChatComponent appendKeyValue(String key, String value) { + appendSibling(new MooChatComponent("\n").appendFreshSibling(new KeyValueChatComponent(key, value))); + return this; + } + + public static class KeyValueChatComponent extends MooChatComponent { + public KeyValueChatComponent(String key, String value) { + this(key, value, ": "); + } + + public KeyValueChatComponent(String key, String value, String separator) { + super(key); + appendText(separator); + gold().appendSibling(new MooChatComponent(value).yellow()); + } + } + + public static class KeyValueTooltipComponent extends MooChatComponent { + public KeyValueTooltipComponent(String key, String value) { + super(key); + appendText(": "); + gray().appendSibling(new MooChatComponent(value).yellow()); + } + } +} diff --git a/src/main/java/eu/olli/cowmoonication/util/Utils.java b/src/main/java/eu/olli/cowmoonication/util/Utils.java index 0ff4010..ec355b9 100644 --- a/src/main/java/eu/olli/cowmoonication/util/Utils.java +++ b/src/main/java/eu/olli/cowmoonication/util/Utils.java @@ -11,6 +11,7 @@ import java.util.regex.Pattern; public final class Utils { public static final Pattern VALID_UUID_PATTERN = Pattern.compile("^(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$"); private static final Pattern VALID_USERNAME = Pattern.compile("^[\\w]{1,16}$"); + private static final char[] LARGE_NUMBERS = new char[]{'k', 'm', 'b', 't'}; private Utils() { } @@ -52,4 +53,28 @@ public final class Utils { dateFormatted); } } + + /** + * Formats a large number with abbreviations for each factor of a thousand (k, m, ...) + * + * @param number the number to format + * @return a String representing the number n formatted in a cool looking way. + * @see <a href="https://stackoverflow.com/a/4753866">Source</a> + */ + public static String formatNumberWithAbbreviations(double number) { + return formatNumberWithAbbreviations(number, 0); + } + + private static String formatNumberWithAbbreviations(double number, int iteration) { + @SuppressWarnings("IntegerDivisionInFloatingPointContext") double d = ((long) number / 100) / 10.0; + boolean isRound = (d * 10) % 10 == 0; //true if the decimal part is equal to 0 (then it's trimmed anyway) + // this determines the class, i.e. 'k', 'm' etc + // this decides whether to trim the decimals + // (int) d * 10 / 10 drops the decimal + return d < 1000 ? // this determines the class, i.e. 'k', 'm' etc + (d > 99.9 || isRound || d > 9.99 ? // this decides whether to trim the decimals + (int) d * 10 / 10 : d + "" // (int) d * 10 / 10 drops the decimal + ) + "" + LARGE_NUMBERS[iteration] + : formatNumberWithAbbreviations(d, iteration + 1); + } } |