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

# Multiplayer Server

Construa scenes multiplayer do Decentraland com um multiplayer server headless.

**📔 Nota**: O Multiplayer Server era anteriormente chamado de **Servidor Autoritativo**. Apenas o nome mudou; a funcionalidade é a mesma. A branch do SDK a instalar ainda se chama `auth-server`, veja [Configuração](#setup).

## Visão geral

Decentraland executa scenes localmente na máquina de um player. Por padrão, os players conseguem se ver e interagir diretamente, mas cada um interage com o ambiente de forma independente. As alterações no ambiente não são compartilhadas entre os players por padrão.

Permitir que todos os players vejam uma scene como tendo o mesmo conteúdo no mesmo estado é extremamente importante para que os players interajam de formas mais significativas. Sem isso, se um player abrir uma porta e entrar em uma house, os outros players verão essa porta ainda fechada, e o primeiro player parecerá atravessar diretamente a porta fechada para os outros players.

O **Multiplayer Server** é um processo de server sem interface que executa o código da sua scene, valida as alterações de state e transmite o resultado para todos os players conectados. Ele segue uma arquitetura de authoritative server: em vez de confiar em cada client para relatar suas próprias ações, o server atua como a única fonte da verdade. Isso o torna a abordagem recomendada para sincronizar scenes multiplayer.

O Multiplayer Server é ideal sempre que a justiça é importante para as mecânicas do jogo, pois você pode implementar validações anti-cheat elaboradas que rodam no server. Você também pode armazenar chaves privadas e outras informações sensíveis no server, evitando precisar expô-las diretamente ao usuário.

Ter um Multiplayer Server também resolve um problema real: em uma configuração peer-to-peer, dois players controlando algo como uma plataforma flutuante podem produzir resultados conflitantes. 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 em um só lugar, então todos os clients convergem para o mesmo estado.

Ele também oferece um lugar para **persistir dados entre sessões**: leaderboards, progressão do player, achievements desbloqueados ou alterações no ambiente, como portas abertas ou itens colocados. Quando os players voltam, o world reflete o que aconteceu antes.

Decentraland hospeda e faz o deploy do server para você. Publicar sua scene pelo processo normal também publica o server de forma transparente, sem etapas extras ou necessidade de pagar por qualquer hospedagem.

## Configuração

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

As APIs nativas do Multiplayer Server (`isServer`, `registerMessages`, `Storage`, `EnvVar`, etc.) estão disponíveis em uma branch separada do SDK. Execute os seguintes comandos para instalá-la no seu projeto em vez da branch padrão do SDK:

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

### 2. Configure scene.json

O Multiplayer Server é ativado pela flag `"authoritativeMultiplayer": true` em `scene.json`, no nível raiz. Você não precisa adicioná-la manualmente: ao usar a branch auth-server do SDK, ela é adicionada automaticamente na primeira vez que você faz build ou preview da scene. Apenas certifique-se de não removê-la; sem essa flag, o server nunca roda e `isServer()` sempre retorna *false*.

Opcionalmente, você também pode adicionar o seguinte ao seu `scene.json` no nível raiz:

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

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

`npx sdk-commands sdk-server-logs`

### 3. Execute o preview

Use o comando padrão de preview, sem etapas extras necessárias. 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á conectada à de produção, então você pode testar as coisas livremente sem afetar os players que estão na sua scene publicada.

## Ramificação Server / Client

O código da scene na pasta `src` do seu projeto roda tanto no server quanto no client. Use a `isServer()` função para dividir 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()) {
    // Apenas no server: lógica do jogo, validação, gerenciamento de state
    initServer()
    return
  } else {
    // Apenas no client: UI, tratamento de input, envio de mensagens
    initClient()
    setupUi()
  }
}
```

O server executa sua scene em modo headless, sem rendering. Ele tem acesso verificado a todas as posições dos players, wearables e outros dados via `PlayerIdentityData` e é a única autoridade sobre o game state.

## Components sincronizados e validação

### Sincronizando Entities para Todos os Clients

Use `syncEntity` para transmitir quaisquer alterações nos components 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 à usada pelo [Serverless multiplayer](/creator/content-creator-pt/scenes-sdk7/networking/serverless-multiplayer.md) feature, tornando trivial fazer upgrade de uma scene de uso dessa arquitetura para o Multiplayer Server. Quando uma scene usa o Multiplayer Server, as atualizações de state não são mais enviadas entre todos os players; em vez disso, todas as atualizações de state agora são roteadas e validadas via o server.

{% hint style="warning" %}
**📔 Nota**: Com o Multiplayer Server, o padrão ideal é fazer com que apenas o server chame `syncEntity`. Dessa forma você não precisa se preocupar com a consistência de entity-id. Em vez disso, a entity é instanciada e compartilhada pelo server, e todos os clients ficam sincronizados sobre essa instância. Sempre proteja isso com `isServer()`. Isso é diferente de [Serverless multiplayer](/creator/content-creator-pt/scenes-sdk7/networking/serverless-multiplayer.md), em que cada client chama `syncEntity` por conta própria.
{% endhint %}

### Validando alterações

Use `validateBeforeChange()` para restringir quaisquer atualizações de state em um component específico de uma entity. Ele permite executar uma função de validação personalizada, e as alterações só têm sucesso quando o teste de validação é atendido.

Se a validação retornar o valor *true*, então a alteração é aceita e propagada para todos os players. Se a validação retornar o valor *false*false

#### Validar valores

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

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

Você pode usar isso para impedir alterações que vão contra a lógica do seu jogo, como mecanismos anti-cheat.

#### Validar proximidade do player

Você também pode validar com base em `validateBeforeChange()` com as posições dos players verificadas pelo server para checar se um player está perto o suficiente de um object antes de permitir que ele interaja com ele. Por exemplo, quando um player tenta pegar um object alterando sua `Transform`, você pode rejeitar a alteração se o object estiver a mais de 5 metros de distância do player:

```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) => {
    // Encontre o player 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

      // Obtenha a posição atual do object, antes da alteração
      const objectTransform = Transform.getOrNull(pickableEntity)
      if (!objectTransform) return false

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

      // Permita a alteração apenas se o player estiver a até 5 metros
      return distance <= 5
    }

    // Remetente não encontrado entre os players conectados — rejeite
    return false
  })
}
```

Esse padrão é útil como mecanismo anti-cheat: ele impede que os players atravessem a scene para pegar objects com os quais não deveriam poder interagir.

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

Você também pode validar com base em *quem* está enviando a alteração. Todo valor recebido inclui um `senderAddress` campo com o endereço de wallet do remetente. Use isso para permitir alterações apenas de certos players. Por exemplo, para permitir que apenas os admins da scene modifiquem um `VideoPlayer` component:

```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 buscar 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()) {
    // Preencha o cache antes de configurar a validação
    await updateAdminAddresses()

    VideoPlayer.validateBeforeChange(videoEntity, (value) => {
      // Sempre permita alterações enquanto estiver rodando 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 do VideoPlayer bloqueada de:",
          senderAddress
        )
        return false
      }
      return true
    })
  }
}
```

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

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

O caso mais rigoroso é aceitar apenas writes que se originam do próprio server, rejeitando qualquer alteração vinda de um client. Esse é o padrão ideal para state que deve ser totalmente authoritative: scores, fase do jogo, entities spawned etc.

Todo valor recebido inclui um `senderAddress` campo. Quando o remetente é o server, esse 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 essa verificação a um ou mais components em uma entity específica. É uma forma conveniente de proteger múltiplos components (como `Transform` e `GltfContainer`) em uma ú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 gerenciada 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**: Sempre chame `protectServerEntity()` dentro de um `isServer()` bloco. Ele envolve `validateBeforeChange()`, que só tem significado no server — chamá-lo em um client produz erros.
{% endhint %}

#### Components customizados

Você também pode aplicar `validateBeforeChange()` em components customizados 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,
})

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

## Mensagens

Components sincronizados são ótimos para state que todos os players devem ver continuamente: coisas como posições, scores ou fase do jogo. Mas nem tudo se encaixa nesse modelo. Às vezes, um player só precisa dizer ao server "cliquei neste button" ou "quero entrar no game", e o server precisa responder com uma resposta única como "a rodada começou" ou "aqui estão suas estatísticas". Isso são eventos, não state contínuo.

É para isso que servem as mensagens. Use `registerMessages()` para comunicação tipada e validada por schema entre clients e o server. Mensagens são fire-and-forget: um client envia uma para o server, o server a processa e opcionalmente envia uma de volta. Elas não criam diretamente nenhum state persistente por conta própria.

### Defina Messages

Defina todas as mensagens em um arquivo compartilhado que tanto o server quanto o client importam. Assim, ambos os lados sempre concordam sobre quais mensagens existem e quais dados elas carregam. Cada mensagem é um `Schemas.Map` que descreve seu 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**: Chame `registerMessages()` uma vez, no nível superior de um módulo compartilhado, como no exemplo acima. Assim ele é executado quando o módulo é carregado pela primeira vez, tanto no server quanto no client. Não o chame condicionalmente nem dentro de funções; ambos os lados devem sempre registrar as mesmas mensagens.
{% endhint %}

### Enviar mensagens

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

```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 mensagens têm um tamanho máximo de cerca de 13 KB. Mensagens maiores são descartadas silenciosamente pelo transporte, sem gerar nenhum erro. Mantenha os payloads pequenos; se precisar compartilhar dados maiores, divida-os em várias mensagens ou use components sincronizados.
{% endhint %}

### Receber mensagens

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

// Client recebe do server
room.onMessage("gameStarted", (data) => {
  console.log(`Round ${data.roundNumber} started!`)
})

// 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, toda mensagem recebida inclui um `context` objeto com o endereço de wallet verificado do remetente. Use isso para saber qual player enviou a mensagem (nunca confie na identidade autodeclarada no payload).

### Aguarde a sincronização do State antes de enviar

Os clients devem esperar até que o state da scene esteja sincronizado antes de enviar sua primeira mensagem, para evitar condições de corrida na entrada:

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

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

  // Agora é seguro enviar mensagens
  room.send("playerReady", { displayName: "Alice" })
})
```

{% hint style="info" %}
**💡 Dica**: `isStateSyncronized()` apenas informa que o state do client está sincronizado; isso não garante que o server tenha terminado de inicializar. Para uma verificação de prontidão mais robusta, faça o server transmitir uma mensagem "ready" ou de heartbeat, e faça os clients aguardarem por ela antes de enviar mensagens de gameplay.
{% endhint %}

### Tipos de Schema disponíveis

Todos os payloads de mensagens e Components customizados usam `Schemas` para serialização binária. Aqui está 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**: Messages *deve* ser definido usando `Schemas.Map(...)`. Você não pode enviar objetos JavaScript puros; eles falharão na serialização binária.
{% endhint %}

## Leitura de Posições de Players pelo Server

O server pode ler **verificadas** as posições dos players; os clients 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 pelo server — nunca confie na posição reportada pelo client
  }
})
```

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

## Armazenamento de Dados

Persista dados entre reinicializações do server. Storage é **apenas do server**, proteja sempre as chamadas com `isServer()`. O server pode tanto gravar 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 players, como leaderboards ou mudanças persistentes no ambiente.
* **Player**: Use isto para dados específicos do player, como salvar progresso ou preferências para esse player.

{% hint style="info" %}
**💡 Dica**: 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` de exclusão **boolean**. Elas nunca lançam exceção — em caso de falha (um erro de rede, ou solicitações concorrentes em excesso, veja abaixo) elas registram o erro e retornam `false`false **não** persistido. Sempre verifique o resultado: um salvamento descartado `false` é um salvamento perdido silenciosamente.
{% endhint %}

### Salve em checkpoints, não em cada mudança

Storage é uma persistência durável para dados que precisam sobreviver a reinicializações e redeploys do server. Não é um datastore ao vivo. Mantenha o estado de jogo ativo na memória no server. Isso é mais rápido e é o padrão certo para um server. Grave em Storage apenas quando realmente precisar, em checkpoints significativos.

{% hint style="warning" %}
**⚠️ Aviso**: O runtime do server permite no máximo **40 host calls em voo** de uma vez, compartilhadas entre *tudo* o que a scene solicita que o runtime faça: cada request de Storage, `signedFetch`, e outras APIs do runtime contam para o mesmo limite. Chamadas em excesso não são **enfileiradas**. Elas são rejeitadas imediatamente com um `erro de muitas host calls concorrentes` erro. O SDK captura a rejeição e resolve a `Storage.set` promessa para `false` em vez de lançar exceção. Se o seu código descartar esse boolean, o salvamento com falha fica invisível e seus dados persistidos acabam desatualizados ou perdidos. Verifique o resultado de toda gravação (veja a Nota acima).
{% endhint %}

Bons momentos para persistir:

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

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

O exemplo abaixo mantém os scores na memória e os grava apenas em um checkpoint, em vez de a cada ponto marcado:

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

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

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

// Persista apenas em um checkpoint, como quando um player 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 foi persistida — mantenha o valor na memória e tente novamente mais tarde
    console.error(`Failed to save score for ${address}`)
  }
}
```

Para scenes com muitos players concorrentes, uma abordagem mais robusta rastreia quais keys têm mudanças não salvas (um "dirty set") e tenta novamente as gravações com falha na próxima flush:

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

// Estado ativo na memória, atualizado em 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, nenhuma chamada de Storage aqui
}

// Faça o flush das keys sujas periodicamente (por exemplo, a cada ~30s) e nos checkpoints
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 key permanece suja e tenta novamente no próximo flush
  }
}

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

### Storage de World — Compartilhado entre todos os players

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

// Gravar — set() retorna 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) : []

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

Você também pode gerenciar o storage da scene via a 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

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

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

### Storage do Player — Por endereço de Wallet

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

// Gravar — set() retorna 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(`Failed to save progress for ${playerAddress} — retry later`)
}

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

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

Você também pode gerenciar o storage do player via a 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...

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

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

# Remover 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 server pela UI de storage, acessando este link:

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

Você também pode acessar esta página pelo Creator Hub. Abra a **Manage** tab, 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 LANDs onde você pode publicar scenes.

Abra sua scene e então a **Scene** ou **Player** tab.

Na **Scene** tab 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)

Na **Player** tab você verá uma lista de todos os players que têm algum dado armazenado no seu server. Você pode pesquisá-los por address ou name e então ver todos os dados associados. 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 **não são apagados quando você publica uma nova versão da sua scene**. Isso é ótimo para leaderboards, progresso do player e mudanças persistentes no ambiente que os players esperam que continuem existindo além de pequenas atualizações na sua scene.

A outra face é que os dados armazenados foram gravados por uma versão mais antiga do seu código. Se o seu novo código espera uma forma diferente, analisar 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 lançará uma exceção quando você tentar acessar uma propriedade nele. Um player que não faz login há meses pode carregar dados que antecedem uma estrutura que seu código já não sabe como lidar.

{% 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 removidos, então um valor em formato antigo pode aparecer a qualquer momento, muitas vezes de um player que voltou e de quem você já 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 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 aos padrões
    }
  }
  ```
* *Adicione campos, não os renomeie nem remova*. A mudança de schema mais segura é 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 faz com que todos os registros antigos quebrem.
* *Versione seus objetos armazenados*. Inclua um `version` campo desde o primeiro dia. Quando você ler dados, faça branch com base na version e migre formas antigas para a atual antes de usá-las. Isso mantém o restante do seu código funcionando com uma única forma 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) {
      // v1 não tinha o campo xp — defina um valor padrão
      return {
        version: 2,
        level: raw.level ?? 1,
        coins: raw.coins ?? 0,
        xp: 0,
      }
    }
    return raw as ProgressV2
  }
  ```
* *Grave novamente o valor migrado*. Depois de atualizar um registro na memória, grave-o de volta para que a próxima leitura já esteja no novo formato. Com o tempo, isso drena o conjunto de registros em formato antigo sem precisar de um script de migração único.
* *Para mudanças que quebram compatibilidade, use uma nova key*. Se a nova estrutura for realmente incompatível e migrar não valer a pena, grave em uma nova key de storage (por exemplo `progress_v2`) e ignore a antiga. A key antiga fica inofensiva no storage e você evita qualquer caminho de leitura que precise interpretá-la. Você pode limpar as keys antigas depois pela [storage UI](https://decentraland.org/storage) ou pelos `npx sdk-commands storage` comandos.
* *Teste contra dados reais de produção*. Antes de fazer deploy de uma mudança estrutural, puxe alguns registros reais da storage UI e execute seu novo código de parse sobre eles. Os casos de borda que causam problemas geralmente são registros cuja existência você nem sabia.
* *Deixe uma válvula de escape*. Lembre-se de que você pode editar ou remover registros individuais na storage UI ou via `npx sdk-commands storage`. Se um único player ficar preso 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. As environment variables são úteis para dados sensíveis e também para feature flags ou parâmetros que podem ser facilmente alterados sem republicar sua scene.

As environment variables são **apenas do server**. `isServer()`Proteja-as com

`EnvVar.get()` retorna um `Promise<string>` e resolve para uma string vazia quando a variável não está 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

As environment variables são especialmente úteis para armazenar chaves privadas, códigos de resgate de recompensa 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 server e fazer com que apenas o server as leia com `isServer()`. Dessa forma, os dados sensíveis nunca passam pela máquina do player.

### Desenvolvimento Local

Para usar environment variables 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 environment variables

A maneira mais fácil de alterar os valores das suas environment variables é 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 acessar esta página pelo Creator Hub. Abra a **Manage** tab, 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 LANDs onde você pode publicar scenes.

Abra sua scene e então a **Environment** tab. Você deve ver todas as environment variables no 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)

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

Você também pode gerenciar environment variables 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

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

# Remover todas as environment variables
npx sdk-commands storage env clear --confirm
```

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

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

# Fazer deploy para um server de desenvolvimento local
npx sdk-commands storage env set MY_KEY --value my_value --target http://localhost:8000
```

As environment variables implantadas têm precedência sobre `.env` valores.

## Estrutura Recomendada do Projeto

Separar o código de server, client e shared mantém a codebase legível à medida que cresce:

```
src/
├── index.ts              # Ponto de entrada — branch de isServer()
├── client/
│   ├── setup.ts          # Handlers de input, senders de mensagens
│   └── ui.tsx            # UI de React ECS (lê estado sincronizado)
├── server/
│   ├── server.ts         # Game loop, handlers de mensagens, mutações de estado
│   └── gameState.ts      # Funções auxiliares para o estado do server
└── shared/
    ├── schemas.ts        # Definições de Component + validateBeforeChange
    └── messages.ts       # registerMessages() — importado por ambos os lados
```

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

## Boas Práticas de Performance

Toda mudança de Component envia o *inteiro* dados do Component pela rede. Isso é diferente do que o Colyseus faz, que envia apenas diffs. Ao projetar custom Components, tenha isso em mente. A solução ideal pode ser armazenar dados em Components separados, com base na frequência de mudança.

### ❌ Evite Components monolíticos

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

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

### ✅ Prefira Components 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 uma frequência semelhante. Separe os dados que mudam rápido (temporizadores, posições) dos dados que mudam devagar (pontuações, configuração).

### Limitar mensagens frequentes

Evite enviar mensagens a cada frame. Agrupe ou limite a frequência 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 temporizador regressivo, não é necessário enviar atualizações a todos os players a cada segundo. O ideal é 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 na scene, então projete sua scene para ficar bem abaixo deles.

### Memória

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

### CPU

Cada turno de execução tem um orçamento de tempo de parede:

* **Execução síncrona**: 10 segundos por turno. Um loop sem limite que exceda isso encerra o isolate.
* **Estabilização do turno assíncrono**: 60 segundos. Se uma `Promise` encadeada ou `await` demorar mais do que isso para resolver, o isolate é encerrado.

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

### Taxa de mensagens de entrada

Cada peer conectado pode enviar até aproximadamente **300 mensagens por 1.000 ms**. Frames de dados em excesso são descartados (não enfileirados). Nunca envie mensagens a cada frame do client. Veja [Boas Práticas de Performance](#performance-best-practices) para padrões de limitação.

### Tamanho da mensagem

* Os pacotes de entrada têm limite de **128 KB** por pacote. Pacotes muito grandes são descartados por completo.
* As mensagens de scene para comunicações têm limite de aproximadamente **30 KB**. Para a camada prática de transporte, mantenha as mensagens sincronizadas bem abaixo de **13 KB** (veja a [Mensagens](#messages) seção).

### Fetch externo

Chamadas concorrentes `signedFetch` têm limite de **32** em voo. Fetches adicionais entram em fila até que uma vaga seja aberta. Cada tentativa de fetch tem um timeout de **15 segundos** As respostas de fetch têm limite de **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 de host em voo

O limite de 40 chamadas descrito em [Armazenamento de Dados](#data-storage) aplica-se a todas as chamadas de host em todo o isolate, incluindo Storage, `signedFetch`, e outras APIs de runtime. Chamadas em excesso são rejeitadas imediatamente e não entram em 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 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 vida, pontuação ou posição:

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

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

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

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

### Aguarde o server iniciar

O server fica ativo somente enquanto houver pelo menos um player 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 uma inicialização a frio de uma nova instância, o que leva aproximadamente **15 segundos em produção**. A prévia local inicia o server instantaneamente, então problemas de inicialização a frio quase sempre aparecem apenas após a publicação.

O código da sua scene deve estar preparado para aguardar o server ficar online. Mensagens enviadas antes de o server terminar de iniciar são perdidas silenciosamente. Exiba uma mensagem de "server despertando" ao player enquanto espera e adicione lógica de nova tentativa para as requisições iniciais ao server.

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

## Exemplo Completo

Um contador multiplayer mínimo: clique em um Button e o server incrementa uma pontuação sincronizada. O server persiste o contador no Storage para que o valor sobreviva a reinicializações do server. `Storage` para que o valor sobreviva às reinicializações do server. Lembre-se de que o server é encerrado quando não há players na scene, então, sem Storage, a contagem seria redefinida 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 (compartilhado)
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 ===

    // Apenas o server pode modificar este component
    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 às 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évia padrão cuida de tudo. Ao usar a branch auth-server do SDK, o server local inicia automaticamente em segundo plano junto com a prévia do client.

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

Usando o Creator Hub, clique no Button Preview uma segunda vez, e isso abre uma segunda janela do explorer Decentraland. Você deve se conectar em ambas as janelas com endereços diferentes. As mesmas sessões permanecerão abertas enquanto a scene é recarregada.

Como alternativa, você pode abrir uma segunda janela do explorer Decentraland escrevendo o seguinte em uma URL do browser:

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

### Dicas de depuração

* *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 a contagem 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 em Produção

Para ver a `console.log()` saída do seu server publicado, o endereço da sua wallet deve 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 proprietário 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 do world nos logs com:

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

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

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

Será solicitado que você assine uma mensagem com uma das wallets listadas em `logsPermissions` para autenticar. Depois de conectado, você verá a saída do server `console.log()` em tempo real, o que é útil para diagnosticar problemas sem precisar fazer redeploy.

### Ver dados do 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 acessar esta página pelo Creator Hub. Abra a **Manage** tab, 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 LANDs onde você pode publicar scenes.

Abra o 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ê poderia localizar esse player pelo endereço e ver quais dados estão armazenados para ele para entender sua situação. Talvez ele tenha caído em um caso extremo em que 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 do server. Isso significa que o código do client e o código do server sempre evoluem juntos; não existe uma janela em que um client executando lógica antiga fale com um server executando lógica nova (ou vice-versa).

Quando você publica 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 do 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 do 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á está na sua scene.

A contrapartida é que, por uma pequena janela logo após um deploy, os players podem acabar divididos entre duas instâncias diferentes do server. Um player que já estava lá e um player que acabou de chegar podem não se ver nem conseguir interagir via a 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) service (como tabelas de classificação, progresso do player ou alterações persistentes do ambiente) são *não* apagados entre versões. O Storage é persistido no nível da localização e compartilhado entre todas as instâncias do 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ê tem uma scene existente 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` + components personalizados             |
| Serialização JSON                  | Serialização binária (automática via `Schemas`)      |
| Aplicação de server separada       | Mesmo codebase — `isServer()` ramificação            |
| Hospedagem personalizada de server | Integrada: a prévia executa o server automaticamente |

Principais diferenças a ter em mente:

* *Serialização*: o Colyseus envia diffs JSON; o SDK envia o component completo a cada mudança. Mantenha os components pequenos (veja [Boas Práticas de Performance](#performance-best-practices)).
* *Modelo de estado*: o Colyseus usa uma árvore de estado 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 recebe deploy 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 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-pt/scenes-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.
