From 6978a4ab8b96c58208f8a2a50ed1132296ac01d4 Mon Sep 17 00:00:00 2001 From: Xujiayao Date: Sun, 22 Feb 2026 19:07:15 +0800 Subject: [PATCH] Implement stats command for viewing Minecraft statistics --- .../commands/CommandAutoCompleter.java | 79 +++++++-- .../commands/CommandManager.java | 12 +- .../commands/impl/StatsCommand.java | 154 ++++++++++++++++++ .../server/discord/DiscordEventHandler.java | 51 ++++++ .../server/discord/DiscordManager.java | 4 + .../discord_mc_chat/utils/JsonUtils.java | 25 +++ core/src/main/resources/lang/en_us.yml | 11 ++ core/src/main/resources/lang/zh_cn.yml | 11 ++ .../minecraft/commands/MinecraftCommands.java | 36 +++- .../events/MinecraftEventHandler.java | 57 +++++++ 10 files changed, 413 insertions(+), 27 deletions(-) create mode 100644 core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/StatsCommand.java diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java index 859be66a..70083084 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java @@ -1,8 +1,10 @@ package com.xujiayao.discord_mc_chat.commands; +import com.xujiayao.discord_mc_chat.commands.impl.StatsCommand; import com.xujiayao.discord_mc_chat.utils.LogFileUtils; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; /** @@ -27,35 +29,78 @@ public class CommandAutoCompleter { if (input == null || input.isBlank()) { // Return all command names - suggestions.addAll(CommandManager.getCommandNames()); + CommandManager.getCommands().stream() + .sorted(Comparator.comparing(Command::name)) + .forEach(cmd -> { + StringBuilder builder = new StringBuilder(); + + builder.append(cmd.name()); + for (Command.CommandArgument arg : cmd.args()) { + builder.append(" <").append(arg.name()).append(">"); + } + + suggestions.add(builder.toString()); + }); + return suggestions; } - String trimmed = input.trim(); - String[] parts = trimmed.split("\\s+", 2); + String[] parts = input.trim().split("\\s+"); String commandName = parts[0].toLowerCase(); if (parts.length == 1 && !input.endsWith(" ")) { - // User is still typing the command name — suggest matching command names - for (String name : CommandManager.getCommandNames()) { - if (name.startsWith(commandName)) { - suggestions.add(name); - } - } + // User is still typing the command name + CommandManager.getCommands().stream() + .filter(cmd -> cmd.name().startsWith(commandName)) + .sorted(Comparator.comparing(Command::name)) + .forEach(cmd -> { + StringBuilder builder = new StringBuilder(); + + builder.append(cmd.name()); + for (Command.CommandArgument arg : cmd.args()) { + builder.append(" <").append(arg.name()).append(">"); + } + + suggestions.add(builder.toString()); + }); + return suggestions; } // User has typed a command name and moved on to arguments - String argInput = parts.length > 1 ? parts[1] : ""; - switch (commandName) { case "log" -> { - // Suggest log file names - List logFiles = LogFileUtils.listLogFiles(); - String lowerArgInput = argInput.toLowerCase(); - for (String file : logFiles) { - if (file.toLowerCase().startsWith(lowerArgInput) || file.toLowerCase().contains(lowerArgInput)) { - suggestions.add("log " + file); + String argInput = parts.length > 1 ? parts[1] : ""; + if (parts.length == 1 || (parts.length == 2 && !input.endsWith(" "))) { + List logFiles = LogFileUtils.listLogFiles(); + String lowerArgInput = argInput.toLowerCase(); + for (String file : logFiles) { + if (file.toLowerCase().startsWith(lowerArgInput) || file.toLowerCase().contains(lowerArgInput)) { + suggestions.add("log " + file); + } + } + } + } + case "stats" -> { + StatsCommand.StatsProvider provider = StatsCommand.getProvider(); + if (provider != null) { + if (parts.length == 1 || (parts.length == 2 && !input.endsWith(" "))) { + String typeInput = parts.length > 1 ? parts[1] : ""; + String lowerType = typeInput.toLowerCase(); + for (String type : provider.getStatTypes()) { + if (type.toLowerCase().startsWith(lowerType) || type.toLowerCase().contains(lowerType)) { + suggestions.add("stats " + type); + } + } + } else if (parts.length == 2 || (parts.length == 3 && !input.endsWith(" "))) { + String type = parts[1]; + String statInput = parts.length > 2 ? parts[2] : ""; + String lowerStat = statInput.toLowerCase(); + for (String stat : provider.getStatNames(type)) { + if (stat.toLowerCase().startsWith(lowerStat) || stat.toLowerCase().contains(lowerStat)) { + suggestions.add("stats " + type + " " + stat); + } + } } } } diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandManager.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandManager.java index 5addfca0..00ee9c4e 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandManager.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandManager.java @@ -6,6 +6,7 @@ import com.xujiayao.discord_mc_chat.commands.impl.InfoCommand; import com.xujiayao.discord_mc_chat.commands.impl.LogCommand; import com.xujiayao.discord_mc_chat.commands.impl.ReloadCommand; import com.xujiayao.discord_mc_chat.commands.impl.ShutdownCommand; +import com.xujiayao.discord_mc_chat.commands.impl.StatsCommand; import com.xujiayao.discord_mc_chat.utils.ExecutorServiceUtils; import com.xujiayao.discord_mc_chat.utils.config.ModeManager; import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager; @@ -46,6 +47,8 @@ public class CommandManager { if ("standalone".equals(ModeManager.getMode())) { register(new ExecuteCommand()); register(new ShutdownCommand()); + } else { + register(new StatsCommand()); } } @@ -77,15 +80,6 @@ public class CommandManager { return new ArrayList<>(COMMANDS.values()); } - /** - * Get all registered command names. - * - * @return A collection of registered command names - */ - public static Collection getCommandNames() { - return new ArrayList<>(COMMANDS.keySet()); - } - /** * Execute a command line. * diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/StatsCommand.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/StatsCommand.java new file mode 100644 index 00000000..d9b3c359 --- /dev/null +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/StatsCommand.java @@ -0,0 +1,154 @@ +package com.xujiayao.discord_mc_chat.commands.impl; + +import com.xujiayao.discord_mc_chat.commands.Command; +import com.xujiayao.discord_mc_chat.commands.CommandSender; +import com.xujiayao.discord_mc_chat.utils.JsonUtils; +import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +/** + * Stats command implementation. + * + * @author Xujiayao + */ +public class StatsCommand implements Command { + + private static StatsProvider provider; + + public static StatsProvider getProvider() { + return provider; + } + + public static void setProvider(StatsProvider provider) { + StatsCommand.provider = provider; + } + + @Override + public String name() { + return "stats"; + } + + @Override + public CommandArgument[] args() { + return new CommandArgument[]{ + new CommandArgument() { + @Override + public String name() { + return "type"; + } + + @Override + public String description() { + return I18nManager.getDmccTranslation("commands.stats.args_desc.type"); + } + }, + new CommandArgument() { + @Override + public String name() { + return "stat"; + } + + @Override + public String description() { + return I18nManager.getDmccTranslation("commands.stats.args_desc.stat"); + } + } + }; + } + + @Override + public String description() { + return I18nManager.getDmccTranslation("commands.stats.description"); + } + + @Override + public void execute(CommandSender sender, String... args) { + if (provider == null) { + sender.reply(I18nManager.getDmccTranslation("commands.stats.server_not_ready")); + return; + } + + provider.saveAll(); // Ensure data is written to disk + + String type = args[0]; + String stat = args[1]; + + Path statsDir = provider.getStatsDirectory(); + + if (statsDir == null || !Files.exists(statsDir) || !Files.isDirectory(statsDir)) { + sender.reply(I18nManager.getDmccTranslation("commands.stats.dir_not_found")); + return; + } + + Map leaderboard = new ConcurrentHashMap<>(); + + try (Stream stream = Files.list(statsDir)) { + stream.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith(".json")) + .forEach(p -> { + String fileName = p.getFileName().toString(); + String uuidStr = fileName.substring(0, fileName.length() - 5); + try { + UUID uuid = UUID.fromString(uuidStr); + int value = JsonUtils.getStat(p, type, stat); + if (value > 0) { + String name = provider.getPlayerName(uuid); + if (name == null || name.isBlank()) { + name = uuidStr; + } + leaderboard.put(name, value); + } + } catch (Exception ignored) { + } + }); + } catch (Exception e) { + sender.reply(I18nManager.getDmccTranslation("commands.stats.read_failed", e.getMessage())); + return; + } + + if (leaderboard.isEmpty()) { + sender.reply(I18nManager.getDmccTranslation("commands.stats.no_stats", type, stat)); + return; + } + + List> sorted = new ArrayList<>(leaderboard.entrySet()); + sorted.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue())); + + StringBuilder sb = new StringBuilder(); + String colon = I18nManager.getDmccTranslation("commands.stats.colon"); + + sb.append("========== ").append(I18nManager.getDmccTranslation("commands.stats.stats")).append(" ==========\n\n") + .append(I18nManager.getDmccTranslation("commands.stats.args_desc.type")).append(colon).append(type).append("\n") + .append(I18nManager.getDmccTranslation("commands.stats.args_desc.stat")).append(colon).append(stat).append("\n\n"); + + for (int i = 0; i < sorted.size(); i++) { + Map.Entry entry = sorted.get(i); + sb.append(i + 1).append(". ").append(entry.getKey()).append(": ").append(entry.getValue()); + if (i < sorted.size() - 1) { + sb.append("\n"); + } + } + + sender.reply(sb.toString()); + } + + public interface StatsProvider { + void saveAll(); + + Path getStatsDirectory(); + + String getPlayerName(UUID uuid); + + List getStatTypes(); + + List getStatNames(String type); + } +} diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java index d26094a1..82572964 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java @@ -1,6 +1,7 @@ package com.xujiayao.discord_mc_chat.server.discord; import com.xujiayao.discord_mc_chat.commands.CommandManager; +import com.xujiayao.discord_mc_chat.commands.impl.StatsCommand; import com.xujiayao.discord_mc_chat.network.NetworkManager; import com.xujiayao.discord_mc_chat.utils.LogFileUtils; import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; @@ -46,6 +47,11 @@ public class DiscordEventHandler extends ListenerAdapter { String file = event.getOption("file", OptionMapping::getAsString); CommandManager.execute(new JdaCommandSender(event), name, file); } + case "stats" -> { + String type = event.getOption("type", OptionMapping::getAsString); + String stat = event.getOption("stat", OptionMapping::getAsString); + CommandManager.execute(new JdaCommandSender(event), name, type, stat); + } default -> CommandManager.execute(new JdaCommandSender(event), name); } } @@ -71,6 +77,14 @@ public class DiscordEventHandler extends ListenerAdapter { choices = getLogFileChoices(currentValue); } } + case "stats" -> { + if ("type".equals(focusedOption)) { + choices = getStatsTypeChoices(currentValue); + } else if ("stat".equals(focusedOption)) { + String type = event.getOption("type", OptionMapping::getAsString); + choices = getStatsStatChoices(type, currentValue); + } + } } event.replyChoices(choices).queue(); @@ -145,6 +159,43 @@ public class DiscordEventHandler extends ListenerAdapter { .collect(Collectors.toList()); } + /** + * Gets auto-complete choices for the 'type' parameter of the stats command. + * + * @param currentValue The current user input for filtering + * @return List of choices + */ + private List getStatsTypeChoices(String currentValue) { + StatsCommand.StatsProvider provider = StatsCommand.getProvider(); + if (provider == null) return List.of(); + + String lowerValue = currentValue.toLowerCase(); + return provider.getStatTypes().stream() + .filter(t -> t.toLowerCase().contains(lowerValue)) + .limit(25) + .map(t -> new Command.Choice(t, t)) + .collect(Collectors.toList()); + } + + /** + * Gets auto-complete choices for the 'stat' parameter of the stats command. + * + * @param type The selected stat type + * @param currentValue The current user input for filtering + * @return List of choices + */ + private List getStatsStatChoices(String type, String currentValue) { + StatsCommand.StatsProvider provider = StatsCommand.getProvider(); + if (provider == null || type == null || type.isBlank()) return List.of(); + + String lowerValue = currentValue.toLowerCase(); + return provider.getStatNames(type).stream() + .filter(s -> s.toLowerCase().contains(lowerValue)) + .limit(25) + .map(s -> new Command.Choice(s, s)) + .collect(Collectors.toList()); + } + @Override public void onMessageReceived(@NotNull MessageReceivedEvent event) { if (event.getAuthor().isBot() || event.isWebhookMessage()) { diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordManager.java b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordManager.java index c3a12e1a..33ae4208 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordManager.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordManager.java @@ -106,6 +106,10 @@ public class DiscordManager { .addOption(OptionType.STRING, "at", I18nManager.getDmccTranslation("commands.execute.args_desc.at"), true, true) .addOption(OptionType.STRING, "command", I18nManager.getDmccTranslation("commands.execute.args_desc.command"), true, true)); commands.add(Commands.slash("shutdown", I18nManager.getDmccTranslation("commands.shutdown.description"))); + } else { + commands.add(Commands.slash("stats", I18nManager.getDmccTranslation("commands.stats.description")) + .addOption(OptionType.STRING, "type", I18nManager.getDmccTranslation("commands.stats.args_desc.type"), true, true) + .addOption(OptionType.STRING, "stat", I18nManager.getDmccTranslation("commands.stats.args_desc.stat"), true, true)); } CompletableFuture> updateFuture = jda.updateCommands().addCommands(commands).submit(); diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/utils/JsonUtils.java b/core/src/main/java/com/xujiayao/discord_mc_chat/utils/JsonUtils.java index 944472ef..f2961ea6 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/utils/JsonUtils.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/utils/JsonUtils.java @@ -1,12 +1,17 @@ package com.xujiayao.discord_mc_chat.utils; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; +import static com.xujiayao.discord_mc_chat.Constants.JSON_MAPPER; import static com.xujiayao.discord_mc_chat.Constants.YAML_MAPPER; /** @@ -53,4 +58,24 @@ public class JsonUtils { return YAML_MAPPER.readValue(inputStream, new TypeReference<>() { }); } + + /** + * Reads a specific statistic from a player's stats JSON file. + * + * @param path The path to the JSON file + * @param type The stat type (e.g., "minecraft:custom") + * @param stat The stat name (e.g., "minecraft:deaths") + * @return The stat value, or 0 if not found + */ + public static int getStat(Path path, String type, String stat) { + try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + JsonNode root = JSON_MAPPER.readTree(reader); + JsonNode typeNode = root.path("stats").path(type); + if (!typeNode.isMissingNode() && typeNode.has(stat)) { + return typeNode.path(stat).asInt(); + } + } catch (Exception ignored) { + } + return 0; + } } diff --git a/core/src/main/resources/lang/en_us.yml b/core/src/main/resources/lang/en_us.yml index 50a6b46a..224f2b8b 100644 --- a/core/src/main/resources/lang/en_us.yml +++ b/core/src/main/resources/lang/en_us.yml @@ -94,6 +94,17 @@ commands: saved_to_cache: "Log file saved to: {}" save_failed: "Failed to save log file to cache: {}" terminal_not_supported: "Retrieving log files in terminal is not supported. Please access the \"./logs\" directory directly." + stats: + description: "View Minecraft statistics" + args_desc: + type: "Stat type" + stat: "Stat name" + server_not_ready: "Minecraft Server is not ready yet." + dir_not_found: "Stats directory not found." + read_failed: "Failed to read stats: {}" + no_stats: "No stats found for {0} {1}." + stats: "Stats" + colon: ": " main: init: diff --git a/core/src/main/resources/lang/zh_cn.yml b/core/src/main/resources/lang/zh_cn.yml index 4f2ae733..4a28ac8f 100644 --- a/core/src/main/resources/lang/zh_cn.yml +++ b/core/src/main/resources/lang/zh_cn.yml @@ -94,6 +94,17 @@ commands: saved_to_cache: "日志文件已保存到:{}" save_failed: "保存日志文件到缓存失败:{}" terminal_not_supported: "在终端中获取日志文件不受支持。请直接访问 \"./logs\" 目录。" + stats: + description: "查看 Minecraft 统计数据" + args_desc: + type: "统计数据类型" + stat: "统计数据名称" + server_not_ready: "Minecraft 服务器尚未准备就绪。" + dir_not_found: "未找到统计数据目录。" + read_failed: "读取统计数据失败:{}" + no_stats: "未找到关于 {0} {1} 的统计数据。" + stats: "统计数据" + colon: ":" main: init: diff --git a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java index 04d464c1..3e2e3a9e 100644 --- a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java +++ b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java @@ -5,8 +5,17 @@ import com.xujiayao.discord_mc_chat.commands.CommandManager; import com.xujiayao.discord_mc_chat.commands.LocalCommandSender; import com.xujiayao.discord_mc_chat.utils.config.ConfigManager; import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.stats.StatType; +import java.util.Optional; + +import static net.minecraft.commands.Commands.argument; import static net.minecraft.commands.Commands.literal; /** @@ -46,11 +55,36 @@ public class MinecraftCommands { CommandManager.execute(new MinecraftCommandSender(ctx.getSource()), "reload"); return 1; }); + var stats = literal("stats") + .requires(source -> source.hasPermission(ConfigManager.getInt("command_permission_levels.stats", 0))) + .then(argument("type", ResourceLocationArgument.id()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource( + BuiltInRegistries.STAT_TYPE.keySet(), builder)) + .then(argument("stat", ResourceLocationArgument.id()) + .suggests((ctx, builder) -> { + try { + ResourceLocation typeLoc = ctx.getArgument("type", ResourceLocation.class); + Optional>> optional = BuiltInRegistries.STAT_TYPE.get(typeLoc); + + if (optional.isPresent()) { + return SharedSuggestionProvider.suggestResource(optional.get().value().getRegistry().keySet(), builder); + } + } catch (Exception ignored) { + } + return builder.buildFuture(); + }) + .executes(ctx -> { + ResourceLocation typeLoc = ctx.getArgument("type", ResourceLocation.class); + ResourceLocation statLoc = ctx.getArgument("stat", ResourceLocation.class); + CommandManager.execute(new MinecraftCommandSender(ctx.getSource()), "stats", typeLoc.toString(), statLoc.toString()); + return 1; + }))); dispatcher.register(root .then(help) .then(info) - .then(reload)); + .then(reload) + .then(stats)); } /** diff --git a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java index dc1593f0..d5787a1c 100644 --- a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java +++ b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java @@ -1,6 +1,7 @@ package com.xujiayao.discord_mc_chat.minecraft.events; import com.xujiayao.discord_mc_chat.DMCC; +import com.xujiayao.discord_mc_chat.commands.impl.StatsCommand; import com.xujiayao.discord_mc_chat.minecraft.commands.MinecraftCommands; import com.xujiayao.discord_mc_chat.minecraft.translations.TranslationManager; import com.xujiayao.discord_mc_chat.network.NetworkManager; @@ -11,15 +12,26 @@ import com.xujiayao.discord_mc_chat.utils.config.ConfigManager; import com.xujiayao.discord_mc_chat.utils.config.ModeManager; import com.xujiayao.discord_mc_chat.utils.events.EventManager; import net.minecraft.advancements.DisplayInfo; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ServerTickRateManager; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.NameAndId; +import net.minecraft.stats.StatType; import net.minecraft.util.TimeUtil; import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.storage.LevelResource; import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; /** @@ -34,6 +46,51 @@ public class MinecraftEventHandler { */ public static void init() { EventManager.register(MinecraftEvents.ServerStarted.class, event -> { + StatsCommand.setProvider(new StatsCommand.StatsProvider() { + @Override + public void saveAll() { + event.minecraftServer().getPlayerList().saveAll(); + } + + @Override + public Path getStatsDirectory() { + return event.minecraftServer().getWorldPath(LevelResource.PLAYER_STATS_DIR); + } + + @Override + public String getPlayerName(UUID uuid) { + return event.minecraftServer().services().nameToIdCache() + .get(uuid) + .map(NameAndId::name) + .orElse(null); + } + + @Override + public List getStatTypes() { + List types = new ArrayList<>(); + for (ResourceLocation loc : BuiltInRegistries.STAT_TYPE.keySet()) { + types.add(loc.toString()); + } + return types; + } + + @Override + public List getStatNames(String typeStr) { + List stats = new ArrayList<>(); + try { + ResourceLocation typeLoc = ResourceLocation.parse(typeStr); + Optional>> optional = BuiltInRegistries.STAT_TYPE.get(typeLoc); + if (optional.isPresent()) { + for (ResourceLocation loc : optional.get().value().getRegistry().keySet()) { + stats.add(loc.toString()); + } + } + } catch (Exception ignored) { + } + return stats; + } + }); + Map placeholders = Map.of(); NetworkManager.sendPacketToServer(new MinecraftEventPacket(MinecraftEventPacket.MessageType.SERVER_STARTED, placeholders));