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:
+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)
|
||||
|
||||
Reference in New Issue
Block a user