#!/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 [""] """ 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()