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

https://mattbruv.github.io/ccsr-bugfix/
  • Перевод
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 передаваемого объекта и функция подумает, что его не было.

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


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

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


Не знаю. Возможно, из-за ностальгии?
Поделиться публикацией
Комментарии 15
    0
    Если есть столько времени и мотивации заниматься такими вещами, почему бы не переписать ту игру на современный лад, чтобы работало на разных платформах.
      0
      Портировать даже исходный код на какой-то другой язык — огромная работа. Что говорить о байт-коде, который придется вначале отреверсить весь целиком. ИМХО если кто и займется такой некромантией, то к окончанию его работы целевая платформа в состоянии устареть — rinse and repeat.
        0
        Под «переписать на современный лад» имелось ввиду не отреверсить байт код, а переписать на наиболее подходящей платформе и языке, поменять графику, имена, локации, сюжет. Оставить только общую идею и механику.
          +2

          Нет, вы не понимаете. Чел играл все детство, например, в Супер Марио, а потом исправил игру. А вы предлагаете написать свой Марио, пусть даже с вистом и юнотками. Тут главное — воспоминания детства.

            0
            Я понял про воспоминания детства, проделанная работа поражает воображение и заслуживает уважение.

            Я имел ввиду, что это конечно очень увлекательно, но к сожалению никому кроме автора скорее всего не нужно, перенос игры на другие платформы принес бы намного больше профита. И раз у человека есть и время, и желание, и необходимые навыки, то почему бы и нет?)
              +1
              Я имел ввиду, что это конечно очень увлекательно, но к сожалению никому кроме автора скорее всего не нужно
              Не соглашусь. Я занимался гораздо более сложной доработкой Need For Speed III 1998 года. Потратил на это конечно же уйму времени (и получил не меньшее количество удовольствия). По логам HTTP-сервера вижу, что кто-то регулярно качает этот патч. На youtube появляются новые записи NFS3, и часто они сделаны именно с использованием моего патча. На почту время от времени приходят письма с благодарностями и прочим фидбеком. Это, конечно, не миллионы заинтересованных пользователей, но и не я один.
                0
                Может я ошибаюсь, но нид фор спид намного более известная, можно сказать легендарная игра, пользующаяся намного большей популярностью.

                Но сейчас я поменял свое мнение, и считаю что популярность игры не имеет большого значения, исправляя этот баг человек получил огромное удовольствие и показал свой профессионализм, сама по себе эта статья может быть весьма полезна другим программистам.
            +5
            Человек способен проанализировать код и, грубо говоря, поменять несколько байт. Количество работы N единиц, выполняется за несколько человеко-часов. Результату он как-то может порадоваться сам. Ему в качестве альтернативы предлагается сделать совершенно другое: проявить и развить таланты художника, сценариста, программиста, музыканта, затратить M единиц работы в тысячи человеко-часов, вероятно в течении нескольких лет. Порадоваться результату труда как игрок он не сможет, т.к. в процессе разработки и отладки будет знать игру досконально, и скорее всего к завершению проекта будет её ненавидеть. Вопрос: зачем ему всё это?
              0
              Может и не нужно, все зависит от амбиций =)

              Чтобы написать заново игру конечно нужно во много раз больше времени, но если игра будет хорошо принята игровым сообществом, то можно нормально заработать.
                0
                Не всё в мире меряется «возможностью на этом заработать». Да и есть огромная разница между «сделать очередной клон чего-то там» (про который наверняка все быстро забудут) и «приобщиться к легенде» (причём представление о «легендарном», конечно же, у каждого своё).
                  0
                  Не буду спорить, Вы безусловно правы.
        +4

        Осталось написать разработчику этой игры и выслать пропатченный вариант. Тогда миссия будет считаться законченной и вы получите "+ репутации в банде"

          0
          Если не пошлёт подальше из-за того, что влез в его код.
          +2
          0x6D = 01101101
          0x66 = 01100110

          Технически, поменять пришлось не целый байт, а всего 3 бита :)
            +3
            Да там и 1 бит можно было поменять — главное «поломать» строку.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое