Files
cobblemon-survival-pack/tools/make_world.py
T
claudeandClaude Opus 5.5 6361636da7 WIP: ore nodes, catching rules, bee nest fix, smaller Deep Dark
Done and checked: surface and cave-floor ore nodes (companion mod
ore/OreNodes.kt), bigger and rarer underground veins; template with nodes
passed the world check and the island quick check (node self-tests).

Built, not yet tested: catching rules (catch rate falls with the level
gap to the trainer's strongest Pokemon, failed catch enrages, strong wild
Pokemon hunt weaker trainers; catching/, Fight or Flight config), bee
nests emptied when their bee is turned away.

In progress: the Deep Dark no longer fills the deepslate layer (split by
temperature: cold = Deep Dark, hot = basalt deltas, warm = ordinary caves;
world/biomes.json), zone layout regenerated; cave zone types added to
world/zones.json but tools/zone_spawns.py doesn't use them yet; template
not rebuilt for the new caves.

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

131 lines
6.2 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: load the circle again row by row until Regenerating
Ore Veins has put its veins and the companion mod its ore nodes into every
chunk (both only fill a new chunk while it is loaded, a few a tick, and
Chunky is faster), 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 math
import re
import shutil
import subprocess
import sys
import time
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
from numpy import hypot as np_hypot # noqa: E402
TEMPLATE = BUILD / "world-template"
SKIP = shutil.ignore_patterns("playerdata", "advancements", "stats", "session.lock")
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 fill_veins(server, radius):
"""Keep each chunk row of the pregenerated circle loaded until the vein mod has filled it; returns chunks left."""
# A forced row also fully loads the two chunks around it (some of them new, past the circle's edge), so the wait
# counts the loaded, unfilled chunks in the row and two chunks around it.
started, left, rows = time.time(), 0, radius // 16 + 1
for cz in range(-rows, rows + 1):
half = math.isqrt(max(0, rows * rows - cz * cz))
area = f"{-half * 16} {cz * 16} {half * 16 + 15} {cz * 16 + 15}"
around = f"{-half * 16 - 32} {cz * 16 - 32} {half * 16 + 47} {cz * 16 + 47}"
server.command(f"forceload add {area}")
deadline = time.time() + 60
while True:
reply = server.command(f"cobblesurvival veins unfilled {around}")
match = re.search(r"unfilled chunks: (\d+)", reply)
if not match:
raise RuntimeError(f"veins unfilled: unexpected reply {reply!r}")
unfilled = int(match[1])
if unfilled == 0 or time.time() > deadline:
break
time.sleep(0.5)
server.command(f"forceload remove {area}")
left += unfilled
print(f"veins filled in {time.time() - started:.0f}s" + (f"; {left} chunks left unfilled" if left else ""))
return left
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")
fill_veins(server, recipe["pregen_radius"])
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()