diff --git a/leaves-server/minecraft-patches/features/0142-Leaves-Server-Command.patch b/leaves-server/minecraft-patches/features/0142-Leaves-Server-Command.patch new file mode 100644 index 00000000..17b8ad71 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0142-Leaves-Server-Command.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MC_XiaoHei +Date: Fri, 22 Aug 2025 09:42:16 +0800 +Subject: [PATCH] Leaves Server Command + + +diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java +index ec1cced129ef42be65d7b2b622638bfae8bd895e..b54b1b56c8df6c6e03c2f53423b273a7c975a498 100644 +--- a/net/minecraft/commands/Commands.java ++++ b/net/minecraft/commands/Commands.java +@@ -182,6 +182,7 @@ public class Commands { + } + public Commands(Commands.CommandSelection selection, CommandBuildContext context, final boolean modern) { + // Paper end - Brigadier API - modern minecraft overloads that do not use redirects but are copies instead ++ org.leavesmc.leaves.neo_command.LeavesCommands.registerLeavesCommands(dispatcher); // Leaves Commands + AdvancementCommands.register(this.dispatcher); + AttributeCommand.register(this.dispatcher, context); + ExecuteCommand.register(this.dispatcher, context); diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/ArgumentNode.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/ArgumentNode.java new file mode 100644 index 00000000..518ebc13 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/ArgumentNode.java @@ -0,0 +1,37 @@ +package org.leavesmc.leaves.neo_command; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; + +import java.util.concurrent.CompletableFuture; + +public abstract class ArgumentNode extends CommandNode { + private final ArgumentType argumentType; + + protected ArgumentNode(String name, ArgumentType argumentType) { + super(name); + this.argumentType = argumentType; + } + + @SuppressWarnings({"unused", "RedundantThrows"}) + protected CompletableFuture getSuggestions(final CommandContext context, final SuggestionsBuilder builder) throws CommandSyntaxException { + return Suggestions.empty(); + } + + @Override + protected ArgumentBuilder compileBase() { + RequiredArgumentBuilder argumentBuilder = Commands.argument(name, argumentType); + if (isMethodOverridden("getSuggestions", ArgumentNode.class)) { + argumentBuilder.suggests( + (context, builder) -> getSuggestions(new CommandContext(context), builder) + ); + } + return argumentBuilder; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandContext.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandContext.java new file mode 100644 index 00000000..5ecf61fa --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandContext.java @@ -0,0 +1,105 @@ +package org.leavesmc.leaves.neo_command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.RedirectModifier; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.tree.CommandNode; +import net.minecraft.commands.CommandSourceStack; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static org.leavesmc.leaves.neo_command.CommandNode.*; + +@SuppressWarnings({"ClassCanBeRecord", "unused"}) +public class CommandContext { + private final com.mojang.brigadier.context.CommandContext source; + + public CommandContext(com.mojang.brigadier.context.CommandContext source) { + this.source = source; + } + + public com.mojang.brigadier.context.CommandContext getChild() { + return source.getChild(); + } + + public com.mojang.brigadier.context.CommandContext getLastChild() { + return source.getLastChild(); + } + + public Command getCommand() { + return source.getCommand(); + } + + public CommandSourceStack getSource() { + return source.getSource(); + } + + public CommandSender getSender() { + return source.getSource().getSender(); + } + + public @NotNull V getArgument(final String name, final Class clazz) { + return source.getArgument(name, clazz); + } + + @SuppressWarnings("unchecked") + public @NotNull V getArgument(final Class> nodeClass) { + String name = getNameForNode(nodeClass); + return (V) source.getArgument(name, Object.class); + } + + public @NotNull V getArgumentOrDefault(final Class> nodeClass, final V defaultValue) { + try { + return getArgument(nodeClass); + } catch (IllegalArgumentException e) { + return defaultValue; + } + } + + public V getArgumentOrDefault(final String name, final Class clazz, final V defaultValue) { + try { + return source.getArgument(name, clazz); + } catch (IllegalArgumentException e) { + return defaultValue; + } + } + + public String getStringOrDefault(final String name, final String defaultValue) { + return getArgumentOrDefault(name, String.class, defaultValue); + } + + public int getIntegerOrDefault(final String name, final int defaultValue) { + return getArgumentOrDefault(name, Integer.class, defaultValue); + } + + public RedirectModifier getRedirectModifier() { + return source.getRedirectModifier(); + } + + public StringRange getRange() { + return source.getRange(); + } + + public String getInput() { + return source.getInput(); + } + + public CommandNode getRootNode() { + return source.getRootNode(); + } + + public List> getNodes() { + return source.getNodes(); + } + + public boolean hasNodes() { + return source.hasNodes(); + } + + public boolean isForked() { + return source.isForked(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandNode.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandNode.java new file mode 100644 index 00000000..a4971c32 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandNode.java @@ -0,0 +1,74 @@ +package org.leavesmc.leaves.neo_command; + +import com.mojang.brigadier.builder.ArgumentBuilder; +import net.minecraft.commands.CommandSourceStack; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public abstract class CommandNode { + private static final Map, String> class2NameMap = new HashMap<>(); + + protected final String name; + protected final List children = new ArrayList<>(); + + protected CommandNode(String name) { + this.name = name; + class2NameMap.put(getClass(), name); + } + + @SafeVarargs + protected final void children(Supplier... childrenClasses) { + this.children.addAll(Stream.of(childrenClasses).map(Supplier::get).toList()); + } + + protected abstract ArgumentBuilder compileBase(); + + protected boolean execute(CommandContext context) { + return true; + } + + protected boolean requires(CommandSourceStack source) { + return true; + } + + protected ArgumentBuilder compile() { + ArgumentBuilder builder = compileBase(); + + if (isMethodOverridden("requires", CommandNode.class)) { + builder = builder.requires(this::requires); + } + + for (CommandNode child : children) { + builder = builder.then(child.compile()); + } + + if (isMethodOverridden("execute", CommandNode.class)) { + builder = builder.executes(mojangCtx -> { + CommandContext ctx = new CommandContext(mojangCtx); + return execute(ctx) ? 1 : 0; + }); + } + + return builder; + } + + protected boolean isMethodOverridden(String methodName, @NotNull Class baseClass) { + for (Method method : getClass().getDeclaredMethods()) { + if (method.getName().equals(methodName)) { + return method.getDeclaringClass() != baseClass; + } + } + return false; + } + + public static String getNameForNode(Class nodeClass) { + return class2NameMap.get(nodeClass); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LeavesCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LeavesCommand.java new file mode 100644 index 00000000..8abe1f59 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LeavesCommand.java @@ -0,0 +1,10 @@ +package org.leavesmc.leaves.neo_command; + +import org.leavesmc.leaves.neo_command.subcommands.ConfigCommand; + +public class LeavesCommand extends LiteralNode { + public LeavesCommand() { + super("leaves_new"); + children(ConfigCommand::new); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LeavesCommands.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LeavesCommands.java new file mode 100644 index 00000000..93e04589 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LeavesCommands.java @@ -0,0 +1,11 @@ +package org.leavesmc.leaves.neo_command; + +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import org.jetbrains.annotations.NotNull; + +public class LeavesCommands { + public static void registerLeavesCommands(@NotNull CommandDispatcher dispatcher) { + new LeavesCommand().register(dispatcher); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LiteralNode.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LiteralNode.java new file mode 100644 index 00000000..0fa870ef --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LiteralNode.java @@ -0,0 +1,25 @@ +package org.leavesmc.leaves.neo_command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import org.jetbrains.annotations.NotNull; + +public class LiteralNode extends CommandNode { + + protected LiteralNode(String name) { + super(name); + } + + @Override + protected ArgumentBuilder compileBase() { + return Commands.literal(name); + } + + @SuppressWarnings("unchecked") + public void register(@NotNull CommandDispatcher dispatcher) { + dispatcher.register((LiteralArgumentBuilder) compile()); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/subcommands/ConfigCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/subcommands/ConfigCommand.java new file mode 100644 index 00000000..4d39b887 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/subcommands/ConfigCommand.java @@ -0,0 +1,147 @@ +package org.leavesmc.leaves.neo_command.subcommands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.command.LeavesCommandUtil; +import org.leavesmc.leaves.config.GlobalConfigManager; +import org.leavesmc.leaves.config.VerifiedConfig; +import org.leavesmc.leaves.neo_command.ArgumentNode; +import org.leavesmc.leaves.neo_command.CommandContext; +import org.leavesmc.leaves.neo_command.LiteralNode; + +import java.util.concurrent.CompletableFuture; + +import static net.kyori.adventure.text.Component.text; + +public class ConfigCommand extends LiteralNode { + + public ConfigCommand() { + super("config"); + children(PathArgument::new); + } + + private static class PathArgument extends ArgumentNode { + + public PathArgument() { + super("path", StringArgumentType.string()); + children(ValueArgument::new); + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgumentOrDefault(PathArgument.class, ""); + int dotIndex = path.lastIndexOf("."); + builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2); + LeavesCommandUtil.getListClosestMatchingLast( + context.getSender(), + path.substring(dotIndex + 1), + GlobalConfigManager.getVerifiedConfigSubPaths(path), + "bukkit.command.leaves.config" + ) + .forEach(builder::suggest); + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String path = context.getArgument(PathArgument.class); + VerifiedConfig verifiedConfig = getVerifiedConfig(context); + if (verifiedConfig == null) { + return false; + } + context.getSender().sendMessage(Component.join(JoinConfiguration.spaces(), + text("Config", NamedTextColor.GRAY), + text(path, NamedTextColor.AQUA), + text("value is", NamedTextColor.GRAY), + text(verifiedConfig.getString(), NamedTextColor.AQUA) + )); + return true; + } + + private static @Nullable VerifiedConfig getVerifiedConfig(@NotNull CommandContext context) { + String path = context.getArgument(PathArgument.class); + VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(path); + if (verifiedConfig == null) { + context.getSender().sendMessage(Component.join(JoinConfiguration.spaces(), + text("Config", NamedTextColor.GRAY), + text(path, NamedTextColor.RED), + text("is Not Found.", NamedTextColor.GRAY) + )); + return null; + } + return verifiedConfig; + } + + private static class ValueArgument extends ArgumentNode { + + public ValueArgument() { + super("value", StringArgumentType.greedyString()); + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgument(PathArgument.class); + String value = context.getArgumentOrDefault(ValueArgument.class, ""); + VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(path); + if (verifiedConfig == null) { + return builder + .suggest("", net.minecraft.network.chat.Component.literal("This config path does not exist.")) + .buildFuture(); + } + LeavesCommandUtil.getListMatchingLast( + context.getSender(), + new String[]{value}, + verifiedConfig.validator().valueSuggest() + ).forEach(builder::suggest); + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + VerifiedConfig verifiedConfig = getVerifiedConfig(context); + String path = context.getArgument(PathArgument.class); + String value = context.getArgument(ValueArgument.class); + if (verifiedConfig == null) { + return false; + } + try { + verifiedConfig.set(value); + context.getSender().sendMessage(Component.join(JoinConfiguration.spaces(), + text("Config", NamedTextColor.GRAY), + text(path, NamedTextColor.AQUA), + text("changed to", NamedTextColor.GRAY), + text(verifiedConfig.getString(), NamedTextColor.AQUA) + )); + Bukkit.getOnlinePlayers() + .stream() + .filter(player -> player.hasPermission("leaves.command.config.notify") && player != context.getSender()) + .forEach( + player -> player.sendMessage(Component.join(JoinConfiguration.spaces(), + text(context.getSender().getName() + ":", NamedTextColor.GRAY), + text("Config", NamedTextColor.GRAY), + text(path, NamedTextColor.AQUA), + text("changed to", NamedTextColor.GRAY), + text(verifiedConfig.getString(), NamedTextColor.AQUA) + )) + ); + return true; + } catch (IllegalArgumentException exception) { + context.getSender().sendMessage(Component.join(JoinConfiguration.spaces(), + text("Config", NamedTextColor.GRAY), + text(path, NamedTextColor.RED), + text("modify error by", NamedTextColor.GRAY), + text(exception.getMessage(), NamedTextColor.RED) + )); + return false; + } + } + } + } +} \ No newline at end of file