diff options
Diffstat (limited to 'src/main')
8 files changed, 135 insertions, 25 deletions
diff --git a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java index 219ec01..d47617a 100644 --- a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java +++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java @@ -163,6 +163,9 @@ public class MooCommand extends CommandBase { handleBestFriendRemove(args); } else if (args[0].equalsIgnoreCase("list")) { handleListBestFriends(); + } else if (args[0].equalsIgnoreCase("online")) { + main.getChatHelper().sendMessage(EnumChatFormatting.GRAY, "Checking online status of " + EnumChatFormatting.WHITE + main.getFriendsHandler().getBestFriends().size() + EnumChatFormatting.GRAY + " best friends. This may take a few seconds."); + main.getFriendsHandler().runBestFriendsOnlineCheck(true); } else if (args[0].equalsIgnoreCase("nameChangeCheck")) { handleNameChangeCheck(args); } @@ -295,7 +298,7 @@ public class MooCommand extends CommandBase { + (session.getMode() != null ? ": " + EnumChatFormatting.GOLD + session.getMode() : "") + (session.getMap() != null ? EnumChatFormatting.YELLOW + " (Map: " + EnumChatFormatting.GOLD + session.getMap() + EnumChatFormatting.YELLOW + ")" : "")); } else { - ApiUtils.fetchPlayerOfflineStatus(stalkedPlayer, hyPlayerData -> { + ApiUtils.fetchHyPlayerDetails(stalkedPlayer, hyPlayerData -> { if (hyPlayerData == null) { throw new ApiContactException("Hypixel", "couldn't stalk " + EnumChatFormatting.DARK_RED + stalkedPlayer.getName() + EnumChatFormatting.RED + " but they appear to be offline currently."); } else if (hyPlayerData.hasNeverJoinedHypixel()) { @@ -501,7 +504,7 @@ public class MooCommand extends CommandBase { // fairy souls: sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Fairy Souls", (member.getFairySoulsCollected() >= 0) ? String.valueOf(member.getFairySoulsCollected()) : "API access disabled")); // profile age: - sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Profile age", fancyFirstJoined.first()).setHover(new MooChatComponent.KeyValueTooltipComponent("Join date", fancyFirstJoined.second()))); + sbStats.appendFreshSibling(new MooChatComponent.KeyValueChatComponent("Profile age", fancyFirstJoined.first()).setHover(new MooChatComponent.KeyValueTooltipComponent("Join date", (fancyFirstJoined.second() == null ? "today" : fancyFirstJoined.second())))); main.getChatHelper().sendMessage(sbStats); } else { @@ -518,6 +521,8 @@ public class MooCommand extends CommandBase { throw new InvalidPlayerNameException(args[1]); } else if (main.getFriendsHandler().isBestFriend(args[1], true)) { throw new MooCommandException(EnumChatFormatting.DARK_RED + args[1] + EnumChatFormatting.RED + " is a best friend already."); + } else if (main.getFriendsHandler().getBestFriends().size() >= 100) { + throw new MooCommandException(EnumChatFormatting.RED + "The best friends list is limited to 100 players. Remove some with " + EnumChatFormatting.WHITE + "/" + getCommandName() + " remove <name> " + EnumChatFormatting.RED + "first"); } else { // TODO Add check if 'best friend' is on normal friend list main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "Fetching " + EnumChatFormatting.YELLOW + args[1] + EnumChatFormatting.GOLD + "'s unique user id. This may take a few seconds..."); @@ -545,7 +550,9 @@ public class MooCommand extends CommandBase { 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: " + ((bestFriends.isEmpty()) + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "\u279C Best friends" + + (bestFriends.isEmpty() ? "" : " (" + EnumChatFormatting.DARK_GREEN + bestFriends.size() + EnumChatFormatting.GREEN + ")") + ": " + + ((bestFriends.isEmpty()) ? EnumChatFormatting.ITALIC + "none :c" : EnumChatFormatting.DARK_GREEN + String.join(EnumChatFormatting.GREEN + ", " + EnumChatFormatting.DARK_GREEN, bestFriends))); } @@ -562,7 +569,7 @@ public class MooCommand extends CommandBase { } else { main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, "Checking if " + bestFriend.getName() + " changed their name... This will take a few seconds..."); // check for name change async - main.getFriendsHandler().updateBestFriend(bestFriend, true); + main.getFriendsHandler().doBestFriendNameChangeCheck(bestFriend, true); } } @@ -591,6 +598,7 @@ public class MooCommand extends CommandBase { .appendSibling(createCmdHelpEntry("add", "Add best friends")) .appendSibling(createCmdHelpEntry("remove", "Remove best friends")) .appendSibling(createCmdHelpEntry("list", "View list of best friends")) + .appendSibling(createCmdHelpEntry("online", "View list of best friends that are currently online")) .appendSibling(createCmdHelpEntry("nameChangeCheck", "Force a scan for a changed name of a best friend")) .appendSibling(createCmdHelpEntry("toggle", "Toggle join/leave notifications")) .appendSibling(createCmdHelpSection(2, "Miscellaneous")) @@ -627,13 +635,15 @@ public class MooCommand extends CommandBase { public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) { if (args.length == 1) { return getListOfStringsMatchingLastWord(args, - /* friends & other players */ "stalk", "askPolitelyWhereTheyAre", "stalkskyblock", "skyblockstalk", "askPolitelyAboutTheirSkyBlockProgress", "analyzeIsland", "deaths", "add", "remove", "list", "nameChangeCheck", "toggle", + /* friends & other players */ "stalk", "askPolitelyWhereTheyAre", "stalkskyblock", "skyblockstalk", "askPolitelyAboutTheirSkyBlockProgress", "analyzeIsland", "deaths", "add", "remove", "list", "online", "nameChangeCheck", "toggle", /* miscellaneous */ "config", "search", "guiscale", "rr", "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 + } + 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 return getListOfStringsMatchingLastWord(args, main.getPlayerCache().getAllNamesSorted()); } return null; diff --git a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java index 84cbc1f..9cbe1b9 100644 --- a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java +++ b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java @@ -38,6 +38,7 @@ public class MooConfig { public static boolean showBestFriendNotifications; public static boolean showFriendNotifications; public static boolean showGuildNotifications; + public static boolean doBestFriendsOnlineCheck; public static boolean showAdvancedTooltips; public static String[] tabCompletableNamesCommands; private static String numeralSystem; @@ -132,6 +133,8 @@ public class MooConfig { "showFriendNotifications", true, "Set to true to receive friends' login/logout messages, set to false hide them."), true); Property propShowGuildNotifications = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT, "showGuildNotifications", true, "Set to true to receive guild members' login/logout messages, set to false hide them."), true); + Property propDoBestFriendsOnlineCheck = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT, + "doBestFriendsOnlineCheck", true, "Set to true to check best friends' online status when joining a server, set to false to disable."), true); Property propShowAdvancedTooltips = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT, "showAdvancedTooltips", true, "Set to true to show advanced tooltips, set to false show default tooltips."), true); Property propNumeralSystem = addConfigEntry(cfg.get(Configuration.CATEGORY_CLIENT, @@ -174,6 +177,7 @@ public class MooConfig { showBestFriendNotifications = propShowBestFriendNotifications.getBoolean(); showFriendNotifications = propShowFriendNotifications.getBoolean(); showGuildNotifications = propShowGuildNotifications.getBoolean(); + doBestFriendsOnlineCheck = propDoBestFriendsOnlineCheck.getBoolean(); showAdvancedTooltips = propShowAdvancedTooltips.getBoolean(); numeralSystem = propNumeralSystem.getString(); tabCompletableNamesCommands = propTabCompletableNamesCommands.getStringList(); @@ -198,6 +202,7 @@ public class MooConfig { propShowBestFriendNotifications.set(showBestFriendNotifications); propShowFriendNotifications.set(showFriendNotifications); propShowGuildNotifications.set(showGuildNotifications); + propDoBestFriendsOnlineCheck.set(doBestFriendsOnlineCheck); propShowAdvancedTooltips.set(showAdvancedTooltips); propNumeralSystem.set(numeralSystem); propTabCompletableNamesCommands.set(tabCompletableNamesCommands); diff --git a/src/main/java/de/cowtipper/cowlection/event/ApiErrorEvent.java b/src/main/java/de/cowtipper/cowlection/event/ApiErrorEvent.java new file mode 100644 index 0000000..ccfc4d3 --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/event/ApiErrorEvent.java @@ -0,0 +1,15 @@ +package de.cowtipper.cowlection.event; + +import net.minecraftforge.fml.common.eventhandler.Event; + +public class ApiErrorEvent extends Event { + private final String playerName; + + public ApiErrorEvent(String playerName) { + this.playerName = playerName; + } + + public String getPlayerName() { + return playerName; + } +} diff --git a/src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java b/src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java index ae1467a..6229e0e 100644 --- a/src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java +++ b/src/main/java/de/cowtipper/cowlection/handler/FriendsHandler.java @@ -2,13 +2,15 @@ package de.cowtipper.cowlection.handler; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; +import com.mojang.realmsclient.util.Pair; import de.cowtipper.cowlection.Cowlection; import de.cowtipper.cowlection.command.exception.ApiContactException; import de.cowtipper.cowlection.command.exception.MooCommandException; import de.cowtipper.cowlection.data.Friend; -import de.cowtipper.cowlection.util.ApiUtils; -import de.cowtipper.cowlection.util.GsonUtils; +import de.cowtipper.cowlection.data.HyPlayerData; +import de.cowtipper.cowlection.util.*; import io.netty.util.internal.ConcurrentSet; +import net.minecraft.client.Minecraft; import net.minecraft.command.PlayerNotFoundException; import net.minecraft.event.ClickEvent; import net.minecraft.event.HoverEvent; @@ -16,14 +18,14 @@ import net.minecraft.util.ChatComponentText; import net.minecraft.util.ChatStyle; import net.minecraft.util.EnumChatFormatting; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.time.DurationFormatUtils; import java.io.File; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -33,13 +35,16 @@ public class FriendsHandler { private final Cowlection main; private final Set<Friend> bestFriends = new ConcurrentSet<>(); private final File bestFriendsFile; - private final AtomicInteger bestFriendQueue = new AtomicInteger(); + private final AtomicInteger bestFriendNameCheckingQueue = new AtomicInteger(); + private final AtomicInteger bestFriendOnlineStatusQueue = new AtomicInteger(); + private final Set<String> bestFriendsOnlineStatusWithApiErrors = new ConcurrentSet<>(); + private long nextBestFriendOnlineCheck = 0; public FriendsHandler(Cowlection main, File friendsFile) { this.main = main; this.bestFriendsFile = friendsFile; loadBestFriends(); - updateBestFriends(); + doBestFriendsNameChangeCheck(); } public boolean isBestFriend(String playerName, boolean ignoreCase) { @@ -90,15 +95,15 @@ public class FriendsHandler { return bestFriends.stream().filter(friend -> friend.getUuid().equals(uuid)).findFirst().orElse(Friend.FRIEND_NOT_FOUND); } - public void updateBestFriends() { + public void doBestFriendsNameChangeCheck() { bestFriends.stream().filter(friend -> System.currentTimeMillis() - friend.getLastChecked() > UPDATE_FREQUENCY_DEFAULT) .forEach(friend1 -> { - bestFriendQueue.incrementAndGet(); - updateBestFriend(friend1, false); + bestFriendNameCheckingQueue.incrementAndGet(); + doBestFriendNameChangeCheck(friend1, false); }); } - public void updateBestFriend(Friend friend, boolean isCommandTriggered) { + public void doBestFriendNameChangeCheck(Friend friend, boolean isCommandTriggered) { ApiUtils.fetchCurrentName(friend, newName -> { if (newName == null) { // skipping friend, something went wrong with API request @@ -132,7 +137,7 @@ public class FriendsHandler { if (isCommandTriggered) { saveBestFriends(); } else { - int remainingFriendsToCheck = bestFriendQueue.decrementAndGet(); + int remainingFriendsToCheck = bestFriendNameCheckingQueue.decrementAndGet(); if (remainingFriendsToCheck == 0) { // we're done with checking for name changes, save updates to file! saveBestFriends(); @@ -141,6 +146,66 @@ public class FriendsHandler { }); } + public void runBestFriendsOnlineCheck(boolean isCommandTriggered) { + long now = System.currentTimeMillis(); + int delay = (isCommandTriggered ? 5 : 1) * 60000; + if (nextBestFriendOnlineCheck < now) { + // ^ prevent too frequent checks + nextBestFriendOnlineCheck = now + delay; + bestFriendOnlineStatusQueue.set(0); + bestFriendsOnlineStatusWithApiErrors.clear(); + final Map<String, HyPlayerData> onlineBestFriends = new ConcurrentHashMap<>(); + + main.getLogger().info("Checking best friends online status... (might take a bit)"); + + for (Friend bestFriend : bestFriends) { + bestFriendOnlineStatusQueue.incrementAndGet(); + ApiUtils.fetchHyPlayerDetails(bestFriend, hyPlayerData -> { + if (hyPlayerData != null && hyPlayerData.getLastLogin() > hyPlayerData.getLastLogout()) { + // online & not hiding their online status + main.getPlayerCache().addBestFriend(bestFriend.getName()); + + onlineBestFriends.put(bestFriend.getName(), hyPlayerData); + } + + int remainingFriendsToCheck = bestFriendOnlineStatusQueue.decrementAndGet(); + if (remainingFriendsToCheck == 0 && Minecraft.getMinecraft().thePlayer != null) { + // we're done with checking for online status + MooChatComponent bestFriendsComponent = new MooChatComponent("⬤ Online best friends (" + + EnumChatFormatting.DARK_GREEN + onlineBestFriends.size() + EnumChatFormatting.GREEN + "/" + EnumChatFormatting.DARK_GREEN + bestFriends.size() + EnumChatFormatting.GREEN + "): ").green(); + if (onlineBestFriends.isEmpty()) { + bestFriendsComponent.appendText("none..."); + } else { + TreeMap<String, HyPlayerData> onlineBestFriendsSorted = new TreeMap<>(onlineBestFriends); + for (Map.Entry<String, HyPlayerData> bestFriendData : onlineBestFriendsSorted.entrySet()) { + if (bestFriendsComponent.getSiblings().size() > 0) { + bestFriendsComponent.appendSibling(new MooChatComponent(", ").green()); + } + HyPlayerData hyBestFriendData = bestFriendData.getValue(); + Pair<String, String> lastOnline = Utils.getDurationAsWords(hyBestFriendData.getLastLogin()); + bestFriendsComponent.appendSibling(new MooChatComponent(bestFriendData.getKey()).darkGreen() + .setHover(new MooChatComponent(hyBestFriendData.getLastGame()).yellow().appendFreshSibling(new MooChatComponent("Online for " + (lastOnline.second() != null ? lastOnline.second() : lastOnline.first())).white()))); + } + } + if (bestFriendsOnlineStatusWithApiErrors.size() > 0) { + String bestFriendsWithApiErrors = String.join(EnumChatFormatting.RED + ", " + EnumChatFormatting.DARK_RED, bestFriendsOnlineStatusWithApiErrors); + bestFriendsComponent.appendFreshSibling(new MooChatComponent("Failed to check " + EnumChatFormatting.DARK_RED + bestFriendsOnlineStatusWithApiErrors.size() + EnumChatFormatting.RED + " best friends' online status due to Hypixel API errors: " + EnumChatFormatting.DARK_RED + bestFriendsWithApiErrors).red()); + bestFriendsOnlineStatusWithApiErrors.clear(); + } + main.getChatHelper().sendMessage(bestFriendsComponent); + } + }); + } + } else { + new TickDelay(() -> main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Couldn't check best friends online status because it has not been long enough since the last check. Next check via " + EnumChatFormatting.WHITE + "/moo online" + EnumChatFormatting.RED + " available in " + DurationFormatUtils.formatDurationWords(nextBestFriendOnlineCheck - System.currentTimeMillis(), true, true)) + , isCommandTriggered ? 1 : 100); + } + } + + public void addErroredApiRequest(String playerName) { + bestFriendsOnlineStatusWithApiErrors.add(playerName); + } + public synchronized void saveBestFriends() { try { String bestFriendsJsonZoned = GsonUtils.toJson(this.bestFriends); diff --git a/src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java b/src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java index 2206473..0092670 100644 --- a/src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java +++ b/src/main/java/de/cowtipper/cowlection/handler/PlayerCache.java @@ -6,11 +6,10 @@ import de.cowtipper.cowlection.Cowlection; import java.util.SortedSet; import java.util.TreeSet; +@SuppressWarnings("UnstableApiUsage") public class PlayerCache { - @SuppressWarnings("UnstableApiUsage") private final EvictingQueue<String> nameCache = EvictingQueue.create(50); - @SuppressWarnings("UnstableApiUsage") - private final EvictingQueue<String> bestFriendCache = EvictingQueue.create(50); + private final EvictingQueue<String> bestFriendCache = EvictingQueue.create(100); private final Cowlection main; public PlayerCache(Cowlection main) { diff --git a/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java b/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java index 228a10b..d179837 100644 --- a/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java +++ b/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java @@ -1,6 +1,8 @@ package de.cowtipper.cowlection.listener; import de.cowtipper.cowlection.Cowlection; +import de.cowtipper.cowlection.config.MooConfig; +import de.cowtipper.cowlection.event.ApiErrorEvent; import de.cowtipper.cowlection.listener.skyblock.DungeonsListener; import de.cowtipper.cowlection.listener.skyblock.SkyBlockListener; import de.cowtipper.cowlection.util.GsonUtils; @@ -72,11 +74,19 @@ public class PlayerListener { public void onServerJoin(FMLNetworkEvent.ClientConnectedToServerEvent e) { main.getVersionChecker().runUpdateCheck(false); new TickDelay(() -> main.getChatHelper().sendOfflineMessages(), 6 * 20); + if (MooConfig.doBestFriendsOnlineCheck) { + main.getFriendsHandler().runBestFriendsOnlineCheck(false); + } isOnSkyBlock = false; main.getLogger().info("Joined the server"); } @SubscribeEvent + public void onApiError(ApiErrorEvent e) { + main.getFriendsHandler().addErroredApiRequest(e.getPlayerName()); + } + + @SubscribeEvent public void onWorldEnter(PlayerSetSpawnEvent e) { // check if player is on SkyBlock or on another gamemode new TickDelay(() -> { diff --git a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java index 763084d..8b37cf7 100644 --- a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java +++ b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java @@ -11,6 +11,8 @@ import de.cowtipper.cowlection.data.Friend; import de.cowtipper.cowlection.data.HyPlayerData; import de.cowtipper.cowlection.data.HySkyBlockStats; import de.cowtipper.cowlection.data.HyStalkingData; +import de.cowtipper.cowlection.event.ApiErrorEvent; +import net.minecraftforge.common.MinecraftForge; import org.apache.http.HttpStatus; import java.io.BufferedReader; @@ -101,17 +103,19 @@ public class ApiUtils { return null; } - public static void fetchPlayerOfflineStatus(Friend stalkedPlayer, ThrowingConsumer<HyPlayerData> action) { - pool.execute(() -> action.accept(stalkOfflinePlayer(stalkedPlayer))); + public static void fetchHyPlayerDetails(Friend stalkedPlayer, ThrowingConsumer<HyPlayerData> action) { + pool.execute(() -> action.accept(stalkHyPlayer(stalkedPlayer))); } - private static HyPlayerData stalkOfflinePlayer(Friend stalkedPlayer) { + private static HyPlayerData stalkHyPlayer(Friend stalkedPlayer) { try (BufferedReader reader = makeApiCall(String.format(PLAYER_URL, MooConfig.moo, UUIDTypeAdapter.fromUUID(stalkedPlayer.getUuid())))) { if (reader != null) { return GsonUtils.fromJson(reader, HyPlayerData.class); } } catch (IOException | JsonSyntaxException e) { e.printStackTrace(); + ApiErrorEvent event = new ApiErrorEvent(stalkedPlayer.getName()); + MinecraftForge.EVENT_BUS.post(event); } return null; } @@ -119,7 +123,7 @@ public class ApiUtils { private static BufferedReader makeApiCall(String url) throws IOException { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setConnectTimeout(5000); - connection.setReadTimeout(10000); + connection.setReadTimeout(8000); connection.addRequestProperty("User-Agent", "Forge Mod " + Cowlection.MODNAME + "/" + Cowlection.VERSION + " (" + Cowlection.GITURL + ")"); connection.getResponseCode(); diff --git a/src/main/resources/assets/cowlection/lang/en_US.lang b/src/main/resources/assets/cowlection/lang/en_US.lang index 80c002e..7e192b8 100644 --- a/src/main/resources/assets/cowlection/lang/en_US.lang +++ b/src/main/resources/assets/cowlection/lang/en_US.lang @@ -6,6 +6,8 @@ cowlection.config.showFriendNotifications=Show friend notifications cowlection.config.showFriendNotifications.tooltip=Set to true to receive friends' login/logout messages, set to false hide them. cowlection.config.showGuildNotifications=Show guild notifications cowlection.config.showGuildNotifications.tooltip=Set to true to receive guild members' login/logout messages, set to false hide them. +cowlection.config.doBestFriendsOnlineCheck=Do best friends online check +cowlection.config.doBestFriendsOnlineCheck.tooltip=Set to true to check best friends' online status when joining a server, set to false to disable.\n§fDoes §dnot §fwork for staff members and players hiding their online status. cowlection.config.showAdvancedTooltips=Show advanced tooltips cowlection.config.showAdvancedTooltips.tooltip=Set to true to show advanced tooltips, set to false show default tooltips. cowlection.config.numeralSystem=Numeral system |