Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus the Kotlin companion mod (cobblesurvival) and its built-in data pack. - Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and export tooling, Prism instance sync, test server with smoke checks. - Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26), Adrian's biome list, no natural ore, structures without ore or vanilla mobs, no Nether portals, world template builder and world report. - Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap fixes), Revival Charm, expensive Healing Machine, Lootr refreshing loot, in-game self-tests on Minecraft's GameTest framework. - Phase 3 so far: 14 zones with their own spawn pools and level bands (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition), Cold Sweat heat and cold as gates with boss-drop insulation recipes, spawn survey and hazard self-tests in tools/quick_check.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4.6 KiB
Python
98 lines
4.6 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: 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
|
|
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 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")
|
|
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()
|