Skip to content

格式细节与位打包 ​

投影工坊支持四种文件。这页讲它们的内部差异,以及最核心的那件事:BlockStates 怎么解包。

四种格式 ​

扩展名是什么我们支持到什么程度
.litematicLitematica 投影全功能:读、改元数据、转格式、3D 预览
.nbt结构方块结构全功能
.schematicMCEdit 旧格式只读元信息
.schemSponge V2只读信息

.schematic 用的是数字方块 ID,要转成现代格式得有 ID 映射表——那是个无底洞,所以只读不改不转。

.nbt 有歧义:Litematica 的投影改名成 .nbt 也是合法的。判别靠读根标签:有 Regions 就是 litematic,有 size 或 palette 就是结构。

两种位打包 ​

方块状态是"调色板索引"的有序序列。调色板多大决定每个索引占几位(BitsNeeded,最少 2 位)。这个序列压进 long[],怎么压有两种,互不兼容。

1.16(DataVersion ≥ 2529)起是连续打包:条目按 bit 流依次排列,一个条目可以跨两个 long。

csharp
if (dataVersion >= 2529)
{
    for (var i = 0; i < count; i++)
    {
        var bitIndex = (long)i * bits;
        var longIndex = bitIndex >> 6;
        if (longIndex >= data.Length) break;
        var offset = (int)(bitIndex & 63);
        var raw = (ulong)data[longIndex] >> offset;
        if (offset + bits > 64 && longIndex + 1 < data.Length)
            raw |= (ulong)data[longIndex + 1] << (64 - offset);
        result[i] = (int)(raw & mask);
    }
    return result;
}

更早的是跨长分割:每个 long 内部独立打包,一个 long 装 64 / bits 个条目,末尾装不下的位直接丢掉。

csharp
var perLong = 64 / bits;
var longIndex = i / perLong;
var offset = (i % perLong) * bits;
result[i] = (int)((ulong)data[longIndex] >> offset & mask);

两种只在 bits 不整除 64 时才有区别——而 5/6/7 位恰恰是常见情况(调色板 33~64 项是 6 位,65~128 项是 7 位)。搞反了整片方块都会错位,而且是"看着有东西但全是错的"那种错法。

两套都必须实现,按 DataVersion 选;写出统一用连续打包。

litematic ↔ 结构 nbt 为什么可靠 ​

因为两者的调色板都是 blockstate 字符串,坐标也都是整数格。转换就是"解包 → 重排 → 重打包",不需要任何方块 ID 映射。

blockstate 字符串形如 minecraft:oak_log[axis=y]。两边存法不同:litematic / 结构 nbt 的调色板是 Compound 列表 {Name: "minecraft:oak_log", Properties: {axis: "y"}},我们在内部统一成字符串,PaletteNames 和 PaletteTags 互为逆运算。

结构 nbt → litematic 有一个细节:结构的调色板通常不含空气(结构只存非空气方块),但 litematica 要求索引 0 是空气,且所有 state 索引必须和调色板对齐。所以要补一个空气到头部,原索引整体后移:

csharp
var airIndex = palette.FindIndex(p => ProjectionReader.IsAir(p.Name));
var indexShift = 0;
if (airIndex < 0)
{
    palette.Insert(0, new BlockStateTagDef("minecraft:air", []));
    airIndex = 0;
    indexShift = 1;
}

漏了 indexShift 的话,所有方块的外观都会差一位——石头变成草方块那种。

litematic → 结构 nbt 反过来:多区域按包围盒合并,只输出非空气方块(结构方块保存的惯例),坐标转成相对包围盒原点的偏移。

NBT 解析器是自己写的 ​

13 种标签全支持:大端序、字符串是 ushort 长度 + UTF-8、List 记元素类型和个数、投影文件外层统一 gzip。

自己写而不是引第三方库,是因为存档解析(level.dat)那边有一个刻意只读、跳过数组的最小解析器在跑,改它有风险;投影这边需要 LongArray、ByteArray 和写入能力。

gzip 必须整体解压到内存

不能拿 GZipStream 逐值读。NBT 解析是"逐标量读 2/4/8 字节",一个 80 万方块的投影光 BlockStates 就有 65 万个 long——逐值读会变成 65 万次 GZipStream.Read,实测 .NET 8 下 60 秒起步。整体解压后同样的解析只要几十毫秒。同理 ReadIntArray / ReadLongArray 都是整块 ReadBytes 再在内存里切。

方块数不可信 ​

Metadata.TotalBlocks 是生成器写的,跟实际内容对不上——实测 redenmc 生成的两个文件都写着 114514,实际只有几千个方块。

所以统计一律自己数:按调色板展开位数组,跳过空气(空气判定兼容 minecraft:air、air、minecraft:cave_air、minecraft:void_air 四种写法),按出现次数累加。

相关链接:投影工坊

基于 MIT 许可证(有修改)开源