nccgs 1.0.0

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.
Files changed (278) hide show
  1. package/.claude/agents/nccgs-accessibility-specialist.md +26 -0
  2. package/.claude/agents/nccgs-adversarial-reviewer.md +26 -0
  3. package/.claude/agents/nccgs-ai-programmer.md +26 -0
  4. package/.claude/agents/nccgs-analytics-engineer.md +26 -0
  5. package/.claude/agents/nccgs-art-direction-lead.md +26 -0
  6. package/.claude/agents/nccgs-audio-direction-lead.md +26 -0
  7. package/.claude/agents/nccgs-creative-director.md +26 -0
  8. package/.claude/agents/nccgs-documentation-manager.md +26 -0
  9. package/.claude/agents/nccgs-economy-designer.md +26 -0
  10. package/.claude/agents/nccgs-engine-programmer.md +26 -0
  11. package/.claude/agents/nccgs-game-design-lead.md +26 -0
  12. package/.claude/agents/nccgs-game-designer.md +26 -0
  13. package/.claude/agents/nccgs-gameplay-programmer.md +26 -0
  14. package/.claude/agents/nccgs-level-designer.md +26 -0
  15. package/.claude/agents/nccgs-live-ops-designer.md +26 -0
  16. package/.claude/agents/nccgs-localization-lead.md +26 -0
  17. package/.claude/agents/nccgs-narrative-lead.md +26 -0
  18. package/.claude/agents/nccgs-network-programmer.md +26 -0
  19. package/.claude/agents/nccgs-performance-analyst.md +26 -0
  20. package/.claude/agents/nccgs-producer.md +26 -0
  21. package/.claude/agents/nccgs-production-coordinator.md +23 -0
  22. package/.claude/agents/nccgs-programming-lead.md +26 -0
  23. package/.claude/agents/nccgs-prototyper.md +26 -0
  24. package/.claude/agents/nccgs-qa-engineer.md +26 -0
  25. package/.claude/agents/nccgs-qa-lead.md +26 -0
  26. package/.claude/agents/nccgs-release-engineer.md +26 -0
  27. package/.claude/agents/nccgs-release-lead.md +26 -0
  28. package/.claude/agents/nccgs-security-engineer.md +26 -0
  29. package/.claude/agents/nccgs-sound-designer.md +26 -0
  30. package/.claude/agents/nccgs-systems-designer.md +26 -0
  31. package/.claude/agents/nccgs-technical-architect.md +26 -0
  32. package/.claude/agents/nccgs-technical-artist.md +26 -0
  33. package/.claude/agents/nccgs-technical-director.md +26 -0
  34. package/.claude/agents/nccgs-tools-programmer.md +26 -0
  35. package/.claude/agents/nccgs-ui-programmer.md +26 -0
  36. package/.claude/agents/nccgs-unity-build-specialist.md +26 -0
  37. package/.claude/agents/nccgs-unity-content-specialist.md +26 -0
  38. package/.claude/agents/nccgs-unity-implementer.md +26 -0
  39. package/.claude/agents/nccgs-unity-rendering-specialist.md +26 -0
  40. package/.claude/agents/nccgs-unity-systems-specialist.md +26 -0
  41. package/.claude/agents/nccgs-unity-ui-specialist.md +26 -0
  42. package/.claude/agents/nccgs-ux-designer.md +26 -0
  43. package/.claude/agents/nccgs-verification-engineer.md +26 -0
  44. package/.claude/agents/nccgs-world-builder.md +26 -0
  45. package/.claude/agents/nccgs-writer.md +26 -0
  46. package/.claude/nccgs/THIRD_PARTY_NOTICES.md +13 -0
  47. package/.claude/nccgs/VERSION +1 -0
  48. package/.claude/nccgs/constitution.md +131 -0
  49. package/.claude/nccgs/hooks/agent-audit.mjs +10 -0
  50. package/.claude/nccgs/hooks/common.mjs +41 -0
  51. package/.claude/nccgs/hooks/post-compact.mjs +1 -0
  52. package/.claude/nccgs/hooks/pre-compact.mjs +8 -0
  53. package/.claude/nccgs/hooks/protect-git.mjs +22 -0
  54. package/.claude/nccgs/hooks/protect-write.mjs +17 -0
  55. package/.claude/nccgs/hooks/session-start.mjs +15 -0
  56. package/.claude/nccgs/hooks/session-stop.mjs +7 -0
  57. package/.claude/nccgs/protocols/agent-contract.md +34 -0
  58. package/.claude/nccgs/protocols/context-packets.md +13 -0
  59. package/.claude/nccgs/protocols/evidence.md +15 -0
  60. package/.claude/nccgs/protocols/model-routing.md +22 -0
  61. package/.claude/nccgs/protocols/orchestration.md +26 -0
  62. package/.claude/nccgs/protocols/unity-boundary.md +11 -0
  63. package/.claude/nccgs/settings.fragment.json +77 -0
  64. package/.claude/nccgs/studio.json +347 -0
  65. package/.claude/nccgs/tools/configure-models.mjs +39 -0
  66. package/.claude/nccgs/unity-skills-manifest.json +727 -0
  67. package/.claude/nccgs/workflow-catalog.json +317 -0
  68. package/.claude/rules/nccgs-canonical-docs.md +19 -0
  69. package/.claude/rules/nccgs-editor-tools.md +12 -0
  70. package/.claude/rules/nccgs-localization.md +12 -0
  71. package/.claude/rules/nccgs-networking.md +13 -0
  72. package/.claude/rules/nccgs-performance.md +14 -0
  73. package/.claude/rules/nccgs-rendering.md +16 -0
  74. package/.claude/rules/nccgs-security.md +13 -0
  75. package/.claude/rules/nccgs-tests.md +18 -0
  76. package/.claude/rules/nccgs-ui.md +14 -0
  77. package/.claude/rules/nccgs-unity-assets.md +21 -0
  78. package/.claude/rules/nccgs-unity-code.md +22 -0
  79. package/.claude/skills/accessibility-review/SKILL.md +14 -0
  80. package/.claude/skills/architecture-decision/SKILL.md +14 -0
  81. package/.claude/skills/asset-audit/SKILL.md +14 -0
  82. package/.claude/skills/audit/SKILL.md +16 -0
  83. package/.claude/skills/audit/references/dimensions.md +46 -0
  84. package/.claude/skills/balance-review/SKILL.md +14 -0
  85. package/.claude/skills/bug-triage/SKILL.md +14 -0
  86. package/.claude/skills/build-live-game/SKILL.md +317 -0
  87. package/.claude/skills/build-live-game/references/achievements.md +779 -0
  88. package/.claude/skills/build-live-game/references/apis.md +280 -0
  89. package/.claude/skills/build-live-game/references/authentication.md +437 -0
  90. package/.claude/skills/build-live-game/references/battlepass.md +860 -0
  91. package/.claude/skills/build-live-game/references/cloud-code.md +563 -0
  92. package/.claude/skills/build-live-game/references/cloud-save.md +474 -0
  93. package/.claude/skills/build-live-game/references/deployment.md +216 -0
  94. package/.claude/skills/build-live-game/references/player-account.md +813 -0
  95. package/.claude/skills/build-live-game/references/remote-config.md +96 -0
  96. package/.claude/skills/build-live-game/references/tooling.md +431 -0
  97. package/.claude/skills/closure/SKILL.md +14 -0
  98. package/.claude/skills/code-review/SKILL.md +14 -0
  99. package/.claude/skills/compatibility-review/SKILL.md +14 -0
  100. package/.claude/skills/context-pack/SKILL.md +14 -0
  101. package/.claude/skills/dependency-review/SKILL.md +14 -0
  102. package/.claude/skills/design/SKILL.md +20 -0
  103. package/.claude/skills/design-review/SKILL.md +14 -0
  104. package/.claude/skills/evidence-review/SKILL.md +14 -0
  105. package/.claude/skills/hotfix/SKILL.md +14 -0
  106. package/.claude/skills/implement-in-app-purchases/README.md +233 -0
  107. package/.claude/skills/implement-in-app-purchases/SKILL.md +158 -0
  108. package/.claude/skills/implement-in-app-purchases/references/api-notes.md +562 -0
  109. package/.claude/skills/implement-in-app-purchases/references/codeless-catalog.md +331 -0
  110. package/.claude/skills/implement-in-app-purchases/references/convert-adapty.md +268 -0
  111. package/.claude/skills/implement-in-app-purchases/references/convert-essentialkit.md +239 -0
  112. package/.claude/skills/implement-in-app-purchases/references/convert-revenuecat.md +275 -0
  113. package/.claude/skills/implement-in-app-purchases/references/convert-unipay.md +145 -0
  114. package/.claude/skills/implement-in-app-purchases/references/migration-v4-to-v5.md +204 -0
  115. package/.claude/skills/implement-in-app-purchases/references/path-add-iap-to-new-project.md +198 -0
  116. package/.claude/skills/implement-in-app-purchases/references/path-convert-native-google-billing.md +356 -0
  117. package/.claude/skills/implement-in-app-purchases/references/path-convert-native-storekit.md +446 -0
  118. package/.claude/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md +680 -0
  119. package/.claude/skills/implement-in-app-purchases/references/platform-notes.md +204 -0
  120. package/.claude/skills/implement-in-app-purchases/references/pre-check.md +205 -0
  121. package/.claude/skills/incident-recovery/SKILL.md +14 -0
  122. package/.claude/skills/initialize-ai-navigation/SKILL.md +95 -0
  123. package/.claude/skills/initialize-ai-navigation/references/navigation-system.md +794 -0
  124. package/.claude/skills/levelplay-unity-integration/CHANGELOG.md +57 -0
  125. package/.claude/skills/levelplay-unity-integration/README.md +74 -0
  126. package/.claude/skills/levelplay-unity-integration/SKILL.md +1126 -0
  127. package/.claude/skills/levelplay-unity-integration/references/banner-api.md +920 -0
  128. package/.claude/skills/levelplay-unity-integration/references/best-practices.md +536 -0
  129. package/.claude/skills/levelplay-unity-integration/references/ilrd-api.md +337 -0
  130. package/.claude/skills/levelplay-unity-integration/references/initialization-api.md +630 -0
  131. package/.claude/skills/levelplay-unity-integration/references/interstitial-api.md +899 -0
  132. package/.claude/skills/levelplay-unity-integration/references/ios-setup.md +491 -0
  133. package/.claude/skills/levelplay-unity-integration/references/migration-sdk-9.md +666 -0
  134. package/.claude/skills/levelplay-unity-integration/references/privacy-settings.md +608 -0
  135. package/.claude/skills/levelplay-unity-integration/references/rewarded-api.md +902 -0
  136. package/.claude/skills/localization/SKILL.md +135 -0
  137. package/.claude/skills/localization/references/api-notes.md +76 -0
  138. package/.claude/skills/localization/resources/L10nBatchProcessor.cs +69 -0
  139. package/.claude/skills/localization/resources/LocalizedFontAsset.cs +18 -0
  140. package/.claude/skills/localize-game/SKILL.md +14 -0
  141. package/.claude/skills/migrate-project/SKILL.md +21 -0
  142. package/.claude/skills/migrate-project/references/procedure.md +63 -0
  143. package/.claude/skills/milestone-review/SKILL.md +14 -0
  144. package/.claude/skills/new-unity-project/SKILL.md +179 -0
  145. package/.claude/skills/optimize-audio/SKILL.md +199 -0
  146. package/.claude/skills/optimize-audio/resources/audio-import-api.md +146 -0
  147. package/.claude/skills/optimize-audio/resources/platform-settings.md +48 -0
  148. package/.claude/skills/optimize-text-mesh-pro/SKILL.md +182 -0
  149. package/.claude/skills/optimize-web/SKILL.md +393 -0
  150. package/.claude/skills/optimize-web/resources/WebOptimizer.cs +21 -0
  151. package/.claude/skills/optimize-web/resources/toktx-examples.sh +11 -0
  152. package/.claude/skills/performance-audit/SKILL.md +14 -0
  153. package/.claude/skills/physics-3d-collision/SKILL.md +442 -0
  154. package/.claude/skills/physics-3d-collision/references/troubleshooting.md +41 -0
  155. package/.claude/skills/physics-3d-collision/resources/CollisionDebugger.cs +33 -0
  156. package/.claude/skills/plan-feature/SKILL.md +14 -0
  157. package/.claude/skills/playtest/SKILL.md +14 -0
  158. package/.claude/skills/project-stage/SKILL.md +14 -0
  159. package/.claude/skills/prototype-feature/SKILL.md +14 -0
  160. package/.claude/skills/qa-plan/SKILL.md +14 -0
  161. package/.claude/skills/release/SKILL.md +18 -0
  162. package/.claude/skills/release-readiness/SKILL.md +14 -0
  163. package/.claude/skills/retrospective/SKILL.md +14 -0
  164. package/.claude/skills/review/SKILL.md +16 -0
  165. package/.claude/skills/security-audit/SKILL.md +14 -0
  166. package/.claude/skills/setup-multiplayer-services/SKILL.md +39 -0
  167. package/.claude/skills/setup-multiplayer-services/references/dgs-entrypoint.md +79 -0
  168. package/.claude/skills/setup-multiplayer-services/references/entrypoints.md +213 -0
  169. package/.claude/skills/setup-multiplayer-services/references/examples.md +33 -0
  170. package/.claude/skills/setup-multiplayer-services/references/implementation-fit.md +30 -0
  171. package/.claude/skills/setup-multiplayer-services/references/underlying-services.md +11 -0
  172. package/.claude/skills/setup-multiplayer-services/references/workflows-prerequisites.md +17 -0
  173. package/.claude/skills/setup-vivox-voice-chat/SKILL.md +118 -0
  174. package/.claude/skills/setup-vivox-voice-chat/evals/.env.example +6 -0
  175. package/.claude/skills/setup-vivox-voice-chat/evals/README.md +101 -0
  176. package/.claude/skills/setup-vivox-voice-chat/evals/promptfooconfig.yaml +32 -0
  177. package/.claude/skills/setup-vivox-voice-chat/evals/tests/init-and-login.yaml +76 -0
  178. package/.claude/skills/setup-vivox-voice-chat/evals/tests/text-chat.yaml +65 -0
  179. package/.claude/skills/setup-vivox-voice-chat/evals/tests/voice-channels.yaml +74 -0
  180. package/.claude/skills/setup-vivox-voice-chat/references/events-and-participants.md +79 -0
  181. package/.claude/skills/setup-vivox-voice-chat/references/init-and-login.md +92 -0
  182. package/.claude/skills/setup-vivox-voice-chat/references/text-chat.md +93 -0
  183. package/.claude/skills/setup-vivox-voice-chat/references/troubleshooting.md +47 -0
  184. package/.claude/skills/setup-vivox-voice-chat/references/voice-channels.md +90 -0
  185. package/.claude/skills/shader-graph-create-custom-node/SKILL.md +25 -0
  186. package/.claude/skills/shader-graph-create-custom-node/resources/all_hints.hlsl +182 -0
  187. package/.claude/skills/sprint-plan/SKILL.md +14 -0
  188. package/.claude/skills/sprite-editor/SKILL.md +66 -0
  189. package/.claude/skills/sprite-editor/references/api_reference.md +151 -0
  190. package/.claude/skills/sprite-editor/references/background.md +112 -0
  191. package/.claude/skills/sprite-editor/references/templates.md +72 -0
  192. package/.claude/skills/sprite-editor/scripts/AutomaticSliceTexture.cs +40 -0
  193. package/.claude/skills/sprite-editor/scripts/GenerateNewSpriteRects.cs +200 -0
  194. package/.claude/skills/sprite-editor/scripts/GetTextureSourceImageSize.cs +32 -0
  195. package/.claude/skills/sprite-editor/scripts/GetTextureToSlice.cs +55 -0
  196. package/.claude/skills/sprite-editor/scripts/GridSliceTexture.cs +40 -0
  197. package/.claude/skills/sprite-editor/scripts/IsometricSliceTexture.cs +141 -0
  198. package/.claude/skills/sprite-editor/scripts/README.md +134 -0
  199. package/.claude/skills/sprite-editor/scripts/SetPivotExample.cs +58 -0
  200. package/.claude/skills/sprite-editor/scripts/SpriteToPng.cs +88 -0
  201. package/.claude/skills/status/SKILL.md +16 -0
  202. package/.claude/skills/story-readiness/SKILL.md +14 -0
  203. package/.claude/skills/test/SKILL.md +16 -0
  204. package/.claude/skills/ui/SKILL.md +142 -0
  205. package/.claude/skills/ui-imgui/SKILL.md +186 -0
  206. package/.claude/skills/ui-imgui/references/gui-elements.md +156 -0
  207. package/.claude/skills/ui-imgui/references/templates.md +141 -0
  208. package/.claude/skills/ui-review/SKILL.md +14 -0
  209. package/.claude/skills/ui-ugui/SKILL.md +282 -0
  210. package/.claude/skills/ui-ugui/references/scrollview-setup.md +29 -0
  211. package/.claude/skills/ui-uitk/SKILL.md +235 -0
  212. package/.claude/skills/ui-uitk/references/common-issues.md +74 -0
  213. package/.claude/skills/ui-uitk/references/custom-elements.md +241 -0
  214. package/.claude/skills/ui-uitk/references/painter2d.md +282 -0
  215. package/.claude/skills/ui-uitk/references/pointermanipulator-guide.md +94 -0
  216. package/.claude/skills/ui-uitk/references/svg-icons.md +136 -0
  217. package/.claude/skills/ui-uitk/references/ui-runtime-binding.md +234 -0
  218. package/.claude/skills/ui-uitk/references/uss-guide.md +138 -0
  219. package/.claude/skills/unity-cli/CHANGELOG.md +233 -0
  220. package/.claude/skills/unity-cli/SECURITY.md +22 -0
  221. package/.claude/skills/unity-cli/SKILL.md +414 -0
  222. package/.claude/skills/unity-cli/references/auth-license-cloud.md +146 -0
  223. package/.claude/skills/unity-cli/references/build-run-test.md +349 -0
  224. package/.claude/skills/unity-cli/references/collaboration.md +472 -0
  225. package/.claude/skills/unity-cli/references/config-hub.md +103 -0
  226. package/.claude/skills/unity-cli/references/diagnostics-maintenance.md +326 -0
  227. package/.claude/skills/unity-cli/references/editors-install.md +327 -0
  228. package/.claude/skills/unity-cli/references/integration-advanced.md +472 -0
  229. package/.claude/skills/unity-cli/references/projects-templates.md +574 -0
  230. package/.claude/skills/unity-package-management/SKILL.md +304 -0
  231. package/.claude/skills/unity-package-management/references/select-packages.md +108 -0
  232. package/.claude/skills/urp-postprocessing/SKILL.md +188 -0
  233. package/.claude/skills/urp-postprocessing/references/code-templates.md +119 -0
  234. package/.claude/skills/urp-postprocessing/references/effect-reference.md +86 -0
  235. package/.claude/skills/validate-urp-render-graph-renderer-feature/SKILL.md +269 -0
  236. package/.claude/skills/work/SKILL.md +36 -0
  237. package/.claude/skills/work/references/classification.md +41 -0
  238. package/.claude/skills/work/references/closure.md +50 -0
  239. package/.claude/skills/work/references/feature-contracts.md +32 -0
  240. package/.claude/skills/work/references/verification.md +26 -0
  241. package/CLAUDE.md +6 -0
  242. package/LICENSE +21 -0
  243. package/README.md +162 -0
  244. package/THIRD_PARTY_NOTICES.md +23 -0
  245. package/UPGRADING.md +32 -0
  246. package/VERSION +1 -0
  247. package/docs/ARCHITECTURE.md +73 -0
  248. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +324 -0
  249. package/docs/MIGRATION-MATRIX.md +23 -0
  250. package/docs/PROJECT-POLICY.md +67 -0
  251. package/docs/WORKFLOWS.md +61 -0
  252. package/package-assets/setup-vivox-voice-chat-evals.gitignore +7 -0
  253. package/package.json +41 -0
  254. package/scaffold/.nccgs/bugs/.gitkeep +1 -0
  255. package/scaffold/.nccgs/closures/.gitkeep +1 -0
  256. package/scaffold/.nccgs/context/.gitkeep +1 -0
  257. package/scaffold/.nccgs/decisions/.gitkeep +1 -0
  258. package/scaffold/.nccgs/evidence/.gitkeep +1 -0
  259. package/scaffold/.nccgs/features/.gitkeep +1 -0
  260. package/scaffold/.nccgs/migrations/.gitkeep +1 -0
  261. package/scaffold/.nccgs/playtests/.gitkeep +1 -0
  262. package/scaffold/.nccgs/project.yaml +103 -0
  263. package/scaffold/.nccgs/requirements.yaml +10 -0
  264. package/scaffold/.nccgs/reviews/.gitkeep +1 -0
  265. package/scaffold/.nccgs/state.md +40 -0
  266. package/scaffold/.nccgs/templates/agent-handoff.md +25 -0
  267. package/scaffold/.nccgs/templates/architecture-decision.md +27 -0
  268. package/scaffold/.nccgs/templates/closure-record.md +51 -0
  269. package/scaffold/.nccgs/templates/context-packet.yaml +17 -0
  270. package/scaffold/.nccgs/templates/evidence-record.md +23 -0
  271. package/scaffold/.nccgs/templates/feature-contract.md +35 -0
  272. package/scaffold/.nccgs/templates/migration-plan.md +40 -0
  273. package/scaffold/.nccgs/templates/waiver.md +11 -0
  274. package/scripts/cli.mjs +56 -0
  275. package/scripts/install.mjs +267 -0
  276. package/scripts/sync-unity-skills.mjs +126 -0
  277. package/scripts/validate.mjs +205 -0
  278. package/tests/framework.test.mjs +121 -0
@@ -0,0 +1,779 @@
1
+ # Achievements Reference
2
+
3
+ ## Table of Contents
4
+
5
+ - [Architecture Overview](#architecture-overview)
6
+ - [Data Models](#data-models)
7
+ - [API Notes](#api-notes)
8
+ - [Namespace Aliases](#namespace-aliases-required)
9
+ - [Access Class Options](#access-class-options-reference)
10
+ - [Bucket Selection](#bucket-selection)
11
+ - [Error Handling](#error-handling)
12
+ - [Load Definitions from Remote Config](#load-definitions-from-remote-config)
13
+ - [Load Player Records from Cloud Save](#load-player-records-from-cloud-save)
14
+ - [Save Player Records (Direct Write)](#save-player-records-direct-write)
15
+ - [Server-Authoritative Writes via Cloud Code](#server-authoritative-writes-via-cloud-code)
16
+ - [Full Client Implementation](#full-client-implementation)
17
+ - [Cloud Code Module](#cloud-code-module)
18
+ - [AchievementModule.cs](#achievementmodulecs)
19
+ - [Server-Side Model](#server-side-model)
20
+ - [Cloud Resources](#cloud-resources)
21
+ - [Deployment Checklist](#deployment-checklist)
22
+ - [Official Unity Reference](#official-unity-reference)
23
+ - [Validation](#validation)
24
+
25
+ ---
26
+
27
+ ## Architecture Overview
28
+
29
+ Achievements combine Remote Config (static definitions), Cloud Save (per-player progress), and Cloud Code (server-authoritative writes). An Access Control policy locks down direct client writes in production.
30
+
31
+ ### Service Mapping
32
+
33
+ | Concern | Service | Key / Endpoint |
34
+ |---|---|---|
35
+ | Achievement definitions | Remote Config | `"achievements"` key (JSON array) |
36
+ | Player records | Cloud Save (Protected or Public) | `"achievements"` key (JSON array) |
37
+ | Server-authoritative writes | Cloud Code Module | `AchievementModule` |
38
+ | Block direct writes | Access Control | `.ac` policy denying player Cloud Save writes |
39
+
40
+ ### Data Flow
41
+
42
+ ```
43
+ Remote Config Cloud Save
44
+ "achievements" key "achievements" key
45
+ ┌─────────────────┐ ┌─────────────────┐
46
+ │ AchievementDef- │ client │ AchievementRe- │
47
+ │ inition[] │───reads────────>│ cord[] │
48
+ │ │ │ │
49
+ │ Id │ │ Id │
50
+ │ Title │ │ Unlocked │
51
+ │ Description │ │ ProgressCount │
52
+ │ IsHidden │ │ │
53
+ │ ProgressTarget │ └────────┬────────┘
54
+ └─────────────────┘ │
55
+ two write paths
56
+ ┌──────────────┴──────────────┐
57
+ │ │
58
+ Direct Write Trusted Write
59
+ (dev / prototype) (production)
60
+ │ │
61
+ ▼ ▼
62
+ Cloud Save Cloud Code
63
+ PublicWriteAccess- "AchievementModule"
64
+ ClassOptions │
65
+ ▼
66
+ Cloud Save
67
+ SetProtectedItemAsync
68
+ (server token bypass)
69
+ ```
70
+
71
+ **Key principle:** In production, route all writes through the Cloud Code module. The Access Control `.ac` file denies direct player writes to the Protected bucket, so only the server can mutate achievement records.
72
+
73
+ ---
74
+
75
+ ## Data Models
76
+
77
+ ### AchievementDefinition (client-side)
78
+
79
+ Stored in Remote Config as a JSON array under the `"achievements"` key. Read-only from the client.
80
+
81
+ ```csharp
82
+ using System;
83
+
84
+ [Serializable]
85
+ public class AchievementDefinition
86
+ {
87
+ public string Id;
88
+ public string Title;
89
+ public string Description;
90
+ public bool IsHidden;
91
+ public int ProgressTarget; // <= 1 = single unlock, > 1 = multi-stage
92
+ }
93
+ ```
94
+
95
+ | Field | Type | Description |
96
+ |---|---|---|
97
+ | `Id` | `string` | Unique identifier (e.g. `"first_win"`) |
98
+ | `Title` | `string` | Display name shown to the player |
99
+ | `Description` | `string` | What the player must do |
100
+ | `IsHidden` | `bool` | If true, hide until unlocked (secret achievements) |
101
+ | `ProgressTarget` | `int` | Target count for multi-stage achievements; 0 or 1 = single unlock |
102
+
103
+ ### AchievementRecord (client-side)
104
+
105
+ Stored in Cloud Save as a JSON array under the `"achievements"` key.
106
+
107
+ ```csharp
108
+ using System;
109
+
110
+ [Serializable]
111
+ public class AchievementRecord
112
+ {
113
+ public string Id;
114
+ public bool Unlocked;
115
+ public int ProgressCount;
116
+ }
117
+ ```
118
+
119
+ | Field | Type | Description |
120
+ |---|---|---|
121
+ | `Id` | `string` | Matches `AchievementDefinition.Id` |
122
+ | `Unlocked` | `bool` | True once the achievement is fully earned |
123
+ | `ProgressCount` | `int` | Current progress toward `ProgressTarget` |
124
+
125
+ ---
126
+
127
+ ## API Notes
128
+
129
+ ### Namespace Aliases (Required)
130
+
131
+ Cloud Save has identically named option classes at different namespace levels. Always use these
132
+ aliases to avoid ambiguity:
133
+
134
+ ```csharp
135
+ using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
136
+ using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
137
+ using PlayerDeleteOptions = Unity.Services.CloudSave.Models.Data.Player.DeleteOptions;
138
+ ```
139
+
140
+ Without these aliases, the compiler will report ambiguous type references.
141
+
142
+ ### Access Class Options Reference
143
+
144
+ | Class | Read | Write | Use case |
145
+ |---|---|---|---|
146
+ | `DefaultWriteAccessClassOptions` / `DefaultReadAccessClassOptions` | Owner only | Owner only | Private player data |
147
+ | `PublicReadAccessClassOptions(playerId)` / `PublicWriteAccessClassOptions` | Anyone | Owner + server | Achievements, scores, public profiles |
148
+ | `ProtectedReadAccessClassOptions` | Owner only | Server only (Cloud Code) | Anti-cheat, server-awarded state |
149
+
150
+ ### Bucket Selection
151
+
152
+ The read access class **must** match the bucket that was written to. Cloud Save has three
153
+ separate buckets — Default, Public, and Protected. Data written to one bucket is **invisible**
154
+ when reading from another.
155
+
156
+ - **Trusted path** (Cloud Code `SetProtectedItemAsync`) — data lands in the **Protected** bucket — read with `ProtectedReadAccessClassOptions()`
157
+ - **Direct path** (`PublicWriteAccessClassOptions`) — data lands in the **Public** bucket — read with `PublicReadAccessClassOptions(playerId)`
158
+
159
+ > **Common mistake:** Switching from direct writes to trusted writes (or vice versa) without
160
+ > updating the read access class. The data appears to vanish — it's still there, just in
161
+ > the other bucket.
162
+
163
+ ### Error Handling
164
+
165
+ Cloud Code and Cloud Save errors surface as typed exceptions on the client:
166
+
167
+ ```csharp
168
+ try
169
+ {
170
+ await CloudCodeService.Instance.CallModuleEndpointAsync(
171
+ "AchievementModule", "UnlockAchievement",
172
+ new Dictionary<string, object> { { "achievementId", "first_win" } });
173
+ }
174
+ catch (CloudCodeException ex)
175
+ {
176
+ // ex.Reason: CloudCodeExceptionReason (e.g. ScriptError for module-thrown exceptions)
177
+ Debug.LogError($"[Achievements] Cloud Code error: {ex.Reason} — {ex.Message}");
178
+ }
179
+ catch (CloudSaveException ex)
180
+ {
181
+ // ex.Reason: CloudSaveExceptionReason
182
+ // A 403 on the Protected bucket means the client attempted a direct write —
183
+ // all writes must go through the Cloud Code module when Access Control is deployed.
184
+ Debug.LogError($"[Achievements] Cloud Save error: {ex.Reason} — {ex.Message}");
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Load Definitions from Remote Config
191
+
192
+ Fetch the `"achievements"` Remote Config key and deserialize the JSON array into a list of definitions.
193
+
194
+ ```csharp
195
+ using Unity.Services.RemoteConfig;
196
+ using Newtonsoft.Json.Linq;
197
+ using System.Collections.Generic;
198
+ using System.Threading.Tasks;
199
+
200
+ struct UserAttributes {}
201
+ struct AppAttributes {}
202
+
203
+ async Task<List<AchievementDefinition>> LoadDefinitionsAsync()
204
+ {
205
+ var result = await RemoteConfigService.Instance.FetchConfigsAsync(
206
+ new UserAttributes(), new AppAttributes());
207
+
208
+ var token = result.config["achievements"];
209
+ return token.ToObject<List<AchievementDefinition>>();
210
+ }
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Load Player Records from Cloud Save
216
+
217
+ **CRITICAL -- bucket selection:** The read access class must match the bucket that was written to.
218
+
219
+ - **Trusted path (production):** The Cloud Code module writes to the **Protected** bucket. The client reads with `ProtectedReadAccessClassOptions()`.
220
+ - **Direct path (dev only):** The client writes to the **Public** bucket. The client reads with `PublicReadAccessClassOptions(playerId)`.
221
+
222
+ If you read from the wrong bucket, the data will appear empty.
223
+
224
+ ```csharp
225
+ using System.Collections.Generic;
226
+ using System.Threading.Tasks;
227
+ using Newtonsoft.Json;
228
+ using Unity.Services.CloudSave;
229
+ using Unity.Services.CloudSave.Models.Data.Player;
230
+ using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
231
+
232
+ async Task<List<AchievementRecord>> LoadRecordsAsync(string playerId, bool useTrustedWrites)
233
+ {
234
+ // Read from whichever bucket was written to -- Protected (trusted) or Public (direct).
235
+ ReadAccessClassOptions accessClass = useTrustedWrites
236
+ ? new ProtectedReadAccessClassOptions()
237
+ : new PublicReadAccessClassOptions(playerId);
238
+
239
+ var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
240
+ new HashSet<string> { "achievements" },
241
+ new PlayerLoadOptions(accessClass));
242
+
243
+ if (!result.TryGetValue("achievements", out var item))
244
+ return new List<AchievementRecord>();
245
+
246
+ return JsonConvert.DeserializeObject<List<AchievementRecord>>(item.Value.GetAsString());
247
+ }
248
+ ```
249
+
250
+ **`ProtectedReadAccessClassOptions`** — reads the current player's own Protected data (no player ID parameter; always reads self).
251
+
252
+ **`PublicReadAccessClassOptions(playerId)`** — reads another player's Public data. Pass `AuthenticationService.Instance.PlayerId` for the current player.
253
+
254
+ ---
255
+
256
+ ## Save Player Records (Direct Write)
257
+
258
+ Direct writes use the **Public** access class. This path is for development and prototyping only -- in production, deploy an Access Control policy to block direct player writes and route everything through Cloud Code.
259
+
260
+ ```csharp
261
+ using System.Collections.Generic;
262
+ using System.Threading.Tasks;
263
+ using Newtonsoft.Json;
264
+ using Unity.Services.CloudSave;
265
+ using Unity.Services.CloudSave.Models;
266
+ using Unity.Services.CloudSave.Models.Data.Player;
267
+ using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
268
+
269
+ async Task SaveRecordsAsync(List<AchievementRecord> records)
270
+ {
271
+ var json = JsonConvert.SerializeObject(records);
272
+ await CloudSaveService.Instance.Data.Player.SaveAsync(
273
+ new Dictionary<string, SaveItem>
274
+ {
275
+ { "achievements", new SaveItem(json, null) }
276
+ },
277
+ new PlayerSaveOptions(new PublicWriteAccessClassOptions()));
278
+ }
279
+ ```
280
+
281
+ ---
282
+
283
+ ## Server-Authoritative Writes via Cloud Code
284
+
285
+ In production, route all mutations through Cloud Code module endpoints. The module uses its service token to write to the Protected bucket, bypassing Access Control restrictions that block the player client.
286
+
287
+ ### Unlock an Achievement
288
+
289
+ ```csharp
290
+ await CloudCodeService.Instance.CallModuleEndpointAsync(
291
+ "AchievementModule", "UnlockAchievement",
292
+ new Dictionary<string, object> { { "achievementId", achievementId } });
293
+ ```
294
+
295
+ ### Update Achievement Progress
296
+
297
+ ```csharp
298
+ await CloudCodeService.Instance.CallModuleEndpointAsync(
299
+ "AchievementModule", "UpdateAchievementProgress",
300
+ new Dictionary<string, object>
301
+ {
302
+ { "achievementId", achievementId },
303
+ { "count", newCount }
304
+ });
305
+ ```
306
+
307
+ ### Reset Achievements
308
+
309
+ ```csharp
310
+ await CloudCodeService.Instance.CallModuleEndpointAsync(
311
+ "AchievementModule", "ResetAchievements",
312
+ new Dictionary<string, object>());
313
+ ```
314
+
315
+ ### Delete / Reset Player Records (Direct)
316
+
317
+ ```csharp
318
+ await CloudSaveService.Instance.Data.Player.DeleteAsync(
319
+ "achievements",
320
+ new PlayerDeleteOptions(new PublicWriteAccessClassOptions()));
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Full Client Implementation
326
+
327
+ Complete `AchievementManager` MonoBehaviour with dual-path support (trusted vs direct writes).
328
+
329
+ ```csharp
330
+ using System;
331
+ using System.Collections.Generic;
332
+ using System.Linq;
333
+ using System.Threading.Tasks;
334
+ using Newtonsoft.Json;
335
+ using Unity.Services.Authentication;
336
+ using Unity.Services.CloudCode;
337
+ using Unity.Services.CloudSave;
338
+ using Unity.Services.CloudSave.Models;
339
+ using Unity.Services.CloudSave.Models.Data.Player;
340
+ using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
341
+ using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
342
+ using PlayerDeleteOptions = Unity.Services.CloudSave.Models.Data.Player.DeleteOptions;
343
+ using Unity.Services.Core;
344
+ using Unity.Services.RemoteConfig;
345
+ using UnityEngine;
346
+
347
+ public class AchievementManager : MonoBehaviour
348
+ {
349
+ // Set to true to route writes through Cloud Code (required if Access Control is deployed).
350
+ // Set to false for direct Cloud Save writes (development only).
351
+ [SerializeField] bool m_UseTrustedWrites = true;
352
+
353
+ public List<AchievementDefinition> Definitions { get; private set; } = new();
354
+ public List<AchievementRecord> Records { get; private set; } = new();
355
+
356
+ public event Action<AchievementDefinition> AchievementUnlocked;
357
+ public event Action Loaded;
358
+
359
+ async void Start()
360
+ {
361
+ await UnityServices.InitializeAsync();
362
+ await AuthenticationService.Instance.SignInAnonymouslyAsync();
363
+ await LoadAsync();
364
+ Loaded?.Invoke();
365
+ }
366
+
367
+ // -- Load ------------------------------------------------------------------
368
+
369
+ public async Task LoadAsync()
370
+ {
371
+ Definitions = await LoadDefinitionsAsync();
372
+ Records = await LoadRecordsAsync(AuthenticationService.Instance.PlayerId);
373
+ }
374
+
375
+ struct UserAttributes {}
376
+ struct AppAttributes {}
377
+
378
+ async Task<List<AchievementDefinition>> LoadDefinitionsAsync()
379
+ {
380
+ var result = await RemoteConfigService.Instance.FetchConfigsAsync(
381
+ new UserAttributes(), new AppAttributes());
382
+
383
+ var token = result.config["achievements"];
384
+ return token.ToObject<List<AchievementDefinition>>();
385
+ }
386
+
387
+ async Task<List<AchievementRecord>> LoadRecordsAsync(string playerId)
388
+ {
389
+ // Read from whichever bucket was written to -- Protected (trusted) or Public (direct).
390
+ ReadAccessClassOptions accessClass = m_UseTrustedWrites
391
+ ? new ProtectedReadAccessClassOptions()
392
+ : new PublicReadAccessClassOptions(playerId);
393
+
394
+ var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
395
+ new HashSet<string> { "achievements" },
396
+ new PlayerLoadOptions(accessClass));
397
+
398
+ if (!result.TryGetValue("achievements", out var item))
399
+ return new List<AchievementRecord>();
400
+
401
+ return JsonConvert.DeserializeObject<List<AchievementRecord>>(item.Value.GetAsString());
402
+ }
403
+
404
+ // -- Unlock ----------------------------------------------------------------
405
+
406
+ public async Task UnlockAsync(string achievementId)
407
+ {
408
+ var def = Definitions.FirstOrDefault(d => d.Id == achievementId);
409
+ if (def == null)
410
+ {
411
+ Debug.LogWarning($"[Achievements] Unknown achievement: {achievementId}");
412
+ return;
413
+ }
414
+
415
+ var record = GetOrCreateRecord(achievementId);
416
+ if (record.Unlocked)
417
+ return;
418
+
419
+ if (m_UseTrustedWrites)
420
+ {
421
+ // Server validates and writes -- bypasses Access Control restrictions.
422
+ await CloudCodeService.Instance.CallModuleEndpointAsync(
423
+ "AchievementModule", "UnlockAchievement",
424
+ new Dictionary<string, object> { { "achievementId", achievementId } });
425
+ }
426
+ else
427
+ {
428
+ record.Unlocked = true;
429
+ await SaveRecordsAsync(Records);
430
+ }
431
+
432
+ record.Unlocked = true;
433
+ AchievementUnlocked?.Invoke(def);
434
+ Debug.Log($"[Achievements] Unlocked: {def.Title}");
435
+ }
436
+
437
+ // -- Progress (multi-stage) ------------------------------------------------
438
+
439
+ public async Task AddProgressAsync(string achievementId, int amount)
440
+ {
441
+ var def = Definitions.FirstOrDefault(d => d.Id == achievementId);
442
+ if (def == null || def.ProgressTarget <= 1)
443
+ return;
444
+
445
+ var record = GetOrCreateRecord(achievementId);
446
+ if (record.Unlocked)
447
+ return;
448
+
449
+ var newCount = record.ProgressCount + amount;
450
+
451
+ if (m_UseTrustedWrites)
452
+ {
453
+ await CloudCodeService.Instance.CallModuleEndpointAsync(
454
+ "AchievementModule", "UpdateAchievementProgress",
455
+ new Dictionary<string, object>
456
+ {
457
+ { "achievementId", achievementId },
458
+ { "count", newCount }
459
+ });
460
+ }
461
+ else
462
+ {
463
+ record.ProgressCount = newCount;
464
+ await SaveRecordsAsync(Records);
465
+ }
466
+
467
+ record.ProgressCount = newCount;
468
+
469
+ if (record.ProgressCount >= def.ProgressTarget)
470
+ await UnlockAsync(achievementId);
471
+ }
472
+
473
+ // -- Reset -----------------------------------------------------------------
474
+
475
+ public async Task ResetAsync()
476
+ {
477
+ await CloudSaveService.Instance.Data.Player.DeleteAsync(
478
+ "achievements",
479
+ new PlayerDeleteOptions(new PublicWriteAccessClassOptions()));
480
+
481
+ Records.Clear();
482
+ }
483
+
484
+ // -- Helpers ---------------------------------------------------------------
485
+
486
+ AchievementRecord GetOrCreateRecord(string id)
487
+ {
488
+ var record = Records.FirstOrDefault(r => r.Id == id);
489
+ if (record == null)
490
+ {
491
+ record = new AchievementRecord { Id = id };
492
+ Records.Add(record);
493
+ }
494
+ return record;
495
+ }
496
+
497
+ async Task SaveRecordsAsync(List<AchievementRecord> records)
498
+ {
499
+ var json = JsonConvert.SerializeObject(records);
500
+ await CloudSaveService.Instance.Data.Player.SaveAsync(
501
+ new Dictionary<string, SaveItem>
502
+ {
503
+ { "achievements", new SaveItem(json, null) }
504
+ },
505
+ new PlayerSaveOptions(new PublicWriteAccessClassOptions()));
506
+ }
507
+ }
508
+ ```
509
+
510
+ ---
511
+
512
+ ## Cloud Code Module
513
+
514
+ The `AchievementModule` reads and writes to the **Protected** Cloud Save bucket using its service token, bypassing Access Control restrictions imposed on the player client.
515
+
516
+ > **Module scaffolding — follow [cloud-code.md — Module Creation](cloud-code.md#module-creation).**
517
+ > Replace `MyModule` / `MyModuleCCM` with `AchievementModule` / `AchievementsCCM` throughout.
518
+ > You **must** create every file listed there — missing any one will cause deployment to fail:
519
+ >
520
+ > - [ ] `.sln` (generate fresh GUIDs)
521
+ > - [ ] `.csproj` (net9.0, CloudCode.Apis + CloudCode.Core)
522
+ > - [ ] `ModuleSetup.cs` (registers `GameApiClient`)
523
+ > - [ ] `Properties/PublishProfiles/FolderProfile.pubxml` — **without this file, deployment fails with "Failed to retrieve main project"**
524
+ > - [ ] `Assets/CloudCode/AchievementModule.ccmr` (points to the `.sln`)
525
+
526
+ ### AchievementModule.cs
527
+
528
+ ```csharp
529
+ using System.Collections.Generic;
530
+ using System.Linq;
531
+ using System.Threading.Tasks;
532
+ using Newtonsoft.Json;
533
+ using Unity.Services.CloudCode.Apis;
534
+ using Unity.Services.CloudCode.Core;
535
+ using Unity.Services.CloudSave.Model;
536
+
537
+ // Each public method decorated with [CloudCodeFunction] becomes a callable endpoint.
538
+ // IExecutionContext and IGameApiClient are injected automatically by the framework.
539
+ public class AchievementModule
540
+ {
541
+ const string AchievementsKey = "achievements";
542
+
543
+ [CloudCodeFunction("UnlockAchievement")]
544
+ public async Task<AchievementRecord> UnlockAchievement(
545
+ IExecutionContext context, IGameApiClient client, string achievementId)
546
+ {
547
+ var records = await LoadRecordsAsync(context, client);
548
+
549
+ var record = records.FirstOrDefault(r => r.Id == achievementId);
550
+ if (record == null)
551
+ {
552
+ record = new AchievementRecord { Id = achievementId, Unlocked = true };
553
+ records.Add(record);
554
+ }
555
+ else
556
+ {
557
+ record.Unlocked = true;
558
+ }
559
+
560
+ await SaveRecordsAsync(context, client, records);
561
+ return record;
562
+ }
563
+
564
+ [CloudCodeFunction("UpdateAchievementProgress")]
565
+ public async Task<AchievementRecord> UpdateAchievementProgress(
566
+ IExecutionContext context, IGameApiClient client, string achievementId, int count)
567
+ {
568
+ var records = await LoadRecordsAsync(context, client);
569
+
570
+ var record = records.FirstOrDefault(r => r.Id == achievementId);
571
+ if (record == null)
572
+ {
573
+ record = new AchievementRecord { Id = achievementId, ProgressCount = count };
574
+ records.Add(record);
575
+ }
576
+ else
577
+ {
578
+ record.ProgressCount += count;
579
+ }
580
+
581
+ await SaveRecordsAsync(context, client, records);
582
+ return record;
583
+ }
584
+
585
+ [CloudCodeFunction("ResetAchievements")]
586
+ public async Task ResetAchievements(IExecutionContext context, IGameApiClient client)
587
+ {
588
+ // Uses ServiceToken so the write bypasses Access Control restrictions on the player client.
589
+ await client.CloudSaveData.DeleteProtectedItemAsync(
590
+ context, context.ServiceToken, AchievementsKey,
591
+ context.ProjectId, context.PlayerId!);
592
+ }
593
+
594
+ async Task<List<AchievementRecord>> LoadRecordsAsync(
595
+ IExecutionContext context, IGameApiClient client)
596
+ {
597
+ var result = await client.CloudSaveData.GetProtectedItemsAsync(
598
+ context, context.ServiceToken, context.ProjectId,
599
+ context.PlayerId, new List<string> { AchievementsKey });
600
+
601
+ var value = result.Data?.Results?.FirstOrDefault()?.Value?.ToString();
602
+ if (string.IsNullOrEmpty(value))
603
+ return new List<AchievementRecord>();
604
+
605
+ return JsonConvert.DeserializeObject<List<AchievementRecord>>(value);
606
+ }
607
+
608
+ async Task SaveRecordsAsync(
609
+ IExecutionContext context, IGameApiClient client, List<AchievementRecord> records)
610
+ {
611
+ var json = JsonConvert.SerializeObject(records);
612
+ var body = new SetItemBody(AchievementsKey, json);
613
+ await client.CloudSaveData.SetProtectedItemAsync(
614
+ context, context.ServiceToken, context.ProjectId, context.PlayerId, body);
615
+ }
616
+ }
617
+ ```
618
+
619
+ ### Server-Side Model
620
+
621
+ Must match client-side field names exactly for JSON round-trip. Server-side uses properties
622
+ (not fields) for serialization compatibility with `Newtonsoft.Json`.
623
+
624
+ ```csharp
625
+ public class AchievementRecord
626
+ {
627
+ public string Id { get; set; }
628
+ public bool Unlocked { get; set; }
629
+ public int ProgressCount { get; set; }
630
+ }
631
+ ```
632
+
633
+ ---
634
+
635
+ ## Cloud Resources
636
+
637
+ ### Package Setup -- `manifest.json`
638
+
639
+ Add these entries to your Unity project's `Packages/manifest.json` dependencies:
640
+
641
+ ```json
642
+ {
643
+ "dependencies": {
644
+ "com.unity.services.authentication": "3.6.1",
645
+ "com.unity.services.cloudcode": "2.10.2",
646
+ "com.unity.services.cloudsave": "3.4.0",
647
+ "com.unity.services.core": "1.16.0",
648
+ "com.unity.services.deployment": "1.7.2",
649
+ "com.unity.remote-config": "4.2.5",
650
+ "com.unity.services.tooling": "1.4.1"
651
+ }
652
+ }
653
+ ```
654
+
655
+ The `com.unity.services.tooling` package is required for `.ac` file deployment. The `com.unity.services.deployment` package provides the Deployment Window. `Newtonsoft.Json` is provided as a transitive dependency and does not need an explicit entry.
656
+
657
+ ### Remote Config -- `Assets/Config/Achievements.rc`
658
+
659
+ Right-click in the Project window → **Assets > Create > Services > Remote Config**. Name the file `Achievements`. Replace the generated contents with:
660
+
661
+ ```json
662
+ {
663
+ "$schema": "https://ugs-config-schemas.unity3d.com/v1/remote-config.schema.json",
664
+ "entries": {
665
+ "achievements": [
666
+ {
667
+ "Id": "first_win",
668
+ "Title": "First Win",
669
+ "Description": "Win your first match.",
670
+ "IsHidden": false,
671
+ "ProgressTarget": 0
672
+ },
673
+ {
674
+ "Id": "kill_100_enemies",
675
+ "Title": "Centurion",
676
+ "Description": "Defeat 100 enemies.",
677
+ "IsHidden": false,
678
+ "ProgressTarget": 100
679
+ },
680
+ {
681
+ "Id": "secret_ending",
682
+ "Title": "???",
683
+ "Description": "Find the secret ending.",
684
+ "IsHidden": true,
685
+ "ProgressTarget": 0
686
+ }
687
+ ]
688
+ },
689
+ "types": {
690
+ "achievements": "JSON"
691
+ }
692
+ }
693
+ ```
694
+
695
+ **Rules:**
696
+
697
+ - `Id` must be unique across all achievements.
698
+ - `ProgressTarget <= 1` means single-unlock (call `UnlockAsync`).
699
+ - `ProgressTarget > 1` means multi-stage (call `AddProgressAsync`).
700
+ - `IsHidden = true` — show obfuscated title/description until unlocked.
701
+
702
+ Deploy via **Services > Deployment** — the Deployment Window auto-discovers `.rc` files under `Assets/`.
703
+
704
+ ### Cloud Code Module
705
+
706
+ > Create the full module solution using the file table and templates in
707
+ > [Cloud Code Module](#cloud-code-module) above.
708
+ > All scaffolding files (`.sln`, `.csproj`, `ModuleSetup.cs`, `FolderProfile.pubxml`) are
709
+ > **required** — the Deployment Window will fail without them. See
710
+ > [cloud-code.md — Module Creation](cloud-code.md#module-creation) for the generic templates;
711
+ > replace `MyModule` / `MyModuleCCM` with `AchievementModule` / `AchievementsCCM`.
712
+
713
+ ### Access Control -- `Assets/Config/DenyPlayerCloudSaveWrites.ac`
714
+
715
+ Right-click in the Project window → **Assets > Create > Services > Access Control**. Name the file `DenyPlayerCloudSaveWrites`. Replace the generated contents with:
716
+
717
+ ```json
718
+ {
719
+ "$schema": "https://ugs-config-schemas.unity3d.com/v1/project-access-policy.schema.json",
720
+ "Statements": [
721
+ {
722
+ "Sid": "DenyPlayerCloudSaveWrites",
723
+ "Action": [
724
+ "Write"
725
+ ],
726
+ "Effect": "Deny",
727
+ "Principal": "Player",
728
+ "Resource": "urn:ugs:cloud-save:*",
729
+ "Version": "1.0.0"
730
+ }
731
+ ]
732
+ }
733
+ ```
734
+
735
+ **Effect:** With this policy active, `AchievementManager` must have `m_UseTrustedWrites = true`. Direct Cloud Save writes from the player client will return `403`.
736
+
737
+ **Without this policy:** `m_UseTrustedWrites = false` works fine — suitable for development or games where anti-cheat is not a concern.
738
+
739
+ Deploy via **Services > Deployment** alongside your other files.
740
+
741
+ ---
742
+
743
+ ## Deployment Checklist
744
+
745
+ Deploy all cloud resources via **Services > Deployment** before testing:
746
+
747
+ - [ ] All packages added to `Packages/manifest.json` with correct minimum versions
748
+ - [ ] `Assets/Config/Achievements.rc` -- Remote Config achievement definitions
749
+ - [ ] `Assets/Config/DenyPlayerCloudSaveWrites.ac` -- Access Control policy blocking direct player writes
750
+ - [ ] Cloud Code module scaffolded with **all required files**: `.sln`, `.csproj`, `ModuleSetup.cs`, `FolderProfile.pubxml`, `.ccmr` (see file table in [Cloud Code Module](#cloud-code-module))
751
+ - [ ] `AchievementModule.cs` added to the module project with `UnlockAchievement`, `UpdateAchievementProgress`, `ResetAchievements` functions
752
+ - [ ] Environment selected in the Deployment Window dropdown
753
+ - [ ] All resources deployed via the Deployment Window
754
+ - [ ] Project linked in **Edit > Project Settings > Services**
755
+
756
+ ---
757
+
758
+ ## Official Unity Reference
759
+
760
+ - **Achievements Building Block (Asset Store):** The official [Unity Building Block — Achievements](https://assetstore.unity.com/packages/essentials/tutorial-projects/unity-building-block-achievements-341918) uses the same Remote Config + Cloud Save + Cloud Code pattern described here. Import from the Asset Store for ready-made UI, Cloud Code module, `.ac` / `.rc` / `.ccmr` cloud resources, and generated client bindings.
761
+ - **Unity Cloud Save documentation:** https://docs.unity.com/ugs/en-us/manual/cloud-save/manual
762
+ - **Unity Remote Config documentation:** https://docs.unity.com/ugs/en-us/manual/remote-config/manual
763
+ - **Unity Cloud Code documentation:** https://docs.unity.com/ugs/en-us/manual/cloud-code/manual
764
+ - **Access Control documentation:** https://docs.unity.com/ugs/en-us/manual/access-control/manual
765
+
766
+ ---
767
+
768
+ ## Validation
769
+
770
+ After implementing achievements, verify:
771
+
772
+ 1. **Compilation:** The project compiles without errors. Ensure namespace aliases (`PlayerLoadOptions`, `PlayerSaveOptions`, `PlayerDeleteOptions`) are present.
773
+ 2. **Initialization order:** `UnityServices.InitializeAsync()` then `AuthenticationService.Instance.SignInAnonymouslyAsync()` then service calls.
774
+ 3. **Access class match:** If `m_UseTrustedWrites` is true, reads use `ProtectedReadAccessClassOptions()`. If false, reads use `PublicReadAccessClassOptions(playerId)`. Mismatched access classes return empty data.
775
+ 4. **Cloud Code module builds:** The `.sln` at `AchievementsCCM/AchievementModule.sln` targets `net9.0` and publishes to `linux-x64`.
776
+ 5. **Server-side model uses properties:** The `AchievementRecord` class in the Cloud Code module uses `{ get; set; }` properties (not fields) for JSON serialization compatibility with `Newtonsoft.Json`.
777
+ 6. **Access Control deployed:** The `.ac` file denies direct player writes to Cloud Save. Without this, the trusted-write path has no enforcement advantage.
778
+ 7. **Remote Config key matches:** The `.rc` file key (`"achievements"`) matches the key used in `result.config["achievements"]` on the client and `AchievementsKey` in the module.
779
+ 8. **All cloud resources deployable:** The `.rc`, `.ac`, and `.ccmr` files are in the `Assets/` folder hierarchy and appear in the Deployment Window.