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

в 12:08, , рубрики: diy или сделай сам, emulation, hacking, Nes, Игровые приставки, метки: , ,

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

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, когда эмуляторов в их нынешнем виде и в проекте не было), я попадал в странные места, которые явно не были предусмотрены разработчиками:
[NES] Пишем редактор уровней для Prince of Persia. Эпилог. Темница
или
[NES] Пишем редактор уровней для Prince of Persia. Эпилог. Темница

Управление в этих «уровнях» работает только частично, внешний вид странный, да и попасть туда нельзя из основной игры.
Сейчас, глядя на то, как движок хранит данные уровней, я даже убедился в том, что больше 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, следовательно, это где-то внутри второй комнаты, которая выглядит так:
[NES] Пишем редактор уровней для Prince of Persia. Эпилог. Темница.
Стражник должен располагаться в левом верхнем углу, исходя из представленного заголовка.

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

Сравним:
[NES] Пишем редактор уровней для Prince of Persia. Эпилог. Темница
Так как граница этой «комнаты» не совпадает с границей второй комнаты второго уровня, то видно некоторое смещение «архитектуры» влево. Также слева виден обрыв, который соответствует соседней комнате, но ее, согласно неправильной геометрии уровня, нет.

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

Теперь я предлагаю посмотреть, как используется информация о стражниках в уровне.

$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. Об этом будет еще пару статей.

Автор: loginsin

Источник

  1. TmpRoot:

    Охренеть.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js