Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 into dev/1.21
  • Loading branch information
RawDiamondMC committed Oct 5, 2024
2 parents 1c0f0b8 + 1bd0a3f commit 72c692a
Show file tree
Hide file tree
Showing 24 changed files with 493 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ public String id() {
public String displayName() {
return displayName;
}

public ModPlatform platform() {
return modPlatform;
}
}
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);
}
}
}
}
Loading

0 comments on commit 72c692a

Please sign in to comment.