Серия «Frostpunk Community Modkit»

6

Как я вскрывал Frostpunk. Часть 3: Почему моё здание было чёрным⁠⁠

Серия Frostpunk Community Modkit

Первая часть: Frostpunk Community Modkit. Или разработка модкита для первого фростпанка
Вторая часть: Как я вскрывал Frostpunk. Часть 2: Картинки, сломанное меню и закон-призрак

К этому моменту я уже умел менять игре настройки, тексты и картинки. Законы, технологии, иконки - всё моё!

Но здания всё равно были чужие. Я мог взять палатку и назвать её "Станция тепловых капсул".

Поменять ей стоимость, количество мест, иконку

А выглядела она всё равно как палатка

Хотелось своё. Свою модель

Ну что ж. Лезем в 3D. Что может пойти не так

Где живёт модель

Первый вопрос - где вообще лежат модели зданий. Отдельного склада с моделями у игры нет.

Было бы слишком просто

Нашлись они в складе templates, внутри шаблонов. Шаблон здания - это такая коробка, в которую запихано всё: как здание выглядит, какие у него эффекты, сколько стоит, сколько людей влезает. И модель тоже там, внутри, где-то между стоимостью и списком эффектов
Код, который её читает, я нашёл в игре тем же способом, что и в первой части. Файл движка называется meshtemplaterenderingdataserialization.cpp - разработчики, как обычно, забыли путь к исходнику внутри exe. Спасибо, ребята, без вас я бы ничего не нашёл!

Модель в 3D - это набор точек в пространстве, их называют вершинами, и треугольники, натянутые между ними. Любое здание, персонаж, дерево в любой игре - это просто дохуища треугольников

Каждая вершина во фростпанке весит 36 байт. Вот начало модели обычного уличного фонаря:

Первые четыре байта a4 01 00 00 - это 420. Столько вершин у одного сраного фонаря. Следующие четыре, 24 00 00 00 - это 36, байт на вершину. Дальше служебное описание буфера и сами вершины
Разберём одну:

  • позиция - три числа: x, y, z. Где точка висит в пространстве. Двенадцать байт

  • нормаль - куда смотрит поверхность в этой точке. По ней игра считает, как падает свет. Сжата до двух байт на ось, потому что жадные

  • касательная - ещё одно направление, поперёк нормали. Нужна, чтобы на ровной поверхности нарисовать рельеф: кирпичную кладку, доски, заклёпки. Без неё стена была бы гладкой

  • цвет - у всех ванильных моделей ff ff ff ff, то есть белый. Разработчикам нахуй не пригодился, но место занимает. В каждой вершине. Четыре байта. Умножьте на все модели игры

  • координаты текстуры - какая точка картинки натягивается на эту вершину. Записаны числами половинной точности, по два байта. Экономят как могут, а на цвете не сэкономили, логика

Дальше треугольники - просто тройки номеров вершин. А в самом конце шаблона - материалы, пути к картинкам и таблица "что и как рисовать"
Лимит - 65 535 вершин на модель. Потому что номер вершины в треугольнике записан двумя байтами, а больше в два байта не влезает хоть ты тресни. Запомните эту цифру, она ещё вылезет и укусит

Первая модель

Модкит научился превращать обычный файл модели OBJ - такие умеет сохранять любая 3D-программа - в формат фростпанка. Переложить вершины, пересчитать нормали и касательные, упаковать, запихнуть в шаблон

Сначала игра послала меня нахуй в принципе. "Compound template element 'mesh' references an invalid template" - мол, ссылаешься на модель, которой не существует. Хотя вот она, лежит, сука, я её сам туда положил

Оказалось, новую модель мало положить в склад, её надо ещё прописать в реестре шаблонов.

Это отдельная история и отдельная боль в жопе, расскажу когда-нибудь потом

В итоге всё собралось. Установил. Запустил игру. Построил

Здание было чёрным

Не тёмным. Не грязным. Не "ну освещение такое"

Чёрным, блядь, как дыра. Как будто кто-то вырезал кусок экрана ножницами и забыл вставить обратно

Вокруг снег, свет, огни генератора, всё красиво. А посреди - чёрная клякса в форме моей станции

Я смотрел на это и думал: ну пиздец. Приехали

Расследование

Когда здание чёрное, причин может быть миллион. Игра не пишет в журнал "у вас тут чёрное здание, потому что вы долбоёб". Она просто рисует чёрное и молчит. Как партизан

Начал копать. И нашёл не одну причину, а четыре. Одновременно. Каждая сама по себе могла сделать здание чёрным. Это был не баг, это была коллекция

Причина первая. Картинка вверх жопой

В Blender и в формате OBJ координаты картинки считаются снизу вверх. А в DirectX, на котором работает фростпанк, - сверху вниз. Потому что стандарты - это когда каждый делает по-своему

То есть каждая точка модели брала цвет из зеркального места картинки. Доски тянулись с металла, крыша - с камня. Само по себе это не чернота, но каша ещё та
Лечится переворотом одной координаты.

Причина вторая. Чужие таблицы

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

Теперь модкит пересчитывает и таблицы, и рамку сам

Причина третья. Старая знакомая

Помните из второй части историю с 144-м байтом? Как модкит клал пиксели картинки не туда и получалась цветная каша?

Так вот. Эта ошибка жила в коде дольше, чем я думал, и выстрелила здесь второй раз. Текстуры модели съезжали, и игра жрала мусор

Ну охуенно. Одни и те же грабли, только ручка теперь длиннее

Причина четвёртая. Главная
А вот тут начинается самое весёлое

У материала здания во фростпанке две картинки. Первая называется _ALB - это просто цвет. Доски коричневые, металл серый, тут всё понятно даже мне

Вторая называется _NOR. По всем законам 3D-графики это карта нормалей. Та самая картинка, которая рисует рельеф: где выпуклость, где щель между досками. Классическая карта нормалей - сиреневая такая, все её видели

Я сделал обычную карту нормалей. Положил. Чёрное

Сделал заново. Положил. Чёрное

Да ёб твою мать

Тогда я пошёл туда, где правда. В шейдеры

Шейдер - это маленькая программа для видеокарты, которая решает, какого цвета будет каждый пиксель на экране. Как падает свет, как блестит металл, как лежит тень. Все шейдеры игры лежат в складе common, уже скомпилированные
Я вытащил их все и прогнал через дизассемблер от Microsoft. Он превращает скомпилированный шейдер обратно в читаемые команды
Шейдеров оказалось 6962. Шесть тысяч девятьсот шестьдесят два. На одну игру про замерзающих людей

И в них нашлось вот это:

RoughnessSourceMask - "откуда брать шероховатость"
MetalnessSourceMask - "откуда брать металличность"
Маска источника. То есть шейдер берёт шероховатость и металличность не из отдельных картинок, а из какого-то канала какой-то картинки
Какой, блядь? Я полез в ванильные текстуры. Взял карту _NOR обычной палатки и посмотрел, что лежит в каждом цвете:

Если бы это была обычная карта нормалей, красный и зелёный были бы в среднем около 128, а синий - около 255. Синий там всегда почти на максимуме, поэтому карты нормалей такие сиреневые
А тут синий в среднем 32. Почти ноль. Красный 170

Чё за хуйня?

Разложил картинку на четыре канала и посмотрел глазами:

И всё встало на свои места

  • красный - это вообще не нормаль. Это фактура материала: где дерево, где ткань, где металл. Шероховатость

  • зелёный - рельеф. Складки ткани, доски. Это половина настоящей нормали

  • синий - почти весь чёрный. Белые только отдельные детали: крепления, котёл, железяки. Это металличность. Чёрное - не металл, белое - металл

  • прозрачность - снова рельеф, но под другим углом. Вторая половина нормали

Четыре разные картинки, упакованные в одну. Разработчики сэкономили на текстурах. А я на этой экономии проебал весь день

А теперь смотрим, что я туда пихал. Обычную карту нормалей:

  • в синем канале 255 - игра читает это как "всё здание сделано из металла. Полированного до блеска"

  • в прозрачности 255 - игра читает это как "поверхность повёрнута набок"
    Здание из полированного металла, у которого все поверхности смотрят вбок. Такое не отражает ничего, кроме пустоты. Поэтому чёрное

    Логика железная. Буквально

Весь день я пихал игре сиреневую карту и не понимал, почему здание выглядит как дыра в пространстве

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

Бонус, который вылез заодно. Прозрачность в первой картинке, _ALB, - это маска свечения. Белым там, где ночью должно светиться. Окна, лампы, раскалённая печь. Про это будет отдельно, потому что окна у меня так и не зажглись. Эта сволочь до сих пор не работает

Ожило

Собрал. Установил. Запустил
Иду строить, а у самого жопа сжимается

Вот оно. Доски деревянные. Металл металлический. Даже снег на крыше лежит
Я минут пять просто крутил камеру вокруг и смотрел. Ну охуенно же, согласитесь

Blender в деле

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

Потом всё это нужно "запечь". Модель состоит из сотни отдельных деталей, у каждой свой материал. А игре нужна одна модель и одна картинка на всё. Капризная, блядь
Поэтому Blender разворачивает все детали на одну большую картинку 4096 на 4096 и "запекает" туда цвет, тени, рельеф, шероховатость и металл. Одна кнопка - и пять карт готовы

Модкит забирает это, упаковывает _NOR как положено, и вставляет в игру

Помните лимит в 65 535 вершин? Вот он и вылез
Модель стройки - полупрозрачные леса вокруг будущего здания - модкит генерирует сам. И у Спуска она получилась на 63 981 вершину. Впритык. Ещё один сраный домик сверху - и не влезло бы

Пришлось научить модкит склеивать одинаковые вершины. Стало 40 653. Выдохнул

Вот он, Спуск. В игре. С лифтом, который ездит вверх и вниз по шахте. Лифт, кстати, я взял у ванильного склада - там есть такой эффект, просто его никто не замечает, и он у меня немного поплыл) Потом исправил координату.

Итого

Что модкит умеет после этой части:

  • брать модель из Blender или любой 3D-программы и превращать её в здание фростпанка

  • правильно упаковывать материалы, чтобы здание не было чёрной дырой

  • сам делать модель стройки и снег на крыше

  • пересчитывать всё служебное: таблицы, рамку, касательные

    Продолжение следует

Показать полностью 10
7

Как я вскрывал Frostpunk. Часть 2: Картинки, сломанное меню и закон-призрак⁠⁠

Серия Frostpunk Community Modkit

Первая часть тут: Frostpunk Community Modkit. Или разработка модкита для первого фростпанка

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

Ага

Щас

Дальше я решил сделать первый настоящий мод. Свою ветку законов со своими картинками.

Что может пойти не так?

Всё
Всё может пойти не так

Картинка, которая на самом деле не картинка

Начнём с того, как фростпанк хранит изображения. Иконки, обложки законов, текстуры зданий - всё лежит в том самом складе textures-s3 на 6,8 гигабайта
Каждая картинка там - это не png и не jpg. Это файл .texture, и устроен он так:

[4 байта] ширина
[4 байта] высота
[4 байта] формат сжатия, буквами. Обычно DXT5
[4 байта] сколько в файле копий картинки

дальше таблица на 16 строк: размер каждой копии и сколько она весит
дальше сами картинки, начиная со 144-го байта

Вот начало атласа иконок технологий, вживую:

Разбираем:
00 08 00 00 - ширина. Задом наперёд, помним? 0x0800 = 2048
00 08 00 00 - высота, тоже 2048
44 58 54 35 - буквами "DXT5"
0c 00 00 00 - копий картинки 12

Дальше таблица. Первая строка: 01 00 01 00 и 10 00 00 00 - копия размером 1 на 1 пиксель, весит 16 байт. Следующая 2 на 2. Потом 4 на 4, 8 на 8, 16 на 16... и последняя 2048 на 2048, весит 4 мегабайта
Четыре строки таблицы пустые, одни нули. Таблица всегда на 16 строк, даже если копий меньше. А картинки начинаются ровно с байта 0x90 - это и есть 144

Внимательный читатель спросит: какого хрена одна картинка хранится двенадцать раз?
Это называется мип-уровни. Когда камера далеко и иконка на экране размером со спичечную головку, видеокарте незачем жевать все 2048 на 2048 пикселей. Она берёт уменьшенную копию.

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

Ещё внимательнее читатель спросит: почему картинка 1 на 1 пиксель весит 16 байт?
А тут начинается магия сжатия

DXT5, или как запихнуть 16 пикселей в 16 байт

Обычная картинка хранит каждый пиксель честно: красный, зелёный, синий, прозрачность. 4 байта на пиксель
DXT5 так не делает. Он режет картинку на квадратики 4 на 4 пикселя и для каждого квадратика записывает:

  • два "главных" цвета

  • для каждого из 16 пикселей: какой он - первый цвет, второй или что-то между ними

  • отдельно то же самое для прозрачности

Итого 16 байт на квадратик из 16 пикселей. Вместо 64. В четыре раза меньше
И видеокарта умеет читать это напрямую, ничего не распаковывая. Поэтому все игры так и делают

А картинка 1 на 1 весит 16 байт, потому что меньше одного квадратика не бывает. Хочешь ты один пиксель или шестнадцать - платишь за квадратик целиком. Как такси с минимальным тарифом

Цена - качество. Внутри квадратика всего два настоящих цвета и смесь между ними. Если приблизить иконку, видно, как края идут ступеньками:

Теперь мои первые грабли

Первая версия модкита записывала картинки неправильно. Я клал пиксели сразу за заполненными строками таблицы. Без пустых строк

А игра всегда читает пиксели со 144-го байта. Всегда. Хоть там пусто, хоть нет

У картинки 1024 на 1024 из-за этого всё съезжало на 40 байт. А 40 не делится на 16 - на размер одного квадратика. Значит игра начинала читать квадратики с середины. Каждый квадратик собирался из хвоста одного и головы другого

Результат - цветная каша вместо иконки

Лечилось одной строкой. Искалось... дольше, чем хотелось бы

Склад, в который нельзя просто положить коробку

С заменой картинок разобрались. Берёшь существующую, перерисовываешь, кладёшь на место

Но мне нужны были новые картинки. Обложка для своей ветки законов. Картинка для своего закона. Такого файла в игре никогда не было

И модкит честно ответил: "Не удалось подготовить изображение ui/sociotech/lastshift_charter_cover.dds"

Ну конечно. Как заменить то, чего нет?

Решение напрашивалось: дописать новую коробку в конец склада и внести её в журнал

Дописывать в конец - это важно. Склад весит 6,8 гигабайта. Перепаковывать его целиком ради одной иконки - это минут десять и гигабайты оперативки. А дописать в хвост - секунды. Старые коробки при этом никто не трогает

Но есть нюанс
Журнал отсортирован. Строго. По хэшу имени - тому самому отпечатку из первой части

Зачем? Потому что игра ищет в журнале файлы методом, которым вы ищете слово в бумажном словаре. Открыла посередине. Нужное слово раньше? Открыла середину первой половины. Позже? Середину второй. И так, пока не найдёт

Это называется двоичный поиск, и он быстрый. Среди 4178 записей нужная находится шагов за двенадцать

Но работает он только если словарь отсортирован. Если хоть одна страница вклеена не туда, поиск уходит не в ту половину и возвращается ни с чем

Я дописал новую запись. Отсортировал журнал заново. Собрал мод. Запустил игру

Главное меню выглядело так, будто его постирали с отбеливателем
Бледное. Без текстур. Кнопки висят в пустоте
Я добавил ДВЕ картинки. Для закона. А сломалось вообще всё

Причина - та самая подстава из первой части. Числа задом наперёд
Хэш - это число из четырёх байт. Записан задом наперёд. Игра сортирует журнал по числу
А я сортировал по байтам. Как они лежат в файле
Разница вот:

Слева хэши, как они лежат в файле: d1, 20, cc, d1, 9a... Скачут как хотят. Справа те же хэши как числа: 11970769, 13419040, 13597900... строго по возрастанию
Проверка внизу подтверждает: журнал отсортирован по числу - да. По байтам - нет

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

Самое обидное - у меня была проверка. Автоматический тест, который проверял, что журнал после добавления отсортирован

Он проверял. И говорил: всё отлично

Потому что проверял сортировку тем же неправильным способом, каким я сортировал

Проверял свою ошибку своей же ошибкой. Ну охуеть теперь

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

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

Игру вернул в исходное состояние из резервных копий. С тех пор модкит перед любым вмешательством кладёт рядом файлы .orig. А кнопка "Вернуть игру к оригиналу" просто отрезает дописанный хвост и возвращает старый журнал. Шесть с лишним гигабайт никуда копировать не надо

Закон-призрак

Меню починено. Картинки ложатся. Делаю свою ветку законов, "Хартию линии". В ней несколько законов, среди них "Ночные смены"

Собираю. Запускаю. Открываю книгу законов

"Ночных смен" нет

Нигде

Ни в одной клетке. Ни после какого закона

А закон "Хартия линии" гордо показывает "ЗАКОН ПРИНЯТ"

Хотя я его не подписывал. Я вообще ничего не подписывал, кроме одного закона, "Приоритета".

Это видно в журнале игры

Дальше была неделя позора. Ну ладно, не неделя. Но гипотез было много, и все мимо:

  • "Игра не поддерживает закон, у которого два родителя". Проверил на сохранении - поддерживает, ванильные законы так устроены

  • "Движок сам подписал Хартию". Я даже нашёл это в файле сохранения! Оказалось, неправильно расшифровал восемь байт

  • "Закон дальше определённой клетки не откроется". Не доказал, не опроверг, но причина была не в этом

А причина оказалась до обидного простой

У каждого закона, кроме названия для игрока, есть внутреннее имя. Служебное. Игрок его не видит, по нему игра различает объекты

И это имя игра хранит в однобайтовой кодировке. Где на букву выделен ровно один байт.

Латиница туда влезает, а русских букв там просто нет

И что игра делает с буквой, которой нет? Правильно. Ставит вопрос

"Хартия линии" превращается в "?????? ?????"
"Ночные смены" превращается в "?????? ?????"

Совпадение? Нет. Шесть букв, пробел, пять букв. В обоих

Два разных закона и в придачу сама ветка "Хартия линии" получили ОДНО И ТО ЖЕ имя

Игра смотрит: "так, закон ?????? ?????. Подписан? Ага, подписан... ой, это другой ?????? ?????. Ну, неважно". И рисует одного вместо другого, путает, кто подписан, а кто нет

Три разных объекта с одним паспортом. Представьте, что у вас в классе три Иванова Ивана Ивановича, и учитель ставит двойку одному из них. Кому? А хер его знает. Всем по очереди

Лечение - одна функция. Модкит теперь сам переводит внутреннее имя в латиницу и следит, чтобы оно было уникальным:

Игрок при этом ничего не замечает. Он видит название из файла текстов - нормальное, русское. А внутри у закона теперь "Khartiya linii", и путать его не с кем

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

Главный урок этой истории, который я выучил очень дорого: не чинить по догадке

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

Правильнее ставить опыт, который разводит причины по разным законам. Сделал бы сразу два закона с латинскими и два с русскими именами - нашёл бы за один запуск

А потом законы замолчали

Бонусные грабли, коротко

Когда подписываешь закон, горожане его обсуждают. "Опять пайки урезали, суки". Вот это всё

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

В игре - тишина

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

Теперь модкит при сборке дописывает свои ключи и туда. Горожане заговорили. Жалуются. Всё как в жизни

Атлас: коммуналка для иконок

Последнее про картинки. Иконки технологий не лежат каждая в своём файле. Они все на одной большой картинке 2048 на 2048, нарезанной на сетку 20 на 20 клеток. Называется атлас
Технология в настройках говорит не "моя иконка - вот этот файл", а "моя иконка - клетка номер такой-то"

Хочешь свою иконку - нужна свободная клетка

Какая клетка свободна? Логичный ответ: та, на которую не ссылается ни одна технология

Нелогичный, но правильный ответ: та, которая полностью прозрачная

Потому что 65 клеток атласа заняты картинками, которые не принадлежат ни одной технологии.

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

Вот он, атлас в модките. Пунктиром отмечены свободные клетки, их 257. Видно, сколько там всего помимо иконок технологий: круги, цифры I, II, III, какая-то табличка DANGER
Коммуналка, в которой половина жильцов не прописана

Итого

Что умеет модкит после этой части:

  • заменять любую картинку игры

  • добавлять новые картинки, которых в игре никогда не было

  • класть свои иконки в свободные клетки атласов, не трогая чужие

  • откатывать всё обратно одной кнопкой

Продолжение следует

Показать полностью 9
8

Frostpunk Community Modkit. Или разработка модкита для первого фростпанка⁠⁠

Серия Frostpunk Community Modkit

Всё началось с простого желания

Я хотел сделать свой мценарий для фростпанка. Свой город. Свои законы. Свои здания. Свою концовку, в которой все умрут, но красиво

Открываю интернет. Ищу редактор модов для первой части

Нет.

Ищу документацию

Нет.

Ищу хоть что-нибудь

Нихуя.

Официальных инструментов у первого Frostpunk просто не существует. Разработчики сделали игру, выпустили её и спокойно ушли делать вторую часть. А мы, люди с идеями, остались стоять перед закрытой дверью.

Ну штош

Раз дверь не открывается, будем снимать её с петель

Ликбез, без которого дальше будет больно

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

Любой файл на пк - это длинная цепочка байтов

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

Показывать их принято не обычными цифрами, а двумя символами от 00 до FF:

  • 00 - это ноль

  • 0A - это десять

  • FF - это 255

Буквы A-F здесь просто работают цифрами от 10 до 15. Непривычно, но так любой байт помещается ровно в два символа.

И главный подвох, который попортит нам кровь позже.

Большие числа записаны задом наперёд...

Число 900 в файле выглядит вот так:

84 03 00 00

Читать надо с конца: 00 00 03 84. Это 3 × 256 + 132 = 900 (базовый курс информатики, да?)

Почему задом наперёд? Потому что так устроены процессоры в наших компьютерах. Не фростпанк виноват. Но когда ты впервые видишь 84 03 и пытаешься понять, где тут девятьсот, хочется вырубить пк и уйти в лес

Склад без подписей

Открываю папку с игрой

Жду, что увижу аккуратные папочки: "здания", "законы", "картинки"

Ага, щас

Вместо этого там лежат несколько здоровенных файлов парами: .dat и .idx.

Работает это как склад с журналом учёта:

.dat - это сам склад. Коробки стоят вплотную, без подписей и промежутков. Просто километровый ряд коробок

.idx - журнал учёта. В нём записано: коробка такая-то начинается вот тут, весит вот столько, запечатана или нет

Без журнала склад — это просто гора хлама

Главные склады такие:

common - настройки, экономика, законы, здания, технологии, события, погода
localizations - языки
textures-s3 - картинки
templates - шаблоны зданий и мест на карте мира
scenes - сцены

Шаг первый. Журнал, в котором нет имён

Тут сразу надо отдать должное

Как устроен журнал .idx, до меня уже выяснил автор утилиты frostract Mystik738 на гитхабе, за что ему огромное спасибо. Переписал её под себя и встроил в модкит. Но формат вскрыл он. Низкий поклон, добрый человек

Устроен журнал так:

00 02 01 - "подпись". Игра смотрит на эти три байта и понимает: да, это моё
[8 байт] - сколько коробок на складе дальше по 29 байт на каждую коробку:
4 байта - отпечаток имени файла
8 байт - сколько весит в сжатом виде
8 байт - сколько весит после распаковки
8 байт - где на складе начинается
1 байт - упакована или нет

Вроде всё понятно

Смотрю на отпечаток имени.

Стоп.

А где имена?

Я ожидал увидеть там что-нибудь вроде configs/frostpunkeconomyconfig.bin

А там четыре байта.

Четыре. Сраных. Байта.

Это называется хэш. Имя файла прогоняют через специальную формулу (здесь это MurmurHash2), и на выходе получается короткое число - отпечаток

Фокус в том, что формула работает только в одну сторону

Из имени отпечаток получить легко: подставил - готово

А из отпечатка имя не получить НИКАК. Формула перемешивает буквы так, что обратно их не собрать

Представьте телефонную книгу, где вместо фамилий записаны номера паспортов. Хочешь найти Иванова? Сначала узнай его номер паспорта. Не знаешь? Ну тогда любуйся столбиком цифр

Выход нашёлся через подглядывание

Когда игра загружается, она сама запрашивает свои файлы по нормальным именам: "дай-ка мне файл экономики". Если в этот момент тихонько стоять рядом и записывать, получится список имён

Дальше просто: берём каждое имя, прогоняем через ту же формулу. Отпечаток совпал с записью в журнале? Поздравляю, вы опознали коробку

Коробки с отрезанным хвостом

Большинство коробок на складе упакованы, как zip, чтобы меньше места занимать. Формат обычный - gzip

Беру коробку. Распаковываю стандартной программой

Файл повреждён

Че?

Беру вторую.

Файл повреждён

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

Стандартная программа видит обрубок и паникует (сука такая)

Пришлось распаковывать в обход проверки. А при упаковке обратно так же аккуратно отрезать этот хвост. Иначе игра смотрит на свой же собственный склад и говорит - Это не моё. Я это не ела

Шаг второй. Тексты, или минутка нежности

После такого начала я ждал, что тексты окажутся филиалом ада на земле

А они оказались лапочками

Реально. Единственное место во всей игре, где разработчики как будто подумали: "а вдруг кто-то захочет это прочитать"

Файл языка (russian.lang, english.lang и остальные) устроен честно и просто:

[4 байта] - размер файла
[4 байта] - сколько в нём строк
дальше строки одна за другой:
[2 байта] - длина ключа
ключ латиницей
[2 байта] - длина текста
сам текст

Ключ - это адрес строки. По нему игра её ищет. Например Blueprints/CoalMine
Текст - то, что видит игрок
Вот настоящая строка из русского файла:

13 00 - длина ключа, 19 символов
42 6C 75 65 70 72 69 6E 74 73... - "Blueprints/CoalMine", одна буква = один байт
05 00 - длина текста, 5 символов
23 04 33 04 3E 04 3B 04 4C 04 - "Уголь"

Внимательный читатель спросит: какого хера пять букв заняли десять байт?
А вот такого. Русские буквы записаны в кодировке UTF-16, и каждая весит два байта. И да, они тоже задом наперёд. А ты чего ждал?

У - номер 0423, в файле лежит как 23 04
г - номер 0433, лежит как 33 04
о - номер 043E, лежит как 3E 04
л - номер 043B, лежит как 3B 04
ь - номер 044C, лежит как 4C 04

Номер буквы - это её место в огромной мировой таблице символов Юникоад. Русский алфавит там живёт где-то с номера 0410. Не спрашивайте почему, так исторически сложилось, как и всё хорошее в этом мире

Итог: любой текст можно заменить на любой другой, любой длины. Добавить свои строки.
Собрать файл обратно
Все 13 языков после "разобрал - собрал" получаются точно такими же, какими были

Я откинулся на спинку кресла и подумал: "да это же легко. Чего все ноют?"

Запомните этот момент. Это последний раз, когда я так думал

Шаг третий. Мозг игры

В складе common лежит 51 файл настроек

Это мозг фростпанка. Сколько стоит здание. Сколько угля жрёт генератор. Что делает закон.

Когда придёт буря и сколько народу она выкосит

Открываю файл экономики. Нахожу кусок про уголь:

01 43 6F 61 6C 00
0B CF 0B 94 B9 43 37 85 9C 44 BC DF 67 F9 BE E1
96 00 00 00
84 03 00 00
00 00 00 00
01 40 52 65 73 6F 75 72 63 65 73 2F 4E 61 6D 65 73 2F 43 6F 61 6C 00

Смотрю на это
Оно смотрит на меня
Никто не моргает
Проходит минута
Я моргнул первым

Забегая вперёд, покажу, что тут написано. Узнал я это сильно позже и ценой нескольких лет жизни и остатков психики:

01 43 6F 61 6C 00 - внутреннее имя. 01 значит "имя есть", дальше буквы C-o-a-l, в конце 00 значит "всё, кончилось". Получается Coal
0B CF ... BE E1 (16 байт) - уникальный номер угля. GUID. По нему на уголь ссылаются все остальные файлы игры
96 00 00 00 - стартовый запас. 150
84 03 00 00 - вместимость склада на старте. 900
00 00 00 00 - условия, при которых ресурс показывается. Список из нуля штук, то есть условий нет
01 40 52 ... 6C 00 - адрес названия в файле текстов: @Resources/Names/Coal

Выглядит просто?

А теперь главная подлянка

В файле нет ни одной подписи. Вообще. Ни одной

Слова "стартовый запас" там нет. Слова "склад" нет. Есть 96 00 00 00, а следом 84 03 00 00. И иди нахуй

Откуда мне знать, что 150 - это запас, а 900 - склад? А не наоборот? И что это вообще два числа по четыре байта? Может, одно по восемь? Или четыре по два? Или это вообще не числа, а чья-то дата рождения?

Это как анкета, у которой оторвали левую колонку с вопросами
Остались одни ответы:
"32"
"Да"
"Иванов"
"17"

Где тут возраст, а где номер квартиры? А где количество судимостей?

Хер его знает

А порядок вопросов знает только одна сущность во вселенной - сам Frostpunk.exe

Значит, лезем в него

Вскрытие

Frostpunk.exe - скомпилированная программа
Когда-то разработчики написали её на нормальном человеческом языке программирования. С названиями, с комментариями, с понятной структурой. Возможно даже с шутками в комментариях. Мы этого уже никогда не узнаем

Потом специальная программа, компилятор, перевела всё это в машинный код - длинную цепочку команд для процессора

При этом всё человеческое выбрасывается к хуям. Остаются голые команды: "возьми число отсюда, прибавь четыре, прыгни туда"

Это как если бы от рецепта борща остался только чек из пятёрочки. Свёкла, капуста, сметана.

Удачи

Чтобы хоть что-то понять, берут декомпилятор. Он превращает машинный код обратно в подобие программы

Я взял Ghidra - бесплатный инструмент, который, внимание, сделало и выложило в открытый доступ АНБ США. Да, те самые ребята, которые читают вашу переписку. Спасибо им, в кои-то веки они сделали что-то полезное и для меня

Подобие программы получается без имён

Вместо "ПрочитатьНастройки" там написано FUN_140e42c60

Вместо "цена" - param_2

Вместо "количество" - param_3

И сидишь, как археолог над черепком: "так. Эта функция берёт что-то, прибавляет четыре и вызывает другую функцию. А та берёт что-то ещё и тоже вызывает функцию. Что же ты делаешь, сука?"

В какой-то момент я поймал себя на том, что разговариваю с FUN_140e42c60 вслух. Ласково. По имени

Но однажды нашлось главное - как игра читает свои настройки
Устроено оно так:

  1. В игре много видов объектов. Классы: "ресурс", "закон", "здание", "уровень холода", "эффект события"

  2. При запуске игры каждый класс регистрирует свои поля в строгом порядке. Для ресурса: сначала имя, потом номер, потом запас, потом склад, потом условия показа...

  3. Когда игра читает файл, она идёт по этому списку и для каждого поля вызывает свою читалку. Для числа - "прочитай 4 байта". Для строки - "читай, пока не встретишь 00". И так далее

И тут до меня дошло!!

Оторванная колонка с вопросами никуда не делась. Она лежит внутри игры. В функциях регистрации полей

Все ответы были у меня перед носом. Нужно было просто вытащить их из exe. Всего-то

"Всего-то" заняло столько, что я перестал считать дни

Функция за функцией. Класс за классом. Поле за полем

Пока не собралась полная схема: какие поля у каждого класса, какого они типа и в каком порядке

Получилось около трёх с половиной тысяч описаний классов

Три. С половиной. Тысячи

В какой-то момент мне начали сниться GUID'ы

Параллельно я подглядывал за игрой прямо во время работы. Для этого есть Frida - инструмент, который встраивается в запущенную программу и перехватывает её функции
По сути я подсадил игре жучка: "каждый раз, когда читаешь поле - стучи мне, какое"

Ghidra говорила, как должно быть

Frida показывала, как оно на самом деле

И когда они расходились, начиналось самое весёлое. В кавычках

Из чего сделан мозг

Хорошая новость: кирпичиков оказалось немного

  • число. 4 байта. Количество, время, цена. Бывает целое (900) и дробное (0,75)

  • флажок. 1 байт. "Да" или "нет", 01 или 00

  • GUID. 16 байт. Уникальный номер объекта, как паспорт

  • строка. Байт "строка есть", потом буквы, потом 00 в конце

  • список. 4 байта "сколько штук", потом сами штуки

  • список с сюрпризом. Про него отдельно, там пиздец

Пройдёмся по непонятному

Дробные числа хранятся так, будто их шифровали специально, чтобы я страдал

Единица выглядит как 00 00 80 3F

Ноль семьдесят пять - как 00 00 40 3F

Вот эти четыре настройки экономики, которые идут в файле подряд, читаются так:

00 00 00 40 - 2.0, как часто списывается содержание зданий
00 00 80 3F - 1.0, возврат за отменённую стройку, 100%
00 00 40 3F - 0.75, возврат за снос, 75%
00 00 00 3F - 0.5, сколько дают за разбор завалов, 50%

GUID - это кровеносная система игры

Закон не пишет "разрешаю построить паб". Он пишет шестнадцатибайтный номер паба

Технология не пишет "стоит 20 угля". Она пишет номер угля - тот самый 0B CF 0B 94... из примера - и рядом число 20

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

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

Поэтому перед каждым эффектом в файле прямо буквами записано имя его класса

Прочитал имя - понял, по какой схеме читать дальше

Ошибся хоть в одном классе - всё, что идёт после, превращается в кашу. Причём не сразу.

Сначала читается нормально, потом чуть криво, а через сто байт у тебя уголь стоит минус восемьсот миллионов, а буря приходит в 1904 году

Грабли

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

Грабли первые. Прабабушки
Классы в игре наследуются друг от друга, как в семье. "Эффект - дать ресурс" - ребёнок "эффекта". А "эффект" - ребёнок "объекта игры". И так далее, вглубь веков
Поля предков читаются раньше собственных
Я честно читал поля класса и его родителя

Про бабушку забыл

Бабушка обиделась

Часть файлов поехала

Пришлось разбираться, где движок проходит всю родословную до седьмого колена, а где останавливается на полпути.

Грабли вторые. Имена-шифровки

У части полей тип хитрый: словарь, массив ровно из десяти штук, массив с ярлыками. Как такое читать, записано только в названии типа внутри игры
Выглядит это название вот так:

.?AV?$RTTIAutoProperty@V$EnumArray@UUITextureTile@@W4eSurvivorsGroupPopulationType@@$02@@@@

Нет, это не пароль от моего вайфая

Это так называемое "искажённое имя" из плюсов. Компилятор упаковывает в него всю информацию о типе, но в таком виде, что читать это может только машина. Ну и я. Спустя время.

Пришлось написать отдельный переводчик с этого языка на человеческий: "сначала 4 байта - количество, потом столько-то пар ресурс + число"

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

Грабли третьи. Оборотень

Один и тот же класс в разных местах записан по-разному. Где-то с дополнительными полями. Где-то без

Одни файлы читаются идеально
Другие ломаются

НА ТОМ ЖЕ САМОМ МЕСТЕ

Я смотрел на это час

Потом ещё час

Потом пошёл за чаем, вернулся и снова смотрел

Помогло только разделить этого оборотня на два варианта в зависимости от того, где он встречается. Днём он один, ночью другой. Как и я, в общем-то

Как понять, что ты не бредишь

Самое опасное в таком деле - получить что-то похожее на правду

Числа выглядят разумно. Названия читаются. Вроде всё работает. Ты уже почти готов идти хвастаться

А на деле ты ошибся в размере одного поля. И всё после него прочитано мусором.

Правдоподобным таким, аккуратным мусором

Поэтому я поставил себе жёсткое правило:

Прочитал файл. Собрал его обратно из того, что понял. Получился тот же самый файл. Байт в байт

Не "примерно такой же". Не "почти". Байт. В байт

Это как разобрать часы и собрать обратно. Лишних винтиков не осталось, часы идут - значит ты понял, как они устроены

Остался хоть один винтик - ты где-то наебался. Даже если стрелки бодро крутятся и тикают

И вот настал момент!

Запускаю проверку на всех файлах

Жду

Первый - совпал
Второй - совпал
Третий...
Сижу, не дышу
...
Пятьдесят первый
Совпал

Все 51 файл настроек разбираются и собираются обратно байт в байт

Программа больше не угадывает. Она понимает каждый байт в каждом файле

Я сидел и совершенно искренне улыбался. Как дурак. В пустой комнате

Зачем всё это было

Модкит не копирует файл кусками, а собирает его заново из понятной структуры. Все служебные числа - "сколько штук в списке", "какой длины строка" - он пересчитывает сам
А значит, можно не только поменять цену паба. Можно:

  • добавить свой закон, свою технологию, своё событие

  • удалить ненужное

  • написать любой текст любой длины

"Я могу поправить одну цифру" превратилось в "я могу сделать свой сценарий"
На этом фундаменте стоит весь модкит

Правда, тогда я ещё не знал, что первый же настоящий мод сломает игре главное меню

А закон, который я сделаю, просто исчезнет из книги законов

Из-за русских букв

Сука

Но это уже совсем другая история

P.S. Ещё раз спасибо Mystik738 за frostract. Без его работы я бы до сих пор стоял перед складом без журнала и плакал


Я добился сильно большего, и разработки до полноценного модкита рабочего модкита осталось совсем немного. Я начал серию постов "дневника разработчика" с целью заранее раскрутить популярность модкита. Он будет бесплатный и доступным каждому. Сейчас 3 моих тестировщика (жена друг и кот) активно собирают свои моды и сами же в них пытаются играть.

Косяков много, попутно исправляются.

Из последнего - я смог засунуть в игру кастомные объекты (постройки) с кастомным функционалом.

Мод, который я поставил себе в качестве точки которой стоит достигнуть тянет на полноценное ДЛС, и в разработке он будет довольно простой любому другому человеку. Он сможет заглянуть в него и понять как и что делалось.

Дальше будет документация

Если тебе интересна игра, интересен моддинг этой игры, следи за серией.

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

Показать полностью 7
Отличная работа, все прочитано!

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества