Pull to refresh

[NES] Пишем редактор уровней для Prince of Persia. Эпилог. Темница

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

Disclaimer

«Раз за разом, проходя полюбившиеся мне игры вдоль и поперек, находя все возможные секреты, мне хотелось играть в них еще и еще, но с новыми уровнями, новыми секретами и новыми возможностями.», — писал я. Естественно, проходя одну и ту же игру в «штатном» режиме, начинался поиск чего-то такого, что скрыто за кадром. Если игра имеет скрытые уровни, комнаты, приемы или систему паролей, то обязательно весь день и еще полночи проходили за голубым экраном в попытках это найти, а пароли взломать. PoP не был исключением. И хотя тут я не подобрал алгоритм составления паролей, но все же смог найти парочку методов, которые позволяют составить правильный пароль из уже имеющегося. Правда сказать, куда ведет новый пароль, до момента его использования, я не мог.


Темница

Система паролей PoP для NES сейчас расписана более чем подробно: есть как и методы изменения имеющегося пароля, так и описание самого алгоритма.

Он коротенький, поэтому приведу его на первом подвернувшемся под руку языке:
Генератор паролей на bash
#!/bin/bash

PLEVEL=$1
PTIME=$2

if ! [[ ${PLEVEL} =~ ^[0-9]{1,2}$ ]] ; then echo "Invalid level" >&2 ; exit 1 ; fi
if ! [[ ${PTIME} =~ ^[0-9]{1,2}$ ]] ; then echo "Invalid time" >&2 ; exit 1 ; fi

if [ "0" == ${PLEVEL} ] ; then echo "Level must be great than 0" >&2 ; exit 1 ; fi

PLEVEL=$[PLEVEL-1]

R1=$[RANDOM % 10]
R2=$[RANDOM % 10]

PASS0=$[((PTIME / 10)+R1) % 10]
PASS3=$[((PTIME % 10)+R2) % 10]

PASS1=$[((PLEVEL & 3)+R1) % 10]
PASS7=$[((PLEVEL / 4)+R2) % 10]

PASS2=$R1
PASS5=$R2

SUM=$[PASS0+PASS1+PASS2+PASS3]
SUM=$[SUM+(SUM % 10)+PASS5]
SUM=$[SUM+(SUM / 10)+PASS7]

PASS4=$[SUM % 10]
PASS6=$[SUM / 10]

echo "${PASS0}${PASS1}${PASS2}${PASS3}${PASS4}${PASS5}${PASS6}${PASS7}${PASS8}${PASS9}"



И вот, перебирая пароли (тогда еще на dendy, когда эмуляторов в их нынешнем виде и в проекте не было), я попадал в странные места, которые явно не были предусмотрены разработчиками:

или


Управление в этих «уровнях» работает только частично, внешний вид странный, да и попасть туда нельзя из основной игры.
Сейчас, глядя на то, как движок хранит данные уровней, я даже убедился в том, что больше 14 уровней в игре просто не предусмотрено. Куда же ведут эти пароли?

Overflow

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

Отсутствие проверки также касается и процедуры составления паролей, алгоритм которой предусматривает номера уровней от 0 до 15. Разработчики, видимо, в целях экономии времени на разработку решили, что раз игра не составляет пароли, в которых будут уровни с номерами 14 и 15 (в индексации от 0), то и вводить их никто не будет. Ага.

Мы знаем, что в наших трех таблицах указателей, из которых строится конечный уровень, всего 14 элементов и 15 с 16 там не предусмотрено. Следовательно уровень строится из мусора, который попадается при интерпретации данных, следующих за имеющимися таблицами, в указатели. Но почему там нельзя полноценно управлять персонажем?

Вспоминаем теорию

Уровень строится на базе трех типов данных, для каждого из которых имеется своя таблица указателей:
  • Блоки, из которых строятся комнаты — 0x1EB4A;
  • Заголовок уровня — 0x1EB66;
  • Геометрия уровня — 0x1EB82.


Также еще есть и вспомогательные данные:
  • Вид уровня;
  • Палитра;
  • Вид стражи;
  • Количество здоровья;
  • Прочие данные, которые не существенно влияют на внешний вид.


Таблицы с указателями находятся друг от друга на расстоянии ровно 28 байт. Иными словами, следуют друг за другом. А раз так, то указатели берутся не из нужной таблицы, а из следующей за ней. Поскольку данные, на которые ссылаются эти указатели, также перемешаны между собой, то переходя в 15 или 16 «уровни», мы попадаем куда-то в середину того массива данных. Причем, данные одного толка будут интерпретироваться как данные другого толка.

Попробуем мысленно представить себе вид, скажем, 15 «уровня». Берем смещение 0x1EB66 (заголовок), прибавляем 28, и смотрим на указатель:
D9 82 61 86 91 89 F1 8C 06 90 85 92 0D 96 61 99 F3 9C CD 9F 2C A3 9B A6 5B A8 AC A9 >> 79 82 << ...
$8279 — это даже раньше, чем заголовок первого уровня [$82D9]. Понятно, что мы попадаем на данные, которые описывают геометрию первого уровня. Но интерпретироваться они теперь будут по другому:
05 00 00 02 06 03 01 00 02 09 00 00 13 0E 14 00 15 01 00 06 08 02 05 00 ...
* 05 — начинаем в 5 комнате;
* 00 00 — начинаем в позиции 00 и смотрим вправо;
* остальные данные говорят нам о том, что стражник будет находится в каждой комнате где-то в районе верхнего левого угла, за небольшими исключениями.

0x1EB4A + 28 = 0x1EB66: $82D9. Уровень будет строится на основе данных, которые когда-то являлись заголовком первого уровня. Но начинать мы будем с пятой комнаты, следовательно, начиная с $82D9, нам нужно пропустить 4 комнаты согласно правилу: +30 байт, если первый байт не равен #FF, иначе +1 байт:
$82D9 = #01. Прибавляем +30.
$82F7 = #05. Прибавляем +30.
$8315 = #08. Прибавляем +30.
$8333 = #20. Прибавляем +30.
$8351: 20 00 00 14 01 03 21 03 14 14 20 00 00 14 14 14 14...
Судя по характеру данных, мы попали куда-то в середину какой-то комнаты второго уровня. Второй уровень начинается по адресу $8331, следовательно, это где-то внутри второй комнаты, которая выглядит так:
.
Стражник должен располагаться в левом верхнем углу, исходя из представленного заголовка.

Теперь посмотрим на геометрию уровня.
0x1EB82 + 28 = 0x1EBA0: 0C 03 C0 30 0C 03... То есть это будет где-то в районе адреса $030C в оперативной памяти (не в ROM!). Данные по этим адресам заведомо больше 24, что говорит о том, что перейти в соседнюю комнату при всем желании не удастся.

Сравним:

Так как граница этой «комнаты» не совпадает с границей второй комнаты второго уровня, то видно некоторое смещение «архитектуры» влево. Также слева виден обрыв, который соответствует соседней комнате, но ее, согласно неправильной геометрии уровня, нет.

Приводим в движение

Теперь я предлагаю посмотреть, как используется информация о стражниках в уровне.
$F284:20 DA C0	JSR $C0DA
$F287:B1 6D	LDA ($6D),Y @ $9FC7 = #$1E
$F289:29 1F	AND #$1F
$F28B:C9 1E	CMP #$1E
$F28D:B0 0E	BCS $F29D
$F28F:A6 17	LDX $0017 = #$00
$F291:9D 11 07	STA $0711,X @ $0723 = #$00
$F294:A5 18	LDA $0018 = #$00
$F296:9D 10 07	STA $0710,X @ $0722 = #$00
$F299:E6 17	INC $0017 = #$00
$F29B:E6 17	INC $0017 = #$00
$F29D:E6 18	INC $0018 = #$00
$F29F:A5 18	LDA $0018 = #$00
$F2A1:C9 19	CMP #$19
$F2A3:D0 D9	BNE $F27E
$F2A5:A6 17	LDX $0017 = #$00
$F2A7:A9 FF	LDA #$FF
$F2A9:9D 10 07	STA $0710,X @ $0722 = #$00
$F2AC:60	RTS


Процедура $C0DA, как мы помним, извлекает указатель на заголовок уровня и помещает его по адресам $6D:$6E, в регистре Y у нас смещение #03, так как информация о стражниках хранится после первых трех байт, которые отвечают за положение принца в начале уровня. Далее видно, что если первые 5 бит складываются в число #1E, то итерация пропускается, иначе по адресам, начиная с $0710, записывается следующая структура: <номер комнаты>:<координата стражника> — по два байта на структуру. Если у нас все комнаты забиты стражей, то последний адрес, куда мы сможем поместить данные, будет $0740, после которого, по адресу $0742, будет помещен маркер #FF. Но адреса, начиная с $0735, используются по другому назначению — это видно при штатной работе игры, а значит в данной ситуации возникает классическое переполнение буфера.
По адресу $0735 у нас хранится флаг, который отвечает за управление с геймпада. Если там 0, то управление стандартное, если не 0, то считается, что мы находимся в стартовой комнате, где управление ограничено. Сейчас, в этом «уровне», в следствие того, что координаты стражников перезаписали важные данные, в ячейке $0735 не ноль, и если мы поставим в нее 0, то сможем полноценно управлять принцем. Правда, дальше этой комнаты нам убежать не удастся, так как геометрия уровня нарушена.

То же касается и 16 «уровня». 17 и выше «уровни», если в момент набора пароля в $70 проставить нужный номер, попадают на откровенный мусор и просто не отображаются.

Эпилог в эпилоге

В этой статье я постарался рассказать, что распространенный миф о «секретных» уровнях не соответствует действительности. В принципе, разбирая код игры, и это было видно в предыдущих статьях, в игре вообще нет никаких секретов: скрытых комнат, уровней или чего-либо подобного. «Секретные уровни» — это банальное отсутствие нужного условия проверки в процедуре чтения паролей.

В качестве итога всего исследования могу сказать следующее: игра предельно простая в рамках второго маппера, и более того, разработана, скорее всего, в спешке. Видна довольно неплохая основа движка, но под конец явно доделывали уже на костылях: реализация двойника или нестандартные переходы между уровнями — просто хардкод, который вставлен посреди ровного кода. Отсюда и проявляются отличия NES-версии от остальных портов: мелочи под конец просто не причесали, хотя для доведения до ума достаточно было пары простеньких процедур. Если дописать это в виде тривиального патча к движку, то можно добиться почти полного соответствия с оригинальной версией, и тогда, как мне кажется, версия на NES даже выиграет по сравнению с DOS-версией. Уж хотя бы наличием музыкального сопровождения, системой паролей и более мрачной атмосферой.

На этом серия статей о реверсе NES закончилась. Теперь в планах стоит спуск на уровень ниже — уровень железа, так как в новую игру хочется поиграть и на настоящей Dendy. Об этом будет еще пару статей.
Tags:
Hubs:
+93
Comments4

Articles