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:
1 parent
82f540aa51
commit
5d275e4e14
19 files changed
+10843
-37
No files matched your search
+96
-12
@@ -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"))
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user