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>
591 lines
33 KiB
Python
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)}
|