# Usage

Essential Kit wraps native iOS (iCloud Key-Value Store) and Android (Google Play Saved Games) APIs into a single Unity interface. Cloud Services automatically initializes with your Essential Kit settings and is ready to use.

## Table of Contents

* [Understanding Cloud Services](#understanding-cloud-services)
* [Import Namespaces](#import-namespaces)
* [Event Registration](#event-registration)
* [Local-Only Storage (No Cloud Sync)](#local-only-storage-no-cloud-sync)
* [Cloud Synchronization](#cloud-synchronization)
* [Storing and Retrieving Data](#storing-and-retrieving-data)
* [Handling Data Conflicts](#handling-data-conflicts)
* [Core APIs Reference](#core-apis-reference)
* [Error Handling](#error-handling)
* [Advanced: Runtime Settings Override](#advanced-runtime-settings-override)
* [Related Guides](#related-guides)

## Understanding Cloud Services

Cloud Services keeps game data in two places:

* **Cloud copy** – Lives in iCloud (iOS) or Google Play Saved Games (Android) and is shared across devices.
* **Device copy** – Stored locally as a JSON cache; all `CloudServices` getters and setters read and write this copy instantly.

Synchronization keeps both copies in step. When you call `Synchronize()` the plugin uploads device changes, downloads cloud updates, and raises change events if the platform copy was newer.

```
Your Scripts ── Get*/Set* ──▶ Device Copy (local cache file)
      │                              │
      └── Synchronize() ◀────────────┤
                                      ▼
                          Cloud Copy (iCloud / Play)
                                      │
                   OnSavedDataChange ─┘ via CloudServicesUtility
```

**Key characteristics:**

* All `Get*/Set*` calls update the device copy immediately
* `Synchronize()` is the only way to push or pull the cloud copy
* If the cloud copy is newer, it overwrites the device copy and fires `OnSavedDataChange`

{% hint style="success" %}
**Two modes of operation:**

* **Local-only storage**: Never call `Synchronize()` - works like enhanced PlayerPrefs
* **Cloud sync**: Call `Synchronize()` to keep data in sync across devices
  {% endhint %}

## Import Namespaces

```csharp
using System;
using System.Collections;
using System.Collections.Generic;
using VoxelBusters.EssentialKit;
using VoxelBusters.CoreLibrary;
```

## Event Registration

Register for events in `OnEnable` and unregister in `OnDisable`:

```csharp
void OnEnable()
{
    CloudServices.OnUserChange += OnUserChange;
    CloudServices.OnSavedDataChange += OnSavedDataChange;
    CloudServices.OnSynchronizeComplete += OnSynchronizeComplete;
}

void OnDisable()
{
    CloudServices.OnUserChange -= OnUserChange;
    CloudServices.OnSavedDataChange -= OnSavedDataChange;
    CloudServices.OnSynchronizeComplete -= OnSynchronizeComplete;
}
```

| Event                   | Trigger                                                                |
| ----------------------- | ---------------------------------------------------------------------- |
| `OnUserChange`          | Cloud account changes (user logs in/out, switches accounts)            |
| `OnSavedDataChange`     | Cloud data differs from local data during sync (provides changed keys) |
| `OnSynchronizeComplete` | Synchronization request finishes (success or failure)                  |

## Local-Only Storage (No Cloud Sync)

Use Cloud Services as enhanced PlayerPrefs without any cloud synchronization:

```csharp
void SavePlayerData()
{
    // Saves to local storage only - no cloud involved
    CloudServices.SetLong("high_score", newHighScore);
    CloudServices.SetBool("tutorial_complete", true);
    CloudServices.SetString("player_name", playerName);

    // DON'T call Synchronize() - keeps data local only
}

void LoadPlayerData()
{
    // Loads from local storage - always works offline
    long highScore = CloudServices.GetLong("high_score");
    bool tutorialDone = CloudServices.GetBool("tutorial_complete");
    string name = CloudServices.GetString("player_name");

    Debug.Log($"High Score: {highScore}, Tutorial: {tutorialDone}, Name: {name}");
}
```

{% hint style="info" %}
Perfect for offline games or games that don't need cross-device sync. Data persists locally across sessions but won't sync to other devices.
{% endhint %}

## Cloud Synchronization

### Why Synchronization is Needed

Synchronization downloads the latest cloud data and uploads local changes. This ensures:

* Fresh installs get saved progress from cloud
* Players switching devices see their latest data
* Concurrent device usage detects and resolves conflicts

### First Sync (Authentication)

Call `Synchronize()` when your app starts to authenticate and download cloud data:

```csharp
void Start()
{
    // First call may show login dialog on Android
    CloudServices.Synchronize((result) =>
    {
        if (result.Success)
        {
            Debug.Log("Cloud sync complete - data is current");
        }
        else
        {
            Debug.Log("Sync failed - using local data only");
        }
    });
}
```

{% hint style="warning" %}
**First call behavior:**

* **iOS**: Uses device-level iCloud settings, no login prompt typically shown
* **Android**: May show Google Play sign-in dialog if user not authenticated
  {% endhint %}

### Checkpoint-Based Sync

Sync only at appropriate game checkpoints for performance:

```csharp
void OnLevelComplete()
{
    Debug.Log("Save game progress locally.");

    // Show syncing UI for better UX
    Debug.Log("Display syncing indicator.");

    CloudServices.Synchronize((result) =>
    {
        Debug.Log("Hide syncing indicator.");

        if (result.Success)
        {
            Debug.Log("Progress synced to cloud");
        }
        else
        {
            Debug.Log("Sync failed - will retry later");
        }
    });
}
```

{% hint style="success" %}
**Best practices:**

* Sync at level completion, major milestones, or app pause
* Show UI feedback during sync operations
* Avoid frequent sync calls during active gameplay
  {% endhint %}

## Storing and Retrieving Data

Cloud Services supports primitive types and binary data as key-value pairs.

### Supported Data Types

```csharp
// Boolean
CloudServices.SetBool("sound_enabled", true);
bool soundEnabled = CloudServices.GetBool("sound_enabled"); // false if not found

// Integer (stored as long internally)
CloudServices.SetInt("player_level", 10);
int level = CloudServices.GetInt("player_level"); // 0 if not found

// Long
CloudServices.SetLong("total_coins", 1000000L);
long coins = CloudServices.GetLong("total_coins"); // 0 if not found

// Float (stored as double internally)
CloudServices.SetFloat("volume", 0.75f);
float volume = CloudServices.GetFloat("volume"); // 0.0 if not found

// Double
CloudServices.SetDouble("completion_percentage", 87.5);
double completion = CloudServices.GetDouble("completion_percentage"); // 0.0 if not found

// String
CloudServices.SetString("player_name", "Hero123");
string name = CloudServices.GetString("player_name"); // null if not found

// Byte Array (for serialized objects)
byte[] saveData = SerializeGameState();
CloudServices.SetByteArray("game_save", saveData);
byte[] loadedData = CloudServices.GetByteArray("game_save"); // null if not found
```

### Storing Complex Data

Use a single serialized class for all save data to simplify versioning:

```csharp
[Serializable]
public class GameSaveData
{
    public int level;
    public float[] playerPosition;
    public List<string> unlockedItems;
    public long coins;
    public string version = "1.0";
}

public class CloudSaveExample : MonoBehaviour
{
    private int currentLevel;
    private long playerCoins;

    void SaveGame()
    {
        GameSaveData saveData = new GameSaveData
        {
            level = currentLevel,
            playerPosition = new float[] { transform.position.x, transform.position.y, transform.position.z },
            unlockedItems = GetUnlockedItems(),
            coins = playerCoins
        };

        // Serialize to JSON
        string json = JsonUtility.ToJson(saveData);
        CloudServices.SetString("complete_save_data", json);

        // Optional: sync to cloud at checkpoints
        CloudServices.Synchronize();
    }

    void LoadGame()
    {
        string json = CloudServices.GetString("complete_save_data");

        if (!string.IsNullOrEmpty(json))
        {
            GameSaveData saveData = JsonUtility.FromJson<GameSaveData>(json);
            currentLevel = saveData.level;
            // Restore other game state
            Debug.Log($"Loaded game at level {saveData.level}");
        }
        else
        {
            Debug.Log("No save data found - starting fresh");
        }
    }

    List<string> GetUnlockedItems() => new List<string>();
}
```

### Key Management

```csharp
// Check if key exists
if (CloudServices.HasKey("player_name"))
{
    string name = CloudServices.GetString("player_name");
}

// Remove key
CloudServices.RemoveKey("temp_data");

// Get all data snapshot
IDictionary snapshot = CloudServices.GetSnapshot();
foreach (DictionaryEntry entry in snapshot)
{
    Debug.Log($"Key: {entry.Key}, Value: {entry.Value}");
}
```

{% hint style="warning" %}
**Storage limits:**

* **iOS**: 64-byte max key length, 1MB per key, 1MB total, 1024 keys max
* **Android**: 3MB total per user
  {% endhint %}

## Handling Data Conflicts

When the same data is modified on multiple devices, Cloud Services detects conflicts and fires `OnSavedDataChange`.

### Understanding Conflict Resolution

Behind the scenes the plugin keeps a snapshot of the previous device values to help you compare changes:

1. Player makes changes on Device A and syncs
2. Player switches to Device B (has old data)
3. Device B calls `Synchronize()` - cloud copy has newer data
4. Local copy is overwritten with cloud copy
5. `OnSavedDataChange` fires with changed keys and reason
6. Your code compares the latest cloud values vs. the previous snapshot using `CloudServicesUtility`
7. Choose winning value and set it back
8. Next sync uploads the resolved data

Use `CloudServicesUtility.TryGetCloudAndLocalCacheValues` inside the event to fetch both values without manually caching them.

### Example: Conflict Resolution

```csharp
void OnEnable()
{
    CloudServices.OnSavedDataChange += OnSavedDataChange;
}

void OnSavedDataChange(CloudServicesSavedDataChangeResult result)
{
    if (result.ChangedKeys == null)
    {
        return;
    }

    Debug.Log($"Data changed - Reason: {result.ChangeReason}");

    foreach (string key in result.ChangedKeys)
    {
        // Use utility to compare the cloud value vs the previous device snapshot
        if (CloudServicesUtility.TryGetCloudAndLocalCacheValues<long>(key, out long cloudValue, out long localValue))
        {
            if (key == "high_score")
            {
                // Keep highest score
                long winnerScore = Math.Max(cloudValue, localValue);
                CloudServices.SetLong(key, winnerScore);
                Debug.Log($"Resolved high_score conflict: cloud={cloudValue}, local={localValue}, winner={winnerScore}");
            }
            else if (key == "total_playtime")
            {
                // Sum playtime from both devices
                long combinedTime = cloudValue + localValue;
                CloudServices.SetLong(key, combinedTime);
                Debug.Log($"Combined playtime: {combinedTime}");
            }
        }
    }
}
```

### Change Reasons

| Reason                 | Description                                            |
| ---------------------- | ------------------------------------------------------ |
| `ServerChange`         | Cloud data differs from local data during sync         |
| `InitialSyncChange`    | First sync after fresh install (cloud data downloaded) |
| `QuotaViolationChange` | Exceeded storage limits - data reset to cloud copy     |
| `AccountChange`        | User switched cloud accounts - all keys invalidated    |

{% hint style="danger" %}
**Critical**: If you don't handle `OnSavedDataChange`, local changes may be lost when cloud data overwrites the local copy. Always maintain a cache and resolve conflicts.
{% endhint %}

### Conflict Resolution Pattern

```csharp
void OnSavedDataChange(CloudServicesSavedDataChangeResult result)
{
    switch (result.ChangeReason)
    {
        case CloudSavedDataChangeReasonCode.ServerChange:
            // Merge changes from another device
            Debug.Log("Resolve conflicts with server-changed data.");
            break;

        case CloudSavedDataChangeReasonCode.InitialSyncChange:
            // First sync - cloud data loaded
            Debug.Log("Load cloud data into the local save.");
            break;

        case CloudSavedDataChangeReasonCode.AccountChange:
            // User switched accounts - reset local data
            Debug.Log("Clear local cached data for the previous account.");
            Debug.Log("Load cloud data for the new account.");
            break;

        case CloudSavedDataChangeReasonCode.QuotaViolationChange:
            // Exceeded storage - compress data
            Debug.LogWarning("Storage quota exceeded - consider data compression");
            break;
    }
}
```

## Core APIs Reference

| API                                                                                 | Purpose                              | Returns                                       |
| ----------------------------------------------------------------------------------- | ------------------------------------ | --------------------------------------------- |
| `CloudServices.SetBool(key, value)`                                                 | Store boolean value locally          | Void - syncs to cloud on next `Synchronize()` |
| `CloudServices.GetBool(key)`                                                        | Retrieve boolean value               | `bool` (false if not found)                   |
| `CloudServices.SetInt(key, value)`                                                  | Store integer value locally          | Void                                          |
| `CloudServices.GetInt(key)`                                                         | Retrieve integer value               | `int` (0 if not found)                        |
| `CloudServices.SetLong(key, value)`                                                 | Store long value locally             | Void                                          |
| `CloudServices.GetLong(key)`                                                        | Retrieve long value                  | `long` (0 if not found)                       |
| `CloudServices.SetFloat(key, value)`                                                | Store float value locally            | Void                                          |
| `CloudServices.GetFloat(key)`                                                       | Retrieve float value                 | `float` (0.0 if not found)                    |
| `CloudServices.SetDouble(key, value)`                                               | Store double value locally           | Void                                          |
| `CloudServices.GetDouble(key)`                                                      | Retrieve double value                | `double` (0.0 if not found)                   |
| `CloudServices.SetString(key, value)`                                               | Store string value locally           | Void                                          |
| `CloudServices.GetString(key)`                                                      | Retrieve string value                | `string` (null if not found)                  |
| `CloudServices.SetByteArray(key, value)`                                            | Store binary data locally            | Void                                          |
| `CloudServices.GetByteArray(key)`                                                   | Retrieve binary data                 | `byte[]` (null if not found)                  |
| `CloudServices.HasKey(key)`                                                         | Check if key exists                  | `bool`                                        |
| `CloudServices.RemoveKey(key)`                                                      | Delete key-value pair                | Void                                          |
| `CloudServices.GetSnapshot()`                                                       | Get all data as dictionary           | `IDictionary`                                 |
| `CloudServices.Synchronize(callback)`                                               | Sync local and cloud data            | `CloudServicesSynchronizeResult` via callback |
| `CloudServices.ActiveUser`                                                          | Get current cloud user info          | `CloudUser` (account status and user ID)      |
| `CloudServicesUtility.TryGetCloudAndLocalCacheValues<T>(key, out cloud, out local)` | Compare cloud vs cache for conflicts | `bool` (true if both values exist)            |

## Error Handling

| Error Code                            | Trigger                                   | Recommended Action                           |
| ------------------------------------- | ----------------------------------------- | -------------------------------------------- |
| Sync failure (result.Success = false) | Network error, user denied authentication | Retry later or continue with local-only data |
| `QuotaViolationChange` reason         | Exceeded storage limits                   | Compress data or remove unnecessary keys     |
| `AccountChange` reason                | User switched cloud accounts              | Clear local data and reload from new account |
| Null/empty key                        | Invalid key parameter                     | Validate keys before calling Get/Set         |

```csharp
void OnSynchronizeComplete(CloudServicesSynchronizeResult result)
{
    if (!result.Success)
    {
        Debug.LogWarning("Cloud sync failed - game will continue with local data");
        // Show optional retry UI
        return;
    }

    Debug.Log("Cloud sync successful");
}
```

## Advanced: Runtime Settings Override

{% hint style="danger" %}
Advanced initialization is for server-driven configuration or runtime feature flags only. For standard usage, configure via [Essential Kit Settings](https://github.com/voxelbusters/essential-kit-docs/blob/master/tutorials/v3/features/cloud-services/setup.md).
{% endhint %}

### Understanding Advanced Initialization

**Default Behavior:** Essential Kit automatically initializes Cloud Services with settings from the ScriptableObject asset.

**Advanced Usage:** Override settings programmatically for:

* Server-driven feature configuration
* A/B testing different sync strategies
* Dynamic platform-specific settings
* Runtime feature flags

### Implementation

```csharp
void Awake()
{
    var settings = new CloudServicesUnitySettings(
        iosProperties: new CloudServicesUnitySettings.IosPlatformProperties(substituteEntitlementIdentifiers: true),
        androidProperties: new CloudServicesUnitySettings.AndroidPlatformProperties(playServicesApplicationId: "123456789012"));

    CloudServices.Initialize(settings);
}
```

{% hint style="warning" %}
Calling `Initialize()` resets all event listeners and clears cached data. Only use for advanced scenarios requiring runtime configuration.
{% endhint %}

## Related Guides

* Demo scene: `Assets/Plugins/VoxelBusters/EssentialKit/Examples/Scenes/CloudServicesDemo.unity`
* Pair with **Game Services** for player authentication and leaderboards
* See [FAQ](/features/cloud-services/faq.md#why-cant-i-see-my-local-changes-after-sync) for common conflict resolution issues
* Check [Testing](/features/cloud-services/testing.md) for cross-device validation procedures


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://assetstore.essentialkit.voxelbusters.com/features/cloud-services/usage.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
