Skip to content

fix: skip spurious conflict when cache wiped but local matches cloud#1228

Merged
utkarshdalal merged 1 commit into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-cloud-conflict-no-change
Apr 16, 2026
Merged

fix: skip spurious conflict when cache wiped but local matches cloud#1228
utkarshdalal merged 1 commit into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-cloud-conflict-no-change

Conversation

@jeremybernstein
Copy link
Copy Markdown
Contributor

@jeremybernstein jeremybernstein commented Apr 15, 2026

Description

A destructive Room migration (e.g. when an auto-migration is missing and fallbackToDestructiveMigration fires) wipes the file_change_lists cache while leaving save files on disk intact. On next launch of any Steam game, the code detected "cache absent + local files present" and unconditionally declared a conflict, surfacing the save-conflict dialog with epoch timestamps (Date(0) on both sides). Users saw every game report a conflict after an app update, with identical dates, even though nothing had changed locally.

This PR checks whether local state is byte-identical to the cloud manifest before declaring a conflict. If it matches, we rehydrate the cache silently and report UpToDate. If it doesn't, we still declare a conflict — but now populate real timestamps.

  • Compare local vs cloud by absolute filesystem path (resilient to the fact that cloud stores (pathPrefixIndex, basename) while local scan stores subpath-relative filenames). Keys are lowercased since Windows paths are case-insensitive and wine/cloud may disagree on case.
  • On match: insert local UserFileInfos into FileChangeListsDao and the cloud change number into ChangeNumbersDao, set UpToDate, skip further work.
  • On mismatch: populate remoteTimestamp/localTimestamp from the cloud manifest + local mtimes so the dialog renders real dates instead of the epoch.

Recording

n/a — backend-only, no UI change on the happy path (dialog is suppressed when it previously fired). The user-visible effect on the conflict path is the dialog showing real dates instead of "Thu Jan 01 01:00:00 1970" on both sides.

Type of Change

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

Test plan

  • Added unit test dbCleared_localMatchesRemote_rehydratesSilently_noConflict that wipes both cache tables, asserts UpToDate + silent rehydrate. Verified it fails cleanly (fast) against origin/master.
  • Manually reproduced on-device: wipe DB files with run-as ... rm databases/pluvia.db*, relaunch, open any Steam game. Before: every game shows conflict with epoch dates. After: silent rehydrate when state actually matches; real dates when it doesn't.

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

Summary by cubic

Prevents false Steam Cloud conflicts after a destructive DB migration by verifying local saves match the cloud and silently rebuilding the cache when they do. No more blanket conflict dialogs after updates; real conflicts now show real timestamps.

  • Bug Fixes
    • When cache is empty but local files exist, compare local and cloud by absolute, case-insensitive path and SHA.
    • If identical, silently rebuild the cache and change number, mark UpToDate, and skip the dialog.
    • If different, raise a conflict and populate accurate local/remote timestamps for the dialog.

Written for commit 5021ecb. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Improved cloud save synchronization to prevent false conflict warnings when the local cache is cleared but files match the cloud version.
    • Automatically recovers cache state when files are verified as identical to remote copies.
  • Tests

    • Added test coverage for cache recovery scenarios with matching local and remote files.

destructive db migration wipes file_change_lists cache, making every
steam game trigger conflict on first launch post-update. check if
local state is byte-identical to remote manifest (by filename + SHA)
before declaring conflict; if so, rehydrate cache and report UpToDate
silently. also populates real timestamps on the genuine-divergence
path (was showing epoch).

test: dbCleared_localMatchesRemote_rehydratesSilently_noConflict
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

The sync logic now detects when the local cache is absent but local save files exist, builds case-normalized filepath maps for both local and remote files, compares their SHA hashes byte-for-byte, and silently rehydrates the cache database if they match exactly, avoiding unnecessary conflict marking.

Changes

Cohort / File(s) Summary
Cache Rehydration Logic
app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
Implements case-normalized map-based comparison of local and remote files by absolute path and SHA hash. When cache is empty but local files exist and match remote files exactly, the sync silently rehydrates the cache into the DB and marks sync as UpToDate, bypassing conflict detection. Falls back to conflict marking with timestamp population if hashes don't match.
Cache Rehydration Test
app/src/test/java/app/gamenative/service/SteamAutoCloudTest.kt
Adds comprehensive test for Scenario 16 simulating cache wipe followed by sync with matching remote files. Validates that sync result is UpToDate, no conflict dialog appears, cache is rehydrated with 5 cached entries, and stored change number matches cloud change number.

Sequence Diagram(s)

sequenceDiagram
    participant Local as Local Files
    participant Cache as Cache/Database
    participant Sync as Sync Engine
    participant Remote as Remote Cloud

    Sync->>Cache: Check if cache exists
    alt Cache missing/empty
        Sync->>Local: Read local save files
        Sync->>Remote: Fetch remote manifest
        Sync->>Sync: Build case-normalized maps<br/>(path → SHA hash)
        Sync->>Sync: Compare local vs remote<br/>byte-for-byte
        alt Files match exactly
            Sync->>Cache: Silently rehydrate cache<br/>(insert files + change number)
            Sync->>Sync: Set result: UpToDate<br/>Mark filesManaged: true
        else Files differ
            Sync->>Sync: Mark hasLocalChanges<br/>with conflict version
            Sync->>Sync: Populate timestamps
            Sync->>Sync: Trigger conflict resolution
        end
    else Cache exists
        Sync->>Sync: Use normal sync flow
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Hop, hop! A cache once lost, now found again,
When local and remote match without a pen,
Silent rehydration flows like morning dew,
No conflicts arise—they align true and true! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing a spurious conflict dialog when cache is wiped but local files match cloud.
Description check ✅ Passed The pull request description is comprehensive, well-structured, and covers all required sections of the template with substantive content.

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

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

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

No issues found across 2 files

@joshuatam
Copy link
Copy Markdown
Contributor

Conflicts Logic LGTM.

But as the save's timestamp was fixed recently by #1199, I can imagine users who using 0.9.0 may still trigger conflict dialog.

Let's say if you first run GN on 0.9.0, install a game and sync the saves once by using Force Cloud Sync (At this point the save files are having the timestamp of current device time)

After that you rebase onto this PR changes, will there be any conflict triggered?

@jeremybernstein
Copy link
Copy Markdown
Contributor Author

@joshuatam I don't think so, the changes here are keyed on path and SHA. A timestamp comparison wasn't reliable enough. If you want to test it to double-check, though, that'd be good.

Comment on lines +897 to +918
val localByPath = allLocalUserFiles.associate {
it.getAbsPath(prefixToPath).toString().lowercase() to it.sha
}
val remoteByPath = appFileListChange.files.associate {
getFullFilePath(it, appFileListChange).toString().lowercase() to it.shaFile
}
val localMatchesRemote = localByPath.keys == remoteByPath.keys &&
localByPath.all { (path, sha) ->
sha.contentEquals(remoteByPath[path])
}

if (localMatchesRemote) {
Timber.i("Cache absent but local matches remote — rehydrating cache silently")
with(steamInstance) {
db.withTransaction {
fileChangeListsDao.insert(appInfo.id, allLocalUserFiles)
changeNumbersDao.insert(appInfo.id, cloudAppChangeNumber)
}
}
syncResult = SyncResult.UpToDate
filesManaged = allLocalUserFiles.size
rehydratedSilently = true
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

we're checking the sha here twice, first here, and then when deciding whether to upload/download games. Can we reduce that duplication? Gonna happen twice on every game boot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The SHA check is gated by hasUncachedLocalFiles, will only happen after a destructive DB update, not on every boot. If hasUncachedLocalFiles == true, there is either:

  • rehydration and no 2nd SHA check, or
  • conflict dialog, which would do a 2nd SHA check via getFilesDiff or hasHashConflicts depending on the resolution.

So the potential duplication is only if there's been a destructive DB update, and only if there's a genuine conflict. I think the (significant) additional complexity for consolidation of those two checks isn't justified. Or are you referring to something else?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

ah fair you're right

@joshuatam
Copy link
Copy Markdown
Contributor

Hey @jeremybernstein & @utkarshdalal, I tested in this way:

  1. First I have a save in steamcloud, timestamp is 2026-04-16 01:50
image
  1. Checkout branch to tag 0.9.0
  2. Install Vampire Survivors
  3. Open the game once, close it
  4. The timestamp ofSaveData in <GSE Saves>/1794680/remote is 2026-04-16 19:26
  5. Checkout current master f5ca0c437adaf3e9a60f40339935d45b30afcfbb
  6. Conflict dialog appears, I cancel it by touching outside the dialiog, without choosing which to keep (below image)
  7. The timestamp ofSaveData in <SteamUserData>/1794680/remote is 2026-04-16 19:26
  8. Merge this PR changes, open the game
  9. The same conflict dialog appears, and cancel it again (same image as below)
Screenshot_20260416_194035

Summary:

  • The conflict dialog on current master is working correctly even user upgraded from 0.9.0
  • As @jeremybernstein mentioned it is useful for destuctive operation, so when developers back and forth testing code on different versions, this PR is useful when dealing with missing records in app_file_change_lists and app_change_numbers

@utkarshdalal utkarshdalal merged commit 637d229 into utkarshdalal:master Apr 16, 2026
3 checks passed
@jeremybernstein jeremybernstein deleted the jb/fix-cloud-conflict-no-change branch April 16, 2026 19:45
xXJSONDeruloXx pushed a commit to xXJSONDeruloXx/GameNative that referenced this pull request Apr 28, 2026
…hdalal#1228)

destructive db migration wipes file_change_lists cache, making every
steam game trigger conflict on first launch post-update. check if
local state is byte-identical to remote manifest (by filename + SHA)
before declaring conflict; if so, rehydrate cache and report UpToDate
silently. also populates real timestamps on the genuine-divergence
path (was showing epoch).

test: dbCleared_localMatchesRemote_rehydratesSilently_noConflict
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants