Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus the Kotlin companion mod (cobblesurvival) and its built-in data pack. - Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and export tooling, Prism instance sync, test server with smoke checks. - Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26), Adrian's biome list, no natural ore, structures without ore or vanilla mobs, no Nether portals, world template builder and world report. - Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap fixes), Revival Charm, expensive Healing Machine, Lootr refreshing loot, in-game self-tests on Minecraft's GameTest framework. - Phase 3 so far: 14 zones with their own spawn pools and level bands (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition), Cold Sweat heat and cold as gates with boss-drop insulation recipes, spawn survey and hazard self-tests in tools/quick_check.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
581 lines
33 KiB
Python
581 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)
|
|
|
|
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)}
|