> 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-pt/scenes-sdk7/rede/authoritative-servers.md).

# Multiplayer Server

Crie scenes multiplayer do Decentraland com um multiplayer server sem interface.

**📔 Nota**: O Multiplayer Server era anteriormente chamado de **Authoritative Server**. Só o nome mudou, a funcionalidade é a mesma. A branch do SDK a instalar continua a chamar-se `auth-server`, consulta [Configuração](#setup).

## Visão geral

Decentraland executa scenes localmente na máquina de um jogador. Por defeito, os jogadores conseguem ver-se uns aos outros e interagir diretamente, mas cada um interage com o ambiente de forma independente. As alterações no ambiente não são partilhadas entre jogadores por defeito.

Permitir que todos os jogadores vejam uma scene como tendo o mesmo conteúdo no mesmo estado é extremamente importante para que os jogadores possam interagir de formas mais significativas. Sem isto, se um jogador abrir uma porta e entrar numa casa, os outros jogadores verão essa porta como ainda fechada, e o primeiro jogador parecerá atravessar diretamente a porta fechada para os outros jogadores.

O **Multiplayer Server** é um processo de server sem interface que executa o código da tua scene, valida alterações de estado e transmite o resultado a todos os jogadores ligados. Segue uma arquitetura de server autoritativo: em vez de confiar em que cada client reporte as suas próprias ações, o server atua como a única fonte de verdade. Isto torna-o a abordagem recomendada para sincronizar scenes multiplayer.

O Multiplayer Server é ideal sempre que a imparcialidade é importante para as mecânicas do jogo, porque podes implementar validações anti-cheat elaboradas que correm no lado do server. Também podes guardar chaves privadas e outras informações sensíveis no server, evitando ter de as expor diretamente ao utilizador.

Ter um Multiplayer Server também resolve um problema real: numa configuração peer-to-peer, dois jogadores a controlar algo como uma plataforma flutuante podem produzir resultados conflituantes. Cada client define a plataforma para uma altura diferente, e ninguém tem autoridade para decidir qual está correta. O Multiplayer Server resolve cada alteração num só lugar, para que todos os clients converjam para o mesmo estado.

Também te dá um lugar para **persistir dados entre sessões**: tabelas de classificação, progressão dos jogadores, conquistas desbloqueadas ou alterações no ambiente, como portas abertas ou itens colocados. Quando os jogadores regressam, o world reflete o que aconteceu antes.

Decentraland aloja e faz o deploy do server por ti. Publicar a tua scene através do processo normal também publica o server de forma transparente, sem passos extra nem necessidade de pagar qualquer alojamento.

## Configuração

### 1. Instala a versão auth-server do SDK

As APIs nativas do Multiplayer Server (`isServer`, `registerMessages`, `Storage`, `EnvVar`, etc.) estão disponíveis numa branch separada do SDK. Executa os seguintes comandos para a instalar no teu projeto em vez da branch padrão do SDK:

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

### 2. Configura scene.json

O Multiplayer Server é ativado pela flag `"authoritativeMultiplayer": true` em `scene.json`, ao nível root. Não precisas de a adicionar manualmente: ao usar a branch auth-server do SDK, ela é adicionada automaticamente da primeira vez que fazes build ou preview da scene. Apenas certifica-te de que não a removes; sem esta flag o server nunca é executado e `isServer()` devolve sempre *false*.

Opcionalmente, também podes adicionar o seguinte ao teu `scene.json` ao nível root:

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

Adiciona `logsPermissions` para listar endereços de wallet que podem ver `console.log()` do server. Os utilizadores listados podem então ver os logs do server em produção executando o seguinte comando:

`npx sdk-commands sdk-server-logs`

### 3. Executa o preview

Usa o comando standard de preview, sem necessidade de passos extra. Ao usar a branch auth-server do SDK, o preview inicia automaticamente uma versão local do Multiplayer Server em segundo plano.

A sessão local do server não está ligada à de produção, por isso podes testar livremente sem afetar os jogadores que estão na tua scene publicada.

## Ramificação Server / Client

O código da scene na pasta `src` do teu projeto corre tanto no server como no client. Usa a `isServer()` função para separar os caminhos de execução:

```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()) {
    // Só no Server: lógica de jogo, validação, gestão de estado
    initServer()
    return
  } else {
    // Só no Client: UI, gestão de input, envio de messages
    initClient()
    setupUi()
  }
}
```

O server executa a tua scene em modo headless, sem rendering. Tem acesso verificado a todas as posições dos jogadores, wearables e outros dados através de `PlayerIdentityData` e é a única autoridade sobre o estado do jogo.

## Componentes sincronizados e validação

### Sincronizar Entities para Todos os Clients

Usa `syncEntity` para transmitir quaisquer alterações nos componentes indicados dessa Entity:

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

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

A sintaxe é idêntica à utilizada pela [multiplayer server sem server](/creator/content-creator-pt/scenes-sdk7/rede/serverless-multiplayer.md) feature, tornando trivial atualizar uma scene de usar esta arquitetura para o Multiplayer Server. Quando uma scene usa o Multiplayer Server, as atualizações de estado deixam de ser enviadas entre todos os jogadores; em vez disso, todas as atualizações de estado passam agora pelo server e são validadas através dele.

{% hint style="warning" %}
**📔 Nota**: Com o Multiplayer Server, o padrão ideal é apenas o server chamar `syncEntity`. Dessa forma, não precisas de te preocupar com a consistência do entity-id. Em vez disso, a Entity é instanciada e partilhada pelo server, e todos os clients recebem sincronização sobre essa instância. Protege-a sempre com `isServer()`. Isto é diferente de [multiplayer server sem server](/creator/content-creator-pt/scenes-sdk7/rede/serverless-multiplayer.md), onde cada client chama `syncEntity` por conta própria.
{% endhint %}

### Validar alterações

Usa `validateBeforeChange()` para restringir quaisquer atualizações de estado num componente específico de uma Entity. Permite-te executar uma função de validação personalizada, e as alterações só têm sucesso quando o teste de validação é cumprido.

Se a validação devolver o valor *true*, então a alteração é aceite e propagada a todos os jogadores. Se a validação devolver o valor *false*, então a alteração é rejeitada. Uma alteração rejeitada não será passada aos outros jogadores e será revertida para o jogador que a tentou fazer.

#### Validar valores

O caso mais simples é validar se o novo *valor* que está a ser escrito está dentro de certos parâmetros. Por exemplo, aceitar apenas alterações a um `Transform` quando a nova posição Y está acima 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) => {
    // Rejeita qualquer atualização que colocaria a Entity em ou abaixo de Y = 0
    return value.newValue.position.y > 0
  })
}
```

Como `validateBeforeChange()` só tem significado no server, protege-o sempre com `isServer()`. No client, a chamada não faz nada de útil.

Podes usar isto para impedir alterações que vão contra a tua lógica de jogo, como mecanismos anti-cheat.

#### Validar proximidade ao jogador

Podes combinar `validateBeforeChange()` com posições de jogadores verificadas pelo server para verificar se um jogador está suficientemente perto de um objeto antes de lhe permitir interagir com ele. Por exemplo, quando um jogador tenta apanhar um objeto alterando o seu `Transform`, podes rejeitar a alteração se o objeto estiver a mais de 5 metros do jogador:

```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) => {
    // Encontra o jogador que enviou esta alteração
    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ém a posição atual do objeto, antes da alteração
      const objectTransform = Transform.getOrNull(pickableEntity)
      if (!objectTransform) return false

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

      // Só permite a alteração se o jogador estiver a menos de 5 metros
      return distance <= 5
    }

    // Remetente não encontrado entre os jogadores ligados — rejeitar
    return false
  })
}
```

Este padrão é útil como mecanismo anti-cheat: impede os jogadores de se esticarem através da scene para agarrar objetos com os quais não deviam conseguir interagir.

#### Permitir alterações apenas por admins

Também podes validar com base em *quem* está a enviar a alteração. Cada valor recebido inclui um `senderAddress` campo com o endereço de wallet do remetente. Usa isto para permitir apenas alterações de certos jogadores. Por exemplo, para permitir apenas que os admins da scene modifiquem um `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"

// Cache de endereços de wallet de admins, atualizado a partir da lista de admins da scene
let adminAddresses = new Set<string>()

async function updateAdminAddresses() {
  if (isPreview()) return
  try {
    const [error, response] = await getSceneAdmins()
    if (error) {
      console.error("[SERVER] Erro ao obter a lista de admins:", error)
      adminAddresses = new Set()
      return
    }
    adminAddresses = new Set(
      (response ?? []).map((admin) => admin.admin.toLowerCase())
    )
    console.log(
      "[SERVER] Cache de endereços de admins atualizado:",
      Array.from(adminAddresses)
    )
  } catch (error) {
    console.error("[SERVER] Erro ao atualizar os endereços de admins:", error)
    adminAddresses = new Set()
  }
}

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

  if (isServer()) {
    // Preenche o cache antes de configurar a validação
    await updateAdminAddresses()

    VideoPlayer.validateBeforeChange(videoEntity, (value) => {
      // Permite sempre alterações enquanto corre em preview, para facilitar os testes locais
      if (isPreview()) return true

      const senderAddress = value.senderAddress.toLowerCase()
      if (!adminAddresses.has(senderAddress)) {
        console.log(
          "[SERVER] Alteração não autorizada de VideoPlayer bloqueada de:",
          senderAddress
        )
        return false
      }
      return true
    })
  }
}
```

Consulta [Scene Admin](/creator/content-creator-pt/scene-editor/operar-ao-vivo/scene-admin.md) para mais contexto sobre como os jogadores se tornam admins numa scene.

#### Permitir alterações apenas pelo server

O caso mais rigoroso é aceitar apenas escritas que venham do próprio server, rejeitando qualquer alteração proveniente de um client. Este é o padrão ideal para estado que deve ser totalmente autoritativo: scores, fase do jogo, Entities geradas, etc.

Cada valor recebido inclui um `senderAddress` campo. Quando o remetente é o server, este campo corresponde à constante `AUTH_SERVER_PEER_ID`, exportada de `@dcl/sdk/network/message-bus-sync`.

O exemplo abaixo define um pequeno helper `protectServerEntity()` que aplica esta verificação a um ou mais componentes numa determinada Entity. É uma forma conveniente de proteger vários componentes (como `Transform` e `GltfContainer`) numa única chamada:

```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()) {
  // Depois de criar uma Entity gerida pelo server:
  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**: Chama sempre `protectServerEntity()` dentro de um `isServer()` bloco. Ele envolve `validateBeforeChange()`, que só tem significado no server — chamá-lo num client produz erros.
{% endhint %}

#### Componentes personalizados

Também podes aplicar `validateBeforeChange()` em componentes personalizados definidos pela 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,
})

// Só o server pode modificar este componente
if (isServer()) {
  GameState.validateBeforeChange((value) => {
    return value.senderAddress === AUTH_SERVER_PEER_ID
  })
}
```

## Messages

Os synced components são ótimos para estado que todos os jogadores devem ver continuamente: coisas como posições, scores ou fase do jogo. Mas nem tudo se encaixa nesse modelo. Por vezes, um jogador só precisa de dizer ao server "cliquei neste botão" ou "quero entrar no jogo", e o server precisa de responder com uma resposta única como "a ronda começou" ou "aqui estão as tuas estatísticas". Isto são events, não estado contínuo.

É para isso que servem as messages. Usa `registerMessages()` para comunicação tipada e validada por schema entre clients e o server. As messages são fire-and-forget: um client envia uma para o server, o server processa-a e, opcionalmente, envia uma de volta. Elas não criam diretamente qualquer estado persistente por si só.

### Definir Messages

Define todas as messages num ficheiro partilhado que tanto o server como o client importam. Desta forma, ambos os lados concordam sempre sobre que messages existem e que dados transportam. Cada message é um `Schemas.Map` a descrever o respetivo payload:

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

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

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

export const room = registerMessages(Messages)
```

{% hint style="warning" %}
**📔 Nota**: Chama `registerMessages()` uma vez, ao nível superior de um módulo partilhado, como no exemplo acima. Assim, é executado quando o módulo é carregado pela primeira vez, tanto no server como no client. Não o chames condicionalmente nem dentro de functions; ambos os lados têm sempre de registar as mesmas messages.
{% endhint %}

### Enviar Messages

Os clients só podem enviar messages para o server. Não existe messaging direto de client para client. O server pode fazer broadcast para todos os clients ou direcionar para jogadores específicos por address.

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

// Client → Server (broadcast, o server recebe)
room.send("playerReady", { displayName: "Alice" })

// Server → todos os clients
room.send("gameStarted", { roundNumber: 1 })

// Server → um client específico (por endereço de wallet)
room.send("gameEnded", { winnerId: "Alice" }, { to: [playerAddress] })
```

{% hint style="warning" %}
**📔 Nota**: As messages têm um tamanho máximo de cerca de 13 KB. Messages maiores são descartadas silenciosamente pelo transporte, sem produzir qualquer erro. Mantém os payloads pequenos; se precisares de partilhar dados maiores, divide-os em várias messages ou usa synced components.
{% endhint %}

### Receber Messages

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

// Client recebe do server
room.onMessage("gameStarted", (data) => {
  console.log(`A ronda ${data.roundNumber} começou!`)
})

// Server recebe do client
room.onMessage("playerReady", (data, context) => {
  if (!context) return
  const senderAddress = context.from // endereço de wallet verificado
  console.log(`[Server] ${data.displayName} está pronto (${senderAddress})`)
})
```

No server, cada message recebida inclui um `context` objeto com o endereço de wallet verificado do remetente. Usa isto para saber que jogador enviou a message (nunca confies na identidade autodeclarada no payload).

### Aguarda pela sincronização do State antes de enviar

Os clients devem aguardar até que o estado da scene esteja sincronizado antes de enviarem a sua primeira message, para evitar race conditions ao entrar:

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

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

  // Seguro para enviar messages agora
  room.send("playerReady", { displayName: "Alice" })
})
```

{% hint style="info" %}
**💡 Dica**: `isStateSyncronized()` apenas te diz que o estado do client está sincronizado; não garante que o server tenha terminado de inicializar. Para uma verificação de disponibilidade mais robusta, faz com que o server transmita uma message "ready" ou heartbeat, e faz com que os clients esperem por ela antes de enviarem messages de gameplay.
{% endhint %}

### Tipos de Schemas disponíveis

Todos os payloads de messages e componentes personalizados usam `Schemas` para serialização binária. Aqui tens uma referência rápida dos tipos disponíveis:

```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 de vetor
Schemas.Vector3 // { x: 1, y: 2, z: 3 }
Schemas.Quaternion // { x, y, z, w }

// Tipos complexos
Schemas.Array(Schemas.String) // ["a", "b", "c"]
Schemas.Entity // referência de Entity
Schemas.Optional(Schemas.String) // "hello" ou undefined
Schemas.Optional(Schemas.Int) // 42 ou undefined

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

{% hint style="warning" %}
**📔 Nota**: As messages *devem* ser definidas usando `Schemas.Map(...)`. Não podes enviar objetos JavaScript simples; eles falharão a serialização binária.
{% endhint %}

## Leitura das posições dos jogadores pelo Server

O server pode ler **verificado** posições dos jogadores; os clientes não podem falsificá-las. Esta é a base do anti-cheat baseado em posição:

```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 posição é verificada no servidor — nunca confie na posição reportada pelo cliente
  }
})
```

{% hint style="warning" %}
**📔 Nota**: Use sempre `PlayerIdentityData` + `Transform` no servidor para obter as posições dos jogadores. Nunca confie nos valores reportados pelo próprio cliente.
{% endhint %}

## Armazenamento de dados

Persista dados entre reinicializações do servidor. O Storage é **apenas no servidor**, proteja sempre as chamadas com `isServer()`. O servidor pode tanto escrever quanto ler esses dados.

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

Os dados podem ser armazenados em dois níveis:

* **World**: Use isto para dados relevantes para todos os jogadores, como rankings ou mudanças persistentes no ambiente.
* **Player**: Use isto para dados específicos do jogador, como salvar o progresso ou preferências desse jogador.

{% hint style="info" %}
**💡 Dica**: O Storage aceita apenas strings. Use `JSON.stringify()` / `JSON.parse()` para objetos e `String()` / `parseInt()` para números.

Durante o desenvolvimento local, o storage é gravado em `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json`.
{% endhint %}

{% hint style="warning" %}
**📔 Nota**: `Storage.set()`, `Storage.player.set()`, e as `delete` variantes retornam para um **booleano**. Elas nunca lançam erros — em caso de falha (um erro de rede, ou chamadas de host simultâneas demais, veja abaixo), elas registram o erro e retornam para `false`false **não** persistido. Verifique sempre o resultado: um `false` descartado é um salvamento perdido silenciosamente.
{% endhint %}

### Salve em pontos de verificação, não a cada alteração

Storage é persistência durável para dados que precisam sobreviver a reinicializações do servidor e a reimplantações. Não é um datastore ativo. Mantenha o estado de jogo em uso na memória do servidor. Isso é mais rápido e é o padrão certo para um servidor. Escreva no Storage apenas quando realmente precisar, em pontos de verificação significativos.

{% hint style="warning" %}
**⚠️ Aviso**: O runtime do servidor permite um máximo de **40 chamadas de host em andamento** ao mesmo tempo, compartilhadas por *tudo* o que a scene solicita ao runtime fazer: cada solicitação de storage, `signedFetch`, e outras APIs do runtime, todas contam para o mesmo limite. Chamadas em excesso **não são enfileiradas**. Elas são rejeitadas imediatamente com um `erro de muitas chamadas de host simultâneas` . O SDK captura a rejeição e resolve a `Storage.set` promessa para `false` em vez de lançar. Se o seu código descartar esse booleano, a gravação com falha fica invisível, e seus dados persistidos acabam desatualizados ou perdidos. Verifique o resultado de cada gravação (veja a Nota acima).
{% endhint %}

Boas momentos para persistir:

* Fim de jogo, ou o final de uma rodada.
* Um jogador sai.
* Um salvamento com debounce periódico (por exemplo, uma vez a cada 30 segundos), e não uma vez por frame.

Persista apenas os dados que precisam sobreviver a uma reinicialização ou reimplantação. Todo o resto pode viver na memória.

O exemplo abaixo mantém as pontuações na memória e as escreve apenas em um ponto de verificação, em vez de a cada ponto marcado:

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

// O estado de trabalho vive na memória, atualizado a cada evento
const scores: Record<string, number> = {}

function onPointScored(address: string) {
  scores[address] = (scores[address] ?? 0) + 1
  // Nenhuma chamada ao Storage aqui — apenas atualize a memória
}

// Persista apenas em um ponto de verificação, como quando um jogador sai
async function onPlayerLeave(address: string) {
  const saved = await Storage.player.set(address, "score", String(scores[address] ?? 0))
  if (!saved) {
    // A gravação não persistiu — mantenha o valor na memória e tente novamente mais tarde
    console.error(`Falha ao salvar a pontuação de ${address}`)
  }
}
```

Para scenes com muitos jogadores simultâneos, uma abordagem mais robusta acompanha quais chaves têm alterações não salvas (um "dirty set") e tenta novamente as gravações com falha no próximo flush:

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

// Estado de trabalho na memória, atualizado a 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) // Marque para flush posterior, sem chamada ao Storage aqui
}

// Esvazie as chaves sujas periodicamente (por exemplo, a cada ~30s) e em pontos de verificação
async function flush() {
  for (const address of dirty) {
    const ok = await Storage.player.set(address, "score", String(scores[address]))
    if (ok) {
      dirty.delete(address) // Salvo com sucesso
    }
    // Se ok for false, a chave permanece suja e tenta novamente no próximo flush
  }
}

// Execute o flush em um timer usando um acumulador de dt
let flushTimer = 0
engine.addSystem((dt) => {
  flushTimer += dt
  if (flushTimer > 30) {
    flushTimer = 0
    flush()
  }
})
```

### World Storage — Compartilhado entre todos os jogadores

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

// Escrever — set() resolve para false se o valor NÃO foi persistido
const ok = await Storage.set(
  "leaderboard",
  JSON.stringify([
    { name: "Alice", score: 100 },
    { name: "Bob", score: 85 },
  ])
)
if (!ok) {
  console.error("Falha ao salvar o leaderboard — tente novamente mais tarde")
}

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

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

Você também pode gerenciar o storage da scene pela linha de comando, usando `npx sdk-commands storage scene`:

```bash
# Definir um valor
npx sdk-commands storage scene set high_score --value 100

# Obter um valor
npx sdk-commands storage scene get high_score

# Excluir um valor
npx sdk-commands storage scene delete high_score

# Excluir todos os dados de storage da scene
npx sdk-commands storage scene clear --confirm
```

### Player Storage — Por endereço da Wallet

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

// Escrever — set() resolve para false se o valor NÃO foi persistido
const ok = await Storage.player.set(
  playerAddress,
  "progress",
  JSON.stringify({
    level: 5,
    coins: 250,
  })
)
if (!ok) {
  console.error(`Falha ao salvar o progresso de ${playerAddress} — tente novamente mais tarde`)
}

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

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

Você também pode gerenciar o storage do player pela linha de comando, usando `npx sdk-commands storage player`:

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

# Obter um valor para um player específico
npx sdk-commands storage player get level --address 0x1234...

# Excluir um valor para um player específico
npx sdk-commands storage player delete level --address 0x1234...

# Excluir todos os dados de um player específico
npx sdk-commands storage player clear --address 0x1234... --confirm

# Excluir todos os dados do player (todos os players)
npx sdk-commands storage player clear --confirm
```

### Acessar dados armazenados

Você pode ver e editar os dados armazenados em tempo real no seu servidor pela storage UI, acessando este link:

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

Você também pode chegar a esta página pelo Creator Hub. Abra a **Manage** aba, clique nos três pontos ao lado de um local onde você publicou conteúdo e selecione **View Storage**.

Lá, você pode ver uma lista de todos os Worlds e LAND onde você pode publicar scenes.

Abra sua scene e depois a **Scene** ou **Player** aba.

Nesta **Scene** aba você verá uma lista de todas as variáveis armazenadas. A partir daqui, você pode editar ou remover qualquer uma dessas variáveis clicando no ícone de lápis ou de lixeira.

![Activate stream](https://2402076176-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)

Nesta **Player** Nesta aba você verá uma lista de todos os jogadores que têm qualquer dado armazenado no seu servidor. Você pode pesquisá-los por endereço ou nome e, então, ver todos os dados associados a eles. Você também pode editar ou remover esses dados clicando no ícone de lápis ou de lixeira.

### Alterando a estrutura dos dados

Os dados armazenados em produção são **não são apagados quando você publica uma nova versão da sua scene**. Isso é ótimo para rankings, progresso do jogador e mudanças persistentes no ambiente que os jogadores esperam que continuem existindo além de pequenas atualizações na sua scene.

O outro lado é que os dados que estão no storage foram escritos por uma versão mais antiga do seu código. Se o seu novo código espera uma forma diferente, fazer o parse ou ler esses dados antigos pode falhar de maneiras sutis. Um campo que você renomeou ficará ausente. Um campo que antes era uma string e agora é um objeto vai lançar um erro quando você tentar acessar uma propriedade nele. Um jogador que não faz login há meses pode carregar dados que antecedem uma estrutura que seu código já não sabe mais como tratar.

{% hint style="warning" %}
**📔 Nota**: Mudanças de schema não afetam apenas a primeira leitura após um deploy. Os dados armazenados permanecem até serem sobrescritos ou excluídos, então um valor em formato antigo pode aparecer a qualquer momento, muitas vezes vindo de um jogador que retornou e de quem você tinha se esquecido.
{% endhint %}

#### Boas práticas

* *Sempre faça o parse de forma defensiva*. Trate qualquer coisa que saia do storage como entrada não confiável, mesmo que você a tenha escrito. Envolva `JSON.parse()` em um `try/catch`, verifique se os campos existem antes de lê-los e tenha um valor padrão sensato pronto quando não existirem:

  ```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 {
      // Dados antigos ou corrompidos — volte para os padrões
    }
  }
  ```
* *Adicione campos, não os renomeie nem remova*. A mudança de schema mais segura é uma aditiva: introduza um novo campo com um valor padrão e deixe os campos existentes em paz. Os dados antigos simplesmente não terão o novo campo, o que seu parse defensivo já trata. Renomear um campo força cada registro antigo a quebrar.
* *Versione seus objetos armazenados*. Inclua um `version` campo desde o primeiro dia. Quando você ler os dados, faça um branch com base na versão e migre formas mais antigas para a atual antes de usá-las. Isso mantém o restante do seu código funcionando com uma única estrutura atual:

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

  function migrate(raw: any): ProgressV2 {
    const version = raw?.version ?? 1
    if (version === 1) {
      // a v1 não tinha o campo xp — use o padrão
      return {
        version: 2,
        level: raw.level ?? 1,
        coins: raw.coins ?? 0,
        xp: 0,
      }
    }
    return raw as ProgressV2
  }
  ```
* *Grave de volta o valor migrado*. Depois que você atualizar um registro na memória, salve-o de volta para que a próxima leitura já esteja no novo formato. Com o tempo, isso esgota o conjunto de registros no formato antigo sem precisar de um script de migração única.
* *Para mudanças incompatíveis, use uma nova chave*. Se a nova estrutura for genuinamente incompatível e migrar não valer a pena, grave em uma nova chave de storage (por exemplo `progress_v2`) e ignore a antiga. A chave antiga fica inofensivamente no storage e você evita qualquer caminho de leitura que precise interpretá-la. Você pode limpar as chaves antigas depois via a [storage UI](https://decentraland.org/storage) ou via `npx sdk-commands storage` comandos.
* *Teste com dados reais de produção*. Antes de implantar uma mudança estrutural, puxe alguns registros reais da storage UI e rode seu novo código de parsing contra eles. Os casos extremos que causam problemas geralmente são registros que você nem sabia que existiam.
* *Deixe uma válvula de escape*. Lembre-se de que você pode editar ou excluir registros individuais pela storage UI ou via `npx sdk-commands storage`. Se um único jogador acabar travado em um estado ruim após uma mudança de schema, você pode corrigir o registro dele diretamente sem fazer redeploy.

## Variáveis de ambiente

Configure sua scene sem codificar valores diretamente no código. Variáveis de ambiente são úteis para dados sensíveis e também para feature flags ou parâmetros que podem ser alterados facilmente sem republicar sua scene.

As variáveis de ambiente são **apenas no servidor**. Proteja-as com `isServer()`. O servidor pode ler variáveis de ambiente, mas não alterar seus valores.

`EnvVar.get()` retorna uma `Promise<string>` e resolve para uma string vazia quando a variável não estiver definida, então sempre forneça um fallback para valores ausentes:

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

### Dados sensíveis

Variáveis de ambiente são especialmente úteis para armazenar chaves privadas, códigos de resgate de recompensas e outros dados sensíveis que seria arriscado expor no código compilado da scene pública.

Você pode armazenar chaves privadas no storage do servidor e fazer com que apenas o servidor as leia com `isServer()`. Dessa forma, os dados sensíveis nunca passam pela máquina do jogador.

### Desenvolvimento local

Para usar variáveis de ambiente enquanto executa seu projeto localmente, crie um `.env` arquivo na raiz do seu projeto:

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

Importante: adicione `.env` ao seu `.gitignore`, para que esses valores potencialmente sensíveis nunca sejam enviados aos servidores públicos de conteúdo.

### Alterar variáveis de ambiente

A maneira mais fácil de alterar os valores das suas variáveis de ambiente é pela storage UI.

Você pode acessar os dados armazenados pelo storage da scene acessando este link:

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

Você também pode chegar a esta página pelo Creator Hub. Abra a **Manage** aba, clique nos três pontos ao lado de um local onde você publicou conteúdo e selecione **View Storage**.

Lá, você pode ver uma lista de todos os Worlds e LAND onde você pode publicar scenes.

Abra sua scene e depois a **Environment** aba. Você deve ver todas as variáveis de ambiente do projeto.

![Activate stream](https://2402076176-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)

Observe que você não pode ler os valores de nenhuma dessas variáveis de ambiente (isso é para proteger os dados sensíveis), mas pode excluir ou sobrescrever qualquer uma delas. Basta clicar no ícone de lápis ou de lixeira.

Você também pode gerenciar variáveis de ambiente pela linha de comando, usando `npx sdk-commands storage env`:

```bash
# Definir uma variável
npx sdk-commands storage env set MAX_PLAYERS --value 8

# Excluir uma variável
npx sdk-commands storage env delete OLD_VAR

# Excluir todas as variáveis de ambiente
npx sdk-commands storage env clear --confirm
```

Você também pode direcionar um ambiente específico com a `--target` flag:

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

# Implantar em um servidor de desenvolvimento local
npx sdk-commands storage env set MY_KEY --value my_value --target http://localhost:8000
```

As variáveis de ambiente implantadas têm precedência sobre `.env` os valores.

## Estrutura de Projeto Recomendada

Separar o código do servidor, do client e do compartilhado mantém a base de código legível à medida que ela cresce:

```
src/
├── index.ts              # Ponto de entrada — ramificação isServer()
├── client/
│   ├── setup.ts          # Manipuladores de entrada, emissores de mensagens
│   └── ui.tsx            # UI do React ECS (lê o estado sincronizado)
├── server/
│   ├── server.ts         # Loop do jogo, manipuladores de mensagens, mutações de estado
│   └── gameState.ts      # Funções auxiliares para o estado do servidor
└── shared/
    ├── schemas.ts        # Definições de Componentes + validateBeforeChange
    └── messages.ts       # registerMessages() — importado por ambos os lados
```

{% hint style="info" %}
**💡 Dica**: Mantenha todas as `registerMessages()` chamadas e definições de componentes personalizados em `shared/`. Tanto o servidor quanto o client importam de lá, garantindo que sempre concordem com os schemas das mensagens.
{% endhint %}

## Melhores práticas de desempenho

Cada alteração em um componente envia os *dados completos* do componente pela rede. Isso é diferente do que o Colyseus faz, que envia apenas diffs. Ao projetar componentes personalizados, tenha isso em mente. A solução ideal pode ser armazenar dados em componentes separados, com base na frequência de mudança.

### ❌ Evite componentes monolíticos

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

// RUIM — alterar a pontuação também envia o array de posições
const GameState = engine.defineComponent("GameState", {
  playerAScore: Schemas.Int,
  playerBScore: Schemas.Int,
  timer: Schemas.Int,
  playerPositions: Schemas.Array(Schemas.Vector3), // payload grande
})
```

### ✅ Prefira componentes atômicos

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

// BOM — cada atualização é pequena e independente
const PlayerScore = engine.defineComponent("PlayerScore", {
  playerA: Schemas.Int,
  playerB: Schemas.Int,
})

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

*Regra geral*: agrupe campos que mudam juntos e com frequência semelhante. Separe dados que mudam rapidamente (timers, posições) de dados que mudam lentamente (pontuações, configuração).

### Limite mensagens frequentes

Evite enviar mensagens em cada frame. Agrupe ou limite a taxa quando possível:

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

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

Por exemplo, se o server controla um cronômetro regressivo, não é necessário enviar atualizações a todos os players a cada segundo. É melhor fazer com que cada client calcule a passagem do tempo por conta própria e que o server transmita seu estado atual a cada 30 segundos aproximadamente, para garantir consistência.

## Limites de recursos do Server

O Multiplayer Server executa cada scene em um isolate em sandbox com limites rígidos de recursos. Ultrapassar esses limites pode descartar dados silenciosamente ou encerrar o server para todos os players na scene, então projete sua scene para ficar bem dentro deles.

### Memória

O isolate tem um **256 MB** limite de memória. Se excedido, o isolate é descartado e o server é encerrado para todos os players conectados. Mantenha o estado de trabalho enxuto e remova dados por player quando os players saírem.

### CPU

Cada turno de execução tem um orçamento de tempo de relógio:

* **Execução síncrona**: 10 segundos por turno. Um loop sem limites que ultrapasse isso encerra o isolate.
* **Conclusão do turno assíncrono**: 60 segundos. Se um `Promise` chain ou `await` demorar mais do que isso para ser resolvido, o isolate é encerrado.

Distribua o trabalho pesado por vários ticks usando um `dt` acumulador em `engine.addSystem()`. Nunca execute loops síncronos sem limites no server.

### Taxa de mensagens inbound

Cada peer conectado pode enviar até aproximadamente **300 mensagens por 1.000 ms**. Frames de dados excessivos são descartados (não ficam na fila). Nunca envie mensagens em cada frame do client. Veja [Melhores práticas de desempenho](#performance-best-practices) para padrões de limitação.

### Tamanho da mensagem

* Pacotes inbound têm limite de **128 KB** por pacote. Pacotes excessivamente grandes são totalmente descartados.
* Mensagens da scene para as comms têm limite de aproximadamente **30 KB**. Para a camada de transporte prática, mantenha as mensagens sincronizadas bem abaixo de **13 KB** (veja a [Messages](#messages) seção).

### Busca externa

Concorrentes `signedFetch` são limitadas a **32** em andamento. As buscas adicionais entram na fila até que uma vaga seja aberta. Cada tentativa de fetch tem um **timeout de 15 segundos** limite de tempo. **10 MB**. As conexões WebSocket são limitadas a **32** sockets concorrentes, com um tamanho máximo de mensagem de **1 MB** por mensagem.

### Chamadas host em voo

O limite de 40 chamadas descrito em [Armazenamento de dados](#data-storage) aplica-se a todas as chamadas host em todo o isolate, incluindo Storage, `signedFetch`, e outras APIs de runtime. Chamadas excessivas são rejeitadas imediatamente e não entram na fila.

## Armadilhas comuns

### Esquecer a validação no estado apenas do server

Sem `validateBeforeChange`, os clients podem escrever em qualquer 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"

// ❌ RUIM — os clients podem trapacear
const Score = engine.defineComponent("Score", { value: Schemas.Int })

// ✅ BOM — apenas do server
if (isServer()) {
  Score.validateBeforeChange((v) => v.senderAddress === AUTH_SERVER_PEER_ID)
}
```

### Confiar em valores fornecidos pelo client

Nunca deixe um client ditar seus próprios valores para dados importantes como health, score ou position:

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

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

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

### Enviar mensagens antes da sincronização do state

Os clients devem esperar até que o state esteja sincronizado antes de interagir:

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

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

### Aguarde o server iniciar

O server só fica ativo enquanto pelo menos um player estiver na scene. Depois que o último player sai, o server permanece ativo por cerca de dois minutos e então é encerrado. A próxima visita faz um cold start de uma nova instância, o que leva aproximadamente **15 segundos em produção**. A pré-visualização local inicia o server instantaneamente, então problemas de cold start quase sempre aparecem apenas depois da publicação.

O código da sua scene deve estar preparado para esperar o server ficar online. Mensagens enviadas antes de o server terminar de iniciar são perdidas silenciosamente. Mostre uma mensagem de "server acordando" ao player enquanto espera e adicione lógica de retry às solicitações iniciais ao server.

{% hint style="info" %}
**Dica:** Uma maneira confiável de detectar se o server está pronto é por heartbeat: faça o server escrever `Date.now()` em um campo de component sincronizado a cada 2 segundos e faça o client acompanhar quando viu a última alteração de valor. Se nenhuma alteração chegar em até 6 segundos, trate o server como offline. Isso é mais robusto do que `isStateSyncronized()`, que confirma apenas que o transporte está conectado, não que o server esteja em execução.
{% endhint %}

## Exemplo completo

Um contador multiplayer mínimo: clique em um botão, o server incrementa um score sincronizado. O server persiste o contador em `Storage` para que o valor sobreviva a reinicializações do server. Lembre-se de que o server é encerrado quando não há players na scene, então sem storage o contador seria redefinido para zero toda vez que a scene ficasse vazia de players.

```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. Defina as mensagens (compartilhadas)
const Messages = {
  increment: Schemas.Map({}),
  stateUpdate: Schemas.Map({
    count: Schemas.Int,
    lastPlayer: Schemas.String,
  }),
}

// 2. Defina um component apenas do server (compartilhado)
const Counter = engine.defineComponent("Counter", {
  value: Schemas.Int,
  lastPlayer: Schemas.String,
})

// 3. Crie a room
const room = registerMessages(Messages)

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

    // Só o server pode modificar este componente
    Counter.validateBeforeChange(
      (v) => v.senderAddress === AUTH_SERVER_PEER_ID
    )

    // Restaure o contador do storage caso o server tenha reiniciado
    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

      // Persista no storage para que o valor sobreviva a reinicializações do server
      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()
    // ... adicione Transform, MeshRenderer etc.

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

    room.onMessage("stateUpdate", (data) => {
      console.log(`Contagem: ${data.count} (último clique por ${data.lastPlayer})`)
    })
  }
}
```

## Testando localmente

A pré-visualização padrão lida com tudo. Ao usar a branch auth-server do SDK, o server local inicia automaticamente em segundo plano junto com a pré-visualização do client.

Para testar interações multiplayer localmente, abra a pré-visualização em duas janelas separadas; cada janela é tratada como um player diferente. Conecte cada janela com um endereço diferente. Ambos os clients se conectarão à mesma instância local do server.

Usando o Creator Hub, ativa **Multi-Instance Preview** em **Desktop Client** submenu de **Play Options**, depois clica no botão **Play** botão uma segunda vez para abrir uma segunda janela do Decentraland explorer. Tem de se ligar em ambas as janelas com endereços diferentes. As mesmas sessões permanecerão abertas à medida que a cena recarrega.

Como alternativa, podes abrir uma segunda janela do Explorer do Decentraland escrevendo o seguinte num URL do navegador:

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

### Dicas de debugging

* *Prefixe seus logs* com `[SERVER]` ou `[CLIENT]` para que você possa distingui-los no terminal:

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

  if (isServer()) {
    console.log("[SERVER] Iniciando...")
  } else {
    console.log("[CLIENT] Iniciando...")
  }
  ```
* *Verifique a sincronização do component* no client registrando as contagens de entity:

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

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

## Debug em produção

Para ver `console.log()` a saída do seu server publicado, o endereço da sua wallet precisa estar listado no `logsPermissions` array em `scene.json`:

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

Sem isso, os logs do server ficam ocultos em produção, até mesmo para o owner da scene.

Transmita os logs ao vivo do server pela linha de comando executando isto na pasta do seu projeto

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

Você também pode especificar manualmente o nome da world nos logs com:

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

Ao visualizar logs de uma scene em uma world com várias scenes ou parcels em Genesis City, passe também uma `position` para as coordenadas:

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

Você será solicitado a assinar uma mensagem com uma das wallets listadas em `logsPermissions` para autenticar. Uma vez conectado, você verá a saída do server-side `console.log()` em tempo real, o que é útil para diagnosticar problemas sem precisar fazer redeploy.

### Ver dados de storage

Você pode acessar os dados armazenados pelo storage da scene acessando este link:

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

Você também pode chegar a esta página pelo Creator Hub. Abra a **Manage** aba, clique nos três pontos ao lado de um local onde você publicou conteúdo e selecione **View Storage**.

Lá, você pode ver uma lista de todos os Worlds e LAND onde você pode publicar scenes.

Abra a world ou os dados do player para ver as informações armazenadas para cada um.

Por exemplo, se um player específico tiver um problema ao jogar sua scene, você pode localizar esse player pelo address e ver quais dados estão armazenados para ele, para entender a situação. Talvez ele tenha encontrado um caso extremo e acabou com dados contraditórios. Você pode até limpar ou editar os dados desse player nesta página para restaurá-lo a um estado estável.

## Controle de versão

Cada versão publicada da sua scene recebe seu próprio hash ID exclusivo, e cada hash é pareado com sua própria instância de server. Isso significa que o código do client e o código do server sempre seguem juntos; não existe uma janela em que um client executando lógica antiga conversa com um server executando lógica nova (ou vice-versa).

Ao publicar uma atualização:

* *Players já na scene* continuam vendo a versão antiga da scene até saírem e voltarem. Seus clients permanecem conectados à instância de server que corresponde ao hash antigo.
* *Novos players chegando* após a atualização carregam a nova versão da scene e se conectam à nova instância de server.

Isso garante que o estado do client e do server nunca saia de sincronia por causa de uma mudança de schema ou de um component renomeado. Uma atualização nunca pode quebrar a sessão de um player que já esteja na sua scene.

A desvantagem é que, por uma breve janela logo após um deploy, os players podem acabar divididos entre duas instâncias diferentes de server. Um player que já estava lá e um player que acabou de chegar podem não se ver ou não conseguir interagir pela scene, mesmo estando na mesma scene, até que os players mais antigos saiam e entrem novamente.

{% hint style="info" %}
**💡 Dica**: Dados armazenados pelo [Storage](#data-storage) serviço (como leaderboards, progresso do player ou mudanças persistentes no ambiente) são *não* apagados entre versões. O storage é persistido no nível da location e compartilhado entre todas as instâncias de server que apontam para a mesma scene, então as novas versões retomam exatamente de onde a anterior parou.
{% endhint %}

## Migrando do Colyseus

Se você já tem uma scene construída no Colyseus, a tabela abaixo mapeia padrões comuns do Colyseus para seus equivalentes no SDK7:

| Colyseus                         | SDK7 Multiplayer Server                                        |
| -------------------------------- | -------------------------------------------------------------- |
| `room.send(type, data)`          | `room.send(type, data)` — mesma API                            |
| `room.onMessage(type, cb)`       | `room.onMessage(type, cb)` — mesma API                         |
| `room.state.players` (schema)    | `syncEntity` + custom components                               |
| serialização JSON                | serialização binária (automática via `Schemas`)                |
| Aplicação server separada        | Mesma base de código — `isServer()` branching                  |
| Hospedagem customizada do server | Integrado: a pré-visualização executa o server automaticamente |

Diferenças principais a ter em mente:

* *Serialização*: O Colyseus envia diffs em JSON; o SDK envia o component completo a cada alteração. Mantenha os components pequenos (veja [Melhores práticas de desempenho](#performance-best-practices)).
* *Modelo de state*: O Colyseus usa uma árvore de state mutável com diffing automático. O SDK usa components ECS sincronizados via `syncEntity` e protegidos com `validateBeforeChange`.
* *Hospedagem*: Não há deploy separado do server. O Multiplayer Server é implantado automaticamente junto com a 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 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-pt/scenes-sdk7/rede/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.
