aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/eu/olli/cowlection/command
diff options
context:
space:
mode:
authorCow <cow@volloeko.de>2020-07-05 05:42:45 +0200
committerCow <cow@volloeko.de>2020-07-05 05:42:45 +0200
commit1b446698398c648b38311975a6cfd54859ea5cfe (patch)
tree521ecc4ce9ad968281094eb8c5453dca606931e3 /src/main/java/eu/olli/cowlection/command
parentedaca1fd41a612c71c526ceb20b89c5dec2d81b3 (diff)
downloadCowlection-1b446698398c648b38311975a6cfd54859ea5cfe.tar.gz
Cowlection-1b446698398c648b38311975a6cfd54859ea5cfe.tar.bz2
Cowlection-1b446698398c648b38311975a6cfd54859ea5cfe.zip
Renamed mod to Cowlection
Bumped version to 1.8.9-0.7.0
Diffstat (limited to 'src/main/java/eu/olli/cowlection/command')
-rw-r--r--src/main/java/eu/olli/cowlection/command/MooCommand.java517
-rw-r--r--src/main/java/eu/olli/cowlection/command/ShrugCommand.java34
-rw-r--r--src/main/java/eu/olli/cowlection/command/TabCompletableCommand.java53
-rw-r--r--src/main/java/eu/olli/cowlection/command/exception/ApiContactException.java7
-rw-r--r--src/main/java/eu/olli/cowlection/command/exception/InvalidPlayerNameException.java10
-rw-r--r--src/main/java/eu/olli/cowlection/command/exception/MooCommandException.java9
-rw-r--r--src/main/java/eu/olli/cowlection/command/exception/ThrowingConsumer.java25
7 files changed, 655 insertions, 0 deletions
diff --git a/src/main/java/eu/olli/cowlection/command/MooCommand.java b/src/main/java/eu/olli/cowlection/command/MooCommand.java
new file mode 100644
index 0000000..1798568
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/command/MooCommand.java
@@ -0,0 +1,517 @@
+package eu.olli.cowlection.command;
+
+import com.mojang.realmsclient.util.Pair;
+import eu.olli.cowlection.Cowlection;
+import eu.olli.cowlection.command.exception.ApiContactException;
+import eu.olli.cowlection.command.exception.InvalidPlayerNameException;
+import eu.olli.cowlection.command.exception.MooCommandException;
+import eu.olli.cowlection.config.MooConfig;
+import eu.olli.cowlection.config.MooGuiConfig;
+import eu.olli.cowlection.data.DataHelper;
+import eu.olli.cowlection.data.Friend;
+import eu.olli.cowlection.data.HySkyBlockStats;
+import eu.olli.cowlection.data.HyStalkingData;
+import eu.olli.cowlection.search.GuiSearch;
+import eu.olli.cowlection.util.*;
+import net.minecraft.client.Minecraft;
+import net.minecraft.command.*;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.item.EntityArmorStand;
+import net.minecraft.event.ClickEvent;
+import net.minecraft.event.HoverEvent;
+import net.minecraft.item.ItemSkull;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.*;
+import net.minecraftforge.common.util.Constants;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class MooCommand extends CommandBase {
+ private final Cowlection main;
+
+ public MooCommand(Cowlection main) {
+ this.main = main;
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ if (args.length == 0) {
+ sendCommandUsage(sender);
+ return;
+ }
+ // sub commands: friends & other players
+ if (args[0].equalsIgnoreCase("stalk")) {
+ if (args.length != 2) {
+ throw new WrongUsageException("/" + getCommandName() + " stalk <playerName>");
+ } else if (!Utils.isValidMcName(args[1])) {
+ 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[0].equalsIgnoreCase("analyzeIsland")) {
+ Map<String, String> minions = DataHelper.getMinions();
+
+ Map<String, Integer> detectedMinions = new HashMap<>();
+ Map<Integer, Integer> detectedMinionsWithSkin = new HashMap<>();
+ int detectedMinionCount = 0;
+ int minionsWithSkinCount = 0;
+ entityLoop:
+ for (Entity entity : sender.getEntityWorld().loadedEntityList) {
+ if (entity instanceof EntityArmorStand) {
+ EntityArmorStand minion = (EntityArmorStand) entity;
+
+ if (minion.isInvisible() || !minion.isSmall() || minion.getHeldItem() == null) {
+ // not a minion: invisible, or not small armor stand, or no item in hand (= minion in a minion chair)
+ continue;
+ }
+ for (int slot = 0; slot < 4; slot++) {
+ if (minion.getCurrentArmor(slot) == null) {
+ // not a minion: missing equipment
+ continue entityLoop;
+ }
+ }
+ ItemStack skullItem = minion.getCurrentArmor(3); // head slot
+ if (skullItem.getItem() instanceof ItemSkull && skullItem.getMetadata() == 3 && skullItem.hasTagCompound()) {
+ // is a player head!
+ if (skullItem.getTagCompound().hasKey("SkullOwner", Constants.NBT.TAG_COMPOUND)) {
+ NBTTagCompound skullOwner = skullItem.getTagCompound().getCompoundTag("SkullOwner");
+ String skullDataBase64 = skullOwner.getCompoundTag("Properties").getTagList("textures", Constants.NBT.TAG_COMPOUND).getCompoundTagAt(0).getString("Value");
+ String skullData = new String(Base64.decodeBase64(skullDataBase64));
+ String minionSkinId = StringUtils.substringBetween(skullData, "http://textures.minecraft.net/texture/", "\"");
+ String detectedMinion = minions.get(minionSkinId);
+ if (detectedMinion != null) {
+ // minion head matches one know minion tier
+ detectedMinions.put(detectedMinion, detectedMinions.getOrDefault(detectedMinion, 0) + 1);
+ detectedMinionCount++;
+ } else {
+ int minionTier = ImageUtils.getTierFromTexture(minionSkinId);
+ if (minionTier > 0) {
+ detectedMinionsWithSkin.put(minionTier, detectedMinionsWithSkin.getOrDefault(minionTier, 0) + 1);
+ minionsWithSkinCount++;
+ } else {
+ // looked like a minion but has no matching tier badge
+ main.getLogger().info("[/moo analyzeIsland] Found an armor stand that could be a minion but it is missing a tier badge: " + minionSkinId + "\t\t\t" + minion.serializeNBT());
+ }
+ }
+ }
+ }
+ }
+ }
+ StringBuilder analysisResults = new StringBuilder("Found ").append(EnumChatFormatting.GOLD).append(detectedMinionCount).append(EnumChatFormatting.YELLOW).append(" minions");
+ if (minionsWithSkinCount > 0) {
+ analysisResults.append(" + ").append(EnumChatFormatting.GOLD).append(minionsWithSkinCount).append(EnumChatFormatting.YELLOW).append(" unknown minions with skins");
+ }
+ analysisResults.append(" on this island");
+ detectedMinions.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey()) // sort alphabetically by minion name and tier
+ .forEach(minion -> {
+ String minionWithTier = minion.getKey();
+ int lastSpace = minionWithTier.lastIndexOf(' ');
+
+ String tierRoman = minionWithTier.substring(lastSpace + 1);
+
+ int tierArabic = Utils.convertRomanToArabic(tierRoman);
+ EnumChatFormatting tierColor = Utils.getMinionTierColor(tierArabic);
+
+ minionWithTier = minionWithTier.substring(0, lastSpace) + " " + tierColor + (MooConfig.useRomanNumerals() ? tierRoman : tierArabic);
+ analysisResults.append("\n ").append(EnumChatFormatting.GOLD).append(minion.getValue()).append(minion.getValue() > 1 ? "✕ " : "⨉ ")
+ .append(EnumChatFormatting.YELLOW).append(minionWithTier);
+ });
+ detectedMinionsWithSkin.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey()) // sort by tier
+ .forEach(minionWithSkin -> {
+ EnumChatFormatting tierColor = Utils.getMinionTierColor(minionWithSkin.getKey());
+ String minionTier = MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(minionWithSkin.getKey()) : String.valueOf(minionWithSkin.getKey());
+ analysisResults.append("\n ").append(EnumChatFormatting.GOLD).append(minionWithSkin.getValue()).append(minionWithSkin.getValue() > 1 ? "✕ " : "⨉ ")
+ .append(EnumChatFormatting.RED).append("Unknown minion ").append(EnumChatFormatting.YELLOW).append("(new or with minion skin) ").append(tierColor).append(minionTier);
+ });
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, analysisResults.toString());
+ } else 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.getFriendsHandler().updateBestFriends(true);
+ }
+ // 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(main.getConfigDirectory())), 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.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.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("shrug")) {
+ main.getChatHelper().sendShrug(buildString(args, 1));
+ } else if (args[0].equalsIgnoreCase("apikey")) {
+ handleApiKey(args);
+ }
+ // sub-commands: update mod
+ else if (args[0].equalsIgnoreCase("update")) {
+ boolean updateCheckStarted = main.getVersionChecker().runUpdateCheck(true);
+
+ if (updateCheckStarted) {
+ 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)));
+ 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))
+ .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 " + Cowlection.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 + Cowlection.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 + Cowlection.MODNAME.replace(" ", "") + "-" + Cowlection.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("directory") || args[0].equalsIgnoreCase("folder")) {
+ try {
+ Desktop.getDesktop().open(main.getModsDirectory());
+ } catch (IOException e) {
+ e.printStackTrace();
+ 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");
+ }
+ } else if (args[0].equalsIgnoreCase("help")) {
+ sendCommandUsage(sender);
+ }
+ // "catch-all" remaining sub-commands
+ else {
+ main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Command " + EnumChatFormatting.DARK_RED + "/" + getCommandName() + " " + args[0] + EnumChatFormatting.RED + " doesn't exist. Use " + EnumChatFormatting.DARK_RED + "/" + getCommandName() + " help " + EnumChatFormatting.RED + "to show command usage.");
+ }
+ }
+
+ private void handleApiKey(String[] args) throws CommandException {
+ if (args.length == 1) {
+ String firstSentence;
+ EnumChatFormatting color;
+ EnumChatFormatting colorSecondary;
+ if (Utils.isValidUuid(MooConfig.moo)) {
+ firstSentence = "You already set your Hypixel API key.";
+ color = EnumChatFormatting.GREEN;
+ colorSecondary = EnumChatFormatting.DARK_GREEN;
+ } else {
+ firstSentence = "You haven't set your Hypixel API key yet.";
+ color = EnumChatFormatting.RED;
+ colorSecondary = EnumChatFormatting.DARK_RED;
+ }
+ main.getChatHelper().sendMessage(color, firstSentence + " Use " + colorSecondary + "/api new" + color + " to request a new API key from Hypixel or use " + colorSecondary + "/" + this.getCommandName() + " apikey <key>" + color + " to manually set your existing API key.");
+ } else {
+ String key = args[1];
+ if (Utils.isValidUuid(key)) {
+ MooConfig.moo = key;
+ main.getConfig().syncFromFields();
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Updated API key!");
+ } else {
+ throw new SyntaxErrorException("That doesn't look like a valid API key...");
+ }
+ }
+ }
+
+ private void handleStalking(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 + ". 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
+ stalkPlayer(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
+ stalkPlayer(stalkedPlayer);
+ }
+ });
+ }
+ }
+
+ private void stalkPlayer(Friend stalkedPlayer) {
+ ApiUtils.fetchPlayerStatus(stalkedPlayer, hyStalking -> {
+ if (hyStalking != null && hyStalking.isSuccess()) {
+ HyStalkingData.HySession session = hyStalking.getSession();
+ if (session.isOnline()) {
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, EnumChatFormatting.GOLD + stalkedPlayer.getName() + EnumChatFormatting.YELLOW + " is currently playing " + EnumChatFormatting.GOLD + session.getGameType() + EnumChatFormatting.YELLOW
+ + (session.getMode() != null ? ": " + EnumChatFormatting.GOLD + session.getMode() : "")
+ + (session.getMap() != null ? EnumChatFormatting.YELLOW + " (Map: " + EnumChatFormatting.GOLD + session.getMap() + EnumChatFormatting.YELLOW + ")" : ""));
+ } else {
+ ApiUtils.fetchPlayerOfflineStatus(stalkedPlayer, slothStalking -> {
+ if (slothStalking == null) {
+ 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(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.getDurationAsWords(slothStalking.getLastLogin());
+
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, slothStalking.getPlayerNameFormatted() + EnumChatFormatting.YELLOW + " was last online " + EnumChatFormatting.GOLD + lastOnline.first() + EnumChatFormatting.YELLOW + " ago"
+ + (lastOnline.second() != null ? " (" + EnumChatFormatting.GOLD + lastOnline.second() + EnumChatFormatting.YELLOW + ")" : "") + ".");
+ } else {
+ Pair<String, String> lastOnline = Utils.getDurationAsWords(slothStalking.getLastLogout());
+
+ main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, slothStalking.getPlayerNameFormatted() + EnumChatFormatting.YELLOW + " is " + EnumChatFormatting.GOLD + "offline" + EnumChatFormatting.YELLOW + " for " + EnumChatFormatting.GOLD + lastOnline.first() + EnumChatFormatting.YELLOW
+ + ((lastOnline.second() != null || slothStalking.getLastGame() != null) ? (" ("
+ + (lastOnline.second() != null ? EnumChatFormatting.GOLD + lastOnline.second() + EnumChatFormatting.YELLOW : "") // = last online date
+ + (lastOnline.second() != null && slothStalking.getLastGame() != null ? "; " : "") // = delimiter
+ + (slothStalking.getLastGame() != null ? "last played gamemode: " + EnumChatFormatting.GOLD + slothStalking.getLastGame() + EnumChatFormatting.YELLOW : "") // = last gamemode
+ + ")") : "") + ".");
+ }
+ });
+ }
+ } else {
+ String cause = (hyStalking != null) ? hyStalking.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 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) {
+ String skillLevel = MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(level) : String.valueOf(level);
+ skillLevels.appendFreshSibling(new MooChatComponent.KeyValueTooltipComponent(skill, skillLevel));
+ }
+
+ 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 {
+ String highestSkillLevel = MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(highestLevel) : String.valueOf(highestLevel);
+ sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Highest Skill", highestSkill + " " + highestSkillLevel).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)) {
+ throw new InvalidPlayerNameException(username);
+ }
+
+ // TODO Add check if 'best friend' is on normal friend list
+ if (main.getFriendsHandler().isBestFriend(username, true)) {
+ 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
+ main.getFriendsHandler().addBestFriend(username);
+ }
+ }
+
+ private void handleBestFriendRemove(String username) throws CommandException {
+ if (!Utils.isValidMcName(username)) {
+ 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 {
+ throw new MooCommandException(EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " isn't a best friend.");
+ }
+ }
+
+ private void handleListBestFriends() {
+ Set<String> bestFriends = main.getFriendsHandler().getBestFriends();
+
+ // 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
+ public String getCommandName() {
+ return "moo";
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return "/" + getCommandName() + " help";
+ }
+
+ private void sendCommandUsage(ICommandSender sender) {
+ IChatComponent usage = new MooChatComponent("\u279C " + Cowlection.MODNAME + " commands:").gold().bold()
+ .appendSibling(createCmdHelpSection(1, "Friends & other players"))
+ .appendSibling(createCmdHelpEntry("stalk", "Get info of player's status"))
+ .appendSibling(createCmdHelpEntry("stalkskyblock", "Get info of player's SkyBlock stats"))
+ .appendSibling(createCmdHelpEntry("analyzeIsland", "Analyze a SkyBlock private island"))
+ .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 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("directory", "Open Minecraft's mods directory"));
+ sender.addChatMessage(usage);
+ }
+
+ private IChatComponent createCmdHelpSection(int nr, String title) {
+ String prefix = Character.toString((char) (0x2789 + nr));
+ return new ChatComponentText("\n").appendSibling(new ChatComponentText(prefix + " " + title).setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GOLD).setBold(true)));
+ }
+
+ private IChatComponent createCmdHelpEntry(String cmd, String usage) {
+ String command = "/" + this.getCommandName() + " " + cmd;
+
+ return new MooChatComponent("\n").reset().appendSibling(new MooChatComponent.KeyValueChatComponent(command, usage, " \u27A1 ").setSuggestCommand(command));
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+
+ @Override
+ public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) {
+ if (args.length == 1) {
+ return getListOfStringsMatchingLastWord(args,
+ /* friends & other players */ "stalk", "stalkskyblock", "skyblockstalk", "analyzeIsland", "add", "remove", "list", "nameChangeCheck", "toggle",
+ /* 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].toLowerCase().contains("stalk")) { // stalk & stalkskyblock
+ return getListOfStringsMatchingLastWord(args, main.getPlayerCache().getAllNamesSorted());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/command/ShrugCommand.java b/src/main/java/eu/olli/cowlection/command/ShrugCommand.java
new file mode 100644
index 0000000..3d48fac
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/command/ShrugCommand.java
@@ -0,0 +1,34 @@
+package eu.olli.cowlection.command;
+
+import eu.olli.cowlection.Cowlection;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.CommandException;
+import net.minecraft.command.ICommandSender;
+
+public class ShrugCommand extends CommandBase {
+ private final Cowlection main;
+
+ public ShrugCommand(Cowlection main) {
+ this.main = main;
+ }
+
+ @Override
+ public String getCommandName() {
+ return "shrug";
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return "/shrug [message]";
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ main.getChatHelper().sendShrug(args);
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/command/TabCompletableCommand.java b/src/main/java/eu/olli/cowlection/command/TabCompletableCommand.java
new file mode 100644
index 0000000..87b799e
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/command/TabCompletableCommand.java
@@ -0,0 +1,53 @@
+package eu.olli.cowlection.command;
+
+import eu.olli.cowlection.Cowlection;
+import net.minecraft.client.Minecraft;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.CommandException;
+import net.minecraft.command.ICommandSender;
+import net.minecraft.util.BlockPos;
+
+import java.util.List;
+
+/**
+ * This is not a real command. Its sole purpose is to add tab completion for usernames to server-side commands that do not provide tab completion for usernames by default.
+ */
+public class TabCompletableCommand extends CommandBase {
+ private final Cowlection main;
+ private final String cmdName;
+
+ public TabCompletableCommand(Cowlection main, String cmdName) {
+ this.main = main;
+ this.cmdName = cmdName;
+ }
+
+ @Override
+ public String getCommandName() {
+ return cmdName;
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return null;
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ // send client-command to server
+ Minecraft.getMinecraft().thePlayer.sendChatMessage("/" + getCommandName() + " " + CommandBase.buildString(args, 0));
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+
+ @Override
+ public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) {
+ if (args.length == 1 || args.length == 2) {
+ // suggest recently 'seen' usernames as tab-completion options.
+ return getListOfStringsMatchingLastWord(args, main.getPlayerCache().getAllNamesSorted());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/command/exception/ApiContactException.java b/src/main/java/eu/olli/cowlection/command/exception/ApiContactException.java
new file mode 100644
index 0000000..804fa1d
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/command/exception/ApiContactException.java
@@ -0,0 +1,7 @@
+package eu.olli.cowlection.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/cowlection/command/exception/InvalidPlayerNameException.java b/src/main/java/eu/olli/cowlection/command/exception/InvalidPlayerNameException.java
new file mode 100644
index 0000000..3c0c06e
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/command/exception/InvalidPlayerNameException.java
@@ -0,0 +1,10 @@
+package eu.olli.cowlection.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/cowlection/command/exception/MooCommandException.java b/src/main/java/eu/olli/cowlection/command/exception/MooCommandException.java
new file mode 100644
index 0000000..0cc55e0
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/command/exception/MooCommandException.java
@@ -0,0 +1,9 @@
+package eu.olli.cowlection.command.exception;
+
+import net.minecraft.command.CommandException;
+
+public class MooCommandException extends CommandException {
+ public MooCommandException(String msg) {
+ super("cowlection.commands.generic.exception", msg);
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/command/exception/ThrowingConsumer.java b/src/main/java/eu/olli/cowlection/command/exception/ThrowingConsumer.java
new file mode 100644
index 0000000..a1ed241
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/command/exception/ThrowingConsumer.java
@@ -0,0 +1,25 @@
+package eu.olli.cowlection.command.exception;
+
+import eu.olli.cowlection.Cowlection;
+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);
+ Cowlection.getInstance().getChatHelper().sendMessage(errorMsg);
+ }
+ }
+
+ void acceptThrows(T t) throws CommandException;
+}