原创教程 · 目标版本:Forge 1.20.1 · 场景:落英枪战服务器 FFGS
自定义命令(Commands)¶
枪战服务器(FFGS)运营中有一堆高频操作:给玩家发枪、查库存、重载枪械配置、治疗玩家、踢人、调试。总不能每次都用控制台敲指令、或者让玩家自己开创造模式。正确的做法是——为模组注册自定义命令,把运营逻辑封装成 /ffgs ... 这样一串漂亮的命令树。
本教程从零开始,带你完整走一遍 Forge 1.20.1 的命令开发流程:注册事件、Brigadier 命令树、参数类型、建议(补全)、权限控制、反馈消息、客户端命令,最后给出一个可直接运行的 /ffgs 完整示例。
目录¶
- 命令的本质:Brigadier 命令树
- 注册命令:RegisterCommandsEvent
- 参数类型全览
- 执行回调与反馈消息
- 建议(Suggestion):Tab 补全
- 权限控制
- 完整示例:FFGS 枪战命令
- 客户端命令
- 常见坑
- 小结
1. 命令的本质:Brigadier 命令树¶
Minecraft 的命令系统由 Mojang 自研的 Brigadier 库驱动(com.mojang.brigadier)。Brigadier 的核心思想是命令树(command tree):
- 树的每个节点要么是字面量(literal),要么是参数(argument);
- 从根节点到叶子节点的一条路径,就是一个完整的命令;
- 叶子节点挂执行回调(executes),输入匹配成功时触发;
- 参数节点可以挂建议(suggests),用于 Tab 补全。
比如 /ffgs weapon give Steve ak47 这棵树的形状是:
ffgs (literal)
└─ weapon (literal)
└─ give (literal)
├─ player (argument: 玩家选择器)
│ └─ gun (argument: 枪械 ID) → executes(发枪逻辑)
└─ list (literal) → executes(列出枪械)
在 Forge 1.20.1 中,命令树的构建入口是一个叫做命令调度器(CommandDispatcher)的对象,泛型参数是 CommandSourceStack(命令源栈,即“谁在什么环境下执行了这条命令”)。它从命令注册事件(RegisterCommandsEvent)里拿。
2. 注册命令:RegisterCommandsEvent¶
Forge 在服务器(以及单人游戏的内部服务器)启动、构建命令树时,会触发 RegisterCommandsEvent(net.minecraftforge.event.RegisterCommandsEvent)。我们的任务就是订阅这个事件,往事件总线(EventBus)提供的命令调度器里塞命令。
订阅方式:在任意类上标注 @Mod.EventBusSubscriber,写一个静态方法加 @SubscribeEvent。
package com.example.ffgs.command;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = FFGSMod.MODID) // 默认挂在 FORGE 总线
public class FFGSCommands {
@SubscribeEvent
public static void register(RegisterCommandsEvent event) {
// 命令调度器:一切命令树的起点
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
dispatcher.register(
Commands.literal("ffgs") // 根节点:字面量
.executes(ctx -> { // 叶子:执行回调
ctx.getSource().sendSuccess(
net.minecraft.network.chat.Component.literal("你好,FFGS!"), false);
return 1;
})
);
}
}
几个关键点:
- 总线必须正确:
@Mod.EventBusSubscriber的bus参数默认是Bus.FORGE(Forge 总线),而RegisterCommandsEvent恰好就发在 Forge 总线上,所以不写也行。如果误写成bus = Bus.MOD,事件永远收不到,命令永远不会注册——这是新手第一大坑。 - 主类(Mod Main Class)定义 MODID:上面代码里的
FFGSMod.MODID来自你的主类:
package com.example.ffgs;
import net.minecraftforge.fml.common.Mod;
@Mod(FFGSMod.MODID)
public class FFGSMod {
public static final String MODID = "ffgs";
public FFGSMod() {
// 初始化逻辑
}
}
- 执行时机:该事件只在服务端逻辑(专用服务器、单人游戏内置服务器)触发一次,命令因此对所有玩家可见。想只在客户端生效的命令请参考第 8 节。
- 单机测试:单人游戏需要开启“对命令方块开放”/局域网作弊,或者直接跑专用服务器测试;否则玩家(包括你自己)没有权限执行。
3. 参数类型全览¶
Brigadier 自带一套参数类型,Forge/Minecraft 又包装了一批游戏相关的参数。表格里统一用「参数类型(English)→ 获取方式」列出 1.20.1 常用款:
| 参数类型 | 构建方式 | 取值方式 | 说明 |
|---|---|---|---|
| 字面量 | Commands.literal("give") |
— | 固定字符串,无值可取,只做分支路由 |
| 单词字符串 | StringArgumentType.word() |
StringArgumentType.getString(ctx, "名") |
不含空格的单词语,如枪械 ID |
| 常规字符串 | StringArgumentType.string() |
同上 | 可带引号包裹的字符串 |
| 贪婪字符串 | StringArgumentType.greedyString() |
同上 | 吃掉命令行剩下的所有内容(可含空格) |
| 整数 | IntegerArgumentType.integer() / integer(min, max) |
IntegerArgumentType.getInteger(ctx, "名") |
可限范围,越界自动报错 |
| 浮点数 | FloatArgumentType.floatArg() / DoubleArgumentType.doubleArg() |
FloatArgumentType.getFloat(ctx, "名") 等 |
同理可限范围 |
| 布尔值 | BoolArgumentType.bool() |
BoolArgumentType.getBool(ctx, "名") |
true / false |
| 单个玩家 | EntityArgumentType.player() |
EntityArgumentType.getPlayer(ctx, "名") → ServerPlayer |
必须精确指定一名在线玩家 |
| 多个玩家 | EntityArgumentType.players() |
EntityArgumentType.getPlayers(ctx, "名") → Collection<ServerPlayer> |
支持 @a、@p 等选择器 |
| 任意实体 | EntityArgumentType.entity() / entities() |
EntityArgumentType.getEntity(ctx, "名") |
玩家、生物、掉落物都算 |
| 物品 | ItemArgumentType.item(Commands.getRegistryAccess()) |
ItemArgumentType.getItem(ctx, "名") → ItemInput |
支持物品 ID,如 minecraft:diamond |
| 方块坐标 | BlockPosArgumentType.blockPos() |
BlockPosArgumentType.getLoadedBlockPos(ctx, "名") |
~ ~ ~ 相对坐标也能解析 |
| 资源路径 | ResourceLocationArgumentType.id() |
ResourceLocationArgumentType.getResourceLocation(ctx, "名") |
命名空间 ID,如 ffgs:ak47 |
| NBT 标签 | CompoundTagArgument.compoundTag() |
CompoundTagArgument.getCompoundTag(ctx, "名") |
1.20.1 可用,直接解析 NBT |
为什么 ItemArgumentType 需要传 Commands.getRegistryAccess()? 1.19.4 之后物品参数需要访问注册表(Registry)来校验物品 ID 是否存在,Commands.getRegistryAccess() 返回当前世界冻结的注册表访问器(RegistryAccess.Frozen)。这是 1.20.1 的固定写法:
拿到的 ItemInput 不是物品本体,需要进一步转换:
ItemInput input = ItemArgumentType.getItem(ctx, "item");
Item item = input.getItem(); // 物品类型
ItemStack stack = input.createItemStack(1, false); // 转成物品堆(数量, 是否允许超堆叠上限)
参数名的作用:Commands.argument("player", ...) 里的 "player" 是这个名字的“变量名”,执行回调里用同一个字符串取回解析结果。同一节点下的参数名不能重复,不同分支之间可以重名。
4. 执行回调与反馈消息¶
每个叶子节点通过 .executes(Command) 挂执行逻辑,Command 是一个函数式接口:
@FunctionalInterface
public interface Command<S> {
int run(CommandContext<S> context) throws CommandSyntaxException;
}
也就是说,回调返回一个 int。返回值约定:惯例返回 1 表示命令成功,0 表示失败;返回负值可用于条件分支场景,日常开发用 1/0 就够。配合游戏内的反馈消息,玩家就能明确知道命令到底成没成。
成功回执:sendSuccess¶
sendSuccess(Component, boolean) 有两个参数:
- 第一个是消息本体(
Component); - 第二个
boolean表示是否记录到服务器日志(并同步给在线管理员)。运营类命令(发枪、治疗)建议填true留审计痕迹;纯查询类命令填false避免刷屏日志。
失败回执:sendFailure¶
sendFailure(Component) 只有消息一个参数,玩家会看到红色的错误反馈。这是 1.20.1 的标准失败反馈 API。
消息组件¶
反馈消息用 Component(net.minecraft.network.chat.Component)构建:
Component.literal("纯文本")—— 快速测试用;Component.translatable("commands.ffgs.give.success", playerName, gunId)—— 正式项目推荐,配合语言文件(lang/zh_cn.json)做国际化,还能带占位符。
完整最小范例¶
把 2、3、4 节串起来:一个带整数参数、带成功/失败反馈的命令。
// /ffgs heal <amount> —— 给执行者回血
dispatcher.register(
Commands.literal("ffgs")
.then(Commands.literal("heal")
.then(Commands.argument("amount", IntegerArgumentType.integer(1, 20))
.executes(ctx -> {
int amount = IntegerArgumentType.getInteger(ctx, "amount");
ServerPlayer player = ctx.getSource().getPlayerOrException();
player.heal(amount);
ctx.getSource().sendSuccess(
Component.literal("已恢复 " + amount + " 点生命值"), false);
return 1;
})))
);
注意 IntegerArgumentType.integer(1, 20) 自带范围校验:玩家输入 0 或 99 时,Brigadier 会直接拒绝输入并提示参数不合法,根本走不到你的执行回调——不需要手写范围判断。
5. 建议(Suggestion):Tab 补全¶
参数节点挂 .suggests(...) 后,玩家按 Tab 就能看到可选项。两种做法:
5.1 内联 Lambda(简单场景)¶
.then(Commands.argument("gun", StringArgumentType.word())
.suggests((ctx, builder) ->
SharedSuggestionProvider.suggest(GunRegistry.allGunIds(), builder))
.executes(...))
SharedSuggestionProvider.suggest(Iterable<String>, SuggestionsBuilder) 是 1.20.1 最常用的建议工具方法,把字符串集合直接喂给补全器。
5.2 独立建议提供者类(推荐复用)¶
package com.example.ffgs.command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import java.util.concurrent.CompletableFuture;
/**
* 枪械 ID 建议提供者:为 /ffgs weapon give <玩家> <枪械> 提供 Tab 补全
*/
public class GunSuggestionProvider implements SuggestionProvider<CommandSourceStack> {
@Override
public CompletableFuture<Suggestions> getSuggestions(
CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
return SharedSuggestionProvider.suggest(GunRegistry.allGunIds(), builder);
}
}
用法:.suggests(new GunSuggestionProvider())。建议是动态的——每次 Tab 都会重新执行,所以枪械配置热重载后,补全列表立刻就是新的,不用重启服务器。
5.3 使用原版自带的建议器¶
物品参数可以直接复用原版的“可用物品”建议:
Commands.argument("item", ItemArgumentType.item(Commands.getRegistryAccess()))
.suggests(SuggestionProviders.AVAILABLE_ITEMS) // net.minecraft.commands.synchronization.SuggestionProviders
6. 权限控制¶
运营命令当然不能人人可敲。Brigadier 的节点支持 .requires(Predicate<S>) 做访问门控:
Commands.literal("weapon")
.requires(src -> src.hasPermission(2)) // 需要 2 级权限
.then(Commands.literal("give")...)
src.hasPermission(level) 是 CommandSourceStack 的权限判断方法,它对比的是操作员等级(op permission level):
| 等级 | 典型命令 | 说明 |
|---|---|---|
| 0 | 普通玩家命令 | 所有玩家默认 |
| 1 | /tell 等 |
绕过出生点保护 |
| 2 | /give、/effect、/tp |
大多数管理命令的门槛,推荐运营命令用这档 |
| 3 | /ban、/kick、/whitelist |
封禁级管理 |
| 4 | /op、/stop |
服务器最高权限 |
给玩家提权的方式:
- 控制台或管理员执行
/op 玩家名(服务器server.properties里的op-permission-level默认 4,被 op 的人默认 4 级); - 模组服务器可搭配 LuckPerms 之类的权限插件做细粒度权限组,插件会把权限桥接给
hasPermission判断(Forge 原生没有权限组概念)。
两个要点:
requires不仅阻止执行,还影响 Tab 补全——没权限的玩家在输入时根本看不到、补不出这些分支。- 建议做双重校验:
requires管“能不能进这个分支”,执行回调里再检查一次权限(尤其是你打算把命令分支组合复用的时候),防止逻辑被绕过:
private static int giveWeapon(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
if (!ctx.getSource().hasPermission(2)) {
ctx.getSource().sendFailure(Component.literal("你没有权限执行此命令"));
return 0;
}
// ... 发枪逻辑
return 1;
}
7. 完整示例:FFGS 枪战命令¶
把前面所有知识整合成一个可直接复制运行的完整示例。场景:落英枪战服务器(FFGS)的运营命令 /ffgs,包含:
/ffgs weapon give <玩家> <枪械ID>—— 给指定玩家发枪(2 级权限,枪械 ID 带 Tab 补全);/ffgs weapon list—— 列出当前可用枪械;/ffgs reload—— 热重载枪械配置(2 级权限,全服广播);/ffgs heal <目标> [数值]—— 治疗玩家,数值可省略(省略则回满)。
7.1 枪械注册表(示意)¶
package com.example.ffgs;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 枪械注册表(示意实现)。
* 正式项目中:枪械数据通常来自配置文件 / 数据包,reload() 里做热重载。
*/
public final class GunRegistry {
private static final Map<String, Item> GUNS = new LinkedHashMap<>();
static {
// 示意:用原版物品代替自定义枪械物品
GUNS.put("ak47", Items.IRON_AXE);
GUNS.put("m4a1", Items.DIAMOND_SWORD);
GUNS.put("awp", Items.BOW);
GUNS.put("deagle", Items.GOLDEN_SWORD);
}
private GunRegistry() {
}
public static Set<String> allGunIds() {
return GUNS.keySet();
}
public static boolean contains(String id) {
return GUNS.containsKey(id);
}
public static ItemStack createGunStack(String id) {
return new ItemStack(GUNS.getOrDefault(id, Items.AIR));
}
public static void reload() {
// 示意:实际项目中在这里重新读取配置文件 / 数据包
}
}
7.2 命令主类¶
package com.example.ffgs.command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import com.example.ffgs.FFGSMod;
import com.example.ffgs.GunRegistry;
import java.util.Collection;
/**
* FFGS 服务端命令。
* 挂在 FORGE 总线上(@Mod.EventBusSubscriber 默认总线),接收 RegisterCommandsEvent。
*/
@Mod.EventBusSubscriber(modid = FFGSMod.MODID)
public final class FFGSCommands {
private FFGSCommands() {
}
@SubscribeEvent
public static void register(RegisterCommandsEvent event) {
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
dispatcher.register(
Commands.literal("ffgs")
// ---- /ffgs weapon ... ----
.then(Commands.literal("weapon")
.requires(src -> src.hasPermission(2)) // 整棵 weapon 子树都需要 2 级权限
// /ffgs weapon give <player> <gun>
.then(Commands.literal("give")
.then(Commands.argument("player", net.minecraft.commands.arguments.EntityArgumentType.player())
.then(Commands.argument("gun", StringArgumentType.word())
.suggests(new GunSuggestionProvider()) // Tab 补全枪械 ID
.executes(FFGSCommands::giveWeapon))))
// /ffgs weapon list
.then(Commands.literal("list")
.executes(FFGSCommands::listWeapons)))
// ---- /ffgs reload ----
.then(Commands.literal("reload")
.requires(src -> src.hasPermission(2))
.executes(FFGSCommands::reloadGuns))
// ---- /ffgs heal <target> [amount] ----
.then(Commands.literal("heal")
.then(Commands.argument("target", net.minecraft.commands.arguments.EntityArgumentType.players())
// 不带数值:回满
.executes(ctx -> healPlayers(ctx, -1))
// 带数值:恢复指定生命值(1~20)
.then(Commands.argument("amount", IntegerArgumentType.integer(1, 20))
.executes(ctx -> healPlayers(ctx, IntegerArgumentType.getInteger(ctx, "amount"))))))
);
}
/** /ffgs weapon give <player> <gun> */
private static int giveWeapon(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
ServerPlayer target = net.minecraft.commands.arguments.EntityArgumentType.getPlayer(ctx, "player");
String gunId = StringArgumentType.getString(ctx, "gun");
// 枪械不存在 -> 失败回执
if (!GunRegistry.contains(gunId)) {
ctx.getSource().sendFailure(Component.literal("枪械不存在:" + gunId));
return 0;
}
ItemStack stack = GunRegistry.createGunStack(gunId);
if (!target.getInventory().add(stack)) {
// 背包满了就掉在地上,避免物品凭空消失
target.drop(stack, false);
}
ctx.getSource().sendSuccess(
Component.literal("已将枪械 [" + gunId + "] 发放给 " + target.getGameProfile().getName()),
true); // true:写入服务器日志,留审计痕迹
return 1;
}
/** /ffgs weapon list */
private static int listWeapons(CommandContext<CommandSourceStack> ctx) {
ctx.getSource().sendSuccess(
Component.literal("当前可用枪械:" + String.join(", ", GunRegistry.allGunIds())),
false);
return 1;
}
/** /ffgs reload */
private static int reloadGuns(CommandContext<CommandSourceStack> ctx) {
GunRegistry.reload();
// 全服广播,让在线玩家知道配置已热更新
ctx.getSource().getServer().getPlayerList().broadcastSystemMessage(
Component.literal("[FFGS] 枪械配置已重载"), false);
return 1;
}
/** /ffgs heal <target> [amount],amount < 0 表示回满 */
private static int healPlayers(CommandContext<CommandSourceStack> ctx, int amount) throws CommandSyntaxException {
Collection<ServerPlayer> targets = net.minecraft.commands.arguments.EntityArgumentType.getPlayers(ctx, "target");
for (ServerPlayer player : targets) {
player.setHealth(amount < 0 ? player.getMaxHealth() : amount);
}
ctx.getSource().sendSuccess(
Component.literal("已治疗 " + targets.size() + " 名玩家"), true);
return 1;
}
}
7.3 完整文件清单¶
src/main/java/com/example/ffgs/
├── FFGSMod.java # 主类,定义 MODID
├── GunRegistry.java # 枪械注册表
└── command/
├── FFGSCommands.java # 服务端命令注册 + 执行逻辑
└── GunSuggestionProvider.java # 枪械 ID Tab 补全
把 7.1、7.2、以及第 5 节的 GunSuggestionProvider 放进工程,运行游戏,试试:
/ffgs weapon list
/ffgs weapon give Steve ak47 # Tab 可补全 ak47
/ffgs heal Steve
/ffgs heal @a 5
/ffgs reload
没 op 的玩家敲 /ffgs weapon ... 时,连补全都看不到这些分支——requires 生效了。
8. 客户端命令¶
8.1 服务端命令 vs 客户端命令¶
| 维度 | 服务端命令 | 客户端命令 |
|---|---|---|
| 注册事件 | RegisterCommandsEvent |
RegisterClientCommandsEvent(net.minecraftforge.client.event) |
| 事件订阅 | @Mod.EventBusSubscriber(FORGE 总线) |
同上,但必须加 value = Dist.CLIENT |
| 命令源 | CommandSourceStack |
ClientCommandSourceStack(net.minecraftforge.client) |
| 执行线程 | 服务端主线程 | 客户端主线程(渲染线程) |
| 可见范围 | 全服玩家(命令由服务端下发) | 仅本机客户端 |
| 能碰的 API | 服务端世界、ServerPlayer、存档 |
客户端世界(ClientLevel)、本地玩家(LocalPlayer)、渲染 |
| 权限 | requires + hasPermission |
一般无权限概念(只影响本机) |
核心区别一句话:服务端命令的操作是“真”的(改世界、改玩家数据),因为命令执行在服务端;客户端命令只作用于本机表现(改 HUD、播放音效、发自定义数据包请求),任何需要影响世界的逻辑都必须走网络通道通知服务端。
8.2 注册客户端命令¶
package com.example.ffgs.command;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.ClientCommandSourceStack;
import net.minecraftforge.client.event.RegisterClientCommandsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import com.example.ffgs.FFGSMod;
/**
* 客户端命令示例。
* 注意:必须 value = Dist.CLIENT,且事件类来自 net.minecraftforge.client.event。
*/
@Mod.EventBusSubscriber(modid = FFGSMod.MODID, value = Dist.CLIENT)
public final class FFGSClientCommands {
private FFGSClientCommands() {
}
@SubscribeEvent
public static void register(RegisterClientCommandsEvent event) {
CommandDispatcher<ClientCommandSourceStack> dispatcher = event.getDispatcher();
dispatcher.register(
Commands.literal("ffgsping")
.executes(ctx -> {
ClientCommandSourceStack src = ctx.getSource();
src.sendSuccess(Component.literal("pong! 客户端命令执行成功"), false);
return 1;
})
);
}
}
ClientCommandSourceStack 常用方法:getPlayer()(LocalPlayer,可能为 null)、getEntity()、getLevel()(ClientLevel)、sendSuccess / sendFailure。注意它没有 getServer(),也没有 getPlayerOrException()——客户端根本没有 MinecraftServer。
客户端命令的坑:
- 客户端命令拿不到
CommandSourceStack,因此EntityArgumentType、ItemArgumentType这类面向服务端命令源的参数类型无法直接使用(类型不匹配,编译都过不了)。客户端命令一般只用字面量、字符串、整数等纯 Brigadier 参数。 - 不要与服务端命令重名:客户端和服务端的命令树会合并,同名命令可能互相覆盖、行为不可预测。客户端命令请用独立命名空间(如上例
ffgsping而不是ffgs)。 - 客户端命令要影响服务端世界时,唯一正道是发送数据包(Packet)给服务端处理,绝不能在客户端直接改世界数据。
9. 常见坑¶
-
事件总线挂错:
RegisterCommandsEvent在 Forge 总线上,订阅类bus参数必须是默认的Bus.FORGE。写成Bus.MOD命令永远不注册,且没有任何报错——排查时先看这里。 -
在服务端命令里引用客户端类:命令类里
import net.minecraft.client.*的任何类(Minecraft、Screen等),专用服务器一加载就ClassNotFoundException/NoClassDefFoundError崩溃。服务端命令只能碰net.minecraft.server.*和公共 API。反过来,客户端命令也不能碰MinecraftServer。拿不准就用Dist拆分到不同类。 -
反馈消息用错 API:1.19 之后
player.sendMessage(Component, UUID)已废弃,更别提player.chat()。统一用CommandSourceStack#sendSuccess(Component, boolean)/#sendFailure(Component)。要广播用server.getPlayerList().broadcastSystemMessage(Component, boolean)。 -
sendSuccess的两个参数:第二参boolean是“是否写日志/通知管理员”,不是“是否广播给所有人”。想全服广播要用broadcastSystemMessage,想发给特定玩家用player.sendSystemMessage(Component)。 -
1.20.1
CommandSourceStack方法速记: getPlayerOrException()—— 执行者必须是玩家,否则抛异常(适合玩家专属命令);getEntityOrException()—— 执行者可以是任意实体;getServer()——MinecraftServer;getLevel()——ServerLevel(服务端世界);getPosition()—— 执行位置;hasPermission(int)—— 权限判断;-
sendSuccess(Component, boolean)/sendFailure(Component)—— 反馈。 -
命令在服务端主线程执行:
executes里做文件 IO、网络请求、重型计算会卡服(所有玩家一起卡)。重活请丢到异步线程或 tick 里做,命令回调只做轻量校验和调度。 -
字符串参数选错类型:
StringArgumentType.string()不接受未加引号的空格;要“吃掉剩余全部输入”(比如一条留言),用greedyString();枪械 ID 这类无空格标识符用word()最合适。 -
整数/浮点范围:
IntegerArgumentType.integer(min, max)越界输入会被 Brigadier 直接拒绝并自动报错,别自己再写一套范围判断。但注意:范围判断只发生在解析阶段,别依赖它做权限校验。 -
同层同名节点重复注册:
.then(Commands.literal("give"))在同一个父节点下只能出现一次,否则IllegalArgumentException。需要多个“同前缀不同后缀”的分支时,在同一个.then(...)里层层嵌套,而不是并列写两个。 -
建议回调要轻量:
suggests的回调在命令树构建/补全时执行,不要阻塞、不要抛未捕获异常(会导致补全崩溃)。数据量小直接SharedSuggestionProvider.suggest(...)即可。 -
返回值约定:执行回调返回
int,惯例成功1、失败0。返回负值可用于“命令链”条件(then(...)链式执行判断),日常用 1/0 足够,别随手return 0后还告诉玩家“成功了”。 -
权限只做门控:
requires只控制“能否进入分支/补全”,不保证执行时一定安全。执行回调里再校验一次(见第 6 节),尤其是涉及给物品、改数据这类不可逆操作。 -
参数变量名前后一致:
Commands.argument("player", ...)与EntityArgumentType.getPlayer(ctx, "player")的字符串必须一字不差。改名只改一处 = 运行时CommandSyntaxException。 -
客户端命令别当服务端用:
RegisterClientCommandsEvent+ClientCommandSourceStack只影响本机。想实现“玩家点一下按钮服务端发枪”,正确链路是:客户端命令 → 发送数据包(Packet)→ 服务端处理并执行发枪。命令本身只是 UI 层的入口。
10. 小结¶
- 命令注册三步走:
@Mod.EventBusSubscriber(FORGE 总线)→@SubscribeEvent接收RegisterCommandsEvent→event.getDispatcher().register(命令树)。 - 命令树 = 字面量(literal)路由 + 参数(argument)取值 + 建议(suggests)补全 + 执行回调(executes)干活。
- 反馈统一
sendSuccess(Component, boolean)/sendFailure(Component),返回值 1 成功 / 0 失败。 - 权限用
requires(src -> src.hasPermission(2)),op 等级 2 是运营命令的常用门槛,执行回调里再兜底校验一次。 - 命令执行在服务端;客户端专属逻辑走
RegisterClientCommandsEvent+ClientCommandSourceStack,且只做本机表现,世界改动必须通过数据包回传服务端。 - 遇到“命令没反应”,先查总线、再查权限、再查参数名,最后查客户端/服务端类混用。
现在,去给你的 FFGS 服务器写第一串 /ffgs 命令吧。别忘了先在控制台 /op 你自己。