Yet another python library to manipulate Minecraft save data
Project description
mcworldlib - Minecraft World Library
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.
Features
- Read and write
.dat
NBT files, both uncompressed and gzip-compressed. - Read and write
.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. - Read and write
.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: externalmcc
if the chunk data outgrows its previous maximum size (~1 MB), and back to themca
if it shrinks enough to fit there again.
Usage
Reading world data
You can open a Minecraft World by several ways:
- Path to a
level.dat
file, or its open file-like stream object; - Path to a world directory, containing the
level.dat
file at its root, as in the example below; - World name, i.e, the directory basename of a world in the platform-dependent
default Minecraft
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:
- Its key in their region dictionary, using relative coordinates, as the examples above.
- Their absolute (cx, cz) chunk position:
world.get_chunk((cx, cz))
- An absolute (x, y, z) world position contained in it:
world.get_chunk_at((x, y, z))
- The player current location:
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.
Editing world data
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/
Contributing
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
Author
Rodrigo Silva (MestreLion) linux@rodrigosilva.com
License and Copyright
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file mcworldlib-2023.7.13.tar.gz
.
File metadata
- Download URL: mcworldlib-2023.7.13.tar.gz
- Upload date:
- Size: 48.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.8.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 801fb6c77ac209b2ca47ae6b99009c0fc87e0c1992d1bf375e2412d98f00718c |
|
MD5 | 8a6f9fee38aed043deecb0e19af6ff46 |
|
BLAKE2b-256 | a68daf7c81d7ee72bc49172bd4cd12a75f0a450d2db3e884c3fc8a4506d95614 |
File details
Details for the file mcworldlib-2023.7.13-py3-none-any.whl
.
File metadata
- Download URL: mcworldlib-2023.7.13-py3-none-any.whl
- Upload date:
- Size: 46.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.8.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 038f483a1bb2f962b84911502b0258e45c69a566efcbd30d5f90ffb123648ed0 |
|
MD5 | f1f92f14b738fb907fa511fc285f0c04 |
|
BLAKE2b-256 | eca55f0fd7bd18a625008a200c1b0b34d33353be7f63ab9d79cfdbad6531d964 |