Skip to content

Keep each device's game settings out of sync - #84

Merged
Omar-VF merged 3 commits into
Liquid-co:dev-publicfrom
ChakraFusion:pr/device-settings-standalone
Oct 19, 2026
Merged

Omar-VF merged 3 commits into
Liquid-co:dev-publicfrom
ChakraFusion:pr/device-settings-standalone

Conversation

@ChakraFusion

Copy link
Copy Markdown

The next standalone PR from the order on #21, rebuilt on dev-public without save versions. Builds on #52 (its commit is the first one here; this PR's own change is the second).

The problem. A game that keeps its settings beside its saves had one PC's graphics settings handed to every other. Crimson Desert's save/user_engine_option_save.xml is one example: the Steam Deck got the desktop's 4K ultra settings, and the desktop got the Deck's back.

Where the list comes from. The game database (the Ludusavi manifest) already says which files are a game's settings: it tags them config rather than save, 16,700 entries.

  • The index keeps those now (indexedGame.Config, index format v3, so the index is rebuilt once after updating).
  • For each tracked game, the entries inside its save folder become exclusion patterns relative to it (presets.DeviceSettings). They're matched component by component, so a template's <storeUserId> wildcard meets the folder actually tracked, with either path separator (Track a game from the game database; one folder, one game #73's relativeUnder).
  • An entry tagged both config and save is a save. A pattern that would cover the whole folder is never applied: a wrong exclusion loses saves, a missed one only syncs a setting.

How they're applied. Exactly like #52's launcher files, through the one function everything that applies exclusions now uses (syncengine.Engine.IgnoreText):

  • never synced in either direction, and never deleted on another device;
  • still in every snapshot, and kept on this device by a restore;
  • the game's own rules come after them, so a ! line brings one back;
  • a per-game switch, "Sync these settings between devices too" in the configuration tab, syncs them all. The tab lists the files. The column is migration 0040; the tab's new text is in the catalogs (en, zh-CN), as agreed with Janlhin on Track a game from the game database; one folder, one game #73.

Updating doesn't count as a change. At the first start with this build, files start being left out without anyone writing a rule. A game's last-snapshot hash is re-taken under the new list wherever the save is still the one it was recorded for, so the watcher doesn't snapshot and sync a game nobody played. A save that did change keeps its old hash and its change counts. The base agreed with each device needs nothing: since #52, the sync engine translates a base recorded under other exclusions.

Left for a follow-up: recognising settings files the game database doesn't list, by their contents. It's in my fork, but it's larger and deserves its own review.

Tests:

  • presets/devicesettings_test.go: patterns from config entries, a both-tagged entry staying a save, a pattern covering the whole folder never applied, the store-user wildcard.
  • syncengine/devicesettings_test.go: settings left out of both directions, the switch syncing them, a game's own ! rule bringing one back.
  • daemon/devicesettings_test.go: an unchanged save's last-snapshot hash carried over to the new view, a changed save's not, and a second start changing nothing.

Checks: unit tests, frontend tests and the related e2e tests pass locally; fork CI is green.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 16, 2026

Copy link
Copy Markdown

@ChakraFusion is attempting to deploy a commit to the sivadaboi's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Omar-VF

Omar-VF commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Reviewed and approved, thanks. Only reading the catalogue's config tags, with the guessing left for later, is the right call. It conflicts with dev-public in the translation files, and with #54 too. Could you rebase it on top of #54 once that's rebased, so the two merge one after the other without another round? It also builds on #52, which is about to be merged.

ChakraFusion and others added 3 commits October 17, 2026 16:35
A pull of a save of many files overflowed the watcher's event queue again
and again, and each overflow was taken for the game saving: an automatic
snapshot of a half-pulled save every few seconds - 76 on one device for one
pull - which pushed the complete ones out of retention.

- The watcher asks whether a sync is writing the game (BeingWritten). If
  so, an overflow is taken for the sync's, and the burst is looked at only
  once the sync has finished.
- A snapshot that could not read every file is discarded, not kept: it
  would stand in the history as a save that never existed. The watcher
  leaves the change unrecorded and takes it again at its next pass; a sync
  that needed the copy before replacing files does not replace them.
- OpenSave's own temporary files (*.opensave.tmp) are never archived.

Builds on the own-writes change (Liquid-co#23).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… screen

Asked in review: discarding every snapshot that could not read a file also
discarded the copy kept before a sync replaces the save, and the sync then
did not replace anything, quietly, for as long as a game held a file open
(a log, a lock file).

- Only the watcher's automatic snapshot (snapshot.CreateAutomatic) is
  discarded when it misses files; the watcher takes the change again at its
  next pass. Every other snapshot, the copy before a sync, restore or
  conflict above all, and one taken by hand, is kept with what could be
  read and a warning in the log, as before Liquid-co#54.
- When a game's automatic snapshots come out incomplete three passes in a
  row, it is said on screen once, until one succeeds again: the files the
  game holds open, and that its changes are not backed up or synced until
  they can be read, usually once the game is closed. The text is in the
  catalogs (en, zh-CN), with a "common.open" key for the toast's button.

Tests: an incomplete automatic snapshot is discarded, while one before
replacing and one by hand are kept (a seam on the archive's file open makes
a file unreadable on any system); the notice comes once per run of
incomplete passes, again after a good snapshot, and never for other
failures; the notice's wording, with the files beyond the first three
counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game that keeps its settings beside its saves had one PC's graphics
settings handed to every other: Crimson Desert's
save/user_engine_option_save.xml, among others.

The game database (Ludusavi manifest) already says which files are a
game's settings: it tags them "config", as opposed to "save" - 16,700
entries. The index now keeps those (indexedGame.Config, index format v3,
rebuilt once), and for each tracked game the ones inside its save folder
become exclusion patterns relative to it (presets.DeviceSettings), matched
component by component so a template's <storeUserId> wildcard meets the
folder actually tracked. An entry tagged both is a save. A pattern that
would cover the whole folder is never applied: a wrong exclusion loses
saves, a missed one only syncs a setting.

They are applied as the game's exclusion rules are, through one function
everything that applies rules now takes them from
(syncengine.Engine.IgnoreText), exactly like Liquid-co#52's launcher files: never
synced in either direction, never deleted on another device, still in
every snapshot, and kept on this device by a restore. The game's own rules
come after them, so a "!" line brings one back; a per-game switch
(games.sync_device_settings, migration 0040, "Sync these settings between
devices too" in the configuration tab, with the files listed) syncs them
all. The tab's new text is in the catalogs (en, zh-CN).

The list changes without anyone writing a rule, at the first start with
this build, so at start a game's last-snapshot hash is re-taken under the
new list where the save is the one it was recorded for
(daemon.adoptExclusionView); otherwise the watcher would snapshot and sync
a game nobody played. The base agreed with each device needs nothing: the
sync engine already translates a base recorded under other exclusions
(Liquid-co#52). Toggling the switch goes through the same path as editing the rules.

On top of Liquid-co#54, so the two merge one after the other (Liquid-co#52 is merged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ChakraFusion
ChakraFusion force-pushed the pr/device-settings-standalone branch from 9fc70a5 to 60f40d0 Compare October 17, 2026 14:46
@ChakraFusion

Copy link
Copy Markdown
Author

Rebased on top of #54 as asked (60f40d0): #52's commit dropped out now that it's merged, so this PR's diff is #54's two commits and its own. Only the catalogs conflicted, and both sides' keys are kept. Unit, frontend and the related e2e tests pass locally.

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