Гайд Анализ Panorama: в поисках графической персонализации CS:GO / Counter Strike 2

Разработчик
Статус
Оффлайн
Регистрация
1 Сен 2018
Сообщения
1,597
Реакции[?]
881
Поинты[?]
116K
Более удобный формат на Notion -
Пожалуйста, авторизуйтесь для просмотра ссылки.


Введение
Всем привет! Графический интерфейс в CS:GO достаточно удобен, и уникален, но при разработке читов вам могла прийти идея заменить его на свою реализацию - более удобную, более красивую, да и в общем идеальную! Ведь этого нету у конкурентов, а у вас есть?

Что же такое Panorama?
Предлагаю обратиться к немного душной документации Valve:
Panorama - это новый фреймворк пользовательского интерфейса, разработанный компанией Valve. Он создан под сильным влиянием современного веб-авторинга (HTML5/CSS/JS) и очень похож на него. Он обеспечивает быструю разработку, высокое качество и высокую производительность интерфейсов, а также бесшовную интеграцию с игровым контентом (3D-модели, частицы и т.д.).
Пожалуйста, авторизуйтесь для просмотра ссылки.
Концепции V8 на примере аниме “Блич”
Перед тем как приступить к реверс-инжерингу панорамы, нужно ознакомиться с базовыми концепциями нужных нам вещей в V8 ( Ведь в панораме используется именно он ). Предлагаю использовать аналогию с аниме “Блич”:
Isolate - это отдельный "мир" в V8, как Уэко Мундо в Блич. В каждом Isolate есть своя память (хип) и стек, и они изолированы друг от друга. Разные скрипты выполняются в разных Isolate, как разные эпизоды аниме в разных мирах.
По такой же аналогии хип - это пространство Уэко Мундо, где находятся все живые персонажи и локации.
В хип помещаются:
  • Объекты JavaScript ( персонажи)
  • Функции ( Сценарии эпизода )
  • Строки ( Локации )
  • и т.д. ( Декорации )
HandleScope - это область видимости в одном Isolate, как один эпизод в Уэко Мундо. В этой области действуют определенные персонажи (хэндлы), а по завершении эпизода они "умирают" (хэндлы освобождаются). Это нужно чтобы избежать переполнения памяти неиспользуемыми персонажами. А вот хэндл (handle) - указатель на объект в памяти Isolate, как персонаж аниме. Хэндл "живет" в области видимости HandleScope, а по завершении - "умирает".

Таким образом, Isolate - это отдельный мир, HandleScope - эпизод в этом мире, хип - пространство всех живых персонажей, а хэндлы - персонажи, "живущие" в этом эпизоде.
Ищем отсылки к Panorama в client.dll
Хендлеры
Если посмотреть в документацию Valve, то можно найти все API методы (
Пожалуйста, авторизуйтесь для просмотра ссылки.
). Они все инициализируются в движке, и там-же имеют свои обёртки.
Пример инициализации одного из методов( За пример я взял метод GetMapBSPName с GameStateAPI):

Давайте пройдёмся по каждой строчке псевдо-кода, сначала мы получаем Isolate от v8, после чего входим в скоп, и получаем объект для метода, создаём темплейт с нашим хендлером, и в конечном счёте регистрируем новый метод, и аргументы к нему.
Но вот вопрос: Что же находится в “Panorama::Handlers::GameStateAPI::GetMapBSPName”?

И опять же ничего сложного! Мы получаем Isolate, входим в скоп, проверяем в a + 8, если 0 или меньше, то вызываем ошибку, в ином случае получаем поинтер на обьект с кучи. Идём дальше, стоит глянуть что в “Panorama::AnotherHandlers::GameStateAPI::GetMapBSPName”

Получаем название карты, создаем строку Utf8 JS строку, и возвращаем её. Что в оригинале?

Думаю этот псевдо-код не нуждается в анализе.
Инициализация целой категории

Вот так выглядит функция которая инициализирует нашу категорию, а вот и наша функция которую мы только что разбирали:

Отлично, пора подняться на уровень выше.

Так, что же тут? Регистрируем новый скоп, заполняем CUtlAbstractDelegate, создаем объект, а после чего и глобальный, и закрываем скоп.

Чем выше поднимаемся, тем шире горизонты открываются
Прошлая анализируемая функция является членом виртуальной таблицы:

Как можно увидеть её смещение относительно начала виртуальной таблицы - 0xC байт ( 12 ), запоминаем и идём дальше. Чтобы продвинуться выше нужно воспользоваться отладчиком, я буду использовать
Пожалуйста, авторизуйтесь для просмотра ссылки.
. Ставим брейкпоинт на начало нашей функции, и ждём пока сработает.

Сработал! Переходим в стэк вызовов, и переходим на предпоследнюю функцию, и вот на функцию выше.


И что же мы тут видим? Тоже самое смещение которые мы запомнили немного ранее. Разбираем важные аспекты:
  1. Получает интерфейсы связанные с графическим интерфейсом.
  2. Проверяем все ли они валидны, если какой-то из них не валидный вызываем ошибку.
  3. Получаем синглетон объект PanoramaUIEngine
  4. Если не удачно - опять устанавливаем байт на 1, и вызываем ошибку.
  5. Итерируем пока (this + 544) не будет достигнута, сразу делаем вывод что это количество категорий
  6. Получаем указатель на наш объект с массива указателей
  7. Вызываем инициализацию нужной категории.
Ничего сложного, правда? Базовая архитектура стала понятна, поэтому продолжать анализ client.dll - не имеет смысла.

За гранью видимого: Panorama.dll
Инициализация UI Engine
При вызове конструктора UI Engine как обычно устанавливаются поля значением по умолчанию, также устанавливается HTTP каллбек:

Немного ниже проверяется был ли уже инициализирован Java-Script движок V8, и инкрементирование количества созданных UI движков, также это всё контролируется мютексами, чтобы избежать состояние гонки:

После чего проверяются разные параметры запуска, входит в Isolate, устанавливает Exception Handler, и вызывается инициализацию контекста ( об этом еще поговорим ниже), а также вызывает “RegisterEventTypesWithEngine” и “RegisterPanelFactoriesWithEngine”:

Error Handler при срабатывании, если включена отладка вызывает __debugbreak, в ином случае Raise при этом выводя всю информацию.

Возвращаемся к инициализации контекста, она инициализирует свои методы по $. ( Как JQuery ). Сначала входит в изолейт, регистрирует скоп, а потом уже определяет методы:

В конце конечно происходит глобал референс:

Коротко о разметке
У загруженной разметки ( Layout ) есть своя структура - CLayoutFile, она поддерживает загрузку как с файла, так и с буффера, а также в любой момент можно перезагрузить разметку с буффера.
Предлагаю рассмотреть как раз перезагрузку с буффера:



Входим в скоп профилировщика ( сколько же этих скопов…), очищаем старый набор стилей для этой разметки, после чего чистим другие дочерние структуры, и вызываем заново загрузку с буффера. Сложно? Вроде как нет.
У файлов разметки также есть свой менеджер, который уже контролирует асинхронную загрузку с файлов, подгрузку стилей, конвертирование пути - ведь панорама использует специфический путь основанный на формате веб-ссылок.
Панели, панели, интерфейс из панелей.
Заметки: Стоило уделить больше времени этой главе.

Весь интерфейс как в VGUI построен на панелях, в панораме этот класс называется CUIPanel, и он имеет свою очень большую виртуальную таблицу. Разбирать каждый метод не имеет смысла, поэтому давайте разберём только в общих чертах за что методы отвечают:
  • Установка/Получение атрибутов
  • Установка/Получение разных свойств панели.
  • Получение родительских/дочерних панелей
  • Добавление разметок, стилей, скриптов
  • Управление скроллом
  • Управление нажатыми клавишами в пределах панели
  • и прочее
Каждая нажатая клавиша — новый горизонт
У Panorama есть свой движок хэндлинга ввода - удивлены? И я вот тоже нет, ведь это очевидно. Он поддерживает управление с геймпадов, и даже стимдека ( Кто мог сомневаться?) . Конструктор выглядит следующим образом:

Как можно увидеть, оно устанавливает поля, а также вызывает конструкторы для геймпадов и контроллеров. Ниже устанавливает стандартный путь до настроек.
Давайте рассмотрим функцию которая вызывается каждый кадр, она кстати так и называется RunFrame:


Но как же он обрабатывает ввод? Всё просто! Для этого ему приходит “InputMessage”, и он его обрабатывает в InputEvent. Также к другим возможностям:
  • Он позволяет перезагружать настройки кейбиндов
  • Регистрировать новые кейбиндинги
  • Устанавливать захват на определенное окно
  • Проверять на активность разных клавиш
  • Получить тип активного контроллера.
Рубикон уже давно пройден
Устали читать? Я думаю на этом можно закончить, ведь основная цель этой статьи была выполнена, и думаю вы узнали о базовой архитектуре панорамы.

Но почему же в названии так-же написано “Counter-Strike 2”, если тут разбирается только CS:GO? Потому что все детали которые были разобраны тут подходят, и под CS 2, и под Dota 2, да и под другие игры где есть панорама.

Все материалы которые были затронуты, а также декомпилированные разбираемые функции, и DLL которые использовались в статье вы можете найти здесь:
Пожалуйста, авторизуйтесь для просмотра ссылки.


Всем пока! Не забывайте подписываться на мой блог, чтобы узнавать о подобном раньше -
Пожалуйста, авторизуйтесь для просмотра ссылки.


Отдельное спасибо:
  • Alexander pers0na2 за предварительный просмотр статьи, как обычный читатель
  • Colby57. За предварительную оценку статьи
  • Team Enterial, за помощь в исследовании
  • Всем подписчикам Something Channel
Если вам понравилась эта статья и вы считаете, что она может быть интересна другим пользователям, буду признателен, если вы поделитесь ею в соц-сетях / с друзьями . Распространение полезного контента - лучшая благодарность автору!
 
Сверху Снизу