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 extends ArgumentProvider>> parser();
+ String[] parserProperties() default {};
+
+ @NoArgsConstructor
+ abstract class ArgumentProvider {
+
+ public abstract ArgumentElement, S> provide(@NotNull String name);
+
+ }
+
+ class StringArg extends ArgumentProvider {
+ @Override
+ public ArgumentElement, String> provide(@NotNull String name) {
+ return BaseCommand.string(name);
+ }
+ }
+
+ class WordArg extends ArgumentProvider {
+ @Override
+ public ArgumentElement, String> provide(@NotNull String name) {
+ return BaseCommand.word(name);
+ }
+ }
+
+ class BooleanArg extends ArgumentProvider {
+ @Override
+ public ArgumentElement, Boolean> provide(@NotNull String name) {
+ return BaseCommand.bool(name);
+ }
+ }
+
+ class GreedyStringArg extends ArgumentProvider {
+ @Override
+ public ArgumentElement, String> provide(@NotNull String name) {
+ return BaseCommand.greedyString(name);
+ }
+ }
+
+ class IntegerArg extends ArgumentProvider {
+ @Override
+ public ArgumentElement, Integer> 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, Integer> 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, Float> 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, Float> 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, Double> 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, Double> 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, NamedTextColor> 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;