Многопользовательская Counter-Strike: Global Offensive наполнена различными раскрасками для оружия разной степени редкости и привлекательности. Некоторые игроки гонятся за уникальными скинами, а другие выбирают на основе субъективного вкуса. Помимо официальной торговой площадки Steam, скины можно купить на сторонних ресурсах, доверие к которым невелико. Но в обоих случаях нет фильтра по цвету.

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

Мотивация



Феечки почти в сборе

Ранее я уже писал про игровые интеграции CS:GO на Хабр. В примере вывода была информация о нике моего профиля – .:WinX:. Musa. Восемь лет назад я и несколько моих друзей взяли себе в качестве ников имена феечек из мультфильма «Клуб Винкс» и создали закрытый клан в Steam, чтобы иметь общий тег в игре.

Наличие от трех до пяти феечек в команде в соревновательном режиме нередко радовало соперников и становилось поводом для обсуждения, какая феечка самая любимая. За весь игровой стаж мне встречались персонажи из мультика «My Little Pony» и команда продуктов Microsoft Office в полном составе.

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


Если вы не знаете, как они выглядят

Выбирать скины вручную из тысячи вариантов — дело не самое приятное. Процесс хотелось автоматизировать. За день я собрал прототип, который представил своим друзьям. В целом, я решил поставленную проблему, но возникла другая…

Обо всем по порядку.

Список возможных скинов


Первый шаг — получение списка всех раскрасок для каждого типа игрового вооружения. Очевидным вариантом кажется автоматизировано «потыкаться» в торговую площадку Steam или в неофициальные ресурсы. Онлайн-сервисы часто не любят, когда их «парсят», и могут сопротивляться. А торговая площадка и при штатном использовании иногда выдает ошибку, так что способ крайне ненадежный.

Идея посмотреть в файлы игры на первый взгляд казалась сложной, но стабильной. При написании текста про игровые интеграции CS:GO я заметил параметр paintkit в информации по вооружению. Параметр был заполнен явно идентификатором раскраски, например, cu_glock_noir. Вооружившись grep’ом я нашел несколько полезных файлов:

  • csgo\scripts\items\items_game.txt — почти 6 МБ различной информации, в том числе список раскрасок и наклеек.
  • csgo\scripts\items\items_game_cdn.txt — ссылки на картинки для предпросмотра скинов, эти же картинки используются на торговой площадке.
  • csgo\resource\csgo_*.txt — файлы локализации.

Начнем с легкого. Файл со ссылками имеет примитивный формат: каждая строка имеет шаблон «ключ=значение». Например:

weapon_glock_cu_glock_noir=http://media.steampowered.com/apps/730/icons/econ/default_generated/weapon_glock_cu_glock_noir_light_large.c93d0cbfa767d1f822a53ebfca0d57f532088c48.png

Файлы локализации и items_game.txt имеют одинаковый формат, похожий на JSON. Отрывок из файла items_game.txt.

"rarities"
{
	"default"
	{
		"value"		"0"
		"loc_key"		"Rarity_Default"
		"loc_key_weapon"		"Rarity_Default_Weapon"
		"loc_key_character"		"Rarity_Default_Character"
		"color"		"desc_default"
		"drop_sound"		"EndMatch.ItemRevealRarityCommon"
	}
	"common"
	{
		"value"		"1"
		"loc_key"		"Rarity_Common"
		"loc_key_weapon"		"Rarity_Common_Weapon"
		"loc_key_character"		"Rarity_Common_Character"
		"color"		"desc_common"
		"weight"		"10000000"
		"next_rarity"		"uncommon"
		"drop_sound"		"EndMatch.ItemRevealRarityCommon"
	}
}

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

  • Если в строке две кавычки, это означает наличие вложенного ассоциативного массива.
  • Открывающую фигурную скобку можно игнорировать, а закрывающая фигурная скобка обозначает конец вложенного ассоциативного массива.
  • Если в строке четыре кавычки, то это строка ключ-значение. Ключ и значение разделены пробельными символами — двумя табуляциями.

В ходе разбора выяснилось несколько неприятных моментов.

  • Ключ и значение разделены не обязательно табуляциями и не обязательно двумя. Разделителем может быть и пробел.
  • В некоторых параметрах идентификатор локализации начинается с символа решетки (#).
  • Идентификатор локализации может иметь разный регистр в разных файлах. Например, в items_game.txt указано Paintkit_sp_palm, а в файле перевода – PaintKit_sp_palm. Возможно, это актуально для всех идентификаторов, поэтому при разборе на всякий случай все ключи перевожу в нижний регистр.

Файл items_game.txt содержит невероятное количество информации. Помимо информации о непосредственно игровых объектах и их атрибутах, можно найти список киберспортсменов и крупных международных турниров, даже статистику по игрокам для старых матчей.



Для моей задачи важна только секция paint_kits, которая содержит список раскрасок. Информация о раскраске включает название, редкость и в некоторых случаях палитру, но не включает информацию о том, где применяется. Я решил убить двух зайцев и в файле items_game_cdn.txt извлечь сразу и ссылку, и название оружия.

Ключ в файле items_game_cdn.txt формируется по такому принципу:

<идентификатор предмета>_<идентификатор раскраски>

Идентификатор скина известен, удаляем его из строки и получаем идентификатор предмета. Для всех вооружений в игре идентификатор предмета начинается с weapon_.

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

Создание библиотеки цветов



Красивый скин, да?

В метаданных для некоторых раскрасок встречаются параметры color0…color3.

"51"
{
	"name"		"am_lightning_awp"
	"description_string"		"#PaintKit_am_lightning_awp"
	"description_tag"		"#PaintKit_am_lightning_awp_Tag"
	"pattern"		"lightning_strike"
	"wear_default"		"0.000000"
	"style"		"5"
	"color0"		"7 5 6"
	"color1"		"65 7 99"
	"color2"		"100 36 38"
	"color3"		"108 108 142"
	"phongalbedoboost"		"40"
	"phongexponent"		"8"
	"pattern_scale"		"1"
	"pattern_offset_x_start"		"0"
	"pattern_offset_x_end"		"0"
	"pattern_offset_y_start"		"0"
	"pattern_offset_y_end"		"0"
	"pattern_rotate_start"		"0"
	"pattern_rotate_end"		"0"
	"ignore_weapon_size_scale"		"1"
	"wear_remap_min"		"0.000000"
	"wear_remap_max"		"0.080000"
}

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

Решение рабочее, но недостаточно хардкорное имеет недостатки:

  • полученные изображения имеют максимальное разрешение 512x512, мелкие детали могут потеряться?
  • изображение демонстрирует предмет с одной стороны?
  • цвета в атрибутах color* могут не совпадать с цветами на изображении.

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


Раскраска по цифрам доступна в игровом клиенте

Рассмотрим основы:

  1. Покраска по цифрам. Разработчики пронумеровали каждую деталь на оружии цифрой от 1 до 4. Задаем цвет для каждой цифры и получаем новый скин. В этом случае цвета в атрибутах можно принять за палитру скина.
  2. Ручная работа. Текстура накладывается на модель как есть. Можно считать палитру на файле текстуры.
  3. Покраска по маске. Четыре цвета наносятся по маске, которая хранится в файле текстур. Рассмотрим этот способ подробнее.


Как работают текстуры в покраске по маске. Источник

Яркая текстура хранит три маски, спрятанных по каналам: красный, зеленый, синий. Цвет в атрибуте color0 задает цвет фона, а color1…color3 — цвета для масок в каналах.


Наложение каждого цвета и промежуточные маски

На языке Python наложение текстур реализовать можно с помощью библиотеки PIL.

# Текстура из файлов игры
image = …
channels = image.split()

# Создаем фон
texture = Image.new("RGB", (image.width, image.height), color[0])

for index, mask in enumerate(channels, 1):
    # Создаем заполнитель
    filler = Image.new("RGB", (image.width, image.height), color[i])
    # Объединяем картинки
    texture = Image.composite(filler, texture, mask)

Остается вопрос извлечения текстур из игры. Скины хранятся в бинарном файле pak01_dir.vpk. Данный тип файлов открывается с помощью утилиты HLExtractor, которая является примером использования библиотеки HLLib. Сами файлы текстур хранятся в известном формате VTF (Valve Texture Format), для которой тоже есть библиотека и реализация на Python.

После обработки всех 1113 скинов имеем JSON-файл с кратким описанием каждого скина и палитры из четырех цветов в форматах RGB и HSV. Что можно сделать для поиска по цвету?

Поиск по цвету


У феечек может быть несколько цветов, но для упрощения сделаем поиск по одному. Наивное решение: в пространстве RGB посчитать евклидово расстояние от запрошенного цвета до всех цветов в палитре, найти минимум и отсортировать все скины по увеличению найденного расстояния. Второе наивное решение: в пространстве HSV искать по минимальному расстоянию между компонентами Hue.

Интересное решение подсказал мне коллега по науке. Палитра цветов — вектор размерностью 12. В этом случае поисковый запрос — аналогичный вектор. Для упрощения пользователь выбирает один цвет, который заполняет собой вектор запроса. Однако такой подход позволяет добавить кнопку «показать похожие» и искать более схожие скины.


Веб-интерфейс

Изначально поиск работал только в интерфейсе командной строки, но открывать ссылки было неудобно. Я взял Bootstrap, FastAPI и быстро из примеров соорудил одностраничный сайт с формой для поиска по цвету.

Остался финальный штрих. Поиск скинов был не только ради спортивного интереса, но и для поиска подходящих, но дешевых скинов, а цен в интерфейсе нет. У торговой площадки Steam также нет общедоступного API. Решение пришло спонтанно: открывать страницу с поиском на торговой площадке.


Я нашел красивый скин, но его цена меня не устраивает :(

После пары сложных поисковых запросов на торговой площадке Steam идентифицированы важные параметры запроса.

  • steamcommunity.com/market/search — базовый URL запроса.
  • appid=730 — ограничить поиск по одной игре, 730 — это SteamID игры CS:GO.
  • q=Запрос — текст запроса, в нашем случае имя скина.
  • category_730_Weapon[]=tag_weapon_glock — тег вида вооружения. Формируется по шаблону tag_%идентификатор_вооружения%. Идентификатор извлекается вместе со ссылками на изображения скинов на CDN.

При переходе по ссылке можно получить больше информации меньшей кровью:

  • доступность скинов различной степени потертости и их цену;
  • наличие StatTrek-версии скина;
  • при желании тут же и купить скин.

Заключение


Я хотел решить проблему подбора скинов суровым, возможно, излишне суровым, техническим методом, и я этого добился. Я поискал по цвету моего персонажа (#A80039) и составил себе вот такой список скинов.


Этот набор мне нравится. Чего не скажешь про его цену. Суммарно он будет стоить от 53 тысяч рублей без ножа или от 93 тысяч рублей с ножом. При этом стоимость возрастает с уменьшением потертостей на скине. Дорогое это удовольствие, быть феечкой!

Исходный код и документация доступны в репозитории на Github.

Комментарии (9)


  1. drondroncki
    23.08.2022 11:56

    Круто


  1. leorikz
    23.08.2022 12:40

    привет, спасибо за статью. все эти рюшечки из лутбоксов изначально берутся? или сразу за деньгу


    1. Firemoon Автор
      23.08.2022 12:55

      По умолчанию из лутбоксов, но ключи к лутбоксам тоже надо покупать...


      1. DrinkFromTheCup
        23.08.2022 13:14

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


        1. ubriaco
          23.08.2022 14:40

          Все было бы так, если бы ключ действительно был универсальным, но у каждого вида кейса свой ключ.


          1. shushu
            23.08.2022 15:30

            Да на самом деле нет разницы. Важно то, что ключ надо покупать и нету ни одной возможности получить ключ бесплатно.

            Есть возможность получить айтемы бесплатно, но они совсем "унылые"


            1. KiddingBanana
              23.08.2022 20:38

              Можно собирать из дешевых айтемов контракты для получения более дорогих, но это, конечно, адский гринд. Равно как и продажа того, что выпало. Однако, в теории получить желаемые скины бесплатно возможно


  1. Gvozdod
    23.08.2022 18:01
    +5

    -- Папа, а можно мне автомат?
    -- Нет
    -- Почему?
    -- Потому что ты девочка и тебе 4 года
    -- А розовый?


    1. yoda776
      24.08.2022 10:25

      Автомат все равно пока нельзя, вот тебе винтовка!