Spiiin's blog

New Ghostbusters Hard Mode Lua Script

После изучения кода игры New GhostBusters 2 у меня появилась расшифрованная информация о привидениях по уровням.

Чтобы ей не пропадать зря, решил сделать редактор врагов и усложнить игру наподобии того, как делал с Battletoads, чтобы пройти её со своим мелким племянником.

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

Но в современных эмуляторах есть встроенный скриптовый язык, позволяющий вмешаться в стандартный поток инструкций процессора. С помощью этого языка хакеры делали с играми вещи, напоминающие магию.

Но любая магия является просто сложной технологией, поэтому решил попробовать пошаманить со скриптами сам. Алгоритм загрузки данных о врагах комнаты в "Охотниках за привидениями" выглядит примерно так:

Записи о комнатах храняться в виде списка из команд и номеров типов призраков. За командой следует переменной число аргументов, за номером типа координаты места появления (4 байта) и переменное число байт дополнительной информации (0 до 3 байт).

Игра узнает объем дополнительной информации из массива, в котором для каждого типа призрака есть набор из 3 флажков “имеет ли данный призрак доп. инфу заданного типа”. Такой информацией может быть, например, признак, что привидение неуязвимо (стулья в конце первого уровня и самовоспламеняющиеся свечи в последнем), подтип (головы в конце второго уровня) или направление поворота (импы в третьем уровне).

Таблица с описанием команд:

Коды команд New GhostBusters 2
Код Описание
FF 0 Команда конца уровня.
FF 1 X Команда указателя на следующую комнату. Описание комнат уровня с номерами хранится отдельно.
FF 2 Чекпоинт. После проигрыша игра начнется не с начала уровня, а с последней сохраненной комнаты.
0D Мешок-приз.
0E Свечи из последнего уровня.
11 Сценка в конце 3-го уровня.
12 Стрелка прибора (появляется после отлова всех призраков. доп. байты - направление и номер комнаты)
13 Лизун
14 Ускоренный лизун
15 Лизун с телегой
1 Лизун-метатель еды
17 Зеленый пацан
18 Тень с бензопилой
19 Змея из стены
1A Пара и тостого и худого призраков (первый босс)
1B Прыгающий стул
1C Розовые крутяшки
1D Шахтер с дрелью
1E Шахтер с киркой
1F Шахтер с граблями
20 Шахтер на вагонетке
21 Мелкая голова из поезда
22 Лизун с лопатой
23 Тыквоголов
24 Летающий имп
25 Имп с гарпуном
26 Дух ниндзя с мечом
27 Дух ниндзя с сюрекенами
28 Тесто
29 Блин из канализации
2A Тролль с цепью
2B Лизун-метатель мусора
2C Лизун-метатель быстрый
2D Вращающаяся труба
2E Босс Ящеры
2F Босс Венс
30 Босс Виго

Проглядел доступную документацию по EmuLua, нашел функцию memory.register, про которую сказано, что она может установить произвольную функцию, отслеживающую изменения в ячейке памяти.

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

Но такому простому способу препятствует то, что memory.register а) работает только в режиме ослеживания записей. б) как следствие работает только в области изменяемой памяти (0x0000-0x8000), а данные уровня лежат в ROM. Хендлер на чтение из ROM установить нельзя.

Почему – не знаю, технически вроде не сложно вытащить такую возможность в скрипты, но факт, что таким способом подстановку данных сейчас сделать нельзя :(

Пришлось прочитать документацию по всем доступным функциям внимательнее, после чего обнаружилась функция memory.registerrun, устанавливающая функцию,выполняющуюся при выполнении кода по указанному в аргументе адресу.

Выяснил на практике, что если ставить обработчик memory.registerrun, то он выполняется ДО выполнения команды по адресу, а если через memory.register, то сразу ПОСЛЕ записи в ячейки.

Алгоритм внедрения модифицировал так - в роме добавил в интерпретатор еще одну команду, которая выставляет флажок в неиспользуемый игрой кусок памяти, а в скриптах сделал функцию, реагирующую на выставление флажка и делающую то же, что команда считывания в оригинальном роме. После тестов я заметил, что функция-обработчик вызывается недетерминированное количество раз (почему-то по 4 за одну запись в ячейку). Поэтому функция-обработчик обязана быть чистой и никаких счетчиков содержать не может :(

Выяснять, почему так, долго и сложно, поэтому просто убедился в том, что хотя бы memory.registerrun работает, как ожидается и решил использовать только её. Дальше попробовал так - навесить после всех инструкций, считывающие данные о комнате, свои функции-хендлеры на выполнение, которые блокируют действие этой инструкции (переписывают аккумулятор, память или регистр статуса) и дальше возвращаться к оригинальному потоку выполнения.

С таким подходом следует еще следить за тем, чтобы адрес, на который ставится обработчик находился в том же блоке памяти, иначе можно напороться на неправильную инструкцию. В качестве контрольной проверки может послужить сравнение значения ячейки, хранящей номер уровня с тем, который ожидается в скрипте. Ну и некоторые в некоторых сложных по возможному графу выполнения участках проще вклиниваться в несколько частей, собирая по ходу необходимые данные. Такой метод работает! Дальше оставалось только перехватить логику обработки чекпоинтов и game over, чтобы сбрасывать свои указатели данных. Еще в ходе исследования я заметил, что потенциально движок игры поддерживает до 8 призраков на экране, хотя в коде стоит ограничение на четырех.

Это ограничение было аккуратно вырезано, после чего игра стала более сложной и веселой. Единственный минус - видимо, за кадр не всегда успевают отрисоваться все объекты на экране, поэтому появляется мерцание. Ну на это уже можно и забить. Оставшиеся проблемы скрипта - я не делал поддержку перезапуска по нажатию кнопок power и reset приставки (это не поддерживается вообще) и загрузку/сохранение вместе с игровыми сейвами (просто неохота было). Так что сейвами пользоваться нельзя, а при перезагрузке игры надо вручную перезапустить скрипт. Вот и вся чёрная магия.

Видео геймплея:

Код скрипта:
http://www.everfall.com/paste/id.php?az548wj3t37t

Ссылка на архив (содержит эмулятор, игру на русском, скрипт и инструкции для запуска):
http://dl.dropbox.com/u/852723/NewGhostBusters_hard.zip