Возможности интеграции Counter-Strike: Global Offensive

Привет! Я Вова, разработчик в Selectel. Люблю игры, но еще больше люблю влезать в их «внутренности». Мне стало интересно, как организован экспорт игрового состояния Counter-Strike в сторонние системы, например, для управления сценическим освещением.

За 20 лет развития Counter-Strike технологии сильно изменились. Крупные соревнования по CS проводятся на огромных стадионах, а количество выводимой на экраны информации зашкаливает. В этой статье я расскажу про то, как это работает, и покажу, как можно превратить телефон на Android в устройство вывода игрового состояния.

<i>Image by <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fwww.flickr.com%2Fphotos%2F72501769%40N03&postId=954550" rel="nofollow noreferrer noopener" target="_blank">andytb</a> under license <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fcreativecommons.org%2Flicenses%2Fby-sa%2F2.0%2F%3Fref%3Dccsearch%26amp%3Batype%3Drich&postId=954550" rel="nofollow noreferrer noopener" target="_blank">CC BY-SA 2.0</a></i>
Image by andytb under license CC BY-SA 2.0

Предыстория

Иногда я смотрю матчи с крупных соревнований по CS:GO. В трансляции за спинами команд видны больше экраны, отображающие вид с камеры игрока, а также состояние персонажа и его вооружение в реальном времени. Кроме этого, все сценическое освещение и огневые эффекты реагируют на текущее состояние раунда: мерцания прекращаются на время активной фазы.

Разработчик игры часто заинтересован в проведении соревнований такого масштаба, а это значит, что существуют непубличные инструменты, которые извлекают из игры необходимые данные и не привлекают внимание античит-системы.

<i>Сцена PGL Major Stockholm 2021 (источник <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Ddcov59mpYbQ%26amp%3Bab_channel%3DWePlayRU&postId=954550" rel="nofollow noreferrer noopener" target="_blank">youtube.com, пользователь WePlay RU</a>).</i>
Сцена PGL Major Stockholm 2021 (источник youtube.com, пользователь WePlay RU).

Разработчики часто готовы оказывать помощь, но не всегда хватает времени сделать это «по уму». Так, например, в 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": "<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": "<SteamID>", "clan": ".:WINX:.", "name": "Musa", "observer_slot": 6, "team": "T", "activity": "playing" }, "auth": { "token": "top-secret-token" } }

Теперь у нас есть минимальный прототип, который можно расширить.

Smart Link

<i>Сколько патронов в лежащем АК-47?</i>
Сколько патронов в лежащем АК-47?

Игра всегда отдает информацию о текущем игроке, и некоторые разделы игрового состояния содержат больше информации, чем отображено на экране. Так, в нижнем правом углу отображается боезапас текущего оружия, а неэкипированное снаряжение скрыто.

В пылу сражения патроны быстро заканчиваются, а выпавшее из противника оружие может быть шансом застать врага врасплох. Тем не менее, в игре необходимо поднять оружие и переключиться на него, чтобы понять доступный боезапас. Если количество патронов близко к нулю, то переключение обратно, на дополнительное оружие, может дать противнику тактическое преимущество.

Обратите внимание, что описанное далее может считаться техническим допингом и наказуемо на турнирах. Будьте честны!

Вот и простая, но практически применимая идея: отображать на экране телефона полное состояние вооружения игрока. Хотя я никогда не занимался разработкой под Android, такая задача выглядит как отличная разминка для мозга, а в случае недостатка оперативной памяти или места на накопителе — еще и для нервной системы.

Я пытался начать с MIT App Inventor — решения для визуального программирования. К счастью или сожалению, HTTP-сервер на Android — это достаточно редкий случай, поэтому пришлось вернуться к традиционному программированию: Android Studio и эмулятор.

С существующими Web-серверами не заладилось, но эту проблему я воспринял как повод вспомнить про формат HTTP и написать собственный парсер, тем более, игра не предъявляет строгих требований к HTTP-серверу.

<i>Шаблон главной и единственной Activity в приложении.</i>
Шаблон главной и единственной Activity в приложении.

Для прототипа я ограничился отображением трех видов вооружения:

  • тактического ножа;
  • дополнительного оружия (пистолет);
  • основного оружия (штурмовая или снайперская винтовка).

Самое верхнее текстовое поле отображает тег клана и имя игрока, текстовые поля около картинок — боезапас.

Для упрощения разработки иконки вооружения можно извлечь из файла iconlib.swf, который находится в ресурсах игры. Обратите внимание, что, согласно лицензионному соглашению, использовать эти файлы можно только в личных некоммерческих целях.

При стрельбе из пулемета приложение немного отстает и пропускает патроны, тем не менее, оно отображает больше информации, чем есть на экране игры.

Прототип хороший, API игры интересное, но есть ложка дегтя в бочке меда.

Подводные камни

<i>Детальная статистика игрока в записи матча.</i>
Детальная статистика игрока в записи матча.

Несмотря на достаточно большой объем информации, передаваемой игрой, некоторые моменты остаются неясными.

Так, с недавних пор Valve ввела короткий соревновательный режим, который длится до 9 побед в 16 раундах. На текущий момент через игровые интеграции нет возможности узнать, сколько нужно выиграть команде для победы в матче.

Можно утверждать, что матчи на соревнованиях всегда длинные, то есть до 16 побед в 30 раундах. Однако на соревнованиях обычно нет понятия «ничья», и матч продлевается на дополнительное время: еще 6 раундов, в которых нужно одержать 4 победы, что также не учитывается в выдаваемой информации.

Тем не менее, игра отдает поля, более подходящие для больших соревнований: например, количество побед в серии и необходимое количество выигранных матчей для победы. На соревнованиях победителя обычно выявляют по количеству побед в трех или пяти матчах (Bo3, Best of 3, или Bo5, Best of 5).

Также в игре есть подробная статистика матча по раундам для каждого игрока, но в сообщениях — только суммарная статистика за матч.

Заключение

Игровые интеграции можно использовать по-разному. Кто-то в крупной компании может вдохновиться идеей и сделать маленький турнир среди коллег с настоящим комментатором и красивой трансляцией в конференц-зале. Кто-то может развлечься и подсвечивать всю комнату белым светом умных ламп, когда игрока ослепляет световая граната. А может, что-то еще.

Исходный код приложения на Android я не выложу. Даже этот прототип — технический допинг, который дает мизерное, но преимущество. Такое должно быть получено собственным трудом, а не лежать в открытом доступе.

В конце ноября мы в Selectel проводим собственный игровой хакатон с призовым фондом 300 000 рублей. Предлагаем вам повеселиться при создании ретроигрушек и побороться за главный приз. Если технический допинг, то только такой.

2424
8 комментариев

Дожили, реклама Selectel и досюда добралась =/
Так погиб старый Habrahabr

2
Ответить

Мы просто пригласили людей на наш хакатон) Кажется, dtf — та самая площадка, где такое приглашение в тему.

1
Ответить

На сценах может сидеть световик с настроенным Touch Designer

Ответить
Комментарий удалён модератором

Чем посоветуете заняться?

Ответить
Комментарий удалён модератором

Нам тоже очень понравилось!

Ответить