#!/usr/bin/env python3 """Write the data behind the pack's own JEI pages (roadmap Phase 8; Adrian, 2026-11-02: every item that can drop gets a JEI page naming its sources; 2026-11-03: alongside the Cobblemon Info add-on, which shows spawns and drops but not the island's zones). JEI runs on the client, which on a dedicated server doesn't have the pack's server data, so the pages are built from this file inside the companion mod: mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json. It holds: pokemon every species of a line that lives on the island: its drops (as the Drop Synthesizer rolls them) and the zones it spawns in, or the zones where its line lives spawn_drops zone spawns that drop something else than their species (e.g. Beheeyem in the End) guardians each zone guardian: species, level, drops, return time end_boss the End boss and what beating it gives altar the Sacrifice Altar's level tiers and type specials; rebirth: the Rebirth Chamber's extra ore_veins per zone, what its regenerating veins give and how fast they regrow pasture_finds per Cobblebase skill and zone, what a pasture's Pokémon can find (the pack's zone-bound tables) The self-test drop_sources_match_the_server compares it with the live server, so run this after changing species drops, spawns, guardians, the End boss, the altar, ore veins or Cobblebase's tables, then rebuild the mod. Needs the Cobblemon jar in build/server/mods (any test-server run). Usage: tools/jei_sources.py """ import json import re import zipfile from collections import defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parent.parent DATA = ROOT / "datapacks/core/data" OUT = ROOT / "mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json" SKILLS = {"mining": "mining", "finder_ore": "ore_finder", "finder_evo": "evolution_item_finder"} def drop_entries(drops): """Cobblemon drop table → [{item, chance (percent, absent = always), count ("a-b")}].""" out = [] for e in (drops or {}).get("entries", []): if "item" not in e: continue entry = {"item": e["item"], "count": str(e.get("quantityRange", e.get("quantity", 1)))} if "percentage" in e and float(e["percentage"]) < 100: entry["chance"] = float(e["percentage"]) out.append(entry) return out def species_data(jar): species = {} with zipfile.ZipFile(jar) as z: for name in z.namelist(): m = re.fullmatch(r"data/cobblemon/species/[^/]+/([^/]+)\.json", name) if not m: continue d = json.loads(z.read(name)) pre = d.get("preEvolution") species[f"cobblemon:{m[1]}"] = { "name": d["name"], "implemented": d.get("implemented", False), "drops": d.get("drops"), "pre": f"cobblemon:{pre.split()[0].lower()}" if pre else None, } # The pack's species additions replace a species' drops (datapacks/core/data/cobblesurvival/species_additions). for path in sorted((DATA / "cobblesurvival/species_additions").glob("*.json")): d = json.loads(path.read_text()) if "drops" in d and d["target"] in species: species[d["target"]]["drops"] = d["drops"] return species def root_of(sid, species): seen = set() while species.get(sid, {}).get("pre") and sid not in seen: seen.add(sid) sid = species[sid]["pre"] return sid def main(): jar = next((ROOT / "build/server/mods").glob("Cobblemon-*.jar")) species = species_data(jar) plan = json.loads((ROOT / "world/zones.json").read_text()) zones = {z["id"]: z for z in plan["zones"] + plan["caves"] + plan.get("dimensions", [])} # Where each species spawns, and spawns with drops of their own. where = defaultdict(dict) spawn_drops = [] for zid in zones: pool = DATA / f"cobblesurvival/spawn_pool_world/{zid}.json" if not pool.exists(): continue for spawn in json.loads(pool.read_text())["spawns"]: sid = f"cobblemon:{spawn['pokemon'].split()[0].lower()}" a, _, b = str(spawn["level"]).partition("-") low, high = where[sid].get(zid, (999, 0)) where[sid][zid] = (min(low, int(a)), max(high, int(b or a))) if "drops" in spawn: entry = {"species": sid, "zone": zid, "drops": drop_entries(spawn["drops"])} if entry not in spawn_drops: spawn_drops.append(entry) line_zones = defaultdict(dict) for sid, zs in where.items(): for zid, levels in zs.items(): line_zones[root_of(sid, species)].setdefault(zid, sid) pokemon = [] for sid, s in sorted(species.items()): drops = drop_entries(s["drops"]) root = root_of(sid, species) if not drops or not s["implemented"] or not (where.get(sid) or line_zones.get(root)): continue entry = {"species": sid, "drops": drops, "zones": [{"zone": z, "levels": f"{lo}-{hi}"} for z, (lo, hi) in sorted(where.get(sid, {}).items())]} if not entry["zones"]: entry["line"] = [{"zone": z, "species": member} for z, member in sorted(line_zones[root].items())] pokemon.append(entry) guardians = [] for path in sorted((DATA / "cobblesurvival/guardians").glob("*.json")): g = json.loads(path.read_text()) name, *props = g["pokemon"].split() guardians.append({"zone": g["zone"], "species": f"cobblemon:{name}", "level": int(next(p.split("=")[1] for p in props if p.startswith("level="))), "drops": g["drops"], "respawn_hours": g["respawn_hours"]}) end = json.loads((DATA / "cobblesurvival/bosses/end.json").read_text())["end"] end_boss = {"species": f"cobblemon:{end['species']}", "level": end["level"], "xp_first_win": end["xp_first_win"], "xp": end["xp"], "respawn_hours": end["respawn_hours"], "first_win": [{"item": "minecraft:dragon_egg", "count": 1}]} machines = DATA / "cobblesurvival/machines" altar = json.loads((machines / "altar_core.json").read_text())["altar"] rebirth = json.loads((machines / "rebirth_core.json").read_text())["rebirth"] ore_veins = defaultdict(list) for ore, spec in plan["ores"].items(): ore_veins[spec["zone"]].append({"item": spec["item"], "regen_minutes": spec["regen_minutes"]}) finds = defaultdict(lambda: defaultdict(set)) for path in sorted((DATA / "cobblebase/loot_table").glob("*.json")): prefix, _, tier = path.stem.rpartition("_") skill = SKILLS.get(prefix) if not skill: continue for pool in json.loads(path.read_text()).get("pools", []): for e in pool.get("entries", []): if e.get("type") != "minecraft:item": continue zone_conditions = [c for c in e.get("conditions", []) if c.get("condition") == "cobblesurvival:in_zone"] for zid in (zone_conditions[0]["zones"] if zone_conditions else ["*"]): finds[(skill, zid)][e["name"]].add(tier) pasture_finds = [{"skill": skill, "zone": None if zid == "*" else zid, "items": [{"item": item, "tiers": sorted(tiers)} for item, tiers in sorted(items.items())]} for (skill, zid), items in sorted(finds.items(), key=lambda kv: (kv[0][0], kv[0][1] != "*", kv[0][1]))] data = { "_comment": "Generated by tools/jei_sources.py from the pack's data; do not edit by hand. Read by the companion " "mod's JEI plugin (jei/DropSourcesJei.kt); checked against the live server by the self-test " "drop_sources_match_the_server.", "zones": {zid: {"name": z["name"], "levels": z["levels"]} for zid, z in zones.items()}, "pokemon": pokemon, "spawn_drops": spawn_drops, "guardians": guardians, "end_boss": end_boss, "altar": {"tiers": altar["tiers"], "specials": altar.get("specials", [])}, "rebirth": rebirth.get("extra", []), "ore_veins": [{"zone": z, "items": items} for z, items in sorted(ore_veins.items())], "pasture_finds": pasture_finds, } OUT.parent.mkdir(parents=True, exist_ok=True) OUT.write_text(json.dumps(data, indent=1, ensure_ascii=False) + "\n") print(f"{len(pokemon)} Pokémon, {len(spawn_drops)} spawns with own drops, {len(guardians)} guardians, " f"{len(ore_veins)} zones with veins, {len(pasture_finds)} pasture find lists; wrote {OUT.relative_to(ROOT)}") if __name__ == "__main__": main()