|
2 | 2 |
|
3 | 3 | [English](README_EN.md) |
4 | 4 |
|
5 | | -`PiSerializeKit` 是 Pi 系列的独立序列化基础库。 |
6 | | - |
7 | | -它负责统一 value codec、NBT codec 与 packet codec 的边界,让多个 Pi 仓库在“如何描述数据”这件事上使用同一套语言,而不是每个模组都重新定义一层。 |
8 | | - |
9 | | -它也应成为 `PiDataGraph` 对象计数、反应链状态与图节点数据的强类型序列化底座。 |
10 | | - |
11 | | -## Start Stage 目标 |
12 | | - |
13 | | -1. 提供稳定的 serializer、type key 与 service 查询接口; |
14 | | -2. 为 `Pibrary`、`PiNet`、`PiKubeJSCompat` 与未来 engine repos 提供共享序列化边界; |
15 | | -3. 保持独立仓库身份,不再塞回 `Pibrary` 单体结构; |
16 | | -4. 把 state schema 与 packet schema 收束到同一套字段语言上。 |
17 | | - |
18 | | -## 当前已提供 |
19 | | - |
20 | | -1. `PiSerializer` |
21 | | - 聚合 value codec、NBT codec、packet codec。 |
22 | | -2. `PiNbtCodec` |
23 | | - `CompoundTag` 编解码契约。 |
24 | | -3. `PiPacketCodec` |
25 | | - `FriendlyByteBuf` 编解码契约。 |
26 | | -4. `PiSerializerType` |
27 | | - 稳定的 serializer 类型标识。 |
28 | | -5. `PiSerializeService` |
29 | | - 注册与查询统一入口。 |
30 | | -6. `PiSerializeServices` |
31 | | - 当前 runtime service 的安装与获取入口。 |
32 | | -7. `@PiSyncModel` / `@PiField` |
33 | | - 编译期生成 state binding、projection、delta 与 schema migration 绑定。 |
34 | | -8. `@PiPacket` / `@PiPacketNamespace` / `@PiPacketUpgrade` |
35 | | - 编译期生成 packet binding、packet provider、packet id、packet version 与 packet migration 链。 |
36 | | -9. `PiSchemas` / `PiPackets` |
37 | | - 基于 `ServiceLoader` 的 schema / packet runtime registry,可按类型或 packet id 查询。 |
38 | | -10. `PiDecodeContext` |
39 | | - 结构化 decode 诊断收集器,支持字段路径、fatal 标记与 migration 失败信息。 |
40 | | -11. `PiRuntimeLookupException` / `PiRuntimeConflictException` / `PiRuntimeBootstrapException` |
41 | | - 统一的 runtime 异常模型,用于缺失绑定、冲突注册与 provider bootstrap 失败,并暴露最小 machine-readable 上下文。 |
42 | | - |
43 | | -## 作者侧硬约束 |
44 | | - |
45 | | -第一次写 `@PiSyncModel` / `@PiPacket` 前,当前需要明确这些规则: |
46 | | - |
47 | | -1. `@PiSyncModel.version` 与 `@PiPacket.version` 必须 `>= 1`; |
48 | | -2. schema id 与 packet id 在同一轮编译内必须唯一,重复声明会编译期失败; |
49 | | -3. `@PiSyncModel`、`@PiPacket`、`@PiLivingService` 必须是 top-level concrete class; |
50 | | -4. `@PiSyncModel` 需要可访问的无参构造器,且该构造器不能声明 checked exception; |
51 | | -5. `@PiPacket` 用于生成 decode 的构造器必须和 `@PiField` 顺序匹配,且不能声明 checked exception; |
52 | | -6. `@PiField(serializer = ...)` provider 需要可访问的无参构造器,且不能声明 checked exception; |
53 | | -7. `@PiAfterDecode`、`@PiSchemaUpgrade`、`@PiPacketUpgrade` 方法不能声明 checked exception。 |
54 | | - |
55 | | -## 验证门槛 |
56 | | - |
57 | | -当前仓库把下面这条命令视为合并前的最低冷验证门槛: |
| 5 | +`PiSerializeKit` 是 Pi 系列里负责“数据怎么写、怎么读、怎么升级、怎么进包”的底层库。 |
58 | 6 |
|
59 | | -```bash |
60 | | -bash ./gradlew clean test --no-daemon |
| 7 | +如果你需要下面这些事,就会用到它: |
| 8 | + |
| 9 | +1. 给一个状态类写持久化和同步结构; |
| 10 | +2. 给一个 packet 写稳定 id、版本和 decode 逻辑; |
| 11 | +3. 给某个类型提供统一 serializer; |
| 12 | +4. 在运行时按 authored class 取回对应的 schema binding 或 packet binding。 |
| 13 | + |
| 14 | +大多数作者平时只会接触三层东西: |
| 15 | + |
| 16 | +1. 注解:`@PiSyncModel`、`@PiField`、`@PiPacket` |
| 17 | +2. 运行时入口:`PiSchemas`、`PiPackets`、`PiSerializeServices` |
| 18 | +3. 少量 serializer API:`PiSerializer`、`PiSerializers` |
| 19 | + |
| 20 | +你通常不需要手写或直接依赖 `_PiSchema`、`_PiPacket` 这类 generated class。 |
| 21 | + |
| 22 | +## 典型用法 |
| 23 | + |
| 24 | +### 1. 写一个可持久化、可同步的状态类 |
| 25 | + |
| 26 | +```java |
| 27 | +@PiSyncModel(id = "example:mana_state", version = 1) |
| 28 | +public final class ManaState { |
| 29 | + @PiField(id = "mana", sync = PiSyncScope.TRACKING, persist = true) |
| 30 | + public int mana; |
| 31 | + |
| 32 | + @PiField(id = "selected_spell", sync = PiSyncScope.OWNER, persist = true) |
| 33 | + public String selectedSpell = ""; |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +运行时你拿到的是 binding,而不是自己去找 generated class: |
| 38 | + |
| 39 | +```java |
| 40 | +PiStateBinding<ManaState> binding = PiSchemas.require(ManaState.class); |
| 41 | + |
| 42 | +CompoundTag full = binding.saveFull(state); |
| 43 | +binding.loadFull(restored, full, PiDecodeContext.strict()); |
| 44 | +``` |
| 45 | + |
| 46 | +如果你只想写默认 client view、persisted view 或 delta,也都走 binding: |
| 47 | + |
| 48 | +```java |
| 49 | +CompoundTag clientView = binding.saveClientView(state); |
| 50 | +CompoundTag persisted = binding.savePersisted(state); |
| 51 | +CompoundTag delta = binding.writeClientDelta(state, dirtySet); |
61 | 52 | ``` |
62 | 53 |
|
63 | | -仓库同时提供 `.github/workflows/ci.yml`,把同一条 `clean test` 变成固定 CI gate,而不是只依赖本地偶尔手跑。 |
| 54 | +### 2. 写一个 packet |
| 55 | + |
| 56 | +先在包上提供 namespace,避免每个包都重复写: |
| 57 | + |
| 58 | +```java |
| 59 | +@PiPacketNamespace("example") |
| 60 | +package com.example.packet; |
| 61 | +``` |
64 | 62 |
|
65 | | -## API 稳定边界 |
| 63 | +然后写 packet 本体: |
66 | 64 |
|
67 | | -当前建议按下面三层理解兼容边界: |
| 65 | +```java |
| 66 | +@PiPacket |
| 67 | +public final class CastSkillPacket extends PiServerPacket { |
| 68 | + @PiField(id = "skill", sync = PiSyncScope.OWNER, persist = false) |
| 69 | + public String skill; |
68 | 70 |
|
69 | | -1. 稳定作者 API: |
70 | | - `org.pickaid.piserializekit.api.schema`、`api.packet`、`api.service`、`api.runtime`,以及运行时入口 `PiSchemas`、`PiPackets`、`PiSerializeServices`。 |
71 | | -2. 内部实现 API: |
72 | | - `processor.*`、`processor.support.*`、`processor.model.*`、`runtime.*.support`、`runtime.*.codec` 等包默认不承诺稳定,不建议下游直接依赖。 |
73 | | -3. 生成物命名边界: |
74 | | - `_PiSchema`、`_PiFields`、`_PiPacket`、`_PiPacketProvider` 这类 generated type 主要是编译产物;下游更推荐通过 `PiSchemas` / `PiPackets` 或上层 pack 暴露的 host API 间接消费,而不是把这些名字当手写稳定契约。 |
| 71 | + @PiField(id = "level", sync = PiSyncScope.OWNER, persist = false) |
| 72 | + public int level; |
75 | 73 |
|
76 | | -## 当前明确不做满 |
| 74 | + public CastSkillPacket(String skill, int level) { |
| 75 | + this.skill = skill; |
| 76 | + this.level = level; |
| 77 | + } |
| 78 | + |
| 79 | + @Override |
| 80 | + protected void handle(PiServerPacketContext context) { |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +运行时同样按 authored class 或稳定 id 取 binding: |
| 86 | + |
| 87 | +```java |
| 88 | +PiPacketBinding<CastSkillPacket, ?> binding = PiPackets.require(CastSkillPacket.class); |
| 89 | +FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); |
| 90 | + |
| 91 | +binding.codec().write(buf, new CastSkillPacket("fireball", 2)); |
| 92 | +CastSkillPacket decoded = binding.codec().read(buf); |
| 93 | +``` |
77 | 94 |
|
78 | | -本仓库当前仍然不负责: |
79 | | -1. channel 安装、发送目标、线程切换与 transport guard; |
80 | | -2. capability / host runtime 绑定与 gameplay service 生命周期; |
81 | | -3. 面向具体 UI / render / world runtime 的高层 author magic; |
82 | | -4. 反射式黑盒自动读写器; |
83 | | -5. 针对单个 engine 的重度运行时打包逻辑。 |
| 95 | +### 3. 安装或临时覆盖 serializer runtime |
84 | 96 |
|
85 | | -## 仓库定位 |
| 97 | +如果你只是要 built-in serializer: |
| 98 | + |
| 99 | +```java |
| 100 | +PiSerializeRuntime runtime = new PiSerializeRuntime(); |
| 101 | +PiBuiltInSerializers.install(runtime); |
| 102 | +PiSerializeServices.install(runtime); |
| 103 | +``` |
| 104 | + |
| 105 | +如果你只是想在某个局部逻辑里临时切换 runtime: |
| 106 | + |
| 107 | +```java |
| 108 | +PiSerializeServices.withScope(runtime, () -> { |
| 109 | + PiSerializer<String> serializer = PiSerializeServices.requireSerializer(PiSerializers.STRING); |
| 110 | +}); |
| 111 | +``` |
| 112 | + |
| 113 | +这个 scoped override 很适合测试、局部覆盖或上层 pack 做隔离策略。 |
| 114 | + |
| 115 | +## 你实际会依赖哪些 API |
| 116 | + |
| 117 | +稳定的作者侧入口主要是这些: |
| 118 | + |
| 119 | +1. `org.pickaid.piserializekit.api.schema.*` |
| 120 | +2. `org.pickaid.piserializekit.api.packet.*` |
| 121 | +3. `org.pickaid.piserializekit.api.service.*` |
| 122 | +4. `org.pickaid.piserializekit.api.runtime.*` |
| 123 | +5. `PiSchemas` |
| 124 | +6. `PiPackets` |
| 125 | +7. `PiSerializeServices` |
| 126 | + |
| 127 | +下面这些默认按内部实现处理,不建议下游直接写死依赖: |
| 128 | + |
| 129 | +1. `processor.*` |
| 130 | +2. `processor.support.*` |
| 131 | +3. `processor.model.*` |
| 132 | +4. `runtime.*.support` |
| 133 | +5. `runtime.*.codec` |
| 134 | +6. generated class 名字本身 |
| 135 | + |
| 136 | +## 作者侧规则 |
| 137 | + |
| 138 | +第一次写 `@PiSyncModel` / `@PiPacket` 前,先记住这些硬规则: |
| 139 | + |
| 140 | +1. `@PiSyncModel.version` 和 `@PiPacket.version` 必须 `>= 1` |
| 141 | +2. schema id 和 packet id 在同一轮编译里必须唯一 |
| 142 | +3. `@PiSyncModel`、`@PiPacket`、`@PiLivingService` 必须是 top-level concrete class |
| 143 | +4. `@PiSyncModel` 需要可访问的无参构造器,而且不能声明 checked exception |
| 144 | +5. `@PiPacket` 的 decode 构造器必须和 `@PiField` 顺序一致,而且不能声明 checked exception |
| 145 | +6. `@PiField(serializer = ...)` provider 需要可访问的无参构造器,而且不能声明 checked exception |
| 146 | +7. `@PiAfterDecode`、`@PiSchemaUpgrade`、`@PiPacketUpgrade` 方法不能声明 checked exception |
| 147 | + |
| 148 | +## 这个库不负责什么 |
| 149 | + |
| 150 | +这些事不属于 `PiSerializeKit`: |
| 151 | + |
| 152 | +1. channel 安装、发送目标、线程切换、transport guard |
| 153 | +2. capability / host runtime 绑定 |
| 154 | +3. gameplay service 生命周期 |
| 155 | +4. UI、render、world 的高层 author magic |
| 156 | +5. 黑盒反射式自动读写 |
| 157 | + |
| 158 | +换句话说: |
| 159 | + |
| 160 | +1. `PiSerializeKit` 负责数据如何描述、升级、诊断、绑定 |
| 161 | +2. `PiNet` 负责 packet 如何发送、路由、守卫 |
| 162 | +3. `Pibrary` 和后续 packs 负责这些 schema / packet 如何进入实际 gameplay host |
| 163 | + |
| 164 | +## 验证 |
| 165 | + |
| 166 | +这个仓库当前把下面这条命令当作最低冷验证门槛: |
| 167 | + |
| 168 | +```bash |
| 169 | +bash ./gradlew clean test --no-daemon |
| 170 | +``` |
86 | 171 |
|
87 | | -1. 这是独立基础仓库; |
88 | | -2. 它可被多个 Pi repo 直接消费; |
89 | | -3. 它负责“数据模型如何被描述、升级、诊断与绑定”; |
90 | | -4. `PiNet` 负责“这些 packet 如何被发送、路由与守卫”; |
91 | | -5. `Pibrary` 与后续 packs 负责“这些 schema / packet 如何进入具体 gameplay host”。 |
| 172 | +仓库里也有 `.github/workflows/ci.yml`,会在 GitHub 上跑同一条 `clean test`。 |
0 commit comments