@musashishao/agent-kit 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/agents/ai-asset-factory.md +700 -0
- package/.agent/agents/ai-audio-factory.md +503 -0
- package/.agent/agents/game-developer.md +4 -4
- package/.agent/agents/orchestrator.md +113 -3
- package/.agent/agents/project-planner.md +67 -0
- package/.agent/agents/unity-mobile-master.md +949 -0
- package/.agent/mcp/config/registry.json +65 -51
- package/.agent/mcp/servers/notebooklm/README.md +114 -0
- package/.agent/mcp/servers/notebooklm/package.json +35 -0
- package/.agent/mcp/servers/notebooklm/src/auth/chrome.ts +225 -0
- package/.agent/mcp/servers/notebooklm/src/auth/index.ts +1 -0
- package/.agent/mcp/servers/notebooklm/src/index.ts +516 -0
- package/.agent/mcp/servers/notebooklm/src/services/index.ts +3 -0
- package/.agent/mcp/servers/notebooklm/src/services/library.ts +217 -0
- package/.agent/mcp/servers/notebooklm/src/services/notebooklm.ts +380 -0
- package/.agent/mcp/servers/notebooklm/tsconfig.json +15 -0
- package/.agent/mcp-gateway/README.md +169 -20
- package/.agent/mcp-gateway/package.json +22 -7
- package/.agent/mcp-gateway/src/auth/index.ts +55 -0
- package/.agent/mcp-gateway/src/auth/middleware.ts +242 -0
- package/.agent/mcp-gateway/src/auth/oauth.ts +462 -0
- package/.agent/mcp-gateway/src/auth/scopes.ts +227 -0
- package/.agent/mcp-gateway/src/index.ts +252 -105
- package/.agent/mcp-gateway/src/observability/index.ts +5 -0
- package/.agent/mcp-gateway/src/observability/otel.ts +405 -0
- package/.agent/mcp-gateway/src/transports/index.ts +5 -0
- package/.agent/mcp-gateway/src/transports/streamableHttp.ts +235 -0
- package/.agent/rules/CODEX.md +89 -0
- package/.agent/rules/CODE_RULES.md +73 -0
- package/.agent/rules/GEMINI.md +25 -0
- package/.agent/rules/MEMORY_STATE.md +110 -0
- package/.agent/rules/REFERENCE.md +33 -141
- package/.agent/rules/REF_SKILLS.md +116 -0
- package/.agent/rules/REF_WORKFLOWS.md +81 -0
- package/.agent/scripts/ak_cli.py +106 -5
- package/.agent/scripts/memory_manager.py +48 -9
- package/.agent/skills/anti-hallucination/SKILL.md +295 -0
- package/.agent/skills/anti-hallucination/scripts/check_hallucination.py +299 -0
- package/.agent/skills/bifurcation-analysis/SKILL.md +56 -0
- package/.agent/skills/brainstorming/SKILL.md +80 -6
- package/.agent/skills/decision-memory/SKILL.md +317 -0
- package/.agent/skills/emergence-detector/SKILL.md +230 -0
- package/.agent/skills/emergence-detector/scripts/check_emergence.py +265 -0
- package/.agent/skills/explained-qa/SKILL.md +142 -0
- package/.agent/skills/explained-qa/game-terminology.md +214 -0
- package/.agent/skills/game-development/ai-dialogue-engine/SKILL.md +442 -0
- package/.agent/skills/game-development/ai-graphics-generator/SKILL.md +463 -0
- package/.agent/skills/game-development/ai-playtest-framework/SKILL.md +570 -0
- package/.agent/skills/game-development/camera-systems/SKILL.md +607 -0
- package/.agent/skills/game-development/card-battle-engine/SKILL.md +618 -0
- package/.agent/skills/game-development/character-controller-3d/SKILL.md +908 -0
- package/.agent/skills/game-development/cloud-save-sync/SKILL.md +527 -0
- package/.agent/skills/game-development/combat-system/SKILL.md +748 -0
- package/.agent/skills/game-development/compliance-rating/SKILL.md +277 -0
- package/.agent/skills/game-development/crossplatform-build/SKILL.md +386 -0
- package/.agent/skills/game-development/cultivation-progression/SKILL.md +520 -0
- package/.agent/skills/game-development/data-driven-balance/SKILL.md +535 -0
- package/.agent/skills/game-development/game-analytics-integrator/SKILL.md +410 -0
- package/.agent/skills/game-development/game-audio-advanced/SKILL.md +646 -0
- package/.agent/skills/game-development/game-economy-designer/SKILL.md +375 -0
- package/.agent/skills/game-development/game-marketing/SKILL.md +85 -0
- package/.agent/skills/game-development/game-state-manager/SKILL.md +883 -0
- package/.agent/skills/game-development/hybrid-game-spec/SKILL.md +220 -0
- package/.agent/skills/game-development/inventory-quest/SKILL.md +747 -0
- package/.agent/skills/game-development/liveops/SKILL.md +308 -0
- package/.agent/skills/game-development/localization/SKILL.md +286 -0
- package/.agent/skills/game-development/mobile-input-patterns/SKILL.md +343 -0
- package/.agent/skills/game-development/monetization-strategy/SKILL.md +94 -0
- package/.agent/skills/game-development/multiplayer-master/SKILL.md +727 -0
- package/.agent/skills/game-development/narrative-branching/SKILL.md +593 -0
- package/.agent/skills/game-development/procedural-level-ai/SKILL.md +367 -0
- package/.agent/skills/game-development/prototyping-rapid/SKILL.md +205 -0
- package/.agent/skills/game-development/spec-ecosystem/SKILL.md +155 -0
- package/.agent/skills/game-development/spec-ecosystem/decision-log-format.md +129 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/PLAN-template.md +178 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/SPEC-template.md +110 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/TASKS-template.md +156 -0
- package/.agent/skills/game-development/survival-systems/SKILL.md +493 -0
- package/.agent/skills/game-development/testing-qa/SKILL.md +270 -0
- package/.agent/skills/game-development/unity-mobile-optimization/SKILL.md +271 -0
- package/.agent/skills/intent-capture/SKILL.md +65 -0
- package/.agent/skills/mcp-composition/SKILL.md +362 -0
- package/.agent/skills/mcp-observability/SKILL.md +323 -0
- package/.agent/skills/mcp-security/SKILL.md +314 -0
- package/.agent/skills/trust-spectrum/SKILL.md +291 -0
- package/.agent/skills/vibe-coding-guard/SKILL.md +328 -0
- package/.agent/templates/AGENTS.game.md +63 -0
- package/.agent/templates/docs/WORKFLOW_GUIDE.en.md +100 -0
- package/.agent/templates/docs/WORKFLOW_GUIDE.vi.md +100 -0
- package/.agent/workflows/ai-agent.md +2 -0
- package/.agent/workflows/autofix.md +1 -0
- package/.agent/workflows/brainstorm.md +1 -0
- package/.agent/workflows/context.md +1 -0
- package/.agent/workflows/create.md +39 -8
- package/.agent/workflows/dashboard.md +1 -0
- package/.agent/workflows/debug.md +14 -0
- package/.agent/workflows/deploy.md +14 -0
- package/.agent/workflows/enhance.md +44 -0
- package/.agent/workflows/gamekit-init.md +177 -0
- package/.agent/workflows/gamekit-launch.md +338 -0
- package/.agent/workflows/gamekit-plan.md +204 -0
- package/.agent/workflows/gamekit-qa.md +153 -0
- package/.agent/workflows/gamekit-spec.md +243 -0
- package/.agent/workflows/gamekit-tasks.md +208 -0
- package/.agent/workflows/marketing.md +2 -0
- package/.agent/workflows/next.md +1 -0
- package/.agent/workflows/orchestrate.md +12 -0
- package/.agent/workflows/pentest.md +2 -0
- package/.agent/workflows/plan.md +42 -0
- package/.agent/workflows/preview.md +1 -0
- package/.agent/workflows/quality.md +1 -0
- package/.agent/workflows/saas.md +2 -0
- package/.agent/workflows/spec.md +42 -0
- package/.agent/workflows/status.md +1 -0
- package/.agent/workflows/test.md +14 -0
- package/.agent/workflows/ui-ux-pro-max.md +1 -0
- package/bin/cli.js +411 -111
- package/package.json +1 -2
- package/.agent/agents/game-asset-curator.md +0 -317
- package/.agent/agents/game-narrative-designer.md +0 -310
- package/.agent/agents/game-qa-agent.md +0 -441
- package/.agent/workflows/game-prototype.md +0 -154
- package/docs/AI_DATA_INFRASTRUCTURE.md +0 -288
- package/docs/CHANGELOG_AI_INFRA.md +0 -141
- package/docs/MIGRATION_GUIDE_V1.9.md +0 -55
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cloud-save-sync
|
|
3
|
+
description: Cloud save and cross-device synchronization. Firebase Cloud Save, PlayFab, conflict resolution, offline-first patterns, and data migration for Unity mobile games.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cloud Save Sync Skill
|
|
7
|
+
|
|
8
|
+
> **Purpose**: Enable seamless cross-device gameplay progression.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
- Implementing cloud save functionality
|
|
12
|
+
- Enabling cross-device play
|
|
13
|
+
- Handling save conflicts
|
|
14
|
+
- Building offline-first games
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Save Data Architecture
|
|
19
|
+
|
|
20
|
+
### Flexible Save Structure
|
|
21
|
+
```csharp
|
|
22
|
+
[System.Serializable]
|
|
23
|
+
public class CloudSaveData
|
|
24
|
+
{
|
|
25
|
+
// Metadata
|
|
26
|
+
public string playerId;
|
|
27
|
+
public string deviceId;
|
|
28
|
+
public long timestamp; // Unix timestamp
|
|
29
|
+
public int saveVersion; // For migration
|
|
30
|
+
public string checksum; // Data integrity
|
|
31
|
+
|
|
32
|
+
// Game Data
|
|
33
|
+
public PlayerProgress player;
|
|
34
|
+
public InventoryData inventory;
|
|
35
|
+
public QuestProgress quests;
|
|
36
|
+
public SettingsData settings;
|
|
37
|
+
|
|
38
|
+
// Custom
|
|
39
|
+
public Dictionary<string, string> customData;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[System.Serializable]
|
|
43
|
+
public class PlayerProgress
|
|
44
|
+
{
|
|
45
|
+
public int level;
|
|
46
|
+
public long experience;
|
|
47
|
+
public int gold;
|
|
48
|
+
public int gems;
|
|
49
|
+
public string currentRealm; // For cultivation games
|
|
50
|
+
public float[] position; // Last position
|
|
51
|
+
public long playTimeSeconds;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
[System.Serializable]
|
|
55
|
+
public class InventoryData
|
|
56
|
+
{
|
|
57
|
+
public InventorySlot[] slots;
|
|
58
|
+
public EquipmentSlot[] equipment;
|
|
59
|
+
public string[] unlockedRecipes;
|
|
60
|
+
public CardDeckData[] decks; // For card games
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 2. Cloud Save Manager
|
|
67
|
+
|
|
68
|
+
### Core Implementation
|
|
69
|
+
```csharp
|
|
70
|
+
public class CloudSaveManager : MonoBehaviour
|
|
71
|
+
{
|
|
72
|
+
public static CloudSaveManager Instance { get; private set; }
|
|
73
|
+
|
|
74
|
+
[SerializeField] private CloudProvider _provider;
|
|
75
|
+
[SerializeField] private float _autoSaveInterval = 300f; // 5 minutes
|
|
76
|
+
|
|
77
|
+
public bool IsOnline { get; private set; }
|
|
78
|
+
public bool IsSyncing { get; private set; }
|
|
79
|
+
public DateTime LastSyncTime { get; private set; }
|
|
80
|
+
|
|
81
|
+
public event Action<CloudSaveData> OnSaveLoaded;
|
|
82
|
+
public event Action OnSaveUploaded;
|
|
83
|
+
public event Action<SaveConflict> OnConflictDetected;
|
|
84
|
+
public event Action<string> OnSyncError;
|
|
85
|
+
|
|
86
|
+
private CloudSaveData _localSave;
|
|
87
|
+
private CloudSaveData _cloudSave;
|
|
88
|
+
|
|
89
|
+
private void Start()
|
|
90
|
+
{
|
|
91
|
+
StartCoroutine(AutoSaveLoop());
|
|
92
|
+
StartCoroutine(MonitorConnectivity());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#region Save Operations
|
|
96
|
+
|
|
97
|
+
public async Task SaveLocal(CloudSaveData data)
|
|
98
|
+
{
|
|
99
|
+
data.timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
100
|
+
data.deviceId = SystemInfo.deviceUniqueIdentifier;
|
|
101
|
+
data.checksum = CalculateChecksum(data);
|
|
102
|
+
|
|
103
|
+
string json = JsonUtility.ToJson(data);
|
|
104
|
+
|
|
105
|
+
// Save to local storage
|
|
106
|
+
PlayerPrefs.SetString("local_save", json);
|
|
107
|
+
PlayerPrefs.Save();
|
|
108
|
+
|
|
109
|
+
_localSave = data;
|
|
110
|
+
|
|
111
|
+
// Attempt cloud sync if online
|
|
112
|
+
if (IsOnline)
|
|
113
|
+
{
|
|
114
|
+
await SyncToCloud(data);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public async Task<CloudSaveData> LoadSave()
|
|
119
|
+
{
|
|
120
|
+
// Load local
|
|
121
|
+
string localJson = PlayerPrefs.GetString("local_save", "");
|
|
122
|
+
_localSave = string.IsNullOrEmpty(localJson)
|
|
123
|
+
? new CloudSaveData()
|
|
124
|
+
: JsonUtility.FromJson<CloudSaveData>(localJson);
|
|
125
|
+
|
|
126
|
+
// Try load cloud
|
|
127
|
+
if (IsOnline)
|
|
128
|
+
{
|
|
129
|
+
try
|
|
130
|
+
{
|
|
131
|
+
_cloudSave = await _provider.FetchCloudSave();
|
|
132
|
+
|
|
133
|
+
// Check for conflict
|
|
134
|
+
if (_cloudSave != null && _localSave.timestamp > 0)
|
|
135
|
+
{
|
|
136
|
+
var resolution = ResolveConflict(_localSave, _cloudSave);
|
|
137
|
+
return resolution;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return _cloudSave ?? _localSave;
|
|
141
|
+
}
|
|
142
|
+
catch (Exception e)
|
|
143
|
+
{
|
|
144
|
+
OnSyncError?.Invoke(e.Message);
|
|
145
|
+
return _localSave;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return _localSave;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async Task SyncToCloud(CloudSaveData data)
|
|
153
|
+
{
|
|
154
|
+
IsSyncing = true;
|
|
155
|
+
|
|
156
|
+
try
|
|
157
|
+
{
|
|
158
|
+
await _provider.UploadSave(data);
|
|
159
|
+
LastSyncTime = DateTime.UtcNow;
|
|
160
|
+
OnSaveUploaded?.Invoke();
|
|
161
|
+
}
|
|
162
|
+
catch (Exception e)
|
|
163
|
+
{
|
|
164
|
+
OnSyncError?.Invoke($"Cloud sync failed: {e.Message}");
|
|
165
|
+
}
|
|
166
|
+
finally
|
|
167
|
+
{
|
|
168
|
+
IsSyncing = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#endregion
|
|
173
|
+
|
|
174
|
+
#region Conflict Resolution
|
|
175
|
+
|
|
176
|
+
private CloudSaveData ResolveConflict(CloudSaveData local, CloudSaveData cloud)
|
|
177
|
+
{
|
|
178
|
+
// Same timestamp = no conflict
|
|
179
|
+
if (local.timestamp == cloud.timestamp)
|
|
180
|
+
return local;
|
|
181
|
+
|
|
182
|
+
// Auto-resolve: newer wins
|
|
183
|
+
if (Mathf.Abs(local.timestamp - cloud.timestamp) < 60) // Within 1 minute
|
|
184
|
+
{
|
|
185
|
+
return local.timestamp > cloud.timestamp ? local : cloud;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Significant difference: ask player
|
|
189
|
+
var conflict = new SaveConflict
|
|
190
|
+
{
|
|
191
|
+
localSave = local,
|
|
192
|
+
cloudSave = cloud,
|
|
193
|
+
localTime = DateTimeOffset.FromUnixTimeSeconds(local.timestamp).LocalDateTime,
|
|
194
|
+
cloudTime = DateTimeOffset.FromUnixTimeSeconds(cloud.timestamp).LocalDateTime
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
OnConflictDetected?.Invoke(conflict);
|
|
198
|
+
|
|
199
|
+
// Default to newer while waiting for resolution
|
|
200
|
+
return local.timestamp > cloud.timestamp ? local : cloud;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public void ResolveConflictWith(ConflictChoice choice)
|
|
204
|
+
{
|
|
205
|
+
CloudSaveData chosen = choice switch
|
|
206
|
+
{
|
|
207
|
+
ConflictChoice.KeepLocal => _localSave,
|
|
208
|
+
ConflictChoice.KeepCloud => _cloudSave,
|
|
209
|
+
ConflictChoice.MergePreferLocal => MergeSaves(_localSave, _cloudSave),
|
|
210
|
+
ConflictChoice.MergePreferCloud => MergeSaves(_cloudSave, _localSave),
|
|
211
|
+
_ => _localSave
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
_localSave = chosen;
|
|
215
|
+
SaveLocal(chosen);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private CloudSaveData MergeSaves(CloudSaveData primary, CloudSaveData secondary)
|
|
219
|
+
{
|
|
220
|
+
var merged = JsonUtility.FromJson<CloudSaveData>(JsonUtility.ToJson(primary));
|
|
221
|
+
|
|
222
|
+
// Take higher values
|
|
223
|
+
merged.player.level = Mathf.Max(primary.player.level, secondary.player.level);
|
|
224
|
+
merged.player.experience = Mathf.Max(primary.player.experience, secondary.player.experience);
|
|
225
|
+
merged.player.gold = Mathf.Max(primary.player.gold, secondary.player.gold);
|
|
226
|
+
merged.player.playTimeSeconds = Mathf.Max(primary.player.playTimeSeconds, secondary.player.playTimeSeconds);
|
|
227
|
+
|
|
228
|
+
// Merge inventory (union of items)
|
|
229
|
+
merged.inventory = MergeInventories(primary.inventory, secondary.inventory);
|
|
230
|
+
|
|
231
|
+
// Merge quests (completed on either = completed)
|
|
232
|
+
merged.quests = MergeQuests(primary.quests, secondary.quests);
|
|
233
|
+
|
|
234
|
+
return merged;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#endregion
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public struct SaveConflict
|
|
241
|
+
{
|
|
242
|
+
public CloudSaveData localSave;
|
|
243
|
+
public CloudSaveData cloudSave;
|
|
244
|
+
public DateTime localTime;
|
|
245
|
+
public DateTime cloudTime;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public enum ConflictChoice
|
|
249
|
+
{
|
|
250
|
+
KeepLocal,
|
|
251
|
+
KeepCloud,
|
|
252
|
+
MergePreferLocal,
|
|
253
|
+
MergePreferCloud
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 3. Firebase Cloud Save
|
|
260
|
+
|
|
261
|
+
### Firebase Provider
|
|
262
|
+
```csharp
|
|
263
|
+
public class FirebaseCloudProvider : CloudProvider
|
|
264
|
+
{
|
|
265
|
+
private DatabaseReference _db;
|
|
266
|
+
private FirebaseAuth _auth;
|
|
267
|
+
|
|
268
|
+
public override async Task Initialize()
|
|
269
|
+
{
|
|
270
|
+
var dependencyStatus = await FirebaseApp.CheckAndFixDependenciesAsync();
|
|
271
|
+
if (dependencyStatus != DependencyStatus.Available)
|
|
272
|
+
{
|
|
273
|
+
throw new Exception($"Firebase error: {dependencyStatus}");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_auth = FirebaseAuth.DefaultInstance;
|
|
277
|
+
_db = FirebaseDatabase.DefaultInstance.RootReference;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public override async Task<CloudSaveData> FetchCloudSave()
|
|
281
|
+
{
|
|
282
|
+
string userId = _auth.CurrentUser.UserId;
|
|
283
|
+
|
|
284
|
+
var snapshot = await _db.Child("saves").Child(userId).GetValueAsync();
|
|
285
|
+
|
|
286
|
+
if (!snapshot.Exists)
|
|
287
|
+
return null;
|
|
288
|
+
|
|
289
|
+
string json = snapshot.GetRawJsonValue();
|
|
290
|
+
return JsonUtility.FromJson<CloudSaveData>(json);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public override async Task UploadSave(CloudSaveData data)
|
|
294
|
+
{
|
|
295
|
+
string userId = _auth.CurrentUser.UserId;
|
|
296
|
+
string json = JsonUtility.ToJson(data);
|
|
297
|
+
|
|
298
|
+
await _db.Child("saves").Child(userId).SetRawJsonValueAsync(json);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
public override async Task DeleteSave()
|
|
302
|
+
{
|
|
303
|
+
string userId = _auth.CurrentUser.UserId;
|
|
304
|
+
await _db.Child("saves").Child(userId).RemoveValueAsync();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Firebase Security Rules
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"rules": {
|
|
313
|
+
"saves": {
|
|
314
|
+
"$userId": {
|
|
315
|
+
".read": "$userId === auth.uid",
|
|
316
|
+
".write": "$userId === auth.uid",
|
|
317
|
+
".validate": "newData.hasChildren(['playerId', 'timestamp', 'saveVersion'])"
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 4. PlayFab Integration
|
|
327
|
+
|
|
328
|
+
### PlayFab Provider
|
|
329
|
+
```csharp
|
|
330
|
+
public class PlayFabCloudProvider : CloudProvider
|
|
331
|
+
{
|
|
332
|
+
public override async Task<CloudSaveData> FetchCloudSave()
|
|
333
|
+
{
|
|
334
|
+
var tcs = new TaskCompletionSource<CloudSaveData>();
|
|
335
|
+
|
|
336
|
+
PlayFabClientAPI.GetUserData(
|
|
337
|
+
new GetUserDataRequest { Keys = new List<string> { "save_data" } },
|
|
338
|
+
result =>
|
|
339
|
+
{
|
|
340
|
+
if (result.Data.TryGetValue("save_data", out var record))
|
|
341
|
+
{
|
|
342
|
+
var data = JsonUtility.FromJson<CloudSaveData>(record.Value);
|
|
343
|
+
tcs.SetResult(data);
|
|
344
|
+
}
|
|
345
|
+
else
|
|
346
|
+
{
|
|
347
|
+
tcs.SetResult(null);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
error => tcs.SetException(new Exception(error.ErrorMessage))
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return await tcs.Task;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
public override async Task UploadSave(CloudSaveData data)
|
|
357
|
+
{
|
|
358
|
+
var tcs = new TaskCompletionSource<bool>();
|
|
359
|
+
|
|
360
|
+
string json = JsonUtility.ToJson(data);
|
|
361
|
+
|
|
362
|
+
PlayFabClientAPI.UpdateUserData(
|
|
363
|
+
new UpdateUserDataRequest
|
|
364
|
+
{
|
|
365
|
+
Data = new Dictionary<string, string> { { "save_data", json } }
|
|
366
|
+
},
|
|
367
|
+
result => tcs.SetResult(true),
|
|
368
|
+
error => tcs.SetException(new Exception(error.ErrorMessage))
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
await tcs.Task;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 5. Offline-First Pattern
|
|
379
|
+
|
|
380
|
+
### Queue-Based Sync
|
|
381
|
+
```csharp
|
|
382
|
+
public class OfflineQueueManager : MonoBehaviour
|
|
383
|
+
{
|
|
384
|
+
private Queue<PendingOperation> _pendingOperations = new();
|
|
385
|
+
private bool _isProcessing = false;
|
|
386
|
+
|
|
387
|
+
public void QueueSave(CloudSaveData data)
|
|
388
|
+
{
|
|
389
|
+
_pendingOperations.Enqueue(new PendingOperation
|
|
390
|
+
{
|
|
391
|
+
type = OperationType.Save,
|
|
392
|
+
data = data,
|
|
393
|
+
timestamp = DateTime.UtcNow
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Save queue locally
|
|
397
|
+
PersistQueue();
|
|
398
|
+
|
|
399
|
+
// Try process if online
|
|
400
|
+
if (CloudSaveManager.Instance.IsOnline)
|
|
401
|
+
{
|
|
402
|
+
ProcessQueue();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private async void ProcessQueue()
|
|
407
|
+
{
|
|
408
|
+
if (_isProcessing || _pendingOperations.Count == 0)
|
|
409
|
+
return;
|
|
410
|
+
|
|
411
|
+
_isProcessing = true;
|
|
412
|
+
|
|
413
|
+
while (_pendingOperations.Count > 0 && CloudSaveManager.Instance.IsOnline)
|
|
414
|
+
{
|
|
415
|
+
var operation = _pendingOperations.Peek();
|
|
416
|
+
|
|
417
|
+
try
|
|
418
|
+
{
|
|
419
|
+
await ExecuteOperation(operation);
|
|
420
|
+
_pendingOperations.Dequeue();
|
|
421
|
+
PersistQueue();
|
|
422
|
+
}
|
|
423
|
+
catch
|
|
424
|
+
{
|
|
425
|
+
// Keep in queue for retry
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
_isProcessing = false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private void OnConnectivityRestored()
|
|
434
|
+
{
|
|
435
|
+
ProcessQueue();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
[System.Serializable]
|
|
440
|
+
public class PendingOperation
|
|
441
|
+
{
|
|
442
|
+
public OperationType type;
|
|
443
|
+
public CloudSaveData data;
|
|
444
|
+
public DateTime timestamp;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
public enum OperationType { Save, Delete, Achievement }
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 6. Data Migration
|
|
453
|
+
|
|
454
|
+
### Version Migration
|
|
455
|
+
```csharp
|
|
456
|
+
public class SaveMigrator
|
|
457
|
+
{
|
|
458
|
+
private static readonly int CURRENT_VERSION = 3;
|
|
459
|
+
|
|
460
|
+
public static CloudSaveData Migrate(CloudSaveData data)
|
|
461
|
+
{
|
|
462
|
+
while (data.saveVersion < CURRENT_VERSION)
|
|
463
|
+
{
|
|
464
|
+
data = MigrateStep(data);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return data;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private static CloudSaveData MigrateStep(CloudSaveData data)
|
|
471
|
+
{
|
|
472
|
+
switch (data.saveVersion)
|
|
473
|
+
{
|
|
474
|
+
case 1:
|
|
475
|
+
return MigrateV1ToV2(data);
|
|
476
|
+
case 2:
|
|
477
|
+
return MigrateV2ToV3(data);
|
|
478
|
+
default:
|
|
479
|
+
return data;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private static CloudSaveData MigrateV1ToV2(CloudSaveData data)
|
|
484
|
+
{
|
|
485
|
+
// V2 added gems currency
|
|
486
|
+
data.player.gems = 0;
|
|
487
|
+
|
|
488
|
+
// V2 split inventory into slots
|
|
489
|
+
if (data.customData.TryGetValue("old_inventory", out string oldInv))
|
|
490
|
+
{
|
|
491
|
+
data.inventory = ConvertOldInventory(oldInv);
|
|
492
|
+
data.customData.Remove("old_inventory");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
data.saveVersion = 2;
|
|
496
|
+
return data;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private static CloudSaveData MigrateV2ToV3(CloudSaveData data)
|
|
500
|
+
{
|
|
501
|
+
// V3 added card decks
|
|
502
|
+
data.inventory.decks = new CardDeckData[0];
|
|
503
|
+
|
|
504
|
+
data.saveVersion = 3;
|
|
505
|
+
return data;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Anti-Patterns
|
|
513
|
+
|
|
514
|
+
| ❌ Don't | ✅ Do |
|
|
515
|
+
|----------|-------|
|
|
516
|
+
| Save on every change | Batch saves, use intervals |
|
|
517
|
+
| Ignore offline | Queue operations for retry |
|
|
518
|
+
| Auto-resolve conflicts silently | Show player clear options |
|
|
519
|
+
| No checksums | Validate data integrity |
|
|
520
|
+
| No versioning | Support migration |
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Related Skills
|
|
525
|
+
- `game-development/game-state-manager` - Local save patterns
|
|
526
|
+
- `firebase` - Firebase integration
|
|
527
|
+
- `game-development/liveops` - Remote config
|