W3P 协议
运行时和外部程序之间的全部契约:八块共享内存、读世界状态、读事件、下命令、回执、车道角色、画板、界面与输入。用 Python 以外的语言接入,看这一页。
运行时和外部程序之间只通过共享内存交换数据,下面这些就是全部。
- 参考实现是 Python 的
sdk/python/w3world.py(读)和sdk/python/w3fast.py(写),每个结构的大小和偏移都写在里面,有测试钉住; - 协议只描述语义,和游戏版本无关。 换游戏版本时运行时自己适配,协议不变;新字段只追加在块尾,老客户端照常能用。
大多数人不需要读这一页 —— 用 Python SDK 就好。只有当你想用 C++ / C# / Rust / Go 等语言直接接入,或者想知道 SDK 底下发生了什么时,才需要它。
1. 八块共享内存
<pid> 是游戏进程号。
| 名字 | 方向 | 内容 | 同步方式 |
|---|---|---|---|
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]= 主人进程号、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 单位(地址、句柄 lo、句柄 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; - 暂停时引擎时钟停住,但命令照常能下;
- 最小化起的游戏模拟是停着的(时钟不动)。