커맨드 등록 — Brigadier로 /명령어 만들기
RegisterCommandsEvent와 Brigadier 빌더 패턴을 사용해 커스텀 슬래시 커맨드를 등록하는 방법을 배웁니다. 인자 타입, 권한 레벨, 실행 로직까지 단계별로 익힙니다.
커맨드 시스템 개요
Minecraft 1.13부터 커맨드 파싱은 Mojang이 직접 만든 Brigadier 라이브러리가 담당합니다. NeoForge는 RegisterCommandsEvent를 통해 이 파서에 커스텀 커맨드를 주입할 수 있는 진입점을 제공합니다.
Brigadier의 핵심 개념은 커맨드 트리입니다. 각 노드는 고정 문자열(literal) 또는 동적 인자(argument)이고, 트리를 따라 내려가며 입력을 파싱합니다.
/hello → "Hello, World!" 출력
/hello <name> → "Hello, <name>!" 출력
RegisterCommandsEvent 구독
커맨드 등록은 NeoForge.EVENT_BUS에서 발생하는 RegisterCommandsEvent를 구독해야 합니다. MinecraftForge.EVENT_BUS(게임 버스)가 아닙니다.
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
@EventBusSubscriber(modid = ExampleMod.MODID)
public class CommandEventHandler {
@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
// event.getDispatcher() → Brigadier CommandDispatcher
registerHelloCommand(event.getDispatcher());
}
}버스 선택 기준:
RegisterCommandsEvent는 서버 시작 시 한 번 발생하는 설정 이벤트입니다. 설정 이벤트는 항상Bus.MOD에서 구독합니다.
기본 커맨드 등록
Commands.literal()로 루트 노드를 만들고, .executes()로 실행 로직을 붙입니다.
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
public static void registerHelloCommand(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(
Commands.literal("hello")
.then(Commands.argument("name", StringArgumentType.word())
.executes(ctx -> {
String name = StringArgumentType.getString(ctx, "name");
ctx.getSource().sendSystemMessage(
Component.literal("Hello, " + name + "!")
);
return Command.SINGLE_SUCCESS;
}))
.executes(ctx -> {
ctx.getSource().sendSystemMessage(
Component.literal("Hello, World!")
);
return Command.SINGLE_SUCCESS;
})
);
}인게임에서 확인합니다.
/hello → Hello, World!
/hello Steve → Hello, Steve!
Brigadier 빌더 패턴
Brigadier는 빌더 패턴으로 커맨드 트리를 구성합니다. 네 가지 메서드가 핵심입니다.
| 메서드 | 역할 | 예시 |
|---|---|---|
Commands.literal("...") | 고정 문자열 노드 | /give, /tp |
Commands.argument("name", type) | 동적 인자 노드 | <player>, <amount> |
.then(...) | 하위 노드 추가 (서브커맨드/인자) | 체이닝 가능 |
.executes(ctx -> {...}) | 이 노드에서 실행할 로직 | return Command.SINGLE_SUCCESS |
.requires(predicate) | 실행 조건/권한 | Commands.hasPermission(Commands.LEVEL_GAMEMASTERS) |
트리 구조 예시
Commands.literal("mymod")
.then(Commands.literal("reload")
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) // OP(게임마스터) 이상만
.executes(ctx -> {
// 리로드 로직
return Command.SINGLE_SUCCESS;
}))
.then(Commands.literal("info")
.executes(ctx -> {
ctx.getSource().sendSystemMessage(
Component.literal("ExampleMod v1.0")
);
return Command.SINGLE_SUCCESS;
}))이 트리는 /mymod reload와 /mymod info 두 서브커맨드를 만듭니다.
인자 타입
Brigadier는 다양한 내장 인자 타입을 제공합니다. 타입을 올바르게 선택하면 자동 완성과 입력 검증이 함께 따라옵니다.
문자열 타입
// 공백 없는 단어 하나
Commands.argument("name", StringArgumentType.word())
// 따옴표로 감싼 문자열 (공백 포함 가능)
Commands.argument("message", StringArgumentType.string())
// 나머지 입력 전체 (항상 마지막 인자로)
Commands.argument("text", StringArgumentType.greedyString())숫자 타입
// 정수 (범위 제한 가능)
Commands.argument("amount", IntegerArgumentType.integer(1, 64))
// 실수
Commands.argument("scale", DoubleArgumentType.doubleArg(0.1, 10.0))게임 오브젝트 타입
// 단일 엔티티 선택자 (@p, @e[type=...] 등)
Commands.argument("target", EntityArgument.entity())
// 복수 엔티티
Commands.argument("targets", EntityArgument.entities())
// 블록 좌표
Commands.argument("pos", BlockPosArgument.blockPos())
// 아이템
Commands.argument("item", ItemArgument.item(buildContext))인자 값을 꺼낼 때는 타입에 맞는 getter를 씁니다.
.executes(ctx -> {
int amount = IntegerArgumentType.getInteger(ctx, "amount");
ServerPlayer target = EntityArgument.getPlayer(ctx, "target");
BlockPos pos = BlockPosArgument.getBlockPos(ctx, "pos");
// ...
return Command.SINGLE_SUCCESS;
})권한 레벨
.requires()로 커맨드 실행 조건을 걸 수 있습니다. 가장 흔한 패턴은 OP 레벨 확인입니다.
| 레벨 | 대상 |
|---|---|
| 0 | 모든 플레이어 (기본값) |
| 1 | 서버 콘솔 |
| 2 | OP (일반 관리 커맨드) |
| 3 | OP (서버 관리 커맨드) |
| 4 | OP (서버 콘솔 수준) |
Commands.literal("admin-reload")
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) // OP 레벨 2(게임마스터) 이상
.executes(ctx -> {
// 관리자 전용 로직
return Command.SINGLE_SUCCESS;
})플레이어 여부도 확인할 수 있습니다.
.requires(src -> src.isPlayer()) // 플레이어만 (콘솔 제외)조건을 조합할 때는 Predicate를 합성합니다. Commands.hasPermission(...)이 Predicate<CommandSourceStack>를 반환하므로 .and(...)로 추가 조건을 붙입니다.
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).and(src -> src.isPlayer()))피드백 메시지 전송
커맨드 실행 결과를 플레이어에게 알릴 때는 CommandSourceStack의 메서드를 씁니다.
CommandSourceStack source = ctx.getSource();
// 시스템 메시지 (채팅창, 노란색 아이콘 없음)
source.sendSystemMessage(Component.literal("완료!"));
// 성공 피드백 (채팅창, 초록색)
source.sendSuccess(() -> Component.literal("성공!"), true);
// 실패 피드백 (채팅창, 빨간색)
source.sendFailure(Component.literal("실패: 권한 없음"));sendSuccess의 두 번째 인자(broadcastToOps)가 true면 OP들에게도 알림이 갑니다.
전체 예제
@EventBusSubscriber(modid = ExampleMod.MODID)
public class CommandEventHandler {
@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
registerHelloCommand(dispatcher);
registerGiveCommand(dispatcher);
}
private static void registerHelloCommand(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(
Commands.literal("hello")
.then(Commands.argument("name", StringArgumentType.word())
.executes(ctx -> {
String name = StringArgumentType.getString(ctx, "name");
ctx.getSource().sendSystemMessage(
Component.literal("Hello, " + name + "!")
);
return Command.SINGLE_SUCCESS;
}))
.executes(ctx -> {
ctx.getSource().sendSystemMessage(
Component.literal("Hello, World!")
);
return Command.SINGLE_SUCCESS;
})
);
}
private static void registerGiveCommand(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(
Commands.literal("examplemod-give")
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
.then(Commands.argument("amount", IntegerArgumentType.integer(1, 64))
.executes(ctx -> {
int amount = IntegerArgumentType.getInteger(ctx, "amount");
// 아이템 지급 로직 (챕터 예제용 스텁)
ctx.getSource().sendSuccess(
() -> Component.literal(amount + "개 지급 완료"),
true
);
return Command.SINGLE_SUCCESS;
}))
);
}
}안티패턴
⚠️ 인자 검증 누락
// ❌ 음수도 받음 — 아이템 amount가 -1이 될 수 있음 .then(Commands.argument("amount", IntegerArgumentType.integer()) .executes(ctx -> player.give(item, IntegerArgumentType.getInteger(ctx, "amount")))) // ✅ 1~64 제한 — Brigadier가 자동으로 범위 검증 .then(Commands.argument("amount", IntegerArgumentType.integer(1, 64)) .executes(...))Brigadier 인자 타입에 min/max를 명시하면 자동 검증이 붙습니다. 별도 if 체크가 필요 없습니다.
⚠️ 메인 스레드 외 호출 금지
// ❌ 비동기 스레드에서 커맨드 실행 결과 처리 CompletableFuture.runAsync(() -> { ctx.getSource().sendSystemMessage(Component.literal("완료")); // 크래시 가능 }); // ✅ 커맨드 실행 컨텍스트는 항상 서버 메인 스레드에서 동작 ctx.getSource().sendSystemMessage(Component.literal("완료"));
CommandSourceStack의 메서드는 서버 메인 스레드에서만 안전합니다. 비동기 작업이 필요하면 결과를 메인 스레드로 돌려보내야 합니다.
정리
RegisterCommandsEvent는Bus.MOD에서 구독한다.Commands.literal()+Commands.argument()+.then()+.executes()로 커맨드 트리를 구성한다.- 인자 타입에 min/max를 명시하면 Brigadier가 자동 검증한다.
.requires()로 권한 레벨과 실행 조건을 제한한다.- 커맨드 실행 로직은 항상 서버 메인 스레드에서 동작한다.
다음 챕터에서는 커스텀 이벤트를 직접 정의하고 발행하는 방법을 배웁니다.