Repository navigation
sessions: see protected games and games installed anywhere; a safety net for the rest - #128
Draft
ChakraFusion wants to merge 18 commits into
Draft
ChakraFusion wants to merge 18 commits into
ChakraFusion wants to merge 18 commits into
Conversation
… ends A game autosaves every few minutes, and each autosave was synced as it came: one afternoon a device took eleven versions of a game in 26 minutes from the device it was being played on, each with its own snapshot, and the playing device snapshotted every autosave too. And a device took other devices' versions of a game running on it, writing a save under the game. While a game is being played on a device (its play sessions): - it is not synced either way there (ErrPlayingHere, quiet as ErrHeld); - another device asking for it is answered "playing", for its manifest and for deletions, and says so once per session instead of failing; the answer contains SettlingMessage, so an older build takes it as busy; - the watcher snapshots a change to it at most once per 30 minutes. When the session ends, the existing "After playing" snapshot is taken and the save goes to the other devices in one sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a session is open the game's save is now held back from syncing, so a session that outlives the game holds the save with it. A process named like the game stayed behind for fifteen minutes after one closed, with under 1 MB and no processor time; a killed one left two that kept their memory and used no processor time at all. Processes are measured on Windows (memory, processor time; sessions.Proc). One holding under 32 MB that used no processor time since the last poll (sessions.Idle), or one that used none for a minute whatever it holds, is not the game (daemon.activeProcesses). A game being played uses some all the time; a paused one still draws its menu. One that cannot be measured still counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ack what is a save The game database names the settings files of the games it knows (Liquid-co#84). For the rest, a file is now taken for a game's settings by its contents (presets/detectsettings.go): the vocabulary of one machine's display - resolution, vsync, shadow quality, upscaler - matched inside identifiers split at case changes and separators ("_enableFrameGeneration", "r_materialAniso"), in UTF-8 or UTF-16. Unreal Engine's Config/<platform> folder is taken by convention. Names only add weight, by how reliably the game database's own labels show them to mean settings: over its 16,700 settings and 9,000 save entries, "settings" names 1,521 settings files and 70 saves, ".ini" 1,831 and 137, so a name never decides on its own. A name with a save word in it rules a file out unless the contents are overwhelming; the game database's save locations are never taken, and nor are logs, binaries, long files that are only partly settings, or Syncthing's conflict copies. A file taken for settings that changes like a save is given back for good (verdict "save", which also overrules the game database): - before it is left out, and at every later run, the game's snapshots are asked whether it changed in most of the snapshots in which the rest of the save changed (snapshot/history.go); - while it is left out, every change here and every manifest another device sends count whether it changed when the rest of the save did; three such changes, outnumbering the times it stayed put, give it back (daemon/detectsettings.go). Detected files and verdicts are kept per game (migration 0042: settings_files, settings_observations, and marks for when each game was last judged) and applied through the device settings, so hashes recorded under the old list are carried over as before. The game's file list says why each excluded file is excluded, and unticking one there syncs it. The configuration tab's new text is in the catalogs (en, zh-Hans, zh-Hant). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked in review: a game minimised in the background can throttle to almost nothing, or be suspended, and Windows trims its memory as well. Either rule - under 32 MB and no processor time since the last poll, or no processor time for a minute - could end its session while it was still running, and the save would be synced under it. There is one rule now: a process that has used no processor time for five minutes is not the game, whatever memory it holds. That still lets go of what a closed game left behind well before the quarter of an hour one lingered. Memory is no longer read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y-content # Conflicts: # cmd/opensave-app/frontend/src/locales/en.json # cmd/opensave-app/frontend/src/locales/zh-Hans.json # cmd/opensave-app/frontend/src/locales/zh-Hant.json
Asked in review: builds of the earlier draft of this change made
settings_files and settings_observations (0043) and marks (0041) with the
same schema, and a database from one of them would not start under a
plain CREATE TABLE ("table already exists"), as happened with
sync_device_settings. IF NOT EXISTS for all three, and a test that opens a
database holding them with 0042 unrecorded; it fails without.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c until confirmed
Asked in review: a wrong guess would stop part of a save syncing without
anyone knowing, a bigger risk than the game database's listed files. So a
file found by its contents is now only suggested (verdict "suggested"): it
keeps syncing, and the game's configuration tab lists it with "Keep on
this device" and "Keep syncing". Keeping it on this device leaves it out
of syncing as the database's files are, and the give-back checks watch it
as before; keeping it syncing records "declined", and it is not suggested
again. A suggestion that the snapshots show changing with the save is
withdrawn. The game database's files stay automatic.
POST /api/games/{id}/suggested-settings {path, keepHere} answers one; the
game's JSON carries suggestedSettings. The tab's new text is in the
catalogs (en, zh-Hans, zh-Hant).
TestSuggestedSettings_SyncUntilConfirmed: detection suggests the settings
files and leaves them syncing, an answer each way does what it says, and
detecting again changes neither.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A process left behind that still ticks over now and then - a crash reporter, a launcher's helper - counts as running however long it lingers, and while a session is open the game's save is held back from syncing. So a session whose save has not changed for an hour is ended: kept and synced as when the game closes. The process still there does not start it again; the save changing again does, and so does the game being started anew. TestQuietSession_EndsAfterAnHourWithoutASave fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hour without a save ended any session, and so two it should not: - a game saved only by hand, at save points, can go hours without a save while it is played, and its save would then be synced under it; - a session begun from outside - the Deck plugin from Steam's launch, `opensave wrap` - ends when that says so, and knows. Now a session begun from outside is never ended this way, and one seen from running processes is ended only if, besides not saving for an hour, the game's processes used under 1% of one core in it (some 36 seconds). A game being played keeps drawing and uses far more; a crash reporter or launcher helper left behind uses less. Measured in hour-long windows from the session's start or the save's last change. A process that cannot be measured is never taken as quiet. sessions.CPUOf sums a game's processes, matched as Running matches them; Tracker.Marked says a session was begun from outside. Tests: a leftover that ticks over is let go, a game played for three hours without saving keeps its session, and a session begun from outside is not ended; the last two fail under the hour alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… play
A 20-minute session of Crimson Desert was never seen as one: its process
keeps its path from everyone (protected against tampering), so the
session tracker, which matches running programs by path, saw nothing,
and every autosave was snapshotted and synced as it came.
- A process whose path cannot be read is listed by its file name, and
matched against the programs found in the game's install folders
(Target.Programs, three folders deep, installers, crash reporters,
redistributables and anti-cheat services left out).
- A save that keeps changing while no session is seen is handled as play
(daemon/sessions.go, noteActivity): one change on its own is kept and
synced at once, as before - a game that saves as it closes; a second
within 10 minutes begins a stretch that is not synced either way, with a
checkpoint at most every 30 minutes, and once the save has stayed
unchanged for 10 minutes it is kept ("After playing") and synced, as a
session's end does. Said once in the log, per game, when it happens.
Options.SyncEveryChange turns the stretch off for the e2e harness, whose
tests of syncing change a save several times a minute and expect every
change to travel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # internal/daemon/daemon.go # internal/p2p/syncengine/engine.go
A session of Crimson Desert installed at G:\Games\Crimson Desert was never seen: session detection knew install folders only from Steam's libraries and the folders games are usually kept in, and the game's entry read "not found on this PC". Its program path was readable all along. - A running program inside a folder named as one of the game's install folders is that game, wherever the folder is: the names the game database knows (Ludusavi's installDir, "Crimson Desert"), and the game's own name when it is two words or a long one - "Rust" is a folder in every Rust toolchain. Generic folder names never count. - The folder a session was seen running from is remembered (marks), so the game is known to be installed there, and its programs can be told by name from then on. - A change made while no session is seen is held from the first, not the second: it is kept and synced once the save has stayed unchanged for 5 minutes, longer than the gap between Crimson Desert's first two autosaves (3.6 minutes), so an unseen session sends one version, at its end. A second change within 10 minutes makes it a stretch that ends after 10 minutes unchanged, as before. - A change waiting out the watcher's debounce already counts as play: a sync landing in those two seconds sent one change on just before it was held (watcher.ChangePending). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestRunning_ByInstallFolderName gave processes Windows paths; a process's program is a native path, so the test now builds them for the system it runs on. Found by CI on Linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The safety net for games whose program is not seen held every change outside a session as possible play: on its own for five minutes, as a stretch for ten after the last. That held every game, including one whose sessions are seen here and is closed, and a change right after a session's end - the game writing as it closes - which the session's end had just sent. A game is recognised once a session of it has been recorded or the folder it ran from is known; while none of its sessions is open, a change to its save is not play and is sent at once. So is one within two minutes of a session's end. Only a game whose program is not seen is held as a stretch. TestActivity_ARecognisedGameIsNotHeldWhileClosed, and Liquid-co#117's TestSessionEndsWhenOnlyAStubIsLeft, fail without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked about in Liquid-co#106's review for its own lookup: a large save must not be read into memory snapshot by snapshot. ChangesLikeASave read every file of each of a game's last 40 snapshots to tell whether the rest of the save changed - some ten million rows for a quarter-million-file save, each time detection ran - and for a snapshot without a recorded file list it unpacked and hashed the whole archive. Now the database compares two snapshots (store.SnapshotsDiffer, stopping at the first difference), only the files being judged are read (SnapshotPathHashes), and a snapshot with no recorded list is left out (SnapshotHasFiles). ArchiveFiles and filesOf, used only here, are gone. TestSnapshotsDiffer covers the comparison; TestChangesLikeASave passes unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@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. |
This was referenced Oct 26, 2026
# Conflicts: # internal/p2p/syncengine/engine.go
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #117 and #118. Their commits are here too until they merge (#118 comes in through merge commits). This PR's own change is four commits: protected games and the safety net; wherever installed; native test paths; recognised games not held.
The rest of the play-sessions draft (#34), so that a session is seen for the games #117's hold is for:
Protected games. A game protected against tampering keeps its program path from everyone, so the session tracker, which matches programs by path, never saw a 20-minute session of Crimson Desert. A process whose path can't be read is now listed by its file name. That name is matched against the programs in the game's install folders, three folders deep, leaving out installers, crash reporters, redistributables, anti-cheat services and launchers.
Wherever installed. A game is found running from a folder named as its install folder in the game database (or, for a distinctive name, the game's own), not only from Steam's library. The folder it was seen running from is remembered (
marks, from #118's migration), so the game is recognised after a restart.A safety net for games still not seen. A save that keeps changing while no session is seen is handled as play. A change on its own is kept and synced after five minutes. A second change within ten minutes begins a stretch that isn't synced either way, with a checkpoint at most every 30 minutes. Once the save has stayed unchanged for ten minutes, it's kept ("After playing") and synced, as a session's end does. This is said once in the log per game.
Recognised games are not held:
Options.SyncEveryChangeturns the safety net off for the e2e harness, whose tests of syncing change a save several times a minute.Behaviour change to note: for a game whose program isn't seen, a single change now reaches the other devices five minutes later than before. Games whose sessions are seen, which is most once they've been played here, are unaffected.
Tests (
daemon,sessions):TestActivityWithoutASessionIsHandledAsPlay;TestActivity_ARecognisedGameIsNotHeldWhileClosed, which fails without that rule, as does sync: a game being played is held back from syncing until the session ends #117'sTestSessionEndsWhenOnlyAStubIsLeft.Checks: the full unit and e2e suites pass locally.
🤖 Generated with Claude Code