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
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.
-
init_storage
()[source]¶ Initializes the storage by performing bitwise reversals.
You should not need to call this method.
-
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
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.
-
non_air
¶ The number of non-air blocks
-
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'})