diff options
author | Cow <cow@volloeko.de> | 2020-09-27 22:34:28 +0200 |
---|---|---|
committer | Cow <cow@volloeko.de> | 2020-09-27 22:34:28 +0200 |
commit | dba1ec3e7885c86e16283d149d088369ecc509fd (patch) | |
tree | 774f581375a219f7d45dc8e1b767d2250347bd91 | |
parent | 586660bb79ca0a53898396dcde3daab809e29fef (diff) | |
download | Cowlection-dba1ec3e7885c86e16283d149d088369ecc509fd.tar.gz Cowlection-dba1ec3e7885c86e16283d149d088369ecc509fd.tar.bz2 Cowlection-dba1ec3e7885c86e16283d149d088369ecc509fd.zip |
New command `/moo dungeon party`
- displays current `/party` members' armor and dungeons floor completions
7 files changed, 347 insertions, 5 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc0b25..46e01e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [1.8.9-0.11.0] - unreleased +### Added +- SkyBlock Dungeons Party: new command `/moo dungeon party` + - short alias: `/m dp`): + - displays current `/party` members' armor and dungeons floor completions + ### Changed - Completely re-done the config gui (`/moo config`) - now separated into sections and sub-sections @@ -12,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Improved SkyBlock dungeon party finder - more config options - marks (non-)joinable parties even better than before + - When a new player joins the party, it shows not only armor, but also completed dungeons stats - Improved SkyBlock dungeon performance overlay - Overlay can be moved more precisely - Dungeons can be "joined" and "left" manually (if the automatic detection fails): `/moo dungeon <enter/leave>` diff --git a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java index 7799e10..811254b 100644 --- a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java +++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java @@ -11,6 +11,7 @@ import de.cowtipper.cowlection.config.gui.MooConfigGui; import de.cowtipper.cowlection.data.*; import de.cowtipper.cowlection.data.HySkyBlockStats.Profile.Pet; import de.cowtipper.cowlection.handler.DungeonCache; +import de.cowtipper.cowlection.listener.skyblock.DungeonsPartyListener; import de.cowtipper.cowlection.search.GuiSearch; import de.cowtipper.cowlection.util.*; import net.minecraft.client.Minecraft; @@ -37,6 +38,7 @@ import java.util.concurrent.TimeUnit; public class MooCommand extends CommandBase { private final Cowlection main; + private DungeonsPartyListener dungeonsPartyListener; public MooCommand(Cowlection main) { this.main = main; @@ -94,7 +96,8 @@ public class MooCommand extends CommandBase { handleStalkingSkyBlock(args); } else if (args[0].equalsIgnoreCase("analyzeIsland")) { handleAnalyzeIsland(sender); - } else if (args[0].equalsIgnoreCase("dungeon") || args[0].equalsIgnoreCase("dung")) { + } else if (args[0].equalsIgnoreCase("dungeon") || args[0].equalsIgnoreCase("dung") + || /* dungeon party: */ args[0].equalsIgnoreCase("dp")) { handleDungeon(args); } //endregion @@ -656,10 +659,28 @@ public class MooCommand extends CommandBase { } else if (args.length == 2 && args[1].equalsIgnoreCase("leave")) { // leave dungeon in case for some reason it wasn't detected automatically dungeonCache.onDungeonEnterOrLeave(false); + } else if ((args.length == 2 && (args[1].equalsIgnoreCase("party") || args[1].equalsIgnoreCase("p"))) + || args.length == 1 && args[0].equalsIgnoreCase("dp")) { + if (!CredentialStorage.isMooValid) { + throw new MooCommandException("You haven't set your Hypixel API key yet or the API key is invalid. 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."); + } else if (dungeonsPartyListener != null) { + throw new MooCommandException("Please wait a few seconds before using this command again."); + } + main.getChatHelper().sendServerCommand("/party list"); + new TickDelay(() -> { + // abort after 10 seconds + if (dungeonsPartyListener.isStillRunning()) { + dungeonsPartyListener.shutdown(); + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Dungeon party analysis timed out. Probably the recognition of the party members failed."); + } + dungeonsPartyListener = null; + }, 10 * 20); + // register dungeon listener + dungeonsPartyListener = new DungeonsPartyListener(main); } else if (dungeonCache.isInDungeon()) { dungeonCache.sendDungeonPerformance(); } else { - throw new MooCommandException(EnumChatFormatting.DARK_RED + "Looks like you're not in a dungeon... However, you can manually enable the Dungeon Performance overlay with " + EnumChatFormatting.RED + "/" + getCommandName() + " dungeon enter" + EnumChatFormatting.DARK_RED + ". You can also force-leave a dungeon with " + EnumChatFormatting.RED + "/" + getCommandName() + " leave"); + throw new MooCommandException(EnumChatFormatting.DARK_RED + "Looks like you're not in a dungeon... However, you can manually enable the Dungeon Performance overlay with " + EnumChatFormatting.RED + "/" + getCommandName() + " dungeon enter" + EnumChatFormatting.DARK_RED + ". You can also force-leave a dungeon with " + EnumChatFormatting.RED + "/" + getCommandName() + " dungeon leave.\n" + EnumChatFormatting.GRAY + "Want to inspect your current party members? Use " + EnumChatFormatting.WHITE + "/" + getCommandName() + " dungeon party"); } } //endregion @@ -764,6 +785,7 @@ public class MooCommand extends CommandBase { .appendSibling(createCmdHelpEntry("stalkskyblock", "Get info of player's SkyBlock stats §d§l⚷")) .appendSibling(createCmdHelpEntry("analyzeIsland", "Analyze a SkyBlock private island")) .appendSibling(createCmdHelpEntry("dungeon", "SkyBlock Dungeons: display current dungeon performance")) + .appendSibling(createCmdHelpEntry("dungeon party", "SkyBlock Dungeons: Shows armor and dungeon info about current party members " + EnumChatFormatting.GRAY + "(alias: " + EnumChatFormatting.WHITE + "/" + getCommandName() + " dp" + EnumChatFormatting.GRAY + ") §d§l⚷")) .appendSibling(createCmdHelpSection(3, "Miscellaneous")) .appendSibling(createCmdHelpEntry("search", "Open Minecraft log search")) .appendSibling(createCmdHelpEntry("guiScale", "Change GUI scale")) @@ -805,6 +827,8 @@ public class MooCommand extends CommandBase { /* rarely used aliases */ "askPolitelyWhereTheyAre", "askPolitelyAboutTheirSkyBlockProgress"); } else if (args.length == 2 && args[0].equalsIgnoreCase("remove")) { return getListOfStringsMatchingLastWord(args, main.getFriendsHandler().getBestFriends()); + } else if (args.length == 2 && args[0].equalsIgnoreCase("dungeon")) { + return getListOfStringsMatchingLastWord(args, "party", "enter", "leave"); } String commandArg = args[0].toLowerCase(); if (args.length == 2 && (commandArg.equals("s") || commandArg.equals("ss") || commandArg.equals("namechangecheck") || commandArg.contains("stalk") || commandArg.contains("askpolitely"))) { // stalk & stalkskyblock + namechangecheck diff --git a/src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java b/src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java index 679a575..335f7fc 100644 --- a/src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java +++ b/src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java @@ -296,6 +296,34 @@ public class HySkyBlockStats { return player_classes.get(selected_dungeon_class).getLevel(); } + public StringBuilder getHighestFloorCompletions(int nHighestFloors, boolean indent) { + StringBuilder output = new StringBuilder(); + String spacer = indent ? "\n " : "\n"; + + Map<String, Type> latestDungeonType = Utils.getLastNMapEntries(dungeon_types, 1); + for (Map.Entry<String, Type> dungeonTypeEntry : latestDungeonType.entrySet()) { + output.append(spacer); + if (dungeonTypeEntry != null) { + Map<String, Integer> highestFloorCompletions = Utils.getLastNMapEntries(dungeonTypeEntry.getValue().getTierCompletions(), nHighestFloors); + String latestDungeonTypeName = Utils.fancyCase(dungeonTypeEntry.getKey()); + if (highestFloorCompletions != null) { + // top n highest floor completions: + output.append(spacer).append(EnumChatFormatting.BOLD).append(highestFloorCompletions.size()).append(" highest ").append(latestDungeonTypeName).append(" floors:"); + + for (Map.Entry<String, Integer> highestFloorEntry : highestFloorCompletions.entrySet()) { + int highestFloorHighestScore = dungeonTypeEntry.getValue().getBestScore().get(highestFloorEntry.getKey()); + output.append(spacer).append(EnumChatFormatting.GRAY).append(" Floor ").append(EnumChatFormatting.YELLOW).append(highestFloorEntry.getKey()).append(EnumChatFormatting.GRAY).append(": ") + .append(EnumChatFormatting.GOLD).append(highestFloorEntry.getValue()).append(EnumChatFormatting.GRAY).append(" completions (best score: ").append(EnumChatFormatting.LIGHT_PURPLE).append(highestFloorHighestScore).append(EnumChatFormatting.GRAY).append(")"); + } + } else { + // no floor completions yet + output.append(EnumChatFormatting.ITALIC).append("No ").append(latestDungeonTypeName).append(" floor completions yet"); + } + } + } + return output; + } + public static class Type { private Map<String, Integer> times_played; private Map<String, Integer> tier_completions; diff --git a/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java b/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java index 68d602e..9f7bb93 100644 --- a/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java +++ b/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java @@ -194,12 +194,23 @@ public class ChatListener { MooConfig.Setting dungPartyFinderArmorLookupDisplay = MooConfig.getDungPartyFinderArmorLookupDisplay(); String delimiter = "\n" + (dungPartyFinderArmorLookupDisplay == MooConfig.Setting.TEXT ? " " : ""); String armorLookupResult = EnumChatFormatting.LIGHT_PURPLE + " ➜ " + EnumChatFormatting.GRAY + dungeonClass + delimiter + String.join(delimiter, member.getArmor()); + + HySkyBlockStats.Profile.Dungeons dungeons = member.getDungeons(); + String highestFloorCompletions = "\n" + EnumChatFormatting.GRAY + "Completed no dungeons yet"; if (dungPartyFinderArmorLookupDisplay == MooConfig.Setting.TEXT) { - armorLookupComponent = new MooChatComponent(armorLookupPrefix + armorLookupResult).green(); + // highest floor completions: + if (dungeons != null && dungeons.hasPlayed()) { + highestFloorCompletions = dungeons.getHighestFloorCompletions(1, true).toString(); + } + armorLookupComponent = new MooChatComponent(armorLookupPrefix + armorLookupResult + highestFloorCompletions).green(); } else { // as a tooltip: == MooConfig.Setting.TOOLTIP - armorLookupComponent = new MooChatComponent(armorLookupPrefix + EnumChatFormatting.GREEN + (playerName.endsWith("s") ? "" : "'s") + " armor (hover me)").green() - .setHover(new MooChatComponent(EnumChatFormatting.BOLD + playerName + armorLookupResult)); + if (dungeons != null && dungeons.hasPlayed()) { + // highest floor completions: + highestFloorCompletions = dungeons.getHighestFloorCompletions(3, false).toString(); + } + armorLookupComponent = new MooChatComponent(armorLookupPrefix + EnumChatFormatting.GREEN + (playerName.endsWith("s") ? "'" : "'s") + " dungeons info (hover me)").green() + .setHover(new MooChatComponent(EnumChatFormatting.BOLD + playerName + armorLookupResult + highestFloorCompletions)); } main.getChatHelper().sendMessage(armorLookupComponent.setSuggestCommand("/p kick " + playerName, dungPartyFinderArmorLookupDisplay == MooConfig.Setting.TEXT)); } diff --git a/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsPartyListener.java b/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsPartyListener.java new file mode 100644 index 0000000..4693b10 --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsPartyListener.java @@ -0,0 +1,248 @@ +package de.cowtipper.cowlection.listener.skyblock; + +import de.cowtipper.cowlection.Cowlection; +import de.cowtipper.cowlection.config.MooConfig; +import de.cowtipper.cowlection.data.DataHelper; +import de.cowtipper.cowlection.data.Friend; +import de.cowtipper.cowlection.data.HySkyBlockStats; +import de.cowtipper.cowlection.util.ApiUtils; +import de.cowtipper.cowlection.util.MooChatComponent; +import de.cowtipper.cowlection.util.Utils; +import net.minecraft.util.EnumChatFormatting; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DungeonsPartyListener { + private static final Pattern PARTY_START_PATTERN = Pattern.compile("^Party Members \\((\\d+)\\)$"); + private static final Pattern PARTY_LEADER_PATTERN = Pattern.compile("^Party Leader: (?:\\[.+?] )?(\\w+) ●$"); + private static final Pattern PARTY_MEMBERS_OR_MODERATORS_PATTERN = Pattern.compile(" (?:\\[.+?] )?(\\w+) ●"); + private Cowlection main; + + private static Step nextStep = Step.STOP; + private int msgCounter = 0; + private boolean listenForChatMsgs = true; + private final AtomicInteger pendingApiRequests = new AtomicInteger(); + private final ConcurrentHashMap<String, Optional<UUID>> partyMembers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap<String, HySkyBlockStats> partyMemberStats = new ConcurrentHashMap<>(); + + public DungeonsPartyListener(Cowlection main) { + if (nextStep == Step.STOP) { // prevent double-registration of this listener + nextStep = Step.START; + this.main = main; + MinecraftForge.EVENT_BUS.register(this); + } + } + + @SubscribeEvent + public void onMessageReceived(ClientChatReceivedEvent e) { + if (e.type != 2 && listenForChatMsgs) { // normal chat or system msg (not above action bar), and not stopped + if (msgCounter > 15) { + // received too many messages without detecting any party-related lines, abort! + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Wasn't able to detect the party member list. Maybe the chat formatting was changed?"); + nextStep = Step.STOP; + } + ++msgCounter; + + String text = EnumChatFormatting.getTextWithoutFormattingCodes(e.message.getUnformattedText().trim()); + + if (text.isEmpty()) { + // spacer message, nothing to see here + return; + } + + switch (nextStep) { + case START: + if (text.equals("You are not currently in a party.")) { + shutdown(); + return; + } + Matcher matcher = PARTY_START_PATTERN.matcher(text); + if (matcher.matches()) { + int partySize = Integer.parseInt(matcher.group(1)); + if (partySize < 2 || partySize > 6) { + // party too small or too large, abort + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Party is too " + (partySize < 2 ? "small" : "large") + " for a dungeons party."); + shutdown(); + } else { + nextStep = Step.LEADER; + } + } + break; + case LEADER: + matcher = PARTY_LEADER_PATTERN.matcher(text); + if (matcher.matches()) { + // found leader + partyMembers.put(matcher.group(1), Optional.empty()); + nextStep = Step.MODERATORS_OR_MEMBERS; + } + break; + case MEMBERS_OR_END: + if (!text.startsWith("Party Members: ")) { + // seems to have reached the end + nextStep = Step.API_REQUESTS; + break; + } + // fallthrough: + case MODERATORS_OR_MEMBERS: + matcher = PARTY_MEMBERS_OR_MODERATORS_PATTERN.matcher(text); + boolean isPartyMods = text.startsWith("Party Moderators: "); + if (isPartyMods || text.startsWith("Party Members: ")) { + while (matcher.find()) { + // found moderators/members + partyMembers.put(matcher.group(1), Optional.empty()); + } + nextStep = isPartyMods ? Step.MEMBERS_OR_END : Step.API_REQUESTS; + } + break; + } + + if (nextStep == Step.API_REQUESTS) { + listenForChatMsgs = false; + nextStep = Step.AWAITING_API_RESPONSE; + if (partyMembers.size() > 0) { + getDungeonPartyStats(); + } else { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "How did you end up in a party with zero members?"); + nextStep = Step.STOP; + } + } + if (nextStep == Step.STOP) { + listenForChatMsgs = false; + shutdown(); + } + } + } + + private void getDungeonPartyStats() { + for (String partyMemberName : partyMembers.keySet()) { + pendingApiRequests.incrementAndGet(); + ApiUtils.fetchFriendData(partyMemberName, partyMember -> { + // (1) send Mojang API request to get uuid + if (partyMember != null && !partyMember.equals(Friend.FRIEND_NOT_FOUND)) { + partyMembers.put(partyMemberName, Optional.ofNullable(partyMember.getUuid())); + ApiUtils.fetchSkyBlockStats(partyMember, hySkyBlockStats -> { + // (2) once completed, request SkyBlock stats + int apiRequestsLeft = pendingApiRequests.decrementAndGet(); + partyMemberStats.put(partyMemberName, hySkyBlockStats); + + if (apiRequestsLeft == 0) { + // (3) wait for all requests to finish + // (4) once completed extract relevant data + nextStep = Step.SEND_PARTY_STATS; + sendDungeonPartyStats(); + } + }); + } else { + // player not found (nicked?) + pendingApiRequests.decrementAndGet(); + } + }); + } + } + + private void sendDungeonPartyStats() { + MooChatComponent dungeonsParty = new MooChatComponent("Dungeons party").gold().bold(); + + StringBuilder playerEntry = new StringBuilder(); + StringBuilder playerTooltip = new StringBuilder(); + String partyMemberName = ""; + for (Map.Entry<String, Optional<UUID>> partyMember : partyMembers.entrySet()) { + partyMemberName = partyMember.getKey(); + if (playerEntry.length() > 0) { + // append previous data + MooChatComponent dungeonPartyEntry = new MooChatComponent(playerEntry.toString()) + .setSuggestCommand("/p kick " + partyMemberName, false); + if (playerTooltip.length() > 0) { + dungeonPartyEntry.setHover(new MooChatComponent(playerTooltip.toString())); + } + dungeonsParty.appendFreshSibling(dungeonPartyEntry); + // reset 'caches' + playerEntry.setLength(0); + playerTooltip.setLength(0); + } + String errorNamePrefix = " " + EnumChatFormatting.RED + partyMemberName + EnumChatFormatting.LIGHT_PURPLE + " ➜ " + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC; + + if (!partyMember.getValue().isPresent()) { + playerEntry.setLength(0); + playerEntry.append(errorNamePrefix).append("player not found, might be nicked?"); + continue; + } + UUID uuid = partyMember.getValue().get(); + // player name: + playerEntry.append(" ").append(EnumChatFormatting.DARK_GREEN).append(partyMemberName).append(EnumChatFormatting.LIGHT_PURPLE).append(" ➜ ").append(EnumChatFormatting.GRAY); + + + HySkyBlockStats sbStats = partyMemberStats.get(partyMemberName); + if (sbStats != null && sbStats.isSuccess()) { + HySkyBlockStats.Profile activeProfile = sbStats.getActiveProfile(uuid); + + if (activeProfile == null) { + // player hasn't played SkyBlock + playerEntry.setLength(0); + playerEntry.append(errorNamePrefix).append("hasn't played SkyBlock yet"); + continue; + } + // ^ abort if any of the above failed, otherwise visualize API data: + HySkyBlockStats.Profile.Member member = activeProfile.getMember(uuid); + + // append player name + class and class level + armor + playerTooltip.append(EnumChatFormatting.WHITE).append(EnumChatFormatting.BOLD).append(partyMemberName).append(EnumChatFormatting.LIGHT_PURPLE).append(" ➜ ").append(EnumChatFormatting.GRAY).append(EnumChatFormatting.ITALIC).append("no class selected") + .append("\n").append(String.join("\n", member.getArmor())); + + HySkyBlockStats.Profile.Dungeons dungeons = member.getDungeons(); + boolean hasNotPlayedDungeons = dungeons == null || !dungeons.hasPlayed(); + if (hasNotPlayedDungeons) { + playerEntry.append(EnumChatFormatting.ITALIC).append("hasn't played Dungeons yet"); + continue; + } + DataHelper.DungeonClass selectedClass = dungeons.getSelectedClass(); + int selectedClassLevel = dungeons.getSelectedClassLevel(); + + String classInfo = selectedClass.getName() + " " + (MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(selectedClassLevel) : selectedClassLevel); + playerEntry.append(classInfo); + // insert class data into str: + String noClassSelected = EnumChatFormatting.ITALIC + "no class selected"; + int start = playerTooltip.indexOf(noClassSelected); + playerTooltip.replace(start, start + noClassSelected.length(), classInfo); + + // highest floor completions: + playerTooltip.append(dungeons.getHighestFloorCompletions(3, false)); + } else { + playerEntry.setLength(0); + playerEntry.append(errorNamePrefix).append("API error").append(sbStats != null && sbStats.getCause() != null ? ": " + sbStats.getCause() : ""); + } + } + MooChatComponent dungeonPartyEntry = new MooChatComponent(playerEntry.toString()) + .setSuggestCommand("/p kick " + partyMemberName, false); + if (playerTooltip.length() > 0) { + dungeonPartyEntry.setHover(new MooChatComponent(playerTooltip.toString())); + } + dungeonsParty.appendFreshSibling(dungeonPartyEntry); + main.getChatHelper().sendMessage(dungeonsParty); + shutdown(); + } + + public boolean isStillRunning() { + return nextStep != Step.STOP; + } + + public void shutdown() { + nextStep = Step.STOP; + MinecraftForge.EVENT_BUS.unregister(this); + } + + private enum Step { + START, LEADER, MODERATORS_OR_MEMBERS, MEMBERS_OR_END, // party detection + API_REQUESTS, AWAITING_API_RESPONSE, SEND_PARTY_STATS, // dungeon stats + STOP + } +} diff --git a/src/main/java/de/cowtipper/cowlection/util/ChatHelper.java b/src/main/java/de/cowtipper/cowlection/util/ChatHelper.java index 28d20ab..0f06bec 100644 --- a/src/main/java/de/cowtipper/cowlection/util/ChatHelper.java +++ b/src/main/java/de/cowtipper/cowlection/util/ChatHelper.java @@ -1,6 +1,7 @@ package de.cowtipper.cowlection.util; import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.util.ChatComponentText; import net.minecraft.util.ChatStyle; import net.minecraft.util.EnumChatFormatting; @@ -79,4 +80,11 @@ public class ChatHelper { } Minecraft.getMinecraft().thePlayer.sendChatMessage(chatMsg); } + + public void sendServerCommand(String command) { + EntityPlayerSP thePlayer = Minecraft.getMinecraft().thePlayer; + if (thePlayer != null) { + thePlayer.sendChatMessage(command); + } + } } diff --git a/src/main/java/de/cowtipper/cowlection/util/Utils.java b/src/main/java/de/cowtipper/cowlection/util/Utils.java index 3cf77b5..b4d62f8 100644 --- a/src/main/java/de/cowtipper/cowlection/util/Utils.java +++ b/src/main/java/de/cowtipper/cowlection/util/Utils.java @@ -10,8 +10,12 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.text.DecimalFormat; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import java.util.stream.Collectors; public final class Utils { public static final Pattern VALID_UUID_PATTERN = Pattern.compile("^(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$"); @@ -255,4 +259,17 @@ public final class Utils { public static String booleanToSymbol(boolean value) { return value ? EnumChatFormatting.GREEN + "✔" : EnumChatFormatting.RED + "✘"; } + + public static <V> Map<String, V> getLastNMapEntries(Map<String, V> map, int numberOfElements) { + if (map == null || map.isEmpty()) { + return null; + } + if (map.size() <= numberOfElements) { + return map; + } + return map.entrySet().stream().sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())) + .limit(numberOfElements) + .sorted(Map.Entry.comparingByKey()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)); + } } |