文件 參考

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、b 1 左鍵 / 2 右鍵)、ui.hover、hotkey(a 熱鍵 id、b 虛擬鍵碼)、mouse.world(x/y 地面座標、value = 1 表示被吞掉了),修飾鍵都在 extra。

4. 下命令

  1. 一個用戶端物件占用一條車道:持有 Local\War3FastMutex_<pid> 找一條閒置(或擁有者行程已結束)的車道,寫入角色、玩家編號、自己的 pid。同一行程需要兩種角色就開兩條;
  2. 填槽:語意命令旗標、操作碼、args[11]、截止時間 deadlineMs;
  3. 所有槽寫完後標記提交,把車道的 submitSeq 加 1;
  4. 等待 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 對目標)。

操作碼

操作碼名稱說明
1point單位對點下令(移動 / 攻擊移動 / 巡邏 / 攻擊地面 / 對點施法)。extra bit0 = 排隊(接在目前訂單後面)
2target單位對目標下令(右鍵攻擊 / 採集 / 修理 / 對目標施法 / 撿物品);目標必須看得見
3immediate無目標命令(停止 / 原地待命 / 訓練 / 研究 / 升級 / 無目標施法)
4build工人蓋建築(座標對齊 32)
5learn英雄學技能
6use_item使用物品欄第 extra 格
7revive在祭壇復活英雄
8rally集結點(對點 / 對目標)
9buy商店把物品賣給旁邊的英雄
10item_drop物品離手:給隊友、賣給商店(code = 接收的單位),或者丟在地上
20 ~ 25查詢q_tech 科技計數、q_feasible 可行性、q_visible 可見性、q_mine_gold 金礦剩餘量、q_captain 電腦隊長、q_dead_heroes 陣亡英雄表
30pause暫停 / 繼續
40 ~ 50鏡頭讀取鏡頭狀態、設定欄位、看向一點、跟隨、重設、旋轉、邊界、平滑、介面顯示 / 隱藏、乾淨畫面、迷霧
60 ~ 63HUD任務按鈕文字、任務面板標題與描述、重新整理、讀取面板是否已開啟
70jass依名稱呼叫 JASS native(1291 個):名稱和字串參數放在槽的附加區,其餘參數依簽章放進 args;回傳值在 value[0]。只開放給本機工具車道;帶函式參數或會暫停腳本執行緒的一律拒絕。見 JASS 通道
71 / 72jass_handle_of / jass_unit_of快照裡的單位 ↔ JASS 控制代碼互換(快照裡的控制代碼對不是 JASS 控制代碼)
73canvas_enable建立畫板共享記憶體、安裝繪製掛鉤;任何車道都能送出(畫板只畫在本機畫面上)。第一次要安裝掛鉤,逾時請設 2 秒以上
74input_enableextra = 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;
  • 暫停時引擎時鐘停止,但命令照常能下;
  • 以最小化方式啟動的遊戲,模擬是停住的(時鐘不動)。