Skip to content

Commit

Permalink
rename on platform class name, add ClientCommandRegistryEvent
Browse files Browse the repository at this point in the history
  • Loading branch information
TexBlock committed Oct 5, 2024
1 parent f9b0045 commit 1bd0a3f
Show file tree
Hide file tree
Showing 23 changed files with 488 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package band.kessoku.lib.api.command.event;

import band.kessoku.lib.api.command.util.ClientCommandSourceExtension;
import band.kessoku.lib.event.api.Event;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.command.CommandRegistryAccess;
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.NonExtendable
public interface ClientCommandRegistryEvent {
Event<ClientCommandRegistryEvent> EVENT = Event.of(clientCommandRegistryEvents -> (dispatcher, registryAccess) -> {
for (ClientCommandRegistryEvent callback : clientCommandRegistryEvents) {
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<ClientCommandSourceExtension> dispatcher, CommandRegistryAccess registryAccess);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package band.kessoku.lib.api.event.command;
package band.kessoku.lib.api.command.event;

import band.kessoku.lib.event.api.Event;
import com.mojang.brigadier.CommandDispatcher;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package band.kessoku.lib.api.command.util;

import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;

public class ClientCommandManager {


/**
* Creates a literal argument builder.
*
* @param name the literal name
* @return the created argument builder
*/
public static LiteralArgumentBuilder<ClientCommandSourceExtension> 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<ClientCommandSourceExtension, T> argument(String name, ArgumentType<T> type) {
return RequiredArgumentBuilder.argument(name, type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package band.kessoku.lib.api.command.util;

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.entity.Entity;
import net.minecraft.text.Text;
import net.minecraft.util.math.Vec2f;
import net.minecraft.util.math.Vec3d;

public interface ClientCommandSourceExtension extends CommandSource {
/**
* Sends a feedback message to the player.
*
* @param message the feedback message
*/
void kessokulib$sendFeedback(Text message);

/**
* Sends an error message to the player.
*
* @param message the error message
*/
void kessokulib$sendError(Text message);

/**
* Gets the client instance used to run the command.
*
* @return the client
*/
MinecraftClient kessokulib$getClient();

/**
* Gets the player that used the command.
*
* @return the player
*/
ClientPlayerEntity kessokulib$getPlayer();

/**
* Gets the entity that used the command.
*
* @return the entity
*/
default Entity kessokulib$getEntity() {
return kessokulib$getPlayer();
}

/**
* Gets the position from where the command has been executed.
*
* @return the position
*/
default Vec3d kessokulib$getPosition() {
return kessokulib$getPlayer().getPos();
}

/**
* Gets the rotation of the entity that used the command.
*
* @return the rotation
*/
default Vec2f kessokulib$getRotation() {
return kessokulib$getPlayer().getRotationClient();
}

/**
* Gets the world where the player used the command.
*
* @return the world
*/
ClientWorld kessokulib$getWorld();

/**
* Gets the meta property under {@code key} that was assigned to this source.
*
* <p>This method should return the same result for every call with the same {@code key}.
*
* @param key the meta key
* @return the meta
*/
default Object kessokulib$getMeta(String key) {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package band.kessoku.lib.api.command.util;

import band.kessoku.lib.api.KessokuLib;
import band.kessoku.lib.api.command.KessokuCommand;
import band.kessoku.lib.mixin.command.HelpCommandAccessor;
import com.google.common.collect.Iterables;
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 net.minecraft.client.MinecraftClient;
import net.minecraft.text.Text;
import net.minecraft.text.Texts;
import org.jetbrains.annotations.Nullable;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class ClientCommandUtils {
private static @Nullable CommandDispatcher<ClientCommandSourceExtension> activeDispatcher;

public static void setActiveDispatcher(@Nullable CommandDispatcher<ClientCommandSourceExtension> dispatcher) {
ClientCommandUtils.activeDispatcher = dispatcher;
}

public static @Nullable CommandDispatcher<ClientCommandSourceExtension> getActiveDispatcher() {
return activeDispatcher;
}

/**
* Executes a client-sided command. Callers should ensure that this is only called
* on slash-prefixed messages and the slash needs to be removed before calling.
* (This is the same requirement as {@code ClientPlayerEntity#sendCommand}.)
*
* @param command the command with slash removed
* @return true if the command should not be sent to the server, false otherwise
*/
public static boolean executeCommand(String command) {
MinecraftClient client = MinecraftClient.getInstance();

// The interface is implemented on ClientCommandSource with a mixin.
// noinspection ConstantConditions
ClientCommandSourceExtension commandSource = (ClientCommandSourceExtension) client.getNetworkHandler().getCommandSource();

client.getProfiler().push(command);

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.
activeDispatcher.execute(command, commandSource);
return true;
} catch (CommandSyntaxException e) {
boolean ignored = isIgnoredException(e.getType());

if (ignored) {
KessokuLib.getLogger().debug("Syntax exception for client-sided command '{}'", command, e);
return false;
}

KessokuLib.getLogger().warn("Syntax exception for client-sided command '{}'", command, e);
commandSource.kessokulib$sendError(getErrorMessage(e));
return true;
} catch (Exception e) {
KessokuLib.getLogger().warn("Error while executing client-sided command '{}'", command, e);
commandSource.kessokulib$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 command 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 ChatInputSuggestor.formatException. 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 ? Text.translatable("command.context.parse_error", message, e.getCursor(), 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 (!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<ClientCommandSourceExtension> help = ClientCommandManager.literal("help");
help.executes(ClientCommandUtils::executeRootHelp);
help.then(ClientCommandManager.argument("command", StringArgumentType.greedyString()).executes(ClientCommandUtils::executeArgumentHelp));

CommandNode<ClientCommandSourceExtension> mainNode = activeDispatcher.register(ClientCommandManager.literal(KessokuCommand.MOD_ID).then(help));
activeDispatcher.register(ClientCommandManager.literal(KessokuCommand.MOD_ID).redirect(mainNode));
}

// noinspection CodeBlock2Expr
activeDispatcher.findAmbiguities((parent, child, sibling, inputs) -> {
KessokuLib.getLogger().warn("Ambiguity between arguments {} and {} with inputs: {}", activeDispatcher.getPath(child), activeDispatcher.getPath(sibling), inputs);
});
}

private static int executeRootHelp(CommandContext<ClientCommandSourceExtension> context) {
return executeHelp(activeDispatcher.getRoot(), context);
}

private static int executeArgumentHelp(CommandContext<ClientCommandSourceExtension> context) throws CommandSyntaxException {
ParseResults<ClientCommandSourceExtension> parseResults = activeDispatcher.parse(StringArgumentType.getString(context, "command"), context.getSource());
List<ParsedCommandNode<ClientCommandSourceExtension>> nodes = parseResults.getContext().getNodes();

if (nodes.isEmpty()) {
throw HelpCommandAccessor.getFailedException().create();
}

return executeHelp(Iterables.getLast(nodes).getNode(), context);
}

private static int executeHelp(CommandNode<ClientCommandSourceExtension> startNode, CommandContext<ClientCommandSourceExtension> context) {
Map<CommandNode<ClientCommandSourceExtension>, String> commands = activeDispatcher.getSmartUsage(startNode, context.getSource());

for (String command : commands.values()) {
context.getSource().kessokulib$sendFeedback(Text.literal("/" + command));
}

return commands.size();
}

public static void addCommands(CommandDispatcher<ClientCommandSourceExtension> target, ClientCommandSourceExtension source) {
Map<CommandNode<ClientCommandSourceExtension>, CommandNode<ClientCommandSourceExtension>> originalToCopy = new HashMap<>();
originalToCopy.put(activeDispatcher.getRoot(), target.getRoot());
copyChildren(activeDispatcher.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<ClientCommandSourceExtension> origin,
CommandNode<ClientCommandSourceExtension> target,
ClientCommandSourceExtension source,
Map<CommandNode<ClientCommandSourceExtension>, CommandNode<ClientCommandSourceExtension>> originalToCopy
) {
for (CommandNode<ClientCommandSourceExtension> child : origin.getChildren()) {
if (!child.canUse(source)) continue;

ArgumentBuilder<ClientCommandSourceExtension, ?> 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<ClientCommandSourceExtension> result = builder.build();
originalToCopy.put(child, result);
target.addChild(result);

if (!child.getChildren().isEmpty()) {
copyChildren(child, result, source, originalToCopy);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package band.kessoku.lib.mixin.command;

import band.kessoku.lib.api.command.util.ClientCommandSourceExtension;
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.text.Text;
import net.minecraft.util.Formatting;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;

@Mixin(ClientCommandSource.class)
abstract class ClientCommandSourceMixin implements ClientCommandSourceExtension {
@Shadow
@Final
private MinecraftClient client;

@Override
public void kessokulib$sendFeedback(Text message) {
this.client.inGameHud.getChatHud().addMessage(message);
this.client.getNarratorManager().narrate(message);
}

@Override
public void kessokulib$sendError(Text message) {
kessokulib$sendFeedback(Text.empty().append(message).formatted(Formatting.RED));
}

@Override
public MinecraftClient kessokulib$getClient() {
return client;
}

@Override
public ClientPlayerEntity kessokulib$getPlayer() {
return client.player;
}

@Override
public ClientWorld kessokulib$getWorld() {
return client.world;
}
}
Loading

0 comments on commit 1bd0a3f

Please sign in to comment.