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:
@@ -91,6 +91,12 @@ def types_cell(types):
|
||||
def key(r):
|
||||
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
|
||||
|
||||
# Logical hardware order for human-facing lists.
|
||||
@@ -152,7 +158,7 @@ BASE_MOCS = {
|
||||
def platform_static_table(recs):
|
||||
head = "| Hack | Base | Version | Status | Type |\n|---|---|---|---|---|"
|
||||
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'])} |"
|
||||
for r in sorted(recs, key=key)
|
||||
]
|
||||
@@ -162,14 +168,14 @@ def base_static_table(recs, with_base):
|
||||
if with_base:
|
||||
head = "| Hack | Base | Version | Dev | Engine | Type |\n|---|---|---|---|---|---|"
|
||||
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'])} |"
|
||||
for r in sorted(recs, key=key)
|
||||
]
|
||||
else:
|
||||
head = "| Hack | Version | Dev | Engine | Type |\n|---|---|---|---|---|"
|
||||
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'])} |"
|
||||
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)}
|
||||
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'])} |"
|
||||
for r in sorted(recs, key=lambda r: (order.get(r["platform"], 99), key(r)))
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -21,6 +21,7 @@ import sys, os, re, zlib, shutil, subprocess, zipfile, unicodedata, tempfile
|
||||
ROMS = "/storage1/Emulation/roms"
|
||||
PATCHES = "/storage1/igir/romhacks/patches"
|
||||
APPLY = "/storage1/igir/romhacks/tools/apply.py"
|
||||
WIN_DEST = "/storage1/Emulation/roms/windows"
|
||||
|
||||
BASES = {
|
||||
"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",
|
||||
"firered": f"{ROMS}/gba/Pokemon - FireRed 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",
|
||||
}
|
||||
# candidate bases to try per target platform (order = preference)
|
||||
CANDIDATES = {
|
||||
"gba": ["emerald", "firered", "leafgreen", "ruby", "sapphire"],
|
||||
"gba": ["firered_jp", "emerald", "firered", "leafgreen", "ruby", "sapphire"],
|
||||
"gbc": ["crystal_rev1", "gold", "silver"],
|
||||
"gb": ["red"],
|
||||
}
|
||||
# IPS patches (no checksum) need an explicit base by name substring
|
||||
IPS_BASE_MAP = [
|
||||
("seaglass", "emerald", "gba"),
|
||||
("emeraldseaglass", "emerald", "gba"),
|
||||
("azure horizons", "firered", "gba"),
|
||||
("sienna", "ruby", "gba"),
|
||||
("crystal kaizo", "crystal_rev1", "gbc"),
|
||||
("kaizo", "crystal_rev1", "gbc"), # fallback for crystal kaizo rar
|
||||
]
|
||||
@@ -57,6 +61,23 @@ ARCHIVE_EXT = {".zip", ".rar", ".7z"}
|
||||
NAME_OVERRIDES = [
|
||||
("emeraldseaglass", "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"),
|
||||
("saiph 2", "Saiph 2"), ("saiph2", "Saiph 2"),
|
||||
("saiph", "Saiph"),
|
||||
@@ -68,12 +89,51 @@ NAME_OVERRIDES = [
|
||||
]
|
||||
# Files we will NOT auto-place (unidentified / need user to name). Reported instead.
|
||||
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
|
||||
report = []
|
||||
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):
|
||||
"""Remove trailing (...) / [...] groups that look like version/variant junk."""
|
||||
verkw = re.compile(r"(\d|version|hotfix|bug.?fix|final|anniversary|classic\+|"
|
||||
@@ -187,6 +247,17 @@ def place_patch(patch, origin):
|
||||
if not ok:
|
||||
report.append(("PATCH", "FAIL", origin, f"ips/{bk}: {msg[:50]}", "")); 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":
|
||||
report.append(("PATCH", "SKIP", origin, "xdelta: handle via xdelta tool", "")); return
|
||||
# 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))
|
||||
|
||||
|
||||
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):
|
||||
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()
|
||||
if ext in ROM_EXT:
|
||||
place_rom(path, ROM_EXT[ext], clean_name(path), origin)
|
||||
@@ -219,10 +302,70 @@ def process_file(path, origin=None):
|
||||
archive_doc(path, origin)
|
||||
elif ext in ARCHIVE_EXT:
|
||||
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:
|
||||
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):
|
||||
# keep the source archive
|
||||
if PLACE:
|
||||
@@ -242,8 +385,12 @@ def process_archive(path, origin):
|
||||
subprocess.run(["docker", "run", "--rm", "-v", f"{os.path.dirname(path)}:/in:ro",
|
||||
"-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; "
|
||||
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)
|
||||
if is_pc_fan_game(tmp):
|
||||
place_pc_archive(path, clean_name(path), origin)
|
||||
return
|
||||
for root, _, files in os.walk(tmp):
|
||||
for fn in files:
|
||||
fp = os.path.join(root, fn)
|
||||
@@ -258,7 +405,16 @@ def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
PLACE = "--place" in sys.argv
|
||||
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)
|
||||
if os.path.isfile(fp):
|
||||
process_file(fp)
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user