Blocks and Chunks

Minecraft uses tightly-packed arrays to store data like light levels, heightmaps and block data. Quarry can read and write these formats in both Chunk Data packets and .mca files. Two classes are available for working with this data:

class quarry.types.chunk.PackedArray(storage, sector_width, value_width, fresh)[source]

This class provides support for an array where values are tightly packed into a number of bits (such as 4 bits for light or 9 bits for height).

All operations associated with fixed-size mutable sequences are supported, such as slicing.

Internally data is stored as a bit array with contiguous values, starting at the leftmost bits. Serializing to/from bytes is achieved by performing bitwise reversals of values and sectors; these reversals are deferred until access to packed values is needed.

Several constructors are available for specific uses of packed arrays:

  • Light data used 4-bit values and 8-bit sectors
  • Height data uses 9-bit values and 64-bit sectors
  • Block data uses 64-bit sectors
storage = None

The bitstring.BitArray object used for storage.

sector_width = None

The width in bits of sectors. Used in (de)serialization.

value_width = None

The width in bits of values.

fresh = None

Whether this array is new and empty

twiddled = None

Whether this array is contiguous (assumes non-empty, non-aligned)

classmethod empty(length, sector_width, value_width)[source]

Creates an empty array.

classmethod empty_light()[source]

Creates an empty array suitable for storing light data.

classmethod empty_block()[source]

Creates an empty array suitable for storing block data.

classmethod empty_height()[source]

Creates an empty array suitable for storing height data.

classmethod from_bytes(bytes, sector_width, value_width=None)[source]

Deserialize a packed array from the given bytes.

classmethod from_light_bytes(bytes)[source]

Deserialize a packed array from the given light data bytes.

classmethod from_block_bytes(bytes, value_width=None)[source]

Deserialize a packed array from the given block data bytes.

classmethod from_height_bytes(bytes)[source]

Deserialize a packed array from the given height data bytes.

to_bytes()[source]

Serialize this packed array to bytes.

init_storage()[source]

Initializes the storage by performing bitwise reversals.

You should not need to call this method.

purge(value_width)[source]

Re-initialize the storage to use a different value width, destroying stored data in the process.

You should not need to call this method.

is_empty()[source]

Returns true if this packed array is entirely zeros.

class quarry.types.chunk.BlockArray(storage, palette, registry, non_air=-1)[source]

This class provides support for block arrays. It wraps a PackedArray object and implements block encoding/decoding, palettes, and counting of non-air blocks for lighting purposes. It stores precisely 4096 (16x16x16) values.

All operations associated with fixed-size mutable sequences are supported, such as slicing.

A palette is used when there are fewer than 256 unique values; the value width varies from 4 to 8 bits depending on the size of the palette, and is automatically adjusted upwards as necessary. Use repack() to reclaim space by eliminating unused entries.

When 256 or more unique values are present, the palette is unused and values are stored directly.

storage = None

The PackedArray object used for storage.

palette = None

List of encoded block values. Empty when palette is not used.

registry = None

The Registry object used to encode/decode blocks

classmethod empty(registry, non_air=-1)[source]

Creates an empty block array.

classmethod from_bytes(bytes, palette, registry, non_air=-1, value_width=None)[source]

Deserialize a block array from the given bytes.

classmethod from_nbt(section, registry, non_air=-1)[source]

Creates a block array that uses the given NBT section tag as storage for block data and the palette. Minecraft 1.13+ only.

to_bytes()[source]

Serialize this block array to bytes.

is_empty()[source]

Returns true if this block array is entirely air.

non_air

The number of non-air blocks

repack(reserve=None)[source]

Re-packs internal data to use the smallest possible bits-per-block by eliminating unused palette entries. This operation is slow as it walks all blocks to determine the new palette.

Packets

On the client side, you can unpack a Chunk Data packet as follows:

def packet_chunk_data(self, buff):
    x, z, full = buff.unpack('ii?')
    bitmask = buff.unpack_varint()
    heightmap = buff.unpack_nbt()  # added in 1.14
    biomes = buff.unpack_array('I', 1024) if full else None  # changed in 1.15
    sections_length = buff.unpack_varint()
    sections = buff.unpack_chunk(bitmask)
    block_entities = [buff.unpack_nbt() for _ in range(buff.unpack_varint())]

On the server side:

def send_chunk(self, x, z, full, heightmap, sections, biomes, block_entities):
    sections_data = self.bt.pack_chunk(sections)
    self.send_packet(
        'chunk_data',
        self.bt.pack('ii?', x, z, full),
        self.bt.pack_chunk_bitmask(sections),
        self.bt.pack_nbt(heightmap),  # added in 1.14
        self.bt.pack_array('I', biomes) if full else b'',  # changed in 1.15
        self.bt.pack_varint(len(sections_data)),
        sections_data,
        self.bt.pack_varint(len(block_entities)),
        b''.join(self.bt.pack_nbt(entity) for entity in block_entities))

The variables used in these examples are as follows:

Variable Value type
x int
z int
full bool
bitmask int
heightmap TagRoot[TagCompound[TagLongArray[PackedArray]]]
sections List[Optional[BlockArray]]
biomes List[int]
block_entities List[TagRoot]

Regions

Quarry can load and save data from the .mca format via the RegionFile class. NBT tags such as "BlockStates", "BlockLight", "SkyLight" and heightmaps such as "MOTION_BLOCKING" make their values available as PackedArray objects.

Use BlockArray.from_nbt() with a LookupRegistry to create a block array backed by NBT data. Modifications to the block array will automatically be reflected in the NBT data, and vice versa.

Putting these pieces together, the following function could be used to set a block in an existing region file:

import os.path

from quarry.types.nbt import RegionFile
from quarry.types.registry import LookupRegistry
from quarry.types.chunk import BlockArray


def set_block(server_path, x, y, z, block):
    rx, x = divmod(x, 512)
    rz, z = divmod(z, 512)
    cx, x = divmod(x, 16)
    cy, y = divmod(y, 16)
    cz, z = divmod(z, 16)

    jar_path = os.path.join(server_path, "minecraft_server.jar")
    region_path = os.path.join(server_path, "world", "region", "r.%d.%d.mca" % (rx, rz))

    registry = LookupRegistry.from_jar(jar_path)
    with RegionFile(region_path) as region:
        chunk, section = region.load_chunk_section(cx, cy, cz)
        blocks = BlockArray.from_nbt(section, registry)
        blocks[256 * y + 16 * z + x] = block
        region.save_chunk(chunk)


set_block("/path/to/server", 10, 80, 40, {'name': 'minecraft:bedrock'})