@dcl-regenesislabs/opendcl 0.1.4 → 0.1.5-23161709858.commit-828c176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +4 -3
  2. package/context/sdk7-cheat-sheet.md +3 -3
  3. package/extensions/permissions/index.ts +1 -12
  4. package/package.json +2 -2
  5. package/skills/add-3d-models/SKILL.md +16 -1
  6. package/skills/add-interactivity/SKILL.md +146 -37
  7. package/skills/add-interactivity/references/input-reference.md +148 -0
  8. package/skills/advanced-input/SKILL.md +1 -1
  9. package/skills/advanced-rendering/SKILL.md +27 -11
  10. package/skills/animations-tweens/SKILL.md +54 -24
  11. package/skills/audio-video/SKILL.md +16 -1
  12. package/skills/audio-video/references/media-reference.md +184 -0
  13. package/skills/authoritative-server/SKILL.md +6 -4
  14. package/skills/authoritative-server/references/server-patterns.md +251 -0
  15. package/skills/build-ui/SKILL.md +27 -1
  16. package/skills/build-ui/references/ui-components.md +228 -0
  17. package/skills/camera-control/SKILL.md +3 -1
  18. package/skills/create-scene/SKILL.md +65 -1
  19. package/skills/deploy-scene/SKILL.md +26 -9
  20. package/skills/deploy-worlds/SKILL.md +13 -1
  21. package/skills/game-design/SKILL.md +230 -0
  22. package/skills/lighting-environment/SKILL.md +23 -15
  23. package/skills/multiplayer-sync/SKILL.md +198 -72
  24. package/skills/multiplayer-sync/references/networking-patterns.md +238 -0
  25. package/skills/nft-blockchain/SKILL.md +18 -31
  26. package/skills/optimize-scene/SKILL.md +55 -12
  27. package/skills/player-avatar/SKILL.md +18 -1
  28. package/skills/player-avatar/references/avatar-apis.md +152 -0
  29. package/skills/scene-runtime/SKILL.md +4 -2
  30. package/skills/scene-runtime/references/runtime-apis.md +206 -0
  31. package/skills/visual-feedback/SKILL.md +16 -1
@@ -1,10 +1,24 @@
1
1
  ---
2
2
  name: animations-tweens
3
- description: Animate objects in Decentraland scenes using Animator (GLTF animations), Tween (move/rotate/scale over time), and TweenSequence (chain animations). Use when user wants to animate, move, rotate, spin, slide, or create motion effects.
3
+ description: Animate objects in Decentraland scenes. Play GLTF model animations with Animator, create procedural motion with Tween (move/rotate/scale), and chain sequences with TweenSequence. Use when the user wants to animate, move, rotate, spin, slide, bob, or create motion effects. Do NOT use for audio/video playback (see audio-video).
4
4
  ---
5
5
 
6
6
  # Animations and Tweens in Decentraland
7
7
 
8
+ ## When to Use Which Animation Approach
9
+
10
+ | Need | Approach | When |
11
+ |------|----------|------|
12
+ | Play animation baked into a .glb model | `Animator` | Character walks, door opens, flag waves — any animation created in Blender/Maya |
13
+ | Move/rotate/scale an entity smoothly | `Tween` | Sliding doors, floating platforms, growing objects — procedural A-to-B motion |
14
+ | Chain multiple animations in sequence | `TweenSequence` | Patrol paths, multi-step doors, complex choreography |
15
+ | Continuous per-frame control | `engine.addSystem()` | Physics-like motion, following a target, custom easing |
16
+
17
+ **Decision flow:**
18
+ 1. Does the .glb model already have the animation? → `Animator`
19
+ 2. Is it a simple move/rotate/scale between two values? → `Tween`
20
+ 3. Do you need frame-by-frame control or custom math? → System with `dt`
21
+
8
22
  ## GLTF Animations (Animator)
9
23
 
10
24
  Play animations embedded in .glb models:
@@ -67,7 +81,7 @@ Tween.create(box, {
67
81
  end: Vector3.create(14, 1, 8)
68
82
  }),
69
83
  duration: 2000, // milliseconds
70
- easingFunction: EasingFunction.EF_EASEINOUTSINE
84
+ easingFunction: EasingFunction.EF_EASESINE
71
85
  })
72
86
  ```
73
87
 
@@ -100,7 +114,7 @@ Tween.create(box, {
100
114
  Chain multiple tweens to play one after another:
101
115
 
102
116
  ```typescript
103
- import { TweenSequence } from '@dcl/sdk/ecs'
117
+ import { TweenSequence, TweenLoop } from '@dcl/sdk/ecs'
104
118
 
105
119
  // First tween
106
120
  Tween.create(box, {
@@ -109,7 +123,7 @@ Tween.create(box, {
109
123
  end: Vector3.create(14, 1, 8)
110
124
  }),
111
125
  duration: 2000,
112
- easingFunction: EasingFunction.EF_EASEINOUTSINE
126
+ easingFunction: EasingFunction.EF_EASESINE
113
127
  })
114
128
 
115
129
  // Chain sequence
@@ -122,7 +136,7 @@ TweenSequence.create(box, {
122
136
  end: Vector3.create(2, 1, 8)
123
137
  }),
124
138
  duration: 2000,
125
- easingFunction: EasingFunction.EF_EASEINOUTSINE
139
+ easingFunction: EasingFunction.EF_EASESINE
126
140
  }
127
141
  ],
128
142
  loop: TweenLoop.TL_RESTART // Loop the entire sequence
@@ -133,12 +147,16 @@ TweenSequence.create(box, {
133
147
 
134
148
  Available easing functions from `EasingFunction`:
135
149
  - `EF_LINEAR` — Constant speed
136
- - `EF_EASEINQUAD` / `EF_EASEOUTQUAD` / `EF_EASEINOUTQUAD` — Quadratic
137
- - `EF_EASEINSINE` / `EF_EASEOUTSINE` / `EF_EASEINOUTSINE` — Sinusoidal (smooth)
138
- - `EF_EASEINEXPO` / `EF_EASEOUTEXPO` / `EF_EASEINOUTEXPO` — Exponential
139
- - `EF_EASEINELASTIC` / `EF_EASEOUTELASTIC` / `EF_EASEINOUTELASTIC` — Elastic bounce
140
- - `EF_EASEOUTBOUNCE` / `EF_EASEINBOUNCE` / `EF_EASEINOUTBOUNCE` — Bounce effect
141
- - `EF_EASEINBACK` / `EF_EASEOUTBACK` / `EF_EASEINOUTBACK` — Overshoot
150
+ - `EF_EASEINQUAD` / `EF_EASEOUTQUAD` / `EF_EASEQUAD` — Quadratic
151
+ - `EF_EASEINSINE` / `EF_EASEOUTSINE` / `EF_EASESINE` — Sinusoidal (smooth)
152
+ - `EF_EASEINEXPO` / `EF_EASEOUTEXPO` / `EF_EASEEXPO` — Exponential
153
+ - `EF_EASEINELASTIC` / `EF_EASEOUTELASTIC` / `EF_EASEELASTIC` — Elastic bounce
154
+ - `EF_EASEOUTBOUNCE` / `EF_EASEINBOUNCE` / `EF_EASEBOUNCE` — Bounce effect
155
+ - `EF_EASEINBACK` / `EF_EASEOUTBACK` / `EF_EASEBACK` — Overshoot
156
+ - `EF_EASEINCUBIC` / `EF_EASEOUTCUBIC` / `EF_EASECUBIC` — Cubic
157
+ - `EF_EASEINQUART` / `EF_EASEOUTQUART` / `EF_EASEQUART` — Quartic
158
+ - `EF_EASEINQUINT` / `EF_EASEOUTQUINT` / `EF_EASEQUINT` — Quintic
159
+ - `EF_EASEINCIRC` / `EF_EASEOUTCIRC` / `EF_EASECIRC` — Circular
142
160
 
143
161
  ## Custom Animation Systems
144
162
 
@@ -165,28 +183,28 @@ engine.addSystem(spinSystem)
165
183
 
166
184
  ### Tween Helper Methods
167
185
 
168
- Use shorthand helpers instead of creating Tween components manually:
186
+ Use shorthand helpers that create or replace the Tween component directly on the entity:
169
187
 
170
188
  ```typescript
171
189
  import { Tween, EasingFunction } from '@dcl/sdk/ecs'
172
190
 
173
- // Move
174
- Tween.createOrReplace(entity, Tween.setMove(
191
+ // Move — signature: Tween.setMove(entity, start, end, duration, easingFunction?)
192
+ Tween.setMove(entity,
175
193
  Vector3.create(0, 1, 0), Vector3.create(0, 3, 0),
176
- { duration: 1500, easingFunction: EasingFunction.EF_EASEINBOUNCE }
177
- ))
194
+ 1500, EasingFunction.EF_EASEINBOUNCE
195
+ )
178
196
 
179
- // Rotate
180
- Tween.createOrReplace(entity, Tween.setRotate(
197
+ // Rotate — signature: Tween.setRotate(entity, start, end, duration, easingFunction?)
198
+ Tween.setRotate(entity,
181
199
  Quaternion.fromEulerDegrees(0, 0, 0), Quaternion.fromEulerDegrees(0, 180, 0),
182
- { duration: 2000, easingFunction: EasingFunction.EF_EASEOUTQUAD }
183
- ))
200
+ 2000, EasingFunction.EF_EASEOUTQUAD
201
+ )
184
202
 
185
- // Scale
186
- Tween.createOrReplace(entity, Tween.setScale(
203
+ // Scale — signature: Tween.setScale(entity, start, end, duration, easingFunction?)
204
+ Tween.setScale(entity,
187
205
  Vector3.One(), Vector3.create(2, 2, 2),
188
- { duration: 1000, easingFunction: EasingFunction.EF_LINEAR }
189
- ))
206
+ 1000, EasingFunction.EF_LINEAR
207
+ )
190
208
  ```
191
209
 
192
210
  ### Yoyo Loop Mode
@@ -229,6 +247,18 @@ anim.states[0].weight = 0.5 // blend walk at 50%
229
247
  anim.states[1].weight = 0.5 // blend idle at 50%
230
248
  ```
231
249
 
250
+ ## Troubleshooting
251
+
252
+ | Problem | Cause | Solution |
253
+ |---------|-------|----------|
254
+ | GLTF animation not playing | Wrong clip name in `Animator.states` | Open the .glb in a viewer (e.g., Blender) to find exact clip names — they are case-sensitive |
255
+ | Animator component has no effect | Entity missing `GltfContainer` | `Animator` only works on entities that have a loaded GLTF model |
256
+ | Tween doesn't move | Start and end positions are the same | Verify `start` and `end` values differ in `Tween.Mode.Move()` |
257
+ | Tween plays once then stops | No `TweenSequence` with loop | Add `TweenSequence.create(entity, { sequence: [], loop: TweenLoop.TL_YOYO })` for back-and-forth |
258
+ | Animation jitters or stutters | Creating new Tween every frame | Only create Tween once, not inside a system — use `tweenSystem.tweenCompleted()` to chain |
259
+
260
+ > **Need 3D models to animate?** See the **add-3d-models** skill for loading GLTF models that contain animation clips.
261
+
232
262
  ## Best Practices
233
263
 
234
264
  - Use Tweens for simple A-to-B animations (doors, platforms, UI elements)
@@ -1,10 +1,23 @@
1
1
  ---
2
2
  name: audio-video
3
- description: Add audio sources, sound effects, music, audio streaming, and video players to Decentraland scenes. Use when user wants sound, music, audio, video screens, speakers, or media playback.
3
+ description: Add sound effects, music, audio streaming, and video players to Decentraland scenes. Covers AudioSource (local files), AudioStream (streaming URLs), VideoPlayer (video surfaces), video events, and media permissions. Use when the user wants sound, music, audio, video screens, radio, or media playback. Do NOT use for 3D model animations (see animations-tweens).
4
4
  ---
5
5
 
6
6
  # Audio and Video in Decentraland
7
7
 
8
+ ## When to Use Which Media Component
9
+
10
+ | Need | Component | Key Difference |
11
+ |------|-----------|---------------|
12
+ | Sound effect from a file (click, explosion, footstep) | `AudioSource` | Local file, spatial, one-shot or looping |
13
+ | Background music or radio stream | `AudioStream` | External URL, non-spatial, continuous |
14
+ | Video on a surface (screen, billboard) | `VideoPlayer` + `Material.Texture.Video` | Requires a mesh to display on |
15
+
16
+ **Decision flow:**
17
+ 1. Is it a local audio file? → `AudioSource`
18
+ 2. Is it a streaming URL (radio, live audio)? → `AudioStream`
19
+ 3. Is it video content? → `VideoPlayer` on a plane/mesh
20
+
8
21
  ## Audio Source (Sound Effects & Music)
9
22
 
10
23
  Play audio clips from files:
@@ -289,6 +302,8 @@ Material.setPbrMaterial(screen2, {
289
302
  - **Supported formats**: `.mp4` (H.264), `.webm`, HLS (`.m3u8`) for live streaming
290
303
  - **Live streaming**: Use HLS (`.m3u8`) URLs — most reliable across clients
291
304
 
305
+ For full component field details, supported formats, and advanced patterns, see `{baseDir}/references/media-reference.md`.
306
+
292
307
  ## Important Notes
293
308
 
294
309
  - Audio files must be in the project's directory (relative paths from project root)
@@ -0,0 +1,184 @@
1
+ # Media Components Reference
2
+
3
+ ## AudioSource — Full Fields
4
+
5
+ ```typescript
6
+ import { AudioSource } from '@dcl/sdk/ecs'
7
+
8
+ AudioSource.create(entity, {
9
+ audioClipUrl: 'sounds/effect.mp3', // Path to local audio file (required)
10
+ playing: false, // Start/stop playback
11
+ loop: false, // Loop when finished
12
+ volume: 1.0, // Volume 0.0 to 1.0
13
+ pitch: 1.0 // Playback speed (0.5 = half, 2.0 = double)
14
+ })
15
+ ```
16
+
17
+ **Supported formats:** `.mp3` (recommended), `.ogg`, `.wav`
18
+
19
+ Audio is spatial by default — volume decreases with distance from the entity. Place the entity where the sound should originate.
20
+
21
+ ### Playback Control
22
+
23
+ ```typescript
24
+ const audio = AudioSource.getMutable(entity)
25
+ audio.playing = true // Play
26
+ audio.playing = false // Stop
27
+ audio.volume = 0.5 // Adjust volume
28
+ audio.pitch = 1.5 // Speed up
29
+ ```
30
+
31
+ ### Reset and Replay
32
+
33
+ To replay a sound effect from the beginning:
34
+ ```typescript
35
+ const audio = AudioSource.getMutable(entity)
36
+ audio.playing = false
37
+ audio.playing = true // Restarts from beginning
38
+ ```
39
+
40
+ ## AudioStream — Full Fields
41
+
42
+ ```typescript
43
+ import { AudioStream } from '@dcl/sdk/ecs'
44
+
45
+ AudioStream.create(entity, {
46
+ url: 'https://stream.example.com/radio.mp3', // Streaming URL (required)
47
+ playing: true, // Start/stop stream
48
+ volume: 0.5 // Volume 0.0 to 1.0
49
+ })
50
+ ```
51
+
52
+ **Supported stream formats:** HTTP/HTTPS audio streams (`.mp3`, `.ogg`, `.aac`)
53
+
54
+ AudioStream is NOT spatial — it plays at the same volume regardless of player distance. Best for background music or radio.
55
+
56
+ ## VideoPlayer — Full Fields
57
+
58
+ ```typescript
59
+ import { VideoPlayer } from '@dcl/sdk/ecs'
60
+
61
+ VideoPlayer.create(entity, {
62
+ src: 'videos/clip.mp4', // Local file or external URL (required)
63
+ playing: true, // Start/stop playback
64
+ loop: false, // Loop when finished
65
+ volume: 1.0, // Volume 0.0 to 1.0
66
+ playbackRate: 1.0, // Playback speed
67
+ position: 0 // Start time in seconds
68
+ })
69
+ ```
70
+
71
+ **Supported formats:**
72
+ - `.mp4` (H.264) — most compatible
73
+ - `.webm` — good quality, smaller files
74
+ - `.ogg` — open format
75
+ - `.m3u8` (HLS) — live streaming, most reliable for streams
76
+
77
+ ### Video Texture Setup
78
+
79
+ VideoPlayer alone doesn't display video. You must create a video texture and apply it to a mesh:
80
+
81
+ ```typescript
82
+ // 1. Create mesh surface
83
+ MeshRenderer.setPlane(entity)
84
+
85
+ // 2. Create video texture referencing the VideoPlayer entity
86
+ const videoTexture = Material.Texture.Video({ videoPlayerEntity: entity })
87
+
88
+ // 3. Apply as basic material (best performance)
89
+ Material.setBasicMaterial(entity, { texture: videoTexture })
90
+
91
+ // OR as PBR material with emissive (self-lit screen)
92
+ Material.setPbrMaterial(entity, {
93
+ texture: videoTexture,
94
+ roughness: 1.0,
95
+ specularIntensity: 0,
96
+ metallic: 0,
97
+ emissiveTexture: videoTexture,
98
+ emissiveIntensity: 0.6,
99
+ emissiveColor: Color3.White()
100
+ })
101
+ ```
102
+
103
+ ### Live Streaming
104
+
105
+ ```typescript
106
+ // HLS stream
107
+ VideoPlayer.create(entity, {
108
+ src: 'https://example.com/stream.m3u8',
109
+ playing: true
110
+ })
111
+
112
+ // LiveKit video stream
113
+ VideoPlayer.create(entity, {
114
+ src: 'livekit-video://current-stream',
115
+ playing: true
116
+ })
117
+ ```
118
+
119
+ ### Video Events
120
+
121
+ ```typescript
122
+ import { videoEventsSystem, VideoState } from '@dcl/sdk/ecs'
123
+
124
+ videoEventsSystem.registerVideoEventsEntity(entity, (event) => {
125
+ console.log('State:', event.state) // VideoState enum
126
+ console.log('Time:', event.currentOffset) // Current playback time
127
+ console.log('Length:', event.videoLength) // Total duration
128
+ })
129
+
130
+ // Poll current state
131
+ const state = videoEventsSystem.getVideoState(entity)
132
+ ```
133
+
134
+ **VideoState values:** `VS_READY`, `VS_PLAYING`, `VS_PAUSED`, `VS_ERROR`, `VS_BUFFERING`, `VS_SEEKING`, `VS_NONE`
135
+
136
+ ### Multiple Screens, One Video
137
+
138
+ ```typescript
139
+ // One VideoPlayer, shared across screens
140
+ VideoPlayer.create(screen1, { src: 'videos/shared.mp4', playing: true })
141
+ const tex = Material.Texture.Video({ videoPlayerEntity: screen1 })
142
+ Material.setBasicMaterial(screen1, { texture: tex })
143
+ Material.setBasicMaterial(screen2, { texture: tex })
144
+ ```
145
+
146
+ ### Video Limits
147
+
148
+ | Quality Setting | Max Simultaneous Videos |
149
+ |----------------|------------------------|
150
+ | Low | 1 |
151
+ | Medium | 5 |
152
+ | High | 10 |
153
+
154
+ ### Media Permissions in scene.json
155
+
156
+ External audio/video URLs require permissions:
157
+
158
+ ```json
159
+ {
160
+ "requiredPermissions": ["ALLOW_MEDIA_HOSTNAMES"],
161
+ "allowedMediaHostnames": ["stream.example.com", "cdn.example.com"]
162
+ }
163
+ ```
164
+
165
+ ## AudioAnalysis (Advanced)
166
+
167
+ Real-time frequency and amplitude data from audio sources:
168
+
169
+ ```typescript
170
+ import { AudioAnalysis, AudioAnalysisView } from '@dcl/sdk/ecs'
171
+
172
+ // Enable analysis on an audio source entity
173
+ AudioAnalysis.create(audioEntity, {})
174
+
175
+ // Read analysis data in a system
176
+ engine.addSystem(() => {
177
+ const view = AudioAnalysisView.getOrNull(audioEntity)
178
+ if (view) {
179
+ // Use frequency/amplitude data for visualizers, beat detection, etc.
180
+ }
181
+ })
182
+ ```
183
+
184
+ Used for music visualizers, reactive environments, and beat-synced animations.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: authoritative-server
3
- description: Build multiplayer scenes with a headless authoritative server that controls game state, validates changes, and prevents cheating. Install @dcl/sdk@auth-server and run with hammurabi-server. Use isServer() to branch logic, registerMessages() for client-server communication, validateBeforeChange() for server-only components, Storage for persistence, and EnvVar for configuration. Use when user wants authoritative server, anti-cheat, server-side validation, persistent storage, environment variables, or server messages.
3
+ description: Build multiplayer Decentraland scenes with a headless authoritative server. Covers isServer() branching, registerMessages() for client-server communication, validateBeforeChange() for server-only state, Storage (world and player persistence), EnvVar (environment variables), and project structure. Use when the user wants authoritative multiplayer, anti-cheat, server-side validation, persistent storage, or server messages. Do NOT use for basic CRDT multiplayer without a server (see multiplayer-sync).
4
4
  ---
5
5
 
6
6
  # Authoritative Server Pattern
@@ -181,10 +181,10 @@ room.onMessage('gameEvent', (data) => {
181
181
  Before sending messages from the client, wait until state is synchronized:
182
182
 
183
183
  ```typescript
184
- import { isStateSynchronized } from '@dcl/sdk/network'
184
+ import { isStateSyncronized } from '@dcl/sdk/network'
185
185
 
186
186
  engine.addSystem(() => {
187
- if (!isStateSynchronized()) return
187
+ if (!isStateSyncronized()) return
188
188
 
189
189
  // Safe to send messages now
190
190
  room.send('playerJoin', { displayName: 'Player' })
@@ -323,10 +323,12 @@ Put synced components and messages in `shared/` so both server and client import
323
323
  ## Important Notes
324
324
 
325
325
  - **Use `Schemas.Int64` for timestamps**: `Schemas.Number` corrupts large numbers (13+ digits). Always use `Schemas.Int64` for values like `Date.now()`.
326
- - **State sync readiness**: Clients must wait for `isStateSynchronized()` (from `@dcl/sdk/network`) to return `true` before sending messages.
326
+ - **State sync readiness**: Clients must wait for `isStateSyncronized()` (from `@dcl/sdk/network`) to return `true` before sending messages. Note the intentional SDK typo: "Syncronized" not "Synchronized".
327
327
  - **Custom vs built-in validation**: Custom components use global `validateBeforeChange((value) => ...)`. Built-in components (Transform, GltfContainer) use per-entity `validateBeforeChange(entity, (value) => ...)`.
328
328
  - **Single codebase**: Both server and client run the same `index.ts` entry point. Use `isServer()` to branch.
329
329
  - **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.
330
330
  - **SDK branch (MANDATORY)**: The auth-server pattern requires `npm install @dcl/sdk@auth-server`, not the standard `@dcl/sdk`. Without it, `isServer()`, `registerMessages()`, `Storage`, and `EnvVar` are unavailable.
331
331
  - **scene.json required fields**: `authoritativeMultiplayer: true` must be set, and `logsPermissions: ["0xWalletAddress"]` must list wallet addresses that should see server logs.
332
332
  - For basic CRDT multiplayer without a server, see the `multiplayer-sync` skill.
333
+
334
+ For complete server setup examples, authentication flow, state reconciliation, Storage patterns, and EnvVar usage, see `{baseDir}/references/server-patterns.md`.
@@ -0,0 +1,251 @@
1
+ # Authoritative Server Patterns Reference
2
+
3
+ ## Complete Server Setup
4
+
5
+ ### Project Structure
6
+
7
+ ```
8
+ src/
9
+ ├── index.ts # Entry point — isServer() branching
10
+ ├── client/
11
+ │ ├── setup.ts # Client init, input handlers, message senders
12
+ │ └── ui.tsx # React ECS UI (reads synced state, sends messages)
13
+ ├── server/
14
+ │ ├── server.ts # Server init, game loop, message handlers
15
+ │ └── gameState.ts # Server state management
16
+ └── shared/
17
+ ├── schemas.ts # Custom component definitions + validateBeforeChange
18
+ └── messages.ts # Message definitions via registerMessages()
19
+ ```
20
+
21
+ ### Entry Point (index.ts)
22
+
23
+ ```typescript
24
+ import { isServer } from '@dcl/sdk/network'
25
+
26
+ export async function main() {
27
+ if (isServer()) {
28
+ const { initServer } = await import('./server/server')
29
+ initServer()
30
+ return
31
+ }
32
+
33
+ const { initClient } = await import('./client/setup')
34
+ const { setupUi } = await import('./client/ui')
35
+ initClient()
36
+ setupUi()
37
+ }
38
+ ```
39
+
40
+ ### Shared Schemas (shared/schemas.ts)
41
+
42
+ ```typescript
43
+ import { engine, Schemas, Entity } from '@dcl/sdk/ecs'
44
+ import { AUTH_SERVER_PEER_ID } from '@dcl/sdk/network/message-bus-sync'
45
+
46
+ // Custom synced component
47
+ export const GameState = engine.defineComponent('game:State', {
48
+ phase: Schemas.String,
49
+ score: Schemas.Int,
50
+ timeRemaining: Schemas.Int
51
+ })
52
+
53
+ // Global validation — only server can modify
54
+ GameState.validateBeforeChange((value) => {
55
+ return value.senderAddress === AUTH_SERVER_PEER_ID
56
+ })
57
+
58
+ // For built-in components, use per-entity validation
59
+ type ComponentWithValidation = {
60
+ validateBeforeChange: (entity: Entity, cb: (value: { senderAddress: string }) => boolean) => void
61
+ }
62
+
63
+ export function protectServerEntity(entity: Entity, components: ComponentWithValidation[]) {
64
+ for (const component of components) {
65
+ component.validateBeforeChange(entity, (value) => {
66
+ return value.senderAddress === AUTH_SERVER_PEER_ID
67
+ })
68
+ }
69
+ }
70
+ ```
71
+
72
+ ### Shared Messages (shared/messages.ts)
73
+
74
+ ```typescript
75
+ import { Schemas } from '@dcl/sdk/ecs'
76
+ import { registerMessages } from '@dcl/sdk/network'
77
+
78
+ export const Messages = {
79
+ // Client → Server
80
+ playerReady: Schemas.Map({ displayName: Schemas.String }),
81
+ playerAction: Schemas.Map({ action: Schemas.String, targetId: Schemas.Int }),
82
+
83
+ // Server → Client
84
+ gameStarted: Schemas.Map({ roundNumber: Schemas.Int }),
85
+ playerScored: Schemas.Map({ playerName: Schemas.String, points: Schemas.Int }),
86
+ gameEnded: Schemas.Map({ winnerId: Schemas.String })
87
+ }
88
+
89
+ export const room = registerMessages(Messages)
90
+ ```
91
+
92
+ ### Server Logic (server/server.ts)
93
+
94
+ ```typescript
95
+ import { engine, PlayerIdentityData, Transform } from '@dcl/sdk/ecs'
96
+ import { syncEntity } from '@dcl/sdk/network'
97
+ import { room } from '../shared/messages'
98
+ import { GameState, protectServerEntity } from '../shared/schemas'
99
+
100
+ export function initServer() {
101
+ // Create server-managed entities
102
+ const stateEntity = engine.addEntity()
103
+ GameState.create(stateEntity, { phase: 'lobby', score: 0, timeRemaining: 60 })
104
+ protectServerEntity(stateEntity, [Transform])
105
+ syncEntity(stateEntity, [GameState.componentId], 1)
106
+
107
+ // Handle client messages
108
+ room.onMessage('playerReady', (data, context) => {
109
+ if (!context) return
110
+ console.log(`[Server] ${data.displayName} ready (${context.from})`)
111
+ })
112
+
113
+ room.onMessage('playerAction', (data, context) => {
114
+ if (!context) return
115
+ // Validate action on server
116
+ const playerPos = getPlayerPosition(context.from)
117
+ if (isValidAction(data.action, playerPos)) {
118
+ applyAction(data)
119
+ }
120
+ })
121
+
122
+ // Game loop
123
+ engine.addSystem(gameLoopSystem)
124
+ }
125
+ ```
126
+
127
+ ## Authentication Flow
128
+
129
+ The auth server automatically provides player identity via `PlayerIdentityData`:
130
+
131
+ ```typescript
132
+ // Server reads actual player positions
133
+ engine.addSystem(() => {
134
+ for (const [entity, identity] of engine.getEntitiesWith(PlayerIdentityData)) {
135
+ const transform = Transform.getOrNull(entity)
136
+ if (!transform) continue
137
+
138
+ // identity.address = wallet address (verified by server)
139
+ // transform.position = actual player position (not client-reported)
140
+ console.log(`[Server] ${identity.address} at`, transform.position)
141
+ }
142
+ })
143
+ ```
144
+
145
+ Never trust client-reported positions. The server sees real positions via `PlayerIdentityData` + `Transform`.
146
+
147
+ ## State Reconciliation
148
+
149
+ When server state diverges from client state, the server always wins:
150
+
151
+ ```typescript
152
+ // Server-side: apply authoritative state
153
+ function reconcileState() {
154
+ const state = GameState.getMutable(stateEntity)
155
+
156
+ // Server calculates correct state
157
+ state.timeRemaining = Math.max(0, state.timeRemaining - 1)
158
+
159
+ if (state.timeRemaining <= 0 && state.phase === 'active') {
160
+ state.phase = 'ended'
161
+ room.send('gameEnded', { winnerId: findWinner() })
162
+ }
163
+ }
164
+ ```
165
+
166
+ Because `validateBeforeChange` blocks client writes, clients can only read the state and send messages. The server is the single source of truth.
167
+
168
+ ## Storage Patterns
169
+
170
+ ### World Storage (Global Data)
171
+
172
+ ```typescript
173
+ import { Storage } from '@dcl/sdk/server'
174
+
175
+ // Save leaderboard
176
+ await Storage.world.set('leaderboard', JSON.stringify([
177
+ { name: 'Alice', score: 100 },
178
+ { name: 'Bob', score: 85 }
179
+ ]))
180
+
181
+ // Load leaderboard
182
+ const data = await Storage.world.get<string>('leaderboard')
183
+ const leaderboard = data ? JSON.parse(data) : []
184
+
185
+ // Delete
186
+ await Storage.world.delete('leaderboard')
187
+ ```
188
+
189
+ ### Player Storage (Per-Player Data)
190
+
191
+ ```typescript
192
+ import { Storage } from '@dcl/sdk/server'
193
+
194
+ // Save player progress
195
+ await Storage.player.set(playerAddress, 'progress', JSON.stringify({
196
+ level: 5,
197
+ coins: 250,
198
+ achievements: ['first_kill', 'speedrun']
199
+ }))
200
+
201
+ // Load player progress
202
+ const saved = await Storage.player.get<string>(playerAddress, 'progress')
203
+ const progress = saved ? JSON.parse(saved) : { level: 1, coins: 0, achievements: [] }
204
+ ```
205
+
206
+ **Note:** Storage only accepts strings. Always `JSON.stringify()` objects and `String()` numbers.
207
+
208
+ **Local dev storage location:** `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json`
209
+
210
+ ## Environment Variables
211
+
212
+ ```typescript
213
+ import { EnvVar } from '@dcl/sdk/server'
214
+
215
+ // Read with defaults
216
+ const maxPlayers = parseInt((await EnvVar.get('MAX_PLAYERS')) || '4')
217
+ const gameDuration = parseInt((await EnvVar.get('GAME_DURATION')) || '300')
218
+ const debugMode = ((await EnvVar.get('DEBUG')) || 'false') === 'true'
219
+ ```
220
+
221
+ ### Local Development (.env file)
222
+
223
+ ```
224
+ MAX_PLAYERS=8
225
+ GAME_DURATION=300
226
+ DEBUG=true
227
+ ```
228
+
229
+ ### Production Deployment
230
+
231
+ ```bash
232
+ npx sdk-commands deploy-env MAX_PLAYERS --value 8
233
+ npx sdk-commands deploy-env GAME_DURATION --value 300
234
+ npx sdk-commands deploy-env OLD_VAR --delete
235
+ ```
236
+
237
+ ## scene.json Required Fields
238
+
239
+ ```json
240
+ {
241
+ "authoritativeMultiplayer": true,
242
+ "worldConfiguration": {
243
+ "name": "my-world.dcl.eth"
244
+ },
245
+ "logsPermissions": ["0xYourWalletAddress"]
246
+ }
247
+ ```
248
+
249
+ - `authoritativeMultiplayer: true` — enables the headless server runtime
250
+ - `worldConfiguration.name` — identifies the world (required for Storage and deploy)
251
+ - `logsPermissions` — wallet addresses that can see `console.log()` from the server