#!/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. trades; the ore veins (world/vein-sources.json, written by tools/zone_ores.py) are always added """ 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 VEIN_SOURCES = ROOT / "world/vein-sources.json" # the regenerating ore veins (tools/zone_ores.py), always read 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()) extras = [VEIN_SOURCES] + args.extra if VEIN_SOURCES.exists() else args.extra model = Model(stack, log=lambda msg: print(" " + msg), extras=extras, 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()