Skip to main content

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

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.

  • Tests: Expand doctest usage, add at least unittest.

  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcworldlib-2023.7.13.tar.gz (48.1 kB view details)

Uploaded Source

Built Distribution

mcworldlib-2023.7.13-py3-none-any.whl (46.9 kB view details)

Uploaded Python 3

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

Hashes for mcworldlib-2023.7.13.tar.gz
Algorithm Hash digest
SHA256 801fb6c77ac209b2ca47ae6b99009c0fc87e0c1992d1bf375e2412d98f00718c
MD5 8a6f9fee38aed043deecb0e19af6ff46
BLAKE2b-256 a68daf7c81d7ee72bc49172bd4cd12a75f0a450d2db3e884c3fc8a4506d95614

See more details on using hashes here.

File details

Details for the file mcworldlib-2023.7.13-py3-none-any.whl.

File metadata

File hashes

Hashes for mcworldlib-2023.7.13-py3-none-any.whl
Algorithm Hash digest
SHA256 038f483a1bb2f962b84911502b0258e45c69a566efcbd30d5f90ffb123648ed0
MD5 f1f92f14b738fb907fa511fc285f0c04
BLAKE2b-256 eca55f0fd7bd18a625008a200c1b0b34d33353be7f63ab9d79cfdbad6531d964

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page