区域文件格式(region-file-format)

基本信息

属性 值
实现类 cubicchunks.regionlib.lib.Region<K extends IKey<K>>
接口 cubicchunks.regionlib.api.region.IRegion<K>
构造方式 Region.<L>builder() → Builder(Region.java:213)
默认扇区大小 512 字节(Builder.sectorSize 字段初始值)
文件路径 directory.resolve(regionKey.getName()),以 FileChannel.open(..., CREATE, READ, WRITE) 打开
线程安全 writeValue / readValue / hasValue 是 synchronized 方法

这是 Minecraft Anvil 区域文件那一套扇区(sector)打包格式的通用实现:一个文件 = 一个头表 + 一堆按扇区对齐的记录。

Builder 参数

方法 默认值 说明
setDirectory(Path) 无 区域文件所在目录
setRegionKey(RegionKey) 无 同时决定文件名(RegionKey.getName())
setKeyProvider(IKeyProvider<K>) 无 决定单区域的 key 数量与 id ↔ key 的换算
setSectorSize(int) 512 扇区字节数
addHeaderEntry(IHeaderDataEntryProvider<?, K>) 空列表 追加一个自定义头表字段
addSpecialSectorMapEntry(marker, value, specialReader, writeConflictHandler) 空列表 注册一个"特殊"头表整数值(见下)

Builder 的 javadoc 明确写着"Using it is very unsafe, there are no safeguards against using it improperly. Should only be used by IRegionProvider implementations."

build() 做了什么

  1. entryMapBytes = Integer.BYTES + Σ 每个 header provider 的 getEntryByteCount()
  2. entryMapSectors = ceilDiv(keyProvider.getKeyCount(regionKey) * entryMapBytes, sectorSize) —— 头表本身占用的前若干扇区会被标记为已用
  3. IntPackedSectorMap.readOrCreate(file, keyCount, specialEntries) —— 文件比头表还短就填零新建
  4. RegionSectorTracker.fromFile(file, sectorMap, entryMapSectors, sectorSize)
  5. headerEntryProviders.add(0, sectorMap.headerEntryProvider()) —— 扇区表自己被插到头表字段列表的第 0 位,所以扇区表永远是文件最前面的一块

头表布局(IntPackedSectorMap)

常量 值 来源
SIZE_BITS 8 IntPackedSectorMap.java:41
OFFSET_BITS Integer.SIZE - SIZE_BITS = 24 IntPackedSectorMap.java:42
SIZE_MASK / MAX_SIZE 255 单条目最大扇区数
OFFSET_MASK / MAX_OFFSET 16777215 单条目最大扇区偏移
每 key 头表字节 Integer.BYTES = 4 Region.Builder.build() 里的 entryMapBytes 初值

打包 / 解包:

packed   = size | (offset << 8)     // 低 8 位存扇区数,高 24 位存起始扇区
unpackOffset(x) = x >>> 8
unpackSize(x)   = x & 0xFF

packed == 0 表示该 id 没有数据(getEntryLocation 直接返回 Optional.empty()),这也是 255 扇区上限之外的另一处隐含限制:size 0 + offset 0 无法被区分。

同一约束在两条路径上抛不同的异常:

路径 异常
setOffsetAndSize(...) UnsupportedDataException("Max supported size 255 but requested N") / ("Max supported offset 16777215 but requested N")
packed(RegionEntryLocation) IllegalArgumentException("Supported entry size range is 0 to 255, but got N") / ("Supported entry offset range is 0 to 16777215, but got N")

记录(payload)布局

每条记录 = 4 字节长度前缀 + 数据:

扇区偏移 S:  int  dataLength = payload.size()      // 不含自身这 4 字节
S+1 字节起:  payload
  • 写入:numSectors = ceilDiv(value.remaining() + Integer.BYTES, sectorSize),先 writeFully(file.position(offset*sectorSize), 4 字节长度),再 writeFully(file, value)
  • 读取:先读 4 字节 dataLength,若 dataLength > sectorCount * sectorSize 抛 CorruptedDataException(字符串为 "Expected data size max" + sectorCount * sectorSize + " but found " + dataLength,原文缺空格),再按 dataLength 分配并读满

单条目字节上限(由 255 扇区推出):

扇区大小 numSectors 上限 255 → payload 上限
512(SaveSection2D / SaveSection3D 默认) 255 × 512 − 4 = 130556 字节
4096(MinecraftSaveSection) 255 × 4096 − 4 = 1044476 字节

超过上限的 key 会被 IntPackedSectorMap 拒绝写,SaveSection 再回退到 .ext 格式。

扇区分配(RegionSectorTracker)

用一个 BitSet usedSectors 记录占用,头表的前 entryMapSectors 个扇区初始化时就标记为已用。分配算法 findSectorFor(oldSector, requestedSize):

  1. newSectorSize <= oldSectorSize → 原地复用旧位置(oldSector.withSize(...)),不触发搬迁
  2. 否则检查从 oldSectorOffset + oldSectorSize 起的连续段是否都空闲 → 能放下就原地扩
  3. 都不行则 findNextFree(requestedSize):用 usedSectors.nextClearBit / nextSetBit 扫描出第一段长度 ≥ 请求值的空闲连续区间

updateUsedSectorsFor 先把旧区间的位清 0,再把新区间的位置 1(重叠也能正确处理)。删除 key 走 removeKey:把头表项写成 (0, 0) 并释放旧扇区。

自定义头表字段

updateHeaders(key) 按 headerEntryProviders 顺序依次追加写入,写第 n 个 provider 时偏移为 n * keyCount + key.getId() * entryByteCount:

Utils.writeFully(file.position(currentHeaderBytes * keyCount + id * prov.getEntryByteCount()), buf);

即字段是列存布局:先写该字段的全部 keyCount 项,再写下一个字段。RegionLib 自带两个 provider:

Provider getEntryByteCount() 内容
EntryLocationHeaderEntryProvider<K> Integer.BYTES 扇区表本身(Region.Builder 自动加在第 0 位);无数据时写 0
TimestampHeaderEntryProvider<L> Integer.BYTES (int) TimeUnit.MILLISECONDS.convert(System.currentTimeMillis(), timeUnit),只有 Minecraft 原版存档区 用到(TimeUnit.SECONDS)

特殊值(special sector map entry)

Region.Builder.addSpecialSectorMapEntry(marker, rawValue, specialReader, writeConflictHandler) 可以把某个特定的头表整数值保留下来当"标记",读取时 IntPackedSectorMap.trySpecialValue(key) 命中就改用自定义的 reader 解析,writeSpecial(key, marker) 写入。RegionLib 自身一处都没用——RegionSectorTracker.fromFile 会跳过 sectorMap.isSpecial(loc) 的条目,冲突时由 writeConflictHandler 处理。这是留给下游 mod(Cubic Chunks 用来标记"这里是 cube 区域")的扩展点。

关闭与落盘

方法 行为
flush() ensureSectorSizeAligned()(文件长度不是扇区整数倍就用 ByteBuffer.allocateDirect 补零)→ file.force(false)
close() flush() 后 file.close()
writeValue(key, null) 删除:regionSectorTracker.removeKey(key) + updateHeaders(key)
forEachKey(cons) 遍历 id 从 0 到 keyCount - 1,只有头表里有位置的 id 才回调

相关条目