Files
claudeandClaude Opus 5.5 1a1fb93da8 Phase 4: veins in every chunk, drops for the mobs the island lacks
Ore veins: make_world.py loads the island again row by row until
Regenerating Ore Veins has filled every chunk (/cobblesurvival veins
unfilled), cave tiers sit at their biome's depths, small zones get denser
veins, showcase veins are full size. The world report checks surface
tiers against the zone grid, cave tiers within two biome cells of their
cave, and fails on unfilled chunks; the template passes (37,689 chunks).

Drops: tools/drop_audit.py checks every creature drop a recipe or brewing
needs (docs/pokemon-drops.md). Blaze rods, ghast tears, breeze rods,
shulker shells and nether stars now drop from Pokemon through species
additions (self-test drop_additions_apply). The field guide counts vanilla
mobs as absent, applies species additions and reads the veins as sources.

Cobblebase tried (loads, self-tests pass) and left out until Adrian
decides its scope. Phase 4 checklist with the open questions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:36:50 +01:00

155 lines
7.5 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. 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()