Skip to content

Streamlined launch dependencies retrieval flow#569

Closed
unbelievableflavour wants to merge 6 commits intoutkarshdalal:masterfrom
unbelievableflavour:streamlined-launch-dependencies-flow
Closed

Streamlined launch dependencies retrieval flow#569
unbelievableflavour wants to merge 6 commits intoutkarshdalal:masterfrom
unbelievableflavour:streamlined-launch-dependencies-flow

Conversation

@unbelievableflavour
Copy link
Copy Markdown
Contributor

@unbelievableflavour unbelievableflavour commented Feb 19, 2026

Extracted retrieving launch dependencies + streamlined the flow.
If we have another (doesn't matter which platform) Just add a new launch dependency file and configure it in the ALL_LAUNCH_DEPENDENCIES list.

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling during game launch with user-facing failure dialogs
  • New Features

    • Enhanced pre-launch preparation with centralized dependency management
    • Added detailed status messages for downloads and setup steps (Proton, Steam client, DRM components)
    • Better progress tracking during first-time file downloads and installations

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 19, 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 refactors the game launch pre-flight dependency handling by consolidating scattered download and extraction logic into a centralized LaunchDependencies system. It replaces ~108 lines of step-by-step operations in PluviaMain with a single ensureLaunchDependencies() call and introduces seven new LaunchDependency implementations to manage ImageFS, Proton packages, DRM extras, Steam client, and Steam tokens.

Changes

Cohort / File(s) Summary
Pre-launch Flow Refactoring
app/src/main/java/app/gamenative/ui/PluviaMain.kt, app/src/main/java/app/gamenative/utils/LaunchDependencies.kt
Simplified PluviaMain's preLaunchApp by replacing step-by-step dependency handling with centralized ensureLaunchDependencies() call wrapped in try/catch. Expanded LaunchDependencies registry with 7 new dependency entries (ImageFS, Proton, DRM, Steam).
ImageFS Dependencies
app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsBaseDependency.kt, app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsPatchesDependency.kt, app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsInstallDependency.kt
Three new dependencies for downloading base ImageFS files, Wine patches (GLIBC-only), and handling ImageFS installation with variant-specific loading messages and Future-to-coroutine bridge for cancellation support.
Proton Package Dependency
app/src/main/java/app/gamenative/utils/launchdependencies/ProtonPackageDependency.kt
New dependency class managing Proton package download and extraction to /opt with version matching, directory verification, and archive extraction via TarCompressorUtils.
Steam Dependencies
app/src/main/java/app/gamenative/utils/launchdependencies/SteamClientDependency.kt, app/src/main/java/app/gamenative/utils/launchdependencies/SteamTokenDependency.kt, app/src/main/java/app/gamenative/utils/launchdependencies/DrmExtrasDependency.kt
Three new dependencies triggering on real Steam launches to download Steam client and token files, plus conditional DRM extras download based on container flags; all scoped to specific launch scenarios.
ImageFS Installer Enhancement
app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
Added public needsInstall(Context, Container) helper method to determine whether ImageFS installation is required, reducing logic duplication.
Localization
app/src/main/res/values/strings.xml
Added six new launch status messages for downloading first-time files, wine, proton, extras, Steam token, and extracting proton.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A bunny hops through launch-time chores,
Dependencies bundled, no more stepping floors,
Proton, Steam, and ImageFS delight,
All organized now—what a beautiful sight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Streamlined launch dependencies retrieval flow' accurately describes the main change: refactoring the launch dependencies handling from scattered, step-by-step logic into a centralized, modular system.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

No issues found across 10 files

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: 6

🧹 Nitpick comments (6)
app/src/main/java/app/gamenative/utils/launchdependencies/LaunchDependency.kt (1)

28-33: CoroutineScope parameter in a suspend function is an atypical structured-concurrency pattern.

The install implementations launch child work on the caller-supplied scope (via SteamService.download*(..., scope, ...)) and then await() the result. In Kotlin structured concurrency, a suspend function's implicit coroutineContext is the idiomatic way to inherit scope — passing an explicit CoroutineScope as a parameter makes the child jobs siblings of (rather than true children of) this call site's coroutine. As long as callers always pass their own this scope (as preLaunchApp does today), cancellation still propagates correctly; but it's an API footgun for future callers.

🤖 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/launchdependencies/LaunchDependency.kt`
around lines 28 - 33, The suspend function install currently takes an explicit
CoroutineScope parameter which is an anti-pattern: remove the CoroutineScope
parameter from the install signature and from all implementations, and use the
calling coroutine's implicit scope via coroutineContext or by launching children
with coroutineScope { ... } / withContext as needed so launched work becomes
proper child coroutines; update calls to SteamService.download*(...) to accept
the current coroutineContext (or be invoked inside coroutineScope) instead of
the removed scope param and adjust callers such as preLaunchApp to stop passing
their scope.
app/src/main/java/app/gamenative/utils/launchdependencies/DrmExtrasDependency.kt (1)

10-10: Date-stamped filename is a maintenance footgun.

"experimental-drm-20260116.tzst" will need a code change every time the DRM asset is refreshed. Consider centralising asset filenames in a single constants file or deriving the name from a config/manifest fetched at runtime, so adding a new DRM file doesn't require touching each dependency.

🤖 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/launchdependencies/DrmExtrasDependency.kt`
at line 10, The hard-coded, date-stamped constant FILE in DrmExtrasDependency.kt
("experimental-drm-20260116.tzst") forces code changes when the DRM asset is
refreshed; replace this ad-hoc string with a centralized, maintainable source
(e.g., read the filename from a shared constants/config/manifest or a runtime
config service) by removing the literal FILE value and instead referencing a
single maintained constant or a config lookup (e.g., Config.DRM_FILE or
Manifest.getDrmFilename()) so future DRM updates only require changing one place
or the external config rather than this class.
app/src/main/java/app/gamenative/utils/LaunchDependencies.kt (2)

53-53: Extract the -1f sentinel into a named constant.

-1f is an opaque magic number. A named constant makes the intent clear and prevents the same literal being scattered across call sites.

♻️ Proposed fix
+private const val PROGRESS_INDETERMINATE = -1f
 
 ...
-    setLoadingProgress(-1f)
+    setLoadingProgress(PROGRESS_INDETERMINATE)
🤖 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/LaunchDependencies.kt` at line 53,
Replace the magic literal -1f passed into setLoadingProgress with a named
constant to convey intent: add a descriptive constant (e.g.,
LOADING_PROGRESS_UNKNOWN or LOADING_PROGRESS_SENTINEL) in LaunchDependencies.kt
(top-level or inside a companion object) and use that constant in the call to
setLoadingProgress instead of the literal; update any other occurrences of -1f
sentinel used for loading progress to reference the same constant so the meaning
is centralized and consistent.

46-53: Missing try-finally: UI stays stuck in loading state on install failure.

If any dep.install() throws (e.g., network error, cancellation), lines 52–53 are skipped and the loading message/progress are never reset, leaving the UI in a perpetual loading state. The caller in PluviaMain (line 1070) does not provide exception handling. Wrap the loop in a try-finally:

♻️ Proposed fix
     val callbacks = LaunchDependencyCallbacks(setLoadingMessage, setLoadingProgress)
+    try {
         for (dep in getLaunchDependencies(container)) {
             if (!dep.isSatisfied(context, container)) {
                 setLoadingMessage(dep.getLoadingMessage(context, container))
                 dep.install(context, container, scope, callbacks)
             }
         }
+    } finally {
+        setLoadingMessage(context.getString(R.string.main_loading))
+        setLoadingProgress(-1f)
+    }
-    setLoadingMessage(context.getString(R.string.main_loading))
-    setLoadingProgress(-1f)
🤖 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/LaunchDependencies.kt` around lines 46
- 53, The loop over getLaunchDependencies(container) can throw inside
dep.install(...) and currently prevents setLoadingMessage(...) and
setLoadingProgress(...) from running; wrap the entire for (dep in
getLaunchDependencies(container)) { ... } block in a try-finally and move the
final setLoadingMessage(context.getString(R.string.main_loading)) and
setLoadingProgress(-1f) into the finally block so the UI is always reset even
when dep.install throws; keep existing calls to dep.isSatisfied,
dep.getLoadingMessage and dep.install unchanged and decide whether to rethrow or
log the exception inside the try as appropriate.
app/src/main/java/app/gamenative/utils/launchdependencies/SteamTokenDependency.kt (2)

14-14: Extract hardcoded loading message to string resource for localization.

"Downloading steam-token" should use a string resource like SteamClientDependency does with R.string.main_downloading_steam. This ensures the message is properly localized across the app's 13+ supported languages.

♻️ Proposed fix
-    override fun getLoadingMessage(context: Context, container: Container) = "Downloading steam-token"
+    override fun getLoadingMessage(context: Context, container: Container) =
+        context.getString(R.string.downloading_steam_token)

Add the corresponding entry to strings.xml and all locale variants:

<string name="downloading_steam_token">Downloading steam-token</string>
🤖 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/launchdependencies/SteamTokenDependency.kt`
at line 14, Replace the hardcoded "Downloading steam-token" in
SteamTokenDependency.getLoadingMessage with a string resource lookup (use
context.getString(R.string.downloading_steam_token) similar to how
SteamClientDependency uses R.string.main_downloading_steam), and add the
downloading_steam_token entry to strings.xml and all localized strings XML files
with the translated text "Downloading steam-token".

15-27: Prefer structured concurrency with coroutineScope { } for safer lifecycle management.

The current implementation passes an external scope parameter to downloadFile(), which relies on that scope being tied to the calling coroutine's lifecycle. While this works correctly when PluviaMain passes this, using coroutineScope { } makes the structured-concurrency intent explicit and prevents accidental lifecycle mismatches if the calling pattern changes:

♻️ Suggested refactor
    override suspend fun install(
        context: Context,
        container: Container,
        scope: CoroutineScope,
        callbacks: LaunchDependencyCallbacks,
    ) {
+       coroutineScope {
            SteamService.downloadFile(
                onDownloadProgress = callbacks.setLoadingProgress,
-               scope,
+               scope = this,
                context = context,
                FILE,
            ).await()
+       }
    }
🤖 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/launchdependencies/SteamTokenDependency.kt`
around lines 15 - 27, The install method in SteamTokenDependency currently
forwards an external scope into SteamService.downloadFile which risks lifecycle
mismatch; wrap the download call inside a coroutineScope { } within
SteamTokenDependency.install so the download is launched in the calling
coroutine's structured scope (use coroutineScope {
SteamService.downloadFile(..., scope = this, ...).await() } or pass a child
coroutine context derived from coroutineScope) and remove reliance on the
external scope parameter to ensure the download is cancelled with the install
coroutine; update references to SteamService.downloadFile and the install
signature accordingly.
🤖 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/PluviaMain.kt`:
- Around line 1068-1076: The call to ensureLaunchDependencies from inside the
CoroutineScope(Dispatchers.IO).launch can throw and leave the loading UI
visible; wrap the invocation of ensureLaunchDependencies in a try/catch/finally
inside that coroutine (or surrounding suspend function) so any exception is
caught, call setLoadingDialogVisible(false) in finally to guarantee the loading
overlay is dismissed, and in catch use the existing UI helpers (e.g.,
setLoadingMessage or an error dialog) to surface the error to the user;
reference ensureLaunchDependencies and the coroutine started with
CoroutineScope(Dispatchers.IO).launch to locate where to add the
try/catch/finally.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsBaseDependency.kt`:
- Line 13: The hardcoded string in ImageFsBaseDependency.getLoadingMessage
should be replaced with a localized string resource; update
getLoadingMessage(context: Context, container: Container) to return
context.getString(R.string.<appropriate_key>) (create a new string resource
entry like "downloading_first_time_files" if needed) to match the localization
approach used in SteamClientDependency and ImageFsInstallDependency; do the same
audit/fix for ImageFsPatchesDependency and DrmExtrasDependency so all loading
messages use context.getString(...) and consistent resource keys.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsInstallDependency.kt`:
- Line 12: The isSatisfied override currently always returns false causing
ensureLaunchDependencies to always show the loading message and call install();
change isSatisfied(context: Context, container: Container) to query the
installer for whether installation is actually required (e.g., call a new or
existing ImageFsInstaller.needsInstall() or similar API) and return that result;
if ImageFsInstaller lacks such a method, add a lightweight
needsInstall()/isInstallNeeded() that checks the installation state without
performing work, and have isSatisfied delegate to that so
setLoadingMessage("Installing glibc/bionic") and install() are only invoked when
real work is needed.
- Around line 25-27: The call to
ImageFsInstaller.installIfNeededFuture(...).get() blocks a thread and ignores
coroutine cancellation; replace the blocking get with a coroutine-friendly await
by wrapping ImageFsInstaller.installIfNeededFuture(...) in
suspendCancellableCoroutine (or use withContext(Dispatchers.IO) and
suspendCancellableCoroutine) so the callback progress still calls
callbacks.setLoadingProgress(progress / 100f), and ensure you cancel the
underlying Future in the coroutine's invokeOnCancellation handler; locate the
call site invoking ImageFsInstaller.installIfNeededFuture and swap .get() for
the cancellable suspension that cancels the Future when the coroutine is
cancelled.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ProtonPackageDependency.kt`:
- Around line 33-40: The call to
callbacks.setLoadingMessage(getLoadingMessage(...)) inside
ProtonPackageDependency.install is redundant because ensureLaunchDependencies
already sets the loading message before calling dep.install; remove that
duplicate call from the install method (look for ProtonPackageDependency.install
and the callbacks.setLoadingMessage invocation) so install only performs the
download via SteamService.downloadFile and progress updates without resetting
the loading message.
- Around line 17-18: The appliesTo implementation uses
wineVersion.contains(fileName.removeSuffix(".txz")) which can match a divergent
wineVersion and later cause extraction to look for the wrong
"<protonVersion>.txz"; change appliesTo to check against fileName (or
fileName.removeSuffix only when you also derive the exact archive name
consistently) so the matched container corresponds exactly to the downloaded
fileName, and when calling TarCompressorUtils.extract (or any extractor) wrap
the call in try/catch to log and rethrow or surface errors via the installer
flow (use processLogger or the class logger) so extraction failures are not
swallowed; update references in ProtonPackageDependency (appliesTo, extraction
call sites) to use the same fileName value and add exception handling around
TarCompressorUtils.extract to log the archive path and the caught exception.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/LaunchDependencies.kt`:
- Line 53: Replace the magic literal -1f passed into setLoadingProgress with a
named constant to convey intent: add a descriptive constant (e.g.,
LOADING_PROGRESS_UNKNOWN or LOADING_PROGRESS_SENTINEL) in LaunchDependencies.kt
(top-level or inside a companion object) and use that constant in the call to
setLoadingProgress instead of the literal; update any other occurrences of -1f
sentinel used for loading progress to reference the same constant so the meaning
is centralized and consistent.
- Around line 46-53: The loop over getLaunchDependencies(container) can throw
inside dep.install(...) and currently prevents setLoadingMessage(...) and
setLoadingProgress(...) from running; wrap the entire for (dep in
getLaunchDependencies(container)) { ... } block in a try-finally and move the
final setLoadingMessage(context.getString(R.string.main_loading)) and
setLoadingProgress(-1f) into the finally block so the UI is always reset even
when dep.install throws; keep existing calls to dep.isSatisfied,
dep.getLoadingMessage and dep.install unchanged and decide whether to rethrow or
log the exception inside the try as appropriate.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/DrmExtrasDependency.kt`:
- Line 10: The hard-coded, date-stamped constant FILE in DrmExtrasDependency.kt
("experimental-drm-20260116.tzst") forces code changes when the DRM asset is
refreshed; replace this ad-hoc string with a centralized, maintainable source
(e.g., read the filename from a shared constants/config/manifest or a runtime
config service) by removing the literal FILE value and instead referencing a
single maintained constant or a config lookup (e.g., Config.DRM_FILE or
Manifest.getDrmFilename()) so future DRM updates only require changing one place
or the external config rather than this class.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/LaunchDependency.kt`:
- Around line 28-33: The suspend function install currently takes an explicit
CoroutineScope parameter which is an anti-pattern: remove the CoroutineScope
parameter from the install signature and from all implementations, and use the
calling coroutine's implicit scope via coroutineContext or by launching children
with coroutineScope { ... } / withContext as needed so launched work becomes
proper child coroutines; update calls to SteamService.download*(...) to accept
the current coroutineContext (or be invoked inside coroutineScope) instead of
the removed scope param and adjust callers such as preLaunchApp to stop passing
their scope.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/SteamTokenDependency.kt`:
- Line 14: Replace the hardcoded "Downloading steam-token" in
SteamTokenDependency.getLoadingMessage with a string resource lookup (use
context.getString(R.string.downloading_steam_token) similar to how
SteamClientDependency uses R.string.main_downloading_steam), and add the
downloading_steam_token entry to strings.xml and all localized strings XML files
with the translated text "Downloading steam-token".
- Around line 15-27: The install method in SteamTokenDependency currently
forwards an external scope into SteamService.downloadFile which risks lifecycle
mismatch; wrap the download call inside a coroutineScope { } within
SteamTokenDependency.install so the download is launched in the calling
coroutine's structured scope (use coroutineScope {
SteamService.downloadFile(..., scope = this, ...).await() } or pass a child
coroutine context derived from coroutineScope) and remove reliance on the
external scope parameter to ensure the download is cancelled with the install
coroutine; update references to SteamService.downloadFile and the install
signature accordingly.

Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt Outdated
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 11 files (changes from recent commits).

Prompt for AI agents (all 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/com/winlator/xenvironment/ImageFsInstaller.java">

<violation number="1" location="app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java:201">
P1: NullPointerException in 'needsInstall' and 'installIfNeededFuture' when container is null - the method dereferences container.getContainerVariant() without null checking, but the 2-arg overload explicitly passes null</violation>
</file>

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

Comment thread app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
@unbelievableflavour unbelievableflavour force-pushed the streamlined-launch-dependencies-flow branch from ef179f6 to 5928c54 Compare February 19, 2026 10:07
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: 6

🤖 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/PluviaMain.kt`:
- Around line 1069-1089: The catch/finally block around ensureLaunchDependencies
must be changed: after logging and calling
setMessageDialogState(MessageDialogState(...)) inside the catch, add a
return@launch to stop further execution (prevent reaching
containerManager.activateContainer and onSuccess), remove the unconditional
setLoadingDialogVisible(false) from the finally block and instead hide the
loading dialog only on the success path after Steam cloud sync completes, and
change DialogType.NONE to DialogType.SYNC_FAIL when building the
MessageDialogState so the dismiss handler is wired up; reference
ensureLaunchDependencies, setMessageDialogState, MessageDialogState,
DialogType.SYNC_FAIL, return@launch, setLoadingDialogVisible, and
containerManager.activateContainer to locate the affected code.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsInstallDependency.kt`:
- Around line 23-40: The install function currently uses an expression body
causing the coroutine to infer a Boolean return and silently discards install
failures; change the implementation to a block body that uses
suspendCancellableCoroutine<Unit> and resume the continuation with Unit on
success, but if ImageFsInstaller.installIfNeededFuture(...) returns false then
resumeWithException (throw a descriptive IOException or IllegalStateException)
so failures are propagated instead of ignored; keep the existing
cont.invokeOnCancellation { future.cancel(true) } and the
callbacks.setLoadingProgress(progress / 100f) progress reporting unchanged.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsPatchesDependency.kt`:
- Around line 16-17: The build fails because
ImageFsPatchesDependency.getLoadingMessage references a missing string resource
R.string.main_downloading_wine; add a new string entry with the key
"main_downloading_wine" to your strings.xml (and any locale variants) containing
an appropriate user-visible message like "Downloading Wine…" so the call in
getLoadingMessage(Context, Container) resolves.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ProtonPackageDependency.kt`:
- Line 29: Replace the hardcoded user-facing strings in
ProtonPackageDependency.kt by using Android string resources with placeholders:
change getLoadingMessage(context, container) to return
context.getString(R.string.downloading_proton, displayName) and replace
callbacks.setLoadingMessage("Extracting $protonVersion") with
callbacks.setLoadingMessage(context.getString(R.string.extracting_proton,
protonVersion)); add the corresponding entries (with %1$s placeholders) to
strings.xml and use the same resource IDs where other dependencies follow this
pattern so localization is preserved.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/SteamTokenDependency.kt`:
- Around line 15-16: The code references a missing string resource
R.string.main_downloading_steam_token in SteamTokenDependency.getLoadingMessage;
add a new string entry with that key to your app's strings.xml (and replicate
into all locale variants or provide placeholders) — e.g., create <string
name="main_downloading_steam_token">Downloading Steam token…</string> in the
main strings.xml and ensure localized strings.xml files are updated so the
resource resolves at build time.

In `@app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java`:
- Around line 201-211: The needsInstall method can NPE when container is null;
update needsInstall(Context, Container) to handle a null container (e.g., treat
null as "no variant" or return true if container is required) by guarding the
final comparison against container.getContainerVariant(), and update
installIfNeededFuture(Context, AssetManager, Container, Callback) to not call
container.getContainerVariant() without a null check; also remove the dead local
ImageFs imageFs in installIfNeededFuture since needsInstall calls
ImageFs.find(context) internally. Ensure references: needsInstall,
installIfNeededFuture, ImageFs.find, imageFs.getVariant(), and
container.getContainerVariant().

Comment thread app/src/main/java/app/gamenative/utils/launchdependencies/SteamTokenDependency.kt Outdated
Comment thread app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.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: 3

🤖 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/Constants.kt`:
- Line 37: Confirm whether the "experimental" prefix in the constant
DRM_EXTRAS_FILENAME is intentional for production; check the upstream/download
endpoint to verify the canonical filename it serves and, if a non-experimental
variant is required, update the DRM_EXTRAS_FILENAME value in Constants.kt to the
correct production filename and update any dependent code, tests, and
documentation that reference this constant so they use the canonical name.

In `@app/src/main/res/values/strings.xml`:
- Line 760: Update the user-facing string resource main_downloading_steam_token
so the value is user-friendly prose: change "Downloading steam-token" to
"Downloading Steam token" (capitalize "Steam" and replace the hyphen with a
space) to match other platform references and normal sentence style.
- Around line 757-760: The four new in-progress string resources
(main_downloading_first_time_files, main_downloading_wine,
main_downloading_extras, main_downloading_steam_token) are missing the trailing
ellipsis used elsewhere; update each string value to append "…" (or "..." if
using ASCII) so they match the other loading strings (e.g.,
main_downloading_steam, main_installing_glibc, main_installing_bionic,
main_loading) and restore visual consistency in the launch progress UI.

Comment thread app/src/main/java/app/gamenative/Constants.kt Outdated
Comment thread app/src/main/res/values/strings.xml Outdated
Comment thread app/src/main/res/values/strings.xml Outdated
@unbelievableflavour unbelievableflavour force-pushed the streamlined-launch-dependencies-flow branch from 21d33a4 to ab17c50 Compare February 20, 2026 07:51
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: 2

🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsInstallDependency.kt (1)

23-48: Previous build failure and silent-failure issues are resolved; two minor residual notes.

The block body, suspendCancellableCoroutine<Unit>, cont.invokeOnCancellation, and the explicit check of result == false (throwing IllegalStateException) all look correct.

Two minor items:

  1. future.get() as Boolean unchecked cast (line 35): At runtime, Java generic erasure means .get() returns Object. The cast succeeds for a normal Future<Boolean> but would throw ClassCastException (caught by the catch block below) if the future ever returns a non-Boolean. A slightly more defensive form avoids the explicit cast:
♻️ Optional: safer cast
-                    val result = future.get() as Boolean
+                    val result = future.get() == true  // null/non-Boolean → treated as failure
                    if (cont.isActive) {
-                        if (result) {
+                        if (result) {
                             cont.resume(Unit)
                         } else {
                             cont.resumeWithException(IllegalStateException("ImageFs install completed but returned false"))
                         }
                     }
  1. Fully-qualified kotlinx.coroutines.Dispatchers.IO (line 28): The other files in this package import and use Dispatchers directly. An import would be consistent:
♻️ Optional: add import
+import kotlinx.coroutines.Dispatchers
...
-        withContext(kotlinx.coroutines.Dispatchers.IO) {
+        withContext(Dispatchers.IO) {
🤖 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/launchdependencies/ImageFsInstallDependency.kt`
around lines 23 - 48, The suspend block in ImageFsInstallDependency.install uses
an unchecked cast (future.get() as Boolean) and a fully-qualified
kotlinx.coroutines.Dispatchers.IO; update the code to defensively check the
returned value's type instead of casting (e.g., call val result = future.get();
if (result is Boolean) { if (result) cont.resume(Unit) else
cont.resumeWithException(IllegalStateException(...)) } else
cont.resumeWithException(IllegalStateException("Unexpected result type:
${result?.javaClass}"))) and keep the existing cancellation handling
(cont.invokeOnCancellation). Also add an import for Dispatchers and replace
kotlinx.coroutines.Dispatchers.IO with Dispatchers.IO to match the package
convention.
app/src/main/java/app/gamenative/utils/LaunchDependencies.kt (1)

46-56: finally block unconditionally resets loading state even on success.

On a fully successful run, the finally block sets the loading message back to main_loading and resets progress to LOADING_PROGRESS_UNKNOWN before returning to the caller. Since PluviaMain then proceeds directly to game launch, the "Loading" message may flash briefly before the game starts. On failure, the same reset happens before the error dialog is shown.

Consider only resetting on failure to avoid the spurious visual reset:

♻️ Proposed refactor
 suspend fun ensureLaunchDependencies(...) {
     val callbacks = LaunchDependencyCallbacks(setLoadingMessage, setLoadingProgress)
+    var succeeded = false
     try {
         for (dep in getLaunchDependencies(container)) {
             if (!dep.isSatisfied(context, container)) {
                 setLoadingMessage(dep.getLoadingMessage(context, container))
                 dep.install(context, container, callbacks)
             }
         }
+        succeeded = true
     } finally {
-        setLoadingMessage(context.getString(R.string.main_loading))
-        setLoadingProgress(LOADING_PROGRESS_UNKNOWN)
+        if (!succeeded) {
+            setLoadingMessage(context.getString(R.string.main_loading))
+            setLoadingProgress(LOADING_PROGRESS_UNKNOWN)
+        }
     }
 }
🤖 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/LaunchDependencies.kt` around lines 46
- 56, The finally block currently always resets the UI (setLoadingMessage and
setLoadingProgress) even on success; change control flow so the reset only
happens on failure—e.g., introduce a boolean flag (e.g., success) set to true
after the dependency loop in getLaunchDependencies finishes without throwing,
then in the finally block only call
setLoadingMessage(context.getString(R.string.main_loading)) and
setLoadingProgress(LOADING_PROGRESS_UNKNOWN) if success is false (or
alternatively move those resets into a catch block); adjust references to
dep.isSatisfied, dep.getLoadingMessage, dep.install and callbacks as needed to
keep behavior identical on exceptions.
🤖 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/PluviaMain.kt`:
- Around line 1152-1172: The catch block handling ensureLaunchDependencies
currently shows update-specific strings (R.string.main_update_failed_title /
R.string.main_update_failed_message) which misleads users; update the dialog to
use a dependency-specific string (e.g., add
R.string.main_dependency_failed_message and use it as the fallback) or reuse the
existing sync-error title/message instead, and update the setMessageDialogState
call (MessageDialogState, DialogType.SYNC_FAIL) to reference the new/appropriate
string resource so dependency/download errors display a correct message.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ProtonPackageDependency.kt`:
- Around line 23-29: The isSatisfied check in
ProtonPackageDependency.isSatisfied currently gates readiness on the archive via
SteamService.isFileInstallable; remove that archive existence check and instead
return true solely when the extracted bin directory exists (use
ImageFs.find(context) to build outFile = File(imageFs.rootDir,
"/opt/${container.wineVersion}") and then check File(outFile, "bin").exists() &&
isDirectory). Leave install() unchanged (it will still use the archive during
extraction) so cleanup of the archive after extraction won’t cause false
negatives.

---

Duplicate comments:
In `@app/src/main/java/app/gamenative/Constants.kt`:
- Around line 36-38: The DRM_EXTRAS_FILENAME constant under object LaunchAssets
currently uses an "experimental" prefixed filename; verify the canonical
filename served by the production download endpoint and update
DRM_EXTRAS_FILENAME in LaunchAssets to match that canonical name (remove or
change the "experimental" prefix if the production file differs), and add a
brief comment above DRM_EXTRAS_FILENAME noting the source/endpoint used to
confirm the filename so future reviewers can trace the value.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsPatchesDependency.kt`:
- Around line 14-15: The isSatisfied implementation in ImageFsPatchesDependency
currently returns SteamService.isFileInstallable(context, FILE), which mirrors
the same inverted logic as DrmExtrasDependency: if isFileInstallable(...)
actually indicates the file "needs to be installed" (i.e. not yet present), flip
the boolean so isSatisfied means the dependency is met; change
ImageFsPatchesDependency.isSatisfied to return
!SteamService.isFileInstallable(context, FILE) (or, alternatively, call a
positively named helper such as SteamService.isFileInstalled) so the semantics
align (reference: ImageFsPatchesDependency.isSatisfied,
SteamService.isFileInstallable, FILE).

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/LaunchDependencies.kt`:
- Around line 46-56: The finally block currently always resets the UI
(setLoadingMessage and setLoadingProgress) even on success; change control flow
so the reset only happens on failure—e.g., introduce a boolean flag (e.g.,
success) set to true after the dependency loop in getLaunchDependencies finishes
without throwing, then in the finally block only call
setLoadingMessage(context.getString(R.string.main_loading)) and
setLoadingProgress(LOADING_PROGRESS_UNKNOWN) if success is false (or
alternatively move those resets into a catch block); adjust references to
dep.isSatisfied, dep.getLoadingMessage, dep.install and callbacks as needed to
keep behavior identical on exceptions.

In
`@app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsInstallDependency.kt`:
- Around line 23-48: The suspend block in ImageFsInstallDependency.install uses
an unchecked cast (future.get() as Boolean) and a fully-qualified
kotlinx.coroutines.Dispatchers.IO; update the code to defensively check the
returned value's type instead of casting (e.g., call val result = future.get();
if (result is Boolean) { if (result) cont.resume(Unit) else
cont.resumeWithException(IllegalStateException(...)) } else
cont.resumeWithException(IllegalStateException("Unexpected result type:
${result?.javaClass}"))) and keep the existing cancellation handling
(cont.invokeOnCancellation). Also add an import for Dispatchers and replace
kotlinx.coroutines.Dispatchers.IO with Dispatchers.IO to match the package
convention.

Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt Outdated
@unbelievableflavour unbelievableflavour force-pushed the streamlined-launch-dependencies-flow branch from ab17c50 to abff57a Compare February 24, 2026 08:57
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 (1)
app/src/main/java/app/gamenative/utils/launchdependencies/ProtonPackageDependency.kt (1)

24-29: isSatisfied still gates on the archive file — causes unnecessary re-downloads if archive is cleaned up after extraction.

If the archive (proton-9.0-arm64ec.txz) is deleted after successful extraction (e.g., storage cleanup), isFileInstallable returns false, causing isSatisfied to return false and triggering a full re-download even though /opt/<wineVersion>/bin already exists. Checking binDir.exists() && binDir.isDirectory alone is sufficient, since install() already handles the "archive missing but needs extraction" path independently.

♻️ Proposed fix
     override fun isSatisfied(context: Context, container: Container, gameSource: GameSource, gameId: Int): Boolean {
-        if (!SteamService.isFileInstallable(context, fileName)) return false
         val imageFs = ImageFs.find(context)
         val outFile = File(imageFs.rootDir, "/opt/${container.wineVersion}")
         val binDir = File(outFile, "bin")
         return binDir.exists() && binDir.isDirectory
     }
🤖 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/launchdependencies/ProtonPackageDependency.kt`
around lines 24 - 29, The isSatisfied method currently returns false if
SteamService.isFileInstallable(context, fileName) is false, causing re-downloads
when the archive is removed after extraction; change isSatisfied (in
ProtonPackageDependency) to stop gating on SteamService.isFileInstallable and
instead only verify the installation directory: use ImageFs.find(context) to
build outFile = File(imageFs.rootDir, "/opt/${container.wineVersion}") and
return binDir.exists() && binDir.isDirectory (where binDir = File(outFile,
"bin")), removing the call to SteamService.isFileInstallable and the fileName
dependency.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/utils/LaunchDependencies.kt (1)

25-63: LaunchDependencies is stateless — consider making it an object instead of a class.

The class has no instance state; the dependency list lives in the companion object, and ensureLaunchDependencies doesn't use this. A new instance is allocated on every launch call in PluviaMain. Converting to a top-level object (or just making both functions top-level/companion) would avoid the unnecessary allocation and make the singleton nature explicit.

♻️ Suggested refactor
-class LaunchDependencies {
-    companion object {
-        private val launchDependencies: List<LaunchDependency> = listOf(
+object LaunchDependencies {
+    private val launchDependencies: List<LaunchDependency> = listOf(
             ImageFsBaseDependency,
             ...
             GogScriptInterpreterDependency,
         )
-    }
 
-    fun getLaunchDependencies(...) = ...
-    suspend fun ensureLaunchDependencies(...) { ... }
+    fun getLaunchDependencies(...) = ...
+    suspend fun ensureLaunchDependencies(...) { ... }
 }

Then in PluviaMain.kt:

-LaunchDependencies().ensureLaunchDependencies(...)
+LaunchDependencies.ensureLaunchDependencies(...)
🤖 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/LaunchDependencies.kt` around lines 25
- 63, The LaunchDependencies class is effectively stateless; replace the
class+companion pattern with a singleton object by converting "class
LaunchDependencies" into "object LaunchDependencies" and moving the existing
companion contents and methods (getLaunchDependencies and
ensureLaunchDependencies) into that object, removing the redundant companion;
update call sites (e.g. in PluviaMain) to call
LaunchDependencies.getLaunchDependencies(...) and
LaunchDependencies.ensureLaunchDependencies(...) directly instead of
instantiating LaunchDependencies.
app/src/main/java/app/gamenative/ui/PluviaMain.kt (1)

1187-1211: Re-throw CancellationException to preserve coroutine cancellation semantics.

The catch (e: Exception) block will intercept CancellationException from the suspend function ensureLaunchDependencies(), which breaks proper coroutine cancellation. Since the TODO comment indicates cancellation support will be added, implement the standard Kotlin pattern of re-throwing CancellationException.

Suggested fix
             } catch (e: Exception) {
+                if (e is kotlinx.coroutines.CancellationException) throw e
                 Timber.tag("preLaunchApp").e(e, "ensureLaunchDependencies failed")
                 setLoadingDialogVisible(false)
🤖 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/PluviaMain.kt` around lines 1187 - 1211,
The catch currently swallows CancellationException from the suspend call
LaunchDependencies().ensureLaunchDependencies which breaks coroutine
cancellation; update the error handling in the surrounding try/catch so that
CancellationException is re-thrown (either add an explicit catch(e:
CancellationException) { throw e } before the general Exception catch, or check
if e is a CancellationException and rethrow it) and keep the existing logging/UI
error handling for non-cancellation exceptions.
🤖 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/utils/launchdependencies/ProtonPackageDependency.kt`:
- Around line 24-29: The isSatisfied method currently returns false if
SteamService.isFileInstallable(context, fileName) is false, causing re-downloads
when the archive is removed after extraction; change isSatisfied (in
ProtonPackageDependency) to stop gating on SteamService.isFileInstallable and
instead only verify the installation directory: use ImageFs.find(context) to
build outFile = File(imageFs.rootDir, "/opt/${container.wineVersion}") and
return binDir.exists() && binDir.isDirectory (where binDir = File(outFile,
"bin")), removing the call to SteamService.isFileInstallable and the fileName
dependency.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Around line 1187-1211: The catch currently swallows CancellationException from
the suspend call LaunchDependencies().ensureLaunchDependencies which breaks
coroutine cancellation; update the error handling in the surrounding try/catch
so that CancellationException is re-thrown (either add an explicit catch(e:
CancellationException) { throw e } before the general Exception catch, or check
if e is a CancellationException and rethrow it) and keep the existing logging/UI
error handling for non-cancellation exceptions.

In `@app/src/main/java/app/gamenative/utils/LaunchDependencies.kt`:
- Around line 25-63: The LaunchDependencies class is effectively stateless;
replace the class+companion pattern with a singleton object by converting "class
LaunchDependencies" into "object LaunchDependencies" and moving the existing
companion contents and methods (getLaunchDependencies and
ensureLaunchDependencies) into that object, removing the redundant companion;
update call sites (e.g. in PluviaMain) to call
LaunchDependencies.getLaunchDependencies(...) and
LaunchDependencies.ensureLaunchDependencies(...) directly instead of
instantiating LaunchDependencies.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab17c50 and abff57a.

📒 Files selected for processing (11)
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/utils/LaunchDependencies.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/DrmExtrasDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsBaseDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsInstallDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsPatchesDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/ProtonPackageDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/SteamClientDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/SteamTokenDependency.kt
  • app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/src/main/res/values/strings.xml
  • app/src/main/java/app/gamenative/utils/launchdependencies/SteamTokenDependency.kt
  • app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
  • app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsInstallDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/ImageFsBaseDependency.kt
  • app/src/main/java/app/gamenative/utils/launchdependencies/DrmExtrasDependency.kt

@unbelievableflavour unbelievableflavour force-pushed the streamlined-launch-dependencies-flow branch from abff57a to 8d1be23 Compare February 24, 2026 09:13
@unbelievableflavour unbelievableflavour force-pushed the streamlined-launch-dependencies-flow branch from 8d1be23 to 047fc11 Compare February 25, 2026 08:41
@unbelievableflavour unbelievableflavour marked this pull request as draft March 14, 2026 22:40
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.

1 participant