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>
152 lines
7.2 KiB
Python
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()
|