W3P 協定
執行環境和外部程式之間的全部契約:八塊共享記憶體、讀取世界狀態、讀取事件、下命令、回執、車道角色、畫板、介面與輸入。要用 Python 以外的語言接入,請看這一頁。
執行環境和外部程式之間只透過共享記憶體交換資料,下面這些就是全部。
- 參考實作是 Python 的
sdk/python/w3world.py(讀)和sdk/python/w3fast.py(寫),每個結構的大小和位移都寫在裡面,有測試釘住; - 協定只描述語意,和遊戲版本無關。 換遊戲版本時由執行環境自行適配,協定不變;新欄位只追加在區塊尾端,舊用戶端照常能用。
大多數人不需要讀這一頁 —— 用 Python SDK 就好。只有當你想用 C++ / C# / Rust / Go 等語言直接接入,或想知道 SDK 底下發生了什麼時,才需要它。
1. 八塊共享記憶體
<pid> 是遊戲的行程 ID。
| 名稱 | 方向 | 內容 | 同步方式 |
|---|---|---|---|
Local\War3World_<pid> | 執行環境 → 你 | 世界狀態:標頭 + 16 個玩家 + 最多 1024 個單位 + 256 份單位細節 + 256 個地上物品 + 擴充區 + 生產表 | seqlock |
Local\War3Trees_<pid> | 執行環境 → 你 | 最多 4096 個可破壞物(樹等),每 2 秒更新 | seqlock |
Local\War3Events_<pid> | 執行環境 → 你 | 事件環,8192 筆 | 每筆自帶序號 |
Local\War3Map_<pid> | 執行環境 → 你 | 地圖:地形格(128 一格,最多 256×256)+ 可玩區邊界 + 出生點;開局後幾秒內分批算完 | seqlock(算好後不再變動) |
Local\War3Fast_<pid> | 雙向 | 命令車道:16 條 × 16 槽;每槽一條命令 + 回執;每條車道帶有角色 | 每槽單一寫入者、單一讀取者 |
Local\War3Canvas_<pid> | 你 → 執行環境 | 畫板:標頭 64 位元組 + 256 個元素 × 112 位元組 + 64 KB 文字 / 點池;送出一次 canvas_enable 後才建立 | seqlock(你寫入,執行環境每幀讀取) |
Local\War3Msgs_<pid> | 執行環境 → 你 | 螢幕訊息環:遊戲提示、聊天、系統訊息的全文,128 筆 × 256 位元組 | 每筆自帶序號 |
Local\War3Input_<pid> | 雙向 | 介面與輸入:執行環境回寫滑鼠位置、指著的地面點、懸停項目;你寫入熱鍵表和滑鼠開關;送出一次 input_enable 後,執行環境才開始接管輸入 | 熱鍵表 seqlock |
好幾個用戶端同時使用畫板和輸入:這兩塊都只有一份,各寫各的會互相覆蓋。約定如下,自己寫用戶端也要照做:
- 畫板:持有具名互斥鎖
Local\War3CanvasMutex_<pid>進行讀取 - 修改 - 寫入,只換掉自己的元素,別人的原樣保留(重新排列池位移);擁有者行程已結束的和沒有擁有者的就清掉。元素的reserved[1]= 擁有者行程 ID、reserved[2]= 行程內序號;元素編號從區塊標頭位移 60 的計數器分配(從0x10000起)。 - 輸入:每個用戶端把自己的熱鍵和滑鼠開關登記在
Local\War3InputClients_<pid>(標頭 16 位元組 + 16 個用戶端 × 528 位元組),持有Local\War3InputMutex_<pid>改完自己那一筆,再把仍在執行的用戶端合併寫入輸入區塊:熱鍵依「鍵碼 + 修飾鍵」去除重複,滑鼠開關取聯集。事件發給所有用戶端,各自依「鍵碼 + 修飾鍵」認出自己的熱鍵。登記表裡還有其他仍在執行的用戶端時,不要送出input_enable 0。 - 執行環境:擁有者行程已結束的可點擊元素不再攔截點擊;每 2 秒檢查一次登記表,登記過的用戶端全都結束了,就把輸入區塊的熱鍵表和滑鼠開關清零。
2. 讀取世界狀態(seqlock)
loop:
s1 = block.seq (位移 8,int32)
if s1 是奇數: 重試 (執行環境正在寫入)
複製 標頭 + players + units[unitCount] + details[detailCount] + items[itemCount]
if block.seq != s1: 重試
- 標頭:發布計數(不增加 = 發布中斷了)、引擎遊戲時鐘、每局 +1 的 epoch、本方玩家編號、是否在局內、倍速、發布週期、這一份在遊戲執行緒上採集花費的微秒數、事件序號、分段耗時。用戶端可以寫入
requestedPeriodMs請求發布週期(16 ~ 1000 ms)。 - 單位(112 位元組):控制代碼對(用控制代碼對辨識單位,位址會被重複使用)、類型四字碼、擁有者、旗標、座標、生命 / 魔力(含上限)、目前訂單 + 訂單目標、任務目標(實際在打誰)、英雄等級 / 經驗 / 技能點、細節索引、
visibleTo(位元 p = 玩家 p 此刻看得見它)。 - 細節(288 位元組,英雄 > 玩家單位 > 野怪,最多 256 個):12 個技能(代碼 / 等級 / 旗標 / 剩餘冷卻秒數)、8 個 buff 代碼、6 格物品欄。
- 區塊尾端擴充(只追加、不移動前面的位移,舊用戶端照常能用):擴充區
EXT1(遊戲內時間、晝夜流速、生產表筆數)和生產表prods[128](正在訓練 / 研究 / 建造 / 升級的建築、佇列、總時長、已進行時間、是否卡住)。magic 對得上才使用。
3. 讀取事件
head = ring.writeSeq (位移 8)
for seq in (cursor, head]:
e = ring.events[(seq - 1) % 8192]
if e.seq > seq: 漏了一筆(讀太慢被覆寫)
elif e.seq != seq: 還沒寫完,下次再讀
else: 處理 e
事件結構 64 位元組:seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra。
- 比對相鄰兩次發布得出(精度 = 發布週期):
unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started; - 引擎層級(執行環境在遊戲執行緒上當場記下,每一下都有):
damage(來源、傷害類型、攻擊類型、位置、實際扣血、護甲前傷害)、killed(兇手); - 追蹤生產表得出:
production.done(完成的四字碼、類別、花了多少遊戲秒;對手的也會發); - 執行環境每次發布時順帶檢查:
spell.cast(技能開始冷卻:a技能四字碼、b等級、value冷卻秒數、x/y施法點)、player.left(a玩家編號、b新的槽位狀態)、selection.changed(本機玩家的選取,完整清單在世界區塊擴充區)、game.ended(離開對局); - 螢幕訊息:
message(a= 訊息序號,全文到共享記憶體Local\War3Msgs_<pid>裡查:128 筆 × 256 位元組,遊戲提示、聊天、系統訊息都在;b= 訊息框編號); - 介面與輸入(開啟
input_enable之後):ui.click(a畫板項目 id、b1 左鍵 / 2 右鍵)、ui.hover、hotkey(a熱鍵 id、b虛擬鍵碼)、mouse.world(x/y地面座標、value= 1 表示被吞掉了),修飾鍵都在extra。
4. 下命令
- 一個用戶端物件占用一條車道:持有
Local\War3FastMutex_<pid>找一條閒置(或擁有者行程已結束)的車道,寫入角色、玩家編號、自己的 pid。同一行程需要兩種角色就開兩條; - 填槽:語意命令旗標、操作碼、
args[11]、截止時間deadlineMs; - 所有槽寫完後標記提交,把車道的
submitSeq加 1; - 等待
Local\War3FastDone_<pid>_<lane>事件(或輪詢),讀取回執,歸還槽。
執行環境在遊戲執行緒的事件分派中批次執行:一次清空的時間預算為 4 ms(真實的高精度計時),超過就把剩下的留到下一次分派。過了截止時間的槽不會再執行 —— 不會出現「暫停恢復後舊命令又執行一遍」的情況。
args 索引:0..2 單位(位址、handle lo、handle hi)、3 訂單編號或四字碼、4..6 目標、7/8 x / y(float 位元)、9 extra(玩家編號 / 格位編號 / 開關 / 排隊位)、10 mode(0 無目標 / 1 對點 / 2 對目標)。
操作碼
| 操作碼 | 名稱 | 說明 |
|---|---|---|
| 1 | point | 單位對點下令(移動 / 攻擊移動 / 巡邏 / 攻擊地面 / 對點施法)。extra bit0 = 排隊(接在目前訂單後面) |
| 2 | target | 單位對目標下令(右鍵攻擊 / 採集 / 修理 / 對目標施法 / 撿物品);目標必須看得見 |
| 3 | immediate | 無目標命令(停止 / 原地待命 / 訓練 / 研究 / 升級 / 無目標施法) |
| 4 | build | 工人蓋建築(座標對齊 32) |
| 5 | learn | 英雄學技能 |
| 6 | use_item | 使用物品欄第 extra 格 |
| 7 | revive | 在祭壇復活英雄 |
| 8 | rally | 集結點(對點 / 對目標) |
| 9 | buy | 商店把物品賣給旁邊的英雄 |
| 10 | item_drop | 物品離手:給隊友、賣給商店(code = 接收的單位),或者丟在地上 |
| 20 ~ 25 | 查詢 | q_tech 科技計數、q_feasible 可行性、q_visible 可見性、q_mine_gold 金礦剩餘量、q_captain 電腦隊長、q_dead_heroes 陣亡英雄表 |
| 30 | pause | 暫停 / 繼續 |
| 40 ~ 50 | 鏡頭 | 讀取鏡頭狀態、設定欄位、看向一點、跟隨、重設、旋轉、邊界、平滑、介面顯示 / 隱藏、乾淨畫面、迷霧 |
| 60 ~ 63 | HUD | 任務按鈕文字、任務面板標題與描述、重新整理、讀取面板是否已開啟 |
| 70 | jass | 依名稱呼叫 JASS native(1291 個):名稱和字串參數放在槽的附加區,其餘參數依簽章放進 args;回傳值在 value[0]。只開放給本機工具車道;帶函式參數或會暫停腳本執行緒的一律拒絕。見 JASS 通道 |
| 71 / 72 | jass_handle_of / jass_unit_of | 快照裡的單位 ↔ JASS 控制代碼互換(快照裡的控制代碼對不是 JASS 控制代碼) |
| 73 | canvas_enable | 建立畫板共享記憶體、安裝繪製掛鉤;任何車道都能送出(畫板只畫在本機畫面上)。第一次要安裝掛鉤,逾時請設 2 秒以上 |
| 74 | input_enable | extra = 1 接管遊戲視窗的輸入(畫板項目點擊 / 懸停、熱鍵、地面點擊),0 = 交還。輸入區塊 Local\War3Input_<pid>:標頭 128 位元組 + 32 條熱鍵 × 16 位元組,你寫入熱鍵表和滑鼠開關,執行環境回寫滑鼠位置、指著的地面點、懸停項目。任何車道都能送出(只影響本機輸入)。見 介面與輸入 |
5. 回執
回執 52 位元組(+8 位元組耗時):status、engineReturn、verdict(被拒原因碼)、orderBefore / orderAfter(同一幀讀回的單位訂單)、value[8](查詢結果)、execUs(這一條在遊戲執行緒上執行了多少微秒)、engineUs(其中引擎下令函式本身)。
全部狀態碼和原因碼見 回執與原因碼。
6. 車道角色
| 角色 | 能做什麼 |
|---|---|
dev | 本機工具:語意命令(指揮本機玩家的單位)+ JASS 通道 |
player | 只能下語意命令,只能指揮車道所屬玩家的單位(別人的 = not_owner) |
observer | 只能查詢、操作鏡頭、讀取 HUD 面板狀態、開啟畫板和本機輸入;其他一律 forbidden |
兩個 AI 對打 = 同一局裡開兩條 player 車道(player 0 / player 1)。
本機模式下,角色由用戶端自行宣告(這是約定,不是安全邊界)。對戰平台 由裁判行程建立車道,只把 player 車道交給選手。
7. 實測過的語意
- 右鍵(smart)對著敵人 = 攻擊這一個(訂單目標、任務目標都是它);原始攻擊令走目標命令只會換上攻擊令、不記錄目標,會去打附近別的單位;
- 引擎不允許對看不見的單位下目標命令:天黑後遠處營地進入迷霧,右鍵一律被拒(1001);
- 建造「接下」只代表工人接了命令:樹林裡的點也會當場接下,工人走到才失敗;明顯被占用的點會當場拒絕;
- 英雄陣亡後約 3 遊戲秒才能復活;人口不夠也會被拒(英雄占人口);
- 物品欄裡的物品不算地上物品;撿起來會發
item.removed; - 暫停時引擎時鐘停止,但命令照常能下;
- 以最小化方式啟動的遊戲,模擬是停住的(時鐘不動)。