Phase 4: veins in every chunk, drops for the mobs the island lacks
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>
This commit is contained in:
1 parent
5d275e4e14
commit
1a1fb93da8
33 files changed
+1279
-126
No files matched your search
+71
-41
@@ -4,11 +4,14 @@
|
||||
* 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.
|
||||
blocks must lie in the zone of its ore tier (world/zones.json → "ores").
|
||||
A surface tier's blocks must lie in its zone's grid cells (the mod checks
|
||||
every block against the zone's boxes; at a cave border a vein can reach
|
||||
into the cave biome). A cave tier's blocks must lie within two biome cells
|
||||
(8 blocks) of its cave biome: the mod checks the biome only at a vein's
|
||||
centre, and Minecraft blends biomes over a few blocks. Untracked ore fails
|
||||
the check, and so (except with --sample) do an ore tier without any vein
|
||||
and a chunk the mod never put veins into.
|
||||
* 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.
|
||||
@@ -48,6 +51,8 @@ import zone_grid # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ORE = re.compile(r"(_ore|:ancient_debris|:raw_(iron|copper|gold)_block)$")
|
||||
CAVE_BIOMES = sorted(zone_grid.load().caves)
|
||||
NEAR_CELLS = 2 # biome cells (4 blocks) around a vein block in which its cave biome may lie
|
||||
# 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"]}
|
||||
@@ -63,7 +68,8 @@ 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, ore_at = {}, {}, 0, {}, [], []
|
||||
names, ores, chunks, present, strongholds, ore_at, full = {}, {}, 0, {}, [], [], []
|
||||
caves = {} # (chunk x, chunk z) -> 96x4x4 biome cells (y, z, x): index into CAVE_BIOMES + 1, 0 = no cave biome
|
||||
loot = {"lootr": 0, "unconverted": 0, "unconverted_at": []}
|
||||
for cx, cz, chunk in anvil.region_chunks(path):
|
||||
for be in chunk.get("block_entities", []):
|
||||
@@ -77,6 +83,7 @@ def scan_region(path):
|
||||
if not str(chunk.get("Status", "")).endswith("full"):
|
||||
continue
|
||||
chunks += 1
|
||||
full.append((cx, cz))
|
||||
ox, oz = (cx - rx * 32) * 16, (cz - rz * 32) * 16
|
||||
of = anvil.heightmap(chunk, "OCEAN_FLOOR")
|
||||
mb = anvil.heightmap(chunk, "MOTION_BLOCKING")
|
||||
@@ -88,6 +95,13 @@ def scan_region(path):
|
||||
for section in sections.values():
|
||||
for name in section.get("biomes", {}).get("palette", []):
|
||||
present[name] = present.get(name, 0) + 1
|
||||
biome_palette = section.get("biomes", {}).get("palette", [])
|
||||
if any(b in CAVE_BIOMES for b in biome_palette):
|
||||
biome_palette, biome_indices = anvil.section_biomes(section)
|
||||
ids = np.array([CAVE_BIOMES.index(b) + 1 if b in CAVE_BIOMES else 0 for b in biome_palette], dtype=np.int8)
|
||||
cells = caves.setdefault((cx, cz), np.zeros((96, 4, 4), dtype=np.int8))
|
||||
y0 = (section["Y"] * 16 - anvil.MIN_Y) // 4
|
||||
cells[y0:y0 + 4] = ids[biome_indices].reshape(4, 4, 4)
|
||||
palette, indices = anvil.section_blocks(section)
|
||||
entries = section.get("block_states", {}).get("palette", [])
|
||||
hits = {i: name for i, name in enumerate(palette)
|
||||
@@ -116,7 +130,20 @@ 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, ore_at
|
||||
# Which cave biomes lie within NEAR_CELLS biome cells of each vein block (chunks of this region only).
|
||||
near_at = []
|
||||
for x, y, z, name, biome_name in ore_at:
|
||||
near = set()
|
||||
qx0, qy0, qz0 = x >> 2, (y - anvil.MIN_Y) >> 2, z >> 2
|
||||
for qx in range(qx0 - NEAR_CELLS, qx0 + NEAR_CELLS + 1):
|
||||
for qz in range(qz0 - NEAR_CELLS, qz0 + NEAR_CELLS + 1):
|
||||
cells = caves.get((qx >> 2, qz >> 2))
|
||||
if cells is None:
|
||||
continue
|
||||
column = cells[max(0, qy0 - NEAR_CELLS):qy0 + NEAR_CELLS + 1, qz & 3, qx & 3]
|
||||
near.update(CAVE_BIOMES[i - 1] for i in np.unique(column) if i)
|
||||
near_at.append((x, y, z, name, biome_name, tuple(sorted(near))))
|
||||
return rx, rz, floor, top, biome, list(names), ores, chunks, present, strongholds, loot, near_at, full
|
||||
|
||||
|
||||
# Entities that aren't mobs: decorations, vehicles, and projectiles (a trial chamber's dispenser trap fires
|
||||
@@ -147,11 +174,12 @@ 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, ore_at = [], {}, 0, {}, [], []
|
||||
names, ores, chunks, present, strongholds, ore_at, full = [], {}, 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, oa in pool.map(scan_region, regions):
|
||||
for rx, rz, f, t, b, local, o, n, pr, sh, lt, oa, fc in pool.map(scan_region, regions):
|
||||
ore_at += oa
|
||||
full += fc
|
||||
for k, v in lt.items():
|
||||
loot[k] += v
|
||||
strongholds += sh
|
||||
@@ -171,19 +199,26 @@ 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, "ore_at": ore_at, "chunks": chunks, "sections_per_biome": present,
|
||||
"biome_names": names, "ores": ores, "ore_at": ore_at, "full_chunks": full, "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)."""
|
||||
"""({(x, y, z): vein id}, {(chunk x, chunk z) the mod has filled}) from Regenerating Ore Veins' saved data."""
|
||||
path = Path(world, "data/regenerating_ore_veins_veins.dat")
|
||||
if not path.exists():
|
||||
return {}
|
||||
return {}, set()
|
||||
import gzip
|
||||
data = anvil.parse_nbt(gzip.decompress(path.read_bytes()))
|
||||
data = data.get("data", data)
|
||||
filled = set()
|
||||
for packed in data.get("GeneratedChunks", []): # ChunkPos.toLong: x in the low 32 bits, z in the high
|
||||
packed = int(packed)
|
||||
x, z = packed & 0xFFFFFFFF, (packed >> 32) & 0xFFFFFFFF
|
||||
filled.add((x - (1 << 32) if x >= 1 << 31 else x, z - (1 << 32) if z >= 1 << 31 else z))
|
||||
veins = {}
|
||||
for entry in data.get("data", data).get("Entries", []):
|
||||
for entry in data.get("Entries", []):
|
||||
pos = entry["Pos"]
|
||||
if isinstance(pos, dict):
|
||||
key = (pos["X"], pos["Y"], pos["Z"])
|
||||
@@ -193,46 +228,41 @@ def read_veins(world):
|
||||
else:
|
||||
key = tuple(int(v) for v in pos)
|
||||
veins[key] = entry.get("VeinId", "")
|
||||
return veins
|
||||
return veins, filled
|
||||
|
||||
|
||||
def check_veins(world, ore_at, sample):
|
||||
def check_veins(world, ore_at, full_chunks, 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)
|
||||
cave_biome = {c["id"]: c["biome"] for c in plan["caves"]}
|
||||
veins, filled = read_veins(world)
|
||||
per_tier, untracked, stray = {}, {}, {}
|
||||
for x, y, z, name, biome, near_caves in ore_at:
|
||||
vein = veins.get((x, y, z))
|
||||
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
|
||||
tier = vein.removesuffix("_showcase").removesuffix("_deep")
|
||||
zone = grid.zone_at(x, z, biome) or "none"
|
||||
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]))
|
||||
counts[zone] = counts.get(zone, 0) + 1
|
||||
want = plan["ores"].get(tier, {}).get("zone")
|
||||
if want in cave_biome:
|
||||
ok = zone == want or cave_biome[want] in near_caves
|
||||
else:
|
||||
ok = want is not None and grid.zone_at(x, z) == want
|
||||
if not ok:
|
||||
stray.setdefault((vein, zone, want), []).append((x, y, z))
|
||||
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)]
|
||||
unfilled = [c for c in full_chunks if c not in filled]
|
||||
if unfilled:
|
||||
problems.append(f"{len(unfilled)} chunks never got their ore veins (e.g. chunk {unfilled[0]})")
|
||||
problems += [f"{len(at)} blocks of vein {vein} in zone {zone}, not {want or '(no tier)'} (e.g. {at[0]})"
|
||||
for (vein, zone, want), at in sorted(stray.items(), key=str)]
|
||||
return per_tier, problems
|
||||
|
||||
|
||||
@@ -427,7 +457,7 @@ 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)
|
||||
vein_counts, vein_problems = check_veins(args.world, data["ore_at"], data["full_chunks"], args.sample)
|
||||
report = {
|
||||
"chunks_scanned": data["chunks"],
|
||||
"ore_blocks": {k: v for k, v in data["ores"].items() if ORE.search(k) or k in VEIN_BLOCKS},
|
||||
|
||||
Reference in new issue
Block a user