Files
pokemon/scripts/annotate-engines.py
ginnoir 0537ceba5d Sync catalog with vault: engine facet, Jaizu Emerald set, and PC placements.
Add engine annotation + MOC/wiki export support, a PC import staging helper, and refresh catalog.json after placing gated fan-games and new hack notes.
2026-06-23 00:35:16 -05:00

146 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Annotate ROM-hack notes with the engine/base framework they're built on.
Adds an `engine:` frontmatter key + an `· engine **X**` token to the note's
`> [!info]` callout line. Idempotent: re-running skips notes already annotated,
so it's safe to run after future enrichment passes.
WHY a frozen map: engine attribution is research, not scrape metadata. This file
doubles as the provenance record — each entry is tagged confirmed vs likely with
the evidence behind it (see comments). Confirmed = a source explicitly names the
engine; likely = strong era + feature-fingerprint inference, flagged "(likely)"
in the value so it stays honest and queryable.
Engine frameworks:
- CFRU (Complete FireRed Upgrade) + DPE (Dynamic Pokémon Expansion) — Skeli789
& ghoulslash. Binary engine injected into a FireRed ROM. The modern FireRed
base for full-dex / Gen-9-mechanics hacks.
- pokefirered (decomp) — pret's FireRed disassembly. The *other* modern FireRed
base; distinct from CFRU (decomp vs binary).
- pokeemerald-expansion — RHH (Rom Hack Hideout), built on pret's pokeemerald
decomp. The Emerald analogue of CFRU.
- pokeemerald (decomp) — pret's plain Emerald decomp / Pokabbie's rogue fork.
- hg-engine — BluRosie. HeartGold engine overhaul (the HGSS analogue).
Run: python scripts/annotate-engines.py ["<path to Obsidian Vault>"]
"""
import os, re, sys, glob
DEFAULT_VAULT = r"C:\Users\MattC\Documents\Obsidian Vault"
CATALOG = "Pokémon ROM Hacks"
# stem -> engine value. "(likely)" suffix = inferred, not source-confirmed.
ENGINE_MAP = {
# --- FireRed → CFRU + DPE (confirmed) -------------------------------------
"Radical Red": "CFRU + DPE", # docs/community: CFRU+DPE, Skeli789
"Unbound": "CFRU", # "powered by CFRU alongside custom code"
"Fire Red 898": "CFRU + DPE", # uses CFRU+DPE+Leon's Rombase
"Aesthetic Red": "CFRU + DPE", # PokeHarbor: CFRU+DPE engine base
# --- FireRed → CFRU (likely: modern full-dex + CFRU feature fingerprint) ---
"Roaring Red": "CFRU + DPE (likely)", # 2025, modern mechanics, 386 dex
"Astral Red": "CFRU + DPE (likely)", # 2024; QoL list = CFRU defaults
"Fire Red Extended": "CFRU (likely)", # being reworked onto CFRU
# --- FireRed → pokefirered decomp (likely; NOT CFRU) ----------------------
"FireRed Reignited": "pokefirered (decomp, likely)",
"FireRed Reignited LeafGreen Regrown": "pokefirered (decomp, likely)",
# --- Emerald → pokeemerald-expansion (RHH) --------------------------------
"Inclement Emerald": "pokeemerald-expansion", # decomp difficulty hack
"Emerald Seaglass": "pokeemerald-expansion", # confirmed
"Modern Emerald": "pokeemerald-expansion", # confirmed
"Energized Emerald": "pokeemerald-expansion", # confirmed
"Inverse Emerald": "pokeemerald-expansion", # confirmed (pret + rh-hideout)
"R.O.W.E.": "pokeemerald-expansion", # open-world decomp, Gen 9
"Blazing Emerald": "pokeemerald-expansion (likely)",
"Ephemerald": "pokeemerald-expansion (likely)",
# --- Emerald → pokeemerald decomp (Pokabbie rogue fork) -------------------
"Emerald Rogue": "pokeemerald (decomp)", # github.com/Pokabbie/pokeemerald-rogue
"Emerald Rogue 2.0": "pokeemerald (decomp)",
# --- Recharged series (Jaizu) → decomp; expansion-feature fingerprint -----
# Vanilla+ QoL hacks with optional Nuzlocke/level-cap toggles; following mons,
# mints/bottle caps, Fairy type, HM revamp == pokeemerald-expansion features.
"Recharged Emerald": "pokeemerald-expansion (likely)",
"Recharged Emerald Rebalanced": "pokeemerald-expansion (likely)", # rebalanced/difficulty variant
"Recharged Pink": "pokeemerald-expansion (likely)",
"Recharged Yellow (previously known as Pokémon Yellow Cross)": "pokeemerald-expansion (likely)",
# Emerald Cross (Jaizu) — vanilla+ QoL/bugfix, no expansion → lighter decomp build
"Emerald Cross": "pokeemerald (decomp, likely)",
# --- HeartGold → hg-engine ------------------------------------------------
"HeartGold Generation": "hg-engine", # body already says hg-engine
}
# Notes whose scrape left base/version broken — fix while we're in there.
FIXUPS = {
"HeartGold Generation": {"base": "HeartGold", "version": "2.0"},
}
def annotate(path):
text = open(path, encoding="utf-8").read()
stem = os.path.splitext(os.path.basename(path))[0]
engine = ENGINE_MAP[stem]
fix = FIXUPS.get(stem, {})
lines = text.split("\n")
# locate frontmatter bounds
if lines[0].strip() != "---":
return f" SKIP {stem}: no frontmatter"
fm_end = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
changed = []
# --- fixups (base/version) in frontmatter + callout -----------------------
for key, val in fix.items():
for i in range(1, fm_end):
m = re.match(rf'^{key}:\s*(.*)$', lines[i])
if m and m.group(1).strip(' "') != val:
lines[i] = f'{key}: {val}'
changed.append(f"{key}->{val}")
break
# --- engine: in frontmatter (after base:, idempotent) ---------------------
if any(re.match(r'^engine:\s', l) for l in lines[1:fm_end]):
pass # already present
else:
base_i = next((i for i in range(1, fm_end)
if re.match(r'^base:\s', lines[i])), fm_end - 1)
lines.insert(base_i + 1, f'engine: "{engine}"')
fm_end += 1
changed.append("frontmatter engine")
# --- callout token (append to the line after `> [!info]`) -----------------
info_i = next((i for i, l in enumerate(lines)
if l.strip().startswith("> [!info]")), None)
if info_i is not None and info_i + 1 < len(lines):
meta = lines[info_i + 1]
if meta.lstrip().startswith(">") and "· engine **" not in meta:
# apply base/version fixups to the callout line too
for key, val in fix.items():
meta = re.sub(rf'{key} \*\*[^*]*\*\*', f'{key} **{val}**', meta)
lines[info_i + 1] = meta.rstrip() + f" · engine **{engine}**"
changed.append("callout token")
if not changed:
return f" ok {stem}: already annotated"
open(path, "w", encoding="utf-8").write("\n".join(lines))
return f" EDIT {stem}: {', '.join(changed)}"
def main():
vault = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_VAULT
hacks = os.path.join(vault, CATALOG, "Hacks")
if not os.path.isdir(hacks):
sys.exit(f"Hacks dir not found: {hacks}")
by_stem = {os.path.splitext(os.path.basename(p))[0]: p
for p in glob.glob(os.path.join(hacks, "*.md"))}
missing = [s for s in ENGINE_MAP if s not in by_stem]
print(f"Annotating {len(ENGINE_MAP)} notes ({len(missing)} missing):")
for stem in ENGINE_MAP:
if stem in by_stem:
print(annotate(by_stem[stem]))
else:
print(f" MISS {stem}: no note found")
if __name__ == "__main__":
main()