Files
pokemon/scripts/annotate-engines.py
T
ginnoirandClaude Opus 5 b8c69ce789 fix: repair 7 mojibake note filenames
Seven hack notes had filenames whose UTF-8 bytes had been decoded as cp437 and
re-encoded, e.g. "Anil.md" stored as "A" + U+251C + U+2592 + "il.md". Only the
filenames were affected; each note's frontmatter title was already correct.

Repaired by reversing the corruption (name.encode('cp437').decode('utf-8')) and
cross-checking every result against that note's own title field. PokeRogue was
left alone - its U+00E9 cannot be cp437-encoded, proving it is genuine UTF-8 and
not mojibake.

This matters because build-wiki-data.py derives each catalog stem from the
filename and the site derives its URL slug from that. Four published URLs were
wrong and are now fixed:

  /hack/a-il/                -> /hack/anil/
  /hack/fused-adimensions/   -> /hack/fused-dimensions/
  /hack/yu-gi-oh-pok-duel/   -> /hack/yu-gi-oh-pokeduel/
  /hack/recharged-yellow-previously-known-as-pok-mon-yellow-cross/
                             -> ...-as-pokemon-yellow-cross/

The other three were already served correctly, because slugifyStem in the site
repo carries a replaceAll for the mojibake apostrophe. Those URLs are unchanged.

Fallout handled: of 19 vault references, 18 live in files build-vault-mocs.py
regenerates and were fixed by rerunning it. The one hand-written reference, in
Nuzlocke-Friendly (Vanilla+).md, was updated directly. annotate-engines.py also
keyed its ENGINE_MAP on the corrupted stem, so a future run would have silently
skipped Recharged Yellow; that key is corrected here.

Verified: zero mojibake sequences remain anywhere in the vault or this repo, all
seven notes resolve under their new names, catalog steady at 411, and the site
builds 486 pages with banners regenerated under the new slugs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:11:31 -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()