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:
co-authored by
Claude Opus 4.8
parent
ae56d101b7
commit
809600a04d
@@ -22,3 +22,6 @@ Thumbs.db
|
|||||||
.scrape/
|
.scrape/
|
||||||
.scrape_lib/
|
.scrape_lib/
|
||||||
scripts/__pycache__/
|
scripts/__pycache__/
|
||||||
|
|
||||||
|
# Per-machine ROM compare/patch scratch (side-by-side emulator runs, patch builds).
|
||||||
|
.compare/
|
||||||
|
|||||||
@@ -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)."
|
||||||
@@ -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.'
|
||||||
@@ -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"
|
||||||
@@ -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)."
|
||||||
@@ -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))
|
||||||
@@ -1,127 +1,53 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.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'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
$PlayniteDir = Join-Path $env:LOCALAPPDATA 'Playnite'
|
$RommConfig = Join-Path $env:LOCALAPPDATA 'Playnite\ExtensionsData\9700aa21-447d-41b4-a989-acd38f407d9f\config.json'
|
||||||
$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'
|
|
||||||
$DestPath = 'C:\Users\MattC\roms\windows'
|
$DestPath = 'C:\Users\MattC\roms\windows'
|
||||||
$EmuName = 'Pokemon PC Hack Launcher'
|
|
||||||
$PlatformSpec = 'pc_windows'
|
$PlatformSpec = 'pc_windows'
|
||||||
$ProfileName = 'Default'
|
|
||||||
$ProfileId = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
if (Get-Process -Name 'Playnite*' -ErrorAction SilentlyContinue) {
|
if (-not (Test-Path $RommConfig)) { throw "RomM config not found: $RommConfig" }
|
||||||
throw 'Close Playnite completely, then re-run this script.'
|
|
||||||
}
|
|
||||||
foreach ($p in @($LiteDbDll, $Launcher, $RommConfig)) {
|
|
||||||
if (-not (Test-Path $p)) { throw "Missing: $p" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$pwshCmd = Get-Command pwsh -ErrorAction SilentlyContinue
|
$emu = Get-Content -Raw $RommConfig | ConvertFrom-Json | ForEach-Object { $_.Mappings } |
|
||||||
if ($pwshCmd) { $pwsh = $pwshCmd.Source } else { $pwsh = (Get-Command powershell).Source }
|
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
|
Add the Playnite emulator first (Library -> Configure Emulators -> Add):
|
||||||
$arguments = "-ExecutionPolicy Bypass -File `"$Launcher`" `"{ImagePath}`""
|
|
||||||
|
|
||||||
function New-BsonArray([object[]]$items) {
|
Name: Pokemon PC Hack Launcher
|
||||||
$list = New-Object 'System.Collections.Generic.List[LiteDB.BsonValue]'
|
Install: C:\Users\MattC\Documents\pokemon\scripts
|
||||||
foreach ($item in $items) {
|
Profile: Default
|
||||||
if ($item -is [LiteDB.BsonDocument]) { $list.Add($item) }
|
Platforms: PC (Windows)
|
||||||
elseif ($item -is [LiteDB.BsonValue]) { $list.Add($item) }
|
Extensions: zip
|
||||||
else { $list.Add([LiteDB.BsonValue]$item) }
|
Executable: C:\Program Files\PowerShell\7\pwsh.exe
|
||||||
}
|
Arguments: -ExecutionPolicy Bypass -File "C:\Users\MattC\Documents\pokemon\scripts\launch-pc-hack.ps1" "{ImagePath}"
|
||||||
return [LiteDB.BsonArray]::new($list)
|
Working dir: {InstallDir}
|
||||||
}
|
|
||||||
|
|
||||||
$profileDoc = [LiteDB.BsonDocument]::new()
|
Then in RomM plugin settings, map pc_windows -> that emulator, destination
|
||||||
$profileDoc['_id'] = [LiteDB.BsonValue][guid]::Parse($ProfileId)
|
'@ + $DestPath + @', Auto-extract ON.
|
||||||
$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
|
|
||||||
|
|
||||||
$db = [LiteDB.LiteDatabase]::new("Filename=$EmuDb")
|
Or re-run this script after the emulator exists (it reads EmulatorId from an existing pc_windows mapping).
|
||||||
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)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$emuDoc['Name'] = $EmuName
|
'@
|
||||||
$emuDoc['InstallDir'] = $installDir
|
exit 1
|
||||||
$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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$cfg = Get-Content -Raw $RommConfig | ConvertFrom-Json
|
$cfg = Get-Content -Raw $RommConfig | ConvertFrom-Json
|
||||||
$mapping = $cfg.Mappings | Where-Object { $_.PlatformId -eq $PlatformSpec } | Select-Object -First 1
|
$m = $cfg.Mappings | Where-Object { $_.PlatformId -eq $PlatformSpec } | Select-Object -First 1
|
||||||
if ($mapping) {
|
if (-not $m) {
|
||||||
$mapping.EmulatorId = $emuId
|
Write-Host 'No pc_windows mapping yet — add it in RomM plugin settings after creating the emulator.'
|
||||||
$mapping.EmulatorProfileId = $ProfileId
|
exit 1
|
||||||
$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
|
|
||||||
}
|
}
|
||||||
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
|
New-Item -ItemType Directory -Force -Path $DestPath | Out-Null
|
||||||
$cfg | ConvertTo-Json -Depth 8 -Compress | Set-Content -Encoding UTF8 $RommConfig
|
$cfg | ConvertTo-Json -Depth 8 -Compress | Set-Content -Encoding UTF8 $RommConfig
|
||||||
Write-Host "Playnite configured. ROM download dir: $DestPath"
|
Write-Host "RomM pc_windows mapping updated (destination $DestPath, AutoExtract on)."
|
||||||
Write-Host "Open Playnite -> Menu -> Library -> Import RomM library (windows games)."
|
|
||||||
|
|||||||
+365
-329
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user