跳到正文
本頁目錄

控制管道協議

PowerShell 客戶端(BeDD.psm1)只是這個協議的一個客戶端;任何能打開命名管道的程序都能以同樣方式與遊戲通信。 Invoke-BeDD @{ op = '...' } 原樣發送一個請求。

請求與回覆

\\.\pipe\<pipeName>(僅本機,默認 BeDD),UTF-8,每個方向每行一個 JSON 對象。id 會原樣帶回。回覆是 {"ok":true,...} 或 {"ok":false,"error":"..."}。輸入類操作在遊戲線程分發完輸入後才回復(timeoutMs,默認 5000)。 協議版本(ping.protocol)保持為 1:較新的操作通過調用來探測(見下文)。

op字段
pingprotocol、pid、hwnd、client{w,h}、cursor{x,y}、realForeground、managed
stateping 的字段 + stats{synthetic, swallowedInput, swallowedFocus, passed, realForegroundSeen, api, gameInput, loopbackRebinds, serverQueriesRefused, audio, tapLoopback, send, journal}、clients(當前連接的控制管道客戶端數)、hooks{name: installed}(各功能安裝的每個遊戲鉤子;功能關閉時為 false)
keykey(名字)或 vk;action press|down|up;holdMs名字:字母、數字、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每個 UTF-16 單元一個 WM_CHAR
mouseaction move|down|up|click|wheel;x,y(客戶區像素)或 rx,ry(比例);button left|right|middle;notches;holdMs先移動虛擬光標(給了座標時),再在虛擬 GameInput 鼠標上按按鈕/滾輪
rawaction move|down|up|click|wheel;dx,dy(鼠標計數);steps;button;notches;holdMs虛擬 GameInput 鼠標:鼠標被鎖定時轉視角
release鬆開所有按住的鍵和按鈕(不管是誰按的),並結束 input 移動覆蓋
focus重新發送"獲得焦點"消息
windowclientWidth、clientHeight從不激活窗口;窗口留在屏幕外
flush等到所有已投遞的輸入都分發完

多個客戶端

可以同時連接多個客戶端(配置 pipeInstances,默認 4;1 = 一次一個),每個由自己的線程服務。投遞窗口輸入的操作是串行的, 所以兩個客戶端的按鍵或輸入的文字不會交錯;ping、state、tap、world、look、input、send 和 stack 不加這把鎖 (它們只讀,或把工作交給客戶端線程)。一個客戶端斷開(或其進程退出)時,釋放它持有的東西,且不動其他客戶端持有的: 它的按鍵和鼠標按鈕、它發起的 button 按住、它設置的 input 覆蓋。管理窗口通過管理連接轉發的操作既不串行,也不歸屬任何客戶端。

對有按客戶端狀態的單元(src/BeDD.h):currentPipeClient() 是當前線程所服務客戶端的 id(從 1 開始,不復用;管理連接和遊戲線程上為 0)。 用它標記狀態,並在 src/Pipe.cpp 的 clientGone() 裡釋放(像 clearMoveInputOf 和 releaseGameButtonsOf 那樣),或在 enable() 裡註冊 onPipeClientGone(fn):兩者都在離開的客戶端線程上運行,在它的按鍵和按鈕釋放之後、輸入操作串行的情況下(關閉時不運行)。

給 bedboo 用的操作(bedd 傳輸)

這些操作在對應配置項打開前都是關閉的,此時回覆 {"ok":false,"code":"disabled","key":"<key>"}。錯誤可能帶 code: disabled、not_in_level、refused、timeout、bad_args、busy。當前版本沒有的操作回覆 unknown op 'x'。 客戶端通過不帶參數調用每個操作一次來探測遊戲有什麼:unknown op = 沒有,code:"disabled" = 有但關著,ok = 有。 每個操作的無參數形式都是隻讀的。

op配置項無參數回覆執行形式
taptapenabled、pipe、attached(有讀取方連接)、link{state up|down, type remote|local, address}、journal{frames, bytes, complete}(當前加入日誌)、lastReplay(上一個讀取方回放了什麼)、replay(裝了日誌)、counters{in, out, dropped, sent, readers}、hooks(無)
worldgameHooksinLevel、levelEpoch(每進入一個世界 +1)、topScreen(如 hud_screen、pause_screen)、mouseGrabbed、showingMenu、inputMode mouse|touch|gamepad|undefined、player{runtimeId, eye{x,y,z}, rot{yaw,pitch}, slot}(不在世界裡時為 null)、tick(見到的客戶端刻數)(無)
lookgameHooks{inLevel, yaw, pitch, tick}yaw、pitch、timeoutMs(1000,最多 60000)
inputgameHooksactive、via hooks|keys、hooks{raw,state}、tick、appliedmove{x,z}、jump、sneak、sprint、ttlMs(300)
slotgameHooks{slot}(不在世界裡時為 null)slot 0..8
actiongameHooks{names}name drop|dropAll|inventory|perspective|pause
buttongameHooks{held}button attack|use|pick;action down|up|click;holdTicks;releaseOn blockChange
sendallowSend{allow}(sendAllow 裡的 id)buf、timeoutMs(2000,最多 10000)
stackallowStack{enabled, via:"raw", stats{sent, answered, rejected, failed, resyncs}}buf、timeoutMs(2000);或 view:true
  • world 是客戶端線程拍的快照:界面字段每幀更新(ClientInstance::$update 後置鉤子,因為暫停的單人世界不走刻),玩家字段每個客戶端刻更新。 管道線程只複製它。
  • look 在客戶端刻裡用 LocalPlayer::localPlayerTurn 轉動玩家,兩個角度都在 0.5 度以內時回覆 yaw、pitch、tick、turns。 角度與 player_auth_input 一致:yaw 0 = +Z,90 = -X;pitch -90 = 向上,像遊戲一樣限制在 ±89.9。localPlayerTurn(d) 按度 1:1 轉動, 軸互換({-dPitch, dYaw}),轉動在請求它的那一刻之後生效。錯誤:not_in_level、timeout(帶當前 yaw/pitch;暫停的單人世界不走刻)、 busy(被更新的 look 取代)、bad_args。
  • input 在遊戲自己的輸入管線裡移動本地玩家,所以每個 player_auth_input 都由客戶端自己構建。move 是 player_auth_input 的移動向量 (x = 左 - 右,z = 前 - 後,各自限制在 [-1, 1];斜向會被歸一化)。這個覆蓋是一個"死人開關":請求後 ttlMs(最多 60000)結束, 所以控制方想繼續移動時要不斷刷新;ttlMs:0、release 和所屬客戶端斷開會立即結束它。
    • via:"hooks":掛在 ClientInputUpdateSystem::extractRawHIDInput(寫 RawMoveInputComponent::mRawInput/mRawMove)和 inputHandlerUpdatePlayerState(在原函數前後寫 MoveInputComponent::mInputState/mRawInputState:MoveInputState 標誌 SneakDown=0、JumpDown=7、SprintDown=8、Up..Right=13..16 以及跳躍/潛行輸入邊沿 21..26,外加 mAnalogMoveVector)的後置鉤子。 只在鼠標被鎖定且沒有界面顯示時生效;與按鍵綁定無關。鍵盤處理只在按鍵事件時改這些標誌,所以覆蓋結束後的第一刻會把它們清零一次。 tick 計數 inputHandlerUpdatePlayerState 的調用,applied 計數寫入覆蓋的刻數。
    • via:"keys"(某個鉤子沒裝上):默認鍵位的虛擬鍵(W/A/S/D、SPACE、LSHIFT、LCTRL)按住到截止時間。它們像 key 一樣經過窗口, 所以也會到達打開的界面,改過的鍵位會讓它失效。
  • slot、action、button:管道線程把它們排隊,下一個客戶端刻執行後回覆(timeoutMs,默認 1000):{"ok":true,"tick":n,...}, 或 code not_in_level、busy(有界面打開;action pause 和 button up 仍可用)、timeout、bad_args、refused。 暫停的單人世界不走刻:在那裡 action pause 之後,用 key 操作(ESC)恢復。
    • slot:ClientInputCallbacks::handleSlotSelectButtonPress;它不改格子時(1.26.51 裡鍵盤玩家就是這樣)改用默認鍵位的快捷欄鍵 1..9。 客戶端自己發送 mob_equipment。回覆 slot 和 via callback|keys。
    • action:對應的 ClientInputCallbacks::handle*ButtonPress(背包用 CraftingType::Inventory)。
    • button:虛擬 GameInput 鼠標上的左/中/右鍵,由刻監聽器在 holdTicks 後鬆開(click 默認 2),或者帶 releaseOn:"blockChange" 時,在按下時客戶端命中結果指向的方塊改變、變成空氣或不再被指向時鬆開(沒有指向方塊時為 refused)。 up 立即生效。從不阻塞管道。回覆 held 和 block{x,y,z}。
  • send:buf 是一個服務端方向數據包的 base64(varint 頭 + 包體,與 bedrock-protocol 的序列化一致)。它在客戶端線程上重建 (MinecraftPackets::createPacket + readNoHeader,用客戶端 NetworkSystem 的 ReflectionCtx),並通過本地玩家發送(sendNetworkPacket), 遠程服務器和單人世界都一樣;發出後回覆 {packetId, name}。遊戲讀不完的字節為 bad_args,什麼都不發;不在 sendAllow 裡的 id 為 refused。 state 報告 stats.send{sent, refused, failed, lastError}。
  • stack:見下文。

安全

send 讓當前用戶下任何能打開管道的進程都能讓客戶端以登錄玩家身份發包,所以它默認關閉(allowSend),且限於 sendAllow (默認 text 9 和 command_request 77:聊天和命令,和玩家能輸入的一樣)。id 1(login)、30(inventory_transaction)、144(player_auth_input) 和 147(item_stack_request)即使列出也始終被拒絕:它們歸客戶端所有,偽造的包會干擾它的預測和記賬。只在你控制的服務器上放寬 sendAllow。 allowStack 打開時,任何能打開管道的程序都能在客戶端所在的任何服務器上移動、合成、丟棄和銷燬登錄玩家的物品。tap 包含登錄玩家的整個會話。 除非測試框架需要,三者都保持關閉;管理管道從不打開它們。

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

形式回覆
{op:"stack"}(探測){ok, enabled:true, via:"raw", stats:{sent, answered, rejected, failed, resyncs}}
{op:"stack", buf, timeoutMs=2000}{ok, via:"raw", requestIds:[客户端 id], map:{"<调用方 id>": 客户端 id}}
{op:"stack", view:true}{ok, inventory:[...], ui:[...], open?:{containerId, type, items:[...]}}:客戶端顯示的內容,每項 {slot, item, count, stackId}
  • buf 是一個完整 item_stack_request 包(id 147)的 base64,varint 頭加包體,與 bedrock-protocol 的序列化一致。其他包 id 以及遊戲讀不完的字節都是 bad_args。
  • 下一個客戶端刻用 MinecraftPackets::createPacket 和 readNoHeader 重建這個包,由本地玩家發送。
  • 請求 id:每個請求獲得一個新的客戶端 id,包發出後回覆。id 是奇數、負數,從 -0x40000001 往下數,遠低於客戶端自己的 -1, -3, ...。 每個指向調用方 id 的格子堆 id 也會改名,包括請求自己的 id(consume,或合成產物)以及同一個包或更早包裡更早的請求(記住最近 256 個 id)。 map 的鍵是調用方 id 的字符串形式。
  • 服務器在 item_stack_response 裡用客戶端 id 回答,bedboo 在 tap 上看到它。回答只能在包發出後到達,但可能先於 stack 的回覆被讀到, 所以調用方要把未匹配的回答保留到回覆到達。
  • via:"raw":請求不經過客戶端的 ItemStackNetManagerClient。這個遊戲版本里該管理器沒有可調用的符號(LeviLamina 頭文件裡每個成員都是 MCNAPI), 而且它的預測來自界面控制器移動物品,而不是請求動作。所以客戶端不會為這些請求做任何預測。
  • 重新同步:服務器接受其中一個請求時(掛鉤 ClientNetworkHandler::handle(ItemStackResponsePacket)),客戶端在下一刻發送 inventory_transaction {type: inventory_mismatch}。這是客戶端自己背包不同步時發的包,服務器會回發整個背包。客戶端隨後顯示服務器的狀態, 包括打開的容器。被拒絕的請求兩邊都不改變,所以不重新同步。
  • 服務器會檢查每個請求(堆 id、打開的界面、配方)。請求需要的界面(背包、箱子)必須已經在遊戲裡打開:用 action inventory、key E 或使用方塊打開。

誰發什麼(bedboo bedd 傳輸)

真實客戶端擁有每個服務端方向的數據包;bedboo 把它本來要發的東西變成意圖。此表與 bedboo 的 lib/bedd/authority.js 相同。

類別數據包處理
clientOwnedlogin、client_to_server_handshake、request_network_settings、resource_pack_client_response、client_cache_status、client_cache_blob_status、request_chunk_radius、subchunk_request、serverbound_loading_screen、set_local_player_as_initialized、network_stack_latency、tick_sync、player_skin、emote_list、respawn、player_action(respawn / dimension_change_ack / start_item_use_on / stop_item_use_on)、animate、interact mouse_over_entitybedboo 丟棄它們(計數):客戶端自己發。重生是用戶在客戶端上點"重生"
translatedplayer_auth_input(move_vector + 標誌 → input;yaw/pitch → look;block_actions start_break/abort_break/stop_break → button attack)、mob_equipment(slot)、inventory_transaction(item_use click_block/click_air、item_release、item_use_on_entity:先 look,再 button use/attack)、interact open_inventory(action inventory)、container_close(按鍵 ESC)、item_stack_request(stack)bedboo 的路由把它們變成操作
sharedtext、command_request通過 send 發送;後備:T 或 SLASH,然後 text,然後 ENTER
unsupportedbook_edit、block_pick_request、crafting_event、set_default_game_type,以及其他所有bedboo 拋出 BeddUnsupportedError(ERR_BEDD_UNSUPPORTED)

只有在 stack 探測不回答 ok 時,item_stack_request 才算 unsupported。

管理管道

\\.\pipe\BeDD-Manager-<会话 id>(僅當前用戶),由 BeDDManager.ps1 提供,和控制管道一樣是 JSON 行。 BEDD_MANAGER_PIPE 可以改用另一個管理窗口的管道(腳本和遊戲都讀它)。

  • 客戶端(每個連接一個請求):ping(games;關閉中時 ok 為 false)、adopt {pid, instance, label}(Start-BeDDGame.ps1 啟動遊戲後立即調用)、 list(games[]:pid、instance、label、pipe、hwnd、linked、managed、job、thumbnail)、 game {pid, action: close}、shutdown。
  • 遊戲(模塊):第一行是 hello {protocol, pid, hwnd, pipe, instance, label}(label 來自 BEDD_LABEL),回覆 {"ok":true}(正在關閉的管理窗口會直接掛斷;遊戲只有被歡迎後才算已連接)。連接保持打開,管理窗口在上面發送請求:每個控制管道操作 (它每秒輪詢一次 state),外加 close(向遊戲窗口發 WM_CLOSE)。沒有管理窗口的遊戲會啟動一個(配置 managerScript); 90 秒內沒有管理窗口連上、或者徹底失去連接的遊戲會自行關閉。