weBumpdevelopers

LESSON 04 OF 07 · RACING TUTORIAL

Save progress privately

Store game-defined JSON and handle conflicts without losing the local save.

1. Start with your own save shape

The demo stores race progress under racing_save and its personal replay under ghost_telemetry. These names belong to the game. Your game can store inventory, unlocks, settings or other bounded JSON.

In a script in this demo, add a save action using the existing autoload:

func save_progress() -> void:
    var progress := {
        "version": 1,
        "unlocked_tracks": ["demo_loop_v1"],
        "settings": {"sound": true}
    }
    WeBumpAPI.put_state("progress", progress, _saved)

func _saved(ok: bool, _value: Variant) -> void:
    if WeBumpAPI.is_mock_mode or not WeBumpAPI.is_authenticated:
        $Status.text = "Saved locally"
    else:
        $Status.text = "Saved and synced" if ok else "Saved locally; sync failed"

put_state writes the local file before attempting the cloud write. Its callback can also succeed in mock/disconnected mode, so don’t label every successful callback “synced.” Add a Status Label if using the example.

2. Load deliberately

func load_progress() -> void:
    WeBumpAPI.get_state("progress", _loaded)

func _loaded(ok: bool, value: Variant) -> void:
    if not ok:
        return # Keep the current local game; show a sync warning.
    if value is Dictionary and value.get("version") == 1:
        # Validate and apply only fields your game understands.
        $Status.text = "Progress loaded"

Read the autoload’s get_state/put_state functions before adapting them. A successful cloud read replaces the cached value for that key. Don’t call it over an unsynced local draft; choose a merge policy for your game, such as combining unlocked tracks while preserving the newest local settings.

3. Understand the write contract

GET /v1/me/state returns private data and a strong ETag. A write to PUT /v1/me/state/progress sends {"value":…} with that exact ETag in If-Match. State, capsule, showcase and shared data all use one revision. The demo serializes writes and fetches a fresh revision before each.

A 409 means another change won the race. Keep the local draft, fetch the latest data and merge intentionally. Fetching a fresh ETag alone does not resolve an old draft’s semantic conflict. Do not automatically replay writes or rotating refresh-token exchanges.

Storage limits and browser persistence

Private state allows 16 keys and 16 KiB total, measured as PostgreSQL JSONB text. Each array is limited to 256 items and each string to 1,024 characters. Keep large files elsewhere; private state is not unlimited storage. Browser storage can be cleared or unavailable, especially for embedded/private browsing. Test local-save recovery in your target browser; a local-first design is not a guarantee of permanent browser storage.

Checkpoint

In editor mode, save and reload your example without a network request. With an approved live connection, verify a state read returns it. Interrupt a sync and confirm the local draft survives. Private state never appears in another player’s visitor response or public profile.