Files
cobblemon-survival-pack/tools/fieldguide/build.py
T
claudeandClaude Opus 5.5 398301f0be Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards
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>
2026-10-29 19:01:21 +01:00

152 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Generate the field guide: where every Pokémon lives on the island, how to get every mod item,
and what each item is for, from the pack's own data.
Reads vanilla data (build/server/libraries), NeoForge, every jar listed in pack/mods (copied to
build/fieldguide-cache/jars from build/stage/full, build/server or the Prism instance), then
datapacks/core on top, plus world/biomes.json and world/world.json for the island. The recipes the
pack's KubeJS scripts add, change or remove come from `/kubejs export` on the test server
(kubejs_export.py); when that export is older than the pack, this boots the test server once to
refresh it (about a minute) unless --no-server is given.
Writes build/field-guide/:
fieldguide.json the single source every renderer reads
index.html, pokemon.html, items.html, data.js, assets/, icons/ the HTML guide
ingame/ftbquests/ FTB Quests "Field Guide" chapters (not installed)
ingame/kubejs/ Pokédex "Found in" lang override (not installed)
Usage: tools/fieldguide/build.py [--out DIR] [--no-icons] [--no-server] [--from-json FILE] [--extra FILE ...]
--no-server never start Minecraft: use the last KubeJS recipe export even if the pack changed since
--from-json skip extraction and render an existing fieldguide.json (e.g. one a runtime exporter wrote)
--extra add sources from a JSON file (format in docs/field-guide.md), e.g. ore veins or trades
"""
import argparse
import json
import sys
import time
from pathlib import Path
sys.dont_write_bytecode = True # keep tools/fieldguide free of __pycache__
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from fieldguide import datasource, export_ftbquests, export_lang, guide, kubejs_export, render_html # noqa: E402
from fieldguide.icons import Icons # noqa: E402
from fieldguide.model import Model # noqa: E402
ROOT = Path(__file__).resolve().parent.parent.parent
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--out", type=Path, default=ROOT / "build/field-guide")
ap.add_argument("--no-icons", action="store_true", help="skip rendering item icons")
ap.add_argument("--no-server", action="store_true", help="use the last KubeJS recipe export as it is")
ap.add_argument("--from-json", type=Path, help="render this fieldguide.json instead of extracting")
ap.add_argument("--extra", type=Path, action="append", default=[], help="extra sources JSON (repeatable)")
args = ap.parse_args()
out = args.out
started = time.time()
if args.from_json:
data = json.loads(args.from_json.read_text())
lang = {}
else:
print("reading the pack's data")
stack = datasource.build_stack(log=_log_once())
model = Model(stack, log=lambda msg: print(" " + msg), extras=args.extra, kubejs=_kubejs(args.no_server))
icons = {}
if not args.no_icons:
wanted = set(model.mod_items) | _vanilla_mentions(model)
ic = Icons(stack, out)
icons = ic.write_all(wanted)
print(f" icons: {len(icons)} rendered, {len(ic.missing)} without a usable model")
data = guide.build(model, icons)
guide.write(data, out / "fieldguide.json")
lang = model.lang
render_html.write(data, out)
chapters = export_ftbquests.write(data, out / "ingame")
if lang:
n, missing = export_lang.write(data, lang, out / "ingame")
print(f" Pokédex override: {n} keys" + (f", {len(missing)} keys not in the jar's lang" if missing else ""))
(out / "ingame/README.md").write_text(INGAME_README)
_summary(data, chapters, out, time.time() - started)
def _kubejs(no_server):
"""The pack's KubeJS recipe changes; refreshed from the test server when the pack changed since."""
data = kubejs_export.load()
if data and not data["stale"]:
print(f" KubeJS recipes: export from {data['exported']}")
return data
why = "no KubeJS recipe export yet" if not data else "pack/kubejs, pack/mods or datapacks/core changed since the last KubeJS recipe export"
if no_server:
print(f" warning: {why}; --no-server, so the guide " +
("uses the old export" if data else "shows no pack recipes"))
return data
print(f" {why}: booting the test server for /kubejs export")
try:
return kubejs_export.export(log=lambda msg: print(" " + msg))
except (SystemExit, Exception) as e: # e.g. no EULA, a server already running, mods failing to load
print(f" warning: KubeJS recipe export failed ({type(e).__name__}: {e}); the guide " +
("uses the old export" if data else "shows no pack recipes"))
return data
def _vanilla_mentions(model):
"""Vanilla items that show up in recipes or sources of mod items (they get icons too)."""
out = set()
mods = set(model.mod_items)
for r in model.recipes.values():
if r["result"] in mods or any(i in mods for s in r["slots"] for i in s["items"]):
out.add(r["result"])
for s in r["slots"]:
out.update(s["items"][:8])
for d in model.species.values():
for e in (d.get("drops") or {}).get("entries", []):
out.add(e["item"] if ":" in e["item"] else "minecraft:" + e["item"])
return {i for i in out if i.startswith("minecraft:")}
def _log_once():
seen = []
def log(msg):
if msg.startswith("warning:") and "not found in" in msg:
seen.append(msg.split()[1])
if len(seen) == 1:
print(" some pack/mods jars are not downloaded yet (run tools/build.py); continuing without them")
return
print(" " + msg)
return log
def _summary(data, chapters, out, secs):
sp = data["species"]
items = data["items"]
wild = sum(1 for s in sp.values() if s["wild"] == "available")
other = sum(1 for s in sp.values() if s["wild"] != "available" and s["status"] == "available")
cond = sum(1 for s in sp.values() if s["status"] == "conditional")
none = [s for s in sp.values() if s["status"] == "unavailable"]
nosrc = [i for i, v in items.items() if v["status"] == "unavailable" and not v["technical"]]
print(f"\nPokémon: {wild} wild on the island, {other} more by evolution/fossil/starter, {cond} only in the End, "
f"{len(none)} not obtainable ({sum(1 for s in none if not s['implemented'])} of them not implemented)")
for loc in data["locations"]:
n = len({m["species"] for i in loc["spawns"] for m in data["spawns"][i]["members"]})
print(f" {loc['name']:<24} {loc['kind']:<8} {n:>4} species")
print(f"Items: {len(items)} mod items, {len(nosrc)} with no source on the island")
print(f"FTB Quests chapters: " + ", ".join(f"{k} ({v} quests)" for k, v in chapters.items()))
print(f"Wrote {out} in {secs:.1f}s; open {out / 'index.html'}")
INGAME_README = """# In-game exports (generated, not installed)
- `ftbquests/`: FTB Quests "Field Guide" chapters. See `ftbquests/README.md`.
- `kubejs/`: Pokédex description override that appends "Found in: …". See `kubejs/README.md`.
Both are generated by `python3 tools/fieldguide/build.py` from `../fieldguide.json`.
Installing them is a separate decision; nothing here is copied into pack/.
"""
if __name__ == "__main__":
main()