Skip to content

现场运行态实现

现场运行态的实现关键不是“有没有 tick”,而是“什么数据放哪里”。我们采用三层持久化和一层活状态。

已验证的持久化与生命周期 API

主题 已验证的 API 或事件 实现结论
世界账本入口 ServerLevel.getDataStorage() 账本挂在 ServerLevel
账本创建或读取 DimensionDataStorage.computeIfAbsent(Function<CompoundTag, T>, Supplier<T>, String) 适合加载 SiteLedgerSavedData
标脏 SavedData.setDirty() 账本改动后必须调用
保存检查点 LevelEvent.Save 服务端保存时可做账本一致性检查
区块 NBT ChunkDataEvent.Load / Save 只放区块辅助数据
区块 load ChunkEvent.Load 不做重世界交互
区块 unload ChunkEvent.Unload 释放局部缓存
区块 capability AttachCapabilitiesEvent<T>IForgeLevelChunk extends ICapabilityProvider 可作为区块局部状态的另一条实现路线
玩家 watching ChunkWatchEvent.Watch / UnWatch 按玩家发送附加区块数据

四层实现分工

推荐实现 保存什么
世界真相 SiteLedgerSavedData 遗址实例、锚点、生命周期、覆盖区块
活状态 SiteRuntimeRegistry + ActiveSiteRuntime 当前压力、阶段、临时事件、owner
区块辅助层 ChunkDataEvent 或 chunk capability 局部表现、缓存、轻量索引
同步层 ChunkWatchEvent.Watch / UnWatch 发给客户端的区块附加载荷

SiteLedgerSavedData 建议骨架

java
public final class SiteLedgerSavedData extends SavedData {
    private final Map<String, DiscoveredSiteRecord> records = new HashMap<>();

    public static SiteLedgerSavedData load(CompoundTag tag) {
        SiteLedgerSavedData data = new SiteLedgerSavedData();
        // 解析 records
        return data;
    }

    @Override
    public CompoundTag save(CompoundTag tag) {
        // 写回 records
        return tag;
    }

    public DiscoveredSiteRecord put(DiscoveredSiteRecord record) {
        records.put(record.ref().toString(), record);
        setDirty();
        return record;
    }
}

这里的关键点不是字段细节,而是:

  • 世界账本用 SavedData
  • 修改后立即 setDirty()
  • 真正写盘由世界保存流程触发。

LevelEvent.Save 的职责

LevelEvent.Save 只在服务端触发。它适合做:

  • 账本一致性断言;
  • 运行态到账本的收束检查;
  • 需要在保存边界前刷新的统计值。

它不适合承担逐 tick 主逻辑。

区块辅助数据的两条路线

路线 A:ChunkDataEvent.Load / ChunkDataEvent.Save

适合保存:

  • 某个 chunk 当前暴露出的遗址局部标记;
  • 已计算好的可见性或表现缓存;
  • coveredChunkKeys 对应的轻量派生值。

注意事项:

  1. ChunkDataEvent.LoadChunkSerializer.read(...) 期间触发,已验证为异步。
  2. 因此这里不要访问重世界逻辑,不要启动运行态,不要跨 chunk 查询。

路线 B:AttachCapabilitiesEvent<LevelChunk>

适合保存:

  • 生命周期跟随 LevelChunk 的局部状态对象;
  • 需要在 chunk 无效化时统一释放的缓存。

这条路线更适合复杂局部状态,但它仍然不是世界账本。

ChunkEvent.LoadChunkEvent.Unload

事件 合理用途 不该做什么
ChunkEvent.Load 回填轻量缓存、准备局部同步 创建 runtime、做重交互、跨区块查询
ChunkEvent.Unload 释放局部缓存、断开弱引用 删除世界账本记录

ChunkEvent.Load 已验证可能早于 LevelChunk 达到 ChunkStatus.FULL。因此它不是现场逻辑入口。

覆盖区块算法建议

对于每条 DiscoveredSiteRecord,我们在账本中保存 coveredChunkKeys。初始化方式建议固定:

  1. 以遗址锚点为中心。
  2. 用事件半径换算出 chunk 半径。
  3. 预计算覆盖区块集合。
  4. chunk 事件和 watch 事件都只围绕这组键工作。

这样可以稳定处理:

  • 进入可见范围时的同步;
  • 区块卸载时的局部缓存释放;
  • 一座大遗址跨多个 chunk 的局部状态追踪。

ChunkWatchEvent.Watch / UnWatch

这组事件最适合做“玩家开始看见这个 chunk 时,再补发附加遗址数据”。

事件 推荐用途
ChunkWatchEvent.Watch 向该玩家同步 chunk 内的遗址局部状态或覆盖标记
ChunkWatchEvent.UnWatch 回收该玩家的相关客户端缓存或订阅状态

它解决的是同步,不是持久化。

对象 职责
SiteLedgerSavedData 世界真相
SiteRuntimeRegistry 当前运行中的现场
ActiveSiteRuntime 单座遗址的动态状态
ChunkSiteAuxData 某个 chunk 的辅助缓存
SiteSyncPayload 发给客户端的最小附加数据

必须避免的实现错误

  1. ChunkDataEvent.Load 当成安全的主线程入口。
  2. ChunkEvent.Unload 当成删除遗址实例的依据。
  3. 改了 SavedData 却不调用 setDirty()