diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e96e1439..064ea9075 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew clean test clean build + run: ./gradlew clean test build env: GIT_BRANCH: ${{ github.ref }} GIT_COMMIT: ${{ github.sha }} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/litecommands-publish.gradle.kts b/buildSrc/src/main/kotlin/litecommands-publish.gradle.kts index 1461916df..0f4cd26dd 100644 --- a/buildSrc/src/main/kotlin/litecommands-publish.gradle.kts +++ b/buildSrc/src/main/kotlin/litecommands-publish.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "dev.rollczi" -version = "3.5.1-SNAPSHOT" +version = "3.6.0-SNAPSHOT" java { withSourcesJar() diff --git a/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/ExamplePlugin.java b/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/ExamplePlugin.java index b5c8387bd..117e75f46 100644 --- a/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/ExamplePlugin.java +++ b/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/ExamplePlugin.java @@ -22,6 +22,7 @@ import dev.rollczi.litecommands.LiteCommands; import dev.rollczi.litecommands.join.JoinArgument; import dev.rollczi.litecommands.programmatic.LiteCommand; +import dev.rollczi.litecommands.strict.StrictMode; import dev.rollczi.litecommands.suggestion.SuggestionResult; import dev.rollczi.litecommands.bukkit.LiteCommandsBukkit; import dev.rollczi.litecommands.schematic.SchematicFormat; @@ -98,6 +99,10 @@ public void onEnable() { // Schematic generator is used to generate schematic for command, for example when you run invalid command. .schematicGenerator(SchematicFormat.angleBrackets()) + // strict mode - you can enable/disable strict mode for each command (default is enabled) + // if strict mode is enabled, the command will fail if the user provides too many arguments + .strictMode(StrictMode.ENABLED) + .build(); } diff --git a/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/KickAllCommand.java b/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/KickAllCommand.java index 60c73e75f..a23a41b65 100644 --- a/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/KickAllCommand.java +++ b/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/KickAllCommand.java @@ -2,15 +2,23 @@ import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.execute.ExecuteDefault; import dev.rollczi.litecommands.annotations.permission.Permission; import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @Command(name = "kickall") @Permission("dev.rollczi.kickall") public class KickAllCommand { + @ExecuteDefault + public void commandHelp(@Context CommandSender sender) { + sender.sendMessage("Correct usage: /kickall -all|"); + } + @Execute(name = "-all") public void kickAll() { for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { diff --git a/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/TeleportCommand.java b/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/TeleportCommand.java index c3634458e..41509a601 100644 --- a/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/TeleportCommand.java +++ b/examples/bukkit/src/main/java/dev/rollczi/example/bukkit/command/TeleportCommand.java @@ -15,8 +15,8 @@ public class TeleportCommand { @Execute - public void teleportSelf(@Context Player sender, @Arg Player to) { - sender.teleport(to.getLocation()); + public void teleportSelf(@Context Player sender, @Arg Player target) { + sender.teleport(target.getLocation()); } @Execute @@ -27,8 +27,8 @@ public void teleportSelfToPosition(@Context Player sender, @Arg Location locatio @Execute @Permission("dev.rollczi.teleport.other") - public void teleportOther(@Arg("target") Player target, @Arg("to") Player to) { - target.teleport(to.getLocation()); + public void teleportOther(@Arg("other") Player other, @Arg("target") Player target) { + other.teleport(target.getLocation()); } } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationInvoker.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationInvoker.java index 7421e6bd5..1678868b7 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationInvoker.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationInvoker.java @@ -7,27 +7,27 @@ public interface AnnotationInvoker { default - AnnotationInvoker on(Class annotationType, AnnotationProcessor.Listener listener) { + AnnotationInvoker on(Class annotationType, AnnotationProcessor.AnyListener listener) { return this; } default - AnnotationInvoker onStructure(Class annotationType, AnnotationProcessor.StructureListener listener) { + AnnotationInvoker onClass(Class annotationType, AnnotationProcessor.ClassListener listener) { return this; } default - AnnotationInvoker onExecutorStructure(Class annotationType, AnnotationProcessor.StructureExecutorListener listener) { + AnnotationInvoker onMethod(Class annotationType, AnnotationProcessor.MethodListener listener) { return this; } default - AnnotationInvoker onRequirement(Class annotationType, AnnotationProcessor.RequirementListener listener) { + AnnotationInvoker onParameter(Class annotationType, AnnotationProcessor.ParameterListener listener) { return this; } default - AnnotationInvoker onRequirementMeta(Class annotationType, AnnotationProcessor.RequirementMetaListener listener) { + AnnotationInvoker onParameterRequirement(Class annotationType, AnnotationProcessor.ParameterRequirementListener listener) { return this; } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessor.java index ea1921b41..e1d2545e1 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessor.java @@ -6,30 +6,32 @@ import dev.rollczi.litecommands.requirement.Requirement; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.Optional; public interface AnnotationProcessor { AnnotationInvoker process(AnnotationInvoker invoker); - interface Listener { + interface AnyListener { void call(A annotation, MetaHolder metaHolder); } - interface StructureListener { - CommandBuilder call(A annotation, CommandBuilder builder); + interface ClassListener { + CommandBuilder call(Class classType, A annotation, CommandBuilder builder); } - interface StructureExecutorListener { - void call(A annotation, CommandBuilder builder, CommandExecutorProvider executorProvider); + interface MethodListener { + void call(Method method, A annotation, CommandBuilder builder, CommandExecutorProvider executorProvider); } - interface RequirementListener { - Optional> call(AnnotationHolder annotationHolder, CommandBuilder builder); + interface ParameterListener { + Optional> call(Parameter parameter, AnnotationHolder annotationHolder, CommandBuilder builder); } - interface RequirementMetaListener { - void call(AnnotationHolder annotationHolder, CommandBuilder builder, Requirement requirement); + interface ParameterRequirementListener { + void call(Parameter parameter, AnnotationHolder annotationHolder, CommandBuilder builder, Requirement requirement); } } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessorService.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessorService.java index 6d4195930..26be14fe0 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessorService.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/AnnotationProcessorService.java @@ -17,6 +17,7 @@ import dev.rollczi.litecommands.annotations.optional.OptionalArgArgumentProcessor; import dev.rollczi.litecommands.annotations.permission.PermissionAnnotationResolver; import dev.rollczi.litecommands.annotations.permission.PermissionsAnnotationResolver; +import dev.rollczi.litecommands.annotations.priority.PriorityAnnotationResolver; import dev.rollczi.litecommands.annotations.quoted.QuotedAnnotationProcessor; import dev.rollczi.litecommands.annotations.shortcut.ShortcutCommandAnnotationProcessor; import dev.rollczi.litecommands.annotations.validator.ValidateAnnotationResolver; @@ -56,6 +57,7 @@ public static AnnotationProcessorService defaultService() { .register(new AsyncAnnotationResolver<>()) .register(new PermissionAnnotationResolver<>()) .register(new PermissionsAnnotationResolver<>()) + .register(new PriorityAnnotationResolver<>()) .register(new ValidateAnnotationResolver<>()) .register(new CooldownAnnotationResolver<>()) // argument meta processors diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ClassInvoker.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ClassInvoker.java index d392dfcae..bc61688f3 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ClassInvoker.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ClassInvoker.java @@ -15,7 +15,7 @@ public ClassInvoker(Class type, CommandBuilder commandBuilder) { } @Override - public AnnotationInvoker on(Class annotationType, AnnotationProcessor.Listener listener) { + public AnnotationInvoker on(Class annotationType, AnnotationProcessor.AnyListener listener) { A annotation = type.getAnnotation(annotationType); if (annotation == null) { @@ -27,14 +27,14 @@ public AnnotationInvoker on(Class annotationTy } @Override - public AnnotationInvoker onStructure(Class annotationType, AnnotationProcessor.StructureListener listener) { + public AnnotationInvoker onClass(Class annotationType, AnnotationProcessor.ClassListener listener) { A annotation = type.getAnnotation(annotationType); if (annotation == null) { return this; } - commandBuilder = listener.call(annotation, commandBuilder); + commandBuilder = listener.call(type, annotation, commandBuilder); return this; } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java index c55a21aa8..55348939e 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java @@ -20,7 +20,7 @@ import java.lang.reflect.Parameter; import java.util.function.Supplier; -class MethodCommandExecutor extends AbstractCommandExecutor { +public class MethodCommandExecutor extends AbstractCommandExecutor { private final Method method; private final Parameter[] parameters; @@ -46,6 +46,18 @@ class MethodCommandExecutor extends AbstractCommandExecutor { this.meta.apply(meta); } + public Object getInstance() { + return instance; + } + + public Method getMethod() { + return method; + } + + public MethodDefinition getDefinition() { + return definition; + } + @Override public CommandExecutorMatchResult match(RequirementsResult results) { Object[] objects = new Object[method.getParameterCount()]; @@ -103,6 +115,10 @@ public CommandExecuteResult get() { } catch (InvocationTargetException exception) { Throwable targetException = exception.getTargetException(); + if (targetException instanceof InvalidUsageException) { //TODO: Use invalid usage handler (when InvalidUsage.Cause is mapped to InvalidUsage) + return CommandExecuteResult.failed(MethodCommandExecutor.this, ((InvalidUsageException) targetException).getErrorResult()); + } + throw new LiteCommandsReflectInvocationException(MethodCommandExecutor.this.method, "Command method threw " + targetException.getClass().getSimpleName(), targetException); } } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodInvoker.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodInvoker.java index 3cd820ffc..026fb9dbb 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodInvoker.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodInvoker.java @@ -38,7 +38,7 @@ public MethodInvoker(AnnotationProcessorService annotationProcessorServi } @Override - public AnnotationInvoker on(Class annotationType, AnnotationProcessor.Listener listener) { + public AnnotationInvoker on(Class annotationType, AnnotationProcessor.AnyListener listener) { A annotation = method.getAnnotation(annotationType); if (annotation == null) { @@ -50,20 +50,20 @@ public AnnotationInvoker on(Class annotationTy } @Override - public AnnotationInvoker onExecutorStructure(Class annotationType, AnnotationProcessor.StructureExecutorListener listener) { + public AnnotationInvoker onMethod(Class annotationType, AnnotationProcessor.MethodListener listener) { A methodAnnotation = method.getAnnotation(annotationType); if (methodAnnotation == null) { return this; } - listener.call(methodAnnotation, commandBuilder, executorProvider); + listener.call(method, methodAnnotation, commandBuilder, executorProvider); isExecutorStructure = true; return this; } @Override - public AnnotationInvoker onRequirement(Class annotationType, AnnotationProcessor.RequirementListener listener) { + public AnnotationInvoker onParameter(Class annotationType, AnnotationProcessor.ParameterListener listener) { for (int index = 0; index < method.getParameterCount(); index++) { Parameter parameter = method.getParameters()[index]; A parameterAnnotation = parameter.getAnnotation(annotationType); @@ -76,7 +76,7 @@ public AnnotationInvoker onRequirement(Class a continue; } - Optional> requirementOptional = listener.call(createHolder(parameterAnnotation, parameter), commandBuilder); + Optional> requirementOptional = listener.call(parameter, createHolder(parameterAnnotation, parameter), commandBuilder); if (requirementOptional.isPresent()) { Requirement requirement = requirementOptional.get(); diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ParameterInvoker.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ParameterInvoker.java index 99669f89a..a0385a22a 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ParameterInvoker.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/ParameterInvoker.java @@ -23,7 +23,7 @@ class ParameterInvoker implements AnnotationInvoker { } @Override - public AnnotationInvoker on(Class annotationType, AnnotationProcessor.Listener listener) { + public AnnotationInvoker on(Class annotationType, AnnotationProcessor.AnyListener listener) { A annotation = parameter.getAnnotation(annotationType); if (annotation == null) { @@ -35,14 +35,14 @@ public AnnotationInvoker on(Class annotationTy } @Override - public AnnotationInvoker onRequirementMeta(Class annotationType, AnnotationProcessor.RequirementMetaListener listener) { + public AnnotationInvoker onParameterRequirement(Class annotationType, AnnotationProcessor.ParameterRequirementListener listener) { A annotation = parameter.getAnnotation(annotationType); if (annotation == null) { return this; } - listener.call(createHolder(annotation, parameter), commandBuilder, requirement); + listener.call(parameter, createHolder(annotation, parameter), commandBuilder, requirement); return this; } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/Command.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/Command.java index 19af38706..ae7df7d60 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/Command.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/Command.java @@ -2,6 +2,7 @@ import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.strict.StrictMode; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -28,4 +29,10 @@ */ String[] aliases() default {}; + /** + * Disables/Enables the strict mode. + * If the strict mode is disabled, the method will be executed even if there are too many arguments. + */ + StrictMode strict() default StrictMode.DEFAULT; + } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/CommandAnnotationProcessor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/CommandAnnotationProcessor.java index 5cc990a20..135aac82c 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/CommandAnnotationProcessor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/CommandAnnotationProcessor.java @@ -2,6 +2,8 @@ import dev.rollczi.litecommands.annotations.AnnotationInvoker; import dev.rollczi.litecommands.annotations.AnnotationProcessor; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.strict.StrictMode; import dev.rollczi.litecommands.util.LiteCommandsUtil; import java.util.Arrays; @@ -10,17 +12,23 @@ public class CommandAnnotationProcessor implements AnnotationProcessor process(AnnotationInvoker invoker) { - return invoker.onStructure(Command.class, (annotation, builder) -> { - boolean isNotEmpty = LiteCommandsUtil.checkConsistent(annotation.name(), annotation.aliases()); + return invoker + .on(Command.class, (annotation, metaHolder) -> { + if (annotation.strict() != StrictMode.DEFAULT) { + metaHolder.meta().put(Meta.STRICT_MODE, annotation.strict()); + } + }) + .onClass(Command.class, (classType, annotation, builder) -> { + boolean isNotEmpty = LiteCommandsUtil.checkConsistent(annotation.name(), annotation.aliases()); - if (isNotEmpty) { - return builder - .routeName(annotation.name()) - .routeAliases(Arrays.asList(annotation.aliases())); - } + if (isNotEmpty) { + return builder + .routeName(annotation.name()) + .routeAliases(Arrays.asList(annotation.aliases())); + } - throw new IllegalArgumentException("Route name cannot be empty"); - }); + throw new IllegalArgumentException("Route name cannot be empty"); + }); } } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommand.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommand.java index d4b35261b..4a40a6307 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommand.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommand.java @@ -1,5 +1,6 @@ package dev.rollczi.litecommands.annotations.command; +import dev.rollczi.litecommands.strict.StrictMode; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -15,4 +16,10 @@ @Retention(RetentionPolicy.RUNTIME) public @interface RootCommand { + /** + * Disables/Enables the strict mode. + * If the strict mode is disabled, the method will be executed even if there are too many arguments. + */ + StrictMode strict() default StrictMode.DEFAULT; + } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommandAnnotationProcessor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommandAnnotationProcessor.java index acbc20f08..e903abcbc 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommandAnnotationProcessor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/command/RootCommandAnnotationProcessor.java @@ -3,13 +3,21 @@ import dev.rollczi.litecommands.annotations.AnnotationInvoker; import dev.rollczi.litecommands.annotations.AnnotationProcessor; import dev.rollczi.litecommands.command.builder.CommandBuilder; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.strict.StrictMode; public class RootCommandAnnotationProcessor implements AnnotationProcessor { @Override public AnnotationInvoker process(AnnotationInvoker invoker) { - return invoker.onStructure(RootCommand.class, (annotation, builder) -> CommandBuilder.createRoot() - .applyMeta(meta -> meta.apply(builder.meta())) - ); + return invoker + .on(RootCommand.class, (annotation, metaHolder) -> { + if (annotation.strict() != StrictMode.DEFAULT) { + metaHolder.meta().put(Meta.STRICT_MODE, annotation.strict()); + } + }) + .onClass(RootCommand.class, (classType, annotation, builder) -> CommandBuilder.createRoot() + .applyMeta(meta -> meta.apply(builder.meta())) + ); } } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/Execute.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/Execute.java index ac344cc62..b981cc7c7 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/Execute.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/Execute.java @@ -2,10 +2,12 @@ import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.strict.StrictMode; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.jetbrains.annotations.ApiStatus; /** * Represents the method that will be executed when the command is called. @@ -27,4 +29,11 @@ */ String[] aliases() default {}; + /** + * Disables/Enables the strict mode. + * If the strict mode is disabled, the method will be executed even if there are too many arguments. + */ + @ApiStatus.Experimental + StrictMode strict() default StrictMode.DEFAULT; + } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/ExecuteAnnotationResolver.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/ExecuteAnnotationResolver.java index b0b701364..a26714326 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/ExecuteAnnotationResolver.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/ExecuteAnnotationResolver.java @@ -2,8 +2,12 @@ import dev.rollczi.litecommands.annotations.AnnotationInvoker; import dev.rollczi.litecommands.annotations.AnnotationProcessor; -import dev.rollczi.litecommands.command.CommandExecutorProvider; import dev.rollczi.litecommands.command.builder.CommandBuilder; +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.command.executor.CommandExecutorDefaultReference; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.reflect.LiteCommandsReflectInvocationException; +import dev.rollczi.litecommands.strict.StrictMode; import dev.rollczi.litecommands.util.LiteCommandsUtil; import java.util.Arrays; @@ -12,20 +16,38 @@ public class ExecuteAnnotationResolver implements AnnotationProcessor process(AnnotationInvoker invoker) { - return invoker.onExecutorStructure(Execute.class, (annotation, context, executorProvider) -> { - boolean isNotEmpty = LiteCommandsUtil.checkConsistent(annotation.name(), annotation.aliases()); - - if (isNotEmpty) { - context.getRealRoute().appendChild(CommandBuilder.create() - .routeName(annotation.name()) - .routeAliases(Arrays.asList(annotation.aliases())) - .applyOnRoute(builder -> builder.appendExecutor(executorProvider))); - - return; - } - - context.getRealRoute().appendExecutor(executorProvider); - }); + return invoker + .on(Execute.class, (annotation, metaHolder) -> { + if (annotation.strict() != StrictMode.DEFAULT) { + metaHolder.meta().put(Meta.STRICT_MODE, annotation.strict()); + } + }) + .on(ExecuteDefault.class, (annotation, metaHolder) -> metaHolder.meta().put(Meta.STRICT_MODE, annotation.strict())) + .onMethod(Execute.class, (method, annotation, context, executorProvider) -> { + boolean isNotEmpty = LiteCommandsUtil.checkConsistent(annotation.name(), annotation.aliases()); + + if (isNotEmpty) { + context.getRealRoute().appendChild(CommandBuilder.create() + .routeName(annotation.name()) + .routeAliases(Arrays.asList(annotation.aliases())) + .applyOnRoute(builder -> builder.appendExecutor(executorProvider))); + + return; + } + + context.getRealRoute().appendExecutor(executorProvider); + }) + .onMethod(ExecuteDefault.class, (method, annotation, context, executorProvider) -> context.getRealRoute() + .appendExecutor(parent -> { + CommandExecutor executor = executorProvider.provide(parent); + + if (!executor.getArguments().isEmpty()) { + throw new LiteCommandsReflectInvocationException(method, "Default executor cannot have any arguments!"); + } + + return new CommandExecutorDefaultReference<>(executor); + }) + ); } } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/ExecuteDefault.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/ExecuteDefault.java new file mode 100644 index 000000000..0ede05089 --- /dev/null +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/execute/ExecuteDefault.java @@ -0,0 +1,28 @@ +package dev.rollczi.litecommands.annotations.execute; + +import dev.rollczi.litecommands.strict.StrictMode; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents the method that will be executed when the command is called and no other method is found. + * This will ignore all input arguments. + * + * @see Execute + */ +@ApiStatus.Experimental +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExecuteDefault { + + /** + * Disables/Enables the strict mode. + * If the strict mode is disabled, the method will be executed even if there are too many arguments. + * Default is {@link StrictMode#DISABLED} + */ + StrictMode strict() default StrictMode.DISABLED; + +} diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMeta.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMeta.java index 39def5197..111b6a0fa 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMeta.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMeta.java @@ -5,6 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +@Deprecated @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MarkMeta { diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMetaAnnotationResolver.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMetaAnnotationResolver.java index 7acd0fe67..449af6377 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMetaAnnotationResolver.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/meta/MarkMetaAnnotationResolver.java @@ -4,6 +4,7 @@ import dev.rollczi.litecommands.annotations.AnnotationProcessor; import dev.rollczi.litecommands.meta.MetaKey; +@Deprecated public class MarkMetaAnnotationResolver implements AnnotationProcessor { @Override diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/Priority.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/Priority.java new file mode 100644 index 000000000..8e44a7453 --- /dev/null +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/Priority.java @@ -0,0 +1,18 @@ +package dev.rollczi.litecommands.annotations.priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Priority { + + @MagicConstant(valuesFromClass = PriorityValue.class) + int value() default PriorityValue.NORMAL; + +} diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/PriorityAnnotationResolver.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/PriorityAnnotationResolver.java new file mode 100644 index 000000000..42fb40b81 --- /dev/null +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/PriorityAnnotationResolver.java @@ -0,0 +1,17 @@ +package dev.rollczi.litecommands.annotations.priority; + +import dev.rollczi.litecommands.annotations.AnnotationInvoker; +import dev.rollczi.litecommands.annotations.AnnotationProcessor; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.priority.PriorityLevel; + +public class PriorityAnnotationResolver implements AnnotationProcessor { + + @Override + public AnnotationInvoker process(AnnotationInvoker invoker) { + return invoker.on(Priority.class, (annotation, metaHolder) -> { + metaHolder.meta().put(Meta.PRIORITY, PriorityValue.toPriorityLevel(annotation.value())); + }); + } + +} diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/PriorityValue.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/PriorityValue.java new file mode 100644 index 000000000..11efb73c1 --- /dev/null +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/priority/PriorityValue.java @@ -0,0 +1,51 @@ +package dev.rollczi.litecommands.annotations.priority; + +import dev.rollczi.litecommands.priority.PriorityLevel; + +public final class PriorityValue { + + public static final int NONE = Integer.MIN_VALUE; + public static final int LOWEST = -1000; + public static final int VERY_LOW = -500; + public static final int LOW = -100; + public static final int BELOW_NORMAL = -50; + public static final int NORMAL = 0; + public static final int ABOVE_NORMAL = 50; + public static final int HIGH = 100; + public static final int VERY_HIGH = 500; + public static final int HIGHEST = 1000; + public static final int MAX = Integer.MAX_VALUE; + + private PriorityValue() { + } + + public static PriorityLevel toPriorityLevel(int value) { + switch (value) { + case NONE: + return PriorityLevel.NONE; + case LOWEST: + return PriorityLevel.LOWEST; + case VERY_LOW: + return PriorityLevel.VERY_LOW; + case LOW: + return PriorityLevel.LOW; + case BELOW_NORMAL: + return PriorityLevel.BELOW_NORMAL; + case NORMAL: + return PriorityLevel.NORMAL; + case ABOVE_NORMAL: + return PriorityLevel.ABOVE_NORMAL; + case HIGH: + return PriorityLevel.HIGH; + case VERY_HIGH: + return PriorityLevel.VERY_HIGH; + case HIGHEST: + return PriorityLevel.HIGHEST; + case MAX: + return PriorityLevel.MAX; + default: + return new PriorityLevel("CUSTOM-" + value, value); + } + } + +} diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/requirement/RequirementProcessor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/requirement/RequirementProcessor.java index 043db935c..f88a7c79e 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/requirement/RequirementProcessor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/requirement/RequirementProcessor.java @@ -28,7 +28,7 @@ public RequirementProcessor(Class annotationClass, Class parsedType) { @Override public AnnotationInvoker process(AnnotationInvoker invoker) { try { - return invoker.onRequirement(annotationClass, (holder, builder) -> { + return invoker.onParameter(annotationClass, (parameter, holder, builder) -> { TypeToken typeToken = holder.getFormat().parsedType(); if (typeToken.isInstanceOf(parsedType)) { diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/shortcut/ShortcutCommandAnnotationProcessor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/shortcut/ShortcutCommandAnnotationProcessor.java index 4f829d919..d9933ad97 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/shortcut/ShortcutCommandAnnotationProcessor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/shortcut/ShortcutCommandAnnotationProcessor.java @@ -12,11 +12,10 @@ public class ShortcutCommandAnnotationProcessor implements AnnotationPro @Override public AnnotationInvoker process(AnnotationInvoker invoker) { - return invoker.onExecutorStructure( - Execute.class, - (executeAnnotation, builder, executorBuilder) -> invoker.onExecutorStructure( + return invoker.onMethod(Execute.class, + (method, executeAnnotation, builder, executorBuilder) -> invoker.onMethod( Shortcut.class, - (shortAnnotation, shortBuilder, shortExecutorBuilder) -> resolve(executeAnnotation, shortAnnotation, builder, shortExecutorBuilder) + (sameMethod, shortAnnotation, shortBuilder, shortExecutorBuilder) -> resolve(executeAnnotation, shortAnnotation, builder, shortExecutorBuilder) ) ); } diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/validator/requirment/AnnotatedValidatorProcessor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/validator/requirment/AnnotatedValidatorProcessor.java index a97fb829d..f94703653 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/validator/requirment/AnnotatedValidatorProcessor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/validator/requirment/AnnotatedValidatorProcessor.java @@ -20,7 +20,7 @@ public AnnotatedValidatorProcessor(Class annotationClass, Class type, Anno @Override public AnnotationInvoker process(AnnotationInvoker invoker) { - return invoker.onRequirementMeta(annotationClass, (annotationHolder, builder, requirement) -> { + return invoker.onParameterRequirement(annotationClass, (parameter, annotationHolder, builder, requirement) -> { Class parsedType = requirement.getWrapperFormat().getParsedType(); if (!type.isAssignableFrom(parsedType)) { diff --git a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/cases/ManyOptionalArgumentsTest.java b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/cases/ManyOptionalArgumentsTest.java new file mode 100644 index 000000000..47be1de5b --- /dev/null +++ b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/cases/ManyOptionalArgumentsTest.java @@ -0,0 +1,115 @@ +package dev.rollczi.litecommands.annotations.cases; + +import dev.rollczi.litecommands.annotations.LiteTestSpec; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.flag.Flag; +import dev.rollczi.litecommands.annotations.varargs.Varargs; +import dev.rollczi.litecommands.argument.resolver.standard.DurationArgumentResolver; +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class ManyOptionalArgumentsTest extends LiteTestSpec { + + enum TestEnum { + A, B, C + } + + @Command(name = "test") + static class TestCommand { + @Execute(name = "with-space") + String test(@Varargs(delimiter = ", ") List enums, @Flag("-s") boolean silent, @Arg Duration duration) { + if (enums.isEmpty()) { + return String.format("Silent: %s, Duration: %s", silent, duration); + } + + String enumsAsString = enums.stream().map(testEnum -> testEnum.name()).collect(Collectors.joining(", ")); + return String.format("Enums: %s, Silent: %s, Duration: %s", enumsAsString, silent, duration); + } + + @Execute(name = "without-space") + String testWithoutSpace(@Varargs(delimiter = ",") List enums, @Flag("-s") boolean silent, @Arg Duration duration) { + if (enums.isEmpty()) { + return String.format("Silent: %s, Duration: %s", silent, duration); + } + + String enumsAsString = enums.stream().map(testEnum -> testEnum.name()).collect(Collectors.joining(", ")); + return String.format("Enums: %s, Silent: %s, Duration: %s", enumsAsString, silent, duration); + } + } + + @Test + void testExecuteWithSpace() { + platform.execute("test with-space A, B, C -s 1h") + .assertSuccess("Enums: A, B, C, Silent: true, Duration: PT1H"); + + platform.execute("test with-space -s 1h") + .assertSuccess("Silent: true, Duration: PT1H"); + + platform.execute("test with-space 1h") + .assertSuccess("Silent: false, Duration: PT1H"); + + platform.execute("test with-space A, B, C 1h") + .assertSuccess("Enums: A, B, C, Silent: false, Duration: PT1H"); + } + + @Test + void testSuggestionsWithSpace() { + platform.suggest("test with-space A") + .assertSuggest("A", "A, "); + + platform.suggest("test with-space A, ") + .assertSuggest("A", "B", "C"); + + platform.suggest("test with-space A, B, C ") + .assertSuggestAndFlush("-s") + .assertAsSuggester(new DurationArgumentResolver<>(), ""); + + platform.suggest("test with-space A, B, C -s ") + .assertAsSuggester(new DurationArgumentResolver<>(), ""); + + platform.suggest("test with-space A, B, C -s 1") + .assertAsSuggester(new DurationArgumentResolver<>(), "1"); + } + + @Test + void testExecuteWithoutSpace() { + platform.execute("test without-space A,B,C -s 1h") + .assertSuccess("Enums: A, B, C, Silent: true, Duration: PT1H"); + + platform.execute("test without-space -s 1h") + .assertSuccess("Silent: true, Duration: PT1H"); + + platform.execute("test without-space 1h") + .assertSuccess("Silent: false, Duration: PT1H"); + + platform.execute("test without-space A,B,C 1h") + .assertSuccess("Enums: A, B, C, Silent: false, Duration: PT1H"); + } + + @Test + void testSuggestionsWithoutSpace() { + platform.suggest("test without-space A") + .assertSuggest("A", "A,"); + + platform.suggest("test without-space A,") + .assertSuggest("A,A", "A,B", "A,C"); + + platform.suggest("test without-space A,B,C ") + .assertSuggestAndFlush("-s") + .assertAsSuggester(new DurationArgumentResolver<>(), ""); + + platform.suggest("test without-space A,B,C -s ") + .assertAsSuggester(new DurationArgumentResolver<>(), ""); + + platform.suggest("test without-space A,B,C -s 1") + .assertAsSuggester(new DurationArgumentResolver<>(), "1"); + } + +} + + + diff --git a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/cooldown/CooldownAnnotationTest.java b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/cooldown/CooldownAnnotationTest.java index fe25bfd75..a87b37537 100644 --- a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/cooldown/CooldownAnnotationTest.java +++ b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/cooldown/CooldownAnnotationTest.java @@ -1,5 +1,7 @@ package dev.rollczi.litecommands.annotations.cooldown; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.invalidusage.InvalidUsage; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -17,16 +19,20 @@ class CooldownAnnotationTest extends LiteTestSpec { @Command(name = "test") - @Cooldown(key = "test-cooldown", count = 1, unit = ChronoUnit.SECONDS) + @Cooldown(key = "test-cooldown", count = 400, unit = ChronoUnit.MILLIS) static class TestCommand { @Execute void execute() {} + @Execute(name = "with-args") + @Cooldown(key = "test-cooldown-with-args", count = 600, unit = ChronoUnit.MILLIS) + void execute(@Arg Integer arg) {} + } @Command(name = "bypass-test") - @Cooldown(key = "bypass-test-cooldown", count = 1, unit = ChronoUnit.SECONDS, bypass = "test.bypass") + @Cooldown(key = "bypass-test-cooldown", count = 400, unit = ChronoUnit.MILLIS, bypass = "test.bypass") static class TestWithBypassCommand { @Execute @@ -42,7 +48,7 @@ void test() { .assertFailedAs(CooldownState.class); assertEquals("test-cooldown", cooldownState.getCooldownContext().getKey()); - assertEquals(Duration.ofSeconds(1), cooldownState.getCooldownContext().getDuration()); + assertEquals(Duration.ofMillis(400), cooldownState.getCooldownContext().getDuration()); assertFalse(cooldownState.getRemainingDuration().isZero()); Awaitility.await() @@ -50,6 +56,32 @@ void test() { .until(() -> platform.execute("test").isSuccessful()); } + @Test + void testInvalidUsage() { + platform.execute("test with-args invalid") + .assertFailureInvalid(InvalidUsage.Cause.INVALID_ARGUMENT); + + platform.execute("test with-args invalid") + .assertFailureInvalid(InvalidUsage.Cause.INVALID_ARGUMENT); + + platform.execute("test with-args invalid") + .assertFailureInvalid(InvalidUsage.Cause.INVALID_ARGUMENT); + + platform.execute("test with-args 10") + .assertSuccess(); + + CooldownState cooldownState = platform.execute("test with-args 10") + .assertFailedAs(CooldownState.class); + + assertEquals("test-cooldown-with-args", cooldownState.getCooldownContext().getKey()); + assertEquals(Duration.ofMillis(600), cooldownState.getCooldownContext().getDuration()); + assertFalse(cooldownState.getRemainingDuration().isZero()); + + Awaitility.await() + .atMost(Duration.ofSeconds(3)) + .until(() -> platform.execute("test with-args 10").isSuccessful()); + } + @Test void testCooldownBypass() { PlatformSender permittedSender = TestPlatformSender.permitted("test.bypass"); @@ -62,7 +94,7 @@ void testCooldownBypass() { .assertFailedAs(CooldownState.class); assertEquals("bypass-test-cooldown", cooldownState.getCooldownContext().getKey()); - assertEquals(Duration.ofSeconds(1), cooldownState.getCooldownContext().getDuration()); + assertEquals(Duration.ofMillis(400), cooldownState.getCooldownContext().getDuration()); assertFalse(cooldownState.getRemainingDuration().isZero()); Awaitility.await() diff --git a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/execute/ExecuteDefaultTest.java b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/execute/ExecuteDefaultTest.java new file mode 100644 index 000000000..24605965f --- /dev/null +++ b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/execute/ExecuteDefaultTest.java @@ -0,0 +1,62 @@ +package dev.rollczi.litecommands.annotations.execute; + +import dev.rollczi.litecommands.annotations.LiteTestSpec; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.unit.TestSender; +import org.junit.jupiter.api.Test; + +class ExecuteDefaultTest extends LiteTestSpec { + + @Command(name = "help") + static class TestCommand { + + @ExecuteDefault + String execute(@Context TestSender sender) { + return "#help"; + } + + @Execute(name = "vip") + String vip(@Context TestSender sender) { + return "#help:vip"; + } + + @Execute + String withArg(@Context TestSender sender, @Arg int page) { + return "#help:" + page; + } + + } + + @Test + void testDefault() { + platform.execute("help") + .assertSuccess("#help"); + + platform.execute("help args") + .assertSuccess("#help"); + + platform.execute("help other args") + .assertSuccess("#help"); + + platform.execute("help 1 args") + .assertSuccess("#help"); + } + + @Test + void testDefaultWithSubCommand() { + platform.execute("help vip") + .assertSuccess("#help:vip"); + } + + @Test + void testDefaultWithArg() { + platform.execute("help 1") + .assertSuccess("#help:1"); + + platform.execute("help 2") + .assertSuccess("#help:2"); + } + +} diff --git a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/priority/PriorityTest.java b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/priority/PriorityTest.java new file mode 100644 index 000000000..c910aa704 --- /dev/null +++ b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/priority/PriorityTest.java @@ -0,0 +1,53 @@ +package dev.rollczi.litecommands.annotations.priority; + +import dev.rollczi.litecommands.annotations.LiteTestSpec; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.execute.Execute; +import static dev.rollczi.litecommands.annotations.priority.Priority.*; +import dev.rollczi.litecommands.priority.PriorityLevel; +import org.junit.jupiter.api.Test; + +class PriorityTest extends LiteTestSpec { + + @Command(name = "test") + static class TestCommand { + + @Execute + @Priority(PriorityValue.NORMAL) + String executeNormal() { + return "NORMAL"; + } + + @Execute + @Priority(PriorityValue.HIGHEST) + String executeHighest() { + return "HIGHEST"; + } + + @Execute(name = "sub") + @Priority(PriorityValue.LOW) + String executeLow() { + return "LOW"; + } + + @Execute(name = "sub") + @Priority(PriorityValue.LOWEST) + String executeLowest() { + return "LOWEST"; + } + + } + + @Test + void test() { + platform.execute("test") + .assertSuccess("HIGHEST"); + } + + @Test + void testSub() { + platform.execute("test sub") + .assertSuccess("LOW"); + } + +} diff --git a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/route/RootArgAnnotationResolverTest.java b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/route/RootArgAnnotationResolverTest.java index bda1a08dd..25e43ce2e 100644 --- a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/route/RootArgAnnotationResolverTest.java +++ b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/route/RootArgAnnotationResolverTest.java @@ -32,8 +32,8 @@ public CommandBuilder getResult() { } @Override - public AnnotationInvoker onStructure(Class annotationType, AnnotationProcessor.StructureListener listener) { - builder = listener.call(rootCommand, builder); + public AnnotationInvoker onClass(Class annotationType, AnnotationProcessor.ClassListener listener) { + builder = listener.call(Command.class, rootCommand, builder); return this; } }); diff --git a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/strict/StrictGlobalSettingsTest.java b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/strict/StrictGlobalSettingsTest.java new file mode 100644 index 000000000..5f02da1ab --- /dev/null +++ b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/strict/StrictGlobalSettingsTest.java @@ -0,0 +1,59 @@ +package dev.rollczi.litecommands.annotations.strict; + +import dev.rollczi.litecommands.annotations.LiteConfig; +import dev.rollczi.litecommands.annotations.LiteTestSpec; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.invalidusage.InvalidUsage; +import dev.rollczi.litecommands.strict.StrictMode; +import org.junit.jupiter.api.Test; + +class StrictGlobalSettingsTest extends LiteTestSpec { + + static LiteConfig config = builder -> builder + .strictMode(StrictMode.DISABLED); + + @Command(name = "test") + static class TestCommand { + + @Execute(name = "no-strict") + void noStrict(@Arg String arg) {} + + + @Execute(name = "strict", strict = StrictMode.ENABLED) + void strict(@Arg String arg) {} + + } + + @Test + void invalid() { + platform.execute("test") + .assertFailureInvalid(InvalidUsage.Cause.UNKNOWN_COMMAND); + } + + @Test + void noStrict() { + platform.execute("test no-strict") + .assertFailureInvalid(InvalidUsage.Cause.MISSING_ARGUMENT); + + platform.execute("test no-strict arg") + .assertSuccess(); + + platform.execute("test no-strict arg arg") + .assertSuccess(); + } + + @Test + void strict() { + platform.execute("test strict") + .assertFailureInvalid(InvalidUsage.Cause.MISSING_ARGUMENT); + + platform.execute("test strict arg") + .assertSuccess(); + + platform.execute("test strict arg arg") + .assertFailureInvalid(InvalidUsage.Cause.TOO_MANY_ARGUMENTS); + } + +} diff --git a/litecommands-annotations/test/dev/rollczi/litecommands/annotations/strict/StrictTest.java b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/strict/StrictTest.java new file mode 100644 index 000000000..d6ae9737d --- /dev/null +++ b/litecommands-annotations/test/dev/rollczi/litecommands/annotations/strict/StrictTest.java @@ -0,0 +1,55 @@ +package dev.rollczi.litecommands.annotations.strict; + +import dev.rollczi.litecommands.annotations.LiteTestSpec; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.invalidusage.InvalidUsage; +import dev.rollczi.litecommands.strict.StrictMode; +import org.junit.jupiter.api.Test; + +class StrictTest extends LiteTestSpec { + + @Command(name = "test", strict = StrictMode.DISABLED) + static class TestCommand { + + @Execute(name = "no-strict") + void noStrict(@Arg String arg) {} + + + @Execute(name = "strict", strict = StrictMode.ENABLED) + void strict(@Arg String arg) {} + + } + + @Test + void invalid() { + platform.execute("test") + .assertFailureInvalid(InvalidUsage.Cause.UNKNOWN_COMMAND); + } + + @Test + void noStrict() { + platform.execute("test no-strict") + .assertFailureInvalid(InvalidUsage.Cause.MISSING_ARGUMENT); + + platform.execute("test no-strict arg") + .assertSuccess(); + + platform.execute("test no-strict arg arg") + .assertSuccess(); + } + + @Test + void strict() { + platform.execute("test strict") + .assertFailureInvalid(InvalidUsage.Cause.MISSING_ARGUMENT); + + platform.execute("test strict arg") + .assertSuccess(); + + platform.execute("test strict arg arg") + .assertFailureInvalid(InvalidUsage.Cause.TOO_MANY_ARGUMENTS); + } + +} diff --git a/litecommands-bukkit/src/dev/rollczi/litecommands/bukkit/LiteBukkitFactory.java b/litecommands-bukkit/src/dev/rollczi/litecommands/bukkit/LiteBukkitFactory.java index c54ff1681..5cd595b39 100644 --- a/litecommands-bukkit/src/dev/rollczi/litecommands/bukkit/LiteBukkitFactory.java +++ b/litecommands-bukkit/src/dev/rollczi/litecommands/bukkit/LiteBukkitFactory.java @@ -51,7 +51,7 @@ public static > B builder(Plugin plugin, Server server, LiteBukkitSettings settings) { - return (B) LiteCommandsFactory.builder(CommandSender.class, new BukkitPlatform(settings)).selfProcessor((builder, internal) -> { + return (B) LiteCommandsFactory.builder(CommandSender.class, new BukkitPlatform(settings)).self((builder, internal) -> { MessageRegistry messageRegistry = internal.getMessageRegistry(); builder diff --git a/litecommands-core/src/dev/rollczi/litecommands/LiteCommandsInternal.java b/litecommands-core/src/dev/rollczi/litecommands/LiteCommandsInternal.java index f94b91a7f..8f3b9ff18 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/LiteCommandsInternal.java +++ b/litecommands-core/src/dev/rollczi/litecommands/LiteCommandsInternal.java @@ -12,6 +12,7 @@ import dev.rollczi.litecommands.scheduler.Scheduler; import dev.rollczi.litecommands.argument.suggester.SuggesterRegistry; import dev.rollczi.litecommands.schematic.SchematicGenerator; +import dev.rollczi.litecommands.strict.StrictService; import dev.rollczi.litecommands.validator.ValidatorService; import dev.rollczi.litecommands.wrapper.WrapperRegistry; import org.jetbrains.annotations.ApiStatus; @@ -61,4 +62,7 @@ public interface LiteCommandsInternal { @ApiStatus.Internal MessageRegistry getMessageRegistry(); + @ApiStatus.Internal + StrictService getStrictService(); + } diff --git a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedParseableInput.java b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedParseableInput.java index 86127f627..e1e2b15d2 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedParseableInput.java +++ b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedParseableInput.java @@ -8,6 +8,8 @@ import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.meta.MetaHolder; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -87,8 +89,8 @@ public NamedParseableInputMatcher copy() { } @Override - public EndResult endMatch() { - if (consumedArguments.size() < namedArguments.size()) { + public EndResult endMatch(boolean isStrict) { + if (consumedArguments.size() < namedArguments.size() && isStrict) { return EndResult.failed(InvalidUsage.Cause.TOO_MANY_ARGUMENTS); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedTypedParseableInput.java b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedTypedParseableInput.java index d954cdfe3..b0ed4e43f 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedTypedParseableInput.java +++ b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/NamedTypedParseableInput.java @@ -8,6 +8,8 @@ import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.meta.MetaHolder; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -96,8 +98,8 @@ public TypeMixedInputMatcher copy() { } @Override - public EndResult endMatch() { - if (consumedArguments.size() < namedArguments.size()) { + public EndResult endMatch(boolean isStrict) { + if (consumedArguments.size() < namedArguments.size() && isStrict) { return EndResult.failed(InvalidUsage.Cause.TOO_MANY_ARGUMENTS); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/ParseableInputMatcher.java b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/ParseableInputMatcher.java index db3b94d97..c4e74fb51 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/ParseableInputMatcher.java +++ b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/ParseableInputMatcher.java @@ -22,7 +22,7 @@ public interface ParseableInputMatcher> SELF copy(); - EndResult endMatch(); + EndResult endMatch(boolean isStrict); class EndResult { private final boolean successful; diff --git a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/RawParseableInput.java b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/RawParseableInput.java index e57e8136f..ca59bb913 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/RawParseableInput.java +++ b/litecommands-core/src/dev/rollczi/litecommands/argument/parser/input/RawParseableInput.java @@ -7,6 +7,8 @@ import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.meta.MetaHolder; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -79,8 +81,9 @@ public RawInputMatcher copy() { } @Override - public EndResult endMatch() { - if (rawInputAnalyzer.getPivotPosition() < rawArguments.size()) { + public EndResult endMatch(boolean isStrict) { + + if (rawInputAnalyzer.getPivotPosition() < rawArguments.size() && isStrict) { return EndResult.failed(InvalidUsage.Cause.TOO_MANY_ARGUMENTS); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/SuggesterRegistryImpl.java b/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/SuggesterRegistryImpl.java index d72f4d971..a87169bdf 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/SuggesterRegistryImpl.java +++ b/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/SuggesterRegistryImpl.java @@ -2,7 +2,6 @@ import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.ArgumentKey; -import dev.rollczi.litecommands.argument.suggester.input.SuggestionInputResult; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.reflect.type.TypeIndex; import dev.rollczi.litecommands.reflect.type.TypeRange; @@ -11,15 +10,12 @@ import dev.rollczi.litecommands.suggestion.SuggestionContext; import dev.rollczi.litecommands.suggestion.SuggestionResult; import dev.rollczi.litecommands.util.StringUtil; -import dev.rollczi.litecommands.wrapper.WrapFormat; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; public class SuggesterRegistryImpl implements SuggesterRegistry, SuggesterChainAccessor { - private final Suggester noneSuggester = new SuggesterNoneImpl<>(); - private final TypeIndex> buckets = new TypeIndex<>(); @Override diff --git a/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/input/SuggestionInputRawImpl.java b/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/input/SuggestionInputRawImpl.java index adeaaa0d1..d1b14b491 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/input/SuggestionInputRawImpl.java +++ b/litecommands-core/src/dev/rollczi/litecommands/argument/suggester/input/SuggestionInputRawImpl.java @@ -4,10 +4,8 @@ import dev.rollczi.litecommands.argument.parser.Parser; import dev.rollczi.litecommands.argument.suggester.Suggester; import dev.rollczi.litecommands.range.Range; -import dev.rollczi.litecommands.requirement.RequirementCondition; import dev.rollczi.litecommands.suggestion.Suggestion; import dev.rollczi.litecommands.suggestion.SuggestionContext; -import dev.rollczi.litecommands.argument.parser.ParseResult; import dev.rollczi.litecommands.input.raw.RawInputAnalyzer; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.suggestion.SuggestionResult; @@ -106,7 +104,7 @@ public SuggestionInputResult nextArgument( return SuggestionInputResult.endWith(result); } - if (context.isLastRawArgument() || context.isPotentialLastArgument()) { + if (context.isLastArgument()) { Suggestion current = Suggestion.from(context.getAllNotConsumedArguments()); SuggestionContext suggestionContext = new SuggestionContext(current); SuggestionResult result = suggester.suggest(invocation, argument, suggestionContext) @@ -114,7 +112,12 @@ public SuggestionInputResult nextArgument( int consumed = suggestionContext.getConsumed(); if (consumed == current.lengthMultilevel()) { - context.consumeAll(); + if (isOptionalArgument(invocation, argument, parser)) { + rawInputAnalyzer.setLastOptionalArgument(true); + return SuggestionInputResult.endWith(result); + } + + rawInputAnalyzer.consumeAll(); return SuggestionInputResult.endWith(result); } } diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRootRouteImpl.java b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRootRouteImpl.java index 0bdf09625..ca201e149 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRootRouteImpl.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRootRouteImpl.java @@ -2,9 +2,8 @@ import dev.rollczi.litecommands.command.executor.CommandExecutor; import dev.rollczi.litecommands.meta.MetaHolder; -import dev.rollczi.litecommands.meta.MetaKey; import dev.rollczi.litecommands.meta.Meta; -import dev.rollczi.litecommands.meta.MetaCollector; +import dev.rollczi.litecommands.priority.PrioritizedList; import dev.rollczi.litecommands.util.StringUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -16,7 +15,6 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; - final class CommandRootRouteImpl implements CommandRoute { private final Map> children = new HashMap<>(); @@ -75,7 +73,7 @@ public Optional> getChild(String name) { } @Override - public List> getExecutors() { + public PrioritizedList> getExecutors() { throw new UnsupportedOperationException("Can not get executors from the root route"); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRoute.java b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRoute.java index 2619c0451..64352598d 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRoute.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRoute.java @@ -2,6 +2,7 @@ import dev.rollczi.litecommands.command.executor.CommandExecutor; import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.priority.PrioritizedList; import dev.rollczi.litecommands.scope.Scopeable; import org.jetbrains.annotations.ApiStatus; @@ -52,7 +53,7 @@ default boolean isReference() { void appendExecutor(CommandExecutor executor); @Unmodifiable - List> getExecutors(); + PrioritizedList> getExecutors(); Meta meta(); diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteExecutorReferenceImpl.java b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteExecutorReferenceImpl.java index 443c5de0e..60de49dea 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteExecutorReferenceImpl.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteExecutorReferenceImpl.java @@ -2,9 +2,11 @@ import dev.rollczi.litecommands.command.executor.CommandExecutor; -import java.util.ArrayList; +import dev.rollczi.litecommands.priority.MutablePrioritizedList; +import dev.rollczi.litecommands.priority.PrioritizedList; import java.util.Collections; import java.util.List; +import org.jetbrains.annotations.Unmodifiable; class CommandRouteExecutorReferenceImpl extends CommandRouteImpl { @@ -25,11 +27,16 @@ public boolean isReference() { } @Override - public List> getExecutors() { - List> executors = new ArrayList<>(super.getExecutors()); + public @Unmodifiable PrioritizedList> getExecutors() { + MutablePrioritizedList> executors = new MutablePrioritizedList<>(); + + for (CommandExecutor executor : super.getExecutors()) { + executors.add(executor); + } + executors.add(referenceExecutor); - return Collections.unmodifiableList(executors); + return executors; } } diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteImpl.java b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteImpl.java index 76aeabafe..c07ec8a97 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteImpl.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/CommandRouteImpl.java @@ -4,6 +4,9 @@ import dev.rollczi.litecommands.meta.Meta; import dev.rollczi.litecommands.meta.MetaCollector; import dev.rollczi.litecommands.meta.MetaHolder; +import dev.rollczi.litecommands.priority.MutablePrioritizedList; +import dev.rollczi.litecommands.priority.PrioritizedList; +import dev.rollczi.litecommands.shared.Preconditions; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -26,7 +29,7 @@ class CommandRouteImpl implements CommandRoute { private final Meta meta = Meta.create(); private final MetaCollector metaCollector = MetaCollector.of(this); - private final List> executors = new ArrayList<>(); + private final MutablePrioritizedList> executors = new MutablePrioritizedList<>(); private final List> childRoutes = new ArrayList<>(); private final Map> childrenByName = new HashMap<>(); @@ -82,8 +85,8 @@ public Optional> getChild(String name) { } @Override - public List> getExecutors() { - return Collections.unmodifiableList(this.executors); + public PrioritizedList> getExecutors() { + return this.executors; } @Override @@ -155,6 +158,8 @@ public void appendChildren(CommandRoute children) { @Override public void appendExecutor(CommandExecutor executor) { + Preconditions.notNull(executor, "executor"); + Preconditions.notContains(this.executors, executor, "executors", "executor"); this.executors.add(executor); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/AbstractCommandExecutor.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/AbstractCommandExecutor.java index 060b6788a..2febce0a1 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/executor/AbstractCommandExecutor.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/AbstractCommandExecutor.java @@ -2,6 +2,7 @@ import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.command.CommandRoute; +import dev.rollczi.litecommands.priority.PriorityLevel; import dev.rollczi.litecommands.requirement.BindRequirement; import dev.rollczi.litecommands.requirement.ContextRequirement; import dev.rollczi.litecommands.meta.Meta; @@ -62,4 +63,9 @@ public List> getBindRequirements() { return bindRequirements; } + @Override + public PriorityLevel getPriority() { + return meta().get(Meta.PRIORITY); + } + } diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteResult.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteResult.java index 94f84f2c6..16705cd49 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteResult.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteResult.java @@ -35,7 +35,7 @@ private CommandExecuteResult(CommandExecutor executor, @Nullable Object resul } public boolean isSuccessful() { - return this.result != null; + return this.error == null && this.throwable == null; } public boolean isFailed() { @@ -47,22 +47,38 @@ public boolean isThrown() { } public static CommandExecuteResult success(CommandExecutor executor, Object result) { + Preconditions.notNull(executor, "executor"); return new CommandExecuteResult(executor, result, null, null); } + public static CommandExecuteResult thrown(Throwable exception) { + Preconditions.notNull(exception, "exception"); + + return new CommandExecuteResult(null, null, exception, null); + } + public static CommandExecuteResult thrown(CommandExecutor executor, Throwable exception) { - Preconditions.notNull(exception, "exception cannot be null"); + Preconditions.notNull(executor, "executor"); + Preconditions.notNull(exception, "exception"); return new CommandExecuteResult(executor, null, exception, null); } public static CommandExecuteResult failed(CommandExecutor executor, Object error) { - Preconditions.notNull(error, "failed cannot be null"); + Preconditions.notNull(executor, "executor"); + Preconditions.notNull(error, "failed"); return new CommandExecuteResult(executor, null, null, error); } + public static CommandExecuteResult failed(Object error) { + Preconditions.notNull(error, "failed cannot be null"); + + return new CommandExecuteResult(null, null, null, error); + } + public static CommandExecuteResult failed(CommandExecutor executor, FailedReason failedReason) { + Preconditions.notNull(executor, "executor"); Preconditions.notNull(failedReason, "failed cannot be null"); return new CommandExecuteResult(executor, null, null, failedReason.getReasonOr(null)); diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteService.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteService.java index 23b07cd9e..5c84e4159 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteService.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecuteService.java @@ -5,8 +5,14 @@ import dev.rollczi.litecommands.argument.parser.input.ParseableInputMatcher; import dev.rollczi.litecommands.bind.BindRegistry; import dev.rollczi.litecommands.command.CommandRoute; +import dev.rollczi.litecommands.command.executor.event.CandidateExecutorFoundEvent; +import dev.rollczi.litecommands.command.executor.event.CandidateExecutorMatchEvent; +import dev.rollczi.litecommands.command.executor.event.CommandPostExecutionEvent; +import dev.rollczi.litecommands.command.executor.event.CommandPreExecutionEvent; +import dev.rollczi.litecommands.command.executor.flow.ExecuteFlow; import dev.rollczi.litecommands.context.ContextRegistry; import dev.rollczi.litecommands.invalidusage.InvalidUsageException; +import dev.rollczi.litecommands.event.EventPublisher; import dev.rollczi.litecommands.requirement.Requirement; import dev.rollczi.litecommands.requirement.RequirementCondition; import dev.rollczi.litecommands.requirement.RequirementsResult; @@ -23,6 +29,7 @@ import dev.rollczi.litecommands.meta.Meta; import dev.rollczi.litecommands.scheduler.Scheduler; import dev.rollczi.litecommands.scheduler.SchedulerPoll; +import dev.rollczi.litecommands.strict.StrictService; import dev.rollczi.litecommands.validator.ValidatorResult; import dev.rollczi.litecommands.validator.ValidatorService; import dev.rollczi.litecommands.validator.requirement.RequirementValidator; @@ -36,7 +43,6 @@ import org.jetbrains.annotations.Nullable; import java.util.List; -import java.util.ListIterator; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -50,6 +56,8 @@ public class CommandExecuteService { private final SchematicGenerator schematicGenerator; private final ScheduledRequirementResolver scheduledRequirementResolver; private final WrapperRegistry wrapperRegistry; + private final EventPublisher publisher; + private final StrictService strictService; public CommandExecuteService( ValidatorService validatorService, @@ -59,23 +67,36 @@ public CommandExecuteService( ParserRegistry parserRegistry, ContextRegistry contextRegistry, WrapperRegistry wrapperRegistry, - BindRegistry bindRegistry + BindRegistry bindRegistry, + EventPublisher publisher, StrictService strictService ) { this.validatorService = validatorService; this.resultResolver = resultResolver; this.scheduler = scheduler; this.schematicGenerator = schematicGenerator; this.wrapperRegistry = wrapperRegistry; + this.publisher = publisher; + this.strictService = strictService; this.scheduledRequirementResolver = new ScheduledRequirementResolver<>(contextRegistry, parserRegistry, bindRegistry, scheduler); } public CompletableFuture execute(Invocation invocation, ParseableInputMatcher matcher, CommandRoute commandRoute) { return execute0(invocation, matcher, commandRoute) - .thenApply(commandExecuteResult -> mapResult(commandRoute, commandExecuteResult, invocation)) + .thenApply(result -> publishAndApplyEvent(invocation, commandRoute, result)) .thenCompose(executeResult -> scheduler.supply(SchedulerPoll.MAIN, () -> this.handleResult(invocation, executeResult))) .exceptionally(new LastExceptionHandler<>(resultResolver, invocation)); } + @SuppressWarnings("unchecked") + private CommandExecuteResult publishAndApplyEvent(Invocation invocation, CommandRoute route, CommandExecuteResult result) { + if (this.publisher.hasSubscribers(CommandPostExecutionEvent.class)) { + CommandExecutor executor = (CommandExecutor) result.getExecutor(); + result = this.publisher.publish(new CommandPostExecutionEvent<>(invocation, route, executor, result)).getResult(); + } + + return result; + } + private CommandExecuteResult handleResult(Invocation invocation, CommandExecuteResult executeResult) { Throwable throwable = executeResult.getThrowable(); if (throwable != null) { @@ -95,21 +116,6 @@ private CommandExecuteResult handleResult(Invocation invocation, Command return executeResult; } - // TODO Support mapping of result in result resolver - private CommandExecuteResult mapResult(CommandRoute commandRoute, CommandExecuteResult executeResult, Invocation invocation) { - Object result = executeResult.getResult(); - if (result != null) { - return CommandExecuteResult.success(executeResult.getExecutor(), mapResult(result, commandRoute, executeResult, invocation)); - } - - Object error = executeResult.getError(); - if (error != null) { - return CommandExecuteResult.failed(executeResult.getExecutor(), mapResult(error, commandRoute, executeResult, invocation)); - } - - return executeResult; - } - @SuppressWarnings("unchecked") private Object mapResult(Object error, CommandRoute commandRoute, CommandExecuteResult executeResult, Invocation invocation) { if (error instanceof Cause) { @@ -128,11 +134,11 @@ private > CompletableFuture matcher, CommandRoute commandRoute ) { - return this.execute(commandRoute.getExecutors().listIterator(), invocation, matcher, commandRoute, null); + return this.execute(commandRoute.getExecutors().iterator(), invocation, matcher, commandRoute, null); } private > CompletableFuture execute( - ListIterator> executors, + Iterator> executors, Invocation invocation, ParseableInputMatcher matcher, CommandRoute commandRoute, @@ -143,22 +149,46 @@ private > CompletableFuture executor = executors.hasPrevious() ? executors.previous() : null; + CommandExecutor executor = commandRoute.getExecutors().isEmpty() + ? null : + commandRoute.getExecutors().last(); if (last != null && last.hasResult()) { return completedFuture(CommandExecuteResult.failed(executor, last)); } - return completedFuture(CommandExecuteResult.failed(executor, InvalidUsage.Cause.UNKNOWN_COMMAND)); + return completedFuture(CommandExecuteResult.failed(InvalidUsage.Cause.UNKNOWN_COMMAND)); } CommandExecutor executor = executors.next(); + + if (publisher.hasSubscribers(CandidateExecutorFoundEvent.class)) { + CandidateExecutorFoundEvent foundEvent = publisher.publish(new CandidateExecutorFoundEvent<>(invocation, executor)); + if (foundEvent.getFlow() == ExecuteFlow.STOP) { + return completedFuture(CommandExecuteResult.failed(executor, foundEvent.getFlowResult())); + } + + if (foundEvent.getFlow() == ExecuteFlow.SKIP) { + return this.execute(executors, invocation, matcher, commandRoute, foundEvent.getFlowResult()); + } + } + // Handle matching arguments return this.match(executor, invocation, matcher.copy()).thenCompose(match -> { + if (publisher.hasSubscribers(CandidateExecutorMatchEvent.class)) { + CandidateExecutorMatchEvent matchEvent = publisher.publish(new CandidateExecutorMatchEvent<>(invocation, executor, match)); + if (matchEvent.getFlow() == ExecuteFlow.STOP) { + return completedFuture(CommandExecuteResult.failed(executor, matchEvent.getFlowResult())); + } + + if (matchEvent.getFlow() == ExecuteFlow.SKIP) { + return this.execute(executors, invocation, matcher, commandRoute, matchEvent.getFlowResult()); + } + } + if (match.isFailed()) { FailedReason current = match.getFailedReason(); @@ -169,38 +199,40 @@ private > CompletableFuture executionEvent = publisher.publish(new CommandPreExecutionEvent<>(invocation, executor)); + if (executionEvent.getFlow() == ExecuteFlow.STOP) { + return completedFuture(CommandExecuteResult.failed(executor, executionEvent.getFlowResult())); + } - if (flow.isStopCurrent()) { - return this.execute(executors, invocation, matcher, commandRoute, flow.failedReason()); + if (executionEvent.getFlow() == ExecuteFlow.SKIP) { + return this.execute(executors, invocation, matcher, commandRoute, executionEvent.getFlowResult()); + } } // Execution SchedulerPoll type = executor.meta().get(Meta.POLL_TYPE); - return scheduler.supply(type, () -> { - try { - return match.executeCommand(); - } - catch (LiteCommandsException exception) { - if (exception.getCause() instanceof InvalidUsageException) { //TODO: Use invalid usage handler (when InvalidUsage.Cause is mapped to InvalidUsage) - return CommandExecuteResult.failed(executor, ((InvalidUsageException) exception.getCause()).getErrorResult()); - } - - return CommandExecuteResult.thrown(executor, exception); - } - catch (Throwable error) { - return CommandExecuteResult.thrown(executor, error); - } - }); + return scheduler.supply(type, () -> execute(match, executor)); }).exceptionally(throwable -> toThrown(executor, throwable)); } + private CommandExecuteResult execute(CommandExecutorMatchResult match, CommandExecutor executor) { + try { + return match.executeCommand(); + } + catch (LiteCommandsException exception) { + if (exception.getCause() instanceof InvalidUsageException) { //TODO: Use invalid usage handler (when InvalidUsage.Cause is mapped to InvalidUsage) + return CommandExecuteResult.failed(executor, ((InvalidUsageException) exception.getCause()).getErrorResult()); + } + + return CommandExecuteResult.thrown(executor, exception); + } + catch (Throwable error) { + return CommandExecuteResult.thrown(executor, error); + } + } + private CommandExecuteResult toThrown(CommandExecutor executor, Throwable throwable) { if (throwable instanceof CompletionException) { return CommandExecuteResult.thrown(executor, throwable.getCause()); @@ -226,7 +258,7 @@ private CompletableFuture match( ParseableInputMatcher matcher ) { if (!requirementIterator.hasNext()) { - ParseableInputMatcher.EndResult endResult = matcher.endMatch(); + ParseableInputMatcher.EndResult endResult = matcher.endMatch(strictService.isStrict(executor)); if (!endResult.isSuccessful()) { return completedFuture(CommandExecutorMatchResult.failed(endResult.getFailedReason())); @@ -243,7 +275,7 @@ private CompletableFuture match( ScheduledRequirement scheduledRequirement = requirementIterator.next(); - return scheduledRequirement.runMatch().thenCompose((requirementResult) -> { + return scheduledRequirement.runMatch().thenCompose(requirementResult -> { Requirement requirement = scheduledRequirement.getRequirement(); if (requirementResult.isFailed()) { diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutor.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutor.java index b945dcb1b..150b48cce 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutor.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutor.java @@ -3,6 +3,7 @@ import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.command.CommandNode; import dev.rollczi.litecommands.command.CommandRoute; +import dev.rollczi.litecommands.priority.Prioritized; import dev.rollczi.litecommands.requirement.BindRequirement; import dev.rollczi.litecommands.requirement.ContextRequirement; import dev.rollczi.litecommands.requirement.RequirementsResult; @@ -18,7 +19,7 @@ * Argument, ContextRequirement and BindRequirement are used to match the command. * @see CommandExecutor#match(RequirementsResult) */ -public interface CommandExecutor extends Scopeable, CommandNode { +public interface CommandExecutor extends Scopeable, CommandNode, Prioritized { @Unmodifiable List> getArguments(); diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutorBuilder.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutorBuilder.java index bb73ef48d..7dbf1ad71 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutorBuilder.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutorBuilder.java @@ -18,6 +18,7 @@ public class CommandExecutorBuilder { private final List> contextRequirements = new ArrayList<>(); private final List> bindRequirements = new ArrayList<>(); private Consumer> executor; + private Consumer> apply; public CommandExecutorBuilder(CommandRoute parent) { this.parent = parent; @@ -33,7 +34,7 @@ public boolean canBuild() { } public CommandExecutorBuilder arguments(Iterable> arguments) { - arguments.forEach(this::argument); + arguments.forEach(argument -> argument(argument)); return this; } @@ -57,8 +58,19 @@ public CommandExecutorBuilder executor(Consumer> exe return this; } + public CommandExecutorBuilder apply(Consumer> apply) { + this.apply = apply; + return this; + } + public CommandExecutor build() { - return new SimpleCommandExecutor<>(parent, executor, arguments, contextRequirements, bindRequirements); + CommandExecutor commandExecutor = new SimpleCommandExecutor<>(parent, executor, arguments, contextRequirements, bindRequirements); + + if (apply != null) { + apply.accept(commandExecutor); + } + + return commandExecutor; } private static class SimpleCommandExecutor extends AbstractCommandExecutor implements CommandExecutor { diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutorDefaultReference.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutorDefaultReference.java new file mode 100644 index 000000000..29af7b139 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/CommandExecutorDefaultReference.java @@ -0,0 +1,68 @@ +package dev.rollczi.litecommands.command.executor; + +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.command.CommandRoute; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.meta.MetaHolder; +import dev.rollczi.litecommands.priority.PriorityLevel; +import dev.rollczi.litecommands.requirement.BindRequirement; +import dev.rollczi.litecommands.requirement.ContextRequirement; +import dev.rollczi.litecommands.requirement.RequirementsResult; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +public class CommandExecutorDefaultReference implements CommandExecutor { + + private final CommandExecutor executor; + + public CommandExecutorDefaultReference(CommandExecutor executor) { + if (!executor.getArguments().isEmpty()) { + throw new IllegalArgumentException("Default executor cannot have any arguments"); + } + + this.executor = executor; + } + + @Override + public @Unmodifiable List> getArguments() { + return Collections.emptyList(); + } + + @Override + public @Unmodifiable List> getContextRequirements() { + return executor.getContextRequirements(); + } + + @Override + public @Unmodifiable List> getBindRequirements() { + return executor.getBindRequirements(); + } + + @Override + public CommandExecutorMatchResult match(RequirementsResult result) { + return executor.match(result); + } + + @Override + public CommandRoute getParent() { + return executor.getParent(); + } + + @Override + public Meta meta() { + return executor.meta(); + } + + @Override + public @Nullable MetaHolder parentMeta() { + return executor.parentMeta(); + } + + @Override + public PriorityLevel getPriority() { + return PriorityLevel.NONE; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/LastExceptionHandler.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/LastExceptionHandler.java index 7013ce9af..e5dd7f98a 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/executor/LastExceptionHandler.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/LastExceptionHandler.java @@ -29,7 +29,7 @@ public CommandExecuteResult apply(Throwable throwable) { lastError.printStackTrace(); } - return CommandExecuteResult.thrown(null, throwable); + return CommandExecuteResult.thrown(throwable); } } diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/LiteContext.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/LiteContext.java index 30e7c3166..4c201e505 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/command/executor/LiteContext.java +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/LiteContext.java @@ -42,7 +42,7 @@ public T argumentNullable(String name, Class type) { } public boolean argumentFlag(String name) { - return Boolean.TRUE.equals(this.get(name, WrapFormat.notWrapped(Boolean.class))); + return Boolean.TRUE.equals(this.get(name, WrapFormat.notWrapped(boolean.class))); } public String argumentJoin(String name) { diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/AbstractCommandExecutorEvent.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/AbstractCommandExecutorEvent.java new file mode 100644 index 000000000..9995f63f9 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/AbstractCommandExecutorEvent.java @@ -0,0 +1,70 @@ +package dev.rollczi.litecommands.command.executor.event; + +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.command.executor.flow.ExecuteFlow; +import dev.rollczi.litecommands.command.executor.flow.ExecuteFlowEvent; +import dev.rollczi.litecommands.event.Event; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.shared.FailedReason; +import dev.rollczi.litecommands.shared.Preconditions; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Experimental +abstract class AbstractCommandExecutorEvent implements Event, CommandExecutorEvent, ExecuteFlowEvent { + + private final Invocation invocation; + private final CommandExecutor executor; + private ExecuteFlow flow = ExecuteFlow.CONTINUE; + private FailedReason cancelReason; + + protected AbstractCommandExecutorEvent(Invocation invocation, CommandExecutor executor) { + this.invocation = invocation; + this.executor = executor; + } + + @Override + public Invocation getInvocation() { + return invocation; + } + + @Override + public CommandExecutor getExecutor() { + return executor; + } + + @Override + public ExecuteFlow getFlow() { + return flow; + } + + @Override + public @Nullable FailedReason getFlowResult() { + if (this.flow == ExecuteFlow.CONTINUE) { + throw new IllegalStateException("Cannot get cancel reason when flow is not TERMINATE or SKIP"); + } + + return cancelReason; + } + + @Override + public void continueFlow() { + this.flow = ExecuteFlow.CONTINUE; + this.cancelReason = null; + } + + @Override + public void stopFlow(FailedReason reason) { + Preconditions.notNull(reason, "reason"); + this.flow = ExecuteFlow.STOP; + this.cancelReason = reason; + } + + @Override + public void skipFlow(FailedReason reason) { + Preconditions.notNull(reason, "reason"); + this.flow = ExecuteFlow.SKIP; + this.cancelReason = reason; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CandidateExecutorFoundEvent.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CandidateExecutorFoundEvent.java new file mode 100644 index 000000000..2845621dd --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CandidateExecutorFoundEvent.java @@ -0,0 +1,14 @@ +package dev.rollczi.litecommands.command.executor.event; + +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.invocation.Invocation; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class CandidateExecutorFoundEvent extends AbstractCommandExecutorEvent { + + public CandidateExecutorFoundEvent(Invocation invocation, CommandExecutor executor) { + super(invocation, executor); + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CandidateExecutorMatchEvent.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CandidateExecutorMatchEvent.java new file mode 100644 index 000000000..e61e95355 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CandidateExecutorMatchEvent.java @@ -0,0 +1,22 @@ +package dev.rollczi.litecommands.command.executor.event; + +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.command.executor.CommandExecutorMatchResult; +import dev.rollczi.litecommands.invocation.Invocation; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class CandidateExecutorMatchEvent extends AbstractCommandExecutorEvent { + + private final CommandExecutorMatchResult matchResult; + + public CandidateExecutorMatchEvent(Invocation invocation, CommandExecutor executor, CommandExecutorMatchResult matchResult) { + super(invocation, executor); + this.matchResult = matchResult; + } + + public CommandExecutorMatchResult getMatchResult() { + return matchResult; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandExecutorEvent.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandExecutorEvent.java new file mode 100644 index 000000000..ffc3f5ce8 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandExecutorEvent.java @@ -0,0 +1,23 @@ +package dev.rollczi.litecommands.command.executor.event; + +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.event.Event; +import dev.rollczi.litecommands.invocation.Invocation; +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents an event fired when a command executor is executed. + * Flow: + * - {@link CandidateExecutorFoundEvent} + * - {@link CandidateExecutorMatchEvent} + * - {@link CommandPreExecutionEvent} + * - {@link CommandPostExecutionEvent} + */ +@ApiStatus.Experimental +public interface CommandExecutorEvent extends Event { + + Invocation getInvocation(); + + CommandExecutor getExecutor(); + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandPostExecutionEvent.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandPostExecutionEvent.java new file mode 100644 index 000000000..efe795435 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandPostExecutionEvent.java @@ -0,0 +1,50 @@ +package dev.rollczi.litecommands.command.executor.event; + +import dev.rollczi.litecommands.command.CommandRoute; +import dev.rollczi.litecommands.command.executor.CommandExecuteResult; +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.shared.Preconditions; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Experimental +public class CommandPostExecutionEvent implements CommandExecutorEvent { + + private final Invocation invocation; + private final CommandRoute route; + private final @Nullable CommandExecutor executor; + private CommandExecuteResult result; + + public CommandPostExecutionEvent(Invocation invocation, CommandRoute route, @Nullable CommandExecutor executor, CommandExecuteResult result) { + this.invocation = invocation; + this.route = route; + this.executor = executor; + this.result = result; + } + + @Override + public Invocation getInvocation() { + return invocation; + } + + public CommandRoute getCommandRoute() { + return route; + } + + @Nullable + @Override + public CommandExecutor getExecutor() { + return executor; + } + + public CommandExecuteResult getResult() { + return result; + } + + public void setResult(CommandExecuteResult result) { + Preconditions.notNull(result, "result cannot be null"); + this.result = result; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandPreExecutionEvent.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandPreExecutionEvent.java new file mode 100644 index 000000000..7fde1e8c4 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/event/CommandPreExecutionEvent.java @@ -0,0 +1,14 @@ +package dev.rollczi.litecommands.command.executor.event; + +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.invocation.Invocation; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class CommandPreExecutionEvent extends AbstractCommandExecutorEvent implements CommandExecutorEvent { + + public CommandPreExecutionEvent(Invocation invocation, CommandExecutor executor) { + super(invocation, executor); + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/flow/ExecuteFlow.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/flow/ExecuteFlow.java new file mode 100644 index 000000000..8a0685963 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/flow/ExecuteFlow.java @@ -0,0 +1,26 @@ +package dev.rollczi.litecommands.command.executor.flow; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents the flow of the command execution. + */ +@ApiStatus.Experimental +public enum ExecuteFlow { + + /** + * Continue the flow. + */ + CONTINUE, + + /** + * Skip the flow and continue to the next one. + */ + SKIP, + + /** + * Terminate the flow. (Stop the execution) + */ + STOP, + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/command/executor/flow/ExecuteFlowEvent.java b/litecommands-core/src/dev/rollczi/litecommands/command/executor/flow/ExecuteFlowEvent.java new file mode 100644 index 000000000..49b774dfd --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/command/executor/flow/ExecuteFlowEvent.java @@ -0,0 +1,19 @@ +package dev.rollczi.litecommands.command.executor.flow; + +import dev.rollczi.litecommands.event.Event; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface ExecuteFlowEvent extends Event { + + ExecuteFlow getFlow(); + + R getFlowResult(); + + void continueFlow(); + + void stopFlow(R result); + + void skipFlow(R result); + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownState.java b/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownState.java index 3185b75be..dd27609ec 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownState.java +++ b/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownState.java @@ -27,4 +27,12 @@ public Instant getExpirationTime() { return expirationTime; } + @Override + public String toString() { + return "CooldownState{" + + "cooldownContext=" + cooldownContext.getKey() + + ", remainingDuration=" + remainingDuration + + ", expirationTime=" + expirationTime + + '}'; + } } diff --git a/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownStateController.java b/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownStateController.java new file mode 100644 index 000000000..b5e12730e --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownStateController.java @@ -0,0 +1,90 @@ +package dev.rollczi.litecommands.cooldown; + +import dev.rollczi.litecommands.command.executor.CommandExecuteResult; +import dev.rollczi.litecommands.command.executor.event.CommandExecutorEvent; +import dev.rollczi.litecommands.command.executor.event.CommandPostExecutionEvent; +import dev.rollczi.litecommands.command.executor.event.CommandPreExecutionEvent; +import dev.rollczi.litecommands.event.EventListener; +import dev.rollczi.litecommands.event.Subscriber; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.platform.PlatformSender; +import dev.rollczi.litecommands.shared.FailedReason; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; +import net.jodah.expiringmap.ExpiringMap; +import org.jetbrains.annotations.Nullable; + +public class CooldownStateController implements EventListener { + + private final ExpiringMap cooldowns; + + public CooldownStateController() { + this.cooldowns = ExpiringMap.builder() + .variableExpiration() + .build(); + } + + @Subscriber + public void onEvent(CommandPreExecutionEvent event) { + Invocation invocation = event.getInvocation(); + PlatformSender sender = invocation.platformSender(); + CooldownContext cooldownContext = getOperativeContext(event, sender); + + if (cooldownContext == null) { + return; + } + + CooldownCompositeKey compositeKey = new CooldownCompositeKey(sender.getIdentifier(), cooldownContext.getKey()); + + Instant now = Instant.now(); + Instant expirationTime = cooldowns.get(compositeKey); + + if (expirationTime != null && expirationTime.isAfter(now)) { + event.stopFlow(FailedReason.of(new CooldownState(cooldownContext, Duration.between(now, expirationTime), expirationTime))); + } + } + + @Subscriber + public void onEvent(CommandPostExecutionEvent event) { + CommandExecuteResult result = event.getResult(); + + if (!result.isSuccessful()) { + return; + } + + Invocation invocation = event.getInvocation(); + PlatformSender sender = invocation.platformSender(); + CooldownContext cooldownContext = getOperativeContext(event, sender); + + if (cooldownContext == null) { + return; + } + + CooldownCompositeKey compositeKey = new CooldownCompositeKey(sender.getIdentifier(), cooldownContext.getKey()); + + Instant now = Instant.now(); + cooldowns.put(compositeKey, now.plus(cooldownContext.getDuration()), cooldownContext.getDuration().toNanos(), TimeUnit.NANOSECONDS); + } + + @Nullable + private CooldownContext getOperativeContext(CommandExecutorEvent event, PlatformSender sender) { + List cooldownContexts = event.getExecutor().metaCollector().collect(Meta.COOLDOWN); + + if (cooldownContexts.isEmpty()) { + return null; + } + + CooldownContext cooldownContext = cooldownContexts.get(0); + String bypassPermission = cooldownContext.getBypassPermission(); + + if (!bypassPermission.isEmpty() && sender.hasPermission(bypassPermission)) { + return null; + } + + return cooldownContext; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownStateValidator.java b/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownStateValidator.java deleted file mode 100644 index 30f203914..000000000 --- a/litecommands-core/src/dev/rollczi/litecommands/cooldown/CooldownStateValidator.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.rollczi.litecommands.cooldown; - -import dev.rollczi.litecommands.argument.suggester.input.SuggestionInput; -import dev.rollczi.litecommands.flow.Flow; -import dev.rollczi.litecommands.invocation.Invocation; -import dev.rollczi.litecommands.meta.Meta; -import dev.rollczi.litecommands.meta.MetaHolder; -import dev.rollczi.litecommands.validator.Validator; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.TimeUnit; -import net.jodah.expiringmap.ExpiringMap; - -public class CooldownStateValidator implements Validator { - - private final ExpiringMap cooldowns; - - public CooldownStateValidator() { - this.cooldowns = ExpiringMap.builder() - .variableExpiration() - .build(); - } - - @Override - public Flow validate(Invocation invocation, MetaHolder metaHolder) { - if (invocation.arguments() instanceof SuggestionInput) { - return Flow.continueFlow(); - } - - List cooldownContexts = metaHolder.metaCollector().collect(Meta.COOLDOWN); - if (cooldownContexts.isEmpty()) { - return Flow.continueFlow(); - } - - return validateCooldown(invocation, cooldownContexts.get(0)); - } - - private Flow validateCooldown(Invocation invocation, CooldownContext cooldownContext) { - String bypassPermission = cooldownContext.getBypassPermission(); - if (!bypassPermission.isEmpty() && invocation.platformSender().hasPermission(bypassPermission)) { - return Flow.continueFlow(); - } - - CooldownCompositeKey compositeKey = new CooldownCompositeKey(invocation.platformSender().getIdentifier(), cooldownContext.getKey()); - - Instant now = Instant.now(); - Instant expirationTime = cooldowns.get(compositeKey); - if (expirationTime != null && expirationTime.isAfter(now)) { - return Flow.terminateFlow(new CooldownState(cooldownContext, Duration.between(now, expirationTime), expirationTime)); - } - - cooldowns.put(compositeKey, now.plus(cooldownContext.getDuration()), cooldownContext.getDuration().toNanos(), TimeUnit.NANOSECONDS); - return Flow.continueFlow(); - } - -} diff --git a/litecommands-core/src/dev/rollczi/litecommands/event/Event.java b/litecommands-core/src/dev/rollczi/litecommands/event/Event.java new file mode 100644 index 000000000..ab7af74c3 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/event/Event.java @@ -0,0 +1,7 @@ +package dev.rollczi.litecommands.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface Event { +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/event/EventListener.java b/litecommands-core/src/dev/rollczi/litecommands/event/EventListener.java new file mode 100644 index 000000000..5e03aaf05 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/event/EventListener.java @@ -0,0 +1,12 @@ +package dev.rollczi.litecommands.event; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents an event listener. + * @see Subscriber + */ +@ApiStatus.Experimental +public interface EventListener { + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/event/EventPublisher.java b/litecommands-core/src/dev/rollczi/litecommands/event/EventPublisher.java new file mode 100644 index 000000000..129273f39 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/event/EventPublisher.java @@ -0,0 +1,14 @@ +package dev.rollczi.litecommands.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface EventPublisher { + + boolean hasSubscribers(Class eventClass); + + E publish(E event); + + void subscribe(EventListener listener); + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/event/SimpleEventPublisher.java b/litecommands-core/src/dev/rollczi/litecommands/event/SimpleEventPublisher.java new file mode 100644 index 000000000..391e97857 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/event/SimpleEventPublisher.java @@ -0,0 +1,115 @@ +package dev.rollczi.litecommands.event; + +import dev.rollczi.litecommands.bind.BindRegistry; +import dev.rollczi.litecommands.reflect.LiteCommandsReflectInvocationException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.ApiStatus; +import panda.std.Result; + +@ApiStatus.Experimental +public class SimpleEventPublisher implements EventPublisher { + + private final Map, Set> listeners = new HashMap<>(); + private final BindRegistry bindRegistry; + + public SimpleEventPublisher(BindRegistry bindRegistry) { + this.bindRegistry = bindRegistry; + } + + @Override + public boolean hasSubscribers(Class eventClass) { + return listeners.containsKey(eventClass); + } + + @Override + public E publish(E event) { + Set methods = listeners.get(event.getClass()); + + if (methods == null) { + return event; + } + + for (SubscriberMethod method : methods) { + method.invoke(event); + } + + return event; + } + + @Override + public void subscribe(EventListener listener) { + Class listenerClass = listener.getClass(); + + for (Method declaredMethod : listenerClass.getDeclaredMethods()) { + Subscriber annotation = declaredMethod.getAnnotation(Subscriber.class); + if (annotation == null) { + continue; + } + + if (declaredMethod.getParameterCount() == 0) { + throw new IllegalArgumentException("Method " + declaredMethod.getName() + " in " + listenerClass.getName() + " must have at least one parameter"); + } + + Class firstEventParameter = declaredMethod.getParameterTypes()[0]; + + if (!Event.class.isAssignableFrom(firstEventParameter)) { + throw new IllegalArgumentException("First parameter in method " + declaredMethod.getName() + " in " + listenerClass.getName() + " must be a subclass of Event"); + } + + if (firstEventParameter.isInterface()) { + throw new IllegalArgumentException("First parameter in method " + declaredMethod.getName() + " in " + listenerClass.getName() + " cannot be an interface"); + } + + Class[] bindClasses = new Class[declaredMethod.getParameterCount() - 1]; + System.arraycopy(declaredMethod.getParameterTypes(), 1, bindClasses, 0, declaredMethod.getParameterCount() - 1); + + declaredMethod.setAccessible(true); + listeners.computeIfAbsent(firstEventParameter, key -> new HashSet<>()).add(new SubscriberMethod(listener, declaredMethod, bindClasses)); + } + } + + private class SubscriberMethod { + + private final EventListener listener; + private final Method declaredMethod; + private final Class[] bindClasses; + + public SubscriberMethod(EventListener listener, Method declaredMethod, Class[] bindClasses) { + this.listener = listener; + this.declaredMethod = declaredMethod; + this.bindClasses = bindClasses; + } + + public void invoke(Event event) { + Object[] args = new Object[bindClasses.length + 1]; + args[0] = event; + + for (int i = 1; i < args.length; i++) { + Result result = bindRegistry.getInstance(bindClasses[i - 1]); + + if (result.isErr()) { + throw new IllegalArgumentException("Cannot bind " + bindClasses[i - 1].getName() + " for " + listener.getClass().getName() + "#" + declaredMethod.getName()); + } + + args[i] = result.get(); + } + + try { + declaredMethod.invoke(listener, args); + } + catch (IllegalAccessException exception) { + throw new LiteCommandsReflectInvocationException(declaredMethod, "Cannot access method", exception); + } + catch (InvocationTargetException exception) { + throw new LiteCommandsReflectInvocationException(declaredMethod, "Cannot invoke method", exception); + } + } + + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/event/Subscriber.java b/litecommands-core/src/dev/rollczi/litecommands/event/Subscriber.java new file mode 100644 index 000000000..34f4b24ac --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/event/Subscriber.java @@ -0,0 +1,8 @@ +package dev.rollczi.litecommands.event; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Subscriber { +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/input/raw/RawInputAnalyzer.java b/litecommands-core/src/dev/rollczi/litecommands/input/raw/RawInputAnalyzer.java index 74a62bc5f..52564dbe4 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/input/raw/RawInputAnalyzer.java +++ b/litecommands-core/src/dev/rollczi/litecommands/input/raw/RawInputAnalyzer.java @@ -15,6 +15,7 @@ public class RawInputAnalyzer { private final List rawArguments; private int lastPivotPosition = 0; private int pivotPosition = 0; + private boolean isLastOptionalArgument = false; public RawInputAnalyzer(List rawArguments) { for (String rawArgument : rawArguments) { @@ -60,6 +61,15 @@ public boolean nextRouteIsLast() { return pivotPosition == rawArguments.size() - 1; } + @ApiStatus.Experimental + public void setLastOptionalArgument(boolean setLastOptionalArgument) { + this.isLastOptionalArgument = setLastOptionalArgument; + } + + public void consumeAll() { + setPivotPosition(rawArguments.size()); + } + public class Context { private final Parser parser; @@ -104,15 +114,15 @@ public boolean matchParseArgument(Invocation invocation) { return parse; } - public boolean isLastRawArgument() { - return pivotPosition == rawArguments.size() - 1 || realArgumentMaxCount >= rawArguments.size(); + public boolean isLastArgument() { + return pivotPosition == rawArguments.size() - 1 || realArgumentMaxCount >= rawArguments.size() || isPotentialLastArgument() || isLastOptionalArgument; } public boolean isMissingFullArgument() { return isNoMoreArguments() && isMissingPartOfArgument(); } - public boolean isNoMoreArguments() { + private boolean isNoMoreArguments() { return pivotPosition >= rawArguments.size(); } @@ -120,17 +130,13 @@ public boolean isMissingPartOfArgument() { return argumentMinCount != realArgumentMinCount; } - public boolean isPotentialLastArgument() { + private boolean isPotentialLastArgument() { return this.realArgumentMaxCount < this.argumentMaxCount; } public List getAllNotConsumedArguments() { return rawArguments.subList(pivotPosition, rawArguments.size()); } - - public void consumeAll() { - setPivotPosition(rawArguments.size()); - } } private static int calculateRealMaxCount(List rawArguments, Range range, int routePosition) { diff --git a/litecommands-core/src/dev/rollczi/litecommands/invalidusage/InvalidUsageResultController.java b/litecommands-core/src/dev/rollczi/litecommands/invalidusage/InvalidUsageResultController.java new file mode 100644 index 000000000..277b14b6d --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/invalidusage/InvalidUsageResultController.java @@ -0,0 +1,61 @@ +package dev.rollczi.litecommands.invalidusage; + +import dev.rollczi.litecommands.command.CommandRoute; +import dev.rollczi.litecommands.command.executor.CommandExecuteResult; +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.command.executor.event.CommandPostExecutionEvent; +import dev.rollczi.litecommands.event.EventListener; +import dev.rollczi.litecommands.event.Subscriber; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.schematic.Schematic; +import dev.rollczi.litecommands.schematic.SchematicGenerator; +import dev.rollczi.litecommands.schematic.SchematicInput; +import org.jetbrains.annotations.Nullable; + +public class InvalidUsageResultController implements EventListener { + + private final SchematicGenerator schematicGenerator; + + public InvalidUsageResultController(SchematicGenerator schematicGenerator) { + this.schematicGenerator = schematicGenerator; + } + + @Subscriber + public void onEvent(CommandPostExecutionEvent event) { + CommandExecutor executor = event.getExecutor(); + CommandRoute commandRoute = event.getCommandRoute(); + Invocation invocation = event.getInvocation(); + CommandExecuteResult executeResult = event.getResult(); + + Object result = executeResult.getResult(); + if (result != null) { + event.setResult(CommandExecuteResult.success(executor, mapResult(result, commandRoute, executeResult, invocation))); + } + + Object error = executeResult.getError(); + if (error != null) { + Object mapped = mapResult(error, commandRoute, executeResult, invocation); + + if (executor == null) { + event.setResult(CommandExecuteResult.failed(mapped)); + return; + } + + event.setResult(CommandExecuteResult.failed(executor, mapped)); + } + } + + @SuppressWarnings("unchecked") + private Object mapResult(Object error, CommandRoute commandRoute, CommandExecuteResult executeResult, Invocation invocation) { + if (error instanceof InvalidUsage.Cause) { + InvalidUsage.Cause cause = (InvalidUsage.Cause) error; + @Nullable CommandExecutor executor = (CommandExecutor) executeResult.getExecutor(); + Schematic schematic = schematicGenerator.generate(new SchematicInput<>(commandRoute, executor, invocation)); + + return new InvalidUsage<>(cause, commandRoute, schematic); + } + + return error; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/meta/Meta.java b/litecommands-core/src/dev/rollczi/litecommands/meta/Meta.java index 17bc82ef7..a91ff2d9f 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/meta/Meta.java +++ b/litecommands-core/src/dev/rollczi/litecommands/meta/Meta.java @@ -1,7 +1,9 @@ package dev.rollczi.litecommands.meta; import dev.rollczi.litecommands.cooldown.CooldownContext; +import dev.rollczi.litecommands.priority.PriorityLevel; import dev.rollczi.litecommands.scheduler.SchedulerPoll; +import dev.rollczi.litecommands.strict.StrictMode; import dev.rollczi.litecommands.validator.Validator; import dev.rollczi.litecommands.validator.requirement.RequirementValidator; import org.jetbrains.annotations.ApiStatus; @@ -19,6 +21,7 @@ public interface Meta { MetaKey> DESCRIPTION = MetaKey.of("description", MetaType.list(), Collections.emptyList()); MetaKey> PERMISSIONS = MetaKey.of("permissions", MetaType.list(), Collections.emptyList()); + MetaKey PRIORITY = MetaKey.of("priority", PriorityLevel.class, PriorityLevel.NORMAL); MetaKey NATIVE_PERMISSIONS = MetaKey.of("native-permissions", Boolean.class, false); MetaKey POLL_TYPE = MetaKey.of("poll-type", SchedulerPoll.class, SchedulerPoll.MAIN); MetaKey ARGUMENT_KEY = MetaKey.of("argument-key", String.class); @@ -26,6 +29,7 @@ public interface Meta { MetaKey>>> VALIDATORS = MetaKey.of("validators", MetaType.list(), Collections.emptyList()); MetaKey>> REQUIREMENT_VALIDATORS = MetaKey.of("requirement-validators", MetaType.list(), Collections.emptyList()); MetaKey COOLDOWN = MetaKey.of("cooldown", CooldownContext.class); + MetaKey STRICT_MODE = MetaKey.of("strict-mode", StrictMode.class, StrictMode.DEFAULT); /** * LiteCommands Annotation API diff --git a/litecommands-core/src/dev/rollczi/litecommands/meta/MetaCollector.java b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaCollector.java index a2ee9f6e0..e168ddf2a 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/meta/MetaCollector.java +++ b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaCollector.java @@ -1,13 +1,25 @@ package dev.rollczi.litecommands.meta; import java.util.List; +import org.jetbrains.annotations.ApiStatus; public interface MetaCollector { List collect(MetaKey key); + @ApiStatus.Experimental + List collectReverse(MetaKey key); + Iterable iterable(MetaKey key); + T findFirst(MetaKey key); + + T findFirst(MetaKey key, T defaultValue); + + T findLast(MetaKey key); + + T findLast(MetaKey key, T defaultValue); + static MetaCollector of(MetaHolder metaHolder) { return new MetaHolderCollectorImpl(metaHolder); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolder.java b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolder.java index 5798b6249..47053deb7 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolder.java +++ b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolder.java @@ -1,6 +1,7 @@ package dev.rollczi.litecommands.meta; import org.jetbrains.annotations.Nullable; + public interface MetaHolder { Meta meta(); @@ -12,4 +13,8 @@ default MetaCollector metaCollector() { return MetaCollector.of(this); } + static MetaHolder empty() { + return MetaHolderEmptyImpl.INSTANCE; + } + } diff --git a/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolderCollectorImpl.java b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolderCollectorImpl.java index 0e0915ed1..f02e4059e 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolderCollectorImpl.java +++ b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolderCollectorImpl.java @@ -1,6 +1,7 @@ package dev.rollczi.litecommands.meta; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; @@ -29,11 +30,92 @@ public List collect(MetaKey key) { return collected; } + @Override + public List collectReverse(MetaKey key) { + List collected = new ArrayList<>(); + MetaHolder current = metaHolder; + + while (current != null) { + if (current.meta().has(key)) { + collected.add(0, current.meta().get(key)); + } + + current = current.parentMeta(); + } + + return collected; + } + @Override public Iterable iterable(MetaKey key) { return new MetaIterable<>(key); } + @Override + public T findFirst(MetaKey key) { + T first = this.findFirst(key, null); + + if (first == null) { + T defaultValue = key.getDefaultValue(); + + if (defaultValue == null) { + throw new NoSuchElementException("Meta value with key " + key.getKey() + " not found!"); + } + + return defaultValue; + } + + return first; + } + + @Override + public T findFirst(MetaKey key, T defaultValue) { + MetaHolder current = metaHolder; + + while (current != null) { + if (current.meta().has(key)) { + return current.meta().get(key); + } + + current = current.parentMeta(); + } + + return defaultValue; + } + + @Override + public T findLast(MetaKey key) { + T last = this.findLast(key, null); + + if (last == null) { + T defaultValue = key.getDefaultValue(); + + if (defaultValue == null) { + throw new NoSuchElementException("Meta value with key " + key.getKey() + " not found!"); + } + + return defaultValue; + } + + return last; + } + + @Override + public T findLast(MetaKey key, T defaultValue) { + MetaHolder current = metaHolder; + T last = null; + + while (current != null) { + if (current.meta().has(key)) { + last = current.meta().get(key); + } + + current = current.parentMeta(); + } + + return last == null ? defaultValue : last; + } + private class MetaIterable implements Iterable { private final MetaKey key; diff --git a/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolderEmptyImpl.java b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolderEmptyImpl.java new file mode 100644 index 000000000..d8b78ae38 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/meta/MetaHolderEmptyImpl.java @@ -0,0 +1,19 @@ +package dev.rollczi.litecommands.meta; + +import org.jetbrains.annotations.Nullable; + +class MetaHolderEmptyImpl implements MetaHolder { + + static final MetaHolder INSTANCE = new MetaHolderEmptyImpl(); + + @Override + public Meta meta() { + return Meta.EMPTY_META; + } + + @Override + public @Nullable MetaHolder parentMeta() { + return null; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/priority/MutablePrioritizedList.java b/litecommands-core/src/dev/rollczi/litecommands/priority/MutablePrioritizedList.java new file mode 100644 index 000000000..a23f03ada --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/priority/MutablePrioritizedList.java @@ -0,0 +1,129 @@ +package dev.rollczi.litecommands.priority; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.TreeMap; +import java.util.stream.Stream; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +public class MutablePrioritizedList implements PrioritizedList { + + private final TreeMap> priorities = new TreeMap<>(Comparator.reverseOrder()); + private final HashSet elements = new HashSet<>(); + + @ApiStatus.Experimental + public MutablePrioritizedList() { + } + + @ApiStatus.Experimental + public MutablePrioritizedList(Iterable elements) { + for (E element : elements) { + this.add(element); + } + } + + @ApiStatus.Experimental + public void add(E element) { + Entry entry = priorities.computeIfAbsent(element.getPriority(), priority -> new Entry<>(priority)); + + entry.elements.add(element); + elements.add(element); + } + + @ApiStatus.Experimental + public void remove(E element) { + Entry entry = priorities.get(element.getPriority()); + + if (entry != null) { + entry.elements.remove(element); + + if (entry.elements.isEmpty()) { + priorities.remove(element.getPriority()); + } + } + + elements.remove(element); + } + + @ApiStatus.Experimental + public void clear() { + priorities.clear(); + elements.clear(); + } + + @ApiStatus.Experimental + @Override + public boolean contains(E element) { + return elements.contains(element); + } + + @ApiStatus.Experimental + @Override + public boolean isEmpty() { + return priorities.isEmpty(); + } + + @ApiStatus.Experimental + @Override + public int size() { + return priorities.values().stream() + .mapToInt(entry -> entry.elements.size()) + .sum(); + } + + @ApiStatus.Experimental + @Override + public E first() { + if (this.priorities.isEmpty()) { + throw new IllegalStateException("PrioritySet is empty"); + } + + Entry entry = this.priorities.firstEntry().getValue(); + return entry.elements.get(0); + } + + @ApiStatus.Experimental + @Override + public E last() { + if (this.priorities.isEmpty()) { + throw new IllegalStateException("PrioritySet is empty"); + } + + Entry entry = this.priorities.lastEntry().getValue(); + return entry.elements.get(entry.elements.size() - 1); + } + + @ApiStatus.Experimental + @Override + public Stream stream() { + return priorities.values().stream() + .flatMap(entry -> entry.elements.stream()); + } + + @Override + @ApiStatus.Experimental + public @NotNull Iterator iterator() { + return priorities.values().stream() + .flatMap(entry -> entry.elements.stream()) + .iterator(); + } + + private static class Entry implements Prioritized { + private final PriorityLevel priority; + private final List elements = new ArrayList<>(); + + private Entry(PriorityLevel priority) { + this.priority = priority; + } + + @Override + public PriorityLevel getPriority() { + return priority; + } + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/priority/Prioritized.java b/litecommands-core/src/dev/rollczi/litecommands/priority/Prioritized.java new file mode 100644 index 000000000..7b14d8cdd --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/priority/Prioritized.java @@ -0,0 +1,17 @@ +package dev.rollczi.litecommands.priority; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface Prioritized extends Comparable { + + @ApiStatus.Experimental + PriorityLevel getPriority(); + + @ApiStatus.Experimental + @Override + default int compareTo(Prioritized prioritized) { + return this.getPriority().compareTo(prioritized.getPriority()); + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/priority/PrioritizedList.java b/litecommands-core/src/dev/rollczi/litecommands/priority/PrioritizedList.java new file mode 100644 index 000000000..1725156d0 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/priority/PrioritizedList.java @@ -0,0 +1,30 @@ +package dev.rollczi.litecommands.priority; + +import java.util.stream.Stream; +import org.jetbrains.annotations.ApiStatus; + +public interface PrioritizedList extends Iterable { + + boolean isEmpty(); + + int size(); + + E first(); + + E last(); + + Stream stream(); + + boolean contains(E element); + + @ApiStatus.Experimental + @SafeVarargs + static PrioritizedList of(E... elements) { + MutablePrioritizedList set = new MutablePrioritizedList<>(); + for (E element : elements) { + set.add(element); + } + return set; + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/priority/PriorityLevel.java b/litecommands-core/src/dev/rollczi/litecommands/priority/PriorityLevel.java new file mode 100644 index 000000000..1cb7bb35f --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/priority/PriorityLevel.java @@ -0,0 +1,45 @@ +package dev.rollczi.litecommands.priority; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Experimental +public final class PriorityLevel implements Comparable { + + public static final PriorityLevel NONE = new PriorityLevel("NONE", Integer.MIN_VALUE); + public static final PriorityLevel LOWEST = new PriorityLevel("LOWEST", -1000); + public static final PriorityLevel VERY_LOW = new PriorityLevel("LOW", -500); + public static final PriorityLevel LOW = new PriorityLevel("LOW", -100); + public static final PriorityLevel BELOW_NORMAL = new PriorityLevel("BELOW_NORMAL", -50); + public static final PriorityLevel NORMAL = new PriorityLevel("NORMAL", 0); + public static final PriorityLevel ABOVE_NORMAL = new PriorityLevel("ABOVE_NORMAL", 50); + public static final PriorityLevel HIGH = new PriorityLevel("HIGH", 100); + public static final PriorityLevel VERY_HIGH = new PriorityLevel("HIGH", 500); + public static final PriorityLevel HIGHEST = new PriorityLevel("HIGHEST", 1000); + public static final PriorityLevel MAX = new PriorityLevel("MAX", Integer.MAX_VALUE); + + private final String name; + private final int priority; + + @ApiStatus.Experimental + public PriorityLevel(String name, int priority) { + this.name = name; + this.priority = priority; + } + + @ApiStatus.Experimental + public String getName() { + return name; + } + + @ApiStatus.Experimental + public int getPriority() { + return priority; + } + + @Override + public int compareTo(@NotNull PriorityLevel o) { + return Integer.compare(this.priority, o.priority); + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/reflect/ReflectIndex.java b/litecommands-core/src/dev/rollczi/litecommands/reflect/ReflectIndex.java index f3f44606e..4ae0c01c2 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/reflect/ReflectIndex.java +++ b/litecommands-core/src/dev/rollczi/litecommands/reflect/ReflectIndex.java @@ -6,7 +6,7 @@ public final class ReflectIndex { - private final static Map, List>> INTERFACE_INDEX = new HashMap<>(); + private static final Map, List>> INTERFACE_INDEX = new HashMap<>(); public static List> getInterfaces(Class type) { List> interfaces = INTERFACE_INDEX.get(type); diff --git a/litecommands-core/src/dev/rollczi/litecommands/shared/Preconditions.java b/litecommands-core/src/dev/rollczi/litecommands/shared/Preconditions.java index 39dc31b16..9f29f3456 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/shared/Preconditions.java +++ b/litecommands-core/src/dev/rollczi/litecommands/shared/Preconditions.java @@ -1,5 +1,6 @@ package dev.rollczi.litecommands.shared; + import org.jetbrains.annotations.Contract; import java.util.Collection; @@ -35,4 +36,18 @@ public static void notEmpty(Collection value, String name) { } } + public static void notContains(Collection collection, Object element, String name, String elementName) { + if (collection.contains(element)) { + throw new IllegalArgumentException("Collection " + name + " already contains " + elementName); + } + } + + public static void notContains(Iterable iterable, Object element, String name, String elementName) { + for (Object obj : iterable) { + if (obj.equals(element)) { + throw new IllegalArgumentException("Collection " + name + " already contains " + elementName); + } + } + } + } diff --git a/litecommands-core/src/dev/rollczi/litecommands/strict/StrictMode.java b/litecommands-core/src/dev/rollczi/litecommands/strict/StrictMode.java new file mode 100644 index 000000000..ff74c3f30 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/strict/StrictMode.java @@ -0,0 +1,7 @@ +package dev.rollczi.litecommands.strict; + +public enum StrictMode { + ENABLED, + DISABLED, + DEFAULT +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/strict/StrictService.java b/litecommands-core/src/dev/rollczi/litecommands/strict/StrictService.java new file mode 100644 index 000000000..61047b4f3 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/strict/StrictService.java @@ -0,0 +1,34 @@ +package dev.rollczi.litecommands.strict; + +import dev.rollczi.litecommands.meta.Meta; +import dev.rollczi.litecommands.meta.MetaHolder; +import dev.rollczi.litecommands.shared.Preconditions; + +public class StrictService { + + private StrictMode defaultMode = StrictMode.ENABLED; + + public void setDefaultMode(StrictMode defaultMode) { + Preconditions.notNull(defaultMode, "defaultMode"); + Preconditions.checkArgument(defaultMode != StrictMode.DEFAULT, "Default mode cannot be DEFAULT"); + + this.defaultMode = defaultMode; + } + + public StrictMode getDefaultMode() { + return defaultMode; + } + + public boolean isStrict(MetaHolder metaHolder) { + StrictMode first = metaHolder.metaCollector().findFirst(Meta.STRICT_MODE); + + switch (first) { + case ENABLED: return true; + case DISABLED: return false; + case DEFAULT: + default: + return defaultMode == StrictMode.ENABLED; + } + } + +} diff --git a/litecommands-core/src/dev/rollczi/litecommands/suggestion/Suggestion.java b/litecommands-core/src/dev/rollczi/litecommands/suggestion/Suggestion.java index 2ff570dcd..c3c395fbf 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/suggestion/Suggestion.java +++ b/litecommands-core/src/dev/rollczi/litecommands/suggestion/Suggestion.java @@ -44,7 +44,7 @@ public int lengthMultilevel() { } public Suggestion deleteLeft(int levels) { - if (levels == 0) { + if (levels <= 0) { return this; } diff --git a/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java b/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java index 425b60b63..d55dcacb2 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java +++ b/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java @@ -7,7 +7,6 @@ import dev.rollczi.litecommands.argument.suggester.input.SuggestionInputMatcher; import dev.rollczi.litecommands.argument.suggester.input.SuggestionInputResult; import dev.rollczi.litecommands.argument.parser.ParserRegistry; -import dev.rollczi.litecommands.argument.parser.ParserSet; import dev.rollczi.litecommands.command.executor.CommandExecutor; import dev.rollczi.litecommands.command.CommandRoute; import dev.rollczi.litecommands.flow.Flow; @@ -144,9 +143,8 @@ private > SuggestionInpu Suggester suggester = suggesterRegistry.getSuggester(parsedType, argument.getKey()); SuggestionInputResult result = matcher.nextArgument(invocation, argument, parser, suggester); - boolean isOptional = matcher.isOptionalArgument(invocation, argument, parser); - if (result.isEnd() && isOptional) { + if (result.isEnd() && matcher.isOptionalArgument(invocation, argument, parser)) { return SuggestionInputResult.continueWith(result); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/validator/ValidatorExecutionController.java b/litecommands-core/src/dev/rollczi/litecommands/validator/ValidatorExecutionController.java new file mode 100644 index 000000000..26cd8f20d --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/validator/ValidatorExecutionController.java @@ -0,0 +1,29 @@ +package dev.rollczi.litecommands.validator; + +import dev.rollczi.litecommands.command.executor.event.CommandPreExecutionEvent; +import dev.rollczi.litecommands.event.EventListener; +import dev.rollczi.litecommands.event.Subscriber; +import dev.rollczi.litecommands.flow.Flow; + +public class ValidatorExecutionController implements EventListener { + + private final ValidatorService validatorService; + + public ValidatorExecutionController(ValidatorService validatorService) { + this.validatorService = validatorService; + } + + @Subscriber + public void onEvent(CommandPreExecutionEvent event) { + Flow flow = this.validatorService.validate(event.getInvocation(), event.getExecutor()); + + if (flow.isTerminate()) { + event.stopFlow(flow.failedReason()); + } + + if (flow.isStopCurrent()) { + event.skipFlow(flow.failedReason()); + } + } + +} diff --git a/litecommands-core/test/dev/rollczi/litecommands/permission/MissingPermissionValidatorTest.java b/litecommands-core/test/dev/rollczi/litecommands/permission/MissingPermissionValidatorTest.java index 2af1cf602..4661440b3 100644 --- a/litecommands-core/test/dev/rollczi/litecommands/permission/MissingPermissionValidatorTest.java +++ b/litecommands-core/test/dev/rollczi/litecommands/permission/MissingPermissionValidatorTest.java @@ -42,7 +42,7 @@ void test() { .appendExecutor(parent -> new TestExecutor<>(parent).onMeta(meta -> meta.listEditor(Meta.PERMISSIONS).add("permission.sub.execute").apply())))); CommandRoute sub = assertPresent(test.getChild("sub")); - CommandExecutor executor = sub.getExecutors().get(0); + CommandExecutor executor = sub.getExecutors().first(); Flow result = validator.validate(invocation, executor); diff --git a/litecommands-core/test/dev/rollczi/litecommands/priority/PriorityTest.java b/litecommands-core/test/dev/rollczi/litecommands/priority/PriorityTest.java new file mode 100644 index 000000000..e773f37b6 --- /dev/null +++ b/litecommands-core/test/dev/rollczi/litecommands/priority/PriorityTest.java @@ -0,0 +1,53 @@ +package dev.rollczi.litecommands.priority; + +import java.util.Iterator; +import java.util.TreeSet; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +class PriorityTest { + + static class TestEntry implements Prioritized { + private final String name; + private final PriorityLevel level; + + TestEntry(String name, PriorityLevel level) { + this.name = name; + this.level = level; + } + + @Override + public PriorityLevel getPriority() { + return level; + } + } + + @Test + void test() { + TreeSet set = new TreeSet<>(); + set.add(new TestEntry("4", PriorityLevel.HIGH)); + set.add(new TestEntry("0", PriorityLevel.NONE)); + set.add(new TestEntry("1", PriorityLevel.LOWEST)); + set.add(new TestEntry("5", PriorityLevel.HIGHEST)); + set.add(new TestEntry("2", PriorityLevel.LOW)); + set.add(new TestEntry("6", PriorityLevel.MAX)); + set.add(new TestEntry("3", PriorityLevel.NORMAL)); + + Iterator iterator = set.iterator(); + + assertPriority(iterator, "0"); + assertPriority(iterator, "1"); + assertPriority(iterator, "2"); + assertPriority(iterator, "3"); + assertPriority(iterator, "4"); + assertPriority(iterator, "5"); + assertPriority(iterator, "6"); + } + + private void assertPriority(Iterator iterator, String name) { + TestEntry entry = iterator.next(); + assertEquals(entry.name, name); + } + +} diff --git a/litecommands-core/test/dev/rollczi/litecommands/schematic/SchematicFastGeneratorTest.java b/litecommands-core/test/dev/rollczi/litecommands/schematic/SchematicFastGeneratorTest.java index 110155849..e39d8340c 100644 --- a/litecommands-core/test/dev/rollczi/litecommands/schematic/SchematicFastGeneratorTest.java +++ b/litecommands-core/test/dev/rollczi/litecommands/schematic/SchematicFastGeneratorTest.java @@ -11,7 +11,7 @@ import dev.rollczi.litecommands.wrapper.WrapperRegistry; import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -124,10 +124,9 @@ void test() { private void assertSchematic(CommandRoute commandRoute, CommandExecutor executor, String... expected) { Schematic schematic = schematicGenerator.generate(new SchematicInput(commandRoute, executor, TestUtil.invocation(""))); List schematicLines = schematic.all(); - assertEquals(expected.length, schematicLines.size()); - for (int i = 0; i < expected.length; i++) { - assertEquals(expected[i], schematicLines.get(i)); - } + + assertThat(schematicLines) + .containsExactly(expected); } } \ No newline at end of file diff --git a/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBaseBuilder.java b/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBaseBuilder.java index 74871179b..7bf61a669 100644 --- a/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBaseBuilder.java +++ b/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBaseBuilder.java @@ -13,11 +13,14 @@ import dev.rollczi.litecommands.bind.BindChainedProvider; import dev.rollczi.litecommands.command.CommandMerger; import dev.rollczi.litecommands.configurator.LiteConfigurator; +import dev.rollczi.litecommands.event.EventPublisher; +import dev.rollczi.litecommands.event.EventListener; +import dev.rollczi.litecommands.event.SimpleEventPublisher; import dev.rollczi.litecommands.context.ContextChainedProvider; import dev.rollczi.litecommands.extension.LiteCommandsProviderExtension; import dev.rollczi.litecommands.extension.annotations.AnnotationsExtension; import dev.rollczi.litecommands.extension.annotations.LiteAnnotationsProcessorExtension; -import dev.rollczi.litecommands.processor.LiteBuilderProcessor; +import dev.rollczi.litecommands.processor.LiteBuilderAction; import dev.rollczi.litecommands.command.executor.CommandExecuteService; import dev.rollczi.litecommands.bind.BindRegistry; import dev.rollczi.litecommands.extension.LiteExtension; @@ -47,6 +50,8 @@ import dev.rollczi.litecommands.schematic.SchematicGenerator; import dev.rollczi.litecommands.schematic.SimpleSchematicGenerator; import dev.rollczi.litecommands.scope.Scope; +import dev.rollczi.litecommands.strict.StrictMode; +import dev.rollczi.litecommands.strict.StrictService; import dev.rollczi.litecommands.suggestion.SuggestionResult; import dev.rollczi.litecommands.suggestion.SuggestionService; import dev.rollczi.litecommands.argument.suggester.SuggesterRegistry; @@ -71,6 +76,7 @@ import java.util.List; import java.util.Set; import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; public class LiteCommandsBaseBuilder> implements LiteCommandsBuilder, @@ -80,8 +86,8 @@ public class LiteCommandsBaseBuilder senderClass; protected final Platform platform; - protected final Set> preProcessors = new LinkedHashSet<>(); - protected final Set> postProcessors = new LinkedHashSet<>(); + protected final Set> preProcessors = new LinkedHashSet<>(); + protected final Set> postProcessors = new LinkedHashSet<>(); protected final List> extensions = new ArrayList<>(); protected final List> commandsProviderExtensions = new ArrayList<>(); @@ -96,8 +102,10 @@ public class LiteCommandsBaseBuilder commandBuilderCollector; protected final MessageRegistry messageRegistry; protected final WrapperRegistry wrapperRegistry; + protected final StrictService strictService; protected Scheduler scheduler; + protected EventPublisher eventPublisher; protected SchematicGenerator schematicGenerator; /** @@ -119,7 +127,8 @@ public LiteCommandsBaseBuilder(Class senderClass, Platform pl new ResultHandleServiceImpl<>(), new CommandBuilderCollector<>(), new MessageRegistry<>(), - new WrapperRegistry() + new WrapperRegistry(), + new StrictService() ); } @@ -140,7 +149,8 @@ public LiteCommandsBaseBuilder( ResultHandleService resultHandleService, CommandBuilderCollector commandBuilderCollector, MessageRegistry messageRegistry, - WrapperRegistry wrapperRegistry + WrapperRegistry wrapperRegistry, + StrictService strictService ) { this.senderClass = senderClass; this.platform = platform; @@ -155,8 +165,10 @@ public LiteCommandsBaseBuilder( this.commandBuilderCollector = commandBuilderCollector; this.messageRegistry = messageRegistry; this.wrapperRegistry = wrapperRegistry; + this.strictService = strictService; this.scheduler = new SchedulerSameThreadImpl(); + this.eventPublisher = new SimpleEventPublisher(bindRegistry); this.schematicGenerator = new SchematicFastGenerator<>(SchematicFormat.angleBrackets(), validatorService, wrapperRegistry); } @@ -234,7 +246,7 @@ public B commands(Object... commands) { * @param commandsProvider provider of commands. */ private void preProcessExtensionsOnProvider(LiteCommandsProvider commandsProvider) { - this.preProcessor((builder, internal) -> { + this.beforeBuild((builder, internal) -> { for (LiteCommandsProviderExtension extension : commandsProviderExtensions) { extension.extendCommandsProvider(this, this, commandsProvider); } @@ -507,6 +519,18 @@ public B schematicGenerator(SchematicFastFormat format) { return this.self(); } + @Override + public B strictMode(StrictMode strictMode) { + this.strictService.setDefaultMode(strictMode); + return this.self(); + } + + @Override + public B listener(EventListener listener) { + this.eventPublisher.subscribe(listener); + return this.self(); + } + @Override public B wrapper(Wrapper wrapper) { this.wrapperRegistry.registerFactory(wrapper); @@ -514,20 +538,20 @@ public B wrapper(Wrapper wrapper) { } @Override - public B selfProcessor(LiteBuilderProcessor processor) { - processor.process(this, this); + public B self(LiteBuilderAction action) { + action.process(this, this); return this.self(); } @Override - public B preProcessor(LiteBuilderProcessor preProcessor) { - this.preProcessors.add(preProcessor); + public B beforeBuild(LiteBuilderAction action) { + this.preProcessors.add(action); return this.self(); } @Override - public B postProcessor(LiteBuilderProcessor postProcessor) { - this.postProcessors.add(postProcessor); + public B afterBuild(LiteBuilderAction action) { + this.postProcessors.add(action); return this.self(); } @@ -566,14 +590,11 @@ public LiteCommands build(boolean register) { throw new IllegalStateException("No platform was set"); } - for (LiteBuilderProcessor processor : preProcessors) { + for (LiteBuilderAction processor : preProcessors) { processor.process(this, this); } - CommandExecuteService commandExecuteService = new CommandExecuteService<>(validatorService, resultHandleService, scheduler, schematicGenerator, parserRegistry, contextRegistry, wrapperRegistry, bindRegistry); - SuggestionService suggestionService = new SuggestionService<>(parserRegistry, suggesterRegistry, validatorService); - CommandManager commandManager = new CommandManager<>(this.platform, commandExecuteService, suggestionService); - + CommandManager commandManager = this.createCommandManager(); CommandMerger commandMerger = new CommandMerger<>(); for (CommandBuilder collected : this.commandBuilderCollector.collectCommands()) { @@ -592,7 +613,7 @@ public LiteCommands build(boolean register) { commandManager.register(mergedCommand); } - for (LiteBuilderProcessor processor : postProcessors) { + for (LiteBuilderAction processor : postProcessors) { processor.process(this, this); } @@ -605,6 +626,13 @@ public LiteCommands build(boolean register) { return liteCommand; } + protected CommandManager createCommandManager() { + CommandExecuteService commandExecuteService = new CommandExecuteService<>(validatorService, resultHandleService, scheduler, schematicGenerator, parserRegistry, contextRegistry, wrapperRegistry, bindRegistry, eventPublisher, strictService); + SuggestionService suggestionService = new SuggestionService<>(parserRegistry, suggesterRegistry, validatorService); + + return new CommandManager<>(this.platform, commandExecuteService, suggestionService); + } + /** * Internal API */ @@ -687,6 +715,12 @@ public MessageRegistry getMessageRegistry() { return this.messageRegistry; } + @Override + @ApiStatus.Internal + public StrictService getStrictService() { + return this.strictService; + } + @Override @ApiStatus.Internal public WrapperRegistry getWrapperRegistry() { diff --git a/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBuilder.java b/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBuilder.java index e84b9901a..bdf0da3e7 100644 --- a/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBuilder.java +++ b/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsBuilder.java @@ -6,9 +6,12 @@ import dev.rollczi.litecommands.bind.BindChainedProvider; import dev.rollczi.litecommands.bind.BindProvider; import dev.rollczi.litecommands.configurator.LiteConfigurator; +import dev.rollczi.litecommands.event.Event; +import dev.rollczi.litecommands.event.EventListener; import dev.rollczi.litecommands.context.ContextChainedProvider; +import dev.rollczi.litecommands.event.Subscriber; import dev.rollczi.litecommands.extension.annotations.AnnotationsExtension; -import dev.rollczi.litecommands.processor.LiteBuilderProcessor; +import dev.rollczi.litecommands.processor.LiteBuilderAction; import dev.rollczi.litecommands.context.ContextProvider; import dev.rollczi.litecommands.extension.LiteExtension; import dev.rollczi.litecommands.editor.Editor; @@ -28,6 +31,7 @@ import dev.rollczi.litecommands.schematic.SchematicGenerator; import dev.rollczi.litecommands.scope.Scope; import dev.rollczi.litecommands.argument.suggester.Suggester; +import dev.rollczi.litecommands.strict.StrictMode; import dev.rollczi.litecommands.suggestion.SuggestionResult; import dev.rollczi.litecommands.validator.Validator; import dev.rollczi.litecommands.validator.ValidatorScope; @@ -199,12 +203,66 @@ default B validatorMarked(Validator validator) { B schematicGenerator(SchematicFastFormat format); - B selfProcessor(LiteBuilderProcessor processor); + /** + * Set the default strict mode for all commands. + * If strict mode is enabled, the command will fail if the user provides too many arguments. + */ + @ApiStatus.Experimental + B strictMode(StrictMode strictMode); + + /** + * Register event listener for the LiteCommands event system. + * See {@link Event}, {@link Subscriber} and {@link EventListener} for more information. + * Example listener:@ + *
+     * {@code
+     *     public class MyListener implements EventListener {
+     *
+     *         \@Subscriber
+     *         public void onEvent(CommandPreExecutionEvent event) {
+     *             // your code
+     *         }
+     *
+     *         \@Subscriber
+     *         public void onEvent(CommandPostExecutionEvent event) {
+     *             // your code
+     *         }
+     *     }
+     * }
+     * 
+ */ + @ApiStatus.Experimental + B listener(EventListener listener); + + /** + * @deprecated use {@link LiteCommandsBuilder#self(LiteBuilderAction)} instead + */ + @Deprecated + default B selfProcessor(LiteBuilderAction processor) { + return self(processor); + } + + /** + * @deprecated use {@link LiteCommandsBuilder#beforeBuild(LiteBuilderAction)} instead + */ + @Deprecated + default B preProcessor(LiteBuilderAction preProcessor) { + return beforeBuild(preProcessor); + } + + /** + * @deprecated use {@link LiteCommandsBuilder#afterBuild(LiteBuilderAction)} instead + */ + @Deprecated + default B postProcessor(LiteBuilderAction postProcessor) { + return afterBuild(postProcessor); + } - B preProcessor(LiteBuilderProcessor preProcessor); + B self(LiteBuilderAction action); - B postProcessor(LiteBuilderProcessor postProcessor); + B beforeBuild(LiteBuilderAction action); + B afterBuild(LiteBuilderAction action); /** * Register extension for this builder. diff --git a/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsFactory.java b/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsFactory.java index ffbf2d122..29b6377ca 100644 --- a/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsFactory.java +++ b/litecommands-framework/src/dev/rollczi/litecommands/LiteCommandsFactory.java @@ -27,7 +27,7 @@ import dev.rollczi.litecommands.context.ContextResult; import dev.rollczi.litecommands.cooldown.CooldownState; import dev.rollczi.litecommands.cooldown.CooldownStateResultHandler; -import dev.rollczi.litecommands.cooldown.CooldownStateValidator; +import dev.rollczi.litecommands.cooldown.CooldownStateController; import dev.rollczi.litecommands.flag.FlagArgument; import dev.rollczi.litecommands.handler.exception.standard.InvocationTargetExceptionHandler; import dev.rollczi.litecommands.handler.exception.standard.LiteCommandsExceptionHandler; @@ -41,6 +41,7 @@ import dev.rollczi.litecommands.invalidusage.InvalidUsageException; import dev.rollczi.litecommands.invalidusage.InvalidUsageExceptionHandler; import dev.rollczi.litecommands.invalidusage.InvalidUsageHandlerImpl; +import dev.rollczi.litecommands.invalidusage.InvalidUsageResultController; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.join.JoinArgument; import dev.rollczi.litecommands.join.JoinStringArgumentResolver; @@ -57,6 +58,7 @@ import static dev.rollczi.litecommands.reflect.type.TypeRange.upwards; import dev.rollczi.litecommands.scheduler.Scheduler; import dev.rollczi.litecommands.scope.Scope; +import dev.rollczi.litecommands.validator.ValidatorExecutionController; import dev.rollczi.litecommands.wrapper.std.CompletableFutureWrapper; import dev.rollczi.litecommands.wrapper.std.OptionWrapper; import dev.rollczi.litecommands.wrapper.std.OptionalWrapper; @@ -91,7 +93,7 @@ private LiteCommandsFactory() { } public static > LiteCommandsBuilder builder(Class senderClass, Platform platform) { - return new LiteCommandsBaseBuilder(senderClass, platform).selfProcessor((builder, internal) -> { + return new LiteCommandsBaseBuilder(senderClass, platform).self((builder, internal) -> { Scheduler scheduler = internal.getScheduler(); MessageRegistry messageRegistry = internal.getMessageRegistry(); ParserRegistry parser = internal.getParserRegistry(); @@ -107,7 +109,10 @@ public static ContextResult.ok(() -> invocation)) // Do not use short method reference here (it will cause bad return type in method reference on Java 8) .validator(Scope.global(), new MissingPermissionValidator<>()) - .validator(Scope.global(), new CooldownStateValidator<>()) + + .listener(new ValidatorExecutionController<>(internal.getValidatorService())) + .listener(new CooldownStateController<>()) + .listener(new InvalidUsageResultController<>(internal.getSchematicGenerator())) .argument(String.class, new StringArgumentResolver<>()) .argument(Boolean.class, new BooleanArgumentResolver<>()) @@ -167,6 +172,7 @@ public static (messageRegistry)) .result(CooldownState.class, new CooldownStateResultHandler<>(messageRegistry)) .result(InvalidUsage.class, new InvalidUsageHandlerImpl<>(messageRegistry)) + ; }); } diff --git a/litecommands-framework/src/dev/rollczi/litecommands/processor/LiteBuilderProcessor.java b/litecommands-framework/src/dev/rollczi/litecommands/processor/LiteBuilderAction.java similarity index 80% rename from litecommands-framework/src/dev/rollczi/litecommands/processor/LiteBuilderProcessor.java rename to litecommands-framework/src/dev/rollczi/litecommands/processor/LiteBuilderAction.java index d6e855d8c..a13012a3d 100644 --- a/litecommands-framework/src/dev/rollczi/litecommands/processor/LiteBuilderProcessor.java +++ b/litecommands-framework/src/dev/rollczi/litecommands/processor/LiteBuilderAction.java @@ -5,7 +5,7 @@ import dev.rollczi.litecommands.platform.PlatformSettings; @FunctionalInterface -public interface LiteBuilderProcessor { +public interface LiteBuilderAction { void process(LiteCommandsBuilder builder, LiteCommandsInternal internal); diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java index 89e9d5dea..c90a86c3d 100644 --- a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java @@ -11,6 +11,7 @@ import dev.rollczi.litecommands.jda.permission.DiscordPermission; import dev.rollczi.litecommands.meta.Meta; import dev.rollczi.litecommands.meta.MetaHolder; +import dev.rollczi.litecommands.priority.PrioritizedList; import dev.rollczi.litecommands.shared.Preconditions; import dev.rollczi.litecommands.wrapper.WrapperRegistry; import net.dv8tion.jda.api.Permission; @@ -160,12 +161,12 @@ private List getPermissions(MetaHolder holder) { } private CommandExecutor translateExecutor(CommandRoute route, TranslateExecutorConsumer consumer) { - List> executors = route.getExecutors(); + PrioritizedList> executors = route.getExecutors(); if (executors.size() != 1) { throw new IllegalArgumentException("Discrod command cannot have more than one executor in same route"); } - CommandExecutor executor = executors.get(0); + CommandExecutor executor = executors.first(); for (Argument argument : executor.getArguments()) { String argumentName = argument.getName(); diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java index 1a21ad120..05fccd5af 100644 --- a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java @@ -6,7 +6,6 @@ import dev.rollczi.litecommands.LiteCommandsException; import dev.rollczi.litecommands.input.raw.RawInput; import dev.rollczi.litecommands.argument.parser.Parser; -import dev.rollczi.litecommands.argument.parser.ParserSet; import dev.rollczi.litecommands.argument.parser.input.ParseableInput; import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invocation.Invocation; @@ -64,8 +63,7 @@ public ParseResult nextArgument(Invocation invo } try { - //TODO: implement parse custom Input to PARSED - throw new LiteJDAParseException("Cannot parse input"); + throw new LiteJDAParseException("Cannot parse input to " + outType.getSimpleName() + " from " + input.getClass().getSimpleName()); } catch (LiteJDAParseException exception) { return parser.parse(invocation, argument, RawInput.of(optionMapping.getAsString().split(" "))); @@ -94,8 +92,8 @@ public JDAInputMatcher copy() { } @Override - public EndResult endMatch() { - if (consumedArguments.size() != arguments.size()) { + public EndResult endMatch(boolean isStrict) { + if (consumedArguments.size() != arguments.size() && isStrict) { return EndResult.failed(InvalidUsage.Cause.MISSING_ARGUMENT); } diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java index 26cce45bd..c3b07adcd 100644 --- a/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java @@ -36,7 +36,7 @@ private LiteJDAFactory() { public static > B builder(JDA jda) { JDAPlatform platform = new JDAPlatform(new LiteJDASettings(), jda); - return (B) LiteCommandsFactory.builder(User.class, platform).selfProcessor((builder, internal) -> builder + return (B) LiteCommandsFactory.builder(User.class, platform).self((builder, internal) -> builder .settings(settings -> settings.translator(createTranslator(internal.getWrapperRegistry()))) .bind(JDA.class, () -> jda) .result(String.class, new StringHandler()) diff --git a/litecommands-minestom/src/dev/rollczi/litecommands/minestom/LiteMinestomFactory.java b/litecommands-minestom/src/dev/rollczi/litecommands/minestom/LiteMinestomFactory.java index ba90406a0..d447ea9f7 100644 --- a/litecommands-minestom/src/dev/rollczi/litecommands/minestom/LiteMinestomFactory.java +++ b/litecommands-minestom/src/dev/rollczi/litecommands/minestom/LiteMinestomFactory.java @@ -33,7 +33,7 @@ public static { + return (B) LiteCommandsFactory.builder(CommandSender.class, new MinestomPlatform(commandManager)).self((builder, internal) -> { MessageRegistry messageRegistry = internal.getMessageRegistry(); builder diff --git a/litecommands-programmatic/src/dev/rollczi/litecommands/programmatic/LiteCommand.java b/litecommands-programmatic/src/dev/rollczi/litecommands/programmatic/LiteCommand.java index b1b3de0f3..854df2a6f 100644 --- a/litecommands-programmatic/src/dev/rollczi/litecommands/programmatic/LiteCommand.java +++ b/litecommands-programmatic/src/dev/rollczi/litecommands/programmatic/LiteCommand.java @@ -13,6 +13,7 @@ import dev.rollczi.litecommands.requirement.BindRequirement; import dev.rollczi.litecommands.requirement.ContextRequirement; import dev.rollczi.litecommands.scheduler.SchedulerPoll; +import dev.rollczi.litecommands.strict.StrictMode; import dev.rollczi.litecommands.wrapper.WrapFormat; import java.util.ArrayList; @@ -29,6 +30,7 @@ public class LiteCommand { protected final String name; protected final List aliases; protected final Meta meta = Meta.create(); + protected final Meta executorMeta = Meta.create(); protected Function, Object> executor = liteContext -> null; protected boolean withExecutor = true; @@ -149,6 +151,18 @@ public LiteCommand withoutExecutor() { return this; } + @ApiStatus.Experimental + public LiteCommand strict(StrictMode strict) { + this.meta.put(Meta.STRICT_MODE, strict); + return this; + } + + @ApiStatus.Experimental + public LiteCommand strictExecutor(StrictMode strict) { + this.executorMeta.put(Meta.STRICT_MODE, strict); + return this; + } + protected void execute(LiteContext context) { Object object = this.executor.apply(context); context.returnResult(object); @@ -173,6 +187,7 @@ CommandBuilder toRoute() { .arguments(arguments) .contextRequirements(contextRequirements) .bindRequirements(bindRequirements) + .apply(commandExecutor -> commandExecutor.meta().apply(this.executorMeta)) .build() ); } diff --git a/litecommands-unit/src/LiteBenchmark.java b/litecommands-unit/src/LiteBenchmark.java new file mode 100644 index 000000000..c83355ef7 --- /dev/null +++ b/litecommands-unit/src/LiteBenchmark.java @@ -0,0 +1,86 @@ +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.unit.LiteCommandsTestFactory; +import dev.rollczi.litecommands.unit.TestPlatform; +import java.time.Duration; +import java.util.Optional; + +public class LiteBenchmark { + + public static final int ITERATIONS = 1_000_000; + static TestPlatform testPlatform; + + @Command(name = "test") + static class TestCommand { + @Execute + void number(@Arg String first, @Arg String second) {} + + @Execute(name = "number") + void execute(@Arg int first, @Arg double second) {} + + @Execute(name = "sub") + void execute(@Arg String first, @Arg Optional second) {} + + @Execute(name = "duration") + void execute(@Arg Duration duration, @Arg String second) {} + } + + public static void main(String[] args) { + testPlatform = LiteCommandsTestFactory.startPlatform(builder -> builder + .commands( + new TestCommand() + ) + ); + + for (int count = 0; count < 3; count++) { + System.out.println("Iteration: " + count); + Stopper stopper = new Stopper("string"); + for (int i = 0; i < ITERATIONS; i++) { + testPlatform.execute("test first second"); + } + stopper.stop(); + + stopper = new Stopper("number"); + for (int i = 0; i < ITERATIONS; i++) { + testPlatform.execute("test number 1 2"); + } + stopper.stop(); + + stopper = new Stopper("sub"); + for (int i = 0; i < ITERATIONS; i++) { + testPlatform.execute("test sub first"); + } + stopper.stop(); + + stopper = new Stopper("sub optional"); + for (int i = 0; i < ITERATIONS; i++) { + testPlatform.execute("test sub first second"); + } + stopper.stop(); + + stopper = new Stopper("duration"); + for (int i = 0; i < ITERATIONS; i++) { + testPlatform.execute("test duration 1m1s second"); + } + stopper.stop(); + } + } + + + public static class Stopper { + private final String name; + private final long start; + + public Stopper(String name) { + this.name = name; + this.start = System.nanoTime(); + } + + public void stop() { + long end = System.nanoTime(); + System.out.println("Time: " + (end - start) / 1000000.0 + "ms, name: " + name); + } + } + +} diff --git a/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertExecute.java b/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertExecute.java index 939900815..a5630048f 100644 --- a/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertExecute.java +++ b/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertExecute.java @@ -98,11 +98,11 @@ public AssertExecute assertThrows(Class exception) { } public AssertExecute assertFailure() { - if (!result.isFailed()) { - if (result.isThrown()) { - throw new AssertionError("Command was thrown", result.getThrowable()); - } + if (result.isThrown()) { + throw new AssertionError("Command was thrown", result.getThrowable()); + } + if (result.isSuccessful()) { throw new AssertionError("Command was not failed. Result: " + result.getResult()); } @@ -110,8 +110,12 @@ public AssertExecute assertFailure() { } public T assertFailedAs(Class type) { - if (!result.isFailed()) { - throw new AssertionError("Command was not failed."); + if (result.isSuccessful()) { + throw new AssertionError("Command was not failed. Result: " + result.getResult()); + } + + if (result.isThrown()) { + throw new AssertionError("Command was thrown", result.getThrowable()); } Object error = result.getError();