feat: ROM-hack dedup, conflict resolution, and vault library_path resync

Catalog cleanup pass across gb/gbc/gba/nds/3ds:
- Standardize nonstandard hack filenames to `Pokemon - <Name> [Hack].<ext>`;
  remove verified byte-identical duplicates (conform-rom-names.sh).
- Resolve 9 version-conflicts via side-by-side emulator comparison
  (resolve-version-conflicts.sh): promote newer builds, keep genuine
  distinct hacks, fix a mislabeled Dark Rising slot (was the Digimon
  crossover "Worlds Collide"), and rebuild Mega Power 5.77 + HeartGold
  Generations v2.0 fresh from their patches.
- Resync 286 vault notes' `library_path` to the real on-disk [Hack] files
  and fix 15 stale platform fields (resync-vault-library-paths.py).
- Regenerate MOC tables + wiki/catalog.json (396 notes).

Also includes pending Playnite PC-hack setup/repair scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ginnoir
2026-06-25 14:29:05 -05:00
co-authored by Claude Opus 4.8
parent ae56d101b7
commit 809600a04d
8 changed files with 755 additions and 435 deletions
+3
View File
@@ -22,3 +22,6 @@ Thumbs.db
.scrape/
.scrape_lib/
scripts/__pycache__/
# Per-machine ROM compare/patch scratch (side-by-side emulator runs, patch builds).
.compare/
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Conform nonstandard ROM-hack filenames to the `Pokemon - <Name> [Hack].<ext>`
# standard, and remove verified byte-identical duplicates. Runs on valhalla.
# DRY_RUN=1 bash conform-rom-names.sh # preview (default)
# DRY_RUN=0 bash conform-rom-names.sh # execute
set -u
ROOT=/storage1/Emulation/roms
DRY_RUN=${DRY_RUN:-1}
del(){ # del <platform> <file> — delete only if it still exists
local f="$ROOT/$1/$2"
if [ ! -f "$f" ]; then echo "SKIP del (missing): $1/$2"; return; fi
if [ "$DRY_RUN" = 1 ]; then echo "DEL $1/$2"; else rm -- "$f" && echo "DEL $1/$2"; fi
}
mv1(){ # mv1 <platform> <src> <dst> — rename only if dst does NOT exist
local s="$ROOT/$1/$2" d="$ROOT/$1/$3"
if [ ! -f "$s" ]; then echo "SKIP mv (missing src): $1/$2"; return; fi
if [ -e "$d" ]; then echo "SKIP mv (dst exists): $1/$3"; return; fi
if [ "$DRY_RUN" = 1 ]; then echo "MV $1/$2 -> $3"; else mv -n -- "$s" "$d" && echo "MV $1/$2 -> $3"; fi
}
echo "=== DELETE: verified byte-identical duplicates of an existing [Hack] file ==="
del gba "sors v1.3 .gba"
del gba "Pokemon FireRed Rocket Edition.gba"
del gba "Pokemon SOTS (v2.1.2).gba"
del gba "U_E_Inclement_Emerald_Beta_1.1.3.gba"
del gba "flora-sky-main-completed.gba"
del gba "Giratina Strikes Back Full.gba"
del gba "PK EMR Kaizo v2.0.gba"
echo
echo "=== RENAME: GBA ==="
mv1 gba "CHROME1.5.gba" "Pokemon - Chrome [Hack].gba"
mv1 gba "DarkVioletFinal2018V1.gba" "Pokemon - Dark Violet [Hack].gba"
mv1 gba "Energized v1.0.21.gba" "Pokemon - Energized [Hack].gba"
mv1 gba "Exceeded v11.5.gba" "Pokemon - Exceeded [Hack].gba"
mv1 gba "Hisui Red.gba" "Pokemon - Hisui Red [Hack].gba"
mv1 gba "Leaf Green Extreme Randomizer.gba" "Pokemon - Leaf Green Extreme Randomizer [Hack].gba"
mv1 gba "Moon_Galaxy_Completed.gba" "Pokemon - Moon Galaxy [Hack].gba"
mv1 gba "PKMN EMR PARTY RANDOMIZER.GBA" "Pokemon - Emerald Party Randomizer [Hack].gba"
mv1 gba "PerfectE v5.4 PSS.gba" "Pokemon - Perfect Emerald [Hack].gba"
mv1 gba "Pokemon Fire Red 898 Randomizer v1.2.3.gba" "Pokemon - Fire Red 898 Randomizer [Hack].gba"
mv1 gba "Pokemon Fusion Origins Ver 3.8.gba" "Pokemon - Fusion Origins [Hack].gba"
mv1 gba "Pokemon Ultimate Fusion.gba" "Pokemon - Ultimate Fusion [Hack].gba"
mv1 gba "Pokemon_Infernal_Legend_beta_2.1.GBA" "Pokemon - Infernal Legend [Hack].gba"
mv1 gba "Pokemon_Sunset_Orange_a1.0.gba" "Pokemon - Sunset Orange [Hack].gba"
mv1 gba "QuetzalSpanishAlpha8v4.gba" "Pokemon - Quetzal (Spanish) [Hack].gba"
mv1 gba "autumn orange v1.2 casual mode.gba" "Pokemon - Autumn Orange [Hack].gba"
mv1 gba "fr multiverse v1.6.gba" "Pokemon - FireRed Multiverse [Hack].gba"
mv1 gba "saffron-d2.gba" "Pokemon - Saffron [Hack].gba"
mv1 gba "the-unown-king.gba" "Pokemon - The Unown King [Hack].gba"
mv1 gba "wings of chaos alpha v2.5.gba" "Pokemon - Wings of Chaos [Hack].gba"
echo
echo "=== RENAME: 3DS ==="
mv1 3ds "Pokemon Binary Sun v1d3d1 - Patched by Ducumon.3ds" "Pokemon - Binary Sun [Hack].3ds"
mv1 3ds "Pokemon Omega Ruby QoL.cxi" "Pokemon - Omega Ruby QoL [Hack].cxi"
echo
echo "=== RENAME: NDS ==="
mv1 nds "Rainbow-Gold-v1.5.nds" "Pokemon - Rainbow Gold [Hack].nds"
mv1 nds "goldenshield.nds" "Pokemon - Golden Shield [Hack].nds"
mv1 nds "Pokemon Definitive HeartGold.nds" "Pokemon - Definitive HeartGold [Hack].nds"
mv1 nds "Pokemon Horror White v1.nds" "Pokemon - Horror White [Hack].nds"
mv1 nds "Pokemon Moon Black 2 (v4.2.3).nds" "Pokemon - Moon Black 2 [Hack].nds"
mv1 nds "Pokemon Black 2 Redux Complete (v1.4.1).nds" "Pokemon - Black 2 Redux [Hack].nds"
echo
echo "=== RENAME: phase 2 — web/header-identified hacks ==="
# Hyper EMR LA = Hyper Emerald v5.7 "Lost Artifacts" (Destvol et al.)
mv1 gba "Hyper EMR LA v5.7 bugfix 2.gba" "Pokemon - Hyper Emerald Lost Artifacts [Hack].gba"
# header BPEE/FireRed-base confirmations below
mv1 gba "darkfireb2.1.3.gba" "Pokemon - DarkFire [Hack].gba"
mv1 gba "MioPidgey.gba" "Pokemon - MioPidgey [Hack].gba"
mv1 gba "re universe.gba" "Pokemon - Re Universe [Hack].gba"
mv1 gba "TRE Johto Release April 30, 2025.gba" "Pokemon - Team Rocket Edition (DragonsDen) [Hack].gba"
# Elysium ships split A/B; this file is the Part B half
mv1 gba "Elysium Part B (v2.0).gba" "Pokemon - Elysium (Part B) [Hack].gba"
# ScalretFinal: product code CTR-P-ECLA = Alpha Sapphire base; "Scalret" = Scarlet
mv1 3ds "ScalretFinal.3ds" "Pokemon - Scarlet [Hack].3ds"
echo
echo "=== RENAME: phase 3 — user-confirmed IDs ==="
mv1 gba "Spirits.gba" "Pokemon - Spirits of the Storm [Hack].gba"
mv1 nds "EotS-v1.01.nds" "Pokemon - Mystery Dungeon - Explorers of the Spirit [Hack].nds"
mv1 gba "all-in-v1.0.gba" "Pokemon - All In [Hack].gba"
echo
echo "Done (DRY_RUN=$DRY_RUN)."
+67
View File
@@ -0,0 +1,67 @@
<#
.SYNOPSIS
Undo Playnite damage from setup-playnite-pc-hack.ps1 LiteDB writes.
Fixes:
1. Removes broken GameScannerConfig (null emulator, scoop\apps) from scanners.db
2. Clears active library filter that restricts view to RomM only
3. Disables pc_windows RomM mapping until emulator is recreated in Playnite UI
Playnite must be fully closed before running.
#>
$ErrorActionPreference = 'Stop'
if (Get-Process -Name 'Playnite*' -ErrorAction SilentlyContinue) {
throw 'Close Playnite completely, then re-run.'
}
$playniteConfig = Join-Path $env:LOCALAPPDATA 'Playnite\config.json'
$scannersDb = Join-Path $env:LOCALAPPDATA 'Playnite\library\scanners.db'
$rommConfig = Join-Path $env:LOCALAPPDATA 'Playnite\ExtensionsData\9700aa21-447d-41b4-a989-acd38f407d9f\config.json'
$liteDbDll = Join-Path $env:LOCALAPPDATA 'Playnite\LiteDB.dll'
$orphanEmuId = 'fa12bc5e-dbb9-4d21-b1e6-a67c2fa6a513'
$nullEmuId = '00000000-0000-0000-0000-000000000000'
# --- 1. Remove broken emulator scanner ---
Add-Type -Path $liteDbDll
$db = [LiteDB.LiteDatabase]::new("Filename=$scannersDb")
$col = $db.GetCollection('GameScannerConfig')
$removed = 0
foreach ($doc in @($col.FindAll())) {
$emuId = if ($doc['EmulatorId'].IsGuid) { $doc['EmulatorId'].AsGuid.ToString() } else { $doc['EmulatorId'].AsString }
$dir = if ($doc.ContainsKey('Directory')) { $doc['Directory'].AsString } else { '' }
if ($emuId -eq $nullEmuId -or $dir -like '*\scoop\apps') {
$col.Delete($doc['_id']) | Out-Null
$removed++
Write-Host "Removed broken scanner: Directory=$dir EmulatorId=$emuId"
}
}
$db.Dispose()
if ($removed -eq 0) { Write-Host 'No broken GameScannerConfig entries found.' }
# --- 2. Clear RomM-only library filter ---
$cfg = Get-Content -Raw $playniteConfig | ConvertFrom-Json
if ($cfg.FilterSettings.Library -and $cfg.FilterSettings.Library.Ids -and $cfg.FilterSettings.Library.Ids.Count -gt 0) {
$ids = @($cfg.FilterSettings.Library.Ids)
Write-Host "Clearing library filter (was: $($ids -join ', '))"
$cfg.FilterSettings.Library.Ids = @()
}
$cfg | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $playniteConfig
Write-Host 'Playnite config: library filter cleared.'
# --- 3. Disable orphan pc_windows RomM mapping ---
$romm = Get-Content -Raw $rommConfig | ConvertFrom-Json
$pc = $romm.Mappings | Where-Object { $_.PlatformId -eq 'pc_windows' } | Select-Object -First 1
if ($pc) {
if ($pc.EmulatorId -eq $orphanEmuId) {
$pc.Enabled = $false
Write-Host "Disabled pc_windows RomM mapping (orphan emulator $orphanEmuId)."
Write-Host 'Re-enable after adding Pokemon PC Hack Launcher in Playnite UI.'
} else {
Write-Host "pc_windows mapping uses emulator $($pc.EmulatorId) - left enabled."
}
$romm | ConvertTo-Json -Depth 8 -Compress | Set-Content -Encoding UTF8 $rommConfig
}
Write-Host "`nDone. Open Playnite - full library should be visible again."
Write-Host 'Emulated-game scan errors on startup should be gone.'
+29
View File
@@ -0,0 +1,29 @@
# Rebuild emulators.db from readable BSON docs (fixes LiteDB page corruption).
$ErrorActionPreference = 'Stop'
Get-Process -Name 'Playnite*' -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2
$lib = Join-Path $env:LOCALAPPDATA 'Playnite\library'
$src = Join-Path $lib 'emulators.db'
$new = Join-Path $lib 'emulators.db.new'
$bak = Join-Path $lib 'emulators.db.broken'
Add-Type -Path (Join-Path $env:LOCALAPPDATA 'Playnite\LiteDB.dll')
if (Test-Path $new) { Remove-Item -Force $new }
$srcDb = [LiteDB.LiteDatabase]::new("Filename=$src")
$dstDb = [LiteDB.LiteDatabase]::new("Filename=$new")
$srcCol = $srcDb.GetCollection('Emulator')
$dstCol = $dstDb.GetCollection('Emulator')
$n = 0
foreach ($doc in $srcCol.FindAll()) {
$dstCol.Insert($doc) | Out-Null
$n++
}
$srcDb.Dispose()
$dstDb.Dispose()
Move-Item -Force $src $bak
Move-Item -Force $new $src
Write-Host "Rebuilt emulators.db from $n documents."
Write-Host "Old file: $bak"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Resolve the side-by-side version-conflict decisions (pairs 1-4,6-8; 5 & 9 were
# swapped live via fresh patches). Guarded: del only if present, mv only if dst free.
# DRY_RUN=1 bash resolve-version-conflicts.sh (default)
# DRY_RUN=0 bash resolve-version-conflicts.sh
set -u
G=/storage1/Emulation/roms/gba
DRY_RUN=${DRY_RUN:-1}
del(){ local f="$1/$2"; if [ ! -f "$f" ]; then echo "SKIP del (missing): $2"; return; fi
if [ "$DRY_RUN" = 1 ]; then echo "DEL $2"; else rm -- "$f" && echo "DEL $2"; fi; }
mv1(){ local s="$1/$2" d="$1/$3"; if [ ! -f "$s" ]; then echo "SKIP mv (missing src): $2"; return; fi
if [ -e "$d" ]; then echo "SKIP mv (dst exists): $3"; return; fi
if [ "$DRY_RUN" = 1 ]; then echo "MV $2 -> $3"; else mv -n -- "$s" "$d" && echo "MV $2 -> $3"; fi; }
echo "=== deletes (older/superseded loose copies) ==="
del "$G" "Pokemon - Unbound [Hack].gba" # pair2: older 2.1.1, replaced by loose 2.1.1.1
del "$G" "supermariomon v1.3.gba" # pair6: older than catalog 1.5
del "$G" "HnS_v1.0.gba" # pair7: older than catalog 1.2.1
del "$G" "advanceredux 31-3-25 (2).gba" # pair8: 2020 build, catalog is 2026
echo
echo "=== pair3 Dark Rising: fix the mislabeled Digimon file, then promote the real DR1 ==="
mv1 "$G" "Pokemon - Dark Rising [Hack].gba" "Pokemon vs Digimon - Worlds Collide [Hack].gba"
mv1 "$G" "PokemonDarkRising1Complete.GBA" "Pokemon - Dark Rising [Hack].gba"
echo
echo "=== pair2 Unbound: promote the newer loose 2.1.1.1 ==="
mv1 "$G" "Pokemon Unbound.gba" "Pokemon - Unbound [Hack].gba"
echo
echo "=== pair1 GS Chronicles: keep both, disambiguate the Gold inc build ==="
mv1 "$G" "Pokemon GS Chronicles (Beta 2.7.1).gba" "Pokemon - GS Chronicles (Gold inc) [Hack].gba"
echo
echo "=== pair4 Dark Worship: keep both, conform the Super Dark Worship name ==="
mv1 "$G" "Super Dark Worship Oficial.gba" "Pokemon - Super Dark Worship [Hack].gba"
echo
echo "Done (DRY_RUN=$DRY_RUN)."
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Resync each vault hack note's `library_path` to the actual on-disk ROM file.
The [Hack] filename standardization renamed ROMs on disk but left the vault
notes' `library_path` frontmatter pointing at old names/paths. This matches each
note to its real file (by normalized title, falling back to the old path's
basename) and rewrites `library_path` + any exact body copy of the old value.
Read-only by default; pass --apply to write. ROM listings are read from
/tmp/romsync/<plat>.txt (one filename per line), pulled from valhalla.
"""
import os, re, sys, glob
VAULT = r"C:\Users\MattC\Documents\Obsidian Vault"
HACKS = os.path.join(VAULT, "Pokémon ROM Hacks", "Hacks")
LISTS = os.path.join(os.path.dirname(__file__), "..", ".scrape", "romsync")
PLAT = {"GBA": "gba", "GBC": "gbc", "GB": "gb", "NDS": "nds", "3DS": "3ds", "PC": "windows"}
APPLY = "--apply" in sys.argv
def norm(s):
s = os.path.splitext(s)[0].lower()
s = re.sub(r"\[[^\]]*\]", "", s) # drop [Hack], [2.0.5], [Full], ...
s = re.sub(r"\([^)]*\)", "", s) # drop (Gold inc), (Spanish), ...
s = re.sub(r"pok[eé]?mon", "", s) # drop the franchise prefix/word
s = re.sub(r"[^a-z0-9]", "", s) # keep bare digits (Dark Rising 2)
return s
# ---- build per-platform file index: normkey -> [filenames] ----
idx = {}
for path in glob.glob(os.path.join(LISTS, "*.txt")):
plat = os.path.splitext(os.path.basename(path))[0]
idx[plat] = {}
for line in open(path, encoding="utf-8"):
fn = line.strip()
if not fn or not re.search(r"\.(gba|gbc|gb|nds|3ds|cci|cxi|zip)$", fn, re.I):
continue
idx[plat].setdefault(norm(fn), []).append(fn)
def fm_get(text, key):
m = re.search(rf"(?m)^{key}:\s*(.*)$", text)
return m.group(1).strip().strip('"').strip("'") if m else None
FUZZY = "--fuzzy" in sys.argv # also write the proposed fuzzy/cross-platform matches
def find_exact_all(key):
"""Exact normalized-key match across ALL platforms (catches wrong platform field)."""
hits = []
for p, files in idx.items():
for fn in files.get(key, []):
hits.append((p, fn))
return hits
# Notes whose only fuzzy candidate is a different/ambiguous hack — held for manual
# review, never auto-applied even with --fuzzy.
FUZZY_HOLD = {
"Bloody Fusion.md", "Too Many Types.md", "HeartGold Generation.md",
"FireRed Reignited LeafGreen Regrown.md", "Kanto Complete.md",
"Mystery Dungeon - Wigglytuff's Bizarre Adventure.md",
}
def find_fuzzy(nk):
"""One key is a substring of the other; require a [Hack] file; must be unique.
Skip very short keys (e.g. a hack literally named 'Z') to avoid spurious hits."""
if len(nk) < 4: return []
hits = []
for p, files in idx.items():
for k, fns in files.items():
if len(k) < 4 or not nk: continue
if nk in k or k in nk:
for fn in fns:
if "[hack]" in fn.lower():
hits.append((p, fn))
return sorted(set(hits))
changed = unchanged = unmatched = ambiguous = 0
report = {"CHANGED": [], "UNMATCHED": [], "AMBIGUOUS": [], "FUZZY": []}
for md in sorted(glob.glob(os.path.join(HACKS, "*.md"))):
text = open(md, encoding="utf-8").read()
title = fm_get(text, "title") or os.path.splitext(os.path.basename(md))[0]
platform = (fm_get(text, "platform") or "").upper()
cur = fm_get(text, "library_path") or ""
plat = PLAT.get(platform)
note = os.path.basename(md)
if not plat or plat not in idx:
continue
def write_path(newpath):
if not APPLY: return
new = re.sub(r"(?m)^(library_path:\s*).*$", lambda m: m.group(1) + newpath, text, count=1)
# Update body copies ONLY when the old value was an emulation-rom path;
# never touch /storage1/labdata/... served-files links.
if cur and cur.startswith(("roms/", "/storage1/Emulation/")):
new = new.replace(cur, newpath)
open(md, "w", encoding="utf-8", newline="").write(new)
nk = norm(title)
# 1) exact match within the declared platform (auto-apply)
cand = idx[plat].get(nk) or (idx[plat].get(norm(os.path.basename(cur))) if cur else None)
if cand and len(cand) == 1:
newpath = f"roms/{plat}/{cand[0]}"
if cur == newpath: unchanged += 1; continue
changed += 1; report["CHANGED"].append((note, cur, newpath)); write_path(newpath); continue
if cand and len(cand) > 1:
ambiguous += 1; report["AMBIGUOUS"].append((note, cand)); continue
# 2) cross-platform exact, then fuzzy substring -> PROPOSAL (apply only with --fuzzy)
prop = find_exact_all(nk) or find_fuzzy(nk)
if len(prop) == 1:
p2, fn = prop[0]; newpath = f"roms/{p2}/{fn}"
if cur == newpath: unchanged += 1; continue
report["FUZZY"].append((note, cur, newpath))
if FUZZY and note not in FUZZY_HOLD: changed += 1; write_path(newpath)
continue
unmatched += 1; report["UNMATCHED"].append((note, platform, cur))
summary = (f"{'APPLIED' if APPLY else 'DRY-RUN'}{' +FUZZY' if FUZZY else ''}: {changed} changed, "
f"{unchanged} already correct, {len(report['FUZZY'])} fuzzy proposals, "
f"{unmatched} unmatched (no on-disk file — likely Wanted/gated), {ambiguous} ambiguous")
lines = [summary, ""]
for tag in ("AMBIGUOUS", "FUZZY", "CHANGED", "UNMATCHED"):
rows = report[tag]
if not rows: continue
lines.append(f"--- {tag} ({len(rows)}) ---")
for r in rows:
if tag in ("CHANGED", "FUZZY"): lines.append(f" {r[0]}\n {r[1] or ''} -> {r[2]}")
elif tag == "AMBIGUOUS": lines.append(f" {r[0]} -> {r[1]}")
else: lines.append(f" {r[0]} [{r[1]}] (cur: {r[2] or ''})")
lines.append("")
rep = os.path.join(os.path.dirname(__file__), "..", ".scrape", "resync-report.txt")
open(rep, "w", encoding="utf-8").write("\n".join(lines))
print(summary)
print("full report ->", os.path.normpath(rep))
+31 -105
View File
@@ -1,127 +1,53 @@
<#
.SYNOPSIS
Configure Playnite + RomM plugin for Pokémon PC hacks (windows / pc_windows).
Configure RomM plugin mapping for PC hacks. Emulator must be added in Playnite UI (see below).
Playnite must be fully closed before running.
Playnite must be fully closed before running (only touches RomM plugin config.json).
#>
$ErrorActionPreference = 'Stop'
$PlayniteDir = Join-Path $env:LOCALAPPDATA 'Playnite'
$LiteDbDll = Join-Path $PlayniteDir 'LiteDB.dll'
$EmuDb = Join-Path $PlayniteDir 'library\emulators.db'
$RommConfig = Join-Path $PlayniteDir 'ExtensionsData\9700aa21-447d-41b4-a989-acd38f407d9f\config.json'
$Launcher = 'C:\Users\MattC\Documents\pokemon\scripts\launch-pc-hack.ps1'
$RommConfig = Join-Path $env:LOCALAPPDATA 'Playnite\ExtensionsData\9700aa21-447d-41b4-a989-acd38f407d9f\config.json'
$DestPath = 'C:\Users\MattC\roms\windows'
$EmuName = 'Pokemon PC Hack Launcher'
$PlatformSpec = 'pc_windows'
$ProfileName = 'Default'
$ProfileId = [guid]::NewGuid().ToString()
if (Get-Process -Name 'Playnite*' -ErrorAction SilentlyContinue) {
throw 'Close Playnite completely, then re-run this script.'
}
foreach ($p in @($LiteDbDll, $Launcher, $RommConfig)) {
if (-not (Test-Path $p)) { throw "Missing: $p" }
}
if (-not (Test-Path $RommConfig)) { throw "RomM config not found: $RommConfig" }
$pwshCmd = Get-Command pwsh -ErrorAction SilentlyContinue
if ($pwshCmd) { $pwsh = $pwshCmd.Source } else { $pwsh = (Get-Command powershell).Source }
$emu = Get-Content -Raw $RommConfig | ConvertFrom-Json | ForEach-Object { $_.Mappings } |
Where-Object { $_.PlatformId -eq $PlatformSpec } | Select-Object -First 1
Add-Type -Path $LiteDbDll
if (-not $emu -or -not $emu.EmulatorId) {
Write-Host @'
$installDir = Split-Path -Parent $Launcher
$arguments = "-ExecutionPolicy Bypass -File `"$Launcher`" `"{ImagePath}`""
Add the Playnite emulator first (Library -> Configure Emulators -> Add):
function New-BsonArray([object[]]$items) {
$list = New-Object 'System.Collections.Generic.List[LiteDB.BsonValue]'
foreach ($item in $items) {
if ($item -is [LiteDB.BsonDocument]) { $list.Add($item) }
elseif ($item -is [LiteDB.BsonValue]) { $list.Add($item) }
else { $list.Add([LiteDB.BsonValue]$item) }
}
return [LiteDB.BsonArray]::new($list)
}
Name: Pokemon PC Hack Launcher
Install: C:\Users\MattC\Documents\pokemon\scripts
Profile: Default
Platforms: PC (Windows)
Extensions: zip
Executable: C:\Program Files\PowerShell\7\pwsh.exe
Arguments: -ExecutionPolicy Bypass -File "C:\Users\MattC\Documents\pokemon\scripts\launch-pc-hack.ps1" "{ImagePath}"
Working dir: {InstallDir}
$profileDoc = [LiteDB.BsonDocument]::new()
$profileDoc['_id'] = [LiteDB.BsonValue][guid]::Parse($ProfileId)
$profileDoc['Name'] = $ProfileName
$profileDoc['Platforms'] = New-BsonArray @($PlatformSpec)
$profileDoc['ImageExtensions'] = New-BsonArray @('zip')
$profileDoc['Executable'] = $pwsh
$profileDoc['Arguments'] = $arguments
$profileDoc['WorkingDirectory'] = '{InstallDir}'
$profileDoc['UseShellExecute'] = $false
$profileDoc['OverrideDefaultArgs'] = $false
$profileDoc['ScriptStartup'] = $false
$profileDoc['ScriptGameImport'] = $false
$profileDoc['TrackingMode'] = 0
$profileDoc['TrackingPath'] = $null
Then in RomM plugin settings, map pc_windows -> that emulator, destination
'@ + $DestPath + @', Auto-extract ON.
$db = [LiteDB.LiteDatabase]::new("Filename=$EmuDb")
try {
$col = $db.GetCollection('Emulator')
$existing = $col.FindOne([LiteDB.Query]::EQ('Name', $EmuName))
if ($existing) {
$emuDoc = $existing
$emuId = $existing['_id'].AsGuid.ToString()
if ($emuId -eq '00000000-0000-0000-0000-000000000000') {
$emuId = $existing['_id'].AsString
}
$profiles = $emuDoc['CustomProfiles'].AsArray
if ($profiles.Count -gt 0) {
$p0 = $profiles[0]
if ($p0['_id'].IsGuid) { $ProfileId = $p0['_id'].AsGuid.ToString() }
else { $ProfileId = $p0['_id'].AsString }
$profileDoc['_id'] = [LiteDB.BsonValue][guid]::Parse($ProfileId)
}
Write-Host "Updating existing emulator '$EmuName' ($emuId)"
} else {
$emuId = [guid]::NewGuid().ToString()
$emuDoc = [LiteDB.BsonDocument]::new()
$emuDoc['_id'] = [LiteDB.BsonValue][guid]::Parse($emuId)
$profileDoc['_id'] = [LiteDB.BsonValue][guid]::Parse($ProfileId)
Write-Host "Creating emulator '$EmuName' ($emuId)"
}
Or re-run this script after the emulator exists (it reads EmulatorId from an existing pc_windows mapping).
$emuDoc['Name'] = $EmuName
$emuDoc['InstallDir'] = $installDir
$emuDoc['BuiltInConfigId'] = $null
$profilesList = New-Object 'System.Collections.Generic.List[LiteDB.BsonValue]'
$profilesList.Add($profileDoc)
$emuDoc['CustomProfiles'] = [LiteDB.BsonArray]::new($profilesList)
$col.Upsert($emuDoc) | Out-Null
} finally {
$db.Dispose()
'@
exit 1
}
$cfg = Get-Content -Raw $RommConfig | ConvertFrom-Json
$mapping = $cfg.Mappings | Where-Object { $_.PlatformId -eq $PlatformSpec } | Select-Object -First 1
if ($mapping) {
$mapping.EmulatorId = $emuId
$mapping.EmulatorProfileId = $ProfileId
$mapping.DestinationPath = $DestPath
$mapping.AutoExtract = $true
$mapping.Enabled = $true
$mapping.SyncSaves = $false
Write-Host 'Updated RomM mapping for pc_windows.'
} else {
$cfg.Mappings += [pscustomobject]@{
MappingId = [guid]::NewGuid()
Enabled = $true
AutoExtract = $true
UseM3u = $false
SyncSaves = $false
SaveStrategy = 0
SaveDirOverride = $null
EmulatorId = $emuId
EmulatorProfileId = $ProfileId
PlatformId = $PlatformSpec
DestinationPath = $DestPath
$m = $cfg.Mappings | Where-Object { $_.PlatformId -eq $PlatformSpec } | Select-Object -First 1
if (-not $m) {
Write-Host 'No pc_windows mapping yet — add it in RomM plugin settings after creating the emulator.'
exit 1
}
Write-Host 'Added RomM mapping for pc_windows.'
}
$m.DestinationPath = $DestPath
$m.AutoExtract = $true
$m.Enabled = $true
$m.SyncSaves = $false
New-Item -ItemType Directory -Force -Path $DestPath | Out-Null
$cfg | ConvertTo-Json -Depth 8 -Compress | Set-Content -Encoding UTF8 $RommConfig
Write-Host "Playnite configured. ROM download dir: $DestPath"
Write-Host "Open Playnite -> Menu -> Library -> Import RomM library (windows games)."
Write-Host "RomM pc_windows mapping updated (destination $DestPath, AutoExtract on)."
+365 -329
View File
File diff suppressed because it is too large Load Diff