From 796ac8441809323d3349cbfdb19f50e8ccfbd801 Mon Sep 17 00:00:00 2001 From: TonytheMacaroni Date: Fri, 10 May 2024 22:45:08 -0400 Subject: [PATCH 1/2] Add angle and rotation parsers --- .../cloud/bukkit/BukkitCaptionKeys.java | 8 + .../bukkit/BukkitDefaultCaptionsProvider.java | 9 + .../incendo/cloud/bukkit/BukkitParsers.java | 6 +- .../internal/BukkitBrigadierMapper.java | 6 + .../cloud/bukkit/parser/rotation/Angle.java | 112 ++++++++++++ .../bukkit/parser/rotation/AngleParser.java | 138 +++++++++++++++ .../bukkit/parser/rotation/Rotation.java | 116 +++++++++++++ .../parser/rotation/RotationParser.java | 161 ++++++++++++++++++ .../bukkit/parser/rotation/package-info.java | 4 + .../bukkit/parser/AngleArgumentTest.java | 97 +++++++++++ .../bukkit/parser/RotationArgumentTest.java | 98 +++++++++++ 11 files changed, 754 insertions(+), 1 deletion(-) create mode 100644 cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Angle.java create mode 100644 cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java create mode 100644 cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Rotation.java create mode 100644 cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java create mode 100644 cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/package-info.java create mode 100644 cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/AngleArgumentTest.java create mode 100644 cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitCaptionKeys.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitCaptionKeys.java index b64e7de5..7e512e27 100644 --- a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitCaptionKeys.java +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitCaptionKeys.java @@ -92,6 +92,14 @@ public final class BukkitCaptionKeys { */ public static final Caption ARGUMENT_PARSE_FAILURE_NAMESPACED_KEY_NEED_NAMESPACE = of("argument.parse.failure.namespacedkey.need_namespace"); + /** + * Variables: {@code } + * + * @since 2.0.0 + */ + public static final Caption ARGUMENT_PARSE_FAILURE_ROTATION_INVALID_FORMAT = of( + "argument.parse.failure.rotation.invalid_format" + ); private BukkitCaptionKeys() { } diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitDefaultCaptionsProvider.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitDefaultCaptionsProvider.java index 843a5cf4..12799079 100644 --- a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitDefaultCaptionsProvider.java +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitDefaultCaptionsProvider.java @@ -84,6 +84,11 @@ public final class BukkitDefaultCaptionsProvider extends DelegatingCaptionPro */ public static final String ARGUMENT_PARSE_FAILURE_NAMESPACED_KEY_NEED_NAMESPACE = "Invalid input '', requires an explicit namespace."; + /** + * Default caption for {@link BukkitCaptionKeys#ARGUMENT_PARSE_FAILURE_ROTATION_INVALID_FORMAT} + */ + public static final String ARGUMENT_PARSE_FAILURE_ROTATION_INVALID_FORMAT = + "'' is not a valid rotation. Required format is ' '"; private static final CaptionProvider PROVIDER = CaptionProvider.constantProvider() .putCaption( @@ -120,6 +125,10 @@ public final class BukkitDefaultCaptionsProvider extends DelegatingCaptionPro BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_NAMESPACED_KEY_NEED_NAMESPACE, ARGUMENT_PARSE_FAILURE_NAMESPACED_KEY_NEED_NAMESPACE ) + .putCaption( + BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_ROTATION_INVALID_FORMAT, + ARGUMENT_PARSE_FAILURE_ROTATION_INVALID_FORMAT + ) .build(); @SuppressWarnings("unchecked") diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitParsers.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitParsers.java index 7581b8c7..ebccce5e 100644 --- a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitParsers.java +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/BukkitParsers.java @@ -45,6 +45,8 @@ import org.incendo.cloud.bukkit.parser.WorldParser; import org.incendo.cloud.bukkit.parser.location.Location2DParser; import org.incendo.cloud.bukkit.parser.location.LocationParser; +import org.incendo.cloud.bukkit.parser.rotation.AngleParser; +import org.incendo.cloud.bukkit.parser.rotation.RotationParser; import org.incendo.cloud.bukkit.parser.selector.MultipleEntitySelectorParser; import org.incendo.cloud.bukkit.parser.selector.MultiplePlayerSelectorParser; import org.incendo.cloud.bukkit.parser.selector.SingleEntitySelectorParser; @@ -74,7 +76,9 @@ public static void register(final CommandManager manager) { .registerParser(Location2DParser.location2DParser()) .registerParser(ItemStackParser.itemStackParser()) .registerParser(SingleEntitySelectorParser.singleEntitySelectorParser()) - .registerParser(SinglePlayerSelectorParser.singlePlayerSelectorParser()); + .registerParser(SinglePlayerSelectorParser.singlePlayerSelectorParser()) + .registerParser(AngleParser.angleParser()) + .registerParser(RotationParser.rotationParser()); /* Register Entity Selector Parsers */ manager.parserRegistry().registerAnnotationMapper( diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/internal/BukkitBrigadierMapper.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/internal/BukkitBrigadierMapper.java index 38b01eba..b4eb83e4 100644 --- a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/internal/BukkitBrigadierMapper.java +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/internal/BukkitBrigadierMapper.java @@ -43,6 +43,8 @@ import org.incendo.cloud.bukkit.parser.NamespacedKeyParser; import org.incendo.cloud.bukkit.parser.location.Location2DParser; import org.incendo.cloud.bukkit.parser.location.LocationParser; +import org.incendo.cloud.bukkit.parser.rotation.AngleParser; +import org.incendo.cloud.bukkit.parser.rotation.RotationParser; import org.incendo.cloud.bukkit.parser.selector.MultipleEntitySelectorParser; import org.incendo.cloud.bukkit.parser.selector.MultiplePlayerSelectorParser; import org.incendo.cloud.bukkit.parser.selector.SingleEntitySelectorParser; @@ -97,6 +99,10 @@ public void registerBuiltInMappings() { this.mapNMS(new TypeToken>() {}, "vec3", this::argumentVec3); /* Map Vec2 */ this.mapNMS(new TypeToken>() {}, "vec2", this::argumentVec2); + /* Map Angle */ + this.mapSimpleNMS(new TypeToken>() {}, "angle"); + /* Map Rotation */ + this.mapSimpleNMS(new TypeToken>() {}, "rotation"); } @SuppressWarnings({"ConstantValue", "unused"}) diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Angle.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Angle.java new file mode 100644 index 00000000..b4cee6c3 --- /dev/null +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Angle.java @@ -0,0 +1,112 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.bukkit.parser.rotation; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents an angle that can be applied to a reference angle. + * + * @since 2.0.0 + */ +public final class Angle { + + private final float angle; + private final boolean relative; + + private Angle( + final float angle, + final boolean relative + ) { + this.angle = angle; + this.relative = relative; + } + + /** + * Create a new angle object. + * + * @param angle angle + * @param relative whether the angle is relative + * @return Created angle instance. + */ + public static @NonNull Angle of( + final float angle, + final boolean relative + ) { + return new Angle(angle, relative); + } + + /** + * Returns the angle. + * + * @return angle + */ + public float angle() { + return this.angle; + } + + /** + * Returns if this angle is relative. + * + * @return whether the angle is relative + */ + public boolean relative() { + return this.relative; + } + + /** + * Applies this angle to a reference angle. + * + * @param angle the reference angle + * @return the modified angle + */ + public float apply(final float angle) { + return this.relative ? this.angle + angle : this.angle; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Angle that = (Angle) o; + return Float.compare(this.angle, that.angle) == 0 && this.relative == that.relative; + } + + @Override + public int hashCode() { + int result = Float.hashCode(this.angle); + result = 31 * result + Boolean.hashCode(this.relative); + return result; + } + + @Override + public String toString() { + return String.format("Angle{angle=%s, relative=%s}", this.angle, this.relative); + } + +} diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java new file mode 100644 index 00000000..b87ec35e --- /dev/null +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java @@ -0,0 +1,138 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.bukkit.parser.rotation; + +import java.util.stream.Collectors; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.parser.standard.FloatParser; +import org.incendo.cloud.parser.standard.IntegerParser; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.incendo.cloud.type.range.Range; + + +/** + * Parser that parsers a {@link Angle} from two floats. + * + * @param Command sender type + * @since 2.0.0 + */ +public final class AngleParser implements ArgumentParser, BlockingSuggestionProvider.Strings { + + private static final Range SUGGESTION_RANGE = Range.intRange(Integer.MIN_VALUE, Integer.MAX_VALUE); + + /** + * Creates a new angle parser. + * + * @param command sender type + * @return the created parser + * @since 2.0.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + public static @NonNull ParserDescriptor angleParser() { + return ParserDescriptor.of(new AngleParser<>(), Angle.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #angleParser()} as the parser. + * + * @param the command sender type + * @return the component builder + * @since 2.0.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + public static CommandComponent.@NonNull Builder angleComponent() { + return CommandComponent.builder().parser(angleParser()); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull Angle> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final String input = commandInput.skipWhitespace().peekString(); + + final boolean relative; + if (commandInput.peek() == '~') { + relative = true; + commandInput.moveCursor(1); + } else { + relative = false; + } + + final float angle; + try { + final boolean empty = commandInput.peekString().isEmpty() || commandInput.peek() == ' '; + angle = empty ? 0 : commandInput.readFloat(); + + // You can have a prefix without a number, in which case we wouldn't consume the + // subsequent whitespace. We do it manually. + if (commandInput.hasRemainingInput() && commandInput.peek() == ' ') { + commandInput.read(); + } + } catch (final Exception e) { + return ArgumentParseResult.failure(new FloatParser.FloatParseException( + input, + new FloatParser<>( + FloatParser.DEFAULT_MINIMUM, + FloatParser.DEFAULT_MAXIMUM + ), + commandContext + )); + } + + return ArgumentParseResult.success( + Angle.of( + angle, + relative + ) + ); + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + String prefix; + if (input.hasRemainingInput() && input.peek() == '~') { + prefix = "~"; + input.moveCursor(1); + } else { + prefix = ""; + } + + return IntegerParser.getSuggestions( + SUGGESTION_RANGE, + input + ).stream().map(string -> prefix + string).collect(Collectors.toList()); + } + +} diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Rotation.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Rotation.java new file mode 100644 index 00000000..6839316e --- /dev/null +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/Rotation.java @@ -0,0 +1,116 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.bukkit.parser.rotation; + +import java.util.Objects; +import org.bukkit.Location; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a rotation that can be applied to a {@link Location}. + * + * @since 2.0.0 + */ +public final class Rotation { + + private final Angle yaw; + private final Angle pitch; + + private Rotation( + final @NonNull Angle yaw, + final @NonNull Angle pitch + ) { + this.yaw = yaw; + this.pitch = pitch; + } + + /** + * Create a new rotation object. + * + * @param yaw yaw + * @param pitch pitch + * @return Created rotation instance. + */ + public static Rotation of( + final @NonNull Angle yaw, + final @NonNull Angle pitch + ) { + return new Rotation(yaw, pitch); + } + + /** + * Returns the yaw of this rotation. + * + * @return yaw + */ + public Angle yaw() { + return this.yaw; + } + + /** + * Returns the pitch of this rotation + * + * @return pitch + */ + public Angle pitch() { + return this.pitch; + } + + /** + * Applies this rotation to a location. + * + * @param location the location to be modified + * @return the modified location + */ + public @NonNull Location apply( + final @NonNull Location location + ) { + location.setYaw(this.yaw.apply(location.getYaw())); + location.setPitch(this.pitch.apply(location.getPitch())); + return location; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Rotation that = (Rotation) o; + return this.yaw.equals(that.yaw) && this.pitch.equals(that.pitch); + } + + @Override + public int hashCode() { + return Objects.hash(this.yaw, this.pitch); + } + + @Override + public String toString() { + return String.format("Rotation{yaw=%s, pitch=%s}", this.yaw, this.pitch); + } + +} diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java new file mode 100644 index 00000000..d1e4d3d6 --- /dev/null +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java @@ -0,0 +1,161 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.bukkit.parser.rotation; + +import java.util.stream.Collectors; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.bukkit.BukkitCaptionKeys; +import org.incendo.cloud.caption.CaptionVariable; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.exception.parsing.ParserException; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.parser.standard.IntegerParser; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.incendo.cloud.type.range.Range; + +/** + * Parser that parses a {@link Rotation} from two floats. + * + * @param Command sender type + * @since 2.0.0 + */ +public final class RotationParser implements ArgumentParser, BlockingSuggestionProvider.Strings { + + private static final Range SUGGESTION_RANGE = Range.intRange(Integer.MIN_VALUE, Integer.MAX_VALUE); + + private final AngleParser angleParser = new AngleParser<>(); + + /** + * Creates a new rotation parser. + * + * @param command sender type + * @return the created parser + * @since 2.0.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + public static @NonNull ParserDescriptor rotationParser() { + return ParserDescriptor.of(new RotationParser<>(), Rotation.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #rotationParser()} as the parser. + * + * @param the command sender type + * @return the component builder + * @since 2.0.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + public static CommandComponent.@NonNull Builder rotationComponent() { + return CommandComponent.builder().parser(rotationParser()); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull Rotation> parse( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput commandInput + ) { + if (commandInput.remainingTokens() < 2) { + return ArgumentParseResult.failure( + new RotationParseException( + commandContext, + commandInput.remainingInput() + ) + ); + } + + Angle[] angles = new Angle[2]; + for (int i = 0; i < 2; i++) { + if (commandInput.peekString().isEmpty()) { + return ArgumentParseResult.failure( + new RotationParseException( + commandContext, + commandInput.remainingInput() + ) + ); + } + + final ArgumentParseResult angle = this.angleParser.parse( + commandContext, + commandInput + ); + if (angle.failure().isPresent()) { + return ArgumentParseResult.failure( + angle.failure().get() + ); + } + + angles[i] = angle.parsedValue().orElseThrow(NullPointerException::new); + } + + return ArgumentParseResult.success( + Rotation.of( + angles[0], + angles[1] + ) + ); + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + final int toSkip = Math.min(2, input.remainingTokens()) - 1; + final StringBuilder prefix = new StringBuilder(); + for (int i = 0; i < toSkip; i++) { + prefix.append(input.readStringSkipWhitespace()).append(" "); + } + + if (input.hasRemainingInput() && input.peek() == '~') { + prefix.append(input.read()); + } + + return IntegerParser.getSuggestions( + SUGGESTION_RANGE, + input + ).stream().map(string -> prefix + string).collect(Collectors.toList()); + } + + static class RotationParseException extends ParserException { + + protected RotationParseException( + final @NonNull CommandContext context, + final @NonNull String input + ) { + super( + RotationParser.class, + context, + BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_ROTATION_INVALID_FORMAT, + CaptionVariable.of("input", input) + ); + } + + } + +} diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/package-info.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/package-info.java new file mode 100644 index 00000000..8e90b056 --- /dev/null +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/package-info.java @@ -0,0 +1,4 @@ +/** + * Vanilla-like rotation arguments + */ +package org.incendo.cloud.bukkit.parser.rotation; diff --git a/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/AngleArgumentTest.java b/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/AngleArgumentTest.java new file mode 100644 index 00000000..6593f9b5 --- /dev/null +++ b/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/AngleArgumentTest.java @@ -0,0 +1,97 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.bukkit.parser; + +import java.util.stream.Stream; +import org.bukkit.command.CommandSender; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.bukkit.parser.rotation.Angle; +import org.incendo.cloud.bukkit.parser.rotation.AngleParser; +import org.incendo.cloud.bukkit.util.ServerTest; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public class AngleArgumentTest extends ServerTest { + + @ParameterizedTest + @MethodSource("Parse_HappyFlow_Success_Source") + void Parse_HappyFlow_Success(final @NonNull String input, final float expectedAngle) { + // Arrange + final AngleParser parser = new AngleParser<>(); + final CommandInput commandInput = CommandInput.of(input); + + // Act + final ArgumentParseResult result = parser.parse( + this.commandContext(), + commandInput + ); + + // Assert + assertThat(result.failure()).isEmpty(); + assertThat(result.parsedValue().isPresent()).isTrue(); + assertThat(result.parsedValue().get().apply(0)).isEqualTo(expectedAngle); + assertThat(commandInput.remainingInput()).isEmpty(); + } + + static @NonNull Stream<@NonNull Arguments> Parse_HappyFlow_Success_Source() { + return Stream.of( + arguments("~", 0f), + arguments("~10", 10f), + arguments("~-10", -10f), + arguments("~0.5", 0.5f), + arguments("~-0.5", -0.5f), + arguments("0", 0f), + arguments("10", 10f), + arguments("-10", -10f), + arguments("0.5", 0.5f), + arguments("-0.5", -0.5f) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"^", "^0", "not an angle"}) + void Parse_InvalidAngle_Failure(final @NonNull String input) { + // Arrange + final AngleParser parser = new AngleParser<>(); + final CommandInput commandInput = CommandInput.of(input); + + // Act + final ArgumentParseResult result = parser.parse( + this.commandContext(), + commandInput + ); + + // Assert + assertThat(result.failure()).isPresent(); + assertThat(result.parsedValue()).isEmpty(); + } + +} diff --git a/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java b/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java new file mode 100644 index 00000000..2f9a3333 --- /dev/null +++ b/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java @@ -0,0 +1,98 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.bukkit.parser; + +import java.util.stream.Stream; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.bukkit.parser.rotation.Rotation; +import org.incendo.cloud.bukkit.parser.rotation.RotationParser; +import org.incendo.cloud.bukkit.util.ServerTest; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public class RotationArgumentTest extends ServerTest { + + @ParameterizedTest + @MethodSource("Parse_HappyFlow_Success_Source") + void Parse_HappyFlow_Success(final @NonNull String input, final @NonNull Location expectedLocation) { + // Arrange + final RotationParser parser = new RotationParser<>(); + final CommandInput commandInput = CommandInput.of(input); + + // Act + final ArgumentParseResult result = parser.parse( + this.commandContext(), + commandInput + ); + + // Assert + assertThat(result.failure()).isEmpty(); + assertThat(result.parsedValue().isPresent()).isTrue(); + assertThat(result.parsedValue().get().apply(new Location(null, 0, 0, 0, 10, 20))).isEqualTo(expectedLocation); + assertThat(commandInput.remainingInput()).isEmpty(); + } + + static @NonNull Stream<@NonNull Arguments> Parse_HappyFlow_Success_Source() { + return Stream.of( + arguments("~ ~", new Location(null, 0, 0, 0, 10, 20)), + arguments("~10 ~10", new Location(null, 0, 0, 0, 20, 30)), + arguments("~-10 ~-10", new Location(null, 0, 0, 0, 0, 10)), + arguments("~0.5 ~-0.5", new Location(null, 0, 0, 0, 10.5f, 19.5f)), + arguments("~-0.5 ~0.5", new Location(null, 0, 0, 0, 9.5f, 20.5f)), + arguments("0 0", new Location(null, 0, 0, 0, 0, 0)), + arguments("10 10", new Location(null, 0, 0, 0, 10, 10)), + arguments("-10 -10", new Location(null, 0, 0, 0, -10, -10)), + arguments("0.5 -0.5", new Location(null, 0, 0, 0, 0.5f, -0.5f)), + arguments("-0.5 0.5", new Location(null, 0, 0, 0, -0.5f, 0.5f)) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"^ ^", "0 ^", "0", "0~", "0 not an angle", "not an angle"}) + void Parse_InvalidAngle_Failure(final @NonNull String input) { + // Arrange + final RotationParser parser = new RotationParser<>(); + final CommandInput commandInput = CommandInput.of(input); + + // Act + final ArgumentParseResult result = parser.parse( + this.commandContext(), + commandInput + ); + + // Assert + assertThat(result.failure()).isPresent(); + assertThat(result.parsedValue()).isEmpty(); + } + +} From 6eef2ecdb7a31983abe04ee2ec156537522bb5a6 Mon Sep 17 00:00:00 2001 From: TonytheMacaroni Date: Tue, 14 May 2024 17:33:40 -0400 Subject: [PATCH 2/2] Adjust angle and rotation parsers for better suggestions, add more tests --- .../bukkit/parser/rotation/AngleParser.java | 6 -- .../parser/rotation/RotationParser.java | 28 +++++-- .../bukkit/parser/RotationArgumentTest.java | 82 +++++++++++++++++++ 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java index b87ec35e..03554d1d 100644 --- a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/AngleParser.java @@ -91,12 +91,6 @@ public final class AngleParser implements ArgumentParser, BlockingS try { final boolean empty = commandInput.peekString().isEmpty() || commandInput.peek() == ' '; angle = empty ? 0 : commandInput.readFloat(); - - // You can have a prefix without a number, in which case we wouldn't consume the - // subsequent whitespace. We do it manually. - if (commandInput.hasRemainingInput() && commandInput.peek() == ' ') { - commandInput.read(); - } } catch (final Exception e) { return ArgumentParseResult.failure(new FloatParser.FloatParseException( input, diff --git a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java index d1e4d3d6..69023747 100644 --- a/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java +++ b/cloud-bukkit/src/main/java/org/incendo/cloud/bukkit/parser/rotation/RotationParser.java @@ -126,16 +126,34 @@ public final class RotationParser implements ArgumentParser, Blo final @NonNull CommandContext commandContext, final @NonNull CommandInput input ) { - final int toSkip = Math.min(2, input.remainingTokens()) - 1; - final StringBuilder prefix = new StringBuilder(); - for (int i = 0; i < toSkip; i++) { - prefix.append(input.readStringSkipWhitespace()).append(" "); + final CommandInput inputCopy = input.copy(); + + int idx = input.cursor(); + for (int i = 0; i < 2; i++) { + input.skipWhitespace(); + idx = input.cursor(); + + if (!input.hasRemainingInput()) { + break; + } + + final ArgumentParseResult angle = this.angleParser.parse( + commandContext, + input + ); + + if (angle.failure().isPresent() || !input.hasRemainingInput()) { + break; + } } + input.cursor(idx); if (input.hasRemainingInput() && input.peek() == '~') { - prefix.append(input.read()); + input.moveCursor(1); } + final String prefix = inputCopy.difference(input, true); + return IntegerParser.getSuggestions( SUGGESTION_RANGE, input diff --git a/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java b/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java index 2f9a3333..ba3b6a93 100644 --- a/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java +++ b/cloud-bukkit/src/test/java/org/incendo/cloud/bukkit/parser/RotationArgumentTest.java @@ -23,21 +23,32 @@ // package org.incendo.cloud.bukkit.parser; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.CommandManager; import org.incendo.cloud.bukkit.parser.rotation.Rotation; import org.incendo.cloud.bukkit.parser.rotation.RotationParser; import org.incendo.cloud.bukkit.util.ServerTest; import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.internal.CommandRegistrationHandler; import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.flag.CommandFlag; +import org.incendo.cloud.suggestion.Suggestion; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.params.provider.Arguments.arguments; public class RotationArgumentTest extends ServerTest { @@ -95,4 +106,75 @@ void Parse_InvalidAngle_Failure(final @NonNull String input) { assertThat(result.parsedValue()).isEmpty(); } + @ParameterizedTest + @MethodSource + void suggestions(final String input, final List expectedSuggestions) { + final CommandManager manager = new CommandManager( + ExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler() + ) { + @Override + public boolean hasPermission(@NonNull CommandSender sender, @NonNull String permission) { + return true; + } + }; + + manager.command( + manager.commandBuilder("rotation") + .required("rotation", RotationParser.rotationParser()) + ); + + final Function suggestion = s -> "rotation " + s; + assertEquals( + expectedSuggestions, + manager.suggestionFactory().suggestImmediately(this.commandContext().sender(), suggestion.apply(input)) + .list().stream().map(Suggestion::suggestion).collect(Collectors.toList()) + ); + } + + @ParameterizedTest + @MethodSource("suggestions") + void suggestionsFlag(final String input, final List expectedSuggestions) { + final CommandManager manager = new CommandManager( + ExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler() + ) { + @Override + public boolean hasPermission(@NonNull CommandSender sender, @NonNull String permission) { + return true; + } + }; + + manager.command( + manager.commandBuilder("flag") + .flag(CommandFlag.builder("rot").withComponent(RotationParser.rotationParser()).build()) + ); + + final Function flagSuggestion = s -> "flag --rot " + s; + assertEquals( + expectedSuggestions, + manager.suggestionFactory().suggestImmediately(this.commandContext().sender(), flagSuggestion.apply(input)) + .list().stream().map(Suggestion::suggestion).collect(Collectors.toList()) + ); + } + + static Stream suggestions() { + return Stream.of( + arguments("", Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")), + arguments("1", Arrays.asList("1", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19")), + arguments("1 ", Arrays.asList("1 0", "1 1", "1 2", "1 3", "1 4", "1 5", "1 6", "1 7", "1 8", "1 9")), + arguments("1 1", Arrays.asList("1 1", "1 10", "1 11", "1 12", "1 13", "1 14", "1 15", "1 16", "1 17", "1 18", "1 19")), + arguments("1 0", Collections.singletonList("1 0")), + arguments("1 1 ", Collections.emptyList()), + arguments("0", Collections.singletonList("0")), + arguments("0 ", Arrays.asList("0 0", "0 1", "0 2", "0 3", "0 4", "0 5", "0 6", "0 7", "0 8", "0 9")), + arguments("0 1", Arrays.asList("0 1", "0 10", "0 11", "0 12", "0 13", "0 14", "0 15", "0 16", "0 17", "0 18", "0 19")), + arguments("0 0", Collections.singletonList("0 0")), + arguments("0 0 ", Collections.emptyList()), + arguments("~", Arrays.asList("~0", "~1", "~2", "~3", "~4", "~5", "~6", "~7", "~8", "~9")), + arguments("~ ~", Arrays.asList("~ ~0", "~ ~1", "~ ~2", "~ ~3", "~ ~4", "~ ~5", "~ ~6", "~ ~7", "~ ~8", "~ ~9")), + arguments("~0", Collections.singletonList("~0")), + arguments("~1", Arrays.asList("~1", "~10", "~11", "~12", "~13", "~14", "~15", "~16", "~17", "~18", "~19")), + arguments("~1 ", Arrays.asList("~1 0", "~1 1", "~1 2", "~1 3", "~1 4", "~1 5", "~1 6", "~1 7", "~1 8", "~1 9")) + ); + } + }