diff --git a/cloud-sponge/build.gradle.kts b/cloud-sponge/build.gradle.kts new file mode 100644 index 00000000..db6e7f5c --- /dev/null +++ b/cloud-sponge/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("conventions.base") + id("conventions.publishing") + id("net.neoforged.moddev") +} + +dependencies { + api(libs.cloud.core) + implementation(libs.cloud.brigadier) + offlineLinkedJavadoc(project(":cloud-minecraft-modded-common")) + implementation(project(":cloud-minecraft-modded-common")) + compileOnly("org.spongepowered:spongeapi:11.0.0-SNAPSHOT") + compileOnly("org.spongepowered:sponge:1.20.6-11.0.0-SNAPSHOT") +} + +neoForge { + enable { + neoFormVersion = "1.20.6-20240627.102356" + } +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/CloudInjectionModule.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/CloudInjectionModule.java new file mode 100644 index 00000000..242c75e0 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/CloudInjectionModule.java @@ -0,0 +1,95 @@ +// +// 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.sponge; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.util.Types; +import java.lang.reflect.Type; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.SenderMapper; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.spongepowered.api.command.CommandCause; + +/** + * Injection module that allows for {@link SpongeCommandManager} to be injectable. + * + * @param Command sender type + */ +public final class CloudInjectionModule extends AbstractModule { + + private final Class commandSenderType; + private final ExecutionCoordinator executionCoordinator; + private final SenderMapper<@NonNull CommandCause, @NonNull C> senderMapper; + + /** + * Create a new injection module. + * + * @param commandSenderType Your command sender type + * @param executionCoordinator Command execution coordinator + * @param senderMapper Function mapping the custom command sender type to a Sponge CommandCause + */ + public CloudInjectionModule( + final @NonNull Class commandSenderType, + final @NonNull ExecutionCoordinator executionCoordinator, + final @NonNull SenderMapper<@NonNull CommandCause, @NonNull C> senderMapper + ) { + this.commandSenderType = commandSenderType; + this.executionCoordinator = executionCoordinator; + this.senderMapper = senderMapper; + } + + /** + * Create a new injection module using Sponge's {@link CommandCause} as the sender type. + * + * @param executionCoordinator Command execution coordinator + * @return new injection module + */ + public static @NonNull CloudInjectionModule<@NonNull CommandCause> createNative( + final @NonNull ExecutionCoordinator executionCoordinator + ) { + return new CloudInjectionModule<>( + CommandCause.class, + executionCoordinator, + SenderMapper.identity() + ); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + protected void configure() { + final Type commandExecutionCoordinatorType = Types.newParameterizedType( + ExecutionCoordinator.class, this.commandSenderType + ); + final Key coordinatorKey = Key.get(commandExecutionCoordinatorType); + this.bind(coordinatorKey).toInstance(this.executionCoordinator); + + final Type commandSenderMapperFunction = Types.newParameterizedType( + SenderMapper.class, CommandCause.class, this.commandSenderType + ); + final Key senderMapperKey = Key.get(commandSenderMapperFunction); + this.bind(senderMapperKey).toInstance(this.senderMapper); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/CloudSpongeCommand.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/CloudSpongeCommand.java new file mode 100644 index 00000000..05119094 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/CloudSpongeCommand.java @@ -0,0 +1,226 @@ +// +// 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.sponge; + +import io.leangen.geantyref.GenericTypeReflector; +import java.lang.reflect.Type; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.internal.CommandNode; +import org.incendo.cloud.parser.aggregate.AggregateParser; +import org.incendo.cloud.parser.standard.LiteralParser; +import org.incendo.cloud.permission.Permission; +import org.incendo.cloud.type.tuple.Pair; +import org.spongepowered.api.command.Command; +import org.spongepowered.api.command.CommandCause; +import org.spongepowered.api.command.CommandCompletion; +import org.spongepowered.api.command.CommandResult; +import org.spongepowered.api.command.parameter.ArgumentReader; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; + +import static net.kyori.adventure.text.Component.text; + +final class CloudSpongeCommand implements Command.Raw { + + private final SpongeCommandManager commandManager; + private final String label; + + CloudSpongeCommand( + final @NonNull String label, + final @NonNull SpongeCommandManager commandManager + ) { + this.label = label; + this.commandManager = commandManager; + } + + @Override + public CommandResult process(final @NonNull CommandCause cause, final ArgumentReader.@NonNull Mutable arguments) { + final C cloudSender = this.commandManager.senderMapper().map(cause); + final String input = this.formatCommandForParsing(arguments.input()); + this.commandManager.commandExecutor().executeCommand(cloudSender, input); + return CommandResult.success(); + } + + @Override + public List complete( + final @NonNull CommandCause cause, + final ArgumentReader.@NonNull Mutable arguments + ) { + return this.commandManager.suggestionFactory() + .suggestImmediately(this.commandManager.senderMapper().map(cause), + this.formatCommandForSuggestions(arguments.input())) + .list() + .stream() + .map(s -> CommandCompletion.of(s.suggestion(), s.tooltip())) + .collect(Collectors.toList()); + } + + @Override + public boolean canExecute(final @NonNull CommandCause cause) { + return this.checkAccess( + cause, + this.namedNode().nodeMeta() + .getOrDefault(CommandNode.META_KEY_ACCESS, Collections.emptyMap()) + ); + } + + @Override + public Optional shortDescription(final CommandCause cause) { + return Optional.of(this.usage(cause)); + } + + @Override + public Optional extendedDescription(final CommandCause cause) { + return Optional.of(this.usage(cause)); + } + + @Override + public Optional help(final @NonNull CommandCause cause) { + return Optional.of(this.usage(cause)); + } + + @Override + public Component usage(final CommandCause cause) { + return text(this.commandManager.commandSyntaxFormatter() + .apply(this.commandManager.senderMapper().map(cause), Collections.emptyList(), this.namedNode())); + } + + private CommandNode namedNode() { + return this.commandManager.commandTree().getNamedNode(this.label); + } + + @Override + public CommandTreeNode.Root commandTree() { + final CommandTreeNode root = CommandTreeNode.root(); + + final CommandNode cloud = this.namedNode(); + + if (canExecute(cloud)) { + root.executable(); + } + + this.addRequirement(cloud, root); + + this.addChildren(root, cloud); + return (CommandTreeNode.Root) root; + } + + private void addChildren(final CommandTreeNode node, final CommandNode cloud) { + for (final CommandNode child : cloud.children()) { + final CommandComponent value = child.component(); + final CommandTreeNode.Argument> treeNode; + if (value.parser() instanceof LiteralParser) { + treeNode = (CommandTreeNode.Argument>) CommandTreeNode.literal(); + } else if (value.parser() instanceof AggregateParser aggregate) { + this.handleAggregate(node, child, aggregate); + continue; + } else { + treeNode = this.commandManager.parserMapper().mapComponent(value); + } + this.addRequirement(child, treeNode); + if (canExecute(child)) { + treeNode.executable(); + } + this.addChildren(treeNode, child); + node.child(value.name(), treeNode); + } + } + + private void handleAggregate( + final CommandTreeNode node, + final CommandNode child, + final AggregateParser compound + ) { + final CommandTreeNode.Argument> treeNode; + final ArrayDeque>>> nodes = new ArrayDeque<>(); + for (final CommandComponent component : compound.components()) { + final String name = component.name(); + nodes.add(Pair.of(name, this.commandManager.parserMapper().mapParser(component.parser()))); + } + Pair>> argument = null; + while (!nodes.isEmpty()) { + final Pair>> prev = argument; + argument = nodes.removeLast(); + if (prev != null) { + argument.second().child(prev.first(), prev.second()); + } else { + // last node + if (canExecute(child)) { + argument.second().executable(); + } + } + this.addRequirement(child, argument.second()); + } + treeNode = argument.second(); + this.addChildren(treeNode, child); + node.child(compound.components().get(0).toString(), treeNode); + } + + private static boolean canExecute(final @NonNull CommandNode node) { + return node.isLeaf() + || !node.component().required() + || node.command() != null + || node.children().stream().noneMatch(c -> c.component().required()); + } + + private void addRequirement( + final @NonNull CommandNode cloud, + final @NonNull CommandTreeNode> node + ) { + final Map accessMap = + cloud.nodeMeta().getOrDefault(CommandNode.META_KEY_ACCESS, Collections.emptyMap()); + node.requires(cause -> this.checkAccess(cause, accessMap)); + } + + private boolean checkAccess(final CommandCause cause, final Map accessMap) { + final C cloudSender = this.commandManager.senderMapper().map(cause); + for (final Map.Entry entry : accessMap.entrySet()) { + if (GenericTypeReflector.isSuperType(entry.getKey(), cloudSender.getClass())) { + if (this.commandManager.testPermission(cloudSender, entry.getValue()).allowed()) { + return true; + } + } + } + return false; + } + + private String formatCommandForParsing(final @NonNull String arguments) { + if (arguments.isEmpty()) { + return this.label; + } + return this.label + " " + arguments; + } + + private String formatCommandForSuggestions(final @NonNull String arguments) { + return this.label + " " + arguments; + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/NodeSource.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/NodeSource.java new file mode 100644 index 00000000..dcd55871 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/NodeSource.java @@ -0,0 +1,41 @@ +// +// 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.sponge; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; + +/** + * Implemented by {@link org.incendo.cloud.parser.ArgumentParser} which also supply a special {@link CommandTreeNode.Argument}. + */ +public interface NodeSource { + + /** + * Get the node for this parser. + * + * @return argument node + */ + CommandTreeNode.@NonNull Argument> node(); + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCaptionKeys.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCaptionKeys.java new file mode 100644 index 00000000..0f02f3d5 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCaptionKeys.java @@ -0,0 +1,92 @@ +// +// 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.sponge; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.caption.Caption; + +/** + * {@link Caption} instances for messages in cloud-sponge. + */ +public final class SpongeCaptionKeys { + + private static final Collection RECOGNIZED_CAPTIONS = new HashSet<>(); + + /** + * Variables: {id}, {registry} + */ + public static final Caption ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY = of( + "argument.parse.failure.registry_entry.unknown_entry" + ); + + /** + * Variables: {name} + */ + public static final Caption ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_NAME = of( + "argument.parse.failure.user.cannot_find_user_with_name" + ); + + /** + * Variables: {uuid} + */ + public static final Caption ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_UUID = of( + "argument.parse.failure.user.cannot_find_user_with_uuid" + ); + + /** + * Variables: {input} + */ + public static final Caption ARGUMENT_PARSE_FAILURE_USER_INVALID_INPUT = of( + "argument.parse.failure.user.invalid_input" + ); + + /** + * Variables: None + */ + public static final Caption ARGUMENT_PARSE_FAILURE_GAME_PROFILE_TOO_MANY_SELECTED = of( + "argument.parse.failure.game_profile.too_many_selected" + ); + + private SpongeCaptionKeys() { + } + + private static @NonNull Caption of(final @NonNull String key) { + final Caption caption = Caption.of(key); + RECOGNIZED_CAPTIONS.add(caption); + return caption; + } + + /** + * Get an immutable collection containing all standard caption keys + * + * @return Immutable collection of keys + */ + public static @NonNull Collection<@NonNull Caption> spongeCaptionKeys() { + return Collections.unmodifiableCollection(RECOGNIZED_CAPTIONS); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandContextKeys.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandContextKeys.java new file mode 100644 index 00000000..f2fcbc44 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandContextKeys.java @@ -0,0 +1,47 @@ +// +// 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.sponge; + +import io.leangen.geantyref.TypeToken; +import org.incendo.cloud.key.CloudKey; +import org.spongepowered.api.command.CommandCause; + +/** + * Sponge related {@link org.incendo.cloud.context.CommandContext} keys. + */ +public final class SpongeCommandContextKeys { + + /** + * The Sponge native {@link org.spongepowered.api.command.CommandCause} instance is stored in the {@link org.incendo.cloud.context.CommandContext} + * by {@link SpongeCommandPreprocessor} + */ + public static final CloudKey COMMAND_CAUSE = CloudKey.of( + "cloud:sponge_command_cause", + TypeToken.get(CommandCause.class) + ); + + private SpongeCommandContextKeys() { + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandManager.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandManager.java new file mode 100644 index 00000000..fca8f492 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandManager.java @@ -0,0 +1,278 @@ +// +// 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.sponge; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import com.google.inject.Module; +import io.leangen.geantyref.TypeToken; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.SenderMapper; +import org.incendo.cloud.SenderMapperHolder; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.meta.CommandMeta; +import org.incendo.cloud.meta.SimpleCommandMeta; +import org.incendo.cloud.parser.ParserParameters; +import org.incendo.cloud.sponge.annotation.specifier.Center; +import org.incendo.cloud.sponge.parser.BlockInputParser; +import org.incendo.cloud.sponge.parser.BlockPredicateParser; +import org.incendo.cloud.sponge.parser.ComponentParser; +import org.incendo.cloud.sponge.parser.DataContainerParser; +import org.incendo.cloud.sponge.parser.GameProfileCollectionParser; +import org.incendo.cloud.sponge.parser.GameProfileParser; +import org.incendo.cloud.sponge.parser.ItemStackPredicateParser; +import org.incendo.cloud.sponge.parser.MultipleEntitySelectorParser; +import org.incendo.cloud.sponge.parser.MultiplePlayerSelectorParser; +import org.incendo.cloud.sponge.parser.NamedTextColorParser; +import org.incendo.cloud.sponge.parser.OperatorParser; +import org.incendo.cloud.sponge.parser.ProtoItemStackParser; +import org.incendo.cloud.sponge.parser.RegistryEntryParser; +import org.incendo.cloud.sponge.parser.ResourceKeyParser; +import org.incendo.cloud.sponge.parser.SingleEntitySelectorParser; +import org.incendo.cloud.sponge.parser.SinglePlayerSelectorParser; +import org.incendo.cloud.sponge.parser.UserParser; +import org.incendo.cloud.sponge.parser.Vector2dParser; +import org.incendo.cloud.sponge.parser.Vector2iParser; +import org.incendo.cloud.sponge.parser.Vector3dParser; +import org.incendo.cloud.sponge.parser.Vector3iParser; +import org.incendo.cloud.sponge.parser.WorldParser; +import org.incendo.cloud.sponge.suggestion.SpongeSuggestion; +import org.incendo.cloud.state.RegistrationState; +import org.incendo.cloud.suggestion.SuggestionFactory; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.command.CommandCause; +import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; +import org.spongepowered.api.registry.DefaultedRegistryType; +import org.spongepowered.api.registry.Registry; +import org.spongepowered.api.registry.RegistryType; +import org.spongepowered.api.registry.RegistryTypes; +import org.spongepowered.math.vector.Vector2d; +import org.spongepowered.math.vector.Vector3d; +import org.spongepowered.plugin.PluginContainer; + +/** + * Command manager for Sponge API v8. + *

+ * The manager supports Guice injection + * as long as the {@link CloudInjectionModule} is present in the injector. + * This can be achieved by using {@link com.google.inject.Injector#createChildInjector(Module...)} + * + * @param Command sender type + */ +public final class SpongeCommandManager extends CommandManager implements SenderMapperHolder { + + private final PluginContainer pluginContainer; + private final SenderMapper senderMapper; + private final SpongeParserMapper parserMapper; + private final SuggestionFactory suggestionFactory; + + /** + * Create a new command manager instance + * + * @param pluginContainer Owning plugin + * @param executionCoordinator Execution coordinator instance + * @param senderMapper Function mapping the custom command sender type to a Sponge CommandCause + */ + @SuppressWarnings("unchecked") + @Inject + public SpongeCommandManager( + final @NonNull PluginContainer pluginContainer, + final @NonNull ExecutionCoordinator executionCoordinator, + final @NonNull SenderMapper senderMapper + ) { + super(executionCoordinator, new SpongeRegistrationHandler()); + this.checkLateCreation(); + this.pluginContainer = pluginContainer; + ((SpongeRegistrationHandler) this.commandRegistrationHandler()).initialize(this); + this.senderMapper = senderMapper; + this.parserMapper = new SpongeParserMapper<>(); + this.registerCommandPreProcessor(new SpongeCommandPreprocessor<>(this)); + this.registerParsers(); + this.captionRegistry().registerProvider(new SpongeDefaultCaptionsProvider<>()); + this.suggestionFactory = super.suggestionFactory().mapped(SpongeSuggestion::spongeSuggestion); + + SpongeDefaultExceptionHandlers.register(this); + } + + @Override + public @NonNull SuggestionFactory suggestionFactory() { + return this.suggestionFactory; + } + + private void checkLateCreation() { + // Not the most accurate check, but will at least catch creation attempted after the server has started + if (!Sponge.isServerAvailable()) { + return; + } + throw new IllegalStateException( + "SpongeCommandManager must be created before the first firing of RegisterCommandEvent. (created too late)" + ); + } + + private void registerParsers() { + this.parserRegistry() + .registerParser(ComponentParser.componentParser()) + .registerParser(NamedTextColorParser.namedTextColorParser()) + .registerParser(OperatorParser.operatorParser()) + .registerParser(WorldParser.worldParser()) + .registerParser(ProtoItemStackParser.protoItemStackParser()) + .registerParser(ItemStackPredicateParser.itemStackPredicateParser()) + .registerParser(ResourceKeyParser.resourceKeyParser()) + .registerParser(GameProfileParser.gameProfileParser()) + .registerParser(GameProfileCollectionParser.gameProfileCollectionParser()) + .registerParser(BlockInputParser.blockInputParser()) + .registerParser(BlockPredicateParser.blockPredicateParser()) + .registerParser(UserParser.userParser()) + .registerParser(DataContainerParser.dataContainerParser()) + .registerAnnotationMapper( + Center.class, + (annotation, type) -> ParserParameters.single(SpongeParserParameters.CENTER_INTEGERS, true) + ) + .registerParserSupplier( + TypeToken.get(Vector2d.class), + params -> new Vector2dParser<>(params.get(SpongeParserParameters.CENTER_INTEGERS, false)) + ) + .registerParserSupplier( + TypeToken.get(Vector3d.class), + params -> new Vector3dParser<>(params.get(SpongeParserParameters.CENTER_INTEGERS, false)) + ) + .registerParser(Vector2iParser.vector2iParser()) + .registerParser(Vector3iParser.vector3iParser()) + .registerParser(SinglePlayerSelectorParser.singlePlayerSelectorParser()) + .registerParser(MultiplePlayerSelectorParser.multiplePlayerSelectorParser()) + .registerParser(SingleEntitySelectorParser.singleEntitySelectorParser()) + .registerParser(MultipleEntitySelectorParser.multipleEntitySelectorParser()); + + this.registerRegistryParsers(); + } + + private void registerRegistryParsers() { + final Set> ignoredRegistryTypes = ImmutableSet.of( + RegistryTypes.OPERATOR // We have a different Operator parser that doesn't use a ResourceKey as input + ); + for (final Field field : RegistryTypes.class.getDeclaredFields()) { + final Type generic = field.getGenericType(); /* RegistryType */ + if (!(generic instanceof ParameterizedType)) { + continue; + } + + final RegistryType registryType; + try { + registryType = (RegistryType) field.get(null); + } catch (final IllegalAccessException ex) { + throw new RuntimeException("Failed to access RegistryTypes." + field.getName(), ex); + } + if (ignoredRegistryTypes.contains(registryType) || !(registryType instanceof DefaultedRegistryType)) { + continue; + } + final DefaultedRegistryType defaultedRegistryType = (DefaultedRegistryType) registryType; + final Type valueType = ((ParameterizedType) generic).getActualTypeArguments()[0]; + + this.parserRegistry().registerParserSupplier( + TypeToken.get(valueType), + params -> new RegistryEntryParser<>(defaultedRegistryType) + ); + } + } + + @Override + public boolean hasPermission( + final @NonNull C sender, + final @NonNull String permission + ) { + if (permission.isEmpty()) { + return true; + } + return this.senderMapper.reverse(sender).hasPermission(permission); + } + + @Override + public @NonNull CommandMeta createDefaultCommandMeta() { + return SimpleCommandMeta.empty(); + } + + /** + * Get the {@link PluginContainer} of the plugin that owns this command manager. + * + * @return plugin container + */ + public @NonNull PluginContainer owningPluginContainer() { + return this.pluginContainer; + } + + /** + * Get the {@link SpongeParserMapper}, responsible for mapping Cloud + * {@link org.incendo.cloud.parser.ArgumentParser ArgumentParser} to Sponge + * {@link org.spongepowered.api.command.registrar.tree.CommandTreeNode.Argument CommandTreeNode.Arguments}. + * + * @return the parser mapper + */ + public @NonNull SpongeParserMapper parserMapper() { + return this.parserMapper; + } + + @Override + public @NonNull SenderMapper senderMapper() { + return this.senderMapper; + } + + void registrationCalled() { + if (!this.registrationCallbackListeners.isEmpty()) { + this.registrationCallbackListeners.forEach(listener -> listener.accept(this)); + this.registrationCallbackListeners.clear(); + } + if (this.state() != RegistrationState.AFTER_REGISTRATION) { + this.lockRegistration(); + } + } + + private final Set>> registrationCallbackListeners = new HashSet<>(); + + /** + * Add a listener to the command registration callback. + * + *

These listeners will be called just before command registration is finalized + * (during the first invocation of Cloud's internal {@link RegisterCommandEvent} listener).

+ * + *

This allows for registering commands at the latest possible point in the plugin + * lifecycle, which may be necessary for certain {@link Registry Registries} to have + * initialized.

+ * + * @param listener listener + */ + public void addRegistrationCallbackListener(final @NonNull Consumer<@NonNull SpongeCommandManager> listener) { + if (this.state() == RegistrationState.AFTER_REGISTRATION) { + throw new IllegalStateException("The SpongeCommandManager is in the AFTER_REGISTRATION state!"); + } + this.registrationCallbackListeners.add(listener); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandPreprocessor.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandPreprocessor.java new file mode 100644 index 00000000..70149772 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeCommandPreprocessor.java @@ -0,0 +1,62 @@ +// +// 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.sponge; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.execution.preprocessor.CommandPreprocessingContext; +import org.incendo.cloud.execution.preprocessor.CommandPreprocessor; +import org.spongepowered.api.command.CommandCause; + +/** + * Command preprocessor which decorates incoming {@link org.incendo.cloud.context.CommandContext} + * with Sponge specific objects + * + * @param Command sender type + */ +final class SpongeCommandPreprocessor implements CommandPreprocessor { + + private final SpongeCommandManager commandManager; + + /** + * The Sponge Command Preprocessor for storing Sponge-specific contexts in the command contexts + * + * @param commandManager command manager + */ + SpongeCommandPreprocessor(final @NonNull SpongeCommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public void accept(final @NonNull CommandPreprocessingContext context) { + final CommandCause commandCause = this.commandManager.senderMapper().reverse(context.commandContext().sender()); + context.commandContext().store(SpongeCommandContextKeys.COMMAND_CAUSE, commandCause); + + // For WrappedBrigadierParser. The CloudBrigadierManager will store this in context as well, however we are not using + // the CloudBrigadierManager, only the WrapperBrigadierParser. CommandCause is mixed into CommandSourceStack, which is + // Minecraft's native Brigadier sender type on the server. + context.commandContext().store(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER, commandCause); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeDefaultCaptionsProvider.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeDefaultCaptionsProvider.java new file mode 100644 index 00000000..cb6770be --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeDefaultCaptionsProvider.java @@ -0,0 +1,90 @@ +// +// 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.sponge; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.caption.CaptionProvider; +import org.incendo.cloud.caption.DelegatingCaptionProvider; + +/** + * Provides the default captions for messages in cloud-sponge. + * + * @param command sender type + */ +public final class SpongeDefaultCaptionsProvider extends DelegatingCaptionProvider { + + /** + * Default caption for {@link SpongeCaptionKeys#ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY} + */ + public static final String ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY = + "No such entry '{id}' in registry '{registry}'."; + + /** + * Default caption for {@link SpongeCaptionKeys#ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_NAME} + */ + public static final String ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_NAME = + "Cannot find a user with the name '{name}'."; + + /** + * Default caption for {@link SpongeCaptionKeys#ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_UUID} + */ + public static final String ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_UUID = + "Cannot find a user with the UUID '{uuid}'."; + + /** + * Default caption for {@link SpongeCaptionKeys#ARGUMENT_PARSE_FAILURE_USER_INVALID_INPUT} + */ + public static final String ARGUMENT_PARSE_FAILURE_USER_INVALID_INPUT = + "Input '{input}' is not a valid UUID or username."; + + /** + * Default caption for {@link SpongeCaptionKeys#ARGUMENT_PARSE_FAILURE_GAME_PROFILE_TOO_MANY_SELECTED} + */ + public static final String ARGUMENT_PARSE_FAILURE_GAME_PROFILE_TOO_MANY_SELECTED = + "The provided selector matched multiple game profiles, but only one is allowed."; + + private static final CaptionProvider PROVIDER = CaptionProvider.constantProvider() + .putCaption( + SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY, + ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY) + .putCaption( + SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_NAME, + ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_NAME) + .putCaption( + SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_UUID, + ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_UUID) + .putCaption( + SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_USER_INVALID_INPUT, + ARGUMENT_PARSE_FAILURE_USER_INVALID_INPUT) + .putCaption( + SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_GAME_PROFILE_TOO_MANY_SELECTED, + ARGUMENT_PARSE_FAILURE_GAME_PROFILE_TOO_MANY_SELECTED) + .build(); + + @SuppressWarnings("unchecked") + @Override + public @NonNull CaptionProvider delegate() { + return (CaptionProvider) PROVIDER; + } +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeDefaultExceptionHandlers.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeDefaultExceptionHandlers.java new file mode 100644 index 00000000..9e49e5f5 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeDefaultExceptionHandlers.java @@ -0,0 +1,101 @@ +// +// 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.sponge; + +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.util.ComponentMessageThrowable; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.incendo.cloud.exception.ArgumentParseException; +import org.incendo.cloud.exception.CommandExecutionException; +import org.incendo.cloud.exception.InvalidCommandSenderException; +import org.incendo.cloud.exception.InvalidSyntaxException; +import org.incendo.cloud.exception.NoPermissionException; +import org.incendo.cloud.exception.NoSuchCommandException; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + +@DefaultQualifier(NonNull.class) +final class SpongeDefaultExceptionHandlers { + private static final Component NULL = text("null"); + private static final Component MESSAGE_INTERNAL_ERROR = + text("An internal error occurred while attempting to perform this command.", RED); + private static final Component MESSAGE_NO_PERMS = + text("I'm sorry, but you do not have permission to perform this command. " + + "Please contact the server administrators if you believe that this is in error.", RED); + private static final Component MESSAGE_UNKNOWN_COMMAND = text("Unknown command. Type \"/help\" for help."); + + private SpongeDefaultExceptionHandlers() { + } + + static void register(final SpongeCommandManager mgr) { + mgr.exceptionController().registerHandler(InvalidSyntaxException.class, ctx -> { + final Audience audience = ctx.context().get(SpongeCommandContextKeys.COMMAND_CAUSE).audience(); + audience.sendMessage(text().append( + text("Invalid Command Syntax. Correct command syntax is: ", RED), + text("/" + ctx.exception().correctSyntax(), GRAY) + ).build()); + }); + mgr.exceptionController().registerHandler(InvalidCommandSenderException.class, ctx -> { + final Audience audience = ctx.context().get(SpongeCommandContextKeys.COMMAND_CAUSE).audience(); + audience.sendMessage(text(ctx.exception().getMessage(), RED)); + }); + mgr.exceptionController().registerHandler(NoPermissionException.class, ctx -> { + final Audience audience = ctx.context().get(SpongeCommandContextKeys.COMMAND_CAUSE).audience(); + audience.sendMessage(MESSAGE_NO_PERMS); + }); + mgr.exceptionController().registerHandler(NoSuchCommandException.class, ctx -> { + final Audience audience = ctx.context().get(SpongeCommandContextKeys.COMMAND_CAUSE).audience(); + audience.sendMessage(MESSAGE_UNKNOWN_COMMAND); + }); + mgr.exceptionController().registerHandler(ArgumentParseException.class, ctx -> { + final Audience audience = ctx.context().get(SpongeCommandContextKeys.COMMAND_CAUSE).audience(); + audience.sendMessage(text().append( + text("Invalid Command Argument: ", RED), + getMessage(ctx.exception().getCause()).colorIfAbsent(GRAY) + ).build()); + }); + mgr.exceptionController().registerHandler(CommandExecutionException.class, ctx -> { + final Audience audience = ctx.context().get(SpongeCommandContextKeys.COMMAND_CAUSE).audience(); + audience.sendMessage(MESSAGE_INTERNAL_ERROR); + mgr.owningPluginContainer().logger() + .error("Exception executing command handler", ctx.exception().getCause()); + }); + mgr.exceptionController().registerHandler(Throwable.class, ctx -> { + final Audience audience = ctx.context().get(SpongeCommandContextKeys.COMMAND_CAUSE).audience(); + audience.sendMessage(MESSAGE_INTERNAL_ERROR); + mgr.owningPluginContainer().logger() + .error("An unhandled exception was thrown during command execution", ctx.exception()); + }); + } + + private static Component getMessage(final Throwable throwable) { + final @Nullable Component msg = ComponentMessageThrowable.getOrConvertMessage(throwable); + return msg == null ? NULL : msg; + } +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeParserMapper.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeParserMapper.java new file mode 100644 index 00000000..d816d6f7 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeParserMapper.java @@ -0,0 +1,309 @@ +// +// 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.sponge; + +import io.leangen.geantyref.GenericTypeReflector; +import io.leangen.geantyref.TypeToken; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.MappedArgumentParser; +import org.incendo.cloud.parser.flag.CommandFlagParser; +import org.incendo.cloud.parser.standard.BooleanParser; +import org.incendo.cloud.parser.standard.ByteParser; +import org.incendo.cloud.parser.standard.DoubleParser; +import org.incendo.cloud.parser.standard.FloatParser; +import org.incendo.cloud.parser.standard.IntegerParser; +import org.incendo.cloud.parser.standard.LongParser; +import org.incendo.cloud.parser.standard.ShortParser; +import org.incendo.cloud.parser.standard.StringArrayParser; +import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.parser.standard.UUIDParser; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; + +import static java.util.Objects.requireNonNull; + +/** + * Class responsible for mapping Cloud {@link org.incendo.cloud.parser.ArgumentParser ArgumentParsers} to Sponge + * {@link CommandTreeNode.Argument CommandTreeNode.Arguments}. + * + * @param command sender type + */ +public final class SpongeParserMapper { + + private final Map, Mapping> mappers = new HashMap<>(); + + SpongeParserMapper() { + this.initStandardMappers(); + } + + CommandTreeNode.Argument> mapComponent(final CommandComponent commandComponent) { + final CommandTreeNode.Argument> result = this.mapParser(commandComponent.parser()); + // final boolean customSuggestionsProvider = !DELEGATING_SUGGESTIONS_PROVIDER.isInstance(commandComponent.getSuggestionsProvider()); + // todo: not exactly the same as in v1... + final boolean customSuggestionsProvider = commandComponent.parser() != commandComponent.suggestionProvider(); + if (customSuggestionsProvider) { + result.customCompletions(); + } + return result; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + CommandTreeNode.Argument> mapParser(final ArgumentParser argumentParser) { + final CommandTreeNode.Argument> result; + ArgumentParser parser = argumentParser; + while (parser instanceof MappedArgumentParser) { + parser = ((MappedArgumentParser) parser).baseParser(); + } + final Mapping mapper = this.mappers.get(parser.getClass()); + if (mapper != null) { + final CommandTreeNode.Argument> apply = + (CommandTreeNode.Argument>) ((Function) mapper.mapper).apply(parser); + if (mapper.cloudSuggestions) { + apply.customCompletions(); + return apply; + } + result = apply; + } else if (parser instanceof NodeSource) { + result = ((NodeSource) parser).node(); + } else { + result = CommandTreeNodeTypes.STRING.get().createNode().customCompletions().word(); + } + return result; + } + + private void initStandardMappers() { + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(stringParser -> { + final StringParser.StringMode mode = stringParser.stringMode(); + if (mode == StringParser.StringMode.SINGLE) { + return CommandTreeNodeTypes.STRING.get().createNode().customCompletions().word(); + } else if (mode == StringParser.StringMode.QUOTED) { + return CommandTreeNodeTypes.STRING.get().createNode().customCompletions(); + } else if (mode == StringParser.StringMode.GREEDY || mode == StringParser.StringMode.GREEDY_FLAG_YIELDING) { + return CommandTreeNodeTypes.STRING.get().createNode().customCompletions().greedy(); + } + throw new IllegalArgumentException("Unknown string mode '" + mode + "'!"); + })); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(byteParser -> { + final CommandTreeNode.Range node = CommandTreeNodeTypes.INTEGER.get().createNode(); + node.min((int) byteParser.range().minByte()); + node.max((int) byteParser.range().maxByte()); + return node; + }).cloudSuggestions(true)); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(shortParser -> { + final CommandTreeNode.Range node = CommandTreeNodeTypes.INTEGER.get().createNode(); + node.min((int) shortParser.range().minShort()); + node.max((int) shortParser.range().maxShort()); + return node; + }).cloudSuggestions(true)); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(integerParser -> { + final CommandTreeNode.Range node = CommandTreeNodeTypes.INTEGER.get().createNode(); + if (integerParser.hasMin()) { + node.min(integerParser.range().minInt()); + } + if (integerParser.hasMax()) { + node.max(integerParser.range().maxInt()); + } + return node; + }).cloudSuggestions(true)); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(floatParser -> { + final CommandTreeNode.Range node = CommandTreeNodeTypes.FLOAT.get().createNode(); + if (floatParser.hasMin()) { + node.min(floatParser.range().minFloat()); + } + if (floatParser.hasMax()) { + node.max(floatParser.range().maxFloat()); + } + return node; + }).cloudSuggestions(true)); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(doubleParser -> { + final CommandTreeNode.Range node = CommandTreeNodeTypes.DOUBLE.get().createNode(); + if (doubleParser.hasMin()) { + node.min(doubleParser.range().minDouble()); + } + if (doubleParser.hasMax()) { + node.max(doubleParser.range().maxDouble()); + } + return node; + }).cloudSuggestions(true)); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(longParser -> { + final CommandTreeNode.Range node = CommandTreeNodeTypes.LONG.get().createNode(); + if (longParser.hasMin()) { + node.min(longParser.range().minLong()); + } + if (longParser.hasMax()) { + node.max(longParser.range().maxLong()); + } + return node; + }).cloudSuggestions(true)); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(booleanParser -> { + return CommandTreeNodeTypes.BOOL.get().createNode(); + })); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(flagArgumentParser -> { + return CommandTreeNodeTypes.STRING.get().createNode().customCompletions().greedy(); + })); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(stringArrayParser -> { + return CommandTreeNodeTypes.STRING.get().createNode().customCompletions().greedy(); + })); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(uuidParser -> { + return CommandTreeNodeTypes.UUID.get().createNode(); + })); + } + + /** + * Register a mapping from a Cloud {@link ArgumentParser} type to a Sponge {@link CommandTreeNode.Argument}. This will + * replace any existing mapping. + * + * @param cloudType cloud argument parser type + * @param configurer builder configurer + * @param cloud argument parser type + */ + public > void registerMapping( + final @NonNull TypeToken cloudType, + final @NonNull Consumer> configurer + ) { + final MappingBuilderImpl builder = new MappingBuilderImpl<>(); + configurer.accept(builder); + this.mappers.put(GenericTypeReflector.erase(cloudType.getType()), builder.build()); + } + + /** + * Set whether to use Cloud suggestions, or to fall back on the suggestions provided + * by the {@link CommandTreeNode.Argument} for an already registered mapping. This is effectively {@code false} by default. + * + * @param parserType cloud argument parser type + * @param cloudSuggestions Whether or not Cloud suggestions should be used + * @param argument value type + * @param cloud argument parser type + * @throws IllegalArgumentException when there is no mapper registered for the provided argument type + */ + public > void cloudSuggestions( + final @NonNull TypeToken parserType, + final boolean cloudSuggestions + ) throws IllegalArgumentException { + final Mapping mapping = this.mappers.get(GenericTypeReflector.erase(parserType.getType())); + if (mapping == null) { + throw new IllegalArgumentException( + "No mapper registered for type: " + GenericTypeReflector + .erase(parserType.getType()) + .toGenericString() + ); + } + this.mappers.put( + GenericTypeReflector.erase(parserType.getType()), + new Mapping<>(mapping.mapper, cloudSuggestions) + ); + } + + /** + * Set whether to use Cloud's custom suggestions for number argument types. If {@code false}, the default Brigadier number + * completions will be used. + * + * @param cloudNumberSuggestions whether to use Cloud number suggestions + */ + public void cloudNumberSuggestions(final boolean cloudNumberSuggestions) { + this.cloudSuggestions(new TypeToken>() {}, cloudNumberSuggestions); + this.cloudSuggestions(new TypeToken>() {}, cloudNumberSuggestions); + this.cloudSuggestions(new TypeToken>() {}, cloudNumberSuggestions); + this.cloudSuggestions(new TypeToken>() {}, cloudNumberSuggestions); + this.cloudSuggestions(new TypeToken>() {}, cloudNumberSuggestions); + this.cloudSuggestions(new TypeToken>() {}, cloudNumberSuggestions); + } + + /** + * Builder for mappings from Cloud {@link ArgumentParser ArgumentParsers} to Sponge + * {@link CommandTreeNode.Argument CommandTreeNode.Arguments} + * + * @param command sender type + * @param parser type + */ + public interface MappingBuilder> { + + /** + * Set whether to use cloud suggestions, or to fall back onto {@link CommandTreeNodeTypes}. By default, this is set to + * {@code false}. + * + * @param cloudSuggestions whether to use cloud suggestions + * @return this builder + */ + @NonNull MappingBuilder cloudSuggestions(boolean cloudSuggestions); + + /** + * Set the mapper function from {@link A} to {@link CommandTreeNode.Argument}. + * + * @param mapper mapper function + * @return this builder + */ + @NonNull MappingBuilder to(@NonNull Function>> mapper); + + } + + private static final class MappingBuilderImpl> implements MappingBuilder { + + private Function>> mapper; + private boolean cloudSuggestions; + + @Override + public @NonNull MappingBuilder cloudSuggestions(final boolean cloudSuggestions) { + this.cloudSuggestions = cloudSuggestions; + return this; + } + + @Override + public @NonNull MappingBuilder to( + final @NonNull Function>> mapper + ) { + this.mapper = mapper; + return this; + } + + private SpongeParserMapper.@NonNull Mapping build() { + requireNonNull(this.mapper, "Must provide a mapper function!"); + return new Mapping<>(this.mapper, this.cloudSuggestions); + } + + } + + private record Mapping>( + Function>> mapper, + boolean cloudSuggestions + ) {} + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeParserParameters.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeParserParameters.java new file mode 100644 index 00000000..fe14fff7 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeParserParameters.java @@ -0,0 +1,50 @@ +// +// 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.sponge; + +import io.leangen.geantyref.TypeToken; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.parser.ParserParameter; + +/** + * {@link ParserParameter} keys for cloud-sponge. + */ +public final class SpongeParserParameters { + + private SpongeParserParameters() { + } + + /** + * Indicates that positions should be centered on the middle of blocks, i.e. x.5. + */ + public static final ParserParameter CENTER_INTEGERS = create("center_integers", TypeToken.get(Boolean.class)); + + private static @NonNull ParserParameter create( + final @NonNull String key, + final @NonNull TypeToken expectedType + ) { + return new ParserParameter<>(key, expectedType); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeRegistrationHandler.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeRegistrationHandler.java new file mode 100644 index 00000000..acb25898 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/SpongeRegistrationHandler.java @@ -0,0 +1,152 @@ +// +// 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.sponge; + +import io.leangen.geantyref.TypeToken; +import java.util.HashSet; +import java.util.Set; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.server.MinecraftServer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.internal.CommandNode; +import org.incendo.cloud.internal.CommandRegistrationHandler; +import org.incendo.cloud.minecraft.modded.internal.ContextualArgumentTypeProvider; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.MappedArgumentParser; +import org.incendo.cloud.parser.aggregate.AggregateParser; +import org.incendo.cloud.parser.standard.EitherParser; +import org.spongepowered.api.Server; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.command.Command; +import org.spongepowered.api.event.EventListenerRegistration; +import org.spongepowered.api.event.Order; +import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; +import org.spongepowered.api.event.lifecycle.StartedEngineEvent; + +import static java.util.Objects.requireNonNull; + +final class SpongeRegistrationHandler implements CommandRegistrationHandler { + + private SpongeCommandManager commandManager; + private final Set> registeredCommands = new HashSet<>(); + + SpongeRegistrationHandler() { + } + + private void handleRegistrationEvent(final RegisterCommandEvent event) { + this.commandManager.registrationCalled(); + for (final CommandNode node : this.commandManager.commandTree().rootNodes()) { + this.registerCommand(event, requireNonNull(node.component())); + } + } + + private void registerCommand(final RegisterCommandEvent event, final CommandComponent rootLiteral) { + final String label = rootLiteral.name(); + event.register( + this.commandManager.owningPluginContainer(), + new CloudSpongeCommand<>(label, this.commandManager), + label, + rootLiteral.alternativeAliases().toArray(new String[0]) + ); + } + + private void startedEngine(final StartedEngineEvent serverStartedEngineEvent) { + final MinecraftServer engine = (MinecraftServer) serverStartedEngineEvent.engine(); + ContextualArgumentTypeProvider.withBuildContext( + this.commandManager, + CommandBuildContext.simple(engine.registryAccess(), engine.getWorldData().enabledFeatures()), + true, + () -> { + for (final org.incendo.cloud.Command registeredCommand : this.registeredCommands) { + for (final CommandComponent component : registeredCommand.components()) { + if (component.type() == CommandComponent.ComponentType.LITERAL + || component.type() == CommandComponent.ComponentType.FLAG) { + continue; + } + + for (final ArgumentParser parser : unwrap(component.parser())) { + if (parser instanceof WrappedBrigadierParser wrappedBrigadierParser) { + wrappedBrigadierParser.nativeArgumentType(); + } + } + } + } + } + ); + } + + void initialize(final @NonNull SpongeCommandManager commandManager) { + this.commandManager = commandManager; + Sponge.eventManager().registerListener( + EventListenerRegistration.builder(new TypeToken>() {}) + .plugin(this.commandManager.owningPluginContainer()) + .listener(this::handleRegistrationEvent) + .order(Order.DEFAULT) + .build() + ); + Sponge.eventManager().registerListener( + EventListenerRegistration.builder(new TypeToken>() {}) + .plugin(this.commandManager.owningPluginContainer()) + .listener(this::startedEngine) + .order(Order.DEFAULT) + .build() + ); + } + + @Override + public boolean registerCommand(final org.incendo.cloud.@NonNull Command command) { + this.registeredCommands.add(command); + return true; + } + + private static Set> unwrap(final ArgumentParser parser) { + final HashSet> parsers = new HashSet<>(); + unwrap(parsers, parser); + return parsers; + } + + private static void unwrap( + final Set> parsers, + final ArgumentParser parser + ) { + if (parser instanceof MappedArgumentParser mapped) { + unwrap(parsers, mapped.baseParser()); + return; + } + if (parser instanceof EitherParser eitherParser) { + unwrap(parsers, eitherParser.primary().parser()); + unwrap(parsers, eitherParser.fallback().parser()); + return; + } + if (parser instanceof AggregateParser aggregateParser) { + for (final CommandComponent component : aggregateParser.components()) { + unwrap(parsers, component.parser()); + } + return; + } + parsers.add(parser); + } +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/package-info.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/package-info.java new file mode 100644 index 00000000..2bd372f6 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/package-info.java @@ -0,0 +1,27 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// 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. +// +/** + * Annotations for cloud-sponge. + */ +package org.incendo.cloud.sponge.annotation; diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/specifier/Center.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/specifier/Center.java new file mode 100644 index 00000000..e347bf09 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/specifier/Center.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.sponge.annotation.specifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.incendo.cloud.sponge.parser.Vector2dParser; +import org.incendo.cloud.sponge.parser.Vector3dParser; + +/** + * Annotation used to enable coordinate centering for {@link Vector3dParser} and {@link Vector2dParser}. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Center { + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/specifier/package-info.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/specifier/package-info.java new file mode 100644 index 00000000..182e4f7f --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/annotation/specifier/package-info.java @@ -0,0 +1,27 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// 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. +// +/** + * Specifier annotations for cloud-sponge. + */ +package org.incendo.cloud.sponge.annotation.specifier; diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/BlockInput.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/BlockInput.java new file mode 100644 index 00000000..987dc8d2 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/BlockInput.java @@ -0,0 +1,77 @@ +// +// 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.sponge.data; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.api.block.BlockState; +import org.spongepowered.api.data.persistence.DataContainer; +import org.spongepowered.api.world.BlockChangeFlag; +import org.spongepowered.api.world.server.ServerLocation; + +/** + * Intermediary result for an argument which parses a {@link BlockState} and optional extra NBT data. + */ +public interface BlockInput { + + /** + * Get the parsed {@link BlockState}. + * + * @return the {@link BlockState} + */ + @NonNull BlockState blockState(); + + /** + * Get any extra data besides the {@link BlockState} that may have been parsed. + * + *

Will return {@code null} if there is no extra data.

+ * + * @return the extra data or {@code null} + */ + @Nullable DataContainer extraData(); + + /** + * Replace the block at the given {@link ServerLocation} with the parsed {@link BlockState}, + * and if the placed block is a {@link org.spongepowered.api.block.entity.BlockEntity}, applies any + * extra NBT data from {@link #extraData()}. + * + * @param location location + * @return return whether the block change was successful + * @see ServerLocation#setBlock(BlockState) + */ + boolean place(@NonNull ServerLocation location); + + /** + * Replace the block at the given {@link ServerLocation} with the parsed {@link BlockState}, + * and if the placed block is a {@link org.spongepowered.api.block.entity.BlockEntity}, applies any + * extra NBT data from {@link #extraData()}. + * + * @param location location + * @param flag the various {@link BlockChangeFlag change flags} controlling some interactions + * @return return whether the block change was successful + * @see ServerLocation#setBlock(BlockState, BlockChangeFlag) + */ + boolean place(@NonNull ServerLocation location, @NonNull BlockChangeFlag flag); + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/BlockPredicate.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/BlockPredicate.java new file mode 100644 index 00000000..b3dc3c79 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/BlockPredicate.java @@ -0,0 +1,51 @@ +// +// 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.sponge.data; + +import java.util.function.Predicate; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.spongepowered.api.world.server.ServerLocation; +import org.spongepowered.api.world.server.ServerWorld; + +/** + * A {@link Predicate} for blocks in a {@link ServerWorld}, parsed from user input. + * + *

By default, a parsed {@link BlockPredicate} will not load chunks to perform tests. It will simply + * return {@code false} when attempting to test a block in unloaded chunks.

+ * + *

To get a {@link BlockPredicate} which will load chunks, use {@link #loadChunks()}.

+ */ +public interface BlockPredicate extends Predicate { + + /** + * Get a version of this {@link BlockPredicate} which will load chunks in order to perform + * tests. + * + *

If this {@link BlockPredicate} already loads chunks, it will simply return itself.

+ * + * @return a {@link BlockPredicate} which loads chunks + */ + @NonNull BlockPredicate loadChunks(); + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/GameProfileCollection.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/GameProfileCollection.java new file mode 100644 index 00000000..0602d135 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/GameProfileCollection.java @@ -0,0 +1,36 @@ +// +// 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.sponge.data; + +import java.util.Collection; +import org.spongepowered.api.profile.GameProfile; + +/** + * Cloud result type for a {@link Collection} of {@link GameProfile GameProfiles}. + * + *

A successfully parsed result will always contain at least {@code 1} element.

+ */ +public interface GameProfileCollection extends Collection { + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/ItemStackPredicate.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/ItemStackPredicate.java new file mode 100644 index 00000000..a52a3fe6 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/ItemStackPredicate.java @@ -0,0 +1,34 @@ +// +// 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.sponge.data; + +import java.util.function.Predicate; +import org.spongepowered.api.item.inventory.ItemStack; + +/** + * A {@link Predicate} for {@link ItemStack ItemStacks}, parsed from user input. + */ +public interface ItemStackPredicate extends Predicate { + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/MultipleEntitySelector.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/MultipleEntitySelector.java new file mode 100644 index 00000000..72b420e9 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/MultipleEntitySelector.java @@ -0,0 +1,34 @@ +// +// 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.sponge.data; + +import org.spongepowered.api.entity.Entity; + +/** + * A wrapper for {@link org.spongepowered.api.command.selector.Selector Selectors} which may select one or more + * {@link org.spongepowered.api.entity.Entity Entities}. + */ +public interface MultipleEntitySelector extends SelectorWrapper { + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/MultiplePlayerSelector.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/MultiplePlayerSelector.java new file mode 100644 index 00000000..707b5c25 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/MultiplePlayerSelector.java @@ -0,0 +1,34 @@ +// +// 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.sponge.data; + +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +/** + * A wrapper for {@link org.spongepowered.api.command.selector.Selector Selectors} which may + * select one or more {@link ServerPlayer Players}. + */ +public interface MultiplePlayerSelector extends SelectorWrapper { + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/ProtoItemStack.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/ProtoItemStack.java new file mode 100644 index 00000000..e8c14be3 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/ProtoItemStack.java @@ -0,0 +1,84 @@ +// +// 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.sponge.data; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.sponge.exception.ComponentMessageRuntimeException; +import org.spongepowered.api.data.persistence.DataContainer; +import org.spongepowered.api.item.ItemType; +import org.spongepowered.api.item.inventory.ItemStack; +import org.spongepowered.api.item.inventory.ItemStackSnapshot; + +/** + * Result for an argument which parses an {@link ItemType} and optional extra NBT data. + */ +public interface ProtoItemStack { + + /** + * Get the {@link ItemType} of this {@link ProtoItemStack}. + * + * @return the {@link ItemType} + */ + @NonNull ItemType itemType(); + + /** + * Get any extra data besides the {@link ItemType} that may have been parsed. + * + *

Will return {@code null} if there is no extra data.

+ * + * @return the extra data or {@code null} + */ + @Nullable DataContainer extraData(); + + /** + * Create a new {@link ItemStack} from the state of this {@link ProtoItemStack}. + * + *

A {@link ComponentMessageRuntimeException} will be thrown if the stack size was too large for the + * provided {@link ItemType}.

+ * + * @param stackSize stack size + * @param respectMaximumStackSize whether to respect {@link ItemType#maxStackQuantity()} + * @return the created {@link ItemStack} + * @throws ComponentMessageRuntimeException if the {@link ItemStack} could not be created + */ + @NonNull ItemStack createItemStack(int stackSize, boolean respectMaximumStackSize) throws ComponentMessageRuntimeException; + + /** + * Create a new {@link ItemStackSnapshot} from the state of this {@link ProtoItemStack}. + * + *

A {@link ComponentMessageRuntimeException} will be thrown if the stack size was too large for the + * provided {@link ItemType}.

+ * + * @param stackSize stack size + * @param respectMaximumStackSize whether to respect {@link ItemType#maxStackQuantity()} + * @return the created {@link ItemStackSnapshot} + * @throws ComponentMessageRuntimeException if the {@link ItemStackSnapshot} could not be created + */ + @NonNull ItemStackSnapshot createItemStackSnapshot( + int stackSize, + boolean respectMaximumStackSize + ) throws ComponentMessageRuntimeException; + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SelectorWrapper.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SelectorWrapper.java new file mode 100644 index 00000000..c1c08268 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SelectorWrapper.java @@ -0,0 +1,82 @@ +// +// 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.sponge.data; + +import java.util.Collection; +import java.util.Collections; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.spongepowered.api.command.selector.Selector; + +/** + * Cloud wrapper for parsed {@link org.spongepowered.api.command.selector.Selector Selectors} and their results. + * + * @param result type + */ +public interface SelectorWrapper { + + /** + * Get the raw string associated with the selector. + * + * @return the input + */ + @NonNull String inputString(); + + /** + * Get the wrapped {@link Selector}. + * + * @return the selector + */ + @NonNull Selector selector(); + + /** + * Resolve the value of this selector. + * + *

A successfully parsed selector must match one or more values

+ * + * @return all matched entities + */ + @NonNull Collection get(); + + /** + * A specialized selector that can only return one value. + * + * @param the value type + */ + interface Single extends SelectorWrapper { + + @Override + default @NonNull Collection get() { + return Collections.singletonList(this.getSingle()); + } + + /** + * Get the single value from this selector. + * + * @return the value + */ + @NonNull R getSingle(); + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SingleEntitySelector.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SingleEntitySelector.java new file mode 100644 index 00000000..024eeeba --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SingleEntitySelector.java @@ -0,0 +1,34 @@ +// +// 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.sponge.data; + +import org.spongepowered.api.entity.Entity; + +/** + * A wrapper for {@link org.spongepowered.api.command.selector.Selector Selectors} which + * may select a single {@link Entity}. + */ +public interface SingleEntitySelector extends SelectorWrapper.Single { + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SinglePlayerSelector.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SinglePlayerSelector.java new file mode 100644 index 00000000..e673366d --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/SinglePlayerSelector.java @@ -0,0 +1,34 @@ +// +// 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.sponge.data; + +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +/** + * A wrapper for {@link org.spongepowered.api.command.selector.Selector Selectors} which may + * select a single {@link ServerPlayer}. + */ +public interface SinglePlayerSelector extends SelectorWrapper.Single { + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/package-info.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/package-info.java new file mode 100644 index 00000000..4f838515 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/data/package-info.java @@ -0,0 +1,27 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// 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. +// +/** + * Data holders for cloud-sponge. + */ +package org.incendo.cloud.sponge.data; diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/exception/ComponentMessageRuntimeException.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/exception/ComponentMessageRuntimeException.java new file mode 100644 index 00000000..021bbf44 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/exception/ComponentMessageRuntimeException.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.sponge.exception; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.adventure.util.ComponentMessageThrowable; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A subclass of {@link RuntimeException} that contains a rich message that is an instance of + * {@link Component} rather than a String. This allows formatted and localized + * exception messages. + */ +@SuppressWarnings("serial") +public class ComponentMessageRuntimeException extends RuntimeException implements ComponentMessageThrowable { + + private static final long serialVersionUID = 2152146048432114275L; + + private final @Nullable Component message; + + /** + * Constructs a new {@link ComponentMessageRuntimeException}. + */ + public ComponentMessageRuntimeException() { + this.message = null; + } + + /** + * Constructs a new {@link ComponentMessageRuntimeException} with the given message. + * + * @param message The detail message + */ + public ComponentMessageRuntimeException(final @Nullable ComponentLike message) { + this.message = message == null ? null : message.asComponent(); + } + + /** + * Constructs a new {@link ComponentMessageRuntimeException} with the given message and + * cause. + * + * @param message The detail message + * @param throwable The cause + */ + public ComponentMessageRuntimeException(final @Nullable ComponentLike message, final @Nullable Throwable throwable) { + super(throwable); + this.message = message == null ? null : message.asComponent(); + } + + /** + * Constructs a new {@link ComponentMessageRuntimeException} with the given cause. + * + * @param throwable The cause + */ + public ComponentMessageRuntimeException(final @Nullable Throwable throwable) { + super(throwable); + this.message = null; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable String getMessage() { + return PlainTextComponentSerializer.plainText().serializeOrNull(this.message); + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable Component componentMessage() { + return this.message; + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/exception/package-info.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/exception/package-info.java new file mode 100644 index 00000000..04622cea --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/exception/package-info.java @@ -0,0 +1,27 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// 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. +// +/** + * Exception types for cloud-sponge. + */ +package org.incendo.cloud.sponge.exception; diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/package-info.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/package-info.java new file mode 100644 index 00000000..c3db0785 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/package-info.java @@ -0,0 +1,27 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// 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. +// +/** + * Cloud for Sponge 8 + */ +package org.incendo.cloud.sponge; diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/BlockInputParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/BlockInputParser.java new file mode 100644 index 00000000..52e7ff6d --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/BlockInputParser.java @@ -0,0 +1,160 @@ +// +// 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.sponge.parser; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.arguments.blocks.BlockStateArgument; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.minecraft.modded.internal.ContextualArgumentTypeProvider; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.sponge.data.BlockInput; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.block.BlockState; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.data.persistence.DataContainer; +import org.spongepowered.api.world.BlockChangeFlag; +import org.spongepowered.api.world.BlockChangeFlags; +import org.spongepowered.api.world.server.ServerLocation; +import org.spongepowered.common.data.persistence.NBTTranslator; +import org.spongepowered.common.util.VecHelper; +import org.spongepowered.common.world.SpongeBlockChangeFlag; + +/** + * An argument for parsing {@link BlockInput} from a {@link BlockState} + * and optional extra NBT data. + * + *

Example input strings:

+ *
    + *
  • {@code stone}
  • + *
  • {@code minecraft:stone}
  • + *
  • {@code andesite_stairs[waterlogged=true,facing=east]}
  • + *
+ * + * @param command sender type + */ +public final class BlockInputParser implements NodeSource, ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link BlockInputParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor blockInputParser() { + return ParserDescriptor.of(new BlockInputParser<>(), BlockInput.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser( + new ContextualArgumentTypeProvider<>(BlockStateArgument::block) + ).flatMapSuccess((ctx, blockInput) -> + ArgumentParseResult.successFuture(new BlockInputImpl(blockInput))); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.BLOCK_STATE.get().createNode(); + } + + private static final class BlockInputImpl implements BlockInput { + + // todo: use accessor + private static final Field COMPOUND_TAG_FIELD = + Arrays.stream(net.minecraft.commands.arguments.blocks.BlockInput.class.getDeclaredFields()) + .filter(f -> f.getType().equals(CompoundTag.class)) + .findFirst() + .orElseThrow(IllegalStateException::new); + + static { + COMPOUND_TAG_FIELD.setAccessible(true); + } + + private final net.minecraft.commands.arguments.blocks.BlockInput blockInput; + private final @Nullable DataContainer extraData; + + BlockInputImpl(final net.minecraft.commands.arguments.blocks.@NonNull BlockInput blockInput) { + this.blockInput = blockInput; + try { + final CompoundTag tag = (CompoundTag) COMPOUND_TAG_FIELD.get(blockInput); + this.extraData = tag == null ? null : NBTTranslator.INSTANCE.translate(tag); + } catch (final IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public @NonNull BlockState blockState() { + return (BlockState) this.blockInput.getState(); + } + + @Override + public @Nullable DataContainer extraData() { + return this.extraData; + } + + @Override + public boolean place(final @NonNull ServerLocation location) { + return this.place(location, BlockChangeFlags.DEFAULT_PLACEMENT); + } + + @Override + public boolean place(final @NonNull ServerLocation location, final @NonNull BlockChangeFlag flag) { + return this.blockInput.place( + (ServerLevel) location.world(), + VecHelper.toBlockPos(location.position()), + ((SpongeBlockChangeFlag) flag).getRawFlag() + ); + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/BlockPredicateParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/BlockPredicateParser.java new file mode 100644 index 00000000..fd840818 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/BlockPredicateParser.java @@ -0,0 +1,128 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import net.minecraft.commands.arguments.blocks.BlockPredicateArgument; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.minecraft.modded.internal.ContextualArgumentTypeProvider; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.sponge.data.BlockPredicate; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.world.server.ServerLocation; +import org.spongepowered.common.util.VecHelper; + +/** + * An argument for parsing {@link BlockPredicate BlockPredicates}. + * + * @param command sender type + */ +public final class BlockPredicateParser implements ArgumentParser.FutureArgumentParser, + NodeSource, SuggestionProvider { + + /** + * Creates a new {@link BlockPredicateParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor blockPredicateParser() { + return ParserDescriptor.of(new BlockPredicateParser<>(), BlockPredicate.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser( + new ContextualArgumentTypeProvider<>(net.minecraft.commands.arguments.blocks.BlockPredicateArgument::blockPredicate) + ).flatMapSuccess((ctx, result) -> ArgumentParseResult.successFuture(new BlockPredicateImpl(result))); + + @Override + public @NonNull CompletableFuture<@NonNull ArgumentParseResult> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + return this.mappedParser.parseFuture(commandContext, commandInput); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.BLOCK_PREDICATE.get().createNode(); + } + + private record BlockPredicateImpl(Predicate predicate) implements BlockPredicate { + + private BlockPredicateImpl(final @NonNull Predicate predicate) { + this.predicate = predicate; + } + + private boolean testImpl(final @NonNull ServerLocation location, final boolean loadChunks) { + return this.predicate.test(new BlockInWorld( + (ServerLevel) location.world(), + VecHelper.toBlockPos(location.position()), + loadChunks + )); + } + + @Override + public boolean test(final @NonNull ServerLocation location) { + return this.testImpl(location, false); + } + + @Override + public @NonNull BlockPredicate loadChunks() { + return new BlockPredicate() { + @Override + public @NonNull BlockPredicate loadChunks() { + return this; + } + + @Override + public boolean test(final @NonNull ServerLocation location) { + return BlockPredicateImpl.this.testImpl(location, true); + } + }; + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ComponentParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ComponentParser.java new file mode 100644 index 00000000..4ccdde92 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ComponentParser.java @@ -0,0 +1,88 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.text.Component; +import net.minecraft.commands.arguments.ComponentArgument; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.minecraft.modded.internal.ContextualArgumentTypeProvider; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.common.adventure.SpongeAdventure; + +/** + * An argument for parsing {@link Component Components} from json formatted text. + * + * @param command sender type + */ +public final class ComponentParser implements ArgumentParser.FutureArgumentParser, NodeSource, SuggestionProvider { + + /** + * Creates a new {@link ComponentParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor componentParser() { + return ParserDescriptor.of(new ComponentParser<>(), Component.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser( + new ContextualArgumentTypeProvider<>(ComponentArgument::textComponent) + ).flatMapSuccess((ctx, component) -> + ArgumentParseResult.successFuture(SpongeAdventure.asAdventure(component))); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.COMPONENT.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/DataContainerParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/DataContainerParser.java new file mode 100644 index 00000000..8b29fa1b --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/DataContainerParser.java @@ -0,0 +1,89 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.arguments.CompoundTagArgument; +import net.minecraft.nbt.CompoundTag; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.data.persistence.DataContainer; +import org.spongepowered.common.data.persistence.NBTTranslator; + +/** + * Argument for parsing {@link DataContainer DataContainers} from + *
SNBT strings. + * + * @param command sender type + */ +public final class DataContainerParser implements ArgumentParser.FutureArgumentParser, + NodeSource, SuggestionProvider { + + /** + * Creates a new {@link DataContainerParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor dataContainerParser() { + return ParserDescriptor.of(new DataContainerParser<>(), DataContainer.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser(CompoundTagArgument.compoundTag()) + .flatMapSuccess((ctx, compoundTag) -> + ArgumentParseResult.successFuture(NBTTranslator.INSTANCE.translate(compoundTag))); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.NBT_COMPOUND_TAG.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/GameProfileCollectionParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/GameProfileCollectionParser.java new file mode 100644 index 00000000..44e4d646 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/GameProfileCollectionParser.java @@ -0,0 +1,140 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.GameProfileArgument; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.sponge.data.GameProfileCollection; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.command.selector.Selector; +import org.spongepowered.api.profile.GameProfile; +import org.spongepowered.common.profile.SpongeGameProfile; + +/** + * Argument for parsing a {@link Collection} of {@link GameProfile GameProfiles} from a + * {@link Selector}. A successfully parsed result will contain at least one element. + * + * @param command sender type + */ +public final class GameProfileCollectionParser implements NodeSource, + ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link GameProfileCollectionParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor gameProfileCollectionParser() { + return ParserDescriptor.of(new GameProfileCollectionParser<>(), GameProfileCollection.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser( + net.minecraft.commands.arguments.GameProfileArgument.gameProfile() + ).flatMapSuccess((ctx, argumentResult) -> { + final Collection profiles; + try { + profiles = argumentResult.getNames( + (CommandSourceStack) ctx.get(SpongeCommandContextKeys.COMMAND_CAUSE) + ); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failureFuture(ex); + } + final List result = profiles.stream() + .map(SpongeGameProfile::of).collect(Collectors.toList()); + return ArgumentParseResult.successFuture(new GameProfileCollectionImpl(Collections.unmodifiableCollection(result))); + }); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.GAME_PROFILE.get().createNode(); + } + + + @DefaultQualifier(NonNull.class) + private static final class GameProfileCollectionImpl extends AbstractCollection + implements GameProfileCollection { + + private final Collection backing; + + private GameProfileCollectionImpl(final Collection backing) { + this.backing = backing; + } + + @Override + public int size() { + return this.backing.size(); + } + + @Override + public Iterator iterator() { + return this.backing.iterator(); + } + + @Override + public boolean add(final GameProfile gameProfile) { + return this.backing.add(gameProfile); + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/GameProfileParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/GameProfileParser.java new file mode 100644 index 00000000..becfea35 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/GameProfileParser.java @@ -0,0 +1,124 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.CommandSourceStack; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCaptionKeys; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.command.selector.Selector; +import org.spongepowered.api.profile.GameProfile; +import org.spongepowered.common.profile.SpongeGameProfile; + +/** + * Argument for parsing a single {@link GameProfile} from a {@link Selector}. + * + * @param command sender type + */ +public final class GameProfileParser implements ArgumentParser.FutureArgumentParser, NodeSource, SuggestionProvider { + + /** + * Creates a new {@link GameProfileParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor gameProfileParser() { + return ParserDescriptor.of(new GameProfileParser<>(), GameProfile.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser( + net.minecraft.commands.arguments.GameProfileArgument.gameProfile() + ).flatMapSuccess((ctx, argumentResult) -> { + final Collection profiles; + try { + profiles = argumentResult.getNames( + (CommandSourceStack) ctx.get(SpongeCommandContextKeys.COMMAND_CAUSE) + ); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failureFuture(ex); + } + if (profiles.size() > 1) { + return ArgumentParseResult.failureFuture(new TooManyGameProfilesSelectedException(ctx)); + } + final GameProfile profile = SpongeGameProfile.of(profiles.iterator().next()); + return ArgumentParseResult.successFuture(profile); + }); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.GAME_PROFILE.get().createNode(); + } + + + /** + * Exception thrown when too many game profiles are selected. + */ + private static final class TooManyGameProfilesSelectedException extends ParserException { + + private static final long serialVersionUID = -2931411139985042222L; + + TooManyGameProfilesSelectedException(final @NonNull CommandContext context) { + super( + GameProfileParser.class, + context, + SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_GAME_PROFILE_TOO_MANY_SELECTED + ); + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ItemStackPredicateParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ItemStackPredicateParser.java new file mode 100644 index 00000000..2b6df2d1 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ItemStackPredicateParser.java @@ -0,0 +1,103 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import net.minecraft.commands.arguments.item.ItemPredicateArgument; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.minecraft.modded.internal.ContextualArgumentTypeProvider; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.sponge.data.ItemStackPredicate; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.item.inventory.ItemStack; + +/** + * An argument for parsing {@link ItemStackPredicate ItemStackPredicates}. + * + * @param command sender type + */ +public final class ItemStackPredicateParser implements ArgumentParser.FutureArgumentParser, + NodeSource, SuggestionProvider { + + /** + * Creates a new {@link ItemStackPredicateParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor itemStackPredicateParser() { + return ParserDescriptor.of(new ItemStackPredicateParser<>(), ItemStackPredicate.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser( + new ContextualArgumentTypeProvider<>(ItemPredicateArgument::itemPredicate) + ).flatMapSuccess((ctx, result) -> ArgumentParseResult.successFuture(new ItemStackPredicateImpl(result))); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.ITEM_PREDICATE.get().createNode(); + } + + private record ItemStackPredicateImpl(Predicate predicate) implements ItemStackPredicate { + + private ItemStackPredicateImpl(final @NonNull Predicate predicate) { + this.predicate = predicate; + } + + @SuppressWarnings("ConstantConditions") + @Override + public boolean test(final @NonNull ItemStack itemStack) { + return this.predicate.test((net.minecraft.world.item.ItemStack) (Object) itemStack); + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/MultipleEntitySelectorParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/MultipleEntitySelectorParser.java new file mode 100644 index 00000000..2d4cf653 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/MultipleEntitySelectorParser.java @@ -0,0 +1,141 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.selector.EntitySelector; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.sponge.data.MultipleEntitySelector; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.command.selector.Selector; +import org.spongepowered.api.entity.Entity; + +/** + * Argument for selecting one or more {@link Entity Entities} using a {@link Selector}. + * + * @param command sender type + */ +public final class MultipleEntitySelectorParser implements NodeSource, + ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link MultipleEntitySelectorParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor multipleEntitySelectorParser() { + return ParserDescriptor.of(new MultipleEntitySelectorParser<>(), MultipleEntitySelector.class); + } + + + private final ArgumentParser nativeParser = new WrappedBrigadierParser<>(EntityArgument.entities()); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final CommandInput originalInput = inputQueue.copy(); + return this.nativeParser.parseFuture(commandContext, inputQueue).thenApply(result -> { + if (result.failure().isPresent()) { + return ArgumentParseResult.failure(result.failure().get()); + } + final String consumedInput = originalInput.difference(inputQueue); + final EntitySelector parsed = result.parsedValue().get(); + final List entities; + try { + entities = parsed.findEntities( + ((CommandSourceStack) commandContext.get(SpongeCommandContextKeys.COMMAND_CAUSE)).withPermission(2) + ).stream().map(e -> (Entity) e).collect(Collectors.toList()); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failure(ex); + } + return ArgumentParseResult.success(new MultipleEntitySelectorImpl((Selector) parsed, consumedInput, entities)); + }); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.nativeParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.ENTITY.get().createNode(); + } + + private static final class MultipleEntitySelectorImpl implements MultipleEntitySelector { + + private final Selector selector; + private final String inputString; + private final Collection result; + + private MultipleEntitySelectorImpl( + final Selector selector, + final String inputString, + final Collection result + ) { + this.selector = selector; + this.inputString = inputString; + this.result = result; + } + + @Override + public @NonNull Selector selector() { + return this.selector; + } + + @Override + public @NonNull String inputString() { + return this.inputString; + } + + @Override + public @NonNull Collection get() { + return this.result; + } + + } +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/MultiplePlayerSelectorParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/MultiplePlayerSelectorParser.java new file mode 100644 index 00000000..2105705f --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/MultiplePlayerSelectorParser.java @@ -0,0 +1,142 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.selector.EntitySelector; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.sponge.data.MultiplePlayerSelector; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.command.selector.Selector; +import org.spongepowered.api.entity.living.player.Player; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +/** + * Argument for selecting one or more {@link Player Players} using a {@link Selector}. + * + * @param command sender type + */ +public final class MultiplePlayerSelectorParser implements NodeSource, + ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link MultiplePlayerSelectorParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor multiplePlayerSelectorParser() { + return ParserDescriptor.of(new MultiplePlayerSelectorParser<>(), MultiplePlayerSelector.class); + } + + private final ArgumentParser nativeParser = new WrappedBrigadierParser<>(EntityArgument.players()); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final CommandInput originalInput = inputQueue.copy(); + return this.nativeParser.parseFuture(commandContext, inputQueue).thenApply(result -> { + if (result.failure().isPresent()) { + return ArgumentParseResult.failure(result.failure().get()); + } + final String consumedInput = originalInput.difference(inputQueue); + final EntitySelector parsed = result.parsedValue().get(); + final List players; + try { + players = parsed.findPlayers( + ((CommandSourceStack) commandContext.get(SpongeCommandContextKeys.COMMAND_CAUSE)).withPermission(2) + ).stream().map(p -> (ServerPlayer) p).collect(Collectors.toList()); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failure(ex); + } + return ArgumentParseResult.success(new MultiplePlayerSelectorImpl((Selector) parsed, consumedInput, players)); + }); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.nativeParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.ENTITY.get().createNode().playersOnly(); + } + + private static final class MultiplePlayerSelectorImpl implements MultiplePlayerSelector { + + private final Selector selector; + private final String inputString; + private final Collection result; + + private MultiplePlayerSelectorImpl( + final Selector selector, + final String inputString, + final Collection result + ) { + this.selector = selector; + this.inputString = inputString; + this.result = result; + } + + @Override + public @NonNull Selector selector() { + return this.selector; + } + + @Override + public @NonNull String inputString() { + return this.inputString; + } + + @Override + public @NonNull Collection get() { + return this.result; + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/NamedTextColorParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/NamedTextColorParser.java new file mode 100644 index 00000000..2d8646ee --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/NamedTextColorParser.java @@ -0,0 +1,84 @@ +// +// 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.sponge.parser; + +import java.util.ArrayList; +import java.util.Locale; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.commands.arguments.ColorArgument; +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.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; + +/** + * An argument for parsing {@link NamedTextColor NamedTextColors}. + * + * @param command sender type + */ +public final class NamedTextColorParser implements NodeSource, ArgumentParser, BlockingSuggestionProvider.Strings { + + /** + * Creates a new {@link NamedTextColorParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor namedTextColorParser() { + return ParserDescriptor.of(new NamedTextColorParser<>(), NamedTextColor.class); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull NamedTextColor> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final String input = inputQueue.readString().toLowerCase(Locale.ROOT); + final NamedTextColor color = NamedTextColor.NAMES.value(input); + if (color != null) { + return ArgumentParseResult.success(color); + } + return ArgumentParseResult.failure(ColorArgument.ERROR_INVALID_VALUE.create(input)); + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return new ArrayList<>(NamedTextColor.NAMES.keys()); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.COLOR.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/OperatorParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/OperatorParser.java new file mode 100644 index 00000000..dca603d3 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/OperatorParser.java @@ -0,0 +1,105 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.stream.Collectors; +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.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.spongepowered.api.command.parameter.managed.operator.Operator; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.registry.RegistryTypes; + +/** + * An argument for parsing {@link Operator Operators}. + * + * @param command sender type + */ +public final class OperatorParser implements NodeSource, ArgumentParser, BlockingSuggestionProvider.Strings { + + /** + * Creates a new {@link OperatorParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor operatorParser() { + return ParserDescriptor.of(new OperatorParser<>(), Operator.class); + } + + private static final SimpleCommandExceptionType ERROR_INVALID_OPERATION; + + static { + try { + // todo: fix in a better way + final Class spongeAccessor = + Class.forName("org.spongepowered.common.accessor.commands.arguments.OperationArgumentAccessor"); + final Method get = spongeAccessor.getDeclaredMethod("accessor$ERROR_INVALID_OPERATION"); + get.setAccessible(true); + ERROR_INVALID_OPERATION = (SimpleCommandExceptionType) get.invoke(null); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException("Couldn't access ERROR_INVALID_OPERATION command exception type.", ex); + } + } + + @Override + public @NonNull ArgumentParseResult<@NonNull Operator> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final String input = inputQueue.readString(); + final Optional operator = RegistryTypes.OPERATOR.get().stream() + .filter(op -> op.asString().equals(input)) + .findFirst(); + if (!operator.isPresent()) { + return ArgumentParseResult.failure(ERROR_INVALID_OPERATION.create()); + } + return ArgumentParseResult.success(operator.get()); + } + + @Override + public @NonNull Iterable<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return RegistryTypes.OPERATOR.get().stream() + .map(Operator::asString) + .collect(Collectors.toList()); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.OPERATION.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ProtoItemStackParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ProtoItemStackParser.java new file mode 100644 index 00000000..d69b572b --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ProtoItemStackParser.java @@ -0,0 +1,166 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.util.ComponentMessageThrowable; +import net.minecraft.commands.arguments.item.ItemArgument; +import net.minecraft.commands.arguments.item.ItemInput; +import net.minecraft.nbt.CompoundTag; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.minecraft.modded.internal.ContextualArgumentTypeProvider; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.sponge.data.ProtoItemStack; +import org.incendo.cloud.sponge.exception.ComponentMessageRuntimeException; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.data.persistence.DataContainer; +import org.spongepowered.api.item.ItemType; +import org.spongepowered.api.item.inventory.ItemStack; +import org.spongepowered.api.item.inventory.ItemStackSnapshot; +import org.spongepowered.common.data.persistence.NBTTranslator; + +/** + * An argument for parsing {@link ProtoItemStack ProtoItemStacks} from an {@link ItemType} identifier + * and optional NBT data. The stack size of the resulting snapshot will always be {@code 1}. + * + *

Example input strings:

+ *
    + *
  • {@code apple}
  • + *
  • {@code minecraft:apple}
  • + *
  • {@code diamond_sword{Enchantments:[{id:sharpness,lvl:5}]}}
  • + *
+ * + * @param command sender type + */ +public final class ProtoItemStackParser implements NodeSource, + ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link ProtoItemStackParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor protoItemStackParser() { + return ParserDescriptor.of(new ProtoItemStackParser<>(), ProtoItemStack.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser(new ContextualArgumentTypeProvider<>(ItemArgument::item)) + .flatMapSuccess((ctx, itemInput) -> ArgumentParseResult.successFuture(new ProtoItemStackImpl(itemInput))); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.ITEM_STACK.get().createNode(); + } + + private static final class ProtoItemStackImpl implements ProtoItemStack { + + // todo: use accessor + private static final Field COMPOUND_TAG_FIELD = + Arrays.stream(ItemInput.class.getDeclaredFields()) + .filter(f -> f.getType().equals(CompoundTag.class)) + .findFirst() + .orElseThrow(IllegalStateException::new); + + static { + COMPOUND_TAG_FIELD.setAccessible(true); + } + + private final ItemInput itemInput; + private final @Nullable DataContainer extraData; + + ProtoItemStackImpl(final @NonNull ItemInput itemInput) { + this.itemInput = itemInput; + try { + final CompoundTag tag = (CompoundTag) COMPOUND_TAG_FIELD.get(itemInput); + this.extraData = tag == null ? null : NBTTranslator.INSTANCE.translate(tag); + } catch (final IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public @NonNull ItemType itemType() { + return (ItemType) this.itemInput.getItem(); + } + + @Override + public @Nullable DataContainer extraData() { + return this.extraData; + } + + @SuppressWarnings("ConstantConditions") + @Override + public @NonNull ItemStack createItemStack( + final int stackSize, + final boolean respectMaximumStackSize + ) throws ComponentMessageRuntimeException { + try { + return (ItemStack) (Object) this.itemInput.createItemStack(stackSize, respectMaximumStackSize); + } catch (final CommandSyntaxException ex) { + throw new ComponentMessageRuntimeException(ComponentMessageThrowable.getMessage(ex), ex); + } + } + + @Override + public @NonNull ItemStackSnapshot createItemStackSnapshot( + final int stackSize, + final boolean respectMaximumStackSize + ) throws ComponentMessageRuntimeException { + return this.createItemStack(stackSize, respectMaximumStackSize).createSnapshot(); + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/RegistryEntryParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/RegistryEntryParser.java new file mode 100644 index 00000000..3eb80252 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/RegistryEntryParser.java @@ -0,0 +1,260 @@ +// +// 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.sponge.parser; + +import io.leangen.geantyref.TypeToken; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.caption.CaptionVariable; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCaptionKeys; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.command.registrar.tree.CommandCompletionProviders; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.registry.DefaultedRegistryType; +import org.spongepowered.api.registry.Registry; +import org.spongepowered.api.registry.RegistryEntry; +import org.spongepowered.api.registry.RegistryHolder; +import org.spongepowered.api.registry.RegistryType; +import org.spongepowered.api.registry.RegistryTypes; + +/** + * An argument for retrieving values from any of Sponge's {@link Registry Registries}. + * + * @param command sender type + * @param value type + */ +public final class RegistryEntryParser implements NodeSource, + ArgumentParser.FutureArgumentParser, BlockingSuggestionProvider.Strings { + + // Start DefaultedRegistryType methods + + /** + * Create a new {@link RegistryEntryParser} for a {@link DefaultedRegistryType}. + * + * @param command sender type + * @param value type + * @param valueType value type + * @param registryType registry type + * @return a new {@link RegistryEntryParser} + */ + public static @NonNull ParserDescriptor registryEntryParser( + final @NonNull TypeToken valueType, + final @NonNull DefaultedRegistryType registryType + ) { + return ParserDescriptor.of(new RegistryEntryParser<>(registryType), valueType); + } + + /** + * Create a new {@link RegistryEntryParser} for a {@link DefaultedRegistryType}. + * + * @param command sender type + * @param value type + * @param valueType value type + * @param registryType registry type + * @return a new {@link RegistryEntryParser} + */ + public static @NonNull ParserDescriptor registryEntryParser( + final @NonNull Class valueType, + final @NonNull DefaultedRegistryType registryType + ) { + return ParserDescriptor.of(new RegistryEntryParser<>(registryType), valueType); + } + + // End DefaultedRegistryType methods + + // Start RegistryType methods + + /** + * Create a new {@link RegistryEntryParser} for a {@link RegistryType} + * using the specified {@link RegistryHolder} function. + * + *

For {@link RegistryType RegistryTypes} which are {@link DefaultedRegistryType DefaultedRegistryTypes}, + * it is suggested to instead use {@link #registryEntryParser(TypeToken, DefaultedRegistryType)}.

+ * + * @param command sender type + * @param value type + * @param valueType value type + * @param registryType registry type + * @param holderSupplier registry holder function + * @return a new {@link RegistryEntryParser} + */ + public static @NonNull ParserDescriptor registryEntryParser( + final @NonNull Class valueType, + final @NonNull RegistryType registryType, + final @NonNull Function, RegistryHolder> holderSupplier + ) { + return ParserDescriptor.of(new RegistryEntryParser<>(holderSupplier, registryType), valueType); + } + + /** + * Create a new {@link RegistryEntryParser} for a {@link RegistryType} + * using the specified {@link RegistryHolder} function. + * + *

For {@link RegistryType RegistryTypes} which are {@link DefaultedRegistryType DefaultedRegistryTypes}, + * it is suggested to instead use {@link #registryEntryParser(TypeToken, DefaultedRegistryType)}.

+ * + * @param command sender type + * @param value type + * @param valueType value type + * @param registryType registry type + * @param holderSupplier registry holder function + * @return a new {@link RegistryEntryParser} + */ + public static @NonNull ParserDescriptor registryEntryParser( + final @NonNull TypeToken valueType, + final @NonNull RegistryType registryType, + final @NonNull Function, RegistryHolder> holderSupplier + ) { + return ParserDescriptor.of(new RegistryEntryParser<>(holderSupplier, registryType), valueType); + } + + // End RegistryType methods + + private static final ArgumentParser RESOURCE_KEY_PARSER = new ResourceKeyParser<>(); + + private final Function, RegistryHolder> holderSupplier; + private final RegistryType registryType; + + /** + * Create a new {@link RegistryEntryParser} using the specified {@link RegistryHolder} function. + * + *

For {@link RegistryType RegistryTypes} which are {@link DefaultedRegistryType DefaultedRegistryTypes}, + * it is suggested to instead use {@link #RegistryEntryParser(DefaultedRegistryType)}.

+ * + * @param holderSupplier registry holder function + * @param registryType registry type + */ + public RegistryEntryParser( + final @NonNull Function, RegistryHolder> holderSupplier, + final @NonNull RegistryType registryType + ) { + this.holderSupplier = holderSupplier; + this.registryType = registryType; + } + + /** + * Create a new {@link RegistryEntryParser}. + * + * @param registryType defaulted registry type + */ + public RegistryEntryParser(final @NonNull DefaultedRegistryType registryType) { + this(ctx -> registryType.defaultHolder().get(), registryType); + } + + private Registry registry(final @NonNull CommandContext commandContext) { + return this.holderSupplier.apply(commandContext).registry(this.registryType); + } + + @SuppressWarnings("unchecked") + @Override + public @NonNull CompletableFuture<@NonNull ArgumentParseResult<@NonNull V>> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return ((ArgumentParser) RESOURCE_KEY_PARSER).parseFuture(commandContext, inputQueue).thenApply(keyResult -> { + if (keyResult.failure().isPresent()) { + return ArgumentParseResult.failure(keyResult.failure().get()); + } + final Optional> entry = this.registry(commandContext).findEntry(keyResult.parsedValue().get()); + if (entry.isPresent()) { + return ArgumentParseResult.success(entry.get().value()); + } + return ArgumentParseResult.failure(new NoSuchEntryException(commandContext, keyResult.parsedValue().get(), this.registryType)); + }); + } + + @Override + public @NonNull List<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return this.registry(commandContext).streamEntries().flatMap(entry -> { + if (!input.isEmpty() && entry.key().namespace().equals(ResourceKey.MINECRAFT_NAMESPACE)) { + return Stream.of(entry.key().value(), entry.key().asString()); + } + return Stream.of(entry.key().asString()); + }).collect(Collectors.toList()); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + if (this.registryType.equals(RegistryTypes.SOUND_TYPE)) { + return CommandTreeNodeTypes.RESOURCE_LOCATION.get().createNode() + .completions(CommandCompletionProviders.AVAILABLE_SOUNDS); + //} else if (this.registryType.equals(RegistryTypes.BIOME)) { + // return CommandTreeNodeTypes.RESOURCE_LOCATION.get().createNode() + // .completions(CommandCompletionProviders.AVAILABLE_BIOMES); + } else if (this.registryType.equals(RegistryTypes.ENTITY_TYPE)) { + // return CommandTreeNodeTypes.ENTITY_SUMMON.get().createNode() + return CommandTreeNodeTypes.RESOURCE_LOCATION.get().createNode() + .completions(CommandCompletionProviders.SUMMONABLE_ENTITIES); + //} else if (this.registryType.equals(RegistryTypes.ENCHANTMENT_TYPE)) { + // return CommandTreeNodeTypes.ITEM_ENCHANTMENT.get().createNode(); + //} else if (this.registryType.equals(RegistryTypes.POTION_EFFECT_TYPE)) { + // return CommandTreeNodeTypes.MOB_EFFECT.get().createNode(); + } else if (this.registryType.equals(RegistryTypes.WORLD_TYPE)) { + return CommandTreeNodeTypes.DIMENSION.get().createNode() + .customCompletions(); // Sponge adds custom types (?) + } + return CommandTreeNodeTypes.RESOURCE_LOCATION.get().createNode().customCompletions(); + } + + /** + * An exception thrown when there is no entry for the provided {@link ResourceKey} in the resolved registry. + */ + private static final class NoSuchEntryException extends ParserException { + + private static final long serialVersionUID = 4472876671109079272L; + + NoSuchEntryException( + final CommandContext context, + final ResourceKey key, + final RegistryType registryType + ) { + super( + RegistryEntryParser.class, + context, + SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY, + CaptionVariable.of("id", key.asString()), + CaptionVariable.of("registry", registryType.location().asString()) + ); + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ResourceKeyParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ResourceKeyParser.java new file mode 100644 index 00000000..7723d478 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ResourceKeyParser.java @@ -0,0 +1,72 @@ +// +// 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.sponge.parser; + +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.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; + +/** + * Argument for parsing {@link ResourceKey ResourceKeys}. + * + * @param command sender type + */ +public final class ResourceKeyParser implements NodeSource, ArgumentParser { + + /** + * Creates a new {@link ResourceKeyParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor resourceKeyParser() { + return ParserDescriptor.of(new ResourceKeyParser<>(), ResourceKey.class); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull ResourceKey> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final String input = inputQueue.readString(); + final ResourceKey key = ResourceKeyUtil.resourceKey(input); + if (key == null) { + return ResourceKeyUtil.invalidResourceKey(); + } + return ArgumentParseResult.success(key); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.RESOURCE_LOCATION.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ResourceKeyUtil.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ResourceKeyUtil.java new file mode 100644 index 00000000..bdff1a9e --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/ResourceKeyUtil.java @@ -0,0 +1,71 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import java.lang.reflect.Field; +import java.util.Arrays; +import net.minecraft.resources.ResourceLocation; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.spongepowered.api.ResourceKey; + +/** + * Shared utilities for ResourceKey based arguments. Not API. + */ +final class ResourceKeyUtil { + + private ResourceKeyUtil() { + } + + private static final SimpleCommandExceptionType ERROR_INVALID_RESOURCE_LOCATION; + + static { + try { + // ERROR_INVALID (todo: use accessor) + final Field errorInvalidResourceLocationField = Arrays.stream(ResourceLocation.class.getDeclaredFields()) + .filter(it -> it.getType().equals(SimpleCommandExceptionType.class)) + .findFirst() + .orElseThrow(IllegalStateException::new); + errorInvalidResourceLocationField.setAccessible(true); + ERROR_INVALID_RESOURCE_LOCATION = (SimpleCommandExceptionType) errorInvalidResourceLocationField.get(null); + } catch (final Exception ex) { + throw new RuntimeException("Couldn't access ERROR_INVALID command exception type.", ex); + } + } + + static ArgumentParseResult invalidResourceKey() { + return ArgumentParseResult.failure(ERROR_INVALID_RESOURCE_LOCATION.create()); + } + + static @Nullable ResourceKey resourceKey(final @NonNull String input) { + try { + return ResourceKey.resolve(input); + } catch (final IllegalStateException ex) { + return null; + } + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/SingleEntitySelectorParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/SingleEntitySelectorParser.java new file mode 100644 index 00000000..4ed76d7a --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/SingleEntitySelectorParser.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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.selector.EntitySelector; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.sponge.data.SingleEntitySelector; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.command.selector.Selector; +import org.spongepowered.api.entity.Entity; + +/** + * Argument for selecting a single {@link Entity} using a {@link Selector}. + * + * @param command sender type + */ +public final class SingleEntitySelectorParser implements NodeSource, + ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link SingleEntitySelectorParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor singleEntitySelectorParser() { + return ParserDescriptor.of(new SingleEntitySelectorParser<>(), SingleEntitySelector.class); + } + + private final ArgumentParser nativeParser = new WrappedBrigadierParser<>(EntityArgument.entity()); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final CommandInput originalInput = inputQueue.copy(); + return this.nativeParser.parseFuture(commandContext, inputQueue).thenApply(result -> { + if (result.failure().isPresent()) { + return ArgumentParseResult.failure(result.failure().get()); + } + final String consumedInput = originalInput.difference(inputQueue); + final EntitySelector parsed = result.parsedValue().get(); + final Entity entity; + try { + entity = (Entity) parsed.findSingleEntity( + ((CommandSourceStack) commandContext.get(SpongeCommandContextKeys.COMMAND_CAUSE)).withPermission(2) + ); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failure(ex); + } + return ArgumentParseResult.success(new SingleEntitySelectorImpl((Selector) parsed, consumedInput, entity)); + }); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.nativeParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.ENTITY.get().createNode().single(); + } + + private static final class SingleEntitySelectorImpl implements SingleEntitySelector { + + private final Selector selector; + private final String inputString; + private final Entity result; + + private SingleEntitySelectorImpl( + final Selector selector, + final String inputString, + final Entity result + ) { + this.selector = selector; + this.inputString = inputString; + this.result = result; + } + + @Override + public @NonNull Selector selector() { + return this.selector; + } + + @Override + public @NonNull String inputString() { + return this.inputString; + } + + @Override + public @NonNull Entity getSingle() { + return this.result; + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/SinglePlayerSelectorParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/SinglePlayerSelectorParser.java new file mode 100644 index 00000000..8ca517b1 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/SinglePlayerSelectorParser.java @@ -0,0 +1,141 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.selector.EntitySelector; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.sponge.data.SinglePlayerSelector; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.command.selector.Selector; +import org.spongepowered.api.entity.living.player.Player; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +/** + * Argument for selecting a single {@link Player} using a {@link Selector}. + * + * @param command sender type + */ +public final class SinglePlayerSelectorParser implements NodeSource, + ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link SinglePlayerSelectorParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor singlePlayerSelectorParser() { + return ParserDescriptor.of(new SinglePlayerSelectorParser<>(), SinglePlayerSelector.class); + } + + private final ArgumentParser nativeParser = new WrappedBrigadierParser<>(EntityArgument.player()); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final CommandInput originalInput = inputQueue.copy(); + return this.nativeParser.parseFuture(commandContext, inputQueue).thenApply(result -> { + if (result.failure().isPresent()) { + return ArgumentParseResult.failure(result.failure().get()); + } + final String consumedInput = originalInput.difference(inputQueue); + final EntitySelector parsed = result.parsedValue().get(); + final ServerPlayer player; + try { + // todo: a more proper fix then setting permission level 2 + player = (ServerPlayer) parsed.findSinglePlayer( + ((CommandSourceStack) commandContext.get(SpongeCommandContextKeys.COMMAND_CAUSE)).withPermission(2) + ); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failure(ex); + } + return ArgumentParseResult.success(new SinglePlayerSelectorImpl((Selector) parsed, consumedInput, player)); + }); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.nativeParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.ENTITY.get().createNode().playersOnly().single(); + } + + + private static final class SinglePlayerSelectorImpl implements SinglePlayerSelector { + + private final Selector selector; + private final String inputString; + private final ServerPlayer result; + + private SinglePlayerSelectorImpl( + final Selector selector, + final String inputString, + final ServerPlayer result + ) { + this.selector = selector; + this.inputString = inputString; + this.result = result; + } + + @Override + public @NonNull Selector selector() { + return this.selector; + } + + @Override + public @NonNull String inputString() { + return this.inputString; + } + + @Override + public @NonNull ServerPlayer getSingle() { + return this.result; + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/UserParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/UserParser.java new file mode 100644 index 00000000..6e480840 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/UserParser.java @@ -0,0 +1,213 @@ +// +// 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.sponge.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.selector.EntitySelector; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +import org.incendo.cloud.caption.Caption; +import org.incendo.cloud.caption.CaptionVariable; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCaptionKeys; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.command.selector.Selector; +import org.spongepowered.api.entity.living.player.User; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.profile.GameProfile; +import org.spongepowered.api.user.UserManager; + +/** + * Argument for parsing {@link User} {@link UUID UUIDs} in the {@link UserManager} from + * a {@link Selector}, last known username, or {@link UUID} string. + * + * @param command sender type + */ +public final class UserParser implements NodeSource, ArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link UserParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor userParser() { + return ParserDescriptor.of(new UserParser<>(), UUID.class); + } + + private final ArgumentParser singlePlayerSelectorParser = + new WrappedBrigadierParser<>(EntityArgument.player()); + + @Override + public @NonNull ArgumentParseResult<@NonNull UUID> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final CommandInput inputCopy = inputQueue.copy(); + final String peek = inputQueue.readString(); + if (peek.startsWith("@")) { + return this.handleSelector(commandContext, inputCopy); + } + + try { + final Optional optionalUser = Sponge.server().gameProfileManager().cache().findByName(peek); + // valid username + if (optionalUser.isPresent()) { + return ArgumentParseResult.success(optionalUser.get().uniqueId()); + } + return ArgumentParseResult.failure(new UserNotFoundException( + commandContext, UserNotFoundException.Type.NAME, peek + )); + } catch (final IllegalArgumentException ex) { + // not a valid username + } + + try { + final UUID uuid = UUID.fromString(peek); + // valid uuid + if (Sponge.server().userManager().exists(uuid)) { + return ArgumentParseResult.success(uuid); + } + + return ArgumentParseResult.failure(new UserNotFoundException( + commandContext, UserNotFoundException.Type.UUID, peek + )); + } catch (final IllegalArgumentException ex) { + // not a valid uuid + } + + return ArgumentParseResult.failure(new UserNotFoundException( + commandContext, UserNotFoundException.Type.INVALID_INPUT, peek + )); + } + + private @NonNull ArgumentParseResult<@NonNull UUID> handleSelector( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final ArgumentParseResult result = this.singlePlayerSelectorParser.parse(commandContext, inputQueue); + if (result.failure().isPresent()) { + return ArgumentParseResult.failure(result.failure().get()); + } + final EntitySelector parsed = result.parsedValue().get(); + final ServerPlayer player; + try { + player = (ServerPlayer) parsed.findSinglePlayer( + (CommandSourceStack) commandContext.get(SpongeCommandContextKeys.COMMAND_CAUSE) + ); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failure(ex); + } + return ArgumentParseResult.success(player.uniqueId()); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return this.singlePlayerSelectorParser.suggestionProvider().suggestionsFuture(commandContext, input) + .thenApply(it -> { + final List suggestions = new ArrayList<>(); + it.forEach(suggestions::add); + final String peek = input.peekString(); + if (!peek.startsWith("@")) { + suggestions.addAll(Sponge.server().userManager().streamOfMatches(peek) + .filter(GameProfile::hasName) + .map(profile -> profile.name().orElse(null)) + .filter(Objects::nonNull) + .filter(name -> suggestions.stream().noneMatch(s -> s.suggestion().equals(name))) + .map(Suggestion::suggestion) + .collect(Collectors.toList())); + } + return suggestions; + }); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.GAME_PROFILE.get().createNode().customCompletions(); + } + + /** + * An exception thrown when a {@link User} cannot be found for the provided input. + */ + private static final class UserNotFoundException extends ParserException { + + private static final long serialVersionUID = -24501459406523175L; + + UserNotFoundException( + final CommandContext context, + final @NonNull Type type, + final @NonNull String input + ) { + super( + UserParser.class, + context, + type.caption, + type.variable(input) + ); + } + + private enum Type { + UUID("uuid", SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_UUID), + NAME("name", SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_USER_CANNOT_FIND_USER_WITH_NAME), + INVALID_INPUT("input", SpongeCaptionKeys.ARGUMENT_PARSE_FAILURE_USER_INVALID_INPUT); + + private final String key; + private final Caption caption; + + Type(final @NonNull String key, final @NonNull Caption caption) { + this.key = key; + this.caption = caption; + } + + CaptionVariable variable(final @NonNull String input) { + return CaptionVariable.of(this.key, input); + } + } + + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector2dParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector2dParser.java new file mode 100644 index 00000000..95619dbc --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector2dParser.java @@ -0,0 +1,117 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.coordinates.Coordinates; +import net.minecraft.commands.arguments.coordinates.Vec2Argument; +import net.minecraft.world.phys.Vec3; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.suggestion.Suggestion; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.math.vector.Vector2d; + +/** + * Argument for parsing {@link Vector2d} from relative, absolute, or local coordinates. + * + *

Example input strings:

+ *
    + *
  • {@code ~ ~}
  • + *
  • {@code 0.1 -0.5}
  • + *
  • {@code ~1 ~-2}
  • + *
+ * + * @param command sender type + */ +public final class Vector2dParser extends VectorParser { + + /** + * Creates a new {@link Vector2dParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor vector2dParser() { + return vector2dParser(false); + } + + /** + * Creates a new {@link Vector2dParser}. + * + * @param command sender type + * @param centerIntegers whether to center integers to x.5 + * @return new parser + */ + public static ParserDescriptor vector2dParser(final boolean centerIntegers) { + return ParserDescriptor.of(new Vector2dParser<>(centerIntegers), Vector2d.class); + } + + private final ArgumentParser mappedParser; + + /** + * Create a new {@link Vector2dParser}. + * + * @param centerIntegers whether to center integers to x.5 + */ + public Vector2dParser(final boolean centerIntegers) { + super(centerIntegers); + this.mappedParser = new WrappedBrigadierParser(new Vec2Argument(centerIntegers)) + .flatMapSuccess((ctx, coordinates) -> { + final Vec3 position = coordinates.getPosition( + (CommandSourceStack) ctx.get(SpongeCommandContextKeys.COMMAND_CAUSE) + ); + return ArgumentParseResult.successFuture(new Vector2d(position.x, position.z)); + }); + } + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.VEC2.get().createNode(); + } +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector2iParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector2iParser.java new file mode 100644 index 00000000..89224a12 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector2iParser.java @@ -0,0 +1,102 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.coordinates.ColumnPosArgument; +import net.minecraft.commands.arguments.coordinates.Coordinates; +import net.minecraft.core.BlockPos; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.math.vector.Vector2i; + +/** + * Argument for parsing {@link Vector2i} from relative, absolute, or local coordinates. + * + *

Example input strings:

+ *
    + *
  • {@code ~ ~}
  • + *
  • {@code 12 -7}
  • + *
  • {@code ^-1 ^0}
  • + *
  • {@code ~-1 ~5}
  • + *
  • {@code ^ ^}
  • + *
+ * + * @param command sender type + */ +public final class Vector2iParser implements NodeSource, ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link Vector2iParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor vector2iParser() { + return ParserDescriptor.of(new Vector2iParser<>(), Vector2i.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser(ColumnPosArgument.columnPos()) + .flatMapSuccess((ctx, coordinates) -> { + final BlockPos pos = coordinates.getBlockPos( + (CommandSourceStack) ctx.get(SpongeCommandContextKeys.COMMAND_CAUSE) + ); + return ArgumentParseResult.successFuture(new Vector2i(pos.getX(), pos.getZ())); + }); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.COLUMN_POS.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector3dParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector3dParser.java new file mode 100644 index 00000000..0c7eff74 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector3dParser.java @@ -0,0 +1,118 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.coordinates.Coordinates; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.suggestion.Suggestion; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.common.util.VecHelper; +import org.spongepowered.math.vector.Vector3d; + +/** + * Argument for parsing {@link Vector3d} from relative, absolute, or local coordinates. + * + *

Example input strings:

+ *
    + *
  • {@code ~ ~ ~}
  • + *
  • {@code 0.1 -0.5 .9}
  • + *
  • {@code ~1 ~-2 ~10}
  • + *
  • {@code ^1 ^ ^-5}
  • + *
+ * + * @param command sender type + */ +public final class Vector3dParser extends VectorParser { + + /** + * Creates a new {@link Vector3dParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor vector3dParser() { + return vector3dParser(false); + } + + /** + * Creates a new {@link Vector3dParser}. + * + * @param command sender type + * @param centerIntegers whether to center integers to x.5 + * @return new parser + */ + public static ParserDescriptor vector3dParser(final boolean centerIntegers) { + return ParserDescriptor.of(new Vector3dParser<>(centerIntegers), Vector3d.class); + } + + private final ArgumentParser mappedParser; + + /** + * Create a new {@link Vector3dParser}. + * + * @param centerIntegers whether to center integers to x.5 + */ + public Vector3dParser(final boolean centerIntegers) { + super(centerIntegers); + this.mappedParser = new WrappedBrigadierParser(new Vec3Argument(centerIntegers)) + .flatMapSuccess((ctx, coordinates) -> { + return ArgumentParseResult.successFuture(VecHelper.toVector3d( + coordinates.getPosition((CommandSourceStack) ctx.get(SpongeCommandContextKeys.COMMAND_CAUSE)) + )); + }); + } + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.VEC3.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector3iParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector3iParser.java new file mode 100644 index 00000000..3bd5e6c2 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/Vector3iParser.java @@ -0,0 +1,100 @@ +// +// 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.sponge.parser; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.commands.arguments.coordinates.Coordinates; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; +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.sponge.NodeSource; +import org.incendo.cloud.sponge.SpongeCommandContextKeys; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.common.util.VecHelper; +import org.spongepowered.math.vector.Vector3i; + +/** + * Argument for parsing {@link Vector3i} from relative, absolute, or local coordinates. + * + *

Example input strings:

+ *
    + *
  • {@code ~ ~ ~}
  • + *
  • {@code 1 1 1}
  • + *
  • {@code ~0.5 ~-2 ~10}
  • + *
  • {@code ^1 ^ ^-5}
  • + *
+ * + * @param command sender type + */ +public final class Vector3iParser implements NodeSource, ArgumentParser.FutureArgumentParser, SuggestionProvider { + + /** + * Creates a new {@link Vector3iParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor vector3iParser() { + return ParserDescriptor.of(new Vector3iParser<>(), Vector3i.class); + } + + private final ArgumentParser mappedParser = + new WrappedBrigadierParser(BlockPosArgument.blockPos()) + .flatMapSuccess((ctx, coordinates) -> { + return ArgumentParseResult.successFuture(VecHelper.toVector3i( + coordinates.getBlockPos((CommandSourceStack) ctx.get(SpongeCommandContextKeys.COMMAND_CAUSE)) + )); + }); + + @Override + public @NonNull CompletableFuture> parseFuture( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + return this.mappedParser.parseFuture(commandContext, inputQueue); + } + + @Override + public @NonNull CompletableFuture> suggestionsFuture( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return this.mappedParser.suggestionProvider().suggestionsFuture(context, input); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.BLOCK_POS.get().createNode(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/VectorParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/VectorParser.java new file mode 100644 index 00000000..f66847e5 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/VectorParser.java @@ -0,0 +1,55 @@ +// +// 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.sponge.parser; + +import org.incendo.cloud.parser.ArgumentParser; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.suggestion.SuggestionProvider; + +/** + * Parent of {@link Vector3dParser} and {@link Vector2dParser} containing shared methods. + * + *

Not for extension by API users.

+ * + * @param command sender type + * @param vector type + */ +public abstract class VectorParser implements NodeSource, ArgumentParser.FutureArgumentParser, SuggestionProvider { + + private final boolean centerIntegers; + + protected VectorParser(final boolean centerIntegers) { + this.centerIntegers = centerIntegers; + } + + /** + * Get whether integers will be centered to x.5. Defaults to false. + * + * @return whether integers will be centered + */ + public final boolean centerIntegers() { + return this.centerIntegers; + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/WorldParser.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/WorldParser.java new file mode 100644 index 00000000..f6fc17e6 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/WorldParser.java @@ -0,0 +1,117 @@ +// +// 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.sponge.parser; + +import com.google.common.base.Suppliers; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.minecraft.commands.arguments.DimensionArgument; +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.ParserDescriptor; +import org.incendo.cloud.sponge.NodeSource; +import org.incendo.cloud.suggestion.BlockingSuggestionProvider; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.command.registrar.tree.CommandTreeNode; +import org.spongepowered.api.command.registrar.tree.CommandTreeNodeTypes; +import org.spongepowered.api.world.server.ServerWorld; +import org.spongepowered.api.world.server.WorldManager; + +/** + * Argument for retrieving {@link ServerWorld ServerWorlds} from the {@link WorldManager} by their {@link ResourceKey}. + * + * @param command sender type + */ +public final class WorldParser implements ArgumentParser, NodeSource, BlockingSuggestionProvider.Strings { + + /** + * Creates a new {@link WorldParser}. + * + * @param command sender type + * @return new parser + */ + public static ParserDescriptor worldParser() { + return ParserDescriptor.of(new WorldParser<>(), ServerWorld.class); + } + + private static final Supplier ERROR_INVALID_VALUE = Suppliers.memoize(() -> { + try { + // ERROR_INVALID_VALUE (todo: use accessor) + final Field errorInvalidValueField = Arrays.stream(DimensionArgument.class.getDeclaredFields()) + .filter(f -> f.getType().equals(DynamicCommandExceptionType.class)) + .findFirst() + .orElseThrow(IllegalStateException::new); + errorInvalidValueField.setAccessible(true); + return (DynamicCommandExceptionType) errorInvalidValueField.get(null); + } catch (final Exception ex) { + throw new RuntimeException("Couldn't access ERROR_INVALID_VALUE command exception type.", ex); + } + }); + + @Override + public @NonNull ArgumentParseResult<@NonNull ServerWorld> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput inputQueue + ) { + final String input = inputQueue.readString(); + final ResourceKey key = ResourceKeyUtil.resourceKey(input); + if (key == null) { + return ResourceKeyUtil.invalidResourceKey(); + } + final Optional entry = Sponge.server().worldManager().world(key); + if (entry.isPresent()) { + return ArgumentParseResult.success(entry.get()); + } + return ArgumentParseResult.failure(ERROR_INVALID_VALUE.get().create(key)); + } + + @Override + public @NonNull List<@NonNull String> stringSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull CommandInput input + ) { + return Sponge.server().worldManager().worlds().stream().flatMap(world -> { + if (!input.isEmpty() && world.key().namespace().equals(ResourceKey.MINECRAFT_NAMESPACE)) { + return Stream.of(world.key().value(), world.key().asString()); + } + return Stream.of(world.key().asString()); + }).collect(Collectors.toList()); + } + + @Override + public CommandTreeNode.@NonNull Argument> node() { + return CommandTreeNodeTypes.RESOURCE_LOCATION.get().createNode().customCompletions(); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/package-info.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/package-info.java new file mode 100644 index 00000000..e6629410 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/parser/package-info.java @@ -0,0 +1,4 @@ +/** + * Parsers for the Sponge 8 environment. + */ +package org.incendo.cloud.sponge.parser; diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/SpongeSuggestion.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/SpongeSuggestion.java new file mode 100644 index 00000000..dccd57f6 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/SpongeSuggestion.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.sponge.suggestion; + +import java.util.Optional; +import net.kyori.adventure.text.Component; +import net.minecraft.network.chat.ComponentUtils; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.brigadier.suggestion.TooltipSuggestion; +import org.incendo.cloud.suggestion.Suggestion; +import org.spongepowered.common.adventure.SpongeAdventure; + +/** + * {@link Suggestion} that has an optional {@link net.kyori.adventure.text.Component} tooltip. + */ +public interface SpongeSuggestion extends Suggestion { + + /** + * Returns a new {@link SpongeSuggestion} with the given {@code suggestion} and {@code tooltip}. + * + * @param suggestion the suggestion + * @param tooltip the optional tooltip that is displayed when hovering over the suggestion + * @return the suggestion instance + */ + static @NonNull SpongeSuggestion suggestion( + final @NonNull String suggestion, + final @Nullable Component tooltip + ) { + return new SpongeSuggestionImpl(suggestion, tooltip); + } + + /** + * Returns a new {@link SpongeSuggestion} that uses the given {@code suggestion} and has a {@code null} tooltip. + * + * @param suggestion the suggestion + * @return the suggestion instance + */ + static @NonNull SpongeSuggestion spongeSuggestion( + final @NonNull Suggestion suggestion + ) { + if (suggestion instanceof SpongeSuggestion) { + return (SpongeSuggestion) suggestion; + } + if (suggestion instanceof TooltipSuggestion tooltipSuggestion) { + final @Nullable Component tooltip = Optional.ofNullable(tooltipSuggestion.tooltip()) + .map(ComponentUtils::fromMessage) + .map(SpongeAdventure::asAdventure) + .orElse(null); + return suggestion(tooltipSuggestion.suggestion(), tooltip); + } + return suggestion(suggestion.suggestion(), null /* tooltip */); + } + + /** + * Returns the tooltip. + * + * @return the tooltip, or {@code null} + */ + @Nullable Component tooltip(); + + @Override + default @NonNull SpongeSuggestion withSuggestion(@NonNull String string) { + return suggestion(string, this.tooltip()); + } + + /** + * Returns a copy of this suggestion instance using the given {@code tooltip} + * + * @param tooltip the new tooltip + * @return the new suggestion + */ + default @NonNull SpongeSuggestion withTooltip(@NonNull Component tooltip) { + return suggestion(this.suggestion(), tooltip); + } + +} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/SpongeSuggestionImpl.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/SpongeSuggestionImpl.java new file mode 100644 index 00000000..4689d2aa --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/SpongeSuggestionImpl.java @@ -0,0 +1,32 @@ +// +// 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.sponge.suggestion; + +import net.kyori.adventure.text.Component; + +record SpongeSuggestionImpl( + String suggestion, + Component tooltip + +) implements SpongeSuggestion {} diff --git a/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/package-info.java b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/package-info.java new file mode 100644 index 00000000..b29fcb74 --- /dev/null +++ b/cloud-sponge/src/main/java/org/incendo/cloud/sponge/suggestion/package-info.java @@ -0,0 +1,4 @@ +/** + * Sponge suggestion related classes. + */ +package org.incendo.cloud.sponge.suggestion; diff --git a/examples/example-sponge/build.gradle.kts b/examples/example-sponge/build.gradle.kts new file mode 100644 index 00000000..e6419965 --- /dev/null +++ b/examples/example-sponge/build.gradle.kts @@ -0,0 +1,58 @@ +import org.spongepowered.gradle.plugin.config.PluginLoaders +import org.spongepowered.plugin.metadata.model.PluginDependency + +plugins { + id("org.spongepowered.gradle.plugin") version "2.2.0" + id("conventions.base") + alias(libs.plugins.shadow) +} + +dependencies { + implementation(project(":cloud-sponge")) + implementation(libs.cloud.minecraft.extras) +} + +sponge { + injectRepositories(false) + apiVersion("11.0.0-SNAPSHOT") + plugin("cloud-example-sponge") { + loader { + name(PluginLoaders.JAVA_PLAIN) + version("1.0") + } + displayName("Cloud example Sponge plugin") + description("Plugin to demonstrate and test the Sponge implementation of cloud") + license("MIT") + entrypoint("cloud.commandframework.examples.sponge.CloudExamplePlugin") + dependency("spongeapi") { + loadOrder(PluginDependency.LoadOrder.AFTER) + optional(false) + } + } +} + +tasks { + assemble { + dependsOn(shadowJar) + } +} + +configurations { + spongeRuntime { + resolutionStrategy { + cacheChangingModulesFor(1, "MINUTES") + eachDependency { + if (target.name == "spongevanilla") { + useVersion("1.20.+") + } + } + } + } +} + +afterEvaluate { + tasks.compileJava { + // TODO - sponge AP not compatible with J21 + options.compilerArgs.remove("-Werror") + } +} diff --git a/examples/example-sponge/src/main/java/org/incendo/cloud/examples/sponge/CloudExamplePlugin.java b/examples/example-sponge/src/main/java/org/incendo/cloud/examples/sponge/CloudExamplePlugin.java new file mode 100644 index 00000000..65ad974f --- /dev/null +++ b/examples/example-sponge/src/main/java/org/incendo/cloud/examples/sponge/CloudExamplePlugin.java @@ -0,0 +1,453 @@ +// +// 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.examples.sponge; + +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import io.leangen.geantyref.TypeToken; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.Command; +import org.incendo.cloud.component.DefaultValue; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.minecraft.extras.MinecraftExceptionHandler; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.permission.PredicatePermission; +import org.incendo.cloud.sponge.CloudInjectionModule; +import org.incendo.cloud.sponge.SpongeCommandManager; +import org.incendo.cloud.sponge.data.BlockInput; +import org.incendo.cloud.sponge.data.BlockPredicate; +import org.incendo.cloud.sponge.data.ItemStackPredicate; +import org.incendo.cloud.sponge.data.MultipleEntitySelector; +import org.incendo.cloud.sponge.data.ProtoItemStack; +import org.incendo.cloud.sponge.data.SinglePlayerSelector; +import org.incendo.cloud.sponge.exception.ComponentMessageRuntimeException; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.command.CommandCause; +import org.spongepowered.api.command.parameter.managed.operator.Operator; +import org.spongepowered.api.command.parameter.managed.operator.Operators; +import org.spongepowered.api.data.Keys; +import org.spongepowered.api.data.persistence.DataContainer; +import org.spongepowered.api.data.type.ProfessionType; +import org.spongepowered.api.data.type.VillagerType; +import org.spongepowered.api.effect.sound.SoundType; +import org.spongepowered.api.entity.EntityType; +import org.spongepowered.api.entity.EntityTypes; +import org.spongepowered.api.entity.living.player.Player; +import org.spongepowered.api.entity.living.player.User; +import org.spongepowered.api.entity.living.trader.Villager; +import org.spongepowered.api.item.enchantment.Enchantment; +import org.spongepowered.api.item.enchantment.EnchantmentType; +import org.spongepowered.api.item.inventory.ItemStack; +import org.spongepowered.api.item.inventory.Slot; +import org.spongepowered.api.item.inventory.entity.Hotbar; +import org.spongepowered.api.item.inventory.transaction.InventoryTransactionResult; +import org.spongepowered.api.registry.RegistryHolder; +import org.spongepowered.api.registry.RegistryTypes; +import org.spongepowered.api.world.DefaultWorldKeys; +import org.spongepowered.api.world.Location; +import org.spongepowered.api.world.biome.Biome; +import org.spongepowered.api.world.schematic.PaletteTypes; +import org.spongepowered.api.world.server.ServerLocation; +import org.spongepowered.api.world.server.ServerWorld; +import org.spongepowered.math.vector.Vector3d; +import org.spongepowered.math.vector.Vector3i; +import org.spongepowered.plugin.builtin.jvm.Plugin; + +import static net.kyori.adventure.text.Component.newline; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.AQUA; +import static net.kyori.adventure.text.format.NamedTextColor.BLUE; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.LIGHT_PURPLE; +import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.TextColor.color; +import static org.incendo.cloud.parser.standard.DoubleParser.doubleParser; +import static org.incendo.cloud.parser.standard.IntegerParser.integerParser; +import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; +import static org.incendo.cloud.parser.standard.StringParser.stringParser; +import static org.incendo.cloud.sponge.parser.BlockInputParser.blockInputParser; +import static org.incendo.cloud.sponge.parser.BlockPredicateParser.blockPredicateParser; +import static org.incendo.cloud.sponge.parser.DataContainerParser.dataContainerParser; +import static org.incendo.cloud.sponge.parser.ItemStackPredicateParser.itemStackPredicateParser; +import static org.incendo.cloud.sponge.parser.MultipleEntitySelectorParser.multipleEntitySelectorParser; +import static org.incendo.cloud.sponge.parser.NamedTextColorParser.namedTextColorParser; +import static org.incendo.cloud.sponge.parser.OperatorParser.operatorParser; +import static org.incendo.cloud.sponge.parser.ProtoItemStackParser.protoItemStackParser; +import static org.incendo.cloud.sponge.parser.RegistryEntryParser.registryEntryParser; +import static org.incendo.cloud.sponge.parser.SinglePlayerSelectorParser.singlePlayerSelectorParser; +import static org.incendo.cloud.sponge.parser.UserParser.userParser; +import static org.incendo.cloud.sponge.parser.Vector3dParser.vector3dParser; +import static org.incendo.cloud.sponge.parser.Vector3iParser.vector3iParser; +import static org.incendo.cloud.sponge.parser.WorldParser.worldParser; + +@Plugin("cloud-example-sponge") +public final class CloudExamplePlugin { + + private static final Component COMMAND_PREFIX = text() + .color(color(0x333333)) + .content("[") + .append(text("Cloud-Sponge", color(0xF7CF0D))) + .append(text(']')) + .build(); + + private final SpongeCommandManager commandManager; + + /** + * Create example plugin instance + * + * @param injector injector + */ + @Inject + public CloudExamplePlugin(final @NonNull Injector injector) { + // Create child injector with cloud module + final Injector childInjector = injector.createChildInjector( + CloudInjectionModule.createNative(ExecutionCoordinator.simpleCoordinator()) + ); + + // Get command manager instance + this.commandManager = childInjector.getInstance(Key.get(new TypeLiteral<>() {})); + + // Register minecraft-extras exception handlers + MinecraftExceptionHandler.create(CommandCause::audience) + .defaultHandlers() + .decorator(message -> Component.text().append(COMMAND_PREFIX, space(), message).build()) + .registerTo(this.commandManager); + + this.registerCommands(); + } + + private void registerCommands() { + this.commandManager.command(this.commandManager.commandBuilder("cloud_test1") + .permission("cloud.test1") + .handler(ctx -> ctx.sender().audience().sendMessage(text("success")))); + this.commandManager.command(this.commandManager.commandBuilder("cloud_test2") + .literal("test") + .literal("test1") + .handler(ctx -> ctx.sender().audience().sendMessage(text("success")))); + final Command.Builder cloudTest3 = this.commandManager.commandBuilder("cloud_test3"); + final Command.Builder test = cloudTest3.literal("test"); + this.commandManager.command(test.required("string_arg", stringParser()) + .literal("test2") + .handler(ctx -> ctx.sender().audience().sendMessage(text("success")))); + this.commandManager.command(test.literal("literal_arg") + .handler(ctx -> ctx.sender().audience().sendMessage(text("success")))); + this.commandManager.command(cloudTest3.literal("another_test") + .handler(ctx -> ctx.sender().audience().sendMessage(text("success")))); + final Command.Builder cloud = this.commandManager.commandBuilder("cloud"); + this.commandManager.command(cloud.literal("string_test") + .required("single", stringParser(StringParser.StringMode.SINGLE)) + .required("quoted", stringParser(StringParser.StringMode.QUOTED)) + .required("greedy", stringParser(StringParser.StringMode.GREEDY)) + .handler(ctx -> ctx.sender().audience().sendMessage(text("success")))); + this.commandManager.command(cloud.literal("int_test") + .required("any", integerParser()) + .required("gt0", integerParser(1)) + .required("lt100", integerParser(Integer.MIN_VALUE, 99)) + .required("5to20", integerParser(5, 20)) + .handler(ctx -> ctx.sender().audience().sendMessage(text("success")))); + this.commandManager.command(cloud.literal("enchantment_type_test") + .required("enchantment_type", registryEntryParser(EnchantmentType.class, RegistryTypes.ENCHANTMENT_TYPE)) + .optional("level", integerParser(), DefaultValue.constant(1)) + .handler(ctx -> { + final Object subject = ctx.sender().subject(); + if (!(subject instanceof Player)) { + ctx.sender().audience().sendMessage(text("This command is for players only!", RED)); + return; + } + final Player player = (Player) subject; + final Hotbar hotbar = player.inventory().hotbar(); + final int index = hotbar.selectedSlotIndex(); + final Slot slot = hotbar.slot(index).get(); + final InventoryTransactionResult.Poll result = slot.poll(); + if (result.type() != InventoryTransactionResult.Type.SUCCESS) { + player.sendMessage(text("You must hold an item to enchant!", RED)); + return; + } + final ItemStack modified = ItemStack.builder() + .fromItemStack(result.polledItem().createStack()) + .add(Keys.APPLIED_ENCHANTMENTS, List.of( + Enchantment.of( + ctx.get("enchantment_type"), + ctx.get("level") + ) + )) + .build(); + slot.set(modified); + })); + this.commandManager.command(cloud.literal("color_test") + .required("color", namedTextColorParser()) + .required("message", greedyStringParser()) + .handler(ctx -> { + ctx.sender().audience().sendMessage( + text(ctx.get("message"), ctx.get("color")) + ); + })); + this.commandManager.command(cloud.literal("operator_test") + .required("first", integerParser()) + .required("operator", operatorParser()) + .required("second", integerParser()) + .handler(ctx -> { + final int first = ctx.get("first"); + final int second = ctx.get("second"); + final Operator operator = ctx.get("operator"); + if (!(operator instanceof Operator.Simple)) { + ctx.sender().audience().sendMessage( + text("That type of operator is not applicable here!", RED) + ); + return; + } + ctx.sender().audience().sendMessage(text() + .color(AQUA) + .append(text(first)) + .append(space()) + .append(text(operator.asString(), BLUE)) + .append(space()) + .append(text(second)) + .append(space()) + .append(text('→', BLUE)) + .append(space()) + .append(text(((Operator.Simple) operator).apply(first, second))) + ); + })); + this.commandManager.command(cloud.literal("modifylevel") + .required("operator", operatorParser()) + .required("value", doubleParser()) + .handler(ctx -> { + final Object subject = ctx.sender().subject(); + if (!(subject instanceof Player)) { // todo: a solution to this + ctx.sender().audience().sendMessage(text("This command is for players only!", RED)); + return; + } + final Player player = (Player) subject; + final Operator operator = ctx.get("operator"); + final double value = ctx.get("value"); + if (operator == Operators.ASSIGN.get()) { + player.offer(Keys.EXPERIENCE, (int) value); + return; + } + if (!(operator instanceof Operator.Simple)) { + ctx.sender().audience().sendMessage( + text("That type of operator is not applicable here!", RED) + ); + return; + } + final int currentXp = player.get(Keys.EXPERIENCE).get(); + player.offer(Keys.EXPERIENCE, (int) ((Operator.Simple) operator).apply(currentXp, value)); + })); + this.commandManager.command(cloud.literal("selectplayer") + .required("player", singlePlayerSelectorParser()) + .handler(ctx -> { + final Player player = ctx.get("player").getSingle(); + ctx.sender().audience().sendMessage(Component.text().append( + text("Display name of selected player: ", GRAY), + player.displayName().get() + ).build()); + })); + this.commandManager.command(cloud.literal("world_test") + .required("world", worldParser()) + .handler(ctx -> { + ctx.sender().audience().sendMessage(text(ctx.get("world").key().asString())); + })); + this.commandManager.command(cloud.literal("test_item") + .required("item", protoItemStackParser()) + .literal("is") + .required("predicate", itemStackPredicateParser()) + .handler(ctx -> { + final ItemStack item = ctx.get("item").createItemStack(1, true); + final ItemStackPredicate predicate = ctx.get("predicate"); + final Component message = text(builder -> { + builder.append(item.get(Keys.DISPLAY_NAME).orElse(item.type().asComponent())) + .append(space()); + if (predicate.test(item)) { + builder.append(text("passes!", GREEN)); + return; + } + builder.append(text("does not pass!", RED)); + }); + ctx.sender().audience().sendMessage(message); + })); + this.commandManager.command(cloud.literal("test_entity_type") + .required("type", registryEntryParser(new TypeToken<>() {}, RegistryTypes.ENTITY_TYPE)) + .handler(ctx -> ctx.sender().audience().sendMessage(ctx.>get("type")))); + final Function, RegistryHolder> holderFunction = ctx -> ctx.sender() + .location() + .map(Location::world) + .orElseGet(() -> Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).orElseThrow()); + this.commandManager.command(cloud.literal("test_biomes") + .required("biome", registryEntryParser(Biome.class, RegistryTypes.BIOME, holderFunction)) + .handler(ctx -> { + final ResourceKey biomeKey = holderFunction.apply(ctx) + .registry(RegistryTypes.BIOME) + .findValueKey(ctx.get("biome")) + .orElseThrow(IllegalStateException::new); + ctx.sender().audience().sendMessage(text(biomeKey.asString())); + })); + this.commandManager.command(cloud.literal("test_sounds") + .required("type", registryEntryParser(SoundType.class, RegistryTypes.SOUND_TYPE)) + .handler(ctx -> { + ctx.sender().audience().sendMessage(text(ctx.get("type").key().asString())); + })); + this.commandManager.command(cloud.literal("summon_villager") + .required("type", registryEntryParser(VillagerType.class, RegistryTypes.VILLAGER_TYPE)) + .required("profession", registryEntryParser(ProfessionType.class, RegistryTypes.PROFESSION_TYPE)) + .handler(ctx -> { + final ServerLocation loc = ctx.sender().location().orElse(null); + if (loc == null) { + ctx.sender().audience().sendMessage(text("No location!")); + return; + } + final ServerWorld world = loc.world(); + final Villager villager = world.createEntity(EntityTypes.VILLAGER, loc.position()); + villager.offer(Keys.VILLAGER_TYPE, ctx.get("type")); + villager.offer(Keys.PROFESSION_TYPE, ctx.get("profession")); + if (world.spawnEntity(villager)) { + ctx.sender().audience().sendMessage(text() + .append(text("Spawned entity!", GREEN)) + .append(space()) + .append(villager.displayName().get()) + .hoverEvent(villager)); + } else { + ctx.sender().audience().sendMessage(text("failed to spawn :(")); + } + })); + this.commandManager.command(cloud.literal("vec3d") + .required("vec3d", vector3dParser()) + .handler(ctx -> { + ctx.sender().audience().sendMessage(text(ctx.get("vec3d").toString())); + })); + this.commandManager.command(cloud.literal("selectentities") + .required("selector", multipleEntitySelectorParser()) + .handler(ctx -> { + final MultipleEntitySelector selector = ctx.get("selector"); + ctx.sender().audience().sendMessage(Component.text().append( + text("Using selector: ", BLUE), + text(selector.inputString()), + newline(), + text("Selected: ", LIGHT_PURPLE), + selector.get().stream() + .map(e -> e.displayName().get()) + .collect(Component.toComponent(text(", ", GRAY))) + ).build()); + })); + + this.commandManager.command(cloud.literal("user") + .required("user", userParser()) + .handler(ctx -> { + ctx.sender().audience().sendMessage(text(ctx.get("user").toString())); + })); + this.commandManager.command(cloud.literal("data") + .required("data", dataContainerParser()) + .handler(ctx -> { + ctx.sender().audience().sendMessage(text(ctx.get("data").toString())); + })); + this.commandManager.command(cloud.literal("setblock") + .permission("cloud.setblock") + .required("position", vector3iParser()) + .required("block", blockInputParser()) + .handler(ctx -> { + final Vector3i position = ctx.get("position"); + final BlockInput input = ctx.get("block"); + final Optional location = ctx.sender().location(); + if (location.isPresent()) { + final ServerWorld world = location.get().world(); + input.place(world.location(position)); + ctx.sender().audience().sendMessage(text("set block!")); + } else { + ctx.sender().audience().sendMessage(text("no location!")); + } + })); + this.commandManager.command(cloud.literal("blockinput") + .required("block", blockInputParser()) + .handler(ctx -> { + final BlockInput input = ctx.get("block"); + ctx.sender().audience().sendMessage(text( + PaletteTypes.BLOCK_STATE_PALETTE.get().stringifier() + .apply(RegistryTypes.BLOCK_TYPE.get(), input.blockState()) + )); + })); + this.commandManager.command(this.commandManager.commandBuilder("gib") + .permission("cloud.gib") + .requiredArgumentPair( + "itemstack", + TypeToken.get(ItemStack.class), + "item", protoItemStackParser(), + "amount", integerParser(), + (sender, proto, amount) -> { + try { + return ArgumentParseResult.successFuture( + proto.createItemStack(amount, true) + ); + } catch (final ComponentMessageRuntimeException e) { + return ArgumentParseResult.failureFuture(e); + } + }, + Description.of("The ItemStack to give") + ) + .handler(ctx -> ((Player) ctx.sender().subject()).inventory().offer(ctx.get("itemstack")))); + this.commandManager.command(cloud.literal("replace") + .permission(PredicatePermission.of(cause -> { + // works but error message is ugly + // todo: cause.cause().root() returns DedicatedServer during permission checks? + return cause.subject() instanceof Player; + })) + .required("predicate", blockPredicateParser()) + .required("radius", integerParser()) + .required("replacement", blockInputParser()) + .handler(ctx -> { + final BlockPredicate predicate = ctx.get("predicate"); + final int radius = ctx.get("radius"); + final BlockInput replacement = ctx.get("replacement"); + + // its a player so get is fine + final ServerLocation loc = ctx.sender().location().get(); + final ServerWorld world = loc.world(); + final Vector3d vec = loc.position(); + + for (double x = vec.x() - radius; x < vec.x() + radius; x++) { + for (double y = vec.y() - radius; y < vec.y() + radius; y++) { + for (double z = vec.z() - radius; z < vec.z() + radius; z++) { + final ServerLocation location = world.location(x, y, z); + if (predicate.test(location)) { + location.setBlock(replacement.blockState()); + } + } + } + } + })); + } + +} diff --git a/examples/example-sponge/src/main/java/org/incendo/cloud/examples/sponge/package-info.java b/examples/example-sponge/src/main/java/org/incendo/cloud/examples/sponge/package-info.java new file mode 100644 index 00000000..e93c92bb --- /dev/null +++ b/examples/example-sponge/src/main/java/org/incendo/cloud/examples/sponge/package-info.java @@ -0,0 +1,27 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// 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. +// +/** + * Cloud example for Sponge API v8 + */ +package org.incendo.cloud.examples.sponge; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fdec37f4..82032f57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ zNeoForge = { module = "net.neoforged:neoforge", version.ref = "neoforge" } cloud-buildLogic-spotless = { id = "org.incendo.cloud-build-logic.spotless", version.ref = "cloud-build-logic" } cloud-buildLogic-rootProject-publishing = { id = "org.incendo.cloud-build-logic.publishing.root-project", version.ref = "cloud-build-logic" } cloud-buildLogic-rootProject-spotless = { id = "org.incendo.cloud-build-logic.spotless.root-project", version.ref = "cloud-build-logic" } +shadow = { id = "com.gradleup.shadow", version = "9.2.2" } [bundles] immutables = ["immutables", "immutablesAnnotate"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 91fb593f..3eca0d70 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,11 +6,19 @@ pluginManagement { mavenContent { snapshotsOnly() } } maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.spongepowered.org/repository/maven-public/") { + mavenContent { includeGroup("org.spongepowered") } + } maven("https://maven.fabricmc.net/") maven("https://maven.neoforged.net/releases/") maven("https://maven.architectury.dev/") maven("https://repo.jpenilla.xyz/snapshots/") } + repositories { + mavenCentral() + gradlePluginPortal() + maven("https://repo.spongepowered.org/repository/maven-public/") + } includeBuild("gradle/build-logic") } @@ -27,6 +35,9 @@ dependencyResolutionManagement { maven("https://central.sonatype.com/repository/maven-snapshots/") { mavenContent { snapshotsOnly() } } + maven("https://repo.spongepowered.org/repository/maven-public/") { + mavenContent { includeGroup("org.spongepowered") } + } maven("https://repo.papermc.io/repository/maven-public/") maven("https://maven.fabricmc.net/") maven("https://maven.neoforged.net/releases/") @@ -46,3 +57,6 @@ include("cloud-fabric") include("cloud-fabric/common-repack") findProject(":cloud-fabric/common-repack")?.name = "cloud-minecraft-modded-common-fabric-repack" include("cloud-neoforge") +include("cloud-sponge") +include("examples/example-sponge") +findProject(":examples/example-sponge")?.name = "example-sponge"