Skip to content

Cloud Saves

Save player progress to the cloud so they can continue on any device. The SDK handles version conflicts, size validation, and local caching with offline fallback.

QuestData.save_game(data: Dictionary, callback: Callable = Callable(), conflict_strategy: int = QuestData.SAVE_CONFLICT_DEFER)
ParameterTypeDefaultDescription
dataDictionaryrequiredGame state to save (must be JSON-serializable)
callbackCallableCallable()Called with a response Dictionary
conflict_strategyintSAVE_CONFLICT_DEFERHow to handle a server-side version conflict (HTTP 409). See Conflict Strategies below

Callback response (success):

KeyTypeDescription
successbooltrue
versionintNew save version number
updated_atStringISO timestamp

Callback response (conflict, only with SAVE_CONFLICT_DEFER):

KeyTypeDescription
successboolfalse
errorString"Version conflict"
conflictbooltrue
server_versionintThe server’s current version
server_dataDictionaryThe server’s current save data

A version conflict happens when the server has a newer save than the local cache — typically because the player saved on another device, or because two save calls raced. The SDK supports multiple strategies for resolving this. Pass one as the third argument to save_game():

ConstantValueBehavior
QuestData.SAVE_CONFLICT_DEFER0Default. Return the conflict to your callback so you can decide. Use this if you want to show a UI, do an auto-merge, or pick a strategy at runtime
QuestData.SAVE_CONFLICT_OVERWRITE1Last-Write-Wins. SDK silently adopts server_version, retries the save once, and your callback fires with success=true on the retry. The server’s old data is overwritten by your local data

Reserved for future SDK versions: SAVE_CONFLICT_SERVER_WINS (2), SAVE_CONFLICT_MERGE (3), SAVE_CONFLICT_USER_CHOICE (4).

Which to pick:

  • Idle / casual / single-device games: use SAVE_CONFLICT_OVERWRITE. Local progress is the source of truth and a conflict almost always means a stale server entry from a long-abandoned device. Silent overwrite gives the smoothest UX
  • Multi-device games (mobile + desktop, console crossplay): use SAVE_CONFLICT_DEFER and show a “Cloud save is newer — Use Cloud / Use Local / Merge” dialog. Otherwise players who switch devices lose progress without warning
  • Single-player roguelikes / RPGs: use SAVE_CONFLICT_DEFER so a duplicate save from a forgotten session doesn’t clobber a current 30-hour run

Example — Last-Write-Wins for an idle game:

func save_to_cloud():
var data = _serialize_player_state()
QuestData.save_game(data, func(result: Dictionary):
if result.get("success", false):
print("Cloud save OK, version=", result.version)
else:
push_warning("Cloud save failed: ", result.get("error"))
, QuestData.SAVE_CONFLICT_OVERWRITE)

Example — Defer with user choice:

func save_to_cloud():
var data = _serialize_player_state()
QuestData.save_game(data, func(result: Dictionary):
if result.get("success", false):
return
if result.get("conflict", false):
_show_conflict_dialog(data, result.server_data, result.server_version)
else:
push_warning("Cloud save failed: ", result.get("error"))
) # third arg defaults to SAVE_CONFLICT_DEFER
QuestData.load_game(callback: Callable = Callable())

Callback signature: func(data: Dictionary, version: int)

If no save exists on the server, data is an empty Dictionary and version is 0.

QuestData.delete_save(callback: Callable = Callable())

Callback signature: func(deleted: bool)

QuestData.get_save_cached() -> Dictionary

Returns the last known save data from the local cache. Returns an empty Dictionary if no save has been loaded yet.

QuestData.get_save_version() -> int

Returns the current save version number. Returns 0 if no save has been loaded.

QuestData.clear_save_cache()

Clears the local save cache.

# Save progress at a checkpoint
func save_at_checkpoint():
var save_data = {
"level": current_level,
"health": player.health,
"inventory": player.get_inventory(),
"playtime_seconds": get_playtime(),
"achievements": unlocked_achievements
}
QuestData.save_game(save_data, func(response: Dictionary):
if response.get("success"):
show_toast("Game saved!")
elif response.get("conflict"):
handle_conflict(response)
else:
show_toast("Save failed: " + response.get("error", "Unknown error"))
)
# Load on game start
func _ready():
QuestData.load_game(func(data: Dictionary, version: int):
if data.is_empty():
start_new_game()
else:
restore_game_state(data)
print("Loaded save v%d" % version)
)
# Delete save (new game)
func start_fresh():
QuestData.delete_save(func(deleted: bool):
if deleted:
QuestData.clear_save_cache()
start_new_game()
)

Version conflicts happen when the save was modified from another device or session. The server returns the conflicting data so you can resolve it:

func handle_conflict(response: Dictionary):
var server_data = response["server_data"]
var local_data = get_current_save_data()
# Strategy: keep the save with more playtime
if local_data.get("playtime_seconds", 0) > server_data.get("playtime_seconds", 0):
# Force overwrite with local data (clear cache to reset version)
QuestData.clear_save_cache()
QuestData.save_game(local_data)
else:
# Use server data
restore_game_state(server_data)
  1. save_game() sends a PUT request with the save data and the current version number
  2. The server checks if the version matches — if not, it returns a 409 Conflict with the server’s data
  3. On success, the local cache is updated with the new version
  4. load_game() fetches from the server and updates the local cache
  5. Save data is cached to disk for offline access
LimitValue
Save data size100 KB max
SerializationMust be JSON-compatible (no Objects, no Callables)

View and manage saves under Players > Cloud Saves:

  • Save browser — Search saves by player ID
  • Save data — View the raw JSON data
  • Version history — See when saves were created/updated
  1. Save at meaningful moments — Checkpoints, level completions, shop exits — not on every frame
  2. Keep saves small — Store IDs and numbers, not full object trees. 100 KB is the limit
  3. Handle conflicts — Always check for conflict: true in the callback response
  4. Use get_save_cached() for quick reads — It’s synchronous and doesn’t hit the network
  5. Test offline behavior — The SDK falls back to cached data when the server is unreachable