From 8c62c77bf395d97a41bcd8a105f23dcf5987dc93 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 2 Jul 2024 23:03:16 +0100 Subject: [PATCH] feat: add annotation-driven command system (#12) --- README.md | 84 +++++++-- .../net/william278/uniform/BaseCommand.java | 15 ++ .../java/net/william278/uniform/Command.java | 134 ++++++++++++- .../william278/uniform/CommandExecutor.java | 43 +++++ .../net/william278/uniform/Permission.java | 10 + .../java/net/william278/uniform/Uniform.java | 9 +- .../uniform/annotations/Argument.java | 177 ++++++++++++++++++ .../annotations/CommandDescription.java | 33 ++++ .../uniform/annotations/CommandNode.java | 38 ++++ .../uniform/annotations/PermissionNode.java | 38 ++++ .../uniform/annotations/Syntax.java | 33 ++++ example-plugin/build.gradle | 4 +- .../william278/uniform/AnnotatedCommand.java | 96 ++++++++++ ...ampleCommand.java => ExtendedCommand.java} | 8 +- .../william278/uniform/UniformExample.java | 2 +- gradle.properties | 2 +- .../uniform/sponge/SpongeCommand.java | 1 - 17 files changed, 694 insertions(+), 33 deletions(-) create mode 100644 common/src/main/java/net/william278/uniform/annotations/Argument.java create mode 100644 common/src/main/java/net/william278/uniform/annotations/CommandDescription.java create mode 100644 common/src/main/java/net/william278/uniform/annotations/CommandNode.java create mode 100644 common/src/main/java/net/william278/uniform/annotations/PermissionNode.java create mode 100644 common/src/main/java/net/william278/uniform/annotations/Syntax.java create mode 100644 example-plugin/src/main/java/net/william278/uniform/AnnotatedCommand.java rename example-plugin/src/main/java/net/william278/uniform/{ExampleCommand.java => ExtendedCommand.java} (93%) diff --git a/README.md b/README.md index 7b87d65..a0a1c89 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Versions are available on maven in the format `net.william278.uniform:ARTIFACT:VERSION`. See below for a table of supported platforms. -Note that Uniform versions omit the `v` prefix. Fabric versions are suffixed with the target Minecraft version (e.g. `1.1.11+1.21`) and also require Fabric API installed on the server. Sponge versions are suffixed with the target Sponge API version (e.g. `1.1.11+11`). +Note that Uniform versions omit the `v` prefix. Fabric versions are suffixed with the target Minecraft version (e.g. `1.2+1.21`) and also require Fabric API installed on the server. Sponge versions are suffixed with the target Sponge API version (e.g. `1.2+11`). @@ -89,7 +89,7 @@ Note that Uniform versions omit the `v` prefix. Fabric versions are suffixed wit
-Example: To target Uniform on Bukkit, the artifact is `net.william278.uniform:uniform-bukkit:1.1.11` (check that this version is up-to-date – make sure you target the latest available!). +Example: To target Uniform on Bukkit, the artifact is `net.william278.uniform:uniform-bukkit:1.2` (check that this version is up-to-date – make sure you target the latest available!). ## Setup Uniform is available [on Maven](https://repo.william278.net/#/releases/net/william278/uniform/). You can browse the Javadocs [here](https://repo.william278.net/javadoc/releases/net/william278/uniform/latest). @@ -104,7 +104,7 @@ repositories { } ``` -Then, add the dependency itself. Replace `VERSION` with the latest release version. (e.g., `1.1.11`) and `PLATFORM` with the platform you are targeting (e.g., `paper`). If you want to target pre-release "snapshot" versions (not recommended), you should use the `/snapshots` repository instead. +Then, add the dependency itself. Replace `VERSION` with the latest release version. (e.g., `1.2`) and `PLATFORM` with the platform you are targeting (e.g., `paper`). If you want to target pre-release "snapshot" versions (not recommended), you should use the `/snapshots` repository instead. ```groovy dependencies { @@ -120,28 +120,53 @@ Uniform lets you create commands either natively per-platform, or cross-platform Check `example-plugin` for a full example of a cross-platform command being registered on Paper. -### Platform-specific commands -Extend the platform-specific `PlatformCommand` class and add your Brigadier syntax. +### Cross-platform commands +Cross-platform commands can be created by registering `Command` objects; you can create these from `@CommandNode` annotated objects, or by extending `Command` and providing these yourself. + +#### Using annotations +You can use the `@CommandNode` annotations to easily create cross-platform Brigadier commands (since: v1.2). This is the recommended way to create commands. ```java -public class ExampleCommand extends PaperCommand { - public ExampleCommand() { - super("example", "platform-specific"); - command.setDefaultExecutor((context) -> { - context.getSource().getBukkitSender().sendMessage("Hello, world!"); - }); - addSyntax((context) -> { - context.getSource().getBukkitSender().sendMessage("Woah!!!!"); - String arg = context.getArgument("message", String.class); - context.getSource().getBukkitSender() - .sendMessage(MiniMessage.miniMessage().deserialize(arg)); - }, stringArg("message")); +@CommandNode( + value = "helloworld", + aliases = {"hello", "hi"}, + description = "A simple hello world command", + permission = @PermissionNode( + value = "example.command.helloworld", + defaultValue = Permission.Default.TRUE + ) +) +public class AnnotatedCommand { + + @Syntax + public void execute(CommandUser user) { + user.getAudience().sendMessage(Component.text("Hello, world!")); } + + @Syntax + public void pongMessage( + CommandUser user, + @Argument(name = "message", parser = Argument.StringArg.class) String message + ) { + user.getAudience().sendMessage(Component.text("Hello, " + message, NamedTextColor.GREEN)); + } + + @CommandNode( + value = "subcommand", + aliases = {"sub", "hi"} + ) + static class SubCommand { + @Syntax + public void execute(CommandUser user) { + user.getAudience().sendMessage(Component.text("Subcommand executed!")); + } + } + } ``` -### Cross-platform commands -Target `uniform-common` and extend the `Command` class. You'll want to use `BaseCommand#getUser` to get a platform-agnostic User from which you can acquire the adventure `Audience` to send messages to. +#### By extending the Command class. +You can also extend the `Command` class to create a Command object you can register. You'll want to use `BaseCommand#getUser` to get a platform-agnostic User from which you can acquire the adventure `Audience` to send messages to. ```java public class ExampleCrossPlatCommand extends Command { @@ -178,6 +203,27 @@ public class ExampleCrossPlatCommand extends Command { } ``` +### Platform-specific commands +If you need platform-specific features, extend the platform-specific `PlatformCommand` class and add your Brigadier syntax. + +```java +public class ExampleCommand extends PaperCommand { + public ExampleCommand() { + super("example", "platform-specific"); + command.setDefaultExecutor((context) -> { + context.getSource().getBukkitSender().sendMessage("Hello, world!"); + }); + addSyntax((context) -> { + context.getSource().getBukkitSender().sendMessage("Woah!!!!"); + String arg = context.getArgument("message", String.class); + context.getSource().getBukkitSender() + .sendMessage(MiniMessage.miniMessage().deserialize(arg)); + }, stringArg("message")); + } +} +``` + + ### Registering Then, register the command with the platform-specific Uniform instance (e.g. `FabricUniform.getInstance()`, `PaperUniform.getInstance()`, etc...) diff --git a/common/src/main/java/net/william278/uniform/BaseCommand.java b/common/src/main/java/net/william278/uniform/BaseCommand.java index 14ceae5..5ad9026 100644 --- a/common/src/main/java/net/william278/uniform/BaseCommand.java +++ b/common/src/main/java/net/william278/uniform/BaseCommand.java @@ -199,6 +199,21 @@ public abstract class BaseCommand { return arg(name, FloatArgumentType.floatArg(min, max)); } + @NotNull + public static ArgumentElement doubleNum(@NotNull String name) { + return arg(name, DoubleArgumentType.doubleArg()); + } + + @NotNull + public static ArgumentElement doubleNum(@NotNull String name, double min) { + return arg(name, DoubleArgumentType.doubleArg(min)); + } + + @NotNull + public static ArgumentElement doubleNum(@NotNull String name, double min, double max) { + return arg(name, DoubleArgumentType.doubleArg(min, max)); + } + @NotNull public static ArgumentElement bool(@NotNull String name) { return arg(name, BoolArgumentType.bool()); diff --git a/common/src/main/java/net/william278/uniform/Command.java b/common/src/main/java/net/william278/uniform/Command.java index 3cf9b5d..15b1e6f 100644 --- a/common/src/main/java/net/william278/uniform/Command.java +++ b/common/src/main/java/net/william278/uniform/Command.java @@ -21,16 +21,32 @@ package net.william278.uniform; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import net.william278.uniform.annotations.Argument; +import net.william278.uniform.annotations.CommandDescription; +import net.william278.uniform.annotations.CommandNode; +import net.william278.uniform.annotations.Syntax; +import net.william278.uniform.element.ArgumentElement; +import net.william278.uniform.element.CommandElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; + +import static net.william278.uniform.CommandExecutor.methodToExecutor; @Getter @Setter -@RequiredArgsConstructor @AllArgsConstructor public abstract class Command implements CommandProvider { @@ -44,13 +60,123 @@ public abstract class Command implements CommandProvider { return Optional.ofNullable(permission); } - public record SubCommand(@NotNull String name, @NotNull List aliases, @Nullable Permission permission, @NotNull CommandProvider provider) { + Command(@NotNull String name) { + this.name = name; + } + + Command(@Nullable CommandNode node) { + if (node == null) { + throw new IllegalArgumentException("@CommandNode annotation is required on annotated command/sub-commands"); + } + this.name = node.value(); + this.aliases = List.of(node.aliases()); + this.description = node.description(); + Permission.annotated(node.permission()).ifPresent(this::setPermission); + } + + static class AnnotatedCommand extends Command { + + private final Object annotated; + + AnnotatedCommand(@NotNull Object annotated) { + super(annotated.getClass().getAnnotation(CommandNode.class)); + this.annotated = annotated; + this.setDescriptionFromAnnotation(); + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public void provide(@NotNull BaseCommand cmd) { + // Add syntaxes + for (Method method : annotated.getClass().getDeclaredMethods()) { + method.setAccessible(true); + final Syntax syntax = method.getAnnotation(Syntax.class); + if (syntax == null) { + continue; + } + + // Default executor + final CommandElement[] args = getMethodArguments(method); + if (args.length == 0) { + cmd.setDefaultExecutor(methodToExecutor(method, annotated, cmd)); + continue; + } + + // Conditional & unconditional syntax + final Optional perm = Permission.annotated(syntax.permission()).map(p -> p.toPredicate(cmd)); + final CommandExecutor executor = methodToExecutor(method, annotated, cmd); + if (perm.isPresent()) { + cmd.addConditionalSyntax(perm.get(), executor, args); + continue; + } + cmd.addSyntax(executor, args); + } + + // Add subcommands + for (Class subClass : annotated.getClass().getDeclaredClasses()) { + if (subClass.getAnnotation(CommandNode.class) == null) { + continue; + } + try { + cmd.addSubCommand(new AnnotatedCommand( + subClass.getDeclaredConstructor().newInstance() + )); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new IllegalArgumentException( + "Failed to create sub-command instance (does it have a zero-arg constructor?)", e + ); + } + } + } + + @NotNull + private static CommandElement[] getMethodArguments(@NotNull Method method) { + try { + final List> elements = new ArrayList<>(); + for (Parameter param : method.getParameters()) { + final Argument arg = param.getAnnotation(Argument.class); + if (arg == null) { + continue; + } + + // Pass parser properties if needed + if (arg.parserProperties().length == 0) { + elements.add(arg.parser().getDeclaredConstructor() + .newInstance().provide(arg.name())); + continue; + } + elements.add(arg.parser().getDeclaredConstructor(String[].class) + .newInstance((Object) arg.parserProperties()).provide(arg.name())); + } + return elements.toArray(new ArgumentElement[0]); + } catch (Throwable e) { + throw new IllegalArgumentException("Failed to create argument elements from method parameters", e); + } + } + + private void setDescriptionFromAnnotation() { + Arrays.stream(annotated.getClass().getFields()) + .filter(f -> f.getAnnotation(CommandDescription.class) != null) + .findFirst().ifPresent(f -> { + try { + f.setAccessible(true); + this.setDescription((String) f.get(annotated)); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Failed to set command description from field", e); + } + }); + } + } + + public record SubCommand(@NotNull String name, @NotNull List aliases, @Nullable Permission permission, + @NotNull CommandProvider provider) { public SubCommand(@NotNull String name, @NotNull List aliases, @NotNull CommandProvider provider) { this(name, aliases, null, provider); } public SubCommand(@NotNull String name, @NotNull CommandProvider provider) { - this(name, List.of(),null, provider); + this(name, List.of(), null, provider); } public SubCommand(@NotNull String name, @Nullable Permission permission, @NotNull CommandProvider provider) { diff --git a/common/src/main/java/net/william278/uniform/CommandExecutor.java b/common/src/main/java/net/william278/uniform/CommandExecutor.java index 3e4d0bf..706861e 100644 --- a/common/src/main/java/net/william278/uniform/CommandExecutor.java +++ b/common/src/main/java/net/william278/uniform/CommandExecutor.java @@ -22,10 +22,53 @@ package net.william278.uniform; import com.mojang.brigadier.context.CommandContext; +import net.william278.uniform.annotations.Argument; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; public interface CommandExecutor { void execute(@NotNull CommandContext context); + @NotNull + static CommandExecutor methodToExecutor(@NotNull Method method, @NotNull Object instance, + @NotNull BaseCommand cmd) { + return (context) -> { + try { + method.invoke(instance, injectParams(method, context, cmd)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Failed to invoke command executor from annotated method", e); + } + }; + } + + @Nullable + private static Object @NotNull [] injectParams(@NotNull Method method, @NotNull CommandContext context, + @NotNull BaseCommand cmd) { + final Object[] params = new Object[method.getParameterCount()]; + for (int i = 0; i < method.getParameterCount(); i++) { + final Parameter param = method.getParameters()[i]; + final Class type = param.getType(); + final Argument arg = param.getAnnotation(Argument.class); + if (arg != null) { + params[i] = context.getArgument(arg.name(), type); + continue; + } + if (type.isAssignableFrom(CommandUser.class)) { + params[i] = cmd.getUser(context.getSource()); + continue; + } + if (type.isAssignableFrom(context.getClass())) { + params[i] = context; + continue; + } + params[i] = null; + } + return params; + } + } diff --git a/common/src/main/java/net/william278/uniform/Permission.java b/common/src/main/java/net/william278/uniform/Permission.java index ca2df73..6c9f8df 100644 --- a/common/src/main/java/net/william278/uniform/Permission.java +++ b/common/src/main/java/net/william278/uniform/Permission.java @@ -21,8 +21,10 @@ package net.william278.uniform; +import net.william278.uniform.annotations.PermissionNode; import org.jetbrains.annotations.NotNull; +import java.util.Optional; import java.util.function.Predicate; @SuppressWarnings("unused") @@ -47,6 +49,14 @@ public record Permission(@NotNull String node, @NotNull Default defaultValue) { return new Permission(node, Default.FALSE); } + @NotNull + static Optional annotated(@NotNull PermissionNode annotation) { + if (annotation.value().isBlank()) { + return Optional.empty(); + } + return Optional.of(new Permission(annotation.value(), annotation.defaultValue())); + } + public enum Default { IF_OP, TRUE, diff --git a/common/src/main/java/net/william278/uniform/Uniform.java b/common/src/main/java/net/william278/uniform/Uniform.java index 8300975..9f01df2 100644 --- a/common/src/main/java/net/william278/uniform/Uniform.java +++ b/common/src/main/java/net/william278/uniform/Uniform.java @@ -23,11 +23,18 @@ package net.william278.uniform; import org.jetbrains.annotations.NotNull; +import java.util.Arrays; import java.util.function.Function; public interface Uniform { - void register(Command... commands); + void register(@NotNull Command... commands); + + default void register(@NotNull Object... annotated) { + register(Arrays.stream(annotated) + .map(c -> c instanceof Command cmd ? cmd : new Command.AnnotatedCommand(c)) + .toArray(Command[]::new)); + } @SuppressWarnings("unchecked") > void register(T... commands); diff --git a/common/src/main/java/net/william278/uniform/annotations/Argument.java b/common/src/main/java/net/william278/uniform/annotations/Argument.java new file mode 100644 index 0000000..d85cff2 --- /dev/null +++ b/common/src/main/java/net/william278/uniform/annotations/Argument.java @@ -0,0 +1,177 @@ +/* + * This file is part of Uniform, licensed under the GNU General Public License v3.0. + * + * Copyright (c) Tofaa2 + * Copyright (c) William278 + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.william278.uniform.annotations; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import net.william278.uniform.BaseCommand; +import net.william278.uniform.element.ArgumentElement; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Argument { + + String name(); + Class> parser(); + String[] parserProperties() default {}; + + @NoArgsConstructor + abstract class ArgumentProvider { + + public abstract ArgumentElement provide(@NotNull String name); + + } + + class StringArg extends ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return BaseCommand.string(name); + } + } + + class WordArg extends ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return BaseCommand.word(name); + } + } + + class BooleanArg extends ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return BaseCommand.bool(name); + } + } + + class GreedyStringArg extends ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return BaseCommand.greedyString(name); + } + } + + class IntegerArg extends ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return BaseCommand.intNum(name); + } + } + + @AllArgsConstructor + class BoundedIntegerArg extends ArgumentProvider { + private final int min; + private final Integer max; + + public BoundedIntegerArg(@NotNull String[] properties) { + if (properties.length == 0) { + throw new IllegalArgumentException("BoundedIntegerArg requires at least one property (min, max)"); + } + this.min = Integer.parseInt(properties[0]); + if (properties.length == 1) { + this.max = null; + return; + } + this.max = Integer.parseInt(properties[1]); + } + + @Override + public ArgumentElement provide(@NotNull String name) { + if (max == null) { + return BaseCommand.intNum(name, min); + } + return BaseCommand.intNum(name, min, max); + } + } + + class FloatArg extends ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return BaseCommand.floatNum(name); + } + } + + @AllArgsConstructor + class BoundedFloatArg extends ArgumentProvider { + private final float min; + private final Float max; + + public BoundedFloatArg(@NotNull String[] properties) { + if (properties.length == 0) { + throw new IllegalArgumentException("BoundedFloatArg requires at least one property (min, max)"); + } + this.min = Float.parseFloat(properties[0]); + if (properties.length == 1) { + this.max = null; + return; + } + this.max = Float.parseFloat(properties[1]); + } + + @Override + public ArgumentElement provide(@NotNull String name) { + if (max == null) { + return BaseCommand.floatNum(name, min); + } + return BaseCommand.floatNum(name, min, max); + } + } + + class DoubleArg extends ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return BaseCommand.doubleNum(name); + } + } + + @AllArgsConstructor + class BoundedDoubleArg extends ArgumentProvider { + private final double min; + private final Double max; + + public BoundedDoubleArg(@NotNull String[] properties) { + if (properties.length == 0) { + throw new IllegalArgumentException("BoundedDoubleArg requires at least one property (min, max)"); + } + this.min = Double.parseDouble(properties[0]); + if (properties.length == 1) { + this.max = null; + return; + } + this.max = Double.parseDouble(properties[1]); + } + + @Override + public ArgumentElement provide(@NotNull String name) { + if (max == null) { + return BaseCommand.doubleNum(name, min); + } + return BaseCommand.doubleNum(name, min, max); + } + } + +} diff --git a/common/src/main/java/net/william278/uniform/annotations/CommandDescription.java b/common/src/main/java/net/william278/uniform/annotations/CommandDescription.java new file mode 100644 index 0000000..3f79bee --- /dev/null +++ b/common/src/main/java/net/william278/uniform/annotations/CommandDescription.java @@ -0,0 +1,33 @@ +/* + * This file is part of Uniform, licensed under the GNU General Public License v3.0. + * + * Copyright (c) Tofaa2 + * Copyright (c) William278 + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.william278.uniform.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandDescription { + +} diff --git a/common/src/main/java/net/william278/uniform/annotations/CommandNode.java b/common/src/main/java/net/william278/uniform/annotations/CommandNode.java new file mode 100644 index 0000000..33e4516 --- /dev/null +++ b/common/src/main/java/net/william278/uniform/annotations/CommandNode.java @@ -0,0 +1,38 @@ +/* + * This file is part of Uniform, licensed under the GNU General Public License v3.0. + * + * Copyright (c) Tofaa2 + * Copyright (c) William278 + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.william278.uniform.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandNode { + + String value(); + String[] aliases() default {}; + String description() default ""; + PermissionNode permission() default @PermissionNode(""); + +} \ No newline at end of file diff --git a/common/src/main/java/net/william278/uniform/annotations/PermissionNode.java b/common/src/main/java/net/william278/uniform/annotations/PermissionNode.java new file mode 100644 index 0000000..459fe64 --- /dev/null +++ b/common/src/main/java/net/william278/uniform/annotations/PermissionNode.java @@ -0,0 +1,38 @@ +/* + * This file is part of Uniform, licensed under the GNU General Public License v3.0. + * + * Copyright (c) Tofaa2 + * Copyright (c) William278 + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.william278.uniform.annotations; + +import net.william278.uniform.Permission; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PermissionNode { + + String value(); + Permission.Default defaultValue() default Permission.Default.FALSE; + +} diff --git a/common/src/main/java/net/william278/uniform/annotations/Syntax.java b/common/src/main/java/net/william278/uniform/annotations/Syntax.java new file mode 100644 index 0000000..ed1b92f --- /dev/null +++ b/common/src/main/java/net/william278/uniform/annotations/Syntax.java @@ -0,0 +1,33 @@ +/* + * This file is part of Uniform, licensed under the GNU General Public License v3.0. + * + * Copyright (c) Tofaa2 + * Copyright (c) William278 + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.william278.uniform.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Syntax { + PermissionNode permission() default @PermissionNode(""); +} diff --git a/example-plugin/build.gradle b/example-plugin/build.gradle index dca4d45..91ff318 100644 --- a/example-plugin/build.gradle +++ b/example-plugin/build.gradle @@ -11,10 +11,10 @@ dependencies { tasks { runServer { - minecraftVersion("1.20.4") + minecraftVersion("1.21") downloadPlugins { - url('https://download.luckperms.net/1549/bukkit/loader/LuckPerms-Bukkit-5.4.134.jar') +// url('https://download.luckperms.net/1549/bukkit/loader/LuckPerms-Bukkit-5.4.134.jar') } } } \ No newline at end of file diff --git a/example-plugin/src/main/java/net/william278/uniform/AnnotatedCommand.java b/example-plugin/src/main/java/net/william278/uniform/AnnotatedCommand.java new file mode 100644 index 0000000..0d4f9dc --- /dev/null +++ b/example-plugin/src/main/java/net/william278/uniform/AnnotatedCommand.java @@ -0,0 +1,96 @@ +/* + * This file is part of Uniform, licensed under the GNU General Public License v3.0. + * + * Copyright (c) Tofaa2 + * Copyright (c) William278 + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.william278.uniform; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.william278.uniform.annotations.*; +import net.william278.uniform.element.ArgumentElement; +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; + +@CommandNode( + value = "annotated", + permission = @PermissionNode(value = "uniform.annotated", defaultValue = Permission.Default.TRUE) +) +public class AnnotatedCommand { + + @CommandDescription // Can use @CommandDescription instead of @CommandNode field for localization support here + public final String DESCRIPTION = "An example Uniform annotated command"; + + public AnnotatedCommand() { + } + + @Syntax + void defaultExecutor(CommandUser user) { + user.getAudience().sendMessage(Component.text("No arguments passed!")); + } + + @CommandNode("ping") + static class Ping { + + public Ping() { + } + + @Syntax + public void pong(CommandUser user) { + user.getAudience().sendMessage(Component.text("Pong!")); + } + + @Syntax + public void pongMessage( + CommandUser user, + @Argument(name = "message", parser = Argument.StringArg.class) String message + ) { + user.getAudience().sendMessage(Component.text("Pong! " + message, NamedTextColor.GREEN)); + } + + @Syntax + public void pongMessageWithColor( + CommandUser user, + @Argument(name = "message", parser = Argument.StringArg.class) String message, + @Argument(name = "color", parser = ColorArg.class) NamedTextColor color + ) { + user.getAudience().sendMessage(Component.text("Colored Pong! " + message, color)); + } + + public static class ColorArg extends Argument.ArgumentProvider { + @Override + public ArgumentElement provide(@NotNull String name) { + return new ArgumentElement<>(name, (r) -> { + final NamedTextColor color = NamedTextColor.NAMES.value(r.readString().toLowerCase(Locale.ENGLISH)); + if (color == null) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().create(); + } + return color; + }, (context, builder) -> { + NamedTextColor.NAMES.keys().forEach(color -> builder.suggest(color.toLowerCase(Locale.ENGLISH))); + return builder.buildFuture(); + }); + } + } + + } + +} diff --git a/example-plugin/src/main/java/net/william278/uniform/ExampleCommand.java b/example-plugin/src/main/java/net/william278/uniform/ExtendedCommand.java similarity index 93% rename from example-plugin/src/main/java/net/william278/uniform/ExampleCommand.java rename to example-plugin/src/main/java/net/william278/uniform/ExtendedCommand.java index fa80a75..c4632e8 100644 --- a/example-plugin/src/main/java/net/william278/uniform/ExampleCommand.java +++ b/example-plugin/src/main/java/net/william278/uniform/ExtendedCommand.java @@ -32,9 +32,10 @@ import java.util.Locale; import static net.william278.uniform.BaseCommand.greedyString; -public class ExampleCommand extends Command { +// Example using the API to extend "Command" +public class ExtendedCommand extends Command { - public ExampleCommand() { + public ExtendedCommand() { super("example"); setDescription("An example command for Uniform"); setAliases(List.of("helloworld")); @@ -62,7 +63,7 @@ public class ExampleCommand extends Command { }, exampleCustomArg())); } - private static ArgumentElement exampleCustomArg() { + private static ArgumentElement exampleCustomArg() { return new ArgumentElement<>("flavor", reader -> { final String flavor = reader.readString(); try { @@ -76,7 +77,6 @@ public class ExampleCommand extends Command { ); return builder.buildFuture(); }); - } enum IceCreamFlavor { diff --git a/example-plugin/src/main/java/net/william278/uniform/UniformExample.java b/example-plugin/src/main/java/net/william278/uniform/UniformExample.java index 269a43d..95d9367 100644 --- a/example-plugin/src/main/java/net/william278/uniform/UniformExample.java +++ b/example-plugin/src/main/java/net/william278/uniform/UniformExample.java @@ -30,7 +30,7 @@ public class UniformExample extends JavaPlugin { @Override public void onEnable() { PaperUniform uniform = PaperUniform.getInstance(this); - uniform.register(new ExampleCommand()); + uniform.register(new ExtendedCommand(), new AnnotatedCommand()); } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3d81d4b..21f3d78 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,6 @@ javaVersion=17 org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true -library_version=1.1.11 +library_version=1.2 library_archive=uniform library_description=Cross-platform wrapper for making Brigadier commands, based on BrigadierWrapper by Tofaa2, itself inspired by emortalmcs command system. \ No newline at end of file diff --git a/sponge-11/src/main/java/net/william278/uniform/sponge/SpongeCommand.java b/sponge-11/src/main/java/net/william278/uniform/sponge/SpongeCommand.java index 54ff176..8881aa2 100644 --- a/sponge-11/src/main/java/net/william278/uniform/sponge/SpongeCommand.java +++ b/sponge-11/src/main/java/net/william278/uniform/sponge/SpongeCommand.java @@ -31,7 +31,6 @@ import net.william278.uniform.Permission; import net.william278.uniform.Uniform; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.spongepowered.api.Game; import org.spongepowered.api.command.Command.Raw; import org.spongepowered.api.command.CommandCause; import org.spongepowered.api.command.CommandCompletion;