@hypersoniclabs/helix-manifest 0.3.3 → 0.3.16
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 +21 -2
- package/dist/schema/manifest-v0.3.schema.json +124 -14
- package/dist/schema/package-envelope-v1.schema.json +12 -3
- package/dist/schema/scene-v2.schema.json +1055 -0
- package/dist/src/bundle-checksum.d.ts +13 -0
- package/dist/src/bundle-checksum.js +20 -0
- package/dist/src/bundle-checksum.js.map +1 -0
- package/dist/src/bundle-rules.js +7 -1
- package/dist/src/bundle-rules.js.map +1 -1
- package/dist/src/index.d.ts +186 -10
- package/dist/src/index.js +1115 -130
- package/dist/src/index.js.map +1 -1
- package/dist/src/interactable-package.d.ts +60 -0
- package/dist/src/interactable-package.js +186 -0
- package/dist/src/interactable-package.js.map +1 -0
- package/dist/src/interactive-item.d.ts +183 -0
- package/dist/src/interactive-item.js +580 -0
- package/dist/src/interactive-item.js.map +1 -0
- package/dist/src/scene-v2.d.ts +437 -0
- package/dist/src/scene-v2.js +1141 -0
- package/dist/src/scene-v2.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/helix-scene-v2.md +241 -0
- package/examples/minimal-home.scene-v2.json +365 -0
- package/package.json +7 -3
- package/schema/manifest-v0.3.schema.json +124 -14
- package/schema/package-envelope-v1.schema.json +12 -3
- package/schema/scene-v2.schema.json +1055 -0
package/README.md
CHANGED
|
@@ -30,10 +30,29 @@ const manifest = result.manifest; // NormalizedHelixManifest — defaults applie
|
|
|
30
30
|
### Exports
|
|
31
31
|
|
|
32
32
|
- `validateManifest(data)` / `parseManifest(json)` — validate + normalize a world `helix.json` (v0.1–v0.3).
|
|
33
|
-
- `validatePackageEnvelope(data)` / `parsePackageEnvelope(json)` — validate the `system | ability | asset-pack` envelope
|
|
33
|
+
- `validatePackageEnvelope(data)` / `parsePackageEnvelope(json)` — validate the `system | ability | asset-pack | interactable` envelope.
|
|
34
|
+
- `validateInteractablePackageManifest(data)` / `validateInteractableServerFragment(data)` — validate a portable interactable manifest, run its authority fragment through the world grammar, and recompute its budget.
|
|
35
|
+
- `validateSceneDocument(data, options?)` / `parseSceneDocument(json, options?)` — validate and normalize a portable HELIX Scene v2 document shared by Home and World profiles.
|
|
36
|
+
- `validateSceneDocumentForPublication(data, options)` — the publication-safe gate whose type requires an Ed25519 implementation-signature verifier.
|
|
37
|
+
- `normalizeSceneDocument(scene)` — return a detached, defaulted copy of an already validated Scene v2 document.
|
|
38
|
+
- `canonicalSceneDocumentPayload(scene)` / `computeSceneDocumentIntegrity(scene)` / `verifySceneDocumentIntegrity(scene)` — RFC 8785 canonical payload and SHA-256 identity helpers; only the top-level self field is excluded.
|
|
39
|
+
- Scene vocabulary: `HELIX_SCENE_FORMAT`, `HELIX_SCENE_VERSION`, `HELIX_SCENE_MEDIA_TYPE`, `HELIX_SCENE_CAPS`, behavior/runtime-extension/signing-key allow-lists, `sceneResourceMimeCompatible`, `sceneBundleUriMatchesResource`, `sceneProfileMemoryCommitmentMiB`, `behaviorImplementationSignaturePayload`, `HELIX_TO_UNREAL_ADAPTER`, and `helixPositionToUnrealCm(position)`.
|
|
34
40
|
- Bundle rules: `BUNDLE_MAX_FILES`, `BUNDLE_MAX_FILE_BYTES`, `BUNDLE_MAX_TOTAL_BYTES`, `BUNDLE_PATH_PATTERN`, `bundleContentType(path)`, `bundleLimitsForKind(kind)`, `classifyBundleFile(kind, path)`, `ASSET_EXTENSIONS`.
|
|
35
41
|
- Vocabulary + federation maps: `BUNDLE_KINDS`, `MAIN_BACKEND_PACKAGE_TYPE`, `MAIN_BACKEND_CONTENT_RATING`, `PERMISSIONS_V01`, `PERMISSIONS_V03`, `PLATFORM_MAX_PLAYERS_PER_ROOM`, `MANIFEST_FILENAME`, `MANIFEST_VERSIONS`.
|
|
36
|
-
- Types: `HelixManifest`, `NormalizedHelixManifest`, `HelixBundleKind`, `HelixContentRating`, `HelixPermission`, `HelixMultiplayerConfig`, `HelixPackageEnvelope`, `NormalizedPackageEnvelope
|
|
42
|
+
- Types: `HelixManifest`, `NormalizedHelixManifest`, `HelixBundleKind`, `HelixContentRating`, `HelixPermission`, `HelixMultiplayerConfig`, `HelixPackageEnvelope`, `NormalizedPackageEnvelope`, `HelixSceneV2`, and the exported Scene v2 section types.
|
|
43
|
+
|
|
44
|
+
## HELIX Scene v2
|
|
45
|
+
|
|
46
|
+
Scene v2 is the portable content contract for both Homes and Worlds. A Home is
|
|
47
|
+
a `home` profile plus residence-owned instance state; it is not a separate
|
|
48
|
+
rendering format. Scene JSON contains immutable resource handles, stable nodes,
|
|
49
|
+
physical lighting intent, surfaces, collision/navigation, streaming/LOD and
|
|
50
|
+
registered declarative behavior. It never contains executable code or arbitrary
|
|
51
|
+
remote URLs.
|
|
52
|
+
|
|
53
|
+
See [the creator contract](docs/helix-scene-v2.md) and the
|
|
54
|
+
[dual-profile example](examples/minimal-home.scene-v2.json). The published JSON
|
|
55
|
+
Schema is `schema/scene-v2.schema.json`.
|
|
37
56
|
|
|
38
57
|
## Develop
|
|
39
58
|
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"items": {
|
|
42
42
|
"enum": ["auth.profile", "multiplayer", "voice.room", "voice.proximity", "camera.capture"]
|
|
43
43
|
},
|
|
44
|
-
"description": "Platform capabilities the world requests, OAuth-scope style. Worlds get nothing by default. auth.profile: read the player's id, username, display name. multiplayer: join authoritative Colyseus rooms and replicate player state (implies requiresAuth — guests are single-player only). voice.room / voice.proximity:
|
|
44
|
+
"description": "Platform capabilities the world requests, OAuth-scope style. Worlds get nothing by default. auth.profile: read the player's id, username, display name. multiplayer: join authoritative Colyseus rooms and replicate player state (implies requiresAuth — guests are single-player only). voice.room / voice.proximity: LiveKit voice chat — room-wide vs distance-attenuated ambient render; each implies multiplayer, and the optional multiplayer.voice block tunes the rendering (mode, falloff, channels). camera.capture: save photos taken with the in-engine world camera to the player's account album (the phone Gallery). wallet.*, inventory.* land in later versions."
|
|
45
45
|
},
|
|
46
46
|
"multiplayer": {
|
|
47
47
|
"type": "object",
|
|
@@ -69,6 +69,53 @@
|
|
|
69
69
|
"default": true,
|
|
70
70
|
"description": "Whether the room server owns authoritative state and validates inputs (Tier 1). Reserved as a seam; v1 only ships the authoritative model, so leave true."
|
|
71
71
|
},
|
|
72
|
+
"voice": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"additionalProperties": false,
|
|
75
|
+
"description": "Voice-chat render config (requires a voice.* permission). CLIENT-render knobs the engine applies — the room server never reads this block (voice is a parallel media plane). mode + distances shape the AMBIENT space (players not in a channel); channels declares known channel render policies. Channel membership is exclusive and runtime-set (Helix.voice.setChannel); undeclared ids are allowed at runtime and render 'global'.",
|
|
76
|
+
"properties": {
|
|
77
|
+
"mode": {
|
|
78
|
+
"enum": ["proximity", "global"],
|
|
79
|
+
"default": "proximity",
|
|
80
|
+
"description": "Ambient render: 'proximity' = distance-attenuated (pairs with the voice.proximity permission); 'global' = flat room-wide volume (pairs with voice.room)."
|
|
81
|
+
},
|
|
82
|
+
"refDistance": {
|
|
83
|
+
"type": "number",
|
|
84
|
+
"exclusiveMinimum": 0,
|
|
85
|
+
"default": 4,
|
|
86
|
+
"description": "Proximity: full volume within this many meters of the speaker."
|
|
87
|
+
},
|
|
88
|
+
"maxDistance": {
|
|
89
|
+
"type": "number",
|
|
90
|
+
"exclusiveMinimum": 0,
|
|
91
|
+
"default": 20,
|
|
92
|
+
"description": "Proximity: silent beyond this many meters (equal-power fade between refDistance and here). Must exceed refDistance (checked at publish)."
|
|
93
|
+
},
|
|
94
|
+
"spatial": {
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
"default": false,
|
|
97
|
+
"description": "Directional (HRTF-panned) ambient voice: each speaker is heard from their in-world direction. Direction ONLY — loudness still follows mode/refDistance/maxDistance, so 'global' + spatial is valid (direction without falloff). Channels never spatialize (radio/phone semantics). Falls back to non-spatial where the client audio hook is unavailable."
|
|
98
|
+
},
|
|
99
|
+
"channels": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"propertyNames": { "pattern": "^[a-z0-9][a-z0-9-]{0,31}$" },
|
|
102
|
+
"additionalProperties": { "$ref": "#/$defs/voiceChannelDecl" },
|
|
103
|
+
"description": "Declared voice channels: id -> { render }. Caps the DECLARED map only — runtime channel ids are free-form (dynamic ids like a per-call channel work); undeclared ids render 'global'."
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"emotes": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"additionalProperties": false,
|
|
110
|
+
"description": "Per-world platform emote policy. Needs no permission (emotes are a player feature in single-player worlds too): absent or enabled true = players may play their equipped emotes here, enabled false turns them off for this world (the engine hides the emote wheel; the room skips resolving per-player emote sets).",
|
|
111
|
+
"properties": {
|
|
112
|
+
"enabled": {
|
|
113
|
+
"type": "boolean",
|
|
114
|
+
"default": true,
|
|
115
|
+
"description": "Whether platform player-emotes are available in this world. Default true — a world opts OUT."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
72
119
|
"state": {
|
|
73
120
|
"type": "object",
|
|
74
121
|
"additionalProperties": false,
|
|
@@ -130,6 +177,23 @@
|
|
|
130
177
|
"additionalProperties": { "$ref": "#/$defs/actionDecl" },
|
|
131
178
|
"description": "Declared client→server actions (Tier 2 Phase 2.5b, spec §11.5): name -> { typed arg schema }. A client sends ActionMessage{name,args}; the room validates args against the schema before firing {on:action,name} rules."
|
|
132
179
|
},
|
|
180
|
+
"counters": {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"propertyNames": { "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" },
|
|
183
|
+
"additionalProperties": { "$ref": "#/$defs/counterDecl" },
|
|
184
|
+
"description": "Declared world-global atomic counters (Data Store): name -> {}. An `increment` effect adds a bounded integer delta to a field of one; the room accumulates deltas and flushes them to the durable key `world:counter:{name}`, which any session can read via Helix.dataStore.get. Instance-safe by construction (increments commute)."
|
|
185
|
+
},
|
|
186
|
+
"leaderboards": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"propertyNames": { "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" },
|
|
189
|
+
"additionalProperties": { "$ref": "#/$defs/leaderboardDecl" },
|
|
190
|
+
"description": "Declared leaderboards (Data Store): name -> { order?, size? } (`order` asc|desc, `size` = the top-N kept). A `submitScore` effect submits a player's score; the backend keeps the BEST per (board, player) with an atomic conditional write, so concurrent instances never conflict and retries are safe. Read via Helix.leaderboard.top(board)."
|
|
191
|
+
},
|
|
192
|
+
"weaponItems": {
|
|
193
|
+
"type": "array",
|
|
194
|
+
"items": { "type": "string", "minLength": 1 },
|
|
195
|
+
"description": "GUNS: interactive-item gun definitions the room pre-resolves for the weaponDamage and itemDamage ops (itemDamage may only name a pin listed here) — each entry \"<vaultAssetId>@<version>\" pins one immutable published definition (a Vault asset UUID + a positive integer version). Entry grammar + the MULTIPLAYER_CAPS.weaponItems cap are validated cross-field for readable errors; the schema only shape-checks the array."
|
|
196
|
+
},
|
|
133
197
|
"rules": {
|
|
134
198
|
"type": "array",
|
|
135
199
|
"items": { "$ref": "#/$defs/rule" },
|
|
@@ -177,22 +241,42 @@
|
|
|
177
241
|
}
|
|
178
242
|
},
|
|
179
243
|
"$defs": {
|
|
244
|
+
"voiceChannelDecl": {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"additionalProperties": false,
|
|
247
|
+
"description": "A declared voice channel's render policy.",
|
|
248
|
+
"properties": {
|
|
249
|
+
"render": {
|
|
250
|
+
"enum": ["proximity", "global"],
|
|
251
|
+
"default": "global",
|
|
252
|
+
"description": "How members of this channel hear each other: 'global' (default) = flat full volume regardless of distance — radio/phone semantics; 'proximity' = keep the ambient distance falloff within the channel."
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
180
256
|
"varDecls": {
|
|
181
257
|
"type": "object",
|
|
182
258
|
"propertyNames": { "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" },
|
|
183
259
|
"additionalProperties": { "$ref": "#/$defs/varType" },
|
|
184
260
|
"description": "Declared custom-state vars: name -> VarType. Names are identifiers (a letter then up to 31 letters/digits/underscores)."
|
|
185
261
|
},
|
|
262
|
+
"mergePolicy": {
|
|
263
|
+
"enum": ["lastWrite", "max", "min", "append", "sum"],
|
|
264
|
+
"description": "Data Store: how a persistent ROOM var reconciles when another live instance of the world wrote the document first. Required on every persistent roomVar. lastWrite = the writer wins (racy by declaration); max/min = monotone, conflict-free; append = the union of every instance's appends, newest maxLen kept; sum = commutative per key. Legal values depend on the var's type."
|
|
265
|
+
},
|
|
266
|
+
"persistKey": {
|
|
267
|
+
"type": "string",
|
|
268
|
+
"description": "Data Store: route this persistent ROOM var to its own document at `mp:world:{key}` instead of the shared default `mp:world`. Splitting buys independent version guards and byte budgets; each dirty document costs its own write. Shape (lower-case letters, digits and hyphens, up to 32) is enforced by the validator, not here — a pattern failure inside this `oneOf` would surface as an unreadable all-branches cascade instead of one actionable message."
|
|
269
|
+
},
|
|
186
270
|
"varType": {
|
|
187
271
|
"description": "A declared variable's type + constraints (Tier 2 declared state, spec §5). Discriminated on `type`.",
|
|
188
272
|
"oneOf": [
|
|
189
|
-
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "number" }, "default": { "type": "number" }, "min": { "type": "number" }, "max": { "type": "number" }, "integer": { "type": "boolean" } } },
|
|
190
|
-
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "string" }, "default": { "type": "string" }, "maxLen": { "type": "integer", "minimum": 1 }, "enum": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true } } },
|
|
191
|
-
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "boolean" }, "default": { "type": "boolean" } } },
|
|
192
|
-
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "vec3" }, "default": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 } } },
|
|
193
|
-
{ "type": "object", "additionalProperties": false, "required": ["type", "of"], "properties": { "type": { "const": "ref" }, "of": { "type": "string", "pattern": "^(player|entity:[a-z0-9][a-z0-9-]*)$" }, "default": { "type": "null" } } },
|
|
194
|
-
{ "type": "object", "additionalProperties": false, "required": ["type", "of", "maxLen"], "properties": { "type": { "const": "list" }, "of": { "$ref": "#/$defs/listElem" }, "maxLen": { "type": "integer", "minimum": 1 } }, "description": "Tier 2 (4.5.13 + P3 Collections): a bounded ordered array whose element is a scalar, a ref, or a flat record. Starts empty; append/removeAt/removeWhere/setField/clear mutate, listLength/listAt/listCount/listIndexOf read." },
|
|
195
|
-
{ "type": "object", "additionalProperties": false, "required": ["type", "keys"], "properties": { "type": { "const": "counterMap" }, "keys": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true } }, "description": "Tier 2 Phase 4.5.13: a closed-enum-keyed number map (each key 0 at start). addCount/clear mutate, count reads." }
|
|
273
|
+
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "number" }, "default": { "type": "number" }, "min": { "type": "number" }, "max": { "type": "number" }, "integer": { "type": "boolean" }, "persistent": { "type": "boolean", "description": "Data Store: this var survives the session. On a playerVar: loaded at join, flushed on save/checkpoint/leave (scalars only, no merge). On a roomVar: loaded at room create, flushed on saveRoom, and `merge` becomes required." }, "merge": { "$ref": "#/$defs/mergePolicy" }, "key": { "$ref": "#/$defs/persistKey" } } },
|
|
274
|
+
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "string" }, "default": { "type": "string" }, "maxLen": { "type": "integer", "minimum": 1 }, "enum": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, "persistent": { "type": "boolean", "description": "Data Store: this var survives the session. On a playerVar: loaded at join, flushed on save/checkpoint/leave (scalars only, no merge). On a roomVar: loaded at room create, flushed on saveRoom, and `merge` becomes required." }, "merge": { "$ref": "#/$defs/mergePolicy" }, "key": { "$ref": "#/$defs/persistKey" } } },
|
|
275
|
+
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "boolean" }, "default": { "type": "boolean" }, "persistent": { "type": "boolean", "description": "Data Store: this var survives the session. On a playerVar: loaded at join, flushed on save/checkpoint/leave (scalars only, no merge). On a roomVar: loaded at room create, flushed on saveRoom, and `merge` becomes required." }, "merge": { "$ref": "#/$defs/mergePolicy" }, "key": { "$ref": "#/$defs/persistKey" } } },
|
|
276
|
+
{ "type": "object", "additionalProperties": false, "required": ["type", "default"], "properties": { "type": { "const": "vec3" }, "default": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 }, "persistent": { "type": "boolean", "description": "Data Store: this var survives the session. On a playerVar: loaded at join, flushed on save/checkpoint/leave (scalars only, no merge). On a roomVar: loaded at room create, flushed on saveRoom, and `merge` becomes required." }, "merge": { "$ref": "#/$defs/mergePolicy" }, "key": { "$ref": "#/$defs/persistKey" } } },
|
|
277
|
+
{ "type": "object", "additionalProperties": false, "required": ["type", "of"], "properties": { "type": { "const": "ref" }, "of": { "type": "string", "pattern": "^(player|entity:[a-z0-9][a-z0-9-]*)$" }, "default": { "type": "null" }, "persistent": { "type": "boolean", "description": "Always invalid on a ref — accepted here only so the validator can explain WHY (a ref names a live player or entity in this session) instead of failing as an unreadable oneOf cascade." }, "merge": { "$ref": "#/$defs/mergePolicy" }, "key": { "$ref": "#/$defs/persistKey" } } },
|
|
278
|
+
{ "type": "object", "additionalProperties": false, "required": ["type", "of", "maxLen"], "properties": { "type": { "const": "list" }, "of": { "$ref": "#/$defs/listElem" }, "maxLen": { "type": "integer", "minimum": 1 }, "persistent": { "type": "boolean", "description": "Data Store: this list survives the session. roomVars only — a player's save is scalar fields. Requires `merge`: \"append\"." }, "merge": { "$ref": "#/$defs/mergePolicy" }, "key": { "$ref": "#/$defs/persistKey" } }, "description": "Tier 2 (4.5.13 + P3 Collections): a bounded ordered array whose element is a scalar, a ref, or a flat record. Starts empty; append/removeAt/removeWhere/setField/clear mutate, listLength/listAt/listCount/listIndexOf read." },
|
|
279
|
+
{ "type": "object", "additionalProperties": false, "required": ["type", "keys"], "properties": { "type": { "const": "counterMap" }, "keys": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, "persistent": { "type": "boolean", "description": "Data Store: this counterMap survives the session. roomVars only — a player's save is scalar fields. Requires `merge`: \"sum\"." }, "merge": { "$ref": "#/$defs/mergePolicy" }, "key": { "$ref": "#/$defs/persistKey" } }, "description": "Tier 2 Phase 4.5.13: a closed-enum-keyed number map (each key 0 at start). addCount/clear mutate, count reads." }
|
|
196
280
|
]
|
|
197
281
|
},
|
|
198
282
|
"listElem": {
|
|
@@ -220,6 +304,17 @@
|
|
|
220
304
|
"propertyNames": { "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" },
|
|
221
305
|
"additionalProperties": { "$ref": "#/$defs/payloadValue" }
|
|
222
306
|
},
|
|
307
|
+
"counterDecl": {
|
|
308
|
+
"description": "A declared world counter (Data Store): {} for now — room for per-counter config later, the way eventDecl grew a payload.",
|
|
309
|
+
"type": "object",
|
|
310
|
+
"additionalProperties": false
|
|
311
|
+
},
|
|
312
|
+
"leaderboardDecl": {
|
|
313
|
+
"description": "A declared leaderboard (Data Store). `order` picks which score is best: 'desc' (default — high scores) or 'asc' (times). `size` is how many entries the board KEEPS (1..100, default 100) — one bounded document per board, scores below the cutoff are not stored. One declared board must be named \"main\" (the world's default, surfaced by the platform).",
|
|
314
|
+
"type": "object",
|
|
315
|
+
"additionalProperties": false,
|
|
316
|
+
"properties": { "order": { "enum": ["desc", "asc"] }, "size": { "type": "integer", "minimum": 1, "maximum": 100 } }
|
|
317
|
+
},
|
|
223
318
|
"timerDecl": {
|
|
224
319
|
"description": "A declared timer (spec §8). Room-scoped by default ({}); `keyed:'player'` makes it per-player — startTimer/cancelTimer/timerRemaining then require a `key` (a player ref) and timerElapsed binds `self`. Keyed-ness is a static property of the declared name (every use must agree).",
|
|
225
320
|
"type": "object",
|
|
@@ -240,6 +335,7 @@
|
|
|
240
335
|
"idleTimeout": { "type": "number", "exclusiveMinimum": 0, "description": "spec §9 (Phase 4.8, optional): seconds a shared entity may stay hostless (frozen, no eligible host) before despawning." },
|
|
241
336
|
"ownerLifecycle": { "enum": ["despawnWithOwner", "persist", "migrateToServer", "hostMigrate"], "description": "spec §9. Single-owner: despawnWithOwner (default) destroys on the owner's leave; persist leaves it frozen; migrateToServer freezes during a transient grace absence + the same owner resumes. Shared: hostMigrate (requires shared:true) — re-elect the least-loaded client on the host's leave." },
|
|
242
337
|
"transferPolicy": { "enum": ["fixed", "request", "takeover"], "description": "spec §9 (Phase 4.5.12): voluntary ownership transfer policy the requestOwnership/takeover verbs honor. 'fixed' (default) = the verbs no-op. 'request' = grant only when unowned (polite pickup). 'takeover' = also steal from a live owner. Requires authority:'owner'. A transfer bumps the entity's authorityEpoch + fires ownershipChanged." },
|
|
338
|
+
"group": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,31}$", "description": "Atomic physics ownership cohort. A claimGroup action claims and wakes every live entity whose declaration has this name." },
|
|
243
339
|
"physics": { "$ref": "#/$defs/entityPhysics" },
|
|
244
340
|
"rejoinOnRelease": { "enum": ["nearestPoint", "timeIndex", "seekBack"], "description": "networked-physics (Phase 3.5): how a HYBRID kind (physics + server motion) re-anchors its declared path when a client RELEASES it back to the server. 'nearestPoint' (default) re-bases the path to the closest point so it doesn't snap; 'timeIndex' resumes the original time origin; 'seekBack' eases toward the path. Only meaningful with both physics and a non-static motion." }
|
|
245
341
|
}
|
|
@@ -322,7 +418,7 @@
|
|
|
322
418
|
{ "type": "string" },
|
|
323
419
|
{ "type": "boolean" },
|
|
324
420
|
{ "type": "object", "additionalProperties": false, "required": ["vec3"], "properties": { "vec3": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 } } },
|
|
325
|
-
{ "type": "object", "additionalProperties": false, "required": ["var"], "properties": { "var": { "type": "string", "pattern": "^((room|self)\\.[A-Za-z][A-Za-z0-9_]{0,31}|action\\.args\\.[A-Za-z][A-Za-z0-9_]{0,31}|[A-Za-z][A-Za-z0-9_]{0,31})$" } }, "description": "A var value read: \"room.<v>\" / \"self.<v>\" / \"action.args.<a>\", or a bare name = a SCALAR list element bound by forEachInList / listCount / listIndexOf (§5 P3 ext). The bare form resolves only against a list-element binding; an unbound bare name is rejected by the validator." },
|
|
421
|
+
{ "type": "object", "additionalProperties": false, "required": ["var"], "properties": { "var": { "type": "string", "pattern": "^((room|self)\\.[A-Za-z][A-Za-z0-9_]{0,31}|action\\.args\\.[A-Za-z][A-Za-z0-9_]{0,31}|purchase\\.[A-Za-z][A-Za-z0-9_]{0,31}|[A-Za-z][A-Za-z0-9_]{0,31})$" } }, "description": "A var value read: \"room.<v>\" / \"self.<v>\" / \"action.args.<a>\" / \"purchase.<field>\" (the receipt payload inside an {on:purchase} rule), or a bare name = a SCALAR list element bound by forEachInList / listCount / listIndexOf (§5 P3 ext). The bare form resolves only against a list-element binding; an unbound bare name is rejected by the validator." },
|
|
326
422
|
{ "$ref": "#/$defs/refLvalue" },
|
|
327
423
|
{ "type": "object", "additionalProperties": false, "required": ["op", "a", "b"], "properties": { "op": { "enum": ["distance", "==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"] }, "a": { "$ref": "#/$defs/expr" }, "b": { "$ref": "#/$defs/expr" } } },
|
|
328
424
|
{ "type": "object", "additionalProperties": false, "required": ["op", "of"], "properties": { "op": { "enum": ["and", "or"] }, "of": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/expr" } } } },
|
|
@@ -337,7 +433,11 @@
|
|
|
337
433
|
{ "type": "object", "additionalProperties": false, "required": ["op", "min", "max"], "properties": { "op": { "const": "randomPoint" }, "min": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 }, "max": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 } }, "description": "Tier 2: a uniformly-random vec3 inside the [min,max] box (each component independent). Server-evaluated each call; vec3-typed. Use for random spawns: respawn to {op:randomPoint,min,max}." },
|
|
338
434
|
{ "type": "object", "additionalProperties": false, "required": ["op", "entity", "by"], "properties": { "op": { "const": "controlledBy" }, "entity": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] }, "by": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "§9 (P3 Ownership/A3): true iff `entity`'s controller is the player `by` resolves to — boolean. O(1) field read." },
|
|
339
435
|
{ "type": "object", "additionalProperties": false, "required": ["op", "of"], "properties": { "op": { "const": "hostLoad" }, "of": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "§9 (P3 Ownership/B8): the count of live entities the player `of` controls (its sim/host load) — number. O(entities)." },
|
|
340
|
-
{ "type": "object", "additionalProperties": false, "required": ["op", "a", "b"], "properties": { "op": { "const": "sameRef" }, "a": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] }, "b": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "§7.3 (P3-B2): true iff two Refs resolve to the same live member (ref identity); false if either is unset. e.g. self is the turn-order current actor." }
|
|
436
|
+
{ "type": "object", "additionalProperties": false, "required": ["op", "a", "b"], "properties": { "op": { "const": "sameRef" }, "a": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] }, "b": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "§7.3 (P3-B2): true iff two Refs resolve to the same live member (ref identity); false if either is unset. e.g. self is the turn-order current actor." },
|
|
437
|
+
{ "type": "object", "additionalProperties": false, "required": ["op", "passKey", "of"], "properties": { "op": { "const": "hasPass" }, "passKey": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$" }, "of": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "In-world purchases: true iff the player `of` holds the pass `passKey` — boolean, read from the room's per-seat entitlement snapshot (O(1), no I/O). `of` is REQUIRED (there is no implicit self). `passKey` mirrors the backend's grants charset; an unregistered key simply reads false." },
|
|
438
|
+
{ "type": "object", "additionalProperties": false, "required": ["op", "code", "of"], "properties": { "op": { "const": "balanceOf" }, "code": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$" }, "of": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "In-world purchases: the player `of`'s balance of the currency `code` — number, from the same O(1) entitlement snapshot. `of` is REQUIRED; an unregistered code reads 0." },
|
|
439
|
+
{ "type": "object", "additionalProperties": false, "required": ["op", "of"], "properties": { "op": { "const": "weaponDamage" }, "of": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] }, "target": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] }, "default": { "type": "number" } }, "description": "GUNS: per-shot damage of the gun the player `of` holds — number. The room resolves the shooter's replicated grip attachment against the definitions declared in multiplayer.weaponItems (base × pellets off the pinned definition; server-trusted, a client never names damage). `target` (a player Ref) applies the definition's distance falloff shooter→target. No gun / no match → `default` (0, must be >= 0). Requires multiplayer.weaponItems." },
|
|
440
|
+
{ "type": "object", "additionalProperties": false, "required": ["op", "item"], "properties": { "op": { "const": "itemDamage" }, "item": { "type": "string", "minLength": 1 }, "default": { "type": "number" } }, "description": "GUNS: per-shot damage of the PINNED item definition — number. `item` is one \"<vaultAssetId>@<version>\" pin that must ALSO be listed in multiplayer.weaponItems; the room returns that definition's damage.base × (pellets ?? 1), and `default` (0, must be >= 0) on any miss (unresolvable pin, no damage block). Keyed by the pin rather than by the gun a player holds — a thrown or fused weapon does its damage long after the hand moved on, where weaponDamage would silently read the wrong gun. NO falloff leg: a blast's distance term is blast-point→victim, which the world's own forEachPlayer fanout computes. Requires multiplayer.weaponItems." }
|
|
341
441
|
]
|
|
342
442
|
},
|
|
343
443
|
"refOp": {
|
|
@@ -388,10 +488,13 @@
|
|
|
388
488
|
]
|
|
389
489
|
},
|
|
390
490
|
"actionDecl": {
|
|
391
|
-
"description": "A declared client→server action (spec §11.5): an optional typed arg schema
|
|
491
|
+
"description": "A declared client→server action (spec §11.5): an optional typed arg schema and optional atomic physics-group claim performed before same-tick entity uploads.",
|
|
392
492
|
"type": "object",
|
|
393
493
|
"additionalProperties": false,
|
|
394
|
-
"properties": {
|
|
494
|
+
"properties": {
|
|
495
|
+
"args": { "type": "object", "propertyNames": { "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" }, "additionalProperties": { "$ref": "#/$defs/actionArgType" } },
|
|
496
|
+
"claimGroup": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,31}$" }
|
|
497
|
+
}
|
|
395
498
|
},
|
|
396
499
|
"refLvalue": {
|
|
397
500
|
"description": "A ref-addressed var slot (spec §7.1): a member's var. `ref` is a bound name (self/other) or a Ref object (refExpr); `var` is a bare identifier (a declared player var or the `position` built-in).",
|
|
@@ -417,12 +520,13 @@
|
|
|
417
520
|
{ "type": "object", "additionalProperties": false, "required": ["on", "zone"], "properties": { "on": { "enum": ["zoneEnter", "zoneExit", "zoneInside"] }, "zone": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" } } },
|
|
418
521
|
{ "type": "object", "additionalProperties": false, "required": ["on", "radius"], "properties": { "on": { "const": "playerContact" }, "radius": { "type": "number", "exclusiveMinimum": 0 } } },
|
|
419
522
|
{ "type": "object", "additionalProperties": false, "required": ["on", "name"], "properties": { "on": { "const": "action" }, "name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" } } },
|
|
523
|
+
{ "type": "object", "additionalProperties": false, "required": ["on"], "properties": { "on": { "const": "purchase" } }, "description": "In-world purchases: fires for the buyer's seat when the room accepts a verified purchase receipt. Binds `self` = the buyer and puts the receipt in scope as the read-only var reads \"purchase.productKey\" / \"purchase.purchaseId\"." },
|
|
420
524
|
{ "type": "object", "additionalProperties": false, "required": ["on", "phase"], "properties": { "on": { "enum": ["stateEnter", "stateExit"] }, "phase": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" } } },
|
|
421
525
|
{ "type": "object", "additionalProperties": false, "required": ["on", "timer"], "properties": { "on": { "const": "timerElapsed" }, "timer": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" } } },
|
|
422
526
|
{ "type": "object", "additionalProperties": false, "required": ["on", "kind"], "properties": { "on": { "enum": ["entitySpawn", "entityDestroy", "ownershipChanged"] }, "kind": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,31}$" } } },
|
|
423
527
|
{ "type": "object", "additionalProperties": false, "required": ["on", "scope", "var", "cmp", "value"], "properties": { "on": { "const": "varReached" }, "scope": { "enum": ["room", "self", "entity"] }, "kind": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,31}$" }, "var": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" }, "cmp": { "enum": ["==", "!=", "<", "<=", ">", ">="] }, "value": { "oneOf": [{ "type": ["number", "string", "boolean"] }, { "type": "object", "additionalProperties": false, "required": ["var"], "properties": { "var": { "type": "string", "pattern": "^((room|self)\\.[A-Za-z][A-Za-z0-9_]{0,31}|action\\.args\\.[A-Za-z][A-Za-z0-9_]{0,31})$" } } }, { "$ref": "#/$defs/refLvalue" }] } } }
|
|
424
528
|
] },
|
|
425
|
-
{ "type": "object", "required": ["on"], "properties": { "on": { "type": "string", "not": { "enum": ["tick", "playerJoin", "playerLeave", "playerDisconnect", "playerReconnect", "zoneEnter", "zoneExit", "zoneInside", "playerContact", "action", "stateEnter", "stateExit", "timerElapsed", "entitySpawn", "entityDestroy", "ownershipChanged", "varReached"] } } } }
|
|
529
|
+
{ "type": "object", "required": ["on"], "properties": { "on": { "type": "string", "not": { "enum": ["tick", "playerJoin", "playerLeave", "playerDisconnect", "playerReconnect", "zoneEnter", "zoneExit", "zoneInside", "playerContact", "action", "purchase", "stateEnter", "stateExit", "timerElapsed", "entitySpawn", "entityDestroy", "ownershipChanged", "varReached"] } } } }
|
|
426
530
|
]
|
|
427
531
|
},
|
|
428
532
|
"ruleEffect": {
|
|
@@ -438,6 +542,12 @@
|
|
|
438
542
|
{ "type": "object", "additionalProperties": false, "required": ["do", "as", "then"], "properties": { "do": { "const": "forEachPlayer" }, "as": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" }, "where": { "$ref": "#/$defs/expr" }, "includeEliminated": { "type": "boolean", "description": "§7.4 (P3-B2): when true, also iterate eliminated (active:false) connected players — for a round reset / announcement. Default false (the active set only)." }, "then": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ruleEffect" } } } },
|
|
439
543
|
{ "type": "object", "additionalProperties": false, "required": ["do", "kind", "as", "then"], "properties": { "do": { "const": "forEachEntity" }, "kind": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,31}$" }, "as": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" }, "where": { "$ref": "#/$defs/expr" }, "then": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ruleEffect" } } } },
|
|
440
544
|
{ "type": "object", "additionalProperties": false, "required": ["do", "list", "as", "then"], "properties": { "do": { "const": "forEachInList" }, "list": { "$ref": "#/$defs/lvalue" }, "as": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" }, "where": { "$ref": "#/$defs/expr" }, "then": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ruleEffect" } } }, "description": "§5 (P3 Collections ext): the bounded single-level loop over a list's elements — `as` binds the element (a scalar value via {var:as}; a Ref for a ref list; a record element whose fields read via {ref:as,var:field} / write via setField `as`). Can't nest a loop or structurally mutate the list it iterates." },
|
|
545
|
+
{ "type": "object", "additionalProperties": false, "required": ["do", "player"], "properties": { "do": { "const": "save" }, "player": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "Data Store: request a flush of this player's persistent vars. Marks the player dirty; the room debounces and batches the actual write." },
|
|
546
|
+
{ "type": "object", "additionalProperties": false, "required": ["do"], "properties": { "do": { "const": "saveRoom" }, "key": { "$ref": "#/$defs/persistKey" } }, "description": "Data Store: request a flush of the world's persistent roomVars. `key` names one declared destination; omitted, it flushes every destination whose snapshot actually changed (an unchanged document costs no write). Marks dirty; the room debounces and batches the actual writes." },
|
|
547
|
+
{ "type": "object", "additionalProperties": false, "required": ["do", "counter", "by"], "properties": { "do": { "const": "increment" }, "counter": { "type": "string", "minLength": 1 }, "field": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$" }, "by": { "$ref": "#/$defs/expr" } }, "description": "Data Store: add a bounded integer delta to a `field` (default \"value\") of a declared world counter. Accumulated room-side and flushed in batches — never a per-tick write." },
|
|
548
|
+
{ "type": "object", "additionalProperties": false, "required": ["do", "board", "player", "score"], "properties": { "do": { "const": "submitScore" }, "board": { "type": "string", "minLength": 1 }, "player": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] }, "score": { "$ref": "#/$defs/expr" } }, "description": "Data Store: submit a player's score to a declared leaderboard. The backend keeps the best per (board, player); a worse score is a no-op, so re-submitting is always safe." },
|
|
549
|
+
{ "type": "object", "additionalProperties": false, "required": ["do", "key", "player"], "properties": { "do": { "const": "awardAchievement" }, "key": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{1,79}$" }, "player": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "Achievements: ask the backend to grant a registered per-world achievement `key` to a player. Fire-and-forget and idempotent backend-side; an unregistered key is a counted runtime no-op, not a publish error. `key` mirrors the backend's registration charset (a lowercase slug, 2-80 chars)." },
|
|
550
|
+
{ "type": "object", "additionalProperties": false, "required": ["do", "code", "amount", "player"], "properties": { "do": { "const": "consume" }, "code": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$" }, "amount": { "type": "integer", "minimum": 1, "maximum": 1000000000 }, "player": { "const": "self" } }, "description": "In-world purchases: spend `amount` of the currency `code` from the event-bound player's own entitlements (`player` must be `self`; literal integer amount 1..1000000000). `code` mirrors the backend's grants charset; an unregistered or underfunded code is a counted runtime no-op, not a publish error." },
|
|
441
551
|
{ "type": "object", "additionalProperties": false, "required": ["do", "phase"], "properties": { "do": { "const": "transitionTo" }, "phase": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" } } },
|
|
442
552
|
{ "type": "object", "additionalProperties": false, "required": ["do", "timer", "seconds"], "properties": { "do": { "const": "startTimer" }, "timer": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" }, "seconds": { "$ref": "#/$defs/expr", "description": "Timer duration in SECONDS — a number Expr (a literal is the degenerate case). A literal below the one-tick floor fails publish; a sub-floor/NaN/∞/negative EXPRESSION result is clamped/skipped at runtime (spec §3.2)." }, "key": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } } },
|
|
443
553
|
{ "type": "object", "additionalProperties": false, "required": ["do", "timer"], "properties": { "do": { "const": "cancelTimer" }, "timer": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" }, "key": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } } },
|
|
@@ -454,7 +564,7 @@
|
|
|
454
564
|
{ "type": "object", "additionalProperties": false, "required": ["do", "order", "index"], "properties": { "do": { "const": "advanceTurn" }, "order": { "$ref": "#/$defs/lvalue" }, "index": { "$ref": "#/$defs/lvalue" } }, "description": "§7.4 (P3-B2): advance a turn `index` (a number var) over an `order` (`list of ref:player`) to the next active member — wraps + skips eliminated/disconnected. No-op if no other active member exists." },
|
|
455
565
|
{ "type": "object", "additionalProperties": false, "required": ["do", "player"], "properties": { "do": { "enum": ["eliminate", "revive"] }, "player": { "oneOf": [{ "type": "string", "minLength": 1 }, { "$ref": "#/$defs/refExpr" }] } }, "description": "§7.4 (P3-B2): toggle a player's reserved `active` flag. eliminate removes a still-connected player from the gameplay active set (a spectator — still rendered + reachable by broadcast); revive restores it." }
|
|
456
566
|
] },
|
|
457
|
-
{ "type": "object", "required": ["do"], "properties": { "do": { "type": "string", "not": { "enum": ["add", "set", "setRef", "teleport", "respawn", "forEachPlayer", "forEachEntity", "forEachInList", "broadcast", "transitionTo", "startTimer", "cancelTimer", "spawnEntity", "destroyEntity", "requestOwnership", "takeover", "append", "clear", "addCount", "removeAt", "removeWhere", "setField", "advanceTurn", "eliminate", "revive"] } } } }
|
|
567
|
+
{ "type": "object", "required": ["do"], "properties": { "do": { "type": "string", "not": { "enum": ["add", "set", "setRef", "teleport", "respawn", "forEachPlayer", "forEachEntity", "forEachInList", "broadcast", "transitionTo", "startTimer", "cancelTimer", "spawnEntity", "destroyEntity", "requestOwnership", "takeover", "append", "clear", "addCount", "removeAt", "removeWhere", "setField", "advanceTurn", "eliminate", "revive", "save", "saveRoom", "increment", "submitScore", "awardAchievement", "consume"] } } } }
|
|
458
568
|
]
|
|
459
569
|
}
|
|
460
570
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://helix.dev/schemas/package-envelope-v1.schema.json",
|
|
4
4
|
"title": "HELIX Package Envelope v1",
|
|
5
|
-
"description": "Thin platform envelope for non-world bundles (system | ability | asset-pack). Derived by the CLI at publish from the
|
|
5
|
+
"description": "Thin platform envelope for non-world bundles (system | ability | asset-pack | interactable). Derived by the CLI at publish from the package payload; worlds keep their own helix.json.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["envelopeVersion", "kind", "slug", "version", "title"],
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"description": "Envelope schema version. Determines which fields are available."
|
|
13
13
|
},
|
|
14
14
|
"kind": {
|
|
15
|
-
"enum": ["system", "ability", "asset-pack"],
|
|
16
|
-
"description": "What this bundle is.
|
|
15
|
+
"enum": ["system", "ability", "asset-pack", "interactable"],
|
|
16
|
+
"description": "What this bundle is. Interactables carry a validated client entry, authoritative server fragment, placement data and interface contract."
|
|
17
17
|
},
|
|
18
18
|
"slug": {
|
|
19
19
|
"type": "string",
|
|
@@ -80,6 +80,11 @@
|
|
|
80
80
|
"type": "string",
|
|
81
81
|
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
|
|
82
82
|
"description": "Systems only (optional): the full three.js version this system's code was built against (e.g. 0.172.0). `helix install` points the world's import map + KTX2 transcoder path at the platform-hosted runtime/three/<version>/ — the single shared three instance — so the world never bundles its own copy."
|
|
83
|
+
},
|
|
84
|
+
"rapier": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
|
|
87
|
+
"description": "Systems only (optional): the full Rapier version this system's code was built against (e.g. 0.14.0). `helix install` wires the world at the platform-hosted runtime/rapier/<version>/ — the single shared Rapier runtime — so the world never bundles its own copy."
|
|
83
88
|
}
|
|
84
89
|
},
|
|
85
90
|
"allOf": [
|
|
@@ -90,6 +95,10 @@
|
|
|
90
95
|
{
|
|
91
96
|
"if": { "required": ["kind"], "properties": { "kind": { "const": "system" } } },
|
|
92
97
|
"then": { "required": ["entry"] }
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"if": { "required": ["kind"], "properties": { "kind": { "const": "interactable" } } },
|
|
101
|
+
"then": { "required": ["entry"] }
|
|
93
102
|
}
|
|
94
103
|
]
|
|
95
104
|
}
|