Возможности интеграции Counter-Strike: Global Offensive
Image by andytb under license CC BY-SA 2.0
Counter-Strike: Global Offensive — современная версия старой доброй «CS 1.6». За двадцать лет развития серии технологии сильно изменились. Ранее соревнования по CS проходили в конференц-залах, а информация, доступная наблюдателю, была крайне скудна. Сейчас крупные соревнования по Counter-Strike проводятся на огромных стадионах, а количество выводимой на экраны информации зашкаливает.
Мне стало интересно, как организован экспорт игрового состояния в сторонние системы, например, для управления сценическим освещением. В этой статье я расскажу про то, как это работает, и покажу, как можно превратить телефон на Android в устройство вывода игрового состояния.
Предыстория
Сцена PGL Major Stockholm 2021 (источник youtube.com, пользователь WePlay RU)
Иногда я смотрю матчи с крупных соревнований по CS: GO. В трансляции за спинами команд видны больше экраны, отображающие вид с камеры игрока, а также состояние персонажа и его вооружение в реальном времени. Кроме этого, все сценическое освещение и огневые эффекты реагируют на текущее состояние раунда: мерцания прекращаются на время активной фазы.
Разработчик игры часто заинтересован в проведении соревнований такого масштаба, а это значит, что существуют непубличные инструменты, которые извлекают из игры необходимые данные и не привлекают внимание античит-системы.
Разработчики часто готовы оказывать помощь, но не всегда хватает времени сделать «по уму». Так, например, в League of Legends не было отдельной роли наблюдателя, от которой ведется трансляция соревнования. На первое время ввели дополнительного невидимого игрока с открытой картой у одной из команд. Изящное решение, но, как только противник применял умение, наносящее урон всем игрокам команды, наблюдатель умирал.
Я долго откладывал погружение в тему, так как полагал, что у разработчиков нет интереса выкладывать инструменты для экспорта состояния, а писать собственное решение для онлайн-игр с античитом — прямая дорога в бан. Началось все с того, что мне подарили клавиатуру, совместимую с технологией RGB-подсветки Razer Chroma.
Изучая возможности клавиатуры, я обнаружил поддержку плагинов, среди которых был плагин для CS: GO. Это расширение подсвечивало клавиши 1–5 в зависимости от наличия соответствующих видов оружия, а блок клавиш F9-F12 превращался в своеобразную шкалу здоровья и брони.
Это не давало какого-то преимущества перед другими игроками, так как это всего лишь другое отображение представленной на экране информации, но античиту такие тонкости не объяснишь, пришлось разбираться, как работает плагин.
По запросу в Google был найден официальный ответ Valve: Counter-Strike: Global Offensive Game State Integration. Информация там не полная, но пользователь Reddit под ником Bkid провел собственное исследование API и написал подробный пост с объяснением многих полей, передаваемых игрой.
Как это работает
Экспорт состояния игры возможен ее собственными средствами. Разработчики выбрали красивое решение: указать в файлах конфигурации все заинтересованные сервисы, а игра будет самостоятельно рассылать игровое состояние.
Данный способ использует официальные средства и не требует вмешательства в память процесса игры. Это значит, что VAC-бан получить невозможно.
По умолчанию игровые интеграции отсутствуют, но добавить собственный сервис очень просто. Находим каталог Steam, далее находим каталог с конфигурационными файлами CS: GO. В моем случае путь выглядит так:
C:\Program Files (x86)\Steam\steamapps\common\Counter-Strike Global Offensive\csgo\cfg\
В указанном каталоге создаем текстовый файл с именем gamestate_integration_%SERVICENAME%.cfg. Обратите внимание на следующие ограничения:
- имя файла должно начинаться на gamestate_integration_;
- имя файла должно заканчиваться на .cfg.
Несоблюдение этих правил приведет к игнорированию игрой файла конфигурации. Рассмотрим файл конфигурации:
"Observer Players"
{
"uri" "http://10.0.1.3:8080/csgo/all"
"timeout" "5.0"
"buffer" "0.1"
"throttle" "0.1"
"heartbeat" "30.0"
"auth"
{
"token" "top-secret-token"
}
"data"
{
// Доступно игроку и наблюдателям
"provider" "1"
"map" "1"
"round" "1"
"player_id" "1"
"player_state" "1"
// Доступно только наблюдателям
"allplayers_id" "1"
"allplayers_state" "1"
"allplayers_match_stats" "1"
"allplayers_weapons" "1"
"allplayers_position" "1"
"phase_countdowns" "1"
"allgrenades" "1"
}
}
Первая строка задает имя сервиса, это больше похоже на комментарий, чем на действительно важную информацию. Рассмотрим поля, необходимые для подписки на рассылки игры:
- uri — адрес для рассылки. Поддерживаются схемы HTTP и HTTPS;
- timeout — в течение этого времени игра ожидает подтверждение получения рассылки;
- buffer — время, в течение которого игра буферизирует игровые события для отправки одним сообщением. Если значение равно 0, то буферизация будет отключена и игра будет отправлять информацию о каждом событии в отдельном сообщении;
- throttle — время, которое должно пройти между последним получением HTTP OK и отправкой следующего пакета;
- heartbeat — если в игре не произойдет ни одного события за это время, она вышлет полное состояние в качестве keep-alive пакета.
Далее немного безопасности: секция auth позволяет задать поле token. Заданная строка будет передаваться в каждом сообщении от игры. Это позволяет защититься от нежелательных пакетов. Лучше всего использовать это вместе с HTTPS.
В секции data указывается, какая информация интересует сервис. Доступные параметры могут меняться с развитием игры. Обратите внимание, что некоторые параметры доступны только зрителям и в режиме игры будут игнорироваться.
В сообщениях могут передаваться большие наборы данных, а недоступные поля и поля без значений в сообщениях опускаются. Для удобства обработки информации в сообщениях есть поля updated и added. Первое поле содержит старую версию значений, а второе — отмечает, какие поля появились или исчезли по сравнению с предыдущим сообщением.
Эти два поля условно можно называть «дельтой». Игра отправляет дельты только тем, кто дает корректные ответы на POST-запросы. Согласно документации, достаточно отправить ответ с кодом HTTP 2XX.
Теперь, когда мы знаем теорию, перейдем к практике и создадим собственный сервер.
Сервер сервиса
В качестве наиболее простого варианта запустим HTTP-сервер с помощью Python 3.7.4 и доступных библиотек. Документация и пост на Reddit достаточно старые, поэтому для начала просто выведем на экран информацию, которую предоставляет нам игра.
Напишем простой обработчик, унаследованный от класса BaseHTTPRequestHandler из пакета http.server:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class RequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers["Content-Length"])
body = self.rfile.read(length).decode("utf-8")
payload = json.loads(body)
print(json.dumps(payload, indent=4, ensure_ascii=True))
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.end_headers()
Мы не передаем никаких данных игре в ответе, поэтому разумно выбрать код 204 No Content. Несмотря на отсутствие данных, необходимо выставить заголовок Content-Type, иначе игра посчитает ответ некорректным и будет раз за разом повторять отправку данных. При малом значении buffer это может вызывать отказ в обслуживании, особенно на слабых устройствах.
Согласно параграфу 7.2.1 в RFC 2616 заголовок Content-Type должен отправляться, если в сообщении есть тело ответа. У нас его нет, но спорить с игрой нецелесообразно.
Запускаем HTTP-сервер:
addr = ('0.0.0.0', 9000)
server = HTTPServer(addr, RequestHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
server.server_close()
Запускаем игру, включаем любую трансляцию GO TV и, в зависимости от указанных настроек в конфигурационном файле, получаем состояние игры. На момент написания статьи информация из поста на Reddit актуальна, за исключением некоторых мелочей, которые я упомяну в конце.
{
"provider": {
"name": "Counter-Strike: Global Offensive",
"appid": 730,
"version": 13807,
"steamid": "",
"timestamp": 1635963515
},
"round": {
"phase": "live",
"bomb": "planted"
},
"map": {
"mode": "casual",
"name": "de_dust2",
"phase": "live",
"round": 0,
"team_ct": {
"score": 0,
"consecutive_round_losses": 0,
"timeouts_remaining": 1,
"matches_won_this_series": 0
},
"team_t": {
"score": 0,
"name": "..::WINX::..",
"consecutive_round_losses": 0,
"timeouts_remaining": 1,
"matches_won_this_series": 0
},
"num_matches_to_win_series": 0,
"current_spectators": 0,
"souvenirs_total": 0
},
"player": {
"match_stats": {
"kills": 0,
"assists": 0,
"deaths": 0,
"mvps": 0,
"score": 2
},
"weapons": {
"weapon_0": {
"name": "weapon_knife_t",
"paintkit": "default",
"type": "Knife",
"state": "holstered"
},
"weapon_1": {
"name": "weapon_glock",
"paintkit": "default",
"type": "Pistol",
"ammo_clip": 20,
"ammo_clip_max": 20,
"ammo_reserve": 120,
"state": "active"
}
},
"state": {
"health": 100,
"armor": 100,
"helmet": true,
"flashed": 0,
"smoked": 0,
"burning": 0,
"money": 1200,
"round_kills": 0,
"round_killhs": 0,
"equip_value": 1200
},
"steamid": "",
"clan": ".:WINX:.",
"name": "Musa",
"observer_slot": 6,
"team": "T",
"activity": "playing"
},
"auth": {
"token": "top-secret-token"
}
}
Теперь у нас есть минимальный прототип, который можно расширить.
Smart Link
Сколько патронов в лежащем АК-47?
Игра всегда отдает информацию о текущем игроке, и некоторые разделы игрового состояния содержат больше информации, чем отображено на экране. Так, в нижнем правом углу отображается боезапас текущего оружия, а неэкипированное снаряжение скрыто.
В пылу сражения патроны быстро заканчиваются, а выпавшее из противника оружие может быть шансом застать врага врасплох. Тем не менее, в игре необходимо поднять оружие и переключиться на него, чтобы понять доступный боезапас. Если количество патронов близко к нулю, то переключение обратно, на дополнительное оружие, может дать противнику тактическое преимущество.
Обратите внимание, что описанное далее может считаться техническим допингом и наказуемо на турнирах. Будьте честны!
Вот и простая, но практически применимая идея: отображать на экране телефона полное состояние вооружения игрока. Хотя я никогда не занимался разработкой под Android, такая задача выглядит как отличная разминка для мозга, а в случае недостатка оперативной памяти или места на накопителе — еще и для нервной системы.
Я пытался начать с MIT App Inventor — решения для визуального программирования. К счастью или сожалению, HTTP-сервер на Android — это достаточно редкий случай, поэтому пришлось вернуться к традиционному программированию: Android Studio и эмулятор.
С существующими Web-серверами не заладилось, но эту проблему я воспринял как повод вспомнить про формат HTTP и написать собственный парсер, тем более, игра не предъявляет строгих требований к HTTP-серверу.
Шаблон главной и единственной Activity в приложении.
Для прототипа я ограничился отображением трех видов вооружения:
- тактического ножа;
- дополнительного оружия (пистолет);
- основного оружия (штурмовая или снайперская винтовка).
Самое верхнее текстовое поле отображает тег клана и имя игрока, текстовые поля около картинок — боезапас.
Для упрощения разработки иконки вооружения можно извлечь из файла iconlib.swf, который находится в ресурсах игры. Обратите внимание, что, согласно лицензионному соглашению, использовать эти файлы можно только в личных некоммерческих целях.
При стрельбе из пулемета приложение немного отстает и пропускает патроны, тем не менее, оно отображает больше информации, чем есть на экране игры.
Прототип хороший, API игры интересное, но есть ложка дегтя в бочке меда.
Подводные камни
Детальная статистика игрока в записи матча
Несмотря на достаточно большой объем информации, передаваемой игрой, некоторые моменты остаются неясными.
Так, с недавних пор Valve ввела короткий соревновательный режим, который длится до 9 побед в 16 раундах. На текущий момент через игровые интеграции нет возможности узнать, сколько нужно выиграть команде для победы в матче.
Можно утверждать, что матчи на соревнованиях всегда длинные, то есть до 16 побед в 30 раундах. Однако на соревнованиях обычно нет понятия «ничья», и матч продлевается на дополнительное время: еще 6 раундов, в которых нужно одержать 4 победы, что также не учитывается в выдаваемой информации.
Тем не менее, игра отдает поля, более подходящие для больших соревнований: например, количество побед в серии и необходимое количество выигранных матчей для победы. На соревнованиях победителя обычно выявляют по количеству побед в трех или пяти матчах (Bo3, Best of 3, или Bo5, Best of 5).
Также в игре есть подробная статистика матча по раундам для каждого игрока, но в сообщениях — только суммарная статистика за матч.
Заключение
Игровые интеграции можно использовать по-разному. Кто-то в крупной компании может вдохновиться идеей и сделать маленький турнир среди коллег с настоящим комментатором и красивой трансляцией в конференц-зале. Кто-то может развлечься и подсвечивать всю комнату белым светом умных ламп, когда игрока ослепляет световая граната. А может, что-то еще.
Исходный код приложения на Android я не выложу. Даже этот прототип — технический допинг, который дает мизерное, но преимущество. Такое должно быть получено собственным трудом, а не лежать в открытом доступе.
Кстати, в конце ноября мы в Selectel проводим собственный игровой хакатон. Предлагаем вам повеселиться при создании ретро-игрушек и побороться за главный приз. Если технический допинг, то только такой.