feat: Adds ability to import Steam games via custom paths#1066
feat: Adds ability to import Steam games via custom paths#1066joshuatam wants to merge 1 commit intoutkarshdalal:masterfrom
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR extends support for imported games with custom install paths by adding a persisted Changes
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
app/schemas/app.gamenative.db.PluviaDatabase/19.jsonapp/src/main/java/app/gamenative/data/AppInfo.ktapp/src/main/java/app/gamenative/db/PluviaDatabase.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/CustomGameScanner.ktapp/src/main/java/com/winlator/core/WineUtils.java
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (1)
1160-1163:⚠️ Potential issue | 🔴 CriticalAvoid 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
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
♻️ Duplicate comments (4)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
1153-1156:⚠️ Potential issue | 🟠 MajorPersist 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 callingCustomGameScanner.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 underlyingdataStore.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 | 🟠 MajorKeep install-dir lookup independent of the service singleton.
This helper still returns
nullwheneverSteamService.instanceis 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 beforeSteamServicestartup.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 onPrefManagerinitialization.🤖 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 | 🟠 MajorGate 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.idalready exists, this block still returns aSTEAM_*item for the manually added folder, so the folder gets relabeled as that Steam app even thoughAppInfostill points somewhere else. Please only take the Steam-import branch when there is no installed row yet, or when the existingcustomInstallPathalready equalsfolderPath.💡 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 | 🟠 MajorDon’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_GAMEand 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 onPrefManagerinitialization.🤖 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
📒 Files selected for processing (3)
app/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/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
phobos665
left a comment
There was a problem hiding this comment.
This is much simpler in execution than I worried it'd be.
Looks great.
635c801 to
d18e4fd
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (3)
app/src/main/java/app/gamenative/utils/CustomGameScanner.kt (1)
512-556:⚠️ Potential issue | 🟠 MajorSteam import path still proceeds when an existing install points elsewhere.
The guard at line 517 (
SteamService.getInstalledApp(steamApp.id) == null) correctly skips theAppInfoinsert and marker, but the SteamLibraryItemreturn (lines 544–554) executes unconditionally. This means a manually added folder matching an already-installed Steam game'sinstallDirwill appear as that Steam game in the library, even thoughAppInfo.customInstallPathpoints 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 | 🟠 MajorMake the manual-folder removal atomic.
This still does a read-modify-write on the whole
customGameManualFoldersset, so concurrent additions/removals can be lost. Please move this behind a single atomicPrefManagerupdate 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 | 🟠 MajorDon't gate Steam import lookup on
SteamService.instance.The DAO query itself is fine, but when the service is not initialized this returns
null, soCustomGameScannercan treat a real Steam install as “no match”. Please route this lookup through a lifecycle-independent DAO/repository access path instead ofinstance?.appDao.
Based on learnings:CustomGameScannerperforms filesystem I/O and depends onPrefManagerinitialization.🤖 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
📒 Files selected for processing (8)
app/schemas/app.gamenative.db.PluviaDatabase/20.jsonapp/src/main/java/app/gamenative/data/AppInfo.ktapp/src/main/java/app/gamenative/db/PluviaDatabase.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/CustomGameScanner.ktapp/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
01ff4a8 to
12f6cd4
Compare
|
I think we need to clarify some things before merging this:
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. |
12f6cd4 to
ab2cb3b
Compare
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.
ab2cb3b to
e3677fb
Compare
Description
Increments the database version to 19, introducing a
custom_install_pathfield to theapp_infotable 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
SteamServiceto use thecustom_install_pathfor installed imported games and modifies the uninstall process for imported games to only remove database entries, preserving the game files.Adjusts
WineUtilsto recognize custom game folders as valid game directories, ensuring correct drive mounting for imported titles.Recording
Checklist
#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.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
custom_install_pathtoapp_info;AppInfo.isImportedderives from it.installDir, import it as a Steam entry, mark main depots downloaded, addDOWNLOAD_COMPLETE, save the path.SteamAppDao.findSteamAppWithInstallDir; expose it inSteamService; resolve imported game dirs viacustom_install_path.WineUtils: accept custom manual folders as valid A: game dirs for proper mounting.Migration
installDir.Written for commit e3677fb. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes