diff --git a/README.md b/README.md index 378a224d..a305bacc 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,17 @@ See [examples/example-minestom](https://github.com/spectr-is/cloud-minecraft/blo ### Minestom Parsers As of right now there's only two built-in parsers that I bothered to add. -| Parser | Type | Description | -|--------------------|--------------|-------------------------------------------| -| `PlayerParser` | `Player` | Resolves an online player by username | -| `EntityTypeParser` | `EntityType` | Resolves an entity type by namespaced key | +## Included parsers + +| Parser | Type | Description | +|-----------------------|-----------------|-----------------------------------------------------------------------------------------------------| +| `PlayerParser` | `Player` | Resolves an online player by username | +| `EntityTypeParser` | `EntityType` | Resolves an entity type by namespaced key | +| `InstanceParser` | `Instance` | Resolves a loaded instance by UUID | +| `GameModeParser` | `GameMode` | Resolves a game mode by name | +| `DimensionTypeParser` | `DimensionType` | Resolves a dimension type by namespaced key | +| `VecParser` | `Vec` | Resolves a `Vec` from `x y z` (supports relative coordinates (`~ ~ ~`) | +| `PosParser` | `Pos` | Resolves a `Pos` from `x y z`, (+ optional pitch/yaw - also supports relative coords (`~ ~ ~ ~ ~`)) | ## links diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java index 7c93be29..ba968eff 100644 --- a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java @@ -36,8 +36,13 @@ import org.incendo.cloud.SenderMapperHolder; import org.incendo.cloud.execution.ExecutionCoordinator; import org.incendo.cloud.minestom.caption.MinestomDefaultCaptionsProvider; +import org.incendo.cloud.minestom.parser.DimensionTypeParser; import org.incendo.cloud.minestom.parser.EntityTypeParser; +import org.incendo.cloud.minestom.parser.GameModeParser; +import org.incendo.cloud.minestom.parser.InstanceParser; import org.incendo.cloud.minestom.parser.PlayerParser; +import org.incendo.cloud.minestom.parser.location.PosParser; +import org.incendo.cloud.minestom.parser.location.VecParser; /** * Command manager for the Minestom platform. @@ -72,7 +77,12 @@ public MinestomCommandManager( this.parserRegistry() .registerParser(PlayerParser.playerParser()) - .registerParser(EntityTypeParser.entityTypeParser()); + .registerParser(EntityTypeParser.entityTypeParser()) + .registerParser(InstanceParser.instanceParser()) + .registerParser(GameModeParser.gameModeParser()) + .registerParser(DimensionTypeParser.dimensionTypeParser()) + .registerParser(PosParser.posParser()) + .registerParser(VecParser.vecParser()); this.captionRegistry().registerProvider(new MinestomDefaultCaptionsProvider<>()); diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java index d7fe086c..48558016 100644 --- a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java @@ -47,6 +47,31 @@ public final class MinestomCaptionKeys { */ public static final Caption ARGUMENT_PARSE_FAILURE_ENTITY_TYPE = of("argument.parse.failure.entity_type"); + /** + * Variables: {@code } + */ + public static final Caption ARGUMENT_PARSE_FAILURE_INSTANCE = of("argument.parse.failure.instance"); + + /** + * Variables: {@code } + */ + public static final Caption ARGUMENT_PARSE_FAILURE_GAME_MODE = of("argument.parse.failure.game_mode"); + + /** + * Variables: {@code } + */ + public static final Caption ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE = of("argument.parse.failure.dimension_type"); + + /** + * Variables: {@code } + */ + public static final Caption ARGUMENT_PARSE_FAILURE_VEC = of("argument.parse.failure.vec"); + + /** + * Variables: {@code } + */ + public static final Caption ARGUMENT_PARSE_FAILURE_POS = of("argument.parse.failure.pos"); + private MinestomCaptionKeys() { } diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java index b006daee..973b8b2d 100644 --- a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java @@ -44,9 +44,39 @@ public final class MinestomDefaultCaptionsProvider extends DelegatingCaptionP */ public static final String ARGUMENT_PARSE_FAILURE_ENTITY_TYPE = "'' is not a valid entity type"; + /** + * Default caption for {@link MinestomCaptionKeys#ARGUMENT_PARSE_FAILURE_INSTANCE} + */ + public static final String ARGUMENT_PARSE_FAILURE_INSTANCE = "No instance found for input ''"; + + /** + * Default caption for {@link MinestomCaptionKeys#ARGUMENT_PARSE_FAILURE_GAME_MODE} + */ + public static final String ARGUMENT_PARSE_FAILURE_GAME_MODE = "'' is not a valid game mode"; + + /** + * Default caption for {@link MinestomCaptionKeys#ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE} + */ + public static final String ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE = "'' is not a valid dimension type"; + + /** + * Default caption for {@link MinestomCaptionKeys#ARGUMENT_PARSE_FAILURE_VEC} + */ + public static final String ARGUMENT_PARSE_FAILURE_VEC = "'' is not a valid vec (expected: x y z)"; + + /** + * Default caption for {@link MinestomCaptionKeys#ARGUMENT_PARSE_FAILURE_POS} + */ + public static final String ARGUMENT_PARSE_FAILURE_POS = "'' is not a valid pos (expected: x y z, optionally with yaw and pitch)"; + private static final CaptionProvider PROVIDER = CaptionProvider.constantProvider() .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_PLAYER, ARGUMENT_PARSE_FAILURE_PLAYER) .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_ENTITY_TYPE, ARGUMENT_PARSE_FAILURE_ENTITY_TYPE) + .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_INSTANCE, ARGUMENT_PARSE_FAILURE_INSTANCE) + .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_GAME_MODE, ARGUMENT_PARSE_FAILURE_GAME_MODE) + .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE, ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE) + .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_VEC, ARGUMENT_PARSE_FAILURE_VEC) + .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_POS, ARGUMENT_PARSE_FAILURE_POS) .build(); @SuppressWarnings("unchecked") diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/DimensionTypeParser.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/DimensionTypeParser.java new file mode 100644 index 00000000..8fa3498e --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/DimensionTypeParser.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.minestom.parser; + +import java.util.stream.Collectors; +import net.kyori.adventure.key.Key; +import net.minestom.server.MinecraftServer; +import net.minestom.server.world.DimensionType; +import org.apiguardian.api.API; +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.minestom.caption.MinestomCaptionKeys; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.jspecify.annotations.NonNull; + +/** + * Parser for {@link DimensionType dimension types}, matched by namespaced key. + * + * @param command sender type + */ +public final class DimensionTypeParser implements ArgumentParser, BlockingSuggestionProvider.Strings { + + /** + * Creates a new dimension type 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 dimensionTypeParser() { + return ParserDescriptor.of(new DimensionTypeParser<>(), DimensionType.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #dimensionTypeParser()} 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 dimensionTypeComponent() { + return CommandComponent.builder().parser(dimensionTypeParser()); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull DimensionType> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final String input = commandInput.readString(); + final Key key; + try { + key = Key.key(input); + } catch (final Exception e) { + return ArgumentParseResult.failure(new DimensionTypeParseException(input, commandContext)); + } + final DimensionType type = MinecraftServer.getDimensionTypeRegistry().get(key); + if (type == null) { + return ArgumentParseResult.failure(new DimensionTypeParseException(input, commandContext)); + } + return ArgumentParseResult.success(type); + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return MinecraftServer.getDimensionTypeRegistry().keys().stream() + .map(key -> key.key().asString()) + .collect(Collectors.toList()); + } + + /** + * Exception thrown when a dimension type cannot be found for the input provided. + */ + public static final class DimensionTypeParseException extends ParserException { + + private final String input; + + /** + * Create a new dimension type parse exception. + * + * @param input string input + * @param context command context + */ + public DimensionTypeParseException( + final @NonNull String input, + final @NonNull CommandContext context + ) { + super( + DimensionTypeParser.class, + context, + MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE, + CaptionVariable.of("input", input) + ); + this.input = input; + } + + /** + * Returns the supplied input. + * + * @return input value + */ + public @NonNull String input() { + return this.input; + } + } +} diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/GameModeParser.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/GameModeParser.java new file mode 100644 index 00000000..6c9f2292 --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/GameModeParser.java @@ -0,0 +1,132 @@ +// +// 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.minestom.parser; + +import java.util.Arrays; +import java.util.Locale; +import java.util.stream.Collectors; +import net.minestom.server.entity.GameMode; +import org.apiguardian.api.API; +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.minestom.caption.MinestomCaptionKeys; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.jspecify.annotations.NonNull; + +/** + * Parser for {@link GameMode game modes}. + * + * @param command sender type + */ +public final class GameModeParser implements ArgumentParser, BlockingSuggestionProvider.Strings { + + /** + * Creates a new game mode 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 gameModeParser() { + return ParserDescriptor.of(new GameModeParser<>(), GameMode.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #gameModeParser()} 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 gameModeComponent() { + return CommandComponent.builder().parser(gameModeParser()); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull GameMode> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final String input = commandInput.readString(); + return Arrays.stream(GameMode.values()) + .filter(gm -> gm.name().equalsIgnoreCase(input)) + .findFirst() + .map(ArgumentParseResult::success) + .orElseGet(() -> ArgumentParseResult.failure(new GameModeParseException(input, commandContext))); + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return Arrays.stream(GameMode.values()) + .map(gm -> gm.name().toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + } + + /** + * Exception thrown when a game mode cannot be found for the input provided. + */ + public static final class GameModeParseException extends ParserException { + + private final String input; + + /** + * Create a new game mode parse exception. + * + * @param input string input + * @param context command context + */ + public GameModeParseException( + final @NonNull String input, + final @NonNull CommandContext context + ) { + super( + GameModeParser.class, + context, + MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_GAME_MODE, + CaptionVariable.of("input", input) + ); + this.input = input; + } + + /** + * Returns the supplied input. + * + * @return input value + */ + public @NonNull String input() { + return this.input; + } + } +} diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/InstanceParser.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/InstanceParser.java new file mode 100644 index 00000000..989516c7 --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/InstanceParser.java @@ -0,0 +1,131 @@ +// +// 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.minestom.parser; + +import java.util.stream.Collectors; +import net.minestom.server.MinecraftServer; +import net.minestom.server.instance.Instance; +import org.apiguardian.api.API; +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.minestom.caption.MinestomCaptionKeys; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.jspecify.annotations.NonNull; + +/** + * Parser for online {@link Instance instances}, matched by UUID string. + * + * @param command sender type + */ +public final class InstanceParser implements ArgumentParser, BlockingSuggestionProvider.Strings { + + /** + * Creates a new instance 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 instanceParser() { + return ParserDescriptor.of(new InstanceParser<>(), Instance.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #instanceParser()} 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 instanceComponent() { + return CommandComponent.builder().parser(instanceParser()); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull Instance> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final String input = commandInput.readString(); + return MinecraftServer.getInstanceManager().getInstances().stream() + .filter(i -> i.getUuid().toString().equals(input)) + .findFirst() + .map(ArgumentParseResult::success) + .orElseGet(() -> ArgumentParseResult.failure(new InstanceParseException(input, commandContext))); + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return MinecraftServer.getInstanceManager().getInstances().stream() + .map(i -> i.getUuid().toString()) + .collect(Collectors.toList()); + } + + /** + * Exception thrown when an instance cannot be found for the input provided. + */ + public static final class InstanceParseException extends ParserException { + + private final String input; + + /** + * Create a new instance parse exception. + * + * @param input string input + * @param context command context + */ + public InstanceParseException( + final @NonNull String input, + final @NonNull CommandContext context + ) { + super( + InstanceParser.class, + context, + MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_INSTANCE, + CaptionVariable.of("input", input) + ); + this.input = input; + } + + /** + * Returns the supplied input. + * + * @return input value + */ + public @NonNull String input() { + return this.input; + } + } +} diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/LocationCoordinate.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/LocationCoordinate.java new file mode 100644 index 00000000..7f2a6311 --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/LocationCoordinate.java @@ -0,0 +1,121 @@ +// +// 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.minestom.parser.location; + +import org.checkerframework.checker.nullness.qual.NonNull; +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.standard.DoubleParser; + +/** + * Parser for Vec/Pos components. + * + * @since 2.0.0 + * @param command sender type + */ +final class LocationCoordinate implements ArgumentParser> { + + private final LocationCoordinateType type; + private final double value; + + private LocationCoordinate(final @NonNull LocationCoordinateType type, final double value) { + this.type = type; + this.value = value; + } + + /** + * Creates a new coordinate component. + * + * @param command sender type + * @param type the coordinate type + * @param value the numeric offset + * @return the created component + */ + static @NonNull LocationCoordinate of(final @NonNull LocationCoordinateType type, final double value) { + return new LocationCoordinate<>(type, value); + } + + @Override + public @NonNull ArgumentParseResult> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final String input = commandInput.skipWhitespace().peekString(); + + final LocationCoordinateType coordinateType; + if (commandInput.peek() == '~') { + coordinateType = LocationCoordinateType.RELATIVE; + commandInput.moveCursor(1); + } else { + coordinateType = LocationCoordinateType.ABSOLUTE; + } + + final double coordinate; + try { + final boolean empty = commandInput.peekString().isEmpty() || commandInput.peek() == ' '; + coordinate = empty ? 0 : commandInput.readDouble(); + if (commandInput.hasRemainingInput()) { + commandInput.skipWhitespace(); + } + } catch (final Exception e) { + return ArgumentParseResult.failure(new DoubleParser.DoubleParseException( + input, + new DoubleParser<>(DoubleParser.DEFAULT_MINIMUM, DoubleParser.DEFAULT_MAXIMUM), + commandContext + )); + } + + return ArgumentParseResult.success(LocationCoordinate.of(coordinateType, coordinate)); + } + + /** + * Returns the final coordinate value against an origin. + * + * @param origin the origin value (x/y/z of sender etc) + * @return the final coordinate + */ + double resolve(final double origin) { + return this.type == LocationCoordinateType.RELATIVE ? origin + this.value : this.value; + } + + /** + * Returns the coordinate type. + * + * @return coordinate type + */ + @NonNull LocationCoordinateType type() { + return this.type; + } + + /** + * Returns the raw numeric offset or absolute value. + * + * @return value + */ + double value() { + return this.value; + } +} diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/LocationCoordinateType.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/LocationCoordinateType.java new file mode 100644 index 00000000..9fd2d0ee --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/LocationCoordinateType.java @@ -0,0 +1,40 @@ +// +// 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.minestom.parser.location; + +/** + * Type of location coordinates + * + * @since 2.0.0 + */ +public enum LocationCoordinateType { + /** + * Absolute coordinate + */ + ABSOLUTE, + /** + * Coordinate relative to the sender's position + */ + RELATIVE, +} diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/PosParser.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/PosParser.java new file mode 100644 index 00000000..2c0c9190 --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/PosParser.java @@ -0,0 +1,202 @@ +// +// 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.minestom.parser.location; + +import java.util.List; +import net.minestom.server.command.CommandSender; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import org.apiguardian.api.API; +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.minestom.caption.MinestomCaptionKeys; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.jspecify.annotations.NonNull; + +/** + * Parser for {@link Pos} - parses the x, y, and z components (+ pitch/yaw components optionally). + *

+ * Can be absolute components (e.g. {@code 100}) or relative (e.g. {@code ~} or {@code ~5}). + *

+ * Non-entity senders using relative components will default to {@code 0}. + * + * @param command sender type + */ +public final class PosParser implements ArgumentParser, BlockingSuggestionProvider.Strings { + + /** + * Creates a new pos parser that does not require yaw and pitch, + * defaulting them to {@code 0} when absent. + * + * @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 posParser() { + return posParser(false); + } + + /** + * Creates a new pos parser. + * + * @param requireRotation whether yaw and pitch tokens are required; if {@code false} they default to {@code 0} + * @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 posParser(final boolean requireRotation) { + return ParserDescriptor.of(new PosParser<>(requireRotation), Pos.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #posParser()} 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 posComponent() { + return CommandComponent.builder().parser(posParser()); + } + + private final LocationCoordinate coordinateParser = LocationCoordinate.of(LocationCoordinateType.ABSOLUTE, 0); + private final boolean requireRotation; + + /** + * Creates a new pos parser. + * + * @param requireRotation whether yaw and pitch tokens are required + */ + public PosParser(final boolean requireRotation) { + this.requireRotation = requireRotation; + } + + @Override + public @NonNull ArgumentParseResult<@NonNull Pos> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final int required = this.requireRotation ? 5 : 3; + if (commandInput.remainingTokens() < required) { + return ArgumentParseResult.failure( + new PosParseException(commandInput.remainingInput(), commandContext) + ); + } + + @SuppressWarnings("unchecked") + final LocationCoordinate[] components = (LocationCoordinate[]) new LocationCoordinate[required]; + for (int i = 0; i < required; i++) { + if (commandInput.peekString().isEmpty()) { + return ArgumentParseResult.failure( + new PosParseException(commandInput.remainingInput(), commandContext) + ); + } + final ArgumentParseResult> result = + this.coordinateParser.parse(commandContext, commandInput); + if (result.failure().isPresent()) { + return ArgumentParseResult.failure(result.failure().get()); + } + components[i] = result.parsedValue().orElseThrow(NullPointerException::new); + } + + final Pos origin = this.resolveOrigin(commandContext); + + if (!this.requireRotation) { + return ArgumentParseResult.success(new Pos( + components[0].resolve(origin.x()), + components[1].resolve(origin.y()), + components[2].resolve(origin.z()) + )); + } + + return ArgumentParseResult.success(new Pos( + components[0].resolve(origin.x()), + components[1].resolve(origin.y()), + components[2].resolve(origin.z()), + (float) components[3].resolve(origin.yaw()), + (float) components[4].resolve(origin.pitch()) + )); + } + + private @NonNull Pos resolveOrigin(final @NonNull CommandContext commandContext) { + final Object sender = commandContext.sender(); + if (sender instanceof CommandSender && sender instanceof Entity entity) { + return entity.getPosition(); + } + return Pos.ZERO; + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return this.requireRotation ? List.of("~ ~ ~ ~ ~", "0 0 0 0 0") : List.of("~ ~ ~", "0 0 0"); + } + + /** + * Exception thrown when a {@link Pos} cannot be parsed from the input provided. + */ + public static final class PosParseException extends ParserException { + + private final String input; + + /** + * Create a new pos parse exception. + * + * @param input string input + * @param context command context + */ + public PosParseException( + final @NonNull String input, + final @NonNull CommandContext context + ) { + super( + PosParser.class, + context, + MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_POS, + CaptionVariable.of("input", input) + ); + this.input = input; + } + + /** + * Returns the supplied input. + * + * @return input value + */ + public @NonNull String input() { + return this.input; + } + } +} diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/VecParser.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/VecParser.java new file mode 100644 index 00000000..b82001a9 --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/VecParser.java @@ -0,0 +1,167 @@ +// +// 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.minestom.parser.location; + +import java.util.List; +import net.minestom.server.command.CommandSender; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import org.apiguardian.api.API; +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.minestom.caption.MinestomCaptionKeys; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.jspecify.annotations.NonNull; + +/** + * Parser for {@link Vec} - parses the x, y, and z components. + *

+ * Can be absolute components (e.g. {@code 100}) or relative (e.g. {@code ~} or {@code ~5}). + *

+ * Non-entity senders using relative components will default to {@code 0}. + * + * @param command sender type + */ +public final class VecParser implements ArgumentParser, BlockingSuggestionProvider.Strings { + + /** + * Creates a new vec 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 vecParser() { + return ParserDescriptor.of(new VecParser<>(), Vec.class); + } + + /** + * Returns a {@link CommandComponent.Builder} using {@link #vecParser()} 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 vecComponent() { + return CommandComponent.builder().parser(vecParser()); + } + + private final LocationCoordinate coordinateParser = LocationCoordinate.of(LocationCoordinateType.ABSOLUTE, 0); + + @Override + public @NonNull ArgumentParseResult<@NonNull Vec> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + if (commandInput.remainingTokens() < 3) { + return ArgumentParseResult.failure( + new VecParseException(commandInput.remainingInput(), commandContext) + ); + } + + @SuppressWarnings("unchecked") + final LocationCoordinate[] components = (LocationCoordinate[]) new LocationCoordinate[3]; + for (int i = 0; i < 3; i++) { + if (commandInput.peekString().isEmpty()) { + return ArgumentParseResult.failure( + new VecParseException(commandInput.remainingInput(), commandContext) + ); + } + final ArgumentParseResult> result = + this.coordinateParser.parse(commandContext, commandInput); + if (result.failure().isPresent()) { + return ArgumentParseResult.failure(result.failure().get()); + } + components[i] = result.parsedValue().orElseThrow(NullPointerException::new); + } + + final Vec origin = this.resolveOrigin(commandContext); + return ArgumentParseResult.success(new Vec( + components[0].resolve(origin.x()), + components[1].resolve(origin.y()), + components[2].resolve(origin.z()) + )); + } + + private @NonNull Vec resolveOrigin(final @NonNull CommandContext commandContext) { + final Object sender = commandContext.sender(); + if (sender instanceof CommandSender && sender instanceof Entity entity) { + final var pos = entity.getPosition(); + return new Vec(pos.x(), pos.y(), pos.z()); + } + return Vec.ZERO; + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return List.of("~ ~ ~", "0 0 0"); + } + + /** + * Exception thrown when a {@link Vec} cannot be parsed from the input provided. + */ + public static final class VecParseException extends ParserException { + + private final String input; + + /** + * Create a new vec parse exception. + * + * @param input string input + * @param context command context + */ + public VecParseException( + final @NonNull String input, + final @NonNull CommandContext context + ) { + super( + VecParser.class, + context, + MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_VEC, + CaptionVariable.of("input", input) + ); + this.input = input; + } + + /** + * Returns the supplied input. + * + * @return input value + */ + public @NonNull String input() { + return this.input; + } + } +} diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/package-info.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/package-info.java new file mode 100644 index 00000000..5246c5db --- /dev/null +++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/location/package-info.java @@ -0,0 +1,4 @@ +/** + * cloud-bukkit location-specific command arguments + */ +package org.incendo.cloud.minestom.parser.location; diff --git a/gradle.properties b/gradle.properties index d24f8afe..6672ced7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=org.incendo -version=2.0.0-SNAPSHOT +version=2.1.0-SNAPSHOT description=Integrations between Minecraft and Cloud Command Framework org.gradle.caching=true