外观
🧩 同步自定义数据:Addon
实现 InvSyncAddon,在插件启用时注册同一实例、停用时注销。也可以监听扩展事件,两种方式不要重复处理同一个 key。
完整格式示例
下面示范玩家飞行状态的两字节格式:版本号 + 布尔值。它是接入示例,不是跨服授予飞行权限的推荐策略;生产环境要先检查目标服权限和玩法。
java
import com.xbaimiao.invsync.api.addon.InvSyncAddon;
import com.xbaimiao.invsync.api.data.SaveReason;
import com.xbaimiao.invsync.api.events.InvSyncPluginDataSaveEvent;
import com.xbaimiao.invsync.api.events.InvSyncPluginDataSyncEvent;
public final class FlyAddon implements InvSyncAddon {
// 使用自己的插件前缀,key 在 PluginDataSync 中不区分大小写。
private static final String KEY = "myplugin:flight-v1";
private boolean valid(byte[] raw) {
// 未知版本/损坏格式不当作正常默认值重新写回。
return raw != null && raw.length == 2 && raw[0] == 1
&& (raw[1] == 0 || raw[1] == 1);
}
@Override
public void onSync(InvSyncPluginDataSyncEvent event) {
byte[] raw = event.readData(KEY);
if (raw == null) return;
if (!valid(raw)) {
// 保留原始数据,交给自己的日志和迁移流程处理。
return;
}
// 调用上下文必须允许访问玩家;目标服应另行校验飞行权限。
event.getPlayer().setAllowFlight(raw[1] == 1);
}
@Override
public void onSave(InvSyncPluginDataSaveEvent event, SaveReason reason) {
// INIT 是预热调用,不把它当成玩家已经正常退出或完成发奖。
if (reason == SaveReason.INIT) return;
byte[] old = event.readData(KEY);
if (old != null && !valid(old)) return;
// 版本化自己的载荷,不依赖 InvSync 内部序列化器。
event.putData(KEY, new byte[]{1,
(byte) (event.getPlayer().getAllowFlight() ? 1 : 0)});
}
}在自己的主类中:
java
import com.xbaimiao.invsync.api.addon.InvSyncAddonManager;
// 保存同一个实例,注销时使用它。
private final FlyAddon flyAddon = new FlyAddon();
@Override
public void onEnable() {
// plugin.yml 的 depend 保证 InvSync 已启用。
InvSyncAddonManager.register(flyAddon);
}
@Override
public void onDisable() {
InvSyncAddonManager.unregister(flyAddon);
}如果用 softdepend,先判断 InvSync 是否存在且启用,再初始化引用 API 的适配类,避免可选依赖未安装时类链接失败。
注册规则
管理器按 Addon 类全名注册。同类再次注册会替换旧实例;不同 key 不会让同一个类变成两个独立注册项。getAddons() 是只读视图,不修改其集合。
调用顺序不保证。单个 Addon 异常由分发器记录,其他 Addon 继续处理;抛异常不是可靠的全局拒绝登录或回滚手段。
key 与空值
putData(key, null) 删除对应扩展键,不是“不更新”。加载失败、格式不认识或自己内存数据缺失时直接不写,保留已有数据,不能拿空列表/默认值覆盖旧记录。
PluginDataSync 是字节 KV 容器,不是线程安全事务存储。不要跨线程共享事件对象或修改已经提交的 ByteArray;不同 Addon 只管理自己的 key。
内存数据的正确生命周期
- 同步成功后才把数据标记为可保存;未知格式标记失败并保留原载荷。
PlayerDataSyncDoneEvent.data为 null 时可能是首次初始化,不等于已有空档。- 首次初始化需要自己的默认值策略,不能依赖不存在的 onSync 一定会触发。
DISCONNECT包含预退出,可能最终切服失败回到源服,不要在 onSave(DISCONNECT) 直接清空内存。- 真正退出后再清理;Folia Kick 可取消,不能把 Kick 当最终 Quit。
- Addon 在注册前已经在线的玩家不会自动全部重放,避免不完整缓存覆盖数据。
指纹和预加载
当前普通同步路径对 pluginData/otherData 排除字段指纹跳过,保证新玩家实例恢复运行时扩展;不要把 onSync 的调用次数视为稳定契约。旧版本和内部应用路径有过跳过分支,支持旧版时必须实测。
预加载只提前准备支持的原版字段,Addon 仍在同步应用阶段恢复。PlayerDataSyncPreEvent 取消不能撤回已经写入离线档案的原版字段,不用这个事件设计强隔离档案切换。
线程与测试
事件由不同入口触发,保存回调不保证永远在传统主线程。自己的内存状态要可正确访问,Bukkit 状态只在合法玩家线程访问;不要简单异步补写然后期望当前同步等待你。
测试:首次进服、重连、跨服、未知载荷、缺失依赖、预退出失败、退出与关服。同一个 key 的格式升级应可读旧版、拒绝损坏、保留未知版本。