mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-21 03:10:54 -04:00
Client-sided commands (#1115)
* 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:
parent
9354966b6c
commit
871300cf73
12 changed files with 778 additions and 1 deletions
fabric-command-api-v1/src
main
java/net/fabricmc/fabric
api/client/command/v1
impl/command/client
mixin/command
resources
testmod
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,6 +11,9 @@
|
|||
"entrypoints": {
|
||||
"main": [
|
||||
"net.fabricmc.fabric.test.command.CommandTest"
|
||||
],
|
||||
"client": [
|
||||
"net.fabricmc.fabric.test.command.client.ClientCommandTest"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue