Automatically sync Steam cloud saves that don't sync automatically
steam-crossplatform-sync performs the following tasks:
In order to detect game launches/closes, the app periodically checks whether
Steam’s game overlay process is running. When a game launches, Steam spawns an
overlay process (GameOverlayUI64.exe on Windows, gameoverlayui on OS
X/Linux) with the running game’s app ID baked into its arguments. On Windows
we get that ID by querying the process list with wmic; on OS X/Linux we read
it straight off the overlay process’s arguments. See
GameOverlayProcessLocator for the gory details.
When a game is closed and is configured to be synchronized, the application checks the “local” game’s save files (the current computer’s state) and what’s configured in the “cloud” sync storage path. We look for the newest file and its modification time, and whichever is newest gets copied over. For directories, we look across all files to find the newest.
Some games use the Steam user’s ID in the save file location (for when the computer is shared across multiple users). steam-crossplatform-sync assumes the most recently logged in user is the primary user.
Figuring out where games are installed is the most complicated part of the application. It can be broken down into the following pieces:
It’s surprisingly hard to figure out what games you have installed.
AppManifestInstalledGameFinder reads the
same files Steam itself uses to track what’s installed:
steamapps/libraryfolders.vdf lists every library folder you’ve set up
(including external drives), and each one has an appmanifest_<id>.acf file
per installed game, with a StateFlags of 4 meaning “fully installed.”
This file format is identical across Windows/Mac/Linux, so the same code
path works everywhere.
Steam can be queried to report the currently running game/app ID. In order to map that to a game, and to figure out what that game even is, we need to load the configuration associated with that game. Every game’s configuration is stored in a VDF file (Valve Data Format), here’s a snippet of Hollow Knight’s configuration file:
"367520"
{
"common"
{
"name" "Hollow Knight"
"type" "Game"
"oslist" "windows,macos,linux"
...
In Steam’s installation directory, there exists a file, appcache/appinfo.vdf,
that stores a cache of apps in a binary format, which includes the VDF config
for every game. AppCacheBufferedReader is
responsible for loading the cache, and it’s assumed that all games in your
library are included in this cache.
Once we have the game’s VDF configuration, we can see where the game says its files are stored. There are a bunch of different ways that developers can express where files are saved. steam-crossplatform-sync tries to parse the configuration and figure out where those are supposed to be.
This is the relevant snippet for Hollow Knight:
...
"ufs"
{
"quota" "2000000"
"maxnumfiles" "10"
"savefiles"
{
"0"
{
"root" "WinAppDataLocalLow"
"path" "Team Cherry/Hollow Knight"
"pattern" "*.dat"
"platforms"
{
"1" "Windows"
}
}
"1"
{
"root" "MacHome"
"path" "/Library/Application Support/unity.Team Cherry.Hollow Knight"
"pattern" "*.dat"
"platforms"
{
"1" "MacOS"
}
}
"2"
{
"root" "LinuxHome"
"path" ".config/unity3d/Team Cherry/Hollow Knight"
"pattern" "*.dat"
"platforms"
{
"1" "Linux"
}
}
}
}
This maps to the following games.yml entry:
- name: "Hollow Knight"
gameId: 367520
windows:
- "%USERPROFILE%/AppData/LocalLow/Team Cherry/Hollow Knight/*.dat"
mac:
- "~/Library/Application Support/unity.Team Cherry.Hollow Knight/*.dat"
linux:
- "~/.config/unity3d/Team Cherry/Hollow Knight/*.dat"
sync: true
Here’s a more complicated example for Slay the Spire:
...
"ufs"
{
"quota" "100000000000"
"maxnumfiles" "10000"
"savefiles"
{
"0"
{
"root" "gameinstall"
"path" "preferences"
"pattern" "*"
}
"2"
{
"root" "gameinstall"
"path" "betaPreferences"
"pattern" "*"
}
"3"
{
"root" "gameinstall"
"path" "saves"
"pattern" "*"
}
}
"rootoverrides"
{
"0"
{
"root" "gameinstall"
"os" "MacOS"
"oscompare" "="
"useinstead" "gameinstall"
"addpath" "SlayTheSpire.app/Contents/Resources/"
}
}
}
This maps to the following games.yml entry:
- name: "Slay the Spire"
gameId: 646570
windows:
- "%PROGRAMFILES(X86)%/Steam/steamapps/common/SlayTheSpire/preferences/*"
- "%PROGRAMFILES(X86)%/Steam/steamapps/common/SlayTheSpire/betaPreferences/*"
- "%PROGRAMFILES(X86)%/Steam/steamapps/common/SlayTheSpire/saves/*"
mac:
- "~/Library/Application Support/Steam/steamapps/common/SlayTheSpire/SlayTheSpire.app/Contents/Resources/preferences/*"
- "~/Library/Application Support/Steam/steamapps/common/SlayTheSpire/SlayTheSpire.app/Contents/Resources/betaPreferences/*"
- "~/Library/Application Support/Steam/steamapps/common/SlayTheSpire/SlayTheSpire.app/Contents/Resources/saves/*"
linux:
- "~/.steam/steamapps/common/SlayTheSpire/preferences/*"
- "~/.steam/steamapps/common/SlayTheSpire/betaPreferences/*"
- "~/.steam/steamapps/common/SlayTheSpire/saves/*"
sync: true