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>
201 lines
9.8 KiB
Python
Executable File
201 lines
9.8 KiB
Python
Executable File
#!/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. Each tier with "showcase" also gets "<tier>_showcase", a full
|
||
radius-2 vein in the same place-only area, which tools/make_world.py places by hand near the zone's sample point.
|
||
* 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 world/vein-sources.json, the 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"
|
||
# What an ore block drops, where it isn't the block itself or "<name>_ore" → "<name>".
|
||
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, 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 spec.get("showcase"):
|
||
blocks = [(spec["stone"], 1)] if spec.get("stone") else [tuple(b) for b in spec["blocks"]]
|
||
showcase = vein(f"{tier}_showcase", blocks, spec, defaults, (0, 0), {"area_whitelist": [SELFTEST_AREA]})
|
||
veins.append({**showcase, "min_radius": 2, "max_radius": 2, "fill_factor": 0.8})
|
||
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")
|
||
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)]
|
||
for item in dict.fromkeys(drop_of(b) for b in blocks):
|
||
sources.append({"item": item, "how": f"Regenerating {tier.replace('_', ' ')} vein in {names[spec['zone']]}",
|
||
"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)}")
|
||
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()
|