From 1b446698398c648b38311975a6cfd54859ea5cfe Mon Sep 17 00:00:00 2001 From: Cow Date: Sun, 5 Jul 2020 05:42:45 +0200 Subject: Renamed mod to Cowlection Bumped version to 1.8.9-0.7.0 --- .../eu/olli/cowlection/command/MooCommand.java | 517 +++++++++++++++++++++ .../eu/olli/cowlection/command/ShrugCommand.java | 34 ++ .../cowlection/command/TabCompletableCommand.java | 53 +++ .../command/exception/ApiContactException.java | 7 + .../exception/InvalidPlayerNameException.java | 10 + .../command/exception/MooCommandException.java | 9 + .../command/exception/ThrowingConsumer.java | 25 + 7 files changed, 655 insertions(+) create mode 100644 src/main/java/eu/olli/cowlection/command/MooCommand.java create mode 100644 src/main/java/eu/olli/cowlection/command/ShrugCommand.java create mode 100644 src/main/java/eu/olli/cowlection/command/TabCompletableCommand.java create mode 100644 src/main/java/eu/olli/cowlection/command/exception/ApiContactException.java create mode 100644 src/main/java/eu/olli/cowlection/command/exception/InvalidPlayerNameException.java create mode 100644 src/main/java/eu/olli/cowlection/command/exception/MooCommandException.java create mode 100644 src/main/java/eu/olli/cowlection/command/exception/ThrowingConsumer.java (limited to 'src/main/java/eu/olli/cowlection/command') 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 "); + } 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 "); + } else if (!Utils.isValidMcName(args[1])) { + throw new InvalidPlayerNameException(args[1]); + } else { + handleStalkingSkyBlock(args[1]); + } + } else if (args[0].equalsIgnoreCase("analyzeIsland")) { + Map minions = DataHelper.getMinions(); + + Map detectedMinions = new HashMap<>(); + Map 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 " + 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 " + 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 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 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 " + 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 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 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 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 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 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 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 extends Consumer { + @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; +} -- cgit