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

mhamr9-n7oeqbaf2vv7crejx0ty.jpeg

Image by andytb under license CC BY-SA 2.0
Counter-Strike: Global Offensive — современная версия старой доброй «CS 1.6». За двадцать лет развития серии технологии сильно изменились. Ранее соревнования по CS проходили в конференц-залах, а информация, доступная наблюдателю, была крайне скудна. Сейчас крупные соревнования по Counter-Strike проводятся на огромных стадионах, а количество выводимой на экраны информации зашкаливает.

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

Предыстория


image-loader.svg

Сцена 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. Обратите внимание на следующие ограничения:

  1. имя файла должно начинаться на gamestate_integration_;
  2. имя файла должно заканчиваться на .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


h0_gwsxahvpphiq2m4tu-a4cciu.jpeg

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

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

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

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

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

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

image-loader.svg

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

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


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

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

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

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

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


lvoeay-cs2cpe1zrhapobtcnwbe.jpeg

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

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

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

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

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

Заключение


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

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

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

image-loader.svg

© Habrahabr.ru