Второе высшее
В предыдущем посте я поделился своей радостью по поводу того, что сумел‑таки выковырять с LHX модельки игры и привести их в современный вид. И ещё самими модельками. И даже способом, котором я это сделал.
Но после этого я, по инерции, решил ковыряться дальше. Факультативно, так сказать.
В ресурсах LHX есть много других файлов, помимо файлов с точками. И некоторые из них выглядят довольно просто (это обманчиво) — к примеру, файлы сценариев (с описаниями миссии); у других интересные расширения ‑.fnt (всегда хотел аутентичный шрифт эпохи CGA/EGA — 4 на 6 точек), ну а уж SECRET.PIC — это просто вызов.
Вообще, расширения у файлов ресурсов я встретил такие:
drv — файлы типа «IBMDRIVE.DRV» подсказывают, что это скучные драйвера старых железок
fmd и fme — судя по тому, что названия у файлов равны названиям игровых вертолетов, и только имя оспрея (который может летать и как вертолет, и как самолет) носят как fmd, так и единственный fme файл — возможно, это данные о flight model
fnt — очевидно, шрифты. К тому же их всего 2 и один из них зовется 4×6 — именно такой размер в пикселях у шрифта из игры
msk — маска?
pic — ну картинки же
s — мешанина всего, но порой проскакивают куски строк из брифингов к миссиям. Сценарии?
sng — очень напрашивается song, но названия файлов странные — CHOPLIB, CHOPPER, CHOPTAN…
w — названия файлов (ASIA, EUROPE, GULF), совпадающие с названиями локейшенов в игре, плюс названия населенных пунктов внутри файлов заставляют поверить, что w — это world (map).
bin — непонятно что, темнейший лес. binary — ну слишком общо
2 — смешно, но это самый очевидный формат. Потому что есть только один файл с таким расширением, и он называется «palette»
Первый подход
Начать я решил с картинок — потому что картинка в компьютерной игре 1990 года — это вряд ли что‑то сложнее обычного битмапа. При просмотре содержимого у всех PIC‑файлов первые 4 символа были «PXPK». Как по мне — это очень похоже на заголовок, типа Pictures Packed — намек на запакованную картинку. Логично (ну 90ые же) — кто ж будет в дикий мир выпускать прям битмап в чистом виде? Он жеж большой! Ну тогда возможно это какой‑то прям стандартный старый формат картинок? Надо гуглить!
Вообще, к этому моменту я пришел в твердому выводу, что в наше время абсолютно любую проблему надо сначала прогуглить — интернет существует уже давно, масса знаний уже накопилась даже на довольно экзотические вопросы. Собственно, эта экзотика мне и выдавалась, вся — нерелеватная, но местами интересная. А порой и сбивающая с пути — я нашел Deluxe Paint, навернутый графический редактор 1990 года выпуска, который был выпущен EA, и более того, в его создании принимал участие Brent Iverson — человек, написавший LHX!
Но, сохраненные в нем файлы не имели в заголовке букв PXPK. Да и сами начальные байты не особо совпадали. Жаль, но… Пришлось на время отступиться.
С остальными расширениями ситуация была такая же. Шрифты с расширением fnt и упоминанием DeluxeFonts внутри? Возможно, но непонятно что там за структуру ожидать в принципе. Другие загадочные файлы bin? Тем более неясно, о чем это. Ну и так далее. Опять был нужен какой‑то инструмент, который делает очень полезные для мега‑узкого круга людей вещи — берет набор байт и разными способами их визуализирует — глядишь, подсказка и проявится.
Интермедия 3
И я такой инструмент нашел — шикарные hobbits. Он берет поток бит — потому и Hobbits — например, из файла, и отображает его в разных видах:
Но и он не помог — все файлы, кроме «текстовых», показывали просто какую‑то мешанину. «Текстовые» в кавычки я взял потому, что там явно проскакивали текстовые фразы (на английском, естественно). Но проскакивали они в основном в начале‑середине файла, а под конец нередко ничего читаемого там уже не было:
Второй подход
И я решил взяться за них — а что мне оставалось? Текст все же гораздо более понятный домен, чем драйвера, например.
Плюс, содержимое этих текстовых файлов наводило на мысль, что, возможно, эти файлы упакованы.
Совсем высокоуровневый принцип простых видов упаковки файлов я знал: «иди по файлу и строй словарь встреченных слов/буквосочетаний, заменяй следующие встреченные такие же на ссылку на словарь». Собственно, характер искромсанности файлов (более‑менее читаемо в начале, полный финиш в конце) на такой принцип и намекал — ведь к концу процесса упаковки словарь разрастается все больше, соответственно, и ссылок вместо текста тоже становится все больше. Но дьявол всегда кроется в деталях.
Так или иначе, я полез гуглить алгоритмы упаковки, придуманные до 90-го года (вот где контекст вместо осложнений принес упрощение). Таких оказалось не так, чтобы много, основой для многих оказались академические LZ77 и LZ78 (совместное творчество Абрахама Лемпеля и Джейкоба Зива 1977 и 1978 года соответственно). Тот, который вроде как походил (при ручной проверке подстановкой букв по адресам/смещениям) на мой случай, назывался LZSS.
Основные идеи LZSS
В целом упаковка по этому алгоритму (с учетом контекста) работает следующим образом.
Во‑первых, результаты упаковки пишутся в отдельный, выходной файл.
Во‑вторых, словарь как таковой не создается. Сам исходный текст и есть словарь. Ну точнее, не весь текст, а только его часть — в этом алгоритме его еще называют «окно», и оно идет от начала текста до текущей позиции курсора.
Процесс упаковки идет так:
-
В словаре (он же «окно» до курсора) ищется последовательность, которая совпадает с последовательностью фиксированной длины (здесь эта длина = 19, позже объясню) идущей после курсора (кандидат на упаковку). Еще раз, до курсора — словарь, после курсора — возможно упаковываемый текст. Куда включается сам курсор — для объяснения несущественно. Если:
Совпадение найдено — то в выходной файл записывается ссылка на совпадение. Это пара чисел (и это 2 байта, но тут есть подвох, позже опишу) — «Расстояние до совпадающего сочетания из словаря» и «Длина сочетания», после чего курсор сдвигается вправо на значение, равное «Длине сочетания», увеличивая длину окна. Или — если длина окна уже максимальна — сдвигая его. И все начинается заново.
-
Если не найдено — то длина рассматриваемой последовательности (той, что справа от курсора) уменьшается на 1. Если:
Длина получилась равной 2 (тоже позже объясню) — то считается, что совпадения не найдено, поэтому в выходной файл пишется 1 символ — тот, что справа от окна. Курсор сдвигается вправо на 1 этот символ — и возврат в пункт 1 с дефолтной длиной рассматриваемой последовательности (которая 19).
Длина еще не 2 — возврат в пункт 1, но с уменьшенной длиной рассматриваемой последовательности.
-
Чтобы потом при распаковке разобраться, где тут настоящий текст, а где ссылки — этот алгоритм использует своеобразные дорожные столбы: в упакованном потоке байт (то есть пропустив все заголовки), начиная с самого первого байта, периодически встречаются байты разметки. Или «байты флагов», по терминологии из Интернета. Повторюсь, первый байт в потоке — тоже байт флагов. Биты этих байтов флагов содержат инфу относительно того, как при распаковке интерпретировать байты (которых будет от 8 до 16), следующие непосредственно за байтом флагов. Если очередной бит байта флагов:
равен, например, 1, то следующий рассматриваемый байт потока — это неупакованный текст и должен быть переписан в выходной файл в неизменном виде, после чего надо сдвинуться в упакованном потоке на 1 байт дальше.
равен противоположному значению (в нашем случае, 0) — то два следующих байта — это ссылка, и в выходной файл файл надо скопировать текст указанной длины из указанного места в предыдущих байтах потока, после чего надо сдвинуться в упакованном потоке на 2 байт дальше. Потому‑то и количество байт, размеченных байтом флагов, может быть от 8 (все байты — неупакованный текст) до 16 (все байты — ссылки).
Парочка уточняющих деталей.
Окно не сразу становится максимальной длины. Сначала, когда курсор стоит в начале файла, для алгоритма окно начинается до от начала файла и имеет размер 3 (потому что вспомним, что минимальная длина совпадающей последовательности = 3). И алгоритм считает, что оно заполнено машинными нулями. Можно чем угодно, конечно, но нули — вполне себе компромиссная идея. По мере продвижения курсора вправо, окно растет и в какой‑то момент (когда его реальная длина становится на 2 меньше официальной) — постепенно выезжает из начала файла и уже никогда туда не возвращается.
Пара значений («Расстояние до совпадающего сочетания из словаря» и «Длина сочетания») ссылки — это 2 байта. Но это не пара байт ака 2 числа с макс. значением = 256. Для расстояния этого маловато, а для длины последовательности — многовато. Поэтому 16 бит этих 2 байт решили резать на 12-битное и 4-битное числа. И поэтому, максимальное расстояние = 2 в 12-й = 4096 байт. А максимальная длина совпадения = 2 в 4-й = 16 байт. Но тут мы помним, что меньше 3 байт мы не пакуем, поэтому алгоритм считает, что к записанной длине надо всегда добавить 3. 16+3 = 19 — вот откуда такая максимальная длина упаковываемой последовательности.
Так почему же пакуются минимум 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 — это картинки кокпитов. И кокпиты — это единственные картинки (из всего списка картинок), которые постоянно показываются во время отрисовки именно самого полета.
Я так понимаю, все картинки для CGA программа на лету даунгрейдит из EGA‑версии всякими там подменами сплошного цвета на паттерны. Но это немного затратно, и поэтому кокпиты все же хранятся готовыми.
Писать конвертер из битмапов в современный графический формат файла мне было лень — и я сделал конвертацию из битмапов (с учетом палитры) в png прям в Mathematica.
Остатки роскоши
А что же остальные типы файлов?
*.s — действительно оказались файлами сценариев, причем со всякими названиями переменных — но тут сюрприза не было, я ж на них реверсил алгоритм распаковки так‑то.
*.bin — некоторые из них — это библиотеки строк. Но опять же, я и на них реверсил алгоритм. Ну и вообще название «strings.bin» как бы намекает. И «strings2.bin» — тоже. А вот всякие «AH‑MCGA.BIN» я не раскусил
*.fnt — тут hobbits показал, что fnt — это действительно шрифты, причем в битовом виде. Их формат я расковыривать не стал, потому что лень — да и так видно же:
*.msk - действительно оказались битовыми масками, очевидно для клиппинга 3д-картинки в кокпите:
*.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)
Я их скачал, быстренько по ним пробежался и:
У них обоих были файлы библиотек, которые отлично распаковались распаковщиком библиотек (github).
У них обоих в экзешнике сохранилась структура описания модели.
У Su-25 оказались упакованы файлы точек, а у файлов точек из CYAC — вообще изменился формат заголовка (между указателями добавились разделители u2, но это неточно)
Файлы ресурсов успешно распаковались, картинки успешно сконвертировались, у CYAC палитру нормализовать надо делением на 64, а не на 256. Собственно, КДПВ - это картинка из Su-25.
На этом я остановился, потому что надо знать меру. Но, возможно, кто‑то захочет продолжить ковыряться в этих старых, старых байтах и разгадывать загадки давно минувших лет.
Я же только вставлю картинку из титров Chuck Yeager's Air Combat — именно здесь я впервые увидел имя человека, который, скорее всего, столько лет назад и намоделил эти милые коробочки, которые назывались M113 и House6 (ну или как минимум понарисовывал для LHX все эти душевные картинки с орущим пилотом) — Cynthia Hamilton.
Непродолжительный гуглеж привел меня к вот этой странице.
И вот к этой:
И вот к этому отрывку в описании игры Budokan: «The graphics team of Mike Kosaka, Nancy Fong, Mike Lubuguin, Cynthia Hamilton and Connie Braat (animations) also worked together on Kings 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.» Выделения и ссылки — мои.
Конец.
Все ссылки в одном месте: