Add Pokémon Infinity money and debug-mode helpers.

These scripts edit Essentials save money offline and toggle the in-game debug menu so pokeball shopping does not require manual save hex editing.
This commit is contained in:
ginnoir
2026-06-26 23:58:39 -05:00
parent 44183fb0fb
commit 9351f8b415
3 changed files with 290 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Lightweight money editor for Pokémon Infinity (Essentials / RPG Maker XP).
Edits @money on PokeBattle_Trainer in Game_*.rxdata save slots under:
%USERPROFILE%\\Saved Games\\Pokémon Infinity
Close the game before running. A timestamped .bak backup is created first.
"""
from __future__ import annotations
import argparse
import glob
import os
import shutil
import sys
from datetime import datetime
from pathlib import Path
try:
from rubymarshal.reader import loads
from rubymarshal.writer import writes
except ImportError:
print("Missing dependency: pip install rubymarshal", file=sys.stderr)
sys.exit(1)
DEFAULT_SAVE_DIR = Path.home() / "Saved Games" / "Pokémon Infinity"
ALT_SAVE_DIR = Path.home() / "Saved Games" / "Pokmon Infinity" # typo seen in ludusavi manifest
DEFAULT_MONEY = 999_999
MAX_MONEY = 9_999_999
def resolve_save_dir(explicit: str | None) -> Path:
if explicit:
path = Path(explicit)
if not path.is_dir():
raise SystemExit(f"Save directory not found: {path}")
return path
for candidate in (DEFAULT_SAVE_DIR, ALT_SAVE_DIR):
if candidate.is_dir():
return candidate
raise SystemExit(
"Could not find Pokémon Infinity saves. Expected one of:\n"
f" {DEFAULT_SAVE_DIR}\n"
f" {ALT_SAVE_DIR}\n"
"Use --save-dir if yours is elsewhere."
)
def list_save_files(save_dir: Path) -> list[Path]:
files = sorted(save_dir.glob("Game*.rxdata"))
return [f for f in files if f.name != "Settings.rxdata"]
def read_money(path: Path) -> int:
with path.open("rb") as fh:
trainer = loads(fh.read())
if getattr(trainer, "ruby_class_name", None) != "PokeBattle_Trainer":
raise ValueError(f"{path.name} is not a trainer save (unexpected format)")
if "@money" not in trainer.attributes:
raise ValueError(f"{path.name} has no @money field")
return int(trainer.attributes["@money"])
def write_money(path: Path, amount: int) -> None:
if amount < 0 or amount > MAX_MONEY:
raise ValueError(f"Money must be between 0 and {MAX_MONEY:,}")
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
backup = path.with_suffix(path.suffix + f".bak.{stamp}")
shutil.copy2(path, backup)
with path.open("rb") as fh:
trainer = loads(fh.read())
trainer.attributes["@money"] = amount
with path.open("wb") as fh:
fh.write(writes(trainer))
print(f" backup -> {backup.name}")
def cmd_show(save_dir: Path) -> None:
files = list_save_files(save_dir)
if not files:
raise SystemExit(f"No Game*.rxdata files in {save_dir}")
print(f"Save folder: {save_dir}\n")
for path in files:
try:
money = read_money(path)
print(f" {path.name:16} ${money:,}")
except Exception as exc: # noqa: BLE001 - surface per-file issues
print(f" {path.name:16} (unreadable: {exc})")
def cmd_set(save_dir: Path, amount: int, slot: str | None) -> None:
files = list_save_files(save_dir)
if not files:
raise SystemExit(f"No Game*.rxdata files in {save_dir}")
if slot:
matches = [f for f in files if f.stem.lower() == slot.lower() or f.name.lower() == slot.lower()]
if not matches:
matches = [f for f in files if slot in f.name]
if not matches:
raise SystemExit(f"No save matching slot {slot!r}. Available: {[f.name for f in files]}")
targets = matches
else:
targets = files
print(f"Save folder: {save_dir}")
for path in targets:
old = read_money(path)
write_money(path, amount)
print(f" {path.name}: ${old:,} -> ${amount:,}")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Set money in Pokémon Infinity save files (close the game first)."
)
parser.add_argument(
"--save-dir",
help="Override save directory (default: %%USERPROFILE%%\\Saved Games\\Pokémon Infinity)",
)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("show", help="Show money in each save slot")
set_p = sub.add_parser("set", help="Set money in one or all slots")
set_p.add_argument(
"amount",
nargs="?",
type=int,
default=DEFAULT_MONEY,
help=f"Target money (default: {DEFAULT_MONEY:,})",
)
set_p.add_argument(
"--slot",
help="Only edit this slot (e.g. Game_1 or Game_1.rxdata). Default: all Game*.rxdata",
)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
save_dir = resolve_save_dir(args.save_dir)
if args.command == "show":
cmd_show(save_dir)
elif args.command == "set":
cmd_set(save_dir, args.amount, args.slot)
if __name__ == "__main__":
main()