深色模式
来源、查询与事件 API
SourceSnapshot 契约
java
SourceSnapshot source = new SourceSnapshot(
new SourceKey("quest", "reward/chapter_1"),
12L,
List.of(
new SourceContribution(
AttributeId.of("sealattributes", "max_health"),
ContributionOperation.ADD,
100.0
)
),
SourcePersistence.PERMANENT,
Optional.empty()
);规则:
- provider 必须等于绑定命名空间;source path 和 ID 必须规范化;
- revision 必须 >0,相同键只接受更新修订;
- contributions 不可变,数值有限,
(attributeId, operation)不重复; RUNTIME、PERMANENT不带 expiry;EXPIRES_AT必须带未来到期时间;- 新快照整体替换旧快照,空变化应使用 withdraw,而不是发布含空/补丁语义的数据。
当前 SourceSnapshot 没有独立 64 项上限;旧 MAX_CONTRIBUTIONS_PER_SOURCE=64 仅为弃用 ABI 字段。真正快照属性上限是 512,单主体来源上限 256。
运行时发布
java
PublishResult result = api.sources().publish(subject, source);
if (!result.successful()) {
logger.warning("publish rejected: " + result.getStatus());
}sources() 是同步内存操作,可从任意线程准备不可变输入,但主体/平台相关动作仍要遵守返回状态。卸载:
java
api.sources().withdraw(subject, key, nextRevision);需要同时换职业与被动时用原子批次:
java
BatchPublishResult result = api.sources().mutate(subject, List.of(
SourceMutation.withdraw(oldClassKey, oldRevision + 1),
SourceMutation.replace(newClassSnapshot),
SourceMutation.replace(newPassiveSnapshot)
));批次只允许本 provider 的 RUNTIME 来源,同键不重复;任一项 stale、越权、无效或超限则整批不落地,只在成功时生成一次快照变更事件。
持久发布
PERMANENT/EXPIRES_AT 走异步存储:
java
api.persistentSources().publish(subject, persistentSnapshot)
.thenAccept(result -> {
switch (result.getStatus()) {
case APPLIED, PERSISTED_SESSION_DEFERRED -> {
// 业务成功;DEFERRED 不得重试。
}
default -> logger.warning("persistent publish failed: " + result.getStatus());
}
});重点状态:STALE_SUBJECT、STALE_OWNER、STALE_REVISION、STORAGE_FAILURE、LIMIT_EXCEEDED。PERSISTED_SESSION_DEFERRED 表示数据库提交成功,但请求时的玩家会话在运行态应用前失效;后续当前会话会从持久事实物化,重复写会制造错误业务语义。
持久写失败不会覆盖最后一次已提交来源。不要先改自己的数据库再假设 SealAttributes 一定成功;业务要定义跨系统补偿或幂等键。
查询
java
QueryResult<SubjectKey> current = api.queries().currentSubject(playerUuid);
if (!current.found()) return;
SubjectKey subject = current.value.orElseThrow();
QueryResult<AttributeSnapshot> snapshot = api.queries().snapshot(subject);
if (snapshot.found()) {
double health = snapshot.value.orElseThrow().valueOr(
AttributeId.of("sealattributes", "max_health"), 0.0
);
}查询是 O(1) 的已提交不可变快照读取,不访问 Bukkit 实体、磁盘或数据库,可在任意线程调用。永远使用当前 SubjectKey,不要只缓存 UUID;重登会产生新 subject epoch,旧 key 返回 STALE_SUBJECT。
snapshot() 的 max_health 是 RPG 目标值;effectiveHealth() 返回 Paper 已证明的当前/有效最大生命,遇到其他 modifier 时两者可能不同。
装备发布闸门
SealItems 一类外部装备 owner 每次发布前都必须 fail closed:
java
boolean mayPublish = false;
try {
QueryResult<Boolean> state = api.builtInEquipmentReaderEnabled();
mayPublish = state.found() && !state.value.orElse(true);
} catch (RuntimeException ignored) {
// 保持 false
}
if (!mayPublish) return;只有明确 FOUND false 才能写;missing、stale、异常一律按内置 reader 仍开启。不要长期缓存结果。
提交后事件
java
EventSubscription subscription = api.events().onAttributeSnapshotChanged(event -> {
logger.fine("revision=" + event.getCurrent().getState().getSnapshotRevision()
+ ", reason=" + event.getReason());
});
// 插件自己的 onDisable 中可显式关闭;owner epoch 失效也会自动关闭。
subscription.close();四类订阅:onAttributeSnapshotChanged、onCombatCompleted、onHealingCompleted、onResourceChanged。它们均是提交后、只读、不可取消;监听异常被隔离。
快照监听器中再次改来源会排到下一轮,避免重入破坏当前通知;combat/healing 完成回调中递归提交相同类操作会 fail closed。事件运行在拥有主体的执行上下文,不要做数据库、网络、文件或长计算;复制少量不可变数据后异步处理。
异常处理策略
- 构造器的非法参数属于开发错误,允许立即抛出并在测试中发现;
- 运行态拒绝通过 status 表达,不解析人类日志;
- Gate 抛异常视为拒绝;公式/扩展异常被隔离且产生诊断;
- stale 不代表“再试一定成功”,先重新获取 current subject/owner;
- capacity/limit 是安全边界,应缩减模型而不是无限重试。