Yet another library to manipulate Minecraft data, inspired by the now-defunct pymclevel, building on top of the amazing nbtlib.
Focused on making the bridge between the on-disk save files and directory structure and their NBT content, much like NBTExplorer, presenting all World data in a structured, convenient way so other tools can build on top of it and add more semantics to that data.
.dat
NBT
files, both uncompressed and gzip-compressed..mca
/.mcr
Anvil
region files, lazily loading their contents only when the data is actually
requested, also monitoring content changes to efficiently save back to disk
only the needed files..mcc
external
chunk files, loading from there when indicated by the chunk header in the .mca
region file, and automatically selecting the appropriate format on save:
external mcc
if the chunk data outgrows its previous maximum size (~1 MB),
and back to the mca
if it shrinks enough to fit there again.You can open a Minecraft World by several ways:
level.dat
file, or its open file-like stream object;level.dat
file at its root,
as in the example below;saves/
path. By default, it is the in-game world name.>>> import mcworldlib as mc
>>> world = mc.load('data/New World')
>>> # Most classes have a pretty print. In many cases, their NBT data.
>>> mc.pretty(world.level)
{
Data: {
WanderingTraderSpawnChance: 25,
BorderCenterZ: 0.0d,
Difficulty: 2b,
...
SpawnAngle: 0.0f,
version: 19133,
BorderSafeZone: 5.0d,
LastPlayed: 1633981265600L,
BorderWarningTime: 15.0d,
ScheduledEvents: [],
LevelName: "New World",
BorderSize: 59999968.0d,
DataVersion: 2730,
DataPacks: {
Enabled: ["vanilla"],
Disabled: ["Fabric Mods"]
}
}
}
World.dimensions
is a dictionary mapping each dimension to categorized Region files:
>>> mc.pretty(world.dimensions)
{ <Dimension.OVERWORLD: 0>: { 'entities': <Regions(6 regions)>,
'poi': <Regions(0 regions)>,
'region': <Regions(6 regions)>},
<Dimension.THE_NETHER: -1>: { 'entities': <Regions(0 regions)>,
'poi': <Regions(0 regions)>,
'region': <Regions(0 regions)>},
<Dimension.THE_END: 1>: { 'entities': <Regions(0 regions)>,
'poi': <Regions(0 regions)>,
'region': <Regions(0 regions)>}}
And World.regions
is handy view of that dictionary containing only the 'region'
category, similarly with World.entities
and World.poi
:
>>> mc.pretty(world.regions)
{ <Dimension.OVERWORLD: 0>: <Regions(6 regions)>,
<Dimension.THE_NETHER: -1>: <Regions(0 regions)>,
<Dimension.THE_END: 1>: <Regions(0 regions)>}
>>> regions = world.regions[mc.OVERWORLD]
>>> regions is world.dimensions[mc.OVERWORLD]['region']
True
Regions
is a dict-like collection of .mca
Anvil region files, grouped in
"categories" that match their sub-folder in a given the dimension, such as
/entities
, /poi
, and of course /region
.
The dictionary keys are region coordinate tuples, and the values represent Region files. Files are lazily loaded, so initially the values contain only their path:
>>> mc.pretty(regions)
{ ( -2, -1): PosixPath('data/New World/region/r.-2.-1.mca'),
( -2, 0): PosixPath('data/New World/region/r.-2.0.mca'),
( -1, -1): PosixPath('data/New World/region/r.-1.-1.mca'),
( -1, 0): PosixPath('data/New World/region/r.-1.0.mca'),
( 0, -1): PosixPath('data/New World/region/r.0.-1.mca'),
( 0, 0): PosixPath('data/New World/region/r.0.0.mca')}
They are automatically loaded when you first access them:
>>> regions[0, 0]
<RegionFile(r.0.0.mca: 167 chunks)>
A RegionFile
is a dictionary of chunks, and each Chunk
contains its NBT data:
>>> region = regions[-2, 0]
>>> mc.pretty(region)
{
( 18, 0): <Chunk [18, 0] from Region ( -2, 0) in world at ( -46, 0) saved on 2021-10-11 16:39:17>,
( 28, 0): <Chunk [28, 0] from Region ( -2, 0) in world at ( -36, 0) saved on 2021-10-11 16:40:50>,
( 29, 0): <Chunk [29, 0] from Region ( -2, 0) in world at ( -35, 0) saved on 2021-10-11 16:40:50>,
...
( 29, 31): <Chunk [29, 31] from Region ( -2, 0) in world at ( -35, 31) saved on 2021-10-11 16:40:14>,
( 30, 31): <Chunk [30, 31] from Region ( -2, 0) in world at ( -34, 31) saved on 2021-10-11 16:40:14>,
( 31, 31): <Chunk [31, 31] from Region ( -2, 0) in world at ( -33, 31) saved on 2021-10-11 16:40:14>
}
>>> chunk = region[30, 31]
>>> mc.pretty(chunk) # alternatively, print(chunk.pretty())
{
Level: {
Status: "structure_starts",
zPos: 31,
LastUpdate: 4959L,
InhabitedTime: 0L,
xPos: -34,
Heightmaps: {},
TileEntities: [],
Entities: [],
...
},
DataVersion: 2730
}
You can fetch a chunk by several means, using for example:
world.get_chunk((cx, cz))
world.get_chunk_at((x, y, z))
world.player.get_chunk()
>>> for chunk in (
... world.get_chunk((-34, 21)),
... world.get_chunk_at((100, 60, 100)),
... world.player.get_chunk(),
... ):
... print(chunk)
...
<Chunk [30, 21] from Region ( -2, 0) in world at ( -34, 21) saved on 2021-10-11 16:40:50>
<Chunk [ 6, 6] from Region ( 0, 0) in world at ( 6, 6) saved on 2021-10-11 16:40:50>
<Chunk [18, 0] from Region ( -1, 0) in world at ( -14, 0) saved on 2021-10-11 16:40:48>
Get the block info at any coordinate:
>>> block = world.get_block_at((100, 60, 100))
>>> print(block)
Compound({'Name': String('minecraft:stone')})
Remember the automatic, lazy-loading feature of Regions
? In the above examples
a few chunks from distinct regions were accessed. So what is the state of the
regions
dictionary now?
>>> mc.pretty(regions)
{ ( -2, -1): PosixPath('data/New World/region/r.-2.-1.mca'),
( -2, 0): <RegionFile(r.-2.0.mca: 133 chunks)>,
( -1, -1): PosixPath('data/New World/region/r.-1.-1.mca'),
( -1, 0): <RegionFile(r.-1.0.mca: 736 chunks)>,
( 0, -1): PosixPath('data/New World/region/r.0.-1.mca'),
( 0, 0): <RegionFile(r.0.0.mca: 167 chunks)>}
As promised, only the accessed region files were actually loaded, automatically.
Reading and modifying the Player's inventory is quite easy:
>>> inventory = world.player.inventory # A handy shortcut
>>> inventory is world.level['Data']['Player']['Inventory']
True
>>> # Easily loop each item as if the inventory is a list. In fact, it *is*!
>>> for item in inventory:
... print(f"Slot {item['Slot']:3}: {item['Count']:2} x {item['id']}")
Slot 0: 1 x minecraft:stone_axe
Slot 1: 1 x minecraft:stone_pickaxe
Slot 2: 1 x minecraft:wooden_axe
Slot 3: 1 x minecraft:stone_shovel
Slot 4: 1 x minecraft:crafting_table
Slot 5: 37 x minecraft:coal
Slot 6: 8 x minecraft:dirt
Slot 11: 2 x minecraft:oak_log
Slot 12: 5 x minecraft:cobblestone
Slot 13: 2 x minecraft:stick
Slot 28: 1 x minecraft:wooden_pickaxe
How about some diamonds? Get 64 blocks of it in each one of your free inventory slots!
>>> backup = mc.List[mc.Compound](inventory[:]) # soon just inventory.copy()
>>> free_slots = set(range(36)) - set(item['Slot'] for item in inventory)
>>> for slot in free_slots:
... print(f"Adding 64 blocks of Diamond to inventory slot {slot}")
... item = mc.Compound({
... 'Slot': mc.Byte(slot),
... 'id': mc.String('minecraft:diamond_block'), # Sweet!
... 'Count': mc.Byte(64), # Enough for you?
... })
... inventory.append(item) # Yup, it's THAT simple!
...
Adding 64 blocks of Diamond to inventory slot 7
Adding 64 blocks of Diamond to inventory slot 8
Adding 64 blocks of Diamond to inventory slot 9
Adding 64 blocks of Diamond to inventory slot 10
Adding 64 blocks of Diamond to inventory slot 14
...
Adding 64 blocks of Diamond to inventory slot 35
>>> # Go on, we both know you want it. I won't judge you.
>>> world.save('data/tests/diamonds')
>>> # Revert it so it doesn't mess with other examples
>>> world.player.inventory = backup
Have fun, you millionaire!
More fun things to do:
>>> chunks = world.entities[mc.OVERWORLD][0, 0]
>>> for chunk in chunks.values():
... for entity in chunk.entities:
... print(entity)
...
Chest Minecart at ( 81, 18, 21)
Chest Minecart at ( 80, 18, 37)
Chest Minecart at ( 2, 38, 112)
Sheep at ( 36, 70, 116)
Sheep at ( 33, 69, 120)
Sheep at ( 37, 70, 116)
Item: 3 String at ( 14, 25, 152)
Item: 2 String at ( 14, 25, 153)
Chicken at ( 13, 64, 158)
Chicken at ( 12, 64, 156)
Chicken at ( 7, 64, 153)
Item: 1 String at ( 0, 35, 167)
Cow at ( 1, 65, 184)
Cow at ( 11, 64, 186)
Chest Minecart at ( 17, 32, 187)
Item: 3 String at ( 39, 35, 195)
Donkey at ( 56, 70, 202)
Donkey at ( 57, 71, 203)
Donkey at ( 56, 70, 201)
Chicken at ( 6, 64, 217)
How about some NBT Explorer nostalgia?
>>> mc.nbt_explorer(world.level)
⊟ Data: 42 entries
├──⊞ CustomBossEvents: 0 entries
├──⊟ DataPacks: 2 entries
│ ├──⊟ Disabled: 1 entry
│ │ ╰─── 0: Fabric Mods
│ ╰──⊟ Enabled: 1 entry
│ ╰─── 0: vanilla
...
├──⊟ Player: 37 entries
│ ├──⊟ abilities: 7 entries
│ │ ├─── flying: Byte(0)
...
│ │ ╰─── walkSpeed: Float(0.10000000149011612)
│ ├──⊟ Brain: 1 entry
│ │ ╰──⊞ memories: 0 entries
...
│ ├──⊟ Inventory: 11 entries
│ │ ├──⊟ 0: 4 entries
│ │ │ ├──⊟ tag: 1 entry
│ │ │ │ ╰─── Damage: Int(0)
│ │ │ ├─── Count: Byte(1)
│ │ │ ├─── id: minecraft:stone_axe
│ │ │ ╰─── Slot: Byte(0)
...
│ │ ╰──⊟ 10: 4 entries
│ │ ├──⊟ tag: 1 entry
│ │ │ ╰─── Damage: Int(18)
│ │ ├─── Count: Byte(1)
│ │ ├─── id: minecraft:wooden_pickaxe
│ │ ╰─── Slot: Byte(28)
...
│ ├─── XpTotal: Int(37)
│ ╰──⊕ UUID: 4 entries
├──⊟ Version: 3 entries
│ ├─── Id: Int(2730)
│ ├─── Name: 1.17.1
│ ╰─── Snapshot: Byte(0)
...
├──⊞ ScheduledEvents: 0 entries
├──⊟ ServerBrands: 1 entry
│ ╰─── 0: fabric
├─── allowCommands: Byte(0)
...
├─── WanderingTraderSpawnDelay: Int(19200)
╰─── WasModded: Byte(1)
You want to click that tree, don't you? Sweet Array
"icon" for UUID
!
Test yourself all the examples in this document:
python3 -m doctest -f -o ELLIPSIS -o NORMALIZE_WHITESPACE README.md
git checkout data/
Patches are welcome! Fork, hack, request pull! Here is a succinct to-do list:
Better documentation: Improve this README
, document classes, methods and
attributes, perhaps adding sphinx-like in-code documentation, possibly hosting
at Read the Docs. Add more in-depth usage scenarios.
Installer: Test and improve current setup.cfg
, possibly uploading to Pypi.
Semantics: Give semantics to some NBT data, providing methods to manipulate blocks, entities and so on.
CLI: Add a command-line interface for commonly used operations.
See the To-Do List for more updated technical information and planned features.
If you find a bug or have any enhancement request, please open a new issue
Rodrigo Silva (MestreLion) linux@rodrigosilva.com
Copyright (C) 2019 Rodrigo Silva (MestreLion) <linux@rodrigosilva.com>.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.