跳到正文
本頁目錄

WebSocket 接口

契約:bedd.asyncapi.yaml(spec/,AsyncAPI 3.0;Studio)。

連接

發現文件 %LOCALAPPDATA%\BeDD\run\<pid>.json(根目錄 BEDD_HOME),監聽期間存在;跳過已退出的 pid。 BeDD.exe ps --json 列出相同內容。

json
{"protocol": 2, "kind": "client", "pid": 23116, "url": "ws://127.0.0.1:53021/v1", "token": "9c1f…(64 hex)",
 "instance": "MyInstance-2", "instanceDir": "…\\instances\\MyInstance-2", "label": "smoke test", "modVersion": "0.4.0", "startedAt": "2026-11-13T09:41:07Z"}
項
kindclient(遊戲)或 server(服務端構建)
端口配置 wsPort(默認 0:每次啟動取空閒端口);從文件讀取
令牌32 個隨機字節,每次啟動重新生成。Authorization: Bearer <token> 或 ?token=<token>;錯誤或缺失:HTTP 401
Origin握手帶 Origin → HTTP 403,除非在配置 allowOrigins 中(默認為空):瀏覽器無法連接
限制僅 127.0.0.1;wsMaxClients 條連接(8);UTF-8 JSON 文本消息 ≤ 16 MiB;不壓縮

Node.js 24(其 WebSocket 不設請求頭):

js
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const dir = join(process.env.BEDD_HOME ?? join(process.env.LOCALAPPDATA, 'BeDD'), 'run');
const game = readdirSync(dir).map((f) => JSON.parse(readFileSync(join(dir, f), 'utf8')))
  .find((e) => e.kind === 'client'); // and its pid alive
const ws = new WebSocket(`${game.url}?token=${game.token}`);
ws.onopen = () => ws.send(JSON.stringify({ id: 1, op: 'world' }));
ws.onmessage = (e) => { console.log(JSON.parse(e.data)); ws.close(); };

請求與回覆

json
{"id": 7, "op": "look", "yaw": 90, "pitch": 0}
{"id": 7, "ok": true, "yaw": 90, "pitch": 0, "tick": 5182, "turns": 1}
  • id:任意 JSON 值,原樣返回。失敗:{id, ok:false, error, code?, key?};按 code 判斷(錯誤)。
  • 每條連接按順序處理;並行請求用多條連接。
  • 探測:不帶參數發送操作(bedboo 操作此時只讀):unknown op = 不存在,disabled = 已關閉,ok = 可用。

選擇模式

先 bedboo 操作,再 ui,最後 input 操作(原因)。ping 列出它們:

json
{"ok": true, "protocol": 2, "pid": 23116, "kind": "client", "defaultMode": "bedboo",
 "modes": {"bedboo": ["world","look","input","slot","action","button","settings","send","stack","timescale","join","tap"],
           "ui": ["ui","oreui"], "input": ["key","text","mouse","raw","release","flush","focus","window","screenshot"]},
 "hwnd": 4589122, "managed": true, "client": {"w": 1280, "h": 720}, "cursor": {"x": 640, "y": 360}, "realForeground": false}

bedboo 操作

配置鍵為 false → disabled。實現。

操作配置鍵探測回覆動作字段(默認)
worldgameHooksinLevel、levelEpoch(每進一個世界 +1)、topScreen、mouseGrabbed、showingMenu、inputMode mouse|touch|gamepad|undefined、player{runtimeId, eye{x,y,z}, rot{yaw,pitch}, slot}(null:無世界)、tick無
lookgameHooksinLevel, yaw, pitch, tickyaw、pitch、timeoutMs(1000*,≤60000)
inputgameHooksactive、via hooks|keys、hooks{raw,state}、tick、applied、ticksLeftmove{x,z}、jump、sneak、sprint、ttlMs(300)或 ttlTicks
slotgameHooksslot(null:無世界)slot 0..8、timeoutMs(1000*)
actiongameHooksnamesname drop|dropAll|inventory|perspective|pause|swing;hand main|off(swing);timeoutMs(1000*)
buttongameHooksheldbutton attack|use|pick;action down|up|click(click);holdTicks(click:2);releaseOn blockChange;timeoutMs(1000*)
settingsgameHooksrenderDistance(區塊;第一次進世界前為 null)、inLevelrenderDistance(區塊,1..255)
sendallowSendallow(sendAllow 中的 id;"*" 時逐個列出)、allowAllbuf、timeoutMs(2000*,≤10000)
stackallowStackenabled, via:"raw", stats{sent, answered, rejected, failed, resyncs}buf、timeoutMs(2000*);或 view:true
taptapenabled、attached、link{state up|down, type remote|local, address}、journal{frames, bytes, complete}、lastReplay、replay、counters{in, out, dropped, sent, readers}、hooksaction:"subscribe" → tap 流
timescale無見下scale、paused、timeoutMs(2000)
join無mode、connection none|local|remote、host、port、inLevel、joining、lasthost(null:離開世界)、port(19132)、wait、timeoutMs(60000)

* 指 ≥20 tick/s 時;更慢時默認值按 tick 間隔 / 50 ms 放大(不超過上限)。not_in_level:20 個 tick 間隔(≥1 s)內 沒有客戶端 tick。

操作行為錯誤
look絕對角度;兩者都在 0.5° 內時回覆 yaw, pitch, tick, turns。Yaw 0 = +Z,90 = -X;pitch -90 = 朝上,限制在 ±89.9not_in_level、timeout(帶當前 yaw/pitch)、busy(有更新的 look)、bad_args
inputmove:x = 左右,z = 前後,限制在 [-1,1],斜向歸一化。在請求後 ttlMs(≤60000,超出截斷)結束(要繼續移動就刷新),或遇 ttlMs:0、release 結束。ttlTicks(≤1200)按客戶端 tick 計數,5 s 無 tick 也結束。比其連接活得久。via:"hooks":僅在鼠標被捕獲且無界面時生效;applied = 已寫入的 tick 數。via:"keys"(鉤子缺失):像 key 一樣按住 W/A/S/D、SPACE、LSHIFT、LCTRL(會作用到界面,改鍵後失效)bad_args(ttlTicks 與 ttlMs 同時給出)、not_in_level(ttlTicks,無 tick)
slot、action、button下一個客戶端 tick 應用後回覆(tick)。暫停的單人世界不 tick:用 key ESC 恢復not_in_level、busy(界面打開;button up 仍可用)、timeout、bad_args、refused
slot快捷欄按鍵 1..9;選中後回覆(slot、via keys|none)
action按鍵在下一 tick 鬆開:drop Q、dropAll Ctrl+Q、inventory E、pause Escape(默認鍵位);perspective:調用遊戲函數。swing:LocalPlayer::swing,揮手不攻擊,客戶端自己發 animate;打開界面時也能用;hand:"off" 只在本客戶端可見。回覆 name、hand
button虛擬 GameInput 左/右/中鍵;holdTicks 後鬆開,或(releaseOn:"blockChange")在 down 時瞄準的方塊改變、變成空氣或不再被瞄準時鬆開。up 立即鬆開。從不阻塞。回覆 held、block{x,y,z}。斷開時只鬆開無終點的 down;其餘跑完,releaseOn 最多再 1200 tickrefused:未瞄準方塊
settingsrenderDistance 在下一客戶端 tick 設置所有渲染器(普通、Vibrant Visuals、光線追蹤)的渲染距離(不寫入 options.txt),再調用 ClientInstance::updateChunkRadius:客戶端發 request_chunk_radius;服務器批准的半徑以 chunk_radius_update 到達(tap)。回覆 renderDistance(當前渲染值)、requested、ticknot_in_level、timeout、bad_args、unavailable
sendbuf:base64 的服務端方向數據包(varint 頭 + 包體,bedrock-protocol),由本地玩家發送;回覆 packetId, name。統計:state.stats.send{sent, refused, failed, lastError}bad_args(無法解析,未發送)、refused(不在 sendAllow 中)
join從標題界面或世界中(先離開:left);自動應答 "Online play is not rated"(Proceed)和 "Trust this server?"(Trust and Join)。開始連接後 started:true;wait:true → joined:true。非本機主機需要 allowHosts(Remote 模式);allowHosts 含 "*" 時解析該主機並加入(解析不了為 not_allowed)。host: null 離開世界(保存並退出 / 斷開連接):left:true(wait:存好並回到菜單後;否則 started:true),不在世界中時 left:falsejoin_failed、timeout(screen)、busy、not_allowed、stopped、no_client、unavailable、bad_args

stack:通過客戶端發送物品堆請求

形式回覆
{op:"stack", buf}發送後 {via:"raw", requestIds, map:{"<caller id>": client id}}
{op:"stack", view:true}{inventory, ui, open?:{containerId, type, items}},條目 {slot, item, count, stackId}
  • buf:一個 item_stack_request 數據包(id 147);其他 id 或無法解析的字節:bad_args。
  • 每個請求分配一個客戶端 id:奇數、負數,從 -0x40000001 遞減。引用調用方 id 的 stack id 會被改寫(本請求和此前 最近 256 個)。map 的鍵:字符串形式的調用方 id。
  • item_stack_response(tap)可能早於回覆到達:先保留。
  • 無客戶端預測。請求被接受:客戶端發送 inventory_transaction {type: inventory_mismatch},服務器重發背包。被拒絕: 無變化。
  • 必須打開所需界面(背包、箱子):action inventory、key E 或使用方塊。

timescale:遊戲速度

本遊戲託管的世界(單人、局域網主機)的速度:客戶端與本地服務端一起調整。請求(scale、paused)在兩側都以該速度 運行後回覆探測內容;探測還含 local、inLevel。

字段
scale0.05..20,超出截斷(1 = 20 tick/s)
paused兩側一起停止/恢復,同暫停菜單
sourcedefault、config(timeScale,進入世界時)、local(本接口)、server(BeDD 服務端)
tps{client, server}最近 2 s 的 tick/s;暫停為 0;遠程服務器上 server 為 null。本地實測:0.05→1、0.5→10、2→40、3→60、5→100、10→200、20→400
ticksPerSecond客戶端計時器基準速率(20)
effective{client, server}每側最近一次應用的 {scale, paused, ticksPerSecond}(此前為 null)

離開世界重置為 1;進入世界應用 timeScale(1)。錯誤:not_in_level、remote(不在本機託管)、server_led(由 BeDD 服務端設置:到那邊改)、timeout(之後仍會生效)、refused、bad_args。BeDD 服務端在變更時和玩家加入時發送 level_event 3007(SimTimeScale,pos 0,0,0,data = float 位模式按 int32,0 = 暫停);客戶端跟隨(0 暫停,保留 scale)。

誰發什麼(bedboo 傳輸 bedd)

所有服務端方向數據包由客戶端發送。bedboo 的 lib/bedd/authority.js 丟棄客戶端自有的數據包,把其他數據包映射到操作 (input、look、slot、button、action、settings、stack、join、key ESC),用 send 發送 text 和 command_request (後備:T/SLASH、text、ENTER),通過 ui 操作離開床和告示牌編輯界面,其他 sendAllow 中的 id 用 send 原樣發送, 其餘拋出 ERR_BEDD_UNSUPPORTED(BeddUnsupportedError)。

ui 操作

ui 把界面讀成帶 ref 的文本並對 ref 執行動作;oreui 返回原始 Ore UI DOM;mouse 可帶 ref。配置鍵 ui 控制這三者。 字段、回覆和錯誤:UI 樹。

input 操作

無配置鍵。遊戲線程派發輸入後回覆(timeoutMs 5000)。

操作字段(默認)
keykey 或 vk(1..254);action press|down|up(press);holdMs(60)字母、數字、ESC、ENTER、BACK、TAB、SPACE、SHIFT、CTRL、ALT、LEFT/UP/RIGHT/DOWN、HOME、END、PGUP、PGDN、INSERT、DELETE、F1..F24、SLASH、GRAVE、MINUS、EQUALS
texttext、delayMs(0)每個 UTF-16 單元一條 WM_CHAR
mouseaction move|down|up|click|wheel(move);x、y(客戶區像素)或 rx、ry(比例)或 ref;button left|right|middle(left);notches(1);holdMs(80,≥50);settleMs(50,先懸停);afterMs(50)移動虛擬光標,然後發 GameInput 按鍵/滾輪
raw同 mouse,另有 dx、dy(計數)、steps(1,分攤到多幀)鼠標被捕獲時轉動視角
release鬆開所有按住的鍵/按鈕(任何連接的)和 input 覆蓋
focus重發“獲得焦點”
windowclientWidth、clientHeight調整大小;回覆 result + 窗口字段;從不激活
flush等待所有已投遞的輸入派發完成
screenshotformat png|jpeg、quality、maxWidth(960)、region{x,y,w,h}{width, height, format, data}(base64)

系統操作

操作回覆
ping示例;窗口出現前 hwnd 為 0,managed = 已有管理器附加,audio = 聲音已打開,grabbed = 遊戲佔用了鼠標
state窗口字段、stats{synthetic, swallowedInput, swallowedFocus, passed, realForegroundSeen, api, gameInput, loopbackRebinds, serverQueriesRefused, audio, tapLoopback, send, journal, sendsRefused}、clients、held[{vk, client}]、hooks{name: installed}、mode ClientOnly|ClientServer|Remote、connection{connection none|local|remote, inLevel, host, port}
manageraction:"attach":本連接成為管理器;窗口保持在屏幕外,不出現在任務欄和 Alt+Tab。配置 manager(true):窗口出現後 30 s 內無人附加,或已附加的斷開,遊戲即關閉;false:隱藏,從不自行關閉。標籤:環境變量 BEDD_LABEL。回覆含 audio。僅限已附加的管理器:action:"audio",on true|false(在本機播放遊戲聲音;管理器斷開即關閉);action:"input",events[] 為 {t:"key", vk, down}、{t:"char", c}、{t:"move", x, y}、{t:"raw", dx, dy}、{t:"button", button, down}(left|right|middle|x1|x2)、{t:"wheel", notches}、{t:"release"}:用戶在管理窗口中控制遊戲時的輸入,不等待;回覆 {grabbed, audio}
close{ok},然後遊戲關閉

錯誤

code含義
disabled配置鍵為 false(key),或正在卸載
not_in_level不在世界中(服務端:未啟動)
refused遊戲拒絕(id 不在 sendAllow 中,releaseOn 無方塊)
timeouttimeoutMs 內未完成(之後可能完成)
bad_args字段值無效或字節無法解析
bad_request操作無法處理:二進制消息;tap/manager 未知 action 或無連接;ui 動作缺 ref、未知 action、setText 缺 text
internalui 動作的子操作回覆無法解析(bug)
busy界面打開、有更新的 look、join 進行中、tap 已被佔用
stopped正在卸載
join_failedjoin 放棄(客戶端顯示原因)
not_allowed非本機主機、loopbackOnly、不在 allowHosts 中(或含 "*" 時無法解析)
unavailable所需鉤子未安裝(見遊戲日誌)
no_client尚無客戶端實例
remote世界在非本機託管的服務器上
server_led速度由 BeDD 服務端主導
stale_ref、occluded、not_ready、unsupportedUI 樹錯誤

無 code:未知操作、不是 JSON 對象、輸入未及時派發、名稱無效(key、button)。

多個客戶端

窗口輸入類操作在各連接間串行;ping、state、manager、close、tap、world、look、input、settings、send、stack、 timescale、join、screenshot、oreui 和 ui 讀取不串行。斷開時:

按住的
key/mouse down鬆開(除非另一條連接重新按下)
button見上文
input 覆蓋運行到截止時間
管理器遊戲關閉

模組中的每連接狀態:代碼規範。

安全

能讀發現文件的人(該用戶的進程)都能驅動遊戲。令牌按常數時間比較。

  • send:以已登錄玩家身份發包,限於 sendAllow(默認 9 text、77 command_request、78 command_block_update、97 book_edit)。id 1、30、144、147 始終拒絕。 只為自己的服務器放寬。
  • sendAllow: "*":除客戶端自己發送的數據包外全部允許,排除:1 login、4 握手、8 資源包應答、23 tick_sync、30、 44 animate、45 respawn、75 show_credits、93 player_skin、94 subclient_login、113 set_local_player_as_initialized、115 network_stack_latency、129/135 客戶端緩存、144、147、152 emote_list、175 subchunk_request、193 request_network_settings、312 serverbound_loading_screen。其他包可能讓客戶端和服務器的狀態對不上(不是客戶端自己發的)。
  • stack:在任何服務器上移動、合成、丟棄和銷燬玩家的物品。
  • tap:整個會話。

關閉不需要的 allowSend、allowStack、tap;不要分享令牌。

服務端目標

--target_type=server(構建)在裝有 LeviLamina 的 BDS 中運行:同樣的 WebSocket, kind:"server",其他操作回覆 unknown op。

操作字段(默認)回覆
pingprotocol、pid、kind、target:"server"
statetarget、clients、loopbackOnly、stats{loopbackRebinds, serverQueriesRefused}
servertick、tps(2 s)、paused、port(server-port)、players[{name, xuid}]
timescalescale 0.05..20、paused、timeoutMs(5000);都不給 = 探測scale、paused、source default|config|local、tps{client:null, server}、ticksPerSecond(20 × scale,暫停為 0)
commandcommand(/ 可省略)、timeoutMs(10000)success、successCount、output[],同控制台

變更時及每個玩家生成時廣播 level_event 3007(見上)。tps.server 可能滯後於 ticksPerSecond。錯誤:not_in_level、 bad_args、timeout。