#!/usr/bin/env python3 """Check and picture a generated world (default: build/world-template). * 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"). 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. * Gamerules in level.dat match world/world.json, and no vanilla mob is stored in the world (only Pokémon; item frames, minecarts etc. don't count). * No mob sources in blocks: no spawners, trial spawners or infested blocks, and no sculk shrieker that can summon the Warden. * Loot containers: Lootr containers (they refill) and vanilla ones still waiting for conversion are counted. Lootr converts a container once the chunks around it are loaded (a player nearby), so a pregenerated world keeps vanilla ones until then; tools/quick_check.py tests the conversion. * Border fit: all land must lie inside the world border, and the border may stand at most `border_margin` (world/world.json) plus a little slack outside the outermost land on its longer axis. * Biome map: a top-down PNG (biome colours, hill shading, water depth, border, island outline from the preset, legend) for review. --sample is for partly generated worlds (tools/quick_check.py): it skips the border, stronghold and map, which only make sense for the whole island. Usage: tools/world_report.py [WORLD] [--map OUT.png] [--json OUT.json] [--no-map] [--sample] """ import argparse import json import math import os import re import sys from concurrent.futures import ProcessPoolExecutor from pathlib import Path 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)$") 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"]} 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 BORDER_SLACK = 32 # blocks: coastline and border are both measured on a 4-block grid def scan_region(path): """Per-region arrays at block resolution plus ore counts.""" rx, rz = (int(v) for v in Path(path).name.split(".")[1:3]) 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, 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", []): if be.get("id", "").startswith("lootr:"): loot["lootr"] += 1 elif be.get("id") in LOOT_CONTAINERS and "LootTable" in be: loot["unconverted"] += 1 loot["unconverted_at"].append([be["x"], be["y"], be["z"]]) for structure in chunk.get("structures", {}).get("starts", {}): strongholds.append((structure, cx * 16 + 8, cz * 16 + 8)) 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") if of is None or mb is None: continue floor[oz:oz + 16, ox:ox + 16] = of top[oz:oz + 16, ox:ox + 16] = mb sections = {s["Y"]: s for s in chunk.get("sections", [])} 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) 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): y = int(mb[qz * 4 + 2, qx * 4 + 2]) - 1 section = sections.get(y // 16) if section is None: continue if y // 16 not in decoded: decoded[y // 16] = anvil.section_biomes(section) 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)) # 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 # potions and arrows when a Pokémon steps on its plate, and they can be saved mid-flight). NOT_MOBS = re.compile(r":(item|item_frame|glow_item_frame|painting|armor_stand|.*minecart.*|.*boat|" r"marker|interaction|block_display|item_display|text_display|end_crystal|falling_block|" r"potion|arrow|spectral_arrow|trident|snowball|egg|ender_pearl|eye_of_ender|experience_orb|" r"experience_bottle|fireball|small_fireball|dragon_fireball|wither_skull|wind_charge|" r"breeze_wind_charge|firework_rocket|llama_spit|shulker_bullet|fishing_bobber|tnt|" r"area_effect_cloud|lightning_bolt|evoker_fangs|ominous_item_spawner)$") def scan_entities(path): counts = {} for _, _, chunk in anvil.region_chunks(path): for entity in chunk.get("Entities", []): counts[entity["id"]] = counts.get(entity["id"], 0) + 1 return counts def analyse(world): regions = sorted(Path(world, "region").glob("r.*.*.mca")) if not regions: sys.exit(f"no region files in {world}/region") coords = [tuple(int(v) for v in p.name.split(".")[1:3]) for p in regions] x0, z0 = min(c[0] for c in coords), min(c[1] for c in coords) w, h = (max(c[0] for c in coords) - x0 + 1) * 512, (max(c[1] for c in coords) - z0 + 1) * 512 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, 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, fc in pool.map(scan_region, regions): ore_at += oa full += fc for k, v in lt.items(): loot[k] += v strongholds += sh for k, v in pr.items(): present[k] = present.get(k, 0) + v ix, iz = (rx - x0) * 512, (rz - z0) * 512 floor[iz:iz + 512, ix:ix + 512] = f top[iz:iz + 512, ix:ix + 512] = t remap = np.array([names.index(name) if name in names else names.append(name) or len(names) - 1 for name in local] + [-1], dtype=np.int16) biome[iz // 4:iz // 4 + 128, ix // 4:ix // 4 + 128] = remap[b] for k, v in o.items(): ores[k] = ores.get(k, 0) + v chunks += n entities = {} for counts in pool.map(scan_entities, sorted(Path(world, "entities").glob("r.*.*.mca"))): 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, "full_chunks": full, "chunks": chunks, "sections_per_biome": present, "strongholds": strongholds, "entities": entities, "loot": loot} def read_veins(world): """({(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 {}, 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("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, filled 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() 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.removesuffix("_showcase").removesuffix("_deep") zone = grid.zone_at(x, z, biome) or "none" counts = per_tier.setdefault(tier, {}) 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] 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 def border_of(level): return level.get("BorderCenterX", 0.0), level.get("BorderCenterZ", 0.0), level.get("BorderSize", 5.9999968e7) def land_mask(data): generated = data["floor"] > -10_000 return generated & (data["top"] == data["floor"]) & (data["floor"] >= SEA_LEVEL), generated def island_mask(data, cell=16, min_share=0.01): """Land that belongs to a large land mass. Icebergs, sea stacks and wrecks out at sea are dropped so they don't stretch the border. Land is grouped on a `cell`-block grid, grown by one cell so rivers don't split the island.""" land, _ = land_mask(data) h, w = land.shape[0] // cell, land.shape[1] // cell cells = land[:h * cell, :w * cell].reshape(h, cell, w, cell).any(axis=(1, 3)) grown = cells.copy() grown[1:] |= cells[:-1] grown[:-1] |= cells[1:] grown[:, 1:] |= cells[:, :-1] grown[:, :-1] |= cells[:, 1:] labels = np.zeros((h, w), dtype=np.int32) sizes = [0] for start in zip(*np.nonzero(grown)): if labels[start]: continue sizes.append(0) labels[start] = len(sizes) - 1 stack = [start] while stack: z, x = stack.pop() sizes[-1] += 1 for nz, nx in ((z + 1, x), (z - 1, x), (z, x + 1), (z, x - 1)): if 0 <= nz < h and 0 <= nx < w and grown[nz, nx] and not labels[nz, nx]: labels[nz, nx] = labels[start] stack.append((nz, nx)) keep = np.array(sizes) >= min_share * sum(sizes) keep[0] = False big = np.repeat(np.repeat(keep[labels], cell, axis=0), cell, axis=1) out = np.zeros_like(land) out[:h * cell, :w * cell] = land[:h * cell, :w * cell] & big return out def check_border(data, level, margin): zs, xs = np.nonzero(island_mask(data)) ox, oz = data["origin"] bounds = (int(xs.min()) + ox, int(zs.min()) + oz, int(xs.max()) + ox + 1, int(zs.max()) + oz + 1) cx, cz, size = border_of(level) half = size / 2 gaps = {"west": bounds[0] - (cx - half), "north": bounds[1] - (cz - half), "east": (cx + half) - bounds[2], "south": (cz + half) - bounds[3]} problems = [f"land beyond the {side} border by {-gap:.0f} blocks" for side, gap in gaps.items() if gap < 0] starts = [s for s in data["strongholds"] if abs(s[1] - cx) < half and abs(s[2] - cz) < half] inside = [[x, z] for name, x, z in starts if name == "minecraft:stronghold"] structures = {} for name, _, _ in starts: structures[name] = structures.get(name, 0) + 1 if not inside: problems.append("no stronghold inside the border, so the End can't be reached") if min(gaps["west"], gaps["east"]) > margin + BORDER_SLACK and min(gaps["north"], gaps["south"]) > margin + BORDER_SLACK: problems.append(f"border is more than {margin + BORDER_SLACK} blocks from the coast on every side") return {"land_bounds": bounds, "strongholds_inside_border": inside, "structures_inside_border": dict(sorted(structures.items(), key=lambda kv: -kv[1])), "border": {"center": [cx, cz], "size": size}, "gaps": {k: round(v) for k, v in gaps.items()}, "problems": problems} # Approximate colours in the style of common seed-map tools. COLOURS = { "ocean": (0, 0, 112), "deep_ocean": (0, 0, 48), "cold_ocean": (32, 32, 112), "deep_cold_ocean": (32, 32, 56), "frozen_ocean": (112, 112, 214), "deep_frozen_ocean": (64, 64, 144), "lukewarm_ocean": (0, 0, 144), "deep_lukewarm_ocean": (0, 0, 64), "warm_ocean": (0, 0, 172), "river": (48, 96, 255), "frozen_river": (160, 160, 255), "beach": (250, 222, 85), "snowy_beach": (250, 240, 192), "stony_shore": (162, 162, 132), "plains": (141, 179, 96), "sunflower_plains": (181, 219, 136), "forest": (5, 102, 33), "flower_forest": (45, 142, 73), "birch_forest": (48, 116, 68), "old_growth_birch_forest": (88, 156, 108), "dark_forest": (64, 81, 26), "meadow": (96, 164, 69), "cherry_grove": (255, 145, 200), "swamp": (7, 249, 178), "mangrove_swamp": (44, 204, 142), "taiga": (11, 102, 89), "old_growth_pine_taiga": (89, 102, 81), "old_growth_spruce_taiga": (129, 142, 121), "snowy_taiga": (49, 85, 74), "snowy_plains": (255, 255, 255), "ice_spikes": (180, 220, 220), "grove": (187, 211, 221), "snowy_slopes": (220, 220, 230), "frozen_peaks": (160, 180, 255), "jagged_peaks": (220, 220, 220), "stony_peaks": (123, 143, 116), "windswept_hills": (96, 96, 96), "windswept_forest": (80, 112, 80), "windswept_gravelly_hills": (136, 136, 136), "desert": (250, 148, 24), "savanna": (189, 178, 95), "savanna_plateau": (167, 157, 100), "windswept_savanna": (229, 218, 135), "badlands": (217, 69, 21), "eroded_badlands": (255, 109, 61), "wooded_badlands": (176, 151, 101), "jungle": (83, 123, 9), "sparse_jungle": (98, 139, 23), "bamboo_jungle": (118, 142, 20), "mushroom_fields": (255, 0, 255), "dripstone_caves": (134, 96, 67), "lush_caves": (102, 140, 50), "deep_dark": (20, 40, 50), } def colour(name): short = name.split(":", 1)[-1] if short in COLOURS: return COLOURS[short] seed = sum(ord(c) * 31 ** i for i, c in enumerate(name)) & 0xFFFFFF return (seed >> 16 & 255, seed >> 8 & 255, seed & 255) def render_map(data, level, preset, out, scale=2): from PIL import Image, ImageDraw, ImageFont full_land, _ = land_mask(data) land, generated = full_land[::scale, ::scale], (data["floor"] > -10_000)[::scale, ::scale] names = data["biome_names"] lut = np.array([colour(n) for n in names] + [(0, 0, 0)], dtype=np.float32) per_cell = 4 // scale rgb = lut[np.repeat(np.repeat(data["biome"], per_cell, axis=0), per_cell, axis=1)] height = data["floor"][::scale, ::scale].astype(np.float32) gz, gx = np.gradient(np.where(generated, height, SEA_LEVEL)) shade = np.clip(1.0 + (gx * -0.6 + gz * -0.6) * 0.18 * scale, 0.55, 1.35) depth = np.clip((data["top"][::scale, ::scale] - data["floor"][::scale, ::scale]).astype(np.float32), 0, 40) water = generated & ~land rgb = np.where(land[..., None], rgb * shade[..., None], rgb) tint = np.array([30, 80, 170], dtype=np.float32) * (1.0 - depth / 55.0)[..., None] rgb = np.where(water[..., None], rgb * 0.35 + tint * 0.65, rgb) rgb = np.where(generated[..., None], rgb, 24) img = Image.fromarray(np.clip(rgb, 0, 255).astype(np.uint8)) ox, oz = data["origin"] to_px = lambda x, z: ((x - ox) / scale, (z - oz) / scale) # noqa: E731 draw = ImageDraw.Draw(img) try: font = ImageFont.truetype("DejaVuSans.ttf", 18) small = ImageFont.truetype("DejaVuSans.ttf", 14) except OSError: font = small = ImageFont.load_default() for entry in (e for e in preset["entries"] if e.get("type", "island") == "island"): rx = entry.get("radius_x", entry.get("radius", 0)) * entry.get("stretch_x", 1.0) rz = entry.get("radius_z", entry.get("radius", 0)) * entry.get("stretch_z", 1.0) pts = [] for i in range(73): a = math.radians(i * 5) x, z = rx * math.cos(a), rz * math.sin(a) r = math.radians(entry.get("rotation", 0)) pts.append(to_px(entry["x"] + x * math.cos(r) + z * math.sin(r), entry["z"] - x * math.sin(r) + z * math.cos(r))) draw.line(pts, fill=(255, 255, 255), width=1) px, pz = to_px(entry["x"], entry["z"]) draw.text((px, pz - 24), entry.get("name", "").split(" (")[0], fill=(255, 255, 255), font=font, anchor="mm", stroke_width=3, stroke_fill=(0, 0, 0)) cx, cz, size = border_of(level) a, b = to_px(cx - size / 2, cz - size / 2), to_px(cx + size / 2, cz + size / 2) draw.rectangle([a, b], outline=(255, 60, 60), width=3) sx, sz = to_px(level.get("SpawnX", 0), level.get("SpawnZ", 0)) draw.ellipse([sx - 6, sz - 6, sx + 6, sz + 6], fill=(255, 230, 0), outline=(0, 0, 0), width=2) counts = np.bincount(data["biome"][full_land[::4, ::4]].clip(0).ravel(), minlength=len(names)) total = counts.sum() or 1 order = [i for i in np.argsort(-counts) if counts[i] / total >= 0.001] legend_w = 300 canvas = Image.new("RGB", (img.width + legend_w, max(img.height, 40 + 22 * len(order))), (24, 24, 24)) canvas.paste(img, (0, 0)) d = ImageDraw.Draw(canvas) d.text((img.width + 14, 12), "Land biomes (share of land)", fill=(240, 240, 240), font=small) for row, i in enumerate(order): y = 40 + row * 22 d.rectangle([img.width + 14, y, img.width + 30, y + 16], fill=tuple(int(c) for c in lut[i])) d.text((img.width + 38, y), f"{names[i].split(':', 1)[-1]} {100 * counts[i] / total:.1f}%", fill=(230, 230, 230), font=small) y = 40 + len(order) * 22 + 16 d.text((img.width + 14, y), f"1 px = {scale} blocks", fill=(180, 180, 180), font=small) d.text((img.width + 14, y + 20), "red: world border", fill=(255, 120, 120), font=small) d.text((img.width + 14, y + 40), "white: island outline (preset)", fill=(230, 230, 230), font=small) d.text((img.width + 14, y + 60), "yellow dot: world spawn", fill=(255, 230, 0), font=small) canvas.save(out, optimize=True) return {names[i]: round(100 * counts[i] / total, 2) for i in order} def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("world", nargs="?", type=Path, default=ROOT / "build/world-template") parser.add_argument("--map", type=Path, default=ROOT / "build/island-map.png") parser.add_argument("--json", type=Path, default=ROOT / "build/island-report.json") parser.add_argument("--no-map", action="store_true") parser.add_argument("--sample", action="store_true", help="partly generated world: skip border, strongholds, map") args = parser.parse_args() recipe = json.loads((ROOT / "world/world.json").read_text()) preset = json.loads((ROOT / recipe["preset"]).read_text()) level = anvil.read_level_dat(args.world) data = analyse(args.world) border = {"problems": []} if args.sample else check_border(data, level, recipe["border_margin"]) land, generated = land_mask(data) rules = level.get("GameRules", {}) wrong_rules = {r: rules.get(r) for r, v in recipe.get("gamerules", {}).items() if rules.get(r) != str(v).lower()} vanilla_mobs = {k: v for k, v in sorted(data["entities"].items(), key=lambda kv: -kv[1]) if not k.startswith("cobblemon:") and not NOT_MOBS.search(k)} 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"], 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}, "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, "loot_containers": data["loot"], "missing_listed_biomes": sorted(set(biomes["allowed"]) - set(data["sections_per_biome"])), "sections_per_biome": dict(sorted(data["sections_per_biome"].items(), key=lambda kv: -kv[1])), "land_km2": round(int(land.sum()) / 1e6, 2), "spawn": [level.get("SpawnX"), level.get("SpawnY"), level.get("SpawnZ")], **border, } if not args.no_map and not args.sample: report["land_biomes_percent"] = render_map(data, level, preset, args.map) report["map"] = str(args.map) 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() 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()]) if failures: print("\nWORLD CHECK FAILED") print("\n".join(f" - {f}" for f in failures)) 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, 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")) if __name__ == "__main__": main()