Files
cobblemon-survival-pack/tools/make_world.py
T
claudeandClaude Opus 5.5 5d275e4e14 Phase 4: regenerating ore veins, each tier in one zone
Regenerating Ore Veins 1.4.0 (CurseForge only, MIT). tools/zone_ores.py
writes its config from world/zones.json: surface zones as boxes from the
zone grid, cave tiers by cave biome, 21 tiers including Cobblemon's
evolution stones (their natural ores are off). The world report now
allows ore only in tracked veins inside their tier's zone. Self-tests:
a mined vein regrows after its chunk unloads and after a restart (new
after_restart pass in selftest.py and quick_check.py). make_world places
showcase veins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:30:34 +01:00

125 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Build the island world template from world/world.json.
1. Prepare the local test server (build/server) with the pack, plus Chunky
(a pregeneration tool, added only for this run and removed afterwards).
2. Create the world (named after the recipe's "name") with its seed, world
type and gamerules, and pregenerate a circle of `pregen_radius` around
0,0 (the island plus a strip of sea; the sea corners inside the square
border generate when someone gets there).
3. In the same session: place the showcase ore veins (world/zones.json →
"ores", tiers with "showcase": one vein sticking out of the ground near
its zone's sample point, so players see what a regenerating vein looks
like), save, measure where the land ends (tools/world_report.py) and set
a square world border `border_margin` blocks outside the outermost coast.
4. Copy the world to build/world-template without player data, then run
the world checks and render the map (build/island-map.png).
The test server's eula.txt must already accept the Minecraft EULA (run
`tools/smoke_test.py --accept-eula` once); this script never writes it.
For a quick check after a change, use tools/quick_check.py instead.
Usage: tools/make_world.py
"""
import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mcserver import BUILD, ROOT, SERVER, Server, chunky, eula_accepted, prepare, scan_log # noqa: E402
import world_report # noqa: E402
import zone_grid # noqa: E402
from numpy import hypot as np_hypot # noqa: E402
TEMPLATE = BUILD / "world-template"
SKIP = shutil.ignore_patterns("playerdata", "advancements", "stats", "session.lock")
# A showcase sits this far from its zone's sample point (where guardians and the self-tests stand), first offset
# that is still inside the zone wins.
SHOWCASE_OFFSETS = [(16, 0), (0, 16), (-16, 0), (0, -16), (24, 24), (-24, -24)]
def place_showcases(server):
"""One hand-placed vein per showcase tier, half sunk into the ground near the zone's sample point."""
plan = json.loads((ROOT / "world/zones.json").read_text())
grid = zone_grid.load()
samples = json.loads(zone_grid.ISLAND.read_text())["samples"]
for tier, spec in plan["ores"].items():
if not spec.get("showcase"):
continue
sx, _, sz = samples[spec["zone"]]
spot = next(((sx + dx, sz + dz) for dx, dz in SHOWCASE_OFFSETS if grid.zone_at(sx + dx, sz + dz) == spec["zone"]), None)
if spot is None:
raise RuntimeError(f"no showcase spot inside {spec['zone']} near its sample point {sx},{sz}")
x, z = spot
server.command(f"forceload add {x} {z}")
reply = server.command(f"execute in minecraft:overworld positioned {x} 0 {z} positioned over world_surface "
f"positioned ~ ~-1 ~ run regenerating_ore_veins place {tier}").strip()
server.command(f"forceload remove {x} {z}")
print(f" showcase {tier} at {x},{z}: {reply[:100]}")
def border_for(world, recipe):
"""Square border around the land, `border_margin` past the outermost coast."""
data = world_report.analyse(world)
land = world_report.island_mask(data)
zs, xs = land.nonzero()
ox, oz = data["origin"]
x0, x1 = int(xs.min()) + ox, int(xs.max()) + ox + 1
z0, z1 = int(zs.min()) + oz, int(zs.max()) + oz + 1
far = float(np_hypot(xs + ox, zs + oz).max())
if far + recipe["border_margin"] > recipe["pregen_radius"]:
sys.exit(f"land reaches {far:.0f} blocks from 0,0; raise pregen_radius ({recipe['pregen_radius']}) in "
f"world/world.json to at least land + border_margin")
cx, cz = round((x0 + x1) / 2), round((z0 + z1) / 2)
size = max(x1 - x0, z1 - z0) + 2 * recipe["border_margin"]
print(f"land spans x {x0}..{x1}, z {z0}..{z1}, at most {far:.0f} blocks from 0,0; "
f"border center {cx},{cz} size {size}")
return cx, cz, size
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.parse_args()
if not eula_accepted():
sys.exit("build/server/eula.txt does not accept the Minecraft EULA yet; run "
"tools/smoke_test.py --accept-eula first if you accept it.")
recipe = json.loads((ROOT / "world/world.json").read_text())
args_file = prepare()
world = SERVER / recipe["name"]
shutil.rmtree(world, ignore_errors=True)
props = {"level-name": recipe["name"], "level-seed": recipe["seed"], "level-type": recipe["level_type"]}
with chunky(recipe), Server(args_file, props, heap="8G", stdout_name="make-world-stdout.log") as server:
print(f"server started in {server.wait_ready():.0f}s")
status = server.command("worldgen_editor status")
if "cobblesurvival_island" not in status:
raise RuntimeError(f"world did not get the island world type:\n{status}")
for rule, value in recipe.get("gamerules", {}).items():
print(f" {server.command(f'gamerule {rule} {str(value).lower()}').strip()}")
seconds = server.pregenerate(0, 0, recipe["pregen_radius"], shape="circle")
print(f"pregeneration finished in {seconds:.0f}s")
place_showcases(server)
server.command("save-all flush")
cx, cz, size = border_for(world, recipe)
print(f" {server.command(f'worldborder center {cx} {cz}').strip()}")
print(f" {server.command(f'worldborder set {size}').strip()}")
server.command("save-all flush")
server.stop()
problems = scan_log(server.log)
if problems:
print("server log problems:\n" + "\n".join(f" - {p}" for p in problems))
shutil.rmtree(TEMPLATE, ignore_errors=True)
shutil.copytree(world, TEMPLATE, ignore=SKIP)
size = sum(f.stat().st_size for f in TEMPLATE.rglob("*") if f.is_file())
print(f"world template: {TEMPLATE.relative_to(ROOT)} ({size / 1e6:.0f} MB)")
result = subprocess.run([sys.executable, "-I", str(ROOT / "tools/world_report.py"), str(TEMPLATE)])
sys.exit(result.returncode or (1 if problems else 0))
if __name__ == "__main__":
main()