fabric: Implement FabricClientCommandManager

This commit is contained in:
jmp 2021-03-10 15:00:07 -08:00 committed by Jason
parent f16cafda3f
commit 31d1f85830
31 changed files with 275 additions and 66 deletions

View file

@ -0,0 +1,114 @@
//
// 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.
//
package cloud.commandframework.fabric;
import cloud.commandframework.CommandTree;
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator;
import cloud.commandframework.execution.CommandExecutionCoordinator;
import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientCommandSource;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.function.Function;
/**
* A command manager for registering client-side commands.
*
* <p>All commands should be registered within mod initializers. Any registrations occurring after the first call to
* {@link CommandRegistrationCallback} will be considered <em>unsafe</em>, and will only be permitted when the unsafe
* registration manager option is enabled.</p>
*
* @param <C> the command sender type
* @since 1.5.0
*/
public final class FabricClientCommandManager<C> extends FabricCommandManager<C, FabricClientCommandSource> {
/**
* Create a command manager using native source types.
*
* @param execCoordinator Execution coordinator instance.
* @return a new command manager
* @see #FabricClientCommandManager(Function, Function, Function) for a more thorough explanation
*/
public static FabricClientCommandManager<FabricClientCommandSource> createNative(
final Function<CommandTree<FabricClientCommandSource>, CommandExecutionCoordinator<FabricClientCommandSource>> execCoordinator
) {
return new FabricClientCommandManager<>(execCoordinator, Function.identity(), Function.identity());
}
/**
* Create a new command manager instance.
*
* @param commandExecutionCoordinator Execution coordinator instance. The coordinator is in charge of executing incoming
* commands. Some considerations must be made when picking a suitable execution coordinator
* for your platform. For example, an entirely asynchronous coordinator is not suitable
* when the parsers used in that particular platform are not thread safe. If you have
* commands that perform blocking operations, however, it might not be a good idea to
* use a synchronous execution coordinator. In most cases you will want to pick between
* {@link CommandExecutionCoordinator#simpleCoordinator()} and
* {@link AsynchronousCommandExecutionCoordinator}
* @param commandSourceMapper Function that maps {@link FabricClientCommandSource} to the command sender type
* @param backwardsCommandSourceMapper Function that maps the command sender type to {@link FabricClientCommandSource}
*/
@SuppressWarnings("unchecked")
public FabricClientCommandManager(
final @NonNull Function<@NonNull CommandTree<C>, @NonNull CommandExecutionCoordinator<C>> commandExecutionCoordinator,
final Function<FabricClientCommandSource, C> commandSourceMapper,
final Function<C, FabricClientCommandSource> backwardsCommandSourceMapper
) {
super(
commandExecutionCoordinator,
commandSourceMapper,
backwardsCommandSourceMapper,
new FabricCommandRegistrationHandler.Client<>(),
() -> (FabricClientCommandSource) new ClientCommandSource(
MinecraftClient.getInstance().getNetworkHandler(),
MinecraftClient.getInstance()
)
);
this.registerParsers();
}
private void registerParsers() {
}
/**
* Check if a sender has a certain permission.
*
* <p>The implementation for client commands always returns true.</p>
*
* @param sender Command sender
* @param permission Permission node
* @return whether the sender has the specified permission
*/
@Override
public boolean hasPermission(@NonNull final C sender, @NonNull final String permission) {
return true;
}
}

View file

@ -74,7 +74,6 @@ 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;
import net.minecraft.util.math.Direction;
@ -107,7 +106,7 @@ import java.util.function.Supplier;
* @param <C> the manager's sender type
* @param <S> the platform sender type
* @see FabricServerCommandManager for server commands
* @since 1.4.0
* @since 1.5.0
*/
public abstract class FabricCommandManager<C, S extends CommandSource> extends CommandManager<C> implements BrigadierManagerHolder<C> {
private static final Logger LOGGER = LogManager.getLogger();
@ -129,8 +128,8 @@ public abstract class FabricCommandManager<C, S extends CommandSource> extends C
* use a synchronous execution coordinator. In most cases you will want to pick between
* {@link CommandExecutionCoordinator#simpleCoordinator()} and
* {@link AsynchronousCommandExecutionCoordinator}
* @param commandSourceMapper Function that maps {@link ServerCommandSource} to the command sender type
* @param backwardsCommandSourceMapper Function that maps the command sender type to {@link ServerCommandSource}
* @param commandSourceMapper Function that maps {@link CommandSource} to the command sender type
* @param backwardsCommandSourceMapper Function that maps the command sender type to {@link CommandSource}
* @param registrationHandler the handler accepting command registrations
* @param dummyCommandSourceProvider a provider of a dummy command source, for use with brigadier registration
*/
@ -323,7 +322,7 @@ public abstract class FabricCommandManager<C, S extends CommandSource> extends C
}
/**
* Gets the mapper from a game {@link ServerCommandSource} to the manager's {@code C} type.
* Gets the mapper from a game {@link CommandSource} to the manager's {@code C} type.
*
* @return Command source mapper
*/
@ -332,7 +331,7 @@ public abstract class FabricCommandManager<C, S extends CommandSource> extends C
}
/**
* Gets the mapper from the manager's {@code C} type to a game {@link ServerCommandSource}.
* Gets the mapper from the manager's {@code C} type to a game {@link CommandSource}.
*
* @return Command source mapper
*/

View file

@ -32,6 +32,8 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import net.fabricmc.fabric.api.client.command.v1.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.minecraft.command.CommandSource;
import net.minecraft.server.command.CommandManager.RegistrationEnvironment;
@ -61,6 +63,80 @@ abstract class FabricCommandRegistrationHandler<C, S extends CommandSource> impl
return this.commandManager;
}
/**
* Returns a literal node that redirects its execution to
* the given destination node.
*
* <p>This method is taken from MIT licensed code in the Velocity project, see
* <a href="https://github.com/VelocityPowered/Velocity/blob/b88c573eb11839a95bea1af947b0c59a5956368b/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java#L33">
* Velocity's BrigadierUtils class</a></p>
*
* @param alias the command alias
* @param destination the destination node
* @param <S> brig sender type
* @return the built node
*/
private static <S> LiteralCommandNode<S> buildRedirect(
final @NonNull String alias,
final @NonNull CommandNode<S> destination
) {
// Redirects only work for nodes with children, but break the top argument-less command.
// Manually adding the root command after setting the redirect doesn't fix it.
// (See https://github.com/Mojang/brigadier/issues/46) Manually clone the node instead.
LiteralArgumentBuilder<S> builder = LiteralArgumentBuilder
.<S>literal(alias)
.requires(destination.getRequirement())
.forward(
destination.getRedirect(),
destination.getRedirectModifier(),
destination.isFork()
)
.executes(destination.getCommand());
for (final CommandNode<S> child : destination.getChildren()) {
builder.then(child);
}
return builder.build();
}
static class Client<C> extends FabricCommandRegistrationHandler<C, FabricClientCommandSource> {
@Override
void initialize(final FabricCommandManager<C, FabricClientCommandSource> manager) {
super.initialize(manager);
}
@Override
@SuppressWarnings("unchecked")
public boolean registerCommand(final @NonNull Command<?> cmd) {
final Command<C> command = (Command<C>) cmd;
final RootCommandNode<FabricClientCommandSource> rootNode = ClientCommandManager.DISPATCHER.getRoot();
final StaticArgument<C> first = ((StaticArgument<C>) command.getArguments().get(0));
final CommandNode<FabricClientCommandSource> baseNode = this
.getCommandManager()
.brigadierManager()
.createLiteralCommandNode(
first.getName(),
command,
(src, perm) -> this.getCommandManager().hasPermission(
this.getCommandManager().getCommandSourceMapper().apply(src),
perm
),
true,
new FabricExecutor<>(
this.getCommandManager(),
source -> source.getPlayer().getName().asString(),
FabricClientCommandSource::sendError
)
);
rootNode.addChild(baseNode);
for (final String alias : first.getAlternativeAliases()) {
rootNode.addChild(buildRedirect(alias, baseNode));
}
return true;
}
}
static class Server<C> extends FabricCommandRegistrationHandler<C, ServerCommandSource> {
private final Set<Command<C>> registeredCommands = ConcurrentHashMap.newKeySet();
@ -112,40 +188,5 @@ abstract class FabricCommandRegistrationHandler<C, S extends CommandSource> impl
dispatcher.addChild(buildRedirect(alias, baseNode));
}
}
/**
* Returns a literal node that redirects its execution to
* the given destination node.
*
* <p>This method is taken from MIT licensed code in the Velocity project, see
* <a href="https://github.com/VelocityPowered/Velocity/blob/b88c573eb11839a95bea1af947b0c59a5956368b/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java#L33">
* Velocity's BrigadierUtils class</a></p>
*
* @param alias the command alias
* @param destination the destination node
* @return the built node
*/
private static <S> LiteralCommandNode<S> buildRedirect(
final @NonNull String alias,
final @NonNull CommandNode<S> destination
) {
// Redirects only work for nodes with children, but break the top argument-less command.
// Manually adding the root command after setting the redirect doesn't fix it.
// (See https://github.com/Mojang/brigadier/issues/46) Manually clone the node instead.
LiteralArgumentBuilder<S> builder = LiteralArgumentBuilder
.<S>literal(alias)
.requires(destination.getRequirement())
.forward(
destination.getRedirect(),
destination.getRedirectModifier(),
destination.isFork()
)
.executes(destination.getCommand());
for (final CommandNode<S> child : destination.getChildren()) {
builder.then(child);
}
return builder.build();
}
}
}

View file

@ -104,7 +104,7 @@ final class FabricExecutor<C, S extends CommandSource> implements Command<S> {
this.sendError.accept(
source,
new LiteralText("Invalid Command Syntax. Correct command syntax is: ")
.append(new LiteralText(e.getCorrectSyntax())
.append(new LiteralText(String.format("/%s", e.getCorrectSyntax()))
.styled(style -> style.withColor(Formatting.GRAY))))
);
} else if (throwable instanceof InvalidCommandSenderException) {

View file

@ -52,7 +52,7 @@ import java.util.function.Function;
* registration manager option is enabled.</p>
*
* @param <C> the command sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class FabricServerCommandManager<C> extends FabricCommandManager<C, ServerCommandSource> {

View file

@ -38,7 +38,7 @@ import java.util.function.BiFunction;
* An argument for an angle, specified in degrees.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class AngleArgument<C> extends CommandArgument<C, AngleArgumentType.Angle> {

View file

@ -42,7 +42,7 @@ import java.util.function.BiFunction;
* An argument for a set of axes, described in Vanilla as a "swizzle".
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class AxisArgument<C> extends CommandArgument<C, EnumSet<Direction.Axis>> {
private static final TypeToken<EnumSet<Direction.Axis>> TYPE = new TypeToken<EnumSet<Direction.Axis>>() {};

View file

@ -39,7 +39,7 @@ import java.util.function.BiFunction;
* An argument for named colors in the {@link Formatting} enum.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class ColorArgument<C> extends CommandArgument<C, Formatting> {

View file

@ -39,7 +39,7 @@ import java.util.function.BiFunction;
* An argument for the string representation of an NBT {@link CompoundTag}.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class CompoundTagArgument<C> extends CommandArgument<C, CompoundTag> {

View file

@ -38,7 +38,7 @@ import java.util.function.BiFunction;
* An argument parsing an entity anchor.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class EntityAnchorArgument<C> extends CommandArgument<C, EntityAnchorArgumentType.EntityAnchor> {

View file

@ -50,7 +50,7 @@ import java.util.HashSet;
/**
* Parsers for Vanilla command argument types.
*
* @since 1.4.0
* @since 1.5.0
*/
public final class FabricArgumentParsers {
@ -68,6 +68,7 @@ public final class FabricArgumentParsers {
.map((ctx, val) -> ArgumentParseResult.success(MinecraftTime.of(val)));
}
/*
public static <C> ArgumentParser<C, CommandFunction> commandFunction() {
// TODO: Should probably write our own parser for this, it's either Identifier or tag.
// Server parsers
@ -76,6 +77,7 @@ public final class FabricArgumentParsers {
source.getCompletions()
})
}
*/
public static <C> ArgumentParser<C, Message> message() {
return new WrappedBrigadierParser<C, MessageArgumentType.MessageFormat>(MessageArgumentType.message())

View file

@ -40,7 +40,7 @@ import java.util.function.BiFunction;
* optional.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class FloatRangeArgument<C> extends CommandArgument<C, NumberRange.FloatRange> {

View file

@ -39,7 +39,7 @@ import java.util.function.BiFunction;
* An argument parsing an identifier, or "resource location".
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class IdentifierArgument<C> extends CommandArgument<C, Identifier> {

View file

@ -40,7 +40,7 @@ import java.util.function.BiFunction;
* optional.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class IntRangeArgument<C> extends CommandArgument<C, NumberRange.IntRange> {

View file

@ -41,7 +41,7 @@ import java.util.function.BiFunction;
* An argument parsing an item identifier and optional NBT data
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class ItemDataArgument<C> extends CommandArgument<C, ItemStackArgument> {

View file

@ -38,7 +38,7 @@ import java.util.function.BiFunction;
* An argument for NBT paths to locations within {@link net.minecraft.nbt.Tag Tags}.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class NbtPathArgument<C> extends CommandArgument<C, NbtPathArgumentType.NbtPath> {

View file

@ -39,7 +39,7 @@ import java.util.function.BiFunction;
* An argument for the string representation of an NBT {@link Tag}.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class NbtTagArgument<C> extends CommandArgument<C, Tag> {

View file

@ -41,7 +41,7 @@ import java.util.function.BiFunction;
* <p>These operations can be used to compare scores on a {@link net.minecraft.scoreboard.Scoreboard}.</p>
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class ParticleEffectArgument<C> extends CommandArgument<C, ParticleEffect> {

View file

@ -58,7 +58,7 @@ import static java.util.Objects.requireNonNull;
*
* @param <C> the command sender type
* @param <V> the registry entry type
* @since 1.4.0
* @since 1.5.0
*/
public class RegistryEntryArgument<C, V> extends CommandArgument<C, V> {
private static final String NAMESPACE_MINECRAFT = "minecraft";

View file

@ -39,7 +39,7 @@ import java.util.function.BiFunction;
* An argument for a {@linkplain ScoreboardCriterion criterion} in a scoreboard.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class ScoreboardCriterionArgument<C> extends CommandArgument<C, ScoreboardCriterion> {

View file

@ -41,7 +41,7 @@ import java.util.function.BiFunction;
* <p>These operations can be used to compare scores on a {@link net.minecraft.scoreboard.Scoreboard}.</p>
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class ScoreboardOperationArgument<C> extends CommandArgument<C, Operation> {

View file

@ -40,7 +40,7 @@ import java.util.function.BiFunction;
* An argument parsing a status effect from the {@link net.minecraft.util.registry.Registry#STATUS_EFFECT status effect registry}
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class StatusEffectArgument<C> extends CommandArgument<C, StatusEffect> {

View file

@ -49,7 +49,7 @@ import java.util.function.BiFunction;
* An argument parsing an entity anchor.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class TeamArgument<C> extends CommandArgument<C, Team> {
@ -182,6 +182,7 @@ public final class TeamArgument<C> extends CommandArgument<C, Team> {
}
public static final class UnknownTeamException extends ParserException {
private static final long serialVersionUID = 4249139487412603424L;
UnknownTeamException(
final @NonNull CommandContext<?> context,

View file

@ -40,7 +40,7 @@ import java.util.function.BiFunction;
* An argument for in-game time
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class TimeArgument<C> extends CommandArgument<C, MinecraftTime> {

View file

@ -52,7 +52,7 @@ import java.util.function.BiFunction;
* An argument similar to a greedy string, but one that resolves selectors.
*
* @param <C> the sender type
* @since 1.4.0
* @since 1.5.0
*/
public final class MessageArgument<C> extends CommandArgument<C, Message> {

View file

@ -25,6 +25,6 @@
/**
* Command arguments that can only be used on the logical server.
*
* @since 1.4.0
* @since 1.5.0
*/
package cloud.commandframework.fabric.argument.server;

View file

@ -35,7 +35,7 @@ import static java.util.Objects.requireNonNull;
*
* <p>The basic unit is 1 <em>tick</em>, which aims to be {@code 50ms}</p>
*
* @since 1.4.0
* @since 1.5.0
*/
public final class MinecraftTime {
private static final MinecraftTime ZERO = new MinecraftTime(0);

View file

@ -0,0 +1,48 @@
//
// 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.
//
package cloud.commandframework.fabric.testmod;
import cloud.commandframework.arguments.standard.StringArgument;
import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.fabric.FabricClientCommandManager;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource;
import net.minecraft.text.Text;
public class FabricClientExample implements ClientModInitializer {
@Override
public void onInitializeClient() {
final FabricClientCommandManager<FabricClientCommandSource> commandManager =
FabricClientCommandManager.createNative(CommandExecutionCoordinator.simpleCoordinator());
commandManager.command(
commandManager.commandBuilder("cloud_client")
.literal("say")
.argument(StringArgument.greedy("message"))
.handler(ctx -> ctx.getSender().sendFeedback(
Text.of("Cloud client commands says: " + ctx.get("message"))
))
);
}
}

View file

@ -16,6 +16,9 @@
"entrypoints": {
"main": [
"cloud.commandframework.fabric.testmod.FabricExample"
],
"client": [
"cloud.commandframework.fabric.testmod.FabricClientExample"
]
},