diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionKeys.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionKeys.java index 4aab6b05..d86a2b1f 100644 --- a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionKeys.java +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionKeys.java @@ -42,6 +42,10 @@ public final class FabricCaptionKeys { "argument.parse.failure.registry_entry.unknown_entry" ); + public static final Caption ARGUMENT_PARSE_FAILURE_TEAM_UNKNOWN = of( + "argument.parse.failure.team.unknown" + ); + private static @NonNull Caption of(final @NonNull String key) { final Caption caption = Caption.of(key); RECOGNIZED_CAPTIONS.add(caption); diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionRegistry.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionRegistry.java index bfacada8..6b375586 100644 --- a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionRegistry.java +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCaptionRegistry.java @@ -30,11 +30,15 @@ public class FabricCaptionRegistry extends SimpleCaptionRegistry { public static final String ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY = "Could not find key {id} in registry '{registry}'"; + public static final String ARGUMENT_PARSE_FAILURE_TEAM_UNKOWN = "Could not find any team named '{input}'!"; + protected FabricCaptionRegistry() { super(); this.registerMessageFactory(FabricCaptionKeys.ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY, (caption, sender) -> ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY); + this.registerMessageFactory(FabricCaptionKeys.ARGUMENT_PARSE_FAILURE_TEAM_UNKNOWN, + (caption, sender) -> ARGUMENT_PARSE_FAILURE_TEAM_UNKOWN); } diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java index d833367c..b377e263 100644 --- a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java @@ -33,7 +33,10 @@ import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; import cloud.commandframework.context.CommandContext; import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.fabric.argument.FabricArgumentParsers; import cloud.commandframework.fabric.argument.RegistryEntryArgument; +import cloud.commandframework.fabric.argument.TeamArgument; +import cloud.commandframework.fabric.data.MinecraftTime; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import com.mojang.brigadier.arguments.ArgumentType; @@ -62,6 +65,7 @@ import net.minecraft.command.argument.ObjectiveCriteriaArgumentType; import net.minecraft.command.argument.OperationArgumentType; import net.minecraft.command.argument.ParticleArgumentType; import net.minecraft.command.argument.SwizzleArgumentType; +import net.minecraft.command.argument.TeamArgumentType; import net.minecraft.command.argument.UuidArgumentType; import net.minecraft.command.suggestion.SuggestionProviders; import net.minecraft.nbt.CompoundTag; @@ -69,6 +73,7 @@ import net.minecraft.nbt.Tag; import net.minecraft.particle.ParticleEffect; import net.minecraft.predicate.NumberRange; import net.minecraft.scoreboard.ScoreboardCriterion; +import net.minecraft.scoreboard.Team; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; @@ -160,6 +165,8 @@ public abstract class FabricCommandManager extends C /* Cloud-native argument types */ brigadier.registerMapping(new TypeToken>() {}, builder -> builder.toConstant(UuidArgumentType.uuid())); this.registerRegistryEntryMappings(); + brigadier.registerMapping(new TypeToken>() {}, builder -> builder.toConstant(TeamArgumentType.team())); + this.getParserRegistry().registerParserSupplier(TypeToken.get(Team.class), params -> new TeamArgument.TeamParser<>()); /* Wrapped/Constant Brigadier types, native value type */ this.registerConstantNativeParserSupplier(Formatting.class, ColorArgumentType.color()); @@ -194,8 +201,8 @@ public abstract class FabricCommandManager extends C this.registerConstantNativeParserSupplier(ScoreboardSlotArgumentType.scoreboardSlot()); this.registerConstantNativeParserSupplier(Team.class, TeamArgumentType.team()); this.registerConstantNativeParserSupplier(/* slot *, ItemSlotArgumentType.itemSlot()); - this.registerConstantNativeParserSupplier(CommandFunction.class, FunctionArgumentType.function()); - this.registerConstantNativeParserSupplier(/* time representation in ticks *, TimeArgumentType.time());*/ + this.registerConstantNativeParserSupplier(CommandFunction.class, FunctionArgumentType.function()); */ + this.getParserRegistry().registerParserSupplier(TypeToken.get(MinecraftTime.class), params -> FabricArgumentParsers.time()); /* Wrapped brigadier requiring parameters */ // score holder: single vs multiple diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/AngleArgument.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/AngleArgument.java index 36064129..09dd57bd 100644 --- a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/AngleArgument.java +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/AngleArgument.java @@ -117,7 +117,7 @@ public final class AngleArgument extends CommandArgument sender type + * @return a parser instance + */ + public static ArgumentParser time() { + return new WrappedBrigadierParser(TimeArgumentType.time()) + .map((ctx, val) -> ArgumentParseResult.success(MinecraftTime.of(val))); + } + + public static ArgumentParser commandFunction() { + // TODO: Should probably write our own parser for this, it's either Identifier or tag. + // Server parsers + return new WrappedBrigadierParser(FunctionArgumentType.function()).map((ctx, val) -> { + final CommandSource source = ctx.get(FabricCommandContextKeys.NATIVE_COMMAND_SOURCE); + source.getCompletions() + }) + } } diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/RegistryEntryArgument.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/RegistryEntryArgument.java index 10ff48f2..f3dc3149 100644 --- a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/RegistryEntryArgument.java +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/RegistryEntryArgument.java @@ -58,6 +58,7 @@ import static java.util.Objects.requireNonNull; * * @param the command sender type * @param the registry entry type + * @since 1.4.0 */ public class RegistryEntryArgument extends CommandArgument { private static final String NAMESPACE_MINECRAFT = "minecraft"; diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/SidedArgumentParser.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/SidedArgumentParser.java new file mode 100644 index 00000000..270870e5 --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/SidedArgumentParser.java @@ -0,0 +1,68 @@ +package cloud.commandframework.fabric.argument; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.fabric.FabricCommandContextKeys; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.command.CommandSource; +import net.minecraft.server.command.ServerCommandSource; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Queue; + +/** + * An argument parser that is resolved in different ways on the logical server and logical client. + * + * @param command sender type + * @param intermediate type to resolve + * @param resolved type + */ +abstract class SidedArgumentParser implements ArgumentParser { + + @Override + public @NonNull ArgumentParseResult<@NonNull R> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue + ) { + final CommandSource source = commandContext.get(FabricCommandContextKeys.NATIVE_COMMAND_SOURCE); + final ArgumentParseResult intermediate = this.parseIntermediate(commandContext, inputQueue); + + return intermediate.flatMapParsedValue(value -> { + if (source instanceof ServerCommandSource) { + return this.resolveServer(commandContext, source, value); + } else if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) { + return this.resolveClient(commandContext, source, value); + } else { + throw new IllegalStateException("Cannot have non-server command source when not on client"); + } + }); + } + + protected abstract ArgumentParseResult parseIntermediate( + @NonNull CommandContext<@NonNull C> commandContext, + @NonNull Queue<@NonNull String> inputQueue + ); + + /** + * Resolve the final value for this argument when running on the client. + * + * @param context Command context + * @param source The command source + * @param value parsed intermediate value + * @return a resolved value + */ + protected abstract ArgumentParseResult resolveClient(CommandContext context, CommandSource source, I value); + + /** + * Resolve the final value for this argument when running on the server. + * + * @param context Command context + * @param source The command source + * @param value Parsed intermediate value + * @return a resolved value + */ + protected abstract ArgumentParseResult resolveServer(CommandContext context, CommandSource source, I value); + +} diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/TeamArgument.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/TeamArgument.java new file mode 100644 index 00000000..4a8b18fc --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/TeamArgument.java @@ -0,0 +1,200 @@ +// +// MIT License +// +// Copyright (c) 2020 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. +// + +package cloud.commandframework.fabric.argument; + +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.captions.CaptionVariable; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.exceptions.parsing.NoInputProvidedException; +import cloud.commandframework.exceptions.parsing.ParserException; +import cloud.commandframework.fabric.FabricCaptionKeys; +import cloud.commandframework.fabric.FabricCommandContextKeys; +import net.minecraft.client.MinecraftClient; +import net.minecraft.command.CommandSource; +import net.minecraft.command.argument.EntityAnchorArgumentType; +import net.minecraft.scoreboard.Team; +import net.minecraft.server.command.ServerCommandSource; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.function.BiFunction; + +/** + * An argument parsing an entity anchor. + * + * @param the sender type + * @since 1.4.0 + */ +public final class TeamArgument extends CommandArgument { + + TeamArgument( + final boolean required, + final @NonNull String name, + final @NonNull String defaultValue, + final @Nullable BiFunction, String, List> suggestionsProvider + ) { + super( + required, + name, + new TeamParser<>(), + defaultValue, + Team.class, + suggestionsProvider + ); + } + + /** + * Create a new builder. + * + * @param name Name of the argument + * @param Command sender type + * @return Created builder + */ + public static TeamArgument.@NonNull Builder newBuilder(final @NonNull String name) { + return new TeamArgument.Builder<>(name); + } + + /** + * Create a new required command argument. + * + * @param name Component name + * @param Command sender type + * @return Created argument + */ + public static @NonNull TeamArgument of(final @NonNull String name) { + return TeamArgument.newBuilder(name).asRequired().build(); + } + + /** + * Create a new optional command argument + * + * @param name Component name + * @param Command sender type + * @return Created argument + */ + public static @NonNull TeamArgument optional(final @NonNull String name) { + return TeamArgument.newBuilder(name).asOptional().build(); + } + + /** + * Create a new optional command argument with a default value + * + * @param name Argument name + * @param defaultValue Default value + * @param Command sender type + * @return Created argument + */ + public static @NonNull TeamArgument optional( + final @NonNull String name, + final EntityAnchorArgumentType.@NonNull EntityAnchor defaultValue + ) { + return TeamArgument.newBuilder(name).asOptionalWithDefault(defaultValue.name()).build(); + } + + public static final class TeamParser extends SidedArgumentParser { + + @Override + public @NonNull List<@NonNull String> suggestions( + final @NonNull CommandContext commandContext, + final @NonNull String input + ) { + return new ArrayList<>(commandContext.get(FabricCommandContextKeys.NATIVE_COMMAND_SOURCE).getTeamNames()); + } + + @Override + protected ArgumentParseResult parseIntermediate( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue + ) { + final String input = inputQueue.peek(); + if (input == null) { + return ArgumentParseResult.failure(new NoInputProvidedException(TeamParser.class, commandContext)); + } + return ArgumentParseResult.success(input); + } + + @Override + protected ArgumentParseResult resolveClient(final CommandContext context, + final CommandSource source, + final String value) { + final Team result = MinecraftClient.getInstance().getNetworkHandler().getWorld().getScoreboard().getTeam(value); + if (result == null) { + return ArgumentParseResult.failure(new UnknownTeamException(context, value)); + } + return ArgumentParseResult.success(result); + } + + @Override + protected ArgumentParseResult resolveServer(final CommandContext context, + final CommandSource source, + final String value) { + final Team result = ((ServerCommandSource) source).getWorld().getScoreboard().getTeam(value); + if (result == null) { + return ArgumentParseResult.failure(new UnknownTeamException(context, value)); + } + return ArgumentParseResult.success(result); + } + + } + + public static final class Builder extends TypedBuilder> { + + Builder(final @NonNull String name) { + super(Team.class, name); + } + + /** + * Build a new criterion argument + * + * @return Constructed argument + */ + @Override + public @NonNull TeamArgument build() { + return new TeamArgument<>(this.isRequired(), this.getName(), this.getDefaultValue(), this.getSuggestionsProvider()); + } + + } + + public static final class UnknownTeamException extends ParserException { + + UnknownTeamException( + final @NonNull CommandContext context, + final @NonNull String input + ) { + super( + TeamParser.class, + context, + FabricCaptionKeys.ARGUMENT_PARSE_FAILURE_TEAM_UNKNOWN, + CaptionVariable.of("input", input) + ); + } + + } + +} diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/TimeArgument.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/TimeArgument.java new file mode 100644 index 00000000..5cc39885 --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/TimeArgument.java @@ -0,0 +1,130 @@ +// +// MIT License +// +// Copyright (c) 2020 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. +// + +package cloud.commandframework.fabric.argument; + +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.fabric.data.MinecraftTime; +import net.minecraft.command.argument.TimeArgumentType; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; +import java.util.function.BiFunction; + +/** + * An argument for in-game time + * + * @param the sender type + * @since 1.4.0 + */ +public final class TimeArgument extends CommandArgument { + + TimeArgument( + final boolean required, + final @NonNull String name, + final @NonNull String defaultValue, + final @Nullable BiFunction, String, List> suggestionsProvider + ) { + super( + required, + name, + FabricArgumentParsers.time(), + defaultValue, + MinecraftTime.class, + suggestionsProvider + ); + } + + /** + * Create a new builder. + * + * @param name Name of the argument + * @param Command sender type + * @return Created builder + */ + public static TimeArgument.@NonNull Builder newBuilder(final @NonNull String name) { + return new TimeArgument.Builder<>(name); + } + + /** + * Create a new required command argument. + * + * @param name Component name + * @param Command sender type + * @return Created argument + */ + public static @NonNull TimeArgument of(final @NonNull String name) { + return TimeArgument.newBuilder(name).asRequired().build(); + } + + /** + * Create a new optional command argument + * + * @param name Component name + * @param Command sender type + * @return Created argument + */ + public static @NonNull TimeArgument optional(final @NonNull String name) { + return TimeArgument.newBuilder(name).asOptional().build(); + } + + /** + * Create a new optional command argument with a default value + * + * @param name Argument name + * @param defaultTime Default time, in ticks + * @param Command sender type + * @return Created argument + */ + public static @NonNull TimeArgument optional( + final @NonNull String name, + final MinecraftTime defaultTime + ) { + return TimeArgument.newBuilder(name).asOptionalWithDefault(defaultTime.toString()).build(); + } + + + public static final class Builder extends TypedBuilder> { + + Builder(final @NonNull String name) { + super(MinecraftTime.class, name); + } + + /** + * Build a new time argument + * + * @return Constructed argument + */ + @Override + public @NonNull TimeArgument build() { + return new TimeArgument<>(this.isRequired(), this.getName(), this.getDefaultValue(), this.getSuggestionsProvider()); + } + + } + +} diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/server/CommandFunctionArgument.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/server/CommandFunctionArgument.java new file mode 100644 index 00000000..a79cd403 --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/server/CommandFunctionArgument.java @@ -0,0 +1,313 @@ +// +// MIT License +// +// Copyright (c) 2020 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. +// + +package cloud.commandframework.fabric.argument.server; + +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.captions.CaptionVariable; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.exceptions.parsing.NoInputProvidedException; +import cloud.commandframework.exceptions.parsing.ParserException; +import cloud.commandframework.fabric.FabricCaptionKeys; +import cloud.commandframework.fabric.FabricCommandContextKeys; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.leangen.geantyref.TypeToken; +import net.minecraft.command.CommandSource; +import net.minecraft.server.function.CommandFunction; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; +import net.minecraft.util.registry.RegistryKey; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.function.BiFunction; + +import static java.util.Objects.requireNonNull; + +/** + * Get a value from a registry. + * + *

Both static and dynamic registries are supported.

+ * + * @param the command sender type + * @since 1.4.0 + */ +public class CommandFunctionArgument extends CommandArgument { + + CommandFunctionArgument( + final boolean required, + final @NonNull String name, + final @NonNull RegistryKey> registry, + final @NonNull String defaultValue, + final @NonNull TypeToken valueType, + final @Nullable BiFunction, String, List> suggestionsProvider + ) { + super(required, name, new RegistryEntryParser<>(registry), defaultValue, valueType, suggestionsProvider); + } + + /** + * Create a new builder. + * + * @param name Name of the argument + * @param type The type of registry entry + * @param registry A key for the registry to get values from + * @param Command sender type + * @param Registry entry type + * @return Created builder + */ + public static CommandFunctionArgument.@NonNull Builder newBuilder( + final @NonNull String name, + final @NonNull Class type, + final @NonNull RegistryKey> registry) { + return new CommandFunctionArgument.Builder<>(registry, type, name); + } + + /** + * Create a new builder. + * + * @param name Name of the argument + * @param type The type of registry entry + * @param registry A key for the registry to get values from + * @param Command sender type + * @param Registry entry type + * @return Created builder + */ + public static CommandFunctionArgument.@NonNull Builder newBuilder( + final @NonNull String name, + final @NonNull TypeToken type, + final @NonNull RegistryKey> registry) { + return new CommandFunctionArgument.Builder<>(registry, type, name); + } + + /** + * Create a new required command argument. + * + * @param name Argument name + * @param type The type of registry entry + * @param registry A key for the registry to get values from + * @param Command sender type + * @param Registry entry type + * @return Created argument + */ + public static @NonNull CommandFunctionArgument of( + final @NonNull String name, + final @NonNull Class type, + final @NonNull RegistryKey> registry + ) { + return CommandFunctionArgument.newBuilder(name, type, registry).asRequired().build(); + } + + /** + * Create a new optional command argument + * + * @param name Argument name + * @param type The type of registry entry + * @param registry A key for the registry to get values from + * @param Command sender type + * @param Registry entry type + * @return Created argument + */ + public static @NonNull CommandFunctionArgument optional( + final @NonNull String name, + final @NonNull Class type, + final @NonNull RegistryKey> registry + ) { + return CommandFunctionArgument.newBuilder(name, type, registry).asOptional().build(); + } + + /** + * Create a new optional command argument with a default value + * + * @param name Argument name + * @param type The type of registry entry + * @param registry A key for the registry to get values from + * @param defaultValue Default value + * @param Command sender type + * @param Registry entry type + * @return Created argument + */ + public static @NonNull CommandFunctionArgument optional( + final @NonNull String name, + final @NonNull Class type, + final @NonNull RegistryKey> registry, + final @NonNull RegistryKey defaultValue + ) { + return CommandFunctionArgument.newBuilder(name, type, registry) + .asOptionalWithDefault(defaultValue.getValue().toString()) + .build(); + } + + /** + * A parser for values stored in a {@link Registry} + * + * @param Command sender type + * @param Registry entry type + */ + public static final class RegistryEntryParser implements ArgumentParser { + private final RegistryKey> registryIdent; + + /** + * Create a new parser for registry entries. + * + * @param registryIdent the registry identifier + */ + public RegistryEntryParser(final RegistryKey> registryIdent) { + this.registryIdent = requireNonNull(registryIdent, "registryIdent"); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull V> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue + ) { + final String possibleIdentifier = inputQueue.peek(); + if (possibleIdentifier == null) { + return ArgumentParseResult.failure(new NoInputProvidedException( + CommandFunctionArgument.class, + commandContext + )); + } + + final Identifier key; + try { + key = Identifier.fromCommandInput(new StringReader(possibleIdentifier)); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failure(ex); + } + inputQueue.poll(); + + final Registry registry = this.getRegistry(commandContext); + if (registry == null) { + return ArgumentParseResult.failure(new IllegalArgumentException("Unknown registry " + this.registryIdent)); + } + + final V entry = registry.get(key); + if (entry == null) { + return ArgumentParseResult.failure(new UnknownEntryException(commandContext, key, this.registryIdent)); + } + + return ArgumentParseResult.success(entry); + } + + @SuppressWarnings("unchecked") + Registry getRegistry(final CommandContext ctx) { + final CommandSource reverseMapped = ctx.get(FabricCommandContextKeys.NATIVE_COMMAND_SOURCE); + // First try dynamic registries (for things loaded from data-packs) + Registry registry = reverseMapped.getRegistryManager().getOptional(this.registryIdent).orElse(null); + if (registry == null) { + // And then static registries + registry = (Registry) Registry.REGISTRIES.get(this.registryIdent.getValue()); + } + return registry; + } + + @Override + public @NonNull List<@NonNull String> suggestions( + final @NonNull CommandContext commandContext, + final @NonNull String input + ) { + final Set ids = this.getRegistry(commandContext).getIds(); + final List results = new ArrayList<>(ids.size()); + for (final Identifier entry : ids) { + if (entry.getNamespace().equals(NAMESPACE_MINECRAFT)) { + results.add(entry.getPath()); + } + results.add(entry.toString()); + } + + return results; + } + + @Override + public boolean isContextFree() { + return true; + } + + /** + * Get the registry associated with this parser + * @return the registry + */ + public RegistryKey> getRegistry() { + return this.registryIdent; + } + + } + + /** + * A builder for registry entry arguments. + * + * @param The sender type + * @param The registry value type + */ + public static final class Builder extends TypedBuilder> { + + Builder( + final @NonNull String name + ) { + super(valueType, name); + } + + @Override + public @NonNull CommandFunctionArgument<@NonNull C, @NonNull V> build() { + return new CommandFunctionArgument<>( + this.isRequired(), + this.getName(), + this.getDefaultValue(), + this.getValueType(), + this.getSuggestionsProvider() + ); + } + } + + /** + * An exception thrown when an entry in a registry could not be found. + */ + private static final class UnknownEntryException extends ParserException { + + private static final long serialVersionUID = 7694424294461849903L; + + UnknownEntryException( + final CommandContext context, + final Identifier key, + final RegistryKey> registry + ) { + super( + CommandFunctionArgument.class, + context, + FabricCaptionKeys.ARGUMENT_PARSE_FAILURE_REGISTRY_ENTRY_UNKNOWN_ENTRY, + CaptionVariable.of("id", key.toString()), + CaptionVariable.of("registry", registry.toString()) + ); + } + + } + +} diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/server/package-info.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/server/package-info.java new file mode 100644 index 00000000..8fa7dc39 --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/argument/server/package-info.java @@ -0,0 +1,6 @@ +/** + * Command arguments that can only be used on the logical server. + * + * @since 1.4.0 + */ +package cloud.commandframework.fabric.argument.server; diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/MinecraftTime.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/MinecraftTime.java new file mode 100644 index 00000000..923e1dfb --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/MinecraftTime.java @@ -0,0 +1,123 @@ +package cloud.commandframework.fabric.data; + +import java.time.temporal.TemporalUnit; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.requireNonNull; + +/** + * An element of in-game time. + * + *

The basic unit is 1 tick, which aims to be {@code 50ms}

+ * + * @since 1.4.0 + */ +public final class MinecraftTime { + private static final MinecraftTime ZERO = new MinecraftTime(0); + + private final long ticks; + + /** + * Get the time instance for the specified number of ticks. + * + * @param ticks the number of ticks + * @return a time holder + */ + public static MinecraftTime of(final long ticks) { + return ticks == 0 ? ZERO : new MinecraftTime(ticks); + } + + /** + * Given an amount of time in another unit, create a game time holding the number of ticks expected to pass in that time. + * + * @param amount the amount of time + * @param unit the unit + * @return a time holder + */ + public static MinecraftTime of(final long amount, final TemporalUnit unit) { + requireNonNull(unit, "unit"); + return new MinecraftTime(Math.round(amount / 50d * unit.getDuration().toMillis())); + } + + /** + * Given an amount of time in another unit, create a game time holding the number of ticks expected to pass in that time. + * + * @param amount the amount of time + * @param unit the unit + * @return a time holder + */ + public static MinecraftTime of(final long amount, final TimeUnit unit) { + requireNonNull(unit, "unit"); + return amount == 0 ? ZERO : new MinecraftTime(TimeUnit.MILLISECONDS.convert(amount, unit) / 50); + } + + MinecraftTime(final long ticks) { + this.ticks = ticks; + } + + /** + * Get the number of in-game ticks represented by this time. + * + *

This time will be truncated to the maximum value of an integer. + * See {@link #getLongTicks()} for the full contents.

+ * + * @return the time in ticks + */ + public int getTicks() { + return (int) this.ticks; + } + + /** + * Get the number of in-game ticks represented by this time. + * + * @return the time in ticks + */ + public long getLongTicks() { + return this.ticks; + } + + /** + * Convert this to another time unit. + * + * @param unit the target unit + * @return the target duration, as represented by the provided unit + */ + public long convertTo(final TemporalUnit unit) { + return this.ticks * 50 / unit.getDuration().toMillis(); + } + + /** + * Convert this to another time unit. + * + * @param unit the target unit + * @return the target duration, as represented by the provided unit + */ + public long convertTo(final TimeUnit unit) { + return unit.convert(this.ticks * 50, TimeUnit.MILLISECONDS); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + + if (other == null || this.getClass() != other.getClass()) { + return false; + } + + return this.ticks == ((MinecraftTime) other).ticks; + } + + @Override + public int hashCode() { + return Objects.hash(this.ticks); + } + + @Override + public String toString() { + return Long.toString(this.ticks); + } + +} diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/Selector.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/Selector.java new file mode 100644 index 00000000..b2e5fc38 --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/Selector.java @@ -0,0 +1,48 @@ +package cloud.commandframework.fabric.data; + +import net.minecraft.command.EntitySelector; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Collection; + +/** + * A selector string to query multiple entity-like values + * + * @param Value type + */ +public interface Selector { + + /** + * Get the raw string associated with the selector. + * + * @return the input + */ + String getInput(); + + /** + * If this value came from a parsed selector, this will provide the details of that selector. + * + * @return the selector + */ + @Nullable EntitySelector getSelector(); + + /** + * Resolve the value of this selector. + * + *

A successfully parsed selector must match one or more values

+ * + * @return all matched entities + */ + Collection get(); + + /** + * A specialized selector that can only return one value. + * + * @param the value type + */ + interface Single extends Selector { + + V getSingle(); + } + +} diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/package-info.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/package-info.java new file mode 100644 index 00000000..04190315 --- /dev/null +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/data/package-info.java @@ -0,0 +1,4 @@ +/** + * Data holders for representing Vanilla argument type values. + */ +package cloud.commandframework.fabric.data;