Cloud saves

Explicit cloud upload, import and conflict resolution for named local saves.

Commands

Import blitz.savecloud As C gives your program these, with C. in front. Each name leads to its part of the reference.

Functions

Reference

Needs
blitz.saves
Examples
cloud_counter.bb

0.1.0 source preview. Upload validated blitz.saves records through native GameKit saved games on iOS 17+ or Play Games Saved Games on Android. Requires Extended runtime 1116+, Import blitz.saves, and a configured native mobile export. Importing the module performs no I/O and shows no sign-in interface.

Local play and saves work independently of cloud availability. macOS, Windows and web report the cloud service as Unsupported. The cloud provider is explicit: set services.cloudSaves, apple.cloudProvider: "gamekit", Game Center and the provisioned iCloud containers for iOS; set the linked android.gamesProjectId and enable Saved Games in Play Console for Android. Configure these in Mobile App Setup and export normally. Provider accounts, provisioning and device/store qualification are separate from local build tests.

API and ownership

FunctionContract
Savecloud.UploadSlot(store:Saves.SaveStore, slot:Str, expectedRevision:Str):IntExport the selected complete local generation, including a tombstone, and submit SaveCloudSave. Empty revision creates a previously absent cloud slot; an existing slot requires the last explicitly observed revision. Returns a request ID or zero. Copy Saves.LastError() for local export failure; native rejection uses GameServiceLastError().
Savecloud.ResolveSlot(store:Saves.SaveStore, slot:Str, token:Str):IntSubmit the current local record as the explicitly chosen resolution of a conflict returned by LoadCloudSave or SaveCloudSave. A token is bound to one authenticated account and slot. It expires on account change or successful resolution.

Both functions borrow the store for the call and copy the encoded record. They never change local files or retain a runtime JSON handle. Events use the ordinary PollGameServiceEvent, GameServiceEventData, and FreeGameServiceEvent APIs. The caller owns event JSON and frees it with FreeJson.

The local adapter lives in blitz.saves, so inspecting or importing a downloaded record does not require native cloud commands:

Function/resultContract
Saves.ExportCloudRecord(store, slot):StrReturn {deleted,save} transport JSON or empty plus LastError. Validates the selected physical generation and checksum, including deletions. Never changes local progress.
Saves.ReadCloudRecord(store, slot, recordPath):CloudSaveRead at most 1 MiB, validate the full cloud and local envelopes, identity, checksums and tombstone, then apply registered schema migrations to an isolated candidate. Returns caller-owned candidate or Null and LastError. Never imports it.
Saves.ImportCloudRecord(store, slot, candidate):IntExplicitly accept a candidate after the game's own domain validation. Creates a new local generation or tombstone. It never copies a remote local-generation counter and never swaps live game state.
Saves.ReleaseCloudRecord(candidate):IntExplicitly remove the downloaded file while preserving the candidate's in-memory payload. Call after successful conflict resolution or after dismissing an ordinary download, never while its conflict still needs resolving.
CloudSaveDeleted():Int, borrowed Payload():Int, AccountID():Str (origin identity), Revision():Str, Title():Str, Description():Str, PlaySeconds():Double. Delete explicitly to free owned memory. A tombstone's payload is zero.

ReadCloudRecord accepts only the private recordPath returned in an event; applications should not accept arbitrary player-provided file paths. Keep up to 128 downloaded records; explicitly release completed downloads. Unresolved conflict files are retained across failures. Account changes invalidate native conflict tokens while retaining files for explicit local recovery. A successful resolution removes its prior candidate files unless another conflict references them. Candidate payloads already read into memory remain owned by the caller.

Account, revision and conflict workflow

  1. Call InitCloudSaves from an explicit player action. It uses the shared native authentication provider and may show Game Center or Play Games sign-in. Background account checks never prompt. Display the returned accountId / adoption decision. Call AdoptCloudSaveAccount(accountId) only after the player explicitly chooses this account for their local progress. This choice persists privately. A new or switched account cannot upload until adopted.
  2. Load before replacing an existing slot. A successful download contains recordPath and revision; reading it does not import or overwrite anything. Missing slots return exists:false. Upload with that revision or empty for a new slot. Native adapters recheck the active account before writing.
  3. Status 13 (Conflict) returns a token and candidates, each with a local recordPath, revision, writer identity and deletion flag. Display all candidates and their validated game summaries. Never pick the greatest local generation, newest clock, or first array element automatically.
  4. The player chooses a candidate, keeps local data, or supplies a game-specific merge. Validate the game data; explicitly import the chosen candidate if appropriate, then call ResolveSlot to publish the resulting local record. A further conflict is another explicit decision, not a retry with a winner.
  5. Account-change event 46 pauses upload until the new account is adopted. Refresh/load again after adoption; old conflict tokens cannot cross accounts.

An accepted write returns state:"accepted" and confirmedSynced:false. Provider SDK completion can mean a local write queued for background upload; it is not evidence that another device has received the record. GameKit does not provide a compare-and-swap operation: preflight revision checks catch observed staleness, and a later concurrent write may create provider conflicts. Load again before editing another device's version. Play Games uses manual conflict policy. Offline errors preserve local state and candidates; retry from an explicit load.

The complete UTF-8 cloud envelope, including the nested local integrity record, metadata, tombstones, account and revisions, must fit 1,048,576 bytes. Android also queries the provider limit and uses the lower limit. Status 14 (TooLarge) leaves the local save intact. A locally valid 1 MiB payload can exceed this cloud budget after JSON escaping and metadata; there is no truncation or partial upload. Public events remain below 64 KiB: large record bytes travel through the private native bridge and are exposed through private downloaded files.

Cloud revisions are random UUIDs, separate from local Long generations. A stable per-installation private writer UUID identifies the author. Origin accountId is metadata, not a cross-device authorization token; the authenticated provider scopes reads and account checks guard uploads. No timestamp resolves a conflict.

Working example and checks

Open examples/cloud_counter.bb. It saves an offline counter and supports native connect/adopt, upload, download, candidate inspection, explicit selection and conflict resolution. Mobile controls work with touch and mouse. Disabled provider controls explain the required configured export rather than simulating success.

Maintained checks: modern/tests/cloud_test.cpp covers the coordinator's bounds, conflicts, accounts, callbacks and persistence; saves/tests/cloud_tests.bb checks local integrity, explicit import, Unicode/Long data, tombstones and ownership. Run the counter example with --verify for its automated offline persistence and request-ownership checks without opening a window or connecting an account. The official package contracts also check exact public interfaces and borrowed store ownership. The native adapters compile against the pinned SDKs. Real two-device/offline, account-switch, uninstall/reinstall and provider-console qualification still need configured physical devices and accounts.

Provider contracts: Apple saved games, Play Games Saved Games, SnapshotsClient.