# Generate the furnished Amberstone Lodge as a deterministic Java 1.21.1 litematic.
# Run with no arguments in a clean working directory to create output.litematic.
from __future__ import annotations
import argparse
import random
import sys
from pathlib import Path
from typing import Dict, Iterable, Optional, Tuple
sys.path.insert(0, "/work/generator/MCIO")
from mcio.nbt.nbt import File as NBTFile
from mcio.nbt.tag import Compound, String, Int, Byte, Short, Double, Float, IntArray, List as NBTList
from mcio.schematic import load_schematic # noqa: E402
from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402
CONFIG = {
"seed": 20260923,
"size_xyz": (45, 41, 35), # width X, height Y, depth Z
"minecraft_version": "1.21.1",
"data_version": 3955,
"output": "output.litematic",
"name": "Amberstone Lodge",
"author": "generator",
"description": "Amberstone Lodge | furnished timber hall, steep gabled tower, smithy and pines | Java 1.21.1",
}
Position = Tuple[int, int, int]
DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}
STONE = ('stone_bricks',) * 6 + ('cobblestone',) * 2 + ('mossy_stone_bricks',)
PAVING = ('stone_bricks',) * 5 + ('andesite',) * 2 + ('cobblestone',)
PLASTER = ('end_stone_bricks',) * 8 + ('sandstone',) * 3 + ('smooth_sandstone',) * 3
FLOOR = ('spruce_planks',) * 8 + ('dark_oak_planks',) * 2 + ('stripped_spruce_log',)
ROOF = ('deepslate_tiles',) * 10 + ('deepslate_bricks',) * 3 + ('cobbled_deepslate',)
CHIMNEY = ('cobblestone',) * 4 + ('andesite',) * 3 + ('stone_bricks',) * 2
class Build:
"""Hold a block volume and write it out as a Litematica schematic."""
def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None:
"""Create an empty volume with one seeded random source.
Args:
size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z.
seed (int): Seed for every random decision in this build.
"""
self.size_x, self.size_y, self.size_z = size_xyz
self.rng = random.Random(seed)
self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x))
self.placed: Dict[Position, str] = {}
def inside(self, x: int, y: int, z: int) -> bool:
"""Report whether a coordinate is inside the volume.
Args:
x (int): Width coordinate.
y (int): Height coordinate.
z (int): Depth coordinate.
Returns:
bool: True when every axis is within range.
"""
return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z
def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None:
"""Place one block, in XYZ order, with an immediate bounds check.
Args:
x (int): Width coordinate.
y (int): Height coordinate.
z (int): Depth coordinate.
block (str | Material): Namespaced block name, a full blockstate string, or a Material.
**properties: Blockstate properties such as facing or half.
Raises:
AssertionError: When the coordinate is outside the volume, naming the coordinate.
"""
assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}"
material = self.material(block, **properties) if isinstance(block, str) else block
self.canvas.block((y, z, x), material)
self.placed[(x, y, z)] = material.blockstate
def material(self, name: str, **properties: str) -> Material:
"""Build a Material from a block name and its properties.
Args:
name (str): Namespaced block name, or a full blockstate string.
**properties: Blockstate properties.
Returns:
Material: Material ready for placement.
"""
if "[" in name:
return parse_blockstate(name)
return self.canvas.material(name, **dict(sorted(properties.items())))
def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None:
"""Fill an inclusive box.
Args:
bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends.
block (str | Material): Block to place.
**properties: Blockstate properties.
"""
x0, y0, z0, x1, y1, z1 = bounds
for x in range(min(x0, x1), max(x0, x1) + 1):
for y in range(min(y0, y1), max(y0, y1) + 1):
for z in range(min(z0, z1), max(z0, z1) + 1):
self.put(x, y, z, block, **properties)
def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None:
"""Fill only the faces of a box, leaving the inside empty.
Args:
bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive.
block (str | Material): Block to place.
**properties: Blockstate properties.
"""
x0, y0, z0, x1, y1, z1 = bounds
for x in range(min(x0, x1), max(x0, x1) + 1):
for y in range(min(y0, y1), max(y0, y1) + 1):
for z in range(min(z0, z1), max(z0, z1) + 1):
on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1)
if on_face:
self.put(x, y, z, block, **properties)
def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None:
"""Fill a box with a weighted mix of related blocks, so the surface is not flat.
Args:
bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive.
palette (Iterable[str]): Block names. Repeat a name to make it more common.
"""
choices = list(palette)
x0, y0, z0, x1, y1, z1 = bounds
for x in range(min(x0, x1), max(x0, x1) + 1):
for y in range(min(y0, y1), max(y0, y1) + 1):
for z in range(min(z0, z1), max(z0, z1) + 1):
self.put(x, y, z, self.rng.choice(choices))
def get(self, x: int, y: int, z: int) -> Optional[str]:
"""Read back what was placed at a coordinate.
Args:
x (int): Width coordinate.
y (int): Height coordinate.
z (int): Depth coordinate.
Returns:
Optional[str]: Blockstate string, or None when the cell is empty.
"""
return self.placed.get((x, y, z))
def block(self, x: int, y: int, z: int, name: str, **props: str) -> None:
"""Place a vanilla block with compact names and validated Java properties."""
self.put(x, y, z, 'minecraft:' + name, **props)
def fill(self, bounds: tuple, name: str, **props: str) -> None:
"""Fill an inclusive box with a vanilla material."""
self.box(bounds, 'minecraft:' + name, **props)
def mix(self, bounds: tuple, palette: tuple) -> None:
"""Texture a structural surface using the seeded material palette."""
self.texture(bounds, ['minecraft:' + p for p in palette])
def beam(self, bounds: tuple, axis: str = 'y') -> None:
"""Place an exposed timber beam."""
self.fill(bounds, 'stripped_spruce_log', axis=axis)
def stair(self, x: int, y: int, z: int, facing: str, material: str = 'spruce', half: str = 'bottom') -> None:
"""Place a fully specified stair, including furniture and roof trim."""
self.block(x, y, z, material + '_stairs', facing=facing, half=half, shape='straight', waterlogged='false')
def slab(self, x: int, y: int, z: int, material: str = 'spruce', kind: str = 'bottom') -> None:
"""Place a slab."""
self.block(x, y, z, material + '_slab', type=kind, waterlogged='false')
def trap(self, x: int, y: int, z: int, facing: str, material: str = 'spruce', opened: bool = True) -> None:
"""Place a shutter or furniture panel."""
self.block(x, y, z, material + '_trapdoor', facing=facing, half='bottom', open=str(opened).lower(), powered='false', waterlogged='false')
def tile(self, pos: Position, name: str, **data: object) -> None:
"""Add deterministic Java block entity data at a placed block."""
x, y, z = pos
self.tiles[pos] = Compound({'id': String('minecraft:' + name), 'x': Int(x), 'y': Int(y), 'z': Int(z), **data})
def container(self, x: int, y: int, z: int, name: str = 'barrel', facing: str = 'north', items: tuple = ()) -> None:
"""Place stocked storage using the 1.21 item stack format."""
props = {'facing': facing}
if name == 'barrel':
props['open'] = 'false'
else:
props.update(type='single', waterlogged='false')
self.block(x, y, z, name, **props)
stacks = [Compound({'Slot': Byte(i), 'id': String('minecraft:' + item), 'count': Int(count)}) for i, (item, count) in enumerate(items)]
self.tile((x, y, z), name, Items=NBTList[Compound](stacks))
def door(self, x: int, y: int, z: int, facing: str, hinge: str = 'left', material: str = 'spruce') -> None:
"""Place matching supported upper and lower door halves."""
for dy, half in enumerate(('lower', 'upper')):
self.block(x, y + dy, z, material + '_door', facing=facing, hinge=hinge, half=half, open='false', powered='false')
self.doors.append((x, y, z))
def lantern(self, x: int, y: int, z: int, hanging: bool = False) -> None:
"""Place a warm lantern at its designed support."""
self.block(x, y, z, 'lantern', hanging=str(hanging).lower(), waterlogged='false')
def candles(self, x: int, y: int, z: int, count: int = 3) -> None:
"""Place lit beeswax candles on a shelf or table."""
self.block(x, y, z, 'candle', candles=str(count), lit='true', waterlogged='false')
def pot(self, x: int, y: int, z: int, plant: str = 'fern') -> None:
"""Place a plant in a terracotta flower pot."""
self.block(x, y, z, 'potted_' + plant)
def frame(self, backing: Position, facing: str, item: str) -> None:
"""Attach an item frame entity to a solid face with a deterministic UUID."""
dx, dz = DIRECTIONS[facing]
bx, by, bz = backing
x, y, z = bx + dx, by, bz + dz
# Hanging entities anchor to the adjacent air cell, offset toward their backing.
pos = (x + .5 - dx * .46875, y + .5, z + .5 - dz * .46875)
entity = Compound({'id': String('minecraft:item_frame'), 'Pos': NBTList[Double]([Double(v) for v in pos]), 'TileX': Int(x), 'TileY': Int(y), 'TileZ': Int(z), 'Facing': Byte({'north': 2, 'south': 3, 'west': 4, 'east': 5}[facing]), 'Rotation': NBTList[Float]([Float({'south': 0, 'west': 90, 'north': 180, 'east': 270}[facing]), Float(0)]), 'ItemRotation': Byte(0), 'ItemDropChance': Float(1), 'Fixed': Byte(1), 'Invisible': Byte(0), 'UUID': IntArray([20260923, bx * 1000 + by, bz, len(self.entities) + 1]), 'Item': Compound({'id': String('minecraft:' + item), 'count': Int(1)})})
self.entities.append(entity)
def window(self, axis: str, plane: int, start: int, end: int, y0: int, y1: int) -> None:
"""Carve a glazed window with timber sill, lintel and exterior shutters."""
for a in range(start, end + 1):
for y in range(y0, y1 + 1):
x, z = (a, plane) if axis == 'z' else (plane, a)
self.block(x, y, z, 'light_gray_stained_glass')
self.windows.append((x, y, z))
for y in (y0 - 1, y1 + 1):
x, z = (a, plane) if axis == 'z' else (plane, a)
self.beam((x, y, z, x, y, z), 'x' if axis == 'z' else 'z')
for a in (start - 1, end + 1):
for y in range(y0, y1 + 1):
x, z = (a, plane) if axis == 'z' else (plane, a)
self.beam((x, y, z, x, y, z))
exterior = -1 if plane in (9, 10, 11) else 1
if axis == 'z':
self.trap(x, y, z + exterior, 'north' if exterior < 0 else 'south')
else:
self.trap(x + exterior, y, z, 'west' if exterior < 0 else 'east')
def banner(self, x: int, y: int, z: int, facing: str) -> None:
"""Hang white curtains with two green vertical stripes in Java 1.21 NBT."""
self.block(x, y, z, 'white_wall_banner', facing=facing)
self.tile((x, y, z), 'banner', patterns=NBTList[Compound]([Compound({'pattern': String('minecraft:stripe_left'), 'color': String('green')}), Compound({'pattern': String('minecraft:stripe_right'), 'color': String('green')})]))
@staticmethod
def low_roof(z: int) -> int:
"""Height of the broad, shallow roof above the lower wing."""
return 14 + min(z - 8, 28 - z) * 7 // 10
@staticmethod
def tall_roof(x: int) -> int:
"""Height of the narrow, steep tower roof, including flared eaves."""
return (37, 34, 31, 28, 25, 22, 19, 17, 16)[abs(x - 31)]
def terrain(self) -> None:
"""Build a broken grass island, raised terrace, stairs and worn approach."""
for x in range(1, 44):
for z in range(1, 34):
# Trim the corners and roughen only the silhouette.
if ((x - 22) / 23)**4 + ((z - 18) / 18)**4 > .97 + self.rng.random() * .14:
continue
self.block(x, 0, z, 'grass_block')
self.mix((4, 1, 7, 40, 2, 30), STONE)
self.mix((5, 3, 8, 39, 3, 29), PAVING)
for x in range(5, 40):
if 24 <= x <= 28:
continue
self.slab(x, 3, 7, 'stone_brick')
for z in range(8, 31):
for x in (4, 40):
self.slab(x, 3, z, 'stone_brick')
self.fill((24, 3, 7, 28, 3, 7), 'stone_bricks')
for z, y in ((4, 1), (5, 2), (6, 3)):
for x in range(24, 29):
self.fill((x, 0, z, x, y - 1, z), 'cobblestone')
self.stair(x, y, z, 'south', 'stone_brick')
for z in range(0, 6):
center = 25 - (5 - z) // 2
for x in range(center - 3, center + 4):
if self.rng.random() < .85:
self.block(x, 0, z, self.rng.choice(('dirt_path', 'coarse_dirt', 'rooted_dirt', 'gravel')))
for x in range(2, 43):
for z in range(3, 33):
if self.get(x, 0, z) != 'minecraft:grass_block' or self.get(x, 1, z):
continue
if self.rng.random() < .09 and not (21 <= x <= 30 and z < 8):
self.block(x, 1, z, self.rng.choice(('short_grass', 'fern', 'dandelion', 'poppy', 'moss_carpet')))
for x, z in ((8, 7), (17, 7), (33, 7), (41, 15), (39, 31), (11, 31)):
self.block(x, 1, z, 'mossy_cobblestone')
def structure(self) -> None:
"""Construct two joined timber masses, floors and their closed roof union."""
# Floors: stone base, first-floor boards, tower bedroom, narrowed attic.
for bounds in ((9, 3, 10, 25, 3, 26), (25, 3, 11, 37, 3, 28)):
self.mix(bounds, PAVING)
for bounds in ((9, 9, 10, 25, 9, 26), (25, 9, 11, 37, 9, 28), (25, 16, 11, 37, 16, 28), (27, 23, 11, 35, 23, 28)):
self.mix(bounds, FLOOR)
for x0, z0, x1, z1, top in ((9, 10, 25, 26, 14), (25, 11, 37, 28, 18)):
for x in range(x0, x1 + 1):
for z in range(z0, z1 + 1):
if x not in (x0, x1) and z not in (z0, z1):
continue
for y in range(4, top + 1):
self.block(x, y, z, self.rng.choice(STONE if y < 9 else PLASTER))
for x in range(x0, x1 + 1):
for z in (z0, z1):
for y in (8, 9, 14):
self.beam((x, y, z, x, y, z), 'x')
for z in range(z0, z1 + 1):
for x in (x0, x1):
for y in (8, 9, 14):
self.beam((x, y, z, x, y, z), 'z')
for x in (x0, x1):
for z in (z0, z1):
self.beam((x, 4, z, x, top, z))
# Low wing gable ends, hidden rear consistent with the facade.
for x in (9, 25):
for z in range(10, 27):
for y in range(15, self.low_roof(z) + 1):
self.block(x, y, z, self.rng.choice(PLASTER))
for x in range(25, 38):
for z in (11, 28):
for y in range(19, self.tall_roof(x) + 1):
self.block(x, y, z, self.rng.choice(PLASTER))
# Half-timber pattern has coherent beams instead of random wood spots.
for x in (9, 14, 20, 25):
for z in (10, 26):
self.beam((x, 9, z, x, 14, z))
for z in (10, 18, 26):
self.beam((9, 9, z, 9, self.low_roof(z) - 1, z))
for z in (11, 28):
for x in (25, 28, 34, 37):
self.beam((x, 9, z, x, min(27, self.tall_roof(x) - 1), z))
self.beam((31, 15, z, 31, 36, z))
for y, xa, xb in ((16, 25, 37), (23, 27, 35), (29, 29, 33)):
self.beam((xa, y, z, xb, y, z), 'x')
for x in range(26, 37):
y = self.tall_roof(x) - 2
self.beam((x, y, z, x, y, z), 'x')
for z in (16, 22):
self.beam((37, 9, z, 37, 18, z))
# Continuous roof shells. Risers fill all vertical gaps between courses.
for x in range(7, 27):
for z in range(8, 29):
height = self.low_roof(z)
outer = z - 1 if z <= 18 else z + 1
bottom = self.low_roof(outer) if 8 <= outer <= 28 else height
for y in range(bottom, height + 1):
if 25 <= x <= 37 and 11 <= z <= 28 and y < self.tall_roof(x):
continue
self.block(x, y, z, self.rng.choice(ROOF))
if not (25 <= x <= 37 and 11 <= z <= 28 and height < self.tall_roof(x)):
if z != 18:
self.stair(x, height + 1, z, 'south' if z < 18 else 'north', 'deepslate_tile')
else:
self.slab(x, height + 1, z, 'deepslate_brick')
for x in range(23, 40):
height = self.tall_roof(x)
outer = x - 1 if x < 31 else x + 1
bottom = self.tall_roof(outer) if 23 <= outer <= 39 else height
for z in range(9, 31):
for y in range(bottom, height + 1):
if 7 <= x <= 25 and 10 <= z <= 26 and y <= self.low_roof(z):
continue
self.block(x, y, z, self.rng.choice(ROOF))
if 7 <= x <= 25 and 10 <= z <= 26 and height + 1 <= self.low_roof(z):
continue
if x == 31:
self.slab(x, height + 1, z, 'deepslate_brick')
else:
self.stair(x, height + 1, z, 'east' if x < 31 else 'west', 'deepslate_tile')
# Amber ridge and carved finials, as in the reference.
for z in range(10, 30):
self.slab(31, 38, z, 'dark_oak')
for z in (10, 29):
self.block(31, 38, z, 'dark_oak_fence')
self.block(31, 39, z, 'dark_oak_fence')
# Exposed rafter tails and supporting brackets below the overhang.
for x in (9, 14, 20, 25):
for z, facing in ((9, 'south'), (27, 'north')):
self.stair(x, 13, z, facing, 'spruce', 'top')
for z in range(12, 29, 4):
self.stair(38, 17, z, 'west', 'spruce', 'top')
# Shared wall has wide, framed passages on the occupied floors.
for y in (4, 10):
self.fill((25, y, 14, 25, y + 3, 17), 'air')
self.beam((25, y + 3, 14, 25, y + 3, 17), 'z')
# Entry hall and storage partition with a three-block-high opening.
self.fill((18, 4, 11, 18, 8, 25), 'spruce_planks')
self.fill((18, 4, 14, 18, 6, 16), 'air')
self.beam((18, 7, 14, 18, 7, 16), 'z')
# Front door is a glazed oak pair, surrounded by a timber portal.
for x, hinge in ((21, 'left'), (22, 'right')):
self.door(x, 4, 10, 'north', hinge, 'oak')
self.block(x, 6, 10, 'light_gray_stained_glass')
for x in (20, 23):
self.beam((x, 4, 10, x, 7, 10))
self.beam((20, 7, 10, 23, 7, 10), 'x')
self.door(9, 4, 16, 'west')
# Window apertures are placed after roof construction and kept as a contract.
for args in (('z', 10, 11, 12, 5, 6), ('z', 10, 11, 12, 11, 13), ('z', 10, 16, 17, 11, 13), ('z', 11, 29, 32, 5, 6), ('z', 11, 29, 32, 11, 13), ('z', 11, 30, 32, 19, 21), ('z', 11, 30, 32, 26, 28), ('z', 26, 11, 12, 11, 13), ('z', 26, 16, 17, 11, 13), ('z', 28, 29, 32, 11, 13), ('z', 28, 30, 32, 19, 21), ('z', 28, 30, 32, 26, 28), ('x', 9, 21, 23, 11, 13), ('x', 37, 14, 15, 11, 13), ('x', 37, 23, 24, 11, 13), ('x', 37, 20, 21, 5, 6)):
self.window(*args)
# Front garden ledges, all supported by corbels.
for xa, xb, z in ((11, 12, 9), (16, 17, 9), (29, 32, 10)):
for x in range(xa, xb + 1):
self.stair(x, 9, z, 'south', 'spruce', 'top')
self.block(x, 10, z, 'grass_block')
self.trap(x, 10, z - 1, 'north')
self.block(x, 11, z, 'poppy' if x % 2 else 'fern')
for x, y, z in ((19, 5, 9), (24, 5, 9), (28, 5, 10), (36, 11, 10), (10, 11, 27), (38, 11, 18)):
self.block(x, y, z, 'spruce_planks')
self.lantern(x, y + 1, z)
# Attached ivy climbs the stone and then the timber corner.
for x, top in ((25, 12), (26, 10), (27, 7)):
for y in range(4, top + 1):
if self.get(x, y, 11) and self.get(x, y, 11) != 'minecraft:air' and not self.get(x, y, 10):
self.block(x, y, 10, 'vine', south='true', north='false', east='false', west='false', up='false')
for z in range(23, 27):
for y in range(4, 9 + (z % 2)):
if self.get(9, y, z) and not self.get(8, y, z):
self.block(8, y, z, 'vine', east='true', north='false', south='false', west='false', up='false')
def stairs(self) -> None:
"""Connect all four floors with two-block-wide stair flights and clear landings."""
for x0, z0, bottom, rise, direction in ((21, 18, 3, 6, 'south'), (34, 18, 9, 7, 'south'), (29, 25, 16, 7, 'north')):
dz = DIRECTIONS[direction][1]
for step in range(rise):
z = z0 + dz * step
y = bottom + 1 + step
for x in (x0, x0 + 1):
self.fill((x, bottom + 1, z, x, y, z), 'spruce_planks')
self.stair(x, y, z, direction)
# Three blocks clear even where the flight pierces a floor.
self.fill((x, y + 1, z, x, y + 3, z), 'air')
self.flights.append((x0, y, z, direction))
landing = z0 + dz * rise
self.fill((x0, bottom + rise, landing, x0 + 1, bottom + rise, landing), 'spruce_planks')
self.fill((x0, bottom + rise + 1, landing, x0 + 1, bottom + rise + 3, landing), 'air')
# Posts and short balustrades leave the landings and stair treads open.
for x, y, z in ((20, 10, 22), (20, 10, 23), (23, 10, 22), (23, 10, 23), (33, 17, 22), (33, 17, 23), (36, 17, 23), (28, 24, 19), (31, 24, 19)):
if self.get(x, y - 1, z) not in (None, 'minecraft:air'):
self.block(x, y, z, 'spruce_fence')
def forge(self) -> None:
"""Build the sheltered, usable blacksmith shop and its tall stone flue."""
self.mix((3, 1, 12, 8, 2, 22), STONE)
self.mix((3, 3, 12, 8, 3, 22), PAVING)
for x, z in ((3, 12), (3, 22), (8, 12), (8, 22)):
self.fill((x, 1, z, x, 3, z), 'stone_bricks')
self.beam((x, 4, z, x, 8, z))
for z in range(11, 24):
for x in range(2, 9):
y = 7 + (x - 2) // 3
self.block(x, y, z, self.rng.choice(ROOF))
self.stair(x, y + 1, z, 'east', 'deepslate_tile')
self.mix((5, 4, 18, 8, 6, 21), STONE)
self.fill((5, 4, 19, 6, 5, 20), 'air')
self.block(6, 4, 19, 'campfire', facing='west', lit='true', signal_fire='false', waterlogged='false')
self.block(6, 4, 20, 'blast_furnace', facing='west', lit='true')
self.tile((6, 4, 20), 'blast_furnace', BurnTime=Short(1600), CookTime=Short(0), CookTimeTotal=Short(200), Items=NBTList[Compound]([Compound({'Slot': Byte(0), 'id': String('minecraft:raw_iron'), 'count': Int(48)}), Compound({'Slot': Byte(1), 'id': String('minecraft:coal'), 'count': Int(48)})]))
for y in range(7, 26):
self.mix((6, y, 19, 7, y, 20), CHIMNEY)
for y in (8, 21, 24):
self.fill((5, y, 18, 8, y, 21), 'stone_brick_slab', type='top', waterlogged='false')
for x in (6, 7):
for z in (19, 20):
self.block(x, 26, z, 'cobblestone_wall', up='true', north='low', south='low', east='low', west='low', waterlogged='false')
self.slab(x, 27, z, 'stone_brick')
self.block(4, 4, 16, 'anvil', facing='north')
self.block(7, 4, 14, 'smithing_table')
self.block(5, 4, 13, 'grindstone', face='floor', facing='south')
self.container(6, 4, 12, items=(('iron_ingot', 32), ('coal', 32)))
self.container(7, 4, 12, items=(('iron_pickaxe', 1), ('iron_axe', 1)))
self.container(3, 4, 21, 'chest', 'east', (('cobblestone', 64), ('charcoal', 32)))
self.lantern(3, 7, 13, True)
self.block(3, 8, 13, 'spruce_planks')
self.frame((7, 4, 14), 'west', 'iron_sword')
def ground_rooms(self) -> None:
"""Furnish the entrance, supply room and stone workshop around clear aisles."""
for z in (12, 18, 22, 24):
for y in (4, 5):
self.container(10, y, z, items=(('oak_log', 32), ('coal', 16)))
for x in (12, 14, 16):
self.container(x, 4, 25, 'chest', 'north', (('wheat', 32), ('bread', 16)))
self.block(12, 4, 18, 'crafting_table')
self.block(13, 4, 18, 'smithing_table')
self.block(14, 4, 18, 'spruce_planks')
self.candles(14, 5, 18)
self.block(12, 4, 19, 'stonecutter', facing='south')
self.block(16, 4, 12, 'spruce_planks')
self.pot(16, 5, 12, 'blue_orchid')
self.frame((18, 5, 20), 'west', 'iron_pickaxe')
self.frame((18, 5, 22), 'west', 'leather')
for x, z in ((13, 13), (15, 22), (21, 14), (29, 16), (33, 24)):
self.block(x, 8, z, 'spruce_planks')
self.lantern(x, 7, z, True)
for z in range(11, 17):
for x in (21, 22):
self.block(x, 4, z, 'red_carpet')
self.container(19, 4, 12, items=(('map', 1), ('torch', 16)))
self.pot(19, 5, 12)
self.container(24, 4, 20)
self.candles(24, 5, 20, 2)
# Tower ground: glazed entrance hall and book wall.
for z in range(17, 26):
for y in (4, 5, 6):
self.block(36, y, z, 'bookshelf' if (z + y) % 3 else 'chiseled_bookshelf', **({} if (z + y) % 3 else {'facing': 'west', 'slot_0_occupied': 'true', 'slot_1_occupied': 'false', 'slot_2_occupied': 'true', 'slot_3_occupied': 'true', 'slot_4_occupied': 'false', 'slot_5_occupied': 'true'}))
for x in range(28, 33):
self.container(x, 4, 27)
self.pot(28, 5, 27, 'spruce_sapling')
self.candles(30, 5, 27)
self.container(32, 5, 27, 'chest')
self.frame((31, 7, 28), 'north', 'clock')
for z in range(17, 23):
self.block(29, 4, z, 'red_carpet')
self.block(30, 4, z, 'red_carpet')
self.fill((26, 4, 21, 27, 5, 22), 'bricks')
self.pot(26, 6, 21, 'fern')
for y in range(6, 9):
self.block(26, y, 22, 'oak_leaves', persistent='true', distance='1', waterlogged='false')
# Service ladder to a storage shelf, backed by the masonry wall.
self.fill((34, 6, 26, 36, 6, 27), 'spruce_planks')
for y in range(4, 7):
self.block(35, y, 27, 'ladder', facing='north', waterlogged='false')
self.container(34, 7, 27)
def living_rooms(self) -> None:
"""Furnish a green sitting room, long dining table and working kitchen."""
# Inlaid living rug and west-wall settle.
for x in range(11, 18):
for z in range(14, 20):
self.block(x, 10, z, 'green_carpet' if x in (11, 17) or z in (14, 19) else 'moss_carpet')
for z in range(14, 18):
self.stair(10, 10, z, 'west', 'dark_oak')
for z in (13, 18):
self.container(10, 10, z)
self.pot(10, 11, 13, 'blue_orchid')
self.lantern(10, 11, 18)
for x in (13, 14):
self.block(x, 10, 16, 'spruce_planks')
self.candles(13, 11, 16)
self.pot(14, 11, 16, 'red_tulip')
for x in (13, 14):
self.stair(x, 10, 18, 'south', 'dark_oak')
# Cabinets and open book nook at the rear wall.
for x in (10, 11, 12, 13, 14, 15):
self.container(x, 10, 25)
if x < 13:
self.container(x, 12, 25)
self.block(14, 11, 25, 'lectern', facing='north', has_book='true', powered='false')
self.book((14, 11, 25))
self.candles(15, 11, 25, 2)
self.block(15, 12, 26, 'bookshelf')
self.frame((20, 12, 26), 'north', 'map')
# Solid top dining table and four chairs in the broad middle aisle.
for z in (17, 18, 19, 20):
self.block(18, 10, z, 'dark_oak_planks')
self.block(19, 10, z, 'dark_oak_planks')
for z in (17, 20):
self.candles(18, 11, z, 2)
for z in (17, 19):
self.stair(17, 10, z, 'west', 'spruce')
self.stair(20, 10, z, 'east', 'spruce')
# Kitchen in the tower, with chimney breast and fireproof hearth.
for x in range(27, 33):
self.container(x, 10, 27)
self.block(26, 10, 27, 'smoker', facing='north', lit='true')
self.block(27, 10, 27, 'furnace', facing='north', lit='true')
self.fill((26, 11, 28, 27, 15, 28), 'stone_bricks')
self.block(31, 11, 27, 'water_cauldron', level='3')
self.pot(28, 11, 27)
self.candles(32, 11, 27)
for x in (26, 27, 33):
self.container(x, 13, 27)
self.block(26, 10, 21, 'crafting_table')
self.container(26, 10, 22)
self.block(26, 10, 23, 'spruce_planks')
self.pot(26, 11, 23, 'brown_mushroom')
for z in (13, 22):
self.beam((9, 14, z, 25, 14, z), 'x')
for z in (16, 24):
self.beam((25, 14, z, 37, 14, z), 'x')
for x, z in ((14, 13), (17, 22), (22, 13), (29, 16), (32, 24)):
self.block(x, 14, z, 'spruce_planks')
self.lantern(x, 13, z, True)
for x in range(28, 32):
for z in range(14, 19):
self.block(x, 10, z, 'red_carpet' if x in (28, 31) else 'brown_carpet')
for x in (29, 32):
self.banner(x, 13, 12, 'south')
self.frame((25, 12, 22), 'east', 'golden_carrot')
def bedroom(self) -> None:
"""Recreate the raised green bed, striped curtains, wall cabinets and hearth."""
# A shallow platform leaves generous standing room around the double bed.
self.fill((27, 17, 13, 30, 17, 16), 'dark_oak_planks')
for x in (28, 29):
self.stair(x, 17, 17, 'north', 'dark_oak')
self.block(x, 18, 15, 'green_bed', part='foot', facing='north', occupied='false')
self.block(x, 18, 14, 'green_bed', part='head', facing='north', occupied='false')
for x in (27, 30):
for z in (14, 15):
self.trap(x, 18, z, 'west' if x == 27 else 'east', 'dark_oak')
for x in (28, 29):
self.trap(x, 18, 16, 'south', 'dark_oak')
self.beam((27, 18, 13, 30, 18, 13), 'x')
for x in (27, 30):
self.container(x, 19, 13, facing='south')
self.block(28, 20, 12, 'bookshelf')
self.block(29, 20, 12, 'bookshelf')
self.fill((27, 21, 12, 29, 21, 12), 'spruce_planks')
self.pot(27, 22, 12, 'fern')
self.lantern(30, 20, 13)
# Window seat and curtains are on the inside face of the front wall.
for x in (31, 32, 33):
self.container(x, 17, 12, facing='south')
self.block(x, 18, 12, 'white_carpet')
for x in (29, 33):
self.banner(x, 21, 12, 'south')
self.container(34, 17, 13, facing='south')
self.pot(34, 18, 13, 'blue_orchid')
# Wardrobe, dressing table and back-wall shelving.
for x in (26, 27):
for y in (17, 18, 19):
self.container(x, y, 26)
self.fill((31, 17, 27, 33, 17, 27), 'spruce_planks')
self.pot(31, 18, 27, 'red_tulip')
self.candles(33, 18, 27)
self.frame((32, 19, 28), 'north', 'clock')
for x in range(31, 34):
for z in range(14, 20):
self.block(x, 17, z, 'green_carpet')
# A solid masonry chimney breast gives the bedroom reference's stone edge.
self.mix((36, 17, 14, 36, 20, 16), CHIMNEY)
self.block(35, 17, 15, 'campfire', facing='west', lit='true', signal_fire='false', waterlogged='false')
self.fill((35, 18, 14, 35, 19, 16), 'stone_bricks')
self.block(35, 17, 14, 'stone_bricks')
self.block(35, 17, 16, 'stone_bricks')
for z in (17, 25):
self.beam((27, 21, z, 35, 21, z), 'x')
self.beam((26, 21, 23, 28, 21, 23), 'x')
for x, z in ((32, 17), (32, 25), (28, 23)):
self.block(x, 21, z, 'spruce_planks')
self.lantern(x, 20, z, True)
self.frame((27, 18, 26), 'south', 'iron_helmet')
def book(self, pos: Position) -> None:
"""Store a readable open book on a lectern using modern item components."""
content = Compound({'title': String('Amberstone Ledger'), 'author': String('The Lodge Keeper'), 'generation': Int(0), 'resolved': Byte(1), 'pages': NBTList[String]([String('{"text":"Amberstone Lodge\\n\\nAutumn stores\\nIron, cedar and candle wax.\\n\\nKeep a lantern burning for the next traveller."}')])})
self.tile(pos, 'lectern', Page=Int(0), Book=Compound({'id': String('minecraft:written_book'), 'count': Int(1), 'components': Compound({'minecraft:written_book_content': content})}))
def attic(self) -> None:
"""Fit an illuminated study under the tall rafters, with a book desk and storage."""
for z in (12, 13, 14, 15):
self.block(28, 24, z, 'bookshelf')
for z in (12, 13, 14, 15):
self.container(34, 24, z, facing='west')
for x in (29, 30, 31, 32):
self.block(x, 24, 13, 'dark_oak_planks')
self.block(31, 25, 13, 'lectern', facing='south', has_book='true', powered='false')
self.book((31, 25, 13))
self.candles(29, 25, 13)
self.pot(32, 25, 13, 'fern')
self.stair(31, 24, 15, 'south', 'dark_oak')
for x in (31, 32):
for z in range(16, 23):
self.block(x, 24, z, 'green_carpet')
for x in (28, 33, 34):
self.container(x, 24, 27, 'chest')
self.container(33, 25, 27)
self.pot(33, 26, 27, 'spruce_sapling')
# Exposed tie beams carry chains and lanterns within three blocks of the floor.
for z in (16, 25):
self.beam((29, 28, z, 33, 28, z), 'x')
self.block(31, 27, z, 'chain', axis='y', waterlogged='false')
self.lantern(31, 26, z, True)
self.frame((31, 30, 11), 'south', 'compass')
for y in range(24, 27):
self.block(32, y, 27, 'ladder', facing='north', waterlogged='false')
self.beam((32, 26, 26, 34, 26, 26), 'x')
self.container(32, 27, 26)
def pine(self, cx: int, cz: int, height: int, radius: int) -> None:
"""Grow a hand-built pine with exposed stepped limbs and persistent leaf tiers."""
self.block(cx, 0, cz, 'podzol')
self.fill((cx, 1, cz, cx, height, cz), 'spruce_log', axis='y')
for dx, dz in DIRECTIONS.values():
self.block(cx + dx, 1, cz + dz, 'spruce_log', axis='x' if dx else 'z')
tiers = list(range(5, height - 1, 3))
for i, y in enumerate(tiers):
r = max(1, radius - i // 2)
for dx, dz in DIRECTIONS.values():
for length in range(1, r + 1):
xx, zz = cx + dx * length, cz + dz * length
self.block(xx, y, zz, 'spruce_log', axis='x' if dx else 'z')
self.block(xx, y + 1, zz, 'spruce_leaves', persistent='true', distance='1', waterlogged='false')
for dx, dz in DIRECTIONS.values():
xx, zz = cx + dx * (r + 1), cz + dz * (r + 1)
if self.inside(xx, y, zz) and self.get(xx, y, zz) in (None, 'minecraft:air'):
self.block(xx, y, zz, 'spruce_leaves', persistent='true', distance='1', waterlogged='false')
for dx in range(-r, r + 1):
for dz in range(-r, r + 1):
if (dx == 0 and dz == 0) or abs(dx) + abs(dz) > r + 1:
continue
if self.inside(cx + dx, y, cz + dz) and self.get(cx + dx, y, cz + dz) in (None, 'minecraft:air'):
self.block(cx + dx, y, cz + dz, 'spruce_leaves', persistent='true', distance='1', waterlogged='false')
if abs(dx) + abs(dz) <= r and (dx or dz):
self.block(cx + dx, y + 1, cz + dz, 'spruce_leaves', persistent='true', distance='1', waterlogged='false')
for y in range(height - 2, height + 2):
self.block(cx, y, cz, 'spruce_leaves', persistent='true', distance='1', waterlogged='false')
if y < height:
for dx, dz in DIRECTIONS.values():
self.block(cx + dx, y, cz + dz, 'spruce_leaves', persistent='true', distance='1', waterlogged='false')
def dormer(self) -> None:
"""Open a timber-framed clerestory in the lower roof, matching the reference."""
self.fill((18, 16, 11, 20, 19, 14), 'air')
for x in (17, 21):
self.mix((x, 16, 10, x, 19, 14), PLASTER)
self.beam((x, 16, 10, x, 19, 10))
self.beam((17, 16, 10, 21, 16, 10), 'x')
self.beam((17, 19, 10, 21, 19, 10), 'x')
for x in range(18, 21):
for y in (17, 18):
self.block(x, y, 10, 'light_gray_stained_glass')
self.windows.append((x, y, 10))
# Shallow hipped cap ties into the existing slope at the back.
for x in range(16, 23):
for z in range(9, 16):
y = 20 if x in (16, 22) or z == 9 else 21
self.block(x, y, z, 'deepslate_tiles')
if y == 20:
facing = 'east' if x == 16 else 'west' if x == 22 else 'south'
self.stair(x, y + 1, z, facing, 'deepslate_tile')
else:
self.slab(x, y + 1, z, 'deepslate_tile')
self.fill((17, 19, 15, 21, 20, 15), 'deepslate_tiles')
self.fill((17, 20, 10, 21, 20, 14), 'spruce_planks')
def landscape_detail(self) -> None:
"""Weather the terrace edge and grow ivy beside the tower entrance."""
# Broken stone toes soften the straight retaining wall without obstructing steps.
for x in (6, 7, 11, 15, 16, 20, 31, 35, 38):
self.block(x, 0, 6, 'coarse_dirt')
self.block(x, 1, 6, 'mossy_cobblestone' if x % 2 else 'cobblestone')
if x % 3:
self.slab(x, 2, 6, 'cobblestone')
for x in (8, 13, 19, 32, 37):
self.block(x, 2, 6, 'vine', south='true', north='false', east='false', west='false', up='false')
for x, z in ((7, 9), (16, 8), (34, 8), (38, 26), (12, 28), (35, 29)):
self.block(x, 3, z, 'mossy_cobblestone')
self.block(x, 4, z, 'azalea_leaves', persistent='true', distance='1', waterlogged='false')
if x % 2:
self.block(x, 5, z, 'flowering_azalea_leaves', persistent='true', distance='1', waterlogged='false')
for x, z, ys in ((25, 9, (4, 5, 7, 8, 10, 11, 12)), (26, 10, (4, 5, 6, 8, 9, 10)), (27, 10, (4, 5, 7))):
for y in ys:
if self.get(x, y, z + 1) not in (None, 'minecraft:air'):
self.block(x, y, z, 'oak_leaves', persistent='true', distance='1', waterlogged='false')
# A low bench and lantern make the rear terrace useful as well as finished.
for x in (16, 17, 18):
self.stair(x, 4, 28, 'south', 'spruce')
self.container(15, 4, 28)
self.lantern(15, 5, 28)
def reflect_x(self) -> None:
"""Orient the finished lodge so the tower is right of the front entrance."""
maximum = self.size_x - 1
blocks = list(self.placed.items())
self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x))
self.placed = {}
for (x, y, z), state in blocks:
name, _, raw = state.partition('[')
props = dict(p.split('=') for p in raw.rstrip(']').split(',') if p)
if props.get('facing') in ('east', 'west'):
props['facing'] = 'west' if props['facing'] == 'east' else 'east'
if 'hinge' in props:
props['hinge'] = 'right' if props['hinge'] == 'left' else 'left'
if 'east' in props and 'west' in props:
props['east'], props['west'] = props['west'], props['east']
self.put(maximum - x, y, z, name, **props)
tiles = {}
for (x, y, z), tile in self.tiles.items():
tile['x'] = Int(maximum - x)
tiles[(maximum - x, y, z)] = tile
self.tiles = tiles
for entity in self.entities:
entity['TileX'] = Int(maximum - int(entity['TileX']))
entity['Pos'][0] = Double(self.size_x - float(entity['Pos'][0]))
facing = int(entity['Facing'])
if facing in (4, 5):
entity['Facing'] = Byte(9 - facing)
entity['Rotation'][0] = Float((-float(entity['Rotation'][0])) % 360)
self.windows = [(maximum - x, y, z) for x, y, z in self.windows]
self.doors = [(maximum - x, y, z) for x, y, z in self.doors]
self.flights = [(maximum - x - 1, y, z, facing) for x, y, z, facing in self.flights]
def build(self) -> 'Build':
"""Generate Amberstone Lodge, its furnished rooms and landscaped setting."""
self.tiles = {}
self.entities = []
self.doors, self.windows, self.flights = [], [], []
self.terrain()
self.structure()
self.dormer()
self.stairs()
self.forge()
self.ground_rooms()
self.living_rooms()
self.bedroom()
self.attic()
self.landscape_detail()
self.pine(5, 28, 23, 4)
self.pine(3, 7, 16, 2)
# Furnishing replacements cannot retain another block type's entity data.
self.tiles = {pos: tile for pos, tile in self.tiles.items() if str(tile['id']).split(':')[1] in self.get(*pos).split('[')[0]}
for pos, kind in (((26, 10, 27), 'smoker'), ((27, 10, 27), 'furnace')):
self.tile(pos, kind, BurnTime=Short(1600), CookTime=Short(0), CookTimeTotal=Short(200), Items=NBTList[Compound]([Compound({'Slot': Byte(0), 'id': String('minecraft:potato' if kind == 'smoker' else 'raw_iron'), 'count': Int(32)}), Compound({'Slot': Byte(1), 'id': String('minecraft:coal'), 'count': Int(32)})]))
for pos, state in self.placed.items():
if state.startswith('minecraft:chiseled_bookshelf['):
stacks = [Compound({'Slot': Byte(i), 'id': String('minecraft:book'), 'count': Int(1)}) for i in (0, 2, 3, 5)]
self.tile(pos, 'chiseled_bookshelf', Items=NBTList[Compound](stacks), last_interacted_slot=Int(0))
self.reflect_x()
return self
def export(self, path: Path) -> Path:
"""Write the schematic so that two runs produce identical bytes.
Args:
path (Path): Destination .litematic path.
Returns:
Path: The path written.
"""
schematic = self.canvas.to_litematica(
name=CONFIG["name"],
author=CONFIG["author"],
description=CONFIG["description"],
minecraft_data_version=CONFIG["data_version"],
)
# The metadata otherwise records the current wall clock, which changes the bytes on every run.
schematic.metadata.time_created = 0
schematic.metadata.time_modified = 0
path.parent.mkdir(parents=True, exist_ok=True)
root = schematic.write_to_nbt()
region = root['Regions'][CONFIG['name']]
region['TileEntities'] = NBTList[Compound]([self.tiles[p] for p in sorted(self.tiles)])
region['Entities'] = NBTList[Compound](self.entities)
NBTFile(root).save(path)
return path
def verify(self, path: Path) -> Dict[str, object]:
"""Reload the written file and confirm it matches what was placed.
Args:
path (Path): Schematic to reload.
Returns:
Dict[str, object]: Size, block count and palette size of the reloaded file.
Raises:
AssertionError: When a reloaded cell differs from what was placed.
"""
loaded = load_schematic(path)
size_y, size_z, size_x = loaded.size_yzx
assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload"
ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx)
for (x, y, z), state in self.placed.items():
assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload"
return {
"size_xyz": [size_x, size_y, size_z],
"non_air_blocks": len(self.canvas._blocks),
"palette_states": len(loaded.palette),
}
def main() -> None:
"""Generate the build, export it, and verify the exported file."""
parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.")
parser.add_argument("--output", type=Path, default=Path(CONFIG["output"]))
parser.add_argument("--seed", type=int, default=CONFIG["seed"])
args = parser.parse_args()
build = Build(seed=args.seed).build()
path = build.export(args.output)
report = build.verify(path)
print(f"wrote {path}: {report}")
if __name__ == "__main__":
main()