mirror of
https://github.com/FabricMC/fabric.git
synced 2024-11-15 03:35:07 -05:00
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:
parent
bb4e3b8e47
commit
d51ff45ef2
7 changed files with 147 additions and 92 deletions
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -5,8 +5,7 @@
|
|||
"client": [
|
||||
"ClientCommandSourceMixin",
|
||||
"ClientPlayerEntityMixin",
|
||||
"ClientPlayNetworkHandlerMixin",
|
||||
"MinecraftClientMixin"
|
||||
"ClientPlayNetworkHandlerMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in a new issue