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>
139 lines
6.9 KiB
Python
139 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Fast check after a change: one server boot, a sampled world, no full template.
|
|
|
|
1. Prepare the test server (mods, dependency check) and boot a fresh world
|
|
"quick-check" with the recipe's seed, world type and gamerules.
|
|
2. Run the smoke checks (tools/smoke_checks.json) and the companion mod's
|
|
in-game tests (`/cobblesurvival selftest`, e.g. the permadeath matrix; they
|
|
play on a platform high above spawn).
|
|
3. `/locate` every structure in world/world.json → quick_check.absent_structures
|
|
and fail if one is found (villages, ocean ruins, ... must not generate).
|
|
4. `/locate` the sample structures (quick_check.structures) and pregenerate a
|
|
square of structure_radius around each, plus spawn_radius around spawn.
|
|
5. Force-load the chunks around the sampled village for a few seconds, as a
|
|
nearby player would, so Lootr converts its loot chests.
|
|
6. Stop, scan the server log, boot the same world again and run the second
|
|
halves of the self-tests that span a restart (`selftest run after_restart`,
|
|
e.g. a mined ore vein regrows), stop and scan that log too.
|
|
7. Run tools/world_report.py --sample on the sampled world: ore only in
|
|
regenerating veins inside their tier's zone, no vanilla mobs or mob sources,
|
|
only listed biomes, gamerules as in the recipe, and no vanilla loot chest
|
|
left near the village.
|
|
|
|
The border, strongholds-inside-border and the map need the whole island:
|
|
tools/make_world.py checks those when the template is rebuilt.
|
|
|
|
Usage: tools/quick_check.py [--accept-eula] (run tools/build.py first)
|
|
"""
|
|
import argparse
|
|
import json
|
|
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 ROOT, SELFTEST_JVM_ARGS, SERVER, Server, chunky, eula_accepted, prepare, scan_log # noqa: E402
|
|
|
|
LEVEL = "quick-check"
|
|
FOUND = re.compile(r"is at \[(-?\d+), [^,\]]+, (-?\d+)\]")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--accept-eula", action="store_true", help="write eula=true for the test server")
|
|
args = parser.parse_args()
|
|
if args.accept_eula:
|
|
(SERVER / "eula.txt").parent.mkdir(parents=True, exist_ok=True)
|
|
(SERVER / "eula.txt").write_text("eula=true\n")
|
|
elif not eula_accepted():
|
|
sys.exit("build/server/eula.txt does not accept the Minecraft EULA yet; pass --accept-eula if you accept it.")
|
|
|
|
started = time.time()
|
|
recipe = json.loads((ROOT / "world/world.json").read_text())
|
|
sample = recipe["quick_check"]
|
|
args_file = prepare()
|
|
shutil.rmtree(SERVER / LEVEL, ignore_errors=True)
|
|
props = {"level-name": LEVEL, "level-seed": recipe["seed"], "level-type": recipe["level_type"]}
|
|
failures, village = [], None
|
|
with chunky(recipe), Server(args_file, props, heap="8G", stdout_name="quick-check-stdout.log",
|
|
jvm_args=SELFTEST_JVM_ARGS) as server:
|
|
try:
|
|
print(f"server started in {server.wait_ready():.0f}s")
|
|
for rule, value in recipe.get("gamerules", {}).items():
|
|
server.command(f"gamerule {rule} {str(value).lower()}")
|
|
failures += server.run_checks()
|
|
failures += server.run_selftest()
|
|
|
|
for structure in sample["absent_structures"]:
|
|
reply = server.command(f"locate structure {structure}").strip()
|
|
found = FOUND.search(reply)
|
|
print(f" [{'FAIL' if found else 'ok'}] {structure} absent: {reply[:120]!r}")
|
|
if found:
|
|
failures.append(f"{structure} generates but should not: {reply}")
|
|
|
|
areas = [("spawn", 0, 0, sample["spawn_radius"])]
|
|
for structure in sample["structures"]:
|
|
reply = server.command(f"locate structure {structure}").strip()
|
|
found = FOUND.search(reply)
|
|
if not found:
|
|
print(f" note: {structure} not found near spawn, not sampled ({reply[:80]!r})")
|
|
continue
|
|
areas.append((structure, int(found[1]), int(found[2]), sample["structure_radius"]))
|
|
for name, x, z, radius in areas:
|
|
seconds = server.pregenerate(x, z, radius, report_every=0)
|
|
print(f" sampled {name} at {x},{z} (radius {radius}) in {seconds:.0f}s")
|
|
village = next(((x, z, r) for name, x, z, r in areas if name == "#minecraft:village"), None)
|
|
if village:
|
|
x, z, r = village
|
|
server.command(f"forceload add {x - r} {z - r} {x + r} {z + r}")
|
|
time.sleep(5)
|
|
server.command(f"forceload remove {x - r} {z - r} {x + r} {z + r}")
|
|
server.command("save-all flush")
|
|
server.stop()
|
|
except Exception as e: # report; the context manager still kills the process
|
|
failures.append(f"{type(e).__name__}: {e}")
|
|
|
|
failures += [f"log: {line}" for line in scan_log(server.log)]
|
|
failures += restart_selftest(args_file, props)
|
|
report = subprocess.run([sys.executable, "-I", str(ROOT / "tools/world_report.py"), str(SERVER / LEVEL),
|
|
"--sample", "--json", str(ROOT / "build/quick-check-report.json")])
|
|
if report.returncode:
|
|
failures.append("world report failed (see above)")
|
|
if village:
|
|
# Lootr only converts a chest once the 5x5 chunks around it are loaded, so check the inner part.
|
|
x, z, r = village
|
|
inner = r - 32
|
|
loot = json.loads((ROOT / "build/quick-check-report.json").read_text())["loot_containers"]
|
|
left = [p for p in loot["unconverted_at"] if abs(p[0] - x) <= inner and abs(p[2] - z) <= inner]
|
|
print(f" loot containers: {loot['lootr']} Lootr, {len(left)} vanilla left near the village")
|
|
if left:
|
|
failures.append(f"Lootr did not convert {len(left)} loot chests near the village: {left[:5]}")
|
|
print(f"\nquick check took {time.time() - started:.0f}s")
|
|
if failures:
|
|
print("QUICK CHECK FAILED")
|
|
print("\n".join(f" - {f}" for f in failures))
|
|
sys.exit(1)
|
|
print("QUICK CHECK PASSED")
|
|
|
|
|
|
def restart_selftest(args_file, props):
|
|
"""Boot the quick-check world again and run the self-tests' after-restart halves; returns the failures."""
|
|
failures = []
|
|
with Server(args_file, props, heap="8G", stdout_name="quick-check-restart-stdout.log",
|
|
jvm_args=SELFTEST_JVM_ARGS) as server:
|
|
try:
|
|
print(f"server restarted in {server.wait_ready():.0f}s")
|
|
failures += server.run_selftest(after_restart=True)
|
|
server.command("save-all flush")
|
|
server.stop()
|
|
except Exception as e: # report; the context manager still kills the process
|
|
failures.append(f"{type(e).__name__}: {e}")
|
|
return failures + [f"log after restart: {line}" for line in scan_log(server.log)]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|