Doc ID: SIRC-080

Item-NBT-API 完整知识文档

Item-NBT-API 是一个用于在 Minecraft 服务器插件中操作 NBT 数据的库,支持 Spigot 和 Paper 服务器,从 1.8 版本开始支持,1.7.10 也可运行但部分功能可

Overview

Item-NBT-API 完整文档

概述

Item-NBT-API 是一个用于在 Minecraft 服务器插件中操作 NBT 数据的库,支持 Spigot 和 Paper 服务器,从 1.8 版本开始支持,1.7.10 也可运行但部分功能可能不可用。

核心类

主要类

  • NBT: 主要的工具类,包含所有核心功能
  • ReadableNBT: 只读 NBT 接口
  • ReadWriteNBT: 可读写 NBT 接口
  • NBTFileHandle: 用于操作 NBT 文件
  • NBTChunk: 用于操作区块数据
  • NBTBlock: 用于操作方块数据
  • DataFixerUtil: 数据版本修复工具

基本用法

创建和操作 NBT 数据

// 创建新的空 NBT 标签
ReadWriteNBT nbt = NBT.createNBTObject();

// 设置 NBT 数据
nbt.setString("Stringtest", "Teststring");
nbt.setInteger("Inttest", 42);
nbt.setDouble("Doubletest", 1.5);
nbt.setBoolean("Booleantest", true);

// 获取 NBT 数据
String s1 = nbt.getString("Stringtest");
int i1 = nbt.getInteger("Inttest");
double d = nbt.getDouble("Doubletest");
boolean b = nbt.getBoolean("Booleantest");

// 使用默认值获取
String s2 = nbt.getOrDefault("Stringtest", "fallback_value");
Integer i2 = nbt.getOrNull("Inttest", Integer.class);

键操作

nbt.getKeys(); // 获取所有键
nbt.hasTag("key"); // 检查标签是否存在
nbt.hasTag("key", NbtType.NBTTagString); // 检查标签是否存在且类型匹配
nbt.removeKey("key"); // 移除标签
NBTType nbtType = nbt.getType("key"); // 获取标签类型

子标签复合体

nbt.getCompound("subtag"); // 获取子标签,或返回 null
nbt.getOrCreateCompound("subtag"); // 获取或创建子标签

NBT 与字符串转换

// 解析 SNBT 为 NBT
ReadWriteNBT nbt = NBT.parseNBT("{Health:20.0f,Motion:[0.0d,10.0d,0.0d],Silent:1b}");

// 将 NBT 转换为 SNBT
String snbt = nbt.toString();

// 重新转换为 NBT
ReadWriteNBT nbt2 = NBT.parseNBT(snbt);

数据解析

解析嵌套数据

// 使用点号分隔复合体
nbt.resolveOrCreateCompound("foo.bar.baz");

// 获取复合体(如果存在)
nbt.resolveCompound("foo.some.key.baz");

// 设置嵌套值
nbt.resolveOrCreateCompound("foo.bar.baz").setInteger("test", 42);

// 获取嵌套值
String s = nbt.resolveOrDefault("foo.bar.Stringtest", "fallback_value");
Integer i = nbt.resolveOrNull("foo\.bar.baz.Inttest", Integer.class);

列表操作

字符串列表

// 获取或创建字符串列表
ReadWriteNBTList<String> stringList = nbt.getStringList("list_key");
stringList.add("value");

// 获取列表类型
NBTType type = nbt.getListType("list_key");

// 获取列表值
nbt.getStringList("list_key").get(0);
nbt.resolveOrNull("list_key[0]", String.class); // 获取第一个值
nbt.resolveOrDefault("list_key[-1]", "fallback_value"); // 获取最后一个值

复合体列表

// 获取列表(如果不存在则创建)
ReadWriteNBTCompoundList nbtList = nbt.getOrCreateCompound("foo").getCompoundList("other_key");

// 添加新复合体到列表
ReadWriteNBT nbtListEntry = nbtList.addCompound();
nbtListEntry.setBoolean("bar", true);

// 添加现有 NBT 复合体到列表
ReadWriteNBT otherNbtListEntry = nbtList.addCompound(NBT.parseNBT("{foo_bar:1b}"));

// 获取列表中的复合体
nbt.resolveCompound("foo.other_key[0]"); // 获取第一个复合体
nbt.resolveOrCreateCompound("foo.other_key[1]"); // 获取第二个复合体或创建

// 从列表复合体中获取数据
boolean bar = nbt.resolveOrDefault("foo.other_key[0].bar", false);

物品操作

设置物品数据

NBT.modify(itemStack, nbt -> {
    nbt.setString("Stringtest", "Teststring");
    nbt.setInteger("Inttest", 42);
    nbt.setDouble("Doubletest", 1.5d);
    nbt.setBoolean("Booleantest", true);
});

读取物品数据

NBT.get(itemStack, nbt -> {
    String stringTest = nbt.getString("Stringtest");
    int intTest = nbt.getOrDefault("Inttest", 0);
    // 更多操作
});

获取物品数据

String string = NBT.get(itemStack, nbt -> (String) nbt.getString("Stringtest"));

修改并获取数据

int someValue = NBT.modify(itemStack, nbt -> {
    int i = nbt.getOrDefault("key", 0) + 1;
    nbt.setInteger(i);
    return i;
});

ItemMeta 使用注意事项

警告: 不要混合使用 ItemMeta 和 NBT。

// 错误示例 - 不要这样做!
ItemMeta meta = itemStack.getItemMeta();
meta.setDisplayName("Modified!");
NBT.modify(itemStack, nbt -> nbt.setBoolean("modified", true));
itemStack.setItemMeta(meta); // 将撤销所有 NBT 更改!

// 正确示例 - 在 NBT 范围内安全修改 ItemMeta
NBT.modify(itemStack, nbt -> {
    nbt.setInteger("kills", nbt.getOrDefault("kills", 0) + 1);
    nbt.modifyMeta((readOnlyNbt, meta) -> {
        // 修改 meta 时不要修改 nbt!
        meta.setDisplayName("Kills: " + readOnlyNbt.getOrDefault("kills", 0));
    });
});

1.20.5+ 版本的特殊处理

从 Minecraft 1.20.5 开始,ItemStacks 在运行时不再有原版 NBT。

// 仅读取原版数据(1.20.5+)
NBT.getComponents(item, nbt -> {
    if (nbt.hasTag("minecraft:custom_name")) {
        String customName = nbt.getString("minecraft:custom_name");
    }
});

// 修改原版数据(1.20.5+)
NBT.modifyComponents(item, nbt -> {
    nbt.setString("minecraft:custom_name", "{"extra":["foobar"],"text":""}");
});

实体和方块实体操作

访问原版 NBT

// 获取数据
boolean silent = NBT.get(entity, nbt -> (boolean) nbt.getBoolean("Silent"));

// 修改数据
NBT.modify(entity, nbt -> {
    nbt.setBoolean("Silent", true);
    nbt.setByte("CanPickUpLoot", (byte) 1);
});

访问自定义数据

警告: 自定义(方块)实体 NBT 的持久数据存储仅在 1.14+ 版本可用。

// 获取数据
boolean test = NBT.getPersistentData(entity, nbt -> (boolean) nbt.getBoolean("custom_key"));

// 修改数据
NBT.modifyPersistentData(entity, nbt -> {
    nbt.setBoolean("custom_key", true);
    nbt.setByte("custom_byte", (byte) 1);
});

模拟 "/data merge" 命令

NBT.modify(zombie, nbt -> {
    nbt.mergeCompound(NBT.parseNBT("{Silent:1b,Invulnerable:1b,Glowing:1b,IsBaby:1b}"));
});

方块操作

区块数据存储(1.16.4+)

ReadWriteNBT nbt = new NBTChunk(chunk).getPersistentDataContainer();

方块数据存储

// 方块数据将存储在区块数据的 "blocks.x_y_z" 子标签中
ReadWriteNBT nbt = new NBTBlock(block).getData();

注意: 此数据仅与位置关联,如果方块被破坏/更改/爆炸/移动等,数据仍将保留在该位置,除非手动清除/移动!

NBT 文件操作

NBTFileHandle

// 如果文件不存在将自动创建
NBTFileHandle nbtFile = NBT.getFileHandle(new File("directory", "test.nbt"));

// 设置数据
nbtFile.setString("foo", "bar");

// 保存文件
nbtFile.save();

读取文件(不维护链接)

File file = new File("directory", "test.nbt");

// 从文件读取数据(如果文件不存在则返回空复合体)
ReadWriteNBT nbt = NBT.readFile(file);

// 保存 NBT 到文件
NBT.writeFile(file, nbt);

Minecraft 对象与 NBT/字符串转换

物品序列化/反序列化

// 保存
ReadWriteNBT nbt = NBT.itemStackToNBT(itemStack);
ReadWriteNBT nbt = NBT.itemStackArrayToNBT(itemStacks);

// 恢复
ItemStack itemStack = NBT.itemStackFromNBT(nbt);
ItemStack[] itemStacks = NBT.itemStackArrayFromNBT(nbt);

// NBT <-> 字符串转换
String snbt = nbt.toString();
ReadWriteNBT nbt = NBT.parseNBT(snbt);

实体序列化

// 保存
ReadWriteNBT entityNbt = NBT.createNBTObject();
NBT.get(entity, entityNbt::mergeCompound);

// 恢复
NBT.modify(entity, nbt -> {
    // 可能需要先过滤 entityNbt,例如移除位置、uuid、entityId 等数据
    nbt.mergeCompound(entityNbt);
});

游戏配置文件

// 保存
ReadWriteNBT nbt = NBT.gameProfileToNBT(profile);

// 恢复
GameProfile profile = NBT.gameProfileFromNBT(nbt);

接口代理

可以定义扩展 NBTProxy 的接口来创建 NBT 包装器。

interface TestInterface extends NBTProxy {
    // 执行: return nbt.hasTag("kills");
    boolean hasKills();
    
    // 执行: nbt.setInteger("kills", amount);
    void setKills(int amount);
    
    // 执行: return nbt.getInteger("kills");
    int getKills();
    
    // 也支持默认方法
    default void addKill() {
        setKills(getKills() + 1);
    }
}

使用 @NBTTarget 注解

interface TestInterface extends NBTProxy {
    // 将从 "other" 键的数据中获取 PointsInterface
    @NBTTarget(type = Type.GET, value = "other")
    PointsInterface getOtherInterface();
}

interface PointsInterface extends NBTProxy {
    int getPoints();
    void setPoints(int points);
}

支持其他数据类型

interface TestInterface extends NBTProxy {
    @Override
    default void init() {
        registerHandler(ItemStack.class, NBTHandlers.ITEM_STACK);
        registerHandler(ReadableNBT.class, NBTHandlers.STORE_READABLE_TAG);
        registerHandler(ReadWriteNBT.class, NBTHandlers.STORE_READWRITE_TAG);
    }
    
    ItemStack getItem();
    void setItem(ItemStack item);
    
    ReadWriteNBT getBlockStateTag();
    void setBlockStateTag(ReadableNBT blockState);
}

数据修复工具

DataFixerUtil 允许将 NBT 从旧版本更新到新版本。

// 例如,将 1.12.2 的数据更新到 1.20.6
DataFixerUtil.fixUpItemData(nbt, DataFixerUtil.VERSION1_12_2, DataFixerUtil.VERSION1_20_6);

// 也可以使用当前版本
DataFixerUtil.fixUpItemData(nbt, DataFixerUtil.VERSION1_12_2, DataFixerUtil.getCurrentVersion());

示例用法

设置头颅皮肤

final String textureValue = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTQyY2M5MjAzYzkwYjg5YmRhYzFkZjI4NDE2NzI2NmI5NTNkZmViZjNjNDY5MGE3Y2QwYjE1NzkxYTYyZTU4MiJ9fX0=";

// 创建 ItemStack
final ItemStack item = new ItemStack(Material.PLAYER_HEAD);

// 应用 NBT
NBT.modify(item, nbt -> {
    ReadWriteNBT skullOwnerCompound = nbt.getOrCreateCompound("SkullOwner");
    skullOwnerCompound.setUUID("Id", UUID.randomUUID());
    skullOwnerCompound.getOrCreateCompound("Properties")
        .getCompoundList("textures")
        .addCompound()
        .setString("Value", textureValue);
});

创建自定义僵尸

// 创建僵尸
Zombie zombie = (Zombie) world.spawnEntity(location, EntityType.ZOMBIE);

// 修改原版 NBT
NBT.modify(zombie, nbt -> {
    nbt.setBoolean("Silent", true);
    nbt.setByte("CanPickUpLoot", (byte) 1);
    
    // 添加属性修饰符
    ReadWriteNBTList<ReadWriteNBT> list = nbt.getOrCreateCompound("Attributes")
        .getCompoundList("generic.attack_damage");
    
    ReadWriteNBT listEntryNbt = list.addCompound();
    listEntryNbt.setString("Name", "generic.attack_damage");
    listEntryNbt.setDouble("Base", damageValue);
});

// 修改自定义数据
NBT.modifyPersistentData(zombie, nbt -> {
    nbt.setBoolean("custom_zombie", true);
});

读取世界数据

// 获取主世界文件夹
File worldDataFolder = Bukkit.getWorlds().getFirst().getWorldFolder();

// 读取等级数据
NBTFileHandle levelNbtFile = NBT.getFileHandle(new File(worldDataFolder, "level.dat"));

// 获取世界名称
String worldName = levelNbtFile.resolveOrNull("Data.LevelName", String.class);

// 读取玩家数据
UUID playerUuid;
File playerFile = new File(worldDataFolder, "playerdata/" + playerUuid + ".dat");
if (!playerFile.exists()) {
    return;
}

NBTFileHandle playerNbtFile = NBT.getFileHandle(playerFile);

// 更改玩家生命值
float health = playerNbtFile.getFloat("Health");
playerNbtFile.setFloat("Health", health + 5);

// 保存文件
playerNbtFile.save();

包结构

主要包名:de.tr7zw.changeme.nbtapi

核心类:

  • de.tr7zw.changeme.nbtapi.NBT
  • de.tr7zw.changeme.nbtapi.ReadableNBT
  • de.tr7zw.changeme.nbtapi.ReadWriteNBT
  • de.tr7zw.changeme.nbtapi.NBTFileHandle
  • de.tr7zw.changeme.nbtapi.NBTChunk
  • de.tr7zw.changeme.nbtapi.NBTBlock
  • de.tr7zw.changeme.nbtapi.DataFixerUtil
  • de.tr7zw.changeme.nbtapi.NBTHandlers
  • de.tr7zw.changeme.nbtapi.NBTProxy

注意事项

  1. 版本兼容性: 支持 Spigot/Paper 1.8+,1.7.10 部分功能可能不可用
  2. ItemMeta 冲突: 不要混合使用 ItemMeta 和 NBT 操作
  3. 1.20.5+ 变化: 从 1.20.5 开始,物品的原版 NBT 处理方式发生变化
  4. 持久数据存储: 自定义实体 NBT 持久存储仅在 1.14+ 可用
  5. 方块实体存在性: 操作方块实体时确保方块实体已在世界中存在
  6. 数据清理: 自定义数据会永久存储,插件移除时可能留下冗余数据

最佳实践

  1. 使用 resolve 方法处理嵌套数据
  2. 对于自定义实体数据,使用 PersistentData 方法
  3. 考虑使用外部存储而不是玩家世界文件存储玩家数据
  4. 使用 DataFixerUtil 处理版本间数据迁移
  5. 对于复杂数据结构,考虑使用接口代理模式