基岩版存档数据研究:DB数据_生物实体
泥土NITU
2025年06月28日 17:12
收录于文集
共4篇

上篇我们介绍了如何解析与修改生物群系的数据,今天我们来介绍生物实体,如果文章对你有帮助就请给我点一个赞吧!十分感谢!

文章发布时MC版本是【1.21.92.1】


与生物群系一样,生物实体现在也分为新版与旧版,在版本 1.18.3 以前的是旧版方式,此版本以后的都是新版实体,我们先介绍旧版的读取方式吧!

旧版生物实体

通过之前 定位数据​ 我们了解,旧版的实体数据的键类型是,所以我们就可以用区块坐标+键类型方式来定位生物实体,而里面的数据也是十分简单,就是正常的小端 NBT,如何解析 NBT 可以看我之前的 解析level.dat​,就不再赘述了,而且在一个区块内可能会有多个生物实体,所以可能有多个根标签,如果你读完一个 CompoundTag 发现还没结束,可以继续读取下一个 CompoundTag,直到文件末尾,我们可以看一下 NaroX 的解析效果:

区块坐标(0, -2)内的旧版生物实体NBT

新版生物实体

新版的生物实体使用的是 digp + actorprefix 方式存储的,每个实体的数据会单独存储,不再像原来那样都整合在一个区块内,如果希望解析一个区块内,我们需要先了解如何定位它:

首先我们要读取 digp 实体键文件来获取生物实体的键名称,定位 digp 实体键的方式是:digp(字符串) + 区块X坐标(int) + 区块Z坐标(int) + 维度(int, 主世界不用添加此项, 1 为下界, 2 为末地),我们可以用下面的代码生成对应的 digp 键:

代码块
clike
自动换行
复制代码
/**
 * 获取Digp实体键
 *
 * @param cx        区块X坐标
 * @param cz        区块Z坐标
 * @param dimension 维度, 0为主世界, 1为地狱, 2为末地
 * @return
 * @throws IOException
 */
public static byte[] generateDigpKey(
        int cx, int cz,
        int dimension) throws IOException {
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
         DataOutputStream dos = new DataOutputStream(bos)) {
        dos.write("digp".getBytes());
        dos.writeInt(Integer.reverseBytes(cx));
        dos.writeInt(Integer.reverseBytes(cz));
        if (dimension != 0) {
            //除了主世界, 其他还要写入维度类型
            dos.writeInt(Integer.reverseBytes(dimension));
        }
        return bos.toByteArray();
    }
}
复制成功

现在我们成功获取了当前区块对应的digp实体键文件,看一下这个文件的内容吧!

digp文件内容

文件结构还是比较简单的,在文件内每 8 个字节就是一个生物实体的存储键,每个存储键可以在后面用来定位生物实体,一般来说这个文件大小都能够被 8 整除的,也可能遇到不足8的情况,我不太清楚发生原因,如果遇到这种情况就忽略不足 8 的这部分吧,看一下解析代码:

代码块
clike
自动换行
复制代码
/**
 * 解析digp
 *
 * @param data 要解析的digp生物实体存储键数据
 * @return 此键对应的所有生物实体的存储键
 * @throws IOException
 */
public static byte[][] parseDigp(byte[] data) throws IOException {
    //先计算有多少个生物实体
    int len = data.length / Long.BYTES;
    byte[][] storageKeys = new byte[len][];
    try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
         DataInputStream dis = new DataInputStream(bis)) {
        for (int i = 0; i < len; i++) {
            //8字节
            storageKeys[i] = new byte[Long.BYTES];
            dis.readFully(storageKeys[i]);
        }
    }
    return storageKeys;
}
复制成功

现在我们已经成功获取了一个区块内所有的实体键,接下来就是根据实体键找对应的实体了,我们可以用:actorprefix(字符串) + 上面获取的实体键 来获取其对应的生物实体:

代码块
clike
自动换行
复制代码
/**
 * 生成定位生物实体键
 *
 * @param actorKey 生物实体键
 * @return
 * @throws IOException
 */
public static byte[] generateActorKey(long actorKey) throws IOException {
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
         DataOutputStream dos = new DataOutputStream(bos)) {
        dos.write("actorprefix".getBytes());
        dos.writeLong(actorKey);
        return bos.toByteArray();
    }
}
复制成功

我们可以看一下最终代码:

代码块
clike
自动换行
复制代码
DBFactory factory = new Iq80DBFactory();
//打开DB数据
try (DB db = factory.open(new File("file/db"), Options.newDefaultOptions())) {
    //获取主世界区块坐标(0, -1)的digp数据
    byte[] digpKey = generateDigpKey(0, -1, 0);
    byte[] digpData = db.get(digpKey);
    if (digpData == null) {
        System.out.println("没有找到这个数据");
        return;
    }
  
    //获取区块内的实体存储键
    byte[][] storageKeys = Util.parseDigp(digpData);
    System.out.println("区块内实体个数:" + storageKeys.length);
  
    //每个生物实体, 这里请替换为自己的NBT读取代码
    List<NBT_Tag> tags = new ArrayList<>();
    for (int i = 0; i < storageKeys.length; i++) {
        byte[] storageKey = storageKeys[i];
        //生成actor键
        byte[] entityKey = generateActorKey(storageKey);
        byte[] entityData = db.get(entityKey);
      
        if (entityData == null) {
            System.out.printf("未能找到键为 %s 的生物实体%n", bytes2Hex(storageKey));
            continue;
        }
      
        //解析实体NBT,具体的NBT解析请替换为自己的方法
        try (ByteArrayInputStream bis = new ByteArrayInputStream(entityData);
             DataInputStream dis = new DataInputStream(bis)) {
            NBT_Tag tag = new NBT_Tag();
            tag.read(dis);
            tags.add(i, tag);
            System.out.printf("第 %d 个生物实体NBT信息:%s%n", i, tag);
        }
    }
}
复制成功

至此我们就成功解析此区块内的实体 NBT ,接下来是一些常用或特别的 NBT 介绍:

  • 【ListTag】Armor:这个标签是当前实体的护甲,内部每个 CompoundTag 都是一个物品,从上到下分别是头盔,护甲,护腿和靴子

  • 【ListTag】Attributes:这个标签是实体的一些数值属性,每个 CompoundTag 是一条,内部用【StringTag】Name 做标识

  • 【ListTag】linkTag:骑在当前实体上的实体,可以设置多个,内部的【Long】entityID 就是骑乘的 UniqueID,但是基岩版有大部分实体都不能骑乘

  • 【ListTag】definitions:字符串标签列表,每项都是一个实体组件,+代表启用,-代表不启用,一般至少会有一个minecraft:当前实体命名空间属性

  • 【LongTag】LeasherID:这是拴绳拴住的对象,对应的是实体的 UniqueID

  • 【LongTag】OwnerNew:主人的 UniqueID,设置此项后当前实体不会攻击对应ID的实体

  • 【LongTag】TargetID:当前实体仇恨对象的唯一独特 ID

  • 【LongTag】UniqueID:当前实体的唯一独特 ID

  • 【StringTag】identifier:实体的命名空间,代表这是个什么实体,比如 minecraft:zombie 就是僵尸

  • 【Compound】internalComponent:新版实体的内部组件,目前只用于存储当前的实体存储键,【StringTag】StorageKey内就是上面 actorprefix 内的存储键

如何注入自定义实体

旧版十分简单,只需要修改区块数据 0x32 键的文件即可,对于新版的生物实体,由于存储方式有所不同,所以注入方式会稍微麻烦一点,我们需要模仿游戏创造实体的思路:

我们要先生成实体的 uniqueID 和 storageKey ,而 storageKey 其实就是由 level.dat 中的 worldStartCount 与 uniqueID 处理与组合而成,可以按我下面步骤来一步步注入:

  1. 定位到当前区块对应的digp数据,然后解析它获取当前区块的实体存储键

  2. 解析 level.dat 的世界NBT,并获取世界关闭数 worldStartCount 标签,将值取出(这个标签每次退出游戏时这个数值就会 -1,游戏会根据这个值生成实体唯一ID)

  3. 根据 digp 获取的实体键,使用下面的 makeUniqueId 代码生成新实体的 uniqueID 数值, entityCount 可以随便填,1、2、3等等都没问题,只要不重复即可(如果一次要注入多个实体时需要注意),因为用本次方法生成的实体都属于下次游玩生成的实体,所以不会和旧的实体的 ID 冲突

  4. 用刚才生成的 uniqueID 搭配 worldStartCount 调用 makeStorageKey 来创建 storageKey 

  5. 在 digp 文件最后写入我们新实体的 storageKey

  6. 在你准备好的生物实体中,将【LongTag】UniqueID中填入刚才生成的 uniqueID,将 【CompoundTag】internalComponents -> 【CompoundTag】EntityStorageKeyComponent -> 【StringTag】StorageKey 中填入我们的 storageKey,如果这几个NBT不存在请手动创建注意 StorageKey 标签,因为一般 StorageKey 都是非文本字符,请不要使用字符串方式写入,尽量使用 byte[] 方式,避免因为编码转换导致实体键数据不对应!

  7. 将生物实体NBT数据按 actorprefix + storageKey 键名放入存档数据中即可

  8. 进入游戏后应该就出现了你创建的生物了

代码块
clike
自动换行
复制代码
/**
 * 初始世界起始生成数
 */
public static final long BASE_WORLD_START_COUNT = 0xFFFFFFFEL;

/**
 * 生成UniqueId
 *
 * @param worldStartCount 世界起始数,无符号int
 * @param entityCount     实体键,无符号int
 * @return
 */
public static long makeUniqueId(@Nullable Long worldStartCount, long entityCount) {
    //默认值, 如果你不传入worldStartCount则使用这个值, 建议还是先读取level.dat获取真实数值来创建, 避免实体ID重复
    if (worldStartCount == null) worldStartCount = BASE_WORLD_START_COUNT;
    //本次游戏生成的实体直接组合即可
    return entityCount | (worldStartCount << 32);
}

/**
 * 生成实体存储键
 *
 * @param worldStartCount,无符号int
 * @param uniqueId
 * @return
 */
public static byte[] makeStorageKey(@Nullable Long worldStartCount, long uniqueId) {
    if (worldStartCount == null) worldStartCount = BASE_WORLD_START_COUNT;//默认值
    //每次退出游戏会将worldStartCount-1,这样就能避免出现重复ID的实体,这样创建的实体就属于下次进入游戏创建的了
    worldStartCount--;

    //反转世界起始数后整合即可得到StorageKey
    long keyVal = (-worldStartCount << 32) | (uniqueId & BASE_WORLD_START_COUNT);
    //将long转为byte[]就是我们的StorageKey了
    ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
    buffer.putLong(keyVal);
    return buffer.array();
}

//示例
long worldStartCount = 4294967090L;
//设为当次游戏的第一个实体
long uniqueId = makeUniqueId(worldStartCount, 1);
byte[] storageKey = makeStorageKey(worldStartCount, uniqueId);
System.out.println(bytes2Hex(storageKey));
复制成功


本篇就到这里结束了,基本上新版本与旧版不同的读取都介绍过了,下篇我们再看看方块实体NBT与其他简单的数据吧!