Phase 8: the pack's own JEI pages for every drop source

Adrian, 2026-11-02/03: every item that can drop gets a JEI page naming its
sources, alongside the Cobblemon Info add-on (which shows drops and biomes,
not zones). The companion mod's JEI plugin (jei/DropSourcesJei.kt) adds:
- Pokemon drops on the island: 794 species of lines that live on the
  island, with chances, amounts and the zones they spawn in (or where their
  line lives); spawns with their own drops (Beheeyem in the End). The Drop
  Synthesizer core is the catalyst.
- Guardians and the End boss; Sacrifice Altar tiers and specials; Rebirth
  Chamber; zone ore veins with regrow times; Cobblebase pasture finds per
  skill and zone.

JEI runs on the client, so the pages read assets/cobblesurvival/jei/
drop_sources.json inside the mod, written by tools/jei_sources.py from the
pack's files. Self-tests drop_sources_match_the_server (file vs live server:
drops, zones, own-drop spawns, guardians, End boss, altar) and
jei_pages_build (every page builds, every item exists). The ore a vein gives
moved into world/zones.json (ores -> item), shared with the quest book.
JEI's API (19.64.0.455) is a compile-only dependency from JEI's own Maven.
Self-tests and the island check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-03 11:58:18 +01:00
1 parent c5d1cf93f4
commit 72965caedf
13 files changed
+19637 -49

No files matched your search

+174
View File
@@ -0,0 +1,174 @@
#!/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()