[Перевод] Устраняем баг в игре 2000 года на Shockwave
История замены единственного байта
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: 0×00000000 (0)
±-----------±----±---------------------------±---------------------+
| hex_offset | … | zip → unzip size / offset | spaces before | info |
±-----------±----±---------------------------±---------------------+
0×00000026 . 164 → 214 / 0×000000ca _ 38 8:7:26:0:1:7b6349f6
0×000000d3 … 3932 → 9169 / 0×0000102f _ 9 8:7:26:0:1: c1079d84
…
0×00080490 . 265 → 472 / 0×00080599 _ 0 8:7:26:0:1:04d6b43f
0×00080599 . 209 → 366 / 0×0008066a _ 0 8:7:26:0:1:7da3ba08
— 249 valid compressed streams found
— 0×0004040d → 0×001565c8 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"]
]
]
]
Это было очень важно, ведь мне удалось многое узнать о том, как работала игра.
- Игра ожидает, что данные карт, текста и событий находятся в похожих на JSON объектах Lingo Objects
- Каждая запись
#member
— это отрисовываемый тайл, блок или персонаж. - Смещения координат и размеров
#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
передаваемого объекта и функция подумает, что его не было.
Как появился этот баг?
Предположу, что из-за лени. Разработчики использовали для создания этой области редактор карт. Скорее всего, они копипастили уже готовые карты в новые области, а потом изменяли их. Это проще, чем создавать всё с нуля. Но так как данные событий и текста перемешаны, они скорее всего недоглядели в некоторых местах и забыли удалить или заменить их. Это выглядит логичнее всего, потому что ошибочный диалог часто брался с соседней карты, где использовался правильно.
Зачем тратить столько труда на нечто столь незначительное?
Не знаю. Возможно, из-за ностальгии?