Files
pokemon/scripts/gen-ludusavi-manifest.py
T
ginnoir 2ebc2cb276 feat: add PC fan-game migration tooling for RomM windows platform
Place RPG-Maker/Essentials hacks into RomM's windows library with dry-run migration, Playnite Game.exe launcher, and Ludusavi save-path manifest.
2026-06-23 00:09:44 -05:00

68 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Generate a Ludusavi secondary manifest for the PC hacks served by RomM.
Pokémon Essentials (v18+) writes saves to:
%USERPROFILE%/Saved Games/<Game.ini Title>/Game.rxdata
The exact subfolder is the `Title=` field of each game's Game.ini, so we read it
straight out of the placed archive instead of guessing. Older Essentials (<=v17)
saved into the game folder itself — covered by the commented `<base>` fallback.
Point Ludusavi at the emitted YAML as a secondary/custom manifest, then pair its
backup target with Syncthing to replicate saves across Windows / Steam Deck.
Run on valhalla after romhack-pc-migrate.py --apply:
python3 gen-ludusavi-manifest.py > ludusavi/pokemon-pc-hacks.yaml
"""
import os, sys, zipfile, configparser, io
WIN = "/storage1/Emulation/roms/windows"
def sq(s):
"""YAML single-quoted scalar: an embedded apostrophe must be doubled."""
return "'" + str(s).replace("'", "''") + "'"
def game_title(zip_path):
"""Pull Title= from the first */Game.ini inside the archive, if present."""
try:
with zipfile.ZipFile(zip_path) as z:
ini = next((n for n in z.namelist() if n.lower().endswith("game.ini")), None)
if not ini:
return None
raw = z.read(ini).decode("utf-8", "ignore")
cp = configparser.ConfigParser(strict=False, interpolation=None)
cp.read_string(raw if raw.lstrip().startswith("[") else "[Game]\n" + raw)
for sect in cp.sections():
if cp.has_option(sect, "Title"):
return cp.get(sect, "Title").strip()
except Exception as e:
print(f"# WARN {os.path.basename(zip_path)}: {e}", file=sys.stderr)
return None
def main():
print("# Ludusavi secondary manifest — Pokémon PC hacks (generated)")
print("# Add via Ludusavi > Settings > Manifests, or merge into a custom manifest.")
print("# Save path = ~/Saved Games/<Game.ini Title> (Essentials v18+).")
print("# Verify each on first save; for Essentials <=v17 uncomment the <base> line.\n")
zips = sorted(f for f in os.listdir(WIN) if f.lower().endswith(".zip"))
for f in zips:
name = os.path.splitext(f)[0] # "Pokemon - <Hack> (Hack)"
title = game_title(os.path.join(WIN, f))
print(f"{sq(name)}:")
print(" files:")
if title:
print(f" {sq('<home>/Saved Games/' + title)}:")
print(" tags: [save]")
else:
print(f" # TODO no Game.ini Title found — set save path manually")
print(f" {sq('<home>/Saved Games/' + name)}:")
print(" tags: [save]")
print(" # '<base>': # older Essentials saved in the game folder")
print(" # tags: [save]")
if __name__ == "__main__":
main()