Skip to content

PDC 服务 (PersistentDataContainer Service)

PDC 服务封装了 Bukkit 的 PersistentDataContainer API,在此基础上加了分区隔离、批量操作、快照编解码和数据签名。简单来说,它让你在物品上存取结构化数据变得更方便,同时避免不同模块之间的键名冲突。

核心概念

PdcService

PdcService 是所有 PDC 操作的入口,负责管理分区。

PdcPartition

PdcPartition 是一个逻辑分区。不同模块的数据放在各自的分区里,键名互不干扰。这样你就不用担心自己的 level 键和别人的 level 键撞车了。

java
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 — 写入数据

java
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 — 读取数据

java
// 读取数据(返回 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 — 检查键是否存在

java
boolean hasLevel = partition.has(itemStack, "level", PersistentDataType.INTEGER);
boolean hasOwner = partition.has(itemStack, "owner", PersistentDataType.STRING);

remove — 删除数据

java
partition.remove(itemStack, "level");
partition.remove(itemStack, "exp");

writeBlob — 写入二进制数据

用来存储序列化后的复杂对象:

java
byte[] data = serializeMyData(myObject);
partition.writeBlob(itemStack, "complex_data", data);

readBlob — 读取二进制数据

java
Optional<byte[]> data = partition.readBlob(itemStack, "complex_data");
data.ifPresent(bytes -> {
    MyObject obj = deserializeMyData(bytes);
    // 使用 obj
});

batchMutate — 批量操作

当你需要同时读写多个字段时,用 batchMutate 把操作打包在一起。它在同一个 PDC 访问上下文中执行所有操作,比多次单独调用 set / get 开销更小。

java
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 工厂方法

java
// 创建基于 YAML 的编解码器
SnapshotCodec codec = SnapshotCodec.yaml();

// 序列化
byte[] bytes = codec.encode(snapshot);

// 反序列化
EmakiItemLayerSnapshot restored = codec.decode(bytes);

与 PDC 结合使用

java
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 的链式迁移,老版本的数据会自动升级到最新版本:

java
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 用来给数据做签名和校验,检测物品数据是否被篡改过。在防作弊场景下很有用。

java
// 生成签名
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 中配置,首次启动时自动生成。

yaml
# config.yml
security:
  # 数据签名密钥(首次启动自动生成,请勿泄露)
  signature_key: "auto_generated_base64_key_here"

提示

签名校验是可选的。对于不需要防篡改的数据,可以跳过签名步骤来节省存储空间。装配系统默认会对所有层快照启用签名校验。

Released under the GPL-3.0 License