Adobe Flash
Разработка игр
Реверс-инжиниринг
11 октября

Устраняем баг в игре 2000 года на Shockwave

Перевод
Оригинал:
Matthew Pitts
image

История замены единственного байта


Cartoon Cartoon Summer Resort


Это было лето 2000 года. Мне исполнилось шесть, я только что закончил первый класс, и начались каникулы. Это означало, что я мог долго играть на улице, смотреть мультфильмы и включать компьютер отца с Windows 98, чтобы искать игры в совершенно новом, неизведанном краю под названием «Интернет». Одним из моих любимых был веб-сайт Cartoon Network. На нём я нашёл множество увлекательных flash-игр на основе телевизионных мультфильмов. Тем летом они выпустили серию игр под названием «Cartoon Cartoon Summer Resort».


Геймплей первого эпизода Cartoon Cartoon Summer Resort

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

Возвращаемся на 18 лет вперёд


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

Кроме того, ни один современный браузер не запустил бы древний и уязвимый плеер Shockwave… за исключением Internet Explorer. Вероятно, я оказался первым человеком, нашедшим оправданную причину использовать Internet Explorer в 2018 году.

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

Тот самый баг


Спустя какое-то время я обнаружил в игре баг:

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

Как видно из анимации, в игре можно арендовать лодку, чтобы передвигаться по воде. При попытке арендовать другую лодку появляется сообщение «Сегодня больше нет арендуемых лодок!». Если уплыть на север и пройтись по правому краю острова, но откроется тот же текст, который должен появляться у лодочного причала.


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

Уже обладая всеми своими знаниями о программирования и глядя на игру, я воспринимал этот баг странно притягательным. Мне казалось, что я раскопал древнюю гробницу и нашёл в ней нетронутую головоломку, которая могла пропасть, так никем и не решённая. Для меня этот баг был возможностью поучиться и открыть что-то новое. Интересно, что именно такие возможности давал мне процесс самой игры в детстве. Есть что-то почти поэтическое в том, как нечто совершенно ненамеренно может поставить новые задачи, если взглянуть на него под другим углом.

Деконструкция игры


Для исправления бага мне нужно было разобраться во внутренней работе игры. Изучив вопрос, я узнал, что игра была создана на Shockwave в приложении Director. При работе с Director проекты сохраняются как файл .dir (Director). Этот файл похож на файл PSD для Photoshop. Аналогично тому, как файл PSD содержит неразрушающую информацию о слоях и тексте, проект .dir сохраняет все ресурсы, сырой исходный код и другую информацию, помогающую в процессе разработки. Для анимирования сцен в Director использовался проприетарный скриптовый язык Lingo.

Если бы игра была сохранена в файле .dir, я мог просто открыть его в Director и легко изучить, как работает игра. Однако игра опубликована как файл .dcr. Файл .dcr — это скомпилированная версия проекта Director. То есть весь исходный код скомпилирован в байт-код, выполняемый на платформе Shockwave. Этот процесс схож с тем, как файл PSD упрощается и становится изображением PNG. Изображение PNG (в нашем случае файл DCR) меньше по размеру, но не содержит информации о слоях и редактирований, и предназначен только для распространения.

Это означало, что у меня на руках двоичный объект размером 500 КБ без документации о его структуре. Даже если бы я разобрался, как найти низкоуровневый байт-код, похоже, никто не выполнял реверс-инжиниринг байт-кода Lingo и не документировал принцип работы платформы Shockwave. Вся эта информация проприетарна, ею владеет Adobe, которая не имеет никаких причин публиковать её. Шансы разобраться в работе игры выглядели довольно мрачно.

Декомпрессия


Чувствуя себя побеждённым оттого, что скорее всего не смогу устранить этот баг, я решил выяснить можно ли каким-то образом извлечь из игры ресурсы. Я посчитал, что есть вероятность найти раздел сжатых данных или чего-то подобного. Поискав, я нашёл пару программ под названием offzip и packzip. Эти инструменты могут искать данные zlib в произвольных двоичных файлах, показывать смещения и извлекать их в отдельные файлы.

Я запустил offzip с файлом DCR и к моему удивлению, он действительно нашёл архивы! 249 штук, если говорить точнее.

$ ./offzip.exe -a 1.dcr

- open input file: 1.dcr
- zip data to check: 32 bytes
- zip windowBits: 15
- seek offset: 0x00000000 (0)
+------------+-----+----------------------------+----------------------+
| hex_offset | ... | zip -> unzip size / offset | spaces before | info |
+------------+-----+----------------------------+----------------------+
0x00000026 . 164 -> 214 / 0x000000ca _ 38 8:7:26:0:1:7b6349f6
0x000000d3 .. 3932 -> 9169 / 0x0000102f _ 9 8:7:26:0:1:c1079d84
...
0x00080490 . 265 -> 472 / 0x00080599 _ 0 8:7:26:0:1:04d6b43f
0x00080599 . 209 -> 366 / 0x0008066a _ 0 8:7:26:0:1:7da3ba08

- 249 valid compressed streams found
- 0x0004040d -> 0x001565c8 bytes covering the 50% of the file


Я извлёк все эти файлы в папку и начал изучать результаты. Там было 206 файлов .dat, 38 файлов .fff, 4 файла .atn и единственный файл .ini.


Открытия


Я начал с файла INI, но от него не оказалось никакой пользы. Это была простая таблица преобразования шрифтов из Directory 7.0 между Windows и Mac. Затем я перешёл к файлам DAT. БОльшая их часть имела размер 1КБ, поэтому я начал с огромного, имевшего размер 144КБ. Я открыл его в hex-редакторе и изучил. В основном это были неразборчивые данные. Однако со временем я нашёл в них несколько слов, которые казались идентификаторами Lingo.


Анализ больших файлов DAT дал мне кое-какие подсказки, в них сохранилось несколько интересных сообщений. Я выяснил, что для графики скорее всего использовался Photoshop 3.0. Также я узнал, что в игре был инструмент редактирования внутренних карт под названием Map-O-Matic v1. Хотелось бы мне увидеть, как он выглядел и создавался.

Также я нашёл название компании, разрабатывавшей игру: Funny Garbage. Имя ведущего разработчика тоже было в файле, но его я называть не буду. Было здорово открыть для себя автора игры, которую я упорно пытался исправить спустя почти 20 лет, и наконец увидеть лицо человека, ставшего вероятной причиной этой агонии. Все эти крохи информации конечно были интересными, но особо ничем не помогли.

Прорыв


Затем я начал изучать в hex-редакторе файлы .fff. К моему большому удивлению, все данные в этих файлах были читаемыми и выглядели как данные карт:


Я вручную извлёк часть этих данных и подчистил их в текстовом редакторе. То, что у меня получилось, очень походило на массив JSON:

[
#member: "block.104",
#type: #FLOR,
#location: [16, 9],
#width: 64,
#WSHIFT: 0,
#height: 32,
#HSHIFT: 0,
#data: [
#item: [
#name: "",
#type: #WALL,
#visi: [
#visiObj: "",
#visiAct: "",
#inviObj: "",
#inviAct: ""
],
#COND: [[#hasObj: "", #hasAct: "", #giveObj: "sunscreen", #giveAct: "gotscreen"], #none, #none, #none]
],
#move: [#U: 0, #d: 0, #L: 0, #R: 0, #COND: 1, #TIMEA: 0, #TIMEB: 0],
#message: [
[#text: "You bought the sun screen.", #plrObj: "", #plrAct: ""],
[#text: "No more sunscreen today!", #plrObj: "", #plrAct: "gotscreen"]
]
]
]


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

  1. Игра ожидает, что данные карт, текста и событий находятся в похожих на JSON объектах Lingo Objects
  2. Каждая запись #member — это отрисовываемый тайл, блок или персонаж.
  3. Смещения координат и размеров #member можно редактировать.

Зная, что диалоги игры сохранялись в эти файлы, я написал короткую строку для экспорта в файл только одного диалога:

grep -a -o '#text: "[^"]*' Uncompressed/*.fff | awk '{print $0,"\r"}' > Dialogue.txt

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


Ошибочный текст находится или в 0004eda0.fff, или в 0004f396.fff. В нашем случае текст бага оказался в первом файле. Мы знаем это, потому что сразу после него находится сообщение, которое мы получаем при взаимодействии с Огом, который является персонажем на той же карте, что и тайл с багом.

Исправление бага


Теперь я мог открыть 0004eda0.fff и найти строку про лодку в hex-редакторе. Найдя её, я смог обнаружить связанный с ней объект #member. После чего изменить его свойства и сохранить файл. Затем я снова сжал его и пропатчил в исходный файл игры DCR с помощью packzip.


$ ./packzip -o 0x0004EDA0 Uncompressed/0004eda0.fff test.dcr

Когда я меняю тип блока с block.11 на block.13 и патчу игру, то могу чётко увидеть контур ошибочного тайла:


Изменив ID тайла, можно увидеть границы проблемной области

Само исправление бага до смешного просто. Всё, что мне нужно было сделать — изменить для этого ошибочного тайла идентификатор #message на #fessage:


Теперь, если мы пропатчим изменения и вернёмся в эту область, то сообщения больше не появятся!


Устранили баг в игре, в буквальном смысле изменив 1 байт

Почему так можно исправить ошибку?


Могу только предположить, что движок игры, вероятно, при движении игрока проверят тайл, на котором он стоит. Если выполняется какое-то условие, то он отображает соответствующее этому тайлу сообщение из массива #message. После изменения #message на #fessage ссылки на массив #message, которую ищет код, больше не находится. Он считает это пустым (или неопределённым?) объектом и ничего не отображает.

Рассмотрим пример на JS:

function foo(bar) {
    if (bar["message"] !== undefined) {
        // display the message
    }
}

Допустим, мы не можем изменить функцию foo(), но нам нужно изменить результат. У нас есть доступ к передаваемым ей данным. Можно переименовать свойство message передаваемого объекта и функция подумает, что его не было.

Как появился этот баг?


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

Зачем тратить столько труда на нечто столь незначительное?


Не знаю. Возможно, из-за ностальгии?
+49
5,9k 27
Комментарии 15
Похожие публикации
Популярное за сутки