当前位置: 首页 > news >正文

Fabric 服务端插件开发简述与聊天事件监听转发

原文链接:Fabric 服务端插件开发简述与聊天事件监听转发 < Ping通途说

0. 引言

以前写过Spigot的插件,非常简单,仅需调用官方封装好的Event类即可。但Fabric这边在开发时由于官方文档和现有互联网资料来看,可能会具有一定的误导性,使得开发者使用更加底层的Mixin注入开发。但后来才发现官方已经封装了这些实现方法,为了避免后续开发者踩坑,也能让开发者明白Fabric的技术实现,于是才写本文。

本文以构建一个监听游戏内玩家聊天事件、玩家进入服务器事件、玩家离开服务器事件,并遵循OnebotV11协议转发到对应服务器的Fabric服务端插件。

环境搭建与项目创建本文不再阐述,有关资料可参考:

  1. 设置开发环境 | Fabric 文档 - Fabric 的环境搭建
  2. Template mod generator | Fabric -  Fabric 模板模组生成器
  3. Minecraft 模组编写基础 [Fabric Wiki] - Fabric的基本介绍
  4. 教程:更新Java - 中文 Minecraft Wiki - 各版本Minecraft最低JAVA版本要求

1. 直接调用Event方法

1.1 获取Event API方法

初学者可能会被官方文档迷惑,在监听事件 [Fabric Wiki]中,只看到了几个官方封装的方法,自己想要的类并不在以下列表中:

点击链接后又发现跳转到了项目源码中,如果对Java不熟悉的开发者可能就不明白为何会跳转到源码,然后我应该看什么。虽然 事件 | Fabric 文档 该文档中简单描述了事件监听的基本过程,但依然没有列出想要事件列表。而官方描述的 net.fabricmc.fabric.api.event ,在进入源代码后发现也只不过是所有Event的抽象方法。

那么真正的Event API到底在哪,官方文档没有像Bukkit/Spigot那样详细的列出来,我们只能借助Deepseek来查询。通过查询后,本文中我们需要监听的聊天事件,就在net.fabricmc.fabric.api.message.v1.ServerMessageEvents.CHAT_MESSAGE中。

进入 ServerMessageEvents查看,可以看到除玩家聊天事件外,还有游戏消息事件和执行指令事件。

再通过AI我们可以找到玩家进入服务器(ServerPlayConnectionEvents.JOIN

)和玩家离开服务器(ServerPlayConnectionEvents.DISCONNECT

)的事件。而该包存在于net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents 类中,因此可以猜测,官方似乎放弃了对net.fabricmc.fabric.api.event的维护,将所有Event分散到不同的模块中。

对于API的查询,官方可能已经放出了相关文档,但普通的开发者无法通过正常的方式通过互联网搜索或从官网中进入。在此官方改善该问题前只能通过AI辅助查找方法实现。

1.2 插件开发

找到API那开发起来就非常简单了,直接在插件初始化中注册事件:

public class EventForwardobv11 implements ModInitializer {public static final String MOD_ID = "event-forward-obv11";public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);public static ModConfig CONFIG;@Overridepublic void onInitialize() {CONFIG = ConfigManager.loadConfig();ServerMessageEvents.CHAT_MESSAGE.register((message, sender, params) -> {HttpUtil.sendGet("[" + sender.getName().getString() + "] " + message.getContent().getString());});ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {ServerPlayerEntity player = handler.player;HttpUtil.sendGet(player.getName().getString() + "加入服务器 (" + (server.getCurrentPlayerCount() + 1) + "/" + server.getMaxPlayerCount() + ")");});ServerPlayConnectionEvents.DISCONNECT.register((handler, sender) -> {ServerPlayerEntity player = handler.player;HttpUtil.sendGet(player.getName().getString() + "退出服务器");});}
}

注册完成后,我们需要将事件通过OnebotV11协议转发至群中,查看协议API,发送群消息的接口是 /send_group_msg 。对于单个小型Minecraft 服务器,请求应该不会大到哪里去,因此我们选择HTTP GET的方式向服务器发送请求,这里需要封装一个请求方法:

// GET 方法实现
class HttpUtil {public static void sendGet(String message) {ModConfig config = EventForwardobv11.CONFIG;String url = "http://"+config.obServer + ":" + config.obPort + "/send_group_msg?access_token=" + config.obToken + "&group_id=" + config.forwardGroup + "&message=" + URLEncoder.encode(message, StandardCharsets.UTF_8);try (CloseableHttpClient client = HttpClients.createDefault()) {HttpGet get = new HttpGet(url);HttpResponse response = client.execute(get);switch (response.getStatusLine().getStatusCode()) {case 401:EventForwardobv11.LOGGER.error("鉴权失败,当前请求需要Token");break;case 403:EventForwardobv11.LOGGER.error("鉴权失败,Token无效");break;case 404:EventForwardobv11.LOGGER.error("请求失败,无效接口:{}",url);}// 主要是根据状态码判断错误,正常返回结果直接忽略} catch (Exception e) {EventForwardobv11.LOGGER.error("发送消息失败:{}", e.getMessage());}}
}

关于代码中的异常处理,根据 HTTP | OneBot 11 标准只有在请求失败时才会返回200以外的状态码。服务器返回200对插件没有任何作用,因此只需关注其他状态码并处理异常即可。

服务器地址与端口是不确定的,因此需要用户来自己配置,我们来自己实现一个配置类。本插件是服务端使用的,因此无需像客户端那样展示GUI配置界面,因此可以利用GSON来生成读取配置:


class ModConfig { // 遵循Onebotv11协议发送事件public String obServer = "127.0.0.1"; // Onebotv11服务器地址public int obPort = 8080; // http端口public String obToken = ""; // 鉴权tokenpublic String forwardGroup = ""; // 要转发的群聊
}class ConfigManager {private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();private static final Path CONFIG_PATH = Paths.get("config", "event-forward-obv11.json");public static ModConfig loadConfig() {try {File configFile = CONFIG_PATH.toFile();if (!configFile.exists()) {return createDefaultConfig(); // 如果文件不存在,创建默认配置}try (FileReader reader = new FileReader(configFile)) {return GSON.fromJson(reader, ModConfig.class); // 读取配置}} catch (Exception e) {System.err.println();EventForwardobv11.LOGGER.error("无法加载配置文件,使用默认配置:{}", e.getMessage());return new ModConfig();}}public static ModConfig createDefaultConfig() {try {File configFile = CONFIG_PATH.toFile();configFile.getParentFile().mkdirs(); // 确保目录存在ModConfig defaultConfig = new ModConfig();try (FileWriter writer = new FileWriter(configFile)) {GSON.toJson(defaultConfig, writer); // 写入默认配置}return defaultConfig;} catch (Exception e) {EventForwardobv11.LOGGER.error("无法创建默认配置文件:{}", e.getMessage());return new ModConfig();}}

现在,插件全部功能已经实现。将构建后的插件装载至服务端的mod文件夹,当玩家进出服务器和聊天时就会转发事件:

2. Mixin注入简述与示例

Mixin是我刚接触Fabric就误入歧途了解的东西。Mixin的主要用途是修改基本游戏中已存在的代码,可以是通过注入自定义逻辑移除机制或者修改值

我是完全参考该项目进行学习与开发的: GitHub - IotaBread/player-events: Fabric mod. Sends a configurable message/command when a player joins the server.

这个项目实现了对玩家事件的监听,例如玩家进服、玩家出服、玩家死亡等事件。正好缺少监听聊天事件,我们可以通过学习项目中玩家进出服的实现,来注册一个我们自己实现的聊天事件监听器!

先来到插件入口,可以看到自定义回调在这里注册了,回调参数通过读取Config文件来获取并传递到函数中。

2.1 玩家进入服务器事件监听器

以玩家加入服务器事件监听器为例子,找到PlayerJoinCallback(api/src/main/java/me/bymartrixx/playerevents/api/event/PlayerJoinCallback.java)。这里是自定义事件的处理模块,

随后继续查阅文件,可以发现在PlayerManagerMixin(api/src/main/java/me/bymartrixx/playerevents/api/mixin/PlayerManagerMixin.java)中引用了以上方法。可以看到Mixin在代码中出现了,其中使用了@Injects向Minecraft源代码中注入了捕获玩家连接的事件。

关于注入(Injects):允许你在已存在的模组中的特定位置放置自定义的代码。

其中,at参数代表当前代码段将会在Minecraft函数中何时执行,method参数则代表代码段会在Minecraft哪个函数中执行。可以看到代码中使用了 TAIL,即代表在Minecraft中onPlayerConnect函数在即将返回值时运行注入的代码。

另外的if判断如果玩家离开服务器的次数少于1次,那就可以判定该玩家是第一次进入服务器,随后再将事件传递到专门的监听器中。

再讲讲代码中的 CallbackInfoCallbackInfo(或 CallbackInfoReturnable)用于控制 原方法的执行流程。它是 Mixin 注入点时的一个参数,可以:

  • 取消原方法执行ci.cancel())。
  • 提前返回ci.setReturnValue(...),仅限 CallbackInfoReturnable)。
  • 获取方法执行状态(如是否已被取消)

什么时候用 CallbackInfoCallbackInfoReturnable<T>

  • 用 CallbackInfo:当目标方法是 void 或无返回值修改需求时。
  • 用 CallbackInfoReturnable<T>:当需要修改返回值时(如 public boolean someMethod())。

2.2 玩家离开服务器事件监听器

再来看看玩家离开服务器事件监听器的代码:

可以看到,除调用方法名称有区别外,与玩家进入服务器的代码基本一致。

同理,再前往调用了PlayerLeaveCallback函数的Mixin中查看:

哦豁,跟玩家进入服务器注入器代码有很大区别,多了很多东西。第一个注入器监听的是指令被执行后的事件,第二个注入器监听的则是玩家离开服务器事件。

首先是 @Shadow ,@Shadow 用于 “声明” 目标类(被 Mixin 的类)的 字段或方法,但并不实际修改它们。它只是让 Mixin 知道:“这个字段/方法在目标类中存在,我可以访问它”。不会生成任何实际代码,仅仅是一个引用标记。

在实际代码中,ServerPlayNetworkHandler 类(原版 Minecraft)有一个 player 字段,存储当前连接的玩家。@Shadow public ServerPlayerEntity player; 的作用是,告诉 Mixin:“ServerPlayNetworkHandler 里有一个 player 字段,我要用它”。这样你就可以在 Mixin 中直接通过 this.player 访问原版字段。

两个函数的at都使用了INVOKE,这代表当前方法将会在Minecraft对应函数被调用前执行。那target参数又是什么呢,为何他的值又是一长串?method参数为什么也是一长串?

首先@At 的 target是指定注入点的目标方法,它指向的是 目标类中的某个方法或字段(通常是原版 Minecraft 的方法)。指定的是 “在哪个方法调用或字段访问时触发注入”。直接看target地址:

Lnet/minecraft/server/network/ServerPlayNetworkHandler;checkForSpam()V

  • 当 ServerPlayNetworkHandler.checkForSpam() 被调用时,触发注入。
  • 格式:L包路径/类名;方法名(参数类型)返回值类型(JVM 字节码描述符)。

这段的知识面涉及JVM的标准设计第 4 章.类 File Format,字段描述符可以参考:

字段描述符原名描述
Bbyte带符号的字节
CcharBasic Multilingual Plane 中的 Unicode 字符代码点,使用 UTF-16 编码
Ddouble双精度浮点值
Ffloat单精度浮点值
Iint正数
Jlong长整数
L类名称;reference类名称的实例
Sshort带符号的短整型
Zbooleantrue 或 false
[reference单数组维度

而4.3.3的方法描述符基本与字段描述符一致,只是多了一个 V 用于表示 void

以下是Fabric文档中的官方描述:

再来看看 method,它指定的是要注入的目标方法,用于告诉 Mixin “你要修改的是哪个方法”,指向的是 被注入的目标方法(即你要修改的方法)。

method = "method_44356(Lnet/minecraft/class_7472;Ljava/util/Optional;)V"

因此这里的 method 表示:

  • 要修改的方法是 method_44356(可能是混淆后的名称)。
  • 它的参数是 ChatCommandC2SPacket(class_7472)和 Optional<MessageSignatureList>。
  • 返回值是 void(V)。

那么如何找到以上我们需要的地址?这就需要反编译Minecraft代码了:配置模组开发环境 [Fabric Wiki]

Fabric已经配置好了Gradle任务,直接运行就可以了,非常的方便。

一般情况下仅需指定target类地址就可以了,如果在实际运行中出现无法找到对应方法的问题,就需要再去指定method的地址。比如例子中的checkForSpam,通过搜索源代码(客户端版本:1.21.5)发现,该方法存在于:

c ate net/minecraft/class_3244 net/minecraft/server/network/ServerPlayNetworkHandler
...
m ()V r method_43669 checkForSpam

因此如果想要让执行指令后监听器生效,那就需要将代码修改为:
@Inject(at=@At(value="INVOKE", target="Lnet/minecraft/server/network/ServerPlayNetworkHandler;checkForSpam()V"),method = "method_43669(Lnet/minecraft/class_3244;Ljava/util/Optional;)V", require = 0)
所以,指定地址的弊端就出现了,客户端一更新,插件就需要重新反编译Minecraft代码来获取对应方法地址。

2.3 一起来实现聊天事件监听!

根据以上我们了解的知识,可以模仿来创作一个新的自定义Mixin!

@Mixin(ServerPlayNetworkHandler.class)
public abstract class ServerPlayNetworkHandlerMixin {@Inject(method = "handleMessage", at = @At("HEAD"))private void onChatMessage(Text message, CallbackInfo ci) {ServerPlayerEntity player = ((ServerPlayNetworkHandler)(Object)this).player;MinecraftServer server = player.getServer();// 触发你的回调事件ActionResult result = PlayerChatCallback.EVENT.invoker().playerChat(player, server);}
}

以及自定义回调事件:

public interface PlayerChatCallback {Event<PlayerChatCallback> EVENT = EventFactory.createArrayBacked(PlayerChatCallback.class,(listeners)->(player, server)->{for (PlayerChatCallback listener: listeners){listener.playerChat(player,server);}});void playerChat(ServerPlayerEntity player, MinecraftServer server);
}

最后在初始化类中注册这个监听器:

public class MyMod implements ModInitializer {@Overridepublic void onInitialize() {// 注册聊天事件处理器PlayerChatHandler.register();}
}

可不要忘记 mixin 配置,让插件知道你创建了新的Mixin

在 src/main/resources/mixins.modid.json 中添加你的 mixin,package为存放Mixin的目录。

{"package": "com.example.mymod.api.mixin","mixins": ["ServerPlayNetworkHandlerMixin"]
}

3. Fabric 与 Bukkit区别

1. 架构设计差异

Bukkit/Spigot/PaperFabric
目标定位为服务器插件提供高层 API,屏蔽底层实现提供底层 Mod 支持,允许直接修改 Minecraft 代码
事件系统提供完整的 事件总线,插件直接注册监听器部分事件需通过 Mixin 注入 或 Fabric API 提供的事件
底层访问权限限制较多,只能通过 Bukkit API 操作允许直接修改游戏代码(通过 Mixin)
兼容性维护由 Bukkit/Spigot 团队维护 API 兼容性依赖社区和 Fabric 团队,部分 API 可能变动

2. 底层实现对比

Bukkit 的实现方式

  1. Bukkit 在服务端启动时,通过反射/CraftBukkit 修改 Minecraft 代码,插入事件触发点。
  2. 插件开发者无需关心注入逻辑,直接使用封装好的 Event 类。

// 伪代码:Bukkit 在底层注入事件调用
public void handleChatPacket(Packet packet) {Player player = getPlayerFromPacket(packet);PlayerChatEvent event = new PlayerChatEvent(player, packet.getMessage());Bukkit.getPluginManager().callEvent(event); // 触发事件if (!event.isCancelled()) {originalHandlePacket(packet); // 继续原逻辑}
}

Fabric 的实现方式

  1. Fabric 本身不修改原版代码,而是通过 Mixin 在编译时动态注入。
  2. 事件系统需要手动实现(或依赖 Fabric API 提供的事件)。
  3. 开发者可以完全控制事件行为,但需自行维护兼容性。

// 伪代码:Fabric 通过 Mixin 注入事件
@Inject(method = "handleChat", at = @At("HEAD"), cancellable = true)
private void onChat(MessagePacket packet, CallbackInfo ci) {PlayerChatEvent event = new PlayerChatEvent(player, packet.getMessage());EventManager.fireEvent(event); // 自定义事件总线if (event.isCancelled()) {ci.cancel(); // 取消原逻辑}
}

相关文章:

  • 【C++ 基础数论】质数判断
  • AI大模型中系统化的KV Cache加速方案,减少KV Cache显存占用的优化方法
  • AI推介-大语言模型LLMs论文速览(arXiv方向):2024.11.25-2024.11.30
  • 【打破信息差】萌新认识与入门算法竞赛
  • QBasic 一款古老的编程语言在现代学习中的价值(附程序)
  • 刷leetcodehot100返航版--双指针5/16
  • 西安前端面试
  • 机器学习中的特征工程:解锁模型性能的关键
  • 计算机组成原理——数据的表示
  • 代码随想录 算法训练 Day3:链表1
  • 隧道结构安全在线监测系统解决方案
  • 从裸机开发到实时操作系统:FreeRTOS详解与实战指南
  • 英飞凌TLE9945GPT12
  • SSH主机密钥验证失败:全面解决方案与技术手册
  • 【言语】刷题5(填空)
  • MySQL数据库——支持远程IP访问的设置方法总结
  • 【RabbitMQ】消息丢失问题排查与解决
  • 将单链表反转【数据结构练习题】
  • (for 循环) VS (LINQ) 性能比拼 ——c#
  • 半导体供应链B2B集成时应该注意的要点
  • 泉州围头湾一港区项目炸礁被指影响中华白海豚,官方:已叫停重新评估
  • 温州通报“一母亲殴打女儿致其死亡”:嫌犯已被刑拘
  • 南京江宁区市监局通报:盒马一批次猕猴桃检出膨大剂超标
  • 《上海市建筑信息模型技术应用指南(2025版)》发布
  • 国防部:菲方应停止一切侵权挑衅危险举动,否则只会自食苦果
  • 哪种“网红减肥法”比较靠谱?医学专家和运动专家共同解答