Tutorials - Dynamic Cloud Sync
By Daniel Nora
This is an optional feature for developers integrating their games with Steam Cloud. It requires extra effort to implement and is disabled by default. Basically, it allows a game that is already running to get notified of changes to a cloud save from a different device.
Relevant GodotSteam classes and functions
Why Is This Useful?
With the arrival of the Steam Deck, it became possible to suspend and resume games mid-session without actually exiting the game. This introduced a new problem that was already familiar to mobile game developers, as illustrated by the following example:
- You play the game on a Steam Deck
- Eventually you decide to stop playing and suspend the Steam Deck without exiting the game
- Later, you decide to play the same game on a desktop computer, where you continue from your last saved game on the Steam Deck
- Eventually, you resume your game on the Steam Deck... only to find that your progress on the desktop computer wasn't synchronized to the Steam Deck
This often happens because traditional implementations only load data from the cloud on game startup. Such implementations are thus unable to accomodate the possibility of changes to the cloud while the game is running.
Notice that in our example, the application never stopped running on the Steam Deck, which is why it didn't sync the changes. Some mechanism by which to respond to external changes to cloud saves was thus necessary - which is the purpose of Dynamic Cloud Sync.
How To Implement
Start by reading the small introduction to this feature on the Steamworks documentation. Basically, you need to complete the following steps:
- Connect and respond to the local_file_changed callback, using the functions getLocalFileChangeCount() and getLocalFileChange(i)
- Wrap your save file writes with beginFileWriteBatch() and endFileWriteBatch() to ensure integrity
Note that Steam uses the terminology "local file", which can be confusing. If you are using Steam Auto-Cloud, your application's local save files are indeed changed directly by Steam. However, if your cloud implementation uses the Steam Cloud API, "local file" refers to the local dowload of the remote cloud files managed by Steam, not to the local save files that you may have stored, for example, in Godot's "user://" folder.
Here is an example implementation demonstrating the necessary calls to the Steam API, which you can integrate with your file saving/loading script:
var steamworks_on: bool = false
var steam_cloud_on: bool = false
func _ready() -> void:
# ...
# TODO: remember to make sure this is a Steam build and that Steamworks is initialized
# If so, set steamworks_on = true
# ...
if steamworks_on:
if Steam.isCloudEnabledForAccount() && Steam.isCloudEnabledForApp():
steam_cloud_on = true
# ...
# TODO: if using the Steam Cloud API directly, load the Cloud files at this point and merge/override your local save files
# If using Auto-Cloud, your files are already synced: just load your local save file as usual
# ...
# Connect to the signal that notifies of remote changes to Cloud files
Steam.local_file_changed.connect(_on_steam_cloud_file_changed)
elif OS.has_feature("debug"):
print("User disabled cloud for game or account")
func perform_save() -> void:
if steam_cloud_on:
Steam.beginFileWriteBatch()
# TODO: here goes your regular save code.
# If using Steam Auto-Cloud, you need only save your file(s) to disk as usual
# (probably resorting to the methods in Godot's FileAccess class).
# If using the Steam Cloud API, after saving to disk, you should also save to the Steam Cloud,
# probably using something like Steam.fileWriteAsync()
if steam_cloud_on:
Steam.endFileWriteBatch()
func _on_steam_cloud_file_changed() -> void:
if OS.has_feature("debug"):
print("Received Steam localFileChanged callback...")
var files_changed_count: int = Steam.getLocalFileChangeCount()
for i: int in files_changed_count:
var result: Dictionary = Steam.getLocalFileChange(i)
match result["path_type"]:
Steam.FILE_PATH_TYPE_ABSOLUTE:
# NOTE: this is the correct situation to respond to if using Steam Auto-Cloud
# If using the Steam Cloud API directly, this file change should probably be ignored, and there is likely something wrong with your implementation
_read_cloud_file_change(result["file"], result["change_type"])
Steam.FILE_PATH_TYPE_API_FILENAME:
# NOTE: this is the correct situation to respond to if using the Steam Cloud API
# If using Steam Auto-Cloud, this file change should probably be ignored, and there is likely something wrong with your implementation
_read_cloud_file_change(result["file"], result["change_type"])
func _read_cloud_file_change(file: String, change_type: Steam.FilePathType) -> void:
match change_type:
Steam.LOCAL_FILE_CHANGE_FILE_UPDATED:
# TODO: handle a save file being MODIFIED on the Cloud - the logic varies according to each game.
# If using Auto-Cloud, the local file is already modified and you can just read from it.
# If using the Steam Cloud API, you probably want to read the cloud file at this point and merge/override your local save file.
# In any case, this is the point at which you must react to external changes (this is probably a resume from suspension).
# For example, if this file is the last game to have been saved by the player, you may want to load it so
# the player can pick up right where they left off on the other device. Make sure that loading a
# save game abruptly at this point does not negatively interfere with your game's logic.
# For example, you may want to defer this if the player is in the options menu, etc.
pass
Steam.LOCAL_FILE_CHANGE_FILE_DELETED:
# TODO: handle a save file being DELETED on the Cloud - the logic varies according to each game.
# Some games might not even implement this, e.g. games that only support automatic saves (like many puzzle games).
# If using Auto-Cloud, the local file is already deleted, and you only need to react to the event.
# If using the Steam Cloud API, you probably want to delete your matching local file as well and then react appropriately.
# Reacting to a deleted file might implicate updating your UI's save file list, etc.
pass
Warning
Notice that Steam does not provide a callback when the device is suspended. This is not a problem if your saves are triggered manually by the player. But if your game relies solely on auto-saves (like for example many puzzle games do), you will have to make sure that your game is saved automatically to the cloud at important moments (e.g. when completing a level) or at regular time intervals, otherwise the progress that the player had attained (up to the point when they suspended the Steam Deck) will not carry over to other devices. Note that saving only on game exit is not sufficient, because suspending the game on a Steam Deck won't trigger game exit.
Finally, go to your game's landing page on Steamworks and click "Configure Cloud Saves", then scroll down to the option "Dynamic Cloud Saves" and check the box "Enable Steam Cloud sync on system suspend and resume".
Warning
Notice the point indicated below the checkbox: "Failure to utilize the Steam Cloud APIs to protect against partial file writes and to receive notifications about updated local files can result in file corruption and/or loss of user progress." Be especially careful if your game has already shipped: test your changes locally before enabling this option. If your game hasn't shipped yet or if you have already tested your implementation thoroughly, you can save your changes and publish them.
How To Test
The Steamworks documentation provides a great step-by-step guide on how to test your Dynamic Cloud Sync implementation, even if you don't own a Steam Deck. To make sure your implementation is working as expected, it is recommended that you follow the optional steps and use two devices to test.