W3P プロトコル
ランタイムと外部プログラムの間の契約のすべて。8 つの共有メモリ、ワールド状態の読み取り、イベントの読み取り、コマンドの送信、レシート、レーンのロール、キャンバス、UI と入力を扱います。Python 以外の言語から接続する場合はこのページを読んでください。
ランタイムと外部プログラムは共有メモリだけでデータをやり取りします。以下がそのすべてです。
- リファレンス実装は Python の
sdk/python/w3world.py(読み取り)とsdk/python/w3fast.py(書き込み)です。各構造体のサイズとオフセットはここに定義されており、テストで固定されています。 - プロトコルが記述するのは意味だけで、ゲームのバージョンとは無関係です。 ゲームのバージョンが変わってもランタイム側が対応し、プロトコルは変わりません。新しいフィールドはブロックの末尾にのみ追加されるため、古いクライアントもそのまま動きます。
ほとんどの人はこのページを読む必要はありません —— Python SDK を使えば十分です。C++ / C# / Rust / Go などの言語から直接接続したい場合や、SDK の下で何が起きているかを知りたい場合にだけ必要になります。
1. 8 つの共有メモリ
<pid> はゲームのプロセス ID です。
| 名前 | 方向 | 内容 | 同期方式 |
|---|---|---|---|
Local\War3World_<pid> | ランタイム → あなた | ワールド状態:ヘッダー + 16 プレイヤー + 最大 1024 ユニット + 256 件のユニット詳細 + 256 個の地面のアイテム + 拡張領域 + 生産テーブル | seqlock |
Local\War3Trees_<pid> | ランタイム → あなた | 最大 4096 個の破壊可能オブジェクト(木など)、2 秒ごとに更新 | seqlock |
Local\War3Events_<pid> | ランタイム → あなた | イベントリング、8192 件 | 各エントリが自身のシーケンス番号を持つ |
Local\War3Map_<pid> | ランタイム → あなた | マップ:地形グリッド(1 マス 128、最大 256×256)+ プレイ可能領域の境界 + スタート地点。試合開始後、数秒かけて分割して計算 | seqlock(計算完了後は変化しない) |
Local\War3Fast_<pid> | 双方向 | コマンドレーン:16 レーン × 16 スロット。各スロットにコマンド 1 件 + レシート。各レーンはロールを持つ | スロットごとに単一ライター・単一リーダー |
Local\War3Canvas_<pid> | あなた → ランタイム | キャンバス:ヘッダー 64 バイト + 256 要素 × 112 バイト + 64 KB のテキスト / 点プール。canvas_enable を 1 回送ってから作成されます | seqlock(あなたが書き、ランタイムが毎フレーム読む) |
Local\War3Msgs_<pid> | ランタイム → あなた | 画面メッセージのリング:ゲームのヒント、チャット、システムメッセージの全文。128 件 × 256 バイト | 各エントリが自身のシーケンス番号を持つ |
Local\War3Input_<pid> | 双方向 | UI と入力:ランタイムがマウス位置、カーソルが指している地面の地点、ホバー中の要素を書き戻し、あなたはホットキー表とマウスのオン・オフを書き込みます。input_enable を 1 回送ってから、ランタイムが入力の横取りを始めます | ホットキー表は seqlock |
複数のクライアントが同時にキャンバスと入力を使う場合:この 2 つのブロックはどちらも 1 つしかなく、各自がばらばらに書くと互いに上書きしてしまいます。取り決めは次のとおりで、自作のクライアントもこれに従ってください。
- キャンバス:名前付きミューテックス
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、自分のプレイヤー番号、試合中かどうか、ゲーム速度、発行周期、この 1 回分をゲームスレッド上で収集するのにかかったマイクロ秒数、イベントのシーケンス番号、区間ごとの所要時間。クライアントは
requestedPeriodMsに書き込んで発行周期(16 ~ 1000 ms)をリクエストできます。 - ユニット(112 バイト):ハンドルペア(ユニットはハンドルペアで識別します。アドレスは再利用されます)、型の 4 文字コード、所有者、フラグ、座標、HP / マナ(上限を含む)、現在のオーダー + オーダーのターゲット、タスクターゲット(実際に攻撃している相手)、ヒーローのレベル / 経験値 / スキルポイント、詳細のインデックス、
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: 1 件取りこぼした(読むのが遅くて上書きされた)
elif e.seq != seq: まだ書き込み途中。次回読む
else: e を処理
イベント構造体は 64 バイトです:seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra。
- 連続する 2 回の発行を比較して得られるもの(精度 = 発行周期):
unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started。 - エンジンレベル(ランタイムがゲームスレッド上でその場で記録するため、1 発ごとに発生):
damage(ダメージ元、ダメージタイプ、攻撃タイプ、位置、実際に減った HP、アーマー適用前のダメージ)、killed(倒したユニット)。 - 生産テーブルの追跡から得られるもの:
production.done(完了した 4 文字コード、種別、かかったゲーム秒数。相手のものも発行されます)。 - ランタイムが発行のたびにあわせてチェックするもの:
spell.cast(スキルのクールダウン開始:aスキルの 4 文字コード、bレベル、valueクールダウン秒数、x/y詠唱地点)、player.left(aプレイヤー番号、b新しいスロット状態)、selection.changed(ローカルプレイヤーの選択。全リストはワールドブロックの拡張領域にある)、game.ended(試合から退出)。 - 画面メッセージ:
message(a= メッセージのシーケンス番号。全文は共有メモリLocal\War3Msgs_<pid>で引く:128 件 × 256 バイトで、ゲームのヒント、チャット、システムメッセージがすべて入っている。b= メッセージ枠の番号)。 - UI と入力(
input_enableを有効にした後):ui.click(aキャンバス要素の id、b1 左クリック / 2 右クリック)、ui.hover、hotkey(aホットキーの id、b仮想キーコード)、mouse.world(x/y地面の座標、value= 1 はゲームに渡さず握りつぶしたことを示す)。修飾キーはいずれもextraに入る。
4. コマンドの送信
- クライアントオブジェクト 1 つがレーン 1 本を占有します:
Local\War3FastMutex_<pid>を保持した状態で、空いている(または所有プロセスが死んでいる)レーンを探し、ロール、プレイヤー番号、自分の pid を書き込みます。同じプロセスで 2 種類のロールが必要なら 2 本開きます。 - スロットを埋めます:セマンティックコマンドのフラグ、オペコード、
args[11]、期限deadlineMs。 - すべてのスロットを書き終えたらコミット済みにし、レーンの
submitSeqを 1 増やします。 Local\War3FastDone_<pid>_<lane>イベントを待ち(またはポーリングし)、レシートを読んで、スロットを返却します。
ランタイムはゲームスレッドのイベントディスパッチ内でまとめて実行します。1 回の処理の時間予算は 4 ms(実時間の高精度タイマー)で、超えた分は次のディスパッチに持ち越されます。期限を過ぎたスロットは実行されません —— 「一時停止から再開したら古いコマンドがもう一度実行された」ということは起きません。
args のインデックス:0..2 ユニット(アドレス、ハンドル lo、ハンドル hi)、3 オーダー ID または 4 文字コード、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 | カメラ | カメラ状態の読み取り、フィールドの設定、地点を見る、追従、リセット、回転、境界、スムージング、UI の表示切替、クリーンな画面、霧 |
| 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 バイト。あなたはホットキー表とマウスのオン・オフを書き込み、ランタイムはマウス位置、カーソルが指している地面の地点、ホバー中の要素を書き戻します。どのレーンからでも送れます(影響するのはローカルの入力だけです)。UI と入力 を参照 |
5. レシート
レシートは 52 バイト(+ 所要時間 8 バイト)です:status、engineReturn、verdict(拒否の理由コード)、orderBefore / orderAfter(同じフレームで読み戻したユニットのオーダー)、value[8](クエリ結果)、execUs(このコマンドがゲームスレッド上で実行に要したマイクロ秒数)、engineUs(そのうちエンジンの命令関数そのものにかかった時間)。
すべてのステータスコードと理由コードは レシートと理由コード を参照してください。
6. レーンのロール
| ロール | できること |
|---|---|
dev | ローカルツール:セマンティックコマンド(ローカルプレイヤーのユニットを指揮)+ JASS チャネル |
player | セマンティックコマンドのみ。レーンが属するプレイヤーのユニットだけを指揮できます(他人のユニット = not_owner) |
observer | クエリ、カメラ、HUD パネル状態の読み取り、キャンバスとローカル入力の有効化のみ。それ以外はすべて forbidden |
AI 同士の対戦 = 同じ試合で player レーンを 2 本開きます(player 0 / player 1)。
ローカルモードでは、ロールはクライアントが自己申告します(取り決めであり、セキュリティ境界ではありません)。アリーナ ではレフェリープロセスがレーンを作成し、選手には player レーンだけを渡します。
7. 実測済みのセマンティクス
- 敵への右クリック(smart)= その 1 体を攻撃します(オーダーのターゲットもタスクターゲットもそのユニット)。生の攻撃オーダーをターゲットコマンドで送ると、攻撃オーダーに切り替わるだけでターゲットは記憶されず、近くの別の敵を攻撃しに行きます。
- エンジンは見えないユニットへのターゲットコマンドを許可しません:夜になって遠くのキャンプが戦場の霧に入ると、右クリックはすべて拒否されます(1001)。
- 建設が「受理」されたというのは、ワーカーが命令を受けたというだけです:木立の中の地点でもその場で受理され、ワーカーが到着してから失敗します。明らかに埋まっている地点はその場で拒否されます。
- ヒーローは死亡後約 3 ゲーム秒経たないと復活できません。人口が足りない場合も拒否されます(ヒーローも人口を使います)。
- インベントリ内のアイテムは地面のアイテムに含まれません。拾うと
item.removedが発行されます。 - 一時停止中はエンジンクロックが止まりますが、コマンドは通常どおり送れます。
- 最小化した状態で起動したゲームはシミュレーションが止まっています(クロックが進みません)。