Add Brigadier (#149)

Co-authored-by: Jason Mitchell <mitchej@gmail.com>
Co-authored-by: Martin Robertz <dream-master@gmx.net>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2025-11-07 17:42:42 +01:00
1 parent 6529f347ec
commit 441850b8b7
8 files changed
+296 -2

No files matched your search

+2
View File
@@ -6,8 +6,10 @@ configurations {
dependencies {
shadowImplementation("it.unimi.dsi:fastutil:8.5.15") // Apache 2.0
shadowImplementation("org.joml:joml:1.10.8") { transitive = false } // MIT
shadowImplementation("com.mojang:brigadier:1.0.18") // MIT
shadowSources("it.unimi.dsi:fastutil:8.5.15:sources")
shadowSources("org.joml:joml:1.10.8:sources") { transitive = false }
shadowSources("com.mojang:brigadier:1.0.18")
testImplementation(platform("org.junit:junit-bom:5.11.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
+7
View File
@@ -8,4 +8,11 @@ repositories {
includeGroup 'com.falsepattern'
}
}
maven {
name = "Mojang Maven"
url = "https://libraries.minecraft.net"
content {
includeGroup 'com.mojang'
}
}
}
@@ -7,6 +7,7 @@ import net.minecraft.util.IChatComponent;
import net.minecraftforge.common.util.FakePlayer;
import com.gtnewhorizon.gtnhlib.block.BlockTest;
import com.gtnewhorizon.gtnhlib.brigadier.BrigadierApi;
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
import com.gtnewhorizon.gtnhlib.eventbus.AutoEventBus;
import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber;
@@ -58,13 +59,17 @@ public class CommonProxy {
public void postInit(FMLPostInitializationEvent event) {}
public void serverAboutToStart(FMLServerAboutToStartEvent event) {}
public void serverAboutToStart(FMLServerAboutToStartEvent event) {
BrigadierApi.init();
}
public void serverStarting(FMLServerStartingEvent event) {}
public void serverStarted(FMLServerStartedEvent event) {}
public void serverStopping(FMLServerStoppingEvent event) {}
public void serverStopping(FMLServerStoppingEvent event) {
BrigadierApi.clear();
}
public void serverStopped(FMLServerStoppedEvent event) {}
@@ -0,0 +1,183 @@
package com.gtnewhorizon.gtnhlib.brigadier;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommand;
import net.minecraft.command.ICommandSender;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.ChatComponentText;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.tree.CommandNode;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class BrigadierApi {
@Nullable
private static CommandDispatcher<ICommandSender> dispatcher = new CommandDispatcher<>();
@NotNull
public static CommandDispatcher<ICommandSender> getCommandDispatcher() {
return Objects.requireNonNull(dispatcher);
}
@ApiStatus.Internal
public static void init() {
dispatcher = new CommandDispatcher<>();
registerTestCommand();
}
@ApiStatus.Internal
public static void clear() {
dispatcher = null;
}
private static void registerTestCommand() {
getCommandDispatcher().register(LiteralArgumentBuilder.<ICommandSender>literal("brigadier").executes(ctx -> {
ctx.getSource().addChatMessage(new ChatComponentText("Brigadier from GTNHLib!"));
return Command.SINGLE_SUCCESS;
}).then(
RequiredArgumentBuilder
.<ICommandSender, String>argument("stringValue", StringArgumentType.greedyString())
.requires(src -> src.canCommandSenderUseCommand(4, "")).suggests((ctx, builder) -> {
for (EntityPlayer playerEntity : ctx.getSource().getEntityWorld().playerEntities) {
builder.suggest(playerEntity.getCommandSenderName());
}
for (int i = 0; i < 9; i++) {
builder.suggest(i);
}
return builder.buildFuture();
}).executes(ctx -> {
String value = StringArgumentType.getString(ctx, "stringValue");
ctx.getSource().addChatMessage(new ChatComponentText("Brigadier: " + value));
return Command.SINGLE_SUCCESS;
})));
}
/**
* Execute the command by brigadier.
*
* @param sender the executor
* @param command the command to execute
* @return the result
*/
public static int executeCommand(ICommandSender sender, String command) {
try {
return getCommandDispatcher().execute(command, sender);
} catch (CommandSyntaxException e) {
// TODO: make the exception message better
throw new CommandException(e.getMessage());
}
}
/**
* Return the suggestions for the given sender with the given typed command.
*
* @param sender the given sender
* @param command the given typed command
* @return the suggestions in text
*/
public static List<String> getPossibleCommands(ICommandSender sender, String command) {
ParseResults<ICommandSender> parse = getCommandDispatcher().parse(command, sender);
try {
return getCommandDispatcher().getCompletionSuggestions(parse).get().getList().stream()
.map(Suggestion::getText).collect(Collectors.toList());
} catch (ExecutionException | InterruptedException e) {
log.warn("Exception occurred while completing command '{}'", command, e);
return Collections.emptyList();
}
}
/**
* Return the wrapped commands that the given sender has the permission to execute.
*
* @param sender the given sender
* @return the wrapped commands
*/
public static Collection<ICommand> getPossibleCommands(ICommandSender sender) {
return getCommandDispatcher().getRoot().getChildren().stream().filter(c -> c.canUse(sender))
.map(BrigadierApi::toICommandForHelpCommand).collect(Collectors.toList());
}
/**
* Wrap the given command node to {@link ICommand} with only used methods implemented.
*
* @param node the given command node
* @return the wrapper
*/
private static ICommand toICommandForHelpCommand(CommandNode<?> node) {
return new ICommand() {
@Override
public String getCommandName() {
return node.getName();
}
@Override
public String getCommandUsage(ICommandSender sender) {
return node.getUsageText();
}
@Override
public List<String> getCommandAliases() {
return Collections.emptyList();
}
@Override
public void processCommand(ICommandSender sender, String[] args) {}
@Override
public boolean canCommandSenderUseCommand(ICommandSender sender) {
return false;
}
@Override
public List<String> addTabCompletionOptions(ICommandSender sender, String[] args) {
return Collections.emptyList();
}
@Override
public boolean isUsernameIndex(String[] args, int index) {
return false;
}
@Override
public int compareTo(@NotNull Object o) {
if (o instanceof ICommand otherCommand) {
return getCommandName().compareTo(otherCommand.getCommandName());
}
return 0;
}
@Override
public String toString() {
return "DummyICommand{" + node.toString() + "}";
}
@Override
public int hashCode() {
return node.hashCode();
}
};
}
}
@@ -24,6 +24,7 @@ public enum Mixins implements IMixins {
DEBUG_TEXTURES(new MixinBuilder("Dump textures sizes")
.addClientMixins("debug.MixinDynamicTexture", "debug.MixinTextureAtlasSprite").setPhase(Phase.EARLY)
.setApplyIf(() -> Boolean.parseBoolean(System.getProperty("gtnhlib.debugtextures", "false")))),
BRIGADIER(Side.COMMON, "MixinCommandHandler", "MixinCommandHelp"),
FONT_RENDERER(new MixinBuilder("Font rendering replacements").addClientMixins("MixinFontRenderer")
.setPhase(Phase.EARLY).setApplyIf(() -> EarlyConfig.enableFontRendererMixin)),
MODEL_TEXTURE_LOADING(new MixinBuilder("Automatically load model textures").addClientMixins(
@@ -0,0 +1,65 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import java.util.Collection;
import java.util.List;
import net.minecraft.command.CommandHandler;
import net.minecraft.command.ICommand;
import net.minecraft.command.ICommandSender;
import net.minecraft.command.ServerCommandManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.gtnewhorizon.gtnhlib.brigadier.BrigadierApi;
@Mixin(CommandHandler.class)
public class MixinCommandHandler {
@Unique
private boolean gtnhlib$isServerCommandManager() {
// this is used to make sure that we only inject into the server command handler
return (Object) this instanceof ServerCommandManager;
}
@Inject(
method = "executeCommand",
at = @At(value = "NEW", target = "()Lnet/minecraft/command/CommandNotFoundException;"),
cancellable = true)
private void gtnhlib$fallbackToBrigadier(ICommandSender sender, String command,
CallbackInfoReturnable<Integer> cir) {
if (gtnhlib$isServerCommandManager()) {
int value = BrigadierApi.executeCommand(sender, command);
cir.setReturnValue(value);
}
}
@Inject(
method = "getPossibleCommands(Lnet/minecraft/command/ICommandSender;Ljava/lang/String;)Ljava/util/List;",
at = @At("RETURN"),
cancellable = true)
private void gtnhlib$gatherBrigadierSuggestion(ICommandSender sender, String command,
CallbackInfoReturnable<List<String>> cir) {
if (gtnhlib$isServerCommandManager()) {
List<String> returnValue = cir.getReturnValue();
List<String> possibleCommands = BrigadierApi.getPossibleCommands(sender, command);
if (returnValue == null) {
cir.setReturnValue(possibleCommands);
} else {
returnValue.addAll(possibleCommands);
}
}
}
@Inject(method = "getPossibleCommands(Lnet/minecraft/command/ICommandSender;)Ljava/util/List;", at = @At("RETURN"))
private void gtnhlib$gatherBrigadierCommands(ICommandSender sender, CallbackInfoReturnable<List<ICommand>> cir) {
if (gtnhlib$isServerCommandManager()) {
Collection<ICommand> possibleCommands = BrigadierApi.getPossibleCommands(sender);
cir.getReturnValue().addAll(possibleCommands);
}
}
}
@@ -0,0 +1,29 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import net.minecraft.command.CommandHelp;
import net.minecraft.command.ICommandSender;
import net.minecraft.command.WrongUsageException;
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 com.gtnewhorizon.gtnhlib.brigadier.BrigadierApi;
import com.mojang.brigadier.tree.CommandNode;
@Mixin(CommandHelp.class)
public class MixinCommandHelp {
@Inject(
method = "processCommand",
at = @At(value = "NEW", target = "()Lnet/minecraft/command/CommandNotFoundException;"),
cancellable = true)
private void gtnhlib$fallbackToBrigadier(ICommandSender sender, String[] args, CallbackInfo ci) {
CommandNode<ICommandSender> node = BrigadierApi.getCommandDispatcher().getRoot().getChild(args[0]);
if (node != null) {
throw new WrongUsageException(node.getUsageText());
}
}
}
@@ -18,3 +18,5 @@ public net.minecraft.client.renderer.Tessellator field_78415_z # isDrawing
public net.minecraft.client.renderer.Tessellator field_78388_E # bufferSize
public net.minecraft.client.renderer.Tessellator field_78410_t # isColorDisabled
public net.minecraft.client.renderer.Tessellator func_78379_d()V # void reset()
public net.minecraft.command.CommandHandler field_71562_a # commandMap