Навигация по сайту
- Игры / Образы
- Игры на русском языке
- Коды / Советы / Секреты
- Наши переводы
- Наши проекты
- Игры на русском языке (OnLine)
- Эмуляторы
- Обзоры игр
- Информация
- Статьи
- Интервью
- Мануалы / Инструкции
Случайная игра
Вступай!!!
Облако тегов
Показать все теги
ДЕБАГГИНГ
Долавливаем баги за разработчиками
Долавливаем баги за разработчиками
Дебаггинг [от англ. debugging – разжукование] – процесс изведения ошибок программного кода, по недосмотру, криворукости или злому умыслу оставленных там разработчиками. Включает в себя детектирование глюка, локализацию его местонахождения в программном коде и последующее убиение.
“Еще одна суперспособность Дона - кидать мяч в любой момент времени. Дон может кинуть мяч во время прыжка, во время пилы, во время получения урона, во время нанесения урона, даже во время бросания мяча. Это позволяет Дону миксовать даже во время прыжков с мячом. С помощью данной способности Дон может и вовсе стать бессмертным (снимается бессмертие либо броском, либо пилой). От чего зависит, получит ли Дон бессмертие или нет, не понятно по сей день. Он может стать бессмертным в любой момент бросания мяча в момент получения/нанесения урона, однако происходит это редко, и обычно с Доном все нормально. Есть еще несколько гарантированных способов стать бессмертным – кинуть мяч в верхней точке пилы, кинуть мяч во время броска”.
После такого тизера, оставленного в предыдущем выпуске в статье про Teenage Mutant Ninja Turtles: Tournament Fighters, пришло время снять покровы с этого бага, а заодно и показать народу, как же вообще находятся причины багов. Название главного инструмента для этого дела весьма капитанское: debugger (по-русски – отладчик). Весь процесс называется reverse engineering (обратная разработка), а также disassembling (перевод машинного кода в ассемблерный). Научившись работать с дебаггером и сопутствующими инструментами, вы сможете пойти ещё глубже и начать собственноручно хакать игры, но о том, как делать хаки/переводы/Game Genie коды, я рассказывать не буду. Во-первых, потому что эти радости невозможны без навыков дебага, во-вторых – потому что с такими навыками осилить ромхакинг не составит труда.
Итак, с чего начать? Создадим-ка эмуляторный мувик, в котором у нас будет срабатывать данный глюк, и потом, перемещаясь по нему, отследим причины.
Скачиваем последнюю версию FCEUX, самого богато- го на нужные нам фичи эмулятора. Запускаем в нем игру Teenage Mutant Ninja Turtles - Tournament Fighters (U) [!]. nes, открываем TAS Editor (меню Tools) для упрощения создания мувика и навигации по нему. В меню TAS Editor’а File -> New, ставим радиокнопку “2 players”.
Игра сбрасывается, после чего в панели Recorder ставим галку Recording и радиокнопку “1P”, средней кнопкой мыши снимаем эмуляцию с паузы. Доходим до времени, с которого можно попасть в главное меню игры, жмем на клавиатуре клавишу, на которую назначен Start, и она записывается в мувик на соответствующие кадры. Задача – попасть в режим игры на двоих. В меню выбора игрока ставим игру опять на паузу и снимаем галку Recording. Нужно выбрать для второго игрока Донателло. Для этого в соответствующем наборе столбцов “ТАС Эдитора” (более темные) мышью кликаем по ячейкам, выставляя нажатия на кадры мувика (Вправо, Вправо, то есть 2 нажатия в ячейку “R” одно за другим с перерывом в кадр). Потом для обоих игроков выставляем нажатия в столбце “Т”, чтобы сработал старт. Можно вернуть галку Recording, отжать паузу и пронажимать Start на клавиатуре для попадания на арену. Потом нужно подолбить одного из игроков и снять ему больше половины здоровья, чтобы вылетел шарик.
За Донателло подбираем его, нажимая Вниз+Б. Делаем спешл: на некоторое время присесть, потом нажать одновременно Вверх+А. Где-то на середине прыжка остановите запись (эмуляторная пауза средней кнопкой мыши). Нам нужен момент, пока Дон еще взлетает. Подогнать нужный кадр можно, крутя колесо мыши с зажатой правой кнопкой (или ставя голубой курсор руками, кликая по самой левой колонке). Кликаете по ячейкам, ставя нажатия Дону: “D” (Вниз) на один из кадров, когда он поднимается в воздухе, на следующий кадр “B” и Вперед (“L” или “R”, в зависимости от направления). Дон в игре должен выплюнуть шарик, и после приземления он будет уже бессмертен.
Нужно это проверить, попробовав побить его другим игроком. Для этого в панели Recorder поставьте галку Recording и радиокнопку 2P, потом снимите с паузы и запишите нужный ввод (т. е. избиение Дона). Мувик готов! Можно переходить к поиску причин глюка.
Мое первое подозрение: игра использует флаг неуязвимости, выставляя его в моменты, когда персонаж неуязвим (спасибо, Кэп!), и при наложении одного приема, дающего неуязвимость, на другой, дающий неуязвимость, этот флаг игра забывает обнулить, и он остается активным. Потом при повторении нужного приема (бросок или спешл) флаг таки сбрасывается опять. Проверим, насколько это предположение соответствует действительности.
ПОИСК АДРЕСОВ В ПАМЯТИ
Теперь нужно найти саму ячейку, в которой хранится этот флаг. Лезем в Tools -> RAM Search. Мотаем игру (ко- лесо мыши с зажатой правой кнопкой) на место, где Дон уже неуязвим, то есть после метания шарика. Жмем в RAM Search кнопку Reset. Проматываем немного вперед. В RAM Search ставим радиокнопки: Comparison Operator - “Equal To”, Compare To/By - “Previous Value”. Data Type/Display - “Hexadecimal”. Жмем кнопку Search. Прописываем Дону бросок оппонента (подойти вплотную, нажать Вперед+B). Меняем радиокнопку Comparison Operator на “Not Equal To”. После окончания анимации броска жмем опять Search. Опять меняем радиокнопку Comparison Operator на “Equal To”, проматываем несколько раз вперед, каждый раз делая Search. После определенного количества повторений этой операции у меня осталось 8 адресов.
Мотаем игру в точку до совершения Доном спешла. Ещё раз жмем Search, отсеивая все что изменилось. У меня остался только 1 адрес - 0581, который имеет значение 00 все время кроме периодов совершения Доном бросков и спешлов, когда он становится 80, то есть из всех восьми битов, входящих в байт, у него в периоды неуязвимости ставится один, самый левый.
Тестируем: если убрать нажатие B из комбинации пускания шара, флаг обнулится в верхней точке спешла. Если бросить шар из сидячего положения, флаг вообще не изменится. Когда Дон перекидывает соперника, флаг становится 80, потом сбрасывается. То есть, мое предположение частично не оправдалось: флаг есть, но шарик его не выставляет. Зато стала яснее причина: в коде метания шарика, судя по всему, отсутствует снятие флага неуязвимости. А раз Дон может метать шарик в любой момент игры, прерывая любое событие, метание в момент неуязвимости прерывает прием, дающий неуязвимость. Следовательно, и процедура снятия оной, прописанная в коде этого приема, попросту не срабатывает.
Однако наша миссия – не построение самой разумной гипотезы, а копание кода. На вопрос “почему, собственно, метание шарика срабатывает у Дона в любое время, а у других нет?” без этого не ответить. А баг в том и состоит, что шарик у него кидается в период неуязвимости, ведь если бы этого не происходило, код снятия неуязвимости не пропускался бы. Следовательно, теперь нужно найти адрес, который должен запрещать кидание шарика в определенные моменты. Либо часть кода кидания шарика, которая у всех персов, читает флаг неуязвимости и не дает его кинуть, если флаг установлен, и которая бы была забагована/отсутствовала у Дона. В общем, вычислить прокол в программировании.
Для начала я хочу найти в памяти сам объект шарика, который создается в момент броска после нажатия определенной комбинации, но не создается во время неуязвимости у обычных персонажей.
Маленькое отступление. Практически все игры имеют системы объектов, которыми являются такие элементы игры, как персонажи, враги, айтемы, снаряды и тому подобное. Объект создается в нужный момент (например, при достижении камерой определенной позиции), у него есть дескриптор (идентифи¬катор, номер спрайта, позиция на экране или в уровне, здоровье, всякие флаги состояния) и уникальный код, отличающий его поведение от поведения прочих объектов. Объекты в памяти могут быть расположены по-разному. Самый простой вариант – это статичная таблица слотов (“гнезд”, в которые садятся объекты) фиксированного размера. В ней объекты лежат либо строками (кончается весь дескриптор одного объекта, начинается дескриптор другого), либо столбцами (игра перечисляет сначала один атрибут для всех объектов, потом другой, потом третий, и так далее). Но может быть и динамическое добавление слотов, например, каждый новый объект вставляется на место последнего созданного, а существующие сдвигаются (Ghosts ‘n Goblins), или они вставляются один за другим вверх или вниз, один объект может иметь ссылку на дескриптор следующего (Adventures Of Batman & Robin) – в общем, кто во что горазд.
Даже поверхностное изучение системы объектов в игре существенно облегчит обратную разработку ее движка и трюков/багов. Также сильно помогают Lua-скрипты, которые могут отобразить на экране нужные нам свойства объектов в виде приятной глазу и доходчивой таблицы значений. Хотя если нет нужды что-то всерьез изучать, можно обойтись и встроенным во FCEUX hex-редактором (Debug -> Hex Editor). В нем можно и имена адресам давать, и брейкпоинты ставить, и просто наблюдать за изменениями ячеек, выслеживая закономерности на глаз. Также можно, конечно, и править память, включая копипаст целых страниц (ну, сколько выделите).
Итак, вернемся к шарику. Проще всего обнаружить его дескриптор, найдя его координаты. Опять же RAM Search, только теперь без пошагового описания. Просто найдите, какое число растет, пока шарик падает, уменьшается, пока он взлетает, и не меняется, пока он лежит на месте. Это будет его Y координата. Также найдите координату X (это лучше делать в самой левой части экрана, где камера равна нулю и байт горизонтальной позиции шарика не будет переполняться).
Что значит “переполняться”? Байт может иметь значение от 0 до 255. Экран NES в ширину 256 пикселей. Если игра использует пространство шире одного экрана, позиция объектов может быть больше 255. В таких случаях играм приходится использовать дополнительный (старший) байт для позиции, который бы считал сколько целых младших байтов игрок уже накрутил. Например, если я пробегу 4 целых экрана и остановлюсь в середине пятого, моя позиция будет чем-то вроде 1120 в десятичном представлении, то есть 0x460 в шестнадцатеричном (которое использует Hex Editor, и префикс которого 0x). Глядя на второе число, сразу понятно: я пробежал 4 целых экрана по 255 пикселей и еще 0x60 пикселей (96 в десятичном виде). Младший байт 4 раза переполнился, каждый раз увеличивая старший на 1.
В общем, я нашел такие адреса для координат: Y - 0x416, X - 0x446. Далее, наблюдая за изменениями соседних ячеек при перемещениях шарика, я выяснил, что дескриптор представляет собой столбец, каждый следующий атрибут расположен в хекс-редакторе на 1 строку ниже предыдущего. Кликая по угаданным атрибутам правой кнопкой мыши, я вызывал диалог задания символьного имени и в итоге получил такой файл Teenage Mutant Ninja Turtles - Tournament Fighters (U) [!].nes.ram.nl в папке с игрой (эти имена помогут нам при дебаге, заменяя собой адреса):
$0406#BallSprite#
$0416#BallYpos#
$0426#BallYposSub#
$0436#BallXposHi#
$0446#BallXposLo#
$0456#BallXposSub#
$0466#BallYspeed#
$0476#BallYspeedSub#
$0416#BallYpos#
$0426#BallYposSub#
$0436#BallXposHi#
$0446#BallXposLo#
$0456#BallXposSub#
$0466#BallYspeed#
$0476#BallYspeedSub#
Эти адреса уже можно использовать для рисования методами Lua. Например, скопируйте следующий текст в текстовый файл, смените расширение на .lua и затащите файл в эмулятор:
function drawBall()
objectBase = 0x400 -- начало таблицы объектов
slot = 6 -- слот
address = objectBase+slot -- адрес в памяти
sprite = memory.readbyte(address) -- значение спрайта
y = memory.readbyte(address+0x10) -- позиция по вертикали
x = memory.readwordsigned(address+0x40,address+0x30) -- позиция по горизонтали, 2 байта
gui.box(x-8,y-8,x+8,y+8,”#ff000040”) -- рисуем рамку
gui.text(x-7,y-7,sprite) -- рисуем текст
end
emu.registerafter(drawBall) -- повторяем каждый кадр
objectBase = 0x400 -- начало таблицы объектов
slot = 6 -- слот
address = objectBase+slot -- адрес в памяти
sprite = memory.readbyte(address) -- значение спрайта
y = memory.readbyte(address+0x10) -- позиция по вертикали
x = memory.readwordsigned(address+0x40,address+0x30) -- позиция по горизонтали, 2 байта
gui.box(x-8,y-8,x+8,y+8,”#ff000040”) -- рисуем рамку
gui.text(x-7,y-7,sprite) -- рисуем текст
end
emu.registerafter(drawBall) -- повторяем каждый кадр
Вы увидите рамку вокруг шарика и число, показывающее номер его спрайта. Также видно, что шарик точно следует за персонажем, который его подобрал. Если вы поменяете значение slot в скрипте на 0 или 1 (и нажмете Restart в диалоге Lua Script), рамка будет показывать на одного из игроков, а если на 7, то на Сплинтера. Итак, значения спрайта шарика:
0 - спрайта нет
1 - красный шар
5 - стрелка
203 - запущенный шар
Вот и наша цель! Надо выяснить, какого лешего после комбинации шарика в его спрайт таки пишется 203 во время спешла и перекидывания. Для Лео (который у меня первый игрок) это значение 202, и он не то что при спешле или перекидывании, он даже при простом прыжке отказывается кидать шар. Значит его код тоже стоит изучить и сравнить с доновским.
Так как между комбинацией и броском шара есть задержка в несколько кадров, проще будет ставить брейкпоинт не на появление шарика, которое всегда следует за анимацией, а на сам запуск анимации у персонажа. Для этого посмотрим, как активируется шарик у Дона. Слегка изменим скрипт:
function draw(slot)
objectBase = 0x400 -- слот
address = objectBase+slot -- адрес в памяти
sprite = memory.readbyte(address) -- значение спрайта
y = memory.readbyte(address+0x10) -- позиция по вертикали
x = memory.readwordsigned(address+0x40,address+0x30) -- позиция по горизонтали, 2 байта
if sprite>0 then gui.text(x-7,y-7,sprite) end -- рисуем текст
end
function all() -- рисуем все 3 объекта
draw(0)
draw(1)
draw(6)
end
emu.registerafter(all) -- повторяем каждый кадр
objectBase = 0x400 -- слот
address = objectBase+slot -- адрес в памяти
sprite = memory.readbyte(address) -- значение спрайта
y = memory.readbyte(address+0x10) -- позиция по вертикали
x = memory.readwordsigned(address+0x40,address+0x30) -- позиция по горизонтали, 2 байта
if sprite>0 then gui.text(x-7,y-7,sprite) end -- рисуем текст
end
function all() -- рисуем все 3 объекта
draw(0)
draw(1)
draw(6)
end
emu.registerafter(all) -- повторяем каждый кадр
Видно, что при выполнении спешла спрайт Дона будет 178/179, потом внезапно он сядет в воздухе (сразу после Вперед+B), а его спрайт станет 153 (0x99). Спрайт Лео же из просто стоящего станет “интенсивно вдыхающим”, со значением 195 (0xС3) сразу после Вперед+B. Мы будем отслеживать запись этих значений в адреса спрайтов.
Последнее недостающее звено - это адреса, в которых игра хранит ввод кнопок. Их найти легко. Выставьте голубой курсор на любой кадр без нажатий, прямо перед ним поставьте нажатие “R” и сделайте поиск ячейки, равной 1 (значение кнопок NES вписывается в 1 байт и каждая кнопка соответствует одному биту). Смените нажатие на “L” и найдите ячейку со значением 2. Потом вместо этого выставьте нажатие “A” и найдите ячейку со значением 128 (0x80). Останутся ячейки 0x91 и 0xFA для первого игрока, и 0x92 и 0xFB для второго. Можно их добавить в RAM Watch или список имен .nl. Из наблюдения в хекс-редакторе за адресами 0x91, 0x92 и сопутствующими им видно, что 0x91 и 0x92 хранят удерживаемые кнопки, а 0x8E и 0x8F - только что нажатые.
Нам понадобится найти зависимость между нажатием кнопок метания мяча и срабатыванием этого приема. Если обнаружится разница в цепочке “обработчик кнопок – передача нажатий – активация приема” для Дона и Лео, задача будет решена.
ДЕБАГ
Сделаем файл символьных имен более универсальным, заодно добавив адреса ввода:
$008E#1pTap#
$008F#2pTap#
$0091#1pHold#
$0092#2pHold#
$0400#Sprite#
$0410#Ypos#
$0420#YposSub#
$0430#XposHi#
$0440#XposLo#
$0450#XposSub#
$0460#Yspeed#
$0470#YspeedSub#
$008F#2pTap#
$0091#1pHold#
$0092#2pHold#
$0400#Sprite#
$0410#Ypos#
$0420#YposSub#
$0430#XposHi#
$0440#XposLo#
$0450#XposSub#
$0460#Yspeed#
$0470#YspeedSub#
Пропишем для Лео кидание шара и перейдем в кадр нажатия Вперед+B. Откроем долгожданный диалог дебага: Debug -> Debugger. Около поля Breakpoints нажмем кнопку Add. В поле Address пишем 400 (для Лео), ставим галку Write, жмем OK. В диалоге дебаггера жмем кнопку Reload Symbols, а затем Run.
Игра остановилась на строке
01:8694:9D 00 04 STA Sprite,X @ Sprite = #$80
которую мы видим в поле дизассемблированного кода.
Что она означает:
01 - это банк РОМа, в данный момент загруженный в память.
8694 - это адрес в РОМе, в котором приписан код записи спрайту определенного значения.
9D 00 04 - это машинный код команды, нам он малоинтересен.
STA - собственно команда (опкод) записи в ячейку памяти приставки (RAM) из аккумулятора (одного из регистров памяти процессора).
Sprite - адрес для записи.
X - указание, что надо записать не непосредственно в указанный адрес, а в ячейку, отстоящую от него на значение в регистре X.
@ Sprite = #$81 - расшифровка, показывающая адрес, полученный после выполнения смещения, и его текущее значение. В следующий момент игры оно изменится на то, которое записано в аккумуляторе.
Взглянем теперь на регистры.
A: C3, X: 00.
Видим, что из аккумулятора (A) в адрес спрайта, который был равен 0x80, запишется число 0xC3. Так как X равен нулю, запись будет в слот первого игрока. При записи второму игроку X будет равен 1, для шарика будет 6, для Сплинтера 7, сдвигая целевой столбец до нужного слота.
Для справки: у процессора NES три наиболее важных для нас регистра: аккумулятор, X и Y. Это подобно наличию в памяти всего трех байтов. Большинство операций производится с аккумулятором. Прочие регистры, хоть и могут хранить значения, но не так критичны для дебага нашего уровня.
Правый клик по 8694, вписываем в поле Name: SetBallSprite.
Теперь найдем обработку кнопок. Предположим, что адреса для удерживания кнопок тут не участвуют, так как шарик активируется немедленно после нажатия Вперед+B. Значит ищем, кто и зачем читает из 0x8E (для Лео) и из 0x8F (для Дона).
Кнопка Add брейкпоинт. Вписываем адрес 8E и ставим галку Read, OK. То же самое для адреса 8F. Пока что выключим их оба и запишем одному из персонажей (у меня это Дон, второй игрок) кидание мяча. Ставим игру на кадр, в который произошло нажатие Вниз+B. В следующий кадр оно будет прочитано, и брейкпоинт должен сработать. Активируем нужный из них и жмем кнопку Run.
Видим код:
Стрелка указывает на текущий шаг.
LDA - загрузка значения из адреса в памяти в аккумулятор.
ORA - побитовое сложение его с другим значением в памяти.
STA - запись значения аккумулятора в память.
Видно, что адреса $90 и $93 хранят сумму нажатий первого и второго игрока и, видимо, игра где-то это использует, но нам это ни к чему, так что новое имя в лист мы добавлять не будем.
Жмем Run еще раз, видим код:
Загружается значение из 2pTap ($8E + номер слота в регистре X), с ним делается побитовое умножение (AND) с числом 0x40, то есть отсекаются все биты 2pTap, которые не содержатся в числе 0x40. В бинарном (двоичном) виде оно будет 0100 0000. Этот бит соответствует нажатию B на геймпаде. Полученное число, которое равно либо 0, либо 0x40, побитово складывается (OR) с $064C ($064B + номер слота) и пишется в него же. Видим, что в этом адресе уже хранится двойка, отвечающая за нажатие Влево, неплохо бы выяснить, как она туда попадает. Для этого смотрим выше по коду:
Берется значение адреса удерживания кнопки, с ним делается AND со значением из адреса $8, который равен двум. Держу пари, он отвечает за то, в какую сторону персонаж смотрит. В окне дизасма кликнем правой кнопкой мыши по $0008 и впишем имя temp Facing.
То есть двойка в $064B взялась как раз от сложения битов адреса удерживания с адресом направления. Напомню:
- 1 (в двоичном виде 0000 0001) это нажатие Вправо
- 2 (в двоичном виде 0000 0010) это нажатие Влево
- 3 (в двоичном виде 0000 0011) это нажатие Влево+Вправо
Персонаж пустит шар только в случае, когда он нажимает в ту же сторону, в которую смотрит.
Для верности глянем, кто пишет двойку в $8. Снимаем текущий бряк, щелкаем хоткей Frame Advance, ставим новый: на запись в адрес $8. Мотаем на кадр нажатия Вперед+Б, жмем Run. Игра встает, и мы видим:
01:99EF:A0 01 LDY #$01
01:99F1:BD 10 05 LDA $0510,X @ $0511 = #$40
01:99F4:0A ASL
01:99F5:10 01 BPL $99F8
01:99F7:C8 INY
>01:99F8:84 08 STY temp Facing = #$87
LDY #$01 – игра загружает в Y единицу (дефолтный фейсинг).
LDA $0510,X – в аккумулятор считывается из адреса $0511 число 0x40.
ASL – арифметический сдвиг влево, то есть значение умножается на 2.
BPL $99F8 – если полученное в аккумуляторе число положительное, прыгнуть на адрес $99F8, иначе продолжить исполнение.
INY – исполнение продолжилось, увеличить Y на 1.
STY temp Facing – выставить полученное число из Y в адрес temp Facing ($8).
Смысл: когда значение $0510, умноженное на 2, имеет 7-й (левый) бит, ответственный за знак (больше или равно 0x80 - значит, отрицательное), игра вписывает двойку в фэйсинг, иначе вписывает единицу. Кликаем правой кнопкой по $0510 и называем его Facing.
Умозаключение “персонаж пустит шар только в случае, когда он нажимает в ту же сторону, в которую смотрит” полностью подтвердилось. Правый клик по $9D7C, задаем имя Check Facing. Правда, как функционирует $064B все ещё не ясно. Ставим бряк на запись в $064С ($064B + оффсет для Дона), перед этим выключив текущий и нажав Frame Advance для выхода из межкадровых событий. Можно мотнуть назад на несколько кадров, еще до нажатия Вниз. Нажав там Run, увидим знакомый код Check Facing, в котором $064С равно нулю, поскольку нажатий нет (AND двойки и нуля дает нуль). В следующих пустых кадрах этот код повторяется, но вот в кадр нажатия Вниз мы видим:
01:9DBD:A9 00 LDA #$00
>01:9DBF:9D 4B 06 STA $064B,X @ $064C = #$00
>01:9DBF:9D 4B 06 STA $064B,X @ $064C = #$00
Нуль эксклюзивно загружается в аккумулятор и пишется в наш адрес. Это называется инициализация, цель ее – выкинуть весь мусор, который может содержаться в $064C, и исключить ошибочные вычисления. Назовем адрес $064B BallCombo. Ведь при прочих приемах значение этого адреса равно нулю, а после нажатия комбинации шарика становится 0x42. Причем, если нажать еще и Вправо (т. е. назад, ведь у меня Дон справа), значение адреса будет опять 0x42, и шар вылетит, а если убрать Влево (вперед), значение будет 0x40, и шар не полетит. Check Facing тогда можно переименовать в Check BallCombo, а $9DBD – в Init BallCombo.
Теперь с чистой совестью можно продолжить мониторить чтение нажатий. Отключаем все бряки, включаем тот, что был на чтение с $8F. Запускаем эмуляцию с кадра нажатия Вперед+Б. Первый код мы уже видели, это адрес 07:F25A, который можно переименовать в SumBothPlayerInput и забыть про него. Потом будет функция Check BallCombo, она уже тоже вся изучена. Снова жмем Run.
01:BA96:BC 26 06 LDY $0626,X @ $0627 = #$00
01:BA99:D0 23 BNE $BABE
>01:BA9B:B5 8E LDA $8E,X @ 2pTap = #$42
01:BA9D:29 03 AND #$03
01:BA9F:F0 0D BEQ $BAAE
01:BAA1:9D 24 06 STA $0624,X @ $0625 = #$00
01:BAA4:A9 01 LDA #$01
01:BAA6:9D 26 06 STA $0626,X @ $0627 = #$00
01:BAA9:A9 1E LDA #$1E
01:BAAB:9D 28 06 STA $0628,X @ $0629 = #$00
01:BAAE:60 RTS ----------------------
01:BA99:D0 23 BNE $BABE
>01:BA9B:B5 8E LDA $8E,X @ 2pTap = #$42
01:BA9D:29 03 AND #$03
01:BA9F:F0 0D BEQ $BAAE
01:BAA1:9D 24 06 STA $0624,X @ $0625 = #$00
01:BAA4:A9 01 LDA #$01
01:BAA6:9D 26 06 STA $0626,X @ $0627 = #$00
01:BAA9:A9 1E LDA #$1E
01:BAAB:9D 28 06 STA $0628,X @ $0629 = #$00
01:BAAE:60 RTS ----------------------
В начале этой процедуры $0626 + слот сравнивается с нулем, и идет прыжок на $BABE, если нулю не равно. Код встал на точке, в которой снова проверяется нажатое направление:
AND #$03 отсекает от 2pTap все, что не содержится в тройке.
BEQ $BAAE сравнивает результат с нулем и прыгает на адрес $BAAE, если результат равен нулю.
$BAAE же содержит команду RTS, что значит выход из сабрутины (процедуры).
Если же результат не равен нулю, он пишется в $0624 со смещением по номеру слота.
Потом в $0626 + слот пишется единица, а в $0628 + слот пишется 0x1E, и таки выходит из процедуры. Назовем $BA96 CheckDirectionTap, а $BAA1 – FireDirectionTap.
Если поставить бряк на выполнение (Execute) по адресу $BAA1 и добавить условие (Condition) X==#1 (следить только за слотом второго игрока), можно будет протестировать работу этой функции. При каждом новом нажатии направления срабатывает бряк. Причем если мы жмем направление 2 раза подряд (активация бега), второй раз он не сработает. А если три, то сработает на первое и третье нажатие. Также он срабатывает, если к нажатию направления добавлять удар. Зато если нажать два раза подряд комбо мяча, бряк не сработает, то есть, видимо, невозможно все-таки пустить мяч, пуская мяч (см. цитату из предыдущего номера). [На самом деле можно отменять анимацию броска мяча броском мяча, пока он еще не вылетел – прим. Сергей Сполан]
Если сменить условие в бряке на X==#0 и потестировать кидание мяча за Лео, увидим, что функция работает, даже когда Лео перед комбо мяча делает нечто запрещающее мяч (например, прыгает или делает спешл). Значит, баг должен быть в промежутке между этой функцией и записью спрайта. Тут нам понадобится трейс-лог исполняемого кода для шарика Лео и для шарика Дона, причем для обоих из них нажмем один раз Вверх за 10 кадров перед нажатием Вниз.
TRACE LOG
Сначала кинем шар за Лео (все бряки можно отключить), потом добавим ему кнопку “U” в десятый кадр перед комбо. Шарик отменился, Лео просто ударил рукой в прыжке. Перейдем на кадр нажатия Вперед+B и откроем трейслоггер:
Debug -> Trace Logger. Ставим галки:
- Log state of registers
- Symbolic trace
- Log Processor status flags
- Use Stack Pointer for code tabbing
Ставим радиокнопку Log to File, задаем файл с именем “Leo” кнопкой Browse. Start Logging. Жмем хоткей Frame Advance один раз. Stop Logging.
Прописываем Дону бросание шарика из стоячего положения и также добавляем “U” за 10 кадров до. Шарик срабатывает. Таким же образом делаем лог в файл “Don” за 1 кадр. Можно посравнивать логи. Для чтения логов я использую Notepad++ с простеньким подсветчиком синтаксиса 6502 (но можно и просто Assembly), перенос строк выключен.
Открываем логи и первым делом ищем исполнение кода по уже известному адресу в каждом из файлов: Ctrl+F и пишем FireDirectionTap. Сразу нашлось:
$BAA1:9D 24 06 STA $0624,X @ $0624 = #$00
Теперь сравним код для Дона и Лео. Один из файлов надо открыть в другом окне Notepad++: правый клик по имени вкладки – Переместить в Другое Окно. Выставляем строчки с нашей командой в этих двух файлах на одном уровне. Потом меню Вид -> Синхрониз. вертикальную прокрутку, Синхрониз. горизонтальную прокрутку. Растягиваем Notepad++ на весь экран.
Вплоть до этого момента (FireDirectionTap) код Konami можно считать правильным. Скроллим вниз и ищем отличия. Логи начинают отличаться после строчки
$D04E:6C 02 00 JMP ($0002)
У Лео адрес $0002 равен $84FC, а у Дона $863B. Скобки означают, что считывается не число напрямую, а значение в указанном адресе. А так как это прыжок по коду, и адреса у нас двухбайтовые, то считывается не 1 байт, а оба. То есть ячейка $0002 равна 0xFC, а ячейка $0003 равна 0x84 (гуглим big endian).
Итак, игра делает прыжок (безусловный переход) по определенному адресу, содержащемуся в $2. Надо выяснить, почему он там разный у разных персонажей. Если поставить бряк на исполнение по $D04E для Лео и выполнить нормальный шарик, увидим, что у шарика Лео функция та же, то есть ($0002) = $863B, как и у Дона. Значит весь косяк в том, что пишется в $0002. Желающие могут дать в дебаггере адресу $863B имя ThrowBall.
Код Лео:
$D041:B1 00 LDA ($00),Y @ $8193 = #$FC A:81 X:00 Y:03
$D043:85 02 STA $0002 = #$82 A:FC X:00 Y:03
$D045:C8 INY A:FC X:00 Y:03
$D046:B1 00 LDA ($00),Y @ $8194 = #$84 A:FC X:00 Y:04
$D048:A4 03 LDY $0003 = #$00 A:84 X:00 Y:04
$D04A:85 03 STA $0003 = #$00 A:84 X:00 Y:00
$D04C:A5 04 LDA $0004 = #$00 A:84 X:00 Y:00
$D04E:6C 02 00 JMP ($0002) = $84FC A:00 X:00 Y:00
$D043:85 02 STA $0002 = #$82 A:FC X:00 Y:03
$D045:C8 INY A:FC X:00 Y:03
$D046:B1 00 LDA ($00),Y @ $8194 = #$84 A:FC X:00 Y:04
$D048:A4 03 LDY $0003 = #$00 A:84 X:00 Y:04
$D04A:85 03 STA $0003 = #$00 A:84 X:00 Y:00
$D04C:A5 04 LDA $0004 = #$00 A:84 X:00 Y:00
$D04E:6C 02 00 JMP ($0002) = $84FC A:00 X:00 Y:00
Код Дона:
$D041:B1 00 LDA ($00),Y @ $819B = #$3B A:81 X:01 Y:0B
$D043:85 02 STA $0002 = #$82 A:3B X:01 Y:0B
$D045:C8 INY A:3B X:01 Y:0B
$D046:B1 00 LDA ($00),Y @ $819C = #$86 A:3B X:01 Y:0C
$D048:A4 03 LDY $0003 = #$00 A:86 X:01 Y:0C
$D04A:85 03 STA $0003 = #$00 A:86 X:01 Y:00
$D04C:A5 04 LDA $0004 = #$00 A:86 X:01 Y:00
$D04E:6C 02 00 JMP ($0002) = $863B A:00 X:01 Y:00
LDA ($00),Y – загрузить в аккумулятор значение из адреса $00, прибавить к нему содержимое регистра Y (3 для Лео, 0xB для Дона), потом взять полученное число как адрес и прочитать с него в аккумулятор. Так как $00 = $8190, для персонажей будут в итоге читаться $8193, равный #$FC, и $819B, равный #$3B, соответственно.
STA $0002 – записать прочитанное число в адрес $0002. Это число будет использовано как младший байт для адреса прыжка. Дон прыгнет в код шарика, Лео - в код удара рукой.
INY – увеличить Y на 1.
LDA ($00),Y – прочитать байт, следующий за только что прочитанным из памяти. Более совершенные консоли читают такие двухбайтовые адреса целиком в 1 присест, здесь же приходится так вот изгаляться.
LDY $0003 – сохранить в регистре Y прежнее значение $0003.
STA $0003 – перезаписать $0003 значением из аккумулятора, то есть старшим байтом адреса прыжка.
LDA $0004 – загрузить в аккумулятор значение адреса $0004.
JMP ($0002) – наконец прыгнуть по тому, что у нас получилось в $0002 и $0003.
Так как вся разница начинается со значения в регистре Y, узнаем, как оно формируется, глядя выше по логу:
$818B:BD 20 05 LDA $0520,X
$818E:20 32 D0 JSR $D032
$D032:84 04 STY $0004
$D034:85 05 STA $0005
$D036:0A ASL
$D037:84 03 STY $0003
$D039:A8 TAY
$D03A:C8 INY
$818E:20 32 D0 JSR $D032
$D032:84 04 STY $0004
$D034:85 05 STA $0005
$D036:0A ASL
$D037:84 03 STY $0003
$D039:A8 TAY
$D03A:C8 INY
У Лео в $0520 единица, у Дона 5. Это число загружается в аккумулятор, потом пишется в ячейку $0005. Потом арифметический сдвиг влево, то есть умножение на 2. Потом результат пишется в регистр Y и увеличивается на 1. Новый вопрос: что за $0520? Мониторим в Хекс Эдиторе, играя за перса. Выглядит как байт состояния:
0 – на земле
1 – в воздухе
3 – получает удар
4 – нокдаун
5 – кидает шар
7 – сидит
8 – делает спешл
9 – перекидывает
10 – перекидывается
Хм, кажется, придется еще покопаться. Например, найти причину записи 5 в состояние Дона. Выделяем этот адрес, подсвечивая зеленым, и скроллим вверх файл Дона (при построчном несовпадении можно отключить вертикальную синхронизацию), видим запись пятерки:
$9D9B:A9 05 LDA #$05
$9D9D:9D 20 05 STA $0520,X @ $0521 = #$01
$9D9D:9D 20 05 STA $0520,X @ $0521 = #$01
Но в логе Лео этого нет! Значит, надо опять искать отличия в предыдущих функциях. В функции Set temp Direction видим у Лео:
$9D36:BD 50 05 LDA $0550,X @ $0550 = #$00
$9D39:C9 03 CMP #$03
$9D3B:F0 0D BEQ $9D4A
$9D3D:BD 20 05 LDA $0520,X @ $0520 = #$01
$9D40:F0 08 BEQ $9D4A
$9D42:C9 07 CMP #$07
$9D44:F0 04 BEQ $9D4A
$9D46:C9 06 CMP #$06
$9D48:D0 EB BNE $9D35
$9D35:60 RTS (from $9D1F) ---------
$9D39:C9 03 CMP #$03
$9D3B:F0 0D BEQ $9D4A
$9D3D:BD 20 05 LDA $0520,X @ $0520 = #$01
$9D40:F0 08 BEQ $9D4A
$9D42:C9 07 CMP #$07
$9D44:F0 04 BEQ $9D4A
$9D46:C9 06 CMP #$06
$9D48:D0 EB BNE $9D35
$9D35:60 RTS (from $9D1F) ---------
У Дона:
$9D36:BD 50 05 LDA $0550,X @ $0551 = #$03
$9D39:C9 03 CMP #$03
$9D3B:F0 0D BEQ $9D4A
$9D4A:BD 50 05 LDA $0550,X @ $0551 = #$03
$9D4D:C9 02 CMP #$02
$9D4F:D0 25 BNE $9D76
$9D76:A5 08 LDA temp Facing = #$02
$9D78:09 40 ORA #$40
$9D7A:85 00 STA $0000 = #$8B
Check BallCombo
В зависимости от значения $0550, код либо прыгает к $9D4A, либо нет. Понаблюдаем за $0550 у обоих персов. Выставляется в начале раунда и не меняется? Да это же персонаж! Называем адрес Character.
0 - Лео
1 - Раф
2 - Майк
3 - Дон
4 - Кейси
5 - Хотхэд
6 - Шреддер
Тут начинаются странности. Номер перса сравнивается с 3, и если равно, игра прыгает на $9D4A, а там... этот же адрес сравнивается с 2!!! И прыгает на $9D76, если не равно. Ясен пень, не равно, ты только что выяснил, что оно равно трем! Код целиком:
01:9D36:BD 50 05 LDA Character,X @ $0551 = #$03
01:9D39:C9 03 CMP #$03
01:9D3B:F0 0D BEQ $9D4A
01:9D3D:BD 20 05 LDA State,X @ $0521 = #$01
01:9D40:F0 08 BEQ $9D4A
01:9D42:C9 07 CMP #$07
01:9D44:F0 04 BEQ $9D4A
01:9D46:C9 06 CMP #$06
01:9D48:D0 EB BNE $9D35
01:9D4A:BD 50 05 LDA Character,X @ $0551 = #$03
01:9D4D:C9 02 CMP #$02
01:9D4F:D0 25 BNE $9D76
01:9D39:C9 03 CMP #$03
01:9D3B:F0 0D BEQ $9D4A
01:9D3D:BD 20 05 LDA State,X @ $0521 = #$01
01:9D40:F0 08 BEQ $9D4A
01:9D42:C9 07 CMP #$07
01:9D44:F0 04 BEQ $9D4A
01:9D46:C9 06 CMP #$06
01:9D48:D0 EB BNE $9D35
01:9D4A:BD 50 05 LDA Character,X @ $0551 = #$03
01:9D4D:C9 02 CMP #$02
01:9D4F:D0 25 BNE $9D76
Если перс равен 3, пропустить, для всех прочих проверить состояние. Если состояние 0 (стоит) или 7 (сидит), прыгнуть на $9D4A (не равен ли перс Майку); если состояние 6, то на $9D35 (RTS). То есть, для Дона пропускается проверка состояний и идет код шара. Но есть еще Майк (2), который, сидя или стоя, тоже прыгнет сразу на $9D76, а остальные персонажи выполнят дополнительный код.
На самом деле, на этом можно остановиться. У Кейси бессмертие все равно зависит не от шарика (он его кидает как все), а прокол в коде Дона мы нашли: разработчики решили добавить ему клевую фичу шарика в любом состоянии, но, видимо, не протестировали шарик при спешле и перекидывании.
Кейси становится бессмертным, если выполнит вихрь во время попадания в нокдаун. Если есть желание, сами отследите причины в качестве домашнего задания. А за сегодня мы научились:
- Использовать TAS Editor
- Находить адреса
- Понимать код
- Ставить брейкпоинты
- Писать трейс-логи
- Давать адресам имена
- Тратить время и, наконец...
- Долавливать баги за разработчиками
Mission accomplished, тратьте время с пользой!
Журнал: DF Mag
Автор статьи: Feos