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 6349e8dc..78e6e01b 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.mojang.brigadier.ParseResults; +import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.suggestion.Suggestion; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.tree.CommandNode; @@ -282,32 +283,123 @@ public class MinecraftEventHandler { null ); - ParseResults parse = serverInstance.getCommands().getDispatcher().parse(event.input(), source); + String rawInput = event.input() == null ? "" : event.input(); + try { - Suggestions suggestions = serverInstance.getCommands().getDispatcher() - .getCompletionSuggestions(parse) - .get(3, TimeUnit.SECONDS); + List currentResult = getSuggestionsForInput(rawInput, source); - boolean isRootToken = event.input() != null && !event.input().contains(" "); + if (!rawInput.isBlank() && !rawInput.endsWith(" ") && isExactPath(rawInput, source)) { + List nextResult = getSuggestionsForInput(rawInput + " ", source); - Set allowedRoot = new HashSet<>(); - for (CommandNode child : serverInstance.getCommands().getDispatcher().getRoot().getChildren()) { - if (!child.getName().isEmpty() && child.canUse(source)) { - allowedRoot.add(child.getName()); + // Priority: + // 1) self (only if self is a valid candidate) + // 2) suggestions for " " + // 3) current suggestions + Set added = new HashSet<>(); + + if (isSelfCandidate(rawInput, source, nextResult, currentResult)) { + added.add(rawInput); + event.suggestions().add(rawInput); } + + for (String s : nextResult) { + if (added.add(s)) { + event.suggestions().add(s); + } + } + + for (String s : currentResult) { + if (added.add(s)) { + event.suggestions().add(s); + } + } + return; } - for (Suggestion suggestion : suggestions.getList()) { - if (isRootToken && !allowedRoot.contains(suggestion.getText())) { - continue; - } - event.suggestions().add(suggestion.apply(event.input())); - } + event.suggestions().addAll(currentResult); } catch (Exception ignored) { } }); } + private static List getSuggestionsForInput(String input, CommandSourceStack source) throws Exception { + ParseResults parse = serverInstance.getCommands().getDispatcher().parse(input, source); + + Suggestions suggestions = serverInstance.getCommands().getDispatcher() + .getCompletionSuggestions(parse) + .get(3, TimeUnit.SECONDS); + + boolean isRootToken = !input.contains(" "); + Set allowedRoot = new HashSet<>(); + for (CommandNode child : serverInstance.getCommands().getDispatcher().getRoot().getChildren()) { + if (!child.getName().isEmpty() && child.canUse(source)) { + allowedRoot.add(child.getName()); + } + } + + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (Suggestion suggestion : suggestions.getList()) { + if (isRootToken && !allowedRoot.contains(suggestion.getText())) { + continue; + } + String applied = suggestion.apply(input); + if (seen.add(applied)) { + result.add(applied); + } + } + return result; + } + + private static boolean isExactPath(String input, CommandSourceStack source) { + ParseResults parse = serverInstance.getCommands().getDispatcher().parse(input, source); + CommandContextBuilder ctx = parse.getContext(); + + if (ctx.getRange().getEnd() != input.length()) { + return false; + } + + if (!parse.getExceptions().isEmpty()) { + return false; + } + + return !ctx.getNodes().isEmpty(); + } + + /** + * Determines whether the raw input itself should be included as the "self" suggestion. + * This avoids showing invalid partial tokens such as "dmcc stats minecraft:cust". + * + * @param rawInput The original user input. + * @param source The command source stack for permission context. + * @param nextResult The suggestions for " " (if applicable). + * @param currentResult The suggestions for the current input. + * @return true if rawInput is a valid candidate to be included as a suggestion, false otherwise. + */ + private static boolean isSelfCandidate(String rawInput, + CommandSourceStack source, + List nextResult, + List currentResult) { + // Fast path: already present in computed suggestions. + if (nextResult.contains(rawInput) || currentResult.contains(rawInput)) { + return true; + } + + // Backspace test: + // if input-1 can suggest rawInput, treat rawInput as a valid candidate. + if (rawInput.length() <= 1) { + return false; + } + + String backspaced = rawInput.substring(0, rawInput.length() - 1); + try { + List fromBackspaced = getSuggestionsForInput(backspaced, source); + return fromBackspaced.contains(rawInput); + } catch (Exception ignored) { + return false; + } + } + /** * Builds an InfoResponsePacket containing real-time metrics of the Minecraft server. *