diff --git "a/docs/sync_system/\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/docs/sync_system/\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 0000000..6b18b18 --- /dev/null +++ "b/docs/sync_system/\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,510 @@ +# 注解驱动数据同步系统 - 开发文档 + +## 目录 + +1. [注解一览](#1-注解一览) +2. [架构概述](#2-架构概述) +3. [数据组件集成](#3-数据组件集成) +4. [核心 API](#4-核心-api) +5. [数据流详解](#5-数据流详解) +6. [物品同步](#6-物品同步) +7. [方块实体集成](#7-方块实体集成) +8. [完整使用示例](#8-完整使用示例) + +--- + +## 1. 注解一览 + +共 7 个注解,**各司其职**。 + +| 注解 | 目标 | 存储方式 | 职责 | +|------------------------------|----|---------------|----------------------------| +| `@SaveField(nbtKey)` | 字段 | NBT (Codec) | 持久化字段到世界存档 | +| `@ItemSave(nbtKey)` | 字段 | DataComponent | 方块掉落时存入物品、放置时从物品恢复 | +| `@SyncToClient` | 字段 | NBT (Codec) | 服务端→客户端实时同步 | +| `@SyncToServer` | 字段 | NBT (Codec) | 客户端→服务端实时同步 | +| `@SyncBoth` | 字段 | NBT (Codec) | 双端同步(S2C + C2S) | +| `@RerenderOnChanged` | 字段 | — | 标记某 SyncToClient 字段变更后需重渲染 | +| `@ClientFieldChangeListener` | 方法 | — | 指定字段同步到客户端后调用此方法 | + +> **序列化机制**:所有数据序列化使用 Minecraft 原生 `Codec` + `NbtOps`。物品存储使用 `DataComponentType`,通过 `ItemStack.set/get` 存取。 + +### `@SaveField` — 世界存档 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SaveField { String nbtKey() default ""; } +``` + +通过 Codec 编解码,在 `saveAdditional()` / `loadAdditional()` 时读写 NBT。不影响物品。 + +### `@ItemSave` — 物品存储 (DataComponent) + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ItemSave { String nbtKey() default ""; } +``` + +方块掉落时通过 `SyncedComponents.BLOCK_ITEM_DATA` (`DataComponentType`)存入物品。放置时自动读取并赋值。 + +可配合 `applyToItemStack()` / `loadFromItemStack()` 手动操作。 + +### `@SyncToClient` — 服务端→客户端 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SyncToClient {} +``` + +系统每 tick 自动扫描对比 `@SyncToClient` 字段值,检测到变更自动序列化并发往客户端。 + +### `@SyncToServer` — 客户端→服务端 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SyncToServer {} +``` + +标记字段允许客户端向服务端发送更新。网络层收到后调用 `handleClientUpdate()` 驱动。 + +### `@SyncBoth` — 双端同步 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SyncBoth {} +``` + +等价于 `@SyncToClient @SyncToServer`。服务端自动扫描推送,同时接受客户端更新。 + +### `@RerenderOnChanged` + `@ClientFieldChangeListener` + +`@RerenderOnChanged` 标记字段需要在客户端更新包到达时重渲染。 +`@ClientFieldChangeListener(fieldName)` 标记方法在客户端收到字段更新后自动调用。 + +**两个注解协作:** +```java +@SyncToClient @RerenderOnChanged private int energy; + +@ClientFieldChangeListener(fieldName = "energy") +public void onEnergyChanged() { /* 客户端回调 */ } +``` + +--- + +## 2. 架构概述 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ ISyncManaged │ +└──────────────────────┬───────────────────────────────────────┘ + │ implements +┌──────────────────────▼───────────────────────────────────────┐ +│ ManagedSyncBlockEntity │ +│ saveAdditional() → @SaveField → Codec → NBT │ +│ saveWithoutMetadata() → @ItemSave → Codec → NBT │ +│ + DataComponent (SyncedComponents.BLOCK_ITEM_DATA)│ +│ loadAdditional() → @SaveField + @ItemSave 恢复 │ +│ getUpdateTag() → @SyncToClient/@SyncBoth 初始同步 │ +│ getUpdatePacket() → @SyncToClient/@SyncBoth 增量同步 │ +│ handleClientUpdate() → @SyncToServer/@SyncBoth C2S │ +│ updateTick() → 定期扫描值变更 │ +└──────────────────────┬───────────────────────────────────────┘ + │ owns +┌──────────────────────▼───────────────────────────────────────┐ +│ SyncDataHolder │ +│ serializeToSaveNBT() / serializeToItemNBT() │ +│ scanAndMarkChanges() → 值比较 + 存储 pending │ +│ getPendingChanges() → 消费变更 │ +│ applyToItemStack() → 写入 DataComponent │ +│ loadFromItemStack() → 读取 DataComponent │ +│ applyServerUpdate() → 应用 C2S │ +└──────────────────────┬───────────────────────────────────────┘ + │ uses +┌──────────────────────▼───────────────────────────────────────┐ +│ ClassSyncData + FieldCodecs │ +│ ClassValue 缓存 + 注解扫描 │ +│ Codec 查找 → 所有序列化均通过 Codec + NbtOps │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 数据组件集成 + +`@ItemSave` 使用注册的 `DataComponentType` 在 ItemStack 上存取数据。 + +### 注册(自动执行) + +```java +// SyncedComponents.java +public static final DeferredRegister> COMPONENTS = + DeferredRegister.create(Registries.DATA_COMPONENT_TYPE, MOD_ID); + +public static final Supplier> BLOCK_ITEM_DATA = + COMPONENTS.register("block_item_data", () -> DataComponentType.builder() + .codec(CompoundTag.CODEC) + .build()); +``` + +在 `@Mod` 类中注册: +```java +public SyncedData(IEventBus bus) { + SyncedComponents.COMPONENTS.register(bus); +} +``` + +### 自动流程 + +``` +方块被破坏: + saveWithoutMetadata() → serializeToItemNBT() → @ItemSave 写入 CompoundTag + → 自动存入物品的 BLOCK_ITEM_DATA DataComponent + +放置方块: + loadAdditional() → deserializeItemNBT() → 从 CompoundTag 恢复 +``` + +### 手动操作 + +```java +// 将 @ItemSave 字段写入物品的 DataComponent +be.getSyncDataHolder().applyToItemStack(stack, registries); + +// 从物品的 DataComponent 读取 @ItemSave 字段 +be.loadFromItemStack(stack); +``` + +--- + +## 4. 核心 API + +### 4.1 `ISyncManaged` + +```java +public interface ISyncManaged { + SyncDataHolder getSyncDataHolder(); + void scheduleRenderUpdate(); + void markAsChanged(); +} +``` + +### 4.2 `SyncDataHolder` + +| 方法 | 存储 | 对应注解 | +|--------------------------------|---------------|--------------------------------| +| `serializeToSaveNBT()` | NBT (Codec) | `@SaveField` | +| `serializeToItemNBT()` | NBT (Codec) | `@ItemSave` | +| `applyToItemStack()` | DataComponent | `@ItemSave` | +| `loadFromItemStack()` | DataComponent | `@ItemSave` | +| `serializeFullClientSyncNBT()` | NBT (Codec) | `@SyncToClient` / `@SyncBoth` | +| `scanAndMarkChanges()` | 值比较 | `@SyncToClient` / `@SyncBoth` | +| `getPendingChanges()` | 消费变更 | `@SyncToClient` / `@SyncBoth` | +| `deserializeNBT()` | NBT (Codec) | `@SaveField` / `@SyncToClient` | +| `deserializeItemNBT()` | NBT (Codec) | `@ItemSave` | +| `applyServerUpdate()` | NBT (Codec) | `@SyncToServer` / `@SyncBoth` | + +### 4.3 `FieldCodecs` — Codec 注册 + +用 Mkane 原生 `Codec` 替代了自建转换器体系: + +```java +// 内置注册类型 +int/Integer → Codec.INT +long/Long → Codec.LONG +boolean → Codec.BOOL +String → Codec.STRING +UUID → Codec.STRING.xmap(UUID::fromString, UUID::toString) +CompoundTag → CompoundTag.CODEC +// 用户可扩展: +FieldCodecs.register(MyType.class, MyType.CODEC); +``` + +所有序列化均通过 `registries.createSerializationContext(NbtOps.INSTANCE)` 编解码。 + +--- + +## 5. 数据流详解 + +### 5.1 `@SaveField` — 世界存档 + +``` +saveAdditional() → serializeToSaveNBT() + → for each @SaveField: codec.encodeStart(NbtOps, value) → tag.put(nbtSaveKey) + +loadAdditional() → deserializeNBT(registries, tag, false) + → for each @SaveField: tag.get(nbtSaveKey) → codec.parse(NbtOps, tag) → VarHandle.set() +``` + +### 5.2 `@ItemSave` — DataComponent 物品存储 + +``` +方块破坏: + saveWithoutMetadata() → serializeToItemNBT() + → for each @ItemSave: codec.encodeStart(NbtOps, value) → tag.put(itemNbtKey) + → 自动作为 BlockEntityTag DataComponent 存入 ItemStack + +放置方块: + loadAdditional() → deserializeItemNBT() + → for each @ItemSave: tag.get(itemNbtKey) → codec.parse(NbtOps, tag) → VarHandle.set() +``` + +### 5.3 `@SyncToClient` — 服务端→客户端 + +``` +updateTick() 每 tick: + 1. scanAndMarkChanges(registries) + ├── 遍历 @SyncToClient/@SyncBoth 字段 + ├── Objects.equals(当前值, 缓存快照) + ├── 有变更 → codec.encodeStart(NbtOps, value) + │ → changes.put(nbtSaveKey, encoded) + │ → 更新快照 + └── 存储 pendingClientChanges + 2. sendBlockUpdated() + 3. getUpdatePacket() → getPendingChanges() 消费 → 发送 + +客户端: + loadAdditional() → deserializeNBT(registries, tag, true) + → codec.parse(NbtOps, tag) → VarHandle.set() + → 更新缓存 + 回调 + 重渲染 +``` + +### 5.4 `@SyncToServer` — 客户端→服务端 + +``` +客户端变更 → 自定义网络包 + +服务端网络 handler: + → handleClientUpdate(registries, tag) + → applyServerUpdate(registries, tag) + → for each @SyncToServer/@SyncBoth: tag.get(fieldName) + → codec.parse(NbtOps, tag) → VarHandle.set() + +⚠ 安全校验由调用方负责 +``` + +--- + +## 6. 物品同步 + +物品使用 `ItemSyncHolder` 配合 `@ItemSave` / `@SyncToClient` / `@SyncToServer` / `@SyncBoth`,所有数据存储在 `SyncedComponents.BLOCK_ITEM_DATA` DataComponent 中。**容器同步由原版 `broadcastChanges()` 自动处理**,注解系统负责值的编解码与变更回调。 + +### 6.1 `ItemSyncHolder` + +```java +public class ItemSyncHolder implements ISyncManaged { + SyncDataHolder syncDataHolder; + + void saveToStack(ItemStack, Provider) // @ItemSave → DataComponent + void loadFromStack(ItemStack, Provider, boolean clientSide) // DataComponent → 字段 + 回调 + boolean scanChanges(Provider) // 扫描 @SyncToClient 变更 + void flushToStack(ItemStack, Provider) // 变更增量写入 DataComponent + void applyServerUpdate(Provider, CompoundTag) // 处理 C2S +} +``` + +### 6.2 物品数据类定义 + +```java +public class BatteryItemData implements ISyncManaged { + + private final ItemSyncHolder holder = new ItemSyncHolder(this); + + @ItemSave @SyncToClient + private int energy; + + @ItemSave @SyncBoth + private int maxEnergy; + + @SyncBoth + private int outputRate; + + @ClientFieldChangeListener(fieldName = "energy") + public void onEnergyChanged() { + // 客户端 GUI 收到 energy 变更后自动刷新 + refreshDisplay(); + } + + public SyncDataHolder getSyncDataHolder() { return holder.getSyncDataHolder(); } + public void scheduleRenderUpdate() {} + public void markAsChanged() {} +} +``` + +### 6.3 容器同步流程 + +```java +// === 服务端:修改值后同步到所有客户端 === +// 在 Container / Menu 中 +void onEnergyChanged(int newEnergy) { + battery.energy = newEnergy; + + // 扫描 @SyncToClient 字段,检测是否有变更 + if (battery.holder.scanChanges(level.registryAccess())) { + // 将变更增量写入 ItemStack DataComponent + battery.holder.flushToStack(batteryStack, level.registryAccess()); + // 原版容器同步广播 + broadcastChanges(); + // 或者针对单槽位: + // sendSlotChange(slotIndex, batteryStack); + } +} + +// === 客户端:收到同步后恢复字段 === +void onSlotUpdate(int slotIndex, ItemStack newStack) { + batteryStack = newStack.copy(); + battery.holder.loadFromStack(batteryStack, level.registryAccess(), true); + // onEnergyChanged() 被 @ClientFieldChangeListener 自动触发 + // 无需额外渲染刷新 +} + +// === C2S:客户端主动修改 === +void onOutputRateSet(int newRate) { + battery.outputRate = newRate; + CompoundTag tag = new CompoundTag(); + tag.putInt("outputRate", newRate); + PacketDistributor.sendToServer(new C2SItemUpdatePacket(slotIndex, tag)); +} + +// 服务端接收 +void handleC2SUpdate(int slotIndex, CompoundTag tag) { + battery.holder.applyServerUpdate(level.registryAccess(), tag); + battery.holder.flushToStack(batteryStack, level.registryAccess()); + broadcastChanges(); +} +``` + +### 6.4 与方块实体的对应关系 + +| 方块实体 | 物品 | +|----------|------| +| `ManagedSyncBlockEntity` | `BatteryItemData implements ISyncManaged` | +| `updateTick()` 定期扫描 | `scanChanges()` 事件驱动扫描 | +| `getUpdatePacket()` 发包 | `flushToStack()` + 容器 `broadcastChanges()` | +| `handleClientUpdate()` | `applyServerUpdate()` | +| `loadAdditional()` | `loadFromStack()` | +| `saveAdditional()` | `saveToStack()` | + +--- + +## 7. 方块实体集成 + +### `ManagedSyncBlockEntity` + +```java +public abstract class ManagedSyncBlockEntity extends BlockEntity implements ISyncManaged { + + protected final SyncDataHolder syncDataHolder; + + // saveAdditional() → @SaveField + // saveWithoutMetadata() → @ItemSave (含 DataComponent) + // loadAdditional() → @SaveField + @ItemSave + // getUpdateTag() → @SyncToClient 全量 + // getUpdatePacket() → @SyncToClient 增量 + // updateTick() → 自动扫描 @SyncToClient/@SyncBoth + // handleClientUpdate() → @SyncToServer/@SyncBoth +} +``` + +### Tick 注册 + +```java +public static void tick(Level level, BlockPos pos, BlockState state, MyBE be) { + be.updateTick(); // 自动检测同步字段变更 + if (be.active) be.progress++; // 无需手动标记 +} +``` + +--- + +## 8. 完整使用示例 + +```java +@Getter +public class MyMachineBlockEntity extends ManagedSyncBlockEntity { + + @SaveField + private UUID ownerUUID; + + @ItemSave(nbtKey = "cfg") + private MachineConfig savedConfig; + + @SyncToClient @RerenderOnChanged + private boolean active; + + @SyncToClient @RerenderOnChanged + private int progress; + + @SyncBoth + private int targetValue; + + public MyMachineBlockEntity(BlockPos pos, BlockState state) { + super(MyBlockEntities.MY_MACHINE.get(), pos, state); + } + + @ClientFieldChangeListener(fieldName = "progress") + public void onProgressChanged() { + if (getLevel().isClientSide) updateProgressBar(); + } + + @ClientFieldChangeListener(fieldName = "active") + public void onActiveChanged() { + if (getLevel().isClientSide) playActivationSound(); + } + + public static void tick(Level level, BlockPos pos, BlockState state, MyMachineBlockEntity be) { + be.updateTick(); + if (be.active && be.progress < 100) be.progress++; + } +} +``` + +### 注解职责对照 + +| 注解 | 序列化 | 做什么 | +|--------------------------|-----------------------|---------------------| +| `@SaveField` UUID | Codec → NBT | 仅世界存档 | +| `@ItemSave` config | Codec → DataComponent | 仅物品存储 | +| `@SyncToClient` active | Codec → NBT 网络包 | 仅 S2C 同步 + 重渲染 | +| `@SyncToClient` progress | Codec → NBT 网络包 | 仅 S2C 同步 + 回调 + 重渲染 | +| `@SyncBoth` targetValue | Codec → NBT 网络包 | 双端同步 | + +--- + +## 附录 + +### A. 序列化路径 + +``` +saveAdditional() → serializeToSaveNBT() → @SaveField → Codec → NBT +saveWithoutMetadata() → serializeToItemNBT() → @ItemSave → Codec → NBT + + DataComponent → BLOCK_ITEM_DATA +getUpdateTag() → serializeFullClientSyncNBT → @SyncToClient/@SyncBoth +updateTick() 检测 → scanAndMarkChanges() → @SyncToClient/@SyncBoth +getUpdatePacket() → getPendingChanges() → @SyncToClient/@SyncBoth +loadAdditional(存档) → deserializeNBT(false) → @SaveField +loadAdditional(客户) → deserializeNBT(true) → @SyncToClient/@SyncBoth +loadAdditional() → deserializeItemNBT() → @ItemSave +handleClientUpdate() → applyServerUpdate() → @SyncToServer/@SyncBoth +``` + +### B. 问答 + +**Q: 序列化现在用什么?** +A: Minecraft 原生的 `Codec` + `NbtOps`。字段类型通过 `FieldCodecs` 查找对应 Codec。可注册自定义 Codec。 + +**Q: @ItemSave 如何存储数据?** +A: 通过注册的 `DataComponentType`(`SyncedComponents.BLOCK_ITEM_DATA`)。掉落时自动写入 ItemStack,放置时自动读取。 + +**Q: 如何手动操作物品数据?** +A: `syncDataHolder.applyToItemStack(stack, registries)` 写入;`be.loadFromItemStack(stack)` 读取。 + +**Q: @SyncToServer 需要手动发包吗?** +A: 是的。注解仅标记字段允许 C2S。网络包需自行实现,服务端收到后调用 `handleClientUpdate()`。 + +**Q: @RerenderOnChanged 需要配合 @SyncToClient 吗?** +A: 是的,它只标记"需要重渲染",不负责同步。 diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..a4df5e5 --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,110 @@ +# SyncedData Lib Wiki + +一个基于 **注解驱动 + 自动扫描 + DataComponent** 的数据同步框架,专为 NeoForge 26.1 设计。 + +## 特性 + +- **7 个注解,各司其职** — 保存、同步、重渲染、回调完全分离 +- **自动变更检测** — 无需手动 `markDirty`,`updateTick()` 定期扫描字段并自动推送变更 +- **基于 Codec** — 所有序列化使用 Minecraft 原生 `Codec` + `NbtOps` +- **基于 DataComponent** — 物品数据通过 `DataComponentType` 存储 +- **可扩展** — 注册自定义 `Codec` 即可支持任意字段类型 +- **无反射运行时开销** — `MethodHandles` + `VarHandle` 直接读写字段 + +## 项目结构 + +``` +com.modularmc.synceddata +├── api +│ ├── blockentity/ +│ │ └── BlockEntityCreationInfo.java — BE 创建信息 record +│ └── sync_system/ ← 同步系统核心 +│ ├── annotations/ — 7 个注解 +│ ├── holder/ — 数据持有实现 +│ │ ├── SyncDataHolder.java — 核心数据容器 +│ │ └── ItemSyncHolder.java — 物品数据包装 +│ ├── meta/ — 元数据处理 +│ │ ├── ClassSyncData.java — 类级注解缓存 +│ │ ├── FieldSyncData.java — 字段元数据 +│ │ ├── FieldCodecs.java — Codec 注册表 +│ │ └── TypeDeclaration.java — 类型声明 +│ ├── ISyncManaged.java — 核心接口 +│ ├── ManagedSyncBlockEntity.java — BE 基类 +│ └── SyncedComponents.java — DataComponent 注册 +├── SyncedData.java — 主 Mod 入口 +└── utils/ + └── FormattingUtil.java — 字符串工具 +``` + +## 快速开始 + +### 1. 注册 DataComponent + +在主 Mod 构造器中注册 `SyncedComponents`: + +```java +public SyncedData(IEventBus bus) { + SyncedComponents.COMPONENTS.register(bus); +} +``` + +### 2. 创建同步方块实体 + +```java +@Getter +public class MyMachineBE extends ManagedSyncBlockEntity { + + @SaveField + private UUID owner; + + @SaveField @ItemSave + private int storedEnergy; + + @SyncToClient @RerenderOnChanged + private boolean active; + + @SyncBoth + private int target; + + @ClientFieldChangeListener(fieldName = "active") + public void onActiveChange() { + if (getLevel() instanceof ClientLevel) playSound(); + } + + public MyMachineBE(BlockPos pos, BlockState state) { + super(MyBlockEntities.MY_MACHINE.get(), pos, state); + } + + public static void tick(Level level, BlockPos pos, BlockState state, MyMachineBE be) { + be.updateTick(); // 自动扫描 @SyncToClient/@SyncBoth + if (be.active) be.storedEnergy++; + } +} +``` + +### 3. 创建物品同步数据 + +```java +public class BatteryData implements ISyncManaged { + + private final ItemSyncHolder holder = new ItemSyncHolder(this); + + @ItemSave @SyncToClient + private int energy; + + @SyncBoth + private int maxEnergy; + + public SyncDataHolder getSyncDataHolder() { return holder.getSyncDataHolder(); } + public void scheduleRenderUpdate() {} + public void markAsChanged() {} +} +``` + +## 下一步 + +- [注解参考](annotations.md) +- [API 参考](api.md) +- [方块实体集成](blockentity.md) +- [物品集成](items.md) +- [同步流程与序列化](sync-flows.md) diff --git a/docs/wiki/annotations.md b/docs/wiki/annotations.md new file mode 100644 index 0000000..e2f4536 --- /dev/null +++ b/docs/wiki/annotations.md @@ -0,0 +1,198 @@ +# 注解参考 + +## 总览 + +| 注解 | 目标 | 职责 | 参数 | +|------------------------------|----|-------------------------|-------------| +| `@SaveField` | 字段 | 持久化到世界存档 NBT | `nbtKey` | +| `@ItemSave` | 字段 | 方块掉落时存入物品 DataComponent | `nbtKey` | +| `@SyncToClient` | 字段 | 服务端→客户端自动推送 | — | +| `@SyncToServer` | 字段 | 客户端→服务端上报标记 | — | +| `@SyncBoth` | 字段 | 双端同步(S2C + C2S) | — | +| `@RerenderOnChanged` | 字段 | 客户端收到更新后重渲染方块 | — | +| `@ClientFieldChangeListener` | 方法 | 客户端收到字段更新后回调 | `fieldName` | + +## `@SaveField` + +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SaveField { + String nbtKey() default ""; +} +``` + +**职责**:仅在世界存档时保存 / 读取字段值。 + +**存储路径**: +- 写入:`saveAdditional(ValueOutput)` → `serializeToSaveNBT()` → `output.store(nbtKey, codec, value)` +- 读取:`loadAdditional(ValueInput)` → `deserializeNBT()` → `codec.parse(input)` + +`nbtKey` 指定 NBT 中的键名,默认使用字段名。一个字段只对应一个键。 + +**注意**:不影响方块掉落/放置时的物品数据,也不同步到客户端。 + +### 使用示例 + +```java +@SaveField +private UUID owner; + +@SaveField(nbtKey = "charge") +private int chargeLevel; +``` + +## `@ItemSave` + +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ItemSave { + String nbtKey() default ""; +} +``` + +**职责**:方块破坏时自动将字段值存入掉落物的 `BLOCK_ITEM_DATA` DataComponent,放置时自动解析回字段。 + +**存储路径**: +- 方块破坏:`collectImplicitComponents()` → `components.set(BLOCK_ITEM_DATA, tag)` → 写入 ItemStack +- 方块放置:`applyImplicitComponents()` → `stack.get(BLOCK_ITEM_DATA)` → `deserializeItemNBT()` + +`nbtKey` 指定 DataComponent 内部 CompoundTag 的键名。 + +### 使用示例 + +```java +@ItemSave +private int storedEnergy; + +@ItemSave(nbtKey = "cfg_override") +private String configHash; +``` + +## `@SyncToClient` + +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SyncToClient {} +``` + +**职责**:标记字段需要服务端→客户端实时同步。 + +**机制**: +1. `updateTick()` 每 tick 调用 `scanAndMarkChanges()` +2. 遍历所有 `@SyncToClient` 字段,用 `Objects.equals()` 比较当前值与快照 +3. 发现变更 → Codec 序列化 → 存储 `pendingClientChanges` +4. `sendBlockUpdated()` → `getUpdatePacket()` → `getPendingChanges()` 消费 → 发送到客户端 +5. 客户端收到后 → `deserializeNBT()` → 更新字段 → 回调 → 重渲染 + +**无需手动调用任何方法**。变更会在下一个 server tick 自动检测并推送。 + +### 配合 `@RerenderOnChanged` + +```java +@SyncToClient +@RerenderOnChanged // 此字段变更时触发方块重渲染 +private boolean active; +``` + +### 配合 `@ClientFieldChangeListener` + +```java +@SyncToClient +private int progress; + +@ClientFieldChangeListener(fieldName = "progress") +public void onProgress() { + // 客户端 UI 更新 +} +``` + +## `@SyncToServer` + +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SyncToServer {} +``` + +**职责**:标记字段允许客户端→服务端上报。 + +**注意**:此注解仅标记"此字段可接受 C2S 更新"。实际的网络包收发需要自行实现,服务端收到后调用: + +```java +be.handleClientUpdate(registries, packetTag); +``` + +安全校验(权限、值范围、防作弊)必须在调用前完成。 + +## `@SyncBoth` + +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SyncBoth {} +``` + +**职责**:等价于 `@SyncToClient @SyncToServer`。 + +- **S2C**:`updateTick()` 自动扫描、检测变更并推送 +- **C2S**:`handleClientUpdate()` + `applyServerUpdate()` 接收 + +不需要同时标注两个注解,一个 `@SyncBoth` 即可。 + +## `@RerenderOnChanged` + +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RerenderOnChanged {} +``` + +**职责**:标记某个 `@SyncToClient` 或 `@SyncBoth` 字段在客户端收到更新后需要**重渲染方块**。 + +**注意**:此注解不负责同步。必须配合 `@SyncToClient` 或 `@SyncBoth` 使用。 + +```java +@SyncToClient @RerenderOnChanged +private boolean active; +``` + +## `@ClientFieldChangeListener` + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ClientFieldChangeListener { + String fieldName(); +} +``` + +**职责**:标记一个方法,在客户端收到指定字段的同步数据后自动调用。 + +**约束**: +- 方法不能是 static +- 方法无参数、无返回值 +- 一个字段可以绑定多个 Listener + +```java +@ClientFieldChangeListener(fieldName = "progress") +public void onProgressChanged() { + // 自动调用 +} +``` + +## 注解组合策略 + +| 场景 | 注解组合 | 说明 | +|-----------|----------------------------------------------|-------------| +| 仅世界存档 | `@SaveField` | 存档持久化 | +| 仅物品存储 | `@ItemSave` | 掉落/放置 | +| 存档+物品 | `@SaveField @ItemSave` | 既存世界又存物品 | +| 仅 S2C | `@SyncToClient` | 服务端→客户端 | +| S2C+重渲染 | `@SyncToClient @RerenderOnChanged` | 同步 + 重渲染 | +| S2C+回调 | `@SyncToClient + @ClientFieldChangeListener` | 同步 + 回调 | +| 双端 | `@SyncBoth` | S2C + C2S | +| 双端+重渲染 | `@SyncBoth @RerenderOnChanged` | 双端 + 重渲染 | +| 存档+物品+S2C | `@SaveField @ItemSave @SyncToClient` | 完整持久化 + S2C | diff --git a/docs/wiki/api.md b/docs/wiki/api.md new file mode 100644 index 0000000..27258ae --- /dev/null +++ b/docs/wiki/api.md @@ -0,0 +1,290 @@ +# API 参考 + +## `ISyncManaged` + +**包路径**:`com.modularmc.synceddata.api.sync_system.ISyncManaged` + +所有需要同步系统的类必须实现的接口。 + +```java +public interface ISyncManaged { + SyncDataHolder getSyncDataHolder(); + void scheduleRenderUpdate(); + void markAsChanged(); +} +``` + +| 方法 | 实现要求 | 说明 | +|--------------------------|------------------------|------------------------------| +| `getSyncDataHolder()` | 返回 `SyncDataHolder` 实例 | 通常用 `ItemSyncHolder` 或直接 new | +| `scheduleRenderUpdate()` | 方块实体调度重渲染 | 物品类直接空实现 | +| `markAsChanged()` | 通知 Minecraft 该区块需要保存 | 调用 `setChanged()` | + +--- + +## `SyncDataHolder` + +**包路径**:`com.modularmc.synceddata.api.sync_system.holder.SyncDataHolder` + +数据同步的核心容器。管理注解字段的序列化、反序列化、变更检测和同步。 + +### 构造 + +```java +public SyncDataHolder(ISyncManaged owner) +``` + +构造时自动扫描 `owner.getClass()` 的注解,初始化缓存快照。 + +### 变更检测 + +```java +boolean scanAndMarkChanges(HolderLookup.Provider registries) +``` + +遍历所有 `@SyncToClient` / `@SyncBoth` 字段,与缓存的快照值比较。有变更时: +1. 序列化变更值 → `pendingClientChanges` +2. 更新快照 + +**返回值**:`true` 表示至少一个字段发生了变更。 + +### 获取变更 + +```java +CompoundTag getPendingChanges() +``` + +消费并返回待发送的客户端变更数据。返回后清空内部缓存。无变更时返回空 `CompoundTag`。 + +### 序列化 + +```java +CompoundTag serializeToSaveNBT(Provider registries); // 世界存档写入 +CompoundTag serializeToItemNBT(Provider registries); // 物品存储写入 +CompoundTag serializeFullClientSyncNBT(Provider registries); // 全量 S2C 同步 +``` + +- `serializeToSaveNBT`:仅序列化 `@SaveField` 字段 → 用于 `saveAdditional()` +- `serializeToItemNBT`:仅序列化 `@ItemSave` 字段 → 用于 `collectImplicitComponents()` +- `serializeFullClientSyncNBT`:序列化所有 `@SyncToClient` / `@SyncBoth` 字段 → 用于 `getUpdateTag()` + +### 反序列化 + +```java +void deserializeNBT(Provider registries, CompoundTag tag, boolean readingClientFields); +void deserializeItemNBT(Provider registries, CompoundTag tag); +``` + +- `deserializeNBT(true)`:反序列化 `@SyncToClient` / `@SyncBoth` 字段(客户端)。触发 `@ClientFieldChangeListener` 和 `@RerenderOnChanged` +- `deserializeNBT(false)`:反序列化 `@SaveField` 字段(服务端存档恢复) +- `deserializeItemNBT`:反序列化 `@ItemSave` 字段(从物品 DataComponent 恢复) + +### C2S 更新 + +```java +void applyServerUpdate(Provider registries, CompoundTag tag); +``` + +接收客户端发送的更新包。仅处理 `@SyncToServer` / `@SyncBoth` 字段。 + +### 物品操作 + +```java +void applyToItemStack(ItemStack stack, Provider registries); +void loadFromItemStack(ItemStack stack, Provider registries); +``` + +- `applyToItemStack`:将 `@ItemSave` 字段写入 `SyncedComponents.BLOCK_ITEM_DATA` DataComponent +- `loadFromItemStack`:从 `BLOCK_ITEM_DATA` DataComponent 读取字段 + +### 其他 + +```java +void resyncAllFields(); +``` + +强制下次扫描时将所有字段视为已变更(全量同步)。 + +--- + +## `ItemSyncHolder` + +**包路径**:`com.modularmc.synceddata.api.sync_system.holder.ItemSyncHolder` + +物品数据同步包装器。 + +```java +public final class ItemSyncHolder implements ISyncManaged { + SyncDataHolder getSyncDataHolder(); + void saveToStack(ItemStack, Provider registries); + void loadFromStack(ItemStack, Provider registries, boolean clientSide); + boolean scanChanges(Provider registries); + void flushToStack(ItemStack, Provider registries); + void applyServerUpdate(Provider registries, CompoundTag tag); +} +``` + +| 方法 | 说明 | +|----------------------------------|----------------------------------------------------------| +| `saveToStack()` | `@ItemSave` 字段 → ItemStack DataComponent | +| `loadFromStack(stack, r, false)` | 服务端:DataComponent → 字段 | +| `loadFromStack(stack, r, true)` | 客户端:DataComponent → 字段 + 触发 `@ClientFieldChangeListener` | +| `scanChanges()` | 扫描 `@SyncToClient` 字段变更 | +| `flushToStack()` | 变更增量写入 DataComponent | +| `applyServerUpdate()` | 处理 `@SyncToServer` 的 C2S 更新 | + +--- + +## `ManagedSyncBlockEntity` + +**包路径**:`com.modularmc.synceddata.api.sync_system.ManagedSyncBlockEntity` + +方块实体基类,封装了完整的同步生命周期。 + +```java +public abstract class ManagedSyncBlockEntity extends BlockEntity implements ISyncManaged { + SyncDataHolder syncDataHolder; + void markAsChanged(); + void updateTick(); // 定期扫描 + 自动推送 + void handleClientUpdate(); // C2S 处理入口 +} +``` + +### 生命周期 + +| 事件 | 触发时机 | 同步路径 | +|-------------------------------|----------------|-------------------------------------------------------| +| `saveAdditional()` | 世界存档 | `serializeToSaveNBT()` → `@SaveField` | +| `collectImplicitComponents()` | 掉落物生成 | `serializeToItemNBT()` → `@ItemSave` → DataComponent | +| `loadAdditional()` | 读档 / 客户端收到更新 | `deserializeNBT()` + `deserializeItemNBT()` | +| `getUpdateTag()` | 客户端初始加载 | `serializeFullClientSyncNBT()` → `@SyncToClient` 全量 | +| `updateTick()` | 每个 server tick | `scanAndMarkChanges()` → `getPendingChanges()` → 发包 | +| `getUpdatePacket()` | 增量更新 | `getPendingChanges()` 消费 | +| `handleClientUpdate()` | 收到 C2S 包 | `applyServerUpdate()` → `@SyncToServer` / `@SyncBoth` | + +--- + +## `SyncedComponents` + +**包路径**:`com.modularmc.synceddata.api.sync_system.SyncedComponents` + +DataComponent 注册。 + +```java +public class SyncedComponents { + DeferredRegister> COMPONENTS; + Supplier> BLOCK_ITEM_DATA; +} +``` + +| 组件 | 类型 | 用途 | +|-------------------|----------------------------------|---------------------| +| `BLOCK_ITEM_DATA` | `DataComponentType` | 存储 `@ItemSave` 字段数据 | + +注册方式: + +```java +public SyncedData(IEventBus bus) { + SyncedComponents.COMPONENTS.register(bus); +} +``` + +--- + +## `ClassSyncData` + +**包路径**:`com.modularmc.synceddata.api.sync_system.meta.ClassSyncData` + +类级别的注解元数据缓存。通过 `ClassValue` 实现: + +```java +ClassSyncData data = ClassSyncData.getClassData(MyMachineBE.class); +``` + +分组集合: + +| 集合 | 来源 | +|--------------------|-------------------------------| +| `worldSaveFields` | `@SaveField` | +| `itemSaveFields` | `@ItemSave` | +| `clientSyncFields` | `@SyncToClient` 或 `@SyncBoth` | +| `serverSyncFields` | `@SyncToServer` 或 `@SyncBoth` | +| `bothSyncFields` | `@SyncBoth` | + +--- + +## `FieldSyncData` + +**包路径**:`com.modularmc.synceddata.api.sync_system.meta.FieldSyncData` + +字段的注解元数据。 + +| 字段 | 类型 | 说明 | +|-------------------------|----------------------|-----------------------------------| +| `fieldName` | `String` | Java 字段名 | +| `nbtSaveKey` | `String` | `@SaveField.nbtKey` 或字段名 | +| `itemNbtKey` | `String` | `@ItemSave.nbtKey` 或字段名 | +| `handle` | `VarHandle` | 字段读写句柄 | +| `codec` | `Codec` | 对应的 Codec | +| `triggerClientRerender` | `boolean` | 是否标注 `@RerenderOnChanged` | +| `changeListenerHandles` | `List` | `@ClientFieldChangeListener` 方法句柄 | + +--- + +## `FieldCodecs` + +**包路径**:`com.modularmc.synceddata.api.sync_system.meta.FieldCodecs` + +Codec 注册表。根据 Java 类型查找对应的 Codec。 + +| 类型 | Codec | +|-----------------------|-------------------------------------------------------| +| `int` / `Integer` | `Codec.INT` | +| `long` / `Long` | `Codec.LONG` | +| `float` / `Float` | `Codec.FLOAT` | +| `double` / `Double` | `Codec.DOUBLE` | +| `short` / `Short` | `Codec.SHORT` | +| `byte` / `Byte` | `Codec.BYTE` | +| `boolean` / `Boolean` | `Codec.BOOL` | +| `String` | `Codec.STRING` | +| `UUID` | `Codec.STRING.xmap(UUID::fromString, UUID::toString)` | +| `CompoundTag` | `CompoundTag.CODEC` | + +**内置 Codec:** + +| 类型 | Codec | +|-----------------------|-----------------------------------| +| `int` / `Integer` | `Codec.INT` | +| `long` / `Long` | `Codec.LONG` | +| `float` / `Float` | `Codec.FLOAT` | +| `double` / `Double` | `Codec.DOUBLE` | +| `short` / `Short` | `Codec.SHORT` | +| `byte` / `Byte` | `Codec.BYTE` | +| `boolean` / `Boolean` | `Codec.BOOL` | +| `char` / `Character` | 自动转换 | +| `String` | `Codec.STRING` | +| `UUID` | `Codec.STRING.xmap()` | +| `CompoundTag` | `CompoundTag.CODEC` | +| `BlockPos` | `BlockPos.CODEC` | +| `Identifier` | `Identifier.CODEC` | +| `ItemStack` | `ItemStack.CODEC` | +| `FluidStack` | `FluidStack.CODEC` | +| `Component` | `ComponentSerialization.CODEC` | +| `int[]` | `Codec.INT_STREAM` | +| `long[]` | `Codec.LONG_STREAM` | +| `byte[]` | 自动转换 | +| `T[]` (对象数组) | `Codec.list(elem)` 自动构建 | +| `Enum` | `Enum.valueOf()` + `.name()` 自动构建 | +| `List` | `Codec.list(elem)` 自动解析泛型 | +| `Set` | 自动转换为 `LinkedHashSet` | +| `Map` | `Codec.unboundedMap(K,V)` 自动解析泛型 | +| `ISyncManaged` | 递归序列化(调用子 holder) | + +**注册自定义 Codec:** + +```java +FieldCodecs.register(MyType.class, MyType.CODEC); + +// 或注册 Supplier(处理泛型继承) +FieldCodecs.registerSupplier(List.class, () -> ListCodec.INSTANCE); +``` diff --git a/docs/wiki/architecture.md b/docs/wiki/architecture.md new file mode 100644 index 0000000..f411e06 --- /dev/null +++ b/docs/wiki/architecture.md @@ -0,0 +1,183 @@ +# 架构设计 + +## 包结构 + +``` +sync_system/ ← 同步模块根目录 +│ +├── annotations/ ← 注解定义(7个文件) +│ ├── SaveField.java — 世界存档 +│ ├── ItemSave.java — 物品存储 +│ ├── SyncToClient.java — S2C +│ ├── SyncToServer.java — C2S +│ ├── SyncBoth.java — 双端 +│ ├── RerenderOnChanged.java — 重渲染 +│ └── ClientFieldChangeListener.java — 回调 +│ +├── meta/ ← 元数据处理(内部实现) +│ ├── ClassSyncData.java — 类级注解缓存 +│ ├── FieldSyncData.java — 字段注解数据 +│ ├── FieldCodecs.java — Codec 注册表 +│ └── TypeDeclaration.java — 类型声明包装 +│ +├── holder/ ← 数据持有层 +│ ├── SyncDataHolder.java — 核心数据容器 +│ └── ItemSyncHolder.java — 物品数据包装 +│ +├── ISyncManaged.java ← 核心接口 +├── ManagedSyncBlockEntity.java ← BE 基类 +└── SyncedComponents.java ← DataComponent 注册 +``` + +## 类图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ISyncManaged (interface) │ +│ ┌──────────────────────┐────────────────────────────────┐ │ +│ │ SyncDataHolder 的内容│ scheduleRenderUpdate() │ │ +│ │ getSyncDataHolder() │ markAsChanged() │ │ +│ └──────────────────────┴────────────────────────────────┘ │ +└────────────────────────┬────────────────────────────────────┘ + │ implements + ┌────────────┴────────────┐ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────────────┐ +│ ManagedSyncBlockEntity│ │ ItemSyncHolder │ +│ (extends BlockEntity) │ │ (holder wrapper) │ +│ │ │ │ +│ @Override │ │ saveToStack() │ +│ saveAdditional() │ │ loadFromStack() │ +│ loadAdditional() │ │ scanChanges() │ +│ updateTick() │ │ flushToStack() │ +│ handleClientUpdate() │ │ applyServerUpdate() │ +└──────────┬───────────┘ └──────────────┬────────────────┘ + │ owns │ owns + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ SyncDataHolder (核心引擎) │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ 变更检测引擎 │ │ 序列化引擎 │ │ 反序列化引擎 │ │ +│ │ scanAndMark │ │ encodeField │ │ decodeField │ │ +│ │ │ │ serializeNBT │ │ deserializeNBT │ │ +│ │ cachedValues│ │ │ │ │ │ +│ │ pending │ │ │ │ │ │ +│ └─────────────┘ └──────────────┘ └──────────────────────┘ │ +│ │ +│ 持有: ClassSyncData (从 holder 的 Class 解析) │ +└─────────────────────────┬───────────────────────────────────┘ + │ uses + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ ClassSyncData │ +│ ClassValue CACHE │ +│ │ +│ Set worldSaveFields (@SaveField) │ +│ Set itemSaveFields (@ItemSave) │ +│ Set clientSyncFields (@SyncToClient) │ +│ Set serverSyncFields (@SyncToServer) │ +│ Set bothSyncFields (@SyncBoth) │ +│ │ +│ private ClassSyncData(Class) │ +│ → 扫描 @ClientFieldChangeListener 方法 │ +│ → 扫描 @SaveField/@SyncToClient/... 字段 │ +│ → 创建 FieldSyncData + FieldCodecs 解析 │ +│ → 递归处理父类 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 数据流总图 + +``` + ┌─────────────────┐ + │ Minecraft │ + │ Server │ + └────────┬────────┘ + │ + ┌──────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────────┐ ┌──────────┐ ┌──────────────────┐ + │ saveAdditional│ │updateTick│ │collectImplicit │ + │ (存档) │ │(每tick) │ │Components(掉落) │ + └──────┬───────┘ └────┬─────┘ └────────┬─────────┘ + │ │ │ + ▼ ▼ ▼ + ┌────────────────────────────────────────────┐ + │ SyncDataHolder │ + │ │ + │ serializeToSaveNBT() ← @SaveField │ + │ serializeToItemNBT() ← @ItemSave │ + │ scanAndMarkChanges() ← @SyncToClient │ + │ getPendingChanges() → 增量更新包 │ + │ serializeFullClientSync → @SyncToClient │ + │ deserializeNBT() → 恢复字段 │ + │ deserializeItemNBT() → @ItemSave 恢复 │ + │ applyServerUpdate() → @SyncToServer │ + └──────────────┬─────────────────────────────┘ + │ + ▼ + ┌────────────────┐ + │ 网络/存储 │ + └────────────────┘ + │ + ▼ + ┌────────────────┐ + │ ClientLevel │ + │ (客户端) │ + │ │ + │ loadAdditional │ + │ → deserializeNBT(true) │ + │ → 更新字段值 + 缓存 │ + │ → @ClientFieldChangeListener 回调 │ + │ → @RerenderOnChanged → 重渲染 │ + └────────────────┘ +``` + +## 变更检测引擎 + +``` + ┌──────────────┐ + │ cachedValues │ + │ Map │ + └──────┬───────┘ + │ + Objects.equals(current, previous)? + ┌──────┐ ┌───────┐ + │ 相同 │ │ 不同 │ + │ 跳过 │ │ 序列化 │ + └──────┘ │ 更新 │ + │ 快照 │ + └───┬───┘ + │ hasChanges = true + ▼ + ┌────────────────┐ + │ pendingClient │ + │ Changes │ + │ (CompoundTag) │ + └────────────────┘ + │ getPendingChanges() + ▼ + getUpdatePacket() + → sendBlockUpdated() +``` + +## Time Complexity + +| 操作 | 复杂度 | 说明 | +|----------|---------------|------------------------------------------| +| 注解扫描 | `O(F)` 每类一次 | F = 字段数,`ClassValue` 全局缓存 | +| 变更检测 | `O(F)` 每 tick | 仅遍历 `@SyncToClient` 字段 | +| 序列化 | `O(F')` | F' = 实际变数字段数 | +| 反序列化 | `O(F')` | F' = 收到包中的字段数 | +| Codec 查找 | `O(R)` | R = 注册数,`Reference2ReferenceOpenHashMap` | + +## 线程安全 + +**非线程安全**。`SyncDataHolder` 及其关联类设计在**单线程环境**使用: +- 方块实体:服务端 tick thread +- 物品:容器所属的 server/client thread + +不要在异步线程中读写同步字段。 diff --git a/docs/wiki/blockentity.md b/docs/wiki/blockentity.md new file mode 100644 index 0000000..59a3d1b --- /dev/null +++ b/docs/wiki/blockentity.md @@ -0,0 +1,207 @@ +# 方块实体集成 + +继承 `ManagedSyncBlockEntity` 即可获得完整的同步生命周期。 + +## 基类 + +```java +public abstract class ManagedSyncBlockEntity extends BlockEntity implements ISyncManaged { + + protected final SyncDataHolder syncDataHolder = new SyncDataHolder(this); + + // ✅ 自动实现的方法 + // saveAdditional(ValueOutput) → @SaveField 世界存档 + // loadAdditional(ValueInput) → 存档/客户端恢复 + // collectImplicitComponents() → @ItemSave 物品 DataComponent + // applyImplicitComponents() → 放置时从物品恢复 + // getUpdateTag(Provider) → 客户端首次加载(全量) + // getUpdatePacket() → 增量更新(仅变更字段) + // markAsChanged() → setChanged() +} +``` + +## 实现步骤 + +### 1. 继承基类 + +```java +public class MyMachineBlockEntity extends ManagedSyncBlockEntity { + + public MyMachineBlockEntity(BlockPos pos, BlockState state) { + super(MyBlockEntities.MY_MACHINE.get(), pos, state); + } +} +``` + +### 2. 添加注解字段 + +```java +@Getter +public class MyMachineBlockEntity extends ManagedSyncBlockEntity { + + @SaveField // 世界存档 + private UUID ownerUUID; + + @SaveField @ItemSave // 世界存档 + 物品存储 + private int storedEnergy; + + @SyncToClient @RerenderOnChanged // 同步 + 重渲染 + private boolean active; + + @SyncToClient @RerenderOnChanged // 同步 + 重渲染 + private int progress; + + @SyncBoth // 双端同步 + private int targetValue; +} +``` + +### 3. 添加变更回调 + +```java +@ClientFieldChangeListener(fieldName = "progress") +public void onProgressChanged() { + if (getLevel() instanceof ClientLevel) { + updateProgressBar(); + } +} + +@ClientFieldChangeListener(fieldName = "active") +public void onActiveChanged() { + if (getLevel() instanceof ClientLevel) { + playActivationSound(); + } +} +``` + +### 4. 注册 Tick 方法 + +```java +public static void tick(Level level, BlockPos pos, BlockState state, MyMachineBlockEntity be) { + be.updateTick(); // ← 自动扫描 @SyncToClient/@SyncBoth 字段 + + if (be.active && be.progress < 100) { + be.progress++; // 下一 tick 自动检测到变更 + } +} +``` + +`updateTick()` 执行的操作: +1. 调用 `setChanged()`(标记区块需要保存) +2. 仅服务端:调用 `scanAndMarkChanges()` +3. 有变更 → `sendBlockUpdated()` → `getUpdatePacket()` → `getPendingChanges()` → 发送到客户端 + +## 数据流详解 + +### 世界存档与读取 + +``` +存档: + saveAdditional(ValueOutput output) + → 获取 registryAccess + → serializeToSaveNBT(registries) // 仅 @SaveField + → serializeToItemNBT(registries) // 仅 @ItemSave + → output.store("synced", CODEC, mergedTag) + +读档: + loadAdditional(ValueInput input) + → 获取 registryAccess + → input.read(MapCodec) → 得到 "synced" 标签 + → deserializeNBT(registries, tag, false) // @SaveField + → deserializeItemNBT(registries, tag) // @ItemSave(仅服务端) +``` + +### S2C 同步 + +``` +每个 Server Tick: + updateTick() + → scanAndMarkChanges(registries) + for each @SyncToClient field: + → VarHandle.get(holder) → 缓存中的旧值 + → Objects.equals() 比较 + → 不同 → codec.encodeStart(NbtOps, value) + → changes.put(nbtSaveKey, serialized) + → 更新缓存快照 + → 存储 pendingClientChanges + → sendBlockUpdated() + → getUpdatePacket() + → getPendingChanges() 消费 pending + → ClientboundBlockEntityDataPacket + +客户端收到: + → loadAdditional() + → deserializeNBT(registries, tag, true) + → codec.parse(NbtOps, tag) + → VarHandle.set(holder) + → 更新缓存 + → 调用 @ClientFieldChangeListener 方法 + → @RerenderOnChanged → scheduleRenderUpdate() +``` + +### 物品掉落与放置 + +``` +方块破坏: + collectImplicitComponents(DataComponentMap.Builder) + → serializeToItemNBT(registries) + → components.set(BLOCK_ITEM_DATA, tag) + +方块放置: + applyImplicitComponents(DataComponentGetter) + → components.get(BLOCK_ITEM_DATA) + → deserializeItemNBT(registries, tag) +``` + +### C2S + +``` +客户端 → 自定义网络包 + +服务端收到: + → 权限校验(调用方负责) + → handleClientUpdate(registries, packetTag) + → applyServerUpdate(registries, tag) + → for each @SyncToServer/@SyncBoth: + → codec.parse(NbtOps, tag) + → VarHandle.set(holder) +``` + +## 完整示例 + +```java +@Getter +public class EnergyCellBlockEntity extends ManagedSyncBlockEntity { + + @SaveField @ItemSave(nbtKey = "e") + private int energy; + + @SaveField @ItemSave(nbtKey = "cap") + private int capacity = 10000; + + @SyncToClient @RerenderOnChanged + private boolean active; + + @SyncBoth + private int outputTarget; + + public EnergyCellBlockEntity(BlockPos pos, BlockState state) { + super(MyBlockEntities.ENERGY_CELL.get(), pos, state); + } + + @ClientFieldChangeListener(fieldName = "active") + public void onActiveStateChanged() { + if (getLevel() instanceof ClientLevel) { + renderActiveState(active); + } + } + + public static void tick(Level level, BlockPos pos, BlockState state, EnergyCellBlockEntity be) { + be.updateTick(); + + if (be.active && be.energy < be.capacity) { + be.energy = Math.min(be.capacity, be.energy + 10); + } + } +} +``` diff --git a/docs/wiki/items.md b/docs/wiki/items.md new file mode 100644 index 0000000..553bc91 --- /dev/null +++ b/docs/wiki/items.md @@ -0,0 +1,169 @@ +# 物品集成 + +物品使用 `ItemSyncHolder` 包装同步字段。所有数据通过 `SyncedComponents.BLOCK_ITEM_DATA` DataComponent 存储。 + +## 核心类 + +`ItemSyncHolder` 是 `ISyncManaged` 的实现,包装物品的同步字段: + +```java +public final class ItemSyncHolder implements ISyncManaged { + SyncDataHolder getSyncDataHolder(); + void saveToStack(ItemStack, Provider); // 写入 DataComponent + void loadFromStack(ItemStack, Provider, boolean); // 读取 DataComponent + boolean scanChanges(Provider); // 扫描变更 + void flushToStack(ItemStack, Provider); // 增量写入 + void applyServerUpdate(Provider, CompoundTag); // C2S +} +``` + +## 定义物品数据类 + +```java +public class BatteryData implements ISyncManaged { + + private final ItemSyncHolder holder = new ItemSyncHolder(this); + + @ItemSave @SyncToClient + private int energy; + + @ItemSave + private int capacity = 10000; + + @SyncBoth + private int outputRate; + + @ClientFieldChangeListener(fieldName = "energy") + public void onEnergyChanged() { + // 客户端 GUI 收到更新后自动刷新 + } + + public SyncDataHolder getSyncDataHolder() { return holder.getSyncDataHolder(); } + public void scheduleRenderUpdate() {} + public void markAsChanged() {} +} +``` + +## 数据流 + +### 服务端:修改并同步 + +```java +// 在容器菜单或机器 tick 中 +battery.energy += chargeAmount; +battery.capacity = newMax; + +// 事件驱动:扫描检测变更 +if (battery.holder.scanChanges(level.registryAccess())) { + // 将变更写入 ItemStack DataComponent + battery.holder.flushToStack(batteryStack, level.registryAccess()); + + // 通知容器广播 + broadcastChanges(); + // 或针对单槽: + // sendSlotUpdate(slotIndex, batteryStack); +} +``` + +### 客户端:接收更新 + +```java +@Override +public ItemStack quickMoveStack(Player player, int index) { + // ... +} + +@Override +public void slotsChanged(Container container) { + // 容器变更后加载同步数据 + ItemStack slotStack = container.getItem(slotIndex); + battery.holder.loadFromStack(slotStack, level.registryAccess(), true); + // true = 客户端模式, 触发 @ClientFieldChangeListener +} +``` + +### C2S:客户端主动上报 + +```java +// 客户端 GUI 操作 +void setOutputRate(int newRate) { + battery.outputRate = newRate; + + // 构建更新包 + CompoundTag tag = new CompoundTag(); + tag.putInt("outputRate", newRate); + + // 发送到服务端 + PacketDistributor.sendToServer(new C2SItemUpdatePacket(slotIndex, tag)); +} + +// 服务端接收 +void handleC2SUpdate(ServerPlayer player, int slotIndex, CompoundTag tag) { + // 1. 权限校验 + if (!canModify(player)) return; + + // 2. 数值校验 + int rate = tag.getInt("outputRate"); + if (rate < 0 || rate > 100) return; + + // 3. 应用更新 + battery.holder.applyServerUpdate(level.registryAccess(), tag); + + // 4. 写回物品并广播 + battery.holder.flushToStack(batteryStack, level.registryAccess()); + broadcastChanges(); +} +``` + +## 容器集成示例 + +```java +public class MachineContainer extends AbstractContainerMenu { + + private final BatteryData battery = new BatteryData(); + private ItemStack batteryStack = ItemStack.EMPTY; + + @Override + public void broadcastChanges() { + // 扫描变更 + if (battery.holder.scanChanges(registryAccess)) { + // 写入物品 + battery.holder.flushToStack(batteryStack, registryAccess); + } + super.broadcastChanges(); + } + + @Override + public void setItem(int slot, int state, ItemStack stack) { + super.setItem(slot, state, stack); + if (slot == BATTERY_SLOT) { + batteryStack = stack.copy(); + battery.holder.loadFromStack(batteryStack, registryAccess, isClientSide()); + } + } +} +``` + +## 注解支持 + +| 注解 | 在物品上适用 | 说明 | +|------------------------------|--------|-------------------| +| `@ItemSave` | ✅ | 存储到 DataComponent | +| `@SyncToClient` | ✅ | S2C 同步(事件驱动) | +| `@SyncToServer` | ✅ | C2S 标记 | +| `@SyncBoth` | ✅ | 双端同步 | +| `@RerenderOnChanged` | ❌ | 物品无渲染概念 | +| `@ClientFieldChangeListener` | ✅ | 客户端回调 | +| `@SaveField` | ❌ | 仅世界存档 | + +**注意**:`@SaveField` 在物品上不适用(物品数据通过 DataComponent 而非世界 NBT 持久化)。 + +## 与方块实体的对比 + +| 方面 | 方块实体 | 物品 | +|------|------------------------------|-----------------------------------| +| 基类 | `ManagedSyncBlockEntity` | `ItemSyncHolder` + `ISyncManaged` | +| 数据存储 | `ValueOutput` / `ValueInput` | `DataComponent` | +| 同步驱动 | `updateTick()` 定期扫描 | `scanChanges()` 事件驱动 | +| 对象管理 | 由 Minecraft 管理生命周期 | 开发者管理生命周期 | +| 容器广播 | `sendBlockUpdated()` | `broadcastChanges()` | diff --git a/docs/wiki/sync-flows.md b/docs/wiki/sync-flows.md new file mode 100644 index 0000000..788d15e --- /dev/null +++ b/docs/wiki/sync-flows.md @@ -0,0 +1,208 @@ +# 同步流程与序列化 + +## 变更检测(核心机制) + +**无需手动标记脏字段**。系统通过定期扫描 + 值比较自动检测变更。 + +### 算法 + +``` +scanAndMarkChanges(registries): + ├── fullSyncPending? + │ ├── true → 所有字段视为已变更 + │ └── false → 逐字段比较 + │ + ├── for each field in @SyncToClient ∪ @SyncBoth: + │ ├─ current = VarHandle.get(holder) ← 取当前值 + │ ├─ previous = cachedValues.get(name) ← 取快照值 + │ ├─ changed = fullSyncPending || !Objects.equals(current, previous) + │ │ + │ └─ if changed: + │ ├─ encodeField(field, current) ← Codec 序列化 + │ ├─ changes.put(nbtSaveKey, tag) ← 收集变更 + │ └─ cachedValues.put(name, current) ← 更新快照 + │ + ├─ changes → pendingClientChanges + ├─ fullSyncPending = false + └─ return hasChanges +``` + +### 比较方式 + +使用 `Objects.equals()` 进行值比较。对于: + +| 类型 | 比较行为 | +|----------------------|--------------------------------------| +| `int`, `long` 等原始包装类 | `equals()` 值比较 | +| `String` | 字符串内容比较 | +| `UUID` | `equals()` | +| `ItemStack` | 需要 `ItemStack.matches()`(需自定义 Codec) | +| `List`, `Map`, `Set` | `AbstractCollection.equals()` 内容比较 | +| 数组 | `Arrays.equals()`(需自定义 Codec) | +| `ISyncManaged` | 引用比较(对象是否变更由内部 holder 管理) | +| `null` | 用 `CompoundTag{"null": true}` 表示 | + +**注意**:对于可变对象(如 `List` 内部元素变更),如果引用不变,`Objects.equals()` 会视为无变更。此时应调用 `resyncAllFields()` 强制全量同步,或替换为新对象。 + +## 序列化机制 + +所有序列化使用 Minecraft 原生 `Codec` + `NbtOps`。 + +### encodeField + +```java +private Tag encodeField(FieldSyncData field, Object value, Provider registries) { + if (value == null) return CompoundTag{"null": true}; + + if (field.codec != null) + return codec.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), value); + + if (value instanceof ISyncManaged) + return value.getSyncDataHolder().serializeToSaveNBT(registries); + + // 找不到 Codec → 报错 + return CompoundTag{}; +} +``` + +### decodeField + +```java +private Object decodeField(FieldSyncData field, Tag tag, Object current, Provider registries) { + if (tag.isNullTag()) return null; + if (tag.isEmpty()) return current; + + if (field.codec != null) + return codec.parse(registries.createSerializationContext(NbtOps.INSTANCE), tag); + + if (field.isSyncManaged && tag instanceof CompoundTag) + return recurseDeserialize(current, tag, registries); + + return current; // 找不到 Codec → 保持原值 +} +``` + +### Codec 查找 + +```java +FieldCodecs.get(field.getGenericType()) +``` + +1. 原始类型 → 装箱类型 +2. 查 `REGISTRY` Map +3. 查 `SUPPLIERS`(按类型可赋值性匹配) +4. 找不到 → `null`(字段将被跳过,日志报错) + +## DataComponent 存储 + +`@ItemSave` 字段通过 `SyncedComponents.BLOCK_ITEM_DATA` DataComponent 存储在 ItemStack 上。 + +``` +ItemStack + └── DataComponentMap + └── synced:block_item_data (DataComponentType) + ├── "energy" → IntTag(100) ← @ItemSave(nbtKey = "energy") + ├── "cfg_override" → StringTag("v2") ← @ItemSave(nbtKey = "cfg_override") + └── ... +``` + +注册方式: + +```java +// SyncedComponents.java +public static final Supplier> BLOCK_ITEM_DATA = + COMPONENTS.register("block_item_data", + () -> DataComponentType.builder() + .persistent(CompoundTag.CODEC) + .build()); +``` + +## 同步流程对比 + +### 方块实体 S2C(定期扫描) + +``` +时间轴: + T0: 值 = 0, 快照 = 0 → 无变更 + T1: progress++ (值 = 1, 快照仍 = 0) + T2: updateTick() + → 扫描: 值=1, 快照=0 → 变更! + → serialize: {"nbtSaveKey": 1} + → sendBlockUpdated() + T2: getUpdatePacket() + → getPendingChanges() → {"nbtSaveKey": 1} + → ClientboundBlockEntityDataPacket + T3: 客户端收到 + → deserializeNBT(true) + → progress = 1 + → 更新快照 + → 触发 @ClientFieldChangeListener + → scheduleRenderUpdate() +``` + +### 物品 S2C(事件驱动) + +``` +服务器: + battery.energy += 100; + holder.scanChanges(registries) → true + holder.flushToStack(stack, registries) + broadcastChanges() + → ClientboundContainerSetSlotPacket + +客户端: + setItem(slot, stack) + holder.loadFromStack(stack, registries, true) + → deserializeItemNBT() 恢复 @ItemSave 字段 + → deserializeNBT(registries, data, true) + → 更新 @SyncToClient 字段 + → 触发 @ClientFieldChangeListener 回调 +``` + +## 注册自定义 Codec + +```java +// 1. 定义 Codec +public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.INT.fieldOf("value").forGetter(MyData::value), + Codec.STRING.fieldOf("name").forGetter(MyData::name) + ).apply(instance, MyData::new)); + +// 2. 注册 +FieldCodecs.register(MyData.class, CODEC); + +// 或注册 Supplier(处理泛型) +FieldCodecs.registerSupplier(List.class, () -> MyListCodec.INSTANCE); +``` + +## 类型支持表 + +| Java 类型 | Codec | 内置 | 说明 | +|-----------------------|--------------------------------|----|------------------------------| +| `int` / `Integer` | `Codec.INT` | ✅ | | +| `long` / `Long` | `Codec.LONG` | ✅ | | +| `float` / `Float` | `Codec.FLOAT` | ✅ | | +| `double` / `Double` | `Codec.DOUBLE` | ✅ | | +| `short` / `Short` | `Codec.SHORT` | ✅ | | +| `byte` / `Byte` | `Codec.BYTE` | ✅ | | +| `boolean` / `Boolean` | `Codec.BOOL` | ✅ | | +| `char` / `Character` | `Codec.STRING.xmap(...)` | ✅ | | +| `String` | `Codec.STRING` | ✅ | | +| `UUID` | `Codec.STRING.xmap(...)` | ✅ | | +| `CompoundTag` | `CompoundTag.CODEC` | ✅ | | +| `BlockPos` | `BlockPos.CODEC` | ✅ | | +| `Identifier` | `Identifier.CODEC` | ✅ | | +| `ItemStack` | `ItemStack.CODEC` | ✅ | | +| `FluidStack` | `FluidStack.CODEC` | ✅ | | +| `Component` | `ComponentSerialization.CODEC` | ✅ | 聊天组件 | +| `int[]` | `Codec.INT_STREAM` | ✅ | | +| `long[]` | `Codec.LONG_STREAM` | ✅ | | +| `byte[]` | `Codec.list(Codec.BYTE)` | ✅ | | +| `Enum` | 自动构建 | ✅ | `Enum.valueOf()` + `.name()` | +| `T[]` (对象数组) | `Codec.list(elem)` | ✅ | 自动构建 | +| `List` | `Codec.list(elem)` | ✅ | 自动解析泛型 | +| `Set` | `Codec.list(elem).xmap(...)` | ✅ | → `LinkedHashSet` | +| `Map` | `Codec.unboundedMap(K,V)` | ✅ | 自动解析泛型 | +| `ISyncManaged` | 递归序列化 | ✅ | 调用子 holder | +| 自定义类型 | 需注册 `FieldCodecs.register` | — | 调用 `register()` |