本帖最后由 754503921 于 2019-6-14 17:31 编辑

Forge | LiteLoader 与 Bukkit / Sponge 之间的通信
—— PluginMessengeChannel 与 FMLNetworkEvent
在实际开发中,如果你有一些天才的设想,比如借助 Forge 让你的插件服务器变得更加有特色,但是苦于无法进行数据传输的话,那么现在你就可以学习如何让 Bukkit / Sponge 和 Forge 之间传输信息了。
此教程适用于 1.7.10-1.12 (已测试) 1.13 可以看这里,1.7.10就把包名改成 cpw 那个就行,Bukkit的插件全版本通用。

概述
Bukkit / Sponge 与 Forge 通信的原理为:
服务端发送 PluginMessage 到 Forge 客户端,客户端使用 FMLNetworkEvent.ClientCustomPacketEvent 接受处理信息。
Forge 使用 FMLEventChannel 将 FMLNetworkPacket 发送至 Bukkit,Bukkit 服务器使用 PluginMessageListener 接受处理消息,Sponge 使用注册的频道添加的监听器 RawDataListener 处理消息。
教程使用的包名为 com.ilummc.msgtutor,主类为 MessageMain。

Bukkit 接收消息部分

不知道你在查阅 Bukkit 的 Javadocs 时有没有注意到这样一个包 org.bukkit.plugin.messaging,这就是用于通信的包。
首先你需要一个实现了 PluginMessageListener 的类,本教程我们将其命名为 MessageListener:
package com.ilummc.msgtutor;

import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;

public class MessageListener implements PluginMessageListener {

        @Override
        public void onPluginMessageReceived(String channel, Player player, byte[] data) {

        }
}
自动补全的方法 onPluginMessageReceived 为接收到消息时调用的方法,channel 为通道名称,data 为具体的数据内容。
(2018/7/25 补充) 自 1.13 后,bukkit 对 channel 的名称做出了限制,需要使用 namespace:name 的格式,比如 fmltutor:fmltutor
接着,你需要注册消息输入和输出的通道,通过 Messenger 类的 registerIncomingPluginChannelregisterOutgoingPluginChannel 方法完成,代码如下:
package com.ilummc.msgtutor;

import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;

public class MessageMain extends JavaPlugin {

        @Override
        public void onEnable() {
                // 注册消息接受通道
                Bukkit.getMessenger().registerIncomingPluginChannel(this, "msgtutor", new MessageListener());
                // 注册消息发送通道
                Bukkit.getMessenger().registerOutgoingPluginChannel(this, "msgtutor");
        }
}
registerIncomingPluginChannel 注册了接受消息的通道,和使用的 PluginMessageListener 实例,registerOutgoingPluginChannel 则注册了发送消息使用的通道。
到此你完成了Bukkit 接收消息的部分。

Forge 客户端接收消息部分

我们需要先注册一个通道,使用 NetworkRegistry 类的 newEventDrivenChannel 方法:
package com.ilummc.msgtutor;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.network.FMLEventChannel;
import net.minecraftforge.fml.common.network.FMLNetworkEvent;
import net.minecraftforge.fml.common.network.NetworkRegistry;

@SuppressWarnings("all")
@Mod(modid="msgtutor", version="tutor", name="MessageTutor")
public class MessageMain {
        static FMLEventChannel channel;
        
        @EventHandler
        public void preload(FMLPreInitializationEvent evt) {
                // 注册事件
                MinecraftForge.EVENT_BUS.register(this);
                FMLCommonHandler.instance().bus().register(this);
                // 注册通道
                channel = NetworkRegistry.INSTANCE.newEventDrivenChannel("msgtutor");
                channel.register(this);
        }
}
接着,我们添加一个监听器,监听 FMLNetworkEvent.ClientCustomPacketEvent 事件:
        @SubscribeEvent
        public void onClientPacket(FMLNetworkEvent.ClientCustomPacketEvent evt) {
                FMLLog.getLogger().info(new String(evt.getPacket().payload().array()));
        }
此事件的 getPacket 方法可以获得一个 FMLProxyPacket 实例数据包,这个实例的方法 payload 可以获得数据包携带的内容 ByteBuf,而 ByteBuf 实例的方法 array 则可以得到 byte[] 类型的数据。
(2018/7/8 补充) 在 forge 的 1.12.2 以后的版本,该事件的 ByteBuf 变成了一个 netty 魔性优化的实例,导致性能的上升以及 array() 方法的失效,你需要手动 new 一个数组然后用 readBytes 来读数据。
到此,你就可以接收来自服务器的消息了。

Sponge 接收消息部分

Sponge.getChannelRegistrar() 方法返回 message channel 的注册器,然后通过其 createRawChannel 方法注册一个新的 channel。
通过 addListener 添加新的监听器。
package com.ilummc.msgtutor;
// 省略导入
@Plugin(id = "msgtutor",
        name = "MessageTutor",
        version = "1.0-SNAPSHOT",
        authors = {"IzzelAliz"})
public class ServerGui {

    private static ChannelBinding.RawDataChannel channel;

    @Listener
    public void onServerStart(GameStartedServerEvent event) {
        // 注册频道
        channel = Sponge.getChannelRegistrar().createRawChannel(this, "msgtutor");
        // 添加监听器
        // PlatformType 指定监听来自哪里的信息,我们监听的是客户端,所以使用 CLIENT

        channel.addListener(Platform.Type.CLIENT, (data, connection, side) -> {
            // 将连接类型转换为 PlayerConnection
            if (connection instanceof PlayerConnection) {
                PlayerConnection conn = (PlayerConnection) connection;
                // 示例给玩家发送消息
                conn.getPlayer().sendMessage(Text.of(new String(data.array())));
            }
        });
    }
}

发送消息
Bukkit 发送消息给客户端的方法为
Bukkit.getPlayer("Izzel_Aliz").sendPluginMessage(Plugin plugin, String channel, byte[] data);
Forge 发送消息给服务器的方法为
byte[] array = ...; // 你要发送的消息的 byte 数组
ByteBuf buf = Unpooled.wrappedBuffer(array);
FMLProxyPacket packet = new FMLProxyPacket(new PacketBuffer(buf), "msgtutor"); // 数据包
channel  // FMLEventChannel 实例
    .sendToServer(packet);
Sponge 发送消息给客户端的方法为
ChannelBinding.RawDataChannel channel = ... ; // 你注册的 channel 实例
Player player = .... ; // 目标玩家
channel.sendTo(player, channelBuf -> channelBuf.writeByteArray("发送的消息").getBytes());
// ChannelBuf 有大量方法,可以写入读取不同种类的数据,使用与 ByteBuf 类似
你可以发送任何东西,只要能将其作为 byte 数组发送。byte 数组的长度限制为 32766 字节。

LiteLoader 接受/发送
请移步 @ustc_zzzz 大佬的帖子的章节 与服务端插件交互 http://www.mcbbs.net/thread-659755-1-1.html
关于线程安全

以上接收时的事件全部是在网络线程被触发,所以对于线程不安全的Minecraft来说,线程安全问题需要额外注意。
由于本人对 Forge 的操作并不是很熟练,所以只能以 Bukkit 作为例子,如果你接收到的信息只是一条字符串,并且你只是想将其发送给玩家(Player#sendMessage),那么你可以随意在网络线程中使用,因为这个方法是线程安全的;但是,如果你需要进行踢出(Player#kickPlayer),那么你必须在主线程(Server Thread)进行这个操作,否则可能得到一个报错、崩溃或者意想不到的结果(尽管 Bukkit 会阻止 Async Kick 的行为并发出警告)。
那么,我们可以用以下的方法将数据转交给主线程处理:
  • 使用 Bukkit 或者 Sponge 的调度器

  • 利用管道IO

ByteBuf 的简单使用

ByteBuf 类被提供于 io.netty 包中,使用可以通过 Maven 导入
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-buffer</artifactId>
    <version>4.1.20.Final</version>
</dependency>
使用 Gradle 导入
compile group: 'io.netty', name: 'netty-buffer', version: '4.1.20.Final'
ByteBuf 可以说是 java.nio.ByteBuffer 类的加强,主要有以下优点:
  • 读写指针分离,不用调用 flip() 方法来切换读写状态
  • 写入时可以自动增加容量
  • 提供了 Unpooled 和 Pooled 两种用于不同的场景

创建一个 ByteBuf 的方法很多,比如
Unpooled.buffer() // 创建一个普通的 ByteBuf
Unpooled.wrappedBuffer(byte[]) // 从已有 byte 数组创建
Pooled.buffer() // 创建一个高并发优化的 ByteBuf
Unpooled 和 Pooled 类重载的方法还有很多,详情可以查阅 Javadocs
另及:Minecraft 还叫不上高并发,Pooled 的高并发优化对于 Minecraft 大概没啥用

写入/读取
ByteBuf buf = ... ;
buf.writeInt(int); // 写入 int 型数据
buf.writeBytes(byte[], int, int); // 写入 byte 数组,后两个参数分别是 offset 和 length
buf.readLong(); // 返回一个 long 型数据
buf.toString(int, int, Charset); // 将 ByteBuf 内部的 byte[] 转换为 String 型数据,前两个参数分别是 offset 和 length
重载的方法还有很多,基本什么数据都能往里写,这里介绍一些实用的示例。

ByteBuf 里的两个读写指针,分别可以通过 readerIndex 和 writerIndex 方法获得
ByteBuf 内部维护了一个 byte 数组,其中 ByteBuf 的 capacity 为数组长度,可以通过 getCapacity 方法获得
0 <= readerIndex <= writerIndex <= capacity
  • 0 到 readerIndex 之间的数组区域称为 discardable bytes,discardReadBytes 方法可以将数组从 readerIndex 之后的部分移动到 0,从而增大可用区。
  • readerIndex 到 writerIndex 之间的数组区域称为 readable bytes,这一部分可以进行读取,每次读取之后,readerIndex 都会相应增加(增加数据长度,如 readLong 就会增加 Long.BYTES)。
  • writerIndex 到 capacity 之间的数组区域称为 writable bytes,这一部分可以写入,每次写入之后,writerIndex 都会相应增加(如 writeShort 就会增加 Short.BYTES)。如果写入的长度大于 capacity - writerIndex,则自动扩容。


后记 & 常用链接 & 补充阅读

Forge的通信 http://www.mcbbs.net/thread-711966-1-1.html
Spigot Javadocs https://hub.spigotmc.org/javadocs/spigot/
Sponge Javadocs https://jd.spongepowered.org/5.1.0/?overview-summary.html
Forge 1.7.10的Javadocs http://jd.ddmcloud.com/forge/1.7.10/
Netty Javadocs http://netty.io/4.1/api/index.html
ByteBuf http://blog.csdn.net/z69183787/article/details/52980426
http://www.2cto.com/kf/201604/500997.html
线程通信 http://www.cnblogs.com/hapjin/p/5492619.html
你可以用你的天才般的设想,让插件服务器玩起来像 Mod 一样,一切都是有可能的
我是一名 Bukkit 插件开发者,碰巧会一丁点的 Forge 开发,甚至刚学了一点 Sponge 开发。
使用 Forge 1.8.9 作为示范。


[groupid=1330]PluginsCDTribe[/groupid]