上篇我们介绍了如何解析与修改生物群系的数据,今天我们来介绍生物实体,如果文章对你有帮助就请给我点一个赞吧!十分感谢!
文章发布时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 键:
/**
* 获取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 的这部分吧,看一下解析代码:
/**
* 解析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(字符串) + 上面获取的实体键 来获取其对应的生物实体:
/**
* 生成定位生物实体键
*
* @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();
}
} 我们可以看一下最终代码:
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 处理与组合而成,可以按我下面步骤来一步步注入:
定位到当前区块对应的digp数据,然后解析它获取当前区块的实体存储键
解析 level.dat 的世界NBT,并获取世界关闭数 worldStartCount 标签,将值取出(这个标签每次退出游戏时这个数值就会 -1,游戏会根据这个值生成实体唯一ID)
根据 digp 获取的实体键,使用下面的 makeUniqueId 代码生成新实体的 uniqueID 数值, entityCount 可以随便填,1、2、3等等都没问题,只要不重复即可(如果一次要注入多个实体时需要注意),因为用本次方法生成的实体都属于下次游玩生成的实体,所以不会和旧的实体的 ID 冲突
用刚才生成的 uniqueID 搭配 worldStartCount 调用 makeStorageKey 来创建 storageKey
在 digp 文件最后写入我们新实体的 storageKey
在你准备好的生物实体中,将【LongTag】UniqueID中填入刚才生成的 uniqueID,将 【CompoundTag】internalComponents -> 【CompoundTag】EntityStorageKeyComponent -> 【StringTag】StorageKey 中填入我们的 storageKey,如果这几个NBT不存在请手动创建,注意 StorageKey 标签,因为一般 StorageKey 都是非文本字符,请不要使用字符串方式写入,尽量使用 byte[] 方式,避免因为编码转换导致实体键数据不对应!
将生物实体NBT数据按 actorprefix + storageKey 键名放入存档数据中即可
进入游戏后应该就出现了你创建的生物了
/**
* 初始世界起始生成数
*/
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与其他简单的数据吧!