NetAllay 是一个用于 AllayMC 服务器的网易 PyRpc 通信 API 插件,提供类似于 NukkitMaster 的通信接口,让插件开发者能够轻松地与网易版 Minecraft 客户端进行双向通信。
- 监听客户端发送的 PyRpc 事件
- 向单个/多个玩家发送服务端事件
- 支持按位置范围广播事件
- 支持按世界/维度广播事件
- 自动处理 MsgPack 编解码
- 线程安全的事件注册机制
- 从 GitHub Releases 下载
NetAllay-x.x.x.jar - 将 jar 文件放入服务器的
plugins目录 - 重启服务器
如果你的插件需要使用 NetAllay,可以直接通过 Maven Central 引入:
repositories {
mavenCentral()
}
dependencies {
compileOnly("org.allaymc:net-allay:<version>")
}
allay {
plugin {
dependency("NetAllay")
}
}import org.allaymc.netallay.NetAllay;
// 方式一:直接获取静态实例
NetAllay netAllay = NetAllay.getInstance();
// 方式二:通过插件管理器获取
NetAllay netAllay = (NetAllay) Server.getInstance()
.getPluginManager()
.getPlugin("NetAllay");使用 listenForEvent 方法注册事件监听器,当客户端发送匹配的事件时,回调函数会被调用。
netAllay.listenForEvent(
"MyMod", // namespace - 客户端系统的命名空间
"MySystemCS", // systemName - 客户端系统名称
"OnButtonClick", // eventName - 事件名称
(player, data) -> {
// player - 发送事件的玩家
// data - 事件数据 (Map<String, Object>)
String buttonId = (String) data.get("buttonId");
pluginLogger.info("玩家 {} 点击了按钮: {}", player.getOriginName(), buttonId);
}
);// 取消特定处理器
netAllay.unlistenForEvent(namespace, systemName, eventName, handler);
// 取消某个事件的所有处理器
netAllay.unlistenAllForEvent(namespace, systemName, eventName);Map<String, Object> data = new LinkedHashMap<>();
data.put("message", "Hello from server!");
data.put("timestamp", System.currentTimeMillis());
boolean success = netAllay.notifyToClient(
player, // 目标玩家
"MyMod", // namespace - 客户端监听的命名空间
"MySystemSS", // systemName - 服务端系统名称
"ServerMessage", // eventName - 事件名称
data // 事件数据
);netAllay.notifyToClientImmediately(player, namespace, systemName, eventName, data);List<Player> players = Arrays.asList(player1, player2, player3);
int successCount = netAllay.notifyToMultiClients(
players,
"MyMod",
"MySystemSS",
"Announcement",
Map.of("content", "服务器公告内容")
);Location3dc center = player.getControlledEntity().getLocation();
int count = netAllay.notifyToClientsNearby(
null, // except - 排除的玩家,可为 null
center, // 中心位置
50.0, // 半径(方块)
"MyMod",
"MySystemSS",
"NearbyEvent",
Map.of("type", "explosion")
);World world = Server.getInstance().getWorldPool().getWorld("world");
netAllay.broadcastToAllClient(
null, // except - 排除的玩家
world, // 目标世界
"MyMod",
"MySystemSS",
"WorldEvent",
data
);Dimension dimension = world.getOverWorld();
netAllay.broadcastToAllClient(
exceptPlayer, // 排除的玩家
dimension, // 目标维度
"MyMod",
"MySystemSS",
"DimensionEvent",
data
);netAllay.broadcastToAllClient(
null, // except - 排除的玩家
"MyMod",
"MySystemSS",
"GlobalEvent",
data
);事件数据 Map<String, Object> 支持以下值类型:
| Java 类型 | 说明 |
|---|---|
null |
空值 |
Boolean |
布尔值 |
Integer / Long |
整数 |
Float / Double |
浮点数 |
String |
字符串 |
byte[] |
二进制数据 |
Map<String, Object> |
嵌套对象 |
List<?> / Iterable<?> |
数组 |
// 使用 -2 表示接收消息的玩家自己的实体
NetAllay.LOCAL_PLAYER_ENTITY_ID // 值为 -2注意: 只能在 notifyToClient 中使用 -2,不要在广播方法中使用,因为 -2 对每个玩家代表不同的实体。
package com.example.myplugin;
import org.allaymc.api.eventbus.EventHandler;
import org.allaymc.api.eventbus.event.server.PlayerJoinEvent;
import org.allaymc.api.plugin.Plugin;
import org.allaymc.api.server.Server;
import org.allaymc.netallay.NetAllay;
import java.util.LinkedHashMap;
import java.util.Map;
public class MyPlugin extends Plugin {
@Override
public void onEnable() {
Server.getInstance().getEventBus().registerListener(this);
// 注册 PyRpc 事件监听
registerPyRpcHandlers();
pluginLogger.info("MyPlugin enabled!");
}
private void registerPyRpcHandlers() {
NetAllay netAllay = NetAllay.getInstance();
// 监听客户端请求
netAllay.listenForEvent("MyMod", "MySystemCS", "RequestData", (player, data) -> {
pluginLogger.info("收到来自 {} 的数据请求", player.getOriginName());
// 构建响应数据
Map<String, Object> response = new LinkedHashMap<>();
response.put("success", true);
response.put("playerName", player.getOriginName());
response.put("serverTime", System.currentTimeMillis());
// 发送响应
netAllay.notifyToClient(
player,
"MyMod",
"MySystemSS",
"ResponseData",
response
);
});
// 监听客户端操作
netAllay.listenForEvent("MyMod", "MySystemCS", "PerformAction", (player, data) -> {
String action = (String) data.get("action");
switch (action) {
case "jump" -> player.getControlledEntity().setMotion(0, 1, 0);
case "heal" -> player.getControlledEntity().setHealth(20);
default -> pluginLogger.warn("未知操作: {}", action);
}
});
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
var player = event.getPlayer();
if (player.isNetEasePlayer()) {
// 向网易玩家发送欢迎消息
NetAllay.getInstance().notifyToClient(
player,
"MyMod",
"MySystemSS",
"Welcome",
Map.of(
"message", "欢迎来到服务器!",
"entityId", NetAllay.LOCAL_PLAYER_ENTITY_ID
)
);
}
}
@Override
public void onDisable() {
// 清理事件监听
NetAllay netAllay = NetAllay.getInstance();
if (netAllay != null) {
netAllay.unlistenAllForEvent("MyMod", "MySystemCS", "RequestData");
netAllay.unlistenAllForEvent("MyMod", "MySystemCS", "PerformAction");
}
}
}在网易客户端 Mod 中,使用以下方式与服务端通信:
# Python 客户端示例
import mod.client.extraClientApi as clientApi
ClientSystem = clientApi.GetClientSystemCls()
class MyClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ListenForEvent("MyMod", "MySystemSS", "ServerMessage", self, self.OnServerMessage)
def OnServerMessage(self, args):
message = args.get("message")
print("收到服务端消息:", message)def SendToServer(self, eventName, data):
self.NotifyToServer("MyMod", "MySystemSS", eventName, data)
# 使用
self.SendToServer("RequestData", {"type": "player_info"})NetAllay 内置网易官方的皮肤反作弊(ConfirmSkinPacket 方案):服务端在向网易客户端确认皮肤之前,先向网易 Web 服务校验皮肤 MD5;未通过校验的皮肤不会被确认,客户端始终显示默认皮肤。功能默认开启,可在 plugins/NetAllay/config.json 中关闭或调整。
- Allay 广播
PlayerListPacket/PlayerSkinPacket时,插件记录每个皮肤的 persona 标记与skinId; - Allay 发送
ConfirmSkinPacket时,插件拦截该包,把需要校验的皮肤扣下(原本一并确认的合法条目会立即补发); - 插件把
md5(base64(皮肤数据))、玩家 NetEase UID、md5(geometry)一次性提交到/pe/web/skin-md5-valid-inner; - 返回合法的皮肤以新的
ConfirmSkinPacket补发,非法皮肤直接丢弃(客户端继续显示史蒂夫)。
persona 皮肤与 .NonsyncCustom 皮肤按官方方案跳过校验。校验结果按 uuid + 皮肤MD5 + geometry MD5 + uid 做并发去重,但不会缓存——官方要求每次都要校验,因为接口同时会检查玩家是否真的购买了对应皮肤组件。
{
"skinAntiCheat": {
"enabled": true,
"sign": "62bd6025d29b17ff",
"testServer": false,
"webServerUrl": "",
"timeoutMillis": 5000,
"maxBatchSize": 32,
"checkGeometry": true,
"skipPersonaSkin": true,
"checkUnknownSkin": true,
"failOpenOnError": false,
"onErrorRetries": 1,
"trustNonPlayerSkins": true,
"whitelistUids": [],
"logDeniedSkin": true,
"logPassedSkin": false
}
}| 字段 | 说明 |
|---|---|
enabled |
总开关,默认 true |
sign |
官方固定的签名值 |
testServer |
使用测试环境接口(该环境永远返回合法),便于先跑通链路 |
webServerUrl |
自定义接口地址,可填完整 URL 或裸域名 |
timeoutMillis |
单次请求超时(毫秒) |
maxBatchSize |
单次请求最多提交的皮肤数量,超出自动分批 |
checkGeometry |
是否携带 geo_list(2.1+ 客户端必需;缺少 geometry 的皮肤会自动单独成批、不带该字段) |
skipPersonaSkin |
跳过 persona / .NonsyncCustom 皮肤(官方要求跳过) |
checkUnknownSkin |
拿不到 persona / skinId 信息时是否仍然校验 |
failOpenOnError |
请求失败时是否放行皮肤;false(默认)为官方行为——不确认即隐藏 |
onErrorRetries |
请求失败重试次数 |
trustNonPlayerSkins |
NPC / 假人皮肤跳过校验(其 UID 是本地生成的,官方接口必然校验失败,默认跳过) |
whitelistUids |
免校验的 NetEase UID 列表(NPC / 机器人账号) |
logDeniedSkin |
是否记录被拦截的皮肤(INFO) |
logPassedSkin |
是否记录通过校验的皮肤(INFO,默认关闭;排查链路时打开可以看到每一次校验结果) |
NetAllay netAllay = NetAllay.getInstance();
boolean enabled = netAllay.isSkinAntiCheatEnabled(); // 当前状态
netAllay.setSkinAntiCheatEnabled(false); // 临时关闭(不落盘)
netAllay.reloadConfig(); // 重新读取 config.json- 服务端协议层必须已包含
org.cloudburstmc.protocol.bedrock.packet.ConfirmSkinPacket(API 0.28+)。仍使用org.allaymc.protocol.extension.packet.ConfirmSkinPacket的旧版服务端无法加载本插件。 - 若服务器无法访问网易校验接口(或
sign配置错误),在failOpenOnError=false时所有非 persona 皮肤都会被隐藏,排查时可先把testServer设为true验证链路。
- 线程安全:NetAllay 的 API 是线程安全的,可以在任何线程调用
- 玩家检查:发送前会自动检查玩家是否为网易客户端,非网易玩家会被跳过
- 广播限制:在广播方法中不要使用
-2作为 entityId - 生命周期:在插件禁用时记得取消注册的事件监听器
- 数据大小:避免发送过大的数据包,建议单次数据不超过 64KB
NetAllay 使用 MsgPack 格式编码数据,遵循网易 PyRpc 协议:
- 服务端到客户端事件类型:
ModEventS2C - 客户端到服务端事件类型:
ModEventC2S - 字符串编码:UTF-8 Binary 格式
MIT License