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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -2
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/CHANGELOG.md +61 -0
- package/README.md +7 -4
- package/assets/api-docs.json.gz +0 -0
- package/dist/cli/commands/install.d.ts +10 -0
- package/dist/cli/commands/install.js +55 -10
- package/dist/cli/commands/login.js +12 -0
- package/dist/cli/commands/publish.js +2 -1
- package/dist/cli/commands/tool.d.ts +7 -0
- package/dist/cli/commands/tool.js +36 -1
- package/dist/core/auth.js +3 -0
- package/dist/core/capabilities/audit/args.d.ts +14 -14
- package/dist/core/capabilities/capture-gameplay.d.ts +69 -0
- package/dist/core/capabilities/capture-gameplay.js +283 -0
- package/dist/core/capabilities/creator.js +1 -1
- package/dist/core/capabilities/doctor.d.ts +9 -1
- package/dist/core/capabilities/doctor.js +47 -4
- package/dist/core/capabilities/engine-run.d.ts +12 -0
- package/dist/core/capabilities/engine-run.js +61 -0
- package/dist/core/capabilities/export-engine-version.d.ts +15 -0
- package/dist/core/capabilities/export-engine-version.js +24 -0
- package/dist/core/capabilities/export-game.d.ts +107 -0
- package/dist/core/capabilities/export-game.js +445 -0
- package/dist/core/capabilities/export-presets.d.ts +50 -0
- package/dist/core/capabilities/export-presets.js +177 -0
- package/dist/core/capabilities/export-templates.d.ts +118 -0
- package/dist/core/capabilities/export-templates.js +291 -0
- package/dist/core/capabilities/image-generation.d.ts +9 -0
- package/dist/core/capabilities/image-generation.js +19 -1
- package/dist/core/capabilities/publish-build.d.ts +77 -0
- package/dist/core/capabilities/publish-build.js +500 -0
- package/dist/core/capabilities/publish-download.d.ts +37 -0
- package/dist/core/capabilities/publish-download.js +168 -0
- package/dist/core/capabilities/runtime-control.d.ts +10 -10
- package/dist/core/capabilities/summer-bundle.d.ts +49 -0
- package/dist/core/capabilities/summer-bundle.js +156 -0
- package/dist/core/capabilities/tool-dispatch.js +43 -1
- package/dist/core/capabilities/ui-control.d.ts +2 -2
- package/dist/core/config.js +2 -2
- package/dist/core/engine-install.d.ts +16 -0
- package/dist/core/engine-install.js +32 -2
- package/dist/core/hosted-mcp.d.ts +115 -0
- package/dist/core/hosted-mcp.js +164 -0
- package/dist/core/launch-posture.d.ts +3 -0
- package/dist/core/launch-posture.js +15 -2
- package/dist/core/library-read.d.ts +2 -2
- package/dist/core/oauth.d.ts +78 -0
- package/dist/core/oauth.js +309 -0
- package/dist/core/util/zip-write.d.ts +18 -0
- package/dist/core/util/zip-write.js +112 -0
- package/dist/core/util/zip.d.ts +22 -0
- package/dist/core/util/zip.js +140 -0
- package/dist/mcp/hosted-mount.d.ts +50 -0
- package/dist/mcp/hosted-mount.js +200 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +14 -6
- package/dist/mcp/tools/creator-tools.js +64 -1
- package/dist/mcp/tools/generate-tools.js +12 -2
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +7 -3
- package/library/skills/README.md +6 -4
- package/library/skills/bounded-engine-runs/SKILL.md +96 -0
- package/library/skills/bounded-engine-runs/resource.yaml +29 -0
- package/library/skills/compatibility-renderer-traps/SKILL.md +78 -0
- package/library/skills/compatibility-renderer-traps/resource.yaml +28 -0
- package/library/skills/concept-art/SKILL.md +2 -2
- package/library/skills/export-and-ship/SKILL.md +4 -0
- package/library/skills/headless-scripting/resource.yaml +1 -0
- package/library/skills/in-game-purchases/SKILL.md +123 -118
- package/library/skills/in-game-purchases/resource.yaml +2 -2
- package/library/skills/multiplayer-publish/SKILL.md +5 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/web-compatibility-look/resource.yaml +1 -0
- package/library/tools/capture-gameplay/resource.yaml +73 -0
- package/library/tools/creator-publish/resource.yaml +4 -3
- package/library/tools/creator-releases/resource.yaml +1 -1
- package/library/tools/export-game/resource.yaml +72 -0
- package/library/tools/export-templates/resource.yaml +63 -0
- package/library/tools/generate-image/resource.yaml +15 -1
- package/library/tools/open/resource.yaml +1 -1
- package/library/tools/publish-build/resource.yaml +78 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +4 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +241 -19
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +4 -2
- package/registry/generated/plugin.codex.json +5 -3
- package/registry/generated/plugin.cursor.json +4 -2
- package/registry/generated/plugin.factory.json +4 -2
- package/registry/generated/skills-index.md +6 -4
- 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
|
-
|
|
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.
|
|
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".
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: in-game-purchases
|
|
3
|
-
description: "Sell items for Sparks
|
|
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
|
|
15
|
-
purchase sheet over the game, and the game reacts to the
|
|
16
|
-
result.
|
|
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 |
|
|
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
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
client-supplied price is exactly what a
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
51
|
-
|
|
52
|
-
|
|
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 `
|
|
56
|
-
a `SummerResult`. *Why:* editor play,
|
|
57
|
-
do not offer purchases; a dead 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
|
-
|
|
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` |
|
|
67
|
-
| `purchased` | done; Sparks debited
|
|
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
|
|
70
|
-
| `rejected` | refused; read `error_code` |
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
80
|
-
`purchased
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
store.
|
|
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
|
|
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.
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
var
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
- Apply the effect when the
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
206
|
-
app restarts; the platform
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
226
|
-
|
|
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,
|
|
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
|
|
238
|
-
web prices or "cheaper on the web" text inside the game. *Why:* it
|
|
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`
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
-
- "
|
|
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`, `
|
|
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.
|