Skip to content

simulatecraft.minecraft.world_settings

simulatecraft.minecraft.world_settings

Persistent world settings: rules, boundaries, and imported Minecraft worlds.

Stored at data/world_settings.json (repo root). Applied to the live server via RCON gamerules / time commands. World folders live under data/minecraft/.

CornerPin

Bases: BaseModel

One world XZ corner of a square play border.

WorldBoundaries

Bases: BaseModel

Square play area on the map (axis-aligned).

Drawn as a square on the viewer; physical border is vanilla worldborder. Soft/hard enforce uses the same box.

WorldRules

Bases: BaseModel

Server gamerules / time behaviour for simulations.

WorldImportInfo

Bases: BaseModel

Metadata about the active imported / mounted world.

repo_root

repo_root() -> Path

Locate the SimulateCraft repo root (has docker-compose.yml).

Source code in src/simulatecraft/minecraft/world_settings.py
def repo_root() -> Path:
    """Locate the SimulateCraft repo root (has docker-compose.yml)."""
    here = Path(__file__).resolve()
    for path in [here, *here.parents]:
        if (path / "docker-compose.yml").exists() and (path / "pyproject.toml").exists():
            return path
    return Path.cwd()

barrier_wall_commands

barrier_wall_commands(*, min_x: int, max_x: int, min_z: int, max_z: int, y_min: int, y_max: int, block: str = 'barrier') -> list[str]

Four vertical walls around an AABB (legacy fill clear only).

Source code in src/simulatecraft/minecraft/world_settings.py
def barrier_wall_commands(
    *,
    min_x: int,
    max_x: int,
    min_z: int,
    max_z: int,
    y_min: int,
    y_max: int,
    block: str = "barrier",
) -> list[str]:
    """Four vertical walls around an AABB (legacy fill clear only)."""
    if min_x > max_x:
        min_x, max_x = max_x, min_x
    if min_z > max_z:
        min_z, max_z = max_z, min_z
    if y_min > y_max:
        y_min, y_max = y_max, y_min
    cmds: list[str] = []
    cmds.extend(
        _chunked_wall_fills(
            x1=min_x, z1=min_z, x2=max_x, z2=min_z, y_min=y_min, y_max=y_max, block=block
        )
    )
    cmds.extend(
        _chunked_wall_fills(
            x1=min_x, z1=max_z, x2=max_x, z2=max_z, y_min=y_min, y_max=y_max, block=block
        )
    )
    cmds.extend(
        _chunked_wall_fills(
            x1=min_x, z1=min_z, x2=min_x, z2=max_z, y_min=y_min, y_max=y_max, block=block
        )
    )
    cmds.extend(
        _chunked_wall_fills(
            x1=max_x, z1=min_z, x2=max_x, z2=max_z, y_min=y_min, y_max=y_max, block=block
        )
    )
    return cmds

barrier_clear_commands

barrier_clear_commands(bounds: WorldBoundaries) -> list[str]

Best-effort clear of legacy fill-based barrier walls (pre-worldborder).

Per-cell air fills are intentionally NOT used — they are hundreds of RCON commands and extremely slow. We only clear old AABB rectangle walls (≤4 chunked fills). Leftover diagonal fill barriers from an older build may remain until removed manually or the world is reset.

Source code in src/simulatecraft/minecraft/world_settings.py
def barrier_clear_commands(bounds: WorldBoundaries) -> list[str]:
    """Best-effort clear of *legacy* fill-based barrier walls (pre-worldborder).

    Per-cell air fills are intentionally NOT used — they are hundreds of RCON
    commands and extremely slow. We only clear old AABB rectangle walls (≤4
    chunked fills). Leftover diagonal fill barriers from an older build may
    remain until removed manually or the world is reset.
    """
    cmds: list[str] = []
    if (
        bounds.last_barrier_min_x is not None
        and bounds.last_barrier_max_x is not None
        and bounds.last_barrier_min_z is not None
        and bounds.last_barrier_max_z is not None
        and bounds.last_barrier_y_min is not None
        and bounds.last_barrier_y_max is not None
    ):
        cmds.extend(
            barrier_wall_commands(
                min_x=int(bounds.last_barrier_min_x),
                max_x=int(bounds.last_barrier_max_x),
                min_z=int(bounds.last_barrier_min_z),
                max_z=int(bounds.last_barrier_max_z),
                y_min=bounds.last_barrier_y_min,
                y_max=bounds.last_barrier_y_max,
                block="air",
            )
        )
    elif bounds.last_barrier_vertices and bounds.last_barrier_y_min is not None:
        # Approximate clear: AABB of the old polygon edges only (4 walls).
        xs = [v.x for v in bounds.last_barrier_vertices]
        zs = [v.z for v in bounds.last_barrier_vertices]
        y_max = bounds.last_barrier_y_max
        if y_max is None:
            y_max = bounds.last_barrier_y_min
        cmds.extend(
            barrier_wall_commands(
                min_x=int(round(min(xs))),
                max_x=int(round(max(xs))),
                min_z=int(round(min(zs))),
                max_z=int(round(max(zs))),
                y_min=bounds.last_barrier_y_min,
                y_max=y_max,
                block="air",
            )
        )
    return cmds

worldborder_commands_for_aabb

worldborder_commands_for_aabb(*, min_x: float, max_x: float, min_z: float, max_z: float) -> list[str]

Vanilla worldborder covering the AABB (square; diameter = longer side).

Source code in src/simulatecraft/minecraft/world_settings.py
def worldborder_commands_for_aabb(
    *,
    min_x: float,
    max_x: float,
    min_z: float,
    max_z: float,
) -> list[str]:
    """Vanilla worldborder covering the AABB (square; diameter = longer side)."""
    if min_x > max_x:
        min_x, max_x = max_x, min_x
    if min_z > max_z:
        min_z, max_z = max_z, min_z
    cx = (min_x + max_x) / 2.0
    cz = (min_z + max_z) / 2.0
    # Inclusive block span; worldborder size is the full width of the square.
    size = max(max_x - min_x, max_z - min_z) + 1.0
    size = max(1.0, size)
    return [
        f"worldborder center {cx:.3f} {cz:.3f}",
        f"worldborder set {size:.0f}",
        "worldborder damage amount 0.2",
        "worldborder damage buffer 2",
        "worldborder warning distance 5",
    ]

worldborder_disable_commands

worldborder_disable_commands() -> list[str]

Reset to an effectively infinite border (vanilla default scale).

Source code in src/simulatecraft/minecraft/world_settings.py
def worldborder_disable_commands() -> list[str]:
    """Reset to an effectively infinite border (vanilla default scale)."""
    return ["worldborder set 59999968"]

rcon_commands_for_boundaries

rcon_commands_for_boundaries(bounds: WorldBoundaries, *, previous: WorldBoundaries | None = None) -> list[str]

Apply play area via worldborder (few cmds) — not per-block fills.

Optionally clears legacy fill-based walls from older SimulateCraft builds.

Source code in src/simulatecraft/minecraft/world_settings.py
def rcon_commands_for_boundaries(
    bounds: WorldBoundaries,
    *,
    previous: WorldBoundaries | None = None,
) -> list[str]:
    """Apply play area via ``worldborder`` (few cmds) — not per-block fills.

    Optionally clears legacy fill-based walls from older SimulateCraft builds.
    """
    cmds: list[str] = []
    prev = previous or bounds
    if prev.barriers_built and (prev.last_barrier_vertices or prev.last_barrier_min_x is not None):
        # Only cheap AABB air clears — never hundreds of per-cell fills.
        cmds.extend(barrier_clear_commands(prev))

    if bounds.enabled:
        cmds.extend(
            worldborder_commands_for_aabb(
                min_x=bounds.min_x,
                max_x=bounds.max_x,
                min_z=bounds.min_z,
                max_z=bounds.max_z,
            )
        )
    else:
        cmds.extend(worldborder_disable_commands())
    return cmds

apply_boundaries_via_rcon

apply_boundaries_via_rcon(bounds: WorldBoundaries | None = None, *, previous: WorldBoundaries | None = None) -> tuple[list[str], WorldBoundaries]

Apply worldborder (and optional legacy clear); return results + metadata.

Source code in src/simulatecraft/minecraft/world_settings.py
def apply_boundaries_via_rcon(
    bounds: WorldBoundaries | None = None,
    *,
    previous: WorldBoundaries | None = None,
) -> tuple[list[str], WorldBoundaries]:
    """Apply worldborder (and optional legacy clear); return results + metadata."""
    from .rcon import run_commands

    settings = load_settings()
    bounds = bounds or settings.boundaries
    previous = previous or settings.boundaries
    cmds = rcon_commands_for_boundaries(bounds, previous=previous)
    results: list[str] = []
    if cmds:
        results = run_commands(cmds)
    if bounds.enabled:
        bounds.barriers_built = True
        bounds.place_barriers = True
        # Track AABB used for worldborder (and for cheap legacy clear next time).
        bounds.last_barrier_vertices = []
        bounds.last_barrier_min_x = float(int(round(bounds.min_x)))
        bounds.last_barrier_max_x = float(int(round(bounds.max_x)))
        bounds.last_barrier_min_z = float(int(round(bounds.min_z)))
        bounds.last_barrier_max_z = float(int(round(bounds.max_z)))
        bounds.last_barrier_y_min = None
        bounds.last_barrier_y_max = None
    else:
        bounds.barriers_built = False
        bounds.place_barriers = True
        bounds.last_barrier_vertices = []
        bounds.last_barrier_y_min = None
        bounds.last_barrier_y_max = None
        bounds.last_barrier_min_x = None
        bounds.last_barrier_max_x = None
        bounds.last_barrier_min_z = None
        bounds.last_barrier_max_z = None
    return results, bounds

rcon_commands_for_rules

rcon_commands_for_rules(rules: WorldRules) -> list[str]

Build RCON commands that apply WorldRules to a live server.

Source code in src/simulatecraft/minecraft/world_settings.py
def rcon_commands_for_rules(rules: WorldRules) -> list[str]:
    """Build RCON commands that apply WorldRules to a live server."""
    cmds: list[str] = []
    cycle = "true" if rules.effective_daylight_cycle() else "false"
    cmds.append(f"gamerule doDaylightCycle {cycle}")
    cmds.append(f"gamerule doWeatherCycle {'true' if rules.weather_cycle else 'false'}")
    cmds.append(f"gamerule mobGriefing {'true' if rules.mob_griefing else 'false'}")
    cmds.append(f"gamerule keepInventory {'true' if rules.keep_inventory else 'false'}")
    cmds.append(f"gamerule doMobSpawning {'true' if rules.do_mob_spawning else 'false'}")
    cmds.append(f"gamerule doFireTick {'true' if rules.fire_spreads else 'false'}")
    # Peaceful removes / prevents hostile mobs while still allowing passive animals.
    cmds.append(f"difficulty {'easy' if rules.hostile_mobs else 'peaceful'}")

    if not rules.allow_night:
        cmds.append("time set day")
    elif rules.time_of_day != "keep":
        cmds.append(TIME_PRESET_COMMANDS[rules.time_of_day])

    if rules.clear_weather:
        cmds.append("weather clear 999999")

    if rules.day_length_minutes is not None and rules.allow_night:
        # Manual cycle: start at day; advancement happens in tick loop.
        cmds.append("time set day")

    return cmds

apply_rules_via_rcon

apply_rules_via_rcon(rules: WorldRules | None = None) -> list[str]

Push current (or given) rules to Minecraft over RCON.

Source code in src/simulatecraft/minecraft/world_settings.py
def apply_rules_via_rcon(rules: WorldRules | None = None) -> list[str]:
    """Push current (or given) rules to Minecraft over RCON."""
    from .rcon import run_commands

    rules = rules or load_settings().rules
    cmds = rcon_commands_for_rules(rules)
    return run_commands(cmds)

ticks_to_add_per_second

ticks_to_add_per_second(day_length_minutes: float) -> float

How many Minecraft time ticks to add each real second for a custom day length.

Source code in src/simulatecraft/minecraft/world_settings.py
def ticks_to_add_per_second(day_length_minutes: float) -> float:
    """How many Minecraft time ticks to add each real second for a custom day length."""
    # Full day = 24000 ticks. Vanilla ≈ 20 real minutes.
    seconds = max(30.0, day_length_minutes * 60.0)
    return 24000.0 / seconds

find_level_dat

find_level_dat(root: Path) -> Path | None

Return path to level.dat under root (supports nested single folder).

Source code in src/simulatecraft/minecraft/world_settings.py
def find_level_dat(root: Path) -> Path | None:
    """Return path to level.dat under root (supports nested single folder)."""
    direct = root / "level.dat"
    if direct.is_file():
        return direct
    world_sub = root / "world" / "level.dat"
    if world_sub.is_file():
        return world_sub
    # One nested directory that contains level.dat
    if root.is_dir():
        children = [p for p in root.iterdir() if p.is_dir() and not p.name.startswith(".")]
        if len(children) == 1 and (children[0] / "level.dat").is_file():
            return children[0] / "level.dat"
        for child in children:
            if (child / "level.dat").is_file():
                return child / "level.dat"
    return None

import_world_zip

import_world_zip(zip_path: Path, *, label: str | None = None) -> WorldSettings

Extract a Minecraft world zip into data/minecraft/world and update settings.

The server must be restarted (docker compose down/up) for the new world to load.

Source code in src/simulatecraft/minecraft/world_settings.py
def import_world_zip(zip_path: Path, *, label: str | None = None) -> WorldSettings:
    """Extract a Minecraft world zip into ``data/minecraft/world`` and update settings.

    The server must be restarted (docker compose down/up) for the new world to load.
    """
    if not zip_path.is_file():
        raise FileNotFoundError(f"World zip not found: {zip_path}")

    staging = data_dir() / "_import_staging"
    if staging.exists():
        shutil.rmtree(staging)
    staging.mkdir(parents=True)

    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(staging)

    level = find_level_dat(staging)
    if level is None:
        shutil.rmtree(staging, ignore_errors=True)
        raise ValueError(
            "No level.dat found in zip. Export a Minecraft Java world folder "
            "(the folder that contains level.dat, region/, data/, …)."
        )

    world_src = level.parent
    dest = minecraft_data_dir() / "world"
    if dest.exists():
        shutil.rmtree(dest)
    shutil.copytree(world_src, dest)
    shutil.rmtree(staging, ignore_errors=True)

    settings = load_settings()
    settings.world = WorldImportInfo(
        source="imported",
        label=label or zip_path.stem,
        level_name="world",
        notes="Restart Minecraft (docker compose down && docker compose up -d) to load this world.",
    )
    save_settings(settings)
    return settings

import_world_folder

import_world_folder(folder: Path, *, label: str | None = None) -> WorldSettings

Copy an existing Java world folder into data/minecraft/world.

Source code in src/simulatecraft/minecraft/world_settings.py
def import_world_folder(folder: Path, *, label: str | None = None) -> WorldSettings:
    """Copy an existing Java world folder into ``data/minecraft/world``."""
    level = find_level_dat(folder)
    if level is None:
        raise ValueError(f"No level.dat under {folder}")
    world_src = level.parent
    dest = minecraft_data_dir() / "world"
    if dest.exists():
        shutil.rmtree(dest)
    shutil.copytree(world_src, dest)

    settings = load_settings()
    settings.world = WorldImportInfo(
        source="imported",
        label=label or folder.name,
        level_name="world",
        notes="Restart Minecraft to load this world.",
    )
    save_settings(settings)
    return settings

prepare_random_world

prepare_random_world(*, seed: int | None = None) -> tuple[WorldSettings, int]

Wipe data/minecraft/world so Docker generates a new world with seed.

Source code in src/simulatecraft/minecraft/world_settings.py
def prepare_random_world(*, seed: int | None = None) -> tuple[WorldSettings, int]:
    """Wipe ``data/minecraft/world`` so Docker generates a new world with ``seed``."""
    import random

    chosen = int(seed) if seed is not None else random.randint(0, 2_147_483_647)
    dest = minecraft_data_dir() / "world"
    if dest.exists():
        shutil.rmtree(dest)
    settings = load_settings()
    settings.world = WorldImportInfo(
        source="default",
        label=f"Random world (seed {chosen})",
        level_name="world",
        notes=f"Generated with SEED={chosen}",
    )
    save_settings(settings)
    return settings, chosen