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.
146 lines
7.1 KiB
Python
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()
|