First steps
This page covers basic usages: how to load & save world, how to edit chunks & blocks, how to edit inventories.
World management
Read the world
from the_blockheads_tools_py import WorldDb
# The path to the save file, it should have `data.mdb` in it.
# ```
# > tree resources
# resources
# └── 3d7
# └── world_db
# └── data.mdb
# ```
world_db_path = "../tests/resources/3d7/world_db"
# Load a world
world_db = WorldDb.open_path(world_db_path)
Print out world information
# Read world information
world_v2 = world_db.main.world_v2
print("world name:", world_v2.world_name)
print("seed:", world_v2.random_seed)
print("world width:", world_v2.world_width_macro)
print("start portal:", (world_v2.translation))
Run it, you should see something similar to this:
Save the world
Say you have made some changes to the world and you are happy about it. You can save it with WorldDb.save_path or WorldDb.save_bytes.
from the_blockheads_tools_py import Arch
world_v2.world_name = "edited"
modified_world_db_bytes = world_db.save_bytes(Arch.Arch32)
# or use save_path
# world_db.save_path("./path/to/your/output", Arch.Arch32)

Chunk & block manipulation
Read chunks and blocks
from the_blockheads_tools_py import Chunk, Block, BlockCoord
# read your world
# world_db = ...
chunks = world_db.chunks
print("num chunks:", len(chunks.keys()))
# Get spawn point coords
world_v2 = world_db.main.world_v2
spawn_x = world_v2.start_portal_pos_x
spawn_y = world_v2.start_portal_pos_y
# Break down the block coord to 1) chunk coord and 2) block coord in that chunk
block_coord = BlockCoord(spawn_x, spawn_y)
chunk_coord, chunk_block_coord = block_coord.decompose()
print("coords:", block_coord, chunk_coord, chunk_block_coord)
# Get chunk
spawn_chunk = chunks.chunk_at(chunk_coord)
assert spawn_chunk is not None
# Get block in chunk
spawn_block = spawn_chunk.block_at(chunk_block_coord)
print("Foreground block type:", spawn_block.fg())
print("Content block type:", spawn_block.content())
You should see something similar to this:
num chunks: 312
coords: BlockCoord(11182, 637) 349_19 ChunkBlockCoord(14, 29)
Foreground block type: BlockType.Air
Content block type: BlockContentType.WorkbenchSprite
Edit chunks and blocks
Basic
Note
Chunk instances are copies of the db data. You must use set_chunk_at to apply your changes back to the WorldDb.
Internal details
The WorldDb and Chunks instance holds a reference to data owned in Rust memory. However Chunk holds data owned in Python memory.
This means, edit to the Chunk instance won't automatically reflect in Chunks - because the ownership is different.
The following snippet tests the identities of objects:
spawn_chunk = chunks.chunk_at(chunk_coord)
assert spawn_chunk is not None
copied_spawn_chunk = chunks.chunk_at(chunk_coord)
assert copied_spawn_chunk is not None
assert copied_spawn_chunk is not spawn_chunk # Different instance!
updated_spawn_chunk = chunks.chunk_at(chunk_coord)
assert updated_spawn_chunk is not None
# Each chunk instance holds their own data,
# thus they are all different instances.
assert updated_spawn_chunk is not spawn_chunk
assert updated_spawn_chunk is not copied_spawn_chunk
from the_blockheads_tools_py import BlockType
# Change spawn portal to time crystal
spawn_block.set_fg(BlockType.TimeCrystal)
# Update chunks to apply change
chunks.set_chunk_at(chunk_coord, spawn_chunk)

Visibility
When your blockheads travel around the world, they will reveal blocks hidden behind the black fog. In this library, we name it "visibility". By setting their value to 255, we get a clear view.
from the_blockheads_tools_py import ChunkBlockCoord
chunk_coord = ChunkCoord(spawn_chunk_coord.x - 1, spawn_chunk_coord.y + 1)
chunk = chunks.chunk_at(chunk_coord)
assert chunk is not None
for x in range(Chunk.WIDTH // 2):
for y in range(Chunk.HEIGHT // 2):
block = chunk.block_at(ChunkBlockCoord(x, y))
block.set_visibility(x * 16 + y)
block = chunk.block_at(ChunkBlockCoord(x + 16, y + 16))
block.set_visibility(255)
chunks.set_chunk_at(chunk_coord, chunk)

Brightness
Even for blocks not in the black fog, they still can't be seen because they don't receive any light. This is described by "brightness".
chunk_coord = ChunkCoord(spawn_chunk_coord.x, spawn_chunk_coord.y)
chunk = chunks.chunk_at(chunk_coord)
assert chunk is not None
for x in range(Chunk.WIDTH // 2):
for y in range(Chunk.HEIGHT // 2):
block = chunk.block_at(ChunkBlockCoord(x + 16, y))
block.set_visibility(255)
block.set_brightness(x * 16 + y)
chunks.set_chunk_at(chunk_coord, chunk)

Foreground, background, content
As you might have noticed, in the game, the world has 3 blocks in depth. We describe them as foreground, background, and content.
While foreground and background are straightforward to understand (grass, dirt, water, stone, lava, etc), the content type is more messy as it describes everything: ores, tree blocks, workbenches and their sprites, etc.
Here's an example of manually building a small 16x16 region filled with ores:
from the_blockheads_tools_py import BlockContentType
from random import choice, random
chunk_coord = ChunkCoord(spawn_chunk_coord.x + 1, spawn_chunk_coord.y + 2)
chunk = Chunk()
for x in range(Chunk.WIDTH // 2):
for y in range(Chunk.HEIGHT // 2):
block = chunk.block_at(ChunkBlockCoord(x, y))
block.set_visibility(255)
block.set_brightness(255)
r = random()
block.set_bg(BlockType.Air)
block.set_fg(BlockType.Air)
if r > 0.2:
block.set_bg(BlockType.Stone)
if r > 0.4:
block.set_fg(BlockType.Stone)
if r > 0.6:
block.set_content(
choice(
[
BlockContentType.Coal,
BlockContentType.CopperOre,
BlockContentType.TinOre,
BlockContentType.IronOre,
BlockContentType.GoldNuggets,
BlockContentType.PlatinumOre,
BlockContentType.TitaniumOre,
]
)
)
chunks.set_chunk_at(chunk_coord, chunk)

Height of water and snow
We will use water height as example here, but the same field works for snow as well.
def get_checkerboard_value(x: int, y: int) -> tuple[bool, int]:
# 1. Define the boundary of the 30x30 interior
is_interior = 1 <= x <= 30 and 1 <= y <= 30
# 2. Check checkerboard parity:
is_white = is_interior and (x + y) % 2 == 0
if not is_white:
return False, -1
# 3. Calculate the index purely based on coordinates
rows_above = y - 1
cells_from_above = rows_above * 15
cells_in_current_row = (x + (1 if y % 2 != 0 else 0)) // 2
return True, cells_from_above + cells_in_current_row
num_white = 15 * 30
chunk_coord = ChunkCoord(spawn_chunk_coord.x - 1, spawn_chunk_coord.y + 2)
chunk = Chunk()
for x in range(Chunk.WIDTH):
for y in range(Chunk.HEIGHT):
block = chunk.block_at(ChunkBlockCoord(x, y))
block.set_visibility(255)
block.set_brightness(255)
match get_checkerboard_value(x, y):
case (True, val):
block.set_height(int(val / num_white * 255))
block.set_fg(BlockType.Water)
block.set_bg(BlockType.Water)
case (False, _):
block.set_fg(BlockType.SteelBlock)
block.set_bg(BlockType.SteelBlock)
chunks.set_chunk_at(chunk_coord, chunk)

Inventory manipulation
Read blockhead's inventory
# world_db = ...
blockheads = world_db.main.blockheads
assert len(blockheads) > 0
blockhead = blockheads[0]
print("blockhead name:", blockhead.name)
inventory = blockhead.inventory
assert inventory is not None
print("blockhead inventory:", inventory)
Output:
blockhead name: JESS
blockhead inventory: [1 Clothing, 1 FlintSpade, 5 Orange, 1 Basket, Empty, Empty, Empty, Empty]
Edit inventory
Basic
You can set item type and number of items:
from the_blockheads_tools_py import Slot, Item, ItemType
inventory[4] = Slot([Item(ItemType.Diamond)] * 1234)
# Remember to write back!
blockhead.inventory = inventory
Note
Similar to Chunk, Inventory instances are copies of the db data. You must write it back to apply your changes.

Containers
Blockheads can carry containers, such as basket and chests, in their inventories.
from the_blockheads_tools_py import BasketExtra, ChestExtra
# Add a basket filled with deprecated blocks
inventory[5] = Slot(
[
Item(
ItemType.Basket,
BasketExtra(
[
Slot([Item(ItemType.DeprecatedDirtBlock)] * 111),
Slot([Item(ItemType.DeprecatedWoodBlock)] * 222),
Slot([Item(ItemType.DeprecatedWorkbench)] * 333),
Slot([Item(ItemType.DeprecatedStoneWorkbench)] * 444),
]
),
)
]
)
# Add a chest with checkerboard pattern of "Double Time"
inventory[6] = Slot(
[
Item(
ItemType.Chest,
ChestExtra(
[
Slot([Item(ItemType.DoubleTime)])
if (i ^ j) & 1 == 0
else Slot()
for i in range(4)
for j in range(4)
]
),
)
]
)
# Write back
blockhead.inventory = inventory

Tool damage, dye
from the_blockheads_tools_py import PigmentColor
chest_extra = inventory[6][0].extra
assert isinstance(chest_extra, ChestExtra)
# Add a broken titanium pickaxe
damaged_titanium_pickaxe = Item(ItemType.TitaniumPickaxe)
damaged_titanium_pickaxe.damage = 65535
chest_extra[1] = Slot([damaged_titanium_pickaxe])
# Add a dyed golden bed
blue_golden_bed = Item(ItemType.GoldenBed)
blue_golden_bed.colors = [PigmentColor.CopperBlue]
chest_extra[3] = Slot([blue_golden_bed])
# and a dyed paint with 3 colors
paint = Item(ItemType.Paint)
paint.colors = [
PigmentColor.IndianYellow,
PigmentColor.TyrianPurple,
PigmentColor.MarbleWhite,
]
chest_extra[6] = Slot([paint])
# Write back
blockhead.inventory = inventory

If you want the damage indication bar to look normal, consider limit the damage value below 16384.
Workbench
Todo