> 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/escenas-sdk7/networking/authoritative-servers.md).

# Multiplayer Server

**📔 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 debes instalar sigue llamándose `auth-server`, ver [Configuración](#setup).

## Resumen

Decentraland ejecuta las escenas localmente en la máquina de cada 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 por defecto.

Permitir que todos los jugadores vean una escena con el mismo contenido y en el mismo estado es extremadamente importante para que los jugadores interactúen 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 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 escena, 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 de sus propias acciones, el servidor actúa como la única fuente de verdad. Esto lo convierte en el enfoque recomendado para sincronizar escenas multijugador.

El Multiplayer Server es ideal siempre que la equidad 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 exponerlas 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 en conflicto. Cada cliente establece la plataforma a una altura distinta, y nadie tiene la autoridad para decidir cuál es la correcta. El Multiplayer Server resuelve cada cambio en un solo lugar, de modo 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 u objetos colocados. Cuando los jugadores regresan, el mundo refleja lo que ocurrió antes.

Decentraland aloja e implementa el servidor por ti. Publicar tu escena mediante el proceso normal también publica el servidor sin problemas, sin pasos extra ni necesidad de pagar por ningún hosting.

## 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`, en el 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 haces preview de la escena. 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` en el 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 el preview

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

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 escena publicada.

## Ramas de Server / Client

El código de la escena en la `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 escena sin interfaz y sin renderizado. Tiene acceso verificado a todas las posiciones de los jugadores, wearables y otros datos mediante `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 transmitir 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 sin servidor](/creator/content-creator-es/escenas-sdk7/networking/serverless-multiplayer.md) feature, lo que hace trivial actualizar una escena de esta arquitectura al Multiplayer Server. Cuando una escena 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 consistencia de los entity-id. Más bien, la entity es instanciada y compartida por el servidor, y todos los clientes se sincronizan sobre esa instancia. Protégelo siempre con `isServer()`. Esto es diferente de [multijugador sin servidor](/creator/content-creator-es/escenas-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

#### Validar valores

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 sea mayor que 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, protégelo siempre con `isServer()`. En el cliente la llamada no hace nada útil.

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

#### Validar proximidad al jugador

También puedes combinar `validateBeforeChange()` con las posiciones verificadas por el servidor de los jugadores para comprobar que un jugador está lo suficientemente 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 se extiendan por la escena para agarrar objetos con los que no deberían poder interactuar.

#### Permitir cambios solo por administradores

También puedes validar según *quién* está enviando el cambio. Cada valor entrante incluye un campo `senderAddress` con la dirección de wallet del remitente. Úsalo para permitir cambios solo de ciertos jugadores. Por ejemplo, para permitir que solo los administradores de la escena 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 desde la lista de administradores de la escena
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] Direcciones de administradores actualizadas en caché:",
      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 cambios mientras se ejecuta en preview, para que las pruebas locales sean más fáciles
      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 [Scene Admin](/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 escena.

#### Permitir cambios solo por el servidor

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

Cada valor entrante incluye un campo `senderAddress` 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 de una entity dada. Es una forma cómoda 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 administrada 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 `isServer()` bloque. Envuelve `validateBeforeChange()`, que solo tiene sentido en el servidor; llamarla en un cliente produce errores.
{% endhint %}

#### Componentes personalizados

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

```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 ideales para el estado que todos los jugadores deben ver de forma continua: cosas como posiciones, puntuaciones o fase del juego. Pero no todo encaja en ese modelo. A veces un jugador solo necesita decirle al servidor "hice clic en este botón" o "quiero unirme al juego", y el servidor debe responder con una respuesta única como "la ronda comenzó" o "aquí están tus estadísticas". Estos son eventos, no estado continuo.

Eso es para lo que sirven los mensajes. Usa `registerMessages()` para comunicación tipada y validada por schema entre 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. Por sí mismos no crean ningún estado persistente.

### Definir mensajes

Define todos los mensajes en un archivo compartido que importen tanto el servidor como el cliente. Así ambas partes siempre están de acuerdo sobre qué mensajes existen y qué datos transportan. Cada mensaje 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. De ese modo se ejecuta cuando el módulo se carga por primera vez, tanto en el servidor como en el cliente. No lo llames condicionalmente ni dentro de funciones; ambas partes siempre deben registrar los mismos mensajes.
{% endhint %}

### Enviar mensajes

Los clientes solo pueden enviar mensajes al servidor. No existe mensajería directa entre clientes. El servidor puede transmitir a todos los clientes o dirigirse a jugadores específicos por dirección.

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

// Cliente → Servidor (transmisió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 mensajes tienen un tamaño máximo de alrededor de 13 KB. Los mensajes más grandes se descartan silenciosamente por el transporte, sin generar ningún error. Mantén los payloads pequeños; si necesitas compartir datos más grandes, divídelos en varios mensajes o usa componentes sincronizados.
{% endhint %}

### Recibir mensajes

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

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

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

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

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

Los clientes deben esperar hasta que el estado de la escena se sincronice 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 dice que el estado del cliente está sincronizado; no garantiza que el servidor haya terminado de inicializarse. Para una comprobación de preparación más robusta, haz que el servidor transmita un mensaje "ready" o de heartbeat, y que los clientes esperen a recibirlo antes de enviar mensajes de gameplay.
{% endhint %}

### Tipos de Schema disponibles

Todos los payloads de mensajes 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 vectoriales
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**: Mensajes *debe* definirse usando `Schemas.Map(...)`. No puedes enviar objetos JavaScript planos; fallarán en la serialización binaria.
{% endhint %}

## Lectura de posiciones de jugadores por el servidor

El servidor puede leer **verificadas** las posiciones de los jugadores; los clientes no pueden suplantarlas. Esta es la base del anti-cheat basado en posiciones:

```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

Persistir datos entre reinicios del servidor. Storage es **solo del servidor**, guarda siempre las llamadas con `isServer()`. El servidor puede tanto escribir como leer estos datos.

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

Los datos pueden almacenarse en dos niveles:

* **World**: Úsalo para datos relevantes para todos los jugadores, como tablas de clasificación o cambios persistentes en el entorno.
* **Player**: Úsalo para datos específicos del 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 `delete` variantes devuelven un **boolean**. Nunca lanzan errores — en caso de fallo (un error de red o demasiadas solicitudes concurrentes, ver abajo) registran el error y devuelven `false`, lo que significa que el valor fue **no** persistido. Comprueba siempre el resultado: una escritura descartada `false` es un guardado perdido en silencio.
{% endhint %}

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

Storage es persistencia duradera para datos que deben sobrevivir a reinicios y redepliegues del servidor. No es un almacén de datos en vivo. Mantén el estado de trabajo de tu juego en memoria en el servidor. Eso 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 curso** 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 para el mismo límite. Las llamadas excesivas **no se encolan**. Se rechazan de inmediato 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 una excepción. 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 del juego, 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.

Persistir solo los datos que deban sobrevivir a un reinicio o redepliegue. Todo lo demás puede vivir en memoria.

El ejemplo siguiente 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 de trabajo vive en memoria, actualizado en cada evento
const scores: Record<string, number> = {}

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

// Persistir 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 vuelve a intentar las escrituras fallidas en el siguiente flush:

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

// Estado de trabajo 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 flush posterior, sin llamada a Storage aquí
}

// Hacer flush de las keys dirty periódicamente (por ejemplo, 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 con éxito
    }
    // Si ok es false, la key permanece dirty y se reintenta en el siguiente flush
  }
}

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

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

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

// Escritura — set() devuelve 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 de nuevo más tarde")
}

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

// Eliminación
await Storage.delete("leaderboard")
```

También puedes administrar 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 almacenamiento de la escena
npx sdk-commands storage scene clear --confirm
```

### Storage del Player — Por dirección de Wallet

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

// Escritura — set() devuelve 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 de nuevo más tarde`)
}

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

// Eliminación
await Storage.player.delete(playerAddress, "progress")
```

También puedes administrar el almacenamiento del Player 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 Player (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 UI de storage, entrando en este enlace:

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

También puedes llegar a esta página a través de Creator Hub. Abre la **Administrar** pestaña, haz clic en los tres puntos junto a un lugar donde hayas publicado contenido y selecciona **Ver Storage**.

Allí 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](/files/57b7c31e0f012c4735f28bc4204516aa3ec879bc)

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 address 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 son **no se borran cuando publicas una nueva versión de tu escena**. Esto es genial para tablas de clasificación, progreso de jugadores y cambios persistentes en el 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 diferente, 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 sobre él. Un jugador que no ha iniciado sesión durante meses puede cargar datos anteriores 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 vuelve y de quien ya te habías olvidado.
{% endhint %}

#### Buenas prácticas

* *Analiza siempre de forma defensiva*. Trata cualquier cosa que salga de storage como entrada no confiable, aunque la hayas escrito tú. Encierra `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 los elimines*. El cambio de esquema más seguro es uno aditivo: introduce un nuevo campo con un valor predeterminado y deja intactos los campos existentes. Los datos antiguos simplemente no tendrán el nuevo campo, y tu análisis defensivo ya se encarga de eso. Renombrar un campo obliga a que se rompa cada registro antiguo.
* *Versiona tus objetos almacenados*. Incluye un `version` desde el primer día. Cuando leas datos, decide en función de la versión y migra las formas antiguas a la actual antes de usarlas. Esto mantiene el resto de tu código funcionando con una sola 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 — asigna 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 siguiente lectura ya esté en el nuevo formato. Con el tiempo, esto va drenando el grupo de registros de forma antigua sin necesidad de un script de migración único.
* *Para cambios incompatibles, usa una nueva key*. Si la nueva estructura es realmente incompatible y no vale la pena migrarla, escribe en una nueva storage key (por ejemplo `progress_v2`) e ignora la anterior. La key antigua 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 [UI de storage](https://decentraland.org/storage) o los `npx sdk-commands storage` comandos.
* *Prueba con datos reales de producción*. Antes de desplegar un cambio estructural, extrae algunos registros reales de la UI de storage y ejecuta tu nuevo código de análisis contra ellos. Los casos extremos que causan problemas suelen ser registros que no sabías que existían.
* *Deja una vía de escape*. Ten en cuenta que puedes editar o eliminar registros individuales desde la UI de storage o mediante `npx sdk-commands storage`. Si un solo jugador acaba atascado en un estado malo después de un cambio de esquema, puedes arreglar 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 pueden cambiarse fácilmente sin volver a publicar tu escena.

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

`EnvVar.get()` devuelve un `Promise<string>` y se resuelve en una cadena vacía cuando la variable no está definida, así que proporciona siempre un valor alternativo 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 reclamación 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()`. De ese modo, los datos sensibles nunca pasan 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 variables de entorno

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

Puedes acceder a los datos que se almacenan mediante el storage de la escena entrando en este enlace:

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

También puedes llegar a esta página a través de Creator Hub. Abre la **Administrar** pestaña, haz clic en los tres puntos junto a un lugar donde hayas publicado contenido y selecciona **Ver Storage**.

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

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

![Activate stream](/files/f93206a7960eef16efb0b018e0f9e3883f9d2264)

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. Solo haz clic en el icono del lápiz o de la papelera.

También puedes administrar 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 el `--target` flag:

```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 prioridad sobre `.env` los valores.

## Estructura recomendada del proyecto

Separar el código del servidor, del client y el compartido mantiene la base de 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 de juego, manejadores de mensajes, mutaciones del estado
│   └── gameState.ts      # Funciones auxiliares para el estado del servidor
└── shared/
    ├── schemas.ts        # Definiciones de Component + validateBeforeChange
    └── messages.ts       # registerMessages() — importado por ambas partes
```

{% hint style="info" %}
**💡 Consejo**: Mantén todas las `registerMessages()` llamadas y las definiciones de Component personalizadas en `shared/`. Tanto el servidor como el client importan desde allí, asegurando que siempre estén de acuerdo en los esquemas de mensajes.
{% endhint %}

## Buenas prácticas de rendimiento

Cada cambio de Component envía los *datos completos* del Component por la red. Esto es diferente de lo que hace Colyseus, que solo envía diffs. Al diseñar Components personalizadas, ten esto en cuenta. La solución óptima puede ser almacenar datos en Components separadas, en función de la frecuencia de cambio.

### ❌ Evita Components monolíticas

```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 Components atómicas

```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 a una frecuencia similar. Separa los datos que cambian rápido (temporizadores, posiciones) de los datos que cambian lento (puntuaciones, configuración).

### Limitar mensajes frecuentes

Evita enviar mensajes en cada frame. Agrupa por lotes 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 server controla un temporizador de cuenta regresiva, no es necesario enviar actualizaciones a todos los jugadores cada segundo. Lo mejor es que cada client calcule el paso del tiempo por su cuenta, y que el server transmita su estado actual cada 30 segundos aproximadamente, para garantizar la coherencia.

## Límites de recursos del server

El Multiplayer Server ejecuta cada Scene en un isolate aislado con límites estrictos de recursos. Alcanzar estos límites puede descartar datos silenciosamente o terminar el server para todos los jugadores de la Scene, así que diseña tu Scene para mantenerte muy por debajo de ellos.

### Memoria

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

### CPU

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

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

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

### Tasa de mensajes entrantes

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

### 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 Scene a comunicaciones están limitados a aproximadamente **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` llamadas están limitadas a **32** en vuelo. Los fetch adicionales se ponen en cola hasta que se libera un cupo. Cada intento de fetch tiene un **timeout 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 de host en vuelo

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

## Errores comunes

### Olvidar la validación en el estado exclusivo del server

Sin `validateBeforeChange`, los clients pueden escribir en cualquier Component:

```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 clients pueden hacer trampa
const Score = engine.defineComponent("Score", { value: Schemas.Int })

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

### Confiar en valores proporcionados por el client

Nunca dejes que un client dicte sus propios valores para datos importantes como health, score o position:

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

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

// ✅ BIEN — el server 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 state

Los clients deben esperar hasta que el state 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 server se inicie

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

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

{% hint style="info" %}
**Consejo:** Una forma fiable de detectar si el server está listo es un heartbeat: haz que el server escriba `Date.now()` a un campo de un component sincronizado cada 2 segundos, y haz que el client rastree cuándo vio por última vez cambiar el valor. Si no llega ningún cambio en 6 segundos, considera que el server está offline. Esto es más robusto que `isStateSyncronized()`, lo que solo confirma que el transporte está conectado, no que el server esté en ejecución.
{% endhint %}

## Ejemplo completo

Un contador multiplayer mínimo: haz clic en un botón, el server incrementa una puntuación sincronizada. El server persiste el contador en `Storage` para que el valor sobreviva a los reinicios del server. Recuerda que el server se apaga cuando no hay jugadores en la Scene, así que sin Storage el conteo se restablecería a cero cada vez que la Scene 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 messages (shared)
const Messages = {
  increment: Schemas.Map({}),
  stateUpdate: Schemas.Map({
    count: Schemas.Int,
    lastPlayer: Schemas.String,
  }),
}

// 2. Define a server-only component (shared)
const Counter = engine.defineComponent("Counter", {
  value: Schemas.Int,
  lastPlayer: Schemas.String,
})

// 3. Create the room
const room = registerMessages(Messages)

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

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

    // Restore the counter from storage in case the server restarted
    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

      // Persist to storage so the value survives server restarts
      await Storage.set("counter", String(counter.value))
      await Storage.set("lastPlayer", counter.lastPlayer)

      room.send("stateUpdate", {
        count: counter.value,
        lastPlayer: context.from,
      })
    })
  } else {
    // === CLIENT ===
    const button = engine.addEntity()
    // ... add 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

La preview estándar lo gestiona todo. Cuando usas la rama auth-server del SDK, el server local se inicia automáticamente en segundo plano junto con la preview del client.

Para probar interacciones multiplayer localmente, abre la preview en dos ventanas separadas; cada ventana se trata como un jugador distinto. Conecta cada ventana con una address diferente. Ambos clients se conectarán a la misma instancia local del server.

Usando Creator Hub, haz clic en el botón Preview una segunda vez, y eso abre una segunda ventana del Explorer de Decentraland. Debes conectarte en ambas ventanas con addresses diferentes. Las mismas sessions permanecerán abiertas mientras la Scene se vuelve a cargar.

Como alternativa, puedes abrir una segunda ventana del Explorer de Decentraland escribiendo lo siguiente en una URL del 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 la terminal:

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

  if (isServer()) {
    console.log("[SERVER] Iniciando...")
  } else {
    console.log("[CLIENT] Iniciando...")
  }
  ```
* *Verificar la sincronización de components* en el client registrando la cantidad de entities:

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

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

## Depurar en producción

Para ver `console.log()` la salida de tu server publicado, tu dirección de Wallet debe estar incluida en el `logsPermissions` array en `scene.json`:

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

Sin esto, los logs del server quedan ocultos en producción, incluso para el owner de la Scene.

Transmite logs en vivo del server 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 Scene en un world con múltiples Scenes o de parcels en Genesis City, pasa también un `position` para las coordinates:

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

Se te pedirá que firmes un mensaje con una de las wallets listadas en `logsPermissions` para autenticarte. Una vez conectado, verás la salida del lado del server `console.log()` en tiempo real, lo que resulta útil para diagnosticar problemas sin necesidad de volver a desplegar.

### Ver datos de Storage

Puedes acceder a los datos que se almacenan mediante el storage de la escena entrando en este enlace:

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

También puedes llegar a esta página a través de Creator Hub. Abre la **Administrar** pestaña, haz clic en los tres puntos junto a un lugar donde hayas publicado contenido y selecciona **Ver Storage**.

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

Abre el world o los datos del player para ver la información almacenada para cada uno.

Por ejemplo, si un player en particular tiene un problema al jugar tu Scene, podrías buscar a este player por address y ver qué datos están almacenados para él para entender su situación. Tal vez se topó con un caso límite y terminó con datos contradictorios. Incluso puedes borrar o editar los datos de ese player desde esta página para devolverlo a un estado estable.

## Control de versiones

Cada versión publicada de tu Scene obtiene su propio hash ID único, y cada hash se empareja con su propia instancia del server. Esto significa que el código del client y el código del server siempre avanzan juntos; no hay una ventana en la que un client que ejecuta lógica antigua hable con un server que ejecuta lógica nueva (o viceversa).

Cuando publicas una actualización:

* *Los jugadores que ya están en la Scene* siguen viendo la versión antigua de la Scene hasta que se van y regresan. Sus clients permanecen conectados a la instancia del server que coincide con el hash antiguo.
* *Los nuevos jugadores que llegan* después de la actualización cargan la nueva versión de la Scene y se conectan a la nueva instancia del server.

Esto garantiza que el state del client y del server nunca se desincronice debido a un cambio de schema o a un Component renombrado. Una actualización nunca puede romper la session de un jugador que ya está en tu Scene.

La contrapartida es que, durante una ventana breve justo después de un deploy, los jugadores pueden quedar divididos entre dos instancias de server diferentes. Un jugador que ya estaba allí y un jugador que acaba de llegar pueden no verse entre sí ni poder interactuar mediante la Scene, aunque estén en la misma Scene, hasta que los jugadores más antiguos se vayan y vuelvan a entrar.

{% hint style="info" %}
**💡 Consejo**: los datos almacenados mediante el [Storage](#data-storage) service (como rankings, progreso del jugador o cambios persistentes del entorno) se *no* borran entre versiones. Storage se conserva a nivel de location y se comparte entre todas las instancias del server que apuntan a la misma Scene, así que las nuevas versiones retoman exactamente donde la anterior lo dejó.
{% endhint %}

## Migración desde Colyseus

Si tienes una Scene existente construida sobre Colyseus, la tabla de abajo mapea patrones comunes de Colyseus a sus equivalentes de SDK7:

| Colyseus                        | Multiplayer Server 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 server separada   | Mismo codebase — `isServer()` branching                 |
| Hosting de server personalizado | Integrado: la preview ejecuta el server automáticamente |

Diferencias clave a 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 [Buenas prácticas de rendimiento](#performance-best-practices)).
* *Modelo de state*: Colyseus usa un árbol de state mutable con diffing automático. El SDK usa components ECS sincronizados mediante `syncEntity` y protegidos con `validateBeforeChange`.
* *Hosting*: No hay un despliegue de server separado. El Multiplayer Server se despliega automáticamente junto con la Scene.


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
