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,860 @@
1
+ # Battle Pass Blueprint
2
+
3
+ ## Table of Contents
4
+
5
+ - [Architecture Overview](#architecture-overview)
6
+ - [Free Track vs Premium Track](#free-track-vs-premium-track)
7
+ - [Multiple Concurrent Passes](#multiple-concurrent-passes)
8
+ - [Data Models](#data-models)
9
+ - [API Notes](#api-notes)
10
+ - [Namespace Aliases](#namespace-aliases-required)
11
+ - [ProtectedReadAccessClassOptions](#protectedreadaccessclassoptions)
12
+ - [Why tiersJson Is Passed from the Client](#why-tiersjson-is-passed-from-the-client)
13
+ - [Source Whitelist Enforcement](#source-whitelist-enforcement)
14
+ - [Error Handling](#error-handling)
15
+ - [Cloud Code Module Functions](#cloud-code-module-functions)
16
+ - [Load Pass Definitions from Remote Config](#load-pass-definitions-from-remote-config)
17
+ - [Load Player Progress](#load-player-progress)
18
+ - [Client to Cloud Code Calls](#client-to-cloud-code-calls)
19
+ - [Full Client Implementation](#full-client-implementation)
20
+ - [BattlePassTester](#battlepasstester)
21
+ - [Cloud Code Module Implementation](#cloud-code-module-implementation)
22
+ - [BattlePassModule.cs](#battlepassmodulecs)
23
+ - [Server-Side Models](#server-side-models-inside-battlepassmodulecs)
24
+ - [Cloud Resources](#cloud-resources)
25
+ - [Deployment Checklist](#deployment-checklist)
26
+ - [Validation](#validation)
27
+ - [Asset Store & Sample Projects](#asset-store--sample-projects)
28
+
29
+ ---
30
+
31
+ ## Architecture Overview
32
+
33
+ | Concern | Service | Key |
34
+ |---|---|---|
35
+ | Pass definitions | Remote Config | `"battle_passes"` |
36
+ | Per-player progress | Cloud Save (Protected) | `"battle_pass_progress"` |
37
+ | Award XP | Cloud Code | `BattlePassModule.AwardXp` |
38
+ | Claim reward | Cloud Code | `BattlePassModule.ClaimReward` |
39
+ | Purchase premium | Cloud Code | `BattlePassModule.PurchasePremium` |
40
+
41
+ ```
42
+ Remote Config Cloud Save (Protected) Cloud Code Module
43
+ "battle_passes" "battle_pass_progress" BattlePassModule
44
+ | | |
45
+ v v v
46
+ BattlePassDefinition[] { passId: Progress } AwardXp
47
+ PassId, Name, Server-writes only ClaimReward
48
+ StartDate, EndDate, PurchasePremium
49
+ Tiers[]
50
+ | ^ |
51
+ | | |
52
+ +------- client reads -------+------- server writes ---------+
53
+ ```
54
+
55
+ **Key principle:** The client reads pass definitions from Remote Config and player progress from Cloud Save (Protected bucket). All mutations -- awarding XP, claiming rewards, purchasing premium -- go through the Cloud Code module, which is the only writer to the Protected bucket.
56
+
57
+ ---
58
+
59
+ ## Free Track vs Premium Track
60
+
61
+ Every tier in a battle pass has **two parallel reward tracks**:
62
+
63
+ - **Free track** -- available to all players who reach the tier.
64
+ - **Premium track** -- visible to all players but **locked** unless the player has purchased the premium pass (`HasPremium = true`).
65
+
66
+ Premium rewards are displayed in the UI alongside free rewards so non-premium players can see what they are missing, encouraging upgrade. The Cloud Code module enforces the lock: `ClaimReward` with `rewardType = "premium"` throws if `HasPremium` is false.
67
+
68
+ ---
69
+
70
+ ## Multiple Concurrent Passes
71
+
72
+ The system supports multiple battle passes running simultaneously:
73
+
74
+ - Each pass has a unique `PassId` (e.g. `"season_03"`, `"easter_2026"`).
75
+ - Passes may overlap in time. A player can have active progress in several passes at once.
76
+ - Progress and premium status are tracked **independently per pass**. The Cloud Save key `"battle_pass_progress"` stores a `Dictionary<string, BattlePassProgress>` keyed by `PassId`.
77
+ - **Date filtering is client-side.** The client fetches all pass definitions from Remote Config and filters to those whose `StartDate <= now <= EndDate`.
78
+ - Expired passes remain in the progress dictionary. The client simply stops showing them. Clean up stale entries via a scheduled Cloud Code trigger if desired.
79
+
80
+ ---
81
+
82
+ ## Data Models
83
+
84
+ ### BattlePassDefinition (client-side, from Remote Config)
85
+
86
+ ```csharp
87
+ [Serializable]
88
+ public class BattlePassDefinition
89
+ {
90
+ public string PassId;
91
+ public string Name;
92
+ public string StartDate; // ISO 8601, e.g. "2026-04-01T00:00:00Z"
93
+ public string EndDate; // ISO 8601
94
+ public List<BattlePassTier> Tiers;
95
+ }
96
+ ```
97
+
98
+ ### BattlePassTier (client-side, from Remote Config)
99
+
100
+ ```csharp
101
+ [Serializable]
102
+ public class BattlePassTier
103
+ {
104
+ public int TierNumber;
105
+ public int XpRequired; // cumulative XP to reach this tier (not delta from previous)
106
+ public string FreeRewardId;
107
+ public string PremiumRewardId;
108
+ }
109
+ ```
110
+
111
+ ### BattlePassProgress (client-side and server-side, from Cloud Save Protected)
112
+
113
+ ```csharp
114
+ [Serializable]
115
+ public class BattlePassProgress
116
+ {
117
+ public int CurrentXp;
118
+ public int CurrentTier;
119
+ public bool HasPremium;
120
+ public List<string> ClaimedRewards; // e.g. "tier_1_free", "tier_2_premium"
121
+ }
122
+ ```
123
+
124
+ One entry per pass, keyed by `PassId` in the progress dictionary stored at Cloud Save key `"battle_pass_progress"`.
125
+
126
+ ### BattlePassTierThreshold (server-side helper, passed from client via tiersJson)
127
+
128
+ ```csharp
129
+ [Serializable]
130
+ public class BattlePassTierThreshold
131
+ {
132
+ public int TierNumber;
133
+ public int XpRequired;
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## API Notes
140
+
141
+ ### Namespace Aliases (Required)
142
+
143
+ Cloud Save has multiple types named `LoadOptions`, `SaveOptions`, and `DeleteOptions` across
144
+ different namespaces. Always declare these aliases at the top of any file that uses Cloud Save:
145
+
146
+ ```csharp
147
+ using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
148
+ using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
149
+ using PlayerDeleteOptions = Unity.Services.CloudSave.Models.Data.Player.DeleteOptions;
150
+ ```
151
+
152
+ Without these aliases, the compiler will report ambiguous type references.
153
+
154
+ ### ProtectedReadAccessClassOptions
155
+
156
+ Use `ProtectedReadAccessClassOptions` when loading player progress. This reads the current
157
+ player's own Protected data — no player ID parameter is required; it always reads the signed-in
158
+ player's data.
159
+
160
+ > **Common mistake:** Reading from the wrong access class (e.g. using default `LoadOptions`
161
+ > instead of `ProtectedReadAccessClassOptions`) will return no data, because the progress
162
+ > is stored in the Protected bucket by the Cloud Code module.
163
+
164
+ ### Why tiersJson Is Passed from the Client
165
+
166
+ Remote Config is not accessible from inside Cloud Code modules. The client serializes the tier
167
+ thresholds from the loaded `BattlePassDefinition` and sends them as the `tiersJson` parameter.
168
+ The server uses these thresholds to recalculate the player's current tier after awarding XP.
169
+
170
+ Tier thresholds are config data and are **not security-sensitive** — the anti-cheat protection
171
+ is on the `source` whitelist and the `amount` validation. A tampered `tiersJson` could only
172
+ cause the player's tier to be recalculated differently, which is bounded by their actual XP.
173
+
174
+ ### Source Whitelist Enforcement
175
+
176
+ The Cloud Code module maintains a server-side whitelist of allowed XP source strings:
177
+
178
+ ```csharp
179
+ static readonly HashSet<string> ValidSources = new()
180
+ {
181
+ "match_complete",
182
+ "daily_login",
183
+ "quest_complete"
184
+ };
185
+ ```
186
+
187
+ Any `AwardXp` call with an unrecognized source throws an exception. Add or remove entries as
188
+ needed for your game. The whitelist prevents clients from inventing arbitrary XP sources.
189
+
190
+ ### Error Handling
191
+
192
+ Cloud Code and Cloud Save errors surface as typed exceptions on the client:
193
+
194
+ ```csharp
195
+ try
196
+ {
197
+ var progress = await AwardXpAsync(passId, 100, "match_complete");
198
+ }
199
+ catch (CloudCodeException ex)
200
+ {
201
+ // ex.Reason: CloudCodeExceptionReason (e.g. ScriptError for module-thrown exceptions)
202
+ Debug.LogError($"[BattlePass] Cloud Code error: {ex.Reason} — {ex.Message}");
203
+ }
204
+ catch (CloudSaveException ex)
205
+ {
206
+ // ex.Reason: CloudSaveExceptionReason
207
+ // A 403 on the Protected bucket means the client attempted a direct write —
208
+ // all writes must go through the Cloud Code module.
209
+ Debug.LogError($"[BattlePass] Cloud Save error: {ex.Reason} — {ex.Message}");
210
+ }
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Cloud Code Module Functions
216
+
217
+ | Function | Parameters | Behavior |
218
+ |---|---|---|
219
+ | `AwardXp` | `passId`, `amount`, `source`, `tiersJson` | Validates `source` against a whitelist (`ValidSources`), rejects non-positive amounts, adds XP to the pass progress, recalculates the current tier from the client-supplied `tiersJson`, saves to Cloud Save Protected |
220
+ | `ClaimReward` | `passId`, `tierNumber`, `rewardType` | Validates the player has reached the tier, validates premium ownership if `rewardType` is `"premium"`, checks reward is not already claimed, records the claim as `"tier_N_type"` (e.g. `"tier_2_premium"`), saves to Cloud Save Protected |
221
+ | `PurchasePremium` | `passId` | Sets `HasPremium = true` on the pass progress. **Idempotent** -- returns immediately if already purchased without error |
222
+
223
+ All three functions return the updated `BattlePassProgress` for the given pass.
224
+
225
+ ---
226
+
227
+ ## Load Pass Definitions from Remote Config
228
+
229
+ ```csharp
230
+ struct UserAttributes {}
231
+ struct AppAttributes {}
232
+
233
+ async Task<List<BattlePassDefinition>> LoadActivePassesAsync()
234
+ {
235
+ var result = await RemoteConfigService.Instance.FetchConfigsAsync(
236
+ new UserAttributes(), new AppAttributes());
237
+
238
+ var token = result.config["battle_passes"];
239
+ if (token == null) return new List<BattlePassDefinition>();
240
+
241
+ var allPasses = token.ToObject<List<BattlePassDefinition>>();
242
+
243
+ var now = DateTime.UtcNow;
244
+ return allPasses
245
+ .Where(p =>
246
+ DateTime.Parse(p.StartDate, null, DateTimeStyles.RoundtripKind) <= now &&
247
+ DateTime.Parse(p.EndDate, null, DateTimeStyles.RoundtripKind) >= now)
248
+ .ToList();
249
+ }
250
+ ```
251
+
252
+ - `FetchConfigsAsync` downloads all Remote Config entries.
253
+ - `result.config["battle_passes"]` returns a `JToken` for the JSON array stored under that key.
254
+ - Date filtering is performed client-side by comparing `StartDate` and `EndDate` against `DateTime.UtcNow`.
255
+
256
+ ---
257
+
258
+ ## Load Player Progress
259
+
260
+ ```csharp
261
+ using Unity.Services.CloudSave;
262
+ using Unity.Services.CloudSave.Models;
263
+ using Unity.Services.CloudSave.Models.Data.Player;
264
+ using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
265
+
266
+ async Task<Dictionary<string, BattlePassProgress>> LoadProgressAsync()
267
+ {
268
+ var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
269
+ new HashSet<string> { "battle_pass_progress" },
270
+ new PlayerLoadOptions(new ProtectedReadAccessClassOptions()));
271
+
272
+ if (!result.TryGetValue("battle_pass_progress", out var item))
273
+ return new Dictionary<string, BattlePassProgress>();
274
+
275
+ return JsonConvert.DeserializeObject<Dictionary<string, BattlePassProgress>>(
276
+ item.Value.GetAsString())
277
+ ?? new Dictionary<string, BattlePassProgress>();
278
+ }
279
+ ```
280
+
281
+ **Important:** Use `ProtectedReadAccessClassOptions` because the Cloud Code module writes to the Protected bucket. Reading from the wrong access class will return no data.
282
+
283
+ > **Stale entries:** Expired pass entries remain in the dictionary indefinitely but are inert —
284
+ > the client ignores any PassId not present in the current active pass list.
285
+
286
+ ---
287
+
288
+ ## Client to Cloud Code Calls
289
+
290
+ ### AwardXpAsync
291
+
292
+ ```csharp
293
+ public async Task<BattlePassProgress> AwardXpAsync(string passId, int amount, string source)
294
+ {
295
+ var pass = ActivePasses.FirstOrDefault(p => p.PassId == passId)
296
+ ?? throw new InvalidOperationException(
297
+ $"Pass '{passId}' is not in the active passes list. Call LoadAsync first.");
298
+
299
+ var progress = await CloudCodeService.Instance.CallModuleEndpointAsync<BattlePassProgress>(
300
+ "BattlePassModule", "AwardXp",
301
+ new Dictionary<string, object>
302
+ {
303
+ { "passId", passId },
304
+ { "amount", amount },
305
+ { "source", source },
306
+ { "tiersJson", JsonConvert.SerializeObject(pass.Tiers) }
307
+ });
308
+
309
+ Progress[passId] = progress;
310
+ return progress;
311
+ }
312
+ ```
313
+
314
+ The `tiersJson` parameter is the serialized tier list from the locally loaded pass definition. The server uses it to recalculate the player's tier after adding XP, without needing its own Remote Config fetch.
315
+
316
+ ### ClaimRewardAsync
317
+
318
+ ```csharp
319
+ public async Task<BattlePassProgress> ClaimRewardAsync(
320
+ string passId, int tierNumber, string rewardType)
321
+ {
322
+ var progress = await CloudCodeService.Instance.CallModuleEndpointAsync<BattlePassProgress>(
323
+ "BattlePassModule", "ClaimReward",
324
+ new Dictionary<string, object>
325
+ {
326
+ { "passId", passId },
327
+ { "tierNumber", tierNumber },
328
+ { "rewardType", rewardType }
329
+ });
330
+
331
+ Progress[passId] = progress;
332
+ return progress;
333
+ }
334
+ ```
335
+
336
+ ### PurchasePremiumAsync
337
+
338
+ ```csharp
339
+ public async Task<BattlePassProgress> PurchasePremiumAsync(string passId)
340
+ {
341
+ var progress = await CloudCodeService.Instance.CallModuleEndpointAsync<BattlePassProgress>(
342
+ "BattlePassModule", "PurchasePremium",
343
+ new Dictionary<string, object> { { "passId", passId } });
344
+
345
+ Progress[passId] = progress;
346
+ return progress;
347
+ }
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Full Client Implementation
353
+
354
+ `BattlePassManager` is a `MonoBehaviour` that initializes services, loads pass definitions and player progress, and exposes methods for XP awards, reward claims, and premium purchases. Attach it to a GameObject in your scene.
355
+
356
+ ```csharp
357
+ using System;
358
+ using System.Collections.Generic;
359
+ using System.Globalization;
360
+ using System.Linq;
361
+ using System.Threading.Tasks;
362
+ using Newtonsoft.Json;
363
+ using Unity.Services.Authentication;
364
+ using Unity.Services.CloudCode;
365
+ using Unity.Services.CloudSave;
366
+ using Unity.Services.CloudSave.Models;
367
+ using Unity.Services.CloudSave.Models.Data.Player;
368
+ using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
369
+ using Unity.Services.Core;
370
+ using Unity.Services.RemoteConfig;
371
+ using UnityEngine;
372
+
373
+ public class BattlePassManager : MonoBehaviour
374
+ {
375
+ public List<BattlePassDefinition> ActivePasses { get; private set; } = new();
376
+ public Dictionary<string, BattlePassProgress> Progress { get; private set; } = new();
377
+
378
+ public event Action Loaded;
379
+
380
+ async void Start()
381
+ {
382
+ try
383
+ {
384
+ await UnityServices.InitializeAsync();
385
+ if (!AuthenticationService.Instance.IsSignedIn)
386
+ {
387
+ await AuthenticationService.Instance.SignInAnonymouslyAsync();
388
+ }
389
+ await LoadAsync();
390
+ Debug.Log($"BattlePassManager: Loaded {ActivePasses.Count} passes.");
391
+ Loaded?.Invoke();
392
+ }
393
+ catch (Exception ex)
394
+ {
395
+ Debug.LogError($"BattlePassManager: Error during loading: {ex.Message}\n{ex.StackTrace}");
396
+ }
397
+ }
398
+
399
+ public async Task LoadAsync()
400
+ {
401
+ ActivePasses = await LoadActivePassesAsync();
402
+ Progress = await LoadProgressAsync();
403
+ }
404
+
405
+ struct UserAttributes {}
406
+ struct AppAttributes {}
407
+
408
+ async Task<List<BattlePassDefinition>> LoadActivePassesAsync()
409
+ {
410
+ var result = await RemoteConfigService.Instance.FetchConfigsAsync(
411
+ new UserAttributes(), new AppAttributes());
412
+
413
+ var token = result.config["battle_passes"];
414
+ if (token == null) return new List<BattlePassDefinition>();
415
+
416
+ var allPasses = token.ToObject<List<BattlePassDefinition>>();
417
+
418
+ var now = DateTime.UtcNow;
419
+ return allPasses
420
+ .Where(p =>
421
+ DateTime.Parse(p.StartDate, null, DateTimeStyles.RoundtripKind) <= now &&
422
+ DateTime.Parse(p.EndDate, null, DateTimeStyles.RoundtripKind) >= now)
423
+ .ToList();
424
+ }
425
+
426
+ async Task<Dictionary<string, BattlePassProgress>> LoadProgressAsync()
427
+ {
428
+ var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
429
+ new HashSet<string> { "battle_pass_progress" },
430
+ new PlayerLoadOptions(new ProtectedReadAccessClassOptions()));
431
+
432
+ if (!result.TryGetValue("battle_pass_progress", out var item))
433
+ return new Dictionary<string, BattlePassProgress>();
434
+
435
+ return JsonConvert.DeserializeObject<Dictionary<string, BattlePassProgress>>(
436
+ item.Value.GetAsString())
437
+ ?? new Dictionary<string, BattlePassProgress>();
438
+ }
439
+
440
+ public async Task<BattlePassProgress> AwardXpAsync(string passId, int amount, string source)
441
+ {
442
+ var pass = ActivePasses.FirstOrDefault(p => p.PassId == passId)
443
+ ?? throw new InvalidOperationException(
444
+ $"Pass '{passId}' is not in the active passes list. Call LoadAsync first.");
445
+
446
+ var progress = await CloudCodeService.Instance.CallModuleEndpointAsync<BattlePassProgress>(
447
+ "BattlePassModule", "AwardXp",
448
+ new Dictionary<string, object>
449
+ {
450
+ { "passId", passId },
451
+ { "amount", amount },
452
+ { "source", source },
453
+ { "tiersJson", JsonConvert.SerializeObject(pass.Tiers) }
454
+ });
455
+
456
+ Progress[passId] = progress;
457
+ return progress;
458
+ }
459
+
460
+ public async Task<BattlePassProgress> ClaimRewardAsync(
461
+ string passId, int tierNumber, string rewardType)
462
+ {
463
+ var progress = await CloudCodeService.Instance.CallModuleEndpointAsync<BattlePassProgress>(
464
+ "BattlePassModule", "ClaimReward",
465
+ new Dictionary<string, object>
466
+ {
467
+ { "passId", passId },
468
+ { "tierNumber", tierNumber },
469
+ { "rewardType", rewardType }
470
+ });
471
+
472
+ Progress[passId] = progress;
473
+ return progress;
474
+ }
475
+
476
+ public async Task<BattlePassProgress> PurchasePremiumAsync(string passId)
477
+ {
478
+ var progress = await CloudCodeService.Instance.CallModuleEndpointAsync<BattlePassProgress>(
479
+ "BattlePassModule", "PurchasePremium",
480
+ new Dictionary<string, object> { { "passId", passId } });
481
+
482
+ Progress[passId] = progress;
483
+ return progress;
484
+ }
485
+ }
486
+ ```
487
+
488
+ ---
489
+
490
+ ## BattlePassTester
491
+
492
+ Attach to the same GameObject as `BattlePassManager` in your test scene. Wire `m_Manager` via
493
+ the Inspector, or let it fall back to `GetComponent`.
494
+
495
+ ```csharp
496
+ using System.Text;
497
+ using UnityEngine;
498
+
499
+ /// <summary>
500
+ /// Attach alongside BattlePassManager to run a quick in-Editor smoke test.
501
+ /// On load: logs all active pass states.
502
+ /// Then: awards 100 XP to the first active pass via Cloud Code and logs the result.
503
+ /// </summary>
504
+ public class BattlePassTester : MonoBehaviour
505
+ {
506
+ [SerializeField] BattlePassManager m_Manager;
507
+
508
+ void Start()
509
+ {
510
+ if (m_Manager == null)
511
+ m_Manager = GetComponent<BattlePassManager>();
512
+ m_Manager.Loaded += OnLoaded;
513
+ }
514
+
515
+ async void OnLoaded()
516
+ {
517
+ LogState("Initial state");
518
+
519
+ if (m_Manager.ActivePasses.Count == 0)
520
+ {
521
+ Debug.Log("[BattlePassTester] No active passes — nothing to award.");
522
+ return;
523
+ }
524
+
525
+ var firstPass = m_Manager.ActivePasses[0];
526
+ Debug.Log($"[BattlePassTester] Awarding 100 XP to '{firstPass.PassId}' " +
527
+ $"(source: match_complete) ...");
528
+
529
+ var result = await m_Manager.AwardXpAsync(firstPass.PassId, 100, "match_complete");
530
+
531
+ Debug.Log($"[BattlePassTester] After award — " +
532
+ $"XP={result.CurrentXp}, Tier={result.CurrentTier}");
533
+ }
534
+
535
+ void LogState(string label)
536
+ {
537
+ var sb = new StringBuilder();
538
+ sb.AppendLine($"[BattlePassTester] {label} — {m_Manager.ActivePasses.Count} active pass(es):");
539
+
540
+ foreach (var pass in m_Manager.ActivePasses)
541
+ {
542
+ m_Manager.Progress.TryGetValue(pass.PassId, out var progress);
543
+ sb.AppendLine($" [{pass.PassId}] {pass.Name}: " +
544
+ $"XP={progress?.CurrentXp ?? 0}, " +
545
+ $"Tier={progress?.CurrentTier ?? 1}, " +
546
+ $"Premium={progress?.HasPremium ?? false}, " +
547
+ $"Claimed={progress?.ClaimedRewards?.Count ?? 0}");
548
+ }
549
+
550
+ Debug.Log(sb.ToString());
551
+ }
552
+ }
553
+ ```
554
+
555
+ ---
556
+
557
+ ## Cloud Code Module Implementation
558
+
559
+ > **Module scaffolding — follow [cloud-code.md — Module Creation](cloud-code.md#module-creation).**
560
+ > Replace `MyModule` / `MyModuleCCM` with `BattlePassModule` / `BattlePassCCM` throughout.
561
+ > You **must** create every file listed there — missing any one will cause deployment to fail:
562
+ >
563
+ > - [ ] `.sln` (generate fresh GUIDs)
564
+ > - [ ] `.csproj` (net9.0, CloudCode.Apis + CloudCode.Core)
565
+ > - [ ] `ModuleSetup.cs` (registers `GameApiClient`)
566
+ > - [ ] `Properties/PublishProfiles/FolderProfile.pubxml` — **without this file, deployment fails with "Failed to retrieve main project"**
567
+ > - [ ] `Assets/CloudCode/BattlePassModule.ccmr` (points to the `.sln`)
568
+
569
+ ### BattlePassModule.cs
570
+
571
+ ```csharp
572
+ using System;
573
+ using System.Collections.Generic;
574
+ using System.Linq;
575
+ using System.Threading.Tasks;
576
+ using Newtonsoft.Json;
577
+ using Unity.Services.CloudCode.Apis;
578
+ using Unity.Services.CloudCode.Core;
579
+ using Unity.Services.CloudSave.Model;
580
+
581
+ public class BattlePassModule
582
+ {
583
+ const string ProgressKey = "battle_pass_progress";
584
+
585
+ static readonly HashSet<string> ValidSources = new()
586
+ {
587
+ "match_complete",
588
+ "daily_login",
589
+ "quest_complete"
590
+ };
591
+
592
+ // ── AwardXp ──────────────────────────────────────────────────────────────
593
+
594
+ [CloudCodeFunction("AwardXp")]
595
+ public async Task<BattlePassProgress> AwardXp(
596
+ IExecutionContext context, IGameApiClient client,
597
+ string passId, int amount, string source, string tiersJson)
598
+ {
599
+ if (!ValidSources.Contains(source))
600
+ throw new Exception(
601
+ $"Invalid XP source: '{source}'. Allowed: {string.Join(", ", ValidSources)}");
602
+
603
+ if (amount <= 0)
604
+ throw new Exception("XP amount must be positive.");
605
+
606
+ var allProgress = await LoadProgressAsync(context, client);
607
+
608
+ if (!allProgress.TryGetValue(passId, out var progress))
609
+ progress = new BattlePassProgress { CurrentTier = 1, ClaimedRewards = new List<string>() };
610
+
611
+ progress.CurrentXp += amount;
612
+
613
+ var tiers = JsonConvert.DeserializeObject<List<BattlePassTierThreshold>>(tiersJson)
614
+ ?? throw new Exception("tiersJson could not be deserialized.");
615
+ progress.CurrentTier = CalculateTier(progress.CurrentXp, tiers);
616
+
617
+ allProgress[passId] = progress;
618
+ await SaveProgressAsync(context, client, allProgress);
619
+
620
+ return progress;
621
+ }
622
+
623
+ // ── ClaimReward ──────────────────────────────────────────────────────────
624
+
625
+ [CloudCodeFunction("ClaimReward")]
626
+ public async Task<BattlePassProgress> ClaimReward(
627
+ IExecutionContext context, IGameApiClient client,
628
+ string passId, int tierNumber, string rewardType)
629
+ {
630
+ if (rewardType != "free" && rewardType != "premium")
631
+ throw new Exception(
632
+ $"Invalid rewardType: '{rewardType}'. Must be 'free' or 'premium'.");
633
+
634
+ var allProgress = await LoadProgressAsync(context, client);
635
+
636
+ if (!allProgress.TryGetValue(passId, out var progress))
637
+ throw new Exception($"No progress found for pass '{passId}'. Award XP first.");
638
+
639
+ if (progress.CurrentTier < tierNumber)
640
+ throw new Exception(
641
+ $"Tier {tierNumber} not yet reached (current tier: {progress.CurrentTier}).");
642
+
643
+ if (rewardType == "premium" && !progress.HasPremium)
644
+ throw new Exception("Premium pass not owned. Purchase premium first.");
645
+
646
+ var rewardKey = $"tier_{tierNumber}_{rewardType}";
647
+ if (progress.ClaimedRewards.Contains(rewardKey))
648
+ throw new Exception($"Reward '{rewardKey}' already claimed.");
649
+
650
+ progress.ClaimedRewards.Add(rewardKey);
651
+ allProgress[passId] = progress;
652
+ await SaveProgressAsync(context, client, allProgress);
653
+
654
+ return progress;
655
+ }
656
+
657
+ // ── PurchasePremium ──────────────────────────────────────────────────────
658
+
659
+ [CloudCodeFunction("PurchasePremium")]
660
+ public async Task<BattlePassProgress> PurchasePremium(
661
+ IExecutionContext context, IGameApiClient client, string passId)
662
+ {
663
+ var allProgress = await LoadProgressAsync(context, client);
664
+
665
+ if (!allProgress.TryGetValue(passId, out var progress))
666
+ progress = new BattlePassProgress { CurrentTier = 1, ClaimedRewards = new List<string>() };
667
+
668
+ if (progress.HasPremium)
669
+ return progress; // idempotent -- already owns premium
670
+
671
+ progress.HasPremium = true;
672
+ allProgress[passId] = progress;
673
+ await SaveProgressAsync(context, client, allProgress);
674
+
675
+ return progress;
676
+ }
677
+
678
+ // ── Helpers ──────────────────────────────────────────────────────────────
679
+
680
+ static int CalculateTier(int currentXp, List<BattlePassTierThreshold> tiers)
681
+ {
682
+ var reached = tiers
683
+ .Where(t => currentXp >= t.XpRequired)
684
+ .OrderByDescending(t => t.TierNumber)
685
+ .FirstOrDefault();
686
+
687
+ return reached?.TierNumber ?? 1;
688
+ }
689
+
690
+ async Task<Dictionary<string, BattlePassProgress>> LoadProgressAsync(
691
+ IExecutionContext context, IGameApiClient client)
692
+ {
693
+ var result = await client.CloudSaveData.GetProtectedItemsAsync(
694
+ context, context.ServiceToken, context.ProjectId,
695
+ context.PlayerId!, new List<string> { ProgressKey });
696
+
697
+ var value = result.Data?.Results?.FirstOrDefault()?.Value?.ToString();
698
+ if (string.IsNullOrEmpty(value))
699
+ return new Dictionary<string, BattlePassProgress>();
700
+
701
+ return JsonConvert.DeserializeObject<Dictionary<string, BattlePassProgress>>(value)
702
+ ?? new Dictionary<string, BattlePassProgress>();
703
+ }
704
+
705
+ async Task SaveProgressAsync(
706
+ IExecutionContext context, IGameApiClient client,
707
+ Dictionary<string, BattlePassProgress> progress)
708
+ {
709
+ var json = JsonConvert.SerializeObject(progress);
710
+ var body = new SetItemBody(ProgressKey, json);
711
+ await client.CloudSaveData.SetProtectedItemAsync(
712
+ context, context.ServiceToken, context.ProjectId, context.PlayerId!, body);
713
+ }
714
+ }
715
+ ```
716
+
717
+ ### Server-Side Models (inside BattlePassModule.cs)
718
+
719
+ ```csharp
720
+ public class BattlePassProgress
721
+ {
722
+ public int CurrentXp { get; set; }
723
+ public int CurrentTier { get; set; }
724
+ public bool HasPremium { get; set; }
725
+ public List<string> ClaimedRewards { get; set; } = new();
726
+ }
727
+
728
+ public class BattlePassTierThreshold
729
+ {
730
+ public int TierNumber { get; set; }
731
+ public int XpRequired { get; set; }
732
+ }
733
+ ```
734
+
735
+ ---
736
+
737
+ ## Cloud Resources
738
+
739
+ ### Remote Config `.rc` File
740
+
741
+ Place at `Assets/BattlePasses.rc`. Contains pass definitions as a JSON array under the `"battle_passes"` key. Example with two concurrent passes:
742
+
743
+ ```json
744
+ {
745
+ "$schema": "https://ugs-config-schemas.unity3d.com/v1/remote-config.schema.json",
746
+ "entries": {
747
+ "battle_passes": [
748
+ {
749
+ "PassId": "season_03",
750
+ "Name": "Season 3",
751
+ "StartDate": "2024-01-01T00:00:00Z",
752
+ "EndDate": "2026-06-30T23:59:59Z",
753
+ "Tiers": [
754
+ { "TierNumber": 1, "XpRequired": 0, "FreeRewardId": "item_bronze_badge", "PremiumRewardId": "currency_gold_500" },
755
+ { "TierNumber": 2, "XpRequired": 500, "FreeRewardId": "item_xp_boost", "PremiumRewardId": "item_rare_skin_fragment" },
756
+ { "TierNumber": 3, "XpRequired": 1200, "FreeRewardId": "item_silver_badge", "PremiumRewardId": "item_legendary_skin" }
757
+ ]
758
+ },
759
+ {
760
+ "PassId": "easter_2026",
761
+ "Name": "Easter Event 2026",
762
+ "StartDate": "2026-04-01T00:00:00Z",
763
+ "EndDate": "2026-04-21T23:59:59Z",
764
+ "Tiers": [
765
+ { "TierNumber": 1, "XpRequired": 0, "FreeRewardId": "item_egg_basket", "PremiumRewardId": "item_golden_egg" },
766
+ { "TierNumber": 2, "XpRequired": 300, "FreeRewardId": "item_bunny_ears", "PremiumRewardId": "item_bunny_mount" }
767
+ ]
768
+ }
769
+ ]
770
+ },
771
+ "types": {
772
+ "battle_passes": "JSON"
773
+ }
774
+ }
775
+ ```
776
+
777
+ **Rules:**
778
+
779
+ - `PassId` must be unique across all passes and stable over time — it is the key used in player
780
+ progress records. Never rename a `PassId` after launch.
781
+ - `XpRequired` is **cumulative** XP to reach the tier, not a delta from the previous tier.
782
+ Tier 1 must always have `XpRequired: 0` so it is immediately reachable.
783
+ - `FreeRewardId` / `PremiumRewardId` are arbitrary strings. The client reads these IDs and maps
784
+ them to display names/icons locally.
785
+ - Date filtering happens **client-side** after fetch. Add passes to the file before their
786
+ `StartDate` — they will be ignored by the client until the start time is reached.
787
+
788
+ ### Cloud Code Module
789
+
790
+ > Create the full module solution using the file table and templates in
791
+ > [Cloud Code Module Implementation](#cloud-code-module-implementation) above.
792
+ > All scaffolding files (`.sln`, `.csproj`, `ModuleSetup.cs`, `FolderProfile.pubxml`) are
793
+ > **required** — the Deployment Window will fail without them. See
794
+ > [cloud-code.md — Module Creation](cloud-code.md#module-creation) for the generic templates;
795
+ > replace `MyModule` / `MyModuleCCM` with `BattlePassModule` / `BattlePassCCM`.
796
+
797
+ ### Access Control
798
+
799
+ **No Access Control (`.ac`) file is needed for the battle pass.** The Cloud Save Protected bucket is inherently server-only for writes -- only Cloud Code (using `IGameApiClient` with the service token) can write to it. Clients can read Protected data but cannot modify it.
800
+
801
+ > **Contrast with achievements:** The achievements skill offered a direct-write dev path using
802
+ > the Public bucket, which required an explicit Access Control policy to lock down in production.
803
+ > Battle pass has no direct-write path, so there is nothing to lock down.
804
+
805
+ ### `manifest.json` Entries
806
+
807
+ Ensure these packages are listed in `Packages/manifest.json`:
808
+
809
+ ```json
810
+ {
811
+ "dependencies": {
812
+ "com.unity.services.authentication": "3.6.1",
813
+ "com.unity.services.cloudcode": "2.10.2",
814
+ "com.unity.services.cloudsave": "3.4.0",
815
+ "com.unity.services.core": "1.16.0",
816
+ "com.unity.services.deployment": "1.7.2",
817
+ "com.unity.remote-config": "4.2.5",
818
+ "com.unity.services.tooling": "1.4.1",
819
+ "com.unity.nuget.newtonsoft-json": "3.2.2"
820
+ }
821
+ }
822
+ ```
823
+
824
+ `com.unity.nuget.newtonsoft-json` is required for `JsonConvert` used in serialization of tier data and progress.
825
+
826
+ ---
827
+
828
+ ## Deployment Checklist
829
+
830
+ - [ ] All packages added to `Packages/manifest.json`
831
+ - [ ] Remote Config `.rc` file created with `"battle_passes"` entry and at least one active pass
832
+ - [ ] Cloud Code module scaffolded with **all required files**: `.sln`, `.csproj`, `ModuleSetup.cs`, `FolderProfile.pubxml`, `.ccmr` (see file table in [Cloud Code Module Implementation](#cloud-code-module-implementation))
833
+ - [ ] `BattlePassModule.cs` added to the module project with `AwardXp`, `ClaimReward`, `PurchasePremium` functions
834
+ - [ ] Environment selected in **Services > Deployment** settings
835
+ - [ ] Deploy all via the Deployment Window: select `BattlePasses.rc` and `BattlePassModule.ccmr`, click **Deploy**
836
+ - [ ] Project linked in **Edit > Project Settings > Services**
837
+
838
+ ---
839
+
840
+ ## Validation
841
+
842
+ After implementing the battle pass feature, verify:
843
+
844
+ 1. **Project compiles** without errors. Both the Unity project and the `BattlePassCCM/` .NET project should build cleanly.
845
+ 2. **Initialization order** is correct: `UnityServices.InitializeAsync()` then `AuthenticationService.Instance.SignInAnonymouslyAsync()` then service calls.
846
+ 3. **Protected bucket reads** use `ProtectedReadAccessClassOptions` -- using the wrong access class returns no data.
847
+ 4. **Cloud Code module name** matches the `.ccmr` filename: `"BattlePassModule"` in `CallModuleEndpointAsync` matches `BattlePassModule.ccmr`.
848
+ 5. **No direct client writes** to `"battle_pass_progress"` -- all mutations go through Cloud Code.
849
+ 6. **tiersJson is serialized** from the locally loaded pass definition before calling `AwardXp`.
850
+ 7. **Date filtering** uses `DateTime.UtcNow` with `DateTimeStyles.RoundtripKind` for consistent UTC parsing.
851
+ 8. **Cloud resources** are present and deployable: `BattlePasses.rc` and `BattlePassModule.ccmr` appear in the Deployment Window.
852
+ 9. **PurchasePremium is idempotent** -- calling it when `HasPremium` is already true returns the existing progress without error.
853
+ 10. **ClaimReward validates** both tier reach and premium ownership before recording the claim.
854
+
855
+ ---
856
+
857
+ ## Asset Store & Sample Projects
858
+
859
+ - **Use Case Samples — Battle Pass:** The official [Use Case Samples](https://github.com/Unity-Technologies/com.unity.services.samples.use-cases) repository includes a complete Battle Pass sample with seasonal reward tiers, free/premium tracks, time-limited rewards, and server-authoritative progression via Cloud Code. Clone the repo and open the "Battle Pass" scene for a working reference.
860
+ - **Use Case Samples — Virtual Shop:** The same repository includes a Virtual Shop sample demonstrating Economy-based in-game stores, currency management, and inventory — patterns commonly paired with a Battle Pass.