summer-engine 3.1.0 → 3.2.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +27 -5
- package/.codex-plugin/plugin.json +28 -6
- package/.cursor-plugin/plugin.json +27 -5
- package/.factory-plugin/plugin.json +27 -5
- package/.opencode/plugins/summer.js +1 -1
- package/CHANGELOG.md +102 -0
- package/README.md +18 -3
- package/assets/audit/scene_audit.gd +4136 -0
- package/assets/seeing/seeing_probe.gd +1873 -0
- package/dist/cli/commands/skills.js +20 -2
- package/dist/core/api-client.d.ts +9 -1
- package/dist/core/api-client.js +21 -4
- package/dist/core/capabilities/agent-playbook.js +46 -0
- package/dist/core/capabilities/audit/accept.d.ts +55 -0
- package/dist/core/capabilities/audit/accept.js +147 -0
- package/dist/core/capabilities/audit/args.d.ts +95 -0
- package/dist/core/capabilities/audit/args.js +89 -0
- package/dist/core/capabilities/audit/audit.d.ts +56 -0
- package/dist/core/capabilities/audit/audit.js +287 -0
- package/dist/core/capabilities/audit/frame.d.ts +14 -0
- package/dist/core/capabilities/audit/frame.js +35 -0
- package/dist/core/capabilities/audit/gaps.d.ts +96 -0
- package/dist/core/capabilities/audit/gaps.js +257 -0
- package/dist/core/capabilities/audit/judge.d.ts +162 -0
- package/dist/core/capabilities/audit/judge.js +1134 -0
- package/dist/core/capabilities/audit/math.d.ts +110 -0
- package/dist/core/capabilities/audit/math.js +263 -0
- package/dist/core/capabilities/audit/summary.d.ts +74 -0
- package/dist/core/capabilities/audit/summary.js +212 -0
- package/dist/core/capabilities/board.d.ts +31 -0
- package/dist/core/capabilities/board.js +43 -0
- package/dist/core/capabilities/camera-view.d.ts +4 -2
- package/dist/core/capabilities/camera-view.js +6 -3
- package/dist/core/capabilities/connect-signal.d.ts +85 -0
- package/dist/core/capabilities/connect-signal.js +285 -0
- package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
- package/dist/core/capabilities/engine-fallbacks.js +5 -0
- package/dist/core/capabilities/engine-ops.d.ts +5 -1
- package/dist/core/capabilities/engine-ops.js +0 -3
- package/dist/core/capabilities/engine-receipt.d.ts +12 -0
- package/dist/core/capabilities/engine-receipt.js +26 -0
- package/dist/core/capabilities/file-read.d.ts +64 -0
- package/dist/core/capabilities/file-read.js +325 -0
- package/dist/core/capabilities/game-task-plan.js +4 -2
- package/dist/core/capabilities/grep.d.ts +60 -0
- package/dist/core/capabilities/grep.js +190 -0
- package/dist/core/capabilities/inspect-node.d.ts +56 -0
- package/dist/core/capabilities/inspect-node.js +225 -0
- package/dist/core/capabilities/inspect-resource.d.ts +61 -0
- package/dist/core/capabilities/inspect-resource.js +376 -0
- package/dist/core/capabilities/math3d.d.ts +28 -0
- package/dist/core/capabilities/math3d.js +88 -0
- package/dist/core/capabilities/motion-generation.d.ts +62 -0
- package/dist/core/capabilities/motion-generation.js +138 -0
- package/dist/core/capabilities/placement-batch.d.ts +102 -0
- package/dist/core/capabilities/placement-batch.js +483 -0
- package/dist/core/capabilities/placement-math.d.ts +70 -0
- package/dist/core/capabilities/placement-math.js +209 -0
- package/dist/core/capabilities/placement-script.d.ts +42 -0
- package/dist/core/capabilities/placement-script.js +1096 -0
- package/dist/core/capabilities/placement.d.ts +485 -0
- package/dist/core/capabilities/placement.js +1731 -0
- package/dist/core/capabilities/project-context.d.ts +33 -2
- package/dist/core/capabilities/project-context.js +87 -4
- package/dist/core/capabilities/replace-node.d.ts +91 -0
- package/dist/core/capabilities/replace-node.js +423 -0
- package/dist/core/capabilities/runtime-control.d.ts +18 -2
- package/dist/core/capabilities/runtime-control.js +67 -2
- package/dist/core/capabilities/scene-batch.d.ts +63 -0
- package/dist/core/capabilities/scene-batch.js +394 -0
- package/dist/core/capabilities/scene-readback.d.ts +36 -0
- package/dist/core/capabilities/scene-readback.js +54 -0
- package/dist/core/capabilities/seeing/args.d.ts +348 -0
- package/dist/core/capabilities/seeing/args.js +178 -0
- package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
- package/dist/core/capabilities/seeing/candidates.js +282 -0
- package/dist/core/capabilities/seeing/math.d.ts +134 -0
- package/dist/core/capabilities/seeing/math.js +269 -0
- package/dist/core/capabilities/seeing/probe.d.ts +62 -0
- package/dist/core/capabilities/seeing/probe.js +188 -0
- package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
- package/dist/core/capabilities/seeing/scoring.js +639 -0
- package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
- package/dist/core/capabilities/seeing/seeing.js +1341 -0
- package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
- package/dist/core/capabilities/seeing/shot-store.js +204 -0
- package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
- package/dist/core/capabilities/surface-snap-mesh.js +425 -0
- package/dist/core/capabilities/surface-snap.d.ts +77 -0
- package/dist/core/capabilities/surface-snap.js +461 -0
- package/dist/core/capabilities/tool-dispatch.js +231 -72
- package/dist/core/capabilities/tscn.d.ts +85 -0
- package/dist/core/capabilities/tscn.js +391 -0
- package/dist/core/capabilities/world-snapshot.d.ts +58 -0
- package/dist/core/capabilities/world-snapshot.js +150 -0
- package/dist/core/capability-skew.d.ts +5 -5
- package/dist/core/capability-skew.js +14 -6
- package/dist/core/headless/worker-engine-client.d.ts +1 -1
- package/dist/core/headless/worker-engine-client.js +2 -2
- package/dist/core/library-read.d.ts +37 -5
- package/dist/core/library-read.js +264 -33
- package/dist/core/library-search.d.ts +2 -2
- package/dist/installer/agent-config.js +5 -3
- package/dist/installer/agent-table.d.ts +16 -0
- package/dist/installer/agent-table.js +52 -17
- package/dist/mcp/server.js +9 -3
- package/dist/mcp/tools/asset-tools.js +21 -3
- package/dist/mcp/tools/audit-tools.d.ts +23 -0
- package/dist/mcp/tools/audit-tools.js +60 -0
- package/dist/mcp/tools/debug-tools.js +3 -1
- package/dist/mcp/tools/file-tools.js +17 -4
- package/dist/mcp/tools/generate-tools.js +74 -66
- package/dist/mcp/tools/library-tools.js +2 -0
- package/dist/mcp/tools/perception-tools.js +6 -19
- package/dist/mcp/tools/placement-tools.d.ts +24 -0
- package/dist/mcp/tools/placement-tools.js +91 -0
- package/dist/mcp/tools/project-tools.js +18 -15
- package/dist/mcp/tools/scene-tools.js +71 -83
- package/dist/mcp/tools/seeing-tools.d.ts +24 -0
- package/dist/mcp/tools/seeing-tools.js +64 -0
- package/dist/mcp/tools/spatial-tools.js +17 -9
- package/dist/mcp/tools/visual-tools.js +59 -7
- package/gemini-extension.json +26 -4
- package/integrations/amp/README.md +1 -1
- package/integrations/crush/README.md +1 -1
- package/integrations/grok-build/README.md +1 -1
- package/integrations/hermes/README.md +1 -1
- package/integrations/kimi-code/README.md +1 -1
- package/integrations/mistral-vibe/README.md +1 -1
- package/integrations/opencode/README.md +1 -1
- package/integrations/rovo-dev/README.md +1 -1
- package/integrations/vscode-copilot/README.md +1 -1
- package/integrations/warp/README.md +1 -1
- package/integrations/windsurf/README.md +1 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/references/summer-folder/resource.yaml +1 -1
- package/library/references/summer-folder/summer-folder.md +1 -1
- package/library/skills/3d-lighting/SKILL.md +1 -1
- package/library/skills/README.md +36 -14
- package/library/skills/adaptive-music/SKILL.md +1 -1
- package/library/skills/agent-playtesting/SKILL.md +1 -1
- package/library/skills/ai-characters-in-games/SKILL.md +220 -0
- package/library/skills/ai-characters-in-games/resource.yaml +30 -0
- package/library/skills/ambient-bed/SKILL.md +1 -1
- package/library/skills/animated-loop/SKILL.md +1 -1
- package/library/skills/animation-tree/SKILL.md +1 -1
- package/library/skills/art-direction/SKILL.md +1 -1
- package/library/skills/asset-strategy/SKILL.md +2 -1
- package/library/skills/asset-strategy/resource.yaml +2 -1
- package/library/skills/audio-direction/SKILL.md +1 -1
- package/library/skills/auto-fire-targeting/SKILL.md +1 -1
- package/library/skills/brainstorm-game/SKILL.md +1 -1
- package/library/skills/brainstorming/SKILL.md +1 -1
- package/library/skills/browse-templates/SKILL.md +3 -3
- package/library/skills/browse-templates/resource.yaml +1 -1
- package/library/skills/camera-rigs/SKILL.md +128 -0
- package/library/skills/camera-rigs/resource.yaml +29 -0
- package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
- package/library/skills/character-animation-wiring/SKILL.md +1 -1
- package/library/skills/character-model/SKILL.md +1 -1
- package/library/skills/character-movement/SKILL.md +326 -0
- package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
- package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
- package/library/skills/character-movement/references/movement-3d.md +136 -0
- package/library/skills/character-movement/references/platformer-feel.md +83 -0
- package/library/skills/character-movement/resource.yaml +40 -0
- package/library/skills/character-portrait/SKILL.md +1 -1
- package/library/skills/cinematic-cutscene/SKILL.md +1 -1
- package/library/skills/combat-basics/SKILL.md +159 -0
- package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
- package/library/skills/combat-basics/resource.yaml +34 -0
- package/library/skills/concept-art/SKILL.md +1 -1
- package/library/skills/create-asset-sheet/SKILL.md +1 -1
- package/library/skills/debug/SKILL.md +1 -1
- package/library/skills/debugging-game-feel/SKILL.md +1 -1
- package/library/skills/design-level/SKILL.md +1 -1
- package/library/skills/design-mechanic/SKILL.md +1 -1
- package/library/skills/design-npc/SKILL.md +1 -1
- package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
- package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
- package/library/skills/environment-kit/SKILL.md +1 -1
- package/library/skills/export-and-ship/SKILL.md +1 -1
- package/library/skills/fabricating-assets/SKILL.md +1 -1
- package/library/skills/facial-and-lipsync/SKILL.md +1 -1
- package/library/skills/fps-controller/SKILL.md +9 -7
- package/library/skills/fps-controller/resource.yaml +4 -1
- package/library/skills/game-feel/SKILL.md +1 -1
- package/library/skills/gameskill/SKILL.md +1 -1
- package/library/skills/gdscript-patterns/SKILL.md +1 -1
- package/library/skills/generate-motion/SKILL.md +9 -9
- package/library/skills/generate-motion/resource.yaml +2 -1
- package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
- package/library/skills/headless-scripting/SKILL.md +1 -1
- package/library/skills/in-game-purchases/SKILL.md +254 -0
- package/library/skills/in-game-purchases/resource.yaml +29 -0
- package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
- package/library/skills/investigating-bugs/SKILL.md +1 -1
- package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
- package/library/skills/make-game/SKILL.md +25 -3
- package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
- package/library/skills/make-game/references/2d-platformer.md +117 -0
- package/library/skills/make-game/references/2d-rpg-content.md +234 -0
- package/library/skills/make-game/references/2d-rpg.md +241 -0
- package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
- package/library/skills/make-game/references/2d-survivors.md +237 -0
- package/library/skills/make-game/references/3d-fps-systems.md +216 -0
- package/library/skills/make-game/references/3d-fps.md +177 -0
- package/library/skills/make-game/references/3d-horror-systems.md +186 -0
- package/library/skills/make-game/references/3d-horror.md +201 -0
- package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
- package/library/skills/make-game/references/3d-third-person.md +215 -0
- package/library/skills/make-game/resource.yaml +6 -1
- package/library/skills/multiplayer/SKILL.md +150 -0
- package/library/skills/multiplayer/resource.yaml +33 -0
- package/library/skills/multiplayer-movement/SKILL.md +510 -0
- package/library/skills/multiplayer-movement/files.md +501 -0
- package/library/skills/multiplayer-movement/resource.yaml +30 -0
- package/library/skills/multiplayer-project/SKILL.md +521 -0
- package/library/skills/multiplayer-project/files.md +448 -0
- package/library/skills/multiplayer-project/resource.yaml +36 -0
- package/library/skills/multiplayer-publish/SKILL.md +229 -0
- package/library/skills/multiplayer-publish/export-errors.md +89 -0
- package/library/skills/multiplayer-publish/resource.yaml +28 -0
- package/library/skills/multiplayer-state/SKILL.md +438 -0
- package/library/skills/multiplayer-state/files.md +592 -0
- package/library/skills/multiplayer-state/resource.yaml +35 -0
- package/library/skills/multiplayer-testing/SKILL.md +236 -0
- package/library/skills/multiplayer-testing/resource.yaml +39 -0
- package/library/skills/music-track/SKILL.md +1 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/new-project/SKILL.md +1 -1
- package/library/skills/organic-model/SKILL.md +1 -1
- package/library/skills/pixel-art/SKILL.md +1 -1
- package/library/skills/play/SKILL.md +1 -1
- package/library/skills/playtesting-a-feature/SKILL.md +1 -1
- package/library/skills/procedural-animation/SKILL.md +2 -2
- package/library/skills/procedural-animation/resource.yaml +1 -1
- package/library/skills/prop-model/SKILL.md +1 -1
- package/library/skills/psx-retro-rendering/SKILL.md +1 -1
- package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
- package/library/skills/remote-deploy/SKILL.md +1 -1
- package/library/skills/retarget/SKILL.md +1 -1
- package/library/skills/running-in-the-cloud/SKILL.md +1 -1
- package/library/skills/save-load/SKILL.md +156 -0
- package/library/skills/save-load/resource.yaml +27 -0
- package/library/skills/scene-composition/SKILL.md +1 -1
- package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
- package/library/skills/scene-scripting/SKILL.md +1 -1
- package/library/skills/scene-to-level/SKILL.md +1 -1
- package/library/skills/skill-create/SKILL.md +1 -1
- package/library/skills/skill-improve/SKILL.md +1 -1
- package/library/skills/skill-test/SKILL.md +1 -1
- package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
- package/library/skills/skybox-panorama/SKILL.md +1 -1
- package/library/skills/sound-effect/SKILL.md +1 -1
- package/library/skills/spatial-placement/SKILL.md +134 -3
- package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
- package/library/skills/spatial-placement/resource.yaml +16 -2
- package/library/skills/sprite-sheet/SKILL.md +1 -1
- package/library/skills/summer-analytics/SKILL.md +147 -0
- package/library/skills/summer-analytics/resource.yaml +28 -0
- package/library/skills/summer-friends/SKILL.md +226 -0
- package/library/skills/summer-friends/resource.yaml +28 -0
- package/library/skills/summer-leaderboards/SKILL.md +231 -0
- package/library/skills/summer-leaderboards/resource.yaml +30 -0
- package/library/skills/summer-match-results/SKILL.md +286 -0
- package/library/skills/summer-match-results/resource.yaml +32 -0
- package/library/skills/summer-matchmaking/SKILL.md +301 -0
- package/library/skills/summer-matchmaking/resource.yaml +32 -0
- package/library/skills/summer-parties/SKILL.md +253 -0
- package/library/skills/summer-parties/resource.yaml +30 -0
- package/library/skills/summer-player-data/SKILL.md +322 -0
- package/library/skills/summer-player-data/resource.yaml +30 -0
- package/library/skills/summer-store/SKILL.md +271 -0
- package/library/skills/summer-store/resource.yaml +29 -0
- package/library/skills/summer-world-chat/SKILL.md +195 -0
- package/library/skills/summer-world-chat/resource.yaml +30 -0
- package/library/skills/summer-world-saves/SKILL.md +284 -0
- package/library/skills/summer-world-saves/resource.yaml +29 -0
- package/library/skills/text-to-motion/SKILL.md +99 -0
- package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
- package/library/skills/text-to-motion/resource.yaml +41 -0
- package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
- package/library/skills/tileable-texture/SKILL.md +1 -1
- package/library/skills/trailer-shot/SKILL.md +1 -1
- package/library/skills/tune-performance/SKILL.md +1 -1
- package/library/skills/ui-basics/SKILL.md +10 -1
- package/library/skills/ui-graphics/SKILL.md +1 -1
- package/library/skills/use-widget-asset/SKILL.md +1 -1
- package/library/skills/using-summer/SKILL.md +2 -2
- package/library/skills/using-summer/resource.yaml +2 -2
- package/library/skills/vehicle-model/SKILL.md +1 -1
- package/library/skills/verification-before-completion/SKILL.md +1 -1
- package/library/skills/verifying-scenes/SKILL.md +76 -2
- package/library/skills/verifying-scenes/resource.yaml +13 -2
- package/library/skills/vfx-dissolve/SKILL.md +1 -1
- package/library/skills/vfx-fire/SKILL.md +1 -1
- package/library/skills/vfx-hit-spark/SKILL.md +1 -1
- package/library/skills/vfx-lightning/SKILL.md +1 -1
- package/library/skills/vfx-magic-glow/SKILL.md +1 -1
- package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
- package/library/skills/vfx-smoke/SKILL.md +1 -1
- package/library/skills/vfx-water-ripple/SKILL.md +1 -1
- package/library/skills/voice-line/SKILL.md +1 -1
- package/library/skills/web-and-touch-input/SKILL.md +38 -0
- package/library/skills/web-and-touch-input/resource.yaml +28 -0
- package/library/skills/web-compatibility-look/SKILL.md +62 -0
- package/library/skills/web-compatibility-look/resource.yaml +30 -0
- package/library/skills/world-building-3d/SKILL.md +8 -1
- package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
- package/library/skills/world-building-3d/resource.yaml +2 -1
- package/library/skills/writing-plans/SKILL.md +1 -1
- package/library/skills/writing-skills/SKILL.md +1 -1
- package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
- package/library/tools/attach-to-surface/resource.yaml +142 -0
- package/library/tools/batch/resource.yaml +9 -3
- package/library/tools/connect-ports/resource.yaml +120 -0
- package/library/tools/connect-signal/resource.yaml +3 -2
- package/library/tools/debug-views/resource.yaml +94 -0
- package/library/tools/frame-nodes/resource.yaml +104 -0
- package/library/tools/frame-shot/resource.yaml +134 -0
- package/library/tools/generate-motion/resource.yaml +44 -9
- package/library/tools/get-board/resource.yaml +50 -0
- package/library/tools/get-project-context/resource.yaml +13 -2
- package/library/tools/grep/resource.yaml +77 -0
- package/library/tools/inspect-asset/resource.yaml +71 -0
- package/library/tools/inspect-node/resource.yaml +9 -2
- package/library/tools/inspect-resource/resource.yaml +16 -5
- package/library/tools/instantiate-scene/resource.yaml +27 -2
- package/library/tools/measure/resource.yaml +96 -0
- package/library/tools/place-adjacent/resource.yaml +113 -0
- package/library/tools/play/resource.yaml +15 -2
- package/library/tools/raycast/resource.yaml +101 -0
- package/library/tools/read-file/resource.yaml +36 -2
- package/library/tools/read-library/resource.yaml +2 -2
- package/library/tools/repeat-along/resource.yaml +129 -0
- package/library/tools/replace-node/resource.yaml +11 -5
- package/library/tools/scene-audit/resource.yaml +144 -0
- package/library/tools/screenshot/resource.yaml +13 -2
- package/library/tools/search-assets/resource.yaml +18 -0
- package/library/tools/shot-sheet/resource.yaml +110 -0
- package/library/tools/snap-to-surface/resource.yaml +4 -1
- package/library/tools/world-snapshot/resource.yaml +24 -3
- package/library/tools/zoom/resource.yaml +105 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +17 -3
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +26 -4
- package/registry/generated/index.json +2154 -469
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +27 -5
- package/registry/generated/plugin.codex.json +28 -6
- package/registry/generated/plugin.cursor.json +27 -5
- package/registry/generated/plugin.factory.json +27 -5
- package/registry/generated/skills-index.md +36 -14
- package/registry/generated/skills-registry.json +421 -124
- package/registry/generated/templates-registry.json +2 -2
- package/library/skills/host-authoritative-state/SKILL.md +0 -332
- package/library/skills/host-authoritative-state/resource.yaml +0 -28
- package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
- package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
- package/library/skills/setup-multiplayer/SKILL.md +0 -336
- package/library/skills/setup-multiplayer/resource.yaml +0 -28
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: summer-parties
|
|
3
|
+
description: "Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: multiplayer-and-networking
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Edit Write summer_get_script_errors summer_project_setting summer_play summer_stop summer_get_diagnostics
|
|
9
|
+
paths: ["**/*.gd", "summer.build.json"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /summer-parties — play together with a Summer party
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
A Summer party belongs to the player's Summer account, not to your game. The
|
|
17
|
+
Summer app creates parties, sends invites, kicks, transfers leadership and runs
|
|
18
|
+
party chat. Your game sees a read-only view through `Summer.client.party`:
|
|
19
|
+
|
|
20
|
+
- who is in the party, who leads, and which members have **this** game open;
|
|
21
|
+
- small text data your game shares with the party;
|
|
22
|
+
- whether the leader started a search in one of your queues.
|
|
23
|
+
|
|
24
|
+
To bring the party into a match, the **leader** joins
|
|
25
|
+
`SummerJoinTarget.party_queue(queue)`. Every other member joins the **same
|
|
26
|
+
target** when `search_started` fires. Summer puts them all in one match, each
|
|
27
|
+
with their own Session. The match itself is ordinary: the authority sees
|
|
28
|
+
normal `join` Commands and Sessions.
|
|
29
|
+
|
|
30
|
+
**Hosted only.** `is_available()` is `false` and every call fails with
|
|
31
|
+
`capability_unavailable`:
|
|
32
|
+
|
|
33
|
+
- in Local Play and editor runs;
|
|
34
|
+
- for guests;
|
|
35
|
+
- for accounts the platform keeps out of social features (`age_restricted`, under 13);
|
|
36
|
+
- in Summer apps without the `player.party@1` capability.
|
|
37
|
+
|
|
38
|
+
Outside a party, or when parties are unavailable, getters are empty and
|
|
39
|
+
`is_in_party()` is `false`. So the same Play button works everywhere: no party
|
|
40
|
+
means an ordinary `SummerJoinTarget.queue(...)` join. Build that path first.
|
|
41
|
+
|
|
42
|
+
This skill extends the Courtyard game from `skill/multiplayer-project`
|
|
43
|
+
(`network/game.gd` as `Game`, `client/main.gd` with `command()`).
|
|
44
|
+
|
|
45
|
+
## Steps
|
|
46
|
+
|
|
47
|
+
### 1. Join from a Play button, not from `_ready`
|
|
48
|
+
|
|
49
|
+
A party member must not join on their own the moment the game starts. Move the
|
|
50
|
+
join out of `client/main.gd`'s `_ready` into `enter(target)`, and keep every
|
|
51
|
+
signal connection from `skill/multiplayer-movement` and `skill/multiplayer-state`:
|
|
52
|
+
|
|
53
|
+
```gdscript
|
|
54
|
+
func _ready() -> void:
|
|
55
|
+
spawner = Game.build_network(self, PlayerArchetype)
|
|
56
|
+
# Connect before joining, or the first values arrive unseen.
|
|
57
|
+
spawner.state_group_created.connect(_on_group_created)
|
|
58
|
+
spawner.entity_spawned.connect(_on_entity_spawned)
|
|
59
|
+
spawner.event_received.connect(_on_event)
|
|
60
|
+
var initialized: SummerResult = await Summer.initialize(Game.GAME_ID).get_result_or_completed_signal()
|
|
61
|
+
if not initialized.ok:
|
|
62
|
+
push_error(initialized.message)
|
|
63
|
+
return
|
|
64
|
+
$Party.start()
|
|
65
|
+
# With parties, the Play button calls $Party.play(). Without them (Local
|
|
66
|
+
# Play, guests, apps without parties) the player plays solo right away.
|
|
67
|
+
if not Summer.client.party.is_available():
|
|
68
|
+
await enter(SummerJoinTarget.queue(Game.QUEUE))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Joins a match through `target`, then enters it. True once the player is in.
|
|
72
|
+
func enter(target: SummerJoinTarget) -> bool:
|
|
73
|
+
var join := Summer.client.join(target)
|
|
74
|
+
# Hosted matchmaking may ask the player to accept a match.
|
|
75
|
+
join.acceptance_required.connect(func(proposal: SummerMatchmakingProposal) -> void: proposal.accept())
|
|
76
|
+
var joined: SummerResult = await join.get_result_or_completed_signal()
|
|
77
|
+
if not joined.ok:
|
|
78
|
+
push_warning("join failed: %s %s" % [joined.code, joined.message])
|
|
79
|
+
return false
|
|
80
|
+
if not spawner.is_network_ready():
|
|
81
|
+
await spawner.network_ready
|
|
82
|
+
var entered := await command({"c": "join"})
|
|
83
|
+
if not entered.ok:
|
|
84
|
+
push_error("could not enter the match: %s" % entered.error)
|
|
85
|
+
return false
|
|
86
|
+
print("COURTYARD_CLIENT_JOINED")
|
|
87
|
+
return true
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Move the Local Play bot hook from `skill/multiplayer-testing` into `enter()`,
|
|
91
|
+
after the joined line.
|
|
92
|
+
|
|
93
|
+
### 2. Add the party node
|
|
94
|
+
|
|
95
|
+
`client/party.gd`, added as a child `Party` of `Main` in `client/main.tscn`.
|
|
96
|
+
It is client-only: authority scenes never touch `Summer.client`.
|
|
97
|
+
|
|
98
|
+
```gdscript
|
|
99
|
+
extends Node
|
|
100
|
+
## Plays with the player's Summer party. The leader presses Play; every member
|
|
101
|
+
## follows into the same match. Without a party, Play joins the queue alone.
|
|
102
|
+
|
|
103
|
+
const Game = preload("res://network/game.gd")
|
|
104
|
+
|
|
105
|
+
signal roster_changed(lines: PackedStringArray)
|
|
106
|
+
signal status(text: String)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
## Main calls this right after Summer.initialize succeeds: a child's _ready
|
|
110
|
+
## runs before that, while Summer.client is still null.
|
|
111
|
+
func start() -> void:
|
|
112
|
+
var party := Summer.client.party
|
|
113
|
+
party.search_started.connect(_on_search_started)
|
|
114
|
+
party.changed.connect(_render)
|
|
115
|
+
_render()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## The Play button.
|
|
119
|
+
func play() -> void:
|
|
120
|
+
var party := Summer.client.party
|
|
121
|
+
if not party.is_in_party():
|
|
122
|
+
await get_parent().enter(SummerJoinTarget.queue(Game.QUEUE))
|
|
123
|
+
elif party.is_leader():
|
|
124
|
+
await get_parent().enter(SummerJoinTarget.party_queue(Game.QUEUE))
|
|
125
|
+
else:
|
|
126
|
+
status.emit("Waiting for %s to press Play." % party.get_leader().display_name)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## The leader started a search: members follow with the same target.
|
|
130
|
+
func _on_search_started(queue: StringName) -> void:
|
|
131
|
+
if not Summer.client.party.is_leader():
|
|
132
|
+
await get_parent().enter(SummerJoinTarget.party_queue(queue))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
func _render() -> void:
|
|
136
|
+
var lines := PackedStringArray()
|
|
137
|
+
for member in Summer.client.party.get_members():
|
|
138
|
+
var tags := []
|
|
139
|
+
if member.leader:
|
|
140
|
+
tags.append("leader")
|
|
141
|
+
if not member.in_game:
|
|
142
|
+
tags.append("not in game")
|
|
143
|
+
if member.get_data("ready") == "yes":
|
|
144
|
+
tags.append("ready")
|
|
145
|
+
lines.append("%s %s" % [member.display_name, ", ".join(tags)])
|
|
146
|
+
roster_changed.emit(lines)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## The Invite button: show it only when the Summer app offers parties.
|
|
150
|
+
func invite() -> void:
|
|
151
|
+
var shown: SummerResult = await Summer.client.party.show_invite_dialog().get_result_or_completed_signal()
|
|
152
|
+
if not shown.ok:
|
|
153
|
+
status.emit("Invites are not available right now.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
## A member's Ready toggle, visible to the whole party.
|
|
157
|
+
func set_ready(ready: bool) -> void:
|
|
158
|
+
var saved: SummerResult = await Summer.client.party.set_member_data("ready", "yes" if ready else "").get_result_or_completed_signal()
|
|
159
|
+
if not saved.ok:
|
|
160
|
+
status.emit("Could not update ready: %s" % saved.code)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Connect `roster_changed` to a `Label` or `ItemList`, and `status` to a status
|
|
164
|
+
line. Show the buttons by state:
|
|
165
|
+
|
|
166
|
+
| Button | Show when |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| Play | not in a party, or leader. A member sees "waiting for the leader" instead |
|
|
169
|
+
| Invite friends | `Summer.client.party.is_available()` |
|
|
170
|
+
| Party | `is_available()` and `is_in_party()`: `show_party_dialog()` opens leave, kick and leadership in the Summer app |
|
|
171
|
+
| Ready | a member in a party |
|
|
172
|
+
|
|
173
|
+
### 3. Share choices with party data
|
|
174
|
+
|
|
175
|
+
Party data is plain text that your game reads from every member's copy of the
|
|
176
|
+
party. Use it for lobby-style choices before the match starts:
|
|
177
|
+
|
|
178
|
+
- **Leader's choices** (map, mode): `party.set_data(key, value)`, read with `party.get_data(key)`. Only the leader can write it; a member gets `not_party_leader`.
|
|
179
|
+
- **Each member's own choices** (ready, character): `party.set_member_data(key, value)`, read with `member.get_data(key)`.
|
|
180
|
+
|
|
181
|
+
Limits:
|
|
182
|
+
|
|
183
|
+
| What | Limit |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| Keys | 1–32 characters from `a-z 0-9 _ . -` |
|
|
186
|
+
| Values | at most 256 UTF-8 bytes of text, no control characters; an empty value deletes the key |
|
|
187
|
+
| Leader keys | 16 per game |
|
|
188
|
+
| Member keys | 8 per member per game |
|
|
189
|
+
|
|
190
|
+
Values pass the Summer chat text filter, so `text_not_allowed` is possible.
|
|
191
|
+
Writes return `SummerOperation`. A conflict is retried once by the engine, then
|
|
192
|
+
fails with retryable `party_version_conflict`. React to `data_changed(key)` and
|
|
193
|
+
`member_data_changed(member, key)`, or just re-render on `changed`.
|
|
194
|
+
|
|
195
|
+
Party data is a lobby convenience, not game state. Once the match starts, the
|
|
196
|
+
authority owns everything (`skill/multiplayer-state`). It never sees party data;
|
|
197
|
+
send choices that matter in your `join` Command and validate them there.
|
|
198
|
+
|
|
199
|
+
### 4. Handle join failures
|
|
200
|
+
|
|
201
|
+
`enter()` returns `false` with the code in the warning. For a party join:
|
|
202
|
+
|
|
203
|
+
| Code | Who | What to show |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `party_member_offline` (retryable) | leader | "Everyone in your party needs the game open." `member.in_game` tells you who |
|
|
206
|
+
| `party_not_searching` (retryable) | member | Nothing; wait for the next `search_started` |
|
|
207
|
+
| `party_not_found` | anyone | The party ended; fall back to a solo Play |
|
|
208
|
+
| `capability_unavailable` | anyone | Parties aren't offered here; use the solo path |
|
|
209
|
+
|
|
210
|
+
A member can follow only while the leader's search is running: from
|
|
211
|
+
`search_started` until `search_ended`.
|
|
212
|
+
|
|
213
|
+
### 5. Test
|
|
214
|
+
|
|
215
|
+
Local Play cannot create a party, so `is_available()` is `false` and each
|
|
216
|
+
client joins solo from `_ready`. Test that path locally, then the party path
|
|
217
|
+
on Summer staging with real accounts.
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
summer_project_setting name="summer/local_play/players" value=2
|
|
221
|
+
summer_play
|
|
222
|
+
summer_get_diagnostics
|
|
223
|
+
summer_stop
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Checklist
|
|
227
|
+
|
|
228
|
+
- [ ] Local Play: `Summer.client.party.is_available()` is `false`, Play joins through the plain queue, and no script errors appear.
|
|
229
|
+
- [ ] Invite and Party buttons are hidden when `is_available()` is `false`.
|
|
230
|
+
- [ ] A member's Play button doesn't join on its own.
|
|
231
|
+
- [ ] Hosted: the leader presses Play, and every member with the game open lands in the same match.
|
|
232
|
+
- [ ] A member who closes the game makes the leader's Play fail with `party_member_offline`, and the message names who is missing.
|
|
233
|
+
- [ ] Ready and leader choices appear on every member's screen.
|
|
234
|
+
|
|
235
|
+
## Common mistakes
|
|
236
|
+
|
|
237
|
+
| Don't | Do | Why |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| Join in `_ready` while parties are available | Join from Play via `enter(target)` | A member joining alone splits the party |
|
|
240
|
+
| Members join `SummerJoinTarget.queue(...)` | Members join `party_queue(queue)` on `search_started` | Only the party target attaches them to the leader's search |
|
|
241
|
+
| Show Invite when `is_available()` is false | Hide it | Local Play, guests and under-13 accounts have no parties |
|
|
242
|
+
| Build your own invite list or party chat | `show_invite_dialog()` / `show_party_dialog()` | The Summer app owns invites, kicks, leadership and chat |
|
|
243
|
+
| Hold `SummerPartyMember` objects | Key UI by `member.user_id` and re-read on `changed` | Each snapshot replaces the member objects |
|
|
244
|
+
| Trust party data on the authority | Send choices in the `join` Command and validate | The authority never sees party data |
|
|
245
|
+
| Put secrets or long text in party data | Short public choices (`map`, `ready`) | 256 bytes, filtered, visible to every member |
|
|
246
|
+
| `await op.completed` | `await op.get_result_or_completed_signal()` | Fast failures complete before you can connect |
|
|
247
|
+
|
|
248
|
+
## See also
|
|
249
|
+
|
|
250
|
+
- `skill/summer-matchmaking`: queues, sizes, teams and the accept prompt
|
|
251
|
+
- `skill/summer-friends`: friends list, profiles and messages
|
|
252
|
+
- `skill/multiplayer-project`: the client entry scene this extends
|
|
253
|
+
- `skill/multiplayer-testing`: Local Play, and what needs staging
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
id: skill/summer-parties
|
|
2
|
+
kind: skill
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
summary: "Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only."
|
|
5
|
+
use_when:
|
|
6
|
+
- "friends in a Summer party should land in the same multiplayer match: party play, play with friends, squad up"
|
|
7
|
+
- "adding invite-friends and party buttons, a party roster or ready checks to a Summer game's menu"
|
|
8
|
+
- "a party join fails with party_member_offline, party_not_searching or capability_unavailable"
|
|
9
|
+
facets:
|
|
10
|
+
lifecycle:
|
|
11
|
+
- build
|
|
12
|
+
domains:
|
|
13
|
+
- multiplayer
|
|
14
|
+
- ui
|
|
15
|
+
modalities:
|
|
16
|
+
- scripts
|
|
17
|
+
compatibility:
|
|
18
|
+
engine: ">=4.6"
|
|
19
|
+
related:
|
|
20
|
+
skills:
|
|
21
|
+
- skill/multiplayer
|
|
22
|
+
- skill/multiplayer-project
|
|
23
|
+
- skill/multiplayer-testing
|
|
24
|
+
- skill/summer-matchmaking
|
|
25
|
+
- skill/summer-friends
|
|
26
|
+
- skill/summer-world-chat
|
|
27
|
+
source: official
|
|
28
|
+
license: MIT
|
|
29
|
+
status: preview
|
|
30
|
+
recommended: false
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: summer-player-data
|
|
3
|
+
description: "Save a player's progress so it follows them to every match: authority load and commit, autosave, safe retries."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: multiplayer-and-networking
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Edit Write summer_get_script_errors summer_project_setting summer_play summer_stop summer_get_diagnostics
|
|
9
|
+
paths: ["**/*.gd", "world.json", "summer.build.json"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /summer-player-data — progress that follows the player
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Player data is what Summer remembers about **one player in one game**:
|
|
17
|
+
coins, unlocks, level, settings. It belongs to the player, not to a match.
|
|
18
|
+
A match World ends and its state is gone. The player's data comes back in
|
|
19
|
+
the next match, on any server and any device.
|
|
20
|
+
|
|
21
|
+
- **Only the authority writes it.** Clients never write player data.
|
|
22
|
+
- **Two slots per player**, written separately:
|
|
23
|
+
- `public`: the authority reads and writes it. The player's own client can read it too, for example to show coins in the menu.
|
|
24
|
+
- `secret`: only the authority reads or writes it, for hidden values such as anti-cheat counters or hidden ratings. It never reaches a client.
|
|
25
|
+
- Nobody else can read either slot.
|
|
26
|
+
- **Each slot is one JSON object of at most 64 KiB.**
|
|
27
|
+
- Allowed values: `bool`, `int`, finite `float`, `String`, `Array` and `Dictionary` with `String` keys, nested at most 16 deep.
|
|
28
|
+
- `Vector3`, `Color` and `null` fail before anything is sent.
|
|
29
|
+
- Whole numbers load back as `int`.
|
|
30
|
+
|
|
31
|
+
**Where it works:**
|
|
32
|
+
|
|
33
|
+
| | Local Play | Hosted |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Authority `load` / `commit_public` / `commit_secret` | Yes: a file store under `user://summer_local_player_data/<game id>/<persona>/` | Yes |
|
|
36
|
+
| Client `Summer.client.player_data.load()` (own public slot) | Yes, from the same files | Yes |
|
|
37
|
+
|
|
38
|
+
Local Play's store keeps the latest value plus five earlier revisions, with
|
|
39
|
+
the same validation and `save_id` rules as hosted. Delete a persona's folder
|
|
40
|
+
to start that player fresh. Only hosted play proves durability.
|
|
41
|
+
|
|
42
|
+
This skill extends the game from `skill/multiplayer-project` and
|
|
43
|
+
`skill/multiplayer-state`: the authority's `_join` Command handler, the
|
|
44
|
+
private `mine` State group, and the `wallets` it keeps.
|
|
45
|
+
|
|
46
|
+
## Steps
|
|
47
|
+
|
|
48
|
+
### 1. Decide what to save
|
|
49
|
+
|
|
50
|
+
Save only what must outlive the match, and save it as a plain document:
|
|
51
|
+
|
|
52
|
+
| Save it | Don't |
|
|
53
|
+
|---|---|
|
|
54
|
+
| Coins, unlocks, best score, cosmetics owned, settings | Positions, health, the round's score: that is match state |
|
|
55
|
+
| A secret anti-cheat counter (`commit_secret`) | Anything another player needs to see: that is a State group |
|
|
56
|
+
|
|
57
|
+
Keep values that must change together in **one** slot: there is no
|
|
58
|
+
transaction across the two slots.
|
|
59
|
+
|
|
60
|
+
### 2. Load when the player arrives
|
|
61
|
+
|
|
62
|
+
The authority starts loading on `session_joined`, before the client's
|
|
63
|
+
`join` Command arrives. Add to `authority/main.gd`:
|
|
64
|
+
|
|
65
|
+
```gdscript
|
|
66
|
+
const AUTOSAVE_SECONDS := 5.0
|
|
67
|
+
## session_id -> {"session", "state": loading|ready|failed|superseded, "data",
|
|
68
|
+
## "dirty", "saving", "saves", "save_prefix", "left"}
|
|
69
|
+
var profiles := {}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
In `_ready()`, next to the other `world` signals, before `Summer.initialize`
|
|
73
|
+
(connect `session_joined` only once if `skill/summer-match-results` already
|
|
74
|
+
did):
|
|
75
|
+
|
|
76
|
+
```gdscript
|
|
77
|
+
world.session_joined.connect(_on_session_joined)
|
|
78
|
+
world.draining.connect(func(_deadline: int) -> void: _save_all())
|
|
79
|
+
var autosave := Timer.new()
|
|
80
|
+
autosave.wait_time = AUTOSAVE_SECONDS
|
|
81
|
+
autosave.timeout.connect(_save_all)
|
|
82
|
+
add_child(autosave)
|
|
83
|
+
autosave.start()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If `_on_session_joined` already exists (`skill/summer-match-results` adds
|
|
87
|
+
one), put this body at its end instead of defining it twice.
|
|
88
|
+
|
|
89
|
+
```gdscript
|
|
90
|
+
## Reads the player's saved progress as soon as their Session arrives.
|
|
91
|
+
func _on_session_joined(session: SummerSession) -> void:
|
|
92
|
+
# save_ids must never repeat for this player, in this World or any later one.
|
|
93
|
+
var prefix := str(int(Time.get_unix_time_from_system() * 1000.0))
|
|
94
|
+
var profile := {"session": session, "state": "loading", "data": {}, "dirty": false, "saving": false, "saves": 0, "save_prefix": prefix, "left": false}
|
|
95
|
+
profiles[session.session_id] = profile
|
|
96
|
+
for attempt in 3:
|
|
97
|
+
var loaded := Summer.authority.player_data.load(session)
|
|
98
|
+
var result: SummerResult = await loaded.get_result_or_completed_signal()
|
|
99
|
+
if result.ok:
|
|
100
|
+
profile.data = loaded.public_record.data
|
|
101
|
+
profile.state = "ready"
|
|
102
|
+
return
|
|
103
|
+
if not result.retryable:
|
|
104
|
+
break
|
|
105
|
+
await get_tree().create_timer(1.0 + attempt).timeout
|
|
106
|
+
# Unknown is not empty: never start a fresh save over data you could not read.
|
|
107
|
+
profile.state = "failed"
|
|
108
|
+
push_warning("player data load failed for %s" % session.session_id)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`loaded.secret_record.data` holds the secret slot. A slot that was never
|
|
112
|
+
written has `exists == false` and empty `data`.
|
|
113
|
+
|
|
114
|
+
### 3. Make `join` wait for the load
|
|
115
|
+
|
|
116
|
+
In `_join`, right after the seat check:
|
|
117
|
+
|
|
118
|
+
```gdscript
|
|
119
|
+
# The client retries "join" while this player's saved progress loads.
|
|
120
|
+
var profile: Dictionary = profiles.get(session.session_id, {"state": "loading"})
|
|
121
|
+
if profile.state != "ready":
|
|
122
|
+
request.refuse(&"loading" if profile.state == "loading" else &"profile_unavailable")
|
|
123
|
+
return
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Start the player from their saved values instead of zero, in the block that
|
|
127
|
+
creates their private group:
|
|
128
|
+
|
|
129
|
+
```gdscript
|
|
130
|
+
if not mine_groups.has(session.session_id):
|
|
131
|
+
wallets[session.session_id] = int(profile.data.get("coins", 0))
|
|
132
|
+
mine_groups[session.session_id] = spawner.create_state_group(Game.MINE, StringName("mine%d" % seat), Game.pack({"coins": wallets[session.session_id]}), null, null, session)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
On the client, in `client/main.gd`, retry `join` while the answer is
|
|
136
|
+
`loading`:
|
|
137
|
+
|
|
138
|
+
```gdscript
|
|
139
|
+
var entered := await command({"c": "join"})
|
|
140
|
+
while not entered.ok and entered.error == "loading":
|
|
141
|
+
await get_tree().create_timer(0.5).timeout
|
|
142
|
+
entered = await command({"c": "join"})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`profile_unavailable` means the load failed. Show "Couldn't load your
|
|
146
|
+
progress", and do **not** let the player play on a blank profile that would
|
|
147
|
+
overwrite their real one.
|
|
148
|
+
|
|
149
|
+
### 4. Mark changes, autosave, flush on leave
|
|
150
|
+
|
|
151
|
+
Wherever progress changes (here, after the wallet changes in `_collect`):
|
|
152
|
+
|
|
153
|
+
```gdscript
|
|
154
|
+
_progress_changed(session.session_id, {"coins": wallets[session.session_id]})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
At the top of `_on_session_left`:
|
|
158
|
+
|
|
159
|
+
```gdscript
|
|
160
|
+
_player_left(session.session_id)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The saving code:
|
|
164
|
+
|
|
165
|
+
```gdscript
|
|
166
|
+
## Records new progress. The autosave writes it a few seconds later.
|
|
167
|
+
func _progress_changed(session_id: String, changes: Dictionary) -> void:
|
|
168
|
+
var profile: Dictionary = profiles.get(session_id, {})
|
|
169
|
+
if profile.get("state", "") == "ready":
|
|
170
|
+
profile.data.merge(changes, true)
|
|
171
|
+
profile.dirty = true
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
func _save_all() -> void:
|
|
175
|
+
for session_id in profiles.keys():
|
|
176
|
+
_save(session_id)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
## One commit per player at a time. Each new save gets a new save_id; a retry
|
|
180
|
+
## of the same save reuses its id and data, so it is never applied twice.
|
|
181
|
+
func _save(session_id: String) -> void:
|
|
182
|
+
var profile: Dictionary = profiles.get(session_id, {})
|
|
183
|
+
if profile.is_empty() or profile.saving or not profile.dirty or profile.state != "ready":
|
|
184
|
+
return
|
|
185
|
+
profile.saving = true
|
|
186
|
+
while profile.dirty and profile.state == "ready":
|
|
187
|
+
profile.dirty = false
|
|
188
|
+
profile.saves += 1
|
|
189
|
+
var save_id := "%s-%d" % [profile.save_prefix, profile.saves]
|
|
190
|
+
if not await _commit(profile, save_id, profile.data.duplicate(true)):
|
|
191
|
+
break
|
|
192
|
+
profile.saving = false
|
|
193
|
+
if profile.left and not profile.dirty:
|
|
194
|
+
profiles.erase(session_id) # gone and fully saved
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
func _commit(profile: Dictionary, save_id: String, data: Dictionary) -> bool:
|
|
198
|
+
for attempt in 4:
|
|
199
|
+
var commit := Summer.authority.player_data.commit_public(profile.session, save_id, data)
|
|
200
|
+
var result: SummerResult = await commit.get_result_or_completed_signal()
|
|
201
|
+
if result.ok:
|
|
202
|
+
return true
|
|
203
|
+
if result.code == &"player_data_superseded":
|
|
204
|
+
profile.state = "superseded" # a newer Session owns this player now
|
|
205
|
+
return false
|
|
206
|
+
if not result.retryable:
|
|
207
|
+
push_error("player data save %s failed: %s" % [save_id, result.code])
|
|
208
|
+
return false
|
|
209
|
+
await get_tree().create_timer(1.0 + attempt).timeout # same save_id, same data
|
|
210
|
+
profile.dirty = true # still unsaved; the next autosave tries again
|
|
211
|
+
return false
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## Commits stay open after a player leaves, so flush their progress now.
|
|
215
|
+
func _player_left(session_id: String) -> void:
|
|
216
|
+
var profile: Dictionary = profiles.get(session_id, {})
|
|
217
|
+
if profile.is_empty():
|
|
218
|
+
return
|
|
219
|
+
profile.left = true
|
|
220
|
+
if profile.dirty:
|
|
221
|
+
_save(session_id)
|
|
222
|
+
elif not profile.saving:
|
|
223
|
+
profiles.erase(session_id)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Why it is shaped this way:
|
|
227
|
+
|
|
228
|
+
- **Autosave, not only save-on-leave.** A crash delivers no signal. With a 5 s autosave a crash loses at most 5 s.
|
|
229
|
+
- **One commit per player and slot at a time.** A second concurrent commit fails with `operation_in_progress`.
|
|
230
|
+
- **A new `save_id` for each new save, the same `save_id` for a retry.**
|
|
231
|
+
- Resending a `save_id` with the same data returns the original revision (`commit.replayed`).
|
|
232
|
+
- The same `save_id` with different data fails with `player_data_save_id_conflict`.
|
|
233
|
+
- Build the id from the time the Session joined plus a counter. Session ids are not enough: Local Play reuses them on every run.
|
|
234
|
+
- **`player_data_superseded` is final.** The player joined a newer World, and only their newest Session may write.
|
|
235
|
+
- **`draining`** fires before the World shuts down. Commits are still accepted until the stop deadline, so flush everyone then.
|
|
236
|
+
- **Under Local Play a player who quits only disconnects.** Their seat is held and `session_left` arrives when the World ends, so the autosave is what saves them.
|
|
237
|
+
|
|
238
|
+
Use `commit_secret` in the same way for the secret slot. Each slot has its own
|
|
239
|
+
revisions and `save_id` space.
|
|
240
|
+
|
|
241
|
+
### 5. Show saved progress in the menu (optional)
|
|
242
|
+
|
|
243
|
+
The client reads its own public slot, even before joining a match:
|
|
244
|
+
|
|
245
|
+
```gdscript
|
|
246
|
+
## The client can read its own public slot, for example to show progress in
|
|
247
|
+
## the menu. It never writes player data.
|
|
248
|
+
func _show_saved_progress() -> void:
|
|
249
|
+
var saved := Summer.client.player_data.load()
|
|
250
|
+
var result: SummerResult = await saved.get_result_or_completed_signal()
|
|
251
|
+
if result.ok:
|
|
252
|
+
$Menu/Coins.text = "Coins: %d" % int(saved.public_record.data.get("coins", 0))
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 6. Keep the publish checks passing
|
|
256
|
+
|
|
257
|
+
The source-graph check in `skill/multiplayer-publish` (step 4b) flags every
|
|
258
|
+
call ending in `load(` without a string literal inside, so
|
|
259
|
+
`Summer.authority.player_data.load(session)` and
|
|
260
|
+
`Summer.client.player_data.load()` report
|
|
261
|
+
`SUMMER_COMPOSITION_DYNAMIC_PATH_UNDECLARED`. They load no file. Declare one
|
|
262
|
+
dependency edge per file that makes such a call, in `source-domains.json`:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
"dependencies": [
|
|
266
|
+
{ "from": "authority/main.gd", "to": "network/game.gd" },
|
|
267
|
+
{ "from": "client/main.gd", "to": "network/game.gd" }
|
|
268
|
+
]
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 7. Test persistence: run Local Play twice
|
|
272
|
+
|
|
273
|
+
Run the same session twice. Local Play personas (`player-1`, `player-2`)
|
|
274
|
+
keep their files between runs, so run 2 must start where run 1 ended.
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
summer_project_setting name="summer/local_play/players" value=2
|
|
278
|
+
summer_play
|
|
279
|
+
summer_get_diagnostics
|
|
280
|
+
summer_stop
|
|
281
|
+
summer_play
|
|
282
|
+
summer_get_diagnostics
|
|
283
|
+
summer_stop
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
From a terminal: `<summer> --path . --summer-local-play 2 --summer-local-play-headless --summer-local-play-timeout 20 -- --bot={client}`,
|
|
287
|
+
twice (see `skill/multiplayer-testing` for bot mode). Print markers such as
|
|
288
|
+
`print("PROFILE_LOADED coins=", ...)` and `print("SAVED ", save_id, " revision=", commit.revision)` while testing, then check them.
|
|
289
|
+
|
|
290
|
+
Files: `~/Library/Application Support/Godot/app_userdata/<project name>/summer_local_player_data/<game id>/<persona>/public.json`
|
|
291
|
+
on macOS (the project's `user://` folder elsewhere). Delete a persona's
|
|
292
|
+
folder to start over.
|
|
293
|
+
|
|
294
|
+
## Checklist
|
|
295
|
+
|
|
296
|
+
- [ ] Run 1: a player earns something, and the authority logs a successful save within the autosave interval.
|
|
297
|
+
- [ ] Run 2: the authority loads the saved value at join; the client menu shows it.
|
|
298
|
+
- [ ] A load failure refuses `join` with `profile_unavailable`; nothing is written over the old data.
|
|
299
|
+
- [ ] No `player_data_save_id_conflict` in the logs, even on the second run.
|
|
300
|
+
- [ ] The saved document stays well under 64 KiB at its largest. Test the maximum.
|
|
301
|
+
|
|
302
|
+
## Common mistakes
|
|
303
|
+
|
|
304
|
+
| Don't | Do | Why |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| Write player data from the client | Commit on the authority | Clients have no write API; the authority is the trust boundary |
|
|
307
|
+
| Treat a failed load as a new player | Refuse `join` with `profile_unavailable` | The next save would wipe their real progress |
|
|
308
|
+
| Save only when the player leaves | Autosave dirty players every few seconds, and flush on `session_left` and `draining` | Crashes deliver no signal |
|
|
309
|
+
| Reuse one `save_id` for new data | A new id per save, the same id per retry | Same id plus different data is `player_data_save_id_conflict` |
|
|
310
|
+
| Build `save_id` from the session id alone | Join time plus a counter | Local Play reuses session ids across runs |
|
|
311
|
+
| Start a second commit while one is pending | One commit per player and slot at a time | The second fails with `operation_in_progress` |
|
|
312
|
+
| Keep retrying after `player_data_superseded` | Stop saving through that Session | A newer World owns the player now |
|
|
313
|
+
| Store `Vector3`, `Color` or `null` | Plain JSON: numbers, strings, arrays, dictionaries | Unsupported types fail with `player_data_unsupported_type` |
|
|
314
|
+
| Save the match (positions, round score) | Save only what outlives the match | The World ends; `skill/summer-world-saves` covers Worlds that persist |
|
|
315
|
+
| Truncate data that grew past 64 KiB | Refuse to grow; test the maximum size | `player_data_too_large` rejects the whole commit |
|
|
316
|
+
|
|
317
|
+
## See also
|
|
318
|
+
|
|
319
|
+
- `skill/multiplayer-state` — the wallet and private State group this skill saves
|
|
320
|
+
- `skill/summer-world-saves` — saving a whole persistent World instead of one player
|
|
321
|
+
- `skill/multiplayer-testing` — bots and repeated Local Play runs
|
|
322
|
+
- `skill/summer-leaderboards` — scores other players can see
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
id: skill/summer-player-data
|
|
2
|
+
kind: skill
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
summary: "Save a player's progress so it follows them to every match: authority load and commit, autosave, safe retries."
|
|
5
|
+
use_when:
|
|
6
|
+
- "saving player progress, coins, unlocks, levels or settings in a multiplayer Summer game so they persist between matches"
|
|
7
|
+
- "a player's progress resets every match, or saves fail with save_id_conflict, superseded or operation_in_progress"
|
|
8
|
+
- "showing a player's saved progress in the menu before they join a match"
|
|
9
|
+
facets:
|
|
10
|
+
lifecycle:
|
|
11
|
+
- build
|
|
12
|
+
domains:
|
|
13
|
+
- multiplayer
|
|
14
|
+
- scripting
|
|
15
|
+
modalities:
|
|
16
|
+
- scripts
|
|
17
|
+
compatibility:
|
|
18
|
+
engine: ">=4.6"
|
|
19
|
+
related:
|
|
20
|
+
skills:
|
|
21
|
+
- skill/multiplayer
|
|
22
|
+
- skill/multiplayer-state
|
|
23
|
+
- skill/multiplayer-testing
|
|
24
|
+
- skill/summer-world-saves
|
|
25
|
+
- skill/summer-leaderboards
|
|
26
|
+
- skill/summer-store
|
|
27
|
+
source: official
|
|
28
|
+
license: MIT
|
|
29
|
+
status: stable
|
|
30
|
+
recommended: false
|