Client-sided commands ()

* Move command API icon to correct location

* Add client-sided command API

* Add jd note about threads

* Add license headers

* ArgumentBuilders => ClientArgumentBuilders

* Yeet custom prefixes

* Migrate testmod to lifecycle-events-v1

* Improve client command test

* Make client command test more similar to the server one

* Update to new yarn names

* Add handling for requires() in command suggestions

* Remove outdated TODO

* Playerification

* Clarify comments in ClientCommandInternals

* Use "s" instead of "it"

* Improve CommandSyntaxException logging

* Add missing import

* Add /fcc help command

* Add comments about server-client precedence rules

* Add missing license header

* Add /fabric-command-api-v1:client as an alias for /fcc
This commit is contained in:
Juuxel 2021-02-23 20:20:49 +02:00 committed by GitHub
parent 9354966b6c
commit 871300cf73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 778 additions and 1 deletions

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.client.command.v1;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
/**
* Manages client-sided commands and provides some related helper methods.
*
* <p>Client-sided commands are fully executed on the client,
* so players can use them in both singleplayer and multiplayer.
*
* <p>Registrations can be done in the {@link #DISPATCHER} during a {@link net.fabricmc.api.ClientModInitializer}'s
* initialization. (See example below.)
*
* <p>The commands are run on the client game thread by default.
* Avoid doing any heavy calculations here as that can freeze the game's rendering.
* For example, you can move heavy code to another thread.
*
* <p>This class also has alternatives to the server-side helper methods in
* {@link net.minecraft.server.command.CommandManager}:
* {@link #literal(String)} and {@link #argument(String, ArgumentType)}.
*
* <p>The precedence rules of client-sided and server-sided commands with the same name
* are an implementation detail that is not guaranteed to remain the same in future versions.
* The aim is to make commands from the server take precedence over client-sided commands
* in a future version of this API.
*
* <h2>Example command</h2>
* <pre>
* {@code
* ClientCommandManager.DISPATCHER.register(
* ClientCommandManager.literal("hello").executes(context -> {
* context.getSource().sendFeedback(new LiteralText("Hello, world!"));
* return 0;
* })
* );
* }
* </pre>
*/
@Environment(EnvType.CLIENT)
public final class ClientCommandManager {
/**
* The command dispatcher that handles client command registration and execution.
*/
public static final CommandDispatcher<FabricClientCommandSource> DISPATCHER = new CommandDispatcher<>();
private ClientCommandManager() {
}
/**
* Creates a literal argument builder.
*
* @param name the literal name
* @return the created argument builder
*/
public static LiteralArgumentBuilder<FabricClientCommandSource> literal(String name) {
return LiteralArgumentBuilder.literal(name);
}
/**
* Creates a required argument builder.
*
* @param name the name of the argument
* @param type the type of the argument
* @param <T> the type of the parsed argument value
* @return the created argument builder
*/
public static <T> RequiredArgumentBuilder<FabricClientCommandSource, T> argument(String name, ArgumentType<T> type) {
return RequiredArgumentBuilder.argument(name, type);
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.client.command.v1;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.command.CommandSource;
import net.minecraft.text.Text;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
/**
* Extensions to {@link CommandSource} for client-sided commands.
*/
@Environment(EnvType.CLIENT)
public interface FabricClientCommandSource extends CommandSource {
/**
* Sends a feedback message to the player.
*
* @param message the feedback message
*/
void sendFeedback(Text message);
/**
* Sends an error message to the player.
*
* @param message the error message
*/
void sendError(Text message);
/**
* Gets the client instance used to run the command.
*
* @return the client
*/
MinecraftClient getClient();
/**
* Gets the player that used the command.
*
* @return the player
*/
ClientPlayerEntity getPlayer();
/**
* Gets the world where the player used the command.
*
* @return the world
*/
ClientWorld getWorld();
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* API for creating client-sided commands.
*
* @see net.fabricmc.fabric.api.client.command.v1.ClientCommandManager
*/
package net.fabricmc.fabric.api.client.command.v1;

View file

@ -0,0 +1,234 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.command.client;
import static net.fabricmc.fabric.api.client.command.v1.ClientCommandManager.DISPATCHER;
import static net.fabricmc.fabric.api.client.command.v1.ClientCommandManager.argument;
import static net.fabricmc.fabric.api.client.command.v1.ClientCommandManager.literal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.mojang.brigadier.AmbiguityConsumer;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.ParsedCommandNode;
import com.mojang.brigadier.exceptions.BuiltInExceptionProvider;
import com.mojang.brigadier.exceptions.CommandExceptionType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.tree.CommandNode;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.minecraft.client.MinecraftClient;
import net.minecraft.command.CommandException;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import net.minecraft.text.Texts;
import net.minecraft.text.TranslatableText;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource;
import net.fabricmc.fabric.mixin.command.HelpCommandAccessor;
@Environment(EnvType.CLIENT)
public final class ClientCommandInternals {
private static final Logger LOGGER = LogManager.getLogger();
private static final char PREFIX = '/';
private static final String API_COMMAND_NAME = "fabric-command-api-v1:client";
private static final String SHORT_API_COMMAND_NAME = "fcc";
/**
* Executes a client-sided command from a message.
*
* @param message the command message
* @return true if the message should not be sent to the server, false otherwise
*/
public static boolean executeCommand(String message) {
if (message.isEmpty()) {
return false; // Nothing to process
}
if (message.charAt(0) != PREFIX) {
return false; // Incorrect prefix, won't execute anything.
}
MinecraftClient client = MinecraftClient.getInstance();
// The interface is implemented on ClientCommandSource with a mixin.
// noinspection ConstantConditions
FabricClientCommandSource commandSource = (FabricClientCommandSource) client.getNetworkHandler().getCommandSource();
client.getProfiler().push(message);
try {
// TODO: Check for server commands before executing.
// This requires parsing the command, checking if they match a server command
// and then executing the command with the parse results.
DISPATCHER.execute(message.substring(1), commandSource);
return true;
} catch (CommandSyntaxException e) {
boolean ignored = isIgnoredException(e.getType());
LOGGER.log(ignored ? Level.DEBUG : Level.WARN, "Syntax exception for client-sided command '{}'", message, e);
if (ignored) {
return false;
}
commandSource.sendError(getErrorMessage(e));
return true;
} catch (CommandException e) {
LOGGER.warn("Error while executing client-sided command '{}'", message, e);
commandSource.sendError(e.getTextMessage());
return true;
} catch (RuntimeException e) {
LOGGER.warn("Error while executing client-sided command '{}'", message, e);
commandSource.sendError(Text.of(e.getMessage()));
return true;
} finally {
client.getProfiler().pop();
}
}
/**
* Tests whether a command syntax exception with the type
* should be ignored and the message sent to the server.
*
* @param type the exception type
* @return true if ignored, false otherwise
*/
private static boolean isIgnoredException(CommandExceptionType type) {
BuiltInExceptionProvider builtins = CommandSyntaxException.BUILT_IN_EXCEPTIONS;
// Only ignore unknown commands and node parse exceptions.
// The argument-related dispatcher exceptions are not ignored because
// they will only happen if the user enters a correct command.
return type == builtins.dispatcherUnknownCommand() || type == builtins.dispatcherParseException();
}
// See CommandSuggestor.method_30505. That cannot be used directly as it returns an OrderedText instead of a Text.
private static Text getErrorMessage(CommandSyntaxException e) {
Text message = Texts.toText(e.getRawMessage());
String context = e.getContext();
return context != null ? new TranslatableText("command.context.parse_error", message, context) : message;
}
/**
* Runs final initialization tasks such as {@link CommandDispatcher#findAmbiguities(AmbiguityConsumer)}
* on the command dispatcher. Also registers a {@code /fcc help} command if there are other commands present.
*/
public static void finalizeInit() {
if (!DISPATCHER.getRoot().getChildren().isEmpty()) {
// Register an API command if there are other commands;
// these helpers are not needed if there are no client commands
LiteralArgumentBuilder<FabricClientCommandSource> help = literal("help");
help.executes(ClientCommandInternals::executeRootHelp);
help.then(argument("command", StringArgumentType.greedyString()).executes(ClientCommandInternals::executeArgumentHelp));
CommandNode<FabricClientCommandSource> mainNode = DISPATCHER.register(literal(API_COMMAND_NAME).then(help));
DISPATCHER.register(literal(SHORT_API_COMMAND_NAME).redirect(mainNode));
}
// noinspection CodeBlock2Expr
DISPATCHER.findAmbiguities((parent, child, sibling, inputs) -> {
LOGGER.warn("Ambiguity between arguments {} and {} with inputs: {}", DISPATCHER.getPath(child), DISPATCHER.getPath(sibling), inputs);
});
}
private static int executeRootHelp(CommandContext<FabricClientCommandSource> context) {
return executeHelp(DISPATCHER.getRoot(), context);
}
private static int executeArgumentHelp(CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
ParseResults<FabricClientCommandSource> parseResults = DISPATCHER.parse(StringArgumentType.getString(context, "command"), context.getSource());
List<ParsedCommandNode<FabricClientCommandSource>> nodes = parseResults.getContext().getNodes();
if (nodes.isEmpty()) {
throw HelpCommandAccessor.getFailedException().create();
}
return executeHelp(Iterables.getLast(nodes).getNode(), context);
}
private static int executeHelp(CommandNode<FabricClientCommandSource> startNode, CommandContext<FabricClientCommandSource> context) {
Map<CommandNode<FabricClientCommandSource>, String> commands = DISPATCHER.getSmartUsage(startNode, context.getSource());
for (String command : commands.values()) {
context.getSource().sendFeedback(new LiteralText("/" + command));
}
return commands.size();
}
public static void addCommands(CommandDispatcher<FabricClientCommandSource> target, FabricClientCommandSource source) {
Map<CommandNode<FabricClientCommandSource>, CommandNode<FabricClientCommandSource>> originalToCopy = new HashMap<>();
originalToCopy.put(DISPATCHER.getRoot(), target.getRoot());
copyChildren(DISPATCHER.getRoot(), target.getRoot(), source, originalToCopy);
}
/**
* Copies the child commands from origin to target, filtered by {@code child.canUse(source)}.
* Mimics vanilla's CommandManager.makeTreeForSource.
*
* @param origin the source command node
* @param target the target command node
* @param source the command source
* @param originalToCopy a mutable map from original command nodes to their copies, used for redirects;
* should contain a mapping from origin to target
*/
private static void copyChildren(
CommandNode<FabricClientCommandSource> origin,
CommandNode<FabricClientCommandSource> target,
FabricClientCommandSource source,
Map<CommandNode<FabricClientCommandSource>, CommandNode<FabricClientCommandSource>> originalToCopy
) {
for (CommandNode<FabricClientCommandSource> child : origin.getChildren()) {
if (!child.canUse(source)) continue;
ArgumentBuilder<FabricClientCommandSource, ?> builder = child.createBuilder();
// Reset the unnecessary non-completion stuff from the builder
builder.requires(s -> true); // This is checked with the if check above.
if (builder.getCommand() != null) {
builder.executes(context -> 0);
}
// Set up redirects
if (builder.getRedirect() != null) {
builder.redirect(originalToCopy.get(builder.getRedirect()));
}
CommandNode<FabricClientCommandSource> result = builder.build();
originalToCopy.put(child, result);
target.addChild(result);
if (!child.getChildren().isEmpty()) {
copyChildren(child, result, source, originalToCopy);
}
}
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.command;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import net.minecraft.server.command.HelpCommand;
@Mixin(HelpCommand.class)
public interface HelpCommandAccessor {
@Accessor("FAILED_EXCEPTION")
static SimpleCommandExceptionType getFailedException() {
throw new AssertionError("mixin");
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.command.client;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.network.MessageType;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Util;
import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource;
@Mixin(ClientCommandSource.class)
abstract class ClientCommandSourceMixin implements FabricClientCommandSource {
@Shadow
@Final
private MinecraftClient client;
@Override
public void sendFeedback(Text message) {
client.inGameHud.addChatMessage(MessageType.SYSTEM, message, Util.NIL_UUID);
}
@Override
public void sendError(Text message) {
client.inGameHud.addChatMessage(MessageType.SYSTEM, message.copy().formatted(Formatting.RED), Util.NIL_UUID);
}
@Override
public MinecraftClient getClient() {
return client;
}
@Override
public ClientPlayerEntity getPlayer() {
return client.player;
}
@Override
public ClientWorld getWorld() {
return client.world;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.command.client;
import com.mojang.brigadier.CommandDispatcher;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.command.CommandSource;
import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket;
import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource;
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
@Mixin(ClientPlayNetworkHandler.class)
abstract class ClientPlayNetworkHandlerMixin {
@Shadow
private CommandDispatcher<CommandSource> commandDispatcher;
@Shadow
@Final
private ClientCommandSource commandSource;
@SuppressWarnings({"unchecked", "rawtypes"})
@Inject(method = "onCommandTree", at = @At("RETURN"))
private void onOnCommandTree(CommandTreeS2CPacket packet, CallbackInfo info) {
// Add the commands to the vanilla dispatcher for completion.
// It's done here because both the server and the client commands have
// to be in the same dispatcher and completion results.
ClientCommandInternals.addCommands((CommandDispatcher) commandDispatcher, (FabricClientCommandSource) commandSource);
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.command.client;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.ClientPlayerEntity;
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
@Mixin(ClientPlayerEntity.class)
abstract class ClientPlayerEntityMixin {
@Inject(method = "sendChatMessage", at = @At("HEAD"), cancellable = true)
private void onSendChatMessage(String message, CallbackInfo info) {
if (ClientCommandInternals.executeCommand(message)) {
info.cancel();
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.command.client;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.RunArgs;
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
@Mixin(MinecraftClient.class)
abstract class MinecraftClientMixin {
@Inject(method = "<init>", at = @At("RETURN"))
private void onConstruct(RunArgs args, CallbackInfo info) {
ClientCommandInternals.finalizeInit();
}
}

View file

@ -3,9 +3,14 @@
"package": "net.fabricmc.fabric.mixin.command",
"compatibilityLevel": "JAVA_8",
"mixins": [
"CommandManagerMixin"
"CommandManagerMixin",
"HelpCommandAccessor"
],
"client": [
"client.ClientCommandSourceMixin",
"client.ClientPlayerEntityMixin",
"client.ClientPlayNetworkHandlerMixin",
"client.MinecraftClientMixin"
],
"injectors": {
"defaultRequire": 1

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.command.client;
import com.mojang.brigadier.arguments.DoubleArgumentType;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.text.LiteralText;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.command.v1.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
@Environment(EnvType.CLIENT)
public final class ClientCommandTest implements ClientModInitializer {
private static final Logger LOGGER = LogManager.getLogger();
private static final DynamicCommandExceptionType IS_NULL = new DynamicCommandExceptionType(x -> new LiteralText("The " + x + " is null"));
private static final SimpleCommandExceptionType UNEXECUTABLE_EXECUTED = new SimpleCommandExceptionType(new LiteralText("Executed an unexecutable command!"));
private boolean wasTested = false;
@Override
public void onInitializeClient() {
ClientCommandManager.DISPATCHER.register(ClientCommandManager.literal("test_client_command").executes(context -> {
context.getSource().sendFeedback(new LiteralText("This is a client command!"));
if (context.getSource().getClient() == null) {
throw IS_NULL.create("client");
}
if (context.getSource().getWorld() == null) {
throw IS_NULL.create("world");
}
if (context.getSource().getPlayer() == null) {
throw IS_NULL.create("player");
}
return 0;
}));
// Command with argument
ClientCommandManager.DISPATCHER.register(ClientCommandManager.literal("test_client_command_with_arg").then(
ClientCommandManager.argument("number", DoubleArgumentType.doubleArg()).executes(context -> {
double number = DoubleArgumentType.getDouble(context, "number");
// Test error formatting
context.getSource().sendError(new LiteralText("Your number is " + number));
return 0;
})
));
// Unexecutable command
ClientCommandManager.DISPATCHER.register(ClientCommandManager.literal("hidden_client_command").requires(source -> false).executes(context -> {
throw UNEXECUTABLE_EXECUTED.create();
}));
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
RootCommandNode<FabricClientCommandSource> rootNode = ClientCommandManager.DISPATCHER.getRoot();
// We climb the tree again
CommandNode<FabricClientCommandSource> testClientCommand = rootNode.getChild("test_client_command");
CommandNode<FabricClientCommandSource> testClientCommandWithArg = rootNode.getChild("test_client_command_with_arg");
CommandNode<FabricClientCommandSource> hiddenClientCommand = rootNode.getChild("hidden_client_command");
if (testClientCommand == null) {
throw new AssertionError("Expected to find 'test_client_command' on the client command dispatcher. But it was not found.");
}
if (testClientCommandWithArg == null) {
throw new AssertionError("Expected to find 'test_client_command_with_arg' on the client command dispatcher. But it was not found.");
}
if (hiddenClientCommand == null) {
throw new AssertionError("Expected to find 'hidden_client_command' on the client command dispatcher. But it was not found.");
}
CommandNode<FabricClientCommandSource> numberArg = testClientCommandWithArg.getChild("number");
if (numberArg == null) {
throw new AssertionError("Expected to find 'number' as a child of 'test_client_command_with_arg' on the client command dispatcher. But it was not found.");
}
LOGGER.info("The client command tests have passed! Please make sure you execute the two commands for extra safety.");
});
ClientTickEvents.START_WORLD_TICK.register(world -> {
if (wasTested) {
return;
}
MinecraftClient client = MinecraftClient.getInstance();
ClientCommandSource commandSource = client.getNetworkHandler().getCommandSource();
RootCommandNode<FabricClientCommandSource> rootNode = ClientCommandManager.DISPATCHER.getRoot();
CommandNode<FabricClientCommandSource> hiddenClientCommand = rootNode.getChild("hidden_client_command");
if (!(commandSource instanceof FabricClientCommandSource)) {
throw new AssertionError("Client command source not a FabricClientCommandSource!");
}
if (hiddenClientCommand.canUse((FabricClientCommandSource) commandSource)) {
throw new AssertionError("'hidden_client_command' should not be usable.");
}
LOGGER.info("The in-world client command tests have passed!");
wasTested = true;
});
}
}

View file

@ -11,6 +11,9 @@
"entrypoints": {
"main": [
"net.fabricmc.fabric.test.command.CommandTest"
],
"client": [
"net.fabricmc.fabric.test.command.client.ClientCommandTest"
]
}
}