#!/usr/bin/env python3 """Write Regenerating Ore Veins' config and the companion mod's ore node table from the zone plan. world/zones.json → "ores" lists the tiers (Adrian, 2026-10-30: small ore nodes on the surface, larger veins underground that are harder to find, and the cave zones' ores as nodes on the cave floor). 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 _, 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: a surface tier's underground veins ("veins"): one vein, or two when the tier has "stone" and "deepslate" blocks (an upper vein from y 0 up, "_deep" below), limited to its zone's boxes and kept out of the cave biomes, which are zones of their own. The mod checks the area for every block and the biome at a vein's centre. Then every tier's "_node": the ore node (the stone ore, or the tier's blocks), placed only by the companion mod, so its area lies far outside the world border, where the mod never generates it by itself. The same goes for "selftest" (a single coal block the self-tests place). * 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. It also writes datapacks/core/data/cobblesurvival/ore_nodes/island.json, the node table the companion mod reads (ore/OreNodes.kt: which node, in which zone, how often per chunk, on the surface or on a cave floor), and world/vein-sources.json, the nodes and veins as item sources for the field guide (tools/fieldguide/build.py reads it by default): what each tier's blocks drop and in which zone. 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" SOURCES = ROOT / "world/vein-sources.json" NODES = ROOT / "datapacks/core/data/cobblesurvival/ore_nodes/island.json" # What an ore block drops, where it isn't the block itself or "_ore" → "". DROPS = {"iron": "raw_iron", "copper": "raw_copper", "gold": "raw_gold", "lapis": "lapis_lazuli", "nether_quartz": "quartz", "amethyst_block": "amethyst_block", "budding_amethyst": "amethyst_shard"} def drop_of(block): namespace, name = block.split(":") base = name.removeprefix("deepslate_").removeprefix("dripstone_").removeprefix("terracotta_") base = base.removesuffix("_ore") if base.endswith("_ore") else base return f"{namespace}:{DROPS.get(base, base)}" 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, shape, defaults, y_range, where): """One entry of veins.json: spec gives the regrowth time, shape the size and spacing.""" 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": shape["radius"][0], "max_radius": shape["radius"][1], "attempts": shape.get("attempts", defaults["attempts"]), "min_y": y_range[0], "max_y": y_range[1], "chunk_minimum_generation_separation": shape.get("spacing", 64), "fill_factor": shape.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, nodes = [], {}, [], [] place_only = {"area_whitelist": [SELFTEST_AREA]} for tier, spec in plan["ores"].items(): zone = spec["zone"] if zone not in caves and zone not in surface: sys.exit(f"ore tier {tier}: unknown zone {zone}") # A node is the stone ore; deepslate where there is no stone ore or the node sits below y 0 (deep caves). deep = not spec.get("stone") or (zone in caves and spec["nodes"]["y"][1] < 0) node_blocks = [tuple(b) for b in spec["blocks"]] if "blocks" in spec else [(spec["deepslate" if deep else "stone"], 1)] node_shape = {**spec["nodes"], "fill": 1.0 if spec["nodes"]["radius"][1] == 0 else defaults["node_fill"]} veins.append(vein(f"{tier}_node", node_blocks, spec, node_shape, defaults, (0, 0), place_only)) node = {"vein": f"{tier}_node", "zone": zone, "where": "cave" if zone in caves else "surface", "chance": spec["nodes"]["chance"], "radius": spec["nodes"]["radius"][1]} if zone in caves: node.update({"biome": caves[zone], "tries": spec["nodes"]["tries"], "y": spec["nodes"]["y"]}) nodes.append(node) if "veins" not in spec: continue 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())} shape = spec["veins"] low, high = shape["y"] if "blocks" in spec: veins.append(vein(tier, spec["blocks"], spec, shape, defaults, (low, high), where)) continue if spec.get("stone") and high >= 0: veins.append(vein(tier, [(spec["stone"], 1)], spec, shape, 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, shape, 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") NODES.parent.mkdir(parents=True, exist_ok=True) NODES.write_text(json.dumps({ "_comment": "Generated by tools/zone_ores.py from world/zones.json; read by the companion mod (ore/OreNodes.kt).", "nodes": nodes}, indent=1) + "\n") names = {z["id"]: z["name"] for z in plan["zones"] + plan["caves"]} sources = [] for tier, spec in plan["ores"].items(): blocks = [b for b, _ in spec.get("blocks", [])] + [spec[k] for k in ("stone", "deepslate") if spec.get(k)] where = (f"ore nodes on the cave floor in {names[spec['zone']]}" if spec["zone"] in caves else f"ore nodes on the ground and veins underground in {names[spec['zone']]}") for item in dict.fromkeys(drop_of(b) for b in blocks): sources.append({"item": item, "how": f"Regenerating {tier.replace('_', ' ')} {where}", "kind": "ore vein", "needs": []}) SOURCES.write_text(json.dumps({ "_comment": "Generated by tools/zone_ores.py from world/zones.json; extra item sources for the field guide.", "sources": sources}, indent=1) + "\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)}, " f"{len(nodes)} node kinds to {NODES.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 nodes)")) if __name__ == "__main__": main()