Глава первая, Глава вторая, Глава третья, Глава четвертая, Глава пятая
Disclaimer
Несмотря на то, что я хотел сделать простой редактор, который позволит изменить лишь «внешний вид» уровней игры, двойник персонажа не давал мне покоя. Мне очень не хотелось лезть глубоко в движок игры, но этот негодяй, появляющийся из зеркала, затем выпивающий бутылку «отравы» в середине игры, а потом и вовсе сбрасывающий принца в пропасть, не давал мне покоя. Неделю я боролся с психологическим эффектом «незавершенного действия», но так и не смог его побороть.
В ночь с пятницы на субботу я снова открыл отладчик, RAM Filter и начал искать…
Появление на свет
Началось все с того, что мне прислали модификацию игры, сделанную в редакторе, с вопросом: Почему виснет при входе в одну из комнат?
Комната выглядела в редакторе вполне обычно. Проверка правильности работы редактора показала, что все в порядке: что попросили, то и сохранил. Я попробовал переместить объекты, которые поставил автор модификации в комнате, и обнаружил, что зависание пропало, но в комнате появился двойник, который, к тому же, еще и имел наглость атаковать:
То есть наличие объектов в комнате как-то влияет на наличие/отсутствие двойника. Со стражниками все ясно — их расстановка прописана прямо в заголовке уровня, а вот про двойника нет ни байта информации. Массивы данных для 4, 5 и 6 уровней ничем не отличались от всех остальных по своей сути. Полностью скопированный пятый уровень, скажем, в первый, не «вызывал» двойника из недр кода движка, а значит это как-то вшито в сам движок. Надо было понять, что меняется, если мы входим в комнату с двойником.
Я начал изучать пятый уровень, так как там двойник выполнял больше всего действий: если нажать на кнопку, которая открывает выход, он появлялся, выпивал драгоценное содержимое бутылки, и убегал. RAM Filter выявил аномальную активность при появлении двойника в памяти в районе адресов $0400-$0410: при входе в комнату взводился флаг в ячейке $0401, после нажатия на кнопку дополнительно взводился флаг в ячейке $0402, а затем, после того как тот убегал за пределы комнаты, ячейка $0401 обнулялась и больше не менялась. Значит, будем изучать, что тут происходит.
Наличие или отсутствие дополнительного персонажа (стражник, скелет или двойник) в комнате в NES версии вызывает замедление работы движка, и на эту особенность стоит обратить внимание.
Запускаем игру, ставим в $0401 единицу и действительно начинаем наблюдать замедление работы движка. Более того, нас снова начинает «атаковать» двойник.
Раз ячейка $0401 отвечает за наличие/отсутствие оного, то идем в отладчик, задаем точку останова:
В поле Condition задаем условие: аккумулятор при записи в ячейку не должен содержать 0. Изменение ячейки может происходить и через индексные регистры, но зачастую это производится именно через аккумулятор. Останов приводит нас сюда:
$A1D0:A9 00 LDA #$00
$A1D2:8D 01 04 STA $0401 = #$00
$A1D5:A5 51 LDA $0051 = #$17
$A1D7:8D DE 04 STA $04DE = #$17
$A1DA:AD 02 04 LDA $0402 = #$00
$A1DD:D0 4D BNE $A22C
$A1DF:A9 3D LDA #$3D
$A1E1:85 2F STA $002F = #$2D
$A1E3:A9 A2 LDA #$A2
$A1E5:85 30 STA $0030 = #$A2
$A1E7:A5 70 LDA $0070 = #$04
$A1E9:C9 05 CMP #$05
$A1EB:D0 04 BNE $A1F1
$A1ED:A5 51 LDA $0051 = #$17
$A1EF:F0 2D BEQ $A21E
$A1F1:A9 4D LDA #$4D
$A1F3:85 2F STA $002F = #$2D
$A1F5:A9 A2 LDA #$A2
$A1F7:85 30 STA $0030 = #$A2
$A1F9:AD FD 04 LDA $04FD = #$00
$A1FC:D0 0C BNE $A20A
$A1FE:A5 70 LDA $0070 = #$04
$A200:C9 03 CMP #$03
$A202:D0 06 BNE $A20A
$A204:A5 51 LDA $0051 = #$17
$A206:C9 03 CMP #$03
$A208:F0 14 BEQ $A21E
$A20A:A9 2D LDA #$2D
$A20C:85 2F STA $002F = #$2D
$A20E:A9 A2 LDA #$A2
$A210:85 30 STA $0030 = #$A2
$A212:A5 70 LDA $0070 = #$04
$A214:C9 04 CMP #$04
$A216:D0 14 BNE $A22C
$A218:A5 51 LDA $0051 = #$17
$A21A:C9 17 CMP #$17
$A21C:D0 0E BNE $A22C
$A21E:A9 01 LDA #$01
$A220:8D 01 04 STA $0401 = #$00 ;; <<< останов
$A223:8D E0 06 STA $06E0 = #$00
$A226:20 AD F2 JSR $F2AD
$A229:20 85 DB JSR $DB85
$A22C:60 RTS
Эта простыня довольно сложна для понимания, поэтому переведу ее в псевдокод, обозвав ячейки следующим образом:
— $70 — LEVEL;
— $51 — ROOM;
— $401 — MIRROR_FLAG.
С остальными позже разберемся:
char sub_A1D0()
{
MIRROR_FLAG = 0;
$04DE = ROOM;
if ( $0402 ) goto label_A22C;
$2F = #3D; $30 = #A2;
if ( LEVEL != #05 ) goto label_A1F1;
if ( !ROOM ) goto label_A21E;
label_A1F1:
$2F = #4D; $30 = #A2;
if ( $04FD ) goto label_A20A;
if ( LEVEL != #03 ) goto label_A20A;
if ( ROOM == #03 ) goto label_A21E;
label_A20A:
$2F = #2D; $30 = #A2;
if ( LEVEL != #04 ) goto label_A22C;
if ( ROOM != #17 ) goto label_A22C;
label_A21E:
MIRROR_FLAG = $06E0 = #01;
sub_F2AD();
sub_DB85();
label_A22C:
return;
}
Теперь это проще изучить. Как видим, тут перебираются аккурат те уровни и комнаты в них, где появляется двойник, устанавливается флаг его наличия и вызываются еще две процедуры. Приводить я их не буду, лишь опишу то, что они делают.
— $F2AD — выполняет поиск удвоенного маркера #FF, начиная с адреса $060E, затем возвращает длину последовательности байт между $060E и маркером #FFFF (не включая последний) в регистре Y;
— $DB85 — выполняет сдвиг последовательности байт, начиная с найденного предыдущей процедурой маркера #FFFF, вперед, на длину последовательности, адрес которой хранится в ячейках $2F:$30, и которая также заканчивается маркером #FFFF.
В ячейки $2F и $30, как мы видим, вносятся жестко заданные адреса где-то в ROM: $A22D, $A23D, $A24D.
Простого внесения единицы в ячейку $0401 недостаточно, надо еще сделать некие магические действия.
Двое из ларца, одинаковых с лица
Так как двойник появляется только тогда, когда взводится флаг в ячейке $0402, то поищем сперва, где производится запись в нее, а затем, где читается значение из нее. Взводится флаг, как и следовало ожидать, тогда, когда мы нажимаем на кнопку, открывающую выход (там ничего интересного). А вот чтение из ячейки производится тут:
$A319:A9 89 LDA #$89
$A31B:85 72 STA $0072 = #$89
$A31D:A9 A3 LDA #$A3
$A31F:85 73 STA $0073 = #$A3
$A321:AD 02 04 LDA $0402 = #$01 ;; << останов
$A324:F0 53 BEQ $A379
$A326:AD 01 04 LDA $0401 = #$01
$A329:F0 4E BEQ $A379
$A32B:EE 04 04 INC $0404 = #$00
$A32E:AC 03 04 LDY $0403 = #$00
$A331:B1 72 LDA ($72),Y @ $A389 = #$15
$A333:C9 FF CMP #$FF
$A335:F0 43 BEQ $A37A
$A337:CD 04 04 CMP $0404 = #$01
$A33A:D0 0B BNE $A347
$A33C:A9 00 LDA #$00
$A33E:8D 04 04 STA $0404 = #$01
$A341:EE 03 04 INC $0403 = #$00
$A344:EE 03 04 INC $0403 = #$00
$A347:C8 INY
$A348:B1 72 LDA ($72),Y @ $A389 = #$15
;; ...
Примечательная процедура, не правда ли? Суть ее действий, если изучить ее целиком, очень напоминает процедуру, которая играет за нас в demo play. В ячейках $72:$73 у нас адрес $A389:
15 01 06 0D 02 02 03 32 0C 4E 02 05 17 01 FF
Тот же маркер #FF, те же структуры, состоящие из двух байт, где первый из них — время, а второй… нет, второй — уже не имитация геймпада, а что-то иное. Когда отсчитываемое в ячейке $0404 значение сравнивается с первым байтом структуры, счетчик в ячейке $0403 увеличивается на 2 и процесс повторяется. Если посмотреть, что происходит во время этого процесса со вторым байтом, то мы придем к некоему массиву указателей:
0x15602: 00 00 AE 96 DB 96 64 97 9D 97 ...
Индексом в этом массиве как раз будет служить наш второй байт. Каждый указатель в этом массиве указывает на структуру, с которой движок работает довольно хитро. Если в этой структуре попадается значение, которое больше некоторого числа, то оно декодируется в индекс, который используется в массиве указателей на определенные процедуры в коде. Если же число меньше некоторого числа, то оно используется как аргумент для вышеобозначенных процедур. Процедуры эти, выполняя определенные действия, вносят следующий указатель в структуру, отвечающую за персонажа. Таким образом, после того, как в структуре персонажа будет задан некий стартовый указатель, начинает выполняться саморегулирующийся цикл, который приводит спрайт в движение. Например, если мы запишем в структуру указатель действия «бег», то каждый шаг бега будет инициироваться предыдущим, задавая новый указатель в этой же структуре в поле ActionPtr во время каждой итерации. Кроме того, в этих процедурах будет производиться перемещение спрайта на экране и озвучивание его действий.
Всего указателей в том массиве 93 штуки. То есть игра поддерживает 93 действия для персонажа. Но поскольку указатели иногда повторяются, то различных действий несколько меньше. Эту структуру (персонажа) я приводил ранее, поэтому не буду подробно останавливаться на ее разборе. Если же изучить действия двойника, то можно заметить, что его структура по смыслу повторяет структуру самого принца. Иными словами, когда в комнате появляется двойник, то после структуры, описывающей принца, вставляется такая же структура, которая описывает уже двойника. Взводится флаг в ячейке $0401, а дальше движок, в зависимости от номера уровня, номера комнаты и наших действий, вносит в эту структуру изменения, приводя, таким образом, двойника в движение.
char sub_A25D()
{
if ( !MIRROR_FLAG ) goto label_A277;
$06E0 = MIRROR_FLAG;
if ( level == #03 ) goto label_A28D;
if ( level != #04 ) goto label_A272;
goto label_A319;
label_A272:
// here CMP #05
goto label_A398;
label_A277:
return;
label_A278:
$0072 = #86;
$0073 = #A3;
$04FE = #00;
MIRROR.Y.LOW = #40;
return sub_A326();
label_A28D:
$04FB = $0402 = #00;
MIRROR.DIRECTION = #FF;
if ( room != #03 ) goto label_A2CF;
if ( $04FD ) goto label_A278;
if ( PRINCE.Y.LOW >= #48 ) goto label_A2CF;
$04FB = $04FC; // if prince around mirror (by Y pos)
if ( !$04FB ) goto label_A2C3;
if ( PRINCE.X.HI ) goto label_A2C3;
if ( PRINCE.X.LOW <= #AC ) goto label_A2D0;
label_A2C3:
A = #02;
Y = #0E;
sub_CAFD();
MIRROR.X.HI = #02;
label_A2CF:
return;
label_A2D0:
X = #98;
if ( !PRINCE.DIRECTION ) goto label_A2D9;
X = #94;
label_A2D9:
MIRROR.X.LOW = X;
MIRROR.X.HI = #00;
MIRROR.Y.LOW = PRINCE.Y.LOW;
if ( PRINCE.ACTION_INDEX == #06 ) goto label_A2C3;
if ( PRINCE.POSE_INDEX <= #06 ) goto label_A2F9;
if ( PRINCE.POSE_INDEX <= #0E ) goto label_A301;
label_A2F9:
if ( PRINCE.POSE_INDEX <= #20 ) goto label_A302;
if ( PRINCE.POSE_INDEX >= #28 ) goto label_A302;
label_A301:
return;
label_A302:
X = #00;
Y = #05;
label_A306:
MIRROR.ACTION_PTR = PRINCE.ACTION_PTR;
X++;
Y--;
if ( Y ) goto label_A306;
MIRROR.DIRECTION = PRINCE.DIRECTION xor #FF;
return;
label_A319:
$0072 = #89;
$0073 = #A3;
if ( !$0402 ) goto label_A379;
label_A326:
if ( !$0401 ) goto label_A379;
$404++;
if ( $72[Y] == #FF ) goto label_A37A;
if ( $0404 ) goto label_A347;
$0404 = #00;
$0403 += 2;
label_A347:
Y++;
A = $72[Y];
Y = #0E;
sub_CAFD();
if ( MIRROR.POSE_INDEX != #6D ) goto label_A379;
$0054 = $06F0 = #00;
$04B1 = #03;
sub_DB23();
A = #27;
sub_F203();
#0610[X] = #02;
A = #20;
sub_F203();
#0610[X] = #02;
label_A379:
return;
label_A37A:
$060E = $061C = $0401 = #00;
return;
A386:
.data[XX:YY],1C:01 FF // XX:time, YY:action, FF:EOF
A389:
.data[XX:YY],15:01 06:0D 02:02 03:32 0C:4E 02:05 17:01 FF
label_A398:
if ( level == #05 && !$610 ) goto label_A3A7;
if ( PRINCE.X.LOW <= #10 ) goto label_A3A7;
goto label_A3D1;
label_A3A7:
$0054 = #00;
$04B1 = #0C;
if ( !sub_DB18() ) goto label_A3D1;
$0072 = #D2;
$0073 = #A3;
sub_A326();
if ( MIRROR.POSE_INDEX != #7C ) goto label_A3D1;
$06FC = #00;
$06FB = #0B;
label_A3D1:
return;
A3D2:
.data[XX:YY]: 04:02 19:2A F0:02 F0:02 F0:02 FF
}
char sub_F203()
{
$0017 = A;
switch_bank(#02);
sub_B298();
$04BF = Y;
switch_bank($06D1);
Y = $04BF;
return;
}
char sub_B298()
{
X=#00;
label_B29A: // aka sub_B29A
Y=#00;
if ( #060E[X] != #FF ) goto label_B2A4;
Y++;
label_B2A4:
if ( #060E[X] & #7F == $0017 ) goto label_B2BC;
if ( #060F[X] != #FF ) goto label_B2B2;
Y++;
label_B2B2:
if ( Y == #02 ) goto label_B2BF;
sub_F215();
goto label_B29A;
label_B2BC:
Y = #01;
return;
label_B2BF:
Y = #00;
return;
}
char sub_8730()
{
if ( A == #0616[Y] ) goto label_874B; // $0616+Y - address of MIRROR.ACTION_INDEX
$0616[Y] = A;
X = A << 1;
#0613[Y] = #95F2[X]; // set MIRROR.ACTION_PTR to new value ($0613 + Y - address of ACTION_PTR in MIRROR struct)
#0614[Y] = #95F3[X];
#0618[Y] = #FF; // set EOF marker
label_874B:
return;
}
Стоит заметить, что довольно интересна выполнена процедура «отражения» в зеркале. Если принц находится рядом с зеркалом, то двойник помещается в соседнюю от него позицию, указатель действия принца копируется в структуру двойника, а байт направления инвертируется. Если позиция принца «прыжок с разбега», то двойник «выбегает» из зеркала и убегает прочь.
Делаем патч
Такой код увязать с редактором довольно сложно. Можно, конечно, редактировать те короткие массивы данных, которые использует вышеприведенный код, но пришлось бы проделать определенную работу, а эффект был бы незначительный. Редактировать же этот код редактором слишком сложно. Хотелось чего-то большего. И решение было найдено.
Для управления двойником нам достаточно будет скопировать его структуру после структуры самого принца и выставить флаг $0401. В дальнейшем мы будем задавать действия, которые он будет выполнять, записывая указатель в его структуру. Но как это сделать? Нужно написать код. Но куда его вставить? В игре практически нет свободных мест, а те небольшие огрызки по несколько байт, которые остались между кодом и данными, использовать невозможно. Значит, надо изыскивать дополнительное место иными способами.
Шире круг
Как мы помним, Mapper #02 содержит в себе два различных вида маппинга. Один из них — UNROM, — содержит в себе 8 банков PRG-ROM, а второй — UOROM, — 16. Если вставить в ROM-файл еще 8x16 кБ, то маппер изменится на UOROM без вреда для игры. Вставить, однако, надо так, чтобы последний банк так и остался последним, а первые 7 должны остаться в начале.
Лезем в шестнадцатеричный редактор, меняем в заголовке число банков (пятый по счету байт, смещение 0x04) с #08 на #10, затем вставляем в файл 8x16 кБ нулей, начиная со смещения 0x1C010. Размер ROM-файла изменится и станет равным 262 160 байт. Запустим полученный ROM в эмуляторе… Работает!
Если мы будем выполнять эту процедуру в «железе», то нам потребуется поменять контроллер маппера, а ROM-память поставить увеличенную — 32x8 кБ, и мы также получим работающую игру.
ROM увеличили, место есть, но как им воспользоваться? Для того, чтобы вызвать код или прочитать оттуда данные, нам надо включить этот банк и передать туда управление. Сделать это можно безопасно только из последнего банка, но в нем нет места. Куда же писать код?
Зададимся требованиями:
- Код должен вызываться из основного цикла игры, так как мы будем управлять двойником прямо во время основной игры;
- Код должен вызываться и возвращать управление в последний банк;
- Вызываемый код не должен нарушать работу оригинального кода.
Вспомним основной цикл. Там у нас вызываются различные процедуры, перед которыми вызываются процедуры включения соответствующего банка. Вставить две новых процедуры и добавить новые вызовы в основной цикл довольно сложно, но мы можем поменять одну из процедур включения банка.
Возьмем конец цикла:
;; ...
$CC3B:20 00 CB JSR $CB00
$CC3E:20 12 9F JSR $9F12
$CC41:20 DD A3 JSR $A3DD
$CC44:4C 1A CC JMP $CC1A
Процедуры $9F12, $A3DD трогать нельзя, так как они в другом банке, на месте которого должен быть наш. Перенести их туда тоже нельзя, так как они потянут за собой все остальное содержимое банка. Можно, однако, поменять адрес $CB00 на адрес новой процедуры, которая будет включать наш банк #07, вызывать наш код, затем вызывать оригинальную процедуру $CB00 и возвращать управление обратно в цикл. Код примерно такой:
LDA #07
JSR $F2D3 ;; включаем банк #07
JSR $8000 ;; вызываем наш код, который будет в начале банка
JMP $CB00 ;; вызываем оригинальную процедуру $CB00,
;; которая сама инструкцией RTS вернет управление в основной цикл
12 байт (по 3 байта на каждую инструкцию). Немного, но их надо куда-то вставить, а места и так нет. Немного места можно выиграть путем модификации существующего кода в последнем банке. Достаточно взять какую-нибудь длинную процедуру, которая не использует данные из банков с #00 по #06, которая так же не вызывает процедур, использующих эти банки, и которая вызывается из процедур, которые не используют эти банки. После того, как мы ее найдем, мы сможем перенести ее в новый банк, на ее месте разместить наш код, а перед нашим кодом поместим вызов страдалицы из нового банка.
Долго ли, коротко ли, такая процедура была найдена:
$C111:AD C6 06 LDA $06C6 = #$00
$C114:C9 06 CMP #$06
$C116:B0 20 BCS $C138
$C118:AD D7 06 LDA $06D7 = #$04
$C11B:D0 1B BNE $C138
$C11D:8D 30 07 STA $0730 = #$00
$C120:85 54 STA $0054 = #$01
$C122:8D F0 06 STA $06F0 = #$00
$C125:AD 17 06 LDA $0617 = #$0C
$C128:C9 11 CMP #$11
$C12A:90 0D BCC $C139
$C12C:C9 2B CMP #$2B
$C12E:B0 09 BCS $C139
$C130:C9 1A CMP #$1A
$C132:90 04 BCC $C138
$C134:C9 26 CMP #$26
$C136:90 01 BCC $C139
$C138:60 RTS
Что она делает — вопрос открытый, но впрочем это и не важно. Главное, что она соответствует всем необходимым требованиям: не вызывается из «динамических» банков, сама не вызывает код из них и не обращается к ним. А переход по адресу $C139 мы немного переделаем.
Немного модифицируем наш код включения банка и поместим его по адресу $C111:
$C111:20 17 C1 JSR $C117 ;; вызываем код включения банка
$C114:4C 90 BF JMP $BF90 ;; и вызываем оригинальную процедуру (она теперь живет по адресу $BF90)
;; потом она инструкцией RTS вернет управление вызвавшему коду сама
;; ======== процедура включения нашего банка ==========
$C117:A9 07 LDA #$07
$C119:4C D3 F2 JMP $F2D3
;; ======== конец процедуры включения нашего банка ==========
;; а теперь код, вызывающий наш патч
$C11C:20 17 C1 JSR $C117 ;; включаем банк
$C11F:20 10 B0 JSR $B010 ;; вызываем наш патч. Начало нашего кода - $B010.
$C122:4C 00 CB JMP $CB00 ;; вызываем оригинальную $CB00, которая вернет нас обратно в цикл
$C125:00 BRK
;; ...
$C138:00 BRK
Мало того, что мы успешно впихнули вызов нашего кода, так у нас еще и осталось уйма места с $C125 по $C138, которое мы можем использовать как-нибудь еще: ведь у нас есть еще целых 7(!) свободных банков, а с ними тоже надо будет работать из последнего банка (если мы их будем в будущем использовать). Адрес нового кода я разместил по адресу $B010 (примерно середина банка), так как нам придется размещать копию уровней и комнат в нашем банке, плюс еще кое-какие данные. Но об этом чуть ниже.
Модифицируем и саму процедуру, так как в ней есть переходы на адрес $C139, который за ее пределами:
$BF90:AD C6 06 LDA $06C6 = #$00
;; ============ CUT ============
$BFAA:90 0D BCC $BFB9
$BFAC:C9 2B CMP #$2B
$BFAE:B0 09 BCS $BFB9
$BFB0:C9 1A CMP #$1A
$BFB2:90 04 BCC $BFB8
$BFB4:C9 26 CMP #$26
$BFB6:90 01 BCC $BFB9
$BFB8:60 RTS
$BFB9:4C 39 C1 JMP $C139
Все! То, что когда-то переходило на адрес $C139 теперь переходит на адрес $BFB9, по которому инструкция JMP заставляет прыгнуть на $C139, словно мы никуда и не перемещали код.
Тело основного цикла теперь мы можем поменять на следующее:
;; ...
$CC3B:20 00 CB JSR $C11С
$CC3E:20 12 9F JSR $9F12
$CC41:20 DD A3 JSR $A3DD
$CC44:4C 1A CC JMP $CC1A
Можем разместить по адресу $B010 какую-нибудь пустышку вроде «RTS» и запустить игру в эмуляторе. Все как было — так и осталось.
Пишем свой «привод» для «отражения»
Осталось только разработать свой алгоритм появления отражения в комнате.
Я разработал следующую структуру данных:
При входе в комнату, мы по номеру уровня извлекаем указатель в первом массиве указателей (Levels ptrs), если он не нулевой, то переходим ко второму массиву (Rooms ptrs).
Если извлеченный по номеру комнаты указатель также не нулевой, то приступаем к чтению структуры отражения.
Структура отражения выглядит следующим образом:
- Структура, описывающая начальное состояние персонажа (struct CHARACTER);
- Пары «время»:«действие», которые описывают, что будет делать персонаж и в какой интервал времени;
- Маркер окончания #FF.
Дабы не утомлять читателя избытком кода, я не буду его приводить здесь. Он будет в конце статьи.
Реагируем на события
Безусловное появление отражения неинтересно. Вроде оно и есть, но толку никакого. Хотелось бы, чтобы он появлялся в соответствии с какими-либо игровыми событиями и что-то умел делать.
Начиная с адреса $0500 в памяти у нас лежат данные, которые определяют те или иные изменения в комнатах. Например, если персонаж выпьет зелье из бутылки, то бутылка там больше не появится. Либо, если упадет плита, то в этом массиве будет хранится как то, что теперь на ее месте дырка, так и то, что на том месте, куда она упала — ее осколки. То же самое с открытыми и закрытыми решетками. Каждое такое событие кодируется двумя битами. В комнате у нас 30 блоков, на каждую строку из 10 блоков приходится по 3 байта (4 блока умещается в 1 байте, причем в третьем байте оставшиеся 4 бита остаются неиспользованными), итого по 9 байт на комнату. На уровень, таким образом, выходит 9*24 = 216 байт.
Как только произошло какое-нибудь действие, соответствующая пара бит в этом массиве устанавливается в определенное значение. Всего возможных комбинаций — 3 (00 — означает, что ничего не происходило), а событий много: решетка открылась, решетка закрылась, выпили из бутылки, плита отсутствует (упала), осколки упавшей плиты; соответственно события перекрываются. Например, если мы поставим бутылку, а над ней повесим падающую плиту, то после ее падения мы либо недосчитаемся бутылки, либо не увидим осколков упавшей плиты.
Применим эти знания к нашему патчу. В структуру, которую мы придумали, добавим адрес, который следует постоянно считывать, и значение, с которым следует сравнивать. Пока по указанному адресу не будет нужного значения мы не будем приводить в движение нашего двойника.
Заставляем нажать кнопку
Ну и напоследок остается научить его что-либо делать. Двойник «из коробки» не умеет делать ровным счетом ничего. Умеет лишь появляться на экране и совершать какие-либо движения, мозоля глаза. Контактировать со стенами он не умеет, нажимать на кнопки не умеет, да и вообще он ничего не умеет.
Пока наш принц что-то делает в игре, его координаты постоянно сравниваются с блоком, в котором он находится, и если этот блок «активный», то предпринимаются определенные действия: например, если мы попали в блок «Кнопка», то эта кнопка будет нажиматься. Двойник же бегает сам по себе и никто за его передвижениями не следит. Значит, мы в своем патче обязаны это сделать за основной движок. Для этого достаточно передать в основной движок (в процедуру проверки) координату двойника вместо координаты принца, а дальше все произойдет само. Но проблема в том, что движок сравнивает блок с массивом данных комнаты, который хранится в другом банке. Тем не менее, в нашем банке еще достаточно места, а во время проверки будет включен именно он, поэтому мы… просто скопируем игровые данные из оригинального банка в наш на то же место, и движок будет считывать эти данные как ни в чем не бывало (помните, ранее мы разместили код в середине банка?). Во время редактирования, правда, теперь надо будет учесть, что изменения следует вносить в основную копию и в резервную.
Результат налицо:
FIN
В конечном итоге удалось заставить двойника быть не просто мебелью, а выполнять простейшее действие — нажать кнопку и открыть нам какую-нибудь дверь. Уже с такой простой вещью в игру удалось привнести элемент головоломки: пока не нажмешь кнопку, не разобьешь плиту или не выпьешь из бутылки в одном конце уровня — в другом не появится отражение, которое не нажмет за тебя кнопку, открывающую, к примеру, выход.
Что ж, теперь можно добавить и игру вдвоем, или новых персонажей, или новые блоки, но… это уже будет бы другая игра. Я к этому времени свое любопытство полностью удовлетворил, поэтому закрыл отладчик и отправился спать. Начинался теплый июльский понедельник.
PS
Но это еще не конец. После окончания последует эпилог: с редактором я закончил, но остался еще один невыясненный вопрос, который хотелось бы разобрать. Так что впереди «Эпилог. Темница».
Прикладываю ссылку, по которой можно загрузить архив с редактором, небольшой документацией по игре и исходному коду патча.
Автор: loginsin