NBT.jl
NBT.jl is a Julia package for reading and writing Minecraft NBT files. The main reading and writing methods convert the NBT file into corresponding Julia types and back.
NBT object - Julia object equivalence
| Byte | NBT object | Julia object produced | Julia objects accepted |
|---|---|---|---|
01 | Byte | UInt8 | UInt8 |
02 | Short | Int16 | Int16 |
03 | Int | Int32 | Int32 |
04 | Long | Int64 | Int64 |
05 | Float | Float32 | Float32 |
06 | Double | Float64 | Float64 |
07 | Byte Array | Vector{Int8} | Vector{Int8} |
08 | String | String | String |
09 | List | Vector{T} | Vector{T} |
0a | Compound | LittleDict{String} | AbstractDict{String} or Nothing if empty |
0b | Int Array | Vector{Int32} | Vector{Int32} |
0c | Long Array | Vector{Int64} | Vector{Int64} |
Edge cases
- When reading an empty NBT list with element type
0, NBT.jl producesAny[]. Similarly writingAny[]will produce an empty NBT list with element type0. - When reading an NBT list with element type
03or04, NBT.jl producesInt32[]andInt64[]respectively. Trying to write this back into an NBT file will produce a tag of type0band0crespectively. Note that this doesn't affect lists of element type01since those produceUInt8[], which is distinct from theInt8[]produced by tags of type07.
Reading
The main way to read an NBT file is to use NBT.read. The main method takes a filename and returns a pair name => data. NBT files always consist of a single Compound tag filled with other tags, but the main outer tag is almost always unnamed, so typically NBT.read will output "" => LittleDict(...) and it's necessary to access the right element to get the data.
NBT files are almost always compressed with GZip, but rarely aren't. To read NBT data from an uncompressed file, there is NBT.read_uncompressed which works in the same way as NBT.read. Both functions can also take an IO instead of a filename.
NBT.read_uncompressed — Function
read_uncompressed(io, ::Type{Tag})Read an nbt tag from an uncompressed IO.
julia> T = NBT.read("/home/intricate/mc/instances/1.17.1/minecraft/saves/New World/level.dat")
"" => OrderedCollections.LittleDict{String, Any, Vector{String}, Vector{Any}}("Data" => OrderedCollections.LittleDict{String, Any, Vector{String}, Vector{Any}}("WanderingTraderSpawnChance" => 25, "BorderCenterZ" => 0.0, "Difficulty" => 0x00, "BorderSizeLerpTime" => 0, "raining" => 0x00, "Time" => 265, "GameType" => 1, "ServerBrands" => ["fabric"], "BorderCenterX" => 0.0, "BorderDamagePerBlock" => 0.2…))
julia> T[2]
OrderedCollections.LittleDict{String, Any, Vector{String}, Vector{Any}} with 1 entry:
"Data" => LittleDict{String, Any, Vector{String}, Vector{Any}}("WanderingTraderSpawnChance"=…
julia> T[2]["Data"]
OrderedCollections.LittleDict{String, Any, Vector{String}, Vector{Any}} with 42 entries:
"WanderingTraderSpawnChance" => 25
"BorderCenterZ" => 0.0
"Difficulty" => 0x00
"BorderSizeLerpTime" => 0
"raining" => 0x00
"Time" => 265
"GameType" => 1
"ServerBrands" => ["fabric"]
"BorderCenterX" => 0.0
"BorderDamagePerBlock" => 0.2
"BorderWarningBlocks" => 5.0
"WorldGenSettings" => LittleDict{String, Any, Vector{String}, Vector{Any}}("bonus_…
"DragonFight" => LittleDict{String, Any, Vector{String}, Vector{Any}}("NeedsS…
"BorderSizeLerpTarget" => 6.0e7
"Version" => LittleDict{String, Any, Vector{String}, Vector{Any}}("Snapsh…
"DayTime" => 265
"initialized" => 0x01
"WasModded" => 0x01
"allowCommands" => 0x01
"WanderingTraderSpawnDelay" => 24000
"CustomBossEvents" => LittleDict{String, Any, Vector{String}, Vector{Any}}()
"GameRules" => LittleDict{String, Any, Vector{String}, Vector{Any}}("doFire…
"Player" => LittleDict{String, Any, Vector{String}, Vector{Any}}("Brain"…
"SpawnY" => 1
"rainTime" => 169707
"thunderTime" => 123751
"SpawnZ" => 8
"hardcore" => 0x00
"DifficultyLocked" => 0x00
"SpawnX" => 8
"clearWeatherTime" => 0
"thundering" => 0x00
"SpawnAngle" => 0.0
"version" => 19133
"BorderSafeZone" => 5.0
"LastPlayed" => 1766296821236
"BorderWarningTime" => 15.0
"ScheduledEvents" => Any[]
"LevelName" => "New World2"
"BorderSize" => 6.0e7
"DataVersion" => 2730
"DataPacks" => LittleDict{String, Any, Vector{String}, Vector{Any}}("Enable…For convenience, NBT.jl provides the NBT.pretty_print function for printing the read data into the REPL. However, note that this will dump the entire file which may be very large.
NBT.pretty_print — Function
pretty_print(data, tab=" ")Print the NBT data in data, using tab as the tab character. data can be an AbstractDict, a Vector, or a Pair{String, <:AbstractDict} (as output by read).
julia> NBT.pretty_print(T)
Unnamed NBT file with 1 entries:
Data: {
WanderingTraderSpawnChance (Int32): 25
BorderCenterZ (Float64): 0.0
Difficulty: 0x00
BorderSizeLerpTime (Int64): 0
raining: 0x00
Time (Int64): 265
GameType (Int32): 1
ServerBrands: ["fabric"]
BorderCenterX (Float64): 0.0
BorderDamagePerBlock (Float64): 0.2
BorderWarningBlocks (Float64): 5.0
WorldGenSettings: {
bonus_chest: 0x00
seed (Int64): -7293489413437051478
generate_features: 0x01
dimensions: {
minecraft:overworld: {
generator: {
settings: "minecraft:overworld"
seed (Int64): -7293489413437051478
biome_source: {
seed (Int64): -7293489413437051478
large_biomes: 0x00
type: "minecraft:vanilla_layered"
}
type: "minecraft:noise"
}
type: "minecraft:overworld"
}
# [I'm cutting it off here to save space]Writing
Writing works basically the same as reading. There is NBT.write which takes a filename and a name => data pair, and writes the data to the file. Similarly there is NBT.write_uncompressed for skipping compression.
NBT.write_uncompressed — Function
write_uncompressed(filename, pair::Pair{String, <:AbstractDict{String}})Write an NBT file to an uncompressed file and return the number of bytes written.
write_uncompressed(io, pair::Pair{String, <:AbstractDict{String}})Write an NBT file to an uncompressed IO and return the number of bytes written.
Advanced writing
Sometimes you have existing data you want written to an NBT file, but it is not already in a nested Dict format, and converting would be an unnecessary intermediate step. To avoid having to convert the data to a specific format before writing, NBT.jl provides some lower-level writing functions. The main method is NBT.write_tag:
NBT.write_tag — Function
write_tag(io, data)Write the data tag and return the number of bytes written. Use only after begin_list.
write_tag(io, name => data)Write the name => data pair and return the number of bytes written. Use only between begin_compound and end_compound.
This function can take a pair name => data, or just data. The first method is intended for writing entries in a Compound tag, and the second for writing entries in a List tag. Combining NBT.write_tag with NBT.begin_nbt_file, NBT.end_nbt_file, NBT.begin_compound, NBT.end_compound, NBT.begin_list, and NBT.end_list, you can build an NBT file manually. Here is an example of how this can be used, taken from Litematica.jl:
function Base.write(io::IO, litematic::Litematic)
s, bytes = begin_nbt_file(io)
bytes += write_tag(s, "MinecraftDataVersion" => litematic.data_version)
bytes += write_tag(s, "Version" => Int32(5))
bytes += write_tag(s, "Metadata" => litematic.metadata)
bytes += begin_compound(s, "Regions")
for region in litematic.regions
bytes += begin_compound(s, region.name)
bytes += write_tag(s, "BlockStates" => CompressedPalettedContainer(_permutedims(region.blocks, (1, 3, 2)), 2).data)
bytes += write_tag(s, "PendingBlockTicks" => TagList())
bytes += write_tag(s, "Position" => _writetriple(region.pos))
bytes += write_tag(s, "BlockStatePalette" => TagList(_Tag.(region.blocks.pool)))
bytes += write_tag(s, "Size" => _writetriple(Int32.(size(region.blocks))))
bytes += write_tag(s, "PendingFluidTicks" => TagList())
bytes += write_tag(s, "TileEntities" => TagList(TagCompound{Any}[t for t in region.tile_entities if t !== nothing]))
bytes += write_tag(s, "Entities" => TagList())
bytes += end_compound(s)
end
bytes += end_compound(s)
bytes += end_nbt_file(s)
return bytes
endNBT.begin_nbt_file — Function
begin_nbt_file(io)Begin an NBT file and return a stream and the number of bytes written.
NBT.end_nbt_file — Function
end_nbt_file(io)End an NBT file and return the number of bytes written.
NBT.begin_compound — Function
begin_compound(io, name)Begin an NBT Compound tag and return the number of bytes written. Use only for the root tag (with an empty name) or between begin_compound and end_compound.
begin_compound(io)Begin an NBT Compound tag and return the number of bytes written. Use only after begin_list.
Note: This method just returns 0, but is included for completeness and to allow for more readable code.
NBT.end_compound — Function
end_compound(io)End the current NBT Compound tag and return the number of bytes written.
NBT.begin_list — Function
begin_list(io, name, length, type)Begin an NBT List tag with the specified length and element type and return the number of bytes written. Use only between begin_compound and end_compound.
begin_list(io, length, type)Begin an NBT List tag with the specified length and element type and return the number of bytes written. Use only after begin_list.
NBT.end_list — Function
end_list(io)
End an NBT List tag and return the number of bytes written.
Note: This method just returns 0, but is included for completeness and to allow for more readable code.