Complete curated-list import automation and vault sync.

Adds list-import tooling, expands romhack-import for PC variants and patch batches, and syncs catalog with 2026-06-23 valhalla placements (Vanguard, Xenoverse, GBA patches, Infinity 3.3.1).
This commit is contained in:
ginnoir
2026-06-23 03:57:50 -05:00
parent 0537ceba5d
commit 52ee1b98a1
8 changed files with 1897 additions and 73 deletions
+10 -4
View File
@@ -91,6 +91,12 @@ def types_cell(types):
def key(r): def key(r):
return r["stem"].casefold() return r["stem"].casefold()
def wikilink(stem: str) -> str:
"""Obsidian-safe wikilink for note stems that contain [ or ]."""
escaped = stem.replace("[", "\\[").replace("]", "\\]")
return f"[[{escaped}]]"
# ---------------------------------------------------------------- MOC config # ---------------------------------------------------------------- MOC config
# Logical hardware order for human-facing lists. # Logical hardware order for human-facing lists.
@@ -152,7 +158,7 @@ BASE_MOCS = {
def platform_static_table(recs): def platform_static_table(recs):
head = "| Hack | Base | Version | Status | Type |\n|---|---|---|---|---|" head = "| Hack | Base | Version | Status | Type |\n|---|---|---|---|---|"
rows = [ rows = [
f"| [[{r['stem']}]] | {cell(r['base'])} | {cell(r['version'])} | " f"| {wikilink(r['stem'])} | {cell(r['base'])} | {cell(r['version'])} | "
f"{cell(r['status'])} | {types_cell(r['type'])} |" f"{cell(r['status'])} | {types_cell(r['type'])} |"
for r in sorted(recs, key=key) for r in sorted(recs, key=key)
] ]
@@ -162,14 +168,14 @@ def base_static_table(recs, with_base):
if with_base: if with_base:
head = "| Hack | Base | Version | Dev | Engine | Type |\n|---|---|---|---|---|---|" head = "| Hack | Base | Version | Dev | Engine | Type |\n|---|---|---|---|---|---|"
rows = [ rows = [
f"| [[{r['stem']}]] | {cell(r['base'])} | {cell(r['version'])} | " f"| {wikilink(r['stem'])} | {cell(r['base'])} | {cell(r['version'])} | "
f"{cell(r['status'])} | {cell(r['engine'])} | {types_cell(r['type'])} |" f"{cell(r['status'])} | {cell(r['engine'])} | {types_cell(r['type'])} |"
for r in sorted(recs, key=key) for r in sorted(recs, key=key)
] ]
else: else:
head = "| Hack | Version | Dev | Engine | Type |\n|---|---|---|---|---|" head = "| Hack | Version | Dev | Engine | Type |\n|---|---|---|---|---|"
rows = [ rows = [
f"| [[{r['stem']}]] | {cell(r['version'])} | {cell(r['status'])} | " f"| {wikilink(r['stem'])} | {cell(r['version'])} | {cell(r['status'])} | "
f"{cell(r['engine'])} | {types_cell(r['type'])} |" f"{cell(r['engine'])} | {types_cell(r['type'])} |"
for r in sorted(recs, key=key) for r in sorted(recs, key=key)
] ]
@@ -236,7 +242,7 @@ def index_directory_table(recs):
"|---|---|---|---|---|---|") "|---|---|---|---|---|---|")
order = {p: i for i, p in enumerate(PLATFORM_ORDER)} order = {p: i for i, p in enumerate(PLATFORM_ORDER)}
rows = [ rows = [
f"| [[{r['stem']}]] | {cell(r['platform'])} | {cell(r['base'])} | " f"| {wikilink(r['stem'])} | {cell(r['platform'])} | {cell(r['base'])} | "
f"{cell(r['version'])} | {cell(r['status'])} | {types_cell(r['type'])} |" f"{cell(r['version'])} | {cell(r['status'])} | {types_cell(r['type'])} |"
for r in sorted(recs, key=lambda r: (order.get(r["platform"], 99), key(r))) for r in sorted(recs, key=lambda r: (order.get(r["platform"], 99), key(r)))
] ]
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Diff URL-list hacks against placed library + attempt direct downloads."""
import json, os, re, shutil, subprocess, sys, tempfile, unicodedata, urllib.request
from pathlib import Path
CATALOG = Path(__file__).resolve().parents[1] / "wiki" / "catalog.json"
INCOMING = "/storage1/igir/romhacks/incoming/list-import"
UA = "Mozilla/5.0 (homelab personal archival)"
# Unique games from the 7 source lists with best-known download URL (None = manual only).
ACQUIRE = [
# --- direct-download candidates ---
("Coral", "rom", "https://raw.githubusercontent.com/vbaemulator/GBC-Roms/main/Pokemon%20Coral%20Version%20v1.2.zip", "pokecommunity"),
("Poketale", "zip", "https://www.mediafire.com/file/n6yzxnclf2uys7c/Poketale.zip/file", "mediafire"),
("Vanguard", "archive", "https://www.mediafire.com/file/w9gy3qldi29imhk/Vanguard+3.0.12.7z/file", "mediafire"),
("Bizarre", "zip", "https://www.mediafire.com/file/lox59f4nubxflw6/Pokemon_Bizarre_ENGLISH.zip/file", "mediafire"),
("Spades and Clubs", "page", "https://senta-storage-system.neocities.org/main", "neocities"),
("Dreamstone Mysteries", "github", "https://github.com/dsmyst/dreamstone-mysteries", "github"),
("Giratina Strikes Back", "staging", None, "pokecommunity"),
# --- catalog-only / gated (reported, not fetched) ---
("Abstract", "manual", "https://www.pokecommunity.com/threads/pokémon-abstract-version.527324/", "pokecommunity"),
("Alchemist", "manual", "https://www.pokecommunity.com/threads/pokémon-alchemist.378280/", "pokecommunity"),
("Awakening", "manual", "https://mega.nz/221fb646-0a04-4316-a163-f9b532c89197", "mega"),
("AfricanVs", "have", None, "mega"),
("Bushido", "have", None, "eeveeexpo"),
("Consonancia", "manual", "https://www.pokecommunity.com/threads/pokémon-consonancia-full-game.537329/", "pokecommunity"),
("Concealed", "manual", "https://www.pokecommunity.com/threads/concealed.527325/", "pokecommunity"),
("Crown", "manual", "https://www.pokecommunity.com/threads/pokémon-crown.463821", "pokecommunity"),
("Decay", "have", None, "eeveeexpo"),
("Deserted", "have", None, "eeveeexpo"),
("Eon Guardians", "manual", "https://www.pokecommunity.com/threads/pokémon-eon-guardians.527913/", "pokecommunity"),
("Fire Ash", "manual", "https://www.pokecommunity.com/threads/pokémon-fire-ash.399096/", "pokecommunity"),
("Floral Tempus", "manual", "https://www.pokecommunity.com/threads/pokémon-floral-tempus-ex-1-9-4-released-12-gym-badges-elite-4-champion.409175/", "pokecommunity"),
("Hero Legacy", "manual", "https://www.pokecommunity.com/threads/pokémon-hero-legacy-eng-de-now-with-new-game-mode.538518/", "pokecommunity"),
("HGSS Sevii", "manual", "https://www.pokecommunity.com/threads/pokémon-hgss-sevii-islands-hoenn-postgame.434636/", "pokecommunity"),
("Itinerant", "manual", "https://www.pokecommunity.com/threads/pokemon-itinerant.528218/", "pokecommunity"),
("Jam Festival", "manual", "https://www.pokecommunity.com/threads/pkmn-jam-festival.536702/", "pokecommunity"),
("Legends of the Arena", "manual", "https://www.pokecommunity.com/threads/pokémon-legends-of-the-arena.298738/", "pokecommunity"),
("Mega Adventures", "manual", "https://www.pokecommunity.com/threads/pokemon-mega-adventure.362058/", "pokecommunity"),
("Nightmare", "manual", "https://www.pokecommunity.com/threads/pokémon-nightmare.504856/", "pokecommunity"),
("Odyssey II", "discord", None, "discord"),
("Legacy Edition", "discord", None, "discord"),
("Iridium", "discord", None, "discord"),
("Light Platinum DS", "manual", None, "twitter"),
("Prisme", "manual", "https://pokemonprisme.com", "website"),
("Relict", "manual", "https://phiongames.blogspot.com/2025/12/descarga-pokemon-relict.html", "blogspot"),
("Reminiscencia", "manual", "https://mega.nz/file/5oNDGI5R#0b_7aQ3fMlUCo6Gwo8dVLXEXnW-seV-o_OanYqxVdRg", "mega"),
("Reloaded", "manual", "https://pokemon-reloaded.blogspot.com", "blogspot"),
("Shattered Light", "manual", "https://eeveeexpo.com/shattered-light/", "eeveeexpo"),
("Sienna", "manual", "https://www.pokecommunity.com/threads/hack-of-the-year-2010-pokémon-sienna-complete-version-released.202372/", "pokecommunity"),
("Tectonic", "manual", "https://www.tectonic-game.com", "website"),
("Uranium", "manual", "https://mega.nz/file/gh82ST6S#makZr_hsZAcMOIKSEPUwejVJ_N4OW7Mk9wEBlWIoIG0", "mega"),
("Vega Fairy EX", "manual", "https://www.pokecommunity.com/threads/pokemon-vega-fairy-edition-ex-minus-v1-4-balance-updates-and-skarmory-evo.475538/", "pokecommunity"),
("Xenoverse", "staging", None, "mega"),
("Zeta", "staging", None, "website"),
("Insurgence", "have", None, "website"),
("Infinite Fusion", "have", None, "pokecommunity"),
("Reborn", "have", None, "website"),
("Rejuvenation", "have", None, "website"),
("Pokerogue", "web", "https://pokerogue.net", "browser"),
("Emerald Seaglass", "manual", "https://ko-fi.com/s/4a1535f351", "ko-fi"),
("Lazarus", "have", "https://ko-fi.com/nemo622", "ko-fi"),
]
def norm(s):
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().lower()
s = re.sub(r"^pokemon\s+", "", s)
s = re.sub(r"[^a-z0-9]+", " ", s).strip()
return s
def load_placed():
"""Read placed library basenames from stdin (piped ssh find output)."""
placed = set()
for line in sys.stdin:
line = line.strip()
if not line:
continue
m = re.match(r"Pokemon - (.+?) \(Hack\)", line, re.I)
if m:
placed.add(norm(m.group(1)))
return placed
def download(url, dest):
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=120) as r, open(dest, "wb") as f:
shutil.copyfileobj(r, f)
return os.path.getsize(dest)
def main():
placed = load_placed()
catalog = {norm(h.get("title", h.get("stem", ""))): h for h in json.loads(CATALOG.read_text(encoding="utf-8"))["hacks"]}
os.makedirs(INCOMING, exist_ok=True)
rows = []
for name, kind, url, host in ACQUIRE:
key = norm(name)
in_cat = key in catalog or any(key in ck or ck in key for ck in catalog)
in_lib = any(key in p or p in key for p in placed)
status = "HAVE" if in_lib or kind == "have" else "MISSING"
if kind == "staging" and not in_lib:
status = "STAGING"
dl = ""
if status in ("MISSING", "STAGING") and kind in ("rom", "zip", "archive", "github", "page") and url:
dest = os.path.join(INCOMING, re.sub(r"[^\w.\- ]", "_", name))
try:
if kind == "github":
# release asset lookup would need gh api; skip for now
dl = "SKIP(github-release)"
elif kind == "page":
dl = "SKIP(scrape-neocities)"
else:
ext = ".zip" if kind in ("rom", "zip") else ".7z"
dest += ext
sz = download(url, dest)
dl = f"OK {sz:,}b -> {dest}"
status = "DOWNLOADED"
except Exception as e:
dl = f"FAIL: {e}"
rows.append((status, name, "catalog" if in_cat else "NEW", host, url or "", dl))
print(f"Placed library entries: {len(placed)}")
print(f"\n{'STATUS':12} {'NAME':28} {'CAT':5} {'HOST':12} URL")
for st, name, cat, host, url, dl in sorted(rows, key=lambda r: (r[0], r[1])):
print(f"{st:12} {name:28} {cat:5} {host:12} {url[:60]}")
if dl:
print(f"{'':12} {dl}")
if __name__ == "__main__":
main()
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""Parse curated URL lists, diff against wiki/catalog.json, report gaps."""
import json, re, unicodedata
from pathlib import Path
CATALOG = Path(__file__).resolve().parents[1] / "wiki" / "catalog.json"
# Games extracted from the 7 source lists (deduped, normalized keys).
LIST_ENTRIES = [
# UfzGCYzA
("Abstract", "https://www.pokecommunity.com/threads/pokémon-abstract-version.527324/"),
("Alchemist", "https://www.pokecommunity.com/threads/pokémon-alchemist.378280/"),
("AfricanVs", "https://mega.nz/file/UgZj3BCQ#Xc6243i1iMfBTTJR7NfEsA7mFgfCFj37e7_u_B1EvMM"),
("Awakening", "https://www.pokecommunity.com/threads/pokémon-awakening.535910/"),
("Bizarre", "https://drive.google.com/file/d/1NXXr96vzeMD8sGplV1sWZFSph2vZijaK/view"),
("Bushido", "https://eeveeexpo.com/bushido/"),
("Empyrean", "https://www.pokecommunity.com/threads/pokémon-empyrean-v1-1-final-arc-released.411322/"),
("Eon Guardians", "https://www.pokecommunity.com/threads/pokémon-eon-guardians.527913/"),
("Floral Tempus", "https://www.pokecommunity.com/threads/pokémon-floral-tempus-ex-1-9-4-released-12-gym-badges-elite-4-champion.409175/"),
("HGSS Sevii", "https://www.pokecommunity.com/threads/pokémon-hgss-sevii-islands-hoenn-postgame.434636/"),
("Infinity", "https://eeveeexpo.com/infinity/"),
("Insurgence", "https://p-insurgence.com"),
("Itinerant", "https://www.pokecommunity.com/threads/pokemon-itinerant.528218/"),
("Infinite Fusion", "https://www.pokecommunity.com/threads/v6-4-pokémon-infinite-fusion-new-pokémon-character-customization-and-more.347883/"),
("Legends of the Arena", "https://www.pokecommunity.com/threads/pokémon-legends-of-the-arena.298738/"),
("Mega Adventures", "https://www.pokecommunity.com/threads/pokemon-mega-adventure.362058/"),
("Nightmare", "https://www.pokecommunity.com/threads/pokémon-nightmare.504856/"),
("Pokerogue", "https://pokerogue.net"),
("Poketale", "https://www.mediafire.com/file/n6yzxnclf2uys7c/Poketale.zip/file"),
("Realidea System", "https://www.pokecommunity.com/threads/pokémon-realidea-system-now-playable-in-english.481090/"),
("Reborn", "https://www.rebornevo.com/pr/index.html/"),
("Reloaded", "https://pokemon-reloaded.blogspot.com"),
("Reminiscencia", "https://www.pokecommunity.com/threads/pokémon-reminiscencia-full-release-spa-en-translation-complete.529202/"),
("Shattered Light", "https://eeveeexpo.com/shattered-light/"),
("Solar Light Lunar Dark", "https://pokemonsolarlightlunardark.fandom.com/wiki/Pokémon_Solar_Light_%26_Lunar_Dark_Wiki"),
("Tectonic", "https://www.tectonic-game.com"),
("Uranium", "https://pokemon-uranium.fandom.com/wiki/Main_Page"),
("Xenoverse", "https://mega.nz/file/FbBgiTTK#qJpzZeUb5XqO5rLt6meW5_tOXeQ3K6y3_mslGNUf41M"),
("Zeta", "https://zo.p-insurgence.com"),
("Atlas", None), ("Unbreakable Ties", None), ("Bioterror", None), ("Fuso Meteor", None),
("Comet", None), ("Ruckus", None), ("Island", None), ("Impetu", None),
("Slowpoke Shack", None), ("Empire", None), ("Flux", None),
# S1CSW465
("Consonancia", "https://www.pokecommunity.com/threads/pokémon-consonancia-full-game.537329/"),
("Concealed", "https://www.pokecommunity.com/threads/concealed.527325/"),
("Chaos in Vesita", "https://www.pokecommunity.com/threads/pokémon-chaos-in-vesita-new-version-out-now-eng-ger.491767/"),
("Dark Horizon", "https://eeveeexpo.com/threads/9332/"),
("Decay", "https://eeveeexpo.com/decay/"),
("Fire Ash", "https://www.pokecommunity.com/threads/pokémon-fire-ash.399096/"),
("Gaidr Deluxe", "https://www.pokecommunity.com/threads/pokémon-gadir-deluxe-complete-v-1-3.539451/"),
("Hero Legacy", "https://www.pokecommunity.com/threads/pokémon-hero-legacy-eng-de-now-with-new-game-mode.538518/"),
("Hollow Woods", "https://www.pokecommunity.com/threads/pokémon-hollow-woods.525865/"),
("Hydro Bliss", "https://eeveeexpo.com/threads/9100/"),
("Jam Festival", "https://www.pokecommunity.com/threads/pkmn-jam-festival.536702/"),
("Keishou", "https://www.pokecommunity.com/threads/pokémon-keishou-v2-06-1.504806/"),
("Prisme", "https://pokemonprisme.com"),
("Re:Union", "https://projetreunion.com"),
("Relict", "https://phiongames.blogspot.com/2025/12/descarga-pokemon-relict.html"),
# 6wPjUcrE
("Spades and Clubs", "https://senta-storage-system.neocities.org/main"),
("Giratina's Legend", "https://www.pokecommunity.com/threads/pokemon-giratinas-legend-07-01-24.289561"),
("Crown", "https://www.pokecommunity.com/threads/pokémon-crown.463821"),
("Odyssey II", None), # Discord only
("Coral", "https://www.pokecommunity.com/threads/pokémon-coral-version-2022-demo-out-now.402361"),
("DarkFire", "https://www.pokecommunity.com/threads/new-2024-beta-2-1-out-now-pokémon-darkfire.421395/"),
("Sovereign of the Skies", "https://www.pokecommunity.com/threads/sovereign-of-the-skies-new-beta-2-0-0-version-released-06-10-2021.292651/"),
("Legacy Edition", None), # Discord only
("Iridium", None), # Discord only
("Light Platinum DS", None), # Twitter only
# ZwA8ng8E
("Deserted", "https://eeveeexpo.com/threads/5344/"),
# 1S4acLQA
("Saiph", "https://www.pokecommunity.com/threads/pokémon-saiph-the-vytroverse-part-1-full-game-released.420990"),
("Sors", "https://www.pokecommunity.com/threads/pokémon-sors-the-vytroverse-part-2-full-game-v1-3-available-now.433238"),
("Saiph 2", "https://www.pokecommunity.com/threads/pokémon-saiph-2-the-vytroverse-part-3-full-game-v1-4-0-update-available.458509"),
("Sors 2", "https://www.pokecommunity.com/threads/pokémon-sors-2-the-vytroverse-part-4-tech-demo-available.526108"),
("Victory Fire", "https://www.pokecommunity.com/threads/pokemon-victory-fire-version-2-75-released.285031/"),
("Resolute", "https://www.pokecommunity.com/threads/pokemon-resolute-version-version-2-97-released.294565/"),
("Mega Power", "https://www.pokecommunity.com/threads/pokemon-mega-power-completed-beta-5-73-released.325422/"),
("Nameless", "https://www.pokecommunity.com/threads/pokemon-nameless-version-complete-beta-5-35-released-with-darker-future-episode.419717/"),
("Ruby Destiny", None),
("ROWE", "https://www.pokecommunity.com/threads/pokemon-r-o-w-e-2-0-an-open-world-version-of-pokémon-emerald-gen-9-16-badges-sevii-islands-following-pokémon-costumes-and-much-more.442592/"),
("Gaia", "https://www.pokecommunity.com/threads/pokémon-gaia-version.326118/"),
("Vanguard", None),
# tARt0w7x
("Adventure Red", "https://www.pokecommunity.com/threads/pokémon-adventure-red-chapter-new-beta-expansion.298920/"),
("AshGray", "https://www.pokecommunity.com/threads/pokémon-ashgray-version-beta-4-5-released.180722/"),
("Azure Horizons", "https://hacksrepairman.blogspot.com/2020/03/pokemon-azure-horizons-fixed-beta2.html"),
("Crystal Clear", "https://shockslayer.com/crystal-clear/"),
("Dark Violet", "https://www.pokecommunity.com/threads/pokémon-darkviolet-full-version-released.291789/"),
("Dreams", "https://www.pokecommunity.com/threads/dreams-completed-version-1-5-1-now-available.443298/"),
("Elite Redux", "https://www.pokecommunity.com/threads/pokémon-elite-redux-v2-1-complete-—-unique-multi-ability-difficulty-hack.499227/"),
("Emerald Rogue", "https://www.pokecommunity.com/threads/pokemon-emerald-rogue.479406/"),
("Emerald Seaglass", "https://ko-fi.com/s/4a1535f351"),
("Elysium", "https://www.pokecommunity.com/threads/pokémon-elysium-gba.502953/"),
("Fire Red Extended", "https://www.pokecommunity.com/threads/pokémon-fire-red-extended-version.466535/"),
("Flora Sky", "https://www.pokemoncoders.com/pokemon-flora-sky/"),
("Glazed", "https://www.pokemoncoders.com/pokemon-glazed/"),
("Light Platinum", "https://www.pokemoncoders.com/pokemon-light-platinum-rom-hack/"),
("Liquid Crystal", "https://www.pokecommunity.com/threads/pokémon-liquid-crystal-3-3-xxxxx-live-beta.242023/"),
("Mariomon", "https://www.pokecommunity.com/threads/super-mariomon.535764/"),
("Odyssey", "https://www.pokecommunity.com/threads/pokémon-odyssey-complete-v4-0-1.488536/"),
("Pisces", "https://pebblerplatoon.miraheze.org/wiki/Pisces_Download"),
("Prism", "https://rainbowdevs.com/"),
("Polished Crystal", "https://www.pokecommunity.com/threads/pokémon-polished-crystal-update-3-1-1.373172/"),
("Radical Red", "https://www.pokecommunity.com/threads/pokémon-radical-red-version-4-1-released-gen-9-dlc-pokemon-character-customization-now-available.437688/"),
("Recharged Series", "https://jaizu.moe"),
("FireRed Rocket Edition", "https://www.pokecommunity.com/threads/pokémon-firered-rocket-edition-completed.360725/"),
("Scorched Silver", "https://www.pokecommunity.com/threads/pokémon-scorched-silver-v1-3-complete.529230/"),
("Sienna", "https://www.pokecommunity.com/threads/hack-of-the-year-2010-pokémon-sienna-complete-version-released.202372/"),
("Snakewood", "https://www.pokecommunity.com/threads/pokémon-snakewood-version.235371/"),
("Team Rocket Edition", "https://www.pokecommunity.com/threads/pokémon-team-rocket-edition-dragonsden-version-kanto-sevii-johto-next-release-dlc-season-4.527368/"),
("The Pit", "https://www.pokecommunity.com/threads/the-pit-v2-roguelite-style-hack.528423/"),
("Unbound", "https://www.pokecommunity.com/threads/pokémon-unbound-completed.382178/"),
("Vega", "https://www.pokecommunity.com/threads/pokémon-vega-and-altair-sirius-english-version.365959/"),
("Vega Fairy EX", "https://www.pokecommunity.com/threads/pokemon-vega-fairy-edition-ex-minus-v1-4-balance-updates-and-skarmory-evo.475538/"),
# Google Doc
("Lazarus", "https://ko-fi.com/nemo622"),
("Renegade Platinum", "https://projectpokemon.org/home/forums/topic/52294-pokémon-renegade-platinum/"),
("Heart n Soul", "https://www.hackdex.app/hack/pokemon-heart-and-soul"),
("Dreamstone Mysteries", "https://github.com/dsmyst/dreamstone-mysteries"),
("Rejuvenation", None),
]
ALIASES = {
"adventure red": "adventure red chapter",
"ashgray": "ash gray",
"ash gray": "ashgray",
"firered rocket edition": "firered rocket edition",
"fire red extended": "fire red extended",
"darkfire": "dark fire",
"gaidr deluxe": "gadir deluxe",
"reunion": "re union",
"re:union": "re union",
"re union dx": "re union",
"rowe": "r o w e",
"r.o.w.e": "r o w e",
"itinerant": "iternant",
"keishou": "kieshou",
"giratina's legend": "giratina",
"vega fairy ex": "vega fairy",
"team rocket edition": "team rocket edition",
"mega adventures": "mega adventure",
"reminiscencia": "reminiscensia",
"spades and clubs": "spades",
"odyssey ii": "odyssey 2",
"heroes of lemuria": "odyssey 2",
"light platinum ds": "light platinum ds",
"heart n soul": "heart and soul",
"recharged series": "recharged",
}
def norm(s: str) -> str:
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().lower()
s = re.sub(r"^pok[eé]mon\s+", "", s)
s = re.sub(r"[^a-z0-9]+", " ", s).strip()
return ALIASES.get(s, s)
def load_catalog():
data = json.loads(CATALOG.read_text(encoding="utf-8"))
by_norm = {}
for h in data["hacks"]:
title = h.get("title") or h.get("stem", "")
key = norm(title)
by_norm.setdefault(key, []).append(h)
if h.get("stem"):
by_norm.setdefault(norm(h["stem"]), []).append(h)
return by_norm
def match_catalog(name, catalog):
key = norm(name)
hits = catalog.get(key, [])
if hits:
return hits[0]
# fuzzy: substring
for ck, hits in catalog.items():
if key in ck or ck in key:
return hits[0]
return None
def main():
catalog = load_catalog()
seen = set()
rows = []
for name, url in LIST_ENTRIES:
nk = norm(name)
if nk in seen:
continue
seen.add(nk)
hit = match_catalog(name, catalog)
rows.append({
"name": name,
"url": url,
"in_catalog": hit is not None,
"catalog_title": hit["title"] if hit else None,
"platform": hit.get("platform") if hit else None,
"library_path": hit.get("library_path") if hit else None,
})
missing_cat = [r for r in rows if not r["in_catalog"]]
in_cat = [r for r in rows if r["in_catalog"]]
print(f"Total unique list entries: {len(rows)}")
print(f"In catalog: {len(in_cat)}")
print(f"NOT in catalog: {len(missing_cat)}")
print("\n=== NOT IN CATALOG ===")
for r in sorted(missing_cat, key=lambda x: x["name"].lower()):
print(f" {r['name']:30} {r['url'] or '(no URL)'}")
print("\n=== IN CATALOG (for library check) ===")
for r in sorted(in_cat, key=lambda x: x["name"].lower()):
lp = r["library_path"] or ""
print(f" {r['name']:30} | {r['platform'] or '?':5} | {lp}")
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Create vault notes for list-import hacks missing from catalog."""
import json, re, textwrap, unicodedata
from datetime import date
from pathlib import Path
VAULT = Path(r"C:\Users\MattC\Documents\Obsidian Vault\Pokémon ROM Hacks\Hacks")
CATALOG = Path(__file__).resolve().parents[1] / "wiki" / "catalog.json"
NEW = [
("Alchemist", "GBA", "FireRed", "Complete", "https://www.pokecommunity.com/threads/pokémon-alchemist.378280/"),
("Concealed", "PC", None, "Complete", "https://www.pokecommunity.com/threads/concealed.527325/"),
("Consonancia", "PC", None, "Complete", "https://www.pokecommunity.com/threads/pokémon-consonancia-full-game.537329/"),
("Crown", "PC", None, "Ongoing", "https://www.pokecommunity.com/threads/pokémon-crown.463821/"),
("Eon Guardians", "GBA", "Emerald", "Complete", "https://www.pokecommunity.com/threads/pokémon-eon-guardians.527913/"),
("Fire Ash", "GBA", "FireRed", "Complete", "https://www.pokecommunity.com/threads/pokémon-fire-ash.399096/"),
("Floral Tempus", "GBA", "Emerald", "Complete", "https://www.pokecommunity.com/threads/pokémon-floral-tempus-ex-1-9-4-released-12-gym-badges-elite-4-champion.409175/"),
("Hero Legacy", "PC", None, "Complete", "https://www.pokecommunity.com/threads/pokémon-hero-legacy-eng-de-now-with-new-game-mode.538518/"),
("HGSS Sevii", "NDS", "HeartGold", "Complete", "https://www.pokecommunity.com/threads/pokémon-hgss-sevii-islands-hoenn-postgame.434636/"),
("Itinerant", "GBA", "FireRed", "Ongoing", "https://www.pokecommunity.com/threads/pokemon-itinerant.528218/"),
("Jam Festival", "PC", None, "Complete", "https://www.pokecommunity.com/threads/pkmn-jam-festival.536702/"),
("Legends of the Arena", "GBA", "Emerald", "Complete", "https://www.pokecommunity.com/threads/pokémon-legends-of-the-arena.298738/"),
("Mega Adventures", "GBA", "Ruby", "Complete", "https://www.pokecommunity.com/threads/pokemon-mega-adventure.362058/"),
("Nightmare", "PC", None, "Complete", "https://www.pokecommunity.com/threads/pokémon-nightmare.504856/"),
("Poketale", "PC", None, "Complete", "https://www.mediafire.com/file/n6yzxnclf2uys7c/Poketale.zip/file"),
("Relict", "PC", None, "Complete", "https://phiongames.blogspot.com/2025/12/descarga-pokemon-relict.html"),
("Shattered Light", "PC", None, "Ongoing", "https://eeveeexpo.com/shattered-light/"),
("Sienna", "GBA", "Ruby", "Complete", "https://www.pokecommunity.com/threads/hack-of-the-year-2010-pokémon-sienna-complete-version-released.202372/"),
("Spades and Clubs", "GBA", "FireRed", "Beta", "https://senta-storage-system.neocities.org/patchrelease"),
("Tectonic", "PC", None, "Ongoing", "https://www.tectonic-game.com"),
("Vega Fairy EX", "GBA", "FireRed", "Complete", "https://www.pokecommunity.com/threads/pokemon-vega-fairy-edition-ex-minus-v1-4-balance-updates-and-skarmory-evo.475538/"),
]
UPDATES = {
"Coral": "roms/gbc/Hacks/Pokemon - Coral (Hack).gbc",
"Giratina Strikes Back": "roms/gba/Hacks/Pokemon - Giratina Strikes Back Full (Hack).gba",
}
def norm(s):
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().lower()
s = re.sub(r"^pokemon\s+", "", s)
return re.sub(r"[^a-z0-9]+", " ", s).strip()
def yaml_quote(s):
if s is None:
return '""'
if any(c in s for c in ':"\\#[]{}'):
return json.dumps(s)
return s
def note_body(title, source):
return textwrap.dedent(f"""\
# {title}
> [!info] From curated YouTube/pastebin list import (2026-06-23)
> Added to catalog from community recommendation lists. Download may be gated — see [[Wanted]].
## Summary
Surfaced from external recommendation lists (pastebin / Google Doc). Not yet in the RomM library unless noted in frontmatter.
## Links
- Info / thread: {source}
## In the library
> [!note] See `library_path` in frontmatter.
[[Index|← back to directory]]
""")
def patch_library_path(path: Path, lp: str):
text = path.read_text(encoding="utf-8")
text = re.sub(r"^library_path:.*$", f'library_path: "{lp}"', text, count=1, flags=re.M)
path.write_text(text, encoding="utf-8")
def main():
existing = {norm(p.stem) for p in VAULT.glob("*.md")}
today = date.today().isoformat()
created = []
for name, platform, base, status, source in NEW:
if norm(name) in existing:
continue
title = f"Pokémon {name}"
tags = ["hack", f"platform/{platform.lower()}", f"status/{status.lower().replace(' ', '-')}"]
if base:
tags.append(f"base/{base.lower().replace(' ', '-')}")
fm = [
"---",
f"title: {title}",
f'platform: {platform}',
f'base: {base or ""}',
'version: ""',
f"status: {status}",
"type: [New Experience]",
'generation: ""',
'library_path: ""',
f"source: {json.dumps(source)}",
f"added: {today}",
"play_status: Unplayed",
"tags:",
]
fm += [f" - {t}" for t in tags]
fm.append("---")
body = note_body(title, source)
out = VAULT / f"{name}.md"
out.write_text("\n".join(fm) + "\n" + body, encoding="utf-8")
created.append(name)
for stem, lp in UPDATES.items():
p = VAULT / f"{stem}.md"
if p.exists():
patch_library_path(p, lp)
print(f"Created {len(created)} notes: {', '.join(created)}")
print(f"Updated library_path: {', '.join(UPDATES)}")
if __name__ == "__main__":
main()
+90
View File
@@ -0,0 +1,90 @@
# Push Downloads batch to valhalla for romhack import.
# Primary Windows builds + alternate platform variants (Linux/macOS/Joiplay/Android).
$ErrorActionPreference = "Continue"
$dl = Join-Path $env:USERPROFILE "Downloads"
$remote = "valhalla:/storage1/igir/romhacks/incoming/downloads-batch"
$files = @(
# --- PC fan-games (Windows primary) ---
"Alchemist.rar",
"Concealed 2.1.rar",
"Pokemon Consonancia 1.2.rar",
"Pokemon Eon Guardians.zip",
"Pokemon Fire Ash 3.4.zip",
"FT EX.zip",
"Pokemon HGSS Sevii Islands - Final 1.0.zip",
"Pokemon Hero Legacy 3.0.7.zip",
"Itinerant v7.9.4.zip",
"PKMN Jam Festival v1.4.rar",
"Legends of the Arena 1.4.1.zip",
"Pokemon Nightmare 1.3.0.zip",
"Poketale.zip",
"Vanguard 3.0.12.7z",
"Pokemon Awakening ENG.rar",
"Pokemon Bizarre ENGLISH.zip",
"ReminiscenciaV2_3.zip",
"Pokemon Uranium 1.2.5.zip",
"Pokemon Tectonic 3.4.1.zip",
"Pokemon Shattered Light.zip",
"Relict V1.2.1.zip",
"Xenoverse-Source-main.zip",
"PokemonPrismeInstaller.exe",
"infinitefusion-e18-6.7.zip",
"InfiniteFusion.zip",
"Reborn-19.5.0-windows.zip",
"Rejuvenation-13.5.0-windows.zip",
# --- Alternate platform builds (separate RomM entries) ---
"Pokemon Hero Legacy 3.0.7 Mac.dmg",
"Pokemon Hero Legacy 3.0.7 Joiplay.zip",
"Itinerant v7.9.4 (Android).zip",
"Reborn-19.5.0-linux.zip",
"Reborn-19.5.0-macos.zip",
"Reborn-19.5.0-joiplay.jgp",
"Rejuvenation-13.5.0-linux.zip",
"Rejuvenation-13.5.0-macos.zip",
"Rejuvenation - Where Love Lies.zip",
# --- ROMs / patches ---
"LightPlatinumV022.zip",
"Azure Horizons True Continued Beta 2.ips",
"Pokemon Vega Fairy Edition EX 1.4.ups",
"Pokemon - Fire Red (J) (V1.0).gba",
"Sienna Complete Version.ips",
"EmeraldSeaglass_v3.0 (1).ips",
"emerald_cross_2_0_5.bps",
"re_rebalanced_version_2.2.8.bps",
"recharged_emerald_version_2.2.8.bps"
)
$extras = @(
(Get-ChildItem $dl -Filter "*Mega Adventure*" -File | Select-Object -First 1).FullName,
(Get-ChildItem $dl -Filter "*Abstract*" -File | Select-Object -First 1).FullName,
(Get-ChildItem $dl -Filter "*Odyssey COMPLETE*" -File | Select-Object -First 1).FullName
) | Where-Object { $_ }
ssh valhalla "mkdir -p /storage1/igir/romhacks/incoming/downloads-batch"
$queued = @()
foreach ($f in $files) {
$p = Join-Path $dl $f
if (Test-Path $p) { $queued += $p } else { Write-Warning "SKIP missing: $f" }
}
foreach ($p in $extras) {
if ($p -and (Test-Path $p) -and $queued -notcontains $p) { $queued += $p }
}
# Skip files already on valhalla (resume-friendly)
$remoteList = ssh valhalla "ls -1 /storage1/igir/romhacks/incoming/downloads-batch/ 2>/dev/null" 2>$null
$remoteSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$remoteList)
$todo = @($queued | Where-Object { -not $remoteSet.Contains((Split-Path $_ -Leaf)) })
$total = ($todo | ForEach-Object { (Get-Item $_).Length } | Measure-Object -Sum).Sum
Write-Host ("Uploading {0} files ({1:N1} GB); {2} already on server" -f $todo.Count, ($total/1GB), ($queued.Count - $todo.Count))
$ok = 0; $fail = 0
foreach ($p in $todo) {
$name = Split-Path $p -Leaf
Write-Host " -> $name"
scp $p "${remote}/"
if ($LASTEXITCODE -eq 0) { $ok++ } else { $fail++; Write-Warning "FAILED: $name" }
}
Write-Host "Upload complete: $ok ok, $fail failed, $($queued.Count - $todo.Count) skipped (already present)"
+160 -4
View File
@@ -21,6 +21,7 @@ import sys, os, re, zlib, shutil, subprocess, zipfile, unicodedata, tempfile
ROMS = "/storage1/Emulation/roms" ROMS = "/storage1/Emulation/roms"
PATCHES = "/storage1/igir/romhacks/patches" PATCHES = "/storage1/igir/romhacks/patches"
APPLY = "/storage1/igir/romhacks/tools/apply.py" APPLY = "/storage1/igir/romhacks/tools/apply.py"
WIN_DEST = "/storage1/Emulation/roms/windows"
BASES = { BASES = {
"crystal_rev1": f"{ROMS}/gbc/Pokemon - Crystal Version (USA, Europe) (Rev 1).gbc", "crystal_rev1": f"{ROMS}/gbc/Pokemon - Crystal Version (USA, Europe) (Rev 1).gbc",
@@ -31,17 +32,20 @@ BASES = {
"emerald": f"{ROMS}/gba/Pokemon - Emerald Version (USA, Europe).gba", "emerald": f"{ROMS}/gba/Pokemon - Emerald Version (USA, Europe).gba",
"firered": f"{ROMS}/gba/Pokemon - FireRed Version (USA, Europe).gba", "firered": f"{ROMS}/gba/Pokemon - FireRed Version (USA, Europe).gba",
"leafgreen": f"{ROMS}/gba/Pokemon - LeafGreen Version (USA, Europe).gba", "leafgreen": f"{ROMS}/gba/Pokemon - LeafGreen Version (USA, Europe).gba",
"firered_jp": f"{ROMS}/gba/Pokemon - Fire Red (J) (V1.0).gba",
"red": f"{ROMS}/gb/Pokemon - Red Version (USA, Europe) (SGB Enhanced).gb", "red": f"{ROMS}/gb/Pokemon - Red Version (USA, Europe) (SGB Enhanced).gb",
} }
# candidate bases to try per target platform (order = preference) # candidate bases to try per target platform (order = preference)
CANDIDATES = { CANDIDATES = {
"gba": ["emerald", "firered", "leafgreen", "ruby", "sapphire"], "gba": ["firered_jp", "emerald", "firered", "leafgreen", "ruby", "sapphire"],
"gbc": ["crystal_rev1", "gold", "silver"], "gbc": ["crystal_rev1", "gold", "silver"],
"gb": ["red"], "gb": ["red"],
} }
# IPS patches (no checksum) need an explicit base by name substring # IPS patches (no checksum) need an explicit base by name substring
IPS_BASE_MAP = [ IPS_BASE_MAP = [
("seaglass", "emerald", "gba"), ("emeraldseaglass", "emerald", "gba"),
("azure horizons", "firered", "gba"),
("sienna", "ruby", "gba"),
("crystal kaizo", "crystal_rev1", "gbc"), ("crystal kaizo", "crystal_rev1", "gbc"),
("kaizo", "crystal_rev1", "gbc"), # fallback for crystal kaizo rar ("kaizo", "crystal_rev1", "gbc"), # fallback for crystal kaizo rar
] ]
@@ -57,6 +61,23 @@ ARCHIVE_EXT = {".zip", ".rar", ".7z"}
NAME_OVERRIDES = [ NAME_OVERRIDES = [
("emeraldseaglass", "Emerald Seaglass"), ("emeraldseaglass", "Emerald Seaglass"),
("emerald seaglass", "Emerald Seaglass"), ("emerald seaglass", "Emerald Seaglass"),
("azure horizons", "Azure Horizons"),
("vega fairy", "Vega Fairy EX"),
("spades", "Spades and Clubs"),
("ft ex", "Floral Tempus"),
("floral tempus", "Floral Tempus"),
("hero legacy", "Hero Legacy"),
("mega adventure", "Mega Adventures"),
("hgss sevii", "HGSS Sevii"),
("jam festival", "Jam Festival"),
("legends of the arena", "Legends of the Arena"),
("fire ash", "Fire Ash"),
("eon guardians", "Eon Guardians"),
("shattered light", "Shattered Light"),
("lightplatinum", "Light Platinum DS"),
("reminiscencia", "Reminiscencia"),
("xenoverse", "Xenoverse"),
("prisme", "Prisme"),
("lazarus", "Lazarus"), ("lazarus", "Lazarus"),
("saiph 2", "Saiph 2"), ("saiph2", "Saiph 2"), ("saiph 2", "Saiph 2"), ("saiph2", "Saiph 2"),
("saiph", "Saiph"), ("saiph", "Saiph"),
@@ -68,12 +89,51 @@ NAME_OVERRIDES = [
] ]
# Files we will NOT auto-place (unidentified / need user to name). Reported instead. # Files we will NOT auto-place (unidentified / need user to name). Reported instead.
SKIP_NAME_SUBSTR = ["beta 15 + expansion"] SKIP_NAME_SUBSTR = ["beta 15 + expansion"]
# Known base ROMs in a drop folder — place under roms/<plat>/, not Hacks/
BASE_ROM_FILES = [
("fire red (j)", "gba", "Pokemon - Fire Red (J) (V1.0).gba"),
]
# Filename hints -> RomM variant tag (separate catalog/download entries).
VARIANT_RULES = [
(r"joiplay|\.jgp\b", "[Joiplay]"),
(r"android", "[Android]"),
(r"linux", "[Linux]"),
(r"macos|\.dmg\b", "[macOS]"),
(r"installer", "[Installer]"),
(r"where love lies", "[DLC]"),
(r"infinitefusion\.zip$", "[Full]"), # 4 GB build vs e18 primary
]
PLACE = False PLACE = False
report = [] report = []
ARCH_SUBDIR = "" # set while recursing an archive, groups its docs under _docs/<subdir>/ ARCH_SUBDIR = "" # set while recursing an archive, groups its docs under _docs/<subdir>/
def detect_variant(filename):
low = os.path.basename(filename).lower()
for pat, tag in VARIANT_RULES:
if re.search(pat, low):
return tag
return None
def pc_dest_dir(variant):
if variant == "[Linux]":
return os.path.join(ROMS, "linux")
if variant == "[macOS]":
return os.path.join(ROMS, "macintosh")
return WIN_DEST
def is_pc_fan_game(root):
"""RPG-Maker / Essentials tree — Game.exe, Game.ini, or mkxp port."""
for walk_root, _, files in os.walk(root):
low = {f.lower() for f in files}
if "game.exe" in low or "game.ini" in low or "mkxp.json" in low:
return True
return False
def strip_trailing_groups(name): def strip_trailing_groups(name):
"""Remove trailing (...) / [...] groups that look like version/variant junk.""" """Remove trailing (...) / [...] groups that look like version/variant junk."""
verkw = re.compile(r"(\d|version|hotfix|bug.?fix|final|anniversary|classic\+|" verkw = re.compile(r"(\d|version|hotfix|bug.?fix|final|anniversary|classic\+|"
@@ -187,6 +247,17 @@ def place_patch(patch, origin):
if not ok: if not ok:
report.append(("PATCH", "FAIL", origin, f"ips/{bk}: {msg[:50]}", "")); return report.append(("PATCH", "FAIL", origin, f"ips/{bk}: {msg[:50]}", "")); return
place_rom(tmp_out, plat, hackname, origin); return place_rom(tmp_out, plat, hackname, origin); return
if ext == ".ups" and "vega" in patch.lower():
# Vega Fairy EX UPS targets 32 MB FireRed (US/EU); JP 1.0 is 16 MB.
for base_key in ("firered", "firered_jp"):
if not os.path.exists(BASES[base_key]):
continue
ok, msg = try_apply(patch, base_key, tmp_out)
if ok:
place_rom(tmp_out, "gba", hackname, origin + f" [base={base_key}]")
return
report.append(("PATCH", "FAIL", origin, f"vega ups: {msg[:50]}", ""))
return
if ext == ".xdelta": if ext == ".xdelta":
report.append(("PATCH", "SKIP", origin, "xdelta: handle via xdelta tool", "")); return report.append(("PATCH", "SKIP", origin, "xdelta: handle via xdelta tool", "")); return
# BPS / UPS: auto-detect base across all platforms (CRC-verified) # BPS / UPS: auto-detect base across all platforms (CRC-verified)
@@ -208,8 +279,20 @@ def archive_doc(path, origin):
report.append(("DOC", "OK", origin, f"{os.path.getsize(path):,}b", rel)) report.append(("DOC", "OK", origin, f"{os.path.getsize(path):,}b", rel))
def place_base_rom(src, plat, out_name, origin):
if PLACE:
d = os.path.join(ROMS, plat); os.makedirs(d, exist_ok=True)
shutil.copy2(src, os.path.join(d, out_name))
report.append(("BASE", "OK", origin, f"{os.path.getsize(src):,}b", f"{plat}/{out_name}"))
def process_file(path, origin=None): def process_file(path, origin=None):
origin = origin or os.path.basename(path) origin = origin or os.path.basename(path)
base = os.path.basename(path).lower()
for sub, plat, out_name in BASE_ROM_FILES:
if sub in base:
place_base_rom(path, plat, out_name, origin)
return
ext = os.path.splitext(path)[1].lower() ext = os.path.splitext(path)[1].lower()
if ext in ROM_EXT: if ext in ROM_EXT:
place_rom(path, ROM_EXT[ext], clean_name(path), origin) place_rom(path, ROM_EXT[ext], clean_name(path), origin)
@@ -219,10 +302,70 @@ def process_file(path, origin=None):
archive_doc(path, origin) archive_doc(path, origin)
elif ext in ARCHIVE_EXT: elif ext in ARCHIVE_EXT:
process_archive(path, origin) process_archive(path, origin)
elif ext == ".exe":
place_pc_installer(path, clean_name(path), origin)
elif ext == ".dmg":
place_pc_archive(path, clean_name(path), origin)
elif ext == ".jgp":
place_pc_archive(path, clean_name(path), origin, variant="[Joiplay]")
else: else:
report.append(("SKIP", "?", origin, f"unknown ext {ext}", "")) report.append(("SKIP", "?", origin, f"unknown ext {ext}", ""))
def place_pc_archive(path, hackname, origin, variant=None):
tag = variant or detect_variant(path) or ""
suffix = f" {tag}" if tag else ""
out_name = f"Pokemon - {hackname} (Hack){suffix}.zip"
dest_dir = pc_dest_dir(tag)
dest = os.path.join(dest_dir, out_name)
ext = os.path.splitext(path)[1].lower()
if ext == ".zip":
if PLACE:
os.makedirs(dest_dir, exist_ok=True)
shutil.copy2(path, dest)
report.append(("PC", "OK", origin, f"{os.path.getsize(path):,}b zip", f"{dest_dir.split('/')[-1]}/{out_name}"))
return
if ext == ".dmg":
# Repack macOS disk image -> zip for RomM download (extract with bsdtar).
ext = ".dmg"
tmp = tempfile.mkdtemp(prefix="pcrepack_")
try:
r = subprocess.run(["bsdtar", "-xf", path, "-C", tmp], capture_output=True, text=True)
if r.returncode != 0:
report.append(("PC", "FAIL", origin, (r.stderr or "bsdtar failed")[:60], "")); return
tmp_zip = dest + ".part"
with zipfile.ZipFile(tmp_zip, "w", zipfile.ZIP_DEFLATED) as z:
for root, _, files in os.walk(tmp):
for fn in files:
fp = os.path.join(root, fn)
z.write(fp, os.path.relpath(fp, tmp))
if PLACE:
os.makedirs(dest_dir, exist_ok=True)
os.replace(tmp_zip, dest)
else:
os.remove(tmp_zip)
report.append(("PC", "OK", origin, f"repacked {ext}->zip", f"{dest_dir.split('/')[-1]}/{out_name}"))
finally:
shutil.rmtree(tmp, ignore_errors=True)
def place_pc_installer(path, hackname, origin):
"""Wrap a standalone .exe installer in a zip so RomM can serve it."""
tag = detect_variant(path) or "[Installer]"
out_name = f"Pokemon - {hackname} (Hack) {tag}.zip"
dest_dir = pc_dest_dir(tag)
dest = os.path.join(dest_dir, out_name)
tmp_zip = dest + ".part"
with zipfile.ZipFile(tmp_zip, "w", zipfile.ZIP_DEFLATED) as z:
z.write(path, os.path.basename(path))
if PLACE:
os.makedirs(dest_dir, exist_ok=True)
os.replace(tmp_zip, dest)
else:
os.remove(tmp_zip)
report.append(("PC", "OK", origin, "installer wrapped in zip", f"{dest_dir.split('/')[-1]}/{out_name}"))
def process_archive(path, origin): def process_archive(path, origin):
# keep the source archive # keep the source archive
if PLACE: if PLACE:
@@ -242,8 +385,12 @@ def process_archive(path, origin):
subprocess.run(["docker", "run", "--rm", "-v", f"{os.path.dirname(path)}:/in:ro", subprocess.run(["docker", "run", "--rm", "-v", f"{os.path.dirname(path)}:/in:ro",
"-v", f"{tmp}:/out", "node:lts", "bash", "-c", "-v", f"{tmp}:/out", "node:lts", "bash", "-c",
"apt-get update -qq>/dev/null 2>&1; apt-get install -y -qq unar>/dev/null 2>&1; " "apt-get update -qq>/dev/null 2>&1; apt-get install -y -qq unar>/dev/null 2>&1; "
f"unar -force-overwrite -o /out '/in/{os.path.basename(path)}' >/dev/null"], f"unar -force-overwrite -o /out '/in/{os.path.basename(path)}' >/dev/null; "
"chown -R $(stat -c %u:%g /out) /out"],
capture_output=True, text=True) capture_output=True, text=True)
if is_pc_fan_game(tmp):
place_pc_archive(path, clean_name(path), origin)
return
for root, _, files in os.walk(tmp): for root, _, files in os.walk(tmp):
for fn in files: for fn in files:
fp = os.path.join(root, fn) fp = os.path.join(root, fn)
@@ -258,7 +405,16 @@ def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")] args = [a for a in sys.argv[1:] if not a.startswith("--")]
PLACE = "--place" in sys.argv PLACE = "--place" in sys.argv
incoming = args[0] incoming = args[0]
for fn in sorted(os.listdir(incoming)):
def _sort_key(fn):
low = fn.lower()
if "fire red (j)" in low:
return (0, fn)
if os.path.splitext(fn)[1].lower() in PATCH_EXT:
return (2, fn)
return (1, fn)
for fn in sorted(os.listdir(incoming), key=_sort_key):
fp = os.path.join(incoming, fn) fp = os.path.join(incoming, fn)
if os.path.isfile(fp): if os.path.isfile(fp):
process_file(fp) process_file(fp)
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Sync vault hack notes with placed RomM library paths (2026-06-23 batch)."""
import re
from pathlib import Path
VAULT = Path(r"C:\Users\MattC\Documents\Obsidian Vault\Pokémon ROM Hacks\Hacks")
# RomM filename stem (without Pokemon - / (Hack) / variant) -> note stem
PLACED = {
# windows primary + variants
"Alchemist": ("PC", "roms/windows/Pokemon - Alchemist (Hack).zip", "Complete"),
"Abstract": ("PC", "roms/windows/Pokemon - Abstract (Hack).zip", "Complete"),
"Awakening ENG": ("PC", "roms/windows/Pokemon - Awakening ENG (Hack).zip", "Complete"),
"Bizarre ENGLISH": ("PC", "roms/windows/Pokemon - Bizarre ENGLISH (Hack).zip", "Complete"),
"Concealed 2.1": ("PC", "roms/windows/Pokemon - Concealed 2.1 (Hack).zip", "Complete"),
"Consonancia 1.2": ("PC", "roms/windows/Pokemon - Consonancia 1.2 (Hack).zip", "Complete"),
"Eon Guardians": ("PC", "roms/windows/Pokemon - Eon Guardians (Hack).zip", "Complete"),
"Fire Ash": ("PC", "roms/windows/Pokemon - Fire Ash (Hack).zip", "Complete"),
"Floral Tempus": ("GBA", "roms/windows/Pokemon - Floral Tempus (Hack).zip", "Complete"),
"HGSS Sevii": ("NDS", "roms/windows/Pokemon - HGSS Sevii (Hack).zip", "Complete"),
"Hero Legacy": ("PC", "roms/windows/Pokemon - Hero Legacy (Hack).zip", "Complete"),
"Jam Festival": ("PC", "roms/windows/Pokemon - Jam Festival (Hack).zip", "Complete"),
"Legends of the Arena": ("PC", "roms/windows/Pokemon - Legends of the Arena (Hack).zip", "Complete"),
"Mega Adventures": ("PC", "roms/windows/Pokemon - Mega Adventures (Hack).zip", "Complete"),
"Nightmare 1.3.0": ("PC", "roms/windows/Pokemon - Nightmare 1.3.0 (Hack).zip", "Complete"),
"Poketale": ("PC", "roms/windows/Pokemon - Poketale (Hack).zip", "Complete"),
"Prisme": ("PC", "roms/windows/Pokemon - Prisme (Hack) [Installer].zip", "Complete"),
"Shattered Light": ("PC", "roms/windows/Pokemon - Shattered Light (Hack).zip", "Ongoing"),
"Tectonic 3.3.1": ("PC", "roms/windows/Pokemon - Tectonic 3.4.1 (Hack).zip", "Ongoing"),
"Tectonic 3.4.1": ("PC", "roms/windows/Pokemon - Tectonic 3.4.1 (Hack).zip", "Ongoing"),
"Uranium": ("PC", "roms/windows/Pokemon - Uranium (Hack) [Installer].zip", "Complete"),
"InfiniteFusion": ("PC", "roms/windows/Pokemon - InfiniteFusion (Hack) [Full].zip", "Complete"),
"Infinity 3.3.1": ("PC", "roms/windows/Pokemon - Infinity 3.3.1 (Hack).zip", "Complete"),
"Rejuvenation": ("PC", "roms/windows/Pokemon - Rejuvenation (Hack).zip", "Ongoing"),
"Itinerant": ("GBA", "roms/windows/Pokemon - Itinerant (Hack).zip", "Ongoing"),
"Relict": ("PC", "roms/windows/Pokemon - Relict (Hack).zip", "Complete"),
"Reminiscencia": ("PC", "roms/windows/Pokemon - Reminiscencia (Hack).zip", "Complete"),
"Vanguard": ("PC", "roms/windows/Pokemon - Vanguard (Hack).zip", "Ongoing"),
"Xenoverse": ("PC", "roms/windows/Pokemon - Xenoverse (Hack).zip", "Complete"),
"Reborn": ("PC", "roms/windows/Pokemon - Reborn (Hack).zip", "Complete"),
"Coral": ("GBC", "roms/gbc/Hacks/Pokemon - Coral (Hack).gbc", "Beta"),
"Giratina Strikes Back Full": ("GBA", "roms/gba/Hacks/Pokemon - Giratina Strikes Back Full (Hack).gba", "Complete"),
"Sienna Complete": ("GBA", "roms/gba/Hacks/Pokemon - Sienna Complete (Hack).gba", "Complete"),
"Spades and Clubs": ("GBA", "roms/gba/Hacks/Pokemon - Spades and Clubs (Hack).gba", "Beta"),
"Emerald Seaglass": ("GBA", "roms/gba/Hacks/Pokemon - Emerald Seaglass (Hack).gba", "Complete"),
}
NOTE_MAP = {
"Awakening ENG": "Awakening",
"Bizarre ENGLISH": "Bizarre",
"Concealed 2.1": "Concealed",
"Consonancia 1.2": "Consonancia",
"Nightmare 1.3.0": "Nightmare",
"Tectonic 3.4.1": "Tectonic",
"Tectonic 3.3.1": "Tectonic",
"Giratina Strikes Back Full": "Giratina Strikes Back",
"Sienna Complete": "Sienna",
"Infinity 3.3.1": "Infinity",
"InfiniteFusion": "Infinite Fusion",
"Reminiscencia": "Reminiscencia",
}
VARIANTS_BLOCK = """## Alternate builds (RomM)
| Variant | Path |
|---|---|
{rows}
> Windows zip is the Playnite primary unless noted. Joiplay/Android/Linux/macOS builds are separate RomM download entries.
"""
def patch_fm(text, key, val):
if re.search(rf"^{key}:", text, re.M):
return re.sub(rf"^{key}:.*$", f'{key}: {val}', text, count=1, flags=re.M)
# insert after title
return re.sub(r"(^title:.*\n)", rf"\1{key}: {val}\n", text, count=1, flags=re.M)
def main():
updated = []
for rom_key, (plat, lp, status) in PLACED.items():
stem = NOTE_MAP.get(rom_key, rom_key)
p = VAULT / f"{stem}.md"
if not p.exists():
print(f"SKIP no note: {stem}")
continue
text = p.read_text(encoding="utf-8")
text = patch_fm(text, "platform", plat)
text = patch_fm(text, "status", status)
text = patch_fm(text, "library_path", f'"{lp}"')
if rom_key == "Infinity" or rom_key == "Infinity 3.3.1":
text = patch_fm(text, "version", '"3.3.1"')
if rom_key == "Nightmare 1.3.0":
text = patch_fm(text, "version", '"1.3.0"')
if rom_key == "Tectonic 3.4.1":
text = patch_fm(text, "version", '"3.4.1"')
# normalize In the library section
if "## In the library" in text:
text = re.sub(
r"(?ms)^## In the library\s*\n.*?(?=^## |\n\[\[Index|\Z)",
f"## In the library\n\n> Placed in RomM library (`{lp}`).\n\n",
text,
count=1,
)
p.write_text(text, encoding="utf-8")
updated.append(stem)
print(f"Updated {len(updated)} notes: {', '.join(sorted(updated))}")
if __name__ == "__main__":
main()
+1048 -65
View File
File diff suppressed because it is too large Load Diff