Assembler
September 2013 4

[NES] Пишем редактор уровней для Prince of Persia. Глава четвертая. Он сам бежит! Или скелет в шкафу

Глава первая, Глава вторая, Глава третья, Глава четвертая, Глава пятая, Эпилог

Disclaimer

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

С небольшой задержкой мы будем исправлять и сюжет.


Что будем редактировать?

Прежде, чем двигаться дальше, хотелось бы себя ограничить в перекройке игры. Действительно, если мы захотим менять все и вся, то проще будет просто написать свою игру с самого начала. Но как и задумывалось изначально, хочется все же сделать что-то новое, сохранив, однако прежнюю базу. Определимся с тем, что мы будем редактировать.
  • Цвета. NES версия отличается от остальных тем, что все уровни в ней имеют скучные однотипные цвета. Меняются лишь цвета стражей порядка;
  • Demo play. Как только мы поменяем хотя бы кирпичик в первой комнате, наш герой в demo play будет как слепой кролик долбиться в стены или пытаться залезть на отсутствующие уступы;
  • «Зеркальное отражение». Оно появляется только при выполнении определенных условий и в строго определенных комнатах строго определенных уровней. Если мы что-то перестроим, то оно либо появится не к месту, либо не появится вовсе, исказив сюжет игры;
  • … Мышка в восьмом уровне :-). Она бежит по просьбе принцессы нас спасать, когда мы оказываемся в заточении, открывая нам решетку.
  • Виды супостатов. В третьем и двенадцатом (в оригинале в двенадцатом уровне «отражение», но разработчики NES немного сэкономили) уровнях нас встречает скелет;
  • Переходы.
    • Первый уровень. По сюжету героя бросают в подземелье и закрывают за ним решетку. Таким образом, в первом уровне он появляется и вслед за этим тут же закрывается путь отступления;
    • Шестой уровень. Герой прыгает через пропасть, но коварное «отражение» закрывает решетку и он срывается в пропасть — в подземелье. Налицо нестандартный переход в следующий уровень;
    • Седьмой уровень. Раз он падает вниз в шестом уровне, то он должен появиться сверху в седьмом уровне. Нестандартный вход в уровень;
    • Двенадцатый и тринадцатый уровни. Здесь разработчики решили разделить один длинный на два уровня покороче. Мы переходим из комнаты в комнату, а попадаем в следующий уровень;
    • Четырнадцатый уровень просто не имеет перехода на следующий уровень

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

Мышку, к слову, я оставил на своем месте.

Код

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

Нажимая кнопку «Step over» в отладчике мы неизбежно перейдем к главному циклу, где основная его составляющая выглядит так:
label_CC1A:
$CC1A:20 F7 F2	JSR $F2F7
$CC1D:20 10 D0	JSR $D010
$CC20:20 1E 86	JSR $861E
$CC23:20 00 CB	JSR $CB00
$CC26:20 E8 A4	JSR $A4E8
$CC29:20 10 D0	JSR $D010
$CC2C:20 04 8B	JSR $8B04
$CC2F:20 10 D0	JSR $D010
$CC32:20 F9 80	JSR $80F9
$CC35:20 FC D8	JSR $D8FC
$CC38:20 DF BA	JSR $BADF
$CC3B:20 00 CB	JSR $CB00
$CC3E:20 12 9F	JSR $9F12
$CC41:20 DD A3	JSR $A3DD
$CC44:4C 1A CC	JMP $CC1A

Выше этого цикла также вызов множества процедур, которые вызываются при переходе между комнатами или уровнями. Перечисленные в приведенном коде процедуры изменяют состояние игры, а также выполняют различного рода проверки. Из них не представляют интереса следующие:
— $F2F7 — ожидание изменения некоего состояния в прерывании VBlank;
— $D010, $CB00 — переключение банков перед вызовом процедур, которые располагаются в соответствующем банке.

Я не буду приводить код всех оставшихся процедур, так как их рассмотрение со всеми ветвлениями выльется еще в десяток постов. Приведу лишь ту, что представляет интерес сейчас. В псевдокод я переводить не буду, но дам комментарии.

A4E8. Он сам бежит!

Перед вызовом этой процедуры вызывается процедура включения банка #02 — $D010. Включаем этот банк путем записи числа #02 в ячейку $FFF2 и переходим к этой процедуре:
$A4E8:AD E4 04	LDA $04E4 = #$01
$A4EB:D0 13	BNE $A500			;; проверяем состояние флага $04E4.
												;; Если не 0, то выполняем основное тело процедуры
$A4ED:60	RTS				;; ...или выходим
$A4EE:4C FF A4	JMP $A4FF
$A4F1:A9 00	LDA #$00
$A4F3:8D E3 04	STA $04E3 = #$44
$A4F6:8D E0 04	STA $04E0 = #$76
$A4F9:8D E4 06	STA $06E4 = #$0A
$A4FC:4C 14 DC	JMP $DC14			;; обнуляем вообще все и уходим в главный цикл
												;; видимо, игра здесь будет начинаться заново с какой-то отправной точки.
$A4FF:60	RTS				;; не буду приводить здесь код $DC14.
												;; в нем очистка стека и переход на адрес чуть ранее основного цикла

label_A500:
$A500:AD 0A 04	LDA $040A = #$01
$A503:29 30	AND #$30
$A505:D0 EA	BNE $A4F1			;; если в некой переменной установлены определенные флаги,
												;; то возвращаем игру в некую начальную точку
$A507:AE E3 04	LDX $04E3 = #$44		;; в индексный регистр помещаем значение из ячейки $04E3
$A50A:BD 0F A7	LDA $A70F,X @ $A763 = #$AA	;; и читаем из ROM-файла значение, соответствующее индексу в ячейке $04E3
$A50D:C9 FF	CMP #$FF			
$A50F:F0 E0	BEQ $A4F1			;; если прочитанное значение соответствует маркеру #FF,
												;; то снова отправляем игру в начальную точку
$A511:CD E0 04	CMP $04E0 = #$76		;; то же прочитанное значением сравниваем с ячейкой $04E0
$A514:D0 0E	BNE $A524			;; если не равно, то... (см. ниже)
$A516:A9 00	LDA #$00			;; иначе обнуляем ряд ячеек...
$A518:8D F4 04	STA $04F4 = #$00
$A51B:8D E0 04	STA $04E0 = #$76
$A51E:EE E3 04	INC $04E3 = #$44		;; а к нашему индексу прибавляем 2 (двойным инкрементом)
$A521:EE E3 04	INC $04E3 = #$44

label_A524:
$A524:BD 10 A7	LDA $A710,X @ $A764 = #$01		
$A527:8D 0A 04	STA $040A = #$01		;; переносим значение соседней ячейки в ROM-файле в $040A
$A52A:EE E0 04	INC $04E0 = #$76		;; увеличиваем некий счетчик
$A52D:60	RTS				;; выходим


Итак, здесь мы имеем пару ячеек с некоторыми состояниями, еще ячейку с индексом и некий массив в ROM-файле. Из кода видно, что он выполняется только в том случае, если установлен флаг в ячейке $04E4. Попробуем его поставить.

Запускаем игру заново, входим в первый уровень и ставим в $04E4 единицу.


Проходит секунда и… он бежит сам! Даже на кнопки нажимать не нужно.
Если мы дождемся Demo play, то обнаружим, что в ячейке $04E4 уже стоит единица, причем, если мы обнулим эту ячейку во время demo, надпись demo play пропадет и управление перейдет в наши руки. Очень удобно: demo проходит за нас основную часть, дальше мы можем обнулить $04E4 и продолжить игру самостоятельно.

Теперь мы можем, опираясь на игру, разобрать код. Очевидно, процедура $DC14 отправляет нас в меню игры. Массив же $A70F хранит некие структуры по два байта, и заканчивается маркером #FF. Причем первый байт — некий временной интервал, при завершении которого индекс увеличивается, и в ячейку $040A переносится второй байт из следующей структуры. Что он означает?
Взглянем на массив:
64 00 . A0 01 . 28 00 . E6 84 . E6 02 ... FF
Сперва мы ожидаем #64 у.в.е. (условных временных единиц), затем #A0 у.в.е, затем #28 и так далее до маркера #FF. Поменяем время в первой паре байт на #FE и в demo play обнаружим, что персонаж стоит как истукан после начала продолжительное время. Если мы туда поставим #02, то он повернется налево и будет некоторое время долбиться в стену. Следовательно, второй байт описывает действие, которое он будет выполнять в течение указанного интервала времени. Поскольку игра детерминирована, то нам достаточно жестко задать последовательность определенных действий, и она будет «играться» сама.
По сути, второй байт — это имитация нажатия кнопок на геймпаде, где первый бит кодирует кнопку вправо, второй бит — кнопку влево и так далее. Совокупность этих бит определяет псевдосостояние геймпада во время demo-play. Редактируя этот массив, мы можем подогнать demo play под наш новый уровень.
Если мы будем отслеживать, что происходит потом со значением в ячейке $040A, то мы придем к структуре данных, которая располагается по адресу $060E. Описывается она следующим образом:
struct CHARACTER
{
	char bCharType;
	unsigned short X;
	unsigned short Y;
	unsigned short ptrAction;
	char bDirection;
	char bActionIndex;
	char bPoseIndex;
	char bReserved[4];
};

Дойдя до этой структуры мы узнаем, что существует массив «действий», состоящий из указателей на определенные структуры, которые потом разворачиваются в последовательность отображений спрайта на экране. Под «действием» подразумевается то, что выполняет персонаж на экране: бежит, прыгает, приседает и прочие действия. Аналогично все с индексом «позы»: стоит он или сидит — определяется этим членом структуры. Эта структура нам понадобится в будущем, а пока нам нужно выяснить, как мы попадаем из уровня в уровень.

Бегаем по лабиринтам

Здесь, как и ранее у нас будет отправная точка — ячейка $70. При переходе из уровня в уровень мы увидим код (как и раньше, мы ловим по точке останова по условию записи в ячейку $70):
$86F3:AD 35 07	LDA $0735 = #$00
$86F6:D0 0D	BNE $8705
$86F8:A5 70	LDA $0070 = #$00
$86FA:18	CLC
$86FB:69 01	ADC #$01
$86FD:C9 0E	CMP #$0E
$86FF:90 02	BCC $8703
$8701:A9 00	LDA #$00
$8703:85 70	STA $0070 = #$00            ;; <<< останов
$8705:A9 00	LDA #$00
$8707:8D 01 20	STA $2001 = #$18
$870A:85 15	STA $0015 = #$18
$870C:4C 1D CB	JMP $CB1D

Тут мы видим, что к ячейке $70 прибавляется единица и дальше мы перемещаемся куда-то в последний банк. Причем, есть и проверка: если число больше 13, то обнуляем это значение. Это значит, что если мы поставим в 14 уровне выход, то мы переместимся обратно в первый. Это инициирующая процедура перехода на следующий уровень. Дальше нам покажут пароль и мы окажемся в следующем уровне. Посмотрим, откуда она вызывается.

Хардкод

Как и в архитектуре x86, так и здесь, вызов процедуры происходит путем занесения в регистр Instruction Pointer аргумента инструкции JSR и помещения в стек адреса возврата. В стеке у нас следующее:

22, CC, ...
Следовательно, вызвала нас инструкция, находящаяся перед инструкцией по адресу $CC22:
$CC20:20 1E 86  JSR $861E

Она громоздкая и всю ее трассировать неинтересно. Переключим точку останова с записи в $70 на чтение и запустим отладку. Сперва мы остановимся в знакомой нам процедуре $C0D5 — она нам неинтересна, а вот следующий останов приведет нас сюда:
$D0B8:A5 70	LDA $0070 = #$0B
$D0BA:C9 0B	CMP #$0B
$D0BC:D0 0C	BNE $D0CA
$D0BE:A5 51	LDA $0051 = #$16
$D0C0:C9 16	CMP #$16
$D0C2:D0 06	BNE $D0CA
$D0C4:20 10 D0	JSR $D010
$D0C7:4C F3 86	JMP $86F3

Значение в нашей ячейке сравнивается с числом #0B == 11 (напомню, что уровни нумеруются с нуля), а дальше идет чтение из ячейки $51, где у нас лежит #16 = 22. Исходя из того, что мы находимся в комнате 22 (если посмотреть карту уровня), то в $51 у нас лежит номер комнаты, который мы не искали в прошлый раз. Если эти проверки выполняются, то мы переходим по адресу $86F3 — то есть в инициирующую переход процедуру. Разработчики банально вшили в код переход на другой уровень из этой комнаты, если персонаж уходит из комнаты влево.
Аналогичную проверку мы можем найти, если будем искать переход из шестого уровня в седьмой, когда персонаж падает в пропасть. То есть нам достаточно отредактировать инструкции CMP, и этими свойствами будет обладать иная комната. Вот только должно будет соблюдаться условие: переход в следующий уровень состоится, если мы будем идти влево (для этой проверки) или падать вниз (для второй).

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

Вынимаем скелет из шкафа и раскрашиваем стены

Редактор почти закончен. Изучая переходные моменты из уровня в уровень или из комнаты в комнату мы найдем и оставшиеся параметры, которые можно отредактировать. Поскольку подходы к поиску подобных данных уже были рассмотрены, я не буду здесь останавливаться подробно. Дам лишь общее описание, а в конце последней главы приведу список смещений в ROM на структуры игры и их описание.

0x13BEB: 00 00 24 00 00 00 00 00 00 00 00 24 00 00 — массив, содержащий код набора тайлов, которыми у нас будет рисоваться стражник (собственно стражник или скелет). Эта структура подобна той, которая описывает вид уровня, и она же подобна той, которая определяет количество здоровья в уровне.

Палитра же строится из двух массивов указателей и собственно массива самих палитр. Сама палитра хранится ровно в том виде, в котором и отправляется в PPU в виде последовательности из 32 байт. Второй массив содержит всего 7 указателей, которые приводят нас к допустимым для уровня палитрам, причем первый указатель равен шестому. А первый массив состоит из 14 указателей (по числу уровней), каждый из которых приводит нас к тому или иному элементу второго массива.

Почти готов


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

В пятой, последней главе, которая называется «Отражение» мы попытаемся научиться управлять отражением принца. Из второстепенного элемента, который практически никак не влиял на ход игры, мы сможем превратить его в один из основных, без которого пройти игру станет невозможно, и принц будет уже не так одинок в тех холодных стенах, в которые его заключили.
+59
14.6k 53
Comments 7
Top of the day