Ore veins: make_world.py loads the island again row by row until Regenerating Ore Veins has filled every chunk (/cobblesurvival veins unfilled), cave tiers sit at their biome's depths, small zones get denser veins, showcase veins are full size. The world report checks surface tiers against the zone grid, cave tiers within two biome cells of their cave, and fails on unfilled chunks; the template passes (37,689 chunks). Drops: tools/drop_audit.py checks every creature drop a recipe or brewing needs (docs/pokemon-drops.md). Blaze rods, ghast tears, breeze rods, shulker shells and nether stars now drop from Pokemon through species additions (self-test drop_additions_apply). The field guide counts vanilla mobs as absent, applies species additions and reads the veins as sources. Cobblebase tried (loads, self-tests pass) and left out until Adrian decides its scope. Phase 4 checklist with the open questions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
7.7 KiB
Python
157 lines
7.7 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 veins into every chunk (it only fills a new chunk while
|
|
it is loaded, 8 a tick, and Chunky is faster), 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 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
|
|
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 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 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}_showcase").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")
|
|
fill_veins(server, recipe["pregen_radius"])
|
|
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()
|