Files
claudeandClaude Opus 5.5 1a1fb93da8 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>
2026-10-30 03:36:50 +01:00

591 lines
33 KiB
Python

"""What can exist on the island: dimensions, biomes, structures, natural blocks, mobs.
Everything is derived from data: world/biomes.json and the world preset for
biomes and dimensions, structure and structure-set JSON for structures, biome
feature lists and NeoForge biome modifiers for natural blocks, biome spawners
and structure spawn overrides for mobs. Minecraft and Cobblemon decide a few
things in code; those are the small tables below, each saying where it is from.
Statuses: "available" (on the island), "conditional" (needs something that may
not exist, e.g. the End or a stronghold inside the border), "unavailable".
"""
import json
import re
from . import nbt
from .datasource import ROOT, norm_id
RANK = {"unavailable": 0, "conditional": 1, "available": 2}
END_BIOMES = ["minecraft:the_end", "minecraft:end_highlands", "minecraft:end_midlands",
"minecraft:small_end_islands", "minecraft:end_barrens"]
NETHER_BIOMES = ["minecraft:nether_wastes", "minecraft:soul_sand_valley", "minecraft:crimson_forest",
"minecraft:warped_forest", "minecraft:basalt_deltas"]
# Cobblemon 1.8.4 adds these placed features from code (CobblemonPlacedFeatures.register()),
# each to the biomes of one tag. None = all biomes; the feature filters itself.
COBBLEMON_CODED_FEATURES = {
"apricorn_trees": ["#cobblemon:has_feature/apricorns_dense", "#cobblemon:has_feature/apricorns_normal",
"#cobblemon:has_feature/apricorns_sparse"], # ApricornTreeFeature checks these
"saccharine_tree": ["#cobblemon:has_feature/saccharine_tree"],
"mints": None, "medicinal_leek": None,
"big_root": ["#minecraft:is_overworld"], "revival_herb": ["#cobblemon:has_feature/revival_herbs"],
"berry_groves": ["#minecraft:is_overworld"], "type_gems": ["#minecraft:is_overworld"],
"swamp_grains": ["#cobblemon:is_swamp"], "plains_grains": ["#cobblemon:is_plains"],
"galarica_nuts": ["#cobblemon:is_beach"], "habitats/treasure_hoard": ["#minecraft:is_overworld"],
"habitats/abandoned_fortress": ["#minecraft:is_nether"], "habitats/ancient_wellsprings": ["#cobblemon:is_lush"],
"habitats/chorus_briars": ["#cobblemon:is_end"], "habitats/deep_roots": ["#cobblemon:is_lush"],
"habitats/earthen_hives": ["#cobblemon:is_dripstone"], "habitats/exposed_geodes": ["#minecraft:is_overworld"],
"habitats/lost_ruins": ["#minecraft:is_overworld"], "habitats/quartz_spikes": ["#cobblemon:nether/is_wasteland"],
"habitats/rocky_tidepools": ["#cobblemon:is_stony_beach"], "habitats/root_nurseries": ["#cobblemon:is_swamp"],
"habitats/sandy_tidepools": ["#cobblemon:is_beach"], "habitats/skeletal_fall": ["#cobblemon:nether/is_soul_sand"],
"habitats/strange_crimson_fungal_dwellings": ["#cobblemon:nether/is_wasteland"],
"habitats/strange_warped_fungal_dwellings": ["#cobblemon:nether/is_wasteland"],
"habitats/volcanic_plumes": ["#minecraft:is_nether"], "habitats/rocky_outcrops": ["#cobblemon:is_mountain"],
"fossils/prehistoric_birch_tree": ["#cobblemon:has_block/birch_log"],
"fossils/prehistoric_dripstone_oasis": ["#cobblemon:is_dripstone"],
"fossils/prehistoric_enhydro_agate": ["#cobblemon:is_lush"],
"fossils/prehistoric_eroded_pillar": ["#cobblemon:has_block/red_sand"],
"fossils/prehistoric_frozen_pond": ["#cobblemon:is_glacial"],
"fossils/prehistoric_frozen_spike": ["#cobblemon:is_glacial"],
"fossils/prehistoric_hydrothermal_vents": ["#cobblemon:is_temperate_ocean"],
"fossils/prehistoric_lush_den": ["#cobblemon:is_jungle"], "fossils/prehistoric_mossy_pond": ["#cobblemon:is_lush"],
"fossils/prehistoric_mud_pit": ["#cobblemon:is_jungle"], "fossils/prehistoric_oak_tree": ["#cobblemon:has_block/oak_log"],
"fossils/prehistoric_powdered_deposit": ["#cobblemon:is_snowy"],
"fossils/prehistoric_rooted_pits": ["#cobblemon:is_swamp"], "fossils/prehistoric_sandy_den": ["#cobblemon:has_block/sand"],
"fossils/prehistoric_spruce_tree": ["#cobblemon:has_block/spruce_log"],
"fossils/prehistoric_submerged_impact": ["#cobblemon:is_temperate_ocean"],
"fossils/prehistoric_submerged_spike": ["#cobblemon:is_frozen_ocean"],
"fossils/prehistoric_preserved_skeleton": ["#cobblemon:is_frozen_ocean"],
"fossils/prehistoric_sunscorched_den": ["#cobblemon:has_block/red_sand"],
"fossils/prehistoric_sunscorched_remains": ["#cobblemon:has_block/red_sand"],
"fossils/prehistoric_suspicious_mounds": ["#cobblemon:is_plains"],
"fossils/prehistoric_underwater_fissure": ["#cobblemon:is_temperate_ocean"],
"fossils/prehistoric_vibrant_hydrothermal_vents": ["#cobblemon:is_warm_ocean"],
"ruins/crumbling_arch": ["#minecraft:is_overworld"], "ruins/deep_crypt": ["#cobblemon:is_deep_dark"],
"ruins/rooted_arch": ["#cobblemon:is_lush"],
}
# Blocks placed by feature (or tree decorator) code that do not appear in the feature's JSON,
# keyed by feature type. Lists may hold #block tags; a string is a regex over block ids.
CODE_FEATURE_BLOCKS = {
"cobblemon:apricorn_tree": ["cobblemon:apricorn_log", "cobblemon:apricorn_leaves"],
"cobblemon:saccharine_tree": ["cobblemon:saccharine_log", "cobblemon:saccharine_leaves"],
"cobblemon:type_gem": r"cobblemon:[a-z]+_gem_cluster", # TypeGemFeature picks a random gem cluster
"minecraft:glowstone_blob": ["minecraft:glowstone"],
"minecraft:basalt_columns": ["minecraft:basalt"], "minecraft:basalt_pillar": ["minecraft:basalt"],
"minecraft:ice_spike": ["minecraft:packed_ice"], "minecraft:iceberg": ["minecraft:packed_ice", "minecraft:blue_ice"],
"minecraft:blue_ice": ["minecraft:blue_ice"],
"minecraft:end_spike": ["minecraft:obsidian", "minecraft:iron_bars"], "minecraft:end_island": ["minecraft:end_stone"],
"minecraft:chorus_plant": ["minecraft:chorus_plant", "minecraft:chorus_flower"],
"minecraft:kelp": ["minecraft:kelp", "minecraft:kelp_plant"],
"minecraft:seagrass": ["minecraft:seagrass", "minecraft:tall_seagrass"], "minecraft:sea_pickle": ["minecraft:sea_pickle"],
"minecraft:coral_tree": ["#minecraft:coral_blocks", "#minecraft:corals", "#minecraft:wall_corals", "minecraft:sea_pickle"],
"minecraft:coral_claw": ["#minecraft:coral_blocks", "#minecraft:corals", "#minecraft:wall_corals", "minecraft:sea_pickle"],
"minecraft:coral_mushroom": ["#minecraft:coral_blocks", "#minecraft:corals", "#minecraft:wall_corals", "minecraft:sea_pickle"],
"minecraft:vines": ["minecraft:vine"], "minecraft:bamboo": ["minecraft:bamboo", "minecraft:podzol"],
"minecraft:monster_room": ["minecraft:cobblestone", "minecraft:mossy_cobblestone", "minecraft:spawner"],
"minecraft:desert_well": ["minecraft:sandstone", "minecraft:sandstone_slab", "minecraft:suspicious_sand"],
"minecraft:dripstone_cluster": ["minecraft:dripstone_block", "minecraft:pointed_dripstone"],
"minecraft:pointed_dripstone": ["minecraft:pointed_dripstone"], "minecraft:large_dripstone": ["minecraft:dripstone_block"],
"minecraft:sculk_patch": ["minecraft:sculk", "minecraft:sculk_vein", "minecraft:sculk_sensor",
"minecraft:sculk_shrieker", "minecraft:sculk_catalyst"],
"minecraft:underwater_magma": ["minecraft:magma_block"], "minecraft:freeze_top_layer": ["minecraft:ice", "minecraft:snow"],
"minecraft:beehive": ["minecraft:bee_nest"], "minecraft:cocoa": ["minecraft:cocoa"],
"minecraft:leave_vine": ["minecraft:vine"], "minecraft:trunk_vine": ["minecraft:vine"],
}
# Loot tables that Minecraft's structure and feature code references directly (not via templates).
CODE_LOOT = {
"minecraft:desert_pyramid": ["chests/desert_pyramid", "archaeology/desert_pyramid"],
"minecraft:jungle_temple": ["chests/jungle_temple", "chests/jungle_temple_dispenser"],
"minecraft:igloo": ["chests/igloo_chest"],
"minecraft:shipwreck": ["chests/shipwreck_map", "chests/shipwreck_supply", "chests/shipwreck_treasure"],
"minecraft:ocean_ruin": ["chests/underwater_ruin_big", "chests/underwater_ruin_small"],
"minecraft:buried_treasure": ["chests/buried_treasure"],
"minecraft:stronghold": ["chests/stronghold_corridor", "chests/stronghold_crossing", "chests/stronghold_library"],
"minecraft:mineshaft": ["chests/abandoned_mineshaft"],
"minecraft:fortress": ["chests/nether_bridge"],
"minecraft:woodland_mansion": ["chests/woodland_mansion"],
"minecraft:end_city": ["chests/end_city_treasure"],
"minecraft:ruined_portal": ["chests/ruined_portal"],
# placed features
"minecraft:monster_room": ["chests/simple_dungeon"], "minecraft:monster_room_deep": ["chests/simple_dungeon"],
"minecraft:desert_well": ["archaeology/desert_well"],
}
# Notable blocks that code-built structures place (only those that matter as a resource).
CODE_STRUCTURE_BLOCKS = {
"minecraft:ocean_monument": ["minecraft:gold_block", "minecraft:wet_sponge", "minecraft:sea_lantern",
"minecraft:prismarine", "minecraft:prismarine_bricks", "minecraft:dark_prismarine"],
"minecraft:mineshaft": ["minecraft:rail", "minecraft:cobweb", "minecraft:oak_planks", "minecraft:oak_fence"],
"minecraft:desert_pyramid": ["minecraft:tnt", "minecraft:suspicious_sand"],
"minecraft:stronghold": ["minecraft:end_portal_frame", "minecraft:bookshelf", "minecraft:iron_bars"],
"minecraft:jungle_temple": ["minecraft:dispenser", "minecraft:tripwire_hook", "minecraft:redstone_wire"],
"minecraft:ocean_ruin": ["minecraft:suspicious_sand", "minecraft:suspicious_gravel"],
"minecraft:ruined_portal": ["minecraft:obsidian", "minecraft:crying_obsidian", "minecraft:netherrack",
"minecraft:magma_block", "minecraft:gold_block"],
}
# Mobs that Minecraft spawns from code rather than biome spawners (kind, requirement, note).
SPECIAL_MOBS = {
"minecraft:villager": ("structure", "#minecraft:village", "lives in villages"),
"minecraft:iron_golem": ("structure", "#minecraft:village", "spawns in villages"),
"minecraft:cat": ("structure", "#minecraft:village", "strays in villages"),
"minecraft:wandering_trader": ("overworld", None, "visits players"),
"minecraft:trader_llama": ("overworld", None, "comes with the wandering trader"),
"minecraft:phantom": ("overworld", None, "attacks players who have not slept for three nights"),
"minecraft:pillager": ("overworld", None, "patrols"),
"minecraft:vindicator": ("structure", "#minecraft:village", "raids"),
"minecraft:evoker": ("structure", "#minecraft:village", "raids"),
"minecraft:ravager": ("structure", "#minecraft:village", "raids"),
"minecraft:witch": ("structure", "#minecraft:village", "raids"),
"minecraft:skeleton_horse": ("overworld", None, "skeleton traps in thunderstorms"),
"minecraft:zombie_villager": ("overworld", None, "zombie variant"),
"minecraft:warden": ("biome", "minecraft:deep_dark", "summoned by sculk shriekers"),
"minecraft:elder_guardian": ("structure", "minecraft:monument", "ocean monuments"),
"minecraft:ender_dragon": ("biome", "minecraft:the_end", "the End"),
"minecraft:shulker": ("structure", "minecraft:end_city", "End cities"),
"minecraft:breeze": ("structure", "minecraft:trial_chambers", "trial spawners"),
"minecraft:sniffer": ("item", "minecraft:sniffer_egg", "hatched from a sniffer egg"),
}
class Island:
def __init__(self, stack, tags, log=print):
self.stack, self.tags, self.log = stack, tags, log
self.btags = tags["worldgen/biome"]
self.notes = []
self._load_biomes()
self._load_structures()
self._load_features()
self._load_mobs()
# --- dimensions and biomes -----------------------------------------------------------
def _load_biomes(self):
cfg = json.loads((ROOT / "world/biomes.json").read_text())
world = json.loads((ROOT / "world/world.json").read_text())
self.world_type = world.get("level_type", "minecraft:normal")
preset = self.stack.get("worldgen/world_preset", self.world_type) or {}
dims = preset.get("dimensions", {})
self.dimensions = sorted(dims) or ["minecraft:overworld", "minecraft:the_nether", "minecraft:the_end"]
water = [norm_id(b) for b in cfg.get("water", [])]
allowed = [norm_id(b) for b in cfg.get("allowed", [])]
preset_biomes = set(re.findall(r'"biome":\s*"([^"]+)"', json.dumps(dims.get("minecraft:overworld", {}))))
extra = preset_biomes - set(allowed) - set(water)
if extra:
self.notes.append(f"world preset lists biomes not in world/biomes.json: {sorted(extra)}")
added_caves = {norm_id(a["biome"]) for a in cfg.get("add", [])
if a.get("parameters", {}).get("depth", [0, 0])[0] > 0}
caves = self.btags.resolve_all(["#c:is_underground", "#c:is_cave"]) | added_caves
self.biomes = {}
for b in allowed + water:
kind = "water" if b in water else "cave" if b in caves else "surface"
self.biomes[b] = {"dimension": "minecraft:overworld", "kind": kind, "status": "available"}
if "minecraft:the_end" in dims or not dims:
for b in END_BIOMES:
self.biomes[b] = {"dimension": "minecraft:the_end", "kind": "end", "status": "conditional",
"reason": "the End: needs a stronghold portal and Eyes of Ender"}
if "minecraft:the_nether" in dims or not dims:
for b in NETHER_BIOMES:
self.biomes.setdefault(b, {"dimension": "minecraft:the_nether", "kind": "nether", "status": "available"})
self.overworld = {b for b, v in self.biomes.items() if v["dimension"] == "minecraft:overworld"}
self.has_nether = "minecraft:the_nether" in dims
def biome_status(self, biomes):
best = "unavailable"
for b in biomes:
s = self.biomes.get(b, {}).get("status", "unavailable")
if RANK[s] > RANK[best]:
best = s
return best
def island_biomes(self, biomes):
return sorted(b for b in biomes if b in self.biomes)
# --- structures ------------------------------------------------------------------------
def _load_structures(self):
in_set, rings = set(), set()
for sid, rel in self.stack.ids("worldgen/structure_set").items():
d = self.stack.json(rel) or {}
members = [s["structure"] for s in d.get("structures", [])] if isinstance(d.get("structures"), list) else []
in_set.update(norm_id(m) for m in members)
if d.get("placement", {}).get("type") == "minecraft:concentric_rings":
rings.update(norm_id(m) for m in members)
stags = self.tags["worldgen/structure"]
self.structures = {}
for sid, rel in self.stack.ids("worldgen/structure").items():
d = self.stack.json(rel) or {}
biomes = self.btags.resolve_all(d.get("biomes", []))
here = self.island_biomes(biomes)
status, reason = self.biome_status(here), ""
if sid not in in_set:
status, reason = "unavailable", "not in any structure set (never generates)"
elif not here:
reason = "its biomes are not on the island"
elif sid in rings and status == "available":
status, reason = "conditional", "generates in rings 1,280+ blocks from the origin; may be outside the world border"
elif status == "conditional":
reason = "only in the End"
self.structures[sid] = {"type": d.get("type"), "biomes": here, "status": status, "reason": reason,
"tags": [], "loot": set(), "blocks": set(), "mobs": set(), "data": d}
for tag in stags.all_tags():
for sid in stags.resolve(tag):
if sid in self.structures:
self.structures[sid]["tags"].append(tag)
for sid, s in self.structures.items():
if s["status"] == "unavailable":
continue
contents = self.walk(s["data"], start=sid)
s["loot"] |= contents["loot"]
s["blocks"] |= contents["blocks"]
s["mobs"] |= contents["entities"]
s["loot"] |= {f"minecraft:{p}" for p in CODE_LOOT.get(s["type"], [])}
if s["type"] == "minecraft:ocean_ruin":
s["loot"].add(f"minecraft:archaeology/ocean_ruin_{s['data'].get('biome_temp', 'cold')}")
s["blocks"] |= set(CODE_STRUCTURE_BLOCKS.get(sid, []) + CODE_STRUCTURE_BLOCKS.get(s["type"], []))
for cat in s["data"].get("spawn_overrides", {}).values():
s["mobs"] |= {norm_id(sp["type"]) for sp in cat.get("spawns", [])}
def structure_status(self, refs):
"""Best status over structure ids or #tags (for spawn conditions and mob rules)."""
stags = self.tags["worldgen/structure"]
best, which = "unavailable", []
for ref in refs:
for sid in stags.resolve(ref):
s = self.structures.get(sid)
if s and RANK[s["status"]] > 0:
which.append(sid)
if RANK[s["status"]] > RANK[best]:
best = s["status"]
return best, sorted(set(which))
def walk(self, obj, start=""):
"""Follow template pools, processor lists, NBT templates and features from a JSON object;
collect loot tables, palette blocks and entity ids. Unknown strings are ignored."""
out = {"loot": set(), "blocks": set(), "entities": set()}
replaced = set() # template marker blocks that rule processors always swap out
rules = [] # (input block or None, output blocks) of rule processors
loot_ids = self.stack.ids("loot_table")
seen = set()
todo = list(_strings(obj))
while todo:
s = todo.pop()
if not isinstance(s, str) or ":" not in s and "/" not in s or s in seen:
continue
seen.add(s)
rid = norm_id(s)
if rid in loot_ids:
out["loot"].add(rid)
continue
for kind in ("worldgen/template_pool", "worldgen/processor_list", "worldgen/configured_feature",
"worldgen/placed_feature"):
d = self.stack.get(kind, rid)
if d is not None:
todo.extend(_strings(d))
if kind.endswith("feature"):
out["blocks"] |= _block_names(d)
elif kind.endswith("processor_list"):
found_rules, removed = _processor_blocks(d)
rules.extend(found_rules)
replaced |= removed
nbt_rel = self.stack.resource("structure", rid, ext=".nbt")
if self.stack.exists(nbt_rel):
try:
tpl = nbt.load(self.stack.read(nbt_rel))
except Exception as e: # a broken template must not stop the guide
self.notes.append(f"unreadable structure template {nbt_rel}: {e}")
continue
palettes = tpl.get("palettes") or [tpl.get("palette", [])]
for pal in palettes:
out["blocks"] |= {norm_id(p["Name"]) for p in pal if "Name" in p}
for b in tpl.get("blocks", []):
if "nbt" in b:
todo.extend(nbt.strings(b["nbt"]))
for e in tpl.get("entities", []):
eid = e.get("nbt", {}).get("id")
if eid:
out["entities"].add(norm_id(eid))
grew = True
while grew: # a rule only places its output where its input block exists
before = len(out["blocks"])
for inp, outs in rules:
if inp is None or inp in out["blocks"]:
out["blocks"] |= outs
grew = len(out["blocks"]) > before
out["blocks"] = {b for b in out["blocks"] - replaced if b not in (
"minecraft:air", "minecraft:structure_void", "minecraft:jigsaw", "minecraft:cave_air")}
return out
# --- natural blocks (features + terrain) -------------------------------------------------
def _load_features(self):
biome_features = {b: set() for b in self.biomes}
for b in self.biomes:
d = self.stack.get("worldgen/biome", b) or {}
for step in d.get("features", []):
biome_features[b].update(norm_id(f) for f in step if isinstance(f, str))
for name, refs in COBBLEMON_CODED_FEATURES.items():
# None means every biome of every dimension; the feature's own checks (dirt, water) keep it
# out of the End, so only overworld biomes are counted.
for b in (self.btags.resolve_all(refs) if refs else self.overworld) & set(self.biomes):
biome_features[b].add(f"cobblemon:{name}")
self.biome_mods = []
removed = set()
for mid, rel in self.stack.ids("neoforge/biome_modifier").items():
d = self.stack.json(rel) or {}
t = d.get("type")
if t not in ("neoforge:add_features", "neoforge:remove_features", "neoforge:add_spawns", "neoforge:remove_spawns"):
continue
biomes = self.holder_set(d.get("biomes"), self.btags)
self.biome_mods.append((mid, t, biomes, d))
if t.endswith("_features"):
feats = self.holder_set(d.get("features"), self.tags["worldgen/placed_feature"])
for b in biomes & set(self.biomes):
if t == "neoforge:add_features":
biome_features[b] |= feats
else:
removed |= biome_features[b] & feats
biome_features[b] -= feats
self.biome_features = biome_features
# Blocks that would generate if the pack had not switched their features off (ores):
# features removed by a biome modifier, and placed features overridden to zero placements.
for fid in self.stack.ids("worldgen/placed_feature"):
d = self.stack.get("worldgen/placed_feature", fid) or {}
if any(p.get("type") == "minecraft:count" and p.get("count") == 0 for p in d.get("placement", [])):
removed.add(fid)
self.removed_blocks = {}
for fid in removed:
for blk in self._feature_blocks(fid, ignore_zero=True)[0]:
self.removed_blocks.setdefault(blk, set()).add(fid)
self.natural = {} # block -> {"biomes": set, "via": set}
self.feature_loot = {} # loot table -> {"biomes", "via"}
feature_cache = {}
for b, feats in biome_features.items():
for f in feats:
if f not in feature_cache:
feature_cache[f] = self._feature_blocks(f)
blocks, filt, loot = feature_cache[f]
if filt and not filt(b):
continue
for blk in blocks:
n = self.natural.setdefault(blk, {"biomes": set(), "via": set()})
n["biomes"].add(b)
n["via"].add(f)
for lt in loot:
n = self.feature_loot.setdefault(lt, {"biomes": set(), "via": set()})
n["biomes"].add(b)
n["via"].add(f)
# terrain: whatever the noise settings place (stone, deepslate, sand, terracotta, ...)
for dim, biomes in (("minecraft:overworld", self.overworld),
("minecraft:the_end", {b for b in self.biomes if self.biomes[b]["kind"] == "end"})):
preset = self.stack.get("worldgen/world_preset", self.world_type) or {}
settings = preset.get("dimensions", {}).get(dim, {}).get("generator", {}).get("settings")
d = settings if isinstance(settings, dict) else self.stack.get("worldgen/noise_settings", settings or "")
for blk in _block_names(d or {}):
n = self.natural.setdefault(blk, {"biomes": set(), "via": set()})
n["biomes"] |= biomes
n["via"].add("terrain")
for blk in ("minecraft:bedrock", "minecraft:water", "minecraft:lava", "minecraft:grass_block", "minecraft:dirt"):
self.natural.setdefault(blk, {"biomes": set(self.overworld), "via": {"terrain"}})
for sid, s in self.structures.items():
if RANK[s["status"]] == 0:
continue
for blk in s["blocks"]:
n = self.natural.setdefault(blk, {"biomes": set(), "via": set()})
n["via"].add(f"structure:{sid}")
def _feature_blocks(self, fid, ignore_zero=False):
"""(blocks, biome filter, loot tables) for a placed feature, or nothing if it was zeroed."""
d = self.stack.get("worldgen/placed_feature", fid)
if d is None:
return set(), None, set()
placement = d.get("placement", [])
if not ignore_zero and any(p.get("type") == "minecraft:count" and p.get("count") == 0 for p in placement):
return set(), None, set()
included, excluded = self._biome_predicates(d)
blocks, loot, seen = set(), set(), set()
todo = [d.get("feature")]
while todo:
f = todo.pop()
if isinstance(f, str):
if f in seen:
continue
seen.add(f)
f = self.stack.get("worldgen/configured_feature", f) or self.stack.get("worldgen/placed_feature", f)
if not isinstance(f, dict):
continue
blocks |= _block_names(f)
inc, exc = self._biome_predicates(f)
included = included if inc is None else (inc if included is None else included & inc)
excluded |= exc
for node in _dicts(f):
extra = CODE_FEATURE_BLOCKS.get(node.get("type"))
if isinstance(extra, list):
blocks |= self.tags["block"].resolve_all(extra)
elif isinstance(extra, str):
pat = re.compile(extra)
blocks |= {k.replace(":blocks/", ":") for k in self.stack.ids("loot_table")
if ":blocks/" in k and pat.fullmatch(k.replace(":blocks/", ":"))}
if node.get("type") == "minecraft:multiface_growth" and isinstance(node.get("config", {}).get("block"), str):
blocks.add(norm_id(node["config"]["block"]))
if f.get("type") in ("cobblemon:structure", "minecraft:fossil"):
w = self.walk(f.get("config", {}))
blocks |= w["blocks"]
loot |= w["loot"]
for s in _strings(f):
if s != fid and (self.stack.get("worldgen/configured_feature", s) is not None
or self.stack.get("worldgen/placed_feature", s) is not None):
todo.append(s)
loot |= {f"minecraft:{p}" for p in CODE_LOOT.get(fid, [])}
def filt(b):
return (included is None or b in included) and b not in excluded
return blocks, (filt if included is not None or excluded else None), loot
def _biome_predicates(self, d):
"""Cobblemon's 'cobblemon:biome' predicates (includedBiomes/excludedBiomes) anywhere in a feature."""
inc, exc = None, set()
for node in _dicts(d):
if node.get("type") == "cobblemon:biome":
if node.get("includedBiomes"):
s = self.btags.resolve_all(node["includedBiomes"])
inc = s if inc is None else inc | s
exc |= self.btags.resolve_all(node.get("excludedBiomes", []))
return inc, exc
def holder_set(self, value, tags):
"""NeoForge holder sets: id, #tag, list, or {"type": "neoforge:any"|"and"|"or"|"not", ...}."""
if value is None:
return set()
if isinstance(value, str):
return tags.resolve(value) if value.startswith("#") else {norm_id(value)}
if isinstance(value, list):
out = set()
for v in value:
out |= self.holder_set(v, tags)
return out
t = value.get("type")
if t == "neoforge:any":
return set(self.biomes) if tags is self.btags else {"*"}
if t == "neoforge:or":
return set().union(*(self.holder_set(v, tags) for v in value.get("values", [])))
if t == "neoforge:and":
sets = [self.holder_set(v, tags) for v in value.get("values", [])]
return set.intersection(*sets) if sets else set()
if t == "neoforge:not":
base = set(self.biomes) if tags is self.btags else set()
return base - self.holder_set(value.get("value"), tags)
return set()
def natural_status(self, block):
n = self.natural.get(block)
if not n:
return "unavailable", []
biomes = self.island_biomes(n["biomes"])
st = self.biome_status(biomes)
for via in n["via"]:
if via.startswith("structure:"):
s = self.structures[via[10:]]["status"]
if RANK[s] > RANK[st]:
st = s
return st, biomes
# --- mobs --------------------------------------------------------------------------------
def _load_mobs(self):
self.mobs = {} # entity -> list of {"via": text, "status": s, "biomes": [...]} (item-gated ones resolved later)
for b in self.biomes:
d = self.stack.get("worldgen/biome", b) or {}
for cat, spawns in d.get("spawners", {}).items():
for sp in spawns:
self._add_mob(norm_id(sp["type"]), "biome", b, self.biomes[b]["status"])
for mid, t, biomes, d in self.biome_mods:
if t == "neoforge:add_spawns":
sps = d.get("spawners", [])
for sp in sps if isinstance(sps, list) else [sps]:
for b in biomes & set(self.biomes):
self._add_mob(norm_id(sp["type"]), "biome", b, self.biomes[b]["status"])
for sid, s in self.structures.items():
if RANK[s["status"]]:
for m in s["mobs"]:
self._add_mob(m, "structure", sid, s["status"])
self.item_mobs = {}
for mob, (kind, req, note) in SPECIAL_MOBS.items():
if kind == "overworld":
self._add_mob(mob, "special", note, "available")
elif kind == "biome" and req in self.biomes:
self._add_mob(mob, "special", note, self.biomes[req]["status"])
elif kind == "structure":
st, which = self.structure_status([req])
if RANK[st]:
self._add_mob(mob, "special", note, st)
elif kind == "item":
self.item_mobs[mob] = (req, note)
# The companion mod lets only Cobblemon's entities join a world (world/OnlyPokemon.kt), apart from the
# entity type tag cobblesurvival:allowed_vanilla_mobs; every other mob never appears, whatever spawns it.
self.allowed_mobs = self.tags["entity_type"].resolve("#cobblesurvival:allowed_vanilla_mobs")
for mob, entries in self.mobs.items():
if not self.mob_allowed(mob):
for e in entries:
e["status"] = "unavailable"
def mob_allowed(self, mob):
return mob.startswith("cobblemon:") or mob in self.allowed_mobs
def _add_mob(self, mob, kind, where, status):
lst = self.mobs.setdefault(mob, [])
for e in lst:
if e["kind"] == kind and kind == "biome":
e["where"].add(where)
if RANK[status] > RANK[e["status"]]:
e["status"] = status
return
lst.append({"kind": kind, "where": {where}, "status": status})
def mob_status(self, mob):
best = "unavailable"
for e in self.mobs.get(mob, []):
if RANK[e["status"]] > RANK[best]:
best = e["status"]
return best
def _strings(obj):
if isinstance(obj, str):
yield obj
elif isinstance(obj, dict):
for v in obj.values():
yield from _strings(v)
elif isinstance(obj, list):
for v in obj:
yield from _strings(v)
def _dicts(obj):
if isinstance(obj, dict):
yield obj
for v in obj.values():
yield from _dicts(v)
elif isinstance(obj, list):
for v in obj:
yield from _dicts(v)
def _processor_blocks(plist):
"""([(input block or None, output blocks)], marker blocks the list always replaces)."""
rules, replaced = [], set()
procs = plist.get("processors", []) if isinstance(plist, dict) else plist
for proc in procs if isinstance(procs, list) else []:
if proc.get("processor_type") == "minecraft:block_ignore":
replaced |= _block_names(proc)
for rule in proc.get("rules", []):
pred = rule.get("input_predicate", {})
inp = norm_id(pred["block"]) if isinstance(pred.get("block"), str) else None
if pred.get("predicate_type") == "minecraft:blockstate_match":
inp = next(iter(_block_names(pred)), None)
rules.append((inp, _block_names(rule.get("output_state", {}))))
certain = pred.get("predicate_type") in ("minecraft:block_match", "minecraft:blockstate_match") or (
pred.get("predicate_type") == "minecraft:random_block_match" and pred.get("probability", 0) >= 1)
if certain and inp:
replaced.add(inp)
return rules, replaced
def _block_names(obj):
"""Block ids a feature or surface rule places: every {"Name": ...} block state in it."""
return {norm_id(d["Name"]) for d in _dicts(obj) if isinstance(d.get("Name"), str)}