@dcl-regenesislabs/opendcl 0.1.3-22336215175.commit-94dca45 → 0.1.3-22336575051.commit-c88f897

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 CHANGED
@@ -28,7 +28,7 @@ The result: **more creators building more scenes, faster.**
28
28
  - **Branded header** — on startup, displays a block-character "Decentraland" ASCII art banner with version and working directory. Falls back to a compact text header on narrow terminals
29
29
  - **Multi-provider LLM support** — works with Claude, OpenAI, Google, Ollama (free/local), OpenRouter, and more
30
30
  - **Scene-aware** — automatically detects your project's `scene.json`, SDK version, and entry points
31
- - **18 built-in skills** — scaffolding, 3D models, interactivity, UI, animations, multiplayer, authoritative server, audio/video, deployment (Genesis City & Worlds), optimization, smart items, camera control, lighting, player/avatar, NFT/blockchain, advanced rendering, advanced input
31
+ - **19 built-in skills** — scaffolding, 3D models, interactivity, UI, animations, multiplayer, authoritative server, audio/video, deployment (Genesis City & Worlds), optimization, smart items, camera control, lighting, player/avatar, NFT/blockchain, advanced rendering, advanced input, scene runtime
32
32
  - **Integrated commands** — `/init` to scaffold, `/preview` to launch the dev server, `/tasks` to manage running processes, `/review` to audit code
33
33
  - **TypeScript validation** — catches type errors immediately after writing code
34
34
  - **Free asset catalogs** — 2,700+ Creator Hub 3D models, 900+ CC0-licensed models, and 50 audio files the agent proactively suggests when building scenes
@@ -126,6 +126,7 @@ OpenDCL loads domain-specific skills on demand based on what you're asking:
126
126
  | `nft-blockchain` | Display NFTs, wallet checks, smart contracts |
127
127
  | `advanced-rendering` | Billboards, 3D text, materials, transparency |
128
128
  | `advanced-input` | Cursor state, movement restriction, WASD patterns |
129
+ | `scene-runtime` | Async tasks, fetch, timers, realm info, restricted actions, testing |
129
130
 
130
131
  ## How It Works
131
132
 
@@ -134,7 +135,7 @@ OpenDCL is built on [pi-coding-agent](https://github.com/badlogic/pi-mono), the
134
135
  - **System prompt** with SDK7 architecture knowledge (ECS, QuickJS sandbox, parcel constraints)
135
136
  - **Extensions** that detect your project, inject context, validate TypeScript, and provide slash commands
136
137
  - **Skills** with detailed guides for every common scene development task
137
- - **Reference docs** (87KB SDK reference, component tables, code examples, 3D asset and audio catalogs)
138
+ - **Reference docs** (SDK cheat sheet, component tables, 3D asset and audio catalogs)
138
139
 
139
140
  The agent has full access to standard coding tools (read, write, edit, bash, grep, find) and uses them to understand and modify your scene code.
140
141
 
@@ -0,0 +1,161 @@
1
+ # SDK7 Cheat Sheet
2
+
3
+ Quick reference for Decentraland SDK7 fundamentals. For detailed API usage, see the relevant skill.
4
+
5
+ ## Imports
6
+
7
+ ```typescript
8
+ import { engine, Entity, Transform, GltfContainer, MeshRenderer, MeshCollider,
9
+ Material, AudioSource, VideoPlayer, TextShape, Animator, Tween, TweenSequence,
10
+ Billboard, VisibilityComponent, PointerEvents, Raycast, RaycastResult,
11
+ AvatarAttach, AvatarModifierArea, NftShape, CameraModeArea, VirtualCamera,
12
+ pointerEventsSystem, tweenSystem, inputSystem, raycastSystem,
13
+ InputAction, ColliderLayer, AvatarAnchorPointType, CameraType,
14
+ syncEntity, parentEntity, removeEntityWithChildren,
15
+ executeTask, Schemas } from '@dcl/sdk/ecs'
16
+ import { Vector3, Quaternion, Color3, Color4, Matrix } from '@dcl/sdk/math'
17
+ import ReactEcs, { ReactEcsRenderer, UiEntity, Label, Button, Input, Dropdown } from '@dcl/sdk/react-ecs'
18
+ import { movePlayerTo, teleportTo, triggerEmote, changeRealm,
19
+ openExternalUrl, openNftDialog, triggerSceneEmote,
20
+ copyToClipboard } from '~system/RestrictedActions'
21
+ import { getSceneInformation, getRealm, readFile } from '~system/Runtime'
22
+ import { getWorldTime, getExplorerInformation } from '~system/EnvironmentApi'
23
+ import { signedFetch, getHeaders } from '~system/SignedFetch'
24
+ import { getPlayer } from '@dcl/sdk/src/players'
25
+ ```
26
+
27
+ ## ECS Core
28
+
29
+ ```typescript
30
+ // Entities
31
+ const entity = engine.addEntity()
32
+ engine.removeEntity(entity)
33
+ removeEntityWithChildren(engine, entity)
34
+
35
+ // Components — CRUD
36
+ Transform.create(entity, { position: Vector3.create(8, 1, 8) })
37
+ const t = Transform.get(entity) // read-only, throws if missing
38
+ const t = Transform.getMutable(entity) // mutable reference
39
+ const t = Transform.getOrNull(entity) // read-only, returns null if missing
40
+ Transform.has(entity) // boolean
41
+ Transform.deleteFrom(entity) // remove component
42
+ Transform.createOrReplace(entity, { ... }) // upsert
43
+
44
+ // Systems
45
+ engine.addSystem((dt: number) => { /* runs every frame */ })
46
+ engine.addSystem(mySystem, priority) // higher priority = runs first
47
+ engine.removeSystem(mySystem)
48
+
49
+ // Queries
50
+ for (const [entity, transform, mesh] of engine.getEntitiesWith(Transform, MeshRenderer)) {
51
+ // iterate entities that have both components
52
+ }
53
+ ```
54
+
55
+ ## Custom Components
56
+
57
+ ```typescript
58
+ const MyComponent = engine.defineComponent('game::MyComponent', {
59
+ score: Schemas.Int,
60
+ label: Schemas.String,
61
+ active: Schemas.Boolean,
62
+ speed: Schemas.Float,
63
+ position: Schemas.Vector3,
64
+ color: Schemas.Color4,
65
+ items: Schemas.Array(Schemas.String),
66
+ data: Schemas.Map({ key: Schemas.String }),
67
+ opt: Schemas.Optional(Schemas.Int),
68
+ kind: Schemas.EnumNumber<MyEnum>(MyEnum, MyEnum.Default),
69
+ choice: Schemas.OneOf({ str: Schemas.String, num: Schemas.Int }),
70
+ timestamp: Schemas.Int64, // use Int64 for Date.now() values
71
+ })
72
+ ```
73
+
74
+ ## Reserved Entities
75
+
76
+ ```typescript
77
+ engine.PlayerEntity // the local player
78
+ engine.CameraEntity // the camera
79
+ engine.RootEntity // scene root (parent of all top-level entities)
80
+ ```
81
+
82
+ ## Math Utilities
83
+
84
+ ```typescript
85
+ // Vector3
86
+ Vector3.create(x, y, z)
87
+ Vector3.add(a, b) Vector3.subtract(a, b)
88
+ Vector3.scale(v, n) Vector3.normalize(v)
89
+ Vector3.distance(a, b) Vector3.lerp(a, b, t)
90
+ Vector3.rotate(v, q) Vector3.Zero() Vector3.One() Vector3.Up()
91
+
92
+ // Quaternion
93
+ Quaternion.fromEulerDegrees(x, y, z)
94
+ Quaternion.fromAngleAxis(degrees, axis)
95
+ Quaternion.lookRotation(forward, up?)
96
+ Quaternion.multiply(a, b)
97
+ Quaternion.toEulerAngles(q)
98
+ Quaternion.slerp(a, b, t)
99
+ Quaternion.Zero() Quaternion.Identity()
100
+
101
+ // Color
102
+ Color3.create(r, g, b) // 0-1 range
103
+ Color4.create(r, g, b, a) // 0-1 range
104
+ Color4.Red() .Green() .Blue() .White() .Black() .Yellow() .Gray() .Purple()
105
+ ```
106
+
107
+ ## ColliderLayer Enum
108
+
109
+ ```typescript
110
+ ColliderLayer.CL_NONE // no collision
111
+ ColliderLayer.CL_POINTER // responds to pointer events / raycasts
112
+ ColliderLayer.CL_PHYSICS // blocks player movement
113
+ ColliderLayer.CL_CUSTOM1 … CL_CUSTOM8 // user-defined layers
114
+ ```
115
+
116
+ ## scene.json Schema
117
+
118
+ ```json
119
+ {
120
+ "ecs7": true,
121
+ "runtimeVersion": "7",
122
+ "display": { "title": "Scene Title", "description": "...", "navmapThumbnail": "thumbnail.png" },
123
+ "scene": { "parcels": ["0,0", "1,0"], "base": "0,0" },
124
+ "main": "bin/index.js",
125
+ "contact": { "name": "Author", "email": "email@example.com" },
126
+ "tags": ["game", "art"],
127
+ "spawnPoints": [
128
+ { "name": "spawn1", "default": true, "position": { "x": [1, 5], "y": [0, 0], "z": [2, 4] }, "cameraTarget": { "x": 8, "y": 1, "z": 8 } }
129
+ ],
130
+ "requiredPermissions": [
131
+ "ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE",
132
+ "ALLOW_TO_TRIGGER_AVATAR_EMOTE",
133
+ "ALLOW_MEDIA_HOSTNAMES"
134
+ ],
135
+ "allowedMediaHostnames": ["video.example.com"],
136
+ "featureToggles": { "voiceChat": "enabled" },
137
+ "worldConfiguration": {
138
+ "name": "my-world.dcl.eth",
139
+ "skyboxConfig": { "fixedHour": 14.0 }
140
+ }
141
+ }
142
+ ```
143
+
144
+ ## Scene Limits (by parcel count)
145
+
146
+ | Parcels | Entities | Triangles | Textures (MB) | Materials | Bodies | Height (m) |
147
+ |---------|----------|-----------|---------------|-----------|--------|------------|
148
+ | 1 | 512 | 10,000 | 10 | 20 | 64 | 20 |
149
+ | 2 | 1,024 | 10,000 | 10 | 20 | 64 | 20 |
150
+ | 4 | 2,048 | 20,000 | 20 | 40 | 128 | 40 |
151
+ | 9 | 4,096 | 40,000 | 40 | 80 | 256 | 40 |
152
+ | 16 | 4,096 | 40,000 | 40 | 80 | 256 | 40 |
153
+
154
+ ## Runtime Restrictions
155
+
156
+ - **Sandboxed QuickJS** — no Node.js APIs (`fs`, `http`, `path`, `process`)
157
+ - **setTimeout/setInterval** — supported (runtime polyfill)
158
+ - **fetch** — supported (plain and signed)
159
+ - **WebSocket** — supported
160
+ - **Entry point** — `export function main() {}` in `src/index.ts`
161
+ - **All coordinates** — in meters, Y is up, origin at southwest corner of base parcel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcl-regenesislabs/opendcl",
3
- "version": "0.1.3-22336215175.commit-94dca45",
3
+ "version": "0.1.3-22336575051.commit-c88f897",
4
4
  "description": "AI coding assistant for Decentraland SDK7 scene development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,5 +66,5 @@
66
66
  "prompts/",
67
67
  "context/"
68
68
  ],
69
- "commit": "94dca4521a7916dddf82d5ce3883c0c853b93c2a"
69
+ "commit": "c88f897269368599d19ce4323a89cc4889e58ec3"
70
70
  }
package/prompts/system.md CHANGED
@@ -9,7 +9,7 @@ You are **OpenDCL**, an AI coding assistant specialized in Decentraland SDK7 sce
9
9
  - You help creators build interactive 3D scenes for Decentraland using SDK7.
10
10
  - You are beginner-friendly: always explain what you're doing and why.
11
11
  - You are precise about SDK7 APIs and never invent components or functions that don't exist.
12
- - When unsure, read the context files in the `context/` directory for accurate SDK7 reference.
12
+ - When unsure, read the `context/sdk7-cheat-sheet.md` for quick SDK7 reference, or rely on the relevant skill for detailed API docs.
13
13
 
14
14
  ## Decentraland SDK7 Fundamentals
15
15
 
@@ -96,8 +96,7 @@ scene-project/
96
96
  ### Empty Folder (No scene.json)
97
97
  1. Ask the user what they want to build.
98
98
  2. **Use the `init` tool first** — this uses the official SDK scaffolding to create scene.json, package.json, tsconfig.json, and src/index.ts with the correct, up-to-date configuration, and installs dependencies. Never create these files manually.
99
- 3. After init completes, customize `scene.json` (title, description, parcels) and `src/index.ts` (scene code) based on what the user wants.
100
- 4. Use the `preview` tool to start the preview server.
99
+ 3. After init completes, customize `scene.json` (title, description, parcels) and add the first element to `src/index.ts`. Then offer next steps don't build the entire scene at once.
101
100
 
102
101
  ### Existing Scene
103
102
  1. Read scene.json and src/index.ts to understand the project.
@@ -110,7 +109,7 @@ scene-project/
110
109
  - For 3D models, use `GltfContainer.create(entity, { src: 'models/myModel.glb' })`.
111
110
  - `GltfContainer` only works with **local files** — never use external URLs for the `src` field. Always download models into the scene's `models/` directory first.
112
111
  - Place `.glb` files in a `models/` directory, textures in `images/`.
113
- - After writing TypeScript, use the `preview` tool to start the preview server.
112
+ - Don't start the preview server automatically after writing code. The user will type `/preview` when ready.
114
113
  - **Proactively suggest 3D assets**: When building a scene, always check both asset catalogs for free models that match the user's theme:
115
114
  - `context/open-source-3d-assets.md` — 991 CC0 models from Polygonal Mind (nature, medieval, cyberpunk, sci-fi, etc.)
116
115
  - `context/asset-packs-catalog.md` — 2,700+ models from the official Decentraland Creator Hub (furniture, structures, decorations, etc.)
@@ -126,3 +125,9 @@ You have these Decentraland-specific tools — **use them directly** when the us
126
125
 
127
126
  The user can also type these as `/init`, `/preview`, `/deploy`, `/tasks` slash commands directly.
128
127
  Additional user-only commands: `/review`, `/explain`, `/setup`, `/setup-ollama`
128
+
129
+ ## Pacing
130
+
131
+ **New scenes (no scene.json):** Work one step at a time. Scaffold first, then add one thing (a model, a piece of interactivity, a UI element). After each step, briefly say what you did and offer 2-3 concrete next steps as a numbered list. Don't combine unrelated changes in one response. If the user asks for something complex ("build a medieval tavern"), break it into steps and do the first one.
132
+
133
+ **Existing scenes:** Do exactly what the user asks — one focused change per response. Don't pile on extras the user didn't request (e.g., if they ask to add a door, don't also add furniture, lighting, and a UI). Keep each response to one logical change unless the user explicitly asks for more.
@@ -128,6 +128,23 @@ curl -o models/tree.glb "https://raw.githubusercontent.com/ToxSam/cc0-models-Pol
128
128
 
129
129
  > **Important**: `GltfContainer` only works with **local files**. Never use external URLs for the model `src` field. Always download models into `models/` first.
130
130
 
131
+ ### Checking Model Load State
132
+
133
+ Use `GltfContainerLoadingState` to check if a model has finished loading:
134
+
135
+ ```typescript
136
+ import { GltfContainer, GltfContainerLoadingState, LoadingState } from '@dcl/sdk/ecs'
137
+
138
+ engine.addSystem(() => {
139
+ const state = GltfContainerLoadingState.getOrNull(modelEntity)
140
+ if (state && state.currentState === LoadingState.FINISHED) {
141
+ console.log('Model loaded successfully')
142
+ } else if (state && state.currentState === LoadingState.FINISHED_WITH_ERROR) {
143
+ console.log('Model failed to load')
144
+ }
145
+ })
146
+ ```
147
+
131
148
  ## Model Best Practices
132
149
 
133
150
  - Keep models under 50MB per file for good loading times
@@ -186,6 +186,33 @@ pointerEventsSystem.onPointerDown(
186
186
  )
187
187
  ```
188
188
 
189
+ ### Raycast System Helpers
190
+
191
+ Use `raycastSystem` for convenient raycasting without manual component management:
192
+
193
+ ```typescript
194
+ import { raycastSystem, RaycastQueryType, ColliderLayer } from '@dcl/sdk/ecs'
195
+
196
+ // Register a continuous local-direction raycast
197
+ raycastSystem.registerLocalDirectionRaycast(
198
+ { entity: myEntity, opts: { queryType: RaycastQueryType.RQT_HIT_FIRST, direction: Vector3.Forward(), maxDistance: 16, collisionMask: ColliderLayer.CL_POINTER } },
199
+ (result) => {
200
+ if (result.hits.length > 0) {
201
+ console.log('Hit:', result.hits[0].entityId)
202
+ }
203
+ }
204
+ )
205
+
206
+ // Register a global-direction raycast
207
+ raycastSystem.registerGlobalDirectionRaycast(
208
+ { entity: myEntity, opts: { queryType: RaycastQueryType.RQT_HIT_FIRST, direction: Vector3.Down(), maxDistance: 20 } },
209
+ (result) => { /* handle hits */ }
210
+ )
211
+
212
+ // Remove raycast from entity
213
+ raycastSystem.removeRaycasterEntity(myEntity)
214
+ ```
215
+
189
216
  ## Best Practices
190
217
 
191
218
  - Always set `maxDistance` on pointer events (8-16m is typical)
@@ -235,4 +235,3 @@ engine.addSystem(actionBarSystem)
235
235
  - WASD keys (`IA_FORWARD`, etc.) also control player movement — polling them reads the movement state but doesn't override it
236
236
 
237
237
  For basic pointer events and click handlers, see the `add-interactivity` skill.
238
- For component field details, see `context/components-reference.md`.
@@ -50,8 +50,7 @@ Transform.create(label, { position: Vector3.create(8, 3, 8) })
50
50
  TextShape.create(label, {
51
51
  text: 'Hello World!',
52
52
  fontSize: 24,
53
- fontWeight: 'bold',
54
- color: Color4.White(),
53
+ textColor: Color4.White(),
55
54
  outlineColor: Color4.Black(),
56
55
  outlineWidth: 0.1,
57
56
  textAlign: TextAlignMode.TAM_MIDDLE_CENTER
@@ -83,7 +82,7 @@ Transform.create(floatingLabel, { position: Vector3.create(8, 4, 8) })
83
82
  TextShape.create(floatingLabel, {
84
83
  text: 'NPC Name',
85
84
  fontSize: 16,
86
- color: Color4.White(),
85
+ textColor: Color4.White(),
87
86
  outlineColor: Color4.Black(),
88
87
  outlineWidth: 0.08,
89
88
  textAlign: TextAlignMode.TAM_BOTTOM_CENTER
@@ -213,6 +212,52 @@ function lodSystem() {
213
212
  engine.addSystem(lodSystem)
214
213
  ```
215
214
 
215
+ ### Per-Node Material Overrides (GltfNodeModifiers)
216
+
217
+ Override materials on specific nodes within a GLTF model without modifying the model file:
218
+
219
+ ```typescript
220
+ import { GltfNode, GltfNodeState } from '@dcl/sdk/ecs'
221
+
222
+ // Hide a specific node in a model
223
+ GltfNode.create(entity, { path: 'RootNode/Armor', visible: false })
224
+
225
+ // Override a node's material
226
+ GltfNode.create(entity, {
227
+ path: 'RootNode/Helmet',
228
+ materialOverride: Material.Texture.Common({ src: 'images/custom-skin.png' })
229
+ })
230
+ ```
231
+
232
+ ### Avatar Texture
233
+
234
+ Generate a texture from a player's avatar:
235
+
236
+ ```typescript
237
+ Material.setPbrMaterial(portraitFrame, {
238
+ texture: Material.Texture.Avatar({ userId: '0x...' })
239
+ })
240
+ ```
241
+
242
+ ### Texture Modes
243
+
244
+ Control how textures are filtered and wrapped:
245
+
246
+ ```typescript
247
+ import { TextureFilterMode, TextureWrapMode } from '@dcl/sdk/ecs'
248
+
249
+ Material.setPbrMaterial(entity, {
250
+ texture: Material.Texture.Common({
251
+ src: 'images/pixel-art.png',
252
+ filterMode: TextureFilterMode.TFM_POINT, // crisp pixels (no smoothing)
253
+ wrapMode: TextureWrapMode.TWM_REPEAT // tile the texture
254
+ })
255
+ })
256
+ ```
257
+
258
+ Filter modes: `TFM_POINT` (pixelated), `TFM_BILINEAR` (smooth), `TFM_TRILINEAR` (smoothest).
259
+ Wrap modes: `TWM_REPEAT` (tile), `TWM_CLAMP` (stretch edges), `TWM_MIRROR` (mirror tile).
260
+
216
261
  ## Best Practices
217
262
 
218
263
  - Use `BillboardMode.BM_Y` instead of `BM_ALL` — looks more natural and renders faster
@@ -222,5 +267,3 @@ engine.addSystem(lodSystem)
222
267
  - `MTM_ALPHA_TEST` is cheaper than `MTM_ALPHA_BLEND` — use cutout when smooth transparency isn't needed
223
268
  - Combine Billboard + TextShape for floating name labels above NPCs or objects
224
269
  - Use VisibilityComponent for LOD systems instead of removing/re-adding entities
225
-
226
- For more component details, see `context/components-reference.md`.
@@ -163,6 +163,72 @@ function spinSystem(dt: number) {
163
163
  engine.addSystem(spinSystem)
164
164
  ```
165
165
 
166
+ ### Tween Helper Methods
167
+
168
+ Use shorthand helpers instead of creating Tween components manually:
169
+
170
+ ```typescript
171
+ import { Tween, EasingFunction } from '@dcl/sdk/ecs'
172
+
173
+ // Move
174
+ Tween.createOrReplace(entity, Tween.setMove(
175
+ Vector3.create(0, 1, 0), Vector3.create(0, 3, 0),
176
+ { duration: 1500, easingFunction: EasingFunction.EF_EASEINBOUNCE }
177
+ ))
178
+
179
+ // Rotate
180
+ Tween.createOrReplace(entity, Tween.setRotate(
181
+ Quaternion.fromEulerDegrees(0, 0, 0), Quaternion.fromEulerDegrees(0, 180, 0),
182
+ { duration: 2000, easingFunction: EasingFunction.EF_EASEOUTQUAD }
183
+ ))
184
+
185
+ // Scale
186
+ Tween.createOrReplace(entity, Tween.setScale(
187
+ Vector3.One(), Vector3.create(2, 2, 2),
188
+ { duration: 1000, easingFunction: EasingFunction.EF_LINEAR }
189
+ ))
190
+ ```
191
+
192
+ ### Yoyo Loop Mode
193
+
194
+ `TL_YOYO` reverses the tween at each end instead of restarting:
195
+
196
+ ```typescript
197
+ TweenSequence.create(entity, {
198
+ sequence: [{ duration: 1000, ... }],
199
+ loop: TweenLoop.TL_YOYO
200
+ })
201
+ ```
202
+
203
+ ### Detecting Tween Completion
204
+
205
+ Use `tweenSystem.tweenCompleted()` to check if a tween finished this frame:
206
+
207
+ ```typescript
208
+ engine.addSystem(() => {
209
+ if (tweenSystem.tweenCompleted(entity)) {
210
+ console.log('Tween finished on', entity)
211
+ }
212
+ })
213
+ ```
214
+
215
+ ### Animator Extras
216
+
217
+ Additional `Animator` features:
218
+
219
+ ```typescript
220
+ // Get a specific clip to modify
221
+ const clip = Animator.getClip(entity, 'Walk')
222
+
223
+ // shouldReset: restart animation from beginning when re-triggered
224
+ Animator.playSingleAnimation(entity, 'Attack', true) // resets to start
225
+
226
+ // weight: blend between animations (0.0 to 1.0)
227
+ const anim = Animator.getMutable(entity)
228
+ anim.states[0].weight = 0.5 // blend walk at 50%
229
+ anim.states[1].weight = 0.5 // blend idle at 50%
230
+ ```
231
+
166
232
  ## Best Practices
167
233
 
168
234
  - Use Tweens for simple A-to-B animations (doors, platforms, UI elements)
@@ -227,6 +227,68 @@ AudioSource.create(entity, { audioClipUrl: 'sounds/ambient_1.mp3', playing: true
227
227
 
228
228
  > **Important**: `AudioSource` only works with **local files**. Never use external URLs for the `audioClipUrl` field. Always download audio into `sounds/` first.
229
229
 
230
+ ### Video State Polling
231
+
232
+ Check video playback state programmatically:
233
+
234
+ ```typescript
235
+ import { videoEventsSystem, VideoState } from '@dcl/sdk/ecs'
236
+
237
+ engine.addSystem(() => {
238
+ const state = videoEventsSystem.getVideoState(videoEntity)
239
+ if (state) {
240
+ console.log('Video state:', state.state) // VideoState.VS_PLAYING, VS_PAUSED, etc.
241
+ console.log('Current time:', state.currentOffset)
242
+ }
243
+ })
244
+ ```
245
+
246
+ ### Audio Playback Events
247
+
248
+ Use the `AudioEvent` component to detect audio state changes:
249
+
250
+ ```typescript
251
+ import { AudioEvent } from '@dcl/sdk/ecs'
252
+
253
+ engine.addSystem(() => {
254
+ const event = AudioEvent.getOrNull(audioEntity)
255
+ if (event) {
256
+ console.log('Audio state:', event.state) // playing, paused, finished
257
+ }
258
+ })
259
+ ```
260
+
261
+ ### Permission for External Media
262
+
263
+ External audio/video URLs require the `ALLOW_MEDIA_HOSTNAMES` permission in scene.json:
264
+
265
+ ```json
266
+ {
267
+ "requiredPermissions": ["ALLOW_MEDIA_HOSTNAMES"],
268
+ "allowedMediaHostnames": ["stream.example.com", "cdn.example.com"]
269
+ }
270
+ ```
271
+
272
+ ### Multiple Video Surfaces
273
+
274
+ Share one VideoPlayer across multiple screens by referencing the same `videoPlayerEntity`:
275
+
276
+ ```typescript
277
+ Material.setPbrMaterial(screen1, {
278
+ texture: Material.Texture.Video({ videoPlayerEntity: videoEntity })
279
+ })
280
+ Material.setPbrMaterial(screen2, {
281
+ texture: Material.Texture.Video({ videoPlayerEntity: videoEntity })
282
+ })
283
+ ```
284
+
285
+ ### Video Limits & Tips
286
+
287
+ - **Simultaneous videos**: 1 in preview, 5 in Explorer, 10 max across the scene
288
+ - **Distance-based control**: Pause video when player is far away to save bandwidth
289
+ - **Supported formats**: `.mp4` (H.264), `.webm`, HLS (`.m3u8`) for live streaming
290
+ - **Live streaming**: Use HLS (`.m3u8`) URLs — most reliable across clients
291
+
230
292
  ## Important Notes
231
293
 
232
294
  - Audio files must be in the project's directory (relative paths from project root)
@@ -7,7 +7,7 @@ description: Build multiplayer scenes with a headless authoritative server that
7
7
 
8
8
  Build multiplayer Decentraland scenes where a **headless server** controls game state, validates changes, and prevents cheating. The same codebase runs on both server and client, with the server having full authority.
9
9
 
10
- Before reading this skill, read `{baseDir}/../../context/sdk7-complete-reference.md` for general SDK7 knowledge. For basic CRDT multiplayer (no server), see the `multiplayer-sync` skill instead.
10
+ For basic CRDT multiplayer (no server), see the `multiplayer-sync` skill instead.
11
11
 
12
12
  ## Setup
13
13
 
@@ -315,7 +315,7 @@ Put synced components and messages in `shared/` so both server and client import
315
315
  - **Log prefixes**: Use `[Server]` and `[Client]` prefixes in `console.log()` to distinguish server and client output in the terminal.
316
316
  - **Stale CRDT files**: If you see "Outside of the bounds of written data" errors, delete `main.crdt` and `main1.crdt` files and restart.
317
317
  - **Storage inspection**: Check `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json` to inspect persisted data during local development.
318
- - **No setTimeout/setInterval**: The DCL runtime does not support these. Use `engine.addSystem()` with a timer variable instead.
318
+ - **Timers**: `setTimeout`/`setInterval` are available via runtime polyfill. For game logic, prefer `engine.addSystem()` with a delta-time accumulator to stay in sync with the frame loop.
319
319
  - **Entity sync issues**: Verify you call `syncEntity(entity, [componentIds])` with the correct component IDs (`MyComponent.componentId`).
320
320
 
321
321
  ## Important Notes
@@ -324,6 +324,6 @@ Put synced components and messages in `shared/` so both server and client import
324
324
  - **Room readiness**: Clients must wait for `RealmInfo.get(engine.RootEntity).isConnectedSceneRoom` before sending messages.
325
325
  - **Custom vs built-in validation**: Custom components use global `validateBeforeChange((value) => ...)`. Built-in components (Transform, GltfContainer) use per-entity `validateBeforeChange(entity, (value) => ...)`.
326
326
  - **Single codebase**: Both server and client run the same `index.ts` entry point. Use `isServer()` to branch.
327
- - **No Node.js APIs**: The DCL runtime uses sandboxed QuickJS — no `fs`, `http`, `setTimeout`, etc. Use SDK-provided APIs (Storage, EnvVar, engine systems) instead.
327
+ - **No Node.js APIs**: The DCL runtime uses sandboxed QuickJS — no `fs`, `http`, etc. `setTimeout`/`setInterval` are supported. Use SDK-provided APIs (Storage, EnvVar, engine systems) for server-side operations.
328
328
  - **SDK branch**: The auth-server pattern requires `@dcl/sdk@auth-server`, not the standard `@dcl/sdk` package.
329
329
  - For basic CRDT multiplayer without a server, see the `multiplayer-sync` skill.
@@ -227,6 +227,76 @@ const HealthBar = () => (
227
227
  />
228
228
  ```
229
229
 
230
+ ### Screen Dimensions
231
+
232
+ Read screen size via `UiCanvasInformation`:
233
+
234
+ ```typescript
235
+ import { UiCanvasInformation } from '@dcl/sdk/ecs'
236
+
237
+ engine.addSystem(() => {
238
+ const canvas = UiCanvasInformation.getOrNull(engine.RootEntity)
239
+ if (canvas) {
240
+ console.log('Screen:', canvas.width, 'x', canvas.height)
241
+ }
242
+ })
243
+ ```
244
+
245
+ ### Nine-Slice Textures
246
+
247
+ Use `textureSlices` for scalable UI backgrounds (buttons, panels) that don't stretch corners:
248
+
249
+ ```tsx
250
+ <UiEntity
251
+ uiTransform={{ width: 200, height: 100 }}
252
+ uiBackground={{
253
+ textureMode: 'nine-slices',
254
+ texture: { src: 'images/panel.png' },
255
+ textureSlices: { top: 0.1, bottom: 0.1, left: 0.1, right: 0.1 }
256
+ }}
257
+ />
258
+ ```
259
+
260
+ ### Hover Events
261
+
262
+ Respond to mouse enter/leave for hover effects:
263
+
264
+ ```tsx
265
+ <UiEntity
266
+ uiTransform={{ width: 100, height: 40 }}
267
+ onMouseEnter={() => { isHovered = true }}
268
+ onMouseLeave={() => { isHovered = false }}
269
+ uiBackground={{ color: isHovered ? Color4.White() : Color4.Gray() }}
270
+ />
271
+ ```
272
+
273
+ ### Flex Wrap
274
+
275
+ Allow UI children to wrap to the next line:
276
+
277
+ ```tsx
278
+ <UiEntity uiTransform={{ flexWrap: 'wrap', width: 300 }}>
279
+ {items.map(item => (
280
+ <UiEntity key={item.id} uiTransform={{ width: 80, height: 80, margin: 4 }} />
281
+ ))}
282
+ </UiEntity>
283
+ ```
284
+
285
+ ### Dropdown Extras
286
+
287
+ The `Dropdown` component supports additional props:
288
+
289
+ ```tsx
290
+ <Dropdown
291
+ options={['Option A', 'Option B', 'Option C']}
292
+ selectedIndex={selectedIdx}
293
+ onChange={(idx) => { selectedIdx = idx }}
294
+ fontSize={14}
295
+ color={Color4.White()}
296
+ disabled={false}
297
+ />
298
+ ```
299
+
230
300
  ## Important Notes
231
301
 
232
302
  - React hooks (`useState`, `useEffect`, etc.) are **NOT** available — use module-level variables
@@ -206,5 +206,3 @@ engine.addSystem(followNpcCamera)
206
206
  - Read camera state via `engine.CameraEntity` — never try to write to it directly
207
207
  - For look-at detection, combine camera position with raycasting (see `add-interactivity` skill)
208
208
  - Camera control is read-only outside of VirtualCamera and CameraModeArea — you cannot directly move the player's camera
209
-
210
- For component field details, see `context/components-reference.md`.