diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java index f2e7325a..289fbdd1 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java @@ -6,6 +6,7 @@ import net.kyori.adventure.text.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; +import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.configuration.MemorySection; import org.bukkit.configuration.file.YamlConfiguration; @@ -30,7 +31,6 @@ import org.leavesmc.leaves.config.api.impl.ConfigValidatorImpl.IntConfigValidato import org.leavesmc.leaves.config.api.impl.ConfigValidatorImpl.ListConfigValidator; import org.leavesmc.leaves.config.api.impl.ConfigValidatorImpl.LongConfigValidator; import org.leavesmc.leaves.config.api.impl.ConfigValidatorImpl.StringConfigValidator; -import org.leavesmc.leaves.neo_command.LeavesCommands; import org.leavesmc.leaves.profile.LeavesMinecraftSessionService; import org.leavesmc.leaves.protocol.CarpetServerProtocol.CarpetRule; import org.leavesmc.leaves.protocol.CarpetServerProtocol.CarpetRules; @@ -95,7 +95,7 @@ public final class LeavesConfig { GlobalConfigManager.init(); registerCommand("leaves", new LeavesCommand()); - LeavesCommands.registerLeavesCommands(); + org.leavesmc.leaves.neo_command.leaves.LeavesCommand.INSTANCE.register(); } public static void reload() { @@ -152,9 +152,16 @@ public final class LeavesConfig { public void verify(Boolean old, Boolean value) throws IllegalArgumentException { if (value) { registerCommand("bot", new BotCommand()); + org.leavesmc.leaves.neo_command.bot.BotCommand.INSTANCE.register(); Actions.registerAll(); } else { unregisterCommand("bot"); + org.leavesmc.leaves.neo_command.bot.BotCommand.INSTANCE.unregister(); + } + if (old != null && !old.equals(value)) { + Bukkit.getOnlinePlayers().stream() + .filter(org.leavesmc.leaves.neo_command.bot.BotCommand::hasPermission) + .forEach(org.bukkit.entity.Player::updateCommands); } } } @@ -190,15 +197,58 @@ public final class LeavesConfig { @GlobalConfig("open-fakeplayer-inventory") public boolean canOpenInventory = false; - @GlobalConfig("use-action") + @GlobalConfig(value = "use-action", validator = CanUseActionValidator.class) public boolean canUseAction = true; - @GlobalConfig("modify-config") + private static class CanUseActionValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (old != null && !old.equals(value)) { + Bukkit.getOnlinePlayers().stream() + .filter(sender -> + org.leavesmc.leaves.neo_command.bot.BotCommand.hasPermission(sender) + || org.leavesmc.leaves.neo_command.bot.BotCommand.hasPermission(sender, "action") + ) + .forEach(org.bukkit.entity.Player::updateCommands); + } + } + } + + @GlobalConfig(value = "modify-config", validator = CanModifyConfigValidator.class) public boolean canModifyConfig = false; - @GlobalConfig("manual-save-and-load") + private static class CanModifyConfigValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (old != null && !old.equals(value)) { + Bukkit.getOnlinePlayers().stream() + .filter(sender -> + org.leavesmc.leaves.neo_command.bot.BotCommand.hasPermission(sender) + || org.leavesmc.leaves.neo_command.bot.BotCommand.hasPermission(sender, "config") + ) + .forEach(org.bukkit.entity.Player::updateCommands); + } + } + } + + @GlobalConfig(value = "manual-save-and-load", validator = CanManualSaveAndLoadValidator.class) public boolean canManualSaveAndLoad = false; + private static class CanManualSaveAndLoadValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (old != null && !old.equals(value)) { + Bukkit.getOnlinePlayers().stream() + .filter(sender -> + org.leavesmc.leaves.neo_command.bot.BotCommand.hasPermission(sender) + || org.leavesmc.leaves.neo_command.bot.BotCommand.hasPermission(sender, "save") + || org.leavesmc.leaves.neo_command.bot.BotCommand.hasPermission(sender, "load") + ) + .forEach(org.bukkit.entity.Player::updateCommands); + } + } + } + @GlobalConfig(value = "cache-skin", lock = true) public boolean useSkinCache = false; diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerMoveAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerMoveAction.java index 32104ea1..3dc723bc 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerMoveAction.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerMoveAction.java @@ -13,7 +13,7 @@ import java.util.Arrays; import java.util.Map; import static java.util.stream.Collectors.toMap; -import static org.leavesmc.leaves.neo_command.leaves.ArgumentSuggestions.strings; +import static org.leavesmc.leaves.neo_command.ArgumentNode.ArgumentSuggestions.strings; public class ServerMoveAction extends ServerStateBotAction { private static final Map NAME_TO_DIRECTION = Arrays.stream(MoveDirection.values()).collect(toMap( diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerTimerBotAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerTimerBotAction.java index 58f21986..6cea5c42 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerTimerBotAction.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerTimerBotAction.java @@ -7,7 +7,7 @@ import org.leavesmc.leaves.neo_command.CommandContext; import java.util.function.Supplier; import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; -import static org.leavesmc.leaves.neo_command.leaves.ArgumentSuggestions.strings; +import static org.leavesmc.leaves.neo_command.ArgumentNode.ArgumentSuggestions.strings; public abstract class ServerTimerBotAction> extends ServerBotAction { 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 index 8f9aea04..0f70d0a0 100644 --- 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 @@ -8,7 +8,10 @@ import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.concurrent.CompletableFuture; public abstract class ArgumentNode extends CommandNode { @@ -36,4 +39,24 @@ public abstract class ArgumentNode extends CommandNode { return argumentBuilder; } + + public static class ArgumentSuggestions { + @Contract(pure = true) + public static WrappedArgument.@NotNull SuggestionApplier strings(String... values) { + return (context, builder) -> { + for (String s : values) { + builder.suggest(s); + } + }; + } + + @Contract(pure = true) + public static WrappedArgument.@NotNull SuggestionApplier strings(List values) { + return (context, builder) -> { + for (String s : values) { + builder.suggest(s); + } + }; + } + } } 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 index 7c0da867..41859e21 100644 --- 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 @@ -35,10 +35,14 @@ public abstract class CommandNode { return true; } - protected boolean requires(CommandSourceStack source) { + public boolean requires(CommandSourceStack source) { return true; } + public String getName() { + return this.name; + } + protected ArgumentBuilder compile() { ArgumentBuilder builder = compileBase(); diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandUtils.java new file mode 100644 index 00000000..d6a04cf0 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/CommandUtils.java @@ -0,0 +1,135 @@ +package org.leavesmc.leaves.neo_command; + +import com.google.common.base.Functions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.bukkit.Bukkit; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CommandUtils { + + public static void registerPermissions(@NotNull List permissions) { + PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + for (String perm : permissions) { + if (pluginManager.getPermission(perm) == null) { + pluginManager.addPermission(new Permission(perm, PermissionDefault.OP)); + } + } + } + + @DefaultQualifier(NonNull.class) + public static @NotNull List getListClosestMatchingLast( + final String last, + final Collection collection + ) { + if (collection.isEmpty()) { + return Collections.emptyList(); + } + + ArrayList candidates = Lists.newArrayList(); + String lastLower = last.toLowerCase(); + for (String item : Iterables.transform(collection, Functions.toStringFunction())) { + String itemLower = item.toLowerCase(); + if (itemLower.startsWith(lastLower)) { + candidates.add(Candidate.of(item, 0)); + } else if (itemLower.contains(lastLower)) { + candidates.add(Candidate.of(item, damerauLevenshteinDistance(lastLower, itemLower))); + } + } + candidates.sort(Comparator.comparingInt(c -> c.score)); + + List results = new ArrayList<>(candidates.size()); + for (Candidate candidate : candidates) { + results.add(candidate.item); + } + + return results; + } + + /** + * Computes the Dameraur-Levenshtein Distance between two strings. Adapted + * from the algorithm at Wikipedia: Damerau–Levenshtein distance + * + * @param s1 The first string being compared. + * @param s2 The second string being compared. + * @return The number of substitutions, deletions, insertions, and + * transpositions required to get from s1 to s2. + */ + @SuppressWarnings("DuplicatedCode") + private static int damerauLevenshteinDistance(@Nullable String s1, @Nullable String s2) { + if (s1 == null && s2 == null) { + return 0; + } + if (s1 != null && s2 == null) { + return s1.length(); + } + if (s1 == null) { + return s2.length(); + } + + int s1Len = s1.length(); + int s2Len = s2.length(); + int[][] H = new int[s1Len + 2][s2Len + 2]; + + int INF = s1Len + s2Len; + H[0][0] = INF; + for (int i = 0; i <= s1Len; i++) { + H[i + 1][1] = i; + H[i + 1][0] = INF; + } + for (int j = 0; j <= s2Len; j++) { + H[1][j + 1] = j; + H[0][j + 1] = INF; + } + + Map sd = new HashMap<>(); + for (char Letter : (s1 + s2).toCharArray()) { + if (!sd.containsKey(Letter)) { + sd.put(Letter, 0); + } + } + + for (int i = 1; i <= s1Len; i++) { + int DB = 0; + for (int j = 1; j <= s2Len; j++) { + int i1 = sd.get(s2.charAt(j - 1)); + int j1 = DB; + + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + H[i + 1][j + 1] = H[i][j]; + DB = j; + } else { + H[i + 1][j + 1] = Math.min(H[i][j], Math.min(H[i + 1][j], H[i][j + 1])) + 1; + } + + H[i + 1][j + 1] = Math.min(H[i + 1][j + 1], H[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1)); + } + sd.put(s1.charAt(i - 1), i); + } + + return H[s1Len + 1][s2Len + 1]; + } + + // Copy from org/bukkit/command/defaults/HelpCommand.java + private record Candidate(String item, int score) { + @Contract("_, _ -> new") + private static @NotNull Candidate of(String item, int score) { + return new Candidate(item, score); + } + } +} 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 deleted file mode 100644 index b16cda68..00000000 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/LeavesCommands.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.leavesmc.leaves.neo_command; - -import com.mojang.brigadier.CommandDispatcher; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.server.MinecraftServer; -import org.leavesmc.leaves.neo_command.bot.BotCommand; -import org.leavesmc.leaves.neo_command.leaves.LeavesCommand; - -public class LeavesCommands { - public static void registerLeavesCommands() { - CommandDispatcher dispatcher = MinecraftServer.getServer().getCommands().getDispatcher(); - new LeavesCommand().register(dispatcher); - new BotCommand().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 index 0fa870ef..94eb69a9 100644 --- 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 @@ -5,7 +5,7 @@ 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; +import net.minecraft.server.MinecraftServer; public class LiteralNode extends CommandNode { @@ -19,7 +19,17 @@ public class LiteralNode extends CommandNode { } @SuppressWarnings("unchecked") - public void register(@NotNull CommandDispatcher dispatcher) { - dispatcher.register((LiteralArgumentBuilder) compile()); + public void register() { + MinecraftServer.getServer() + .getCommands() + .getDispatcher() + .register((LiteralArgumentBuilder) compile()); + } + + public void unregister() { + CommandDispatcher dispatcher = MinecraftServer.getServer() + .getCommands() + .getDispatcher(); + dispatcher.getRoot().removeCommand(name); } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotArgument.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotArgument.java index d65d41bf..ee16c072 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotArgument.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotArgument.java @@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture; public class BotArgument implements CustomArgumentType { @Override public ArgumentType getBaseArgumentType() { - return StringArgumentType.string(); + return StringArgumentType.word(); } @Override diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotCommand.java index f505d7a9..ef72ef89 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotCommand.java @@ -1,33 +1,57 @@ package org.leavesmc.leaves.neo_command.bot; +import com.mojang.brigadier.builder.ArgumentBuilder; import net.minecraft.commands.CommandSourceStack; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; -import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.neo_command.CommandNode; +import org.leavesmc.leaves.neo_command.CommandUtils; import org.leavesmc.leaves.neo_command.LiteralNode; import org.leavesmc.leaves.neo_command.bot.subcommands.ActionCommand; +import org.leavesmc.leaves.neo_command.bot.subcommands.ConfigCommand; import org.leavesmc.leaves.neo_command.bot.subcommands.CreateCommand; import org.leavesmc.leaves.neo_command.bot.subcommands.ListCommand; +import org.leavesmc.leaves.neo_command.bot.subcommands.LoadCommand; +import org.leavesmc.leaves.neo_command.bot.subcommands.SaveCommand; + +import java.util.ArrayList; +import java.util.List; public class BotCommand extends LiteralNode { + public static final BotCommand INSTANCE = new BotCommand(); private static final String PERM_BASE = "bukkit.command.bot"; - public BotCommand() { + private BotCommand() { super("bot_neo"); this.children( ActionCommand::new, ListCommand::new, - CreateCommand::new + CreateCommand::new, + LoadCommand::new, + SaveCommand::new, + ConfigCommand::new ); } @Override - protected boolean requires(CommandSourceStack source) { - return LeavesConfig.modify.fakeplayer.enable && source.getSender().hasPermission(PERM_BASE); + protected ArgumentBuilder compile() { + List permissions = new ArrayList<>(); + permissions.add(PERM_BASE); + permissions.addAll(this.children.stream().map(CommandNode::getName).toList()); + CommandUtils.registerPermissions(permissions); + return super.compile(); } - public static boolean hasPermission(@NotNull CommandSourceStack source, String subcommand) { - CommandSender sender = source.getSender(); + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return children.stream().anyMatch(child -> child.requires(source)); + } + + public static boolean hasPermission(@NotNull CommandSender sender) { + return sender.hasPermission(PERM_BASE); + } + + public static boolean hasPermission(@NotNull CommandSender sender, String subcommand) { return sender.hasPermission(PERM_BASE) || sender.hasPermission(PERM_BASE + "." + subcommand); } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotSubcommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotSubcommand.java new file mode 100644 index 00000000..10998310 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/BotSubcommand.java @@ -0,0 +1,17 @@ +package org.leavesmc.leaves.neo_command.bot; + +import net.minecraft.commands.CommandSourceStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.neo_command.LiteralNode; + +public abstract class BotSubcommand extends LiteralNode { + + protected BotSubcommand(String name) { + super(name); + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return BotCommand.hasPermission(source.getSender(), this.name); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ActionCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ActionCommand.java index 81591c28..32bc4a94 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ActionCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ActionCommand.java @@ -3,16 +3,16 @@ package org.leavesmc.leaves.neo_command.bot.subcommands; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.commands.CommandSourceStack; import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; import org.leavesmc.leaves.bot.ServerBot; import org.leavesmc.leaves.neo_command.CommandContext; import org.leavesmc.leaves.neo_command.CustomArgumentNode; -import org.leavesmc.leaves.neo_command.LiteralNode; -import org.leavesmc.leaves.neo_command.bot.BotCommand; +import org.leavesmc.leaves.neo_command.bot.BotSubcommand; import org.leavesmc.leaves.neo_command.bot.subcommands.action.ListCommand; import org.leavesmc.leaves.neo_command.bot.subcommands.action.StartCommand; import org.leavesmc.leaves.neo_command.bot.subcommands.action.StopCommand; -public class ActionCommand extends LiteralNode { +public class ActionCommand extends BotSubcommand { public ActionCommand() { super("action"); @@ -20,8 +20,8 @@ public class ActionCommand extends LiteralNode { } @Override - protected boolean requires(CommandSourceStack source) { - return BotCommand.hasPermission(source, "action"); + public boolean requires(@NotNull CommandSourceStack source) { + return LeavesConfig.modify.fakeplayer.canUseAction && super.requires(source); } public static class BotArgument extends CustomArgumentNode { diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ConfigCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ConfigCommand.java new file mode 100644 index 00000000..051bb3b1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ConfigCommand.java @@ -0,0 +1,10 @@ +package org.leavesmc.leaves.neo_command.bot.subcommands; + +import org.leavesmc.leaves.neo_command.bot.BotSubcommand; + +public class ConfigCommand extends BotSubcommand { + + public ConfigCommand() { + super("config"); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/CreateCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/CreateCommand.java index 5fecbd06..da03268b 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/CreateCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/CreateCommand.java @@ -22,12 +22,12 @@ import org.leavesmc.leaves.bot.BotList; import org.leavesmc.leaves.event.bot.BotCreateEvent; import org.leavesmc.leaves.neo_command.ArgumentNode; import org.leavesmc.leaves.neo_command.CommandContext; -import org.leavesmc.leaves.neo_command.LiteralNode; +import org.leavesmc.leaves.neo_command.bot.BotSubcommand; import static net.kyori.adventure.text.Component.text; import static net.minecraft.commands.arguments.DimensionArgument.getDimension; -public class CreateCommand extends LiteralNode { +public class CreateCommand extends BotSubcommand { public CreateCommand() { super("create"); @@ -133,7 +133,7 @@ public class CreateCommand extends LiteralNode { } @Override - protected boolean requires(@NotNull CommandSourceStack source) { + public boolean requires(@NotNull CommandSourceStack source) { return source.getSender() instanceof ConsoleCommandSender; } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ListCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ListCommand.java index 9a43293f..00952077 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ListCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/ListCommand.java @@ -18,7 +18,7 @@ import org.leavesmc.leaves.bot.BotList; import org.leavesmc.leaves.bot.ServerBot; import org.leavesmc.leaves.neo_command.ArgumentNode; import org.leavesmc.leaves.neo_command.CommandContext; -import org.leavesmc.leaves.neo_command.LiteralNode; +import org.leavesmc.leaves.neo_command.bot.BotSubcommand; import java.util.List; import java.util.Objects; @@ -31,7 +31,7 @@ import static net.kyori.adventure.text.format.NamedTextColor.*; import static net.minecraft.commands.arguments.DimensionArgument.getDimension; -public class ListCommand extends LiteralNode { +public class ListCommand extends BotSubcommand { public ListCommand() { super("list"); diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/LoadCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/LoadCommand.java new file mode 100644 index 00000000..aebc4efa --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/LoadCommand.java @@ -0,0 +1,80 @@ +package org.leavesmc.leaves.neo_command.bot.subcommands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.commands.CommandSourceStack; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.bot.BotList; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.neo_command.ArgumentNode; +import org.leavesmc.leaves.neo_command.CommandContext; +import org.leavesmc.leaves.neo_command.bot.BotSubcommand; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import static io.papermc.paper.adventure.PaperAdventure.asAdventure; +import static net.kyori.adventure.text.Component.join; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.JoinConfiguration.spaces; + +public class LoadCommand extends BotSubcommand { + + public LoadCommand() { + super("load"); + children(BotNameArgument::new); + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return LeavesConfig.modify.fakeplayer.canManualSaveAndLoad && super.requires(source); + } + + private static class BotNameArgument extends ArgumentNode { + + public BotNameArgument() { + super("bot_name", StringArgumentType.word()); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + String botName = context.getArgument(BotNameArgument.class); + BotList botList = BotList.INSTANCE; + CommandSender sender = context.getSender(); + if (!botList.getSavedBotList().contains(botName)) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().create(); + } + + ServerBot bot = botList.loadNewBot(botName); + if (bot == null) { + sender.sendMessage(text("Failed to load bot, please check log", NamedTextColor.RED)); + return false; + } + sender.sendMessage(join( + spaces(), + text("Successfully loaded bot", NamedTextColor.GRAY), + asAdventure(bot.getDisplayName()) + )); + + return true; + } + + @Override + protected CompletableFuture getSuggestions(CommandContext context, @NotNull SuggestionsBuilder builder) { + BotList botList = BotList.INSTANCE; + Set bots = botList.getSavedBotList().keySet(); + if (bots.isEmpty()) { + return builder + .suggest("", net.minecraft.network.chat.Component.literal("There are no bots saved before, save one first.")) + .buildFuture(); + } + bots.forEach(builder::suggest); + return builder.buildFuture(); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/SaveCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/SaveCommand.java new file mode 100644 index 00000000..f3a32bd8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/bot/subcommands/SaveCommand.java @@ -0,0 +1,57 @@ +package org.leavesmc.leaves.neo_command.bot.subcommands; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.commands.CommandSourceStack; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.bot.BotList; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.event.bot.BotRemoveEvent; +import org.leavesmc.leaves.neo_command.CommandContext; +import org.leavesmc.leaves.neo_command.CustomArgumentNode; +import org.leavesmc.leaves.neo_command.bot.BotSubcommand; + +import static io.papermc.paper.adventure.PaperAdventure.asAdventure; +import static net.kyori.adventure.text.Component.join; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.JoinConfiguration.spaces; + +public class SaveCommand extends BotSubcommand { + + public SaveCommand() { + super("save"); + children(BotArgument::new); + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return LeavesConfig.modify.fakeplayer.canManualSaveAndLoad && super.requires(source); + } + + private static class BotArgument extends CustomArgumentNode { + + public BotArgument() { + super("bot", new org.leavesmc.leaves.neo_command.bot.BotArgument()); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + ServerBot bot = context.getCustomArgument(BotArgument.class); + CommandSender sender = context.getSender(); + BotList botList = BotList.INSTANCE; + + if (!botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, true)) { + sender.sendMessage(text("Failed to save bot, please check log", NamedTextColor.RED)); + return false; + } + sender.sendMessage(join(spaces(), + text("Successfully saved bot", NamedTextColor.GRAY), + asAdventure(bot.getDisplayName()), + text("as " + bot.createState.realName(), NamedTextColor.GRAY) + )); + return true; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/ArgumentSuggestions.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/ArgumentSuggestions.java deleted file mode 100644 index 30e6daf4..00000000 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/ArgumentSuggestions.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.leavesmc.leaves.neo_command.leaves; - -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.leavesmc.leaves.neo_command.WrappedArgument; - -import java.util.List; - -public class ArgumentSuggestions { - @Contract(pure = true) - public static WrappedArgument.@NotNull SuggestionApplier strings(String... values) { - return (context, builder) -> { - for (String s : values) { - builder.suggest(s); - } - }; - } - - @Contract(pure = true) - public static WrappedArgument.@NotNull SuggestionApplier strings(List values) { - return (context, builder) -> { - for (String s : values) { - builder.suggest(s); - } - }; - } -} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/LeavesCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/LeavesCommand.java index 145d39e3..8da87769 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/LeavesCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/LeavesCommand.java @@ -1,26 +1,41 @@ package org.leavesmc.leaves.neo_command.leaves; +import com.mojang.brigadier.builder.ArgumentBuilder; import net.minecraft.commands.CommandSourceStack; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.neo_command.CommandNode; +import org.leavesmc.leaves.neo_command.CommandUtils; import org.leavesmc.leaves.neo_command.LiteralNode; import org.leavesmc.leaves.neo_command.leaves.subcommands.ConfigCommand; +import java.util.ArrayList; +import java.util.List; + public class LeavesCommand extends LiteralNode { + public static final LeavesCommand INSTANCE = new LeavesCommand(); private static final String PERM_BASE = "bukkit.command.leaves"; - public LeavesCommand() { + private LeavesCommand() { super("leaves_new"); children(ConfigCommand::new); } @Override - protected boolean requires(@NotNull CommandSourceStack source) { - return source.getSender().hasPermission(PERM_BASE); + protected ArgumentBuilder compile() { + List permissions = new ArrayList<>(); + permissions.add(PERM_BASE); + permissions.addAll(this.children.stream().map(CommandNode::getName).toList()); + CommandUtils.registerPermissions(permissions); + return super.compile(); } - public static boolean hasPermission(@NotNull CommandSourceStack source, String subcommand) { - CommandSender sender = source.getSender(); + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return children.stream().anyMatch(child -> child.requires(source)); + } + + public static boolean hasPermission(@NotNull CommandSender sender, String subcommand) { return sender.hasPermission(PERM_BASE) || sender.hasPermission(PERM_BASE + "." + subcommand); } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/subcommands/ConfigCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/subcommands/ConfigCommand.java index 5ac52e9d..ad547038 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/subcommands/ConfigCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/neo_command/leaves/subcommands/ConfigCommand.java @@ -21,6 +21,7 @@ import static net.kyori.adventure.text.Component.join; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.JoinConfiguration.spaces; import static net.kyori.adventure.text.format.NamedTextColor.*; +import static org.leavesmc.leaves.neo_command.CommandUtils.getListClosestMatchingLast; public class ConfigCommand extends LiteralNode { @@ -30,8 +31,8 @@ public class ConfigCommand extends LiteralNode { } @Override - protected boolean requires(@NotNull CommandSourceStack source) { - return LeavesCommand.hasPermission(source, "config"); + public boolean requires(@NotNull CommandSourceStack source) { + return LeavesCommand.hasPermission(source.getSender(), "config"); } private static class PathArgument extends ArgumentNode { @@ -46,13 +47,10 @@ public class ConfigCommand extends LiteralNode { String path = context.getArgumentOrDefault(PathArgument.class, ""); int dotIndex = path.lastIndexOf("."); builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2); - LeavesCommandUtil.getListClosestMatchingLast( - context.getSender(), + getListClosestMatchingLast( path.substring(dotIndex + 1), - GlobalConfigManager.getVerifiedConfigSubPaths(path), - "bukkit.command.leaves" - ) - .forEach(builder::suggest); + GlobalConfigManager.getVerifiedConfigSubPaths(path) + ).forEach(builder::suggest); return builder.buildFuture(); }