summer-engine 3.2.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/CHANGELOG.md +61 -0
  7. package/README.md +7 -4
  8. package/assets/api-docs.json.gz +0 -0
  9. package/dist/cli/commands/install.d.ts +10 -0
  10. package/dist/cli/commands/install.js +55 -10
  11. package/dist/cli/commands/login.js +12 -0
  12. package/dist/cli/commands/publish.js +2 -1
  13. package/dist/cli/commands/tool.d.ts +7 -0
  14. package/dist/cli/commands/tool.js +36 -1
  15. package/dist/core/auth.js +3 -0
  16. package/dist/core/capabilities/audit/args.d.ts +14 -14
  17. package/dist/core/capabilities/capture-gameplay.d.ts +69 -0
  18. package/dist/core/capabilities/capture-gameplay.js +283 -0
  19. package/dist/core/capabilities/creator.js +1 -1
  20. package/dist/core/capabilities/doctor.d.ts +9 -1
  21. package/dist/core/capabilities/doctor.js +47 -4
  22. package/dist/core/capabilities/engine-run.d.ts +12 -0
  23. package/dist/core/capabilities/engine-run.js +61 -0
  24. package/dist/core/capabilities/export-engine-version.d.ts +15 -0
  25. package/dist/core/capabilities/export-engine-version.js +24 -0
  26. package/dist/core/capabilities/export-game.d.ts +107 -0
  27. package/dist/core/capabilities/export-game.js +445 -0
  28. package/dist/core/capabilities/export-presets.d.ts +50 -0
  29. package/dist/core/capabilities/export-presets.js +177 -0
  30. package/dist/core/capabilities/export-templates.d.ts +118 -0
  31. package/dist/core/capabilities/export-templates.js +291 -0
  32. package/dist/core/capabilities/image-generation.d.ts +9 -0
  33. package/dist/core/capabilities/image-generation.js +19 -1
  34. package/dist/core/capabilities/publish-build.d.ts +77 -0
  35. package/dist/core/capabilities/publish-build.js +500 -0
  36. package/dist/core/capabilities/publish-download.d.ts +37 -0
  37. package/dist/core/capabilities/publish-download.js +168 -0
  38. package/dist/core/capabilities/runtime-control.d.ts +10 -10
  39. package/dist/core/capabilities/summer-bundle.d.ts +49 -0
  40. package/dist/core/capabilities/summer-bundle.js +156 -0
  41. package/dist/core/capabilities/tool-dispatch.js +43 -1
  42. package/dist/core/capabilities/ui-control.d.ts +2 -2
  43. package/dist/core/config.js +2 -2
  44. package/dist/core/engine-install.d.ts +16 -0
  45. package/dist/core/engine-install.js +32 -2
  46. package/dist/core/hosted-mcp.d.ts +115 -0
  47. package/dist/core/hosted-mcp.js +164 -0
  48. package/dist/core/launch-posture.d.ts +3 -0
  49. package/dist/core/launch-posture.js +15 -2
  50. package/dist/core/library-read.d.ts +2 -2
  51. package/dist/core/oauth.d.ts +78 -0
  52. package/dist/core/oauth.js +309 -0
  53. package/dist/core/util/zip-write.d.ts +18 -0
  54. package/dist/core/util/zip-write.js +112 -0
  55. package/dist/core/util/zip.d.ts +22 -0
  56. package/dist/core/util/zip.js +140 -0
  57. package/dist/mcp/hosted-mount.d.ts +50 -0
  58. package/dist/mcp/hosted-mount.js +200 -0
  59. package/dist/mcp/server.d.ts +2 -1
  60. package/dist/mcp/server.js +14 -6
  61. package/dist/mcp/tools/creator-tools.js +64 -1
  62. package/dist/mcp/tools/generate-tools.js +12 -2
  63. package/gemini-extension.json +3 -1
  64. package/library/references/mcp-tools-reference/mcp-tools-reference.md +7 -3
  65. package/library/skills/README.md +6 -4
  66. package/library/skills/bounded-engine-runs/SKILL.md +96 -0
  67. package/library/skills/bounded-engine-runs/resource.yaml +29 -0
  68. package/library/skills/compatibility-renderer-traps/SKILL.md +78 -0
  69. package/library/skills/compatibility-renderer-traps/resource.yaml +28 -0
  70. package/library/skills/concept-art/SKILL.md +2 -2
  71. package/library/skills/export-and-ship/SKILL.md +4 -0
  72. package/library/skills/headless-scripting/resource.yaml +1 -0
  73. package/library/skills/in-game-purchases/SKILL.md +123 -118
  74. package/library/skills/in-game-purchases/resource.yaml +2 -2
  75. package/library/skills/multiplayer-publish/SKILL.md +5 -1
  76. package/library/skills/navigate-summer/SKILL.md +1 -1
  77. package/library/skills/web-compatibility-look/resource.yaml +1 -0
  78. package/library/tools/capture-gameplay/resource.yaml +73 -0
  79. package/library/tools/creator-publish/resource.yaml +4 -3
  80. package/library/tools/creator-releases/resource.yaml +1 -1
  81. package/library/tools/export-game/resource.yaml +72 -0
  82. package/library/tools/export-templates/resource.yaml +63 -0
  83. package/library/tools/generate-image/resource.yaml +15 -1
  84. package/library/tools/open/resource.yaml +1 -1
  85. package/library/tools/publish-build/resource.yaml +78 -0
  86. package/package.json +1 -1
  87. package/registry/generated/aliases.json +4 -0
  88. package/registry/generated/counts.json +3 -3
  89. package/registry/generated/gemini-extension.json +3 -1
  90. package/registry/generated/index.json +241 -19
  91. package/registry/generated/marketplace.claude.json +2 -2
  92. package/registry/generated/plugin.claude.json +4 -2
  93. package/registry/generated/plugin.codex.json +5 -3
  94. package/registry/generated/plugin.cursor.json +4 -2
  95. package/registry/generated/plugin.factory.json +4 -2
  96. package/registry/generated/skills-index.md +6 -4
  97. package/registry/generated/skills-registry.json +28 -1
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: compatibility-renderer-traps
3
+ description: "Make a Forward+ Summer game also look right on the Compatibility renderer: instance-uniform budget, black MultiMesh colors, sRGB vertex colors."
4
+ license: MIT
5
+ compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
+ category: rendering-and-lighting
7
+ user-invocable: true
8
+ allowed-tools: Read Grep Glob Edit Write summer_project_setting summer_get_script_errors
9
+ paths: ["**/*.gdshader", "**/*.gdshaderinc", "**/*.gd", "project.godot"]
10
+ ---
11
+
12
+ # Forward+ and Compatibility without surprises
13
+
14
+ Games built on Forward+ can end up on the Compatibility renderer (OpenGL): phones where Metal/Vulkan is
15
+ unavailable, weak GPUs, or a project that picks it for battery. The failures are silent: no error, just
16
+ black fruit, pale colors, or objects that stop animating after the first few hundred. These are the traps
17
+ that cost real renders in a shipped-quality game, each with the fix and the reason.
18
+
19
+ ## 1. Know which renderer you actually got
20
+
21
+ - Phones use `rendering/renderer/rendering_method.mobile`, which defaults to `mobile`, not Forward+.
22
+ Set it explicitly.
23
+ - A device can fall back to another renderer when it lacks a capability. Trust the renderer name printed
24
+ at startup (or `RenderingServer.get_current_rendering_method()`), not the project setting.
25
+ - Preview Compatibility on your computer:
26
+ `<summer> --path . --rendering-method gl_compatibility`.
27
+
28
+ ## 2. The traps
29
+
30
+ | Symptom on Compatibility | Cause | Fix |
31
+ |---|---|---|
32
+ | Objects past roughly the 250th stop growing, lose color or read as default | GLES3 reserves 16 `instance uniform` slots per instance out of 4096, so only about 256 instances in the whole scene can use them. Beyond that, values silently read 0. | Don't use `instance uniform` for anything numerous. Put per-instance state in a one-instance MultiMesh's `INSTANCE_CUSTOM` (a vec4), or bake it into mesh vertex colors. |
33
+ | Fruit/flowers render solid black | GLES3 multiplies `COLOR` by the MultiMesh instance color, which is 0 when `use_colors` is off. | Enable `use_colors` and set every instance color to white, or don't read `COLOR` in that path. |
34
+ | Everything looks pastel on Forward+ (saturated on Compatibility) | Forward+ shades in linear space, Compatibility in sRGB. Colors you bake into vertex `COLOR` or hard-code into `ALBEDO` are usually sRGB. | Convert once, only where needed: `if (!OUTPUT_IS_SRGB) col = to_linear(col);` with the helper below (the shading language has no built-in `srgb_to_linear`). Or pass colors as `uniform vec3 c : source_color`, which converts for you. |
35
+ | Whole scene pale/lime on Compatibility while Forward+ looks right | Tonemap, glow and ambient respond differently. | Keep a Compatibility branch of your lighting profile (lower ambient and exposure) and check both. |
36
+ | Foliage glows white | Glow threshold too low for bright leaves. | Raise the glow HDR threshold (~1.5) and keep bloom near 0. |
37
+
38
+ Everything the player must see (ripe crop, enemy, pickup) must stay readable without glow, SSR,
39
+ volumetric fog or other Forward+-only effects: use shape, color and icons too.
40
+
41
+ The helper (standard sRGB curve):
42
+
43
+ ```glsl
44
+ vec3 to_linear(vec3 c) {
45
+ return mix(pow((c + vec3(0.055)) * (1.0 / 1.055), vec3(2.4)), c * (1.0 / 12.92), lessThan(c, vec3(0.04045)));
46
+ }
47
+ ```
48
+
49
+ ## 3. Keep one shader path
50
+
51
+ Write shaders in plain spatial math: vertex-color ramps, simple rim terms, no custom `light()` unless
52
+ you test it on both renderers. One path for both renderers means one set of bugs and no drift.
53
+
54
+ ## 4. Verify, don't assume
55
+
56
+ 1. Render the same scene on Forward+ and `gl_compatibility` offscreen:
57
+ `<summer> --audio-driver Dummy --summer-offscreen --path . --resolution 1600x900 -s res://tools/capture.gd -- --out=<dir>`
58
+ (add `--rendering-method gl_compatibility` for the second run).
59
+ 2. Read both images side by side, and include a stress shot with many instances (the 256 limit only shows
60
+ at scale).
61
+ 3. Grep the log for `SCRIPT ERROR`, `SHADER ERROR` and `ERROR:`. A shader that fails to compile still lets
62
+ the capture exit 0 and draws the object untextured, so a missing grep hides the failure.
63
+ 4. If later captures repeat the same frame, the offscreen run froze (it happens when another engine
64
+ renders at the same time). Check with `md5 -r *.png` and rerun alone.
65
+
66
+ ## Common mistakes
67
+
68
+ - Checking only the project setting and calling it "Forward+ on phones".
69
+ - Using `instance uniform` for per-plant or per-tile state.
70
+ - Reading `COLOR` from a MultiMesh without instance colors.
71
+ - Testing Compatibility with ten objects when the game shows five hundred.
72
+ - Budgeting one run per fix: every look change needs two captures (one per renderer). Plan runs in pairs.
73
+
74
+ ## See also
75
+
76
+ - `web-compatibility-look` (a finished look when the game only runs on Compatibility, as on the web)
77
+ - `3d-lighting` (lighting profiles)
78
+ - `bounded-engine-runs` (safe offscreen capture runs)
@@ -0,0 +1,28 @@
1
+ id: skill/compatibility-renderer-traps
2
+ kind: skill
3
+ version: 1.0.0
4
+ summary: "Make a Forward+ Summer game also look right on the Compatibility renderer: instance-uniform budget, black MultiMesh colors, sRGB vertex colors."
5
+ use_when:
6
+ - a game must run on phones or weak GPUs that may use the Compatibility or Mobile renderer
7
+ - objects render black, pale or stop animating on one renderer but not another
8
+ - writing shaders for many instances (crops, foliage, tiles) that must work everywhere
9
+ facets:
10
+ lifecycle:
11
+ - build
12
+ domains:
13
+ - rendering
14
+ - performance
15
+ modalities:
16
+ - shaders
17
+ - scripts
18
+ compatibility:
19
+ engine: ">=4.6"
20
+ related:
21
+ skills:
22
+ - skill/web-compatibility-look
23
+ - skill/3d-lighting
24
+ - skill/bounded-engine-runs
25
+ source: official
26
+ license: MIT
27
+ status: preview
28
+ recommended: true
@@ -77,7 +77,7 @@ summer_generate_image(
77
77
 
78
78
  `style: "none"` is intentional, but be precise about why: only `cartoon` and `anime` append anything to the prompt, `realistic` and `none` append nothing, and any other value is coerced to `none`. So the preset cannot override the prompt's stylistic direction — it just contributes nothing, which is what concept art wants. All four variants' divergence has to come from the prompt.
79
79
 
80
- There is no `image_size` argument, and `options` only recognizes Nano Banana provider keys plus `removeBackground` — an unrecognized key is dropped without an error. Every MCP image comes back at the server's 1:1 default. For wide key art, ask for "wide cinematic framing" in the prompt and accept the square canvas, or generate in the Summer dashboard where aspect ratio is exposed. Since these are exploration images the user reacts to and throws away, square is usually fine.
80
+ For a shape other than square, pass `aspectRatio` (`"16:9"`, `"9:16"`, ...) or `width` + `height`; the result reports the real width and height and the model that ran. Exploration images are thrown away, so square is usually fine.
81
81
 
82
82
  ### 5. Present the batch
83
83
 
@@ -121,7 +121,7 @@ The structure: **subject + axis variant + "concept art" + lighting/atmosphere**.
121
121
 
122
122
  - **User says "just one" after you propose 4.** That's a `character-portrait` job. Hand off.
123
123
  - **User wants 8+ variants.** Generate 4 first; if the user wants more on the same axis, generate 4 more. Don't dump 8 at once — overload paralyzes choice.
124
- - **Subject is a fully described scene, not an asset.** You're producing key art, not asset reference. Ask for wide cinematic framing in the prompt and bias toward atmospheric — you cannot set an aspect ratio over MCP.
124
+ - **Subject is a fully described scene, not an asset.** You're producing key art, not asset reference. Pass `aspectRatio: "16:9"` (or `width: 1920, height: 1080` for store key art) and bias toward atmospheric.
125
125
  - **User wants the final to be 3D.** After they pick a direction, route to `asset-strategy` — concept-art images are not 3D-ready references (they have backgrounds, scene context, dramatic lighting baked in). The 3D pipeline needs its own clean white-background reference.
126
126
 
127
127
  ## Fallback (no MCP)
@@ -291,6 +291,10 @@ For each build:
291
291
 
292
292
  Per platform, point the user at the correct upload path:
293
293
 
294
+ - **summer.games:** `summer_export_game` writes the summer.games `.zip`; `summer_publish_build` uploads it to the game's store listing after the user confirms the exact game, file and version (needs `summer login --store`). If it answers `store_auth_refused`, the user uploads the `.zip` in Studio instead.
295
+ - Per platform, the store takes: iPhone, Android, macOS and Windows in the Summer apps = one summer.games bundle (`summer_export_game` `targets`, format `bundle`; no template, no signing; a game without a server targets `ios`/`android` only). Web = an HTML5 `.zip` on Summer's WebGPU Forward+ template (`targets:["web"]`; the project must render with Forward+ or Mobile). A macOS or Windows download for the store page = `targets:["macos"]` or `["windows"]` with format `download`. Download exports need the template: `summer_export_templates` `install` fetches it from Summer's CDN.
296
+ - Hosted games: if `summer_export_game` fails with "… is in the source graph's authority_engine domain, which the client pack leaves out", an authority root in `source-domains.json` holds a file the client loads (often the composition `.tres`). Narrow that root to the server-only files, or move the shared file under a `shared` path, then export again. Every exporter message is in the `multiplayer-publish` skill's export-errors.md; the folder rules are in `multiplayer-project`.
297
+ - Store page art: key art, covers and icons carry no text, logos or UI (the store page shows the title). Screenshots are real gameplay and may show the game's own HUD; add no captions, logos or overlays. Capture them with `summer_capture_gameplay` (offscreen, no editor needed; `resolution` `1920x1080`, or `1080x1920` for portrait; pass the gameplay `scene` when the main scene is a title menu), or `summer_screenshot` target `game` while the editor runs the game.
294
298
  - **Steam:** `steamcmd` + Steam Pipe (`builder_app_xxx.vdf`). Set up depots first in Steamworks.
295
299
  - **itch.io:** `butler push <path> <user>/<game>:<channel>` (e.g. `butler push build/win <user>/mygame:windows`).
296
300
  - **Web (itch hosted):** zip + drop into itch upload UI as "playable in browser".
@@ -18,6 +18,7 @@ compatibility:
18
18
  related:
19
19
  skills:
20
20
  - skill/scene-scripting
21
+ - skill/bounded-engine-runs
21
22
  source: official
22
23
  license: MIT
23
24
  status: stable
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: in-game-purchases
3
- description: "Sell items for Sparks and let short players buy Sparks in game via the Summer purchase sheet: checkout, Sparks purchase, results, testing, store policy."
3
+ description: "Sell items for Sparks in a game shop via the Summer purchase sheet: checkout, results, not enough Sparks, inventory unlocks, authority grants, store policy."
4
4
  license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: gameplay
@@ -11,14 +11,19 @@ paths: ["**/*.gd"]
11
11
  # In-game purchases with Sparks
12
12
 
13
13
  Sparks are Summer's currency. A game never takes money, never holds a
14
- balance and never approves a payment. It asks Summer, Summer shows its own
15
- purchase sheet over the game, and the game reacts to the authoritative
16
- result. There are two calls and they follow the same shape:
14
+ balance and never approves a payment. It asks Summer for a checkout, Summer
15
+ shows its own purchase sheet over the game, and the game reacts to the
16
+ authoritative result.
17
17
 
18
18
  | The player wants to | Call | Validated by |
19
19
  | --- | --- | --- |
20
20
  | buy an item with Sparks they have | `Summer.client.store.request_checkout(lines)` | the Summer ledger (debit) |
21
- | buy Sparks because they are short | `Summer.client.store.request_sparks_purchase(minimum_sparks)` | the payment provider (Stripe or Apple), then the ledger credit |
21
+ | buy Sparks because they are short | no game call: the player gets Sparks in the Summer app | the Summer app |
22
+
23
+ The engine has no call that sells Sparks from inside the game. When a
24
+ checkout fails with `insufficient_funds`, tell the player they need more
25
+ Sparks and that they can get them in the Summer app, then let them press Buy
26
+ again.
22
27
 
23
28
  ## When to sell with Sparks
24
29
 
@@ -28,9 +33,9 @@ result. There are two calls and they follow the same shape:
28
33
  game.
29
34
  - Do not invent your own currency that is bought with money. *Why:* Summer
30
35
  can only refund, protect and pay you out for Sparks purchases.
31
- - Offer "Get Sparks" only when a purchase failed for lack of Sparks, or on an
32
- explicit shop button. *Why:* the Summer sheet interrupts play; surprise
33
- payment prompts read as pressure and get games reported.
36
+ - Open the shop only from an explicit shop or buy button. *Why:* the Summer
37
+ sheet interrupts play; surprise payment prompts read as pressure and get
38
+ games reported.
34
39
 
35
40
  ## Rules and the reason for each
36
41
 
@@ -38,66 +43,66 @@ result. There are two calls and they follow the same shape:
38
43
  `Summer.client.items` shows the player owns it. *Why:* the receipt proves
39
44
  the debit; the inventory projection is what the platform guarantees the
40
45
  player owns, including after a crash or on another device.
41
- 2. **Never send prices, amounts or providers.** The game passes an offer ID
42
- and a quantity, or a Sparks hint. *Why:* the platform prices everything; a
43
- client-supplied price is exactly what a cheater edits.
44
- 3. **One call is one purchase.** Every call creates a new identity. Never
45
- call again to "retry" an unknown outcome. *Why:* the first request may
46
- already be charged; the SDK and the Summer app recover it and deliver the
47
- result on the signal.
48
- 4. **Handle results only in the status signals, and deduplicate by
46
+ 2. **Never send prices or amounts.** The game passes an offer ID and a
47
+ quantity (`SummerCheckoutLine.create(offer_id, quantity)`). *Why:* the
48
+ platform prices everything; a client-supplied price is exactly what a
49
+ cheater edits.
50
+ 3. **One call is one purchase.** Every `request_checkout` creates a new
51
+ identity. Never call again to "retry" an unknown outcome
52
+ (`SummerCheckoutOperation.is_outcome_unknown()`). *Why:* the first request
53
+ may already be charged; the SDK recovers it and delivers the result on the
54
+ signal.
55
+ 4. **Handle results in `checkout_status_changed`, and deduplicate by
49
56
  `request_id` plus `version`.** Connect normally (not deferred) before
50
- requesting. *Why:* results can arrive after a restart or twice after a
51
- crash; a synchronous handler is how the SDK knows the result was delivered.
52
- 5. **Treat `manual_review` and pending as not done.** *Why:* the platform
57
+ requesting. To draw a checkout panel, read `get_checkout_statuses()` and
58
+ `has_pending_checkouts()` instead of keeping your own list. *Why:* results
59
+ can arrive after a restart or twice after a crash; a synchronous handler is
60
+ how the SDK knows the result was delivered.
61
+ 5. **Treat `manual_review` and `pending` as not done.** *Why:* the platform
53
62
  has not decided yet; showing success or failure would be a guess.
54
63
  6. **Check readiness and hide what is unavailable.**
55
- `check_checkout_readiness()` and `check_sparks_purchase_readiness()` return
56
- a `SummerResult`. *Why:* editor play, guest sessions and older Summer apps
57
- do not offer purchases; a dead button is worse than no button.
64
+ `check_checkout_readiness()` (buying) and `check_browse_readiness()`
65
+ (listing with `list_items()`) return a `SummerResult`. *Why:* editor play,
66
+ guest sessions and older Summer apps do not offer purchases; a dead button
67
+ is worse than no button.
58
68
 
59
69
  ## Result states
60
70
 
61
- Both signals deliver a status with `state`:
71
+ `checkout_status_changed(status: SummerCheckoutStatus)` delivers a status
72
+ whose `state` is one of:
62
73
 
63
74
  | State | Meaning | What the game does |
64
75
  | --- | --- | --- |
65
76
  | `awaiting_approval` | the Summer sheet is open | nothing; optionally dim the shop |
66
- | `pending` | payment or debit in progress | show "processing" |
67
- | `purchased` | done; Sparks debited (checkout) or credited (Sparks) | checkout: refresh items and unlock from inventory. Sparks: say "Sparks added", let the player buy again |
77
+ | `pending` | the debit is in progress | show "processing" |
78
+ | `purchased` | done; Sparks debited, `status.receipt` is set | refresh items and unlock from inventory |
68
79
  | `declined` | the player closed the sheet | return to the shop quietly |
69
- | `expired` | the sheet or payment timed out | same as declined |
70
- | `rejected` | refused; read `error_code` | checkout with `insufficient_funds`: offer "Get Sparks". Sparks: `provider_rejected`, say the payment did not go through |
80
+ | `expired` | the sheet timed out | same as declined |
81
+ | `rejected` | refused; read `error_code` | `insufficient_funds`: "Not enough Sparks. Get Sparks in the Summer app." Other codes: "The purchase was not completed." |
71
82
  | `manual_review` | platform is checking it | "We are confirming this purchase" |
72
- | `refunded` | Sparks only: a credited purchase was refunded or charged back (`full_refund`, `dispute_lost`) | nothing to undo; Summer already took the Sparks back. Refresh any balance display |
73
83
 
74
- A Sparks request can also fail to start with `purchase_limit` (the player
75
- reached the daily in-game Sparks limit) or `age_required` / `adult_required`
76
- (only adults with a saved age can buy Sparks). Show a short message and no
77
- retry button. *Why:* both limits are platform policy; retrying changes nothing.
84
+ `status.is_terminal()` tells you whether the state can still change. Unknown
85
+ future `error_code` values never mean success.
78
86
 
79
- The operation returned by each call also completes (`completed` signal) on
80
- `purchased` (ok) or a terminal failure with codes such as `checkout_declined`
81
- or `sparks_purchase_declined`. The status signal is the one to build UI on.
87
+ The operation returned by `request_checkout` also completes: ok on
88
+ `purchased`, a failure otherwise. The status signal is the one to build UI on.
82
89
 
83
90
  ## Minimal complete example
84
91
 
85
92
  ```gdscript
86
93
  extends Node
87
- ## A one-item shop. Buys `offer_id`; if the player is short, lets them buy
88
- ## Sparks through the Summer sheet, then they press Buy again.
94
+ ## A one-item shop. Buys `offer_id` and unlocks from inventory.
89
95
 
90
96
  @export var offer_id := "offer_sword"
91
97
  @export var definition_id := "item_sword"
92
- @export var price_sparks := 250 # display only; the platform charges its own price
93
98
 
94
99
  signal message(text: String)
95
100
  var _seen := {}
96
101
 
97
102
  func _ready() -> void:
98
- var store := Summer.client.store
99
- store.checkout_status_changed.connect(_on_checkout)
100
- store.sparks_purchase_status_changed.connect(_on_sparks)
103
+ if Summer.client == null:
104
+ return # not running as a player's game
105
+ Summer.client.store.checkout_status_changed.connect(_on_checkout)
101
106
  Summer.client.items.refreshed.connect(_sync_owned)
102
107
  _sync_owned()
103
108
 
@@ -108,13 +113,7 @@ func buy() -> void:
108
113
  var lines: Array[SummerCheckoutLine] = [SummerCheckoutLine.create(offer_id, 1)]
109
114
  Summer.client.store.request_checkout(lines) # result arrives in _on_checkout
110
115
 
111
- func get_sparks() -> void:
112
- if not Summer.client.store.check_sparks_purchase_readiness().ok:
113
- message.emit("Get Sparks from the Summer app.")
114
- return
115
- Summer.client.store.request_sparks_purchase(price_sparks) # hint only
116
-
117
- func _first_time(status) -> bool:
116
+ func _first_time(status: SummerCheckoutStatus) -> bool:
118
117
  var key := "%s:%d" % [status.request_id, status.version]
119
118
  if _seen.has(key):
120
119
  return false
@@ -129,7 +128,7 @@ func _on_checkout(status: SummerCheckoutStatus) -> void:
129
128
  Summer.client.items.refresh() # ownership comes from inventory
130
129
  &"rejected":
131
130
  if status.error_code == &"insufficient_funds":
132
- message.emit("Not enough Sparks.") # show a Get Sparks button -> get_sparks()
131
+ message.emit("Not enough Sparks. Get Sparks in the Summer app, then buy again.")
133
132
  else:
134
133
  message.emit("The purchase was not completed.")
135
134
  &"manual_review":
@@ -137,84 +136,91 @@ func _on_checkout(status: SummerCheckoutStatus) -> void:
137
136
  &"declined", &"expired":
138
137
  pass
139
138
 
140
- func _on_sparks(status: SummerSparksPurchaseStatus) -> void:
141
- if not _first_time(status):
142
- return
143
- match status.state:
144
- &"purchased":
145
- message.emit("%d Sparks added." % status.receipt.sparks_credited)
146
- &"rejected":
147
- message.emit("The payment did not go through.")
148
- &"manual_review":
149
- message.emit("We are confirming your payment.")
150
-
151
139
  func _sync_owned() -> void:
152
140
  if Summer.client.items.has_owned_definition(definition_id):
153
141
  message.emit("Sword unlocked.") # the only place the item is granted
154
142
  ```
155
143
 
156
- After Sparks are added the player presses Buy again. That is a new checkout
157
- and Summer shows it again. *Why:* the player confirms each spend separately.
158
-
159
144
  ## Multiplayer: granting an effect on a dedicated authority
160
145
 
161
- Buying Sparks only adds Sparks to the wallet. Anything the Sparks buy
162
- ("Grow now", "Summon rain") is a separate checkout, and on a server the
163
- authority must never grant it because a client says it paid. *Why:* a client
164
- message is the one thing a cheater controls.
165
-
166
- Model the effect as a consumable catalog item with instance ownership (each
167
- purchase is a separate item, not a stack). *Why:* a stackable (quantity) item
168
- shares one owned item ID across purchases, so the ID could not tell two
169
- purchases apart. The client checks out; the receipt's `owned_item_ids` are item instances in
170
- the player's Summer inventory. The client sends the authority only that owned
171
- item ID. The authority consumes it with
172
- `Summer.authority.items.consume(session, owned_item_id, idempotency_key)` and
173
- applies the effect only after a successful receipt. *Why:* the platform ledger
174
- consumes an instance item once, so the same item cannot be redeemed twice, in
175
- this world or any other.
146
+ Anything the Sparks buy ("Grow now", "Summon rain") is a checkout for an
147
+ item, and on a server the authority must never grant it because a client says
148
+ it paid. *Why:* a client message is the one thing a cheater controls.
149
+
150
+ Model the effect as a catalog item with instance ownership (each purchase is a
151
+ separate owned item, not a stack). *Why:* a stackable item shares one owned
152
+ item ID across purchases, so the ID could not tell two purchases apart.
153
+
154
+ The client checks out; the receipt's `items[i].owned_item_ids` are the owned
155
+ items. The client sends the authority only an owned item ID. The authority:
156
+
157
+ 1. reads that player's inventory with
158
+ `Summer.authority.items.inventory_for_session(session)` and checks the
159
+ item is there, owned, and of the right definition;
160
+ 2. checks its own record of redeemed item IDs, kept in the player's secret
161
+ player data (`Summer.authority.player_data`), so the item grants once per
162
+ player in every world;
163
+ 3. writes the new record, and applies the effect only after that write
164
+ succeeds.
176
165
 
177
166
  ```gdscript
178
- # On the dedicated authority. `redeemed` is saved with the world.
167
+ # On the dedicated authority.
168
+ const EFFECT_DEFINITION := "item_grow_now"
169
+
179
170
  func redeem(session: SummerSession, owned_item_id: String) -> void:
171
+ var inv := Summer.authority.items.inventory_for_session(session)
172
+ var read: SummerResult = await inv.get_result_or_completed_signal()
173
+ if not read.ok:
174
+ return # unavailable: the client may ask again later
175
+ var item: SummerInventoryItem = null
176
+ for candidate in inv.items:
177
+ if candidate.get_owned_item_id() == owned_item_id:
178
+ item = candidate
179
+ if item == null or not item.is_owned() or item.get_definition_id() != EFFECT_DEFINITION:
180
+ return # not owned by this player: never grant
181
+
182
+ var load_op := Summer.authority.player_data.load(session)
183
+ var loaded: SummerResult = await load_op.get_result_or_completed_signal()
184
+ if not loaded.ok:
185
+ return
186
+ var data: Dictionary = load_op.secret_record.data
187
+ var redeemed: Dictionary = data.get("redeemed", {})
180
188
  if redeemed.has(owned_item_id):
181
189
  return # a repeated message grants once
182
- var key := "redeem_" + owned_item_id.sha256_text().substr(0, 40)
183
- var op := Summer.authority.items.consume(session, owned_item_id, key)
184
- var result: SummerResult = await op.get_result_or_completed_signal()
185
- if not result.ok:
186
- # item_not_found: not owned or already used, never grant.
187
- # unavailable: retry later with the same key.
188
- return
189
- redeemed[owned_item_id] = op.command_id # record, apply, then save the world
190
+ redeemed[owned_item_id] = true
191
+ data["redeemed"] = redeemed
192
+ var save_id := "redeem_" + owned_item_id.sha256_text().substr(0, 40)
193
+ var commit := Summer.authority.player_data.commit_secret(session, save_id, data)
194
+ var saved: SummerResult = await commit.get_result_or_completed_signal()
195
+ if not saved.ok:
196
+ return # retryable: commit again with the same save_id
190
197
  apply_grow_now(session)
191
198
  ```
192
199
 
193
- - Derive the key from the owned item ID. *Why:* the same key returns the same
194
- receipt (`op.replayed` is true), so a retry after a timeout never consumes
195
- twice, and a different item with that key fails with `idempotency_conflict`.
196
- - Apply the effect when the receipt arrives even if `op.replayed` is true, as
197
- long as `redeemed` does not have the item. *Why:* a replay means the item was
198
- consumed earlier but your world may have crashed before saving the effect.
199
- - Save the world right after applying. *Why:* the receipt replay is tied to
200
- the authority process; after a restart as a different process the same key
201
- is a new consumption and the already-consumed item returns `item_not_found`.
200
+ - The save ID comes from the owned item ID. *Why:* resending the same save ID
201
+ with the same content returns the earlier commit (`commit.replayed`), so a
202
+ retry after a timeout records the redemption once.
203
+ - Apply the effect when the commit succeeds even if `commit.replayed` is true
204
+ and the effect is not in your world yet. *Why:* a replay means the
205
+ redemption was recorded earlier, but your world may have crashed before it
206
+ applied the effect.
207
+ - The engine has no call that uses up an owned item. The item stays in the
208
+ player's inventory after it is redeemed; the redeemed list is what stops a
209
+ second grant. Show redeemed items as used in your UI.
202
210
 
203
211
  ## Idempotency
204
212
 
205
- The SDK generates the request ID and replays it across transport failures and
206
- app restarts; the platform credits a Sparks purchase once per provider
207
- transaction and debits a checkout once per request. Your only job is the
208
- `request_id:version` dedupe above, so a replayed signal does not show a
209
- message or play an effect twice.
213
+ The SDK generates the checkout request ID and replays it across transport
214
+ failures and app restarts; the platform debits a checkout once per request.
215
+ Your only job is the `request_id:version` dedupe above, so a replayed signal
216
+ does not show a message or play an effect twice.
210
217
 
211
218
  ## Per-surface behaviour
212
219
 
213
- - **Summer Games app on iOS:** a native Summer sheet over the game. Sparks are
214
- bought with Apple In-App Purchase only.
215
- - **Summer desktop app:** an overlay sheet; Sparks are paid with Stripe in the
216
- browser, and the result returns to the game on the signal.
217
- - **Web:** the same sheet in the page, paid with Stripe.
220
+ - **Summer Games app on iOS:** a native Summer sheet over the game.
221
+ - **Summer desktop app:** an overlay sheet; the result returns to the game on
222
+ the signal.
223
+ - **Web:** the same sheet in the page.
218
224
  - **Editor play and local runs:** readiness is `unavailable`. Build the
219
225
  unavailable path first.
220
226
 
@@ -222,31 +228,30 @@ The game code is identical on every surface.
222
228
 
223
229
  ## Testing
224
230
 
225
- - Test against Summer staging, which runs payments in sandbox mode: Stripe
226
- sandbox cards on web and desktop, an Apple Sandbox tester account on iOS test
227
- builds. *Why:* no real money moves and results are identical to production.
231
+ - Test against Summer staging, which runs in sandbox mode. *Why:* no real
232
+ money moves and results are identical to production.
228
233
  - Walk every state: decline the sheet, let it expire, buy with too few
229
- Sparks, buy Sparks, then buy the item. Kill the game while a payment is
230
- pending and relaunch; the result must still arrive once.
234
+ Sparks, get Sparks in the Summer app, then buy the item. Kill the game while
235
+ a checkout is pending and relaunch; the result must still arrive once.
231
236
  - Verify the unlock comes from `items.refreshed`, not from the checkout
232
237
  signal.
233
238
 
234
239
  ## Store policy notes
235
240
 
236
241
  - Apple requires In-App Purchase for digital goods and currency sold inside
237
- iOS apps. Summer's iOS sheet handles this; never add your own payment links,
238
- web prices or "cheaper on the web" text inside the game. *Why:* it can get
239
- the Summer app removed from the App Store for every game.
242
+ iOS apps. Summer's iOS sheet and app handle this; never add your own payment
243
+ links, web prices or "cheaper on the web" text inside the game. *Why:* it
244
+ can get the Summer app removed from the App Store for every game.
240
245
  - Randomized paid rewards must disclose their odds before purchase (Apple and
241
246
  most regions require it).
242
247
  - Say what a purchase gives in plain words next to the price.
243
248
 
244
249
  ## Availability
245
250
 
246
- `request_checkout` ships today. `request_sparks_purchase` needs a Summer
247
- Engine and Summer app release that advertise `player.store.sparks_purchase@1`;
248
- until then `check_sparks_purchase_readiness()` reports `unavailable` and the
249
- example above falls back to "Get Sparks from the Summer app".
251
+ `request_checkout`, `list_items`, `Summer.client.items`,
252
+ `Summer.authority.items.inventory_for_session` and `Summer.authority.player_data`
253
+ ship in the engine today. Buying Sparks inside the game does not: players get
254
+ Sparks in the Summer app.
250
255
 
251
256
  ## See also
252
257
 
@@ -1,10 +1,10 @@
1
1
  id: skill/in-game-purchases
2
2
  kind: skill
3
3
  version: 1.0.0
4
- summary: "Sell items for Sparks and let short players buy Sparks in game via the Summer purchase sheet: checkout, Sparks purchase, results, testing, store policy."
4
+ summary: "Sell items for Sparks in a game shop via the Summer purchase sheet: checkout, results, not enough Sparks, inventory unlocks, authority grants, store policy."
5
5
  use_when:
6
6
  - "adding a shop, buy button or paid item to a Summer game and wiring request_checkout"
7
- - "a checkout was rejected with insufficient_funds and the player should be able to buy Sparks without leaving the game"
7
+ - "the player does not have enough Sparks (checkout rejected with insufficient_funds) and the game should tell them how to buy more Sparks"
8
8
  - "deciding whether an item should be sold for Sparks, or handling pending, declined, expired, rejected or manual_review purchase results"
9
9
  do_not_use_when:
10
10
  - "designing multiplayer state ownership unrelated to purchases (use multiplayer-state)"
@@ -197,7 +197,11 @@ writes `rejection.json`; its codes are in [export-errors.md](export-errors.md).
197
197
  `gameId` and section `builds`, or summerengine.com → Studio → your game →
198
198
  Builds.
199
199
  3. Upload the `.zip` as a new Build. Summer checks it again: unknown fields,
200
- queue rules, both packs, and the composition match.
200
+ queue rules, both packs, and the composition match. From an agent:
201
+ `summer_export_game`, then `summer_publish_build` with `gameId` and
202
+ `clientVersion`; it asks for confirmation first and needs
203
+ `summer login --store`. If it answers `store_auth_refused`, upload in
204
+ Studio.
201
205
  4. Release the Build to an environment. Its queues go live there, and
202
206
  clients join them with `Summer.client.join(SummerJoinTarget.queue(...))`.
203
207
  The code is the same as in Local Play.
@@ -22,7 +22,7 @@ Rigid skill: follow the rules below exactly.
22
22
  | The user wants to… | Do |
23
23
  |---|---|
24
24
  | **see / check / decide** — plan and billing (money), their published games and releases, usage, pricing, the MCP setup guide for their agent, docs, the scene or node you just built ("show me") | **Open the surface** with `summer_open` |
25
- | **get a result** — add a node, set a property, import an asset, run the game, publish | **Act through Summer tools** (mutation tools, `summer_play`, `summer_creator_publish`); do not open the editor to do by hand what a tool does |
25
+ | **get a result** — add a node, set a property, import an asset, run the game, publish | **Act through Summer tools** (mutation tools, `summer_play`, `summer_publish_build`); do not open the editor to do by hand what a tool does |
26
26
  | **both** ("add a camera and show me") | act first, then open the scene / select the node so the user sees the result |
27
27
 
28
28
  Never open a browser or switch the editor's tab as a side effect of building. Opening is a user-visible action — do it because the user asked to look, and say what will open before it opens.
@@ -24,6 +24,7 @@ related:
24
24
  - skill/art-direction
25
25
  - skill/tune-performance
26
26
  - skill/ui-basics
27
+ - skill/compatibility-renderer-traps
27
28
  source: official
28
29
  license: MIT
29
30
  status: preview