Skip to content

Fix and Enhance Real Steam Client Functionality With Focus on Reducing Black Screens#1347

Open
TideGear wants to merge 36 commits into
utkarshdalal:masterfrom
TideGear:Fix-Steam-Client
Open

Fix and Enhance Real Steam Client Functionality With Focus on Reducing Black Screens#1347
TideGear wants to merge 36 commits into
utkarshdalal:masterfrom
TideGear:Fix-Steam-Client

Conversation

@TideGear
Copy link
Copy Markdown

@TideGear TideGear commented May 2, 2026

This PR hardens the existing Launch Steam Client ("real-Steam") path so it stops vague-black-screening, becomes more compatible across a wider set of titles, and stops cascading state corruption between containers. Most of the work is in the real-Steam launch flow, but several lifecycle/correctness fixes that surfaced along the way also benefit standard cold-client launches.

Beta caveat. Real-Steam mode is still very much beta. This PR makes it dramatically more usable in my testing, but it's not magic — please keep that label visible in the UI for now.

What it does:

  • Removes -silent from the Steam launch flags. All -silent was really doing was hiding the cloud-conflict-resolution dialog, so failed boots looked identical to silent hangs. With it removed, you can actually see what Steam is doing when something doesn't work. Steam still launches behind the game window thanks to -vgui -tcp -no-browser -nobigpicture -nofriendsui -nochatui -nointro.
  • Stops the "gray Play" reconcile loop. PICS-driven shared-depot resolution (replacing a hardcoded 228980 depot table that mismatched many games), proper child-acf SharedDepots declaration, and writes appmanifest_241100.acf alongside appmanifest_228980.acf so Steam doesn't reparent depots on every launch.
  • Suppresses the per-launch vcredist/DXSETUP reinstall cycle by writing Installed=1/Run=1 install-script registry shims for the canonical 228980 depots. The reinstaller was racing the game's own MSVC loader and (on Unity titles) tripping UnityCrashHandler mid-init.
  • Cloud-save sanity. Filters localhost pending-upload phantoms from Steam's "Pending Upload" dialog, probes-before-dismissing so we don't corrupt ChangeNumber, and clears stale AppSessionActive phantoms in beginLaunchApp. Cloud saves are still synced through GameNative's own SteamAutoCloud rather than the in-Wine Steam Client — same as before — for stability. We mirror to/from <userdata>/<appid>/remote/ ourselves before/after launch.
  • Graceful red-exit. The quick-menu exit button now sends WM_CLOSE to the game (and steam.exe in real-Steam mode), waits 5 s for clean save flush, then proceeds with the existing exit path. Second tap force-quits.
  • Per-container .arch and Wine prefix paths to stop one container's wine/proton variant from cascading into another's. Several SteamUtils paths complete that migration end-to-end.
  • Overlay disable now actually disables the overlay — adds WINEDLLOVERRIDES=gameoverlayrenderer=;gameoverlayrenderer64= so Wine refuses to load the overlay PE entirely (in addition to the existing Vulkan-layer skip and SteamNoOverlayUIDrawing=1). Real-Steam-only.
  • disableSteamOverlay per-container toggle in the General tab when real-Steam is on.
  • A few non-Steam-Client side fixes that came up while debugging the above and stayed: SysVSharedMemory.delete lock guard, X server BadIdChoice defensive null-check in DRI3, GLX leak chain skip in compositor, GLX-window OOM cap, recursive-delete fix in AdrenotoolsManager. The point of this branch is to reduce vague black screens when using Launch Steam Client; these are byproducts of that work.

Known issues / things to be aware of:

  • Some games need to be launched twice on a cold session. First boot black-screens, second boot works. Hollow Knight is the most fussy example — if you haven't played it since opening GameNative, expect to bounce once.
  • Some games are picky about container settings. Several titles work great with Launch Steam Client across a variety of presets; others will only boot under one specific combination of Proton variant, DXVK version, and box64 preset. Compatibility list / per-game recommended settings are something the community will need to flesh out.
  • Shiren the Wanderer 6 still does not work under Launch Steam Client in any configuration I've tried. I traced the failure to Wine's NTLM/Kerberos LSA packages being unavailable, which Steam's IPC handshake (steamclient64.dll → secur32 → sspicli) requires. BannerHub ships a full Samba + Kerberos stack to make this work; we don't, and adding the runtime files alone isn't enough — Wine itself needs to be built with --with-krb5 --with-samba. If anyone with proton/wine cross-build experience can take a swing at this, I'd be very grateful.
  • Most other titles I tested work cleanly with Launch Steam Client on. I didn't track an exhaustive list — please test things you care about and report back.
  • Please back up your saves before trying real-Steam mode. This stack touches cloud sync code paths and ACF/manifest writes; corruption is unlikely given the new sanity checks but the surface area is large enough that I want to be cautious.

I also added per-launch logging of steam.exe / steamclient.dll / steamclient64.dll size+mtime fingerprints so we can correlate "Steam updated under us" with launch failures.

Recording

https://photos.app.goo.gl/5XTwUjdaQ2YKa7556

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

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.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Reduces black screens and stalls in Steam launches by hardening cloud sync, SDK‑cloud bridging, overlay disabling, and shutdown flows. Extends the Cloud Save Bridge and graceful Steam shutdown to Bionic Steam, and makes SDK‑cloud mirroring recursive for nested saves.

  • Bug Fixes

    • Cloud sync: only auto-dismiss self phantoms; probe-before-dismiss to avoid ChangeNumber bumps; moved the offline gate below local prep; skip localhost AppLaunchIntent in real‑Steam; filter probed ops by machine name; cross‑device pending ops now surface to the conflict dialog; always rewrite remotecache.vdf (also on deletes); kick stale sessions on exit and before real‑Steam launches.
    • SDK cloud bridge: prompt shows only on game launches; skips during conflict flows; suppression only when persistence succeeds; guarded by a mutex and primed in the background; rejects empty Ludusavi parses and deletes poisoned empty disk caches before refetching; now available in Bionic Steam; mirroring is recursive to copy nested save dirs.
    • Overlay: in real‑Steam, WINEDLLOVERRIDES now blocks gameoverlayrenderer(64) at Wine’s loader; Vulkan layer disabled; per‑container toggle defaults to on.
    • DRI3/GL: fixed double-unmap by registering destroy listeners after pixmap creation; throw BadIdChoice on ID conflicts and unmap SHM on failures; skip orphaned GLX leak chains and cap leak windows; reaper predicate tightened to only reap 1×1 orphan‑chain parents.
    • Files/exec and process info: anchored glob matching to prevent false positives; WinHandler.exec(filename, parameters) now handles quoted paths; multi-listener support for process info with per-callback try/catch; snapshot collection serialized via mutex to prevent cross‑pollution; distinguish send failures from legitimate empty snapshots to avoid false “Steam exited”.
    • VcRedist: per‑year+arch sidecar markers replace the single container‑wide marker; skip already‑installed (year, arch) pairs; migrates legacy (year‑only) markers conservatively to x86; tests updated.
    • Manifests/login/defaults: shared‑manifest reuse now checks LastOwner; auto‑login ensures the Steam config dir exists; new containers inherit all PrefManager defaults.
    • Steam DLLs: treat missing pipe‑asset hashes and absent steam_api*.dll as verification failures to avoid silent desyncs.
    • XServer/exit: graceful Steam shutdown falls back to force‑quit on errors; env merge re‑pins WINEPREFIX after container vars; process‑info watchers no longer clobber each other; Bionic Steam now uses Steam’s graceful shutdown too; non‑Steam grace window now waits before kill.
  • Dependencies

    • .gitmodules: set ignore = dirty for submodule app/src/main/cpp/lsfg-vk-android to suppress false dirty state from quarantined test assets in transitive deps.

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

Summary by CodeRabbit

  • New Features

    • Disable Steam Overlay toggle in container settings (default: disabled)
    • SDK Cloud Save Bridge: detect/recommend subdir, confirm/persist flows, mirroring between SDK-cloud and game save dirs
    • Background preload of Ludusavi-based cloud-save recommendations
    • Two-tap graceful game exit with Steam shutdown handling
  • Improvements

    • More robust cloud sync/mirroring, conflict handling, and remote-cache updates
    • Better save-dir detection/validation UI and persistence
    • Reliability and launch/exit flow hardening

@TideGear TideGear requested a review from utkarshdalal as a code owner May 2, 2026 07:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 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

Adds per-container Steam settings and an SDK‑cloud bridge (Ludusavi recommendations + mirroring), introduces a real‑Steam launch mode that alters cloud/launch flows, hardens Steam DLL/ACF handling and AutoCloud remote‑cache behavior, updates UI/launch prompts, and applies multiple robustness fixes across file, process, and container handling.

Changes

Real‑Steam Mode, SDK‑Cloud Bridge & Cloud Sync

Layer / File(s) Summary
Data Shape / Config
app/src/main/java/com/winlator/container/Container.java, app/src/main/java/com/winlator/container/ContainerData.kt, app/src/main/java/app/gamenative/PrefManager.kt
Add container fields disableSteamOverlay: Boolean = true and sdkCloudSaveSubdir: String = ""; PrefManager exposes disableSteamOverlay.
Registry (external data)
app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt, app/src/main/java/app/gamenative/PluviaApp.kt
New LudusaviRegistry with disk TTL cache and streaming YAML upstream parsing; PluviaApp primes the cache on startup in a background coroutine.
Core Steam logic & APIs
app/src/main/java/app/gamenative/utils/SteamUtils.kt
Add install‑script shim, SHA‑256 DLL verification, per‑container .wine usage, ACF generation/validation, symlink‑safe cleanup, remotecache VDF writer, SDK‑cloud recommendation/detect/validate/mirroring APIs, and gated GSE↔Steam save migrations.
Auto‑Cloud / Sync logic
app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
Detect Auto‑Cloud patterns, rebase SteamUserData downloads into save targets via basename glob matching, skip broad SteamUserData recursive scans for Auto‑Cloud apps, unify byte‑identical checks and silent cache rehydrate, and consistently write remotecache.vdf.
Service wiring & mode flags
app/src/main/java/app/gamenative/service/SteamService.kt
Add isLaunchRealSteam: Boolean = false to beginLaunchApp, forceSyncUserFiles, closeApp; branch pending‑ops, launch‑intent, mirroring and cleanup behavior on that flag; add null‑guards releasing sync lock when context missing.
UI wiring & prompts
app/src/main/java/app/gamenative/ui/PluviaMain.kt, app/src/main/java/app/gamenative/ui/enums/DialogType.kt
Add SDK_CLOUD_BRIDGE_SUGGESTION dialog, preLaunchApp(skipBridgePrompt) gating, Ludusavi‑driven suggestion/detect/persist/relaunch flow, and pass isLaunchRealSteam into SteamService calls.
Container config UI/state
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt, .../ContainerConfigState.kt, .../BaseAppScreen.kt
ContainerConfigDialog/state accept optional appId: Int?; BaseAppScreen supplies appId only for Steam items.
SDK Cloud settings UI
app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
Add disableSteamOverlay toggle and SdkCloudSaveSubdirField with trimming, validation, detect/recommend flows, confirmation dialog for auto‑filled values, and immediate commit for manual edits.
Exit/Overlay & X environment
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Two‑tap graceful exit with real‑Steam shutdown probe/poll and escalation dialog; re‑pin WINEPREFIX after env overrides; fully disable Steam overlay via env/DLL overrides when configured; harden Steam extraction invalidation keyed by wineVersion
Callsite integration
app/src/main/java/app/gamenative/ui/model/MainViewModel.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Persist lastSteamMode and run real→emu cleanup; propagate isLaunchRealSteam into close/force‑sync calls and modernize callsite argument forms.
Strings & Box64 RC assets
app/src/main/res/values/strings.xml, app/src/main/assets/box86_64/*.box64rc
Add UI strings for graceful exit and SDK‑cloud bridge; adjust box64rc by removing -silent from steam WINEARGS and replace -cef-remote-fonts-cef-disable-remote-fonts in ultralight RC.
Persistence / default containers
app/src/main/java/app/gamenative/utils/ContainerUtils.kt
Default container data reads/writes disableSteamOverlay and sdkCloudSaveSubdir; new containers inherit these defaults and apply them when mapping to/from Container objects.

Infrastructure & Robustness

Layer / File(s) Summary
File utilities
app/src/main/java/app/gamenative/utils/FileUtils.kt
Add case‑insensitive tokenized glob matcher matchesGlob and refactor findFiles/findFilesRecursive to use it; reduce per‑file logging.
Preinstall & VC Redist
app/src/main/java/app/gamenative/utils/PreInstallSteps.kt, app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt, app/src/test/java/app/gamenative/utils/preInstallSteps/VcRedistStepTest.kt
Reset/persist VC‑redist markers at container root and game dir when variant changes; introduce per‑year/per‑arch marker sidecars, migration helpers, installer skipping logic, and expanded tests.
Wine exec timeout
app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
Execute guest commands via single‑thread Executor + CompletableFuture with 30s timeout; cancel on timeout, redact logs, and simplify token config decision.
File extraction cleanup
app/src/main/java/com/winlator/contents/AdrenotoolsManager.java
Use recursive FileUtils.delete(dst) when destination exists or on extraction failure to avoid partial leftover directories.
Window leak prevention & renderer tweaks
app/src/main/java/com/winlator/xserver/WindowManager.java, .../renderer/GLRenderer.java
Add reapLeakedClientWindows to destroy stale empty‑class client windows; renderer skips placeholder windows and makes fullscreen insertion explicit.
SHM & DRI3 safety
app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java, app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java
Null‑check drawable/pixmap creation and unregister on failure; avoid SHM unmap leaks; synchronize shared‑memory deletion to prevent races.
WinHandler exec normalization & multi-listener
app/src/main/java/com/winlator/winhandler/WinHandler.java
Add exec(filename, parameters) overload and parse/delegate from exec(command); support multiple process‑info listeners with add/remove APIs and per‑listener isolation; introduce processSnapshotMutex.
Installer readability
app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
Refactor preinstall guard into local variables for clarity while preserving behavior.
Git submodule tracking
.gitmodules
Set ignore = dirty for the app/src/main/cpp/lsfg-vk-android submodule.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PluviaMain
    participant Ludusavi
    participant SteamUtils
    participant SteamService
    participant SteamAutoCloud

    User->>PluviaMain: preLaunchApp(real‑Steam)
    PluviaMain->>SteamUtils: shouldSuggestSdkCloudBridge(appId)
    SteamUtils->>Ludusavi: lookup(appId)
    Ludusavi-->>SteamUtils: recommendation?
    SteamUtils-->>PluviaMain: recommendation
    alt recommended
        PluviaMain->>User: show SDK_CLOUD_BRIDGE_SUGGESTION
        User->>PluviaMain: confirm/dismiss
        PluviaMain->>Container: save sdkCloudSaveSubdir
    end
    PluviaMain->>SteamService: beginLaunchApp(isLaunchRealSteam=true)
    SteamService->>SteamAutoCloud: sync & detect auto‑cloud
    SteamAutoCloud->>SteamAutoCloud: rebase SteamUserData downloads (glob)
    SteamAutoCloud->>SteamUtils: write remotecache.vdf
    SteamService->>SteamUtils: mirrorSdkCloudRemoteToSave(appId)
    SteamUtils-->>SteamService: mirror complete
    SteamService-->>PluviaMain: launch complete
Loading
sequenceDiagram
    participant User
    participant XServerScreen
    participant Wine
    participant SteamService

    User->>XServerScreen: EXIT_GAME (first tap)
    XServerScreen->>Wine: steam.exe -shutdown
    XServerScreen->>XServerScreen: poll for steam.exe termination
    alt steam exits
        XServerScreen->>SteamService: closeApp(isLaunchRealSteam=true)
        SteamService->>XServerScreen: exit sync done
        XServerScreen->>User: exit
    else still running
        XServerScreen->>User: show "still shutting down" dialog
        alt user waits
            XServerScreen->>Wine: continue polling
        else user force quits
            XServerScreen->>Wine: kill steam.exe
            XServerScreen->>User: exit
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • utkarshdalal/GameNative#1228 — Cloud sync refactor for byte‑identical checks and silent cache rehydration; closely overlaps SteamAutoCloud changes.
  • utkarshdalal/GameNative#438 — Prior XServerScreen process‑watch/exit watcher work; related to the updated exit/watchdog logic.
  • utkarshdalal/GameNative#1143 — VC Redist test coverage and marker behavior; related to the expanded VcRedistStep tests and migration logic.

Suggested reviewers

  • utkarshdalal
  • phobos665

"I nibbled through the code and found,
Recommendations whispered, safe and sound.
Saves rebound, overlays tucked tight,
Steam sleeps, Ludusavi sheds its light.
Hop, sync, and sleep — a rabbit's delight."

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

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 35 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: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/app/gamenative/utils/FileUtils.kt (1)

136-176: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

matchesGlob is too permissive for exact and wildcarded filenames.

This implementation only checks that each *-separated token appears in order, so patterns like DXSETUP.exe, *.sav, and Capture*.sav can match unintended filenames such as fooDXSETUP.exe.bak or Capture1.sav.tmp. That means both findFiles(...) and findFilesRecursive(...) can return the wrong files.

♻️ Proposed fix
 fun matchesGlob(fileName: String, pattern: String): Boolean {
     if (pattern.isEmpty() || pattern == "*") return true
-    val patternParts = pattern.split("*").filter { it.isNotEmpty() }
-    var startIndex = 0
-    for (part in patternParts) {
-        val index = fileName.indexOf(part, startIndex, ignoreCase = true)
-        if (index < 0) return false
-        startIndex = index + part.length
-    }
-    return true
+    val regex = buildString {
+        append('^')
+        pattern.split("*").forEachIndexed { idx, part ->
+            if (idx > 0) append(".*")
+            append(Regex.escape(part))
+        }
+        append('$')
+    }
+    return Regex(regex, RegexOption.IGNORE_CASE).matches(fileName)
 }
🤖 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/FileUtils.kt` around lines 136 - 176,
matchesGlob is too permissive because it only ensures tokens appear in order;
update matchesGlob to enforce anchors: if pattern has no leading '*', require
the first token to match at fileName start; if pattern has no trailing '*',
require the last token to align with fileName end; keep the existing
ordered-internal-token checks (with ignoreCase) and preserve the special-case
for empty or "*" and exact-match when pattern contains no '*'. Modify the
matchesGlob function (used by findFiles and findFilesRecursive) to: parse tokens
via split("*"), detect hasLeadingStar and hasTrailingStar, if no tokens return
true, check prefix match when !hasLeadingStar by verifying
fileName.startsWith(firstToken, ignoreCase=true), check suffix match when
!hasTrailingStar by verifying fileName.endsWith(lastToken, ignoreCase=true), and
still scan intermediate tokens in order updating startIndex after each match;
this prevents matching names like "fooDXSETUP.exe.bak" for "DXSETUP.exe" and
similar false positives.
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

5110-5120: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Steam extraction still writes via shared ImageFs root

Line 5118 still extracts steam.tzst into imageFs.getRootDir(). That path is tied to shared/global layout, while adjacent changes explicitly avoid global symlink-root writes for container isolation. This mismatch can still allow cross-container contamination under concurrent container activation.

Please route Steam archive extraction through a container-scoped destination/path mapping so both existence check and write target are consistently per-container.

🤖 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/xserver/XServerScreen.kt` around
lines 5110 - 5120, The extraction currently writes steam.tzst into the shared
imageFs.getRootDir(), risking cross-container writes; change the extraction
destination to a container-scoped path (same scope used in the existence check)
by computing the target root off container.rootDir (or a container-specific
subdir) and pass that path to TarCompressorUtils.extract; ensure the File
downloaded variable and the TarCompressorUtils.extract call both use the
container-scoped destination instead of imageFs.getRootDir() so existence check
and write target match.
🧹 Nitpick comments (1)
app/src/main/java/com/winlator/container/Container.java (1)

342-344: ⚡ Quick win

Validate sdkCloudSaveSubdir against path separators/traversal.

Given this value is used as a relative subdir contract, normalizing to a single safe path component in the setter will prevent invalid values from persisted config edits.

Suggested hardening
     public void setSdkCloudSaveSubdir(String sdkCloudSaveSubdir) {
-        this.sdkCloudSaveSubdir = sdkCloudSaveSubdir == null ? "" : sdkCloudSaveSubdir;
+        String value = sdkCloudSaveSubdir == null ? "" : sdkCloudSaveSubdir.trim();
+        if (value.contains("/") || value.contains("\\") || value.contains("..")) {
+            value = "";
+        }
+        this.sdkCloudSaveSubdir = value;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/winlator/container/Container.java` around lines 342 -
344, The setter Container.setSdkCloudSaveSubdir currently accepts any string;
instead validate and normalize sdkCloudSaveSubdir to a single safe path
component: trim input, return empty string for null/empty, reject or sanitize
any values containing path separators or traversal tokens (e.g., '/', '\', or
".."), and enforce a whitelist pattern (e.g., allow only letters, digits,
hyphen, underscore); set this.sdkCloudSaveSubdir to the sanitized value and if
invalid either set to empty string or throw an IllegalArgumentException
depending on project policy—update Container.setSdkCloudSaveSubdir and document
the constraint near the sdkCloudSaveSubdir field.
🤖 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/service/SteamAutoCloud.kt`:
- Around line 249-256: The rebase lambda (rebaseToAutoCloud) moves downloaded
files under an Auto-Cloud root but downstream hash validation still looks up
entries by the original cloud prefix from AppFileChangeList, so
hasHashConflicts(...) can miss mismatches; fix by resolving the
absolute/normalized path with getFullFilePath(...) for both the local downloaded
file and the corresponding AppFileChangeList entry before any hash lookup or
comparison (i.e., compute resolvedPath = getFullFilePath(prefix, filename) or
otherwise map the AppFileChangeList key to the rebase result and use that
resolved path when calling hasHashConflicts/getHash/etc.), and apply the
identical change to the duplicate logic later in the file (the block analogous
to lines 284-293) so both sides of the hash check use the same resolved path
key.
- Around line 221-247: The writeRemoteCache lambda currently skips writing when
entries.isEmpty(), leaving stale remotecache.vdf; update the lambda
(writeRemoteCache) to handle the zero-entry case by either deleting the existing
remotecache.vdf in the Steam user data directory (constructed from
steamUserDataBase and appInfo.id) or by invoking SteamUtils.writeRemoteCacheVdf
with an explicit empty files list/appropriate changeNumber to overwrite the
file; ensure the removal/overwrite happens only for SteamUserData-rooted entries
and keep the existing exception handling (the SteamUtils.writeRemoteCacheVdf
call and steamUserDataBase/appInfo.id symbols are the targets to modify).

In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Line 2278: The call using the non-null assertion
SteamUtils.migrateGSESavesToSteamUserdata(instance?.applicationContext!!, appId,
...) can throw before the try/finally that calls releaseSync(appId), risking a
stuck sync flag; in both beginLaunchApp(...) and forceSyncUserFiles(...) remove
the !! and either (a) obtain and null-check applicationContext before acquiring
the sync (e.g., instance?.applicationContext?.let { ctx -> ... } and return/log
if null), or (b) move the migrateGSESavesToSteamUserdata call inside the try
block after acquireSync so that any exception still goes through the finally
that calls releaseSync(appId); ensure all early-return/error paths always call
releaseSync(appId) (or acquire only after validating context) and replace the !!
with safe null handling (let/if-null-return) to avoid leaking locks.
- Around line 2421-2425: The localhost filter is applied under the wrong
condition: change the pendingRemoteOperations assignment so rawPending.filterNot
{ it.machineName.equals("localhost", ignoreCase = true) } runs when
emulation/launch intent RPC is used (i.e., when isLaunchRealSteam is false) and
leave pendingRemoteOperations as rawPending when isLaunchRealSteam is true;
update the conditional around pendingRemoteOperations (the isLaunchRealSteam
check) to invert the boolean or swap branches accordingly.
- Around line 2314-2333: The code currently dismisses all pending ops whenever
probed.isNotEmpty(), which can remove legitimate cross-device conflicts; update
this to only auto-dismiss operations that are phantoms/local to this host:
compute a filtered list (e.g., val phantomOps = probed.filter { it.machineName
== ourMachineName || it.clientId == proactiveClientId || it.operation.isPhantom
/*or other local/phantom predicate*/ } ), and only call
steamCloud.signalAppLaunchIntent(..., ignorePendingOperations = true) and
steamCloud.signalAppExitSyncDone(...) when phantomOps.isNotEmpty(); if probed
contains non-phantom ops (probed.any { !isPhantomPredicate(it) }), do not set
ignorePendingOperations=true (or skip auto-dismiss) so real cross-device
conflicts are preserved; update references: probed, ourMachineName,
proactiveClientId, steamCloud.signalAppLaunchIntent and
steamCloud.signalAppExitSyncDone accordingly.

In `@app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt`:
- Around line 497-520: The async recommend/detect flow uses the snapshot
variable current (captured at click time) to decide whether to prompt/apply;
after the suspend call to SteamUtils.getRecommendedSdkCloudSaveSubdirAsync in
the TextButton handler (and the similar handler around lines 533-549), re-read
the latest value from state.config.value.sdkCloudSaveSubdir into a local
variable and use that up-to-date value instead of the earlier current before
setting pendingValue/showConfirmDialog or applying the subdir; update both
handlers (the coroutine launched via scope.launch that sets
recommendLoading/detectMessage and checks current) to fetch the fresh
state.config.value.sdkCloudSaveSubdir after the suspend and base the
prompt/apply decision on that.

In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Around line 793-845: The relaunch lambda discards original launch options
(temporary container overrides and bootToContainer) by calling preLaunchApp with
only appId and isOffline, causing a different target to be launched after the
dialog; update the code so relaunch captures and forwards the original launch
parameters used for the interrupted launch (e.g., the full LaunchOptions/intent
parameters or the same arguments that started the first preLaunchApp), not just
state.launchedAppId and viewModel.isOffline.value — specifically change relaunch
to accept or close over the original options and call preLaunchApp with those
same values (preserving skipBridgePrompt, bootToContainer/temporary container
override, setLoadingDialogVisible/Progress/Message, setMessageDialogState,
onSuccess = viewModel::launchApp, etc.) so the second attempt uses identical
launch data as the initial attempt.

In `@app/src/main/java/app/gamenative/utils/ContainerUtils.kt`:
- Line 103: createNewContainer() currently constructs a ContainerData manually
and omits the new default fields (e.g., disableSteamOverlay), causing fresh
containers to miss those defaults; update createNewContainer() to populate
disableSteamOverlay from PrefManager.disableSteamOverlay (or better, call the
shared default->ContainerData conversion routine used elsewhere) so the
fresh-container path uses the same defaults as the rest of the conversion path
and includes the new overlay/cloud-save defaults.

In `@app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt`:
- Around line 67-68: The timeout logging and exception in SteamTokenLogin
currently print the full variable command (which may include raw refresh
tokens); change both the Timber.tag("SteamTokenLogin").e(...) call and the
IllegalStateException(...) to avoid leaking the command: log a static operation
label or only the executable name (e.g., "wine exec" or derive basename from
command) and include WINE_EXEC_TIMEOUT_SECONDS but not the full command string,
and throw the IllegalStateException with a redacted message (e.g., "wine exec
timed out" or include "[REDACTED_COMMAND]") while preserving the original caught
exception e as the cause so stack traces remain available; update references to
command, Timber.tag, WINE_EXEC_TIMEOUT_SECONDS, and the IllegalStateException in
the SteamTokenLogin code accordingly.
- Around line 60-67: The timeout logic in SteamTokenLogin (where
CompletableFuture.supplyAsync is used and future.cancel(true) is called) does
not stop the launched native process because
GlibcProgramLauncherComponent.execShellCommand and
BionicProgramLauncherComponent.execShellCommand block on process.waitFor() and
stream reads; update those execShellCommand implementations to be
interrupt-aware and to forcibly destroy the child process on cancellation (or
refactor SteamTokenLogin to use the project’s timeout-aware pattern such as
withTimeout): ensure execShellCommand tracks the Process returned by
Runtime.getRuntime().exec(), closes streams, checks
Thread.currentThread().isInterrupted() during I/O, kills/destroys the Process if
interrupted or timeout elapses (and propagates an exception), and make
SteamTokenLogin use WINE_EXEC_TIMEOUT_SECONDS with that timeout-aware call
instead of relying on future.cancel(true).

In `@app/src/main/java/app/gamenative/utils/SteamUtils.kt`:
- Around line 2569-2583: The current mirroring loop in SteamUtils.kt iterates
remoteDir.listFiles() and skips non-files, which drops nested directories (see
the remoteDir -> gameSaveDir copy loop using Files.copy and Timber logging and
the similar block around the mirror back at 2593-2610). Replace the flat loop
with a recursive copy (e.g., implement a helper like copyRecursively(srcRoot:
File, dstRoot: File) or use Files.walkFileTree) that visits directories and
files, creates destination directories as needed, and calls Files.copy for files
with StandardCopyOption.REPLACE_EXISTING and COPY_ATTRIBUTES, preserving
relative paths; update Timber logs to report each copied file path and keep the
existing catch/Timber.w behavior for exceptions.
- Around line 249-264: The verifyRestoredState function can incorrectly return
true when neither steam_api.dll nor steam_api64.dll are present; modify
verifyRestoredState to track whether any target Steam API DLLs were encountered
(e.g., a boolean foundTargetDll), set it to true inside the loop when n ==
"steam_api.dll" or "steam_api64.dll" is processed, and after the walkTopDown
loop return false if foundTargetDll is false (indicating missing DLLs despite
the RESTORED marker) before returning true; reference the existing function name
verifyRestoredState, the loop over Paths.get(appDirPath).toFile().walkTopDown(),
and the filename checks for "steam_api.dll"/"steam_api64.dll" when adding the
flag and final check.
- Around line 1330-1334: The createSteamCommonLink function currently uses
link.exists() which follows symlinks and misses broken or incorrect links;
change the logic to check for existence without following links using
Files.exists(link.toPath(), LinkOption.NOFOLLOW_LINKS), and if that returns true
use Files.readSymbolicLink(link.toPath()) to compare the existing symlink target
to target.toPath(); if they match, return, otherwise remove the existing link
entry with deleteTreeNoFollowSymlinks(link) before calling
Files.createSymbolicLink(link.toPath(), target.toPath()); ensure parent dirs are
created as before and handle any exceptions from readSymbolicLink/delete
appropriately.

In `@app/src/main/java/com/winlator/contents/AdrenotoolsManager.java`:
- Around line 136-145: Check the results of FileUtils.delete(dst) and
dst.mkdirs() in AdrenotoolsManager before calling TarCompressorUtils.extract: if
FileUtils.delete(dst) returns false when dst.exists() or dst.mkdirs() returns
false, log an error (use Log.e) and set hasExtracted=false (or return/abort the
extraction) so TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD,
mContext, src, dst) is not invoked on a failed/dirty target; ensure the existing
cleanup branch still deletes dst when hasExtracted is false.

In `@app/src/main/java/com/winlator/core/WineUtils.java`:
- Around line 199-201: The child path strings use a leading slash which causes
new File(imageFsRoot, "/opt/...") to ignore the parent; update the path
arguments in WineUtils so wineSystem32Dir and wineSysWoW64Dir are constructed
with relative child paths (e.g., "opt/wine/lib/wine/x86_64-windows" and
"opt/wine/lib/wine/i386-windows") instead of starting with "/" so the
directories resolve under ImageFs.find(context).getRootDir(); also scan and fix
the similar occurrences used by the DLL copy operations referenced around the
copy calls (the code that reads wineSystem32Dir/wineSysWoW64Dir) and other
ImageFs usages throughout the codebase.

In `@app/src/main/java/com/winlator/renderer/GLRenderer.java`:
- Around line 361-366: The current early "return" in GLRenderer's window-check
(the block using window.getClassName(), window.getProcessId(), and parent vs
xServer.windowManager.rootWindow) stops recursion into child windows and drops
real renderable descendants; change the logic to skip processing/adding the
current phantom window but do not return from the traversal—allow recursion to
continue into its children (i.e., remove the return and ensure the code still
recurses or continues the loop to visit window.getChildren() or the existing
recursive call that collects renderable windows).

In `@app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java`:
- Around line 83-90: The code holds the shmemories monitor while calling native
XConnectorEpoll.closeFd, which can deadlock; fix by retrieving the SHMemory and
its fd inside the synchronized(shmemories) block, set the SHMemory fd to -1 via
SHMemory.access$002(shmemory, -1) and remove the entry
(shmemories.remove(shmid)) while still synchronized, then release the lock and
call XConnectorEpoll.closeFd(fd) outside the synchronized block; use the local
fd variable (from SHMemory.access$000(shmemory)) so you don’t call the native
method while holding the shmemories monitor.

In `@app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java`:
- Around line 218-222: The code eagerly calls imageFs.getVersion() and
imageFs.getVariant() before checking imageFs.isValid(), which can trigger
failures for invalid state and also uses a non-null-safe String comparison;
change the logic in ImageFsInstaller so you first evaluate boolean valid =
imageFs.isValid() and short-circuit if false (avoid calling
getVersion/getVariant), then only call imageFs.getVersion() and
imageFs.getVariant() when valid is true, compare version against LATEST_VERSION,
and use a null-safe comparison (e.g., Objects.equals(currentVariant,
requestedVariant) or similar) for currentVariant vs
container.getContainerVariant().

In `@app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java`:
- Around line 161-164: createPixmap() can return null after a drawable was
already created, leaving a leaked/stale drawable when you throw BadIdChoice;
update both code paths (the block using
client.xServer.drawableManager.createDrawable(...) and the similar block at
lines 177–182) so that if client.xServer.pixmapManager.createPixmap(drawable)
returns null you first remove/unregister/destroy the previously created drawable
(e.g. call the drawable manager's removal method or drawable.destroy() —
reference the created Drawable from drawable variable) and only then throw new
BadIdChoice(pixmapId); ensure you use the same drawable manager API used
elsewhere in the class to unregister the drawable.

In `@app/src/main/java/com/winlator/xserver/WindowManager.java`:
- Around line 205-220: The leak reaper is currently collecting candidate windows
into matches without excluding mapped (active) windows, so destroyWindow(...)
can remove live surfaces; update the candidate-filtering logic (the loop that
checks cand.originClient, cand.isInputOutput(), cand.getClassName(),
cand.getWidth()/getHeight()) to also skip mapped windows by adding a
mapped-state guard such as if (cand.isMapped()) continue (or the inverse if your
API is isUnmapped/isVisible), ensuring only unmapped/inactive Window instances
are added to matches before sorting and reaping (symbols: matches,
LEAK_CLIENT_CAP, Window, cand, destroyWindow).

---

Outside diff comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 5110-5120: The extraction currently writes steam.tzst into the
shared imageFs.getRootDir(), risking cross-container writes; change the
extraction destination to a container-scoped path (same scope used in the
existence check) by computing the target root off container.rootDir (or a
container-specific subdir) and pass that path to TarCompressorUtils.extract;
ensure the File downloaded variable and the TarCompressorUtils.extract call both
use the container-scoped destination instead of imageFs.getRootDir() so
existence check and write target match.

In `@app/src/main/java/app/gamenative/utils/FileUtils.kt`:
- Around line 136-176: matchesGlob is too permissive because it only ensures
tokens appear in order; update matchesGlob to enforce anchors: if pattern has no
leading '*', require the first token to match at fileName start; if pattern has
no trailing '*', require the last token to align with fileName end; keep the
existing ordered-internal-token checks (with ignoreCase) and preserve the
special-case for empty or "*" and exact-match when pattern contains no '*'.
Modify the matchesGlob function (used by findFiles and findFilesRecursive) to:
parse tokens via split("*"), detect hasLeadingStar and hasTrailingStar, if no
tokens return true, check prefix match when !hasLeadingStar by verifying
fileName.startsWith(firstToken, ignoreCase=true), check suffix match when
!hasTrailingStar by verifying fileName.endsWith(lastToken, ignoreCase=true), and
still scan intermediate tokens in order updating startIndex after each match;
this prevents matching names like "fooDXSETUP.exe.bak" for "DXSETUP.exe" and
similar false positives.

---

Nitpick comments:
In `@app/src/main/java/com/winlator/container/Container.java`:
- Around line 342-344: The setter Container.setSdkCloudSaveSubdir currently
accepts any string; instead validate and normalize sdkCloudSaveSubdir to a
single safe path component: trim input, return empty string for null/empty,
reject or sanitize any values containing path separators or traversal tokens
(e.g., '/', '\', or ".."), and enforce a whitelist pattern (e.g., allow only
letters, digits, hyphen, underscore); set this.sdkCloudSaveSubdir to the
sanitized value and if invalid either set to empty string or throw an
IllegalArgumentException depending on project policy—update
Container.setSdkCloudSaveSubdir and document the constraint near the
sdkCloudSaveSubdir field.
🪄 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: 59159aa4-d344-48b3-923b-81d424d1aeea

📥 Commits

Reviewing files that changed from the base of the PR and between 9973dd7 and 4113357.

📒 Files selected for processing (35)
  • app/src/main/assets/box86_64/lightsteam.box64rc
  • app/src/main/assets/box86_64/ultralightsteam.box64rc
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/gamefixes/types/RegistryKeyFix.kt
  • app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/app/gamenative/ui/enums/DialogType.kt
  • app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/app/gamenative/utils/FileUtils.kt
  • app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
  • app/src/main/java/app/gamenative/utils/PreInstallSteps.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/contents/AdrenotoolsManager.java
  • app/src/main/java/com/winlator/core/WineThemeManager.java
  • app/src/main/java/com/winlator/core/WineUtils.java
  • app/src/main/java/com/winlator/renderer/GLRenderer.java
  • app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
  • app/src/main/java/com/winlator/xserver/WindowManager.java
  • app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java
  • app/src/main/res/values/strings.xml

Comment thread app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
Comment thread app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt Outdated
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment on lines +361 to +366
if (window.getClassName().isEmpty() && window.getProcessId() == 0) {
Window parent = window.getParent();
if (parent != null && parent != xServer.windowManager.rootWindow
&& parent.getClassName().isEmpty() && parent.getProcessId() == 0) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid returning before visiting descendants of skipped phantom windows.

At Line 365, return prevents recursion into children. If a real renderable child exists under this placeholder chain, it will never be collected (black-screen risk). Skip only the current window, but keep traversing descendants.

Suggested fix
-            if (window.getClassName().isEmpty() && window.getProcessId() == 0) {
+            boolean skipSelf = false;
+            if (window.getClassName().isEmpty() && window.getProcessId() == 0) {
                 Window parent = window.getParent();
                 if (parent != null && parent != xServer.windowManager.rootWindow
                         && parent.getClassName().isEmpty() && parent.getProcessId() == 0) {
-                    return;
+                    skipSelf = true;
                 }
             }
             boolean viewable = true;
@@
-            if (viewable) {
+            if (viewable && !skipSelf) {
                 if (forceFullscreenWMClass != null) {
                     ...
                 } else {
                     renderableWindows.add(new RenderableWindow(window.getContent(), x, y));
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/winlator/renderer/GLRenderer.java` around lines 361 -
366, The current early "return" in GLRenderer's window-check (the block using
window.getClassName(), window.getProcessId(), and parent vs
xServer.windowManager.rootWindow) stops recursion into child windows and drops
real renderable descendants; change the logic to skip processing/adding the
current phantom window but do not return from the traversal—allow recursion to
continue into its children (i.e., remove the return and ensure the code still
recurses or continues the loop to visit window.getChildren() or the existing
recursive call that collects renderable windows).

Comment on lines +83 to +90
synchronized (shmemories) {
SHMemory shmemory = shmemories.get(shmid);
if (shmemory != null) {
if (SHMemory.access$000(shmemory) != -1) {
XConnectorEpoll.closeFd(SHMemory.access$000(shmemory));
SHMemory.access$002(shmemory, -1);
}
shmemories.remove(shmid);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the SysVSharedMemory.java file
find . -type f -name "SysVSharedMemory.java" | head -5

Repository: utkarshdalal/GameNative

Length of output: 130


🏁 Script executed:

# Also look for XConnectorEpoll class
find . -type f -name "XConnectorEpoll.java" | head -5

Repository: utkarshdalal/GameNative

Length of output: 132


🏁 Script executed:

# And find SHMemory class
find . -type f -name "SHMemory.java" | head -5

Repository: utkarshdalal/GameNative

Length of output: 49


🏁 Script executed:

# Examine XConnectorEpoll.java to check closeFd method
cat -n ./app/src/main/java/com/winlator/xconnector/XConnectorEpoll.java | head -100

Repository: utkarshdalal/GameNative

Length of output: 4536


🏁 Script executed:

# Examine SysVSharedMemory.java - full file to understand structure
wc -l ./app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java

Repository: utkarshdalal/GameNative

Length of output: 134


🏁 Script executed:

# Examine SysVSharedMemory.java - read full content
cat -n ./app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java

Repository: utkarshdalal/GameNative

Length of output: 6087


🏁 Script executed:

# Search for SHMemory class to understand access$000 and access$002
fd -t f "SHMemory" | head -10

Repository: utkarshdalal/GameNative

Length of output: 49


Avoid holding shmemories while calling the native close.

XConnectorEpoll.closeFd() is a native JNI call. Keeping the monitor held blocks concurrent get(), attach(), and detach() operations, and risks deadlock if the native implementation re-enters shared-memory code.

Suggested fix
 public void delete(int shmid) {
-    synchronized (shmemories) {
-        SHMemory shmemory = shmemories.get(shmid);
-        if (shmemory != null) {
-            if (SHMemory.access$000(shmemory) != -1) {
-                XConnectorEpoll.closeFd(SHMemory.access$000(shmemory));
-                SHMemory.access$002(shmemory, -1);
-            }
-            shmemories.remove(shmid);
-        }
-    }
+    int fd = -1;
+    synchronized (shmemories) {
+        SHMemory shmemory = shmemories.get(shmid);
+        if (shmemory == null) return;
+        fd = SHMemory.access$000(shmemory);
+        SHMemory.access$002(shmemory, -1);
+        shmemories.remove(shmid);
+    }
+    if (fd != -1) {
+        XConnectorEpoll.closeFd(fd);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java` around lines 83
- 90, The code holds the shmemories monitor while calling native
XConnectorEpoll.closeFd, which can deadlock; fix by retrieving the SHMemory and
its fd inside the synchronized(shmemories) block, set the SHMemory fd to -1 via
SHMemory.access$002(shmemory, -1) and remove the entry
(shmemories.remove(shmid)) while still synchronized, then release the lock and
call XConnectorEpoll.closeFd(fd) outside the synchronized block; use the local
fd variable (from SHMemory.access$000(shmemory)) so you don’t call the native
method while holding the shmemories monitor.

Comment on lines +218 to +222
boolean valid = imageFs.isValid();
int version = imageFs.getVersion();
String currentVariant = imageFs.getVariant();
String requestedVariant = container.getContainerVariant();
if (!valid || version < LATEST_VERSION || !currentVariant.equals(requestedVariant)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid eager metadata reads before the validity gate.

On Line 219–222, getVersion()/getVariant() are now executed even when isValid() is false, which changes short-circuit behavior and can fail on invalid state. Also, Line 222 should use a null-safe variant comparison.

Suggested patch
-        boolean valid = imageFs.isValid();
-        int version = imageFs.getVersion();
-        String currentVariant = imageFs.getVariant();
         String requestedVariant = container.getContainerVariant();
-        if (!valid || version < LATEST_VERSION || !currentVariant.equals(requestedVariant)) {
+        boolean valid = imageFs.isValid();
+        int version = valid ? imageFs.getVersion() : -1;
+        String currentVariant = valid ? imageFs.getVariant() : null;
+        if (!valid || version < LATEST_VERSION || !java.util.Objects.equals(currentVariant, requestedVariant)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java` around
lines 218 - 222, The code eagerly calls imageFs.getVersion() and
imageFs.getVariant() before checking imageFs.isValid(), which can trigger
failures for invalid state and also uses a non-null-safe String comparison;
change the logic in ImageFsInstaller so you first evaluate boolean valid =
imageFs.isValid() and short-circuit if false (avoid calling
getVersion/getVariant), then only call imageFs.getVersion() and
imageFs.getVariant() when valid is true, compare version against LATEST_VERSION,
and use a null-safe comparison (e.g., Objects.equals(currentVariant,
requestedVariant) or similar) for currentVariant vs
container.getContainerVariant().

Comment thread app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java Outdated
Comment thread app/src/main/java/com/winlator/xserver/WindowManager.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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/PluviaMain.kt (1)

811-824: 💤 Low value

Prefer structured coroutine scope over standalone CoroutineScope.

CoroutineScope(Dispatchers.IO).launch creates an unstructured coroutine that won't be cancelled if the composable leaves composition (e.g., activity destroyed). The APP_UPDATE handler at lines 1092-1117 uses scope.launch with the existing rememberCoroutineScope() — use that pattern here for consistency and lifecycle safety.

Suggested fix for onConfirmClick
         onConfirmClick = {
             // Enable: write Ludusavi's suggested subdir, then continue the launch.
             msgDialogState = MessageDialogState(false)
-            CoroutineScope(Dispatchers.IO).launch {
+            scope.launch {
                 val gameId = ContainerUtils.extractGameIdFromContainerId(state.launchedAppId)
-                val rec = runCatching {
-                    SteamUtils.getRecommendedSdkCloudSaveSubdirAsync(context, gameId)
-                }.getOrNull()
+                val rec = withContext(Dispatchers.IO) {
+                    runCatching {
+                        SteamUtils.getRecommendedSdkCloudSaveSubdirAsync(context, gameId)
+                    }.getOrNull()
+                }
                 if (rec != null) {
-                    runCatching {
-                        val container = ContainerUtils.getContainer(context, state.launchedAppId)
-                        container.sdkCloudSaveSubdir = rec.subdir
-                        container.saveData()
-                    }.onFailure { Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") }
+                    withContext(Dispatchers.IO) {
+                        runCatching {
+                            val container = ContainerUtils.getContainer(context, state.launchedAppId)
+                            container.sdkCloudSaveSubdir = rec.subdir
+                            container.saveData()
+                        }.onFailure { Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") }
+                    }
                 }
-                withContext(Dispatchers.Main) { relaunch() }
+                relaunch()
             }
         }

Also applies to: 834-841

🤖 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 811 - 824,
The coroutine launched with CoroutineScope(Dispatchers.IO).launch is
unstructured and should be replaced with the composable's remembered scope to
tie it to lifecycle; change to using the existing rememberCoroutineScope (e.g.
scope.launch { withContext(Dispatchers.IO) { ... } } ) when calling
ContainerUtils.extractGameIdFromContainerId(state.launchedAppId) and
SteamUtils.getRecommendedSdkCloudSaveSubdirAsync(context, gameId), then persist
via ContainerUtils.getContainer(...).saveData() on the IO dispatcher and finally
call relaunch() on the Main dispatcher (or call relaunch() after the IO
withContext block) so the coroutine is cancelled when the composable/activity is
destroyed; apply the same replacement for the similar block around lines
834-841.
🤖 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/xserver/XServerScreen.kt`:
- Around line 5148-5150: extractSteamFiles() currently checks per-container
install presence using File(container.rootDir, ".wine/drive_c/Program Files
(x86)/Steam/steam.exe") but then extracts steam.tzst into imageFs.getRootDir()
(the shared imagefs root), allowing another container to flip the shared layout
during extraction; change the extraction target to a container-owned path (use
container.rootDir or a container-scoped ImageFs API) so all writes go under the
container prefix (or, if feasible, repack steam.tzst to be unpackable without
depending on the global root) and update any references in extractSteamFiles()
that call imageFs.getRootDir() to use the container-specific root instead.
- Around line 359-381: The loop in awaitSteamShutdown currently calls
requestWineProcessSnapshot(winHandler) repeatedly which swaps the shared
OnGetProcessInfoListener and can be clobbered by
startExitWatchForUnmappedGameWindow and the quick-menu flows; instead obtain an
immutable process snapshot per poll and use that for isSteamExeAlive checks.
Modify the code so awaitSteamShutdown calls a new non-listener API (e.g., add a
WinHandler method like requestProcessSnapshotSync or getProcessSnapshot) that
returns a serialized snapshot object, or change requestWineProcessSnapshot to
return a snapshot without swapping the global listener, then use that snapshot
inside the polling loop (keep references to awaitSteamShutdown,
requestWineProcessSnapshot, startExitWatchForUnmappedGameWindow, and
OnGetProcessInfoListener when updating callers). Ensure no listener swapping
occurs from awaitSteamShutdown so other flows aren’t clobbered.
- Around line 3137-3143: The per-container WINEPREFIX set with
envVars.put("WINEPREFIX", File(container.rootDir, ".wine").absolutePath) is
being overwritten by envVars.putAll(container.envVars); move or reapply that
WINEPREFIX assignment after the merge (i.e., call the WINEPREFIX envVars.put
after envVars.putAll(container.envVars)) so the per-container prefix always
wins, or alternatively remove WINEPREFIX from container.envVars before putAll
and then set it; update references around the envVars.putAll and the WINEPREFIX
put to ensure the per-container path is final.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Around line 811-824: The coroutine launched with
CoroutineScope(Dispatchers.IO).launch is unstructured and should be replaced
with the composable's remembered scope to tie it to lifecycle; change to using
the existing rememberCoroutineScope (e.g. scope.launch {
withContext(Dispatchers.IO) { ... } } ) when calling
ContainerUtils.extractGameIdFromContainerId(state.launchedAppId) and
SteamUtils.getRecommendedSdkCloudSaveSubdirAsync(context, gameId), then persist
via ContainerUtils.getContainer(...).saveData() on the IO dispatcher and finally
call relaunch() on the Main dispatcher (or call relaunch() after the IO
withContext block) so the coroutine is cancelled when the composable/activity is
destroyed; apply the same replacement for the similar block around lines
834-841.
🪄 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: 2c08ef91-4f82-44bd-b2d9-5ae99d94139d

📥 Commits

Reviewing files that changed from the base of the PR and between 4113357 and f3781a1.

📒 Files selected for processing (8)
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
  • app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (4)
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt
  • app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/com/winlator/container/ContainerData.kt

Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.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.

10 issues found across 35 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/utils/preInstallSteps/VcRedistStep.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt:60">
P1: Short-circuiting on a container-wide VCREDIST marker can skip redistributable installs needed by later games in the same prefix.</violation>
</file>

<file name="app/src/main/java/com/winlator/contents/AdrenotoolsManager.java">

<violation number="1" location="app/src/main/java/com/winlator/contents/AdrenotoolsManager.java:137">
P2: Deleting the existing driver directory before extraction can erase a working driver if extraction fails, with no rollback to the previous contents.</violation>
</file>

<file name="app/src/main/java/com/winlator/xserver/WindowManager.java">

<violation number="1" location="app/src/main/java/com/winlator/xserver/WindowManager.java:187">
P1: Leak-reaping can destroy legitimate same-client windows (including ancestors), corrupting the window tree and leaving createWindow() to return a destroyed window.</violation>
</file>

<file name="app/src/main/java/com/winlator/core/WineUtils.java">

<violation number="1" location="app/src/main/java/com/winlator/core/WineUtils.java:200">
P1: Absolute child paths make `imageFsRoot` ineffective, so Wine DLLs are copied from `/opt/...` on the host instead of the imagefs root.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt:66">
P2: Timeout cancellation does not actually stop the underlying Wine subprocess, so hung commands can keep running after the timeout.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:395">
P1: SteamUserData files are skipped for mixed-root Auto-Cloud apps, so they never enter the local scan map and can fall out of sync/cache handling.</violation>
</file>

<file name="app/src/main/java/com/winlator/core/WineThemeManager.java">

<violation number="1" location="app/src/main/java/com/winlator/core/WineThemeManager.java:47">
P2: Per-container migration is incomplete: `user.reg` is moved to the container root, but wallpaper generation still uses the shared `ImageFs` root, which can desynchronize the registry and wallpaper asset paths across containers.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/SteamUtils.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/SteamUtils.kt:2584">
P2: SDK cloud mirroring is non-recursive, so nested save directories are never synchronized.</violation>
</file>

<file name="app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java">

<violation number="1" location="app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java:162">
P2: The new `drawable == null` early-throw path skips SHM unmapping, leaking the mapped buffer when drawable creation fails.</violation>
</file>

<file name="app/src/main/java/com/winlator/renderer/GLRenderer.java">

<violation number="1" location="app/src/main/java/com/winlator/renderer/GLRenderer.java:365">
P2: Anonymous-window filtering returns before child traversal, so valid descendant windows under matching wrappers are skipped from rendering.</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/app/gamenative/utils/preInstallSteps/VcRedistStep.kt Outdated
Comment thread app/src/main/java/com/winlator/xserver/WindowManager.java
Comment on lines +200 to +201
File wineSystem32Dir = new File(imageFsRoot, "/opt/wine/lib/wine/x86_64-windows");
File wineSysWoW64Dir = new File(imageFsRoot, "/opt/wine/lib/wine/i386-windows");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Absolute child paths make imageFsRoot ineffective, so Wine DLLs are copied from /opt/... on the host instead of the imagefs root.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/com/winlator/core/WineUtils.java, line 200:

<comment>Absolute child paths make `imageFsRoot` ineffective, so Wine DLLs are copied from `/opt/...` on the host instead of the imagefs root.</comment>

<file context>
@@ -189,10 +196,11 @@ public static void applySystemTweaks(Context context, WineInfo wineInfo) {
-        File containerSystem32Dir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/windows/system32");
-        File containerSysWoW64Dir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/windows/syswow64");
+        File imageFsRoot = ImageFs.find(context).getRootDir();
+        File wineSystem32Dir = new File(imageFsRoot, "/opt/wine/lib/wine/x86_64-windows");
+        File wineSysWoW64Dir = new File(imageFsRoot, "/opt/wine/lib/wine/i386-windows");
+        File containerSystem32Dir = new File(containerRoot, ".wine/drive_c/windows/system32");
</file context>
Suggested change
File wineSystem32Dir = new File(imageFsRoot, "/opt/wine/lib/wine/x86_64-windows");
File wineSysWoW64Dir = new File(imageFsRoot, "/opt/wine/lib/wine/i386-windows");
File wineSystem32Dir = new File(imageFsRoot, "opt/wine/lib/wine/x86_64-windows");
File wineSysWoW64Dir = new File(imageFsRoot, "opt/wine/lib/wine/i386-windows");

Comment thread app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
// an interrupted prior extract that subsequent extracts can't cleanly overwrite.
if (dst.exists())
dst.delete();
FileUtils.delete(dst);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Deleting the existing driver directory before extraction can erase a working driver if extraction fails, with no rollback to the previous contents.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/com/winlator/contents/AdrenotoolsManager.java, line 137:

<comment>Deleting the existing driver directory before extraction can erase a working driver if extraction fails, with no rollback to the previous contents.</comment>

<file context>
@@ -131,15 +131,17 @@ private boolean extractDriverFromResources(String adrenotoolsDriverId) {
+        // an interrupted prior extract that subsequent extracts can't cleanly overwrite.
         if (dst.exists())
-            dst.delete();
+            FileUtils.delete(dst);
 
         dst.mkdirs();
</file context>

try {
future.get(WINE_EXEC_TIMEOUT_SECONDS, TimeUnit.SECONDS)
} catch (e: TimeoutException) {
future.cancel(true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Timeout cancellation does not actually stop the underlying Wine subprocess, so hung commands can keep running after the timeout.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt, line 66:

<comment>Timeout cancellation does not actually stop the underlying Wine subprocess, so hung commands can keep running after the timeout.</comment>

<file context>
@@ -43,8 +51,28 @@ class SteamTokenLogin(
+            try {
+                future.get(WINE_EXEC_TIMEOUT_SECONDS, TimeUnit.SECONDS)
+            } catch (e: TimeoutException) {
+                future.cancel(true)
+                // Don't log/throw the full command — `command` may contain a refresh JWT
+                // (steam-token.exe encrypt <user> <token>). Log just the executable name.
</file context>

public static void apply(Context context, ThemeInfo themeInfo, ScreenInfo screenInfo, com.winlator.container.Container container) {
// Per-container path; xuser symlink is unsafe for writes (see
// WineUtils.applySystemTweaks rationale).
File userRegFile = new File(container.getRootDir(), ".wine/user.reg");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Per-container migration is incomplete: user.reg is moved to the container root, but wallpaper generation still uses the shared ImageFs root, which can desynchronize the registry and wallpaper asset paths across containers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/com/winlator/core/WineThemeManager.java, line 47:

<comment>Per-container migration is incomplete: `user.reg` is moved to the container root, but wallpaper generation still uses the shared `ImageFs` root, which can desynchronize the registry and wallpaper asset paths across containers.</comment>

<file context>
@@ -41,9 +41,10 @@ public ThemeInfo(String value) {
+    public static void apply(Context context, ThemeInfo themeInfo, ScreenInfo screenInfo, com.winlator.container.Container container) {
+        // Per-container path; xuser symlink is unsafe for writes (see
+        // WineUtils.applySystemTweaks rationale).
+        File userRegFile = new File(container.getRootDir(), ".wine/user.reg");
         String background = Color.red(themeInfo.backgroundColor)+" "+Color.green(themeInfo.backgroundColor)+" "+Color.blue(themeInfo.backgroundColor);
 
</file context>

Comment thread app/src/main/java/app/gamenative/utils/SteamUtils.kt
Comment thread app/src/main/java/com/winlator/renderer/GLRenderer.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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)

2545-2548: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove duplicate GSE→userdata migration in forceSyncUserFiles.

Migration is already done at Line 2542 with isLaunchRealSteam. The second call at Line 2547 duplicates work and ignores the new mode flag (default path), which can cause inconsistent behavior.

Suggested fix
- val context = instance?.applicationContext ?: return@async PostSyncInfo(SyncResult.UnknownFail)
- // Migrate GSE Saves to Steam userdata
- SteamUtils.migrateGSESavesToSteamUserdata(context, appId)
+ val context = instance?.applicationContext ?: return@async PostSyncInfo(SyncResult.UnknownFail)
🤖 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 2545 -
2548, The duplicate migration call SteamUtils.migrateGSESavesToSteamUserdata
found in forceSyncUserFiles should be removed — keep only the earlier invocation
that runs under the isLaunchRealSteam path; delete the second call that
unconditionally runs using the local context variable so migration respects the
new mode flag and isn’t executed twice.
🤖 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/service/SteamService.kt`:
- Around line 2277-2286: The call to SteamUtils.migrateGSESavesToSteamUserdata
is executed before the try/finally that calls releaseSync(appId), so any
exception thrown there can skip releaseSync and leak the sync flag; move the
null-guard and the call into the existing try block (inside the async block that
returns PostSyncInfo) so that releaseSync(appId) in the finally always runs,
keeping the null check for instance?.applicationContext and preserving the
existing return of PostSyncInfo(SyncResult.UnknownFail) when appropriate; apply
the same change for the other occurrence around lines 2535-2543.

In `@app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt`:
- Around line 463-477: The current handler opens the confirm dialog as soon as
the first non-whitespace character is typed because it uses wasBlank &&
nowNonBlank; change the condition so the dialog only opens when the new trimmed
value is meaningfully non-empty (e.g., trimmed.length > 1) to allow normal
typing: in the NoExtractOutlinedTextField onValueChange block, replace the
branch condition (wasBlank && nowNonBlank) with something like (wasBlank &&
nowNonBlank && trimmed.length > 1) and keep the existing
pendingValue/showConfirmDialog logic and the else path that writes
state.config.value = config.copy(sdkCloudSaveSubdir = trimmed).

---

Outside diff comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 2545-2548: The duplicate migration call
SteamUtils.migrateGSESavesToSteamUserdata found in forceSyncUserFiles should be
removed — keep only the earlier invocation that runs under the isLaunchRealSteam
path; delete the second call that unconditionally runs using the local context
variable so migration respects the new mode flag and isn’t executed twice.
🪄 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: e2c45603-2fe0-4849-8524-1b595e87adcf

📥 Commits

Reviewing files that changed from the base of the PR and between f3781a1 and 258e296.

📒 Files selected for processing (8)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/app/gamenative/utils/FileUtils.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/com/winlator/xserver/WindowManager.java
  • app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/winlator/xserver/WindowManager.java
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt

Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment thread app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
@TideGear TideGear marked this pull request as draft May 2, 2026 07:41
@TideGear TideGear marked this pull request as ready for review May 2, 2026 08:02
@utkarshdalal
Copy link
Copy Markdown
Owner

Nice, great fixes - please undo the per container arch and variant because that's already in master

TideGear added 20 commits May 2, 2026 01:08
File.delete() is a silent no-op on non-empty directories, so an extraction
interrupted mid-stream (OOM, storage hiccup, process kill) leaves partial
files behind. The next launch can't clear them, extract-over-stale returns
false, and setDriverById falls through to the "Driver not found" branch —
which excludes asset-bundled drivers from its installed-list fallback.
ADRENOTOOLS_DRIVER_PATH/HOOKS_PATH/NAME never get set, wrapper_icd has
nothing to dlopen, and the game renders a black screen with audio while
the process appears healthy.

Swap both dst.delete() calls for FileUtils.delete(dst), which clears
directory contents before unlinking. Toggling the Graphics Driver Version
dropdown off and on was the user-visible workaround — it just forced a
retry that happened to not get interrupted.
…ation

pixmapFromHardwareBuffer and pixmapFromFd dereferenced the return values of
drawableManager.createDrawable / pixmapManager.createPixmap directly. Both
return null on resource-ID conflicts (the requested ID was already in use),
which produced an opaque NullPointerException at the next line instead of
a proper X protocol error.

Throw BadIdChoice on null returns so the client sees a meaningful failure
and can recover, rather than getting a JVM stacktrace logged into
System.err and a half-initialized swapchain.

Note: this commit historically also added registerAsOwnerOfResource calls
to make the pixmaps client-owned (preventing a leak on disconnect that
caused BadIdChoice cascades). That half has been removed pending a soak
test post-cascade-fix to determine whether the leak still triggers in
normal use or was primarily exposed by cascade-driven Wine client cycling.
Wine's GLX/DRI3 helper (resourceIDBase 0x5C00000) under real-Steam
mode can create blank input-output windows every ~10ms without ever
destroying them, exhausting the 512MB Dalvik heap via 1.92MB per
Drawable ByteBuffer and aborting in XConnectorEpoll.

reapLeakedClientWindows runs at end of createWindow. When the same
originClient has >= LEAK_CLIENT_CAP (8) blank-className, same-size
input-output windows alive, destroy the oldest. Parent-agnostic —
the leaked windows get reparented to a 1x1 orphanage before the
check runs.
Blank-className pid=0 windows in the Wine winex11.drv/zink path
cover two populations: the game's active GL render target (child of
the game's top-level HWND, parent has real className+pid) and the
orphan leak chain reparented to a 1x1 blank orphanage window.

Only the orphan chain has a blank parent. Filter on that: skip a
blank window only when its parent is also blank. The active render
target falls through and gets drawn.

A broader filter (skip all blank windows) hid the game's output. No
filter let fresh leak children at (0,0) paint blank over the game
every frame before their reparent.
SysVSharedMemory.delete(int) mutated the shmemories SparseArray without
synchronizing on it, while every other mutator (get, attach, detach,
deleteAll) held synchronized(shmemories). delete is invoked from the
SysV SHM connector thread (SysVSHMRequestHandler), and detach is invoked
from the X11 connector thread (MITSHMExtension), so the two can run
concurrently on the same array.

When delete ran mid-iteration in detach, SparseArray's internal DELETED
sentinel (a bare Object) could surface at valueAt(i), producing:

  java.lang.ClassCastException: java.lang.Object cannot be cast to
    com.winlator.sysvshm.SysVSharedMemory$SHMemory
      at SysVSharedMemory.detach(SysVSharedMemory.java:117)
      at SHMSegmentManager.detach(SHMSegmentManager.java:26)
      at MITSHMExtension.detach(MITSHMExtension.java:74)

Fix: wrap the body of delete(int) in synchronized(shmemories). The lock
is reentrant, so deleteAll (which already holds it while calling delete
per-id) is unaffected.

Files:
  app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java
Stabilizes the "real Steam client" launch path (`steam.exe -applaunch`) so
games like Darkest Dungeon and 868-HACK boot cleanly without the multi-minute
"gray Play" reconcile loop, and adds a per-container toggle to disable the
Steam overlay. Also hardens cloud sync and GSE→userdata migration against
mode switches between emu and real-Steam launches.

- `SteamUtils.writeSteamworksCommonManifest` now writes only the depots whose
  `_CommonRedist/<folder>/installscript.vdf` is actually present on disk
  (typically 228985 vcredist 2013 + 228989 vcredist 2022) instead of the full
  24 PICS-declared depots. The SKIP check compares against the same
  present-depot baseline, so a correct manifest survives across launches.
- `SteamUtils.createAppManifest` emits a `SharedDepots` block in the child
  game's acf for any depot whose parent app id is known (via
  `DepotInfo.depotFromApp`). Declaring shared ownership up front stops Steam
  from reparenting depots and cascading `Update Queued` onto the child on
  every launch.
- Removed the now-unused `canonical228980SizeOnDisk`,
  `canonical228980BytesToDownload`, and `canonical228980BytesToStage`
  constants; size-on-disk is summed from the present depots and
  bytes-to-download is `0`.

Why: writing all 24 depots made Steam prune 22 phantoms every boot (~3 min of
gray Play). Writing an empty manifest made Steam try to download ~52 MB,
hang `Suspended`, and permanently cascade `Update Queued`. The 2-depot +
`SharedDepots` shape is the only combination that survives Steam's reconcile.

- New per-container `disableSteamOverlay` flag (`Container.java`,
  `ContainerData.kt`, `PrefManager.kt`, `strings.xml`).
- Toggle surfaced in the container dialog's General tab, visible only when
  "Launch Real Steam" is enabled (`GeneralTab.kt`).
- When enabled, `XServerScreen` exports `DISABLE_VK_LAYER_VALVE_steam_overlay_1=1`
  and `SteamNoOverlayUIDrawing=1` into the Wine env so the overlay DLL never
  injects.

- `SteamService.beginLaunchApp` / `syncUserFiles` / `closeApp` now take an
  `isLaunchRealSteam` flag. In real-Steam mode the Goldberg achievement sync
  and the GSE→Steam userdata migration are skipped so the real client owns
  its own save state.
- `MainViewModel` tracks the last launch mode (`lastSteamMode` extra) and,
  on real→emu transitions, cleans up artifacts from the previous mode to
  avoid mixing GSE and real-client save layouts.
- `PluviaMain` threads the launch-mode flag through to `syncUserFiles`.

- `SteamAutoCloud` refactors the byte-identical detection into a shared
  lambda and adds a "cache lost but local == remote" branch that silently
  rehydrates the cache instead of surfacing a spurious conflict prompt.

- `MainViewModel` adds `SteamFixDiagnostics` (last mapped window class,
  game-window-mapped flag, last unmatched window class) and a helper
  window-class filter (`STEAM_HELPER_WINDOW_CLASSES`) so stall reports
  distinguish the real game window from Steam's own helper UI.
- `XServerScreen` integrates a 60 s stall watchdog and logs window-mapping
  events for post-mortem analysis of gray-Play hangs.

- `SteamTokenLogin`, `PreInstallSteps`, `preInstallSteps/VcRedistStep`,
  `ContainerUtils`, and `ImageFsInstaller` receive small wiring changes to
  support the real-Steam-client launch path (vcredist gating, container
  defaults, ImageFs layout).

- [ ] Launch Darkest Dungeon (262060) three times in a row — Play button
      should auto-dismiss; no multi-minute gray Play.
- [ ] Launch 868-HACK — clean boot via real Steam client.
- [ ] Switch a container between emu and real-Steam mode; verify saves from
      the prior mode are not blended into the new one.
- [ ] Toggle "Disable Steam Overlay" on a real-Steam container and confirm
      the overlay DLL does not inject at runtime.
- [ ] With a stale cloud cache matching remote, confirm no conflict dialog
      appears.
Three fixes to real-Steam-client launch path, all in SteamUtils.kt:

1. applySteamInstallScriptShim (new): writes HKLM\Software\Valve\Steam\Apps
   and \InstallScripts entries for appId, 228980, and all 24 canonical
   228980 depots with Installed=1 / Run=1. This stops Steam from re-running
   bundled vc_redist.x86.exe / vc_redist.x64.exe / DXSETUP.exe on every
   launch, which was racing against the game's own MSVC loader and (on
   Unity titles) triggering UnityCrashHandler. Called from restoreSteamApi
   after createAppManifest.

2. verifyRestoredState: no longer requires a .orig sibling next to
   steam_api*.dll. Games with useLegacyDRM=false never go through
   replaceSteamApi and so never produce a .orig, which caused the old
   check to log "DLL marker desync" on every launch and needlessly
   re-copy pipe DLLs. Now compares the on-disk DLL hash against the pipe
   asset hash: match = still pipe (bad), mismatch = restored (good).

3. createAppManifest regularDepots-empty path: downgraded from W to I when
   the existing appmanifest already has a valid buildId + depots +
   UpdateResult=0. The PICS "regularDepots empty" result is a transient
   refresh flake and not actionable when the acf is otherwise healthy.

Validated on Shiren (2178480) and Darkest Dungeon (262060): shim writes
300 reg entries, vcredist windows no longer appear on 2nd+ launches, DLL
hash check logs "DLL marker + hash ok, skipping DLL copy" instead of
desync warnings.
The real-Steam launch path is stable across DD / 868-HACK / Shotgun King /
Baba Is You — fold in the remaining structural fixes and strip the
investigation-era diagnostic scaffolding that was left behind.

SteamUtils.kt:
- Replace hardcoded canonical228980Depots table with a runtime PICS resolver
  driven by the child acf's SharedDepots. Fixes permanent "gray Play" on
  games whose shared-redist depot set diverges from DD's (was silently
  writing InstalledDepots{} + BytesToDownload=52MB on those).
- Add SHA-256 verify-and-reheal to STEAM_DLL_REPLACED / STEAM_DLL_RESTORED
  markers in replaceSteamApi/restoreSteamApi so a stale marker can no
  longer lie about on-disk state (2nd-launch black-screen repro).
- Add validateAcfShape() self-check: after writing any child or 228980
  manifest, log Timber.e on shapes known to cause gray Play (Update
  Required bit set, BytesToDownload > 0, InstalledDepots empty when it
  shouldn't be). Catches future regressions loudly instead of silently.

Diagnostic cleanup:
- Drop SteamFixDiagnostics object, STEAM_HELPER_WINDOW_CLASSES,
  REAL_STEAM_STALL_WATCHDOG_MS, and all Timber.tag("SteamFix") log lines
  across SteamUtils, SteamService, SteamAutoCloud, SteamTokenLogin,
  MainViewModel, XServerScreen, ImageFsInstaller.
- Rewrite the comments that referenced the investigation's numbered
  SteamFix list (utkarshdalal#8, utkarshdalal#9, utkarshdalal#11, utkarshdalal#16utkarshdalal#24) so they stand on their own.

UX default:
- Default disableSteamOverlay to true in Container.java, ContainerData.kt,
  and PrefManager.kt. Overlay-on is an opt-in now.

Housekeeping:
- .gitignore: exclude .claude/ session state.
- Drop tracked .claude/scheduled_tasks.lock.
Addresses three related issues seen when launching games in real-Steam mode.

1. Suppress Wine-hosted Steam client's cloud sync (Option B)
   The Wine-hosted Steam client was racing GameNative's SteamKit cloud
   sync on every launch/exit. Per-app gates now disable the Wine
   client's cloud path in userdata/<steamID>/config/localconfig.vdf
   (cloud_enabled=0, cce=0) for both the existing-file and new-file
   branches, with a setOrReplaceKey helper to avoid duplicates.

2. Graceful red-exit with 5s grace window
   The quick-menu EXIT_GAME action previously went straight to a SIGKILL
   of the Wine process tree, giving games no chance to flush saves. It
   now sends WM_CLOSE to the game exe (and to steam.exe when real-Steam
   mode is on), waits 5s, then proceeds with the existing exit path. A
   second tap within the grace window cancels the wait and force-quits.

3. Filter localhost pending-operations in real-Steam mode
   Steam's server reports the Wine-hosted client's self-registered
   session as machineName="localhost" with UploadPending, causing a
   spurious "Pending Upload" dialog before every real-Steam launch.
   beginLaunchApp now filters pendingRemoteOperations by
   machineName="localhost" when isLaunchRealSteam is true. Genuine
   entries from other devices still surface the dialog, and the
   kickPlayingSession path is unaffected.

4. Fix inverted uploadsRequired flag on exit sync
   signalAppExitSyncDone was passing uploadsRequired = (... == false),
   producing impossible states like "upload succeeded but wasn't
   required". Flipped to == true so Steam's server gets a truthful
   signal and stops holding stale pending markers.

Also: ignore .claude/ session state directory.
Three audit findings from the Fix-Steam-Client branch review:

- XServerScreen: cancel gracefulExitJob in DisposableEffect.onDispose.
  If the screen disposed during the 5s force-quit grace window, the
  coroutine leaked (only exitWatchJob was cancelled).

- SteamUtils.createAppManifest: filter shared depots whose depotFromApp
  is 0 before writing the SharedDepots block. Writing `"<id>" "0"` told
  Steam the depot belonged to a nonexistent app 0, which could
  re-trigger the PICS reconcile / gray-Play cascade this branch was
  built to prevent. Unowned shared depots are now logged and omitted.

- SteamUtils.applySteamInstallScriptShim: narrow the blanket
  `catch (Exception)` to IOException + SecurityException so programming
  errors (NPE, ISE) surface instead of being silently swallowed.
In real-Steam mode the Wine-hosted Steam client logs into Steam's
servers and registers its own AppSessionActive under machineName
"localhost" (the container's hostname). With cloud_enabled=0 set on
the Wine client's localconfig.vdf (Option B from the earlier
real-Steam sync-race fix), the Wine client never signals upload state
for its localhost session, so Steam's server parks it in
UploadPending forever. Desktop Steam then reads that on next launch
and shows "Cloud Out of Date — you played on localhost, upload not
started" even after GameNative's own sync completed cleanly.

After GameNative's post-exit SteamKit sync calls signalAppExitSyncDone,
also call kickPlayingSession() to clear any lingering session for
this user on Steam's server. The game is already exited at this
point, so kicking is safe and only affects the stale Wine-client
entry that would otherwise trigger the desktop dialog.
… modes

Reconstructing the timeline of a broken Steam dir from ls -la mtimes
wasted hours this session. A Timber line per launch with size+mtime of
steam.exe, steamclient.dll, steamclient64.dll will surface any unexpected
change the moment it happens.

Also lift the steam.cfg writer out of restoreSteamApi (emu-only path) into
an ensureSteamCfg helper called from the real-Steam extractSteamFiles path
too, so update-inhibit keys land on fresh Wine-prefix seeds regardless of
launch mode.
Real-Steam launches stalled after cloud sync because Steam's server saw
a pending cloud operation from machine 'localhost' and refused to start
the game (BYieldingAppLaunchIntent returned 1 pending remote operations).

The phantom operation came from GameNative's own SteamKit client: on
every beginLaunchApp it called signalAppLaunchIntent(machineName=
'localhost'), which registers a launch-intent server-side. In emulation
mode this is correct — GameNative IS the client running the game. In
real-Steam mode it's harmful — the Wine-hosted Steam client performs
its own BYieldingAppLaunchIntent, sees the localhost entry we just
created, treats it as a conflicting session, and sits forever waiting
for it to clear.

The earlier ef5b5b5 fix filtered localhost entries out of our local
view of pendingRemoteOperations but never stopped creating them. This
change skips the RPC entirely when isLaunchRealSteam=true, so the
server never records the phantom operation in the first place.

Verified by cloud_log.txt on a stuck Baba Is You (appid 736260) launch:
  [AppID 736260] BYieldingAppLaunchIntent returned 1 pending remote operations:
  	Operation '3' from machine 'localhost'
after which Steam never attempted to launch the game.
Two defense-in-depth additions to SteamService.beginLaunchApp that close
gaps where a pending AppSessionActive op on Valve's servers can survive
a bad launch and block future launches in either mode.

1. Proactive kick before real-Steam launch. When isLaunchRealSteam=true,
   call kickPlayingSession() up front. Wine-Steam establishes its own
   session via BYieldingAppLaunchIntent once it boots, so wiping any
   stale orphaned entry first costs nothing and clears phantoms left by
   a prior emulation crash that never reached the exit-cleanup path.

2. Finally-block cleanup on aborted emulation launches. Track whether
   we registered a launch intent via signalAppLaunchIntent, and in the
   finally block kick the session if the intent was registered but the
   flow didn't end in Success/UpToDate (PendingOperations dialog
   dismissed, exception partway through, caller gave up). Stops leaked
   phantoms from blocking subsequent launches; the successful-launch-
   then-exit case is still handled by closeApp as before.

Motivating incident: Dead Cells black-screened after the fe0f313
localhost-skip fix because the phantom was under device name, not
"localhost" — from an earlier emulation launch that errored mid-flight.
Manual reset via desktop Steam cleared it. These hardening steps close
that loop automatically.
When every pending remote operation returned by signalAppLaunchIntent is
from our own machine name, treat them as self-phantoms from a prior
session that died without cleaning up (Android-killed process, upload
in-flight at force-close). Kick any stale AppSessionActive and re-signal
with ignorePendingOperations=true in-flight, then proceed silently.
Cross-device conflicts (different machine name) still surface the dialog
so genuine "someone else is uploading from your PC" cases are preserved.

Closes the last repro path where the user had to manually launch in
emulation mode with "Play Anyway" to clear a stuck Pending Upload dialog
before the next real-Steam launch would work. The proactive kick added
in 79c112c only clears AppSessionActive; this handles UploadPending
and the other upload-side markers that kickPlayingSession doesn't
release, matching what the manual workaround was doing.
Fixes two real-Steam launch stalls that manifest as black screens after
cloud sync:

1. Cloud-conflict dialog hidden by -silent
   STEAM_LAUNCH_FLAGS passed -silent to steam.exe, which not only hides
   the main window but also suppresses the cloud-conflict resolution
   dialog. Games with save-sync conflicts (or pending uploads) sat at a
   black screen forever with no way to resolve them. -silent was being
   injected from two sources; both are removed:
   - app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
   - app/src/main/assets/box86_64/lightsteam.box64rc
   - app/src/main/assets/box86_64/ultralightsteam.box64rc
     (the box64rc WINEARGS= entry was shadowing the Kotlin change)

2. localhost pending-operation phantoms
   Wine-Steam's own BYieldingAppLaunchIntent was observing stale
   UploadPending / UploadInProgress markers from a prior Wine-Steam
   session (registered under machineName="localhost") that was killed
   mid-cycle, and stalling the launch. The existing proactive
   kickPlayingSession() only clears AppSessionActive; upload markers
   survive it.

   SteamService.beginLaunchApp now performs a full phantom-clearing
   cycle in real-Steam mode before Wine-Steam is spawned:
   - kickPlayingSession()              — clears AppSessionActive
   - signalAppLaunchIntent(ignore=true) — server-side "Play Anyway"
                                         dismissal that also clears
                                         UploadPending / UploadInProgress
                                         regardless of origin (self or
                                         stale localhost)
   - signalAppExitSyncDone()           — cleanly closes the session so
                                         Wine-Steam's subsequent
                                         BYieldingAppLaunchIntent sees
                                         no GN-side session

   Mirrors the existing self-phantom auto-clear technique, but runs it
   proactively in the real-Steam path where signalAppLaunchIntent is
   otherwise skipped entirely.

Also in SteamUtils.kt:

3. Shared-helper manifest writer refactor
   createAppManifest now delegates to writeAllSharedAppManifests
   (data-driven over a sharedHelperApps list) instead of the single-
   purpose writeSteamworksCommonManifest. Lets future helper apps
   plug in without touching the child-manifest path. 228980 is the
   only entry for now; 241100 was tried, caused a 20s Steam reconcile
   loop during gameplay, and is listed in retiredSharedHelperAppIds
   so leftover acfs from the buggy build get cleaned up on next
   launch. validateAcfShape now loops over the helper list rather
   than hard-coding one appId.

Verified
- Shotgun King (1972440) launched cleanly with the new phantom-clearing
  cycle after black-screening before the fix; cloud_log.txt confirmed
  the "Operation '3' from machine 'localhost'" pending op no longer
  appears on retry.
…Number

The prior proactive phantom-clear always ran the full
signalAppLaunchIntent(ignore=true) + signalAppExitSyncDone(
uploadsCompleted=true) cycle on every real-Steam launch. On a clean
launch with no phantom present this lied to Steam's cloud that a
session had finished with uploads completed, which advanced the
server-side ChangeNumber and caused Wine-Steam to "forget"
steam_autocloud.vdf on the next launch. Games with Steamworks cloud
integration (observed on 868-HACK) then exited with code 1 a few
seconds after launch.

Probe first: bare signalAppLaunchIntent with ignorePendingOperations=
false returns pending ops without dismissing them. Only run the
dismissal cycle when the probe actually reports a phantom. When we do
run it, use honest signalAppExitSyncDone(uploadsCompleted=false,
uploadsRequired=false) — we didn't upload anything.

Always kickPlayingSession at the end to release the AppSessionActive
the probe RPC registered under our machineName, so Wine-Steam's
subsequent localhost intent doesn't see a conflicting session from us.

Shotgun King phantom-clearing path still works (the probe detects the
phantom and runs the dismissal); clean launches no longer touch
ChangeNumber.
The red exit button was hard-killing steam.exe, which prevented Steam
from flushing config/localconfig, installscript completion markers, and
sending its own clean-shutdown IPC to the running game. Next launch of
the same game would see the prefix in an incomplete state and re-run
the game's vcredist/DirectX installscripts.

Replace killProcess with `steam.exe -shutdown` — Steam's own graceful
exit. Poll listProcesses at 1Hz to detect when steam.exe is gone, then
proceed with the normal exit sequence. Cap the wait at 15s and show a
modal asking "Keep waiting" or "Force quit" if Steam is still winding
down (cloud saves, workshop sync, etc.). The dialog is a Compose
overlay and does not pause emulation.

Also adds a WinHandler.exec(filename, parameters) overload so callers
can pass paths containing spaces (the existing single-string exec
splits on the first space, which mangles "C:\Program Files (x86)\..."
invocations).
Pluvia's SteamAutoCloud owns cloud in both modes with Wine-Steam
cloudenabled=0 + -no-browser. That left SDK-cloud games reading stale
local state because Wine-Steam's ISteamRemoteStorage had no index and
some games read saves from <install>/<subdir>/ rather than
<userdata>/<appid>/remote/.

SteamAutoCloud:
 - writeRemoteCacheVdf called after every sync path (downloadUserFiles,
   upload, rehydrate, no-change) so Wine-Steam's SDK sees cloud files.
 - rebaseToAutoCloud redirects SteamUserData-rooted cloud entries to the
   matching saveFilePattern target for Auto-Cloud games whose uploads
   were misrooted.
 - Skip the SteamUserData catch-all scan for Auto-Cloud apps so ghost
   SteamUserData files don't get re-uploaded forever.

SteamUtils:
 - RemoteCacheFile + writeRemoteCacheVdf writer.
 - sdkCloudSaveMirrors registry + mirrorSdkCloudRemoteToSave /
   mirrorSdkCloudSaveToRemote (seeded with 588650 -> "save").
 - writeSharedConfigCloudDisabled mirrors cloudenabled=0 into
   sharedconfig.vdf; both "cloudenabled" and "cloud_enabled" written,
   last_sync_state / autocloud bookkeeping cleared.
 - purgePhantomAppUserdata removes userdata/<sid>/<phantomAppId>/ so a
   prior synthetic appmanifest can't leave an AutoCloud watcher behind
   that blocks steam.exe -shutdown.

SteamService:
 - beginLaunchApp calls mirrorSdkCloudRemoteToSave after a successful
   sync so downloaded bytes reach the install dir before the game runs.
 - closeApp calls mirrorSdkCloudSaveToRemote before the upload sync so
   progress in <install>/save/ reaches remote/ before Pluvia uploads.

XServerScreen:
 - steam.exe launched with -no-browser (webhelper is a documented cause
   of shutdown hangs under Wine).
 - STEAM_SHUTDOWN_WAIT_MS 15s -> 20s.
 - purgePhantomAppUserdata("241100") on real-Steam container prep.

SteamAppScreen: thread isLaunchRealSteam through forceSyncUserFiles so
manual cloud sync uses the right GSE <-> SteamUserData migration path.

FileUtils: extract matchesGlob helper; drop per-file log spam from
findFiles / findFilesRecursive.
Adds per-container support for "Pattern B" SDK-cloud games (Dead
Cells-style) that read saves from a subdirectory of their install
directory rather than from <userdata>/<appid>/remote/. Replaces the
hardcoded one-entry registry with the Ludusavi save-path manifest
(~1944 Steam games matching Pattern B).

Container setting:
 - New sdkCloudSaveSubdir field on Container/ContainerData, persisted
   through JSON and Compose Saver. Empty = bridge disabled.
 - "Cloud Save Bridge" section in GeneralTab (visible when Launch
   Steam Client is on) with text field + Use Recommended / Detect /
   Clear buttons, confirmation dialog on first activation, and
   validation rejecting path separators, .., and drive letters.

Ludusavi integration (utils/LudusaviRegistry.kt):
 - Fetches manifest.yaml from mtkennerly/ludusavi-manifest via the
   existing OkHttp client. Streaming line-parser avoids the OOM that
   SnakeYAML's eager map load caused on the 5 MB file.
 - Filters to Steam-IDed entries with <base>/<subdir> save paths
   tagged "save" and applicable to Windows (or no OS filter).
 - Writes ~190 KB filtered JSON to filesDir/ludusavi_pattern_b.json,
   7-day TTL, falls back to stale disk cache on fetch failure.
 - Primed in background at PluviaApp.onCreate so Use Recommended and
   the launch-time prompt are instant after the first session.

Launch-time prompt:
 - preLaunchApp runs a Pattern B check (Ludusavi match AND no
   saveFilePatterns in PICS UFS) before cloud sync. Match fires an
   SDK_CLOUD_BRIDGE_SUGGESTION dialog with Enable / Skip / Don't ask
   again. Catches users who install with real-Steam default and never
   touch settings.
 - Don't-ask-again persists per-container as
   extraData.sdkCloudBridgePromptDismissed.

Runtime mirror (utils/SteamUtils.kt):
 - sdkCloudGameSaveDir now reads the user-configured subdir only; no
   implicit fallback, to avoid REPLACE_EXISTING copies into a guessed
   dir that might be wrong.
 - detectSdkCloudSaveSubdir resolves paths via the container's own
   rootDir rather than the global xuser symlink so it works even when
   a different container is activated.

Removed:
 - Bundled assets/sdk_cloud_save_bridge.json (single hardcoded entry);
   Ludusavi covers it plus ~1943 others.

Cosmetic: replaced "Pluvia" with "GameNative" in comments/strings
written on this branch. PluviaApp/PluviaTheme/PluviaPreferences class
names and storage keys left alone.
DRI3Extension.pixmapFromFd
- Move drawable.setOnDestroyListener(onDestroyDrawableListener) so it only
  fires after createPixmap succeeds. Registering it earlier caused a
  double-unmap on the failure path: removeDrawable() invoked the listener
  (unmap #1), then the finally block unmapped again because
  handedOffToDrawable was still false. (Bug introduced by my earlier
  SHM-leak fix.)

GeneralTab SdkCloudSaveSubdirField
- Drop the wasBlank/meaningful/confirm-dialog gate from the typing path
  entirely. The original wasBlank-on-first-keystroke check fired the
  confirm dialog on the first character and disrupted typing; my length>1
  patch made it never fire because by char 2 wasBlank was already false.
  Manual typing is intentional — commit each keystroke directly. The
  first-activation confirmation is reserved for auto-filled values from
  the Recommend / Detect buttons below, which the user didn't type.

SteamService phantom dismiss
- Filter probed pending ops by machineName before auto-dismissing. Only
  entries from our machineName (or "localhost") are treated as phantoms;
  any cross-device entry blocks the auto-dismiss path so legitimate
  cloud conflicts surface to the SYNC_CONFLICT dialog instead of getting
  silently wiped by ignorePendingOperations=true.

SteamService beginLaunchApp offline gate
- Move the isOffline/!isConnected early-return below the GSE→userdata
  migrate, inside the existing try block. Local prep should run even
  offline (one-shot file moves, not cloud-dependent). The cloud RPCs
  below the gate continue to short-circuit when offline.

PluviaMain SDK-cloud-bridge prompt
- Don't fire the prompt for non-game launches: bootToContainer (Open
  Container), useTemporaryOverride, or skipCloudSync flows. The post-
  prompt `relaunch` lambda doesn't carry those flags forward, so an
  Open-Container that triggered the prompt would silently turn into a
  game launch on dismiss. Easier to gate the prompt out than thread
  every flag through the relaunch.

SteamUtils verifyReplacedState / verifyRestoredState
- Treat a missing pipe-asset hash as a verification FAILURE rather than
  a silent skip. The previous `?: return@forEach` would skip the file
  and leave the function returning true — meaning a future build that
  drops a pipe DLL asset would silently let the marker check pass and
  suppress DLL repair. Fail closed instead.

LudusaviRegistry.ensureLoaded
- Wrap the fetch + disk-write + memoryCache-populate path in a
  kotlinx.coroutines Mutex with a double-check on entry. Without it,
  concurrent primeCache() + lookup() callers could both pass the null
  check and both fetch the 5 MB manifest. Now the second waiter sees
  the populated cache after the first finishes.

Findings flagged but not changed:
- SteamUtils SDK cloud mirror nested directories (third time): mirror
  is by design for Dead Cells flat saves; recursive expansion remains
  scope creep.
@TideGear
Copy link
Copy Markdown
Author

TideGear commented May 2, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Reviews resumed.

@TideGear
Copy link
Copy Markdown
Author

TideGear commented May 2, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)

595-612: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Create the per-container Steam/config directory before writing loginusers.vdf.

On a fresh isolated prefix, writeText() can fail here because the parent directory has never been created. The catch just logs and skips the whole autologin/bootstrap write.

♻️ Suggested fix
         val winePrefixBase: File = container?.let { File(it.rootDir, ".wine") }
             ?: File(imageFs.wineprefix)
         val steamConfigDir = File(winePrefixBase, "drive_c/Program Files (x86)/Steam/config")
         try {
+            steamConfigDir.mkdirs()
             File(steamConfigDir, "loginusers.vdf").writeText(vdfFileText)
             val userRegFile = File(winePrefixBase, "user.reg")
             val steamRoot = "C:\\Program Files (x86)\\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/SteamUtils.kt` around lines 595 - 612,
The code may fail when writing loginusers.vdf because the steamConfigDir doesn't
exist; before calling File(steamConfigDir, "loginusers.vdf").writeText(...)
ensure the directory exists by creating steamConfigDir (e.g., call
mkdirs()/createDirectories on steamConfigDir) and check its success, then
proceed to write; keep the existing try/catch around the file and registry
operations (winePrefixBase, steamConfigDir, loginusers.vdf, user.reg,
WineRegistryEditor) so failures are still logged.
♻️ Duplicate comments (3)
app/src/main/java/app/gamenative/utils/ContainerUtils.kt (1)

813-867: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fresh containers can still miss persisted defaults.

This branch still hand-builds ContainerData instead of starting from getDefaultContainerData(). It already diverges again: localSavesOnly, useSteamInput, and the sharpness fields are present in getDefaultContainerData() but not here, so brand-new containers won't inherit those saved defaults until the config is opened and saved again.

♻️ Suggested fix
-        } else {
-            // Use default config with drives
-            ContainerData(
-                screenSize = PrefManager.screenSize,
-                envVars = PrefManager.envVars,
-                cpuList = PrefManager.cpuList,
-                cpuListWoW64 = PrefManager.cpuListWoW64,
-                graphicsDriver = PrefManager.graphicsDriver,
-                graphicsDriverVersion = PrefManager.graphicsDriverVersion,
-                graphicsDriverConfig = PrefManager.graphicsDriverConfig,
-                dxwrapper = initialDxWrapper,
-                dxwrapperConfig = PrefManager.dxWrapperConfig,
-                audioDriver = PrefManager.audioDriver,
-                wincomponents = PrefManager.winComponents,
-                drives = drives,
-                execArgs = PrefManager.execArgs,
-                showFPS = false,
-                launchRealSteam = PrefManager.launchRealSteam,
-                disableSteamOverlay = PrefManager.disableSteamOverlay,
-                wow64Mode = PrefManager.wow64Mode,
-                startupSelection = PrefManager.startupSelection.toByte(),
-                box86Version = PrefManager.box86Version,
-                box64Version = PrefManager.box64Version,
-                box86Preset = PrefManager.box86Preset,
-                box64Preset = PrefManager.box64Preset,
-                desktopTheme = WineThemeManager.DEFAULT_DESKTOP_THEME,
-                language = PrefManager.containerLanguage,
-                containerVariant = PrefManager.containerVariant,
-                wineVersion = PrefManager.wineVersion,
-                emulator = PrefManager.emulator,
-                fexcoreVersion = PrefManager.fexcoreVersion,
-                fexcoreTSOMode = PrefManager.fexcoreTSOMode,
-                fexcoreX87Mode = PrefManager.fexcoreX87Mode,
-                fexcoreMultiBlock = PrefManager.fexcoreMultiBlock,
-                fexcorePreset = PrefManager.fexcorePreset,
-                renderer = PrefManager.renderer,
-                csmt = PrefManager.csmt,
-                videoPciDeviceID = PrefManager.videoPciDeviceID,
-                offScreenRenderingMode = PrefManager.offScreenRenderingMode,
-                strictShaderMath = PrefManager.strictShaderMath,
-                useDRI3 = PrefManager.useDRI3,
-                videoMemorySize = PrefManager.videoMemorySize,
-                mouseWarpOverride = PrefManager.mouseWarpOverride,
-                enableXInput = PrefManager.xinputEnabled,
-                enableDInput = PrefManager.dinputEnabled,
-                dinputMapperType = PrefManager.dinputMapperType.toByte(),
-                disableMouseInput = PrefManager.disableMouseInput,
-                forceDlc = PrefManager.forceDlc,
-                steamOfflineMode = PrefManager.steamOfflineMode,
-                useLegacyDRM = PrefManager.useLegacyDRM,
-                unpackFiles = PrefManager.unpackFiles,
-                suspendPolicy = PrefManager.suspendPolicy,
-                portraitMode = PrefManager.portraitMode,
-                externalDisplayMode = PrefManager.externalDisplayInputMode,
-                externalDisplaySwap = PrefManager.externalDisplaySwap,
-            )
-        }
+        } else {
+            getDefaultContainerData().copy(
+                drives = drives,
+                dxwrapper = initialDxWrapper,
+            )
+        }
🤖 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/ContainerUtils.kt` around lines 813 -
867, The code constructs a new ContainerData instance manually which omits
persisted default fields (e.g., localSavesOnly, useSteamInput, and
sharpness-related fields) and thus new containers miss those saved defaults;
instead, initialize from getDefaultContainerData() and then override only
differing properties (or copyInto) so ContainerData created here includes all
fields from getDefaultContainerData(), ensuring fields like localSavesOnly,
useSteamInput, and sharpness settings are preserved; adjust the creation site
where ContainerData(...) is constructed to start from getDefaultContainerData()
and apply the drives and any required overrides.
app/src/main/java/app/gamenative/utils/SteamUtils.kt (2)

256-277: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

verifyRestoredState() still false-passes when the Steam API DLLs are missing.

If a stale STEAM_DLL_RESTORED marker exists and neither steam_api.dll nor steam_api64.dll is present, this returns true and skips repair. That leaves the next launch with no Steam API DLL on disk.

♻️ Suggested fix
     private fun verifyRestoredState(context: Context, appDirPath: String): Boolean {
         return try {
             val assetHashes = pipeDllHashes(context)
+            var found = false
             Paths.get(appDirPath).toFile().walkTopDown().maxDepth(10).forEach { file ->
                 if (!file.isFile) return@forEach
                 val n = file.name.lowercase()
                 if (n == "steam_api.dll" || n == "steam_api64.dll") {
+                    found = true
                     val pipeHash = assetHashes[n]
                     if (pipeHash == null) {
                         // Missing pipe-asset hash means we can't tell whether this is the pipe
                         // DLL or the original Valve DLL — fail closed so the caller re-restores.
                         Timber.w("DLL marker desync: pipe asset hash missing for %s, can't verify restored state", n)
@@
                     }
                 }
             }
+            if (!found) {
+                Timber.w("DLL marker desync: no steam_api DLL found under %s but RESTORED marker present", appDirPath)
+                return false
+            }
             true
         } catch (e: Exception) {
             Timber.w(e, "verifyRestoredState failed, treating as desync")
             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/utils/SteamUtils.kt` around lines 256 - 277,
verifyRestoredState currently returns true when neither steam_api.dll nor
steam_api64.dll are present, which falsely signals a restored state; update
verifyRestoredState to track whether any target DLLs were encountered while
walking the directory (use the existing loop that checks file.name lowercase and
references pipeDllHashes and sha256OfFile), and if none are found at the end of
the check, log a warning and return false so the caller will re-restore the
DLLs; keep the existing desync checks intact and only change the final return
behavior based on a "found" flag.

2595-2609: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

SDK-cloud mirroring still drops nested save trees.

Both directions still copy only direct files. Games that keep saves under subdirectories inside remote/ or the install save folder will silently lose part of their state during mirror.

♻️ Suggested fix
-        remoteDir.listFiles()?.forEach { src ->
-            if (!src.isFile) return@forEach
-            val dst = File(gameSaveDir, src.name)
+        remoteDir.walkTopDown().forEach { src ->
+            if (!src.isFile) return@forEach
+            val rel = remoteDir.toPath().relativize(src.toPath())
+            val dst = gameSaveDir.toPath().resolve(rel).toFile()
             try {
+                Files.createDirectories(dst.toPath().parent)
                 Files.copy(
                     src.toPath(),
                     dst.toPath(),
                     StandardCopyOption.REPLACE_EXISTING,
                     StandardCopyOption.COPY_ATTRIBUTES,
@@
-        gameSaveDir.listFiles()?.forEach { src ->
-            if (!src.isFile) return@forEach
+        gameSaveDir.walkTopDown().forEach { src ->
+            if (!src.isFile) return@forEach
             // Skip local-only artifacts (e.g. Dead Cells writes backup-YYYY-MM-DD-N.zip snapshots
             // alongside saves; those aren't cloud-synced).
             if (src.name.startsWith("backup-") && src.name.endsWith(".zip")) return@forEach
-            val dst = File(remoteDir, src.name)
+            val rel = gameSaveDir.toPath().relativize(src.toPath())
+            val dst = remoteDir.toPath().resolve(rel).toFile()
             try {
+                Files.createDirectories(dst.toPath().parent)
                 Files.copy(
                     src.toPath(),
                     dst.toPath(),
                     StandardCopyOption.REPLACE_EXISTING,
                     StandardCopyOption.COPY_ATTRIBUTES,

Also applies to: 2619-2636

🤖 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/SteamUtils.kt` around lines 2595 -
2609, The mirror loop only copies direct files and skips nested directories,
causing loss of saves; replace the per-file Files.copy logic in the
remoteDir.listFiles()?.forEach block (and the corresponding block around lines
2619-2636) with a recursive copy that preserves directory structure and
attributes (e.g., use Kotlin's File.copyRecursively(dst, overwrite = true) or a
Files.walkFileTree-based copier) so subdirectories and their files are mirrored
too; ensure you still skip non-regular entries appropriately, preserve
attributes (equivalent to StandardCopyOption.COPY_ATTRIBUTES), and keep the
Timber.i / Timber.w logging for each copied file or directory so failures are
reported per-item.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/FileUtils.kt (1)

136-163: ⚡ Quick win

Add focused tests for the anchoring and overlap cases.

This matcher is now the shared implementation behind both search APIs, and the prefix/suffix overlap rules are easy to regress. A small table-driven test set for cases like ab*bc vs abc, *foo*foo*, and exact no-* matches would lock the behavior down.

🤖 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/FileUtils.kt` around lines 136 - 163,
Add focused, table-driven unit tests for matchesGlob to cover anchoring and
overlap rules: verify exact-match behavior when pattern contains no '*' (e.g.,
"abc" vs "abc" and "ABC"), verify non-overlap for patterns like "ab*bc" against
"abc" and "abxbc", verify trailing-anchor overlap rejection when prefix consumes
into the last token, and test repeated tokens like "*foo*foo*" and variants;
exercise hasLeadingStar/hasTrailingStar paths and middle-token scanning by
calling matchesGlob with these patterns and asserting true/false expected
outcomes.
🤖 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 811-823: The code always calls relaunch() (which sets
skipBridgePrompt = true) even when no recommendation was found or persisting
failed; change the flow so relaunch only proceeds with skipBridgePrompt=true
after a successful persistence of the chosen subdir: call
SteamUtils.getRecommendedSdkCloudSaveSubdirAsync and if it returns non-null
attempt to get the Container via ContainerUtils.getContainer(context,
state.launchedAppId) and call container.saveData(); only if container.saveData()
completes without throwing (or returns an explicit success) then call relaunch()
with skipBridgePrompt=true; otherwise either call relaunch() without
skipBridgePrompt or pass the chosen subdir in-memory into relaunch so the
one-time launch uses the intended subdir without marking the bridge enabled
permanently (reference symbols:
SteamUtils.getRecommendedSdkCloudSaveSubdirAsync, ContainerUtils.getContainer,
container.sdkCloudSaveSubdir, container.saveData, relaunch,
state.launchedAppId).
- Around line 1641-1648: The bridge dialog gate in preLaunchApp() allows the
SDK-cloud prompt even when re-entered with preferredSave or
ignorePendingOperations set, causing relaunch() to drop those choices; update
the conditional that checks skipBridgePrompt, bootToContainer,
useTemporaryOverride, skipCloudSync, and gameSource == GameSource.STEAM to also
require that preferredSave and ignorePendingOperations are not already set
(i.e., skip the prompt when either preferredSave or ignorePendingOperations is
true), so the bridge dialog is not shown on conflict/pending-op relaunches and
user choices are preserved.

In `@app/src/main/java/app/gamenative/utils/SteamUtils.kt`:
- Around line 1107-1117: The early-return reuse logic in the block that calls
parseAcfBuildId, parseAcfInstalledDepotIds, parseAcfInstallScriptDepotIds and
parseAcfUpdateResult ignores the manifest's LastOwner, allowing a stale
appmanifest with a different Steam account to be reused; update the condition to
also compare the manifest LastOwner (e.g. parseAcfLastOwner(staleAcf)) against
the current/shared last-owner value (the same source used for
sharedBuildId/sharedDepotIds), and only allow the early return (and skip
staleAcf.delete()) when depotsMatch, buildIdMatch, scriptsMatch AND
lastOwnerMatch; add the parseAcfLastOwner call and the lastOwnerMatch boolean to
the same if-check before returning.

---

Outside diff comments:
In `@app/src/main/java/app/gamenative/utils/SteamUtils.kt`:
- Around line 595-612: The code may fail when writing loginusers.vdf because the
steamConfigDir doesn't exist; before calling File(steamConfigDir,
"loginusers.vdf").writeText(...) ensure the directory exists by creating
steamConfigDir (e.g., call mkdirs()/createDirectories on steamConfigDir) and
check its success, then proceed to write; keep the existing try/catch around the
file and registry operations (winePrefixBase, steamConfigDir, loginusers.vdf,
user.reg, WineRegistryEditor) so failures are still logged.

---

Duplicate comments:
In `@app/src/main/java/app/gamenative/utils/ContainerUtils.kt`:
- Around line 813-867: The code constructs a new ContainerData instance manually
which omits persisted default fields (e.g., localSavesOnly, useSteamInput, and
sharpness-related fields) and thus new containers miss those saved defaults;
instead, initialize from getDefaultContainerData() and then override only
differing properties (or copyInto) so ContainerData created here includes all
fields from getDefaultContainerData(), ensuring fields like localSavesOnly,
useSteamInput, and sharpness settings are preserved; adjust the creation site
where ContainerData(...) is constructed to start from getDefaultContainerData()
and apply the drives and any required overrides.

In `@app/src/main/java/app/gamenative/utils/SteamUtils.kt`:
- Around line 256-277: verifyRestoredState currently returns true when neither
steam_api.dll nor steam_api64.dll are present, which falsely signals a restored
state; update verifyRestoredState to track whether any target DLLs were
encountered while walking the directory (use the existing loop that checks
file.name lowercase and references pipeDllHashes and sha256OfFile), and if none
are found at the end of the check, log a warning and return false so the caller
will re-restore the DLLs; keep the existing desync checks intact and only change
the final return behavior based on a "found" flag.
- Around line 2595-2609: The mirror loop only copies direct files and skips
nested directories, causing loss of saves; replace the per-file Files.copy logic
in the remoteDir.listFiles()?.forEach block (and the corresponding block around
lines 2619-2636) with a recursive copy that preserves directory structure and
attributes (e.g., use Kotlin's File.copyRecursively(dst, overwrite = true) or a
Files.walkFileTree-based copier) so subdirectories and their files are mirrored
too; ensure you still skip non-regular entries appropriately, preserve
attributes (equivalent to StandardCopyOption.COPY_ATTRIBUTES), and keep the
Timber.i / Timber.w logging for each copied file or directory so failures are
reported per-item.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/FileUtils.kt`:
- Around line 136-163: Add focused, table-driven unit tests for matchesGlob to
cover anchoring and overlap rules: verify exact-match behavior when pattern
contains no '*' (e.g., "abc" vs "abc" and "ABC"), verify non-overlap for
patterns like "ab*bc" against "abc" and "abxbc", verify trailing-anchor overlap
rejection when prefix consumes into the last token, and test repeated tokens
like "*foo*foo*" and variants; exercise hasLeadingStar/hasTrailingStar paths and
middle-token scanning by calling matchesGlob with these patterns and asserting
true/false expected outcomes.
🪄 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: 14017118-f581-4a2b-9382-53273867a491

📥 Commits

Reviewing files that changed from the base of the PR and between 021df35 and 6be9db1.

📒 Files selected for processing (32)
  • app/src/main/assets/box86_64/lightsteam.box64rc
  • app/src/main/assets/box86_64/ultralightsteam.box64rc
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/app/gamenative/ui/enums/DialogType.kt
  • app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/app/gamenative/utils/FileUtils.kt
  • app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
  • app/src/main/java/app/gamenative/utils/PreInstallSteps.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/contents/AdrenotoolsManager.java
  • app/src/main/java/com/winlator/renderer/GLRenderer.java
  • app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
  • app/src/main/java/com/winlator/xserver/WindowManager.java
  • app/src/main/java/com/winlator/xserver/extensions/DRI3Extension.java
  • app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (14)
  • app/src/main/assets/box86_64/ultralightsteam.box64rc
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/assets/box86_64/lightsteam.box64rc
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/utils/PreInstallSteps.kt
  • app/src/main/java/app/gamenative/ui/enums/DialogType.kt
  • app/src/main/res/values/strings.xml
  • app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java
  • app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java
  • app/src/main/java/com/winlator/xserver/WindowManager.java
  • app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
🚧 Files skipped from review as they are similar to previous changes (10)
  • app/src/main/java/com/winlator/contents/AdrenotoolsManager.java
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
  • app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/renderer/GLRenderer.java
  • app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/utils/SteamTokenLogin.kt

Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt
Comment thread app/src/main/java/app/gamenative/utils/SteamUtils.kt
TideGear added 2 commits May 2, 2026 22:47
Set submodule.app/src/main/cpp/lsfg-vk-android.ignore=dirty in .gitmodules
so git/GitHub Desktop stops flagging the parent repo as having uncommitted
changes whenever the submodule's working tree is dirty.

The dirtiness comes from a chain three levels deep: lsfg-vk-android pulls
in thirdparty/pe-parse, which pulls in tests/assets/corkami-poc-dataset.
The corkami dataset is a collection of intentionally-weird PE files used
to test pe-parse — Windows Defender flags some of them as PUA and
quarantines them, leaving the submodule with deleted files. Those files
aren't consumed by our build (they're pe-parse's own test fixtures), so
hiding the working-tree dirtiness is the right call. The submodule's
commit pin is still tracked normally; only the 'this submodule has
local modifications' notice is suppressed.
PluviaMain SDK-cloud-bridge dialog
- relaunch() now takes a suppressPrompt flag; only set skipBridgePrompt=true
  when persistence actually succeeded. Previous behavior unconditionally
  suppressed the prompt for the next launch even when container.saveData()
  threw and the subdir wasn't actually saved — bridge ended up "off but
  prompt hidden" instead of either fully on or normal.
- Tightened the prompt-fire gate to also skip when preferredSave != None
  or ignorePendingOperations is true. Those flags come from conflict-
  resolution dialogs; firing the bridge prompt during a conflict re-entry
  would drop the user's chosen save / dismiss-pending choice via the
  relaunch lambda.

SteamUtils.writeSharedAppManifest
- Added LastOwner to the early-return reuse criteria. Previously,
  depots/buildId/scripts could all match while the existing acf was
  attributed to a different signed-in account; reusing it would leave
  the manifest pointing at a stale LastOwner and break cloud/ownership
  checks for the current user. Force-rewrite when the owner doesn't
  match. New helper parseAcfLastOwner() and acfLastOwnerRegex.

SteamUtils.autoLoginUserChanges (loginusers.vdf)
- Ensure the Steam config dir exists before writeText(). On a fresh
  container the Wine prefix skeleton is laid down but Steam's own
  drive_c/Program Files (x86)/Steam/config/ subdir hasn't been
  populated yet — without mkdirs() the write throws
  FileNotFoundException and the auto-login fails silently.

ContainerUtils.createNewContainer
- Replace the open-coded ContainerData(...) construction in the default
  branch with getDefaultContainerData().copy(drives = drives,
  dxwrapper = initialDxWrapper). The manual builder was missing several
  fields that getDefaultContainerData() carries — localSavesOnly,
  useSteamInput, sharpnessEffect/Level/Denoise, vibrationMode/Intensity,
  lsfgEnabled — so new containers inherited Kotlin defaults instead of
  the user's PrefManager defaults. Inheriting through copy() also means
  future fields don't need to be added in two places.

Findings flagged but not changed:
- verifyRestoredState true on no DLLs: the function is meant to
  detect "DLLs are NOT the pipe-DLL" (i.e., they're the original
  Valve DLLs or absent). Returning false on absent would force
  re-extraction of pipe DLLs into game folders that don't ship them,
  which is wrong for non-Steamworks games.
- SDK cloud mirror non-recursive (4th time): by design for Dead Cells
  flat saves; recursive expansion is scope creep.
- matchesGlob unit-test nitpick: skipping; no test infra changes.
@TideGear TideGear marked this pull request as ready for review May 4, 2026 03:53
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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)

2605-2657: ⚡ Quick win

SDK-cloud mirroring only handles flat directories.

Both mirrorSdkCloudRemoteToSave and mirrorSdkCloudSaveToRemote iterate with listFiles() and skip non-files, meaning any subdirectory structure under remote/ or the save subdir is silently ignored. Games that store saves in nested directories would lose part of their state during the mirror.

If flat-directory-only is an intentional design constraint (matching the "single-component only" subdir validation), consider documenting this limitation in the function comments. Otherwise, replacing listFiles() with walkTopDown() and preserving relative paths would handle nested structures.

♻️ Suggested recursive copy (if nested support is desired)
 fun mirrorSdkCloudRemoteToSave(context: Context, appId: Int) {
     val gameSaveDir = sdkCloudGameSaveDir(context, appId) ?: return
     val remoteDir = sdkCloudRemoteDir(context, appId) ?: return

     if (!remoteDir.exists()) {
         Timber.i("mirrorSdkCloudRemoteToSave: remote/ missing for appId=$appId")
         return
     }
     if (!gameSaveDir.exists()) gameSaveDir.mkdirs()

-    remoteDir.listFiles()?.forEach { src ->
-        if (!src.isFile) return@forEach
-        val dst = File(gameSaveDir, src.name)
+    remoteDir.walkTopDown().forEach { src ->
+        if (!src.isFile) return@forEach
+        val rel = remoteDir.toPath().relativize(src.toPath())
+        val dst = gameSaveDir.toPath().resolve(rel).toFile()
         try {
+            Files.createDirectories(dst.toPath().parent)
             Files.copy(
                 src.toPath(),
                 dst.toPath(),
                 StandardCopyOption.REPLACE_EXISTING,
                 StandardCopyOption.COPY_ATTRIBUTES,
             )
🤖 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/SteamUtils.kt` around lines 2605 -
2657, The current mirrorSdkCloudRemoteToSave and mirrorSdkCloudSaveToRemote only
copy top-level files (using listFiles()) and thus ignore nested directories;
change both to recursively traverse their source directories (use
File.walkTopDown() or walk()) and for each visited file preserve its relative
path when creating the destination (create intermediate directories with mkdirs
before copying). Keep existing filters (skip non-files when copying, the
"backup-*.zip" rule in mirrorSdkCloudSaveToRemote), and retain copy options
(REPLACE_EXISTING, COPY_ATTRIBUTES) and logging/error handling, but compute dst
paths by resolving the file's path relative to the source root so nested
directories are mirrored correctly.
🤖 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 795-850: The dialog dismissal logic is reopening the prompt
because relaunch(false) keeps skipBridgePrompt=false; change the call sites so
the current relaunch always suppresses the prompt regardless of whether
persistence succeeded: in the onConfirmClick continuation (after computing
persisted), call relaunch(true) for the immediate launch; in onDismissClick,
onActionClick (after attempting to persist the "sdkCloudBridgePromptDismissed"
flag), and onDismissRequest call relaunch(true). Keep the existing
persistence/write-to-container code (ContainerUtils.getContainer,
container.saveData(), etc.) unchanged so future launches still respect persisted
fields, but always pass true into the relaunch lambda for the current attempt.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/SteamUtils.kt`:
- Around line 2605-2657: The current mirrorSdkCloudRemoteToSave and
mirrorSdkCloudSaveToRemote only copy top-level files (using listFiles()) and
thus ignore nested directories; change both to recursively traverse their source
directories (use File.walkTopDown() or walk()) and for each visited file
preserve its relative path when creating the destination (create intermediate
directories with mkdirs before copying). Keep existing filters (skip non-files
when copying, the "backup-*.zip" rule in mirrorSdkCloudSaveToRemote), and retain
copy options (REPLACE_EXISTING, COPY_ATTRIBUTES) and logging/error handling, but
compute dst paths by resolving the file's path relative to the source root so
nested directories are mirrored correctly.
🪄 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: 5b0d164b-b580-4b1a-9437-d0c559faa68e

📥 Commits

Reviewing files that changed from the base of the PR and between 6be9db1 and ffcac75.

📒 Files selected for processing (4)
  • .gitmodules
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
✅ Files skipped from review due to trivial changes (1)
  • .gitmodules

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.

7 issues found across 33 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/utils/LudusaviRegistry.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt:100">
P2: Fetched manifest data is cached without validating that the parser produced any usable entries, so a silent parse drift can poison both disk and memory caches with an empty dataset.</violation>
</file>

<file name="app/src/main/java/com/winlator/xserver/WindowManager.java">

<violation number="1" location="app/src/main/java/com/winlator/xserver/WindowManager.java:193">
P1: Heuristic leak cleanup can destroy legitimate unmapped windows from the same client because it uses only broad attributes (empty WM_CLASS, same size, unmapped) instead of a proven leak marker.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/SteamUtils.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/SteamUtils.kt:256">
P1: `verifyRestoredState()` returns true even when no Steam API DLL is present, so a stale RESTORED marker can skip `putBackSteamDlls()` and leave the app directory without required DLLs.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/PluviaMain.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/PluviaMain.kt:833">
P1: Skip/dismiss relaunches preLaunchApp without suppressing the prompt, causing the same SDK cloud bridge dialog to reappear instead of continuing the launch.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamService.kt:2441">
P1: Real-Steam cross-device pending ops are detected but never propagated into `PendingOperations`, so the conflict UI cannot run.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:239">
P2: Skipping the remote-cache rewrite when the SteamUserData set is empty leaves stale `remotecache.vdf` data behind.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt:60">
P1: Container-wide VC redist marker short-circuits later games and can skip needed per-game redistributable installs.</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/xserver/WindowManager.java
Comment thread app/src/main/java/app/gamenative/utils/SteamUtils.kt
Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt Outdated
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment thread app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt Outdated
Comment thread app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
Comment thread app/src/main/java/app/gamenative/service/SteamAutoCloud.kt Outdated
Resolves PR utkarshdalal#1347 conflicts after upstream picked up:
- 0592ae0: refactored Wine process snapshot into WineProcessSnapshotHelper
- 45c9f0d: extracted hasStoredCredentials, awaitSteamLogin

XServerScreen.kt: kept this PR's requestWineProcessSnapshot/isSteamExeAlive/
awaitSteamShutdown helpers (still used by graceful Steam exit at line 1303);
upstream's QuickMenu refactor to WineProcessSnapshotHelper.readFromProc and
onEndWineProcess callback merged cleanly.

SteamUtils.kt: kept per-container .wine prefix routing on autoLoginUserChanges
and setupLightweightSteamConfig (avoids xuser-symlink races during concurrent
launches), and adopted upstream's offline steamId64 fallback chain plus the
Steam3 accountID fix for userdata path (matches restoreSteamApi).

PluviaMain.kt: dropped duplicate SteamUtils import auto-merge produced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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/ui/screen/xserver/XServerScreen.kt (3)

1291-1321: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a non-cancellation fallback so first-tap exit can’t become a no-op.

Only CancellationException is handled. If winHandler.exec(...) or awaitSteamShutdown(...) throws any other exception, exit(...) is skipped.

Suggested fix
                 gracefulExitJob = CoroutineScope(Dispatchers.Main).launch {
                     try {
                         if (shutdownSteam) {
@@
                             }
                             exit(winHandler, frameRating, currentAppInfo, container, appId, onExit, navigateBack)
-                        } catch (_: kotlinx.coroutines.CancellationException) {
-                            // Force-quit path already called exit().
+                        } catch (t: Throwable) {
+                            if (t is kotlinx.coroutines.CancellationException) {
+                                // Force-quit path already called exit().
+                                throw t
+                            }
+                            Timber.w(t, "Graceful exit failed, falling back to immediate exit")
+                            exit(winHandler, frameRating, currentAppInfo, container, appId, onExit, navigateBack)
                         } finally {
                             steamShutdownDialogResolver?.let { if (!it.isCompleted) it.cancel() }
                             steamShutdownDialogResolver = null
                         }
                     }
🤖 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/xserver/XServerScreen.kt` around
lines 1291 - 1321, The coroutine launched into gracefulExitJob currently only
catches CancellationException so any other exception from winHandler.exec(...)
or awaitSteamShutdown(...) will skip calling exit(...); modify the
try/catch/finally in the CoroutineScope launched block (gracefulExitJob) so that
non-CancellationException errors are caught (e.g., catch Throwable or catch
Exception) and still ensure exit(winHandler, frameRating, currentAppInfo,
container, appId, onExit, navigateBack) is invoked (either by moving that call
into a finally block or by calling it from the non-cancellation catch), and
still preserve the existing CancellationException behavior and cleanup of
steamShutdownDialogResolver.

3245-3247: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reapply WINEPREFIX after merging container.envVars.

envVars.putAll(container.envVars) can override the per-container prefix set earlier (Line 3135), which undermines container isolation when persisted env contains WINEPREFIX.

Suggested fix
         envVars.putAll(container.envVars)
+        envVars.put("WINEPREFIX", imageFs.wineprefix)
         if (!envVars.has("WINEESYNC")) envVars.put("WINEESYNC", "1")
🤖 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/xserver/XServerScreen.kt` around
lines 3245 - 3247, The merge using envVars.putAll(container.envVars) can
overwrite the per-container WINEPREFIX set earlier; after calling
envVars.putAll(container.envVars) reapply the intended per-container prefix by
setting envVars.put("WINEPREFIX", <the-per-container-prefix-variable>) if that
prefix variable is non-null (or otherwise ensure the originally established
WINEPREFIX value is preserved), and then keep the existing WINEESYNC check
(i.e., ensure envVars.has("WINEESYNC") still results in
envVars.put("WINEESYNC","1") when absent); reference envVars, container.envVars,
and WINEPREFIX to locate where to reapply the prefix.

313-353: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Process snapshot polling still races by swapping a shared OnGetProcessInfoListener.

awaitSteamShutdown() still installs/removes a global listener every poll, which can clobber startExitWatchForUnmappedGameWindow() (same listener slot) and make shutdown detection nondeterministic.

Also applies to: 360-382

🤖 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/xserver/XServerScreen.kt` around
lines 313 - 353, The listener swap in requestWineProcessSnapshot (using
winHandler.setOnGetProcessInfoListener / getOnGetProcessInfoListener) races with
other code that uses the same global slot (e.g., awaitSteamShutdown and
startExitWatchForUnmappedGameWindow); fix by avoiding install/remove of the
global listener on each poll—either add support for multiple listeners on
WinHandler (addOnGetProcessInfoListener/removeOnGetProcessInfoListener) or, if
you cannot change WinHandler, install a single persistent composite listener
once that forwards events to registered snapshot collectors; update
requestWineProcessSnapshot to register itself with that multi-listener mechanism
(or register with the composite) instead of replacing previousListener, and
remove only its own registration when done so it no longer clobbers other
watchers.
app/src/main/java/app/gamenative/ui/PluviaMain.kt (1)

800-855: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip/dismiss still traps the user in the same bridge prompt.

relaunch(false) keeps skipBridgePrompt = false, so the next preLaunchApp() call immediately re-hits the gate at Line 1655 and shows this dialog again. That makes Skip / outside-dismiss non-functional, and Enable / Don't ask again loop too whenever persistence fails. Suppress the prompt for the current relaunch attempt regardless of persistence; let persistence control only future launches.

Possible fix
-            val relaunch = { suppressPrompt: Boolean ->
+            val relaunch = {
                 preLaunchApp(
                     context = context,
                     appId = state.launchedAppId,
-                    // Only suppress the prompt next time when we actually persisted a subdir
-                    // (or the user picked "Don't ask again"). If persistence failed, leave
-                    // the prompt enabled so it fires again on the next attempt rather than
-                    // silently disabling itself in memory.
-                    skipBridgePrompt = suppressPrompt,
+                    // Suppress only this immediate relaunch. Future launches should still be
+                    // controlled by the persisted container fields.
+                    skipBridgePrompt = true,
                     setLoadingDialogVisible = viewModel::setLoadingDialogVisible,
                     setLoadingProgress = viewModel::setLoadingDialogProgress,
                     setLoadingMessage = viewModel::setLoadingDialogMessage,
                     setMessageDialogState = setMessageDialogState,
                     onSuccess = viewModel::launchApp,
                     isOffline = viewModel.isOffline.value,
                 )
             }
@@
-                    withContext(Dispatchers.Main) { relaunch(persisted) }
+                    withContext(Dispatchers.Main) { relaunch() }
@@
-                relaunch(false)
+                relaunch()
@@
-                    withContext(Dispatchers.Main) { relaunch(persisted) }
+                    withContext(Dispatchers.Main) { relaunch() }
@@
-                relaunch(false)
+                relaunch()
🤖 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 800 - 855,
The bridge-prompt suppression flag is being used for the current relaunch
instead of only for future launches: change calls to relaunch so the current run
always suppresses the prompt (pass true) while keeping persistence logic
unchanged to control future behavior; specifically, update onDismissClick and
onDismissRequest to call relaunch(true) and change the withContext(...)
callbacks in onConfirmClick and onActionClick to call relaunch(true) (leave
ContainerUtils.getContainer/putExtra/saveData and the persisted boolean logic
as-is so persistence still determines future suppression).
🤖 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/ui/PluviaMain.kt`:
- Around line 800-855: The bridge-prompt suppression flag is being used for the
current relaunch instead of only for future launches: change calls to relaunch
so the current run always suppresses the prompt (pass true) while keeping
persistence logic unchanged to control future behavior; specifically, update
onDismissClick and onDismissRequest to call relaunch(true) and change the
withContext(...) callbacks in onConfirmClick and onActionClick to call
relaunch(true) (leave ContainerUtils.getContainer/putExtra/saveData and the
persisted boolean logic as-is so persistence still determines future
suppression).

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 1291-1321: The coroutine launched into gracefulExitJob currently
only catches CancellationException so any other exception from
winHandler.exec(...) or awaitSteamShutdown(...) will skip calling exit(...);
modify the try/catch/finally in the CoroutineScope launched block
(gracefulExitJob) so that non-CancellationException errors are caught (e.g.,
catch Throwable or catch Exception) and still ensure exit(winHandler,
frameRating, currentAppInfo, container, appId, onExit, navigateBack) is invoked
(either by moving that call into a finally block or by calling it from the
non-cancellation catch), and still preserve the existing CancellationException
behavior and cleanup of steamShutdownDialogResolver.
- Around line 3245-3247: The merge using envVars.putAll(container.envVars) can
overwrite the per-container WINEPREFIX set earlier; after calling
envVars.putAll(container.envVars) reapply the intended per-container prefix by
setting envVars.put("WINEPREFIX", <the-per-container-prefix-variable>) if that
prefix variable is non-null (or otherwise ensure the originally established
WINEPREFIX value is preserved), and then keep the existing WINEESYNC check
(i.e., ensure envVars.has("WINEESYNC") still results in
envVars.put("WINEESYNC","1") when absent); reference envVars, container.envVars,
and WINEPREFIX to locate where to reapply the prefix.
- Around line 313-353: The listener swap in requestWineProcessSnapshot (using
winHandler.setOnGetProcessInfoListener / getOnGetProcessInfoListener) races with
other code that uses the same global slot (e.g., awaitSteamShutdown and
startExitWatchForUnmappedGameWindow); fix by avoiding install/remove of the
global listener on each poll—either add support for multiple listeners on
WinHandler (addOnGetProcessInfoListener/removeOnGetProcessInfoListener) or, if
you cannot change WinHandler, install a single persistent composite listener
once that forwards events to registered snapshot collectors; update
requestWineProcessSnapshot to register itself with that multi-listener mechanism
(or register with the composite) instead of replacing previousListener, and
remove only its own registration when done so it no longer clobbers other
watchers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3dadb8a0-9c03-447c-b049-973490e42cc6

📥 Commits

Reviewing files that changed from the base of the PR and between ffcac75 and 41edf45.

📒 Files selected for processing (5)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt

LudusaviRegistry: refuse to cache an empty parse result; fall through to the
stale-disk fallback instead. Guards against silent schema drift in the upstream
manifest poisoning both caches with a no-data result for CACHE_TTL_MS.

WindowManager.reapLeakedClientWindows: tighten the orphan-chain predicate to
match the compositor's signature from dd3987b (blank WM_CLASS, _NET_WM_PID==0,
no WM_NAME, no children, parent is non-root with blank className+pid==0). The
prior heuristic could destroy a legitimate unmapped popup from the same client
that happened to share size and lack WM_CLASS at create time.

SteamUtils.verifyRestoredState: fail closed when no steam_api*.dll is present
under appDir. Mirrors the existing verifyReplacedState `found` guard so a
stale RESTORED marker on a relocated/wiped game dir lets putBackSteamDlls
re-attempt the restore on next launch.

SteamService.beginLaunchApp: propagate detected real-Steam cross-device pending
ops into PostSyncInfo(SyncResult.PendingOperations, …) so the conflict dialog
in PluviaMain actually fires. Same-device phantom auto-dismiss is unchanged;
the kickPlayingSession call still runs before the new bail.

SteamAutoCloud.writeRemoteCache: rewrite remotecache.vdf even when the
SteamUserData entry list is empty so a wipe of local saves truncates the
manifest. Previously skipped, leaving stale entries on disk.

VcRedistStep: replace the container-wide .vcredist_installed marker with
per-year sidecars (_2005…_2022 plus _legacy). appliesTo only short-circuits
when every required year is already covered; buildCommand skips already-
installed years; one-shot legacy-marker migration converts the old marker on
first encounter. Tests rewritten.

PluviaMain SDK bridge prompt: every dialog handler now calls relaunch(true) so
the current relaunch always suppresses; persistence (sdkCloudSaveSubdir,
sdkCloudBridgePromptDismissed) still gates future launches via the
preLaunchApp gate. The previous relaunch(persisted) / relaunch(false) calls
re-fired the dialog when persistence failed or on Skip/Dismiss.

XServerScreen graceful Steam exit: fall through to exit() on non-cancellation
throwables from winHandler.exec()/awaitSteamShutdown() instead of leaving the
game process stuck.

XServerScreen env merge: re-pin envVars["WINEPREFIX"] = imageFs.wineprefix
after envVars.putAll(container.envVars) so a stale WINEPREFIX baked into
container.envVars doesn't redirect Wine to the wrong prefix.

WinHandler + XServerScreen process-info listener: add multi-listener support
(addOnGetProcessInfoListener / removeOnGetProcessInfoListener, backed by a
CopyOnWriteArrayList) and migrate requestWineProcessSnapshot and
startExitWatchForUnmappedGameWindow off the single-slot setter so concurrent
shutdown polling and unmapped-window watching no longer trample each other's
listener registration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

4 issues found across 11 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/utils/preInstallSteps/VcRedistStep.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt:206">
P1: VC redistributable markers are too coarse: x86 and x64 installers for the same year share one key, so installing one architecture can incorrectly skip the other.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt:211">
P1: All yearless VC redist payloads are collapsed to a single `legacy` marker, so installing one can incorrectly suppress other distinct x86/x64 redistributables.</violation>
</file>

<file name="app/src/main/java/com/winlator/winhandler/WinHandler.java">

<violation number="1" location="app/src/main/java/com/winlator/winhandler/WinHandler.java:436">
P1: Process-info listener callbacks are not exception-isolated; a bad listener can terminate the worker thread and break future polling.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt:89">
P2: Stale disk-cache fallback still accepts an empty parsed map and repopulates `memoryCache`, so a poisoned `{}` cache can keep the registry empty despite the new empty-fetch guard.</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/app/gamenative/utils/preInstallSteps/VcRedistStep.kt Outdated
Comment thread app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt Outdated
Comment thread app/src/main/java/com/winlator/winhandler/WinHandler.java
Comment thread app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/winlator/winhandler/WinHandler.java (1)

199-207: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the single-string exec(...) overload compatible with quoted paths.

split(" ", 2) turns a valid command like "C:\Program Files (x86)\Steam\steam.exe" -shutdown into the wrong filename, so the legacy overload no longer works for executables whose path contains spaces. Since this overload is explicitly retained for backwards compatibility, it needs quoted-path parsing instead of a raw split.

Possible fix
 public void exec(String command) {
+    if (command == null) return;
     String command2 = command.trim();
     if (command2.isEmpty()) {
         return;
     }
-    String[] cmdList = command2.split(" ", 2);
-    final String filename = cmdList[0];
-    final String parameters = cmdList.length > 1 ? cmdList[1] : "";
+    final String filename;
+    final String parameters;
+    if (command2.startsWith("\"")) {
+        int endQuote = command2.indexOf('"', 1);
+        if (endQuote <= 1) return;
+        filename = command2.substring(1, endQuote);
+        parameters = command2.substring(endQuote + 1).trim();
+    } else {
+        String[] cmdList = command2.split(" ", 2);
+        filename = cmdList[0];
+        parameters = cmdList.length > 1 ? cmdList[1] : "";
+    }
     exec(filename, parameters);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/winlator/winhandler/WinHandler.java` around lines 199 -
207, The single-argument exec(String command) incorrectly splits paths with
spaces because it uses split(" ", 2); instead, update exec(String command) to
handle quoted paths: trim the command, if it starts with a double quote (") find
the matching closing quote and treat the quoted substring as filename and the
rest (after optional whitespace) as parameters; otherwise split on the first
whitespace as before. Keep the same variable names (command2, filename,
parameters) and call exec(filename, parameters) unchanged so backwards
compatibility is preserved.
♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

313-355: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Concurrent listProcesses() polls still race on one uncorrelated event stream.

The multi-listener change fixes slot clobbering, but app/src/main/java/com/winlator/winhandler/WinHandler.java:419-440 still broadcasts every GET_PROCESS packet to every listener without a request id. If awaitSteamShutdown() and startExitWatchForUnmappedGameWindow() poll at the same time, each listener resets on any index == 0 packet and can complete from the other caller’s snapshot. Serialize snapshot collection per WinHandler, or add a request token to the wire format before allowing concurrent polls.

🤖 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/xserver/XServerScreen.kt` around
lines 313 - 355, requestWineProcessSnapshot races with other concurrent polls
because WinHandler.listProcesses() broadcasts GET_PROCESS packets to all
listeners with no request id; fix by serializing snapshot collection per
WinHandler or by adding a request token to the wire protocol and plumbing it
through listProcesses()/OnGetProcessInfoListener. Concretely: either (A) add a
single-call mutex/lock field on WinHandler and have requestWineProcessSnapshot
acquire it around
addOnGetProcessInfoListener/listProcesses/removeOnGetProcessInfoListener to
prevent concurrent polls (affecting functions requestWineProcessSnapshot,
awaitSteamShutdown, startExitWatchForUnmappedGameWindow), or (B) extend
WinHandler.listProcesses() to accept a requestId, include that ID in the
GET_PROCESS packet from WinHandler.java broadcast logic and in the
OnGetProcessInfoListener callback signature, then have
requestWineProcessSnapshot provide a unique requestId and ignore events with
mismatched IDs so multiple listeners can coexist safely.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/PluviaMain.kt (1)

824-829: 💤 Low value

Unused persisted variable.

The persisted variable is computed but never read. While the comment explains the intent (persistence controls future launches), the variable itself is dead code. Consider either removing it or logging/acting on persistence failures more explicitly.

♻️ Optional cleanup
             onConfirmClick = {
                 // Enable: write Ludusavi's suggested subdir, then continue the launch.
                 msgDialogState = MessageDialogState(false)
                 CoroutineScope(Dispatchers.IO).launch {
                     val gameId = ContainerUtils.extractGameIdFromContainerId(state.launchedAppId)
                     val rec = runCatching {
                         SteamUtils.getRecommendedSdkCloudSaveSubdirAsync(context, gameId)
                     }.getOrNull()
-                    val persisted = rec != null && runCatching {
+                    if (rec != null) {
+                        runCatching {
-                        val container = ContainerUtils.getContainer(context, state.launchedAppId)
-                        container.sdkCloudSaveSubdir = rec.subdir
-                        container.saveData()
-                    }.onFailure { Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") }
-                        .isSuccess
+                            val container = ContainerUtils.getContainer(context, state.launchedAppId)
+                            container.sdkCloudSaveSubdir = rec.subdir
+                            container.saveData()
+                        }.onFailure { Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") }
+                    }
                     withContext(Dispatchers.Main) { relaunch(true) }
                 }
             }
🤖 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 824 - 829,
The local variable persisted is computed but never used; remove the dead
variable and either directly run the runCatching block for its side effects or
act on the result (e.g., log or handle failure). Locate the block that calls
ContainerUtils.getContainer(context, state.launchedAppId), sets
container.sdkCloudSaveSubdir = rec.subdir and calls container.saveData();
replace the assignment to persisted with a direct runCatching { ... }.onFailure
{ Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") } (or
capture the boolean and explicitly log/handle when false) so there is no unused
variable left.
🤖 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/xserver/XServerScreen.kt`:
- Around line 323-327: The current OnGetProcessInfoListener treats (count == 0
&& processInfo == null) as a successful empty snapshot and completes deferred
with emptyList(), which causes isSteamExeAlive() to return false on a
request/send failure; instead, detect this exact failure case in the listener
(the block inside synchronized(lock) that currently completes deferred with
emptyList()), do NOT complete deferred with emptyList(), and either
completeDeferred exceptionally (e.g.,
deferred.completeExceptionally(ProcessSnapshotException("request/send
failure"))) or simply log the failure and leave deferred uncompleted so the
caller can distinguish a request failure from an actual empty process list;
update references to listener/OnGetProcessInfoListener/lock/deferred and ensure
callers of isSteamExeAlive() handle the exceptional/failure path accordingly.

In `@app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt`:
- Around line 153-217: requiredVersions() and versionKey() collapse x86/x64 into
the same year key, so add an architecture tag to the key and marker names to
avoid x64 installers being skipped after an x86 marker exists: change
versionKey(winPath: String) to detect architecture (e.g., look for "x64",
"amd64", "x86", "wow64", "x86_64", or explicit filenames like
"vc_redist.x64.exe"/"vc_redist.x86.exe") and return a string like
"$year-x64"/"$year-x86" (or "legacy-x64"/"legacy-x86" for non-yeared paths);
ensure requiredVersions() uses this new versionKey result when building the
marker name (used by recordInstalledVersions()) so markers are per-year-per-arch
rather than per-year-only.
- Around line 90-92: The current return short-circuits on
MarkerUtils.hasMarker(gameDirPath, Marker.VCREDIST_INSTALLED) even when
`missing` is non-empty, hiding newly missing VC runtimes; update the logic so
that if `missing` is not empty and `container.rootDir` is available you verify
per-year/container-level VCREDIST markers (use the container root markers as the
source of truth) instead of the coarse game-dir marker — only fall back to
Marker.VCREDIST_INSTALLED on the gameDirPath when `container.rootDir` is null or
when no per-year markers exist; adjust the return in the function containing
`missing`, `container.rootDir`, and the current MarkerUtils.hasMarker call to
reflect this conditional check.

In `@app/src/main/java/com/winlator/xserver/WindowManager.java`:
- Around line 225-237: Add a parent-size guard to the orphan reaper checks:
after obtaining candParent in the WindowManager orphan-reap logic (the block
that checks cand.getProcessId(), cand.getName(), cand.getChildren(), candParent
== null/rootWindow, candParent.getClassName(), candParent.getProcessId()),
verify candParent's size is exactly 1x1 (e.g., check width and height == 1 via
the parent window size accessors) and skip reaping unless that predicate holds;
place this check immediately after the candParent null/root check so the reaper
only targets children under a 1x1 blank-class/pid=0 orphanage.

---

Outside diff comments:
In `@app/src/main/java/com/winlator/winhandler/WinHandler.java`:
- Around line 199-207: The single-argument exec(String command) incorrectly
splits paths with spaces because it uses split(" ", 2); instead, update
exec(String command) to handle quoted paths: trim the command, if it starts with
a double quote (") find the matching closing quote and treat the quoted
substring as filename and the rest (after optional whitespace) as parameters;
otherwise split on the first whitespace as before. Keep the same variable names
(command2, filename, parameters) and call exec(filename, parameters) unchanged
so backwards compatibility is preserved.

---

Duplicate comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 313-355: requestWineProcessSnapshot races with other concurrent
polls because WinHandler.listProcesses() broadcasts GET_PROCESS packets to all
listeners with no request id; fix by serializing snapshot collection per
WinHandler or by adding a request token to the wire protocol and plumbing it
through listProcesses()/OnGetProcessInfoListener. Concretely: either (A) add a
single-call mutex/lock field on WinHandler and have requestWineProcessSnapshot
acquire it around
addOnGetProcessInfoListener/listProcesses/removeOnGetProcessInfoListener to
prevent concurrent polls (affecting functions requestWineProcessSnapshot,
awaitSteamShutdown, startExitWatchForUnmappedGameWindow), or (B) extend
WinHandler.listProcesses() to accept a requestId, include that ID in the
GET_PROCESS packet from WinHandler.java broadcast logic and in the
OnGetProcessInfoListener callback signature, then have
requestWineProcessSnapshot provide a unique requestId and ignore events with
mismatched IDs so multiple listeners can coexist safely.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Around line 824-829: The local variable persisted is computed but never used;
remove the dead variable and either directly run the runCatching block for its
side effects or act on the result (e.g., log or handle failure). Locate the
block that calls ContainerUtils.getContainer(context, state.launchedAppId), sets
container.sdkCloudSaveSubdir = rec.subdir and calls container.saveData();
replace the assignment to persisted with a direct runCatching { ... }.onFailure
{ Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") } (or
capture the boolean and explicitly log/handle when false) so there is no unused
variable left.
🪄 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: be142422-4034-4bab-adb6-487dd2d0222f

📥 Commits

Reviewing files that changed from the base of the PR and between 41edf45 and 2347384.

📒 Files selected for processing (11)
  • app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
  • app/src/main/java/app/gamenative/utils/PreInstallSteps.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/java/com/winlator/xserver/WindowManager.java
  • app/src/test/java/app/gamenative/utils/preInstallSteps/VcRedistStepTest.kt
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt

Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Comment thread app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt Outdated
Comment thread app/src/main/java/com/winlator/xserver/WindowManager.java
VcRedistStep: per-year markers now also encode architecture (`$year-x86` /
`$year-x64`, plus `legacy-x86` / `legacy-x64` for yearless paths). Without
this, an x86-only install (e.g. vc_redist.x86.exe for 2015) wrote a year-only
marker that incorrectly suppressed the matching x64 install on a later game.
Yearless redists (A:\redist\vcredist_*, A:\_CommonRedist\VC_redist.*) are
similarly distinguished. archKey() inspects the win path for x64/amd64/wow64/
x86_64 hints (and explicit .x64./.x86. filenames) and defaults to x86 for
ambiguous paths. Round-5 archless markers are migrated as `_$year-x86` only —
conservative: any actual x64 install will re-run once. appliesTo no longer
short-circuits on the coarse game-dir VCREDIST_INSTALLED marker when per-year/
arch markers indicate a missing (year, arch) pair; the game-dir marker is now
only a fallback when no per-year markers exist or containerRoot is null.

WinHandler.GET_PROCESS dispatch: each listener invocation (slot listener and
each multi-listener entry) is now wrapped in try/catch so one bad listener
can't kill the worker thread. Same fix applied to the listProcesses() send-
failure broadcast.

WinHandler.exec(String): handle quoted paths. A leading `"` triggers quoted-
path parsing (filename = quoted substring; parameters = trimmed remainder);
unquoted commands still split on the first whitespace. Without this,
"C:\Program Files (x86)\Steam\steam.exe" -shutdown shredded into a bogus
filename of "C:\Program".

XServerScreen.requestWineProcessSnapshot: distinguish request/send failure
from a legitimate empty process list. WinHandler.listProcesses() emits
(0, 0, null) only when sendPacket() fails — Wine itself never broadcasts
empty. Added ProcessSnapshotException, listener completes deferred
exceptionally on (count==0 && info==null), await maps it to null so
isSteamExeAlive(null) == true keeps the polling loop alive instead of
falsely concluding Steam exited.

WinHandler + XServerScreen: add a snapshot-collection mutex
(processSnapshotMutex). Both Kotlin pollers (requestWineProcessSnapshot and
startExitWatchForUnmappedGameWindow) now serialize their add-listener →
listProcesses → await → remove-listener sequence so concurrent listProcesses
calls don't cross-pollute each other's deferreds. Multi-listener support is
preserved for long-running watchers; only the per-iteration snapshot is
serialized.

LudusaviRegistry.ensureLoaded: extend the empty-set guard to the disk-cache
fallback. A poisoned `{}` cache file (e.g., one written before the round-5
empty-fetch guard shipped) used to populate memoryCache with emptyMap(),
after which the fast path short-circuited forever. Now: if the parsed disk
cache is empty, log and delete the poisoned file (best-effort), fall through
to null so the next ensureLoaded retries the fetch. Parser exceptions still
leave the file alone.

WindowManager.reapLeakedClientWindows: add the 1x1 parent-size predicate to
match the compositor's full orphan-chain marker (dd3987b). Without it, a
legitimate window whose parent happens to be blank-class+pid==0 but is NOT
a 1x1 orphanage stub could still be reaped.

PluviaMain SDK bridge prompt: drop the dead `persisted` boolean computed in
onConfirmClick / onActionClick (round-5 changed every relaunch(...) to
relaunch(true), so the Boolean is no longer read). Side effects and the
existing onFailure Timber.w log are preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 7 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/utils/preInstallSteps/VcRedistStep.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt:249">
P2: Legacy marker migration can drop install-state if replacement creation fails, because the old marker is deleted unconditionally.</violation>
</file>

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

if (!migrated.exists()) {
runCatching { migrated.createNewFile() }
}
runCatching { f.delete() }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Legacy marker migration can drop install-state if replacement creation fails, because the old marker is deleted unconditionally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt, line 249:

<comment>Legacy marker migration can drop install-state if replacement creation fails, because the old marker is deleted unconditionally.</comment>

<file context>
@@ -192,23 +199,55 @@ object VcRedistStep : PreInstallStep {
+            if (!migrated.exists()) {
+                runCatching { migrated.createNewFile() }
             }
+            runCatching { f.delete() }
         }
-        // Generic / yearless installers — track under a shared key so they
</file context>

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

846-896: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ignore steam.exe in the unmapped-window exit watcher for real-Steam sessions.

On real-Steam runs, Line 893 still treats steam.exe as a non-essential process. Once the game window closes normally, the snapshot can legitimately contain only Steam, so this watcher never reaches exit(...) and the session can sit on a black screen until timeout/manual intervention. Add steam to the allowlist when container.isLaunchRealSteam, or special-case it in the predicate.

Suggested fix
-            val allowlist = buildEssentialProcessAllowlist()
+            val allowlist = buildEssentialProcessAllowlist().let { base ->
+                if (container.isLaunchRealSteam) base + "steam" else base
+            }
@@
-                        val hasNonEssential = snapshot.any {
-                            !allowlist.contains(normalizeProcessName(it.name))
-                        }
+                        val hasNonEssential = snapshot.any {
+                            !allowlist.contains(normalizeProcessName(it.name))
+                        }
🤖 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/xserver/XServerScreen.kt` around
lines 846 - 896, The exit watcher treats "steam.exe" as non-essential causing
real-Steam sessions to hang; update the allowlist logic used in the snapshot
check so that when container.isLaunchRealSteam is true "steam" (normalized via
normalizeProcessName) is considered essential or special-case the predicate
computing hasNonEssential to ignore "steam" for real-Steam launches; modify the
code around allowlist, the snapshot any { ... } check (where hasNonEssential is
computed), and use container.isLaunchRealSteam to include "steam" in the
allowlist or bypass it in that predicate so the watcher will call exit(...) when
only Steam remains.
🧹 Nitpick comments (2)
app/src/test/java/app/gamenative/utils/preInstallSteps/VcRedistStepTest.kt (2)

39-59: 💤 Low value

appliesTo_returnsFalse_whenGameDirMarkerExists implicitly exercises the legacy fallback — consider a brief inline comment

With containerRoot now always non-null (wired in @Before), this test exercises the legacy game-dir marker path only because installed happens to be empty (no .vcredist_installed_* files in containerRoot). A future reader may not immediately see why the game-dir marker still "wins" here when the production code's primary branch checks container markers. A one-liner comment makes the intent explicit.

📝 Suggested clarification comment
 `@Test`
 fun appliesTo_returnsFalse_whenGameDirMarkerExists() {
+    // No container-level per-year markers exist, so the legacy game-dir marker
+    // fallback applies and short-circuits the step.
     seedInstaller()
     MarkerUtils.addMarker(gameDir.absolutePath, Marker.VCREDIST_INSTALLED)
     val applies = VcRedistStep.appliesTo(container, GameSource.STEAM, gameDir.absolutePath)
     assertFalse(applies)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/test/java/app/gamenative/utils/preInstallSteps/VcRedistStepTest.kt`
around lines 39 - 59, The test appliesTo_returnsFalse_whenGameDirMarkerExists
unintentionally exercises the legacy game-dir marker branch of
VcRedistStep.appliesTo because containerRoot is non-null in the `@Before` setup
and there are no .vcredist_installed_* files in the seeded container; add a
one-line inline comment above the MarkerUtils.addMarker call (or at the start of
the test) noting that this intentionally verifies the legacy game-dir marker
fallback (Marker.VCREDIST_INSTALLED) path even though container markers are
normally preferred by VcRedistStep.appliesTo so future readers understand why
the game-dir marker "wins".

193-203: 💤 Low value

recordInstalledVersions_writesPerYearAndArchMarkers only checks positive marker creation — no guard against spurious extra files

The test verifies the three expected markers are created, but doesn't assert that no extra (e.g., stale or duplicate) marker files appear in containerRoot. Adding a count or listFiles() check would catch a regression where recordInstalledVersions writes unexpected markers.

📝 Suggested negative assertion
     assertTrue(File(containerRoot, ".vcredist_installed_2017-x86").isFile)
     assertTrue(File(containerRoot, ".vcredist_installed_2017-x64").isFile)
     assertTrue(File(containerRoot, ".vcredist_installed_2022-x86").isFile)
+    // Confirm no spurious extra marker files were written.
+    val markers = containerRoot.listFiles { f -> f.name.startsWith(".vcredist_installed_") }
+    assertEquals(3, markers?.size)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/test/java/app/gamenative/utils/preInstallSteps/VcRedistStepTest.kt`
around lines 193 - 203, The test
recordInstalledVersions_writesPerYearAndArchMarkers currently only asserts the
expected three marker files exist but doesn't guard against extra markers;
update the test to also assert that containerRoot contains exactly three
vcredist marker files by listing files whose names start with
".vcredist_installed_" (use containerRoot.listFiles / Kotlin equivalent) and
asserting the resulting array/list size equals 3 after calling
VcRedistStep.recordInstalledVersions, keeping the existing positive assertions
(method: recordInstalledVersions_writesPerYearAndArchMarkers, subject:
VcRedistStep.recordInstalledVersions, variable: containerRoot).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 846-896: The exit watcher treats "steam.exe" as non-essential
causing real-Steam sessions to hang; update the allowlist logic used in the
snapshot check so that when container.isLaunchRealSteam is true "steam"
(normalized via normalizeProcessName) is considered essential or special-case
the predicate computing hasNonEssential to ignore "steam" for real-Steam
launches; modify the code around allowlist, the snapshot any { ... } check
(where hasNonEssential is computed), and use container.isLaunchRealSteam to
include "steam" in the allowlist or bypass it in that predicate so the watcher
will call exit(...) when only Steam remains.

---

Nitpick comments:
In `@app/src/test/java/app/gamenative/utils/preInstallSteps/VcRedistStepTest.kt`:
- Around line 39-59: The test appliesTo_returnsFalse_whenGameDirMarkerExists
unintentionally exercises the legacy game-dir marker branch of
VcRedistStep.appliesTo because containerRoot is non-null in the `@Before` setup
and there are no .vcredist_installed_* files in the seeded container; add a
one-line inline comment above the MarkerUtils.addMarker call (or at the start of
the test) noting that this intentionally verifies the legacy game-dir marker
fallback (Marker.VCREDIST_INSTALLED) path even though container markers are
normally preferred by VcRedistStep.appliesTo so future readers understand why
the game-dir marker "wins".
- Around line 193-203: The test
recordInstalledVersions_writesPerYearAndArchMarkers currently only asserts the
expected three marker files exist but doesn't guard against extra markers;
update the test to also assert that containerRoot contains exactly three
vcredist marker files by listing files whose names start with
".vcredist_installed_" (use containerRoot.listFiles / Kotlin equivalent) and
asserting the resulting array/list size equals 3 after calling
VcRedistStep.recordInstalledVersions, keeping the existing positive assertions
(method: recordInstalledVersions_writesPerYearAndArchMarkers, subject:
VcRedistStep.recordInstalledVersions, variable: containerRoot).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 125309c3-577b-47a7-b18d-7c245ef384cb

📥 Commits

Reviewing files that changed from the base of the PR and between 2347384 and b233279.

📒 Files selected for processing (7)
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/java/com/winlator/xserver/WindowManager.java
  • app/src/test/java/app/gamenative/utils/preInstallSteps/VcRedistStepTest.kt
✅ Files skipped from review due to trivial changes (2)
  • app/src/main/java/app/gamenative/utils/LudusaviRegistry.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/app/gamenative/utils/preInstallSteps/VcRedistStep.kt
  • app/src/main/java/com/winlator/xserver/WindowManager.java

@TideGear TideGear marked this pull request as draft May 4, 2026 06:07
@TideGear
Copy link
Copy Markdown
Author

TideGear commented May 4, 2026

@utkarshdalal Found a nasty bug. Seems achievements aren't working with Launch Steam Client. Working on it.

TideGear added 2 commits May 14, 2026 12:06
…m mode too

The SdkCloudSaveSubdirField was nested under `if (config.launchRealSteam)`,
and the launch-time bridge prompt in PluviaMain only fired for
isLaunchRealSteam. Pattern B games (Dead Cells etc.) hit the same
<install>/<subdir>/ vs <userdata>/<appid>/remote/ path mismatch in
bionic mode as they do in Wine-Steam mode, so the bridge should be
reachable from both. SteamAutoCloud's sync paths are already
mode-agnostic; this just exposes the configuration UI/prompt.
@TideGear TideGear marked this pull request as ready for review May 14, 2026 20:06
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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 1343-1346: The current branch calls
winHandler.killProcess(gameExe) immediately which hard-kills the game before the
GRACEFUL_EXIT_GRACE_MS delay; change it to first attempt a graceful
WM_CLOSE-style shutdown (e.g., call a new or existing
winHandler.sendWmClose/closeWindow(gameExe) or similar graceful-close method)
then await delay(GRACEFUL_EXIT_GRACE_MS) and only call
winHandler.killProcess(gameExe) after the delay if the process is still running;
update XServerScreen.kt to send the close signal on first tap and reserve
killProcess(...) as the escalation after the timeout.
- Around line 4523-4538: The cache key steamExtractedKey only uses
wineVersion|containerVariant and must also include the Steam launch mode and a
bundle/image version so extractSteamFiles(...) and payload updates are correctly
invalidated; update the code that builds steamExtractedKey to append the
container's Steam launch mode (from the container state/metadata you use to
decide bionic vs real Steam) and a bundle/image fingerprint/version (obtainable
via ImageFs.find(context) or the container's bundle/image metadata), then
continue to use container.putExtra("steamExtractedForWine", steamExtractedKey)
so future checks (container.getExtra("steamExtractedForWine")) reflect those
dimensions and force re-extraction when launch mode or bundle changes.
🪄 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: 4b408510-3217-4365-95de-8b3e4e25879f

📥 Commits

Reviewing files that changed from the base of the PR and between b233279 and 300fbf7.

📒 Files selected for processing (16)
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (14)
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt

Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Comment on lines +4523 to +4538
val steamExtractedKey = "${container.wineVersion}|${container.containerVariant}"
val steamExtractedPrev = container.getExtra("steamExtractedForWine")
val steamExeFile = File(container.rootDir, ".wine/drive_c/Program Files (x86)/Steam/steam.exe")
if (steamExtractedPrev != steamExtractedKey && steamExeFile.exists()) {
// Symlink-safe delete: steamapps/common/<installdir> symlinks point at
// GameNative's own Steam dir, and a following recursive delete would wipe
// every installed game's files.
val steamDir = File(container.rootDir, ".wine/drive_c/Program Files (x86)/Steam")
SteamUtils.deleteTreeNoFollowSymlinks(steamDir)
}
extractSteamFiles(context, container, onExtractFileListener)
SteamUtils.ensureSteamCfg(ImageFs.find(context), container)
SteamUtils.purgePhantomAppUserdata(ImageFs.find(context), "241100", container)
SteamUtils.logSteamBinaryFingerprint(ImageFs.find(context), "prepareContainer:realSteam", container)
container.putExtra("steamExtractedForWine", steamExtractedKey)
containerDataChanged = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Broaden the Steam extraction cache key.

steamExtractedKey only tracks wineVersion|containerVariant. That misses launch-mode transitions and bundle updates: switching from bionic-Steam to real-Steam on the same Wine/variant leaves the cached bionic steam.exe in place because extractSteamFiles() sees an existing binary and returns early, and updated steam.tzst payloads also won't invalidate existing installs. Include at least the Steam launch mode and a bundle/image version in the key.

Suggested fix
-        val steamExtractedKey = "${container.wineVersion}|${container.containerVariant}"
+        val steamModeKey = when {
+            container.isLaunchBionicSteam -> "bionic"
+            container.isLaunchRealSteam -> "real"
+            else -> "none"
+        }
+        val steamExtractedKey =
+            "${container.wineVersion}|${container.containerVariant}|$imgVersion|$steamModeKey"

Also applies to: 5282-5284

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around
lines 4523 - 4538, The cache key steamExtractedKey only uses
wineVersion|containerVariant and must also include the Steam launch mode and a
bundle/image version so extractSteamFiles(...) and payload updates are correctly
invalidated; update the code that builds steamExtractedKey to append the
container's Steam launch mode (from the container state/metadata you use to
decide bionic vs real Steam) and a bundle/image fingerprint/version (obtainable
via ImageFs.find(context) or the container's bundle/image metadata), then
continue to use container.putExtra("steamExtractedForWine", steamExtractedKey)
so future checks (container.getExtra("steamExtractedForWine")) reflect those
dimensions and force re-extraction when launch mode or bundle changes.

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.

2 issues found across 34 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/utils/SteamUtils.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/SteamUtils.kt:2669">
P1: SDK cloud mirroring routines `mirrorSdkCloudRemoteToSave` and `mirrorSdkCloudSaveToRemote` are non-recursive and silently skip nested directories/files, causing incomplete save restores or cloud uploads for games with nested save structures.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:1344">
P1: Non-real-Steam exit still force-kills the game on first tap. `killProcess(gameExe)` runs before the grace delay, so the app cannot execute normal shutdown/save hooks during the intended graceful window.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Comment thread app/src/main/java/app/gamenative/utils/SteamUtils.kt
Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt Outdated
TideGear added 2 commits May 14, 2026 14:16
…d mirror

Two issues raised by AI review on the merged Real-Steam branch:

1. XServerScreen.kt graceful exit (non-Steam path): killProcess ran
   immediately before the GRACEFUL_EXIT_GRACE_MS delay, defeating the
   purpose of the grace window. Swap the order so the delay precedes
   the kill, giving any in-flight game-side exit (autosave, in-game
   quit dialog) time to complete before hard-kill. Note: a proper
   WM_CLOSE escalation requires a new winhandler.exe RequestCode
   (native code change outside this commit's scope).

2. SteamUtils.kt mirrorSdkCloud{RemoteToSave,SaveToRemote} were
   non-recursive — top-level listFiles + 'is regular file' filter
   silently skipped any subdirectories. Pattern B games with nested
   save structures (slots/profiles/etc.) would have partial or no
   restore. Replaced with Files.walk + relativize + createDirectories
   for full-tree recursion. backup-*.zip filter retained on filename
   (so a backup-* zip nested in a saves hierarchy is still excluded,
   matching the pre-recursive semantics for that one exclusion).
The `shutdownSteam` flag was gated only on `isLaunchRealSteam`, so
bionic-mode exits fell through to the else branch's hard-kill (with a
synthetic delay-then-kill that doesn't actually signal the game).

Bionic Steam runs the same steam.exe in Wine — just with the native
libsteamclient.so loaded in-process. `steam.exe -shutdown` works there
identically: Steam delivers SteamShutdown_t to running games via the
SteamAPI callback, games autosave + exit cleanly, Steam then flushes
its own state.

Broaden the flag to cover bionic mode so the existing graceful path
(winHandler.exec("steam.exe -shutdown") + awaitSteamShutdown loop)
runs for both Steam modes. Emu mode keeps the delay-then-kill
fallback — no Steam to talk to there.
@utkarshdalal
Copy link
Copy Markdown
Owner

Good changes here, but the PR needs to be greatly reduced in scope. The changes that apply to the actual steam client and anything around that should be kept, everything else is really out of scope here. SteamAutoCloud.kt should not be touched - cloud saves are not enabled when real steam is. SteamService should also not be touched unless there's a reason, I see some big changes in there but don't understand what they're doing.
The Redist install steps are also not relevant here and shouldn't be touched. Ludusavi should not be added.

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.

2 participants