文档 参考

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、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 单位(地址、句柄 lo、句柄 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;
  • 暂停时引擎时钟停住,但命令照常能下;
  • 最小化起的游戏模拟是停着的(时钟不动)。