Change client command api to use registration event (#2264)

* wip client command changes

* Move field and add javadoc

* Switch to jetbrains annotations and add missing annotations

* Add note about integrated servers

* Rename DISPATCHER field
This commit is contained in:
Jason 2022-05-31 04:11:27 -07:00 committed by GitHub
parent bb4e3b8e47
commit d51ff45ef2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 147 additions and 92 deletions

View file

@ -20,9 +20,11 @@ import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
/**
* Manages client-sided commands and provides some related helper methods.
@ -30,8 +32,8 @@ import net.fabricmc.api.Environment;
* <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>Registrations can be done in handlers for {@link ClientCommandRegistrationCallback#EVENT}
* (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.
@ -49,25 +51,33 @@ import net.fabricmc.api.Environment;
* <h2>Example command</h2>
* <pre>
* {@code
* ClientCommandManager.DISPATCHER.register(
* ClientCommandManager.literal("hello").executes(context -> {
* context.getSource().sendFeedback(new LiteralText("Hello, world!"));
* return 0;
* })
* );
* ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
* dispatcher.register(
* ClientCommandManager.literal("hello").executes(context -> {
* context.getSource().sendFeedback(Text.literal("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() {
}
/**
* Gets the active command dispatcher that handles client command registration and execution.
*
* <p>May be null when not connected to a server (dedicated or integrated).</p>
*
* @return active dispatcher if present
*/
public static @Nullable CommandDispatcher<FabricClientCommandSource> getActiveDispatcher() {
return ClientCommandInternals.getActiveDispatcher();
}
/**
* Creates a literal argument builder.
*

View file

@ -0,0 +1,47 @@
/*
* 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.v2;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.command.CommandRegistryAccess;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
/**
* Callback for when client commands are registered to the dispatcher.
*
* <p>To register some commands, you would register an event listener and implement the callback.
*
* <p>See {@link ClientCommandManager} for more details and an example.
*/
public interface ClientCommandRegistrationCallback {
Event<ClientCommandRegistrationCallback> EVENT = EventFactory.createArrayBacked(ClientCommandRegistrationCallback.class, (callbacks) -> (dispatcher, registryAccess) -> {
for (ClientCommandRegistrationCallback callback : callbacks) {
callback.register(dispatcher, registryAccess);
}
});
/**
* Called when registering client commands.
*
* @param dispatcher the command dispatcher to register commands to
* @param registryAccess object exposing access to the game's registries
*/
void register(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess);
}

View file

@ -16,7 +16,6 @@
package net.fabricmc.fabric.impl.command.client;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.DISPATCHER;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
@ -37,6 +36,7 @@ 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.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -55,6 +55,15 @@ public final class ClientCommandInternals {
private static final Logger LOGGER = LoggerFactory.getLogger(ClientCommandInternals.class);
private static final String API_COMMAND_NAME = "fabric-command-api-v2:client";
private static final String SHORT_API_COMMAND_NAME = "fcc";
private static @Nullable CommandDispatcher<FabricClientCommandSource> activeDispatcher;
public static void setActiveDispatcher(@Nullable CommandDispatcher<FabricClientCommandSource> dispatcher) {
ClientCommandInternals.activeDispatcher = dispatcher;
}
public static @Nullable CommandDispatcher<FabricClientCommandSource> getActiveDispatcher() {
return activeDispatcher;
}
/**
* Executes a client-sided command. Callers should ensure that this is only called
@ -77,7 +86,7 @@ public final class ClientCommandInternals {
// 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(command, commandSource);
activeDispatcher.execute(command, commandSource);
return true;
} catch (CommandSyntaxException e) {
boolean ignored = isIgnoredException(e.getType());
@ -132,29 +141,29 @@ public final class ClientCommandInternals {
* 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()) {
if (!activeDispatcher.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));
CommandNode<FabricClientCommandSource> mainNode = activeDispatcher.register(literal(API_COMMAND_NAME).then(help));
activeDispatcher.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);
activeDispatcher.findAmbiguities((parent, child, sibling, inputs) -> {
LOGGER.warn("Ambiguity between arguments {} and {} with inputs: {}", activeDispatcher.getPath(child), activeDispatcher.getPath(sibling), inputs);
});
}
private static int executeRootHelp(CommandContext<FabricClientCommandSource> context) {
return executeHelp(DISPATCHER.getRoot(), context);
return executeHelp(activeDispatcher.getRoot(), context);
}
private static int executeArgumentHelp(CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
ParseResults<FabricClientCommandSource> parseResults = DISPATCHER.parse(StringArgumentType.getString(context, "command"), context.getSource());
ParseResults<FabricClientCommandSource> parseResults = activeDispatcher.parse(StringArgumentType.getString(context, "command"), context.getSource());
List<ParsedCommandNode<FabricClientCommandSource>> nodes = parseResults.getContext().getNodes();
if (nodes.isEmpty()) {
@ -165,7 +174,7 @@ public final class ClientCommandInternals {
}
private static int executeHelp(CommandNode<FabricClientCommandSource> startNode, CommandContext<FabricClientCommandSource> context) {
Map<CommandNode<FabricClientCommandSource>, String> commands = DISPATCHER.getSmartUsage(startNode, context.getSource());
Map<CommandNode<FabricClientCommandSource>, String> commands = activeDispatcher.getSmartUsage(startNode, context.getSource());
for (String command : commands.values()) {
context.getSource().sendFeedback(Text.literal("/" + command));
@ -176,8 +185,8 @@ public final class ClientCommandInternals {
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);
originalToCopy.put(activeDispatcher.getRoot(), target.getRoot());
copyChildren(activeDispatcher.getRoot(), target.getRoot(), source, originalToCopy);
}
/**

View file

@ -26,9 +26,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.command.CommandSource;
import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket;
import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
@ -41,6 +44,14 @@ abstract class ClientPlayNetworkHandlerMixin {
@Final
private ClientCommandSource commandSource;
@Inject(method = "onGameJoin", at = @At("HEAD"))
private void onGameJoin(GameJoinS2CPacket packet, CallbackInfo info) {
final CommandDispatcher<FabricClientCommandSource> dispatcher = new CommandDispatcher<>();
ClientCommandInternals.setActiveDispatcher(dispatcher);
ClientCommandRegistrationCallback.EVENT.invoker().register(dispatcher, new CommandRegistryAccess(packet.registryManager()));
ClientCommandInternals.finalizeInit();
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Inject(method = "onCommandTree", at = @At("RETURN"))
private void onOnCommandTree(CommandTreeS2CPacket packet, CallbackInfo info) {

View file

@ -1,35 +0,0 @@
/*
* 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

@ -5,8 +5,7 @@
"client": [
"ClientCommandSourceMixin",
"ClientPlayerEntityMixin",
"ClientPlayNetworkHandlerMixin",
"MinecraftClientMixin"
"ClientPlayNetworkHandlerMixin"
],
"injectors": {
"defaultRequire": 1

View file

@ -26,14 +26,16 @@ import org.slf4j.LoggerFactory;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.command.argument.ItemStackArgument;
import net.minecraft.command.argument.ItemStackArgumentType;
import net.minecraft.text.Text;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
@Environment(EnvType.CLIENT)
@ -46,43 +48,55 @@ public final class ClientCommandTest implements ClientModInitializer {
@Override
public void onInitializeClient() {
ClientCommandManager.DISPATCHER.register(ClientCommandManager.literal("test_client_command").executes(context -> {
context.getSource().sendFeedback(Text.literal("This is a client command!"));
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
dispatcher.register(ClientCommandManager.literal("test_client_command").executes(context -> {
context.getSource().sendFeedback(Text.literal("This is a client command!"));
if (context.getSource().getClient() == null) {
throw IS_NULL.create("client");
}
if (context.getSource().getClient() == null) {
throw IS_NULL.create("client");
}
if (context.getSource().getWorld() == null) {
throw IS_NULL.create("world");
}
if (context.getSource().getWorld() == null) {
throw IS_NULL.create("world");
}
if (context.getSource().getPlayer() == null) {
throw IS_NULL.create("player");
}
if (context.getSource().getPlayer() == null) {
throw IS_NULL.create("player");
}
return 0;
}));
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");
// Command with argument
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(Text.literal("Your number is " + number));
// Test error formatting
context.getSource().sendError(Text.literal("Your number is " + number));
return 0;
})
));
return 0;
})
));
// Unexecutable command
ClientCommandManager.DISPATCHER.register(ClientCommandManager.literal("hidden_client_command").requires(source -> false).executes(context -> {
throw UNEXECUTABLE_EXECUTED.create();
}));
// Unexecutable command
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();
// Command with argument using CommandRegistryAccess
dispatcher.register(ClientCommandManager.literal("test_client_command_with_registry_using_arg").then(
ClientCommandManager.argument("item", ItemStackArgumentType.itemStack(registryAccess)).executes(context -> {
final ItemStackArgument item = ItemStackArgumentType.getItemStackArgument(context, "item");
context.getSource().sendFeedback(item.createStack(1, false).toHoverableText());
return 0;
})
));
// Tests
RootCommandNode<FabricClientCommandSource> rootNode = dispatcher.getRoot();
// We climb the tree again
CommandNode<FabricClientCommandSource> testClientCommand = rootNode.getChild("test_client_command");
@ -118,7 +132,7 @@ public final class ClientCommandTest implements ClientModInitializer {
MinecraftClient client = MinecraftClient.getInstance();
ClientCommandSource commandSource = client.getNetworkHandler().getCommandSource();
RootCommandNode<FabricClientCommandSource> rootNode = ClientCommandManager.DISPATCHER.getRoot();
RootCommandNode<FabricClientCommandSource> rootNode = ClientCommandManager.getActiveDispatcher().getRoot();
CommandNode<FabricClientCommandSource> hiddenClientCommand = rootNode.getChild("hidden_client_command");
if (!(commandSource instanceof FabricClientCommandSource)) {