WebSocket 接口
契約:bedd.asyncapi.yaml(spec/,AsyncAPI 3.0;Studio)。
連接
發現文件 %LOCALAPPDATA%\BeDD\run\<pid>.json(根目錄 BEDD_HOME),監聽期間存在;跳過已退出的 pid。
BeDD.exe ps --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"}| 項 | |
|---|---|
kind | client(遊戲)或 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 不設請求頭):
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(); };請求與回覆
{"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 列出它們:
{"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。實現。
| 操作 | 配置鍵 | 探測回覆 | 動作字段(默認) |
|---|---|---|---|
world | gameHooks | inLevel、levelEpoch(每進一個世界 +1)、topScreen、mouseGrabbed、showingMenu、inputMode mouse|touch|gamepad|undefined、player{runtimeId, eye{x,y,z}, rot{yaw,pitch}, slot}(null:無世界)、tick | 無 |
look | gameHooks | inLevel, yaw, pitch, tick | yaw、pitch、timeoutMs(1000*,≤60000) |
input | gameHooks | active、via hooks|keys、hooks{raw,state}、tick、applied、ticksLeft | move{x,z}、jump、sneak、sprint、ttlMs(300)或 ttlTicks |
slot | gameHooks | slot(null:無世界) | slot 0..8、timeoutMs(1000*) |
action | gameHooks | names | name drop|dropAll|inventory|perspective|pause|swing;hand main|off(swing);timeoutMs(1000*) |
button | gameHooks | held | button attack|use|pick;action down|up|click(click);holdTicks(click:2);releaseOn blockChange;timeoutMs(1000*) |
settings | gameHooks | renderDistance(區塊;第一次進世界前為 null)、inLevel | renderDistance(區塊,1..255) |
send | allowSend | allow(sendAllow 中的 id;"*" 時逐個列出)、allowAll | buf、timeoutMs(2000*,≤10000) |
stack | allowStack | enabled, via:"raw", stats{sent, answered, rejected, failed, resyncs} | buf、timeoutMs(2000*);或 view:true |
tap | tap | enabled、attached、link{state up|down, type remote|local, address}、journal{frames, bytes, complete}、lastReplay、replay、counters{in, out, dropped, sent, readers}、hooks | action:"subscribe" → tap 流 |
timescale | 無 | 見下 | scale、paused、timeoutMs(2000) |
join | 無 | mode、connection none|local|remote、host、port、inLevel、joining、last | host(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.9 | not_in_level、timeout(帶當前 yaw/pitch)、busy(有更新的 look)、bad_args |
input | move: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 tick | refused:未瞄準方塊 |
settings | renderDistance 在下一客戶端 tick 設置所有渲染器(普通、Vibrant Visuals、光線追蹤)的渲染距離(不寫入 options.txt),再調用 ClientInstance::updateChunkRadius:客戶端發 request_chunk_radius;服務器批准的半徑以 chunk_radius_update 到達(tap)。回覆 renderDistance(當前渲染值)、requested、tick | not_in_level、timeout、bad_args、unavailable |
send | buf: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:false | join_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。
| 字段 | |
|---|---|
scale | 0.05..20,超出截斷(1 = 20 tick/s) |
paused | 兩側一起停止/恢復,同暫停菜單 |
source | default、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)。
| 操作 | 字段(默認) | |
|---|---|---|
key | key 或 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 |
text | text、delayMs(0) | 每個 UTF-16 單元一條 WM_CHAR |
mouse | action 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 | 重發“獲得焦點” | |
window | clientWidth、clientHeight | 調整大小;回覆 result + 窗口字段;從不激活 |
flush | 等待所有已投遞的輸入派發完成 | |
screenshot | format 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} |
manager | action:"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 無方塊) |
timeout | timeoutMs 內未完成(之後可能完成) |
bad_args | 字段值無效或字節無法解析 |
bad_request | 操作無法處理:二進制消息;tap/manager 未知 action 或無連接;ui 動作缺 ref、未知 action、setText 缺 text |
internal | ui 動作的子操作回覆無法解析(bug) |
busy | 界面打開、有更新的 look、join 進行中、tap 已被佔用 |
stopped | 正在卸載 |
join_failed | join 放棄(客戶端顯示原因) |
not_allowed | 非本機主機、loopbackOnly、不在 allowHosts 中(或含 "*" 時無法解析) |
unavailable | 所需鉤子未安裝(見遊戲日誌) |
no_client | 尚無客戶端實例 |
remote | 世界在非本機託管的服務器上 |
server_led | 速度由 BeDD 服務端主導 |
stale_ref、occluded、not_ready、unsupported | UI 樹錯誤 |
無 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。
| 操作 | 字段(默認) | 回覆 |
|---|---|---|
ping | protocol、pid、kind、target:"server" | |
state | target、clients、loopbackOnly、stats{loopbackRebinds, serverQueriesRefused} | |
server | tick、tps(2 s)、paused、port(server-port)、players[{name, xuid}] | |
timescale | scale 0.05..20、paused、timeoutMs(5000);都不給 = 探測 | scale、paused、source default|config|local、tps{client:null, server}、ticksPerSecond(20 × scale,暫停為 0) |
command | command(/ 可省略)、timeoutMs(10000) | success、successCount、output[],同控制台 |
變更時及每個玩家生成時廣播 level_event 3007(見上)。tps.server 可能滯後於 ticksPerSecond。錯誤:not_in_level、
bad_args、timeout。