PDC 服务 (PersistentDataContainer Service)
PDC 服务封装了 Bukkit 的 PersistentDataContainer API,在此基础上加了分区隔离、批量操作、快照编解码和数据签名。简单来说,它让你在物品上存取结构化数据变得更方便,同时避免不同模块之间的键名冲突。
核心概念
PdcService
PdcService 是所有 PDC 操作的入口,负责管理分区。
PdcPartition
PdcPartition 是一个逻辑分区。不同模块的数据放在各自的分区里,键名互不干扰。这样你就不用担心自己的 level 键和别人的 level 键撞车了。
PdcService pdcService = EmakiServiceRegistry.get(PdcService.class);
// 获取或创建分区
PdcPartition partition = pdcService.partition("my_module");键格式
PDC 键采用 namespace:path.field 格式:
emaki:forge.level → 锻造等级
emaki:forge.exp → 锻造经验
emaki:gem.slots.0.type → 第一个宝石槽的类型
emaki:cooking.recipe_id → 烹饪配方 ID| 部分 | 说明 |
|---|---|
namespace | 命名空间,通常是插件名或模块名(如 emaki) |
path | 数据路径,用 . 分隔层级 |
field | 最终字段名 |
提示
: 分隔命名空间和路径,. 分隔路径层级。完整的键最终会被转换为 Bukkit 的 NamespacedKey。
API 方法
set — 写入数据
PdcPartition partition = pdcService.partition("forge");
// 写入基本类型
partition.set(itemStack, "level", PersistentDataType.INTEGER, 5);
partition.set(itemStack, "exp", PersistentDataType.DOUBLE, 1250.5);
partition.set(itemStack, "owner", PersistentDataType.STRING, "Steve");get — 读取数据
// 读取数据(返回 Optional,不存在时为空)
Optional<Integer> level = partition.get(itemStack, "level", PersistentDataType.INTEGER);
Optional<Double> exp = partition.get(itemStack, "exp", PersistentDataType.DOUBLE);
Optional<String> owner = partition.get(itemStack, "owner", PersistentDataType.STRING);
// 带默认值的版本,省去 Optional 处理
int lvl = partition.getOrDefault(itemStack, "level", PersistentDataType.INTEGER, 0);has — 检查键是否存在
boolean hasLevel = partition.has(itemStack, "level", PersistentDataType.INTEGER);
boolean hasOwner = partition.has(itemStack, "owner", PersistentDataType.STRING);remove — 删除数据
partition.remove(itemStack, "level");
partition.remove(itemStack, "exp");writeBlob — 写入二进制数据
用来存储序列化后的复杂对象:
byte[] data = serializeMyData(myObject);
partition.writeBlob(itemStack, "complex_data", data);readBlob — 读取二进制数据
Optional<byte[]> data = partition.readBlob(itemStack, "complex_data");
data.ifPresent(bytes -> {
MyObject obj = deserializeMyData(bytes);
// 使用 obj
});batchMutate — 批量操作
当你需要同时读写多个字段时,用 batchMutate 把操作打包在一起。它在同一个 PDC 访问上下文中执行所有操作,比多次单独调用 set / get 开销更小。
partition.batchMutate(itemStack, (accessor) -> {
// 读取当前值
int currentLevel = accessor.getOrDefault("level", PersistentDataType.INTEGER, 0);
double currentExp = accessor.getOrDefault("exp", PersistentDataType.DOUBLE, 0.0);
// 计算新值
int newLevel = currentLevel + 1;
double newExp = currentExp - requiredExp;
// 写入新值
accessor.set("level", PersistentDataType.INTEGER, newLevel);
accessor.set("exp", PersistentDataType.DOUBLE, newExp);
accessor.set("last_upgrade", PersistentDataType.LONG, System.currentTimeMillis());
// 删除临时数据
accessor.remove("temp_bonus");
});提示
典型的使用场景是"读取旧值 → 计算 → 写入新值"这种原子性操作。比如升级时同时更新等级和扣除经验,用 batchMutate 可以确保这些操作在同一次 PDC 访问中完成。
SnapshotCodec
SnapshotCodec 负责层快照的序列化和反序列化,主要是给装配系统用的——把 EmakiItemLayerSnapshot 编码成字节数组存进 PDC,需要时再解码回来。
YAML 工厂方法
// 创建基于 YAML 的编解码器
SnapshotCodec codec = SnapshotCodec.yaml();
// 序列化
byte[] bytes = codec.encode(snapshot);
// 反序列化
EmakiItemLayerSnapshot restored = codec.decode(bytes);与 PDC 结合使用
PdcPartition partition = pdcService.partition("assembly");
SnapshotCodec codec = SnapshotCodec.yaml();
// 写入快照
byte[] encoded = codec.encode(snapshot);
partition.writeBlob(itemStack, "forge", encoded);
// 读取快照
Optional<byte[]> data = partition.readBlob(itemStack, "forge");
EmakiItemLayerSnapshot restored = data.map(codec::decode).orElse(null);版本迁移
随着功能迭代,快照的数据结构可能会变。SnapshotCodec 支持基于 schemaVersion 的链式迁移,老版本的数据会自动升级到最新版本:
SnapshotCodec codec = SnapshotCodec.yaml()
.registerMigration(1, 2, (oldData) -> {
// 从 v1 迁移到 v2
oldData.put("new_field", "default_value");
oldData.remove("deprecated_field");
return oldData;
})
.registerMigration(2, 3, (oldData) -> {
// 从 v2 迁移到 v3
// ...
return oldData;
});SignatureUtil
SignatureUtil 用来给数据做签名和校验,检测物品数据是否被篡改过。在防作弊场景下很有用。
// 生成签名
String signature = SignatureUtil.sign(snapshotBytes, secretKey);
// 写入签名
partition.set(itemStack, "forge.signature", PersistentDataType.STRING, signature);
// 校验签名
Optional<String> storedSig = partition.get(itemStack, "forge.signature", PersistentDataType.STRING);
boolean valid = storedSig
.map(sig -> SignatureUtil.verify(snapshotBytes, sig, secretKey))
.orElse(false);
if (!valid) {
logger.warning("物品数据签名校验失败,可能被篡改!");
}签名流程
原始数据 → HMAC-SHA256(data, secretKey) → Base64 编码 → 签名字符串| 方法 | 说明 |
|---|---|
SignatureUtil.sign(byte[], String) | 对数据生成 HMAC-SHA256 签名 |
SignatureUtil.verify(byte[], String, String) | 校验数据与签名是否匹配 |
SignatureUtil.generateKey() | 生成随机密钥 |
注意
签名密钥(secretKey)应该存在服务器配置文件里,不要硬编码在代码中。密钥泄露会让整个签名机制形同虚设。建议在 config.yml 中配置,首次启动时自动生成。
# config.yml
security:
# 数据签名密钥(首次启动自动生成,请勿泄露)
signature_key: "auto_generated_base64_key_here"提示
签名校验是可选的。对于不需要防篡改的数据,可以跳过签名步骤来节省存储空间。装配系统默认会对所有层快照启用签名校验。