@aqian0/dsh-unity 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +53 -0
- package/LICENSE-UNITY-COMPANION.md +122 -0
- package/LICENSE.md +31 -0
- package/NOTICE.md +90 -0
- package/README.md +273 -2
- package/cordis.patch.yml +22 -0
- package/docs/design.md +297 -0
- package/icon.svg +16 -0
- package/lib/frontmatter.js +392 -0
- package/lib/gating.js +105 -0
- package/lib/harness.js +16 -0
- package/lib/index.js +171 -0
- package/lib/project.js +210 -0
- package/lib/registry.js +268 -0
- package/lib/requirements.js +214 -0
- package/lib/skills.js +191 -0
- package/lib/types/frontmatter.d.ts +84 -0
- package/lib/types/gating.d.ts +80 -0
- package/lib/types/harness.d.ts +152 -0
- package/lib/types/index.d.ts +75 -0
- package/lib/types/project.d.ts +93 -0
- package/lib/types/registry.d.ts +96 -0
- package/lib/types/requirements.d.ts +67 -0
- package/lib/types/skills.d.ts +54 -0
- package/lib/types/watch.d.ts +34 -0
- package/lib/watch.js +84 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +70 -4
- package/skills/2d-pixel-perfect/SKILL.md +208 -0
- package/skills/2d-pixel-perfect/references/api-reference.md +81 -0
- package/skills/2d-pixel-perfect/references/camera-setup-builtin.cs +40 -0
- package/skills/2d-pixel-perfect/references/camera-setup-urp.cs +49 -0
- package/skills/2d-pixel-perfect/references/pipeline-detection.cs +50 -0
- package/skills/2d-pixel-perfect/references/sprite-settings.cs +37 -0
- package/skills/asset-transformer-toolkit/SECURITY.md +15 -0
- package/skills/asset-transformer-toolkit/SKILL.md +39 -0
- package/skills/asset-transformer-toolkit/references/create-importer.md +121 -0
- package/skills/asset-transformer-toolkit/references/lods.md +61 -0
- package/skills/asset-transformer-toolkit/references/rule-api.md +154 -0
- package/skills/asset-transformer-toolkit/references/ruleblock-api.md +37 -0
- package/skills/asset-transformer-toolkit/references/ruleset-api.md +138 -0
- package/skills/asset-transformer-toolkit/references/rulesets-and-actions.md +227 -0
- package/skills/audio-setup-mixers/SECURITY.md +16 -0
- package/skills/audio-setup-mixers/SKILL.md +127 -0
- package/skills/audio-setup-mixers/references/api.md +162 -0
- package/skills/build-gtk/SECURITY.md +16 -0
- package/skills/build-gtk/SKILL.md +200 -0
- package/skills/build-gtk/references/api-reference.md +33 -0
- package/skills/build-gtk/references/graph-api-gaps.md +96 -0
- package/skills/build-gtk/references/runtime-and-visualization.md +191 -0
- package/skills/build-gtk/references/state-machine-api.md +270 -0
- package/skills/build-gtk/references/where-the-docs-stop.md +49 -0
- package/skills/build-live-game/SKILL.md +159 -0
- package/skills/build-live-game/references/achievements.md +757 -0
- package/skills/build-live-game/references/apis.md +271 -0
- package/skills/build-live-game/references/authentication.md +431 -0
- package/skills/build-live-game/references/battlepass.md +904 -0
- package/skills/build-live-game/references/blueprints-and-samples.md +75 -0
- package/skills/build-live-game/references/cloud-code-cloudbehaviours.md +1111 -0
- package/skills/build-live-game/references/cloud-code-modules.md +1276 -0
- package/skills/build-live-game/references/cloud-code.md +58 -0
- package/skills/build-live-game/references/cloud-save.md +464 -0
- package/skills/build-live-game/references/deployment.md +231 -0
- package/skills/build-live-game/references/orientation.md +30 -0
- package/skills/build-live-game/references/player-account.md +821 -0
- package/skills/build-live-game/references/remote-config.md +96 -0
- package/skills/build-live-game/references/services-overview.md +76 -0
- package/skills/build-live-game/references/tooling.md +375 -0
- package/skills/build-live-game/references/ugs-cli.md +86 -0
- package/skills/craft-game-floors/SECURITY.md +23 -0
- package/skills/craft-game-floors/SKILL.md +134 -0
- package/skills/craft-game-floors/references/art-direction.md +85 -0
- package/skills/craft-game-floors/references/camera-rigs.md +51 -0
- package/skills/craft-game-floors/references/character-motion.md +35 -0
- package/skills/craft-game-floors/references/game-audio.md +47 -0
- package/skills/craft-game-floors/references/game-feel.md +68 -0
- package/skills/craft-game-floors/references/game-shell.md +45 -0
- package/skills/craft-game-floors/references/live-floor.md +35 -0
- package/skills/craft-game-floors/references/placeholder-assets.md +35 -0
- package/skills/craft-game-floors/references/scene-lighting.md +52 -0
- package/skills/craft-game-floors/references/toon-shading.md +34 -0
- package/skills/craft-game-floors/references/vfx.md +40 -0
- package/skills/craft-game-floors/references/visual-scorecard.md +43 -0
- package/skills/create-2d-physics/SECURITY.md +16 -0
- package/skills/create-2d-physics/SKILL.md +128 -0
- package/skills/create-2d-physics/references/2d-physics-core.md +155 -0
- package/skills/create-2d-physics/references/legacy-2d-physics.md +45 -0
- package/skills/create-tile-palette-or-rule-tiles/SKILL.md +95 -0
- package/skills/create-tile-palette-or-rule-tiles/references/hexagonalruletile.md +71 -0
- package/skills/create-tile-palette-or-rule-tiles/references/palette-create.md +38 -0
- package/skills/create-tile-palette-or-rule-tiles/references/ruletile-createempty.md +37 -0
- package/skills/create-tile-palette-or-rule-tiles/references/ruletile-createfromsegment.md +219 -0
- package/skills/create-tile-palette-or-rule-tiles/references/ruletile.md +95 -0
- package/skills/create-tile-palette-or-rule-tiles/scripts/CreatePaletteTemplate.cs +148 -0
- package/skills/create-tile-palette-or-rule-tiles/scripts/ManualWorkflowExample.cs +63 -0
- package/skills/create-tile-palette-or-rule-tiles/scripts/RuleTileGenerator.cs +61 -0
- package/skills/create-tile-palette-or-rule-tiles/scripts/TilemapRuleTileCreateFromSegment.cs +225 -0
- package/skills/fix-3d-physics-collisions/SECURITY.md +17 -0
- package/skills/fix-3d-physics-collisions/SKILL.md +139 -0
- package/skills/fix-3d-physics-collisions/references/collision-checklist.md +80 -0
- package/skills/fix-3d-physics-collisions/references/collision-debugger.md +18 -0
- package/skills/fix-3d-physics-collisions/references/console-errors-table.md +9 -0
- package/skills/fix-3d-physics-collisions/references/kinematic-trigger-facts.md +18 -0
- package/skills/fix-3d-physics-collisions/references/other-symptoms.md +29 -0
- package/skills/fix-3d-physics-collisions/references/raycast-checklist.md +8 -0
- package/skills/fix-3d-physics-collisions/references/topic-map.md +42 -0
- package/skills/fix-3d-physics-collisions/references/trigger-checklist.md +9 -0
- package/skills/fix-3d-physics-collisions/scripts/CollisionDebugger.cs +561 -0
- package/skills/generate-editor-search-query/SECURITY.md +17 -0
- package/skills/generate-editor-search-query/SKILL.md +117 -0
- package/skills/generate-editor-search-query/references/open-search-window.md +70 -0
- package/skills/generate-editor-search-query/references/query-patterns.md +124 -0
- package/skills/implement-in-app-purchases/SKILL.md +82 -0
- package/skills/implement-in-app-purchases/references/api-details.md +41 -0
- package/skills/implement-in-app-purchases/references/api-notes.md +105 -0
- package/skills/implement-in-app-purchases/references/codeless-catalog.md +197 -0
- package/skills/implement-in-app-purchases/references/convert-adapty.md +270 -0
- package/skills/implement-in-app-purchases/references/convert-essentialkit.md +239 -0
- package/skills/implement-in-app-purchases/references/convert-revenuecat.md +277 -0
- package/skills/implement-in-app-purchases/references/convert-unipay.md +145 -0
- package/skills/implement-in-app-purchases/references/migration-v4-to-v5.md +46 -0
- package/skills/implement-in-app-purchases/references/path-add-iap-to-new-project.md +197 -0
- package/skills/implement-in-app-purchases/references/path-convert-native-google-billing.md +302 -0
- package/skills/implement-in-app-purchases/references/path-convert-native-storekit.md +357 -0
- package/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md +253 -0
- package/skills/implement-in-app-purchases/references/platform-notes.md +55 -0
- package/skills/implement-in-app-purchases/references/pre-check.md +207 -0
- package/skills/implement-in-app-purchases/references/topic-map.md +46 -0
- package/skills/initialize-ai-navigation/SKILL.md +109 -0
- package/skills/initialize-ai-navigation/references/navigation-system.md +793 -0
- package/skills/initialize-ai-navigation/references/topic-map.md +66 -0
- package/skills/levelplay-unity-integration/SKILL.md +122 -0
- package/skills/levelplay-unity-integration/references/adding-more-ad-formats.md +15 -0
- package/skills/levelplay-unity-integration/references/banner-api.md +927 -0
- package/skills/levelplay-unity-integration/references/best-practices.md +604 -0
- package/skills/levelplay-unity-integration/references/dependency-resolution.md +94 -0
- package/skills/levelplay-unity-integration/references/examples.md +10 -0
- package/skills/levelplay-unity-integration/references/ilrd-api.md +337 -0
- package/skills/levelplay-unity-integration/references/initialization-api.md +851 -0
- package/skills/levelplay-unity-integration/references/interstitial-api.md +907 -0
- package/skills/levelplay-unity-integration/references/ios-setup.md +463 -0
- package/skills/levelplay-unity-integration/references/migration-sdk-9.md +666 -0
- package/skills/levelplay-unity-integration/references/privacy-settings.md +593 -0
- package/skills/levelplay-unity-integration/references/production-release-checklist.md +22 -0
- package/skills/levelplay-unity-integration/references/rewarded-api.md +918 -0
- package/skills/levelplay-unity-integration/references/step-0-new-or-migration.md +8 -0
- package/skills/levelplay-unity-integration/references/step-1-verify-environment.md +4 -0
- package/skills/levelplay-unity-integration/references/step-10-testing.md +15 -0
- package/skills/levelplay-unity-integration/references/step-2-business-goals.md +12 -0
- package/skills/levelplay-unity-integration/references/step-3-install-sdk.md +55 -0
- package/skills/levelplay-unity-integration/references/step-4-native-dependencies.md +22 -0
- package/skills/levelplay-unity-integration/references/step-5-app-key-and-ad-units.md +18 -0
- package/skills/levelplay-unity-integration/references/step-6-5-privacy-and-regulation.md +51 -0
- package/skills/levelplay-unity-integration/references/step-6-admob-keys.md +16 -0
- package/skills/levelplay-unity-integration/references/step-7-initialize.md +50 -0
- package/skills/levelplay-unity-integration/references/step-8-ad-unit-strategy.md +16 -0
- package/skills/levelplay-unity-integration/references/step-9-implement-ad-units.md +66 -0
- package/skills/levelplay-unity-integration/references/testing-and-validation.md +222 -0
- package/skills/levelplay-unity-integration/references/troubleshooting.md +99 -0
- package/skills/localize-unity-project/SKILL.md +159 -0
- package/skills/localize-unity-project/references/api-notes.md +15 -0
- package/skills/localize-unity-project/references/cjk-fonts.md +83 -0
- package/skills/localize-unity-project/references/docs-map.md +42 -0
- package/skills/localize-unity-project/references/documentation-lookup.md +33 -0
- package/skills/localize-unity-project/references/locale-switching.md +17 -0
- package/skills/localize-unity-project/references/table-completeness-check.md +47 -0
- package/skills/localize-unity-project/references/translating-existing-project.md +47 -0
- package/skills/localize-unity-project/references/translation-strategy.md +41 -0
- package/skills/localize-unity-project/references/troubleshooting.md +12 -0
- package/skills/localize-unity-project/scripts/L10nBatchProcessor.cs +191 -0
- package/skills/localize-unity-project/scripts/LocalizedFontAsset.cs +18 -0
- package/skills/manage-sprite-atlas/SKILL.md +202 -0
- package/skills/manage-sprite-atlas/references/addressables-delivery.md +58 -0
- package/skills/manage-sprite-atlas/references/advanced-manual-authoring.md +19 -0
- package/skills/manage-sprite-atlas/references/api.md +382 -0
- package/skills/manage-sprite-atlas/references/best-practices.md +183 -0
- package/skills/manage-sprite-atlas/references/common-errors.md +123 -0
- package/skills/manage-sprite-atlas/references/complete-workflow.md +61 -0
- package/skills/manage-sprite-atlas/references/custom-packing.md +147 -0
- package/skills/manage-sprite-atlas/references/delivery-mechanism.md +30 -0
- package/skills/manage-sprite-atlas/references/key-requirements.md +21 -0
- package/skills/manage-sprite-atlas/references/manual-authoring.md +39 -0
- package/skills/manage-sprite-atlas/references/namespaces.md +15 -0
- package/skills/manage-sprite-atlas/references/v2-architecture.md +21 -0
- package/skills/manage-sprite-atlas/resources/authoringvsruntime.cs +176 -0
- package/skills/manage-sprite-atlas/resources/buildaddressablespostprocess.cs +59 -0
- package/skills/manage-sprite-atlas/resources/cacheloadedatlases.cs +145 -0
- package/skills/manage-sprite-atlas/resources/cleanupbatchoperations.cs +119 -0
- package/skills/manage-sprite-atlas/resources/createandconfiguremasteratlas.cs +57 -0
- package/skills/manage-sprite-atlas/resources/custompackersetup.cs +88 -0
- package/skills/manage-sprite-atlas/resources/deprecatedmethods.cs +142 -0
- package/skills/manage-sprite-atlas/resources/dontmodifywithoutsaving.cs +120 -0
- package/skills/manage-sprite-atlas/resources/dontpackinruntimebuilds.cs +190 -0
- package/skills/manage-sprite-atlas/resources/dontscriptspriteatlasineditor.cs +114 -0
- package/skills/manage-sprite-atlas/resources/editorconfiguration.cs +95 -0
- package/skills/manage-sprite-atlas/resources/enablespritepacking.cs +172 -0
- package/skills/manage-sprite-atlas/resources/gridspritepacker.cs +50 -0
- package/skills/manage-sprite-atlas/resources/handlelatebinding.cs +147 -0
- package/skills/manage-sprite-atlas/resources/handlenullcases.cs +141 -0
- package/skills/manage-sprite-atlas/resources/migratev1tov2.cs +335 -0
- package/skills/manage-sprite-atlas/resources/multipagepacker.cs +80 -0
- package/skills/manage-sprite-atlas/resources/optimizetexturesettings.cs +191 -0
- package/skills/manage-sprite-atlas/resources/packerdebugging.cs +158 -0
- package/skills/manage-sprite-atlas/resources/packtransform.cs +51 -0
- package/skills/manage-sprite-atlas/resources/platformnames.cs +127 -0
- package/skills/manage-sprite-atlas/resources/queryspritesefficiently.cs +222 -0
- package/skills/manage-sprite-atlas/resources/registercallbacks.cs +152 -0
- package/skills/manage-sprite-atlas/resources/runtimeatlasloader.cs +15 -0
- package/skills/manage-sprite-atlas/resources/runtimespriteaccess.cs +19 -0
- package/skills/manage-sprite-atlas/resources/savespriteatlasasset.cs +107 -0
- package/skills/manage-sprite-atlas/resources/scriptablepacker.cs +81 -0
- package/skills/manage-sprite-atlas/resources/setincludeinbuild.cs +164 -0
- package/skills/manage-sprite-atlas/resources/sizeoptimizedpacker.cs +88 -0
- package/skills/manage-sprite-atlas/resources/spriteatlaslatebinding.cs +123 -0
- package/skills/manage-sprite-atlas/resources/spriteatlasprebuildgenerator.cs +219 -0
- package/skills/manage-sprite-atlas/resources/spritedata.cs +35 -0
- package/skills/manage-sprite-atlas/resources/spritepack.cs +39 -0
- package/skills/manage-sprite-atlas/resources/texturedata.cs +15 -0
- package/skills/manage-sprite-atlas/resources/updatebuildscripts.cs +357 -0
- package/skills/manage-sprite-atlas/resources/updatesettingsconfiguration.cs +135 -0
- package/skills/manage-sprite-atlas/resources/usevariantswisely.cs +179 -0
- package/skills/manage-sprite-atlas/resources/variantatlas.cs +39 -0
- package/skills/migrate-birp-to-urp/SECURITY.md +16 -0
- package/skills/migrate-birp-to-urp/SKILL.md +167 -0
- package/skills/migrate-birp-to-urp/references/capturing-the-editor.md +66 -0
- package/skills/migrate-birp-to-urp/references/complex-shader-situations.md +272 -0
- package/skills/migrate-birp-to-urp/references/conversion-and-rendering-triage.md +113 -0
- package/skills/migrate-birp-to-urp/references/custom-shader-triage.md +123 -0
- package/skills/migrate-birp-to-urp/references/implementation-patterns.md +529 -0
- package/skills/migrate-birp-to-urp/references/migration-workflow.md +265 -0
- package/skills/migrate-birp-to-urp/references/parity-validation-and-troubleshooting.md +114 -0
- package/skills/migrate-birp-to-urp/references/preflight-and-urp-setup.md +195 -0
- package/skills/migrate-birp-to-urp/references/quality-settings-map.md +69 -0
- package/skills/new-unity-project/SECURITY.md +26 -0
- package/skills/new-unity-project/SKILL.md +171 -0
- package/skills/new-unity-project/references/checklist.md +18 -0
- package/skills/new-unity-project/references/common-mistakes.md +15 -0
- package/skills/new-unity-project/references/skill-setup.md +66 -0
- package/skills/new-unity-project/references/step-1-concept.md +13 -0
- package/skills/new-unity-project/references/step-2-platforms-and-create.md +31 -0
- package/skills/new-unity-project/references/step-3-join-and-reconcile.md +16 -0
- package/skills/new-unity-project/references/step-4-source-control.md +9 -0
- package/skills/new-unity-project/references/step-5-packages.md +18 -0
- package/skills/new-unity-project/references/step-6-identity-and-floors.md +20 -0
- package/skills/new-unity-project/references/step-7-visual-baseline.md +75 -0
- package/skills/new-unity-project/references/step-8-first-commit.md +21 -0
- package/skills/new-unity-project/references/step-9-handoff.md +23 -0
- package/skills/optimize-audio/SECURITY.md +16 -0
- package/skills/optimize-audio/SKILL.md +120 -0
- package/skills/optimize-audio/references/diagnostics.md +42 -0
- package/skills/optimize-audio/references/troubleshooting.md +41 -0
- package/skills/optimize-audio/resources/audio-import-api.md +149 -0
- package/skills/optimize-audio/resources/platform-settings.md +48 -0
- package/skills/optimize-text-mesh-pro/SKILL.md +181 -0
- package/skills/optimize-text-mesh-pro/references/dynamic-atlas-and-profiling.md +28 -0
- package/skills/optimize-text-mesh-pro/references/topic-map.md +36 -0
- package/skills/optimize-web/SECURITY.md +16 -0
- package/skills/optimize-web/SKILL.md +157 -0
- package/skills/optimize-web/references/editor-csharp.md +120 -0
- package/skills/optimize-web/references/topic-map.md +49 -0
- package/skills/optimize-web/references/troubleshooting.md +45 -0
- package/skills/optimize-web/references/understand-request.md +19 -0
- package/skills/optimize-web/references/web-assets-quality.md +59 -0
- package/skills/optimize-web/references/web-player-settings.md +39 -0
- package/skills/optimize-web/references/web-profiling-memory.md +71 -0
- package/skills/optimize-web/scripts/WebOptimizer.cs +46 -0
- package/skills/optimize-web/scripts/toktx-examples.sh +11 -0
- package/skills/setup-multiplayer-services/SKILL.md +36 -0
- package/skills/setup-multiplayer-services/references/dgs-entrypoint.md +79 -0
- package/skills/setup-multiplayer-services/references/entrypoints.md +213 -0
- package/skills/setup-multiplayer-services/references/examples.md +33 -0
- package/skills/setup-multiplayer-services/references/implementation-fit.md +30 -0
- package/skills/setup-multiplayer-services/references/underlying-services.md +11 -0
- package/skills/setup-multiplayer-services/references/workflows-prerequisites.md +17 -0
- package/skills/setup-vivox-voice-chat/SECURITY.md +16 -0
- package/skills/setup-vivox-voice-chat/SKILL.md +166 -0
- package/skills/setup-vivox-voice-chat/references/api-reference-lookup.md +28 -0
- package/skills/setup-vivox-voice-chat/references/docs-topic-map.md +13 -0
- package/skills/setup-vivox-voice-chat/references/events-and-participants.md +79 -0
- package/skills/setup-vivox-voice-chat/references/init-and-login.md +92 -0
- package/skills/setup-vivox-voice-chat/references/text-chat.md +93 -0
- package/skills/setup-vivox-voice-chat/references/troubleshooting.md +47 -0
- package/skills/setup-vivox-voice-chat/references/voice-channels.md +92 -0
- package/skills/shader-graph-create-custom-node/SKILL.md +25 -0
- package/skills/shader-graph-create-custom-node/resources/all_hints.hlsl +182 -0
- package/skills/sprite-editor/SECURITY.md +16 -0
- package/skills/sprite-editor/SKILL.md +66 -0
- package/skills/sprite-editor/references/api_reference.md +151 -0
- package/skills/sprite-editor/references/background.md +112 -0
- package/skills/sprite-editor/references/templates.md +72 -0
- package/skills/sprite-editor/scripts/AutomaticSliceTexture.cs +40 -0
- package/skills/sprite-editor/scripts/GenerateNewSpriteRects.cs +200 -0
- package/skills/sprite-editor/scripts/GetTextureSourceImageSize.cs +32 -0
- package/skills/sprite-editor/scripts/GetTextureToSlice.cs +55 -0
- package/skills/sprite-editor/scripts/GridSliceTexture.cs +40 -0
- package/skills/sprite-editor/scripts/IsometricSliceTexture.cs +141 -0
- package/skills/sprite-editor/scripts/README.md +134 -0
- package/skills/sprite-editor/scripts/SetPivotExample.cs +58 -0
- package/skills/sprite-editor/scripts/SpriteToPng.cs +88 -0
- package/skills/sprite-segment-3x3grid/SKILL.md +177 -0
- package/skills/sprite-segment-3x3grid/references/topic-map.md +37 -0
- package/skills/sprite-segment-3x3grid/scripts/SpriteGridAnalysis.cs +119 -0
- package/skills/ui/SKILL.md +142 -0
- package/skills/ui-imgui/SKILL.md +180 -0
- package/skills/ui-imgui/references/gui-elements.md +156 -0
- package/skills/ui-imgui/references/templates.md +141 -0
- package/skills/ui-ugui/SKILL.md +217 -0
- package/skills/ui-ugui/references/layout-and-anchors.md +61 -0
- package/skills/ui-ugui/references/scrollview-setup.md +29 -0
- package/skills/ui-uitk/SKILL.md +229 -0
- package/skills/ui-uitk/references/common-issues.md +74 -0
- package/skills/ui-uitk/references/custom-elements.md +241 -0
- package/skills/ui-uitk/references/painter2d.md +282 -0
- package/skills/ui-uitk/references/pointermanipulator-guide.md +94 -0
- package/skills/ui-uitk/references/svg-icons.md +136 -0
- package/skills/ui-uitk/references/ui-runtime-binding.md +234 -0
- package/skills/ui-uitk/references/uss-guide.md +138 -0
- package/skills/unity-cli/CHANGELOG.md +524 -0
- package/skills/unity-cli/SECURITY.md +68 -0
- package/skills/unity-cli/SKILL.md +160 -0
- package/skills/unity-cli/references/auth-license-cloud.md +318 -0
- package/skills/unity-cli/references/bootstrap-project.md +177 -0
- package/skills/unity-cli/references/build-run-test.md +555 -0
- package/skills/unity-cli/references/catalog-plugins.md +86 -0
- package/skills/unity-cli/references/ci-recipes.md +76 -0
- package/skills/unity-cli/references/cloud-automation.md +819 -0
- package/skills/unity-cli/references/cloud-workflows.md +19 -0
- package/skills/unity-cli/references/collaboration.md +533 -0
- package/skills/unity-cli/references/config-hub.md +239 -0
- package/skills/unity-cli/references/diagnostics-maintenance.md +718 -0
- package/skills/unity-cli/references/docs-workflow.md +179 -0
- package/skills/unity-cli/references/editor-targeting.md +29 -0
- package/skills/unity-cli/references/editors-install.md +490 -0
- package/skills/unity-cli/references/environment-variables.md +42 -0
- package/skills/unity-cli/references/exit-codes.md +47 -0
- package/skills/unity-cli/references/global-flags.md +32 -0
- package/skills/unity-cli/references/integration-advanced.md +1130 -0
- package/skills/unity-cli/references/notes.md +20 -0
- package/skills/unity-cli/references/notifications.md +39 -0
- package/skills/unity-cli/references/playmode-verification-loop.md +279 -0
- package/skills/unity-cli/references/projects-templates.md +997 -0
- package/skills/unity-cli/references/scene-editing-preflight.md +25 -0
- package/skills/unity-cli/references/setup-and-help.md +54 -0
- package/skills/unity-cli/references/version-control.md +612 -0
- package/skills/unity-package-management/SKILL.md +169 -0
- package/skills/unity-package-management/references/discovering-packages.md +66 -0
- package/skills/unity-package-management/references/headless-run.md +31 -0
- package/skills/unity-package-management/references/installer-script.md +92 -0
- package/skills/unity-package-management/references/select-packages.md +126 -0
- package/skills/unity-pipeline/SECURITY.md +17 -0
- package/skills/unity-pipeline/SKILL.md +205 -0
- package/skills/unity-pipeline/references/csharp-eval.md +23 -0
- package/skills/unity-pipeline/references/multiplayer-play-mode.md +103 -0
- package/skills/urp-postprocessing/SECURITY.md +16 -0
- package/skills/urp-postprocessing/SKILL.md +188 -0
- package/skills/urp-postprocessing/references/code-templates.md +150 -0
- package/skills/urp-postprocessing/references/effect-reference.md +86 -0
- package/skills/validate-urp-render-graph-renderer-feature/SKILL.md +92 -0
- package/skills/validate-urp-render-graph-renderer-feature/references/validation-checklist.md +179 -0
|
@@ -0,0 +1,904 @@
|
|
|
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 Tier Thresholds Live in the Module](#why-tier-thresholds-live-in-the-module)
|
|
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` (integration stub; always throws until you add purchase validation) |
|
|
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 (defined once, in the cloud assembly)
|
|
112
|
+
|
|
113
|
+
Stored in Cloud Save Protected, one entry per pass, keyed by `PassId` in the progress dictionary at Cloud Save key `"battle_pass_progress"`. Define it once, in `BattlePassModule.cs` (see [Server-Side Models](#server-side-models-inside-battlepassmodulecs)), as a class of public settable members. The source generator emits a data-only stub of the same name and namespace, `BattlePassModule.Cloud.BattlePassProgress`, into the client assembly, so client code uses that type and never declares its own copy. `ClaimedRewards` holds keys such as `"tier_1_free"` and `"tier_2_premium"`.
|
|
114
|
+
|
|
115
|
+
`BattlePassTierThreshold` is a server-only helper that mirrors the `Tiers` in `BattlePasses.rc`. It never appears in an endpoint signature, so it has no client stub.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## API Notes
|
|
120
|
+
|
|
121
|
+
### Namespace Aliases (Required)
|
|
122
|
+
|
|
123
|
+
Cloud Save has multiple types named `LoadOptions`, `SaveOptions`, and `DeleteOptions` across
|
|
124
|
+
different namespaces. Always declare these aliases at the top of any file that uses Cloud Save:
|
|
125
|
+
|
|
126
|
+
```csharp
|
|
127
|
+
using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
|
|
128
|
+
using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
|
|
129
|
+
using PlayerDeleteOptions = Unity.Services.CloudSave.Models.Data.Player.DeleteOptions;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Without these aliases, the compiler will report ambiguous type references.
|
|
133
|
+
|
|
134
|
+
### ProtectedReadAccessClassOptions
|
|
135
|
+
|
|
136
|
+
Use `ProtectedReadAccessClassOptions` when loading player progress. This reads the current
|
|
137
|
+
player's own Protected data — no player ID parameter is required; it always reads the signed-in
|
|
138
|
+
player's data.
|
|
139
|
+
|
|
140
|
+
> **Common mistake:** Reading from the wrong access class (e.g. using default `LoadOptions`
|
|
141
|
+
> instead of `ProtectedReadAccessClassOptions`) will return no data, because the progress
|
|
142
|
+
> is stored in the Protected bucket by the Cloud Code module.
|
|
143
|
+
|
|
144
|
+
### Why Tier Thresholds Live in the Module
|
|
145
|
+
|
|
146
|
+
Remote Config is not accessible from inside Cloud Code modules, and anything the client sends is
|
|
147
|
+
untrusted: a client that supplies its own tier thresholds can set a tier-three threshold of zero
|
|
148
|
+
and reach tier three with 1 XP. The module therefore holds the thresholds in a server-side
|
|
149
|
+
constant (`PassTiers` in `BattlePassModule.cs`) and `AwardXp` takes no tier data from the client.
|
|
150
|
+
Pass dates get the same treatment: `PassWindows` holds each pass's UTC start and end, and `AwardXp`
|
|
151
|
+
and `ClaimReward` reject any pass outside `[Start, End)` using server time, so an expired pass cannot
|
|
152
|
+
gain XP and a staged future pass cannot be claimed early. Rewards must be claimed while the pass is
|
|
153
|
+
active. Keep `PassTiers` and `PassWindows` in sync with the `XpRequired`, `StartDate` and `EndDate`
|
|
154
|
+
values in `BattlePasses.rc` whenever you change a pass. Passes missing from either are rejected.
|
|
155
|
+
|
|
156
|
+
### Source Whitelist Enforcement
|
|
157
|
+
|
|
158
|
+
The Cloud Code module maintains a server-side whitelist of allowed XP source strings:
|
|
159
|
+
|
|
160
|
+
```csharp
|
|
161
|
+
static readonly Dictionary<string, int> MaxXpPerSource = new()
|
|
162
|
+
{
|
|
163
|
+
["match_complete"] = 200,
|
|
164
|
+
["daily_login"] = 50,
|
|
165
|
+
["quest_complete"] = 300
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Any `AwardXp` call with an unrecognized source throws an exception, and each source has a
|
|
170
|
+
per-call XP cap (`MaxXpPerSource`). Add or remove entries as needed for your game. The whitelist
|
|
171
|
+
and caps prevent clients from inventing XP sources or awarding themselves arbitrary amounts.
|
|
172
|
+
For full protection, derive XP from server-verified events instead of a client-supplied `amount`.
|
|
173
|
+
|
|
174
|
+
### Error Handling
|
|
175
|
+
|
|
176
|
+
Cloud Code and Cloud Save errors surface as typed exceptions on the client:
|
|
177
|
+
|
|
178
|
+
```csharp
|
|
179
|
+
try
|
|
180
|
+
{
|
|
181
|
+
var progress = await AwardXpAsync(passId, 100, "match_complete");
|
|
182
|
+
}
|
|
183
|
+
catch (CloudCodeException ex)
|
|
184
|
+
{
|
|
185
|
+
// ex.Reason: CloudCodeExceptionReason (e.g. ScriptError for module-thrown exceptions)
|
|
186
|
+
Debug.LogError($"[BattlePass] Cloud Code error: {ex.Reason} — {ex.Message}");
|
|
187
|
+
}
|
|
188
|
+
catch (CloudSaveException ex)
|
|
189
|
+
{
|
|
190
|
+
// ex.Reason: CloudSaveExceptionReason
|
|
191
|
+
// A 403 on the Protected bucket means the client attempted a direct write —
|
|
192
|
+
// all writes must go through the Cloud Code module.
|
|
193
|
+
Debug.LogError($"[BattlePass] Cloud Save error: {ex.Reason} — {ex.Message}");
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
| Error | Cause | Fix |
|
|
198
|
+
| :-- | :-- | :-- |
|
|
199
|
+
| `CloudCodeException` with reason `ScriptError` | The module threw, for example on a failed validation | Read `ex.Message`, then correct the arguments or the player's state |
|
|
200
|
+
| `CloudSaveException` with a `403` | The client attempted a direct write to the Protected bucket | Route the write through the Cloud Code module |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Cloud Code Module Functions
|
|
205
|
+
|
|
206
|
+
| Function | Parameters | Behavior |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `AwardXp` | `passId`, `amount`, `source` | Validates `source` against a whitelist (`MaxXpPerSource` keys), rejects non-positive amounts and amounts above the source's cap, rejects unknown passes, adds XP to the pass progress, recalculates the current tier from the server-side `PassTiers`, saves to Cloud Save Protected |
|
|
209
|
+
| `ClaimReward` | `passId`, `tierNumber`, `rewardType` | Validates the tier exists in the server-side `PassTiers` and the player's XP meets its threshold, 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 |
|
|
210
|
+
| `PurchasePremium` | `passId` | **Integration stub.** Returns the existing progress if `HasPremium` is already true; otherwise the sample always throws, because nothing in the blueprint sets `HasPremium`. Add server-side purchase validation that sets `HasPremium = true` before premium purchasing works; do not deploy this path as-is. It fails closed and never grants premium unverified. |
|
|
211
|
+
|
|
212
|
+
All three functions return the updated `BattlePassProgress` for the given pass.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Load Pass Definitions from Remote Config
|
|
217
|
+
|
|
218
|
+
```csharp
|
|
219
|
+
struct UserAttributes {}
|
|
220
|
+
struct AppAttributes {}
|
|
221
|
+
|
|
222
|
+
async Task<List<BattlePassDefinition>> LoadActivePassesAsync()
|
|
223
|
+
{
|
|
224
|
+
var result = await RemoteConfigService.Instance.FetchConfigsAsync(
|
|
225
|
+
new UserAttributes(), new AppAttributes());
|
|
226
|
+
|
|
227
|
+
var token = result.config["battle_passes"];
|
|
228
|
+
if (token == null) return new List<BattlePassDefinition>();
|
|
229
|
+
|
|
230
|
+
var allPasses = token.ToObject<List<BattlePassDefinition>>();
|
|
231
|
+
|
|
232
|
+
var now = DateTime.UtcNow;
|
|
233
|
+
return allPasses
|
|
234
|
+
.Where(p =>
|
|
235
|
+
DateTime.Parse(p.StartDate, null, DateTimeStyles.RoundtripKind) <= now &&
|
|
236
|
+
DateTime.Parse(p.EndDate, null, DateTimeStyles.RoundtripKind) >= now)
|
|
237
|
+
.ToList();
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
- `FetchConfigsAsync` downloads all Remote Config entries.
|
|
242
|
+
- `result.config["battle_passes"]` returns a `JToken` for the JSON array stored under that key.
|
|
243
|
+
- Date filtering is performed client-side by comparing `StartDate` and `EndDate` against `DateTime.UtcNow`.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Load Player Progress
|
|
248
|
+
|
|
249
|
+
```csharp
|
|
250
|
+
using Unity.Services.CloudSave;
|
|
251
|
+
using Unity.Services.CloudSave.Models;
|
|
252
|
+
using Unity.Services.CloudSave.Models.Data.Player;
|
|
253
|
+
using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
|
|
254
|
+
|
|
255
|
+
async Task<Dictionary<string, BattlePassProgress>> LoadProgressAsync()
|
|
256
|
+
{
|
|
257
|
+
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
|
|
258
|
+
new HashSet<string> { "battle_pass_progress" },
|
|
259
|
+
new PlayerLoadOptions(new ProtectedReadAccessClassOptions()));
|
|
260
|
+
|
|
261
|
+
if (!result.TryGetValue("battle_pass_progress", out var item))
|
|
262
|
+
return new Dictionary<string, BattlePassProgress>();
|
|
263
|
+
|
|
264
|
+
return JsonConvert.DeserializeObject<Dictionary<string, BattlePassProgress>>(
|
|
265
|
+
item.Value.GetAsString())
|
|
266
|
+
?? new Dictionary<string, BattlePassProgress>();
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Important:** Use `ProtectedReadAccessClassOptions` because the Cloud Code module writes to the Protected bucket. Reading from the wrong access class will return no data.
|
|
271
|
+
|
|
272
|
+
> **Stale entries:** Expired pass entries remain in the dictionary indefinitely but are inert —
|
|
273
|
+
> the client ignores any PassId not present in the current active pass list.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Client to Cloud Code Calls
|
|
278
|
+
|
|
279
|
+
### AwardXpAsync
|
|
280
|
+
|
|
281
|
+
```csharp
|
|
282
|
+
public async Task<BattlePassProgress> AwardXpAsync(string passId, int amount, string source)
|
|
283
|
+
{
|
|
284
|
+
if (!ActivePasses.Any(p => p.PassId == passId))
|
|
285
|
+
throw new InvalidOperationException(
|
|
286
|
+
$"Pass '{passId}' is not in the active passes list. Call LoadAsync first.");
|
|
287
|
+
|
|
288
|
+
var progress = await m_Client.AwardXp(passId, amount, source);
|
|
289
|
+
|
|
290
|
+
Progress[passId] = progress;
|
|
291
|
+
return progress;
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The client sends no tier data; the server recalculates the player's tier from its own `PassTiers`. The active-pass check above is UX only; the module enforces pass dates itself. `m_Client` is the generated `BattlePassModuleClient`, created after sign-in.
|
|
296
|
+
|
|
297
|
+
### ClaimRewardAsync
|
|
298
|
+
|
|
299
|
+
```csharp
|
|
300
|
+
public async Task<BattlePassProgress> ClaimRewardAsync(
|
|
301
|
+
string passId, int tierNumber, string rewardType)
|
|
302
|
+
{
|
|
303
|
+
var progress = await m_Client.ClaimReward(passId, tierNumber, rewardType);
|
|
304
|
+
|
|
305
|
+
Progress[passId] = progress;
|
|
306
|
+
return progress;
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### PurchasePremiumAsync
|
|
311
|
+
|
|
312
|
+
Calls the `PurchasePremium` integration stub. It throws for any player without `HasPremium` until you add a validated purchase flow to the module.
|
|
313
|
+
|
|
314
|
+
```csharp
|
|
315
|
+
public async Task<BattlePassProgress> PurchasePremiumAsync(string passId)
|
|
316
|
+
{
|
|
317
|
+
var progress = await m_Client.PurchasePremium(passId);
|
|
318
|
+
|
|
319
|
+
Progress[passId] = progress;
|
|
320
|
+
return progress;
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Full Client Implementation
|
|
327
|
+
|
|
328
|
+
`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.
|
|
329
|
+
|
|
330
|
+
```csharp
|
|
331
|
+
using System;
|
|
332
|
+
using System.Collections.Generic;
|
|
333
|
+
using System.Globalization;
|
|
334
|
+
using System.Linq;
|
|
335
|
+
using System.Threading.Tasks;
|
|
336
|
+
using BattlePassModule.Client;
|
|
337
|
+
using BattlePassModule.Cloud;
|
|
338
|
+
using Newtonsoft.Json;
|
|
339
|
+
using Unity.Services.Authentication;
|
|
340
|
+
using Unity.Services.CloudSave;
|
|
341
|
+
using Unity.Services.CloudSave.Models;
|
|
342
|
+
using Unity.Services.CloudSave.Models.Data.Player;
|
|
343
|
+
using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
|
|
344
|
+
using Unity.Services.Core;
|
|
345
|
+
using Unity.Services.RemoteConfig;
|
|
346
|
+
using UnityEngine;
|
|
347
|
+
|
|
348
|
+
public class BattlePassManager : MonoBehaviour
|
|
349
|
+
{
|
|
350
|
+
BattlePassModuleClient m_Client;
|
|
351
|
+
|
|
352
|
+
public List<BattlePassDefinition> ActivePasses { get; private set; } = new();
|
|
353
|
+
public Dictionary<string, BattlePassProgress> Progress { get; private set; } = new();
|
|
354
|
+
|
|
355
|
+
public event Action Loaded;
|
|
356
|
+
|
|
357
|
+
async void Start()
|
|
358
|
+
{
|
|
359
|
+
try
|
|
360
|
+
{
|
|
361
|
+
await UnityServices.InitializeAsync();
|
|
362
|
+
if (!AuthenticationService.Instance.IsSignedIn)
|
|
363
|
+
{
|
|
364
|
+
await AuthenticationService.Instance.SignInAnonymouslyAsync();
|
|
365
|
+
}
|
|
366
|
+
m_Client = new BattlePassModuleClient();
|
|
367
|
+
await LoadAsync();
|
|
368
|
+
Debug.Log($"BattlePassManager: Loaded {ActivePasses.Count} passes.");
|
|
369
|
+
Loaded?.Invoke();
|
|
370
|
+
}
|
|
371
|
+
catch (Exception ex)
|
|
372
|
+
{
|
|
373
|
+
Debug.LogError($"BattlePassManager: Error during loading: {ex.Message}\n{ex.StackTrace}");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public async Task LoadAsync()
|
|
378
|
+
{
|
|
379
|
+
ActivePasses = await LoadActivePassesAsync();
|
|
380
|
+
Progress = await LoadProgressAsync();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
struct UserAttributes {}
|
|
384
|
+
struct AppAttributes {}
|
|
385
|
+
|
|
386
|
+
async Task<List<BattlePassDefinition>> LoadActivePassesAsync()
|
|
387
|
+
{
|
|
388
|
+
var result = await RemoteConfigService.Instance.FetchConfigsAsync(
|
|
389
|
+
new UserAttributes(), new AppAttributes());
|
|
390
|
+
|
|
391
|
+
var token = result.config["battle_passes"];
|
|
392
|
+
if (token == null) return new List<BattlePassDefinition>();
|
|
393
|
+
|
|
394
|
+
var allPasses = token.ToObject<List<BattlePassDefinition>>();
|
|
395
|
+
|
|
396
|
+
var now = DateTime.UtcNow;
|
|
397
|
+
return allPasses
|
|
398
|
+
.Where(p =>
|
|
399
|
+
DateTime.Parse(p.StartDate, null, DateTimeStyles.RoundtripKind) <= now &&
|
|
400
|
+
DateTime.Parse(p.EndDate, null, DateTimeStyles.RoundtripKind) >= now)
|
|
401
|
+
.ToList();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async Task<Dictionary<string, BattlePassProgress>> LoadProgressAsync()
|
|
405
|
+
{
|
|
406
|
+
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
|
|
407
|
+
new HashSet<string> { "battle_pass_progress" },
|
|
408
|
+
new PlayerLoadOptions(new ProtectedReadAccessClassOptions()));
|
|
409
|
+
|
|
410
|
+
if (!result.TryGetValue("battle_pass_progress", out var item))
|
|
411
|
+
return new Dictionary<string, BattlePassProgress>();
|
|
412
|
+
|
|
413
|
+
return JsonConvert.DeserializeObject<Dictionary<string, BattlePassProgress>>(
|
|
414
|
+
item.Value.GetAsString())
|
|
415
|
+
?? new Dictionary<string, BattlePassProgress>();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
public async Task<BattlePassProgress> AwardXpAsync(string passId, int amount, string source)
|
|
419
|
+
{
|
|
420
|
+
if (!ActivePasses.Any(p => p.PassId == passId))
|
|
421
|
+
throw new InvalidOperationException(
|
|
422
|
+
$"Pass '{passId}' is not in the active passes list. Call LoadAsync first.");
|
|
423
|
+
|
|
424
|
+
var progress = await m_Client.AwardXp(passId, amount, source);
|
|
425
|
+
|
|
426
|
+
Progress[passId] = progress;
|
|
427
|
+
return progress;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
public async Task<BattlePassProgress> ClaimRewardAsync(
|
|
431
|
+
string passId, int tierNumber, string rewardType)
|
|
432
|
+
{
|
|
433
|
+
var progress = await m_Client.ClaimReward(passId, tierNumber, rewardType);
|
|
434
|
+
|
|
435
|
+
Progress[passId] = progress;
|
|
436
|
+
return progress;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Requires the PurchasePremium integration stub to be completed server-side.
|
|
440
|
+
public async Task<BattlePassProgress> PurchasePremiumAsync(string passId)
|
|
441
|
+
{
|
|
442
|
+
var progress = await m_Client.PurchasePremium(passId);
|
|
443
|
+
|
|
444
|
+
Progress[passId] = progress;
|
|
445
|
+
return progress;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## BattlePassTester
|
|
453
|
+
|
|
454
|
+
Attach to the same GameObject as `BattlePassManager` in your test scene. Wire `m_Manager` via
|
|
455
|
+
the Inspector, or let it fall back to `GetComponent`.
|
|
456
|
+
|
|
457
|
+
```csharp
|
|
458
|
+
using System.Text;
|
|
459
|
+
using UnityEngine;
|
|
460
|
+
|
|
461
|
+
/// <summary>
|
|
462
|
+
/// Attach alongside BattlePassManager to run a quick in-Editor smoke test.
|
|
463
|
+
/// On load: logs all active pass states.
|
|
464
|
+
/// Then: awards 100 XP to the first active pass via Cloud Code and logs the result.
|
|
465
|
+
/// </summary>
|
|
466
|
+
public class BattlePassTester : MonoBehaviour
|
|
467
|
+
{
|
|
468
|
+
[SerializeField] BattlePassManager m_Manager;
|
|
469
|
+
|
|
470
|
+
void Start()
|
|
471
|
+
{
|
|
472
|
+
if (m_Manager == null)
|
|
473
|
+
m_Manager = GetComponent<BattlePassManager>();
|
|
474
|
+
m_Manager.Loaded += OnLoaded;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async void OnLoaded()
|
|
478
|
+
{
|
|
479
|
+
LogState("Initial state");
|
|
480
|
+
|
|
481
|
+
if (m_Manager.ActivePasses.Count == 0)
|
|
482
|
+
{
|
|
483
|
+
Debug.Log("[BattlePassTester] No active passes — nothing to award.");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
var firstPass = m_Manager.ActivePasses[0];
|
|
488
|
+
Debug.Log($"[BattlePassTester] Awarding 100 XP to '{firstPass.PassId}' " +
|
|
489
|
+
$"(source: match_complete) ...");
|
|
490
|
+
|
|
491
|
+
var result = await m_Manager.AwardXpAsync(firstPass.PassId, 100, "match_complete");
|
|
492
|
+
|
|
493
|
+
Debug.Log($"[BattlePassTester] After award — " +
|
|
494
|
+
$"XP={result.CurrentXp}, Tier={result.CurrentTier}");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
void LogState(string label)
|
|
498
|
+
{
|
|
499
|
+
var sb = new StringBuilder();
|
|
500
|
+
sb.AppendLine($"[BattlePassTester] {label} — {m_Manager.ActivePasses.Count} active pass(es):");
|
|
501
|
+
|
|
502
|
+
foreach (var pass in m_Manager.ActivePasses)
|
|
503
|
+
{
|
|
504
|
+
m_Manager.Progress.TryGetValue(pass.PassId, out var progress);
|
|
505
|
+
sb.AppendLine($" [{pass.PassId}] {pass.Name}: " +
|
|
506
|
+
$"XP={progress?.CurrentXp ?? 0}, " +
|
|
507
|
+
$"Tier={progress?.CurrentTier ?? 1}, " +
|
|
508
|
+
$"Premium={progress?.HasPremium ?? false}, " +
|
|
509
|
+
$"Claimed={progress?.ClaimedRewards?.Count ?? 0}");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
Debug.Log(sb.ToString());
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Cloud Code Module Implementation
|
|
520
|
+
|
|
521
|
+
> **Create the module with the Editor, not by hand.** Right-click a folder under `Assets/` ->
|
|
522
|
+
> **Create > Services > Cloud Code Module Script** and name it `BattlePassModule`. See
|
|
523
|
+
> the Creating a Cloud Code Module section of `references/cloud-code-modules.md`.
|
|
524
|
+
>
|
|
525
|
+
> The Editor generates the following. Use the list to check what you got — there is nothing here to
|
|
526
|
+
> type out, and no `.sln`, `.csproj`, `.pubxml` or test project:
|
|
527
|
+
>
|
|
528
|
+
> - [ ] `Assets/CloudCode/BattlePassModule/BattlePassModule.ccmu` (holds the two asmdef GUIDs)
|
|
529
|
+
> - [ ] `Cloud/BattlePassModule.asmdef` — Editor-only, `noEngineReferences`, `autoReferenced: false`
|
|
530
|
+
> - [ ] `Cloud/BattlePassModule.cs` — rename/replace with the `[CloudCodeFunction]` endpoints below
|
|
531
|
+
> - [ ] `Client/BattlePassModuleClient.asmdef` — references the cloud assembly
|
|
532
|
+
> - [ ] `Client/BattlePassModuleClient.cs` — `[CloudModuleClient(typeof(BattlePassModule))]` partial
|
|
533
|
+
>
|
|
534
|
+
> One file the Editor does **not** generate, which this module needs because its endpoints inject
|
|
535
|
+
> `IGameApiClient`:
|
|
536
|
+
>
|
|
537
|
+
> - [ ] `Cloud/CloudSetup.cs` — an `ICloudCodeSetup` calling `config.AddGameApiClient()`
|
|
538
|
+
> - [ ] `Newtonsoft.Json.dll` added to `precompiledReferences` in `Cloud/BattlePassModule.asmdef` — the generated asmdef ships that array empty, and this module's code uses `JsonConvert` (see `references/cloud-code-modules.md`)
|
|
539
|
+
>
|
|
540
|
+
> The deployed module name is the **cloud asmdef's name** (`BattlePassModule`), not the `.ccmu` filename.
|
|
541
|
+
>
|
|
542
|
+
> The client calls the module through the generated `BattlePassModuleClient`. The assembly that holds `BattlePassManager` must reference the module's client assembly; the Editor creates it auto-referenced, so the default game assembly already does.
|
|
543
|
+
|
|
544
|
+
### BattlePassModule.cs
|
|
545
|
+
|
|
546
|
+
```csharp
|
|
547
|
+
using System;
|
|
548
|
+
using System.Collections.Generic;
|
|
549
|
+
using System.Linq;
|
|
550
|
+
using System.Threading.Tasks;
|
|
551
|
+
using Newtonsoft.Json;
|
|
552
|
+
using Unity.Services.CloudCode.Apis;
|
|
553
|
+
using Unity.Services.CloudCode.Core;
|
|
554
|
+
using Unity.Services.CloudSave.Model;
|
|
555
|
+
|
|
556
|
+
namespace BattlePassModule.Cloud
|
|
557
|
+
{
|
|
558
|
+
public class BattlePassModule
|
|
559
|
+
{
|
|
560
|
+
const string ProgressKey = "battle_pass_progress";
|
|
561
|
+
|
|
562
|
+
static readonly Dictionary<string, int> MaxXpPerSource = new()
|
|
563
|
+
{
|
|
564
|
+
["match_complete"] = 200,
|
|
565
|
+
["daily_login"] = 50,
|
|
566
|
+
["quest_complete"] = 300
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Keep in sync with the Tiers in BattlePasses.rc; clients are never trusted for thresholds.
|
|
570
|
+
static readonly Dictionary<string, List<BattlePassTierThreshold>> PassTiers = new()
|
|
571
|
+
{
|
|
572
|
+
["season_03"] = new()
|
|
573
|
+
{
|
|
574
|
+
new BattlePassTierThreshold { TierNumber = 1, XpRequired = 0 },
|
|
575
|
+
new BattlePassTierThreshold { TierNumber = 2, XpRequired = 500 },
|
|
576
|
+
new BattlePassTierThreshold { TierNumber = 3, XpRequired = 1200 }
|
|
577
|
+
},
|
|
578
|
+
["easter_2026"] = new()
|
|
579
|
+
{
|
|
580
|
+
new BattlePassTierThreshold { TierNumber = 1, XpRequired = 0 },
|
|
581
|
+
new BattlePassTierThreshold { TierNumber = 2, XpRequired = 300 }
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// Keep in sync with StartDate/EndDate in BattlePasses.rc. UTC, End is exclusive.
|
|
586
|
+
static readonly Dictionary<string, (DateTime Start, DateTime End)> PassWindows = new()
|
|
587
|
+
{
|
|
588
|
+
["season_03"] = (new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
|
589
|
+
new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc)),
|
|
590
|
+
["easter_2026"] = (new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc),
|
|
591
|
+
new DateTime(2026, 4, 22, 0, 0, 0, DateTimeKind.Utc))
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// ── AwardXp ──────────────────────────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
[CloudCodeFunction("AwardXp")]
|
|
597
|
+
public async Task<BattlePassProgress> AwardXp(
|
|
598
|
+
[CloudCodeService] IExecutionContext context, [CloudCodeService] IGameApiClient client,
|
|
599
|
+
string passId, int amount, string source)
|
|
600
|
+
{
|
|
601
|
+
if (!MaxXpPerSource.TryGetValue(source, out var maxXp))
|
|
602
|
+
throw new Exception(
|
|
603
|
+
$"Invalid XP source: '{source}'. Allowed: {string.Join(", ", MaxXpPerSource.Keys)}");
|
|
604
|
+
|
|
605
|
+
if (amount <= 0 || amount > maxXp)
|
|
606
|
+
throw new Exception($"XP amount must be between 1 and {maxXp} for source '{source}'.");
|
|
607
|
+
|
|
608
|
+
if (!PassTiers.TryGetValue(passId, out var tiers))
|
|
609
|
+
throw new Exception($"Unknown pass '{passId}'.");
|
|
610
|
+
|
|
611
|
+
RequireActive(passId);
|
|
612
|
+
|
|
613
|
+
var allProgress = await LoadProgressAsync(context, client);
|
|
614
|
+
|
|
615
|
+
if (!allProgress.TryGetValue(passId, out var progress))
|
|
616
|
+
progress = new BattlePassProgress { CurrentTier = 1, ClaimedRewards = new List<string>() };
|
|
617
|
+
|
|
618
|
+
progress.CurrentXp += amount;
|
|
619
|
+
|
|
620
|
+
progress.CurrentTier = CalculateTier(progress.CurrentXp, tiers);
|
|
621
|
+
|
|
622
|
+
allProgress[passId] = progress;
|
|
623
|
+
await SaveProgressAsync(context, client, allProgress);
|
|
624
|
+
|
|
625
|
+
return progress;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── ClaimReward ──────────────────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
[CloudCodeFunction("ClaimReward")]
|
|
631
|
+
public async Task<BattlePassProgress> ClaimReward(
|
|
632
|
+
[CloudCodeService] IExecutionContext context, [CloudCodeService] IGameApiClient client,
|
|
633
|
+
string passId, int tierNumber, string rewardType)
|
|
634
|
+
{
|
|
635
|
+
if (rewardType != "free" && rewardType != "premium")
|
|
636
|
+
throw new Exception(
|
|
637
|
+
$"Invalid rewardType: '{rewardType}'. Must be 'free' or 'premium'.");
|
|
638
|
+
|
|
639
|
+
RequireActive(passId);
|
|
640
|
+
|
|
641
|
+
var allProgress = await LoadProgressAsync(context, client);
|
|
642
|
+
|
|
643
|
+
if (!allProgress.TryGetValue(passId, out var progress))
|
|
644
|
+
throw new Exception($"No progress found for pass '{passId}'. Award XP first.");
|
|
645
|
+
|
|
646
|
+
if (!PassTiers.TryGetValue(passId, out var tiers))
|
|
647
|
+
throw new Exception($"Unknown pass '{passId}'.");
|
|
648
|
+
|
|
649
|
+
var tier = tiers.FirstOrDefault(t => t.TierNumber == tierNumber)
|
|
650
|
+
?? throw new Exception($"Tier {tierNumber} does not exist in pass '{passId}'.");
|
|
651
|
+
|
|
652
|
+
if (progress.CurrentXp < tier.XpRequired)
|
|
653
|
+
throw new Exception(
|
|
654
|
+
$"Tier {tierNumber} not yet reached ({progress.CurrentXp}/{tier.XpRequired} XP).");
|
|
655
|
+
|
|
656
|
+
if (rewardType == "premium" && !progress.HasPremium)
|
|
657
|
+
throw new Exception("Premium pass not owned. Purchase premium first.");
|
|
658
|
+
|
|
659
|
+
var rewardKey = $"tier_{tierNumber}_{rewardType}";
|
|
660
|
+
if (progress.ClaimedRewards.Contains(rewardKey))
|
|
661
|
+
throw new Exception($"Reward '{rewardKey}' already claimed.");
|
|
662
|
+
|
|
663
|
+
progress.ClaimedRewards.Add(rewardKey);
|
|
664
|
+
allProgress[passId] = progress;
|
|
665
|
+
await SaveProgressAsync(context, client, allProgress);
|
|
666
|
+
|
|
667
|
+
return progress;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ── PurchasePremium ──────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
[CloudCodeFunction("PurchasePremium")]
|
|
673
|
+
public async Task<BattlePassProgress> PurchasePremium(
|
|
674
|
+
[CloudCodeService] IExecutionContext context, [CloudCodeService] IGameApiClient client, string passId)
|
|
675
|
+
{
|
|
676
|
+
var allProgress = await LoadProgressAsync(context, client);
|
|
677
|
+
|
|
678
|
+
if (!allProgress.TryGetValue(passId, out var progress))
|
|
679
|
+
progress = new BattlePassProgress { CurrentTier = 1, ClaimedRewards = new List<string>() };
|
|
680
|
+
|
|
681
|
+
if (progress.HasPremium)
|
|
682
|
+
return progress; // idempotent -- already owns premium
|
|
683
|
+
|
|
684
|
+
// Replace this throw with server-side purchase validation, then set HasPremium.
|
|
685
|
+
throw new Exception("PurchasePremium requires server-side purchase validation.");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
static void RequireActive(string passId)
|
|
691
|
+
{
|
|
692
|
+
if (!PassWindows.TryGetValue(passId, out var window))
|
|
693
|
+
throw new Exception($"Unknown pass '{passId}'.");
|
|
694
|
+
|
|
695
|
+
var now = DateTime.UtcNow;
|
|
696
|
+
if (now < window.Start || now >= window.End)
|
|
697
|
+
throw new Exception($"Pass '{passId}' is not active.");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
static int CalculateTier(int currentXp, List<BattlePassTierThreshold> tiers)
|
|
701
|
+
{
|
|
702
|
+
var reached = tiers
|
|
703
|
+
.Where(t => currentXp >= t.XpRequired)
|
|
704
|
+
.OrderByDescending(t => t.TierNumber)
|
|
705
|
+
.FirstOrDefault();
|
|
706
|
+
|
|
707
|
+
return reached?.TierNumber ?? 1;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async Task<Dictionary<string, BattlePassProgress>> LoadProgressAsync(
|
|
711
|
+
IExecutionContext context, IGameApiClient client)
|
|
712
|
+
{
|
|
713
|
+
var result = await client.CloudSaveData.GetProtectedItemsAsync(
|
|
714
|
+
context, context.ServiceToken, context.ProjectId,
|
|
715
|
+
context.PlayerId!, new List<string> { ProgressKey });
|
|
716
|
+
|
|
717
|
+
var value = result.Data?.Results?.FirstOrDefault()?.Value?.ToString();
|
|
718
|
+
if (string.IsNullOrEmpty(value))
|
|
719
|
+
return new Dictionary<string, BattlePassProgress>();
|
|
720
|
+
|
|
721
|
+
return JsonConvert.DeserializeObject<Dictionary<string, BattlePassProgress>>(value)
|
|
722
|
+
?? new Dictionary<string, BattlePassProgress>();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async Task SaveProgressAsync(
|
|
726
|
+
IExecutionContext context, IGameApiClient client,
|
|
727
|
+
Dictionary<string, BattlePassProgress> progress)
|
|
728
|
+
{
|
|
729
|
+
var json = JsonConvert.SerializeObject(progress);
|
|
730
|
+
var body = new SetItemBody(ProgressKey, json);
|
|
731
|
+
await client.CloudSaveData.SetProtectedItemAsync(
|
|
732
|
+
context, context.ServiceToken, context.ProjectId, context.PlayerId!, body);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Server-Side Models (inside BattlePassModule.cs)
|
|
739
|
+
|
|
740
|
+
The single definition of both types, in the module's `Cloud` namespace. Use public members with a public getter and setter, as the auto-properties below: the generator copies only those into the client stub of `BattlePassProgress`, as `{ get; set; }` properties, and `Newtonsoft.Json` serializes them for Cloud Save. Do not declare a second `BattlePassProgress` in client code.
|
|
741
|
+
|
|
742
|
+
```csharp
|
|
743
|
+
namespace BattlePassModule.Cloud
|
|
744
|
+
{
|
|
745
|
+
public class BattlePassProgress
|
|
746
|
+
{
|
|
747
|
+
public int CurrentXp { get; set; }
|
|
748
|
+
public int CurrentTier { get; set; }
|
|
749
|
+
public bool HasPremium { get; set; }
|
|
750
|
+
public List<string> ClaimedRewards { get; set; } = new();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
public class BattlePassTierThreshold
|
|
754
|
+
{
|
|
755
|
+
public int TierNumber { get; set; }
|
|
756
|
+
public int XpRequired { get; set; }
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## Cloud Resources
|
|
764
|
+
|
|
765
|
+
### Remote Config `.rc` File
|
|
766
|
+
|
|
767
|
+
Place at `Assets/BattlePasses.rc`. Contains pass definitions as a JSON array under the `"battle_passes"` key. Example with two concurrent passes:
|
|
768
|
+
|
|
769
|
+
```json
|
|
770
|
+
{
|
|
771
|
+
"$schema": "https://ugs-config-schemas.unity3d.com/v1/remote-config.schema.json",
|
|
772
|
+
"entries": {
|
|
773
|
+
"battle_passes": [
|
|
774
|
+
{
|
|
775
|
+
"PassId": "season_03",
|
|
776
|
+
"Name": "Season 3",
|
|
777
|
+
"StartDate": "2024-01-01T00:00:00Z",
|
|
778
|
+
"EndDate": "2026-06-30T23:59:59Z",
|
|
779
|
+
"Tiers": [
|
|
780
|
+
{ "TierNumber": 1, "XpRequired": 0, "FreeRewardId": "item_bronze_badge", "PremiumRewardId": "item_gold_frame" },
|
|
781
|
+
{ "TierNumber": 2, "XpRequired": 500, "FreeRewardId": "item_xp_boost", "PremiumRewardId": "item_rare_skin_fragment" },
|
|
782
|
+
{ "TierNumber": 3, "XpRequired": 1200, "FreeRewardId": "item_silver_badge", "PremiumRewardId": "item_legendary_skin" }
|
|
783
|
+
]
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
"PassId": "easter_2026",
|
|
787
|
+
"Name": "Easter Event 2026",
|
|
788
|
+
"StartDate": "2026-04-01T00:00:00Z",
|
|
789
|
+
"EndDate": "2026-04-21T23:59:59Z",
|
|
790
|
+
"Tiers": [
|
|
791
|
+
{ "TierNumber": 1, "XpRequired": 0, "FreeRewardId": "item_egg_basket", "PremiumRewardId": "item_golden_egg" },
|
|
792
|
+
{ "TierNumber": 2, "XpRequired": 300, "FreeRewardId": "item_bunny_ears", "PremiumRewardId": "item_bunny_mount" }
|
|
793
|
+
]
|
|
794
|
+
}
|
|
795
|
+
]
|
|
796
|
+
},
|
|
797
|
+
"types": {
|
|
798
|
+
"battle_passes": "JSON"
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
**Rules:**
|
|
804
|
+
|
|
805
|
+
- `PassId` must be unique across all passes and stable over time — it is the key used in player
|
|
806
|
+
progress records. Never rename a `PassId` after launch.
|
|
807
|
+
- `XpRequired` is **cumulative** XP to reach the tier, not a delta from the previous tier.
|
|
808
|
+
Tier 1 must always have `XpRequired: 0` so it is immediately reachable.
|
|
809
|
+
- `FreeRewardId` / `PremiumRewardId` are arbitrary strings. The client reads these IDs and maps
|
|
810
|
+
them to display names/icons locally. `ClaimReward` records the claim in `ClaimedRewards`; the game grants what
|
|
811
|
+
the ID stands for — unlock it client-side from the claimed set, or have the module grant it through
|
|
812
|
+
Cloud Save Protected or another Cloud Code call alongside the claim.
|
|
813
|
+
- Treat a claim as an entitlement only if the XP behind it is trustworthy. The sample `AwardXp`
|
|
814
|
+
accepts any `amount` up to the `MaxXpPerSource` cap from a signed-in client, so before
|
|
815
|
+
granting anything valuable, derive XP from server-verified events (or make `AwardXp`
|
|
816
|
+
non-client-callable and drive it from server-validated events).
|
|
817
|
+
- Make the grant idempotent and retryable from the saved claim: if the grant fails after the claim
|
|
818
|
+
is saved, a retry of `ClaimReward` fails its duplicate-claim check, so grant from the stored claim
|
|
819
|
+
(or persist claim and grant in one recoverable operation) rather than only on the first call.
|
|
820
|
+
- `ClaimedRewards` stores keys such as `tier_1_free`, not reward IDs. Keep reward IDs immutable
|
|
821
|
+
for the life of a pass, or store the granted reward ID with the claim, so a changed ID cannot
|
|
822
|
+
unlock a different reward for an old claim.
|
|
823
|
+
- Date filtering happens **client-side** after fetch. Add passes to the file before their
|
|
824
|
+
`StartDate` — they will be ignored by the client until the start time is reached.
|
|
825
|
+
|
|
826
|
+
### Cloud Code Module
|
|
827
|
+
|
|
828
|
+
> Create the module files listed in [Cloud Code Module Implementation](#cloud-code-module-implementation) above.
|
|
829
|
+
> See the Creating a Cloud Code Module section of `references/cloud-code-modules.md`
|
|
830
|
+
> for the generic templates; replace `MyModule` with `BattlePassModule`.
|
|
831
|
+
>
|
|
832
|
+
> The `.ccmu` is deployed by the Deployment Window like any other cloud asset, but its code is compiled by
|
|
833
|
+
> Unity — a project that does not compile has nothing to deploy.
|
|
834
|
+
|
|
835
|
+
### Access Control
|
|
836
|
+
|
|
837
|
+
**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.
|
|
838
|
+
|
|
839
|
+
> **Contrast with achievements:** The achievements skill offered a direct-write dev path using
|
|
840
|
+
> the Public bucket, which required an explicit Access Control policy to lock down in production.
|
|
841
|
+
> Battle pass has no direct-write path, so there is nothing to lock down.
|
|
842
|
+
|
|
843
|
+
### `manifest.json` Entries
|
|
844
|
+
|
|
845
|
+
Ensure these packages are listed in `Packages/manifest.json`:
|
|
846
|
+
|
|
847
|
+
```json
|
|
848
|
+
{
|
|
849
|
+
"dependencies": {
|
|
850
|
+
"com.unity.services.authentication": "3.6.1",
|
|
851
|
+
"com.unity.services.cloudcode": "3.0.0",
|
|
852
|
+
"com.unity.services.cloudsave": "3.4.0",
|
|
853
|
+
"com.unity.services.core": "1.16.0",
|
|
854
|
+
"com.unity.services.deployment": "1.7.2",
|
|
855
|
+
"com.unity.remote-config": "4.2.5",
|
|
856
|
+
"com.unity.services.tooling": "1.4.1",
|
|
857
|
+
"com.unity.nuget.newtonsoft-json": "3.2.2"
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
> `com.unity.services.cloudcode` **`3.0.0`** is the native-authoring line, which the `.ccmu` module
|
|
863
|
+
> below requires — see the Requirements section of `references/cloud-code-modules.md`. The `2.10.x` line
|
|
864
|
+
> deploys legacy `.ccmr` modules only; use it when maintaining one, not for new work.
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
`com.unity.nuget.newtonsoft-json` is required for `JsonConvert` used in serialization of tier data and progress.
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
## Deployment Checklist
|
|
872
|
+
|
|
873
|
+
- [ ] All packages added to `Packages/manifest.json`
|
|
874
|
+
- [ ] Remote Config `.rc` file created with `"battle_passes"` entry and at least one active pass
|
|
875
|
+
- [ ] Cloud Code module authored as a `.ccmu`: module asset, `Cloud/` + `Client/` asmdefs, endpoint class, `CloudSetup.cs` (see file list in [Cloud Code Module Implementation](#cloud-code-module-implementation))
|
|
876
|
+
- [ ] `BattlePassModule.cs` added to the module project with `AwardXp`, `ClaimReward`, `PurchasePremium` functions
|
|
877
|
+
- [ ] Environment selected in **Services > Deployment** settings
|
|
878
|
+
- [ ] Deploy all via the Deployment Window: select `BattlePasses.rc` and `BattlePassModule.ccmu`, click **Deploy**
|
|
879
|
+
- [ ] Project linked in **Edit > Project Settings > Services**
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Validation
|
|
884
|
+
|
|
885
|
+
After implementing the battle pass feature, verify:
|
|
886
|
+
|
|
887
|
+
1. **Project compiles** without errors — the module's Editor-only cloud assembly is compiled with the rest of the Unity project.
|
|
888
|
+
2. **Initialization order** is correct: `UnityServices.InitializeAsync()` then `AuthenticationService.Instance.SignInAnonymouslyAsync()` then service calls.
|
|
889
|
+
3. **Protected bucket reads** use `ProtectedReadAccessClassOptions` -- using the wrong access class returns no data.
|
|
890
|
+
4. **Client bindings resolve**: the game assembly references the module's client assembly and calls `BattlePassModuleClient`. The deployed module name is the **cloud asmdef name**, `BattlePassModule` (the `name` field of `Cloud/BattlePassModule.asmdef`), not the `.ccmu` filename.
|
|
891
|
+
5. **One progress definition**: `BattlePassProgress` is declared only in the Cloud Code module, as `{ get; set; }` properties, and the client uses its generated stub.
|
|
892
|
+
6. **No direct client writes** to `"battle_pass_progress"` -- all mutations go through Cloud Code.
|
|
893
|
+
7. **No client-supplied tier data**: `AwardXp` and `ClaimReward` use the module's server-side `PassTiers`, which matches the `XpRequired` values in `BattlePasses.rc`, and reject passes outside the server-side `PassWindows` (matching `StartDate`/`EndDate`).
|
|
894
|
+
8. **Date filtering** uses `DateTime.UtcNow` with `DateTimeStyles.RoundtripKind` for consistent UTC parsing.
|
|
895
|
+
9. **Cloud resources** are present and deployable: `BattlePasses.rc` and `BattlePassModule.ccmu` appear in the Deployment Window.
|
|
896
|
+
10. **PurchasePremium is an integration stub** -- it returns existing progress when `HasPremium` is already true and otherwise throws until you add purchase validation.
|
|
897
|
+
11. **ClaimReward validates** the pass window (`PassWindows`), tier reach (against `PassTiers`) and premium ownership before recording the claim.
|
|
898
|
+
12. **Premium purchasing is not deployable until a validated purchase flow sets `HasPremium`** -- keep the fail-closed throw until then.
|
|
899
|
+
|
|
900
|
+
---
|
|
901
|
+
|
|
902
|
+
## Asset Store & Sample Projects
|
|
903
|
+
|
|
904
|
+
- **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.
|