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>
131 lines
6.2 KiB
Python
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()
|