From 23f7d1a55065989bfd6e8f5cbecffc08092a0988 Mon Sep 17 00:00:00 2001 From: kento2 Date: Sun, 9 Aug 2026 18:14:24 +0200 Subject: [PATCH] bump version to 0.38-SNAPSHOT + pass input to SuggestionProvider + rewrite command parsing + ... --- MODULE.bazel | 2 - .../PlayerSuggestionProvider.java | 2 +- .../type/OfflinePlayerArgumentType.java | 20 ++-- .../type/PlayerArgumentType.java | 22 +++- .../kencommandapi/type/WorldArgumentType.java | 19 +-- core/BUILD | 4 + .../kencommandapi/api/CommandHandler.java | 63 +++++++--- .../api/argument/ArgumentType.java | 4 +- .../api/argument/CommandArgumentSpec.java | 5 +- .../api/argument/CommandArgumentSpecImpl.java | 14 ++- .../argument/types/BooleanArgumentType.java | 22 ++++ .../argument/types/DoubleArgumentType.java | 19 +++ .../argument/types/IntegerArgumentType.java | 7 +- .../argument/types/StringArgumentType.java | 6 +- .../api/suggestion/NoSuggestionProvider.java | 2 +- .../api/suggestion/SuggestionProvider.java | 2 +- .../internal/CommandContextBuilder.java | 26 +++++ .../internal/CommandContextImpl.java | 3 +- .../internal/NodeTreeWalker.java | 101 ++++++++++++++++ .../kencommandapi/internal/WalkResult.java | 80 +++++++++++++ .../internal/parser/CommandParseHelper.java | 28 ----- .../parser/InvocationCommandParser.java | 108 ------------------ .../parser/SuggestionCommandParser.java | 99 ---------------- defs.bzl | 2 +- .../PlayerSuggestionProvider.java | 2 +- .../type/PlayerArgumentType.java | 22 +++- 26 files changed, 382 insertions(+), 302 deletions(-) create mode 100644 core/src/main/java/de/kentoj/kencommandapi/api/argument/types/BooleanArgumentType.java create mode 100644 core/src/main/java/de/kentoj/kencommandapi/api/argument/types/DoubleArgumentType.java create mode 100644 core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextBuilder.java create mode 100644 core/src/main/java/de/kentoj/kencommandapi/internal/NodeTreeWalker.java create mode 100644 core/src/main/java/de/kentoj/kencommandapi/internal/WalkResult.java delete mode 100644 core/src/main/java/de/kentoj/kencommandapi/internal/parser/CommandParseHelper.java delete mode 100644 core/src/main/java/de/kentoj/kencommandapi/internal/parser/InvocationCommandParser.java delete mode 100644 core/src/main/java/de/kentoj/kencommandapi/internal/parser/SuggestionCommandParser.java diff --git a/MODULE.bazel b/MODULE.bazel index 6dce93b..a792429 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,8 +1,6 @@ bazel_dep(name = "rules_java", version = "9.7.0") bazel_dep(name = "rules_jvm_external", version = "7.1") -VERSION = "0.36-SNAPSHOT" - git_override( module_name = "rules_jvm_external", remote = "http://forge.kentoj.de/scrow/rules_jvm_external.git", diff --git a/bukkit/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java b/bukkit/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java index c6880c6..ac779b8 100644 --- a/bukkit/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java +++ b/bukkit/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java @@ -8,7 +8,7 @@ import java.util.List; public final class PlayerSuggestionProvider implements SuggestionProvider { @Override - public List suggest(T __) { + public List suggest(T __, String ___) { return Bukkit.getOnlinePlayers().stream() .map(Player::getName) .toList(); diff --git a/bukkit/src/main/java/de/kentoj/kencommandapi/type/OfflinePlayerArgumentType.java b/bukkit/src/main/java/de/kentoj/kencommandapi/type/OfflinePlayerArgumentType.java index 5b17dd0..9e5a500 100644 --- a/bukkit/src/main/java/de/kentoj/kencommandapi/type/OfflinePlayerArgumentType.java +++ b/bukkit/src/main/java/de/kentoj/kencommandapi/type/OfflinePlayerArgumentType.java @@ -1,5 +1,7 @@ package de.kentoj.kencommandapi.type; +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider; @@ -11,20 +13,16 @@ import java.util.UUID; public class OfflinePlayerArgumentType implements ArgumentType { @Override - public OfflinePlayer parseInputUnchecked(String string) { + public Result parseInputUnchecked(String string) { // UUIDs are always 36 chars - if (string.length() == 36) { - try { - return Bukkit.getOfflinePlayer(UUID.fromString(string)); - } catch (IllegalArgumentException ex) { - throw new IllegalArgumentException("Invalid UUID"); - } - } + if (string.length() == 36) + return Results.ofCallable(() -> Bukkit.getOfflinePlayer(UUID.fromString(string))) + .mapFailure(_ -> "Invalid UUID."); - OfflinePlayer player = Bukkit.getPlayerExact(string); + var player = Bukkit.getPlayerExact(string); if (player == null || (!player.hasPlayedBefore() && !player.isOnline())) - throw new IllegalArgumentException("Player not found"); - return player; + return Results.failure("Player not found."); + return Results.success(player); } @Override diff --git a/bukkit/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java b/bukkit/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java index 2b4065e..6264de0 100644 --- a/bukkit/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java +++ b/bukkit/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java @@ -1,18 +1,32 @@ package de.kentoj.kencommandapi.type; +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider; import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import java.util.UUID; + public class PlayerArgumentType implements ArgumentType { @Override - public Player parseInputUnchecked(String string) { - var player = Bukkit.getPlayer(string); - if (player == null) throw new IllegalArgumentException("player not online"); - return player; + public Result parseInputUnchecked(String input) { + // UUIDs are always 36 chars + Player player; + if (input.length() == 36) { + try { + player = Bukkit.getPlayer(UUID.fromString(input)); + } catch (IllegalArgumentException _) { + return Results.failure("Invalid UUID."); + } + } else { + player = Bukkit.getPlayer(input); + } + + return Results.ofNullable(player, "Player not online."); } @Override diff --git a/bukkit/src/main/java/de/kentoj/kencommandapi/type/WorldArgumentType.java b/bukkit/src/main/java/de/kentoj/kencommandapi/type/WorldArgumentType.java index f51e7b1..6e162fc 100644 --- a/bukkit/src/main/java/de/kentoj/kencommandapi/type/WorldArgumentType.java +++ b/bukkit/src/main/java/de/kentoj/kencommandapi/type/WorldArgumentType.java @@ -1,5 +1,7 @@ package de.kentoj.kencommandapi.type; +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import org.bukkit.Bukkit; @@ -10,21 +12,24 @@ import java.util.stream.Stream; public class WorldArgumentType implements ArgumentType { @Override - public World parseInputUnchecked(String input) throws IllegalArgumentException { - var world = Bukkit.getWorld(input); - if (world == null && input.length() == 36) { + public Result parseInputUnchecked(String input) throws IllegalArgumentException { + World world; + if (input.length() == 36) { try { - return Bukkit.getWorld(UUID.fromString(input)); + world = Bukkit.getWorld(UUID.fromString(input)); } catch (IllegalArgumentException _) { - throw new IllegalArgumentException("World not found"); + return Results.failure("Invalid UUID."); } + } else { + world = Bukkit.getWorld(input); } - return world; + + return Results.ofNullable(world, "World not found"); } @Override public SuggestionProvider getDefaultSuggestionProvider() { - return _ -> Stream.concat( + return (_, _) -> Stream.concat( Bukkit.getWorlds().stream().map(World::getName), Bukkit.getWorlds().stream().map(w -> w.getUID().toString()) ).toList(); diff --git a/core/BUILD b/core/BUILD index 2fc5dc4..17ddb95 100644 --- a/core/BUILD +++ b/core/BUILD @@ -14,4 +14,8 @@ java_export( artifact("org.slf4j:slf4j-api"), artifact("net.kyori:adventure-api"), ], + exports = [ + artifact("com.leakyabstractions:result-api"), + artifact("com.leakyabstractions:result"), + ], ) \ No newline at end of file diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/CommandHandler.java b/core/src/main/java/de/kentoj/kencommandapi/api/CommandHandler.java index f728250..c9d6af0 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/CommandHandler.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/CommandHandler.java @@ -1,47 +1,72 @@ package de.kentoj.kencommandapi.api; +import de.kentoj.kencommandapi.api.literal.Literal; import de.kentoj.kencommandapi.api.literal.RootLiteral; import de.kentoj.kencommandapi.api.platform.HasPermissionMethod; import de.kentoj.kencommandapi.api.platform.SendMessageMethod; -import de.kentoj.kencommandapi.internal.parser.CommandParseHelper; -import de.kentoj.kencommandapi.internal.parser.InvocationCommandParser; -import de.kentoj.kencommandapi.internal.parser.SuggestionCommandParser; +import de.kentoj.kencommandapi.internal.NodeTreeWalker; +import de.kentoj.kencommandapi.internal.WalkResult; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; + public class CommandHandler { private final SendMessageMethod sendMessageMethod; - private final InvocationCommandParser invocationParser; - private final SuggestionCommandParser suggestionParser; + private final NodeTreeWalker nodeTreeWalker; public CommandHandler(SendMessageMethod sendMessageMethod, HasPermissionMethod hasPermissionMethod) { this.sendMessageMethod = sendMessageMethod; - var parseHelper = new CommandParseHelper<>(hasPermissionMethod); - invocationParser = new InvocationCommandParser<>(parseHelper); - suggestionParser = new SuggestionCommandParser<>(parseHelper); + nodeTreeWalker = new NodeTreeWalker<>(hasPermissionMethod); } public void invoke(RootLiteral rootLiteral, T sender, Stream args) { - invocationParser.parse(rootLiteral, sender, args.toList().iterator()) - .ifSuccessOrElse(data -> exec(rootLiteral, data), - err -> sendMessageMethod.send(sender, rootLiteral.messageStyle().err(err))); - } + var walkResult = nodeTreeWalker.walk(rootLiteral, sender, args.iterator()); + if (!walkResult.isValidUsage()) { + sendMessageMethod.send(sender, rootLiteral.messageStyle().err(walkResult.message())); + return; + } - private void exec(RootLiteral rootLiteral, InvocationCommandParser.ExecutionData data) { - var ctx = data.context(); - data.executor().execute(ctx) + requireNonNull(walkResult.lastLiteral().executor()) + .execute(walkResult.ctx()) .whenComplete((result, ex) -> { if (ex != null) - sendMessageMethod.send(ctx.sender(), rootLiteral.messageStyle().exception(ex)); + sendMessageMethod.send(sender, rootLiteral.messageStyle().exception(ex)); result.ifFailure(err -> - sendMessageMethod.send(ctx.sender(), rootLiteral.messageStyle().err(err))); + sendMessageMethod.send(sender, rootLiteral.messageStyle().err(err))); }); } - public List getSuggestions(RootLiteral rootNode, T sender, String[] args, boolean trailingSpace) { - return suggestionParser.suggest(rootNode, sender, Arrays.stream(args).iterator(), trailingSpace); + public List getSuggestions(RootLiteral rootLiteral, T sender, String[] args, boolean trailingSpace) { + var walkResult = nodeTreeWalker.walk(rootLiteral, sender, Arrays.stream(args).iterator()); + Stream stream; + String input = null; + switch (walkResult) { + case WalkResult.LiteralUnknown res -> { + stream = res.lastLiteral().literals().stream().map(Literal::names).flatMap(Arrays::stream); + input = res.input(); + } + case WalkResult.ArgumentIllegal res -> { + stream = res.arg().suggestionProvider().suggest(sender, res.input()).stream(); + input = res.input(); + } + case WalkResult.LiteralExpected res when trailingSpace -> + stream = res.lastLiteral().literals().stream().map(Literal::names).flatMap(Arrays::stream); + case WalkResult.ArgumentExpected res when trailingSpace -> + stream = res.arg().suggestionProvider().suggest(sender, "").stream(); + default -> { + return Collections.emptyList(); + } + } + + if (input == null) return stream.toList(); + final var finalInput = input; + return stream + .filter(s -> s.startsWith(finalInput)) + .toList(); } } \ No newline at end of file diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/argument/ArgumentType.java b/core/src/main/java/de/kentoj/kencommandapi/api/argument/ArgumentType.java index 360a3fc..4651703 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/argument/ArgumentType.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/argument/ArgumentType.java @@ -1,5 +1,6 @@ package de.kentoj.kencommandapi.api.argument; +import com.leakyabstractions.result.api.Result; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import java.util.function.Predicate; @@ -10,11 +11,10 @@ public interface ArgumentType { * parses the given input and returns it. * Unlike {@link CommandArgumentSpec#parseInput(String)}, this does not check any additional requirements. * - * @throws IllegalArgumentException if string invalid * @see CommandArgumentSpec#parseInput(String) * @see CommandArgumentSpecBuilder#withRequirement(Predicate, String) */ - V parseInputUnchecked(String input) throws IllegalArgumentException; + Result parseInputUnchecked(String input); /** * @see CommandArgumentSpec#suggestionProvider() diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpec.java b/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpec.java index 726ad14..2868813 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpec.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpec.java @@ -1,5 +1,6 @@ package de.kentoj.kencommandapi.api.argument; +import com.leakyabstractions.result.api.Result; import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import org.jetbrains.annotations.NotNull; @@ -18,9 +19,9 @@ public sealed interface CommandArgumentSpec permits CommandArgumentSpecImp ArgumentType type(); /** - * @throws IllegalArgumentException if string invalid or if a requirement is not met + * @return Result of either the parsed value or a message describing the error */ - V parseInput(String rawInput); + Result parseInput(String rawInput); @Nullable Function, V> defaultValueProvider(); diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpecImpl.java b/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpecImpl.java index 2f80387..4169a53 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpecImpl.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/argument/CommandArgumentSpecImpl.java @@ -1,5 +1,7 @@ package de.kentoj.kencommandapi.api.argument; +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import org.jetbrains.annotations.NotNull; @@ -17,13 +19,15 @@ public record CommandArgumentSpecImpl( ) implements CommandArgumentSpec { @Override - public V parseInput(String rawInput) { - var parsed = type.parseInputUnchecked(rawInput); + public Result parseInput(String rawInput) { + var parseResult = type.parseInputUnchecked(rawInput); + if (parseResult.hasFailure()) + return parseResult.mapSuccess(_ -> null); for (var requirement : requirements) { - if (!requirement.predicate().test(parsed)) - throw new IllegalArgumentException(requirement.message()); + if (!requirement.predicate().test(parseResult.getSuccess().orElseThrow())) + return Results.failure(requirement.message()); } - return parsed; + return parseResult; } @Override diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/BooleanArgumentType.java b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/BooleanArgumentType.java new file mode 100644 index 0000000..437482b --- /dev/null +++ b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/BooleanArgumentType.java @@ -0,0 +1,22 @@ +package de.kentoj.kencommandapi.api.argument.types; + +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; +import de.kentoj.kencommandapi.api.argument.ArgumentType; +import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; + +import java.util.List; + +public class BooleanArgumentType implements ArgumentType { + + @Override + public Result parseInputUnchecked(String input) { + return Results.ofCallable(() -> Boolean.parseBoolean(input)) + .mapFailure(_ -> "Must be true or false."); + } + + @Override + public SuggestionProvider getDefaultSuggestionProvider() { + return (_, _) -> List.of("true", "false"); + } +} diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/DoubleArgumentType.java b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/DoubleArgumentType.java new file mode 100644 index 0000000..46e2341 --- /dev/null +++ b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/DoubleArgumentType.java @@ -0,0 +1,19 @@ +package de.kentoj.kencommandapi.api.argument.types; + +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; +import de.kentoj.kencommandapi.api.argument.ArgumentType; +import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; + +public class DoubleArgumentType implements ArgumentType { + @Override + public Result parseInputUnchecked(String input) { + return Results.ofCallable(() -> Double.parseDouble(input)) + .mapFailure(_ -> "Not a valid double."); + } + + @Override + public SuggestionProvider getDefaultSuggestionProvider() { + return SuggestionProvider.none(); + } +} diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/IntegerArgumentType.java b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/IntegerArgumentType.java index 5f81e03..8190191 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/IntegerArgumentType.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/IntegerArgumentType.java @@ -1,13 +1,16 @@ package de.kentoj.kencommandapi.api.argument.types; +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; public class IntegerArgumentType implements ArgumentType { @Override - public Integer parseInputUnchecked(String string) { - return Integer.parseInt(string); + public Result parseInputUnchecked(String string) { + return Results.ofCallable(() -> Integer.parseInt(string)) + .mapFailure(_ -> "Not a number."); } @Override diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/StringArgumentType.java b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/StringArgumentType.java index f90488b..3bd9673 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/StringArgumentType.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/argument/types/StringArgumentType.java @@ -1,5 +1,7 @@ package de.kentoj.kencommandapi.api.argument.types; +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; @@ -10,8 +12,8 @@ public record StringArgumentType(boolean greedy) implements ArgumentType parseInputUnchecked(String string) { + return Results.success(string); } @Override diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/NoSuggestionProvider.java b/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/NoSuggestionProvider.java index 44bb856..2b20d06 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/NoSuggestionProvider.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/NoSuggestionProvider.java @@ -5,7 +5,7 @@ import java.util.List; class NoSuggestionProvider implements SuggestionProvider { @Override - public List suggest(T __) { + public List suggest(T __, String ___) { return Collections.emptyList(); } } diff --git a/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/SuggestionProvider.java b/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/SuggestionProvider.java index 135f3aa..6a3c1f7 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/SuggestionProvider.java +++ b/core/src/main/java/de/kentoj/kencommandapi/api/suggestion/SuggestionProvider.java @@ -11,5 +11,5 @@ public interface SuggestionProvider { /** * @return an unfiltered(no permission checks or filtering by input) list of strings that may be used for the argument/literal */ - List suggest(T sender); + List suggest(T sender, String input); } diff --git a/core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextBuilder.java b/core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextBuilder.java new file mode 100644 index 0000000..3c4bca9 --- /dev/null +++ b/core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextBuilder.java @@ -0,0 +1,26 @@ +package de.kentoj.kencommandapi.internal; + +import de.kentoj.kencommandapi.api.argument.CommandArgument; +import de.kentoj.kencommandapi.api.argument.CommandArgumentImpl; +import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; +import de.kentoj.kencommandapi.api.invocation.CommandContext; + +import java.util.Map; + +public class CommandContextBuilder { + private final T sender; + private final Map> parsedArgument; + + public CommandContextBuilder(T sender, Map> parsedArgument) { + this.sender = sender; + this.parsedArgument = parsedArgument; + } + + public void addParsed(CommandArgumentSpec arg, S value) { + parsedArgument.put(arg.id(), new CommandArgumentImpl<>(value, arg)); + } + + public CommandContext build() { + return new CommandContextImpl<>(sender, parsedArgument); + } +} diff --git a/core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextImpl.java b/core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextImpl.java index 5b8db98..98513ce 100644 --- a/core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextImpl.java +++ b/core/src/main/java/de/kentoj/kencommandapi/internal/CommandContextImpl.java @@ -5,11 +5,10 @@ import de.kentoj.kencommandapi.api.invocation.CommandContext; import java.util.Map; -public record CommandContextImpl( +record CommandContextImpl( T sender, Map> parsedArguments ) implements CommandContext { - @Override public CommandArgument getParsedArgument(String id) { try { diff --git a/core/src/main/java/de/kentoj/kencommandapi/internal/NodeTreeWalker.java b/core/src/main/java/de/kentoj/kencommandapi/internal/NodeTreeWalker.java new file mode 100644 index 0000000..315b54b --- /dev/null +++ b/core/src/main/java/de/kentoj/kencommandapi/internal/NodeTreeWalker.java @@ -0,0 +1,101 @@ +package de.kentoj.kencommandapi.internal; + +import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; +import de.kentoj.kencommandapi.api.literal.Literal; +import de.kentoj.kencommandapi.api.literal.RootLiteral; +import de.kentoj.kencommandapi.api.platform.HasPermissionMethod; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Iterator; + +import static java.util.Objects.requireNonNull; + +public class NodeTreeWalker { + + private final HasPermissionMethod hasPermission; + + public NodeTreeWalker(HasPermissionMethod hasPermission) { + this.hasPermission = hasPermission; + } + + public WalkResult walk( + RootLiteral rootLiteral, + T sender, + Iterator iter + ) { + var cursor = new Cursor(iter); + var ctxBuilder = new CommandContextBuilder<>(sender, new HashMap<>()); + Literal literal = rootLiteral; + String token; + + while (true) { + for (var arg : literal.arguments()) { + @SuppressWarnings("unchecked") + var res = walkArgument(cursor, literal, ctxBuilder, (CommandArgumentSpec) arg); + if (res != null) + return res; + } + + token = cursor.nextToken(); + if (token == null) + break; + var nextLiteral = literal.getLiteral(token); + if (nextLiteral == null) + return new WalkResult.LiteralUnknown<>(ctxBuilder.build(), literal, token); + if (nextLiteral.permission() != null && hasPermission.test(sender, nextLiteral.permission())) + return new WalkResult.LiteralUnknown<>(ctxBuilder.build(), literal, token); + literal = nextLiteral; + } + + if (!literal.literals().isEmpty()) { + var isRequired = literal.executor() == null; + return new WalkResult.LiteralExpected<>(ctxBuilder.build(), literal, isRequired); + } + + return new WalkResult.Complete<>(ctxBuilder.build(), literal); + } + + private @Nullable WalkResult walkArgument( + Cursor cursor, + Literal literal, + CommandContextBuilder ctxBuilder, + CommandArgumentSpec arg + ) { + var rawInput = arg.type().greedy() ? cursor.remainingTokens() : cursor.nextToken(); + if (rawInput == null) { + if (arg.defaultValueProvider() == null) + return new WalkResult.ArgumentExpected<>(ctxBuilder.build(), literal, false, arg); + var value = requireNonNull(arg.defaultValueProvider()).apply(ctxBuilder.build()); + ctxBuilder.addParsed(arg, value); + return null; + } + + var parseResult = arg.parseInput(rawInput); + if (parseResult.hasFailure()) + return new WalkResult.ArgumentIllegal<>(ctxBuilder.build(), literal, + false, arg, rawInput, parseResult.getFailure().orElseThrow()); + ctxBuilder.addParsed(arg, parseResult.getSuccess().orElseThrow()); + return null; + } + + private static class Cursor { + private final Iterator tokenIter; + + public Cursor(Iterator tokenIter) { + this.tokenIter = tokenIter; + } + + public @Nullable String nextToken() { + if (!tokenIter.hasNext()) + return null; + return tokenIter.next(); + } + + public @Nullable String remainingTokens() { + var builder = new StringBuilder(); + tokenIter.forEachRemaining(s -> builder.append(' ').append(s)); + return builder.toString(); + } + } +} diff --git a/core/src/main/java/de/kentoj/kencommandapi/internal/WalkResult.java b/core/src/main/java/de/kentoj/kencommandapi/internal/WalkResult.java new file mode 100644 index 0000000..c861188 --- /dev/null +++ b/core/src/main/java/de/kentoj/kencommandapi/internal/WalkResult.java @@ -0,0 +1,80 @@ +package de.kentoj.kencommandapi.internal; + +import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; +import de.kentoj.kencommandapi.api.invocation.CommandContext; +import de.kentoj.kencommandapi.api.literal.Literal; +import org.jetbrains.annotations.Nullable; + +public sealed interface WalkResult { + CommandContext ctx(); + + Literal lastLiteral(); + + boolean isValidUsage(); + + @Nullable String message(); + + record ArgumentIllegal( + CommandContext ctx, + Literal lastLiteral, + boolean isValidUsage, + CommandArgumentSpec arg, + String input, + String message + ) implements WalkResult { + } + + record ArgumentExpected( + CommandContext ctx, + Literal lastLiteral, + boolean isValidUsage, + CommandArgumentSpec arg + ) implements WalkResult { + @Override + public String message() { + return "Argument expected."; + } + } + + record LiteralExpected( + CommandContext ctx, + Literal lastLiteral, + boolean isValidUsage + ) implements WalkResult { + @Override + public String message() { + return "Literal expected."; + } + } + + record LiteralUnknown( + CommandContext ctx, + Literal lastLiteral, + String input + ) implements WalkResult { + @Override + public boolean isValidUsage() { + return false; + } + + @Override + public String message() { + return "Unknown literal \"" + input + "\"."; + } + } + + record Complete( + CommandContext ctx, + Literal lastLiteral + ) implements WalkResult { + @Override + public boolean isValidUsage() { + return true; + } + + @Override + public @Nullable String message() { + return null; + } + } +} diff --git a/core/src/main/java/de/kentoj/kencommandapi/internal/parser/CommandParseHelper.java b/core/src/main/java/de/kentoj/kencommandapi/internal/parser/CommandParseHelper.java deleted file mode 100644 index 17b7774..0000000 --- a/core/src/main/java/de/kentoj/kencommandapi/internal/parser/CommandParseHelper.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.kentoj.kencommandapi.internal.parser; - -import de.kentoj.kencommandapi.api.literal.Literal; -import de.kentoj.kencommandapi.api.platform.HasPermissionMethod; - -import java.util.Iterator; - -public final class CommandParseHelper { - private final HasPermissionMethod hasPermission; - - public CommandParseHelper(HasPermissionMethod hasPermission) { - this.hasPermission = hasPermission; - } - - public boolean hasPermission(T sender, Literal literal) { - if (literal.permission() == null) return true; - return hasPermission.test(sender, literal.permission()); - } - - public String parseInput(Iterator tokenIter, boolean isGreedy) { - if (!isGreedy) - return tokenIter.next(); - var builder = new StringBuilder(); - tokenIter.forEachRemaining(tok -> builder.append(" ").append(tok)); - builder.deleteCharAt(0); - return builder.toString(); - } -} \ No newline at end of file diff --git a/core/src/main/java/de/kentoj/kencommandapi/internal/parser/InvocationCommandParser.java b/core/src/main/java/de/kentoj/kencommandapi/internal/parser/InvocationCommandParser.java deleted file mode 100644 index 8809dea..0000000 --- a/core/src/main/java/de/kentoj/kencommandapi/internal/parser/InvocationCommandParser.java +++ /dev/null @@ -1,108 +0,0 @@ -package de.kentoj.kencommandapi.internal.parser; - -import com.leakyabstractions.result.api.Result; -import com.leakyabstractions.result.core.Results; -import de.kentoj.kencommandapi.api.argument.CommandArgument; -import de.kentoj.kencommandapi.api.argument.CommandArgumentImpl; -import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; -import de.kentoj.kencommandapi.api.invocation.CommandContext; -import de.kentoj.kencommandapi.api.invocation.CommandExecutor; -import de.kentoj.kencommandapi.api.literal.Literal; -import de.kentoj.kencommandapi.internal.CommandContextImpl; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import static java.util.Objects.requireNonNull; - -public class InvocationCommandParser { - - private final CommandParseHelper helper; - - public InvocationCommandParser(CommandParseHelper helper) { - this.helper = helper; - } - - public Result parse( - Literal rootLiteral, - T sender, - Iterator tokenIter - ) { - Literal currentLiteral = rootLiteral; - var ctx = new CommandContextImpl<>(sender, new HashMap<>()); - - while (true) { - { - var result = processArguments(currentLiteral, ctx, tokenIter) - .ifSuccess(ctx.parsedArguments()::putAll); - if (result.hasFailure()) - return result.mapSuccess(_ -> null); - } - - if (!tokenIter.hasNext()) { - if (currentLiteral.executor() == null) - return Results.failure("Literal expected"); - break; - } - - var literalName = tokenIter.next(); - var nextLiteral = currentLiteral.getLiteral(literalName); - var isIncompleteOrUnknown = nextLiteral == null || !helper.hasPermission(sender, nextLiteral); - if (isIncompleteOrUnknown) - return Results.failure("Unknown literal: " + literalName); - currentLiteral = nextLiteral; - } - - return Results.success(new ExecutionData(currentLiteral.executor(), ctx)); - } - - private Result>, String> processArguments( - Literal literal, - CommandContext ctx, - Iterator tokenIter - ) { - Map> parsedArguments = new HashMap<>(); - - for (var _argument : literal.arguments()) { - @SuppressWarnings("unchecked") - var argument = ((CommandArgumentSpec) _argument); - - Object value; - if (!tokenIter.hasNext()) { - if (argument.defaultValueProvider() == null) - return Results.failure("Argument expected: " + argument.id()); - value = requireNonNull(argument.defaultValueProvider()).apply(ctx); - } else { - var input = helper.parseInput(tokenIter, argument.type().greedy()); - try { - value = argument.parseInput(input); - } catch (IllegalArgumentException ex) { - return Results.failure("Illegal argument '" + input + "' for " + argument.id() + ": " + ex.getMessage()); - } - } - - parsedArguments.put(argument.id(), new CommandArgumentImpl<>(value, argument)); - } - - return Results.success(parsedArguments); - } - - public class ExecutionData { - private final CommandExecutor executor; - private final CommandContext context; - - public ExecutionData(CommandExecutor executor, CommandContext context) { - this.executor = executor; - this.context = context; - } - - public CommandExecutor executor() { - return executor; - } - - public CommandContext context() { - return context; - } - } -} diff --git a/core/src/main/java/de/kentoj/kencommandapi/internal/parser/SuggestionCommandParser.java b/core/src/main/java/de/kentoj/kencommandapi/internal/parser/SuggestionCommandParser.java deleted file mode 100644 index 088a740..0000000 --- a/core/src/main/java/de/kentoj/kencommandapi/internal/parser/SuggestionCommandParser.java +++ /dev/null @@ -1,99 +0,0 @@ -package de.kentoj.kencommandapi.internal.parser; - -import de.kentoj.kencommandapi.api.literal.Literal; -import org.jetbrains.annotations.Nullable; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.stream.Stream; - -public class SuggestionCommandParser { - - private final CommandParseHelper helper; - - public SuggestionCommandParser(CommandParseHelper helper) { - this.helper = helper; - } - - public List suggest( - Literal rootLiteral, - T sender, - Iterator tokenIter, - boolean suggestNext - ) { - Literal currentLiteral = rootLiteral; - Literal previousLiteral = null; - - while (true) { - var argumentSuggestions = processArguments(currentLiteral, tokenIter, sender, suggestNext); - if (argumentSuggestions != null) - return argumentSuggestions; - - if (!tokenIter.hasNext()) { - if (suggestNext || previousLiteral == null) - return suggestions(currentLiteral, sender).toList(); - return suggestions(previousLiteral, sender).toList(); - } - - var input = tokenIter.next(); - var nextLiteral = currentLiteral.getLiteral(input); - var isIncompleteOrUnknown = nextLiteral == null || !helper.hasPermission(sender, nextLiteral); - if (isIncompleteOrUnknown) { - var finalInput = input.toLowerCase(); - return suggestions(currentLiteral, sender) - .filter(name -> name.toLowerCase().startsWith(finalInput)) - .toList(); - } - - previousLiteral = currentLiteral; - currentLiteral = nextLiteral; - } - } - - private @Nullable List processArguments( - Literal literal, - Iterator tokenIter, - T sender, - boolean suggestNext - ) { - var argsIter = literal.arguments().iterator(); - - /* - for each argument: - if no next token: suggest current - parse token - if not last token: skip - if nextToken: - if has next argument: suggest next argument - else: return null - return current argument's suggestions filtered - - */ - - while (argsIter.hasNext()) { - var argument = argsIter.next(); - if (!tokenIter.hasNext()) - return argument.suggestionProvider().suggest(sender); - var input = helper.parseInput(tokenIter, argument.type().greedy()).toLowerCase(); - if (tokenIter.hasNext()) - continue; - if (suggestNext) { - if (argsIter.hasNext()) - return argsIter.next().suggestionProvider().suggest(sender); - return null; - } - return argument.suggestionProvider().suggest(sender).stream() - .filter(suggestion -> suggestion.toLowerCase().startsWith(input)) - .toList(); - } - return null; - } - - private Stream suggestions(Literal literal, T sender) { - return literal.literals().stream() - .filter(lit -> helper.hasPermission(sender, lit)) - .map(Literal::names) - .flatMap(Arrays::stream); - } -} diff --git a/defs.bzl b/defs.bzl index 3dce00c..0ad48ea 100644 --- a/defs.bzl +++ b/defs.bzl @@ -1,2 +1,2 @@ GROUP = "de.kentoj.scrow" -VERSION = "0.37-SNAPSHOT" \ No newline at end of file +VERSION = "0.38-SNAPSHOT" \ No newline at end of file diff --git a/velocity/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java b/velocity/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java index 44fc5f8..91167d8 100644 --- a/velocity/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java +++ b/velocity/src/main/java/de/kentoj/kencommandapi/suggestionprovider/PlayerSuggestionProvider.java @@ -16,7 +16,7 @@ public final class PlayerSuggestionProvider implements } @Override - public List suggest(T __) { + public List suggest(T __, String ___) { return proxyServer.getAllPlayers().stream() .map(Player::getUsername) .toList(); diff --git a/velocity/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java b/velocity/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java index d3a8dbb..8bf2e7e 100644 --- a/velocity/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java +++ b/velocity/src/main/java/de/kentoj/kencommandapi/type/PlayerArgumentType.java @@ -1,5 +1,7 @@ package de.kentoj.kencommandapi.type; +import com.leakyabstractions.result.api.Result; +import com.leakyabstractions.result.core.Results; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; @@ -7,6 +9,8 @@ import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.suggestionprovider.PlayerSuggestionProvider; +import java.util.UUID; + public class PlayerArgumentType implements ArgumentType { private final ProxyServer server; @@ -16,10 +20,20 @@ public class PlayerArgumentType implements ArgumentType } @Override - public Player parseInputUnchecked(String input) { - var player = server.getPlayer(input).orElse(null); - if (player == null) throw new IllegalArgumentException("Player not online"); - return player; + public Result parseInputUnchecked(String input) { + // TODO UUID support + Player player; + if (input.length() == 36) { + try { + player = server.getPlayer(UUID.fromString(input)).orElse(null); + } catch (IllegalArgumentException _) { + return Results.failure("Invalid UUID."); + } + } else { + player = server.getPlayer(input).orElse(null); + } + + return Results.ofNullable(player, "Player not online."); } @Override