mcpField
A
authoritative-server

authoritative-server

github.com/dcl-regenesislabs开发1737 次安装21 周前更新
代码仓库

关于 authoritative-server

用于开发 Decentraland 多人场景的技能,由无头服务器掌控游戏状态并验证变更。内容涵盖服务端与客户端分支、同步状态、消息通信、持久化和环境配置。

主要功能

  1. 01使用 isServer() 区分共享代码
  2. 02验证服务端权威组件变更
  3. 03向客户端同步实体和状态
  4. 04在客户端与服务器间交换消息
  5. 05持久化世界和玩家数据

适用场景

  1. 01构建由服务器控制状态的多人场景
  2. 02验证玩家操作以防客户端篡改
  3. 03跨重启保存排行榜或玩家分数
  4. 04通过环境变量配置场景行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dcl-regenesislabs/opendcl --skill authoritative-server

安装后在 Claude Code 中输入/authoritative-server确认已加载。

附带文件references/server-patterns.md

详细介绍

权威服务器模式

构建多人 Decentraland 场景,由无头服务器控制游戏状态、验证更改并防止作弊。服务器和客户端运行同一套代码,且服务器拥有完全控制权。

如需了解基础 CRDT 多人游戏(无服务器),请改用 multiplayer-sync 技能。

设置

1. 安装 auth-server SDK 分支(必需)

必须使用 auth-server 标签——标准的 @dcl/sdk 不包含权威服务器 API(isServer、registerMessages、Storage、EnvVar 等):

npm install @dcl/sdk@auth-server
2. 配置 scene.json

你的 scene.json 必须包含以下属性:

  • authoritativeMultiplayer: true — 启用权威服务器运行时。
  • worldConfiguration.name — 标识用于部署、Storage 和 EnvVar 的世界。
  • logsPermissions — 允许在控制台中查看服务器日志的钱包地址数组。若缺少此项,服务器的 console.log() 输出会被隐藏。
{
  "authoritativeMultiplayer": true,
  "worldConfiguration": {
    "name": "my-world-name.dcl.eth"
  },
  "logsPermissions": ["0xYourWalletAddress"]
}
3. 运行场景

直接使用常规预览即可——当 scene.json 中设置了 authoritativeMultiplayer: true 时,预览会自动在后台启动权威服务器。

调试说明(不要告诉用户运行此命令): 在底层,预览会运行 npx @dcl/hammurabi-server@next。如果 auth server 未启动,请检查 hammurabi 进程是否正在运行,并查看其输出中的错误。

服务器/客户端分支

使用 isServer() 在同一套代码中分支处理逻辑:

import { isServer } from '@dcl/sdk/network'

export async function main() {
  if (isServer()) {
    // Server-only: game logic, validation, state management
    const { server } = await import('./server/server')
    server()
    return
  }

  // Client-only: UI, input, message sending
  setupClient()
  setupUi()
}

服务器会以无头模式运行你的场景代码(不进行渲染)。它可以通过 PlayerIdentityData 访问所有玩家的位置,并管理所有权威游戏状态。

带验证的同步组件

定义从服务器同步到所有客户端的自定义组件。始终使用 validateBeforeChange(),以防止客户端修改服务器权威状态。

自定义组件(全局验证)
import { engine, Schemas } from '@dcl/sdk/ecs'
import { AUTH_SERVER_PEER_ID } from '@dcl/sdk/network/message-bus-sync'

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

// Restrict ALL modifications to server only
GameState.validateBeforeChange((value) => {
  return value.senderAddress === AUTH_SERVER_PEER_ID
})
内置组件(按实体验证)

对于 Transform 和 GltfContainer 等内置组件,请使用按实体验证,以免阻止玩家自身实体上的客户端变换:

import { Entity, Transform, GltfContainer } from '@dcl/sdk/ecs'
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
    })
  }
}

// Usage: after creating a server-managed entity
const entity = engine.addEntity()
Transform.create(entity, { position: Vector3.create(10, 5, 10) })
GltfContainer.create(entity, { src: 'assets/model.glb' })
protectServerEntity(entity, [Transform, GltfContainer])
同步实体

创建实体并设置保护后,将其同步到所有客户端:

import { syncEntity } from '@dcl/sdk/network'

syncEntity(entity, [Transform.componentId, GameState.componentId])

消息

使用 registerMessages() 进行客户端到服务器以及服务器到客户端的通信:

定义消息
import { Schemas } from '@dcl/sdk/ecs'
import { registerMessages } from '@dcl/sdk/network'

export const Messages = {
  // Client -> Server
  playerJoin: Schemas.Map({ displayName: Schemas.String }),
  playerAction: Schemas.Map({ actionType: Schemas.String, data: Schemas.Number }),

  // Server -> Client
  gameEvent: Schemas.Map({ eventType: Schemas.String, playerName: Schemas.String })
}

export const room = registerMessages(Messages)
发送消息
// Client sends to server
room.send('playerJoin', { displayName: 'Alice' })

// Server sends to ALL clients
room.send('gameEvent', { eventType: 'ROUND_START', playerName: '' })

// Server sends to ONE client
room.send('gameEvent', { eventType: 'YOU_WIN', playerName: 'Alice' }, { to: [playerAddress] })
接收消息
// Server receives from client
room.onMessage('playerJoin', (data, context) => {
  if (!context) return
  const playerAddress = context.from  // Wallet address of sender
  console.log(`[Server] Player joined: ${data.displayName} (${playerAddress})`)
})

// Client receives from server
room.onMessage('gameEvent', (data) => {
  console.log(`Event: ${data.eventType}`)
})
等待状态同步

从客户端发送消息前,请等待状态同步完成:

import { isStateSyncronized } from '@dcl/sdk/network'

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

  // Safe to send messages now
  room.send('playerJoin', { displayName: 'Player' })
})

服务器读取玩家位置

服务器可以读取玩家的实际位置——这对防作弊至关重要:

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
    // Use actual server-verified position, not client-reported data
  }
})

切勿信任客户端报告的位置。始终在服务器上读取 PlayerIdentityData 和 Transform。

存储

在服务器重启后保留数据。仅限服务器使用——请用 isServer() 进行保护。

import { Storage } from '@dcl/sdk/server'
世界存储(全局)

在所有玩家之间共享:

// Store
await Storage.world.set('leaderboard', JSON.stringify(leaderboardData))

// Retrieve
const data = await Storage.world.get<string>('leaderboard')
if (data) {
  const leaderboard = JSON.parse(data)
}

// Delete
await Storage.world.delete('oldKey')
玩家存储(按玩家)

以玩家钱包地址为键:

// Store
await Storage.player.set(playerAddress, 'highScore', String(score))

// Retrieve
const saved = await Storage.player.get<string>(playerAddress, 'highScore')
const highScore = saved ? parseInt(saved) : 0

// Delete
await Storage.player.delete(playerAddress, 'highScore')

Storage 仅接受字符串。对象请使用 JSON.stringify()/JSON.parse(),数字请使用 String()/parseInt()。

本地开发存储位于 node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json。

环境变量

无需硬编码值即可配置场景。仅限服务器使用——请用 isServer() 进行保护。

import { EnvVar } from '@dcl/sdk/server'

// Read a variable with default
const maxPlayers = parseInt((await EnvVar.get('MAX_PLAYERS')) || '4')
const debugMode = ((await EnvVar.get('DEBUG')) || 'false') === 'true'
本地开发

在项目根目录中创建 .env 文件:

MAX_PLAYERS=8
GAME_DURATION=300
DEBUG=true

将 .env 添加到 .gitignore。

部署到生产环境
# Set a variable
npx sdk-commands deploy-env MAX_PLAYERS --value 8

# Delete a variable
npx sdk-commands deploy-env OLD_VAR --delete

已部署的环境变量优先于 .env 文件中的值。

推荐的项目结构

src/
├── index.ts              # Entry point — isServer() branching
├── client/
│   ├── setup.ts          # Client initialization, message handlers
│   └── ui.tsx            # React ECS UI reading synced state
├── server/
│   ├── server.ts         # Server init, systems, message handlers
│   └── gameState.ts      # Server state management class
└── shared/
    ├── schemas.ts        # Synced component definitions + validateBeforeChange
    └── messages.ts       # Message definitions via registerMessages()

将同步组件和消息放在 shared/ 中,以便服务器和客户端导入相同的定义。将服务器逻辑(Storage、EnvVar、游戏系统)放在 server/ 中。将 UI 和客户端输入处理放在 client/ 中。

测试与调试

  • 日志前缀:在 console.log() 中使用 [Server] 和 [Client] 前缀,以便在终端中区分服务端和客户端输出。
  • 过期的 CRDT 文件:如果看到 "Outside of the bounds of written data" 错误,请删除 main.crdt 和 main1.crdt 文件,然后重新启动。
  • 存储检查:在本地开发期间,检查 node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json,查看已持久化的数据。
  • 计时器:运行时 polyfill 提供了 setTimeout/setInterval。对于游戏逻辑,优先使用带有 delta-time 累加器的 engine.addSystem(),以便与帧循环保持同步。
  • 实体同步问题:确认使用正确的组件 ID 调用 syncEntity(entity, [componentIds])(MyComponent.componentId)。

重要说明

  • 时间戳使用 Schemas.Int64:Schemas.Number 会损坏较大的数字(13 位或更多)。对于 Date.now() 等值,请始终使用 Schemas.Int64。
  • 状态同步就绪:客户端在发送消息前,必须等待 isStateSyncronized()(来自 @dcl/sdk/network)返回 true。请注意 SDK 中有意保留的拼写错误:"Syncronized" 而非 "Synchronized"。
  • 自定义验证与内置验证:自定义组件使用全局的 validateBeforeChange((value) => ...)。内置组件(Transform、GltfContainer)使用针对每个实体的 validateBeforeChange(entity, (value) => ...)。
  • 单一代码库:服务端和客户端运行相同的 index.ts 入口点。使用 isServer() 进行分支判断。
  • 不支持 Node.js API:DCL 运行时使用沙箱化的 QuickJS,不支持 fs、http 等。支持 setTimeout/setInterval。服务端操作请使用 SDK 提供的 API(Storage、EnvVar、engine systems)。
  • SDK 分支(必须):auth-server 模式要求使用 npm install @dcl/sdk@auth-server,而非标准的 @dcl/sdk。否则,isServer()、registerMessages()、Storage 和 EnvVar 将不可用。
  • scene.json 必填字段:必须设置 authoritativeMultiplayer: true,并在 logsPermissions: ["0xWalletAddress"] 中列出应能查看服务端日志的钱包地址。
  • 如需不使用服务端的基础 CRDT 多人游戏,请参阅 multiplayer-sync 技能。

如需完整的服务端设置示例、身份验证流程、状态协调、Storage 模式和 EnvVar 用法,请参阅 {baseDir}/references/server-patterns.md。

评论

支持 Markdown