Skip to content

サーバー活用例 ​

ここでは createServer() や Server クラスを利用したサーバー実装の例をまとめます。クライアントの接続・認証・切断はライブラリが面倒を見るため、目指す機能に集中できます。

前提

ゲームとして成立させるには、ワールド初期化パケット群(start_game・チャンク・play_status(player_spawn) など)を送ってプレイヤーをスポーンさせる必要があります。完全な実装はリポジトリの examples/server/server.js を参考にしてください。このページの例は「接続〜切断」「メッセージ・UI・音」周りに焦点を当てています。

基本形: 接続を受け取ってキック ​

リポジトリの examples/server/basicServer.js 相当です。

js
const bedrock = require('bedrock-protocol')

const server = bedrock.createServer({
  host: '0.0.0.0',
  port: 19132,
  version: '1.21.50',
  motd: { motd: 'Funtime Server', levelName: 'Wonderland' }
})

server.on('connect', player => {
  player.on('join', () => {
    // 認証・暗号化が完了し、ゲームパケットを送れる状態
    player.disconnect('ようこそ! このサーバーはデモです。')
  })
})

参加ログの出力と切断理由の表示 ​

connect / login / close でプレイヤーの流れを記録します。

js
server.on('connect', player => {
  player.on('login', ({ user, authentication }) => {
    console.log(`ログイン: ${user.displayName} (XUID=${user.XUID})`)
  })

  player.on('join', () => {
    console.log('参加:', player.profile?.name)
  })

  player.on('close', reason => {
    console.log('退出:', player.profile?.name, '理由:', reason)
  })
})

オフライン接続の拒否(認証必須サーバー) ​

正規アカウントの XBL 検証に合格した接続だけを許可し、オフライン(自己申告)接続は早めに切断します。

js
server.on('connect', player => {
  // login は認証検証の後・プレイヤーがゲームに入る前に発火する
  player.on('login', ({ authentication }) => {
    if (!authentication.authenticated) {
      console.log('不正な接続を拒否:', player.connection.address)
      player.disconnect('このサーバーは認証必須です')
    }
  })
})

信頼の判断について

オフライン接続では自己申告の XUID は 0 に正規化されるため、XUID の値ではなく必ず authentication.authenticated で判定してください。

プレイヤーへの通知(Toast / チャット / 効果音) ​

Player インスタンスから任意の clientbound パケットを送れます。入室時に通知を出してみます。

js
server.on('connect', player => {
  player.on('join', () => {
    // チャット欄にシステムメッセージ
    player.queue('text', {
      type: 'system', needs_translation: false,
      message: `${player.profile?.name} さんが参加しました`,
      xuid: '', platform_chat_id: '', filtered_message: ''
    })

    // トースト通知
    player.queue('toast_request', {
      title: 'ようこそ!',
      message: 'マナーを守って楽しんでください'
    })

    // 効果音
    player.queue('play_sound', {
      name: 'note.pling',
      coordinates: { x: 0, y: 0, z: 0 },
      volume: 1.0,
      pitch: 1.0
    })
  })
})

チャット欄・Toast・効果音・フォームの詳細はRelay ガイドの送信欄を参照してください(サーバーの Player でも同じコードで動作します)。

全員へのブロードキャスト ​

server.clients は接続アドレスをキーにした Player の集合です。全員に同じメッセージを送ります。

js
function broadcast(server, message) {
  for (const addr in server.clients) {
    server.clients[addr].queue('text', {
      type: 'system', needs_translation: false, message,
      xuid: '', platform_chat_id: '', filtered_message: ''
    })
  }
}

// 使用例: 参加時に全員へ告知
server.on('connect', player => {
  player.on('join', () => {
    broadcast(server, `${player.profile?.name} が参加しました`)
  })
})

チャットの中継(サーバー主導のチャット) ​

プレイヤーが送った text(type chat)を受信して、形式を整えて全員に配信します。

js
server.on('connect', player => {
  // player 単位でも 'packet' イベントを購読できる
  player.on('packet', ({ name, params }) => {
    if (name === 'text' && params.type === 'chat') {
      broadcast(server, `<${params.source_name}> ${params.message}`)
    }
  })
})

フォーム(メニュー)を出して選択を受け取る ​

サーバー側でも modal_form_request でメニューを出し、serverbound ではなく送信元のプレイヤーイベントで応答を受け取ります。

js
server.on('connect', player => {
  player.on('join', () => {
    player.queue('modal_form_request', {
      form_id: 1,
      data: JSON.stringify({
        type: 'modal',
        title: 'スポーン地点',
        content: '初期スポーン地点を設定しますか?',
        button1: 'はい',
        button2: 'いいえ'
      })
    })
  })

  player.on('packet', ({ name, params }) => {
    if (name === 'modal_form_response' && params.form_id === 1) {
      const accepted = params.has_response_data && params.data === 'true'
      console.log(player.profile?.name, 'は', accepted ? 'はい' : 'いいえ', 'を選んだ')
    }
  })
})

管理コマンドの処理 ​

プレイヤーの command_request を独自に処理します。

js
server.on('connect', player => {
  player.on('packet', ({ name, params }) => {
    if (name !== 'command_request') return

    // サーバー独自コマンドはここで完結(クライアントに応答を返す)
    if (params.command.startsWith('/opme')) {
      player.queue('text', {
        type: 'system', needs_translation: false,
        message: 'あなたを運営に昇格しました(デモ)',
        xuid: '', platform_chat_id: '', filtered_message: ''
      })
    }
  })
})

command_output を返す場合

サーバー専用コマンドの結果をチャットではなくコマンド欄に返したい場合は command_output パケットで返します。origin はリクエストの params.origin.uuid をそのまま使います。

最大人数・MOTD の動的制御 ​

起動後に告知を差し替えたり、満員時の挙動を変えたりできます。

js
// 現在のプレイヤー数
console.log('現在:', server.clientCount, '/', server.options.maxPlayers)

// 告知(ServerAdvertisement)の動的更新
server.on('connect', player => {
  player.on('join', () => {
    const adv = server.getAdvertisement()
    console.log('告知先 MOTD:', adv.motd, '| 人数:', adv.playersOnline)
    // server.advertisement.motd = '新メッセージ' で動的変更も可能
  })
})

// 満員時は接続させない(ログイン前の disconnect 用)
server.on('connect', player => {
  if (server.clientCount > server.options.maxPlayers) {
    player.sendDisconnectStatus('failed_server_full')
  }
})

パケットの受信ログ / 解析 ​

'packet' イベントで特定パケットだけをログに残せます。

js
server.on('connect', player => {
  player.on('packet', ({ name, params }) => {
    if (name === 'level_chunk') {
      console.log(`${player.profile?.name} がチャンク ${params.x},${params.z} を受信`)
    }
  })
})

全サーバー共通のログ

DEBUG ログは process.env.DEBUG = 'minecraft-protocol' をスクリプト先頭に書くか、conLog オプションを設定すると見られます。

スポーンシーケンス(実サーバーとして動作させる骨組み) ​

ゲームとして成立させるには start_game・chunk_radius_update・play_status(player_spawn) などを送る必要があります。最小の骨組みは以下の流れです(実チャンク配信は bedrock-provider を参照)。

js
const server = bedrock.createServer({ host: '0.0.0.0', port: 19132, version: '1.21.50' })

server.on('connect', player => {
  player.on('join', () => {
    // ① リソースパック応答チェーンを処理
    player.once('resource_pack_client_response', () => {
      // ② 初期化パケット群を送る(start_game に必須フィールドが多数あるため、
      //    事前に dump した実パケットを流すのが確実)
      player.queue('start_game', { /* ... start_game の全フィールド ... */ })
      player.queue('set_spawn_position', { position: { x: 0, y: 100, z: 0 } })
      player.queue('set_time', { time: 0 })
      player.queue('set_difficulty', { difficulty: 1 })
      player.queue('set_commands_enabled', { enabled: true })
      player.queue('chunk_radius_update', { chunk_radius: 4 })

      // ③ チャンクを送る
      // player.queue('level_chunk', chunk)

      // ④ プレイヤーをスポーンさせる
      player.queue('play_status', { status: 'player_spawn' })
    })
  })
})

省略フィールドに注意

start_game には必須のフィールドが大量にあります。手書きは壊れやすいため、実サーバーから npm run dumpPackets で流れをダンプし、その JSON を get('start_game') のように流すのが推奨です(examples/server/server.js の方式)。

メンテナンスモード ​

接続を全員切断して受付を止める例です。

js
async function maintenance(reason = 'メンテナンス中です') {
  await server.close(reason) // 接続中の全員に理由付きで切断を通知
  // 再度開く場合は server.listen() を呼ぶ
  await server.listen()
}