Rilevamento della libreria giochi
Il rilevamento della libreria giochi gira interamente dentro l'app Electron playroll-ui (processo main). Enumera i giochi installati localmente dai launcher PC supportati, senza richiedere alcun login agli store, poi sincronizza il risultato su Supabase in modo che possa alimentare la home view del launcher, i controlli di eleggibilità e il LaunchPad.
Il codice sorgente vive in playroll-ui sotto src/main/library/. Il recorder playroll-cpp non partecipa al rilevamento — consuma solo il game_id risultante una volta avviata una sessione.
Obiettivi
- Rilevamento solo locale. Nessuna credenziale di store, nessuna chiamata API remota. Ogni detector legge ciò che il launcher memorizza già su disco (manifesti, registro, SQLite, VDF).
- Best-effort, mai bloccante. Un detector che fallisce restituisce una lista vuota, non solleva mai eccezioni nell'orchestrator. Il launcher deve comunque funzionare se Steam è rotto o se GOG Galaxy manca.
- Identità deterministica. L'
iddi un gioco è un UUID v5 derivato daprovider:providerGameId, così la stessa installazione si hasha sempre allo stesso id tra macchine e reinstallazioni diverse.
Pipeline
Il flusso è:
LibraryManager(src/main/library/library-manager.ts) istanzia ogni detector allo startup e avvia uno scan iniziale completo.LibraryScanneresegue i detector in parallelo: primaisAvailable()(controllo economico di presenza launcher), poidetectInstalled()solo per quelli disponibili. Ogni risultato è chiavato perprovider:providerGameId, così rilevamenti duplicati tra detector sono impossibili.- Lo scanner calcola un diff
added / removed / changedrispetto allo scan precedente e spinge il risultato al renderer. LibraryWatcherosserva i file Steamappmanifest_*.acf(uno per ogni cartella di libreria Steam) e i manifesti Epic*.itemper eventi di install/uninstall granulari; entrambi sono debounced (~2s) e fanno scattare un rescan single-store. Gli altri store si affidano al rescan completo periodico di 30 minuti (LIBRARY_SCAN_INTERVAL_MS).LibrarySyncfa upsert della lista corrente su Supabase tramite l'RPCsync_user_game_library(con throttle). La riga Supabase è ciò che la UI legge in seguito per il matching del catalogo, l'eleggibilità e il LaunchPad.
Un modulo separato — running-games.ts — enumera i processi del sistema operativo e li confronta con gli eseguibili dei giochi rilevati per segnalare quali titoli sono attualmente in esecuzione. Questo è indipendente dal rilevamento ma usa gli stessi record DetectedGame.
Contratto del detector
Ogni detector implementa StoreDetector da src/main/library/detector.ts:
interface StoreDetector {
readonly name: StoreProvider;
isAvailable(): Promise<boolean>;
detectInstalled(): Promise<DetectedGame[]>;
}
isAvailable() è intenzionalmente economico (hit sul registro, existsSync su file, ecc.) così l'orchestrator può saltare i launcher morti senza pagare il costo di un'enumerazione completa.
Un DetectedGame trasporta l'id canonico, il provider, il path di installazione, gli eseguibili, lo stato di installazione, la dimensione su disco, il comando di lancio e metadata opzionali (icona, tempo di gioco, generi, sviluppatori, publisher, data di rilascio, piattaforma). I campi mancanti sono normali — la maggior parte degli store non-Steam espone molti meno metadata localmente.
Store supportati
| Store | provider | Sorgente di rilevamento | Watch install/uninstall |
|---|---|---|---|
| Steam | steam | libraryfolders.vdf + appmanifest_*.acf per ogni cartella di libreria, più i path di registro dei mod GoldSrc e Source | Sì (chokidar su steamapps/appmanifest_*.acf di ogni libreria) |
| Epic Games | epic | Manifesti in %PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests\*.item + LauncherInstalled.dat. Le sub-app/DLC vengono saltate quando MainGameAppName è presente e diverso da AppName; i titoli standalone hanno "MainGameAppName": "" e vengono tenuti (EC-523). Il software non-gioco distribuito da Epic (Discord, Voicemod, …) viene saltato quando AppCategories non contiene games | Sì (chokidar su *.item) |
| GOG Galaxy | gog | Registro di disinstallazione Windows (*_is1, sia view x86 che x64) + goggame-{id}.info per gioco | No — solo rescan periodico |
| Xbox / Microsoft Store | xbox | Enumerazione PowerShell di pacchetti UWP firmati dallo Store + parse di AppxManifest.xml, con cache token XboxAccountClient per metadata più ricchi | No — solo rescan periodico |
| EA (EA Desktop) | ea | %PROGRAMDATA%\EA Desktop\machine.ini + dir dati di installazione EA Desktop + registro, manifesti parsati con fast-xml-parser | No — solo rescan periodico |
| Ubisoft Connect | ubisoft | HKLM\SOFTWARE\ubisoft\Launcher\Installs (dir di installazione per gameId, sia view x86 che x64) | No — solo rescan periodico |
| Battle.net | battlenet | C:\ProgramData\Battle.net\Agent\product.db + registro disinstallazione Battle.net, con un catalogo prodotti integrato per i titoli default e classici | No — solo rescan periodico |
| Amazon Games | amazon | %LOCALAPPDATA%\Amazon Games\Data\Games\Sql\GameInstallInfo.sqlite | No — solo rescan periodico |
| itch.io | itchio | Butler DB in %APPDATA%\itch\db\butler.db (filtrato a classification in tool) | No — solo rescan periodico |
| Riot Client | riot | Chiavi di registro del Riot client (HKCR\riotclient, HKLM\SOFTWARE\Classes\riotclient) + tabella prodotti integrata (League, Valorant, TFT, Wild Rift, 2XKO) | No — solo rescan periodico |
| Rockstar Games Launcher | rockstar | Registro Uninstall di Windows (voci Launcher.exe … -uninstall=<titleId>) + chiavi per-gioco HKLM\SOFTWARE\(WOW6432Node\)Rockstar Games\<game>. La chiave per-gioco copre installazioni interrotte e copie gestite da altri store: i valori vengono letti nell'ordine InstallFolder (gestita da Rockstar), InstallFolderEpic (acquistata su Epic — l'installazione è gestita dal launcher Rockstar, quindi non compare nei manifest di Epic), poi qualsiasi variante InstallFolder* sconosciuta. InstallFolderSteam è lasciata deliberatamente al detector Steam (la copia ha un appmanifest Steam; una seconda riga duplicherebbe il gioco) ma viene conteggiata nella telemetria come steam_managed_only. | No — solo rescan periodico |
L'union StoreProvider include anche microsoft, other e manual per voci legacy / aggiunte dall'utente che non provengono da un detector live.
Identità, nomi e deduplicazione
generateGameId(provider, providerGameId)produce un UUID v5 deterministico sotto un namespace Playroll fisso, così due macchine diverse con Hades installato via Steam finiscono con lo stessogame_id.normalizeGameNamerimuove™ ® ©, collassa gli spazi bianchi e fa il trim, così il matching lato display non deve gestire la punteggiatura specifica del launcher.- Lo scanner chiavizza i giochi per
provider:providerGameId, non per nome — un titolo installato sia su Steam che su Epic compare come due voci di libreria separate (corretto, dato che ogni installazione ha il proprio eseguibile e comando di lancio).
Modello di fallimento
- Un detector che solleva eccezioni durante
detectInstalled()viene catturato dallo scanner e trattato come "ha restituito[]" — il resto dello scan si completa comunque. - Un detector che solleva eccezioni durante
isAvailable()viene trattato come non disponibile. - I fallimenti di sync risalgono al listener del manager come
onError("sync", ...)ma non interrompono lo scanning. - Lo scanner rifiuta di eseguire un secondo scan mentre uno è già in volo; i trigger concorrenti (timer + watcher) collassano in un singolo passaggio.