From e21ef2adf36cfe8e4f643e40a179fbef7922203c Mon Sep 17 00:00:00 2001 From: Gero Date: Wed, 11 Feb 2026 12:54:45 +0100 Subject: [PATCH 1/2] fix ConcurrentModificationException in async command sending --- .../minecraft/commands/Commands.java.patch | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch index dfe4e32943b8..385f54ab6347 100644 --- a/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch +++ b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch @@ -134,7 +134,7 @@ } return null; -@@ -409,17 +_,110 @@ +@@ -409,17 +_,113 @@ } public void sendCommands(ServerPlayer player) { @@ -147,9 +147,10 @@ + // CraftBukkit start + // Register Vanilla commands into builtRoot as before + // Paper start - Perf: Async command map building -+ // Copy root children to avoid concurrent modification during building -+ final java.util.Collection> commandNodes = new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren()); -+ COMMAND_SENDING_POOL.execute(() -> this.sendAsync(player, commandNodes)); ++ // Copy root node to avoid concurrent modification during building ++ final RootCommandNode rootNode = new RootCommandNode<>(); ++ this.dispatcher.getRoot().getChildren().forEach(rootNode::addChild); ++ COMMAND_SENDING_POOL.execute(() -> this.sendAsync(player, rootNode)); + } + + // Fixed pool, but with discard policy @@ -163,13 +164,16 @@ + new java.util.concurrent.ThreadPoolExecutor.DiscardPolicy() + ); + -+ private void sendAsync(ServerPlayer player, java.util.Collection> dispatcherRootChildren) { ++ private void sendAsync(ServerPlayer player, RootCommandNode dispatcherRoot) { + // Paper end - Perf: Async command map building Map, CommandNode> map = new HashMap<>(); RootCommandNode rootCommandNode = new RootCommandNode<>(); - map.put(this.dispatcher.getRoot(), rootCommandNode); +- map.put(this.dispatcher.getRoot(), rootCommandNode); - fillUsableCommands(this.dispatcher.getRoot(), rootCommandNode, player.createCommandSourceStack(), map); -+ fillUsableCommands(dispatcherRootChildren, rootCommandNode, player.createCommandSourceStack(), map); // Paper - Perf: Async command map building; pass copy of children ++ // Paper start - Perf: Async command map building; pass copy of root ++ map.put(dispatcherRoot, rootCommandNode); ++ fillUsableCommands(dispatcherRoot, rootCommandNode, player.createCommandSourceStack(), map); ++ // Paper start - Perf: Async command map building; pass copy of root + + java.util.Collection bukkit = new java.util.LinkedHashSet<>(); + for (CommandNode node : rootCommandNode.getChildren()) { @@ -198,10 +202,8 @@ player.connection.send(new ClientboundCommandsPacket(rootCommandNode, COMMAND_NODE_INSPECTOR)); } -- private static void fillUsableCommands(CommandNode root, CommandNode current, S source, Map, CommandNode> output) { -- for (CommandNode commandNode : root.getChildren()) { -+ private static void fillUsableCommands(java.util.Collection> children, CommandNode current, S source, Map, CommandNode> output) { // Paper - Perf: Async command map building; pass copy of children -+ for (CommandNode commandNode : children) { // Paper - Perf: Async command map building; pass copy of children + private static void fillUsableCommands(CommandNode root, CommandNode current, S source, Map, CommandNode> output) { + for (CommandNode commandNode : root.getChildren()) { + // Paper start - Brigadier API + if (commandNode.clientNode != null) { + commandNode = commandNode.clientNode; @@ -248,12 +250,3 @@ if (argumentBuilder.getRedirect() != null) { argumentBuilder.redirect(output.get(argumentBuilder.getRedirect())); } -@@ -428,7 +_,7 @@ - output.put(commandNode, commandNode1); - current.addChild(commandNode1); - if (!commandNode.getChildren().isEmpty()) { -- fillUsableCommands(commandNode, commandNode1, source, output); -+ fillUsableCommands(commandNode.getChildren(), commandNode1, source, output); // Paper - Perf: Async command map building; pass copy of children - } - } - } From 4733cfafc9522eb7fd1155bdb82fbe1c5338ec08 Mon Sep 17 00:00:00 2001 From: Warrior <50800980+Warriorrrr@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:39:17 +0100 Subject: [PATCH 2/2] minor comment fix --- .../patches/sources/net/minecraft/commands/Commands.java.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch index 385f54ab6347..02314401301f 100644 --- a/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch +++ b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch @@ -173,7 +173,7 @@ + // Paper start - Perf: Async command map building; pass copy of root + map.put(dispatcherRoot, rootCommandNode); + fillUsableCommands(dispatcherRoot, rootCommandNode, player.createCommandSourceStack(), map); -+ // Paper start - Perf: Async command map building; pass copy of root ++ // Paper end - Perf: Async command map building; pass copy of root + + java.util.Collection bukkit = new java.util.LinkedHashSet<>(); + for (CommandNode node : rootCommandNode.getChildren()) {