Грустная история о том, как я переделывал игру Inferno под ZX Spectrum 128 — и не преуспел…
Давным‑давно я где‑то читал интернет‑нытьё зарубежных спектрумистов (а раз уж нытьё — то на 98% это были русские), что игра Inferno, которую написал Universum и издала Proxima в 1993 году, вроде выглядит интересно, но на том «странном западнославянском тарабарском». Никто за пределами наших двух стран поэтому не знает, о чём в игре речь, а что хуже — не понимают, как ей пользоваться: какую команду написать, какое оружие выбрать и какое оружие для чего служит.
Inferno я, правда, никогда не умел играть особенно хорошо — но любил! Игра для меня имела параметры и проработанность тех амиговских, которыми я глазками пожирал у друзей. Часто в детстве я её включал, смаковал интро и пролетал пару уровней, пока меня «не сняли». Даже саундтрек Scalex у меня был записан на кассету в walkman.
Так что вы наверняка понимаете: эмоциональная привязанность к этой игре у меня реально сильная. Последние лет десять я ставил себе одной из миллиарда личных жизненных целей, которые хочу оставить этой планете, — полностью дизассемблировать Inferno и перевести на английский. И тогда эта игра, подобно тому как «Krajina tieňov» вышла как «Twilight», попала бы в поле зрения всего мира. На Forever я у Solarisa подтвердил, что ничего подобного не существует и опереться на чью‑то работу не могу. Разговор подслушал и братан Roman — и подбросил мысль: а нельзя ли, используя сегодняшние фантастически эффективные методы сжатия, всю игру запихнуть в ZX Spectrum 128. Это меня как‑то подстегнуло, и после Forever, где‑то весной, когда на работе уже начались скрипы и я стал подозревать, что буду первым на вылет, я принялся разбирать INFERNO.TAP…
Toolset
Кто уже видел исходники моего Speccy‑творчества (все они open‑source на github), знает, что вспомогательные скрипты я пишу на Node.js. Ими генерирую таблицы или код, рисующий графику, или, например, собираю блоки ассетов воедино. Для этой переделки появился depackblock.js, который над упакованной бинаркой запускает эмуляцию Z80 и вырезает из неё распакованный блок — чем бы тот ни был запакован. При 15 блоках Inferno, пакованных Mr. Pack II «с дождиком», или картинках Pressor‑ом, мне стоило написать этот маленький скрипт, который пробегает меньше чем за секунду, вместо того чтобы распаковывать каждый блок отдельно в эмуляторе.
Когда блоки так лежали «кучкой», можно было начинать дизассемблирование. При беглом осмотре выяснилось, что блоки отдельных миров содержат тексты и графику, а если и какой‑то код — то скорее скриптовый для данной миссии, так что с моей точки зрения несущественный. Это упрощало ситуацию: главный загрузчик, управляющая консоль и движок игры — в главном блоке и нигде больше. Я запустил дизассемблер IDA и пометил участки памяти, про которые уже знал, что это шрифты, спрайты или иная графика — чтобы IDA понимала: это данные. Начал с интро, которое у Universum лежит отдельной частью в будущей рабочей области движка: то есть в начале только запускается, а потом перезаписывается буферами и загруженными мирами. Это интро — в основном куча векторов, текста и музыка, на саму игру никак не влияет, поэтому дизассемблировалось очень быстро и без необходимости идеально понимать код.
Завёл репозиторий, вложил Regression demo kernel, слегка подстроил build‑систему под нужды игры и раскрутил интро. Последующее дизассемблирование основного кода игры заняло несколько дней. Идентифицировать куски кода, перемешанные с таблицами и графическими шаблонами, понять и назвать метки мне нужно было в основном в загрузочной части и «главном меню» (в случае Inferno — консоль бортового компьютера). Разумеется и ожидаемо, после вложения в репозиторий это не работало, и до бегущего состояния я это отлаживал и расследовал ещё следующие 2–3 недели.
Поскольку хотел всё запихнуть в 128‑ку, запаковал с помощью ZX0 все 7 догружаемых блоков — но сумма их упакованных длин, достигающая 75 килобайт, уже подняла предупреждающий палец: похоже, не выйдет… но я ещё не сдавался! :)
С темой toolset связан ещё один ловкий кусок, который я для этого разработал и отладил. Поскольку отдельные упакованные миры имеют от 10 до 12 килобайт, их никак разумно нельзя было разложить по банкам так, чтобы распаковать «одним куском». К тому же рабочая область Inferno — с адреса 44000 до конца памяти, то есть при распаковке должна быть подстраничена нулевая страница. Поэтому появился «ZX0 128k depacker». Он работает с содержимым банков 128‑ки как с одним цельным пространством. Рутина ожидает во входном регистре HL абсолютный указатель в это пространство, сдвинутый вправо на 1 бит. Из значения #891A получается адрес #1234 в 4‑й странице, то есть физически #D234. По логике рутина ожидает блоки, выровненные на чётный адрес — но это не мешает. В регистре DE затем, разумеется, стандартно передаю адрес в RAM, куда блок распаковать, а в аккумуляторе — номер страницы, которая при записи должна быть подстраничена. Распаковка идёт между исходной и целевой страницами; адреса могут спокойно перекрываться, потому что рутина каждое чтение и запись сторожит и выставляет нужную страницу. Также при чтении упакованного блока рутина сама переходит в следующую страницу, так что блоки могут быть «разлиты» через границы страниц. И оно реально работает!
Перевод
На минуту ещё отклонюсь к теме, которую уже задел в начале, — к самому переводу игры на международно понятный язык. Я использовал опыт прежних работ и применил для этого обученную языковую модель искусственного интеллекта. Сначала я её набрифовал: мол, пойдёт чешский sci‑fi текст с военной, авиационной и морской терминологией (звания, названия кораблей, оружейные системы, ход миссий…), для верности научил AI ещё паре жаргонных слов и собственных имён, которыми пользуется Universum, — и пошёл готовить тексты. Для этого снова напилил Node.js‑скрипт, который из дизассемблированных исходников «выковыривал» все тексты и конвертировал Desktop‑encoding национальных символов в UTF‑8.
Затем прогнал текстовый файл через языковую модель. Должен признать: у AI действует то же правило Парето, что и во всём творческом. Оно помогло сделать 80% работы, а оставшиеся 20% заняли ещё вторую половину времени. Вывод я фраза за фразой, предложение за предложением тщательно прошёл. Где‑то казалось уместнее другое слово, где‑то я перефразировал предложение, в другом месте удобнее оказалось другое время, чем AI понял из оригинала, а в паре мест чешский оригинал был излишне «словоохотливым» и повторял один смысл двумя предложениями подряд, только другими словами. Благодаря этому результат красиво сократился, описания миссий стали прямее, без «подливы». Отдельно потом переводил названия и описания оружия — это немного потрепало творческое полушарие, потому что нужно было уложиться в лимит символов. Но ощущение хорошее, и думаю, в целом получилось. В любом случае это стоило бы посмотреть ещё нескольким парам глаз. Желающие — откликайтесь. ;)
Текущее состояние
«Inferno 128» сейчас функциональна и играбельна. Содержит заглавную картинку, интро с музыкой, консоль бортового компьютера, все 6 «миров» и победное outro с музыкой. Миры запускаются практически мгновенно, но без их вступительного экрана и музыки — потому что места на это просто нет. Несмотря на то что картинки и музыку мне удалось фантастически упаковать, для них 15 килобайт я уже найти не могу. Без этого всё кажется так, будто я на чью‑то работу… наплевал. Что с этим? Решений вижу по сути только два:
-
После загрузки игры в памяти будут 4 мира (Земля, Луна, Фиалка, NGC32/4). После их прохождения — та самая морока: с носителя догрузится содержимое ещё примерно 2–3 банков по 128k, где будут последние 2 мира (Zynaps, Orga) и победное outro. Но мне это кажется кривым: 128k‑версия, которая всё равно догружается. К тому же если представить технически: когда на Zynaps игрок умирает и хочет играть снова, я должен уметь загрузить содержимое исходных 5 страниц, и т.д. и т.п… Не буду этого делать!
-
Это будет не «Inferno 128», а «Inferno Remastered». Миры будут догружаться, как и раньше, но всё будет красиво по‑английски, по графике и UX причёсано. Win‑win…
У меня даже есть несколько новых идей, которые могли бы сдвинуть игру дальше:
- На 128k‑машине музыка, играющая перед загрузкой мира, продолжала бы звучать и после показа вступительной картинки. То есть после возврата в консоль бортового компьютера и показа брифинга миссии она продолжалась бы и после одного‑двух повторов автоматически затихала. Было бы интереснее, чем нынешний обрыв.
- Игра на 128k давала бы возможность сохранять позиции и в память — для более быстрой загрузки после смерти пилота.
- В начале игры (после интро) я бы реализовал выбор сложности, и появился бы «Story mode». Это современное название более лёгкого режима в играх для людей, которые хотят насладиться историей и не дать стандартной сложности себя доконать. В случае Inferno это была бы своего рода бессмертность пилота: после аварии или сбития игрок возвращался бы в консоль и мог снова сыграть ту же миссию, в которой погиб. Статистика и снаряжение — в том состоянии, в каком он в миссию входил. Так это было бы проходимо и для праздничных любителей шутеров, и не пришлось бы после каждого сбития начинать игру с первой миссии на Земле. Что‑то вроде shoot'em up rogue‑lite. :) Что добавить в конце? Завершение этого ремастера ещё требует немного работы, и, наверное, помог бы «пинок» от кого‑то другого. Мне нужен был бы языковой корректор, который прошёл бы все тексты и поправил несовершенства. Нужен был бы тестер, которому хотелось бы всё пройти и снапшотить каждое несоответствие, на которое наткнётся. Если думаете, что могли бы помочь привести эту отличную игру к сегодняшнему виду — пишите на martin@borik.net или куда угодно, где меня можно застать.
Репозиторий игры, разумеется, публичный: github.com/mborik/inferno128
Martin Bórik
Поделитесь вашим мнением о статье