@hypersoniclabs/helix-mcp 0.2.4 → 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.
- package/README.md +81 -11
- package/dist/continuumCanary.d.ts +17 -0
- package/dist/continuumCanary.js +17 -0
- package/dist/continuumCanary.js.map +1 -0
- package/dist/server.d.ts +14 -1
- package/dist/server.js +4058 -162
- package/dist/server.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/vehicleTools.d.ts +86 -0
- package/dist/vehicleTools.js +229 -0
- package/dist/vehicleTools.js.map +1 -0
- package/docs/avatar-face.md +115 -0
- package/docs/bridge.md +98 -0
- package/docs/bring-your-world.md +117 -0
- package/docs/catalog.md +69 -1
- package/docs/character-animation.md +442 -0
- package/docs/character-attachments.md +166 -0
- package/docs/character-world.md +785 -130
- package/docs/continuum.md +153 -0
- package/docs/items.md +73 -0
- package/docs/lighting-world.md +667 -0
- package/docs/locomotion-clip-spec.md +294 -0
- package/docs/manifest.md +31 -6
- package/docs/multiplayer-logic.md +460 -17
- package/docs/multiplayer-templates/chrono-orchard.md +36 -22
- package/docs/multiplayer-templates/collect-a-thon.md +28 -38
- package/docs/multiplayer-templates/collections.md +24 -24
- package/docs/multiplayer-templates/hangout.md +124 -111
- package/docs/multiplayer-templates/npc-wave.md +310 -0
- package/docs/multiplayer-templates/obby.md +13 -16
- package/docs/multiplayer-templates/persistent-progress.md +218 -0
- package/docs/multiplayer-templates/physics-bumper.md +27 -9
- package/docs/multiplayer-templates/physics-football.md +22 -8
- package/docs/multiplayer-templates/relic-bearers.md +12 -15
- package/docs/multiplayer-templates/server-motion.md +16 -19
- package/docs/multiplayer-templates/shooter-range.md +275 -0
- package/docs/multiplayer-templates/team-control.md +28 -15
- package/docs/multiplayer-templates/turn-arena.md +31 -22
- package/docs/multiplayer-templates/voice-radio.md +166 -0
- package/docs/multiplayer-templates/wave-survival.md +7 -8
- package/docs/multiplayer-templates/world-shop.md +240 -0
- package/docs/multiplayer-world.md +222 -129
- package/docs/npc-world.md +623 -0
- package/docs/publishing.md +108 -28
- package/docs/purchases.md +223 -0
- package/docs/scene-performance.md +64 -0
- package/docs/screenshots.md +140 -0
- package/docs/sdk.md +324 -5
- package/docs/shooter-worlds.md +537 -0
- package/docs/terrain.md +173 -0
- package/docs/upgrades.md +324 -0
- package/docs/vehicles.md +727 -0
- package/docs/world-inspect.md +156 -0
- package/docs/world-look.md +241 -0
- package/docs/world-recipe.md +65 -6
- package/package.json +15 -4
- package/skills/README.md +91 -0
- package/skills/helix-assets/SKILL.md +491 -0
- package/skills/helix-assets/references/asset-sources.md +143 -0
- package/skills/helix-assets/references/vault-api.md +105 -0
- package/skills/helix-avatar-qa/SKILL.md +85 -0
- package/skills/helix-avatars/SKILL.md +206 -0
- package/skills/helix-avatars/references/contract.md +166 -0
- package/skills/helix-avatars/references/dynamics.md +367 -0
- package/skills/helix-avatars/references/face.md +50 -0
- package/skills/helix-avatars/references/publish.md +76 -0
- package/skills/helix-avatars/references/qa.md +251 -0
- package/skills/helix-avatars/references/rigging.md +88 -0
- package/skills/helix-avatars/references/source-generated.md +190 -0
- package/skills/helix-avatars/references/source-model.md +90 -0
- package/skills/helix-avatars/references/source-rigid.md +90 -0
- package/skills/helix-avatars/references/source-vrm.md +61 -0
- package/skills/helix-gauntlet/SKILL.md +128 -0
- package/skills/helix-multiplayer/SKILL.md +150 -0
- package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
- package/skills/helix-vehicles/SKILL.md +218 -0
- package/skills/helix-vehicles/references/addons.md +212 -0
- package/skills/helix-vehicles/references/appearance.md +339 -0
- package/skills/helix-vehicles/references/audio-import.md +138 -0
- package/skills/helix-vehicles/references/audio.md +580 -0
- package/skills/helix-vehicles/references/cabin.md +225 -0
- package/skills/helix-vehicles/references/host-manifest.md +174 -0
- package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
- package/skills/helix-vehicles/references/publish.md +214 -0
- package/skills/helix-vehicles/references/qa.md +177 -0
- package/skills/helix-vehicles/references/reference-package.json +3481 -0
- package/skills/helix-vehicles/references/reference-package.md +69 -0
- package/skills/helix-vehicles/references/source-beamng.md +167 -0
- package/skills/helix-vehicles/references/source-concept.md +38 -0
- package/skills/helix-vehicles/references/source-model.md +100 -0
- package/skills/helix-vehicles/references/source-scratch.md +60 -0
- package/skills/helix-world-build/SKILL.md +376 -0
- package/skills/helix-world-build/references/config-gates.md +104 -0
- package/skills/helix-world-director/SKILL.md +210 -0
- package/skills/helix-world-qa/SKILL.md +371 -0
- package/skills/helix-world-qa/references/perf-budgets.md +240 -0
- package/skills/helix-world-qa/references/perf-handle.md +125 -0
- package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
package/docs/publishing.md
CHANGED
|
@@ -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
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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`.
|