# 4. Write Locks Conflicts and Offline Part of [[Cloud Save and Unity Services]]. Prev: [[3. Player Data Save and Load]]. Next: [[5. Worked Example High Scores Mirror]]. A write lock is how Cloud Save stops you from blindly overwriting a value that **someone else** (another device, a Cloud Code script, a second client) already changed. Official concept page: [Write locks](https://docs.unity.com/en-us/cloud-save/concepts/write-locks). Official request type: [`SaveItem`](https://docs.unity3d.com/Packages/[email protected]/api/Unity.Services.CloudSave.Models.SaveItem.html) in 3.4.2. This is **in addition to** your game-level comparison of `savedUtcTicks`. Locks protect the **service item**. Ticks protect the **player's progress story**. You want both. ## 4.1 How a lock works Official: - Load (and a successful save) return a `writeLock` string. Store it next to the item (in memory is enough; you can also persist it if you want, but the next load will give you a fresh one). - When you save, you may include that lock. If it does **not** match what the service has, the service returns a conflict error (`CloudSaveConflictException` on the `SaveItem` overload). - If you are **creating** a new item, **omit** the lock (official: omitting it on create avoids unnecessary errors). - If you want to **force** an overwrite, pass a **null** write lock on `SaveItem`. Official: a null lock **skips validation** and overwrites any existing value. `SaveAsync(IDictionary<string, object>)` from part 3 also overwrites without validation - same idea, no lock parameter. ```csharp using System.Collections.Generic; using System.Threading.Tasks; using Unity.Services.CloudSave; using Unity.Services.CloudSave.Models; /// <summary> /// Locked write of a JSON string. NOT compiled in Unity. /// </summary> public static class CloudSaveLocked { public static async Task<string> SaveJsonAsync(string key, string json, string writeLock) { var data = new Dictionary<string, SaveItem> { { key, new SaveItem(json, writeLock) } }; var locks = await CloudSaveService.Instance.Data.Player.SaveAsync(data); return locks.TryGetValue(key, out var updated) ? updated : null; } } ``` Official constructor: `SaveItem(object value, string writeLock)`. After a success, replace your stored lock with the returned one. ## 4.2 Conflict resolution (game policy) Official options when a write-lock error comes back: 1. If you are **certain** the value you have is the one to keep, bypass the lock (null lock / object dictionary overload) and overwrite. 2. Otherwise **load again**, then **manually** resolve. "Manually" in this course means **ask the player**, using the slot's `savedUtcTicks` (and any other human-readable summary: play time, location, score). Suggested prompts: - "This machine has a later save than the cloud. Upload this one?" - "The cloud has a later save than this machine. Download and replace the local file?" - "They differ and we cannot tell. Keep local, keep cloud, or cancel." Never pick "the cloud always wins" in a class project. A student who played offline on a bus will lose a session. Implementation sketch (not a full UI): 1. Local save already succeeded (or you are at startup). 2. Load cloud `slot_0` (if any) and its `WriteLock`. 3. If missing: `SaveItem(json, null)` or the object-dictionary `SaveAsync` to **create**. 4. If present: compare ticks. - Same ticks and same JSON: nothing to do. - Local newer, player confirms upload: `SaveItem(localJson, cloudWriteLock)`. If you get `CloudSaveConflictException`, load again and repeat - someone else wrote in between. - Cloud newer, player confirms download: write the cloud JSON to the **local** atomic save path (the High Scores save system's existing write, so `.bak` still works). Do not only keep it in memory. - Equal ticks but different JSON: treat as a conflict and ask. Do not invent a merge of two `JsonUtility` blobs unless you have tests for it (you will not, in this course). ## 4.3 Offline is normal Wrap every Cloud Save call: ```csharp try { await CloudSaveLocked.SaveJsonAsync(key, json, writeLock); } catch (CloudSaveRateLimitedException e) { Debug.LogWarning(quot;Cloud Save rate limited: {e}"); } catch (CloudSaveConflictException e) { Debug.LogWarning(quot;Cloud Save write lock conflict: {e}"); // Ask, reload, or (only if the player confirmed a force) save with a null lock. } catch (CloudSaveValidationException e) { Debug.LogWarning(quot;Cloud Save validation: {e}"); } catch (CloudSaveException e) { Debug.LogWarning(quot;Cloud Save failed (offline is OK): {e}"); } ``` If `EnsureSignedInAsync` itself fails, skip the mirror for this attempt. **Do not** retry in `Update`. A single retry on the next successful save, or a "Sync now" button, is enough for class. Cancellation: pass a `CancellationToken` through your wrapper if you add one ([[3. Cancellation Tokens]]); if you leave the official 3.4.2 `SaveAsync` signatures as they are (they do not take a token on the pages we fetched), cancel by ignoring a late result when the player has left the menu, not by inventing a parameter. ## 4.4 Exercise With two Editor data folders (or two machines) on the **same** anonymous account only if you can actually share that token - otherwise use one machine and simulate: load `slot_0`, save A with the lock, then try to save B with the **old** lock and confirm `CloudSaveConflictException`. Then save B with the **fresh** lock and confirm it lands. Write the player-facing prompt text you will use for "local newer" vs "cloud newer." ## Sources - Write locks: https://docs.unity.com/en-us/cloud-save/concepts/write-locks - `SaveItem` 3.4.2: https://docs.unity3d.com/Packages/[email protected]/api/Unity.Services.CloudSave.Models.SaveItem.html - `IPlayerDataService` 3.4.2: https://docs.unity3d.com/Packages/[email protected]/api/Unity.Services.CloudSave.Internal.IPlayerDataService.html