Skip to content

feat: Adds ability to import Steam games via custom paths#1066

Open
joshuatam wants to merge 1 commit intoutkarshdalal:masterfrom
joshuatam:feat/import-to-steam
Open

feat: Adds ability to import Steam games via custom paths#1066
joshuatam wants to merge 1 commit intoutkarshdalal:masterfrom
joshuatam:feat/import-to-steam

Conversation

@joshuatam
Copy link
Copy Markdown
Contributor

@joshuatam joshuatam commented Apr 1, 2026

Description

Increments the database version to 19, introducing a custom_install_path field to the app_info table for tracking user-defined installation locations.

Enhances the custom game scanner to identify and import existing Steam games by matching installation directories. Imported Steam games will now leverage existing metadata and be managed as native Steam entries.

Updates SteamService to use the custom_install_path for installed imported games and modifies the uninstall process for imported games to only remove database entries, preserving the game files.

Adjusts WineUtils to recognize custom game folders as valid game directories, ensuring correct drive mounting for imported titles.

Recording

  • Check Discord video

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Import Steam games from custom install folders so they appear as native Steam entries with full metadata. Uninstall keeps the original files and cleans app data.

  • New Features

    • DB v20: add custom_install_path to app_info; AppInfo.isImported derives from it.
    • Scanner: if a custom folder name matches a Steam app installDir, import it as a Steam entry, mark main depots downloaded, add DOWNLOAD_COMPLETE, save the path.
    • DAO/Service: add SteamAppDao.findSteamAppWithInstallDir; expose it in SteamService; resolve imported game dirs via custom_install_path.
    • Uninstall (imported): remove DB only, drop from custom folders, remove marker, invalidate scanner cache; keep files; UI navigates back after success.
    • WineUtils: accept custom manual folders as valid A: game dirs for proper mounting.
  • Migration

    • Auto-migrates 19 → 20.
    • To import, add the install folder to custom game folders; the folder name must match the Steam installDir.

Written for commit e3677fb. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added support for tracking games installed at custom paths outside default locations.
    • Enabled Steam auto-detection when scanning custom-installed games.
    • Improved game directory scanning to better recognize manually installed games.
  • Bug Fixes

    • Enhanced uninstall experience for custom-installed games with improved navigation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR extends support for imported games with custom install paths by adding a persisted customInstallPath field to AppInfo, implementing Steam app detection by directory name, updating deletion and navigation flows for imported games, and refining drive mapping logic to recognize custom game folders.

Changes

Cohort / File(s) Summary
AppInfo Room Entity
app/src/main/java/app/gamenative/data/AppInfo.kt
Added customInstallPath: String column (default "") and computed isImported property based on whether customInstallPath is non-empty.
Database Schema & Migration
app/src/main/java/app/gamenative/db/PluviaDatabase.kt, app/schemas/app.gamenative.db.PluviaDatabase/20.json
Updated database version from 19 to 20 with auto-migration and added new schema JSON capturing all entity tables and column definitions.
DAO Query Addition
app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
Added findSteamAppWithInstallDir(dirName: String) query to locate Steam apps by installation directory pattern matching.
Service Logic Updates
app/src/main/java/app/gamenative/service/SteamService.kt
Updated deletion logic to handle imported apps separately (removing from custom folders, invalidating cache, marker cleanup); modified getAppDirPath() to short-circuit for imported apps; added companion helper to query Steam apps by directory; refined DB updates to use .copy() instead of full reconstruction.
UI Navigation Enhancement
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Extended uninstall confirmation flow to fetch installed app info and conditionally navigate back when uninstalling imported games.
Custom Game Detection
app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
Added Steam auto-detection in folder scanning: attempts to match folder by directory name via SteamService, auto-inserts AppInfo with custom install path and download markers if match found and not yet in database, returns Steam-sourced LibraryItem, bypassing custom game ID generation.
Wine Drive Mapping
app/src/main/java/com/winlator/core/WineUtils.java
Extended A: drive detection to recognize custom game manual folders in addition to Steam directories via PrefManager lookup.

Sequence Diagram

sequenceDiagram
    actor User
    participant Scanner as CustomGameScanner
    participant Service as SteamService
    participant DAO as SteamAppDao
    participant DB as Room Database
    participant Marker as MarkerUtils

    User->>Scanner: Scan custom game folder
    Scanner->>Service: findSteamAppWithInstallDir(folderName)
    Service->>DAO: findSteamAppWithInstallDir(folderName)
    DAO->>DB: Query steam_app by config LIKE installDir
    DB-->>DAO: SteamApp matches
    DAO-->>Service: Return List<SteamApp>
    Service->>Service: Check if exactly one match
    Service-->>Scanner: SteamApp (if single match)
    
    Scanner->>Service: getInstalledApp(steamAppId)
    Service->>DB: Query app_info
    DB-->>Service: null (not yet in database)
    Service-->>Scanner: null
    
    Scanner->>DB: Insert AppInfo with customInstallPath
    Scanner->>Service: getMainAppDepots(steamAppId)
    Service->>DB: Query app depots
    DB-->>Service: Depot list
    Service-->>Scanner: Filtered depots
    Scanner->>Marker: addMarker(folderPath, DOWNLOAD_COMPLETE)
    Marker->>DB: Record download marker
    
    Scanner-->>User: Return LibraryItem (Steam source)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

🐰 A rabbit hops through folders bright,
Finding Steam games left and right,
Custom paths now marked and stored,
Import flows perfectly restored!
thump thump

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: adding the ability to import Steam games via custom paths, which aligns with the core changes across database schema, scanner, service, and utilities.
Description check ✅ Passed The PR description covers all required sections with clear explanations of changes, references a recording, and includes a completed checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt:1187">
P2: onBack() is invoked from a Dispatchers.IO coroutine without switching to the main thread, which can break UI navigation/state updates.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt`:
- Around line 156-157: The current findSteamAppWithInstallDir method should be
narrowed to only owned apps and match installDir exactly: change the query in
findSteamAppWithInstallDir to select from the DAO's owned-app subset (join the
same owned-app table/view used by other methods in this DAO) instead of raw
steam_app rows, and replace the LIKE pattern with a JSON extraction equality
check such as json_extract(config, '$.installDir') = :dirName so underscores and
other characters are matched literally.

In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 1148-1149: In deleteApp(), avoid force-unwrapping
getInstalledApp(appId)!!—treat appInfo as nullable, call val appInfo =
getInstalledApp(appId) and branch on null: if appInfo == null perform the
cleanup actions that apply to interrupted installs (e.g., remove
downloadingAppInfo via downloadingAppInfoDao and any filesystem/metadata
cleanup) and set the result path for a missing-installed-row case; otherwise
proceed with the existing isImported logic for installed apps. Update any code
using appInfo.isImported to first null-check (or use safe calls) so deleteApp()
never NPEs when the installed AppInfo row is absent.
- Around line 1153-1156: PrefManager.customGameManualFolders is being mutated
and written back asynchronously which can lose concurrent changes and allows
CustomGameScanner.invalidateCache() to run before the removal is durably stored;
change this to an atomic update that completes before invalidating. Add or use
an atomic update API on PrefManager (e.g.
PrefManager.updateCustomGameManualFolders { set -> ... } or a
synchronized/transactional helper) that reads the set, removes folderPath,
persists the change synchronously or returns after durable write, then call
CustomGameScanner.invalidateCache() only after that update completes; reference
PrefManager.customGameManualFolders, the new update helper (e.g.
updateCustomGameManualFolders), folderPath, and
CustomGameScanner.invalidateCache() when making the change.
- Around line 909-913: The imported-install branch returns customInstallPath
from getInstalledApp, but completeAppDownload rebuilds AppInfo with only
download-related fields and branch, which clears customInstallPath and
isImported; update completeAppDownload to preserve those fields by merging the
existing AppInfo into the new one: fetch the current AppInfo via
getInstalledApp(gameId) (or accept an optional existing AppInfo) and copy over
customInstallPath and isImported (and any other non-download metadata) into the
AppInfo that completeAppDownload writes, so imported installs keep their custom
path and isImported flag after verify/update.
- Around line 596-598: The helper findSteamAppWithInstallDir currently calls
runBlocking { instance?.appDao?.findSteamAppWithInstallDir(dirName) } and
returns null if SteamService.instance isn't initialized; update it to avoid
depending on the SteamService singleton by routing the DAO call through a
lifecycle-independent access path (e.g., a repository or directly via the appDao
provider used outside the service), so CustomGameScanner and
PrefManager–initialized code can always query for a SteamApp regardless of
SteamService.instance; locate and replace uses of instance?.appDao in
findSteamAppWithInstallDir (and any other similar helpers) to call the
repository/DAO directly (or accept an appDao/repository parameter) and ensure
the method returns the DAO result rather than null when the service singleton is
absent.

In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 1160-1163: The code force-unwraps
getInstalledApp(libraryItem.gameId) into installedAppInfo on the main thread but
only uses it inside the IO coroutine; move the call and null-check into the
CoroutineScope(Dispatchers.IO).launch block (i.e., call
getInstalledApp(libraryItem.gameId) inside that launch), check for null and
return early if missing, and then proceed to call SteamService.deleteApp(gameId)
and use installedAppInfo safely without !! to avoid crashes from a deleted DB
row.

In `@app/src/main/java/app/gamenative/utils/CustomGameScanner.kt`:
- Around line 516-553: The Steam-import flow must be executed only when there is
no existing install record or when the existing record already points at this
folder; change the gate around the entire block that creates AppInfo, adds the
download marker, and returns the Steam LibraryItem by first retrieving val
existing = SteamService.getInstalledApp(steamApp.id) and then only proceeding if
existing == null || existing.customInstallPath == folderPath; leave AppInfo
insertion (AppInfo), MarkerUtils.addMarker(folderPath,
Marker.DOWNLOAD_COMPLETE_MARKER), and the creation/return of LibraryItem
unchanged inside that guarded block so a manually added folder won’t be
relabeled unless the install record is absent or matches the same path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30b9be43-2391-49ae-b5fd-6710e55ef2a5

📥 Commits

Reviewing files that changed from the base of the PR and between 497d334 and e5671ec.

📒 Files selected for processing (8)
  • app/schemas/app.gamenative.db.PluviaDatabase/19.json
  • app/src/main/java/app/gamenative/data/AppInfo.kt
  • app/src/main/java/app/gamenative/db/PluviaDatabase.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
  • app/src/main/java/com/winlator/core/WineUtils.java

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (1)

1160-1163: ⚠️ Potential issue | 🔴 Critical

Avoid force-unwrapping and blocking installed-app lookup in the click handler.

Line 1160 force-unwraps a nullable DB lookup and can crash if the row is missing; it also does a blocking call before the IO coroutine starts. Move the lookup into the coroutine and null-check before proceeding.

Proposed fix
-                            val installedAppInfo = getInstalledApp(libraryItem.gameId)!!
-
                             CoroutineScope(Dispatchers.IO).launch {
+                                val installedAppInfo = getInstalledApp(libraryItem.gameId) ?: run {
+                                    withContext(Dispatchers.Main) {
+                                        SnackbarManager.show(context.getString(R.string.steam_uninstall_failed))
+                                    }
+                                    return@launch
+                                }
                                 val success = SteamService.deleteApp(gameId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`
around lines 1160 - 1163, The code force-unwraps
getInstalledApp(libraryItem.gameId) on the main thread and performs the DB
lookup before launching the IO coroutine, which can crash and block UI; move the
getInstalledApp call into the CoroutineScope(Dispatchers.IO).launch block, call
val installedAppInfo = getInstalledApp(libraryItem.gameId) without !!, check for
null (return/early exit) and only then call SteamService.deleteApp(gameId) (and
any further work) so the lookup is off the main thread and null-safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 1185-1190: The code currently calls onBack() whenever
installedAppInfo.isImported is true regardless of whether the uninstall
(deleteApp) succeeded; change the logic so onBack() is only invoked when both
installedAppInfo.isImported is true AND the deleteApp call returned success
(i.e., gate the withContext(Dispatchers.Main) { onBack() } behind the success
boolean returned by deleteApp), ensuring you reference the deleteApp result
(success) and installedAppInfo.isImported before calling onBack().

---

Duplicate comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 1160-1163: The code force-unwraps
getInstalledApp(libraryItem.gameId) on the main thread and performs the DB
lookup before launching the IO coroutine, which can crash and block UI; move the
getInstalledApp call into the CoroutineScope(Dispatchers.IO).launch block, call
val installedAppInfo = getInstalledApp(libraryItem.gameId) without !!, check for
null (return/early exit) and only then call SteamService.deleteApp(gameId) (and
any further work) so the lookup is off the main thread and null-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f786bb5-215c-408b-b7ee-a83d2b6d38b0

📥 Commits

Reviewing files that changed from the base of the PR and between e5671ec and 900815d.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/service/SteamService.kt">

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamService.kt:1148">
P1: deleteApp now proceeds when the installed record is null, and getAppDirPath falls back to the shared steamapps/common root when metadata is missing. This can trigger recursive deletion of the shared install directory if uninstall is invoked for a stale appId.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
app/src/main/java/app/gamenative/service/SteamService.kt (2)

1153-1156: ⚠️ Potential issue | 🟠 Major

Persist the manual-folder removal before invalidating the scanner cache.

PrefManager.customGameManualFolders = ... is fire-and-forget, so this can invalidate the cache against the old folder set and immediately re-import the just-deleted folder on the next rebuild. The read/modify/write also risks dropping concurrent edits. Please make the preference update atomic and wait for it to finish before calling CustomGameScanner.invalidateCache().

💡 One workable direction
-                val manualFolders = PrefManager.customGameManualFolders.toMutableSet()
-                manualFolders.remove(folderPath)
-                PrefManager.customGameManualFolders = manualFolders
-                CustomGameScanner.invalidateCache()
+                runBlocking {
+                    PrefManager.updateCustomGameManualFolders { folders ->
+                        folders - folderPath
+                    }
+                }
+                CustomGameScanner.invalidateCache()

Also make PrefManager.updateCustomGameManualFolders(...) suspend until the underlying dataStore.edit {} has completed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 1153 -
1156, Current code removes a folder from PrefManager.customGameManualFolders
then immediately calls CustomGameScanner.invalidateCache(), risking a race where
the cache is invalidated before the underlying DataStore write completes and
concurrent edits are lost; fix by adding and using an atomic suspend update
method (e.g., PrefManager.updateCustomGameManualFolders(updatedSet: Set<String>)
: suspend fun) that performs the read/modify/write inside dataStore.edit { } and
suspends until completion, then call CustomGameScanner.invalidateCache() only
after that suspend function returns so the preference change is durably
persisted before invalidating the scanner cache.

596-598: ⚠️ Potential issue | 🟠 Major

Keep install-dir lookup independent of the service singleton.

This helper still returns null whenever SteamService.instance is not initialized, so callers still fall back to the custom-game path until the service starts. Please route this through a DAO/database access path that exists before SteamService startup.

Based on learnings: In the GameNative project, custom/locally-added games (CUSTOM_GAME source) are filesystem-based and are NOT stored in the Room database. They are discovered by CustomGameScanner, which performs filesystem I/O and depends on PrefManager initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 596 -
598, The helper findSteamAppWithInstallDir currently returns null when
SteamService.instance is not initialized because it calls instance?.appDao;
change it to use the Room/DAO access path that exists before SteamService
startup (e.g., obtain the AppDatabase/AppDao singleton directly rather than via
SteamService.instance) and keep the runBlocking(Dispatchers.IO) call;
specifically replace usage of instance?.appDao with a direct call to the shared
AppDatabase/AppDao singleton (or a DB-backed repository accessor) so
findSteamAppWithInstallDir(dirName) queries the DB independently of SteamService
initialization.
app/src/main/java/app/gamenative/utils/CustomGameScanner.kt (2)

517-554: ⚠️ Potential issue | 🟠 Major

Gate the whole Steam-import return path on the existing install record.

Only the insert is conditional right now. If a different install record for steamApp.id already exists, this block still returns a STEAM_* item for the manually added folder, so the folder gets relabeled as that Steam app even though AppInfo still points somewhere else. Please only take the Steam-import branch when there is no installed row yet, or when the existing customInstallPath already equals folderPath.

💡 Minimal shape of the fix
-                if (SteamService.getInstalledApp(steamApp.id) == null) {
+                val existing = SteamService.getInstalledApp(steamApp.id)
+                if (existing == null || existing.customInstallPath == folderPath) {
+                    if (existing == null) {
                         val preferredLanguage = PrefManager.containerLanguage
                         val mainDepots = getMainAppDepots(steamApp.id, preferredLanguage)
                         val mainAppDepots = mainDepots.filter { (_, depot) ->
                             depot.dlcAppId == INVALID_APP_ID
                         }
                         val mainAppDepotIds = mainAppDepots.keys.sorted()
 
                         runBlocking {
                             SteamService.instance?.appInfoDao?.insert(
                                 AppInfo(
                                     steamApp.id,
                                     isDownloaded = true,
                                     downloadedDepots = mainAppDepotIds,
                                     dlcDepots = emptyList(),
                                     branch = "public",
                                     customInstallPath = folderPath
                                 ),
                             )
                         }
 
                         MarkerUtils.addMarker(folderPath, Marker.DOWNLOAD_COMPLETE_MARKER)
+                    }
+
+                    return LibraryItem(
+                        index = 0,
+                        appId = "${GameSource.STEAM.name}_${steamApp.id}",
+                        name = steamApp.name,
+                        iconHash = steamApp.clientIconHash,
+                        capsuleImageUrl = steamApp.getCapsuleUrl(),
+                        headerImageUrl = steamApp.getHeaderImageUrl().orEmpty().ifEmpty { steamApp.headerUrl },
+                        heroImageUrl = steamApp.getHeroUrl().ifEmpty { steamApp.headerUrl },
+                        isShared = false,
+                        gameSource = GameSource.STEAM,
+                    )
                 }
-
-                val idPart = steamApp.id
-                val appId = "${GameSource.STEAM.name}_$idPart"
-
-                return LibraryItem(
-                    index = 0,
-                    appId = appId,
-                    name = steamApp.name,
-                    iconHash = steamApp.clientIconHash,
-                    capsuleImageUrl = steamApp.getCapsuleUrl(),
-                    headerImageUrl = steamApp.getHeaderImageUrl().orEmpty().ifEmpty { steamApp.headerUrl },
-                    heroImageUrl = steamApp.getHeroUrl().ifEmpty { steamApp.headerUrl },
-                    isShared = false,
-                    gameSource = GameSource.STEAM,
-                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/CustomGameScanner.kt` around lines 517
- 554, Fetch the existing install record once (val installedApp =
SteamService.getInstalledApp(steamApp.id)) and use it to gate the entire
Steam-import branch: only perform the insert, add marker, and return the STEAM_*
LibraryItem when installedApp == null OR installedApp.customInstallPath ==
folderPath; otherwise skip the insert/marker and do not return the
Steam-specific LibraryItem (let the method fall through to the
non-Steam/manual-folder branch). Update references around the existing
SteamService.getInstalledApp usage and keep the runBlocking
SteamService.instance?.appInfoDao?.insert and MarkerUtils.addMarker calls inside
that guarded block.

512-514: ⚠️ Potential issue | 🟠 Major

Don’t make Steam-folder import depend on SteamService.instance.

When the service is stopped, this skips Steam detection entirely and falls through to the custom-game path even if the Steam app row is already in Room. That can misclassify a real Steam install as CUSTOM_GAME and even write custom metadata into the folder before the service starts. Please remove this gate once the install-dir lookup works without the singleton.

Based on learnings: In the GameNative project, custom/locally-added games (CUSTOM_GAME source) are filesystem-based and are NOT stored in the Room database. They are discovered by CustomGameScanner, which performs filesystem I/O and depends on PrefManager initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/CustomGameScanner.kt` around lines 512
- 514, The code currently gates Steam-folder detection on SteamService.instance
which causes Steam installs to be misclassified when the service is stopped;
remove that singleton check in CustomGameScanner and call
SteamService.findSteamAppWithInstallDir(folder.name) unconditionally (or use a
static/safe lookup path) so install-dir lookup runs even if
SteamService.instance is null; if findSteamAppWithInstallDir currently depends
on the instance, make it instance-safe or add a static/DAO-backed helper that
queries the Room table directly so CustomGameScanner can detect Steam rows
without requiring the SteamService singleton.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 1153-1156: Current code removes a folder from
PrefManager.customGameManualFolders then immediately calls
CustomGameScanner.invalidateCache(), risking a race where the cache is
invalidated before the underlying DataStore write completes and concurrent edits
are lost; fix by adding and using an atomic suspend update method (e.g.,
PrefManager.updateCustomGameManualFolders(updatedSet: Set<String>) : suspend
fun) that performs the read/modify/write inside dataStore.edit { } and suspends
until completion, then call CustomGameScanner.invalidateCache() only after that
suspend function returns so the preference change is durably persisted before
invalidating the scanner cache.
- Around line 596-598: The helper findSteamAppWithInstallDir currently returns
null when SteamService.instance is not initialized because it calls
instance?.appDao; change it to use the Room/DAO access path that exists before
SteamService startup (e.g., obtain the AppDatabase/AppDao singleton directly
rather than via SteamService.instance) and keep the runBlocking(Dispatchers.IO)
call; specifically replace usage of instance?.appDao with a direct call to the
shared AppDatabase/AppDao singleton (or a DB-backed repository accessor) so
findSteamAppWithInstallDir(dirName) queries the DB independently of SteamService
initialization.

In `@app/src/main/java/app/gamenative/utils/CustomGameScanner.kt`:
- Around line 517-554: Fetch the existing install record once (val installedApp
= SteamService.getInstalledApp(steamApp.id)) and use it to gate the entire
Steam-import branch: only perform the insert, add marker, and return the STEAM_*
LibraryItem when installedApp == null OR installedApp.customInstallPath ==
folderPath; otherwise skip the insert/marker and do not return the
Steam-specific LibraryItem (let the method fall through to the
non-Steam/manual-folder branch). Update references around the existing
SteamService.getInstalledApp usage and keep the runBlocking
SteamService.instance?.appInfoDao?.insert and MarkerUtils.addMarker calls inside
that guarded block.
- Around line 512-514: The code currently gates Steam-folder detection on
SteamService.instance which causes Steam installs to be misclassified when the
service is stopped; remove that singleton check in CustomGameScanner and call
SteamService.findSteamAppWithInstallDir(folder.name) unconditionally (or use a
static/safe lookup path) so install-dir lookup runs even if
SteamService.instance is null; if findSteamAppWithInstallDir currently depends
on the instance, make it instance-safe or add a static/DAO-backed helper that
queries the Room table directly so CustomGameScanner can detect Steam rows
without requiring the SteamService singleton.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 418639db-3b8d-46f4-86aa-b12d72016dc9

📥 Commits

Reviewing files that changed from the base of the PR and between 900815d and 7d2ecab.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt

Copy link
Copy Markdown
Contributor

@phobos665 phobos665 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much simpler in execution than I worried it'd be.

Looks great.

@joshuatam joshuatam force-pushed the feat/import-to-steam branch 2 times, most recently from 635c801 to d18e4fd Compare April 7, 2026 10:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
app/src/main/java/app/gamenative/utils/CustomGameScanner.kt (1)

512-556: ⚠️ Potential issue | 🟠 Major

Steam import path still proceeds when an existing install points elsewhere.

The guard at line 517 (SteamService.getInstalledApp(steamApp.id) == null) correctly skips the AppInfo insert and marker, but the Steam LibraryItem return (lines 544–554) executes unconditionally. This means a manually added folder matching an already-installed Steam game's installDir will appear as that Steam game in the library, even though AppInfo.customInstallPath points to a different location.

Either wrap the entire Steam path (lines 541–554) inside the existing guard, or add a condition like:

val existing = SteamService.getInstalledApp(steamApp.id)
if (existing == null || existing.customInstallPath == folderPath) {
    // proceed with Steam import...
}

Otherwise fall through to the custom-game path so the folder isn't mislabeled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/CustomGameScanner.kt` around lines 512
- 556, The Steam import branch returns a LibraryItem even when a Steam app is
already installed elsewhere; update the guard around
SteamService.getInstalledApp(steamApp.id) so the Steam LibraryItem creation only
runs if the app is not installed or its AppInfo.customInstallPath equals
folderPath (e.g., call SteamService.getInstalledApp(steamApp.id) once into a
variable `existing` and then only perform the runBlocking AppInfo insert,
MarkerUtils.addMarker, and the LibraryItem return when existing == null ||
existing.customInstallPath == folderPath); otherwise fall through to the
custom-game path to avoid mislabeling the folder.
app/src/main/java/app/gamenative/service/SteamService.kt (2)

1154-1157: ⚠️ Potential issue | 🟠 Major

Make the manual-folder removal atomic.

This still does a read-modify-write on the whole customGameManualFolders set, so concurrent additions/removals can be lost. Please move this behind a single atomic PrefManager update helper and invalidate the scanner only after that update completes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 1154 -
1157, The removal from PrefManager.customGameManualFolders must be done
atomically to avoid lost updates; replace the read-modify-write block with a
single PrefManager helper that applies the mutation inside an atomic update
(e.g., add or use a method like PrefManager.updateCustomGameManualFolders { set
-> set.remove(folderPath) } or a generic PrefManager.update { ... } that returns
when complete), and only call CustomGameScanner.invalidateCache() after that
atomic update completes so concurrent modifications are preserved and cache
invalidation happens post-update.

596-598: ⚠️ Potential issue | 🟠 Major

Don't gate Steam import lookup on SteamService.instance.

The DAO query itself is fine, but when the service is not initialized this returns null, so CustomGameScanner can treat a real Steam install as “no match”. Please route this lookup through a lifecycle-independent DAO/repository access path instead of instance?.appDao.
Based on learnings: CustomGameScanner performs filesystem I/O and depends on PrefManager initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 596 -
598, The method findSteamAppWithInstallDir should not depend on
SteamService.instance (which can be null) because that causes valid Steam
installs to be missed; change it to call a lifecycle-independent DAO/repository
instead of instance?.appDao (e.g., obtain the appDao from a
static/application-scoped repository or database getter used elsewhere), ensure
the lookup is executed on Dispatchers.IO as before, and update callers like
CustomGameScanner to use this new repository access path so the query works even
when SteamService.instance is not initialized and PrefManager may not be ready.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 1154-1157: The removal from PrefManager.customGameManualFolders
must be done atomically to avoid lost updates; replace the read-modify-write
block with a single PrefManager helper that applies the mutation inside an
atomic update (e.g., add or use a method like
PrefManager.updateCustomGameManualFolders { set -> set.remove(folderPath) } or a
generic PrefManager.update { ... } that returns when complete), and only call
CustomGameScanner.invalidateCache() after that atomic update completes so
concurrent modifications are preserved and cache invalidation happens
post-update.
- Around line 596-598: The method findSteamAppWithInstallDir should not depend
on SteamService.instance (which can be null) because that causes valid Steam
installs to be missed; change it to call a lifecycle-independent DAO/repository
instead of instance?.appDao (e.g., obtain the appDao from a
static/application-scoped repository or database getter used elsewhere), ensure
the lookup is executed on Dispatchers.IO as before, and update callers like
CustomGameScanner to use this new repository access path so the query works even
when SteamService.instance is not initialized and PrefManager may not be ready.

In `@app/src/main/java/app/gamenative/utils/CustomGameScanner.kt`:
- Around line 512-556: The Steam import branch returns a LibraryItem even when a
Steam app is already installed elsewhere; update the guard around
SteamService.getInstalledApp(steamApp.id) so the Steam LibraryItem creation only
runs if the app is not installed or its AppInfo.customInstallPath equals
folderPath (e.g., call SteamService.getInstalledApp(steamApp.id) once into a
variable `existing` and then only perform the runBlocking AppInfo insert,
MarkerUtils.addMarker, and the LibraryItem return when existing == null ||
existing.customInstallPath == folderPath); otherwise fall through to the
custom-game path to avoid mislabeling the folder.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 189e4b6a-6647-49d3-8933-ea8f318306fe

📥 Commits

Reviewing files that changed from the base of the PR and between 635c801 and d18e4fd.

📒 Files selected for processing (8)
  • app/schemas/app.gamenative.db.PluviaDatabase/20.json
  • app/src/main/java/app/gamenative/data/AppInfo.kt
  • app/src/main/java/app/gamenative/db/PluviaDatabase.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
  • app/src/main/java/com/winlator/core/WineUtils.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/com/winlator/core/WineUtils.java
  • app/src/main/java/app/gamenative/db/PluviaDatabase.kt
  • app/src/main/java/app/gamenative/data/AppInfo.kt

@joshuatam joshuatam force-pushed the feat/import-to-steam branch 3 times, most recently from 01ff4a8 to 12f6cd4 Compare April 13, 2026 13:01
@utkarshdalal
Copy link
Copy Markdown
Owner

I think we need to clarify some things before merging this:

  1. would games imported this way show under Steam or under Custom? Because it looks like they'll show under Steam
  2. However, when deleting we treat these games as custom games and don't delete them, just remove the folder.
  3. We should also make the ownership stricter. It matches roughly on the game folder name now, we should also check if the user actually owns it. We don't want to be accused of allowing piracy.

I think we treat these games as custom games (show them under custom games, not steam) and just do the drm handling etc as if it were a steam game. I don't think we'll know which depots and DLC are installed.

@joshuatam joshuatam force-pushed the feat/import-to-steam branch from 12f6cd4 to ab2cb3b Compare April 13, 2026 15:24
Increments the database version to 20, introducing a `custom_install_path` field to the `app_info` table for tracking user-defined installation locations.

Enhances the custom game scanner to identify and import existing Steam games by matching installation directories. Imported Steam games will now leverage existing metadata and be managed as native Steam entries.

Updates `SteamService` to use the `custom_install_path` for installed imported games and modifies the uninstall process for imported games to only remove database entries, preserving the game files.

Adjusts `WineUtils` to recognize custom game folders as valid game directories, ensuring correct drive mounting for imported titles.
@joshuatam joshuatam force-pushed the feat/import-to-steam branch from ab2cb3b to e3677fb Compare April 14, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants