Phase 4: regenerating ore veins, each tier in one zone

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>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-30 02:30:34 +01:00
1 parent 82f540aa51
commit 5d275e4e14
19 files changed
+10843 -37

No files matched your search

+30 -3
View File
@@ -7,9 +7,11 @@
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.
3. In the same session: 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).
@@ -29,10 +31,34 @@ 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 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}").strip()
server.command(f"forceload remove {x} {z}")
print(f" showcase {tier} at {x},{z}: {reply[:100]}")
def border_for(world, recipe):
@@ -75,6 +101,7 @@ def main():
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")
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()}")
+6 -3
View File
@@ -227,9 +227,12 @@ class Server:
failures.append(f"{label}: {check['command']!r} did not match {check['expect']!r}")
return failures
def run_selftest(self, timeout=300):
"""Run the companion mod's in-game tests (needs SELFTEST_JVM_ARGS); returns the failures."""
reply = self.command("cobblesurvival selftest run").strip()
def run_selftest(self, timeout=300, after_restart=False):
"""Run the companion mod's in-game tests (needs SELFTEST_JVM_ARGS); returns the failures.
after_restart runs the second halves of the cases that span a restart (stop, start again, then call this).
"""
reply = self.command("cobblesurvival selftest run" + (" after_restart" if after_restart else "")).strip()
if "started" not in reply:
return [f"selftest did not start: {reply!r}"]
print(f" {reply}")
+23 -3
View File
@@ -12,9 +12,13 @@
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, and run tools/world_report.py --sample on the
sampled world: no ore, no vanilla mobs or mob sources, only listed biomes,
gamerules as in the recipe, and no vanilla loot chest left near the village.
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.
@@ -93,6 +97,7 @@ def main():
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:
@@ -114,5 +119,20 @@ def main():
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()
+11 -1
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
"""Run the companion mod's in-game tests on their own: one server boot on a fresh flat
world, `/cobblesurvival selftest run`, wait for the results, stop, scan the log.
world, `/cobblesurvival selftest run`, wait for the results, stop, scan the log; then boot
the world again for the second halves of the tests that span a restart
(`/cobblesurvival selftest run after_restart`).
The tests (mod/src/main/kotlin/dev/cobblesurvival/selftest/) play real situations on the
live server with every mod and config of the pack, e.g. the permadeath matrix: Pokémon
@@ -47,6 +49,14 @@ def main():
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)]
with Server(args_file, props, stdout_name="selftest-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.stop()
except Exception as e: # report; the context manager still kills the process
failures.append(f"{type(e).__name__}: {e}")
failures += [f"log after restart: {line}" for line in scan_log(server.log)]
print(f"\nselftest took {time.time() - started:.0f}s")
if failures:
print("SELFTEST FAILED")
+96 -12
View File
@@ -1,8 +1,14 @@
#!/usr/bin/env python3
"""Check and picture a generated world (default: build/world-template).
* Ore scan: every block in every stored chunk is checked; any ore block
("*_ore", ancient debris, raw ore blocks from large veins) fails the check.
* Ore scan: every block in every stored chunk is checked. Ore ("*_ore",
ancient debris, raw ore blocks from large veins) is only allowed as part of
a regenerating vein (Regenerating Ore Veins' saved data), and each vein's
blocks must lie in the zone of its ore tier (world/zones.json → "ores"),
or at most two radii from a block of the same vein that does: the mod
checks the biome only at a vein's centre, so its edge can reach over the
border of a cave zone. Untracked ore fails the check, and so (except with
--sample) does an ore tier without any vein.
* Biome set: every biome stored in any chunk section (surface and caves)
must be listed in world/biomes.json ("allowed" or "water").
* End access: at least one stronghold must start inside the world border.
@@ -38,9 +44,13 @@ import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent))
import anvil # noqa: E402
import zone_grid # noqa: E402
ROOT = Path(__file__).resolve().parent.parent
ORE = re.compile(r"(_ore|:ancient_debris|:raw_(iron|copper|gold)_block)$")
# Blocks the regenerating veins are made of (amethyst isn't "ore" by name but is a tier).
VEIN_BLOCKS = {b for v in json.loads((ROOT / "pack/config/regenerating_ore_veins/veins.json").read_text())
for b in v["blocks"]}
MOB_BLOCK = re.compile(r":(spawner|trial_spawner|infested_\w+)$")
LOOT_CONTAINERS = {"minecraft:chest", "minecraft:trapped_chest", "minecraft:barrel", "minecraft:shulker_box"}
SEA_LEVEL = 63
@@ -53,7 +63,7 @@ def scan_region(path):
floor = np.full((512, 512), -10_000, dtype=np.int16)
top = np.full((512, 512), -10_000, dtype=np.int16)
biome = np.full((128, 128), -1, dtype=np.int16)
names, ores, chunks, present, strongholds = {}, {}, 0, {}, []
names, ores, chunks, present, strongholds, ore_at = {}, {}, 0, {}, [], []
loot = {"lootr": 0, "unconverted": 0, "unconverted_at": []}
for cx, cz, chunk in anvil.region_chunks(path):
for be in chunk.get("block_entities", []):
@@ -80,13 +90,20 @@ def scan_region(path):
present[name] = present.get(name, 0) + 1
palette, indices = anvil.section_blocks(section)
entries = section.get("block_states", {}).get("palette", [])
hits = {i: name for i, name in enumerate(palette) if ORE.search(name) or MOB_BLOCK.search(name)}
hits = {i: name for i, name in enumerate(palette)
if ORE.search(name) or MOB_BLOCK.search(name) or name in VEIN_BLOCKS}
hits.update({i: "minecraft:sculk_shrieker[can_summon=true]" for i, e in enumerate(entries)
if e["Name"] == "minecraft:sculk_shrieker" and e.get("Properties", {}).get("can_summon") == "true"})
for i, name in hits.items():
count = int(np.count_nonzero(indices == i))
if count:
ores[name] = ores.get(name, 0) + count
if count and (ORE.search(name) or name in VEIN_BLOCKS):
biome_palette, biome_indices = anvil.section_biomes(section)
for idx in np.flatnonzero(indices == i).tolist():
y, z, x = idx // 256, (idx // 16) % 16, idx % 16
biome_name = biome_palette[int(biome_indices[(y // 4) * 16 + (z // 4) * 4 + x // 4])]
ore_at.append((cx * 16 + x, section["Y"] * 16 + y, cz * 16 + z, name, biome_name))
decoded = {}
for qz in range(4):
for qx in range(4):
@@ -99,7 +116,7 @@ def scan_region(path):
palette, indices = decoded[y // 16]
name = palette[int(indices[((y % 16) // 4) * 16 + qz * 4 + qx])]
biome[oz // 4 + qz, ox // 4 + qx] = names.setdefault(name, len(names))
return rx, rz, floor, top, biome, list(names), ores, chunks, present, strongholds, loot
return rx, rz, floor, top, biome, list(names), ores, chunks, present, strongholds, loot, ore_at
# Entities that aren't mobs: decorations, vehicles, and projectiles (a trial chamber's dispenser trap fires
@@ -130,10 +147,11 @@ def analyse(world):
floor = np.full((h, w), -10_000, dtype=np.int16)
top = np.full((h, w), -10_000, dtype=np.int16)
biome = np.full((h // 4, w // 4), -1, dtype=np.int16)
names, ores, chunks, present, strongholds = [], {}, 0, {}, []
names, ores, chunks, present, strongholds, ore_at = [], {}, 0, {}, [], []
loot = {"lootr": 0, "unconverted": 0, "unconverted_at": []}
with ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
for rx, rz, f, t, b, local, o, n, pr, sh, lt in pool.map(scan_region, regions):
for rx, rz, f, t, b, local, o, n, pr, sh, lt, oa in pool.map(scan_region, regions):
ore_at += oa
for k, v in lt.items():
loot[k] += v
strongholds += sh
@@ -153,10 +171,71 @@ def analyse(world):
for k, v in counts.items():
entities[k] = entities.get(k, 0) + v
return {"origin": (x0 * 512, z0 * 512), "floor": floor, "top": top, "biome": biome,
"biome_names": names, "ores": ores, "chunks": chunks, "sections_per_biome": present,
"biome_names": names, "ores": ores, "ore_at": ore_at, "chunks": chunks, "sections_per_biome": present,
"strongholds": strongholds, "entities": entities, "loot": loot}
def read_veins(world):
"""{(x, y, z): vein id} from Regenerating Ore Veins' saved data (empty if the mod never ran)."""
path = Path(world, "data/regenerating_ore_veins_veins.dat")
if not path.exists():
return {}
import gzip
data = anvil.parse_nbt(gzip.decompress(path.read_bytes()))
veins = {}
for entry in data.get("data", data).get("Entries", []):
pos = entry["Pos"]
if isinstance(pos, dict):
key = (pos["X"], pos["Y"], pos["Z"])
elif isinstance(pos, int): # BlockPos.asLong
x, z, y = pos >> 38, (pos << 26) >> 38, ((pos & 0xFFF) ^ 0x800) - 0x800
key = (x, y, ((z + (1 << 25)) % (1 << 26)) - (1 << 25))
else:
key = tuple(int(v) for v in pos)
veins[key] = entry.get("VeinId", "")
return veins
def check_veins(world, ore_at, sample):
"""Every ore block belongs to a tracked vein in its tier's zone; returns (blocks per tier and zone, problems)."""
plan = json.loads((ROOT / "world/zones.json").read_text())
grid = zone_grid.load()
tier_zone = {}
for tier, spec in plan["ores"].items():
tier_zone[tier] = tier_zone[f"{tier}_deep"] = (spec["zone"], spec["radius"][1])
veins = read_veins(world)
per_tier, untracked, stray = {}, {}, []
blocks = {(x, y, z): (name, biome) for x, y, z, name, biome in ore_at}
zone_of = {pos: grid.zone_at(pos[0], pos[2], biome) for pos, (_, biome) in blocks.items()}
for pos, (name, _) in blocks.items():
vein = veins.get(pos)
if vein is None:
if ORE.search(name): # amethyst outside a vein is a natural geode
untracked[name] = untracked.get(name, 0) + 1
continue
tier = vein[:-5] if vein.endswith("_deep") else vein
counts = per_tier.setdefault(tier, {})
counts[zone_of[pos] or "none"] = counts.get(zone_of[pos] or "none", 0) + 1
want, radius = tier_zone.get(vein, (None, 0))
if zone_of[pos] == want:
continue
reach = 2 * radius
near = any(veins.get(q) == vein and zone_of.get(q) == want
for q in ((pos[0] + dx, pos[1] + dy, pos[2] + dz) for dx in range(-reach, reach + 1)
for dy in range(-reach, reach + 1) for dz in range(-reach, reach + 1)))
if not near:
stray.append((vein, pos, zone_of[pos]))
problems = [f"{n} {name} outside any regenerating vein" for name, n in sorted(untracked.items())]
if not sample:
problems += [f"ore tier {tier} has no vein in the world" for tier in plan["ores"] if tier not in per_tier]
by_vein = {}
for vein, pos, zone in stray:
by_vein.setdefault((vein, zone), []).append(pos)
problems += [f"{len(at)} blocks of vein {vein} in zone {zone}, not {tier_zone.get(vein, ('(no tier)',))[0]} "
f"(e.g. {at[0]})" for (vein, zone), at in sorted(by_vein.items(), key=str)]
return per_tier, problems
def border_of(level):
return level.get("BorderCenterX", 0.0), level.get("BorderCenterZ", 0.0), level.get("BorderSize", 5.9999968e7)
@@ -348,10 +427,12 @@ def main():
biomes = json.loads((ROOT / "world/biomes.json").read_text())
listed = set(biomes["allowed"]) | set(biomes["water"])
unlisted = {b: n for b, n in data["sections_per_biome"].items() if b not in listed}
vein_counts, vein_problems = check_veins(args.world, data["ore_at"], args.sample)
report = {
"chunks_scanned": data["chunks"],
"ore_blocks": {k: v for k, v in data["ores"].items() if ORE.search(k)},
"mob_source_blocks": {k: v for k, v in data["ores"].items() if not ORE.search(k)},
"ore_blocks": {k: v for k, v in data["ores"].items() if ORE.search(k) or k in VEIN_BLOCKS},
"vein_blocks_per_tier_and_zone": vein_counts,
"mob_source_blocks": {k: v for k, v in data["ores"].items() if MOB_BLOCK.search(k) or "shrieker" in k},
"unlisted_biomes": unlisted,
"gamerules_not_as_recipe": wrong_rules,
"vanilla_mobs_in_world": vanilla_mobs,
@@ -368,7 +449,9 @@ def main():
args.json.write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps({k: v for k, v in report.items() if k != "land_biomes_percent"}, indent=2))
failures = ([f"{n} {name}" for name, n in data["ores"].items()] + border["problems"]
failures = ([f"{n} {name}" for name, n in data["ores"].items() if MOB_BLOCK.search(name) or "shrieker" in name]
+ vein_problems
+ border["problems"]
+ [f"unlisted biome {b} in {n} chunk sections" for b, n in unlisted.items()]
+ [f"gamerule {r} is {v}, recipe says {recipe['gamerules'][r]}" for r, v in wrong_rules.items()]
+ [f"{n} vanilla {mob} in the world" for mob, n in vanilla_mobs.items()])
@@ -378,7 +461,8 @@ def main():
sys.exit(1)
if report["missing_listed_biomes"] and not args.sample:
print(f"note: listed biomes not on this island: {', '.join(report['missing_listed_biomes'])}")
print(f"\nWORLD CHECK PASSED: {data['chunks']} chunks, no ore, no vanilla mobs or mob sources, only listed biomes"
print(f"\nWORLD CHECK PASSED: {data['chunks']} chunks, ore only in regenerating veins in their zones, "
f"no vanilla mobs or mob sources, only listed biomes"
+ ("" if args.sample else ", border fits the coast"))
+33
View File
@@ -0,0 +1,33 @@
"""The zone grid as the companion mod sees it (datapacks/core/data/cobblesurvival/zones/island.json).
zone_at() answers like zone/Zones.kt: a cave biome is its cave zone; otherwise the grid cell's zone.
"""
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ISLAND = ROOT / "datapacks/core/data/cobblesurvival/zones/island.json"
class Grid:
def __init__(self, data):
self.radius, self.step, self.size = data["radius"], data["step"], data["size"]
self.ids = [z["id"] for z in data["zones"]]
self.caves = {c["biome"]: c["id"] for c in data["caves"]}
self.cells = [value for value, count in data["grid_rle"] for _ in range(count)]
if len(self.cells) != self.size * self.size:
raise ValueError(f"zone grid has {len(self.cells)} cells, expected {self.size ** 2}")
def zone_at(self, x, z, biome=None):
if biome in self.caves:
return self.caves[biome]
col = (x + self.radius + self.step // 2) // self.step
row = (z + self.radius + self.step // 2) // self.step
if not (0 <= col < self.size and 0 <= row < self.size):
return None
index = self.cells[row * self.size + col]
return self.ids[index] if 0 <= index < len(self.ids) else None
def load(path=ISLAND):
return Grid(json.loads(Path(path).read_text()))
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Write Regenerating Ore Veins' config from the zone plan, so every ore tier only has veins in its zone.
world/zones.json → "ores" lists the tiers. This tool writes pack/config/regenerating_ore_veins/:
* areas.json: each surface zone that has a tier, as boxes from the zone grid (build/zone-layout.json,
24-block cells; rows of cells merged into rectangles), named <zone>_<n>, reaching from bedrock to
the build limit. The mod's boxes are centre plus half-size, so a box covers its cells except their
last row and column of blocks: no box reaches into a neighbouring zone.
* veins.json: one vein per tier, or two when the tier has "stone" and "deepslate" blocks (an upper vein
from y 0 up, "<tier>_deep" below). A surface tier's veins are limited to its zone's boxes and kept out
of the cave biomes, which are zones of their own; a cave tier's veins to its cave biome. The mod checks
the area for every block and the biome at a vein's centre. One vein only for the self-tests comes
last ("selftest", a single coal block); its area lies far outside the world border, so it never
generates, and the tests place it by command.
* global.json: regenerators can't be broken by players (allow_breaking false), so nobody can remove a
vein for good; explosions don't destroy veins.
* vein_locator.json: the Vein Locator from a compass and a copper ingot (mid-game) instead of netherite.
Re-run after changing the zone plan or the grid (tools/zone_layout.py) and commit the result.
Usage: tools/zone_ores.py
"""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "pack/config/regenerating_ore_veins"
LAYOUT = ROOT / "build/zone-layout.json"
BOTTOM, TOP = -64, 320
SELFTEST_AREA = "selftest_nowhere"
def rectangles(mask, size):
"""Cover the True cells of a size×size row-major mask with rectangles (col0, row0, col1, row1)."""
open_runs, done = {}, []
for row in range(size + 1):
runs = set()
col = 0
while row < size and col < size:
if mask[row * size + col]:
start = col
while col < size and mask[row * size + col]:
col += 1
runs.add((start, col - 1))
col += 1
for run in list(open_runs):
if run not in runs:
done.append((run[0], open_runs.pop(run), run[1], row - 1))
for run in runs:
open_runs.setdefault(run, row)
return sorted(done, key=lambda r: (r[1], r[0]))
def zone_areas(layout, zone_id):
size, step, radius = layout["size"], layout["step"], layout["radius"]
index = layout["zones"].index(zone_id)
mask = [value == index for value in layout["grid"]]
areas = []
for n, (c0, r0, c1, r1) in enumerate(rectangles(mask, size)):
# Cell i covers -radius - step/2 + i*step ... +step-1 (as in zone/Zones.kt).
x0, z0 = c0 * step - radius - step // 2, r0 * step - radius - step // 2
w, h = (c1 - c0 + 1) * step, (r1 - r0 + 1) * step
areas.append({"dimension": "minecraft:overworld", "name": f"{zone_id}_{n}", "type": "box",
"x": x0 + w // 2 - 1, "y": (BOTTOM + TOP) // 2, "z": z0 + h // 2 - 1,
"dimx": w // 2 - 1, "dimy": (TOP - BOTTOM) // 2, "dimz": h // 2 - 1})
return areas
def vein(vein_id, blocks, spec, defaults, y_range, where):
seconds = spec["regen_minutes"] * 60
jitter = round(seconds * spec.get("jitter", defaults["jitter"]))
return {
"id": vein_id,
"blocks": [b for b, _ in blocks],
"weights": [w for _, w in blocks],
"dimension_whitelist": ["minecraft:overworld"],
**where,
"shape": "sphere",
"min_radius": spec["radius"][0],
"max_radius": spec["radius"][1],
"attempts": spec.get("attempts", defaults["attempts"]),
"min_y": y_range[0],
"max_y": y_range[1],
"chunk_minimum_generation_separation": spec["spacing"],
"fill_factor": spec.get("fill", defaults["fill"]),
"regeneration_interval_seconds": seconds,
"regeneration_interval_jitter": {"range_min": -jitter, "range_max": jitter},
}
def selftest_vein():
return {"id": "selftest", "blocks": ["minecraft:coal_ore"], "dimension_whitelist": ["minecraft:overworld"],
"area_whitelist": [SELFTEST_AREA], "shape": "sphere", "min_radius": 0, "max_radius": 0,
"attempts": 1, "min_y": 0, "max_y": 0, "chunk_minimum_generation_separation": 64,
"fill_factor": 1.0, "regeneration_interval_seconds": 3600,
"regeneration_interval_jitter": {"range_min": 0, "range_max": 0}}
def main():
if not LAYOUT.exists():
sys.exit(f"{LAYOUT.relative_to(ROOT)} is missing: run tools/zone_layout.py first.")
layout = json.loads(LAYOUT.read_text())
plan = json.loads((ROOT / "world/zones.json").read_text())
defaults = plan["ore_defaults"]
caves = {c["id"]: c["biome"] for c in plan["caves"]}
surface = {z["id"] for z in plan["zones"]}
areas, area_names, veins = [], {}, []
for tier, spec in plan["ores"].items():
zone = spec["zone"]
if zone in caves:
where = {"biome_whitelist": [caves[zone]]}
elif zone in surface:
if zone not in area_names:
boxes = zone_areas(layout, zone)
if not boxes:
sys.exit(f"zone {zone} (ore tier {tier}) has no cells in the grid")
areas += boxes
area_names[zone] = [a["name"] for a in boxes]
where = {"area_whitelist": area_names[zone], "biome_blacklist": sorted(caves.values())}
else:
sys.exit(f"ore tier {tier}: unknown zone {zone}")
low, high = spec["y"]
if "blocks" in spec:
veins.append(vein(tier, spec["blocks"], spec, defaults, (low, high), where))
continue
if spec.get("stone") and high >= 0:
veins.append(vein(tier, [(spec["stone"], 1)], spec, defaults, (max(low, 0), high), where))
if spec.get("deepslate") and low < 0:
deep_id = f"{tier}_deep" if spec.get("stone") and high >= 0 else tier
veins.append(vein(deep_id, [(spec["deepslate"], 1)], spec, defaults, (low, min(high, -1)), where))
areas.append({"dimension": "minecraft:overworld", "name": SELFTEST_AREA, "type": "box",
"x": 29_000_000, "y": 0, "z": 29_000_000, "dimx": 0, "dimy": 0, "dimz": 0})
veins.append(selftest_vein())
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "areas.json").write_text(json.dumps(areas, indent=1) + "\n")
(OUT / "veins.json").write_text(json.dumps(veins, indent=1) + "\n")
(OUT / "global.json").write_text(json.dumps({
"allow_breaking": False,
"break_hardness": 5.0,
"default_regeneration_seconds": 3600,
"default_jitter_interval": {"range_min": 0, "range_max": 0},
"regeneration_smoke_particles": True,
"destroyed_by_explosives": False,
"default_fill_factor": defaults["fill"],
}, indent=2) + "\n")
(OUT / "vein_locator.json").write_text(json.dumps({
"allow_crafting": True,
"allow_use": True,
"crafting_recipe": ["minecraft:compass", "minecraft:copper_ingot"],
"craft_any_order": True,
"chance_to_break": 0.75,
"use_durability": False,
"durability": 2,
"stack_size": 64,
}, indent=2) + "\n")
per_zone = {}
for tier, spec in plan["ores"].items():
per_zone.setdefault(spec["zone"], []).append(tier)
print(f"wrote {len(veins)} veins ({len(plan['ores'])} tiers) and {len(areas)} areas to {OUT.relative_to(ROOT)}")
for zone, tiers in per_zone.items():
boxes = len(area_names.get(zone, []))
print(f" {zone}: {', '.join(tiers)}" + (f" ({boxes} boxes)" if boxes else " (cave biome)"))
if __name__ == "__main__":
main()