diff --git a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch index c6f8e00d2..a0588f8cf 100644 --- a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch @@ -35,7 +35,7 @@ @Nullable private RconThread rconThread; public DedicatedServerSettings settings; -@@ -81,33 +92,99 @@ +@@ -81,36 +92,102 @@ private DebugSampleSubscriptionTracker debugSampleSubscriptionTracker; public ServerLinks serverLinks; @@ -143,8 +143,12 @@ + thread.setDaemon(true); thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(DedicatedServer.LOGGER)); - thread.start(); -@@ -126,13 +203,33 @@ +- thread.start(); ++ // thread.start(); // Paper - Enhance console tab completions for brigadier commands; moved down + DedicatedServer.LOGGER.info("Starting minecraft server version {}", SharedConstants.getCurrentVersion().getName()); + if (Runtime.getRuntime().maxMemory() / 1024L / 1024L < 512L) { + DedicatedServer.LOGGER.warn("To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\""); +@@ -126,13 +203,34 @@ this.setPreventProxyConnections(dedicatedserverproperties.preventProxyConnections); this.setLocalIp(dedicatedserverproperties.serverIp); } @@ -165,6 +169,7 @@ + this.getPlayerList().loadAndSaveFiles(); // Must be after convertNames + // Paper end - fix converting txt to json file + org.spigotmc.WatchdogThread.doStart(org.spigotmc.SpigotConfig.timeoutTime, org.spigotmc.SpigotConfig.restartOnCrash); // Paper - start watchdog thread ++ thread.start(); // Paper - Enhance console tab completions for brigadier commands; start console thread after MinecraftServer.console & PaperConfig are initialized + io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now @@ -179,16 +184,17 @@ DedicatedServer.LOGGER.info("Default game type: {}", dedicatedserverproperties.gamemode); InetAddress inetaddress = null; -@@ -156,21 +253,31 @@ +@@ -155,22 +253,32 @@ + DedicatedServer.LOGGER.warn("Perhaps a server is already running on that port?"); return false; } - ++ + // CraftBukkit start + // this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage)); // Spigot - moved up + this.server.loadPlugins(); + this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.STARTUP); + // CraftBukkit end -+ + if (!this.usesAuthentication()) { DedicatedServer.LOGGER.warn("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!"); DedicatedServer.LOGGER.warn("The server will make no attempt to authenticate usernames. Beware."); @@ -216,7 +222,7 @@ this.debugSampleSubscriptionTracker = new DebugSampleSubscriptionTracker(this.getPlayerList()); this.tickTimeLogger = new RemoteSampleLogger(TpsDebugDimensions.values().length, this.debugSampleSubscriptionTracker, RemoteDebugSampleType.TICK_TIME); long i = Util.getNanos(); -@@ -178,13 +285,13 @@ +@@ -178,13 +286,13 @@ SkullBlockEntity.setup(this.services, this); GameProfileCache.setUsesAuthentication(this.usesAuthentication()); DedicatedServer.LOGGER.info("Preparing level \"{}\"", this.getLevelIdName()); @@ -232,7 +238,7 @@ } if (dedicatedserverproperties.enableQuery) { -@@ -197,7 +304,7 @@ +@@ -197,7 +305,7 @@ this.rconThread = RconThread.create(this); } @@ -241,7 +247,7 @@ Thread thread1 = new Thread(new ServerWatchdog(this)); thread1.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(DedicatedServer.LOGGER)); -@@ -215,6 +322,12 @@ +@@ -215,6 +323,12 @@ } } @@ -254,7 +260,7 @@ @Override public boolean isSpawningMonsters() { return this.settings.getProperties().spawnMonsters && super.isSpawningMonsters(); -@@ -227,7 +340,7 @@ +@@ -227,7 +341,7 @@ @Override public void forceDifficulty() { @@ -263,7 +269,7 @@ } @Override -@@ -286,13 +399,14 @@ +@@ -286,13 +400,14 @@ } if (this.rconThread != null) { @@ -280,7 +286,7 @@ } @Override -@@ -302,19 +416,29 @@ +@@ -302,19 +417,29 @@ } @Override @@ -316,7 +322,7 @@ } } -@@ -383,7 +507,7 @@ +@@ -383,7 +508,7 @@ @Override public boolean isUnderSpawnProtection(ServerLevel world, BlockPos pos, Player player) { @@ -325,7 +331,7 @@ return false; } else if (this.getPlayerList().getOps().isEmpty()) { return false; -@@ -453,7 +577,11 @@ +@@ -453,7 +578,11 @@ public boolean enforceSecureProfile() { DedicatedServerProperties dedicatedserverproperties = this.getProperties(); @@ -338,7 +344,7 @@ } @Override -@@ -541,16 +669,52 @@ +@@ -541,16 +670,52 @@ @Override public String getPluginNames() { @@ -395,7 +401,7 @@ } public void storeUsingWhiteList(boolean useWhitelist) { -@@ -660,4 +824,15 @@ +@@ -660,4 +825,15 @@ } } } diff --git a/paper-server/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/paper-server/src/main/java/com/destroystokyo/paper/console/PaperConsole.java index a4070b59e..6ee39b534 100644 --- a/paper-server/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +++ b/paper-server/src/main/java/com/destroystokyo/paper/console/PaperConsole.java @@ -1,5 +1,8 @@ package com.destroystokyo.paper.console; +import io.papermc.paper.configuration.GlobalConfiguration; +import io.papermc.paper.console.BrigadierCompletionMatcher; +import io.papermc.paper.console.BrigadierConsoleParser; import net.minecraft.server.dedicated.DedicatedServer; import net.minecrell.terminalconsole.SimpleTerminalConsole; import org.bukkit.craftbukkit.command.ConsoleCommandCompleter; @@ -16,11 +19,20 @@ public final class PaperConsole extends SimpleTerminalConsole { @Override protected LineReader buildReader(LineReaderBuilder builder) { - return super.buildReader(builder + builder .appName("Paper") .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history")) .completer(new ConsoleCommandCompleter(this.server)) - ); + .option(LineReader.Option.COMPLETE_IN_WORD, true); + if (io.papermc.paper.configuration.GlobalConfiguration.get().console.enableBrigadierHighlighting) { + builder.highlighter(new io.papermc.paper.console.BrigadierCommandHighlighter(this.server)); + } + if (GlobalConfiguration.get().console.enableBrigadierCompletions) { + System.setProperty("org.jline.reader.support.parsedline", "true"); // to hide a warning message about the parser not supporting + builder.parser(new BrigadierConsoleParser(this.server)); + builder.completionMatcher(new BrigadierCompletionMatcher()); + } + return super.buildReader(builder); } @Override diff --git a/paper-server/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java b/paper-server/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java new file mode 100644 index 000000000..bf7b9518c --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java @@ -0,0 +1,119 @@ +package io.papermc.paper.console; + +import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; +import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion; +import com.google.common.base.Suppliers; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.suggestion.Suggestion; +import io.papermc.paper.adventure.PaperAdventure; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import net.kyori.adventure.text.Component; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ComponentUtils; +import net.minecraft.server.dedicated.DedicatedServer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +import static com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion.completion; + +public final class BrigadierCommandCompleter { + private final Supplier commandSourceStack; + private final DedicatedServer server; + + public BrigadierCommandCompleter(final @NonNull DedicatedServer server) { + this.server = server; + this.commandSourceStack = Suppliers.memoize(this.server::createCommandSourceStack); + } + + public void complete(final @NonNull LineReader reader, final @NonNull ParsedLine line, final @NonNull List candidates, final @NonNull List existing) { + //noinspection ConstantConditions + if (this.server.overworld() == null) { // check if overworld is null, as worlds haven't been loaded yet + return; + } else if (!io.papermc.paper.configuration.GlobalConfiguration.get().console.enableBrigadierCompletions) { + this.addCandidates(candidates, Collections.emptyList(), existing, new ParseContext(line.line(), 0)); + return; + } + final CommandDispatcher dispatcher = this.server.getCommands().getDispatcher(); + final ParseResults results = dispatcher.parse(new StringReader(line.line()), this.commandSourceStack.get()); + this.addCandidates( + candidates, + dispatcher.getCompletionSuggestions(results, line.cursor()).join().getList(), + existing, + new ParseContext(line.line(), results.getContext().findSuggestionContext(line.cursor()).startPos) + ); + } + + private void addCandidates( + final @NonNull List candidates, + final @NonNull List brigSuggestions, + final @NonNull List existing, + final @NonNull ParseContext context + ) { + brigSuggestions.forEach(it -> { + if (it.getText().isEmpty()) return; + candidates.add(toCandidate(it, context)); + }); + for (final AsyncTabCompleteEvent.Completion completion : existing) { + if (completion.suggestion().isEmpty() || brigSuggestions.stream().anyMatch(it -> it.getText().equals(completion.suggestion()))) { + continue; + } + candidates.add(toCandidate(completion)); + } + } + + private static Candidate toCandidate(final Suggestion suggestion, final @NonNull ParseContext context) { + Component tooltip = null; + if (suggestion.getTooltip() != null) { + tooltip = PaperAdventure.asAdventure(ComponentUtils.fromMessage(suggestion.getTooltip())); + } + return toCandidate(context.line.substring(context.suggestionStart, suggestion.getRange().getStart()) + suggestion.getText(), tooltip); + } + + private static @NonNull Candidate toCandidate(final @NonNull Completion completion) { + return toCandidate(completion.suggestion(), completion.tooltip()); + } + + private static @NonNull Candidate toCandidate(final @NonNull String suggestionText, final @Nullable Component tooltip) { + final String suggestionTooltip = PaperAdventure.ANSI_SERIALIZER.serializeOr(tooltip, null); + //noinspection SpellCheckingInspection + return new PaperCandidate( + suggestionText, + suggestionText, + null, + suggestionTooltip, + null, + null, + /* + in an ideal world, this would sometimes be true if the suggestion represented the final possible value for a word. + Like for `/execute alig`, pressing enter on align would add a trailing space if this value was true. But not all + suggestions should add spaces after, like `/execute as @`, accepting any suggestion here would be valid, but its also + valid to have a `[` following the selector + */ + false + ); + } + + private static @NonNull Completion toCompletion(final @NonNull Suggestion suggestion) { + if (suggestion.getTooltip() == null) { + return completion(suggestion.getText()); + } + return completion(suggestion.getText(), PaperAdventure.asAdventure(ComponentUtils.fromMessage(suggestion.getTooltip()))); + } + + private record ParseContext(String line, int suggestionStart) { + } + + public static final class PaperCandidate extends Candidate { + public PaperCandidate(final String value, final String display, final String group, final String descr, final String suffix, final String key, final boolean complete) { + super(value, display, group, descr, suffix, key, complete); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java b/paper-server/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java new file mode 100644 index 000000000..0b21dac44 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java @@ -0,0 +1,67 @@ +package io.papermc.paper.console; + +import com.google.common.base.Suppliers; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.dedicated.DedicatedServer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.jline.reader.Highlighter; +import org.jline.reader.LineReader; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; + +public final class BrigadierCommandHighlighter implements Highlighter { + private static final int[] COLORS = {AttributedStyle.CYAN, AttributedStyle.YELLOW, AttributedStyle.GREEN, AttributedStyle.MAGENTA, /* Client uses GOLD here, not BLUE, however there is no GOLD AttributedStyle. */ AttributedStyle.BLUE}; + private final Supplier commandSourceStack; + private final DedicatedServer server; + + public BrigadierCommandHighlighter(final @NonNull DedicatedServer server) { + this.server = server; + this.commandSourceStack = Suppliers.memoize(this.server::createCommandSourceStack); + } + + @Override + public AttributedString highlight(final @NonNull LineReader reader, final @NonNull String buffer) { + //noinspection ConstantConditions + if (this.server.overworld() == null) { // check if overworld is null, as worlds haven't been loaded yet + return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + } + final AttributedStringBuilder builder = new AttributedStringBuilder(); + final ParseResults results = this.server.getCommands().getDispatcher().parse(new StringReader(buffer), this.commandSourceStack.get()); + int pos = 0; + int component = -1; + for (final ParsedCommandNode node : results.getContext().getLastChild().getNodes()) { + if (node.getRange().getStart() >= buffer.length()) { + break; + } + final int start = node.getRange().getStart(); + final int end = Math.min(node.getRange().getEnd(), buffer.length()); + builder.append(buffer.substring(pos, start), AttributedStyle.DEFAULT); + if (node.getNode() instanceof LiteralCommandNode) { + builder.append(buffer.substring(start, end), AttributedStyle.DEFAULT); + } else { + if (++component >= COLORS.length) { + component = 0; + } + builder.append(buffer.substring(start, end), AttributedStyle.DEFAULT.foreground(COLORS[component])); + } + pos = end; + } + if (pos < buffer.length()) { + builder.append((buffer.substring(pos)), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + } + return builder.toAttributedString(); + } + + @Override + public void setErrorPattern(final Pattern errorPattern) {} + + @Override + public void setErrorIndex(final int errorIndex) {} +} diff --git a/paper-server/src/main/java/io/papermc/paper/console/BrigadierCompletionMatcher.java b/paper-server/src/main/java/io/papermc/paper/console/BrigadierCompletionMatcher.java new file mode 100644 index 000000000..1e8028a43 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/console/BrigadierCompletionMatcher.java @@ -0,0 +1,27 @@ +package io.papermc.paper.console; + +import com.google.common.collect.Iterables; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jline.reader.Candidate; +import org.jline.reader.CompletingParsedLine; +import org.jline.reader.LineReader; +import org.jline.reader.impl.CompletionMatcherImpl; + +public class BrigadierCompletionMatcher extends CompletionMatcherImpl { + + @Override + protected void defaultMatchers(final Map options, final boolean prefix, final CompletingParsedLine line, final boolean caseInsensitive, final int errors, final String originalGroupName) { + super.defaultMatchers(options, prefix, line, caseInsensitive, errors, originalGroupName); + this.matchers.addFirst(m -> { + final Map> candidates = new HashMap<>(); + for (final Map.Entry> entry : m.entrySet()) { + if (Iterables.all(entry.getValue(), BrigadierCommandCompleter.PaperCandidate.class::isInstance)) { + candidates.put(entry.getKey(), entry.getValue()); + } + } + return candidates; + }); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/console/BrigadierConsoleParser.java b/paper-server/src/main/java/io/papermc/paper/console/BrigadierConsoleParser.java new file mode 100644 index 000000000..8239a8ba5 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/console/BrigadierConsoleParser.java @@ -0,0 +1,79 @@ +package io.papermc.paper.console; + +import com.mojang.brigadier.ImmutableStringReader; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.context.StringRange; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.dedicated.DedicatedServer; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.jline.reader.SyntaxError; + +public class BrigadierConsoleParser implements Parser { + + private final DedicatedServer server; + + public BrigadierConsoleParser(DedicatedServer server) { + this.server = server; + } + + @Override + public ParsedLine parse(final String line, final int cursor, final ParseContext context) throws SyntaxError { + final ParseResults results = this.server.getCommands().getDispatcher().parse(new StringReader(line), this.server.createCommandSourceStack()); + final ImmutableStringReader reader = results.getReader(); + final List words = new ArrayList<>(); + CommandContextBuilder currentContext = results.getContext(); + int currentWordIdx = -1; + int wordIdx = -1; + int inWordCursor = -1; + if (currentContext.getRange().getLength() > 0) { + do { + for (final ParsedCommandNode node : currentContext.getNodes()) { + final StringRange nodeRange = node.getRange(); + String current = nodeRange.get(reader); + words.add(current); + currentWordIdx++; + if (wordIdx == -1 && nodeRange.getStart() <= cursor && nodeRange.getEnd() >= cursor) { + // if cursor is in the middle of a parsed word/node + wordIdx = currentWordIdx; + inWordCursor = cursor - nodeRange.getStart(); + } + } + currentContext = currentContext.getChild(); + } while (currentContext != null); + } + final String leftovers = reader.getRemaining(); + if (!leftovers.isEmpty() && leftovers.isBlank()) { + // if brig didn't consume the whole line, and everything else is blank, add a new empty word + currentWordIdx++; + words.add(""); + if (wordIdx == -1) { + wordIdx = currentWordIdx; + inWordCursor = 0; + } + } else if (!leftovers.isEmpty()) { + // if there are unparsed leftovers, add a new word with the remaining input + currentWordIdx++; + words.add(leftovers); + if (wordIdx == -1) { + wordIdx = currentWordIdx; + inWordCursor = cursor - reader.getCursor(); + } + } + if (wordIdx == -1) { + currentWordIdx++; + words.add(""); + wordIdx = currentWordIdx; + inWordCursor = 0; + } + return new BrigadierParsedLine(words.get(wordIdx), inWordCursor, wordIdx, words, line, cursor); + } + + record BrigadierParsedLine(String word, int wordCursor, int wordIndex, List words, String line, int cursor) implements ParsedLine { + } +} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/paper-server/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java index 15bc85f47..a45e65899 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java @@ -18,9 +18,11 @@ import org.bukkit.event.server.TabCompleteEvent; public class ConsoleCommandCompleter implements Completer { private final DedicatedServer server; // Paper - CraftServer -> DedicatedServer + private final io.papermc.paper.console.BrigadierCommandCompleter brigadierCompleter; // Paper - Enhance console tab completions for brigadier commands public ConsoleCommandCompleter(DedicatedServer server) { // Paper - CraftServer -> DedicatedServer this.server = server; + this.brigadierCompleter = new io.papermc.paper.console.BrigadierCommandCompleter(this.server); // Paper - Enhance console tab completions for brigadier commands } // Paper start - Change method signature for JLine update @@ -64,7 +66,7 @@ public class ConsoleCommandCompleter implements Completer { } } - if (!completions.isEmpty()) { + if (false && !completions.isEmpty()) { for (final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion completion : completions) { if (completion.suggestion().isEmpty()) { continue; @@ -80,6 +82,7 @@ public class ConsoleCommandCompleter implements Completer { )); } } + this.addCompletions(reader, line, candidates, completions); return; } @@ -99,10 +102,12 @@ public class ConsoleCommandCompleter implements Completer { try { List offers = waitable.get(); if (offers == null) { + this.addCompletions(reader, line, candidates, Collections.emptyList()); // Paper - Enhance console tab completions for brigadier commands return; // Paper - Method returns void } // Paper start - JLine update + /* for (String completion : offers) { if (completion.isEmpty()) { continue; @@ -110,6 +115,8 @@ public class ConsoleCommandCompleter implements Completer { candidates.add(new Candidate(completion)); } + */ + this.addCompletions(reader, line, candidates, offers.stream().map(com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion::completion).collect(java.util.stream.Collectors.toList())); // Paper end // Paper start - JLine handles cursor now @@ -138,5 +145,9 @@ public class ConsoleCommandCompleter implements Completer { } return false; } + + private void addCompletions(final LineReader reader, final ParsedLine line, final List candidates, final List existing) { + this.brigadierCompleter.complete(reader, line, candidates, existing); + } // Paper end }