# Generate Reedwater Cabin and validate a deterministic single-region Java 1.21.1 export.
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.schematic import load_schematic # noqa: E402
from mcio.nbt.nbt import File as NBTFile
from mcio.nbt.tag import Byte, Compound, Double, Float, Int, IntArray, List as NBTList, String
from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402

CONFIG = {
    "seed": 20260923,
    "size_xyz": (28, 21, 24), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Reedwater Cabin",
    "author": "generator",
    "description": "Oak stilt fisherman cabin with spruce deck, dark oak gable, furnished room, shallow pond and oak boat.",
}

# Water occupies y=1..2. Floor tops are y=7, landing top y=4.
WATER_Y = 2
FLOOR_Y = 6
WALLS = (9, 6, 23, 16) # x0, z0, x1, z1
DOORS = ((9, 7, 10), (9, 7, 11))
BOAT_POSITION = (10.25, 2.65, 21.1)
POSTS = ((9, 6), (16, 6), (23, 6), (9, 16), (16, 16), (23, 16), (9, 9), (9, 12), (23, 11))
DECK_POSTS = ((3, 5), (3, 11), (3, 17), (8, 5), (8, 17), (14, 19))
WINDOWS = tuple((x, y, z) for z in (6, 16) for x in (12, 13, 19, 20) for y in range(8, 11)) + tuple((x, y, z) for x in (9, 23) for z in (7, 8, 14, 15) for y in range(8, 11))
ROOM_BOUNDS = (10, 7, 7, 22, 11, 15)
LIGHTS = ((12, 10, 9), (19, 10, 9), (12, 10, 13), (19, 10, 13))
Position = Tuple[int, int, int]


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] = {}
        self.block_entities = []
        self.entities = []

    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 build(self) -> "Build":
        """Build a timber fishing cabin with a navigable room and a shallow pond.

        Returns:
            Build: Completed canvas.
        """
        self.pond()
        self.structure()
        self.deck()
        self.roof()
        self.furnish()
        self.plants_and_mooring()
        self.connect_fences()
        return self

    def pond(self) -> None:
        """Lay a varied silt bed, two water layers, and irregular reed banks."""
        self.texture((0, 0, 0, 27, 0, 23), ["minecraft:mud"] * 5 + ["minecraft:dirt"] * 3 + ["minecraft:rooted_dirt", "minecraft:clay"])
        self.box((0, 1, 0, 27, WATER_Y, 23), "minecraft:water", level="0")
        for x in range(28):
            for z in range(24):
                bank = z <= 1 and (x > 15 or x < 5) or x >= 26 and z < 17
                fringe = (z == 2 and x in (0, 1, 3, 18, 19, 23, 24, 26, 27)) or (x == 25 and z in (1, 2, 5, 6, 12, 13, 14))
                if bank or fringe:
                    self.put(x, 1, z, "minecraft:dirt")
                    self.put(x, 2, z, self.rng.choice(["minecraft:mud", "minecraft:mud", "minecraft:grass_block", "minecraft:rooted_dirt"]))
                elif self.rng.random() < 0.08:
                    self.put(x, 1, z, "minecraft:seagrass")

    def structure(self) -> None:
        """Raise the cabin on oak piles and build the sealed timber envelope."""
        # Intentional bands preserve timber framing rather than random wall noise.
        self.box((9, FLOOR_Y, 6, 23, FLOOR_Y, 16), "minecraft:spruce_planks")
        for x in range(10, 23):
            for z in range(7, 16):
                self.put(x, FLOOR_Y, z, "minecraft:oak_planks" if x % 4 else "minecraft:stripped_oak_log", **({"axis": "z"} if not x % 4 else {}))
        for z in (6, 16):
            self.box((9, 5, z, 23, 5, z), "minecraft:spruce_log", axis="x")
            for x in range(9, 24):
                self.box((x, 7, z, x, 11, z), "minecraft:stripped_oak_log" if x in (10, 15, 17, 22) else "minecraft:oak_planks", **({"axis": "y"} if x in (10, 15, 17, 22) else {}))
            self.box((9, 12, z, 23, 12, z), "minecraft:stripped_spruce_log", axis="x")
        for x in (9, 23):
            self.box((x, 5, 6, x, 5, 16), "minecraft:spruce_log", axis="z")
            self.box((x, 7, 6, x, 11, 16), "minecraft:oak_planks")
            self.box((x, 12, 6, x, 12, 16), "minecraft:stripped_spruce_log", axis="z")
        for x, z in POSTS:
            self.box((x, 1, z, x, 12, z), "minecraft:oak_log", axis="y")
            # Submerged footings are solid and visible through the shallow water.
            self.put(x, 0, z, "minecraft:oak_log", axis="y")
        for x, y, z in WINDOWS:
            long_wall = z in (6, 16)
            self.put(x, y, z, "minecraft:glass_pane", north=str(not long_wall).lower(), south=str(not long_wall).lower(), east=str(long_wall).lower(), west=str(long_wall).lower(), waterlogged="false")
        # Sills and shutters frame the glass without overlapping its cells.
        for z, face in ((5, "north"), (17, "south")):
            for start in (12, 19):
                for x in range(start, start + 2):
                    self.put(x, 7, z, "minecraft:spruce_stairs", facing=face, half="top", shape="straight", waterlogged="false")
                for x in (start - 1, start + 2):
                    for y in (8, 9, 10):
                        self.put(x, y, z, "minecraft:spruce_trapdoor", facing=face, half="bottom", open="true", powered="false", waterlogged="false")
        for x, face in ((8, "west"), (24, "east")):
            for start in (7, 14):
                for z in (start, start + 1):
                    self.put(x, 7, z, "minecraft:spruce_stairs", facing=face, half="top", shape="straight", waterlogged="false")
        for index, (x, y, z) in enumerate(DOORS):
            for dy, half in ((0, "lower"), (1, "upper")):
                self.put(x, y + dy, z, "minecraft:spruce_door", facing="west", half=half, hinge="left" if index == 0 else "right", open="false", powered="false")
            self.put(x, y + 2, z, "minecraft:oak_planks")
        # Full ceiling closes every roof void above the habitable room.
        self.box((10, 12, 7, 22, 12, 15), "minecraft:spruce_planks")
        for x in (12, 19):
            self.box((x, 12, 7, x, 12, 15), "minecraft:stripped_oak_log", axis="z")

    def deck(self) -> None:
        """Build the level wraparound deck, connected stairs, and low landing."""
        self.box((3, 6, 5, 8, 6, 17), "minecraft:spruce_planks")
        self.box((8, 6, 17, 14, 6, 19), "minecraft:spruce_planks")
        self.box((8, 5, 19, 14, 5, 19), "minecraft:spruce_log", axis="x")
        for z in (5, 11, 17):
            self.box((3, 5, z, 8, 5, z), "minecraft:spruce_log", axis="x")
        for x in (3, 8):
            self.box((x, 6, 5, x, 6, 17), "minecraft:stripped_spruce_log", axis="z")
        for x, z in DECK_POSTS:
            top = 6 if (x, z) == (8, 17) else 7
            self.box((x, 0, z, x, top, z), "minecraft:oak_log", axis="y")
            if top == 7:
                self.put(x, 8, z, "minecraft:spruce_slab", type="bottom", waterlogged="false")
        rails = {(3, z) for z in range(5, 18)} | {(x, 5) for x in range(3, 9)} | {(7, 17)} | {(x, 19) for x in range(8, 15)} | {(14, 17), (14, 18)}
        for x, z in sorted(rails):
            if (x, z) not in DECK_POSTS:
                self.put(x, 7, z, "minecraft:spruce_fence", waterlogged="false")
        for z, y in ((18, 5), (19, 4), (20, 3)):
            self.box((4, y, z, 6, y, z), "minecraft:spruce_stairs", facing="north", half="bottom", shape="straight", waterlogged="false")
            for x in (3, 7):
                self.box((x, 1, z, x, y, z), "minecraft:stripped_spruce_log", axis="y")
                self.put(x, y + 1, z, "minecraft:spruce_fence", waterlogged="false")
        self.box((3, 3, 21, 8, 3, 22), "minecraft:spruce_planks")
        for x, z in ((3, 21), (3, 22), (8, 21), (8, 22)):
            self.box((x, 0, z, x, 3, z), "minecraft:oak_log", axis="y")
        # Mooring bollard stands next to open water, outside the stair path.
        self.box((8, 4, 21, 8, 5, 21), "minecraft:oak_log", axis="y")
        self.put(8, 6, 21, "minecraft:spruce_slab", type="bottom", waterlogged="false")

    def roof(self) -> None:
        """Lay a low half-step dark-oak gable with deep, thick spruce verges."""
        for z in range(4, 19):
            course = 7 - abs(z - 11)
            y = 12 + course // 2
            face = "south" if z < 11 else "north"
            for x in range(6, 26):
                # Continuous plank backing closes stair and slab seams.
                self.put(x, y - 1, z, "minecraft:dark_oak_planks")
                wood = "spruce" if x in (6, 25) else "dark_oak"
                if z == 11:
                    self.put(x, y, z, f"minecraft:{wood}_planks")
                    self.put(x, y + 1, z, f"minecraft:{wood}_slab", type="bottom", waterlogged="false")
                elif course % 2:
                    self.put(x, y, z, f"minecraft:{wood}_stairs", facing=face, half="bottom", shape="straight", waterlogged="false")
                else:
                    self.put(x, y, z, f"minecraft:{wood}_slab", type="bottom", waterlogged="false")
            for x in (9, 23):
                if y > 12:
                    self.box((x, 12, z, x, y - 1, z), "minecraft:oak_planks")
            # A second stepped verge underneath makes the gable ends substantial.
            for x in (6, 25):
                self.put(x, y - 1, z, "minecraft:spruce_planks")
        for x in (8, 24):
            self.box((x, 11, 5, x, 11, 17), "minecraft:oak_log", axis="z")
        for z in (5, 17):
            self.box((7, 11, z, 24, 11, z), "minecraft:oak_log", axis="x")
        for x in (6, 25):
            self.box((x, 16, 11, x, 17, 11), "minecraft:spruce_planks")
            self.put(x, 18, 11, "minecraft:spruce_slab", type="bottom", waterlogged="false")
        # Gable timber and knee braces emphasize the sheltered entrance.
        self.box((8, 12, 11, 8, 14, 11), "minecraft:stripped_oak_log", axis="y")
        for z, facing in ((6, "south"), (16, "north")):
            self.put(8, 10, z, "minecraft:spruce_stairs", facing=facing, half="top", shape="straight", waterlogged="false")
        for x in (7, 24):
            self.put(x, 11, 18, "minecraft:chain", axis="y", waterlogged="false")
            self.put(x, 10, 18, "minecraft:lantern", hanging="true", waterlogged="false")

    def inventory(self, pos: Position, block_id: str, items: Tuple[Tuple[str, int], ...]) -> None:
        """Store actual Java 1.21.1 gear inside a placed container.

        Args:
            pos (Position): Container XYZ coordinate.
            block_id (str): Block entity ID.
            items (Tuple[Tuple[str, int], ...]): Item names and counts in slot order.
        """
        x, y, z = pos
        self.block_entities.append(Compound({"id": String(f"minecraft:{block_id}"), "x": Int(x), "y": Int(y), "z": Int(z), "Items": NBTList[Compound]([Compound({"Slot": Byte(slot), "id": String(f"minecraft:{name}"), "count": Int(count)}) for slot, (name, count) in enumerate(items)])}))

    def furnish(self) -> None:
        """Furnish the room while keeping broad circulation routes and clear windows."""
        for x, y, z in LIGHTS:
            self.put(x, y + 1, z, "minecraft:chain", axis="y", waterlogged="false")
            self.put(x, y, z, "minecraft:lantern", hanging="true", waterlogged="false")
        # A smoker provides cooking without a spreading open fire in a timber house.
        for x, block in ((15, "smoker"), (16, "crafting_table"), (17, "water_cauldron"), (18, "barrel")):
            props = {"facing": "south", "lit": "false"} if block == "smoker" else {"level": "3"} if block == "water_cauldron" else {"facing": "south", "open": "false"} if block == "barrel" else {}
            self.put(x, 7, 7, f"minecraft:{block}", **props)
        self.inventory((15, 7, 7), "smoker", (("cod", 8), ("charcoal", 12)))
        self.inventory((18, 7, 7), "barrel", (("bread", 12), ("bowl", 3), ("cooked_salmon", 8)))
        for x, y, z in ((10, 7, 12), (10, 7, 13), (10, 8, 12), (22, 7, 9)):
            self.put(x, y, z, "minecraft:barrel", facing="south" if x < 20 else "west", open="false")
            self.inventory((x, y, z), "barrel", (("fishing_rod", 1), ("string", 16), ("cod", 6)))
        self.put(22, 8, 9, "minecraft:potted_spruce_sapling")
        for z, part in ((9, "head"), (10, "foot")):
            self.put(21, 7, z, "minecraft:green_bed", facing="north", part=part, occupied="false")
        # Shelving remains above two-block headroom and away from the glazing.
        for x in (15, 16, 17, 18):
            self.put(x, 10, 7, "minecraft:spruce_slab", type="top", waterlogged="false")
        self.put(16, 11, 7, "minecraft:flower_pot")
        for x in (19, 20):
            self.put(x, 7, 14, "minecraft:oak_fence", waterlogged="false")
            self.put(x, 8, 14, "minecraft:spruce_slab", type="top", waterlogged="false")
        self.put(19, 9, 14, "minecraft:potted_blue_orchid")
        for x, face in ((18, "east"), (21, "west")):
            self.put(x, 7, 14, "minecraft:spruce_stairs", facing=face, half="bottom", shape="straight", waterlogged="false")
        for x in range(13, 17):
            for z in range(10, 13):
                self.put(x, 7, z, "minecraft:brown_carpet" if x in (13, 16) or z in (10, 12) else "minecraft:green_carpet")
        self.put(4, 7, 6, "minecraft:barrel", facing="south", open="false")
        self.inventory((4, 7, 6), "barrel", (("fishing_rod", 1), ("lead", 2), ("salmon", 10)))
        self.put(6, 7, 6, "minecraft:chest", facing="south", type="single", waterlogged="false")
        self.inventory((6, 7, 6), "chest", (("oak_boat", 1), ("leather_boots", 1), ("kelp", 20)))
        # Bench under the entrance overhang, backed by the cabin wall.
        for z in (14, 15):
            self.put(5, 7, z, "minecraft:spruce_stairs", facing="west", half="bottom", shape="straight", waterlogged="false")

    def plants_and_mooring(self) -> None:
        """Add a supported wall bush, living reeds, lily pads, and the actual boat."""
        self.put(16, 7, 17, "minecraft:spruce_planks")
        self.put(16, 7, 18, "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false")
        for x, y, z in ((16, 8, 17), (16, 9, 17), (16, 10, 17), (15, 9, 17), (17, 9, 17), (16, 9, 18), (15, 10, 17), (17, 10, 17)):
            self.put(x, y, z, self.rng.choice(("minecraft:oak_leaves", "minecraft:oak_leaves", "minecraft:azalea_leaves")), persistent="true", distance="1", waterlogged="false")
        # Reeds have real dirt substrate and an adjacent source-water cell.
        for x, z in ((1, 4), (1, 5), (2, 5), (1, 17), (2, 18), (1, 20), (18, 22), (19, 22), (21, 21), (24, 19), (25, 18), (24, 3), (20, 2), (6, 2)):
            self.put(x, 1, z, "minecraft:dirt")
            self.put(x, 2, z, "minecraft:mud")
            for y in range(3, self.rng.choice((5, 5, 6))):
                self.put(x, y, z, "minecraft:sugar_cane", age="0")
        for x, z in ((2, 9), (1, 13), (11, 20), (14, 22), (17, 19), (20, 18), (23, 21), (26, 20), (15, 3), (9, 2), (4, 2), (26, 22)):
            if self.get(x, 2, z) == "minecraft:water[level=0]":
                self.put(x, 3, z, "minecraft:lily_pad")
        # Boats are entities in Java 1.21.1. The geometry-only renderer omits them.
        self.entities.append(Compound({
            "id": String("minecraft:boat"),
            "Type": String("oak"),
            "Pos": NBTList[Double]([Double(v) for v in BOAT_POSITION]),
            "Motion": NBTList[Double]([Double(0), Double(0), Double(0)]),
            "Rotation": NBTList[Float]([Float(90), Float(0)]),
            "UUID": IntArray([20260923, 1953066084, 1466000227, 1]),
            "Invulnerable": Byte(0),
            "OnGround": Byte(0),
        }))

    def connect_fences(self) -> None:
        """Set fence connections explicitly because schematics do not update states."""
        for (x, y, z), state in list(self.placed.items()):
            if state.split("[")[0] not in ("minecraft:spruce_fence", "minecraft:oak_fence"):
                continue
            props = {"waterlogged": "false"}
            for face, dx, dz in (("east", 1, 0), ("west", -1, 0), ("south", 0, 1), ("north", 0, -1)):
                neighbor = self.get(x + dx, y, z + dz) or "minecraft:air"
                props[face] = str(any(kind in neighbor for kind in ("_fence", "_log", "_planks"))).lower()
            self.put(x, y, z, state.split("[")[0], **props)

    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.block_entities)
        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"
        root = NBTFile.load_regardless_of_gzipped(path)
        region = root["Regions"][CONFIG["name"]]
        assert list(region["Entities"]) == self.entities, "entity data changed on reload"
        assert list(region["TileEntities"]) == self.block_entities, "block entity data changed on reload"
        return {
            "size_xyz": [size_x, size_y, size_z],
            "placed_blocks": len(self.placed),
            "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}")
    print(f"verified {len(build.block_entities)} stocked block entities and {len(build.entities)} oak boat entity")


if __name__ == "__main__":
    main()
