@hypersoniclabs/helix-mcp 0.2.5 → 0.2.12

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 (98) hide show
  1. package/README.md +81 -11
  2. package/dist/continuumCanary.d.ts +17 -0
  3. package/dist/continuumCanary.js +17 -0
  4. package/dist/continuumCanary.js.map +1 -0
  5. package/dist/server.d.ts +14 -1
  6. package/dist/server.js +4058 -162
  7. package/dist/server.js.map +1 -1
  8. package/dist/tsconfig.build.tsbuildinfo +1 -1
  9. package/dist/vehicleTools.d.ts +86 -0
  10. package/dist/vehicleTools.js +229 -0
  11. package/dist/vehicleTools.js.map +1 -0
  12. package/docs/avatar-face.md +115 -0
  13. package/docs/bridge.md +98 -0
  14. package/docs/bring-your-world.md +117 -0
  15. package/docs/catalog.md +69 -1
  16. package/docs/character-animation.md +442 -0
  17. package/docs/character-attachments.md +166 -0
  18. package/docs/character-world.md +785 -130
  19. package/docs/continuum.md +153 -0
  20. package/docs/items.md +73 -0
  21. package/docs/lighting-world.md +667 -0
  22. package/docs/locomotion-clip-spec.md +294 -0
  23. package/docs/manifest.md +31 -6
  24. package/docs/multiplayer-logic.md +460 -17
  25. package/docs/multiplayer-templates/chrono-orchard.md +36 -22
  26. package/docs/multiplayer-templates/collect-a-thon.md +28 -38
  27. package/docs/multiplayer-templates/collections.md +24 -24
  28. package/docs/multiplayer-templates/hangout.md +124 -111
  29. package/docs/multiplayer-templates/npc-wave.md +310 -0
  30. package/docs/multiplayer-templates/obby.md +13 -16
  31. package/docs/multiplayer-templates/persistent-progress.md +218 -0
  32. package/docs/multiplayer-templates/physics-bumper.md +27 -9
  33. package/docs/multiplayer-templates/physics-football.md +22 -8
  34. package/docs/multiplayer-templates/relic-bearers.md +12 -15
  35. package/docs/multiplayer-templates/server-motion.md +16 -19
  36. package/docs/multiplayer-templates/shooter-range.md +275 -0
  37. package/docs/multiplayer-templates/team-control.md +28 -15
  38. package/docs/multiplayer-templates/turn-arena.md +31 -22
  39. package/docs/multiplayer-templates/voice-radio.md +166 -0
  40. package/docs/multiplayer-templates/wave-survival.md +7 -8
  41. package/docs/multiplayer-templates/world-shop.md +240 -0
  42. package/docs/multiplayer-world.md +222 -129
  43. package/docs/npc-world.md +623 -0
  44. package/docs/publishing.md +108 -28
  45. package/docs/purchases.md +223 -0
  46. package/docs/scene-performance.md +64 -0
  47. package/docs/screenshots.md +140 -0
  48. package/docs/sdk.md +324 -5
  49. package/docs/shooter-worlds.md +537 -0
  50. package/docs/terrain.md +173 -0
  51. package/docs/upgrades.md +324 -0
  52. package/docs/vehicles.md +727 -0
  53. package/docs/world-inspect.md +156 -0
  54. package/docs/world-look.md +241 -0
  55. package/docs/world-recipe.md +65 -6
  56. package/package.json +15 -4
  57. package/skills/README.md +91 -0
  58. package/skills/helix-assets/SKILL.md +491 -0
  59. package/skills/helix-assets/references/asset-sources.md +143 -0
  60. package/skills/helix-assets/references/vault-api.md +105 -0
  61. package/skills/helix-avatar-qa/SKILL.md +85 -0
  62. package/skills/helix-avatars/SKILL.md +206 -0
  63. package/skills/helix-avatars/references/contract.md +166 -0
  64. package/skills/helix-avatars/references/dynamics.md +367 -0
  65. package/skills/helix-avatars/references/face.md +50 -0
  66. package/skills/helix-avatars/references/publish.md +76 -0
  67. package/skills/helix-avatars/references/qa.md +251 -0
  68. package/skills/helix-avatars/references/rigging.md +88 -0
  69. package/skills/helix-avatars/references/source-generated.md +190 -0
  70. package/skills/helix-avatars/references/source-model.md +90 -0
  71. package/skills/helix-avatars/references/source-rigid.md +90 -0
  72. package/skills/helix-avatars/references/source-vrm.md +61 -0
  73. package/skills/helix-gauntlet/SKILL.md +128 -0
  74. package/skills/helix-multiplayer/SKILL.md +150 -0
  75. package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
  76. package/skills/helix-vehicles/SKILL.md +218 -0
  77. package/skills/helix-vehicles/references/addons.md +212 -0
  78. package/skills/helix-vehicles/references/appearance.md +339 -0
  79. package/skills/helix-vehicles/references/audio-import.md +138 -0
  80. package/skills/helix-vehicles/references/audio.md +580 -0
  81. package/skills/helix-vehicles/references/cabin.md +225 -0
  82. package/skills/helix-vehicles/references/host-manifest.md +174 -0
  83. package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
  84. package/skills/helix-vehicles/references/publish.md +214 -0
  85. package/skills/helix-vehicles/references/qa.md +177 -0
  86. package/skills/helix-vehicles/references/reference-package.json +3481 -0
  87. package/skills/helix-vehicles/references/reference-package.md +69 -0
  88. package/skills/helix-vehicles/references/source-beamng.md +167 -0
  89. package/skills/helix-vehicles/references/source-concept.md +38 -0
  90. package/skills/helix-vehicles/references/source-model.md +100 -0
  91. package/skills/helix-vehicles/references/source-scratch.md +60 -0
  92. package/skills/helix-world-build/SKILL.md +376 -0
  93. package/skills/helix-world-build/references/config-gates.md +104 -0
  94. package/skills/helix-world-director/SKILL.md +210 -0
  95. package/skills/helix-world-qa/SKILL.md +371 -0
  96. package/skills/helix-world-qa/references/perf-budgets.md +240 -0
  97. package/skills/helix-world-qa/references/perf-handle.md +125 -0
  98. package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
@@ -1,5 +1,10 @@
1
1
  # Publishing to HELIX Instant
2
2
 
3
+ `publish_world` / `helix publish` now automatically capture a ready-world cover when no selected custom thumbnail exists. The image is verified and assigned atomically with the Build; capture failure refuses publication before World/Build writes. Register a `hero` shot and call `screenshots.ready` after assets load. Automatic covers refresh per Build; selected custom covers are preserved. Pass `thumbnail` / `--thumbnail` to select a cover without automatic capture. Manual capture/inspect/attach below remains the workflow for selecting custom media and optional preview images.
4
+
5
+ Native Scene v2, semantic Continuum Releases and Unreal/shared-runtime promotion currently require a selected custom cover. They explicitly fail with `WORLD_THUMBNAIL_CAPTURE_UNSUPPORTED` if no cover exists; automatic capture adapters for those runtimes are not integrated. This is a publication failure, never a successful generic placeholder.
6
+
7
+
3
8
  ## The pipeline
4
9
 
5
10
  `publish_world` runs the full creator pipeline against the platform:
@@ -10,6 +15,64 @@
10
15
  4. **Finalize** — the platform verifies every declared file landed byte-exact, then atomically activates the build. A draft world auto-publishes on its first successful build.
11
16
  5. You get back the **play URL** — shareable, instant, no install.
12
17
 
18
+ ## Native platform updates
19
+
20
+ New system-bearing web Worlds publish with platform systems outside the creator
21
+ bundle. Compatible `^`/`~` ranges receive safe native character updates—such as
22
+ emotes, the emote customizer, standard camera behavior, pointing, and avatar
23
+ presentation—on a fresh launch without rebuilding or republishing the World.
24
+ **Engine systems are ranges.** Declare `^`/`~` lines (`"humanoid-character": "^0.3"`,
25
+ `"engine-core": "^0.1"`, `"scene": "^0.3"`, `"visual": "^0.1"`); never an exact `x.y.z`. An
26
+ exact version freezes the World on one immutable build, so `validate_world` and
27
+ `publish_world` **refuse** it (an exact `systems` value, a `helix.runtime.json` system with
28
+ `mode: "pinned"`, or an `@helix/*` import-map entry aimed at an immutable
29
+ `/packages/<id>/<rev>/` URL). If an engine fix is published but not promoted, **promote it**
30
+ (`POST /api/v1/instant-packages/<slug>/versions/<version>/rollout`
31
+ `{"rolloutPercent":100,"reason":"…"}`) — never pin a World to get it. The only override is
32
+ the human running the CLI with `helix publish dist --allow-exact-engine-pin "<reason>"`: the
33
+ reason is required, a loud warning is printed, and the override is logged to
34
+ `~/.helix/engine-pin-overrides.jsonl`. Agents do not use it.
35
+
36
+ This does not hot-swap arbitrary creator code: the creator SDK, installed
37
+ abilities, game code, and game assets remain in the immutable World build.
38
+ Feature-level refusals also remain available without pinning everything. Every
39
+ native avatar feature — emotes, the radial wheel and its Customize door, x-ray,
40
+ pointing, sitting, the avatar camera, the two camera legs, and universal-avatar
41
+ bodies — is ON in every character World, and a World names the ones it does not
42
+ want in `character.native.disabled` at create time (a blocklist, never an
43
+ allow-list, so features added later still arrive; `universal-avatar` is the
44
+ all-or-nothing top switch). That is runtime config, not manifest, and not a
45
+ publish-time concern — full reference: `read_doc({ name: "character-world" })`
46
+ section 8g, which carries the humanoid-character version this surface needs;
47
+ an older runtime ignores the key silently. Validation and the publish server both reject legacy bundled system
48
+ delivery, unsafe `latest`/`*` ranges, and import-map drift.
49
+
50
+ Older source-less bundles are never rewritten. Recover the source, run the
51
+ current `helix install --update`, rebuild, validate, and republish.
52
+
53
+ ## Source upload is OPT-IN, and it is the human's decision
54
+
55
+ A published world ships only its **built bundle**. Nothing else is kept, so the world cannot be
56
+ opened and edited in the website editor later — there is nothing there to edit.
57
+
58
+ `publish_world({ uploadSource: true })` additionally sends the project's source, on a separate
59
+ **private** channel, which is what makes that later edit possible.
60
+
61
+ - **Off by default.** Source is never uploaded unless you set it.
62
+ - **ASK THE HUMAN FIRST.** This sends their code to the platform. Some creators deliberately do not
63
+ want that, and it is not a decision an agent makes on their behalf. Do not set it because it
64
+ seems helpful.
65
+ - **What is sent**: the project directory, honouring `.gitignore`, always excluding `node_modules`,
66
+ `.git`, `dist` and `.vite`. Compressed cap 128 MiB, checked before anything is uploaded.
67
+ - **Which directory**: `directory` is the BUILT bundle, so the source defaults to its parent. Pass
68
+ `sourceDir` only when the project root is somewhere else. `sourceDir` on its own is refused, not
69
+ silently ignored.
70
+ - **Nothing about the build changes.** The published bundle, its files, its URLs and its behaviour
71
+ are identical either way. The source never becomes part of the build — build files are public and
72
+ long-cached; source is not.
73
+ - The publish response says which happened, every time. Relay it: a creator who published without
74
+ source should know the option exists.
75
+
13
76
  ## Login (human-in-the-loop)
14
77
 
15
78
  Publishing requires a creator account session. The sign-in itself is human (browser + website), but you drive it: call `start_login` — the human's browser opens to the HELIX sign-in page (relay the returned URL if it doesn't) — then poll `check_login` until it reports success. The minted token is saved locally by the MCP process and never enters the conversation.
@@ -28,6 +91,9 @@ Creator access is currently invite-only (curated launch); the account must have
28
91
  - Validation errors list every problem at once — fix all of them, rebuild, re-validate.
29
92
  - `409` on world creation: the slug is taken by another creator. Choose a different slug in `helix.json`.
30
93
  - "missing upload" / "size mismatch" at finalize: the build directory changed between validate and publish — rebuild and re-publish.
94
+ - A bundle refused for containing `helix-sdk/dev-shell`: world code imported the `helix dev` simulator. It is
95
+ the CLI's half of local testing, never a world API — remove the import; the world talks to whatever shell
96
+ embeds it, simulated or real, through the ordinary `Helix.*` surface.
31
97
  - `401`/`403`: login expired or the account lacks the creator flag — back to the human.
32
98
 
33
99
  ## After publishing
@@ -35,41 +101,55 @@ Creator access is currently invite-only (curated launch); the account must have
35
101
  - The publish response returns the canonical **play URL** — share that link directly (don't construct portal URLs by hand; they differ per environment).
36
102
  - Title, content rating, mobile support, and `requiresAuth` on the world page all come from the manifest — re-publish to update them.
37
103
 
104
+ ## Achievements are registered separately (and BEFORE the rules that award them)
105
+
106
+ A world's badges are a per-world registry the bundle cannot express: `helix.json` has no achievements block, and
107
+ publish cannot check that an awarded key exists — so an `awardAchievement` rule naming an unregistered key ships clean
108
+ and silently grants nobody anything. Register with `register_achievement` / `helix achievement register <slug>` (a 2D
109
+ icon file is required), read the registry back with `list_achievements`, and only then publish the world whose rules
110
+ name those keys. Test the unlock BEFORE registering: the same rows (no icon needed) seed `.helix/dev-achievements.json`
111
+ for `helix dev`, where criteria unlock off simulated evidence as you play. Registering on an already-live world takes
112
+ effect immediately — no republish — and runs a backfill crediting players who already qualify. Full model:
113
+ `multiplayer-logic` §19.
114
+
115
+ ## In-world products are registered separately too (and BEFORE the code that buys them)
116
+
117
+ What a world sells is a per-world registry the bundle cannot express either: `helix.json` has no products block, and
118
+ world code buys a product by the creator-authored key — `Helix.marketplace.purchaseProduct("speed_boost")` buys
119
+ `pkey:speed_boost`, and an unregistered key returns `ProductNotFound` at runtime. Register with
120
+ `register_world_product` / `helix product register <slug>` first, because the key is immutable once registered and the
121
+ price lives server-side only (a world never sends an amount). Author what the purchase hands over as a `grants`
122
+ effect array — pass | currency; several = a bundle, `[]` = a tip jar — which the backend fulfils **durably at
123
+ settle** and the world reads back with `Helix.purchases.getEntitlements` (spend balances via `Helix.purchases.consume`,
124
+ never a client-side counter); the effect list is as immutable as the key. Ordering against publish is practical, not
125
+ enforced: nothing refuses a registration on a draft world — only the all-worlds creator listing (`helix product list`
126
+ with no slug) filters to Published — but a **purchase** needs a live world (Published or Unlisted), so a product
127
+ registered on a draft is simply unbuyable until the world ships. What a registration DOES need is the world ROW, so on
128
+ a brand-new project run `create_world` / `helix world create --slug <slug> --title <title>` first — it mints the Draft
129
+ with no build that both registries hang off, and without it every registration fails with `No world of yours has the
130
+ slug ...`. Read the registry back with `list_world_products`. Test the whole loop first under `helix dev`
131
+ (`.helix/dev-products.json` seeds the catalog; nothing is charged), and note a registration on an already-live world
132
+ takes effect immediately — no republish.
133
+ Full API: `sdk` → `Helix.marketplace` + `Helix.purchases`.
134
+
135
+ World Products never mint universal items. For a portable Standard item or fixed-supply
136
+ Collectible, publish an item definition, create a Marketplace or World distribution, and have world
137
+ code request that distribution through the shell. Read `read_doc({ name: "items" })`; there is no raw
138
+ `grantItem(itemId)` path.
139
+
38
140
  ## Preview video (YouTube)
39
141
 
40
- A world's detail page shows a carousel: the **thumbnail is always the cover**, and an optional
41
- **preview video** plays as an extra slide. Set it after the world is published:
142
+ A world's detail page keeps its thumbnail as the cover and can show a YouTube
143
+ video as an additional carousel slide. Set or clear it through the creator CLI:
42
144
 
43
- ```
145
+ ```bash
44
146
  helix preview-video <slug> "https://www.youtube.com/watch?v=VIDEO_ID"
45
147
  helix preview-video <slug> --clear
46
148
  ```
47
149
 
48
- From an agent, the MCP tool is `set_preview_video({ slug, url })` / `set_preview_video({ slug, clear: true })`.
49
- From code, the SDK exports `setWorldPreviewVideo(slug, url, creds)` and `clearWorldPreviewVideo(slug, creds)`.
50
-
51
- **Video is referenced, never uploaded.** There is deliberately no way to upload an mp4/webm: the
52
- world's media slots take images only, and YouTube does the transcoding, adaptive streaming,
53
- moderation and bandwidth. We store an 11-character video id.
54
-
55
- Any usual link shape works — extra params like `&t=` and `&list=` are discarded:
56
-
57
- | accepted |
58
- | --- |
59
- | `https://www.youtube.com/watch?v=ID` |
60
- | `https://youtu.be/ID` |
61
- | `https://www.youtube.com/embed/ID` |
62
- | `https://www.youtube.com/shorts/ID` |
63
- | `https://www.youtube.com/live/ID` |
64
- | `ID` (a bare 11-character id) |
65
-
66
- The link is normalised and host-checked **server-side**, so a non-YouTube URL is rejected with a
67
- `400` rather than stored — there is no best-effort fallback. Don't pre-validate or rewrite the URL
68
- before sending it; pass what the human gave you and let the API be the one authority.
69
-
70
- > **Make the video earn its slide.** It sits alongside the thumbnail and screenshots, so a clip that
71
- > just re-shows the cover image gives players a carousel that says the same thing twice. Show
72
- > something the stills can't: movement, a mechanic, a moment.
150
+ Video is referenced, never uploaded. Pass the creator's YouTube URL or bare
151
+ video id through unchanged; the backend remains the authority for
152
+ normalization and validation.
73
153
 
74
154
  ## Keeping the toolchain current
75
155
 
@@ -0,0 +1,223 @@
1
+ # In-world purchases (IWP) — selling things from a world
2
+
3
+ How a world sells something and honours it afterwards. Four parties, and a world is only one of them:
4
+
5
+ | Who | Does what | You reach it with |
6
+ |---|---|---|
7
+ | **The creator** | registers the products the world may sell | `register_world_product` (MCP) / `helix product` (CLI) |
8
+ | **The shell** | raises the confirm popup, charges LIX, settles the sale | nothing — it is not yours to call |
9
+ | **The backend** | fulfils the `grants` durably at settle, into entitlements | nothing — it writes, you read |
10
+ | **Your world** | asks for a purchase, then reads what the player owns | `Helix.marketplace` · `Helix.purchases` · the `purchase` rule |
11
+
12
+ **Your world never handles money.** It names a product, and reads entitlements back. It cannot mint a product,
13
+ set a price, or write an entitlement — which is exactly why a player can trust it, and why the failure modes
14
+ below are all about *reading* the truth rather than *keeping* it.
15
+
16
+ Worked end-to-end world: `read_template({ name: "world-shop" })`. Room-side grammar:
17
+ `read_doc({ name: "multiplayer-logic" })` §20. SDK signatures: `read_doc({ name: "sdk" })`.
18
+
19
+ ## 1. Register the product BEFORE writing the code that buys it
20
+
21
+ `helix.json` has **no products block**, and nothing in world code can mint one. A world can only sell what its
22
+ creator registered **on that world**, so `purchaseProduct` on an unregistered key returns `ProductNotFound` at
23
+ runtime — which looks like a button that silently does nothing.
24
+
25
+ ```
26
+ register_world_product({
27
+ worldSlug: "my-world",
28
+ key: "coin-pack", // IMMUTABLE — world code buys `pkey:coin-pack`
29
+ title: "Coin Pack",
30
+ priceLix: 25, // the ONLY place a price lives
31
+ grants: [{ kind: "currency", code: "coin", amount: 500, display: "wallet" }],
32
+ })
33
+ ```
34
+
35
+ - `dryRun: true` validates the whole shape locally — no network, works logged out.
36
+ - The **key is immutable** (a rename would break every build naming it) and so is the **`grants` array** (it
37
+ would re-label every copy already sold). Choose both once.
38
+ - **Registration does not need a published world; buying does.** A product on a draft world exists and lists
39
+ fine, and refuses to sell until the world is Published or Unlisted.
40
+ - **It does need the world to exist.** On a brand-new project call **`create_world`** first (`helix world
41
+ create --slug <slug> --title <title>`): it mints the world row as a Draft with no build, which is all a
42
+ registration needs. Without it you get `No world of yours has the slug ...`.
43
+ - **On a LIVE world, registration takes effect immediately — no republish.** The registry is server-side and
44
+ worlds read it at runtime, so a new product is buyable the moment it registers; only the world CODE that
45
+ names the key ships via publish. Same JSON as your `.helix/dev-products.json` seed — test under `helix dev`
46
+ first, register second.
47
+ - `list_world_products` / `update_world_product` — update changes title, description, price and active state.
48
+ Never the key, never the grants.
49
+
50
+ ### The grant shapes
51
+
52
+ | `kind` | Fields | Hands over |
53
+ |---|---|---|
54
+ | `currency` | `code`, `amount`, `display: "wallet" \| "uses"` | adds to a named server-held balance |
55
+ | `pass` | `passKey` (defaults to the product key), `durationSeconds` | a server-held boolean; omit the duration for permanent |
56
+
57
+ Several effects in one array = a **bundle** in one charge. An empty array = a **tip jar**. `priceLix: 0` = a
58
+ free **Claim** the popup still confirms. Caps: **8 effects** per product, **16 distinct currency codes** per
59
+ world; `code`/`passKey` are lowercase slugs (`^[a-z0-9][a-z0-9_-]{0,63}$`).
60
+
61
+ **A permanent-pass-only product is capped at `maxPerUser: 1`** and registration refuses a higher value — a
62
+ re-buy would charge again and deliver nothing new. Add a timed pass or a currency effect to make it re-buyable.
63
+
64
+ World Products never mint universal items. For portable ownership, publish an item definition and create an
65
+ ItemDistribution; read `read_doc({ name: "items" })`. Legacy item-grant rows may still appear during
66
+ migration, but agents must not create new ones.
67
+
68
+ ## 2. Buy
69
+
70
+ ```ts
71
+ const result = await Helix.marketplace.purchaseProduct('coin-pack'); // a bare key becomes pkey:coin-pack
72
+ if (result.completed) refresh();
73
+ ```
74
+
75
+ The shell raises the popup and settles. `PurchaseResult.status` is the canonical outcome:
76
+
77
+ | Status | Means | `completed` |
78
+ |---|---|---|
79
+ | `Granted` | paid and delivered | ✅ |
80
+ | `Claimed` | free claim delivered | ✅ |
81
+ | `AlreadyOwned` | they already had it — **nothing charged, nothing new** | ✅ |
82
+ | `Pending` | not a final answer — re-read with `getPurchase` | — |
83
+ | `InsufficientFunds` · `MaxPerUserReached` · `ProductInactive` · `ProductNotFound` · `NotInWorld` · `Unauthorized` · `RateLimited` · `Failed` | refused, nothing charged | — |
84
+ | `Cancelled` · `Timeout` · `NotFound` | client-side outcome; the true result may still be resolvable | — |
85
+ | `PriceChanged` | the displayed price went stale; nothing charged, the shell re-prompts | — |
86
+
87
+ **Branch on `completed`, not on `Granted`** — otherwise every free claim reads as a failure. And treat
88
+ `AlreadyOwned` as "they have it", never as "a sale happened".
89
+
90
+ Universal items use `purchaseDistributionKey`, `purchaseDistribution`, or `purchaseListing`; raw item ids
91
+ are not purchase references. `getListings(query?)` browses active Marketplace distributions and listings.
92
+
93
+ ## 3. Read and spend what fulfilment granted
94
+
95
+ ```ts
96
+ const ent = await Helix.purchases.getEntitlements();
97
+ // { v: 1, passes: { vip: { since, expiresAt, active } }, balances: { coin: 740 }, owned: { 'starter-pack': 1 } }
98
+ if (ent.passes.vip?.active) unlockLounge();
99
+
100
+ const spent = await Helix.purchases.consume('potion', 1); // { applied, balance, reason?: 'insufficient' }
101
+ if (spent.applied) drinkPotion(); else offerShop(spent.balance);
102
+
103
+ const off = Helix.purchases.onEntitlementsChanged(() => refresh()); // no payload — re-read
104
+ ```
105
+
106
+ - **Server-held, backend-written.** Entitlements are the durable projection of every settled purchase in THIS
107
+ world for THIS player. A world reads and spends; it can never write.
108
+ - `consume` is **atomic and floor-at-zero**: `applied: false` with `reason: 'insufficient'` left the balance
109
+ untouched. It is **idempotent per call** — a retry of the same call cannot double-spend, but calling it again
110
+ is a genuinely new spend.
111
+ - Pass expiry is judged at read time via `active`.
112
+ - **Preview mode (no shell)** resolves empty entitlements and an insufficient consume, so display code never
113
+ breaks the world loop.
114
+
115
+ ## 4. Surviving a reload mid-purchase
116
+
117
+ Supply your own `idempotencyKey`, persist it, and re-read on boot. Without this, a tab that reloads during the
118
+ popup leaves a player charged with your world unaware:
119
+
120
+ ```ts
121
+ const key = localStorage.getItem('buy:coin-pack') ?? crypto.randomUUID();
122
+ localStorage.setItem('buy:coin-pack', key);
123
+ await Helix.marketplace.purchaseProduct('coin-pack', { idempotencyKey: key });
124
+
125
+ // on boot:
126
+ const prior = await Helix.purchases.getPurchase(key, 'coin-pack'); // PASS the ref
127
+ if (prior?.completed) { localStorage.removeItem('buy:coin-pack'); refresh(); }
128
+ ```
129
+
130
+ **Always pass the `ref`.** Without it the answer is bound only to (key, player, world), so a key that settled
131
+ for one product would answer for any other.
132
+
133
+ ## 5. A custom confirm UI
134
+
135
+ ```ts
136
+ const ctx = await Helix.marketplace.getPurchaseContext('vip-pass');
137
+ // product + the player's live balance + eligibility + the grants effect list, in one call
138
+ ```
139
+
140
+ The built-in popup uses this internally. You can render your own pre-confirm screen from it — but the popup
141
+ still settles the sale, because consent to spend is the shell's job, not yours.
142
+
143
+ ## 6. Multiplayer — a sale reaches the room with no world code
144
+
145
+ The shell settles, then the platform forwards the backend receipt to the room the buyer occupies, firing a
146
+ `{"when":{"on":"purchase"}}` rule with `self` = the buyer.
147
+
148
+ ```jsonc
149
+ { "when": {"on":"purchase"},
150
+ "if": {"op":"==","a":{"var":"purchase.productKey"},"b":"vip_pass"},
151
+ "then": [ {"do":"set","target":"self.tier","to":"vip"}, {"do":"save","player":"self"} ] }
152
+ ```
153
+
154
+ - Readable only inside that rule: `purchase.productKey` and `purchase.purchaseId`. The event takes **no
155
+ params** — branch on the key in `if`.
156
+ - **Exactly once per purchase**, platform-enforced. A re-forward, a rejoin, a dead tab and a second instance of
157
+ the world all collapse to one firing — **so grant durably** (`save`, `increment`, `submitScore`,
158
+ `awardAchievement`), never into state a disconnect resets.
159
+ - Rules read the same entitlements: `{"op":"hasPass","passKey":"vip","of":"self"}` and
160
+ `{"op":"balanceOf","code":"coin","of":"self"}` (O(1), refreshed before the purchase rule runs), and spend
161
+ with `{"do":"consume","code":"coin","amount":10,"player":"self"}`.
162
+ - A **guest seat owns no purchase**, so nothing fires for one.
163
+
164
+ Full grammar and worked rules: `read_doc({ name: "multiplayer-logic" })` §20.
165
+
166
+ ## 7. Single-player worlds
167
+
168
+ Everything in §1–§5 works with no multiplayer block at all — registration, buying, entitlements and consume are
169
+ all shell/backend features. Only §6 needs a room. A single-player world applies the effect itself on
170
+ `completed` and reads entitlements for anything durable.
171
+
172
+ ## 8. Footguns — every one of these fails silently
173
+
174
+ - **Never mirror a pass or balance into client state, a playerVar or a roomVar.** A session copy resets while
175
+ the entitlement does not, and the two then disagree about money. Read the entitlement where you need it.
176
+ - **Publish does not check the product registry.** A typo in a `passKey` or currency `code` publishes clean and
177
+ reads empty forever — `hasPass` false, `balanceOf` 0, deny by default.
178
+ - **`of` is REQUIRED** on `hasPass`/`balanceOf`; there is no implicit `self`, and an entity ref is a publish error.
179
+ - **Do not invent a manifest permission.** `Helix.marketplace` and `Helix.purchases` ride the world-scoped
180
+ session and need none — and the manifest is `additionalProperties: false`, so a guessed permission string is
181
+ a hard validation error, not a warning.
182
+ - **A consumable bought under legacy `type: "consumable"` grants nothing durable** — your world applies the
183
+ effect on `Granted`, and a crashed tab loses it. Use a `currency` grant instead.
184
+ - **The price is never sent by the world.** If you find yourself passing an amount, you are building the wrong
185
+ thing.
186
+
187
+ ## 9. Testing before you ship
188
+
189
+ Register with `dryRun: true` to check shapes offline. Then actually execute your buy / grant / consume code —
190
+ in preview mode (`vite preview`, no shell) none of it runs: `purchaseProduct` refuses, entitlements read empty,
191
+ and the SDK warns once per method that it did nothing.
192
+
193
+ **Start with `helix dev` — the simulated shell.** Build the world, then `helix dev` serves the built bundle
194
+ inside a HELIX shell simulated on your machine: no login, no backend, no room, no engine clone, nothing to boot.
195
+ It simulates the whole shell lane — `purchaseProduct`, `getPurchaseContext`, `getPurchase`, entitlements,
196
+ `consume`, wallet, inventory, and the data store with the backend's real key policy — applies a product's
197
+ `grants` to durable local state and fires `entitlements-changed`, so the code you wrote for a real sale is the
198
+ code that runs. It enforces production's caps (30 data-store writes/min per player, 120 consumes/min, 256 KB per
199
+ value), so a world that would be rate-limited live is rate-limited here. Products seed from
200
+ `.helix/dev-products.json`, achievements from `.helix/dev-achievements.json` (criteria-bearing ones UNLOCK
201
+ locally off the simulated evidence — see the sdk doc); the simulated player lives in `.helix/dev-state.json` and survives reloads
202
+ (`--fresh` = a new player, `--reset` = wipe). Every answer is console-logged `[helix dev · SIMULATED]` under a
203
+ permanent banner — the mode is *simulated*, never "test", and it spends nothing real.
204
+
205
+ It raises the shell's purchase confirm dialog too (Buy / Cancel with real price and balance figures), so the
206
+ player-cancelled path — `Cancelled`, nothing charged, nothing recorded, a retry confirms again — is testable
207
+ locally. Its debug menu is what you cannot get anywhere else: **Purchases Always Fail** (cycling
208
+ `InsufficientFunds` → `PriceChanged` → a throw that leaves the purchase `Pending`), **Force Save Failure**,
209
+ **Exhaust Consume Fence**, grant / remove an entitlement by hand. §8's footguns are all silent failures — this
210
+ is how you make them loud. Full reference: `read_doc({ name: "sdk" })`.
211
+
212
+ **What the simulator cannot prove.** It never charges, escrows or fulfils anything, and it mints **no receipt** —
213
+ so nothing settles, and §6's `purchase` rule never fires. It cannot tell you a product is genuinely registered
214
+ (only `list_world_products` against the real backend can), and it exercises no room-lane behaviour at all: DSL
215
+ rules, the `consume` / `save` verbs, `awardAchievement`, multiplayer. Treat a green `helix dev` run as "my world
216
+ handles the answers correctly", never as "the sale works".
217
+
218
+ **Then cross-check on the dev stack — the real pipeline.**
219
+ `node helix-web-engine-new/tools/dev-stack/dev-stack.mjs up iwp-shop` (single-player) or `up iwp-mp` (the room
220
+ harness) boots the backend, the room and the economy service, seeds products and funds a wallet, so a purchase
221
+ settles end to end against real services rather than a mock. Run the same world both ways and compare: when
222
+ `helix dev` and the dev stack answer differently, that is signal, not noise — one of them is wrong, and it is
223
+ worth knowing which.
@@ -0,0 +1,64 @@
1
+ # Scene performance: measure the first build
2
+
3
+ Run `analyze_scene_performance` as soon as an imported or generated Scene has its first build. Repeat it after material, lighting, streaming, or geometry changes and on the exact release candidate.
4
+
5
+ ```text
6
+ analyze_scene_performance({
7
+ directory: "/absolute/path/to/generated",
8
+ scene: "/absolute/path/to/generated/scene.helix-scene.json",
9
+ package: "/absolute/path/to/generated/continuum-package-version.json",
10
+ snapshot: "/absolute/path/to/qa/inspect.json",
11
+ perf: "/absolute/path/to/qa/perf.json",
12
+ profile: "web-desktop-balanced"
13
+ })
14
+ ```
15
+
16
+ The terminal equivalent is `helix world performance-report generated --scene <scene> --package <package> --snapshot <snapshot> --perf <receipt>`. Its `estimated-budget-utilization` compares facts to an existing Palette Showcase device profile. This is a useful reference tier, not a universal Scene promise. Unknown residency, GPU allocation, shader, transparency, or pass costs stay unknown and cannot improve the rating. Static counts never predict FPS.
17
+
18
+ The analyzer runs the canonical Scene v2 and Continuum Package validators before counting. It accepts a structurally valid older Scene's declared capabilities and extensions for analysis; that does not prove the target runtime supports them. Missing or mismatched local bundle resources, paths that escape the Scene directory, invalid contracts, and an FPS claim that contradicts its rendered-frame count and duration fail the report. Vault or other non-bundle resources stay explicitly unresolved, make affected counters and reference checks unknown, and require exact resolved bytes or runtime evidence.
19
+
20
+ Start from platform-owned defaults when they preserve the scene: use an existing Palette material Package for common material families and the built-in visual runtime for lighting and grade. Consider actual lightmaps for suitable static surfaces only after the importer/runtime/shell path is verified. Keep authored PBR, dynamic lights, or probes where the intended look, movement, or editability requires them. Confirm the renderer consumed each native binding; Package dependencies and provenance are not live-binding proof.
21
+
22
+ ## Read each counter literally
23
+
24
+ | Counter | Meaning | It does not mean |
25
+ |---|---|---|
26
+ | `meshDefinitionTriangles` | triangle sum across authored render mesh definitions in exact Scene resources | triangles submitted per frame |
27
+ | `visibleSceneTriangles` | visible geometry in one inspection snapshot | worst-case submitted geometry |
28
+ | `submittedTriangles` | renderer submissions measured on the driven route, including repeated placements and passes | unique authored geometry |
29
+ | `uniqueVisualMaterials` | visual definitions after texture refs resolve to external or embedded content hashes; names/provenance extras are ignored and visual extensions remain | file records, slots, or programs |
30
+ | `fileLocalMaterialRecords` | sum of GLB `materials[]` records | visually distinct looks |
31
+ | `materialSlots` | mesh primitive/material slots | shader programs |
32
+ | `sourceBoundSurfaces` | Scene v2 `surfaces[].default.kind: "source"` | Palette bindings |
33
+ | `nativeMaterialBoundSurfaces` | Scene v2 surfaces using native material definitions | live Palette shader use |
34
+ | `packageNativeBindings` | exact Package id/version/manifest CID material bindings | Vault pins or live Palette use |
35
+ | `observedPaletteBindings` | a renderer probe explicitly counted live Palette bindings | material Package provenance |
36
+ | `materialKtx2Resources` | Scene-owned KTX2 resources actually referenced by render-material texture slots | every declared texture resource or GPU residency |
37
+
38
+ Do not invent Vault pins to close a Package-native gap. A legacy Scene can have Palette dependencies in Package provenance while every surface remains source-bound.
39
+
40
+ `assetLocalDraws` estimates GLB submissions from placements. Repeated source objects are only review candidates: material-batched modules may already collapse them into fewer draws, while instancing can add draws or create broad bounds that cull poorly. Compare the current material-batch draw count with proposed instance draws and active instance bounds/cell culling on the same route. Runtime instancing also requires repeated top-level Scene nodes that reference the same eligible single-static-mesh/single-primitive resource.
41
+
42
+ ## Delivery and memory are separate
43
+
44
+ - `declaredOwnedBytes` is the Scene document plus exact resources in its table. Unattached directory files are reported separately.
45
+ - `transitiveClosureBytes` is reachability, not actual transfer.
46
+ - Cold and warm transfer require deliberate separate runs. Cached `transferSize: 0` entries do not prove zero cold cost.
47
+ - Compressed KTX2 bytes are not GPU memory. 1K maps are a strong default, but separate base/normal/ORM maps can raise cold bytes.
48
+ - ReadySets or stream groups shape first-frame residency. Dependency count does not.
49
+
50
+ ## Runtime evidence remains a supplied claim
51
+
52
+ A JSON file can claim any FPS. The analyzer uses `supplied-unbound` unless exact entry and Scene hashes match and the receipt includes hardware renderer, exact browser build, build revision, Scene identity, viewport, DPR, effective quality profile, and explicit frame cap including `null`. Even then it says `supplied-identity-matched`: hashes bind bytes, not measurement origin. A harness that owns the capture may make a stronger verified-run claim in its own output.
53
+
54
+ Wait for compilation idle, the declared stream closure, and a recorded GI-asleep quiet interval before sampling. Apply the same ordered preconditioning, then drive a grounded walk through the same midpoint. Report rendered frames and duration alongside FPS and frame percentiles; do not substitute an average of sampled reciprocal FPS or a fly route. Record avatar and vehicle occupancy, the rendered bundle SHA, and a digest of dirty code. Keep the compositor's internal resolution separate from canvas size and DPR. A frame cap explains a plateau; reaching it does not prove spare headroom. Include draw calls, submitted triangles, shader programs, pass multipliers, shadow-casting and punctual lights, GPU texture allocation, and observed Palette count when available.
55
+
56
+ ## Fix costs without trading away the scene
57
+
58
+ Use the built-in visual runtime. Before planning lightmaps, verify that the active importer preserves lightmap UVs and that the target runtime and shell consume the binding. A probe-field consumer does not establish a shipped shell lightmap path. Where the whole path is verified, lightmaps can remove suitable dynamic light work for truly static surfaces. For probe baking, follow `read_doc({ name: "lighting-world" })` and the canonical `helix bake-lighting` route, freeze the authored sun/time path, and keep movable avatars and vehicles outside immutable architectural transport. Keep gameplay/moving lights and required editability; limit range and decorative shadows. Baked probes avoid live captures but still sample per fragment. They are not lightmaps, and “baked” does not mean fast.
59
+
60
+ Treat transmission, GI/probe sampling, and shadows as pass multipliers. Huge frosted panes can dominate through transmission/upscale paths with unchanged authored triangles. Change a feature only after a matched ablation identifies it.
61
+
62
+ ## Visual QA closes the optimization
63
+
64
+ A capped low-resolution screenshot is not fullscreen proof. Compare before and after on the same camera route, exposure, time of day, viewport, DPR, effective quality, and frame cap. Drive while measuring, then inspect close views for stone/wood normal shimmer, frosted-glass errors, contact-shadow loss, and light leaks. An FPS gain that loses those properties is not a successful optimization.
@@ -0,0 +1,140 @@
1
+ # HELIX Instant — World Screenshots & Thumbnails
2
+
3
+ `publish_world` / `helix publish` now automatically capture a ready-world cover when no selected custom thumbnail exists. The image is verified and assigned atomically with the Build; capture failure refuses publication before World/Build writes. Register a `hero` shot and call `screenshots.ready` after assets load. Automatic covers refresh per Build; selected custom covers are preserved. Pass `thumbnail` / `--thumbnail` to select a cover without automatic capture. Manual capture/inspect/attach below remains the workflow for selecting custom media and optional preview images.
4
+
5
+ Native Scene v2, semantic Continuum Releases and Unreal/shared-runtime promotion currently require a selected custom cover. They explicitly fail with `WORLD_THUMBNAIL_CAPTURE_UNSUPPORTED` if no cover exists; automatic capture adapters for those runtimes are not integrated. This is a publication failure, never a successful generic placeholder.
6
+
7
+
8
+ Every published world starts with a generated placeholder image. This doc covers capturing a real,
9
+ deterministic screenshot of your world and deliberately attaching it as the world's `thumbnail`/`preview`.
10
+ Capturing is local and non-destructive — it never touches the published world; attaching is a separate,
11
+ explicit step.
12
+
13
+ ## 1. Declare a hero shot in world code
14
+
15
+ Two calls, both cheap no-ops during normal play — they only activate when the page is loaded with
16
+ `?helixScreenshot=1`, which `capture_world_screenshot` (and the `helix screenshot` CLI command) do for you:
17
+
18
+ ```ts
19
+ import { screenshots } from '@helix/engine-core/screenshot';
20
+ screenshots.register('hero', { position: [12, 8, -20], lookAt: [0, 2, 0], fov: 55 });
21
+ // ...after your world's own async setup resolves, right before the render loop starts:
22
+ screenshots.ready({ renderer, scene, camera });
23
+ ```
24
+
25
+ `scaffold_world` wires this in `src/main.ts` with a placeholder pose — adjust `position`/`lookAt`/`fov`
26
+ to frame your scene. (True from helix-cli 0.1.13-helix3.79 onward. Earlier scaffolds emitted the
27
+ `engine-core` pin but none of the wiring, so `capture_world_screenshot` timed out on a brand-new world
28
+ and the error told you to do what the scaffold claimed to have done. On an older project, follow §9.) `renderer`/`scene`/`camera` are required: a capture applies the registered pose to your
29
+ world's own camera for the frame and restores it afterwards — no separate camera is created.
30
+
31
+ **Keep the placeholder body out of frame.** Headless captures run logged out, so the local player renders as
32
+ the bare default mannequin — usually not what you want standing in your cover shot. `ready()` takes an optional
33
+ `hide` list of scene objects made invisible for the whole screenshot session (a no-op during normal play):
34
+
35
+ ```ts
36
+ screenshots.ready({ renderer, scene, camera, hide: [mp.local.model] });
37
+ ```
38
+
39
+ Needs `engine-core >= 0.1.1`. Omit `hide` (or leave the list empty) if you deliberately want the character in frame.
40
+
41
+ ## 2. Shot naming & warmup
42
+
43
+ - Name shots by what they show, not by number — `hero`, `overview`, `arena`. Register more than one if it
44
+ helps; `capture_world_screenshot` captures every declared shot by default, or a single one via `shot`.
45
+ - The default warmup (a handful of rendered frames) is enough for a static scene but not for physics settling
46
+ (ragdolls, dropped props, water). Pass a per-shot `warmupMs` — `screenshots.register('hero', { position, lookAt, fov, warmupMs: 500 })` — so the world visibly settles before the frame is grabbed.
47
+
48
+ ## 3. The workflow: capture → inspect → iterate → attach
49
+
50
+ 1. Build the world (`npm run build`).
51
+ 2. `capture_world_screenshot({ directory: "<dist>" })` — runs headless Chromium in-process (the same code
52
+ path as `helix screenshot`), writes PNGs to disk, and returns a preview image per capture so you can
53
+ actually see the framing, not just a file path.
54
+ 3. Look at the returned image. Framing off? The fast lane is the pose override — `capture_world_screenshot({
55
+ directory, camera: [x, y, z], lookAt: [x, y, z], fov })` renders exactly that one pose (written as
56
+ `custom.png`) with NO rebuild, so you can audition angles rapidly; add `previewImages: 1` to keep each
57
+ iteration cheap. A single declared shot re-captures via `shot: "name"`. Once you like the frame, bake the
58
+ winning numbers into `screenshots.register(...)` and rebuild so the shot is stable for everyone.
59
+ 4. Once you're satisfied, run `helix thumbnail set <slug> <file>` to attach it. Never skip straight to
60
+ attaching a capture you haven't looked at.
61
+
62
+ The same flow is available from the terminal: `helix screenshot <dist>` then `helix thumbnail set <slug> <file>`.
63
+
64
+ ## 4. The two slots
65
+
66
+ - **`thumbnail`** — the world's cover image everywhere: catalog cards, the detail page's first carousel
67
+ slide, the loading screen. Target 1280×720.
68
+ - **`preview`** — one extra detail-carousel slide, optional. Target 2560×1440 — capture with `preview: true`
69
+ to also get the 2560×1440 variant of each shot.
70
+
71
+ `helix thumbnail set <slug> <file> --kind preview` fills the second slot; omit `--kind` for `thumbnail`.
72
+
73
+ ## 5. The anti-churn policy
74
+
75
+ - Publishing preserves a selected custom cover. Otherwise `publish_world` assigns supplied/bundled
76
+ artwork or automatically captures the ready Web Build and assigns its cover during activation.
77
+ Later publications refresh automatic covers; native/semantic publication requires a selected cover.
78
+ - Filling an **empty** slot is a normal finishing step — do it without asking.
79
+ - Replacing an **occupied** slot requires `overwrite: true`, and you should only pass it when the human
80
+ explicitly asked for a replacement. Without it, `helix thumbnail set` refuses and returns the current URL
81
+ so you can relay the situation instead of silently skipping it.
82
+ - When a change materially alters the world's visuals, suggest a retake to the human and stop — never
83
+ recapture and overwrite on your own judgment.
84
+
85
+ ## 6. The `screenshots/` folder
86
+
87
+ Captures land on disk — by default a `screenshots/` folder next to the built bundle directory, or wherever
88
+ `out` points. It's there so you and the human can inspect files directly; it's excluded from publish
89
+ bundling, so nothing in it ever ships with the world.
90
+
91
+ ## 7. Blind mode — a labeled last resort
92
+
93
+ A world published before it integrated the screenshot helper has no `window.__helixScreenshots` to drive.
94
+ `capture_world_screenshot({ directory, blindMs: 800 })` waits `blindMs` milliseconds, then screenshots the
95
+ canvas exactly as it looks — no declared pose, no warmup guarantee. The result is clearly labeled
96
+ non-deterministic; treat it as a stopgap, not a stable thumbnail source. Prefer republishing the world with
97
+ a declared shot instead.
98
+
99
+ ## 8. Capture as eyes while you build (optional)
100
+
101
+ Attaching thumbnails is the finishing step, but `capture_world_screenshot` is useful earlier too: when you
102
+ have no browser tool, it is the only way to actually SEE the world you are editing. Rebuild, capture, look
103
+ at the returned image — a placed model, a material, lighting, scene layout, all confirmable without leaving
104
+ the loop. The `camera`/`lookAt`/`fov` override inspects any angle without touching world code, and even a
105
+ failed capture reports the page's boot errors, so it doubles as a headless smoke test.
106
+
107
+ Use it deliberately, not habitually:
108
+
109
+ - Capture when a change is **visual** and you cannot confirm it any other way. Skip it for logic-only
110
+ changes — every capture costs a build plus several seconds, and image results are heavy.
111
+ - One well-chosen angle beats a burst of captures. Prefer the declared shots you already framed.
112
+ - If a live browser tool (e.g. a Chrome MCP) is available, prefer it for interactive debugging — console,
113
+ network, clicking. Headless capture is the no-browser fallback and the framing-exact check, not a
114
+ replacement for a real browser session.
115
+
116
+ ## 9. Adding the helper to an existing world
117
+
118
+ A world scaffolded before the helper moved to its own package needs these steps:
119
+
120
+ 1. Add `"engine-core": "^0.1.0"` to `public/helix.json` → `systems` (alongside `humanoid-character`).
121
+ 2. Resolve it: `install_world_packages` (or the CLI's `helix install`).
122
+ 3. If `vite.config.ts` predates the engine-core aliases, add these three lines to `resolve.alias`
123
+ BEFORE any `@helix/humanoid-character` entry — subpath aliases must precede the root alias, or
124
+ rollup's prefix substitution mangles `@helix/engine-core/screenshot` into an ENOENT:
125
+
126
+ ```ts
127
+ '@helix/engine-core/screenshot': fileURLToPath(new URL('./public/helix_modules/engine-core/screenshot/index.js', import.meta.url)),
128
+ '@helix/engine-core/world-camera': fileURLToPath(new URL('./public/helix_modules/engine-core/world-camera/index.js', import.meta.url)),
129
+ '@helix/engine-core': fileURLToPath(new URL('./public/helix_modules/engine-core/index.js', import.meta.url)),
130
+ ```
131
+
132
+ 4. Add the integration lines from §1 to `src/main.ts`:
133
+
134
+ ```ts
135
+ import { screenshots } from '@helix/engine-core/screenshot';
136
+ screenshots.register('hero', { position: [12, 8, -20], lookAt: [0, 2, 0], fov: 55 });
137
+ screenshots.ready({ renderer, scene, camera });
138
+ ```
139
+
140
+ 5. Rebuild (`npm run build`), then capture as usual: `capture_world_screenshot`.