Pull to refresh

[NES] Пишем редактор уровней для Prince of Persia. Глава вторая. Букетно-конфетный период

Reading time 7 min
Views 14K
Глава первая, Глава вторая, Глава третья, Глава четвертая, Глава пятая, Эпилог

Disclaimer

Листая в редакторе изображений (в нем было очень удобно переходить между изображениями в папке) скриншоты, которые сделала для меня утилита, я… проснулся от будильника. На экране монитора светился очередной скриншот, который в списке находился где-то в первой трети из общего числа. Вопрос надо было решать каким-то иным способом.

Вечером того же дня…


Заглядываем в банку. Способ №2.

Когда мы смотрим в шестнадцатеричный набор данных в памяти (как выше говорилось, что достаточно смотреть на первые 2 кБ данных) — важно «высмотреть» важные для нас вещи. Но какие?
Возьмем за очевидные следующие предположения:
  • Данные считываются из ROM непосредственно перед их использованием. Аргумент: свободной памяти мало, хранить данные особенно негде, поэтому мы берем их только тогда, когда они понадобились, и кладем обратно, если надобность отпала;
  • Игра простая, поэтому вероятность того, что данные зашифрованы, достаточно мала;
  • Данные хранятся в абстрактном виде для экономии памяти ROM, которые затем преобразуются в блоки тайлов при выводе на экран.

Как могут храниться данные комнаты и/или уровня мы с уверенностью сказать не можем. Можем опять же предположить, что структура довольно проста.
Исходя из этих предположений попробуем представить, что нам нужно найти:
  1. Данные будут считываться из ROM-файла в моменты перехода между комнатами или уровнями (когда экран черный);
  2. Данные считываются небольшими порциями (может даже по 1 байту) для трансляции в блок тайлов при выводе на экран;


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

Как искать? Можно применить RAM Filter в FCEUXDSP, но можно и посмотреть глазами. Я прибегнул ко второму способу, так как точно не смог предположить, что искать.
  • Во-первых, номер может храниться в виде числа. Если он хранится в виде числа, то отсчет ведется с 0 или с 1?
  • Во-вторых, он может храниться в виде указателя на некую структуру данных этого уровня. Тут с наскока сложно будет что-либо найти.

Можно перебрать эти варианты, а можно перед выходом на следующий уровень сохранить состояние (Save state), перейти, снова посмотреть. И так несколько раз. Откровенно говоря, этот способ на удачу. Если не повезет, то применим RAM Filter.

Развиваем орлиное зрение

Запускаем игру, бежим до выхода из уровня, сохраняем состояние (Save state), открываем Tools->Hex editor, и переходим…
«Дергающиеся» байты сразу пропустил. Начиная примерно с ячейки $80 очевидно хранится текущее изображение спрайта главного героя. А остальное, в принципе, обозримо. После пары проходов из первого во второй, а затем из второго в третий, орлиный взор выловил ячейку $70, значение которой менялось из 0 в 1, а затем из 1 в 2. Попробуем с ней поиграться.

Начинаем первый уровень. Замечаем, что в ячейке действительно 0, меняем его на единицу и спускаемся из первой комнаты вниз — в следующую. Но что это? Попадая в нижнюю комнату мы падаем в пропасть, которая находится аж в конце второго уровня! После того, как принц разбился, игра просит нажать Start. Нажимаем и попадаем в начало второго уровня, как-будто мы с него и начали.

Скорее всего, это оно и есть. Смущает во всем этом лишь одно: когда мы начинаем игру в этой ячейке стоит число 9. Коль скоро у нас отсчет с нуля, то это значит, что мы в 10 уровне? Чуть позже выяснится, что это действительно так, а пока нам нужно найти те кирпичики, из которых строится уровень.

Открываем банку консервным ножом. Способ №1

Я не буду рассматривать этот метод в чистом виде, так как изучать простыню кода без применения к игровому процессу — то еще удовольствие. Так или иначе мы будем упираться в кучу различных процедур по воспроизведению фоновой мелодии, отрисовку спрайтов, да и прочие служебные процедуры, а они сейчас мало нас интересуют. Нужна лишь одна единственная процедура (пока).

Открываем отладчик (Tools->Debugger) и добавляем точку останова.
У нас сейчас нет конкретного адреса, куда ее можно было бы поставить, но отладчик нам дает возможность установить точку останова не только по конкретному адресу. В том числе мы можем заставить его остановиться при изменении ячейки памяти (или диапазона ячеек).
Добавляем точку, нажав Add. Указываем в окне адрес «70», отмечаем, что нас интересует запись в эту ячейку. Также указываем, что нас интересует CPU Memory. Как-нибудь называем точку в поле Name и ждем.

Действительно, при переходе из стартовой комнаты в первый уровень игра замирает и мы переносимся в отладчик.
$D0E6:A2 00	LDX #$00
$D0E8:86 15	STX $0015 = #$00
$D0EA:8E 35 07 	STX $0735 = #$00
$D0ED:86 70	STX $0070 = #$09	;;; <<<<<<
$D0EF:8E 01 20	STX $2001 = #$18
$D0F2:CA	DEX
$D0F3:9A	TXS


Откровенно говоря, полезной информации тут нет вообще. Тут видно обнуление части переменных и… стека. После DEX у нас в X оказывается #FF, которое помещается в стековой регистр, что означает, что стек у нас теперь имеет максимальный размер, а значит там ничего нет. Раз информации тут нет, то мы можем использовать эту точку как опорную. С этого момента мы можем зайти в Trace Logger нажать Start, и ждать, пока первая комната первого уровня не отобразится на экране. Кроме того, мы можем точку останова переключить на чтение из ячейки $70. Пока у нас будут читаться данные в трассировщике, мы посмотрим, где у нас происходит обращение к ячейке.

Нажимаем Edit, и перемещаем галочку с Write на Read.

Знакомимся с содержимым

Первое же попадание нас приведет сюда:
$CB2E:A6 70	LDX $0070 = #$00			;;; <<<<<<
$CB30:BD F5 EA	LDA $EAF5,X @ $EAF9 = #$01
$CB33:8D BB 04	STA $04BB = #$00

Видно, что порядковый номер уровня используется как индекс в массиве каких-то данных, из которых извлекается байт и помещается в ячейку $04BB. Тут я предлагаю вспомнить то, как у нас организована память и как работает наш маппер.

Мы помним, что последний банк помещается по адресам $C000-$FFFF, а вот остальные динамически помещаются в предыдущие 16 кБ оперативной памяти, то есть с $8000 по $BFFF. Эта память неизменяемая и берется она из ROM-файла. Следовательно, любое чтение из ячеек с адресами $8000-$FFFF можно считать чтением прямо из ROM-файла. Разве что нам придется переводить адреса АП в смещения ROM-файла. Как это сделать?

Арифметика

Если у нас попадание в адреса $C000-$FFFF, то здесь все предельно просто. По этим адресам у нас расположен последний банк из ROM-файла, а значит, это последние 16 кБ в нем. Посчитаем: 16 (размер заголовка) + 8(число банков)*16 кБ (размер одного банка) = 131088 байт, что в точности соответствует размеру исходного файла. Считаем смещение в банке:
$EAF5-$C000=0x2AF5
В файле банк начинается со смещения:
131088 - 16 кБ = 114704 = 0x1C010
Теперь совсем просто:
0x1C010+0x2AF5=0x1EB05
Чтобы миновать все эти вычисления, просто найдем разницу:
0x1EB05-0xEAF5=0x10010
В будущем, если нам понадобится перевести адрес в памяти в смещение в файле (это можно будет делать только для адресов $C000-$FFFF), то мы просто прибавим 0x10010, иначе — вычтем.
С остальными банками будет чуть сложнее.

Попадание!

Итак, открываем шестнадцатеричный редактор, переходим к смещению 0x1EB05 и видим:
00 00 00 01 01 01 00 00 00 01 01 00 00 01 03 12 14 ...
Очевидно, что последние (из указанных здесь) три байта выбиваются из последовательности нулей и единиц. Может это совпадение, а может и нет. Отбросим то, что выбивается и посчитаем оставшиеся нули и единицы.
Их ровно 14. Уровней тоже 14. Если посмотреть на код, где остановилось выполнение, то мы видим, что номер уровня — это индекс в этой последовательности, а значит каждый нолик или единичка отвечают за что-то, что характерно для всего уровня в целом.
Можно попробовать поменять нолик на единичку и наоборот. Лично я, когда увидел эту последовательность сразу вспомнил, что с первого по третий уровень у нас подземелье, с четвертого по шестой — дворец, потом 7, 8, 9 снова подземелье… Хм. Да это же внешний вид уровня!
Махнем первый нолик на единичку и запускаем эмулятор:

Действительно дворец, но выглядит подозрительно. Эдакий подземный дворец.
Таким образом, мы нашли штукатуров, но не нашли маляров и строителей. Судя по всему, для того, чтобы превратить подземелье в дворец, поменять нолик на единичку недостаточно.

Копаем дальше. Жмем Run…
$C0D5:A5 70	LDA $0070 = #$00	;; Помещаем в регистр A номер уровня
$C0D7:0A	ASL			;; Удваиваем его сдвигом влево
$C0D8:AA	TAX			;; Помещаем полученное значение в индексный(!) регистр
$C0D9:60	RTS			;; Выходим.

Здесь уже не все так просто. В индексный регистр мы помещаем удвоенный номер уровня (раз индексный, то опять полезем в какой-то массив) и выходим. Проследуем дальше:
$C0DD:BD 56 EB	LDA $EB56,X @ $EB56 = #$30
$C0E0:85 6D	STA $006D = #$1C
$C0E2:BD 57 EB	LDA $EB57,X @ $EB57 = #$83
$C0E5:85 6E	STA $006E = #$30
$C0E7:60	RTS

Снова читаем что-то по адресам из последнего банка и вносим в $6D:$6E. Опять лезем в редактор (0xEB56+0x10010=0x1EB66):
D9 82 61 86 91 89 F1 8C ...
Видим, что в $6D:$6E у нас должно появится #D9:#82. Выходим и отсюда:
$F225:B1 6D		LDA ($6D),Y @ $82D9 = #$01

Теперь мы ячейки $6D:$6E используем для косвенной адресации (причем, регистр Y у нас индексный и в нем 0).

Сложная арифметика

Мы можем посмотреть непосредственно в памяти содержимое банка по адресу $82D9 (а затем поиском найти в редакторе), но давайте разберемся. Номер банка, как мы помним из процедуры переключения банков, у нас хранится по адресу $06D1. Сейчас там число #06, а значит у нас включен банк #6 (по счету седьмой), то есть предпоследний. Начало его в памяти по адресу $8000, стало быть смещение нашей ячейки $82D9-$8000=$02D9. Считаем смещение в файле:
16(заголовок)+6(номер банка)*16 кБ(размер банков) + 0x2D9(смещение относительно начала банка) = 0x182E9
Это будет смещение в файле.

Каша...

01 00 00 9E 1E 11 1E 1E...
Ни о чем не говорит, правда? Тогда поэкспериментируем.
Поменяв 01 на 02 мы начинаем стартовать из комнаты ниже. То есть первый байт последовательности как-то отвечает за комнату, где начинается уровень. Перебирая значения мы будем оказываться то тут, то там без какой-либо определенной логики. Меняя последующие два нолика мы не получаем вообще никакого результата. А если мы поменяем четвертый байт с 9E на что-нибудь поменьше (02 или 03), затем запустим игру, то при входе в уровень… нас тут же убивает стражник!
Полная бессмыслица. Мы можем и дальше жать кнопку «Run...» попадая в различные части ROM-файла, но я в какой-то момент времени от этого устал.

Делаем комбинацию

Чтобы не гулять по коду, я решил посмотреть, какие данные собрались в трассировщике с момента изменения ячейки $70 до момента появления картинки на экране. Момент появления картинки мы (пока!) отладчиком поймать не можем, поэтому будем тренировать сноровку. Это нужно сделать вовремя, так как если мы опоздаем, то получим излишек, а если поторопимся, то получим недостачу.
Лезем в Tools -> Trace logger…

Считываемый из файла код нам не нужен, нужны только данные. Расставляем галочки, выбираем файл для сохранения данных и жмем Start Logging после того, как у нас сработала точка останова на записи в ячейку $70.
После того, как мы остановим сбор данных, на выходе получится файл по размеру идентичный оригинальному ROM-файлу, однако в нем будут содержаться только те данные, которые были взяты из оригинального файла за время работы трассировщика. Оставшиеся участки будут заполнены нулями.
Очевидно мы можем пропустить первые два банка, так как там лежат тайлы, также пропустим те данные, которые мы нашли во время отладки. Методом проб и ошибок я остановился на смещении 0x18010.

Анализ

Конечно, более «прямой» способ — это анализировать код в отладчике, бегать по процедурам, смотреть, что дальше происходит с данными, но пока опыта мало, а соломинка всего одна — ячейка номер 70. Изучая любым из вышеперечисленных способов данные, этих соломинок будет становиться все больше и больше, и к таким грубым методам можно будет прибегать все реже и реже. В конце концов, мы сможем ограничиться только отладчиком и шестнадцатеричным редактором.

Но это будет позже, а пока впереди третья глава «Первые строчки кода».
Tags:
Hubs:
+70
Comments 1
Comments Comments 1

Articles