> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/creator/content-creator-es/scenes-sdk7/networking/authoritative-servers.md).

# Multiplayer Server

Crea escenas multijugador de Decentraland con un Multiplayer Server sin interfaz.

**📔 Nota**: El Multiplayer Server se llamaba anteriormente el **Servidor autoritativo**. Solo cambió el nombre, la función es la misma. La rama del SDK que hay que instalar sigue llamándose `auth-server`, ver [Configuración](#setup).

## Descripción general

Decentraland ejecuta las scenes localmente en la máquina de un jugador. De forma predeterminada, los jugadores pueden verse entre sí e interactuar directamente, pero cada uno interactúa con el entorno de manera independiente. Los cambios en el entorno no se comparten entre los jugadores de forma predeterminada.

Permitir que todos los jugadores vean una scene como si tuviera el mismo contenido en el mismo estado es extremadamente importante para que los jugadores puedan interactuar de formas más significativas. Sin esto, si un jugador abre una puerta y entra en una casa, los demás jugadores verán esa puerta como todavía cerrada, y el primer jugador parecerá atravesar directamente la puerta cerrada para los demás.

El **Multiplayer Server** es un proceso de servidor sin interfaz que ejecuta el código de tu scene, valida los cambios de estado y transmite el resultado a todos los jugadores conectados. Sigue una arquitectura de servidor autoritativo: en lugar de confiar en que cada cliente informe sus propias acciones, el servidor actúa como la única fuente de verdad. Esto lo convierte en el enfoque recomendado para sincronizar scenes multijugador.

El Multiplayer Server es ideal siempre que la justicia sea importante para la mecánica del juego, ya que puedes implementar validaciones antitrampas elaboradas que se ejecutan del lado del servidor. También puedes almacenar claves privadas y otra información sensible en el servidor, evitando tener que exponerla directamente al usuario.

Tener un Multiplayer Server también resuelve un problema real: en una configuración peer-to-peer, dos jugadores que controlan algo como una plataforma flotante pueden producir resultados contradictorios. Cada cliente establece la plataforma a una altura diferente, y nadie tiene la autoridad para decidir cuál es la correcta. El Multiplayer Server resuelve cada cambio en un solo lugar, por lo que todos los clientes convergen en el mismo estado.

También te da un lugar para **persistir datos entre sesiones**: tablas de clasificación, progreso del jugador, logros desbloqueados o cambios en el entorno como puertas abiertas o ítems colocados. Cuando los jugadores regresan, el mundo refleja lo que ocurrió antes.

Decentraland aloja e implementa el servidor por ti. Publicar tu scene mediante el proceso normal también publica el servidor sin problemas, sin pasos adicionales ni necesidad de pagar por alojamiento.

## Configuración

### 1. Instala la versión auth-server del SDK

Las APIs nativas del Multiplayer Server (`isServer`, `registerMessages`, `Storage`, `EnvVar`, etc.) están disponibles en una rama separada del SDK. Ejecuta los siguientes comandos para instalarla en tu proyecto en lugar de la rama estándar del SDK:

```bash
npm install @dcl/sdk@auth-server
npm install @dcl/js-runtime@auth-server
```

### 2. Configura scene.json

El Multiplayer Server se habilita con la bandera `"authoritativeMultiplayer": true` en `scene.json`, a nivel raíz. No necesitas añadirla manualmente: cuando usas la rama auth-server del SDK, se añade automáticamente la primera vez que construyes o previsualizas la scene. Solo asegúrate de no eliminarla; sin esta bandera el servidor nunca se ejecuta y `isServer()` siempre devuelve *false*.

Opcionalmente, también puedes añadir lo siguiente a tu `scene.json` , a nivel raíz:

```json
{
  "logsPermissions": ["0xYourWalletAddress"]
}
```

Añade `logsPermissions` para listar las direcciones de wallet que pueden ver `console.log()` del servidor. Los usuarios listados pueden ver luego los logs del servidor en producción ejecutando el siguiente comando:

`npx sdk-commands sdk-server-logs`

### 3. Ejecuta la preview

Usa el comando estándar de preview, no se necesitan pasos adicionales. Cuando usas la rama auth-server del SDK, la preview inicia automáticamente una versión local del Multiplayer Server en segundo plano.

La sesión local del servidor no está conectada con la de producción, así que puedes probar cosas libremente sin afectar a los jugadores que estén en tu scene publicada.

## Ramificación servidor / cliente

El código de la scene en la carpeta `src` de tu proyecto se ejecuta tanto en el servidor como en el cliente. Usa la `isServer()` función para separar las rutas de ejecución:

```typescript
import { isServer } from "@dcl/sdk/network"
import { initServer } from "./server/server"
import { initClient } from "./client/setup"
import { setupUi } from "./client/ui"

function main() {
  if (isServer()) {
    // Solo servidor: lógica del juego, validación, gestión de estado
    initServer()
    return
  } else {
    // Solo cliente: UI, manejo de entrada, envío de mensajes
    initClient()
    setupUi()
  }
}
```

El servidor ejecuta tu scene sin interfaz y sin renderizado. Tiene acceso verificado a todas las posiciones de los jugadores, wearables y otros datos a través de `PlayerIdentityData` y es la única autoridad sobre el estado del juego.

## Componentes sincronizados y validación

### Sincronizar Entities con todos los clientes

Usa `syncEntity` para emitir cualquier cambio en los componentes indicados de esa Entity:

```typescript
import { isServer, syncEntity } from "@dcl/sdk/network"

if (isServer()) {
  syncEntity(
    entity,
    [Transform.componentId, GameState.componentId],
    /* enumId */ 1
  )
}
```

La sintaxis es idéntica a la que usa la función de [multijugador serverless](/creator/content-creator-es/scenes-sdk7/networking/serverless-multiplayer.md) feature, lo que hace trivial actualizar una scene que usa esta arquitectura al Multiplayer Server. Cuando una scene usa el Multiplayer Server, las actualizaciones de estado ya no se envían entre todos los jugadores; en su lugar, todas las actualizaciones de estado ahora se enrutan y validan a través del servidor.

{% hint style="warning" %}
**📔 Nota**: Con el Multiplayer Server, el patrón ideal es que solo el servidor llame a `syncEntity`. De ese modo no tienes que preocuparte por la coherencia del entity-id. En cambio, la Entity es instanciada y compartida por el servidor, y todos los clientes reciben la sincronización de esa instancia. Protéjalo siempre con `isServer()`. Esto es diferente de [multijugador serverless](/creator/content-creator-es/scenes-sdk7/networking/serverless-multiplayer.md), donde cada cliente llama a `syncEntity` por su cuenta.
{% endhint %}

### Validar cambios

Usa `validateBeforeChange()` para restringir cualquier actualización de estado en un componente específico de una Entity. Te permite ejecutar una función de validación personalizada, y los cambios solo se realizan correctamente cuando se cumple la prueba de validación.

Si la validación devuelve el valor *true*, entonces el cambio se acepta y se propaga a todos los jugadores. Si la validación devuelve el valor *false*false

#### valores de validación

El caso más simple es validar que el nuevo *valor* que se escribe esté dentro de ciertos parámetros. Por ejemplo, aceptar solo cambios en un `Transform` cuando la nueva posición Y esté por encima de 0:

```typescript
import { engine, Transform } from "@dcl/sdk/ecs"
import { Vector3 } from "@dcl/sdk/math"
import { isServer } from "@dcl/sdk/network"

const entity = engine.addEntity()
Transform.create(entity, { position: Vector3.create(10, 2, 10) })

if (isServer()) {
  Transform.validateBeforeChange(entity, (value) => {
    // Rechaza cualquier actualización que colocaría la Entity en Y = 0 o por debajo
    return value.newValue.position.y > 0
  })
}
```

Porque `validateBeforeChange()` solo tiene sentido en el servidor, siempre protégelo con `isServer()`. En el cliente la llamada no hace nada útil.

Puedes usar esto para evitar cambios que van en contra de la lógica de tu juego, como mecanismos antitrampas.

#### Validar la proximidad al jugador

Puedes combinar `validateBeforeChange()` con posiciones de jugadores verificadas por el servidor para comprobar que un jugador está lo bastante cerca de un objeto antes de permitirle interactuar con él. Por ejemplo, cuando un jugador intenta recoger un objeto cambiando su `Transform`, puedes rechazar el cambio si el objeto está a más de 5 metros del jugador:

```typescript
import { engine, Transform, PlayerIdentityData } from "@dcl/sdk/ecs"
import { Vector3 } from "@dcl/sdk/math"
import { isServer } from "@dcl/sdk/network"

const pickableEntity = engine.addEntity()
Transform.create(pickableEntity, { position: Vector3.create(8, 1, 8) })

if (isServer()) {
  Transform.validateBeforeChange(pickableEntity, (value) => {
    // Encuentra al jugador que envió este cambio
    for (const [playerEntity, identity] of engine.getEntitiesWith(
      PlayerIdentityData
    )) {
      if (identity.address.toLowerCase() !== value.senderAddress.toLowerCase())
        continue

      const playerTransform = Transform.getOrNull(playerEntity)
      if (!playerTransform) return false

      // Obtén la posición actual del objeto, antes del cambio
      const objectTransform = Transform.getOrNull(pickableEntity)
      if (!objectTransform) return false

      const distance = Vector3.distance(
        playerTransform.position,
        objectTransform.position
      )

      // Permite el cambio solo si el jugador está dentro de 5 metros
      return distance <= 5
    }

    // El remitente no se encontró entre los jugadores conectados — rechazar
    return false
  })
}
```

Este patrón es útil como mecanismo antitrampas: impide que los jugadores alcancen objetos a través de la scene para recogerlos cuando no deberían poder interactuar con ellos.

#### Permitir cambios solo a administradores

También puedes validar en función de *quién* está enviando el cambio. Cada valor entrante incluye un campo `senderAddress` con la dirección de wallet del remitente. Usa esto para permitir cambios solo de ciertos jugadores. Por ejemplo, para permitir que solo los administradores de la scene modifiquen un `VideoPlayer` componente:

```typescript
import { engine, VideoPlayer } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"
import { isPreview } from "@dcl/asset-packs/dist/admin-toolkit-ui/fetch-utils"
import { getSceneAdmins } from "@dcl/asset-packs/dist/admin-toolkit-ui/ModerationControl/api"

// Caché de direcciones de wallet de administradores, actualizada a partir de la lista de administradores de la scene
let adminAddresses = new Set<string>()

async function updateAdminAddresses() {
  if (isPreview()) return
  try {
    const [error, response] = await getSceneAdmins()
    if (error) {
      console.error("[SERVER] Error al obtener la lista de administradores:", error)
      adminAddresses = new Set()
      return
    }
    adminAddresses = new Set(
      (response ?? []).map((admin) => admin.admin.toLowerCase())
    )
    console.log(
      "[SERVER] Caché de direcciones de administradores actualizada:",
      Array.from(adminAddresses)
    )
  } catch (error) {
    console.error("[SERVER] Error al actualizar las direcciones de administradores:", error)
    adminAddresses = new Set()
  }
}

export async function main() {
  const videoEntity = engine.addEntity()
  VideoPlayer.create(videoEntity, { src: "videos/intro.mp4" })

  if (isServer()) {
    // Rellena la caché antes de conectar la validación
    await updateAdminAddresses()

    VideoPlayer.validateBeforeChange(videoEntity, (value) => {
      // Permite siempre los cambios mientras se ejecuta en preview, para facilitar las pruebas locales
      if (isPreview()) return true

      const senderAddress = value.senderAddress.toLowerCase()
      if (!adminAddresses.has(senderAddress)) {
        console.log(
          "[SERVER] Cambio no autorizado de VideoPlayer bloqueado desde:",
          senderAddress
        )
        return false
      }
      return true
    })
  }
}
```

Ver [Admin de la scene](/creator/content-creator-es/scene-editor/operar-en-vivo/scene-admin.md) para más contexto sobre cómo los jugadores se convierten en administradores de una scene.

#### Permitir cambios solo al servidor

El caso más estricto es aceptar solo escrituras que se originan en el propio servidor, rechazando cualquier cambio que venga de un cliente. Este es el patrón recomendado para estado que debe ser completamente autoritativo: puntuaciones, fase del juego, Entities generadas, etc.

Cada valor entrante incluye un campo `senderAddress` . Cuando el remitente es el servidor, este campo coincide con la constante `AUTH_SERVER_PEER_ID`, exportada desde `@dcl/sdk/network/message-bus-sync`.

El ejemplo siguiente define un pequeño helper `protectServerEntity()` que aplica esta comprobación a uno o más componentes en una Entity dada. Es una forma práctica de proteger varios componentes (como `Transform` y `GltfContainer`) en una sola llamada:

```typescript
import { engine, Entity, Transform, GltfContainer } from "@dcl/sdk/ecs"
import { Vector3 } from "@dcl/sdk/math"
import { isServer } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"

type ComponentWithValidation = {
  validateBeforeChange: (
    entity: Entity,
    cb: (value: { senderAddress: string }) => boolean
  ) => void
}

function protectServerEntity(
  entity: Entity,
  components: ComponentWithValidation[]
) {
  for (const component of components) {
    component.validateBeforeChange(entity, (value) => {
      return value.senderAddress === AUTH_SERVER_PEER_ID
    })
  }
}

if (isServer()) {
  // Después de crear una Entity gestionada por el servidor:
  const entity = engine.addEntity()
  Transform.create(entity, { position: Vector3.create(10, 5, 10) })
  GltfContainer.create(entity, { src: "assets/model.glb" })
  protectServerEntity(entity, [Transform, GltfContainer])
}
```

{% hint style="warning" %}
**📔 Nota**: Llama siempre a `protectServerEntity()` dentro de un bloque `isServer()` if. Envuelve `validateBeforeChange()`, que solo tiene sentido en el servidor; llamarlo en un cliente produce errores.
{% endhint %}

#### Componentes personalizados

También puedes aplicar `validateBeforeChange()` en componentes personalizados definidos por la scene.

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"

export const GameState = engine.defineComponent("game:State", {
  phase: Schemas.String,
  score: Schemas.Int,
  timeRemaining: Schemas.Int,
})

// Solo el servidor puede modificar este componente
if (isServer()) {
  GameState.validateBeforeChange((value) => {
    return value.senderAddress === AUTH_SERVER_PEER_ID
  })
}
```

## Mensajes

Los componentes sincronizados son excelentes para el estado que todos los jugadores deben ver continuamente: cosas como posiciones, puntuaciones o fase del juego. Pero no todo encaja en ese modelo. A veces, un jugador solo necesita decirle al servidor "he hecho clic en este botón" o "quiero unirme al juego", y el servidor debe responder con una respuesta de un solo uso como "la ronda ha comenzado" o "aquí están tus estadísticas". Eso son eventos, no estado persistente.

Eso es para lo que sirven los mensajes. Usa `registerMessages()` para la comunicación tipada y validada por esquema entre los clientes y el servidor. Los mensajes son fire-and-forget: un cliente envía uno al servidor, el servidor lo procesa y opcionalmente envía uno de vuelta. No crean por sí mismos ningún estado persistente directamente.

### Definir Messages

Define todos los messages en un archivo compartido que importen tanto el servidor como el cliente. De este modo, ambos lados siempre están de acuerdo sobre qué messages existen y qué datos transportan. Cada message es un `Schemas.Map` que describe su payload:

```typescript
import { Schemas } from "@dcl/sdk/ecs"
import { registerMessages } from "@dcl/sdk/network"

export const Messages = {
  // Cliente → Servidor
  playerReady: Schemas.Map({ displayName: Schemas.String }),
  playerAction: Schemas.Map({ action: Schemas.String, targetId: Schemas.Int }),

  // Servidor → Cliente
  gameStarted: Schemas.Map({ roundNumber: Schemas.Int }),
  gameEnded: Schemas.Map({ winnerId: Schemas.String }),
}

export const room = registerMessages(Messages)
```

{% hint style="warning" %}
**📔 Nota**: Llama a `registerMessages()` una vez, en el nivel superior de un módulo compartido, como en el ejemplo anterior. Así se ejecuta cuando el módulo se carga por primera vez, tanto en el servidor como en el cliente. No lo llames de forma condicional ni dentro de funciones; ambos lados siempre deben registrar los mismos messages.
{% endhint %}

### Enviar Messages

Los clientes solo pueden enviar mensajes al servidor. No existe mensajería directa de cliente a cliente. El servidor puede difundir mensajes a todos los clientes o dirigirlos a jugadores específicos por dirección.

```typescript
import { room } from "./shared/messages"

// Cliente → Servidor (difusión, el servidor lo recibe)
room.send("playerReady", { displayName: "Alice" })

// Servidor → todos los clientes
room.send("gameStarted", { roundNumber: 1 })

// Servidor → un cliente específico (por dirección de wallet)
room.send("gameEnded", { winnerId: "Alice" }, { to: [playerAddress] })
```

{% hint style="warning" %}
**📔 Nota**: Los messages tienen un tamaño máximo de alrededor de 13 KB. Los messages más grandes se descartan silenciosamente por el transporte, sin producir ningún error. Mantén los payloads pequeños; si necesitas compartir datos más grandes, divídelos en varios messages o usa componentes sincronizados.
{% endhint %}

### Recibir Messages

```typescript
import { room } from "./shared/messages"

// El cliente recibe del servidor
room.onMessage("gameStarted", (data) => {
  console.log(`¡La ronda ${data.roundNumber} ha comenzado!`)
})

// El servidor recibe del cliente
room.onMessage("playerReady", (data, context) => {
  if (!context) return
  const senderAddress = context.from // dirección de wallet verificada
  console.log(`[Server] ${data.displayName} está listo (${senderAddress})`)
})
```

En el servidor, cada mensaje recibido incluye un `context` objeto con la dirección de wallet verificada del remitente. Úsalo para saber qué jugador envió el mensaje (nunca confíes en la identidad autodeclarada en el payload).

### Espera a que se sincronice el estado antes de enviar

Los clientes deben esperar hasta que el estado de la scene esté sincronizado antes de enviar su primer mensaje, para evitar condiciones de carrera al unirse:

```typescript
import { engine } from "@dcl/sdk/ecs"
import { isStateSyncronized } from "@dcl/sdk/network"
import { room } from "./shared/messages"

engine.addSystem(() => {
  if (!isStateSyncronized()) return

  // Ya es seguro enviar mensajes
  room.send("playerReady", { displayName: "Alice" })
})
```

{% hint style="info" %}
**💡 Consejo**: `isStateSyncronized()` solo te indica que el estado del cliente está sincronizado; no garantiza que el servidor haya terminado de inicializarse. Para una comprobación de disponibilidad más robusta, haz que el servidor emita un mensaje de "listo" o de latido, y haz que los clientes esperen ese mensaje antes de enviar mensajes de gameplay.
{% endhint %}

### Tipos de esquema disponibles

Todos los payloads de los messages y los componentes personalizados usan `Schemas` para serialización binaria. Aquí tienes una referencia rápida de los tipos disponibles:

```typescript
import { Schemas } from "@dcl/sdk/ecs"

// Tipos básicos
Schemas.String // "hello"
Schemas.Int // 42
Schemas.Float // 3.14
Schemas.Boolean // true / false
Schemas.Int64 // Date.now()

// Tipos Vector
Schemas.Vector3 // { x: 1, y: 2, z: 3 }
Schemas.Quaternion // { x, y, z, w }

// Tipos complejos
Schemas.Array(Schemas.String) // ["a", "b", "c"]
Schemas.Entity // Referencia a Entity
Schemas.Optional(Schemas.String) // "hello" o undefined
Schemas.Optional(Schemas.Int) // 42 o undefined

// Objetos anidados
Schemas.Map({
  name: Schemas.String,
  health: Schemas.Int,
  position: Schemas.Vector3,
  playerId: Schemas.Optional(Schemas.String),
})
```

{% hint style="warning" %}
**📔 Nota**: Los messages *deben* definirse usando `Schemas.Map(...)`. No puedes enviar objetos JavaScript planos; fallarán en la serialización binaria.
{% endhint %}

## Lectura de posiciones de jugadores por parte del servidor

El servidor puede leer las posiciones de los jugadores **verificadas** ; los clientes no pueden falsificarlas. Esta es la base de la antitrampa basada en la posición:

```typescript
import { engine, PlayerIdentityData, Transform } from "@dcl/sdk/ecs"

engine.addSystem(() => {
  for (const [entity, identity] of engine.getEntitiesWith(PlayerIdentityData)) {
    const transform = Transform.getOrNull(entity)
    if (!transform) continue

    const address = identity.address
    const position = transform.position
    // Esta posición está verificada por el servidor — nunca confíes en la posición reportada por el cliente
  }
})
```

{% hint style="warning" %}
**📔 Nota**: Usa siempre `PlayerIdentityData` + `Transform` en el servidor para obtener las posiciones de los jugadores. Nunca confíes en los valores reportados por el propio cliente.
{% endhint %}

## Almacenamiento de datos

Persiste datos entre reinicios del servidor. Storage es **solo del servidor**, así que protege siempre las llamadas con `isServer()`. El servidor puede escribir y leer estos datos.

```typescript
import { Storage } from "@dcl/sdk/server"
```

Los datos se pueden almacenar en dos niveles:

* **World**: Úsalo para datos relevantes para todos los jugadores, como tablas de clasificación o cambios persistentes del entorno.
* **Player**: Úsalo para datos específicos de cada jugador, como guardar el progreso o las preferencias de ese jugador.

{% hint style="info" %}
**💡 Consejo**: Storage solo acepta strings. Usa `JSON.stringify()` / `JSON.parse()` para objetos y `String()` / `parseInt()` para números.

Durante el desarrollo local, Storage se escribe en `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json`.
{% endhint %}

{% hint style="warning" %}
**📔 Nota**: `Storage.set()`, `Storage.player.set()`, y las variantes de `delete` resuelven a un **booleano**. Nunca lanzan errores — en caso de fallo (un error de red, o demasiadas solicitudes concurrentes, ver abajo) registran el error y resuelven a `false`, lo que significa que el valor fue **no** persistió. Comprueba siempre el resultado: un `false` booleano descartado es un guardado perdido silenciosamente.
{% endhint %}

### Guarda en puntos de control, no en cada cambio

Storage es persistencia duradera para datos que deben sobrevivir a reinicios y redeploys del servidor. No es un datastore en vivo. Mantén el estado en curso de tu juego en memoria en el servidor. Es más rápido y es el patrón correcto para un servidor. Escribe en Storage solo cuando realmente lo necesites, en puntos de control significativos.

{% hint style="warning" %}
**⚠️ Advertencia**: El runtime del servidor permite un máximo de **40 llamadas al host en vuelo** a la vez, compartidas entre *todo* lo que la escena le pide al runtime que haga: cada solicitud de storage, `signedFetch`, y otras APIs del runtime cuentan todas contra el mismo límite. Las llamadas excesivas **no se ponen en cola**. Se rechazan inmediatamente con un `error de demasiadas llamadas concurrentes al host` . El SDK captura el rechazo y resuelve la `Storage.set` promesa a `false` en lugar de lanzar un error. Si tu código descarta ese booleano, el guardado fallido es invisible, y tus datos persistidos terminan desactualizados o perdidos. Comprueba el resultado de cada escritura (consulta la Nota de arriba).
{% endhint %}

Buenos momentos para persistir:

* Fin de partida, o el final de una ronda.
* Un jugador se va.
* Un guardado periódico con debounce (por ejemplo, una vez cada 30 segundos), no una vez por frame.

Persiste solo los datos que deben sobrevivir a un reinicio o redeploy. Todo lo demás puede vivir en memoria.

El ejemplo de abajo mantiene las puntuaciones en memoria y las escribe solo en un punto de control, en lugar de en cada punto anotado:

```typescript
import { Storage } from "@dcl/sdk/server"

// El estado en curso vive en memoria, actualizado en cada evento
const scores: Record<string, number> = {}

function onPointScored(address: string) {
  scores[address] = (scores[address] ?? 0) + 1
  // Sin llamada a Storage aquí — solo actualiza la memoria
}

// Persiste solo en un punto de control, como cuando un jugador se va
async function onPlayerLeave(address: string) {
  const saved = await Storage.player.set(address, "score", String(scores[address] ?? 0))
  if (!saved) {
    // La escritura no se persistió — mantén el valor en memoria e inténtalo de nuevo más tarde
    console.error(`No se pudo guardar la puntuación de ${address}`)
  }
}
```

Para escenas con muchos jugadores concurrentes, un enfoque más robusto rastrea qué keys tienen cambios no guardados (un "dirty set") y reintenta las escrituras fallidas en el siguiente flush:

```typescript
import { engine } from "@dcl/sdk/ecs"
import { Storage } from "@dcl/sdk/server"

// Estado en curso en memoria, actualizado en cada evento
const scores: Record<string, number> = {}
const dirty = new Set<string>()

function onPointScored(address: string) {
  scores[address] = (scores[address] ?? 0) + 1
  dirty.add(address) // Marcar para el siguiente flush, sin llamada a Storage aquí
}

// Vaciar las keys sucias periódicamente (p. ej., cada ~30s) y en puntos de control
async function flush() {
  for (const address of dirty) {
    const ok = await Storage.player.set(address, "score", String(scores[address]))
    if (ok) {
      dirty.delete(address) // Guardado correctamente
    }
    // Si ok es false, la key sigue sucia y se reintenta en el siguiente flush
  }
}

// Ejecuta el flush en un temporizador usando un acumulador dt
let flushTimer = 0
engine.addSystem((dt) => {
  flushTimer += dt
  if (flushTimer > 30) {
    flushTimer = 0
    flush()
  }
})
```

### Almacenamiento del World — Compartido entre todos los jugadores

```typescript
import { Storage } from "@dcl/sdk/server"

// Escribir — set() se resuelve en false si el valor NO se persistió
const ok = await Storage.set(
  "leaderboard",
  JSON.stringify([
    { name: "Alice", score: 100 },
    { name: "Bob", score: 85 },
  ])
)
if (!ok) {
  console.error("No se pudo guardar la tabla de clasificación — inténtalo más tarde")
}

// Leer
const raw = await Storage.get<string>("leaderboard")
const leaderboard = raw ? JSON.parse(raw) : []

// Eliminar
await Storage.delete("leaderboard")
```

También puedes gestionar el almacenamiento de la escena desde la línea de comandos, usando `npx sdk-commands storage scene`:

```bash
# Establecer un valor
npx sdk-commands storage scene set high_score --value 100

# Obtener un valor
npx sdk-commands storage scene get high_score

# Eliminar un valor
npx sdk-commands storage scene delete high_score

# Eliminar todos los datos de storage de la escena
npx sdk-commands storage scene clear --confirm
```

### Almacenamiento del jugador — Por dirección de Wallet

```typescript
import { Storage } from "@dcl/sdk/server"

// Escribir — set() se resuelve en false si el valor NO se persistió
const ok = await Storage.player.set(
  playerAddress,
  "progress",
  JSON.stringify({
    level: 5,
    coins: 250,
  })
)
if (!ok) {
  console.error(`No se pudo guardar el progreso de ${playerAddress} — inténtalo más tarde`)
}

// Leer
const saved = await Storage.player.get<string>(playerAddress, "progress")
const progress = saved ? JSON.parse(saved) : { level: 1, coins: 0 }

// Eliminar
await Storage.player.delete(playerAddress, "progress")
```

También puedes gestionar el almacenamiento del jugador desde la línea de comandos, usando `npx sdk-commands storage player`:

```bash
# Establecer un valor para un jugador específico
npx sdk-commands storage player set level --value 10 --address 0x1234...

# Obtener un valor para un jugador específico
npx sdk-commands storage player get level --address 0x1234...

# Eliminar un valor para un jugador específico
npx sdk-commands storage player delete level --address 0x1234...

# Eliminar todos los datos de un jugador específico
npx sdk-commands storage player clear --address 0x1234... --confirm

# Eliminar todos los datos del jugador (todos los jugadores)
npx sdk-commands storage player clear --confirm
```

### Acceder a los datos almacenados

Puedes ver y editar los datos almacenados en vivo en tu servidor a través de la storage UI, ingresando este enlace:

[decentraland.org/storage](https://decentraland.org/storage)

También puedes acceder a esta página desde el Creator Hub. Abre la **Manage** pestaña, haz clic en los tres puntos junto al lugar donde has publicado contenido y selecciona **View Storage**.

Ahí puedes ver una lista de todos los Worlds y LAND donde puedes publicar escenas.

Abre tu escena y luego la **Scene** o **Player** pestaña.

En la **Scene** pestaña verás una lista de todas las variables almacenadas. Desde aquí puedes editar o eliminar cualquiera de estas variables haciendo clic en el icono del lápiz o de la papelera.

![Activate stream](https://1216664193-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-cc204e102da9c2e63419b91f818f22a579e55ebf%2Fscene-data.png?alt=media\&token=c8cc218c-e4ea-4689-ba83-387115558db1)

En la **Player** pestaña verás una lista de todos los jugadores que tienen algún dato almacenado en tu servidor. Puedes buscarlos por dirección o nombre, y luego ver todos sus datos asociados. También puedes editar o eliminar estos datos haciendo clic en el icono del lápiz o de la papelera.

### Cambiar la estructura de datos

Los datos almacenados en producción **no se borran cuando publicas una nueva versión de tu escena**. Esto es ideal para tablas de clasificación, progreso del jugador y cambios persistentes del entorno que los jugadores esperan que sigan existiendo más allá de pequeñas actualizaciones de tu escena.

La otra cara es que los datos que están en storage fueron escritos por una versión anterior de tu código. Si tu nuevo código espera una forma distinta, analizar o leer esos datos antiguos puede fallar de maneras sutiles. Un campo que renombraste faltará. Un campo que antes era una string y ahora es un objeto lanzará un error cuando intentes acceder a una propiedad en él. Un jugador que no ha iniciado sesión en meses puede cargar datos que preceden a una estructura que tu código ya no sabe cómo manejar.

{% hint style="warning" %}
**📔 Nota**: Los cambios de esquema no solo afectan a la primera lectura después de un deploy. Los datos almacenados viven hasta que se sobrescriben o se eliminan, así que un valor de formato antiguo puede aparecer en cualquier momento, a menudo de un jugador que regresó y de quien te habías olvidado.
{% endhint %}

#### Mejores prácticas

* *Analiza siempre de forma defensiva*. Trata todo lo que salga del storage como entrada no confiable, aunque lo hayas escrito tú. Envuelve `JSON.parse()` en un `try/catch`, comprueba que los campos existan antes de leerlos, y ten listo un valor predeterminado sensato cuando no existan:

  ```typescript
  import { Storage } from "@dcl/sdk/server"

  const raw = await Storage.player.get<string>(playerAddress, "progress")
  let progress = { level: 1, coins: 0 }
  if (raw) {
    try {
      const parsed = JSON.parse(raw)
      progress = {
        level: typeof parsed.level === "number" ? parsed.level : 1,
        coins: typeof parsed.coins === "number" ? parsed.coins : 0,
      }
    } catch {
      // Datos antiguos o corruptos — vuelve a los valores predeterminados
    }
  }
  ```
* *Añade campos, no los renombres ni elimines*. El cambio de esquema más seguro es uno aditivo: introduce un nuevo campo con un valor predeterminado y deja los campos existentes intactos. Los datos antiguos simplemente no tendrán el nuevo campo, y tu análisis defensivo ya lo maneja. Renombrar un campo obliga a que todos los registros antiguos fallen.
* *Versiona tus objetos almacenados*. Incluye un `version` desde el primer día. Cuando leas los datos, decide según la versión y migra las formas antiguas a la actual antes de usarlas. Así el resto de tu código sigue funcionando con una única forma actual:

  ```typescript
  type ProgressV2 = { version: 2; level: number; coins: number; xp: number }

  function migrate(raw: any): ProgressV2 {
    const version = raw?.version ?? 1
    if (version === 1) {
      // v1 no tenía el campo xp — dale un valor predeterminado
      return {
        version: 2,
        level: raw.level ?? 1,
        coins: raw.coins ?? 0,
        xp: 0,
      }
    }
    return raw as ProgressV2
  }
  ```
* *Vuelve a escribir el valor migrado*. Una vez que hayas actualizado un registro en memoria, guárdalo de nuevo para que la próxima lectura ya esté en el nuevo formato. Con el tiempo esto drena el grupo de registros con forma antigua sin necesidad de un script de migración de una sola vez.
* *Para cambios incompatibles, usa una nueva key*. Si la nueva estructura es realmente incompatible y no merece la pena migrarla, escribe en una nueva key de storage (por ejemplo `progress_v2`) e ignora la anterior. La key antigua se queda inofensivamente en storage y evitas cualquier ruta de lectura que tenga que interpretarla. Puedes limpiar las keys antiguas más tarde a través de la [storage UI](https://decentraland.org/storage) o el `npx sdk-commands storage` commands.
* *Prueba con datos reales de producción*. Antes de desplegar un cambio estructural, extrae algunos registros reales desde la storage UI y ejecuta tu nuevo código de análisis sobre ellos. Los casos límite que causan problemas suelen ser registros que no sabías que existían.
* *Deja una salida de escape*. Ten en cuenta que puedes editar o eliminar registros individuales desde la storage UI o mediante `npx sdk-commands storage`. Si un solo jugador termina atascado en un mal estado después de un cambio de esquema, puedes corregir su registro directamente sin redeployar.

## Variables de entorno

Configura tu escena sin codificar valores directamente en el código. Las variables de entorno son útiles para datos sensibles, y también para feature flags o parámetros que se pueden cambiar fácilmente sin volver a publicar tu escena.

Las variables de entorno son **solo del servidor**solo del servidor `isServer()`. Protégelas con

`EnvVar.get()` devuelve un `Promise<string>` y se resuelve a una cadena vacía cuando la variable no está definida, así que proporciona siempre un valor de respaldo para los valores faltantes:

```typescript
import { EnvVar } from "@dcl/sdk/server"
import { isServer } from "@dcl/sdk/network"

export async function main() {
  if (!isServer()) return

  const maxPlayers = parseInt((await EnvVar.get("MAX_PLAYERS")) || "4")
  const gameDuration = parseInt((await EnvVar.get("GAME_DURATION")) || "300")
  const debugMode = ((await EnvVar.get("DEBUG")) || "false") === "true"
}
```

### Datos sensibles

Las variables de entorno son especialmente útiles para almacenar claves privadas, códigos de canje de recompensas y otros datos sensibles que sería arriesgado exponer en el código compilado de la escena pública.

Puedes almacenar claves privadas en el storage del servidor, y hacer que solo el servidor las lea con `isServer()`. Así los datos sensibles nunca viajan por la máquina del jugador.

### Desarrollo local

Para usar variables de entorno mientras ejecutas tu proyecto localmente, crea un `.env` archivo en la raíz de tu proyecto:

```
MAX_PLAYERS=8
GAME_DURATION=300
DEBUG=true
```

Importante: Añade `.env` a tu `.gitignore`, para que estos valores potencialmente sensibles nunca se suban a los servidores públicos de contenido.

### Cambiar las variables de entorno

La forma más fácil de cambiar los valores de tus variables de entorno es a través de la storage UI.

Puedes acceder a los datos almacenados por el storage de la escena ingresando este enlace:

[decentraland.org/storage](https://decentraland.org/storage)

También puedes acceder a esta página desde el Creator Hub. Abre la **Manage** pestaña, haz clic en los tres puntos junto al lugar donde has publicado contenido y selecciona **View Storage**.

Ahí puedes ver una lista de todos los Worlds y LAND donde puedes publicar escenas.

Abre tu escena y luego la **Environment** pestaña. Deberías ver todas las variables de entorno del proyecto.

![Activate stream](https://1216664193-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-3661fea25e77812e3b8eb0c4ec56a5ee808f9ff1%2Fenvironment-variables.png?alt=media\&token=b8c1a2c0-907d-402a-9e07-303bbf8732b2)

Ten en cuenta que no puedes leer los valores de ninguna de estas variables de entorno (eso es para proteger datos sensibles), pero sí puedes eliminar o sobrescribir cualquiera de ellas. Simplemente haz clic en el icono del lápiz o de la papelera.

También puedes gestionar las variables de entorno desde la línea de comandos, usando `npx sdk-commands storage env`:

```bash
# Establecer una variable
npx sdk-commands storage env set MAX_PLAYERS --value 8

# Eliminar una variable
npx sdk-commands storage env delete OLD_VAR

# Eliminar todas las variables de entorno
npx sdk-commands storage env clear --confirm
```

También puedes apuntar a un entorno específico con la bandera `--target` :

```bash
# Desplegar en staging
npx sdk-commands storage env set MY_KEY --value my_value --target https://storage.decentraland.zone

# Desplegar en un servidor de desarrollo local
npx sdk-commands storage env set MY_KEY --value my_value --target http://localhost:8000
```

Las variables de entorno desplegadas tienen precedencia sobre `.env` los valores.

## Estructura de proyecto recomendada

Separar el código del servidor, del cliente y del compartido mantiene el código legible a medida que crece:

```
src/
├── index.ts              # Punto de entrada — rama isServer()
├── client/
│   ├── setup.ts          # Manejadores de input, emisores de mensajes
│   └── ui.tsx            # UI de React ECS (lee el estado sincronizado)
├── server/
│   ├── server.ts         # Bucle del juego, manejadores de mensajes, mutaciones de estado
│   └── gameState.ts      # Funciones auxiliares para el estado del servidor
└── shared/
    ├── schemas.ts        # Definiciones de Component + validateBeforeChange
    └── messages.ts       # registerMessages() — importado por ambos lados
```

{% hint style="info" %}
**💡 Consejo**: Mantén todas las `registerMessages()` llamadas y definiciones de componentes personalizados en `shared/`. Tanto el servidor como el cliente importan desde allí, lo que garantiza que siempre coincidan en los esquemas de los mensajes.
{% endhint %}

## Mejores prácticas de rendimiento

Cada cambio de Component envía el *todo* los datos del componente por la red. Esto es diferente de lo que hace Colyseus, que envía solo los diffs. Al diseñar componentes personalizados, ten esto en cuenta. La solución óptima puede ser almacenar datos en Components separados, según la frecuencia de cambio.

### ❌ Evita componentes monolíticos

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"

// MAL — cambiar la puntuación también envía el array de posiciones
const GameState = engine.defineComponent("GameState", {
  playerAScore: Schemas.Int,
  playerBScore: Schemas.Int,
  timer: Schemas.Int,
  playerPositions: Schemas.Array(Schemas.Vector3), // payload grande
})
```

### ✅ Prefiere componentes atómicos

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"

// BIEN — cada actualización es pequeña e independiente
const PlayerScore = engine.defineComponent("PlayerScore", {
  playerA: Schemas.Int,
  playerB: Schemas.Int,
})

const GameTimer = engine.defineComponent("GameTimer", {
  secondsLeft: Schemas.Int,
})
```

*Regla general*: agrupa los campos que cambian juntos y con una frecuencia similar. Separa los datos que cambian rápido (temporizadores, posiciones) de los que cambian lento (puntuaciones, configuración).

### Limita la frecuencia de los mensajes

Evita enviar mensajes en cada frame. Agrupa o limita la frecuencia cuando sea posible:

```typescript
import { engine } from "@dcl/sdk/ecs"
import { room } from "./shared/messages"

let lastSend = 0
engine.addSystem((dt) => {
  lastSend += dt
  if (lastSend > 0.1) {
    // cada 100 ms
    room.send("position", transform.position)
    lastSend = 0
  }
})
```

Por ejemplo, si el servidor controla un temporizador de cuenta regresiva, no es necesario enviar actualizaciones a todos los jugadores cada segundo. Lo mejor es que cada cliente calcule el paso del tiempo por su cuenta y que el servidor emita su estado actual cada 30 segundos aproximadamente, para asegurar la consistencia.

## Límites de recursos del servidor

El Servidor Multijugador ejecuta cada escena en un isolate aislado con límites estrictos de recursos. Alcanzar estos límites puede descartar datos silenciosamente o terminar el servidor para todos en la escena, así que diseña tu escena para que se mantenga muy por debajo de ellos.

### Memoria

El isolate tiene un **256 MB** de límite de memoria. Si se supera, el isolate se descarta y el servidor se apaga para todos los jugadores conectados. Mantén ligero el estado de trabajo y elimina los datos por jugador cuando los jugadores se vayan.

### CPU

Cada turno de ejecución tiene un presupuesto de tiempo real:

* **Ejecución síncrona**: 10 segundos por turno. Un bucle sin límite que supere esto destruirá el isolate.
* **Resolución del turno asíncrono**: 60 segundos. Si una `Promise` o una cadena de `await` tarda más que esto en resolverse, el isolate se termina.

Distribuye el trabajo pesado entre varios ticks usando un acumulador de `dt` en `engine.addSystem()`. Nunca ejecutes bucles síncronos sin límite en el servidor.

### Tasa de mensajes entrantes

Cada par conectado puede enviar hasta aproximadamente **300 mensajes por 1.000 ms**. Los frames de datos adicionales se descartan (no se ponen en cola). Nunca envíes mensajes en cada frame desde el cliente. Consulta [Mejores prácticas de rendimiento](#performance-best-practices) para patrones de limitación.

### Tamaño de mensaje

* Los paquetes entrantes están limitados a **128 KB** por paquete. Los paquetes demasiado grandes se descartan por completo.
* Los mensajes de escena a comms están limitados aproximadamente a **30 KB**. Para la capa de transporte práctica, mantén los mensajes sincronizados muy por debajo de **13 KB** (consulta la [Mensajes](#messages) sección).

### Fetch externo

Concurrentes `signedFetch` las llamadas están limitadas a **32** en vuelo. Los fetch adicionales se ponen en cola hasta que se abre un espacio. Cada intento de fetch tiene un **límite de tiempo de 15 segundos** . Las respuestas de fetch están limitadas a **10 MB**. Las conexiones WebSocket están limitadas a **32** sockets concurrentes, con un tamaño máximo de mensaje de **1 MB** por mensaje.

### Llamadas al host en vuelo

El límite de 40 llamadas descrito en [Almacenamiento de datos](#data-storage) se aplica a todas las llamadas al host en todo el isolate, incluyendo Storage, `signedFetch`, y otras APIs de runtime. Las llamadas excesivas se rechazan de inmediato y no se ponen en cola.

## Errores comunes

### Olvidar la validación en estado solo para el servidor

Sin `validateBeforeChange`, los clientes pueden escribir en cualquier componente:

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"

// ❌ MAL — los clientes pueden hacer trampa
const Score = engine.defineComponent("Score", { value: Schemas.Int })

// ✅ BIEN — solo para el servidor
if (isServer()) {
  Score.validateBeforeChange((v) => v.senderAddress === AUTH_SERVER_PEER_ID)
}
```

### Confiar en valores proporcionados por el cliente

Nunca dejes que un cliente dicte sus propios valores para datos importantes como salud, puntuación o posición:

```typescript
import { room } from "./shared/messages"

// ❌ MAL
room.onMessage("setHealth", (data) => {
  player.health = data.health // ¡el cliente controla el valor!
})

// ✅ BIEN — el servidor calcula el resultado
room.onMessage("takeDamage", (data) => {
  const damage = calculateDamage(data.source)
  player.health = Math.max(0, player.health - damage)
})
```

### Enviar mensajes antes de la sincronización del estado

Los clientes deben esperar a que el estado se sincronice antes de interactuar:

```typescript
import { engine } from "@dcl/sdk/ecs"
import { isStateSyncronized } from "@dcl/sdk/network"

engine.addSystem(() => {
  if (!isStateSyncronized()) return
  // seguro para enviar mensajes
})
```

### Espera a que el servidor se inicie

El servidor solo está activo mientras haya al menos un jugador en la escena. Después de que el último jugador se vaya, el servidor permanece arriba durante aproximadamente dos minutos y luego se apaga. La siguiente visita inicia en frío una instancia nueva, lo que tarda aproximadamente **15 segundos en producción**. La Preview local inicia el servidor al instante, así que los problemas de inicio en frío casi siempre aparecen solo después de publicar.

El código de tu escena debe estar preparado para esperar a que el servidor esté en línea. Los mensajes enviados antes de que el servidor termine de iniciarse se pierden silenciosamente. Muestra al jugador un mensaje de "el servidor se está despertando" mientras espera, y añade lógica de reintento para las solicitudes iniciales al servidor.

{% hint style="info" %}
**Consejo:** Una forma fiable de detectar que el servidor está listo es un heartbeat: haz que el servidor escriba `Date.now()` en un campo de componente sincronizado cada 2 segundos, y que el cliente haga un seguimiento de cuándo vio por última vez cambiar el valor. Si no llega ningún cambio en 6 segundos, considera que el servidor está fuera de línea. Esto es más robusto que `isStateSyncronized()`, que solo confirma que el transporte está conectado, no que el servidor esté ejecutándose.
{% endhint %}

## Ejemplo completo

Un contador multijugador mínimo: haz clic en un botón y el servidor incrementa una puntuación sincronizada. El servidor persiste el contador en `Storage` para que el valor sobreviva a los reinicios del servidor. Recuerda que el servidor se apaga cuando no hay jugadores en la escena, así que sin Storage el conteo se restablecería a cero cada vez que la escena quedara vacía de jugadores.

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"
import { registerMessages, isServer, syncEntity } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"
import { pointerEventsSystem } from "@dcl/sdk/ecs"
import { Storage } from "@dcl/sdk/server"

// 1. Define mensajes (compartidos)
const Messages = {
  increment: Schemas.Map({}),
  stateUpdate: Schemas.Map({
    count: Schemas.Int,
    lastPlayer: Schemas.String,
  }),
}

// 2. Define un componente solo para el servidor (compartido)
const Counter = engine.defineComponent("Counter", {
  value: Schemas.Int,
  lastPlayer: Schemas.String,
})

// 3. Crea la sala
const room = registerMessages(Messages)

export async function main() {
  if (isServer()) {
    // === SERVIDOR ===

    // Solo el servidor puede modificar este componente
    Counter.validateBeforeChange(
      (v) => v.senderAddress === AUTH_SERVER_PEER_ID
    )

    // Restaura el contador desde Storage por si el servidor se reinició
    const savedCount = await Storage.get<string>("counter")
    const savedPlayer = await Storage.get<string>("lastPlayer")
    const initialCount = savedCount ? parseInt(savedCount) : 0
    const initialPlayer = savedPlayer ?? "none"

    const counterEntity = engine.addEntity()
    syncEntity(counterEntity, [Counter.componentId], 1)
    Counter.create(counterEntity, {
      value: initialCount,
      lastPlayer: initialPlayer,
    })

    room.onMessage("increment", async (_data, context) => {
      if (!context) return
      const counter = Counter.getMutable(counterEntity)
      counter.value += 1
      counter.lastPlayer = context.from

      // Persistir en Storage para que el valor sobreviva a los reinicios del servidor
      await Storage.set("counter", String(counter.value))
      await Storage.set("lastPlayer", counter.lastPlayer)

      room.send("stateUpdate", {
        count: counter.value,
        lastPlayer: context.from,
      })
    })
  } else {
    // === CLIENTE ===
    const button = engine.addEntity()
    // ... añade Transform, MeshRenderer, etc.

    pointerEventsSystem.onPointerDown(button, () => {
      room.send("increment", {})
    })

    room.onMessage("stateUpdate", (data) => {
      console.log(`Count: ${data.count} (last click by ${data.lastPlayer})`)
    })
  }
}
```

## Pruebas locales

El Preview estándar se encarga de todo. Cuando usas la rama auth-server del SDK, el servidor local se inicia automáticamente en segundo plano junto con la Preview del cliente.

Para probar interacciones multijugador localmente, abre el Preview en dos ventanas separadas; cada ventana se trata como un jugador distinto. Conecta cada ventana con una dirección diferente. Ambos clientes se conectarán a la misma instancia del servidor local.

Usando Creator Hub, activa **Vista previa con múltiples instancias** en el **Desktop Client** el submenú de **Opciones de reproducción**, luego haz clic en el **Play** botón una segunda vez para abrir una segunda ventana del explorer de Decentraland. Debes conectarte en ambas ventanas con direcciones diferentes. Las mismas sesiones permanecerán abiertas mientras la escena se recarga.

Como alternativa, puedes abrir una segunda ventana del explorer de Decentraland escribiendo lo siguiente en la URL de un navegador:

> `decentraland://realm=http://127.0.0.1:8000&local-scene=true&debug=true&multi-instance=true`

### Consejos de depuración

* *Prefija tus logs* con `[SERVER]` o `[CLIENT]` para que puedas distinguirlos en el terminal:

  ```typescript
  import { isServer } from "@dcl/sdk/network"

  if (isServer()) {
    console.log("[SERVER] Starting...")
  } else {
    console.log("[CLIENT] Starting...")
  }
  ```
* *Verifica la sincronización de components* en el cliente registrando los recuentos de entities:

  ```typescript
  import { engine } from "@dcl/sdk/ecs"

  engine.addSystem(() => {
    const entities = Array.from(engine.getEntitiesWith(MyComponent))
    console.log("[CLIENT] Synced entities:", entities.length)
  })
  ```

## Depurar en producción

Para ver la salida `console.log()` de tu servidor publicado, la dirección de tu wallet debe aparecer listada en el `logsPermissions` array de `scene.json`:

```json
{
  "logsPermissions": ["0xYourWalletAddress"]
}
```

Sin esto, los logs del servidor se ocultan en producción, incluso para el propietario de la escena.

Transmite logs en vivo del servidor desde la línea de comandos ejecutando esto en la carpeta de tu proyecto

```bash
npx sdk-commands sdk-server-logs
```

También puedes especificar manualmente el nombre del world en los logs con:

```bash
npx sdk-commands sdk-server-logs --world WORLD_NAME.dcl.eth
```

Al ver logs de una escena en un world con varias escenas o de parcels en Genesis City, pasa también una `posición` para las coordenadas:

```bash
npx sdk-commands sdk-server-logs --world WORLD_NAME.dcl.eth --position=x,y
```

Se te pedirá firmar un mensaje con una de las wallets listadas en `logsPermissions` para autenticarte. Una vez conectado, verás la salida del lado del servidor en tiempo real, lo cual es útil para diagnosticar problemas sin necesidad de redeploy. `console.log()` Ver datos de Storage

### Abre el world o los datos del jugador para ver la información que se almacena para cada uno.

Puedes acceder a los datos almacenados por el storage de la escena ingresando este enlace:

[decentraland.org/storage](https://decentraland.org/storage)

También puedes acceder a esta página desde el Creator Hub. Abre la **Manage** pestaña, haz clic en los tres puntos junto al lugar donde has publicado contenido y selecciona **View Storage**.

Ahí puedes ver una lista de todos los Worlds y LAND donde puedes publicar escenas.

Por ejemplo, si un jugador en particular tiene un problema al jugar tu escena, podrías buscar a ese jugador por dirección y ver qué datos se almacenan para él para entender su situación. Quizá se encontró con un caso límite en el que terminó con datos contradictorios. Incluso puedes borrar o editar los datos de ese jugador desde esta página para devolverlo a un estado estable.

Control de versiones

## Cada versión publicada de tu escena obtiene su propio hash ID único, y cada hash se empareja con su propia instancia de servidor. Esto significa que el código del cliente y el código del servidor siempre avanzan juntos; no existe una ventana en la que un cliente que ejecuta lógica antigua hable con un servidor que ejecuta lógica nueva (o al revés).

Cuando publicas una actualización:

Los jugadores que ya están en la escena

* *siguen viendo la versión antigua de la escena hasta que se van y vuelven. Sus clientes permanecen conectados a la instancia del servidor que coincide con el hash antiguo.* Los nuevos jugadores que llegan
* *después de la actualización cargan la nueva versión de la escena y se conectan a la nueva instancia del servidor.* Esta garantía evita que el estado del cliente y del servidor se desincronicen debido a un cambio de schema o a un componente renombrado. Una actualización nunca puede romper la sesión de un jugador que ya está en tu escena.

La contrapartida es que, durante una pequeña ventana justo después de un deploy, los jugadores pueden quedar divididos entre dos instancias de servidor diferentes. Es posible que un jugador que ya estaba allí y otro que acaba de llegar no se vean entre sí o no puedan interactuar mediante la escena, aunque estén en la misma escena, hasta que los jugadores más antiguos se vayan y vuelvan a entrar.

: Los datos almacenados mediante el

{% hint style="info" %}
**💡 Consejo**service (como clasificaciones, progreso del jugador o cambios persistentes en el entorno) se [Storage](#data-storage) borran entre versiones. Storage se conserva a nivel de ubicación y se comparte entre todas las instancias de servidor que apuntan a la misma escena, por lo que las nuevas versiones continúan exactamente donde la anterior se quedó. *no* borran entre versiones. Storage se conserva a nivel de ubicación y se comparte entre todas las instancias de servidor que apuntan a la misma escena, por lo que las nuevas versiones continúan exactamente donde la anterior se quedó.
{% endhint %}

## Migrar desde Colyseus

Si ya tienes una escena construida sobre Colyseus, la tabla de abajo mapea los patrones comunes de Colyseus a sus equivalentes en SDK7:

| Colyseus                              | Servidor Multijugador de SDK7                             |
| ------------------------------------- | --------------------------------------------------------- |
| `room.send(type, data)`               | `room.send(type, data)` — la misma API                    |
| `room.onMessage(type, cb)`            | `room.onMessage(type, cb)` — la misma API                 |
| `room.state.players` (schema)         | `syncEntity` + components personalizados                  |
| Serialización JSON                    | Serialización binaria (automática mediante `Schemas`)     |
| Aplicación de servidor separada       | Misma base de código — `isServer()` ramificación          |
| Alojamiento de servidor personalizado | Integrado: la Preview ejecuta el servidor automáticamente |

Diferencias clave que debes tener en cuenta:

* *Serialización*: Colyseus envía diffs JSON; el SDK envía el component completo en cada cambio. Mantén los components pequeños (consulta [Mejores prácticas de rendimiento](#performance-best-practices)).
* *Modelo de estado*: Colyseus usa un árbol de estado mutable con diffing automático. El SDK usa components ECS sincronizados mediante `syncEntity` y protegidos con `validateBeforeChange`.
* *Alojamiento*: No hay un despliegue de servidor separado. El Servidor Multijugador se despliega automáticamente junto con la escena.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.decentraland.org/creator/content-creator-es/scenes-sdk7/networking/authoritative-servers.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
