Move game store discovery into main-process scanners#22011
Move game store discovery into main-process scanners#22011
Conversation
|
Do we have any data on how many extensions (if any) are affected? Maybe @erri120 extension's analysis? |
|
Would be good to find out. All the ones in our codebase are very old, and the small changes I included fixed those. Any extension that uses "query args" is fine and I think most do. |
|
I considered updating the extensions that used the |
Based on all extensions (data snapshot from last month):
Extensions are using Queries used for verifying API changes: -- checks for usage of 'registerGameStore'
SELECT
extensionId,
json_extract_string(calls.value, '$.method') AS method
FROM extensions,
json_each(value, '$.analysis') AS file,
json_each(json_extract(file.value, '$.apiUsage.calls')) AS calls
WHERE method = 'registerGameStore'-- checks for explicit steam usage
SELECT
extensionId,
file.key AS file,
json_extract_string(component.value, '$.component') AS component
FROM
extensions,
json_each (extensions.value, '$.analysis') AS file,
json_each (json_extract(file.value, '$.vortexApiUsage.components')) AS component
WHERE
extensionId NOT LIKE 'github-Nexus-Mods-vortex-games%' AND
json_extract_string(component.value, '$.component') ILIKE '%steam%'
ORDER BY
component ASC,
extensionId ASC,
file ASC; |
src/shared/src/types/preload.ts
Outdated
| /** Store game entry as returned from main-process discovery */ | ||
| export interface IStoreGameRow { | ||
| store_type: string; | ||
| store_id: string; | ||
| install_path: string; | ||
| name: string | null; | ||
| store_metadata: string | null; | ||
| } |
There was a problem hiding this comment.
Is this misplaced? Doesn't seem to be used as a preload type.
There was a problem hiding this comment.
yes, I can remove it now, we used to send game store results across the IPC, but now it's more "find" based instead of querying for all games in one call
| ? Bluebird.all([ | ||
| Bluebird.resolve(matchStoreGames(game.queryArgs, storeGames)), | ||
| game.queryArgs.registry !== undefined | ||
| ? GameStoreHelper.find({ registry: game.queryArgs.registry }) | ||
| : Bluebird.resolve([]), |
There was a problem hiding this comment.
matchStoreGames returns IGameStoreEntry[] the method isn't async so we don't need Bluebird here.
src/renderer/src/util/queryClient.ts
Outdated
| force?: boolean; | ||
| } | ||
|
|
||
| function toQueryError(err: unknown): Error { |
There was a problem hiding this comment.
Should have custom QueryError type at this point.
| const entry = { | ||
| appid: row.store_id, | ||
| gamePath: row.install_path, | ||
| name: row.name ?? "", | ||
| gameStoreId: row.store_type, | ||
| priority: this.storePriority(row.store_type), | ||
| } as IGameStoreEntry & Record<string, unknown>; |
There was a problem hiding this comment.
All keys are known since they come from IStoreGameMetadata.
| const entry = { | |
| appid: row.store_id, | |
| gamePath: row.install_path, | |
| name: row.name ?? "", | |
| gameStoreId: row.store_type, | |
| priority: this.storePriority(row.store_type), | |
| } as IGameStoreEntry & Record<string, unknown>; | |
| const entry: IGameStoreEntry & Partial<Record<keyof IStoreGameMetadata, unknown>> = { | |
| appid: row.store_id, | |
| gamePath: row.install_path, | |
| name: row.name ?? "", | |
| gameStoreId: row.store_type, | |
| priority: this.storePriority(row.store_type), | |
| }; |
| return toBlue(async (): Promise<void> => { | ||
| if (!appInfo) { | ||
| throw new ProcessCanceled("appInfo is undefined/null"); | ||
| } |
| private launchGOGGame(api: IExtensionApi, appInfo: any): Bluebird<void> { | ||
| return toBlue(async (): Promise<void> => { |
There was a problem hiding this comment.
Lots of bluebird references that can be removed. Public facing API should return PromiseLike<T> for compatibility and implementations can use native Promise<T>.
| // eslint-disable-next-line no-restricted-imports | ||
| require("node:fs").statSync(libraryFoldersPath); |
There was a problem hiding this comment.
No reason to use require here, can just do import { statSync } from"node:fs"; at the top if you need it...
erri120
left a comment
There was a problem hiding this comment.
Important points:
- API changes break extensions, instead use stubs that redirect and mark them as deprecated.
- No new CLI. Our build process is still a mess and I really don't want to complicate it even more with a second entry point. This PR is already large enough, we don't need a CLI in here.
Code quality:
- no new Jest tests, new tests should use Vitest,
- no Bluebird resolve code, use
PromiseLike<T>for API and nativePromise<T>for implementations
Phase 0 of Game Adaptors architecture: all store scanners (Steam, GOG, Epic, Xbox, Origin, Uplay, Registry) now run in the main process and write results to a DuckDB store_games table. A query watcher pushes updates to the renderer via IPC. Infrastructure complete: - DuckDB store_games table + all_store_games query - IStoreScanner interface + DiscoveryCoordinator - 7 scanner implementations migrated from renderer - IPC channels (discovery:start, discovery:get-store-games, etc.) - Preload API exposure (window.api.discovery.*) - Renderer quickDiscovery() refactored to use main-process data - Query type generation updated for non-pivot tables - SQL files copied to out/ during build Known issues to fix: - Scanner output contains undefined values causing DuckDB write failures - Debounce prevents IPC-triggered rescan after initial scan failure - Legacy fallback (GameStoreHelper) has timing issues when stores aren't loaded yet Design spec: docs/superpowers/specs/2026-03-26-game-adaptors-design.md Linear: Game Discovery project (APP-163 through APP-171, APP-178-179)
…ed compat
- Add CLI bundle step to src/main/build.mjs that compiles list-games.ts into
out/cli-list-games.cjs with logging alias swap
- Add cli:list-games pnpm script to src/main/package.json
- Fix DiscoveryCoordinator to use a safeAllSettled helper that builds plain
{status, value} objects, working around turbowalk's global Promise=bluebird
patch which made Promise.allSettled return bluebird inspection objects
- Add ON CONFLICT DO UPDATE to store_games INSERT to handle duplicate store_id
entries (e.g. Steam games appearing in multiple library paths with different case)
Replace per-query IPC channels with a generic system. The main process broadcasts dirty query names via query:dirty after invalidation, and the renderer can execute any named query via query:execute. Removes discovery-specific query IPC (discovery:get-store-games, discovery:store-games-updated) in favor of the generic channels.
…nd stubs Extensions using the legacy game store API will continue to work via shims that delegate to util.GameStoreHelper with the appropriate storeId. Annotated @deprecated to signal migration path.
- Remove misplaced IStoreGameRow interface from preload.ts (already
defined in discovery.ts where it belongs)
- Add QueryError class to queryClient.ts so callers can distinguish
query failures from other errors via instanceof
- Replace require("node:fs").statSync with top-level import in
SteamScanner.ts (removes eslint-disable comment)
…ive Promise - Change all public method signatures from Bluebird<T> to PromiseLike<T> so extensions consuming the API get a stable interface compatible with any Promise implementation - Convert private methods (runtimeLaunchStore, launchURI, launchGOGGame, launchXboxGame) from toBlue() wrappers to native async functions - Fix rowToEntry type cast: use Partial<Record<keyof IStoreGameMetadata, unknown>> instead of Record<string, unknown> for better type safety - Remove Bluebird and toBlue imports (no longer needed)
Move GameModeManager and discoveryQueries tests from Jest __tests__/ directories to colocated Vitest test files. Replace jest.fn()/jest.mock() with vi.fn()/vi.mock() and add explicit vitest imports.
fcd23d6 to
d2ce0b8
Compare
matchStoreGames is synchronous so no Bluebird.resolve() wrapper needed. Replace Bluebird.all with Promise.all and convert to async/await.
|
This PR has conflicts. You need to rebase the PR before it can be merged. |
|
I absolutely agree with that we need to have stubs, but I would expand it to have stubs for any API that was exposed by us and got removed. I believe just redirecting everything should not be an issue. Just deprecate the stubs and show the proper way to do it in the future |
Summary
This PR replaces the old renderer-side game store extension model with a main-process discovery system backed by DuckDB query data.
The new flow is:
store_gamesDuckDB tableall_store_gamesto the rendererChanges
1. Main-process discovery and query plumbing
DiscoveryCoordinator,IStoreScanner, and store-specific scanners undersrc/main/src/games/scannersdiscovery.startquery:executeandquery:dirty) and wires discovery init into main persistence startupstore_gamessetup/select SQL and generated query typesstore_gamesafter each successful run2. CLI support
cli:list-gamesentry point and bundle in@vortex/mainall_store_games, and prints store, id, name, and install path3. Renderer migration to query-backed stores
GameStoreHelperto load store data fromall_store_gamesrather than calling legacy store implementationsStarterInfoand game mode discovery to use the new query-backed helper methodsstartQuickDiscovery()now triggers a main-process scan and then consumes the query results4. Standardized game finders and store-specific cleanup
queryArgs/GameStoreHelper.findBy*()lookups instead of store-specific utility modules and direct launcher objectsBreaking Changes
This branch changes extension-facing behavior in ways that can break extensions which rely on the old store registration model.
Notable breakages:
registerGameStoreis removed fromIExtensionContextextensions/gamestore-*packages are removed for GOG, Origin, Uplay, and XboxGameStoreHelperandqueryArgsflowIn practice, any out-of-tree/community extension that still expects to register a custom game store or talk to the removed bundled store extensions will need an update.