Реверс-инжиниринг ресурсов игры LHX. Часть 5, заключительная

Интригующая картинка

Интригующая картинка

Второе высшее

В предыдущем посте я поделился своей радостью по поводу того, что сумел‑таки выковырять с LHX модельки игры и привести их в современный вид. И ещё самими модельками. И даже способом, котором я это сделал.

Но после этого я, по инерции, решил ковыряться дальше. Факультативно, так сказать.

В ресурсах LHX есть много других файлов, помимо файлов с точками. И некоторые из них выглядят довольно просто (это обманчиво) — к примеру, файлы сценариев (с описаниями миссии); у других интересные расширения — .fnt (всегда хотел аутентичный шрифт эпохи CGA/EGA — 4 на 6 точек), ну, а уж SECRET.PIC — это просто вызов.

Вызов некрупным планом

Вызов некрупным планом

Вообще, расширения у файлов ресурсов я встретил такие:

  1. drv — файлы типа «IBMDRIVE.DRV» подсказывают, что это скучные драйвера старых железок

  2. fmd и fme — судя по тому, что названия у файлов равны названиям игровых вертолетов, и только имя оспрея (который может летать и как вертолет, и как самолет) носят как fmd, так и единственный fme файл — возможно, это данные о flight model

  3. fnt — очевидно, шрифты. К тому же их всего 2 и один из них зовется 4×6 — именно такой размер в пикселях у шрифта из игры

  4. msk — маска?

  5. pic — ну картинки же

  6. s — мешанина всего, но порой проскакивают куски строк из брифингов к миссиям. Сценарии?

  7. sng — очень напрашивается song, но названия файлов странные — CHOPLIB, CHOPPER, CHOPTAN…

  8. w — названия файлов (ASIA, EUROPE, GULF), совпадающие с названиями локейшенов в игре, плюс названия населенных пунктов внутри файлов заставляют поверить, что w — это world (map).

  9. bin — непонятно что, темнейший лес. binary — ну слишком общо

  10. 2 — смешно, но это самый очевидный формат. Потому что есть только один файл с таким расширением, и он называется «palette»

Первый подход

Начать я решил с картинок — потому что картинка в компьютерной игре 1990 года — это вряд ли что‑то сложнее обычного битмапа. При просмотре содержимого у всех PIC‑файлов первые 4 символа были «PXPK». Как по мне — это очень похоже на заголовок, типа Pictures Packed — намек на запакованную картинку. Логично (ну 90ые же) — кто ж будет в дикий мир выпускать прям битмап в чистом виде? Он жеж большой! Ну тогда возможно это какой‑то прям стандартный старый формат картинок? Надо гуглить!

Вообще, к этому моменту я пришел в твердому выводу, что в наше время абсолютно любую проблему надо сначала прогуглить — интернет существует уже давно, масса знаний уже накопилась даже на довольно экзотические вопросы. Собственно, эта экзотика мне и выдавалась, вся — нерелеватная, но местами интересная. А порой и сбивающая с пути — я нашел Deluxe Paint, навернутый графический редактор 1990 года выпуска, который был выпущен EA, и более того, в его создании принимал участие Brent Iverson — человек, написавший LHX!

Подсказка о том, кто написал (ну или в крайнем случае руководил всем) LHX

Подсказка о том, кто написал (ну или в крайнем случае руководил всем) LHX

Но, сохраненные в нем файлы не имели в заголовке букв PXPK. Да и сами начальные байты не особо совпадали. Жаль, но… Пришлось на время отступиться.

С остальными расширениями ситуация была такая же. Шрифты с расширением fnt и упоминанием DeluxeFonts внутри? Возможно, но непонятно что там за структуру ожидать в принципе. Другие загадочные файлы bin? Тем более неясно, о чем это. Ну и так далее. Опять был нужен какой‑то инструмент, который делает очень полезные для мега‑узкого круга людей вещи — берет набор байт и разными способами их визуализирует — глядишь, подсказка и проявится.

Интермедия 3  

И я такой инструмент нашел — шикарные hobbits. Он берет поток бит — потому и Hobbits — например, из файла, и отображает его в разных видах:

Вверху - возможные способы представления битового потока, чуть пониже - очевидная картинка заданная битами - ноликами и единичками. А вот байтовый поток, содержащий эти биты местами выглядит так - 1F9C07003F и так далее. Без такого инструмента - ни в жысть не догадаешься, что он описывает картинку.

Вверху — возможные способы представления битового потока, чуть пониже — очевидная картинка заданная битами — ноликами и единичками. А вот байтовый поток, содержащий эти биты местами выглядит так — 1F9C07 003F и так далее. Без такого инструмента — ни в жысть не догадаешься, что он описывает картинку.

Но и он не помог — все файлы, кроме «текстовых», показывали просто какую‑то мешанину. «Текстовые» в кавычки я взял потому, что там явно проскакивали текстовые фразы (на английском, естественно). Но проскакивали они в основном в начале‑середине файла, а под конец нередко ничего читаемого там уже не было:

Выглядит как шифровка из Центра

Выглядит как шифровка из Центра

Второй подход

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

Плюс, содержимое этих текстовых файлов наводило на мысль, что, возможно, эти файлы упакованы.

Совсем высокоуровневый принцип простых видов упаковки файлов я знал: «иди по файлу и строй словарь встреченных слов/буквосочетаний, заменяй следующие встреченные такие же на ссылку на словарь». Собственно, характер искромсанности файлов (более‑менее читаемо в начале, полный финиш в конце) на такой принцип и намекал — ведь к концу процесса упаковки словарь разрастается все больше, соответственно, и ссылок вместо текста тоже становится все больше. Но дьявол всегда кроется в деталях.

Так или иначе, я полез гуглить алгоритмы упаковки, придуманные до 90-го года (вот где контекст вместо осложнений принес упрощение). Таких оказалось не так, чтобы много, основой для многих оказались академические LZ77 и LZ78 (совместное творчество Абрахама Лемпеля и Джейкоба Зива 1977 и 1978 года соответственно). Тот, который вроде как походил (при ручной проверке подстановкой букв по адресам/смещениям) на мой случай, назывался LZSS.

Основные идеи LZSS

В целом упаковка по этому алгоритму (с учетом контекста) работает следующим образом.

Во‑первых, результаты упаковки пишутся в отдельный, выходной файл.

Во‑вторых, словарь как таковой не создается. Сам исходный текст и есть словарь. Ну точнее, не весь текст, а только его часть — в этом алгоритме его еще называют «окно», и оно идет от начала текста до текущей позиции курсора.

Процесс упаковки идет так:

  1. В словаре (он же «окно» до курсора) ищется последовательность, которая совпадает с последовательностью фиксированной длины (здесь эта длина = 19, позже объясню) идущей после курсора (кандидат на упаковку). Еще раз, до курсора — словарь, после курсора — возможно упаковываемый текст. Куда включается сам курсор — для объяснения несущественно. Если:

    1. Совпадение найдено — то в выходной файл записывается ссылка на совпадение. Это пара чисел (и это 2 байта, но тут есть подвох, позже опишу) — «Расстояние до совпадающего сочетания из словаря» и «Длина сочетания», после чего курсор сдвигается вправо на значение, равное «Длине сочетания», увеличивая длину окна. Или — если длина окна уже максимальна — сдвигая его. И все начинается заново.

    2. Если не найдено — то длина рассматриваемой последовательности (той, что справа от курсора) уменьшается на 1. Если:

      1. Длина получилась равной 2 (тоже позже объясню) — то считается, что совпадения не найдено, поэтому в выходной файл пишется 1 символ — тот, что справа от окна. Курсор сдвигается вправо на 1 этот символ — и возврат в пункт 1 с дефолтной длиной рассматриваемой последовательности (которая 19).

      2. Длина еще не 2 — возврат в пункт 1, но с уменьшенной длиной рассматриваемой последовательности.

  2. Чтобы потом при распаковке разобраться, где тут настоящий текст, а где ссылки — этот алгоритм использует своеобразные дорожные столбы: в упакованном потоке байт (то есть пропустив все заголовки), начиная с самого первого байта, периодически встречаются байты разметки. Или «байты флагов», по терминологии из Интернета. Повторюсь, первый байт в потоке — тоже байт флагов. Биты этих байтов флагов содержат инфу относительно того, как при распаковке интерпретировать байты (которых будет от 8 до 16), следующие непосредственно за байтом флагов. Если очередной бит байта флагов:

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

    2. равен противоположному значению (в нашем случае, 0) — то два следующих байта — это ссылка, и в выходной файл файл надо скопировать текст указанной длины из указанного места в предыдущих байтах потока, после чего надо сдвинуться в упакованном потоке на 2 байт дальше. Потому‑то и количество байт, размеченных байтом флагов, может быть от 8 (все байты — неупакованный текст) до 16 (все байты — ссылки).

Как структура архива видна даже в текстовом просмотре (кодировка - ANSI-1251). Зеленые - байты флагов.

Как структура архива видна даже в текстовом просмотре (кодировка — ANSI-1251). Зеленые — байты флагов. «я»- это FF в ANSI-1251. То есть байт, у которого все биты = 1. Значит, все следующие 8 байт — текст, и ссылок там нет. Поэтому за «я» — всегда 8 читаемых символов. А вот последний зеленый символ — непечатаемый, и в Hex-виде видно, что он равен 7F, что в битовом представлении = 0111 1111. Старший (последний) бит равен 0 — значит, последние 2 байта  (подчеркнуты) в последовательности за этим байтом флагов — это ссылка, а всего байтов в последовательности — 9.

Парочка уточняющих деталей.

  1. Окно не сразу становится максимальной длины. Сначала, когда курсор стоит в начале файла, для алгоритма окно начинается до от начала файла и имеет размер 3 (потому что вспомним, что минимальная длина совпадающей последовательности = 3). И алгоритм считает, что оно заполнено машинными нулями. Можно чем угодно, конечно, но нули — вполне себе компромиссная идея. По мере продвижения курсора вправо, окно растет и в какой‑то момент (когда его реальная длина становится на 2 меньше официальной) — постепенно выезжает из начала файла и уже никогда туда не возвращается.

  2. Пара значений («Расстояние до совпадающего сочетания из словаря» и «Длина сочетания») ссылки — это 2 байта. Но это не пара байт ака 2 числа с макс. значением = 256. Для расстояния этого маловато, а для длины последовательности — многовато. Поэтому 16 бит этих 2 байт решили резать на 12-битное и 4-битное числа. И поэтому, максимальное расстояние = 2 в 12-й = 4096 байт. А максимальная длина совпадения = 2 в 4-й = 16 байт. Но тут мы помним, что меньше 3 байт мы не пакуем, поэтому алгоритм считает, что к записанной длине надо всегда добавить 3. 16+3 = 19 — вот откуда такая максимальная длина упаковываемой последовательности.

  3. Так почему же пакуются минимум 3 байта? Я думаю, тут уже очевидно — если ссылка весит 2 байта, то нету смысла менять одну двухбайтную последовательность на другую. Поэтому и 3.

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

В заголовке архива указывается размер распакованного файла в байтах и пара ноликов, после чего идет сам архив в виде уже описанной выше последовательности кортежей вида «байт флагов, байты контента». Завершающий кортеж «байт флагов, байты потока» не обязательно будет содержать байты потока для всех 8 битов флагов (просто потому, что распаковываемый файл закончился), но у бита из байта флагов только 2 состояния — текст/ссылка. Состояния «конец» нету — так что указание размера распакованного файла очень помогает понять, что распаковка завершена, без внезапных эксепшенов.

Как видно (особенно выше, под катом), тут масса нюансов, половину из которых пришлось реверсить вручную, потому что задокументированные в Интернете варианты — не про фиксированные окна. Все это сопровождалось написанием распаковщика (на C#, естественно, зачем коней менять‑то). Все это постоянно тестировалось на упакованных «текстовых» файлах (с учетом того, что их было сравнительно немного, а упаковщика сделать новые и сразу правильные, естественно, не было), и заработало это все далеко не сразу. Но заработало. Текстовые файлы полностью корректно распаковались и были встречены восторженными аплодисментами. Победа!

А спустя 2 минуты после восторженных аплодисментов я абсолютно шутки ради скормил распаковщику другие файлы ресурсов — и шутка удалась. Практически все файлы полностью корректно распаковались.

Кроме PNT и PIC.

А теперь — картинки

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

А PIC — потому что у них, перед заголовком архива (размер будущего файла и два ноля) стоит еще заголовок формата картинки. Тот самый, начинающийся с PXPK (что бы это ни значило). Заголовок довольно тривиального формата — «сигнатура PXPK, сколько цветов у картинки (256 или 16 или 4, ну то есть VGA/EGA/CGA), размер картинки по икс, мусор, размер картинки по игрек, нули». А потом идет сам архив, в обычном формате. Тот самый практически простейший битмап — матрица цветов пикселей.

При этом цветность (256/16/4 цветов) налагает свою специфику, позволяя упаковать картинку еще больше, но другим подходом.

256 цветов требует 2 в 8-й значений, то есть 8 бит или 1 байт на каждый пиксель. А вот 4 цвета — только 2 во 2-й значений, то есть 2 бита (четверть байта).

Поэтому, как вы уже догадались, у 4-цветной CGA картинки каждый байт содержит в себе цвета аж 4 пикселей подряд, у 16-цветной EGA картинки каждый байт — это цвет двух пикселей, а вот у 256-цветной VGA никакой упаковки не выйдет — каждый байт это цвет одного пикселя.

И только когда я писал код этой дораспаковки в своем распаковщике (ссылка на github), я вспомнил, как детстве составлял таблицы цветов для редактирования спрайтов в CGA‑игре Goody и понял наконец, почему там одна цифра обозначала цвет сразу двух пикселей.

Так или иначе — картинки я тоже распаковал и получил те самые битмапы с учетом цветности. В битмапе указывается номер цвета. А сами цвета хранятся в том самом «palette.2» в самом простейшем формате — RGB‑значения компонент для 256 цветов, записанные просто в ряд. И размер у него = 256×3 = 768 байт.

Интересно, что CGA‑шных битмапов оказалось только 4 — это картинки кокпитов. И кокпиты — это единственные картинки (из всего списка картинок), которые постоянно показываются во время отрисовки именно самого полета.

Спрайты пробития кабины нарисованы с минимальной цветностью = 16.

Спрайты пробития кабины нарисованы с минимальной цветностью = 16.

И спрайты разбитого экрана - тоже.

И спрайты разбитого экрана — тоже.

И вот они же в игре в 4-цветном (CGA) режиме.

И вот они же в игре в 4-цветном (CGA) режиме.

Я так понимаю, все картинки для CGA программа на лету даунгрейдит из EGA‑версии всякими там подменами сплошного цвета на паттерны. Но это немного затратно, и поэтому кокпиты все же хранятся готовыми.

Кусок карты мира из игры в CGA-режиме

Кусок карты мира из игры в CGA‑режиме

Он же, но в EGA-режиме. Цвета поменялись, формы - нет.

Он же, но в EGA‑режиме. Цвета поменялись, формы — нет.

Писать конвертер из битмапов в современный графический формат файла мне было лень — и я сделал конвертацию из битмапов (с учетом палитры) в png прям в Mathematica.

Он же, но в EGA‑режиме. Цвета поменялись, формы — нет.

Вот такой у меня элементарный получился конвертер. Только имя файла должно быть в формате примерно таком формате — «cp_blk4-picture-320×200×4-unpacked.png», чтоб не возиться с ручным указанием цветности и размеров. И у палитры значения цветов пришлось нормализовать, поделив на 256 (3 строка).

Пример результата - картинка из ресурсного файла со всеми медальками игры в VGA-варианте.

Пример результата — картинка из ресурсного файла со всеми медальками игры в VGA‑варианте.

Остатки роскоши

А что же остальные типы файлов?

*.s — действительно оказались файлами сценариев, причем со всякими названиями переменных —, но тут сюрприза не было, я ж на них реверсил алгоритм распаковки так‑то.

*.bin — некоторые из них — это библиотеки строк. Но опять же, я и на них реверсил алгоритм. Ну и вообще название «strings.bin» как бы намекает. И «strings2.bin» — тоже. А вот всякие «AH‑MCGA.BIN» я не раскусил

*.fnt — тут hobbits показал, что fnt — это действительно шрифты, причем в битовом виде. Их формат я расковыривать не стал, потому что лень — да и так видно же:

Битовый растр файла

Битовый растр файла »4×6.fnt»

Битовый растр файла

Битовый растр файла «PROP.fnt»

*.msk — действительно оказались битовыми масками, очевидно для клиппинга 3д-картинки в кокпите:

Вырезанные скрины масок из hobbits поверх картинки кокпита

Вырезанные скрины масок из hobbits поверх картинки кокпита

*.w — я начинал с ними ковыряться, и на 98% уверен, что это карты уровней, но я их не расковырял.

Содержимое остальных типов (*.drv, *.fmd, *.fme, *.sng) — так и осталось для меня загадкой просто потому, что они меня не заинтересовали.

Неожиданный конец

Ну и на закуску.

Спустя какое‑то время после того, как я закончил всю эту эпопею, мне в голову пришла довольно тривиальная, в сущности, мысль.

Electronic Arts — это игровая компания, про которую знают до сих пор — ведь она клепает игрушки тоннами. И вряд ли даже в 90-х она под каждую свою игру писала уникальный вспомогательный инструментарий. Может, мой распаковщик может распаковать что‑нибудь еще?

Я зашел на old‑games.ru, отсортировал игры, выпущенные EA, по годам и нашел еще парочку летных симуляторов, выпущенных примерно в то же время (и о которых я никакого абсолютно понятия никогда в жизни не имел):

  • Stormovik: Su-25 (1990) (какая ирония, именно с Su-25 я начал расковыривать LHX)

  • Chuck Yeager«s Air Combat (1991)

Некоторые из игр EA 1990-91 года.

Некоторые из игр EA 1990–91 года.

Я их скачал,  быстренько по ним пробежался и:

  1. У них обоих были файлы библиотек, которые отлично распаковались распаковщиком библиотек (github).

  2. У них обоих в экзешнике сохранилась структура описания модели.

  3. У Su-25 оказались упакованы файлы точек, а у файлов точек из CYAC — вообще изменился формат заголовка (между указателями добавились разделители u2, но это неточно)

  4. Файлы ресурсов успешно распаковались, картинки успешно сконвертировались, у CYAC палитру нормализовать надо делением на 64, а не на 256.

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

Я же только вставлю картинку из титров Chuck Yeager’s Air Combat — именно здесь я впервые увидел имя человека, который, скорее всего, столько лет назад и намоделил эти милые коробочки, которые назывались M113 и House6 (ну или как минимум понарисовывал для LHX все эти душевные картинки с орущим пилотом) — Cynthia Hamilton.

Экран титров Chuck Yeager’s Air Combat с именем создательницы (уверен на 86%) всех тех моделек, которые я так долго выковыривал.

Экран титров Chuck Yeager«s Air Combat с именем создательницы (уверен на 86%) всех тех моделек, которые я так долго выковыривал.

Непродолжительный гуглеж привел меня к вот этой странице.

И вот к этой:

88b79b084336beffd9e0dadb3811f151.png

 И вот к этому отрывку в описании игры Budokan:»The graphics team of Mike Kosaka, Nancy Fong, Mike Lubuguin, Cynthia Hamilton and Connie Braat (animations)also worked together onKings of the Beach, Lakers vs Celtics and the NBA Playoffs, and Ski or Die.

Connie and Cynthia also worked on the graphics for LHX Attack Chopper and Stormovik: SU-25 Soviet Attack Fighter, with Rick Tiberi doing the programming.»  Выделения и ссылки — мои.

Конец.

Тот самый SECRET.PIC

Тот самый SECRET.PIC

Все ссылки в одном месте:

  1. Программы:

    1. Распаковщик.LIB файлов 

    2. Распаковщик файлов ресурсов для некоторых игр EA

    3. Конвертер 3D‑моделек из бинарного формата EA в формат OBJ

  2. Извлеченные из LHX модельки:

    1. В бинарном формате EA

    2. В формате OBJ с наиболее смотрибельными настройками

    3. Модельки во всех вариантах одним архивом

© Habrahabr.ru