Build with Roofs, Arches, Stairways and Schematics#

The mciwb.builders package has higher level building blocks than single blocks and boxes: roofs, arches, columns, stairways, roads, foundations that follow the terrain, and Sponge schematic (.schem) files. They are all in mciwb.imports, so mciwb shell and scripts run with mciwb run have them already.

How they work#

  • Positions are Vec3(x, y, z): Y is up, north is -z, east is +x (see Minecraft’s Coordinate System).

  • Blocks are Item members (Item.OAK_STAIRS), block state strings ("minecraft:oak_stairs[facing=east]") or mciwb.builders.BlockState objects.

  • A block state string or BlockState may end with block entity data in SNBT, as setblock takes it: a sign’s text or a chest’s contents. Placer().setblock and Placer().fill send it with the block:

    sign = BlockState.of(Item.OAK_SIGN, rotation=4).with_nbt(
        """{front_text:{messages:['"Hi"','""','""','""']}}""")
    Placer().setblock(Vec3(0, 70, 0), sign)
    Placer().setblock(Vec3(1, 70, 0),
                      'chest[facing=west]{Items:[{Slot:0b,id:"minecraft:apple",count:3}]}')
    
  • Each function builds with setblock and fill commands over RCON, using one fill for each rectangle of the same block where it can.

  • Each function returns the bounding box of the blocks it placed, as a Volume. Use it to read the build back, save it, or clear it again:

    box = gable_roof(Vec3(0, 70, 0), Vec3(8, 70, 6))
    print(box.start, box.end, box.size)
    Volume.from_corners(box.start, box.end).fill(get_client(), Item.AIR)
    
  • A command that the server rejects (an unknown block, or a position in a chunk that is not loaded) raises mciwb.builders.BuildError. Build near a player, or world.cmd("forceload add X1 Z1 X2 Z2") first.

  • Stairs and slabs get explicit block states. Minecraft works out the shape of stairs (straight, or an inner or outer corner) from their neighbours, and the builders work out the same shapes, so what they send is what the world ends up with.

A small house#

This script builds a house on uneven ground: a foundation, walls, an arched doorway, a hip roof, columns either side of the door and a path to it. Run it with mciwb run house.py:

from mciwb.imports import (
    Direction, Item, Vec3, Volume, arch, column, foundation, get_client,
    hip_roof, road,
)

client = get_client()
floor = Vec3(640, 70, -1650)           # the north west corner of the floor
far = floor + Vec3(10, 0, 8)            # the south east corner

# level the site, with a porch to the south: a platform at floor.y,
# filled down to the ground
foundation(floor, far + Direction.SOUTH * 4, Item.COBBLESTONE)

# hollow walls 4 high, on top of the platform
walls = Volume.from_corners(floor + Direction.UP, far + Direction.UP * 4)
walls.walls(client, Item.STONE_BRICKS, top=False, bottom=False)

# an arched doorway through the south wall
door = Vec3(643, 71, far.z)
arch(door, door + Direction.EAST * 4, Item.STONE_BRICKS, clear=True)

# a hip roof sitting on the walls, overhanging by one block
hip_roof(floor + Direction.UP * 4, far + Direction.UP * 4,
         Item.DARK_OAK_STAIRS, overhang=1)

# columns on the porch, either side of the door
for x in (642, 648):
    column(Vec3(x, 71, far.z + 2), 4, Item.QUARTZ_PILLAR,
           plinth=Item.SMOOTH_QUARTZ)

# a road from the porch south over the ground (it starts beyond the
# roof's overhang, which would otherwise count as the ground)
road([Vec3(645, 0, far.z + 2), Vec3(645, 0, far.z + 20)], Item.DIRT_PATH)

Check what you built#

Each builder returns the Volume it placed, so you can render exactly that region to an image (see Render a Build to an Image) and look at it, then fix the script and build again:

roof = hip_roof(floor + Direction.UP * 4, far + Direction.UP * 4,
                Item.DARK_OAK_STAIRS, overhang=1)
world.render(roof.start, roof.end, "roof.png", views=["top", "iso", "south"])

Or render the whole site from the command line with mciwb render. The images show blocks as flat colours and draw stairs and slabs as full blocks, so to check the facing or shape of stairs read them back with Schematic.from_world(volume) (see Schematics).

Roofs#

The roofs cover the rectangle between two opposite corners, which are usually the top corners of the walls. The roof starts one block above them.

mciwb.builders.gable_roof

two slopes of stairs meeting at a ridge, which runs along the longer side unless you give axis="x" or axis="z". Give gable= a block to fill in the triangular ends:

gable_roof(Vec3(0, 70, 0), Vec3(8, 70, 6), Item.SPRUCE_STAIRS,
           gable=Item.SPRUCE_PLANKS, overhang=1)
mciwb.builders.hip_roof

four slopes, with outer corner stairs where they meet.

Over an odd width, both sloping roofs finish with a ridge of slabs (or the ridge block), on a full block (a double slab) that fills the gap between the top stairs.

mciwb.builders.flat_roof

one layer of a block or slab, with an optional parapet around the edge. Walls, fences and panes in the parapet join up by themselves.

Arches and columns#

mciwb.builders.arch builds an arch standing in a wall. Its two feet are the outermost blocks at the bottom, in a line along x or z. style is "round" or "pointed", thickness is the thickness of the ring, depth the thickness through the wall (going south or east), and clear=True also sets the opening under the arch to air, to cut a doorway through an existing wall. mciwb.builders.arch_profile gives the shape of an arch without building it, which helps to plan the wall around it.

mciwb.builders.column builds a square column, with an optional wider plinth at the bottom and capital at the top.

Stairways and roads#

Both follow a path: a list of points where each straight section runs along x or along z. Add a point at each corner.

mciwb.builders.stairway climbs between the heights of its points. Each point is a block that people stand on; a section can climb at most one block for each block along it. Where it climbs there are stairs facing up the slope, elsewhere level landing blocks (by default the full block that matches the stairs, e.g. stone bricks for stone brick stairs). Points at the same height give a level path. support fills the space under the stairway and clearance cuts headroom through the terrain:

stairway([Vec3(0, 64, 0), Vec3(10, 70, 0), Vec3(10, 72, 6)],
         Item.STONE_BRICK_STAIRS, width=3, support=Item.STONE_BRICKS,
         clearance=3)

Round a corner that climbs, the middle and the outside of a wide stairway climb one block at a time, but the inside of the corner is shorter and too steep to walk up. To make every part of a wide stairway easy to walk, keep it level for a few blocks either side of each corner.

A stairway can pass over itself, like a spiral staircase round a tower or a switchback: every pass is built, and support under a higher pass stops above the headroom of the pass below it (clearance, or at least 2 blocks):

stairway([Vec3(0, 64, 0), Vec3(6, 67, 0), Vec3(6, 70, 6),
          Vec3(0, 73, 6), Vec3(0, 76, 0), Vec3(6, 79, 0)],
         Item.OAK_STAIRS)

mciwb.builders.road ignores the heights of its points and lays the road on the ground, replacing the top block of each column, with an optional border. The ground is the highest block in the column (see below), so keep roads out from under roofs and overhangs.

Foundations and the ground#

mciwb.builders.foundation fills each column of a footprint from the ground up to a level platform. Build it before anything else on the site: the ground is the highest block in each column, so a building already there would count as the ground.

mciwb.builders.ground_height and mciwb.builders.ground_heights give the y of the top ground block in a column, using Minecraft’s heightmaps (one RCON command per column). mciwb.builders.Heightmap chooses what counts as the ground: OCEAN_FLOOR ignores water, MOTION_BLOCKING_NO_LEAVES ignores leaves and plants but stops at water, WORLD_SURFACE stops at anything:

y = ground_height(640, -1650)
world.set_block(Vec3(640, y + 1, -1650), Item.TORCH)

Schematics#

mciwb.builders.Schematic holds a box of blocks with their block states, and loads and saves Sponge schematic (.schem) files, the format that WorldEdit and other Minecraft tools use:

house = Schematic.from_world(Volume.from_corners(corner1, corner2))
house.save("house.schem")

copy = Schematic.load("house.schem").rotate(1)   # a quarter turn clockwise
copy.paste(Vec3(700, 70, -1650))

world.save("house.schem", volume) and world.load("house.schem", position) do the same through the Iwb object.

A schematic’s blocks are a numpy array of mciwb.builders.BlockState, indexed [x][y][z] from its bottom north west corner, the same as the cuboids of Item that grab returns. Convert with Schematic.from_cuboid(cuboid) and schematic.to_cuboid(). rotate and mirror turn the block states with the blocks: stairs, doors, logs, signs, fences, walls and rails all face the right way afterwards.

Reading the world back is slow: one RCON command per block, plus a few more to find the facing, half, shape, type and axis of stairs, slabs and log-like blocks. Other block states (the facing of a door or a furnace, say) cannot be read over RCON, so those blocks come back in their default state. Block entity data (chest contents, sign text) is kept when loading and saving files, but it is not pasted into the world, nor read from it.