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 what Steam
reports is currently running via the Steam registry (RunningAppID
). On
Windows, this is through querying the registry. On OS X/Linux, this is read
through the registry file (registry.vdf
) located in Steam’s installation
directory.
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. The following strategies are used (in descending order of preference and accuracy):
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"
...
There are three ways of loading this information, in descending order of preference (all strategies are tried until a config is found):
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.steamcmd
, that can theoretically print a
game/app’s VDF config. Unfortunately, it’s buggy. When you try to call it
programmatically, you need to call app_info_print
twice in a row in order
for it to actually print the app info. Additionally, on Windows, due to
a strange buffering implementation in steamcmd, it’s difficult to
programatically read the output the second time app_info_print
is called.
It’s specifically for Windows that the next option was created.remoteAppInfoUrl
being set in
the configuration. When this is set, it sends a HTTP request to a server
that’s running steam-cross-platform sync in server mode. The server (Linux
or OS X) should be able to successfully run steamcmd and return the output.
For these servers, app_info_print
is called twice in a row. Note: if this
option isn’t set through the configuration, it won’t be attempted.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