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

ByteNBT objectJulia object producedJulia objects accepted
01ByteUInt8UInt8
02ShortInt16Int16
03IntInt32Int32
04LongInt64Int64
05FloatFloat32Float32
06DoubleFloat64Float64
07Byte ArrayVector{Int8}Vector{Int8}
08StringStringString
09ListVector{T}Vector{T}
0aCompoundLittleDict{String}AbstractDict{String} or Nothing if empty
0bInt ArrayVector{Int32}Vector{Int32}
0cLong ArrayVector{Int64}Vector{Int64}

Edge cases

  • When reading an empty NBT list with element type 0, NBT.jl produces Any[]. Similarly writing Any[] will produce an empty NBT list with element type 0.
  • When reading an NBT list with element type 03 or 04, NBT.jl produces Int32[] and Int64[] respectively. Trying to write this back into an NBT file will produce a tag of type 0b and 0c respectively. Note that this doesn't affect lists of element type 01 since those produce UInt8[], which is distinct from the Int8[] produced by tags of type 07.

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.readFunction
read(filename)

Read an NBT file and return the data as Julia objects.

source
read(io)

Read NBT data from an IO and return the data as Julia objects.

source
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_printFunction
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).

source
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.writeFunction
write(filename, pair::Pair{String, <:AbstractDict{String}})

Write an NBT file and return the number of bytes written.

source
write(io, pair::Pair{String, <:AbstractDict{String}})

Write an NBT file to an IO and return the number of bytes written.

source
NBT.write_uncompressedFunction
write_uncompressed(filename, pair::Pair{String, <:AbstractDict{String}})

Write an NBT file to an uncompressed file and return the number of bytes written.

source
write_uncompressed(io, pair::Pair{String, <:AbstractDict{String}})

Write an NBT file to an uncompressed IO and return the number of bytes written.

source

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:

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
end
NBT.begin_nbt_fileFunction
begin_nbt_file(io)

Begin an NBT file and return a stream and the number of bytes written.

source
NBT.begin_compoundFunction
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.

source
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.

source
NBT.end_compoundFunction
end_compound(io)

End the current NBT Compound tag and return the number of bytes written.

source
NBT.begin_listFunction
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.

source
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.

source
NBT.end_listFunction

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.

source

Index