Протокол 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: повторить
- Заголовок: счётчик публикаций (не растёт = публикация остановилась), игровые часы движка, epoch (+1 за каждую партию), номер своего игрока, идёт ли партия, скорость игры, период публикации, сколько микросекунд ушло на сбор этой копии в игровом потоке, порядковый номер событий, время по этапам. Клиент может записать
requestedPeriodMs, чтобы запросить период публикации (16 ~ 1000 ms). - Юнит (112 байт): пара дескрипторов (опознавайте юниты по паре дескрипторов — адреса переиспользуются), четырёхсимвольный код типа, владелец, флаги, координаты, здоровье / мана (с максимумами), текущий приказ + цель приказа, текущая цель (кого юнит реально атакует), уровень героя / опыт / очки навыков, индекс деталей,
visibleTo(бит p = игрок p видит юнит прямо сейчас). - Детали (288 байт; герои > юниты игроков > крипы; до 256): 12 способностей (код / уровень / флаги / оставшаяся перезарядка в секундах), 8 кодов баффов, 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. Отправка команд
- Один клиентский объект занимает одну полосу: захватите
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 капитан компьютерного AI, 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; - на паузе часы движка стоят, но команды отдавать можно;
- у игры, запущенной свёрнутой, симуляция стоит (часы не идут).