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:
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Enable or disable Pokémon Infinity debug mode by patching Data/Scripts.rxdata."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import zlib
|
||||||
|
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)
|
||||||
|
|
||||||
|
RGSS2_SCRIPT_INDEX = 4
|
||||||
|
DEBUG_FALSE = b"$DEBUG=false"
|
||||||
|
DEBUG_TRUE = b"$DEBUG=true"
|
||||||
|
|
||||||
|
|
||||||
|
def patch_scripts(path: Path, enable: bool) -> bool:
|
||||||
|
with path.open("rb") as fh:
|
||||||
|
scripts = loads(fh.read())
|
||||||
|
|
||||||
|
entry = scripts[RGSS2_SCRIPT_INDEX]
|
||||||
|
name = entry[1].decode() if isinstance(entry[1], bytes) else str(entry[1])
|
||||||
|
if name != "RGSS2Compatibility":
|
||||||
|
raise ValueError(f"Unexpected script at index {RGSS2_SCRIPT_INDEX}: {name!r}")
|
||||||
|
|
||||||
|
source = zlib.decompress(entry[2])
|
||||||
|
has_true = DEBUG_TRUE in source
|
||||||
|
has_false = DEBUG_FALSE in source
|
||||||
|
|
||||||
|
if enable:
|
||||||
|
if has_true and not has_false:
|
||||||
|
print("Debug mode already enabled.")
|
||||||
|
return False
|
||||||
|
if not has_false:
|
||||||
|
raise ValueError("Could not find $DEBUG=false in RGSS2Compatibility")
|
||||||
|
patched = source.replace(DEBUG_FALSE, DEBUG_TRUE, 1)
|
||||||
|
else:
|
||||||
|
if has_false and not has_true:
|
||||||
|
print("Debug mode already disabled.")
|
||||||
|
return False
|
||||||
|
if not has_true:
|
||||||
|
raise ValueError("Could not find $DEBUG=true in RGSS2Compatibility")
|
||||||
|
patched = source.replace(DEBUG_TRUE, DEBUG_FALSE, 1)
|
||||||
|
scripts[RGSS2_SCRIPT_INDEX] = [entry[0], entry[1], zlib.compress(patched, 9)]
|
||||||
|
|
||||||
|
with path.open("wb") as fh:
|
||||||
|
fh.write(writes(scripts))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Toggle Pokémon Infinity debug mode.")
|
||||||
|
parser.add_argument(
|
||||||
|
"game_dir",
|
||||||
|
nargs="?",
|
||||||
|
default=r"C:\Users\MattC\roms\windows\Pokemon Infinity",
|
||||||
|
help="Path to the extracted game folder (contains Game.exe)",
|
||||||
|
)
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument("--enable", action="store_true", default=True, help="Enable debug (default)")
|
||||||
|
group.add_argument("--disable", action="store_true", help="Restore normal mode")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
game_dir = Path(args.game_dir)
|
||||||
|
scripts_path = game_dir / "Data" / "Scripts.rxdata"
|
||||||
|
if not (game_dir / "Game.exe").is_file():
|
||||||
|
raise SystemExit(f"Game.exe not found under {game_dir}")
|
||||||
|
if not scripts_path.is_file():
|
||||||
|
raise SystemExit(f"Scripts.rxdata not found at {scripts_path}")
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
backup = scripts_path.with_suffix(f".rxdata.bak.{stamp}")
|
||||||
|
shutil.copy2(scripts_path, backup)
|
||||||
|
print(f"Backup: {backup}")
|
||||||
|
|
||||||
|
changed = patch_scripts(scripts_path, enable=not args.disable)
|
||||||
|
if changed:
|
||||||
|
state = "enabled" if not args.disable else "disabled"
|
||||||
|
print(f"Debug mode {state}.")
|
||||||
|
if not args.disable:
|
||||||
|
print()
|
||||||
|
print("In-game: open the pause menu -> Debug -> Set Money / Add Item")
|
||||||
|
print("Load screen also gets a Debug option. Back up saves before experimenting.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Quick money edit for Pokémon Infinity save files.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Wrapper around scripts/infinity-money-trainer.py. Close the game first.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\infinity-money.ps1
|
||||||
|
.\infinity-money.ps1 -Amount 500000
|
||||||
|
.\infinity-money.ps1 -Show
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[int]$Amount = 999999,
|
||||||
|
[string]$Slot,
|
||||||
|
[switch]$Show,
|
||||||
|
[string]$SaveDir
|
||||||
|
)
|
||||||
|
|
||||||
|
$script = Join-Path $PSScriptRoot 'infinity-money-trainer.py'
|
||||||
|
if (-not (Test-Path -LiteralPath $script)) {
|
||||||
|
throw "Missing trainer script: $script"
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = @($script)
|
||||||
|
if ($SaveDir) { $args += @('--save-dir', $SaveDir) }
|
||||||
|
|
||||||
|
if ($Show) {
|
||||||
|
$args += 'show'
|
||||||
|
} else {
|
||||||
|
$args += @('set', [string]$Amount)
|
||||||
|
if ($Slot) { $args += @('--slot', $Slot) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Pokémon Infinity money trainer" -ForegroundColor Cyan
|
||||||
|
Write-Host "Close the game before continuing." -ForegroundColor Yellow
|
||||||
|
python @args
|
||||||
Reference in New Issue
Block a user