aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCow <cow@volloeko.de>2020-09-27 22:34:28 +0200
committerCow <cow@volloeko.de>2020-09-27 22:34:28 +0200
commitdba1ec3e7885c86e16283d149d088369ecc509fd (patch)
tree774f581375a219f7d45dc8e1b767d2250347bd91
parent586660bb79ca0a53898396dcde3daab809e29fef (diff)
downloadCowlection-dba1ec3e7885c86e16283d149d088369ecc509fd.tar.gz
Cowlection-dba1ec3e7885c86e16283d149d088369ecc509fd.tar.bz2
Cowlection-dba1ec3e7885c86e16283d149d088369ecc509fd.zip
New command `/moo dungeon party`
- displays current `/party` members' armor and dungeons floor completions
-rw-r--r--CHANGELOG.md6
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/MooCommand.java28
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/HySkyBlockStats.java28
-rw-r--r--src/main/java/de/cowtipper/cowlection/listener/ChatListener.java17
-rw-r--r--src/main/java/de/cowtipper/cowlection/listener/skyblock/DungeonsPartyListener.java248
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/ChatHelper.java8
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/Utils.java17
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));
+ }
}