Содержание
Двоичный поиск такой же быстрый и эффективный, как стрельба по мухе из базуки. Он позволит тебе найти не только стандартные игровые адреса, которые можно локализовать и через окно RAM Search, но еще и адреса, которые отвечают на вопрос "почему происходит именно так, а не вот так".
Я вообще не пользуюсь RAM Search. Я изучаю отличия двух сохранений и комбинирую с двоичным поиском.
Не имеет значения в каком формате хранятся эти данные в адресе. Если за игровую функцию отвечает некий адрес RAM, его всегда можно вычислить двоичным поиском.
Еще одним плюсом является то, что во время поиска ты вполне можешь заприметить и другие адреса в определенных локациях RAM, которые сможешь изучить позднее, когда разберешься с текущим искомым адресом.
Для просмотра подробностей выбери соответствующую опцию.
Описание поиска | Увеличение шансов | Почему поиск не работает | Стек | Спрайты | Рандом |
Поиск по своему принципу является методом исключения. Демонстрацию двоичного поиска смотри на этом видео.
Допустим, нужно найти координату объекта. Подготавливаешь 2 сохранения, в которых отличается этот игровой параметр, то есть объект должен находиться в разных местах на двух сохранениях.
Далее желательно поставить эмулятор на паузу, затем загружаешь первое сохранение, копируешь из него полностью все байты RAM диапазона $0000-$07FF и вставляешь по тому же месту во второе сохранение. По принципу копирование/вставка байтов похожа на механику загрузки сохранения, то есть ты заменяешь состояние игры на другое.
Вставка байтов аналогична тому, как если бы ты вручную перезаписывал байт в адресе с координатами. Если ты правильно нашел адрес с кооординатами любыми другими способами, то изменив вручную байт в этом адресе, объект будет телепортирован в другую точку. То же самое происходит и при копировании данных из одного сохранения в другое.
Очевидно, что координаты находятся в RAM, следовательно вставка байтов приведет к изменению координат объекта. Но если эмулятор стоит на паузе, сразу ты этого не увидишь. Нужно снять эмулятор с паузы/промотать игру на пару кадров вперед, после чего объект телепортируется в ту точку, где он находился на первом сохранении.
Когда ты убедился, что этот способ работает для поиска координат, проверяешь любую из двух половин RAM, которая тебе кажется наиболее вероятной. Половинами будут диапазоны $0000-$03FF и $0400-$07FF. Копируешь одну из них из первого сохранения и вставляешь во второе по тому же месту.
Если же копирование диапазона $0000-$07FF вызвало ошибки, первым делом следует исключить из копирования адреса стека $0100-$01FF, про которые рассказано в подразделе этой статьи.
Если половина была выбрана правильно, объект будет телепортирован. А если неправильно, объект останется на том же месте, как он был на втором сохранении. Отсекаешь лишнюю половину, и продолжаешь поиск во второй половине, сразу же поделив ее на две части и проверив одну из них.
Всего в RAM 2048 адресов в диапазоне $0000-$07FF. Это означает, что практически всегда можно найти адрес с 11-й попытки, каждый раз уменьшая количество адресов вдвое, отсекая лишнюю половину.
Состояния игры на твоих сохранениях должны быть максимально похожими между собой, и в тоже время отличаться по искомому игровому параметру. Если ты ищешь адрес с жизнями, очень хорошим первым сохранением будет сразу сохраниться после начала игры, не двигаясь своим персонажем.
Затем убиваешься об врага, возрождаешься в той же точке на экране и делаешь сохранение на другой слот. У тебя в наличии окажутся 2 практически аналогичных сохранения, где персонаж стоит в том же месте, но его количество жизней отличается.
Это значительно снизит любые риски и ускорит процесс двоичного поиска, так как даже необязательно будет повторять двоичный поиск с самого начала, можно просто сравнить 2 сохранения на паузе, найти отличия и проверить их.
Перед копированием и вставкой байтов в некоторых случаях нужно ставить эмулятор на паузу, ведь в то время, пока ты будешь копировать байты, что-то в RAM может успеть поменяться настолько, что исключит искомую игровую ситуацию.
А при вставке без паузы возможно тебе будет проблематично оценить результат двоичного поиска. Для просмотра результата в замедленном режиме пользуйся горячей клавишей Frame Advance.
В редких случаях потребуется сохраниться на протяжении кадра в особой временной точке, когда игра еще не успела проверить искомый адрес. Зная код игры, нужно поставить Execute брейкпоинт в том месте, где он будет срабатывать ежекадрово лишь 1 раз за кадр, а после его срабатывания сохраниться.
Такое же сохранение нужно сделать и для второго слота, чтобы та точка во времени, где сохранено игровое состояние, была приблизительно одинаковая, уменьшая шансы на провал при копировании байтов.
После чего ты даешь срабатывать этому брейкпоинту, сохраняешься и тестируешь двоичый поиск. Если не получилось, снова даешь брейкпоинту сработать, постепенно приближаясь к игровой ситуации, и повторяешь попытку поиска.
Предположим, ты скопировал диапазон $0000-$07FF из первого сохранения во второе, но интересующее тебя игровое событие продолжает повторяться на втором сохранении.
Сохранение хранит в себе множество других технических параметров, относящихся к текущему игровому состоянию. Это не только байты в адресах RAM, но еще и байты в регистрах CPU, PPU и APU, байты в адресах PPU Memory, подключенные PRG банки и различные внутренние тайминги процессора.
Здесь опять же действует метод двоичного поиска. Если причина кроется не в RAM адресах, но на сохранениях искомое действие отличается, значит надо исключить RAM из уравнения и копать глубже.
К счастью, такое встречается крайне редко, поскольку без RAM и вмешательства со стороны пользователя нажатием кнопок на джойстике, код игры бы всегда выполнялся одинаково. Главное - подобрать правильные сохранения.
Также к RAM могут относиться адреса батарейки в диапазоне $6000-$7FFF, в которых RPG-игры могут сохранять данные прохождения. Если батарейка используется в игре, ее тоже стоит проверить, когда поиск в основной RAM не принес результатов.
Несмотря на то, что SRAM по размеру в 4 раза больше RAM, поиск займет не в 4 раза дольше по времени, а просто на 2 попытки больше.
В редких случаях бывает такое, что поделив некий диапазон на 2 части, копирование ни одной из этих частей не приводит к нужным изменениям, но в то же время копирование целого диапазона работает. Это означает, что в искомом диапазоне находятся сразу 2 адреса, отвечающих за определенное действие.
Следует потихоньку исключать по несколько строк с адресами, копируя оставшиеся в промежуточное третье сохранение на паузе эмулятора. После завершения копирования снимать паузу и проверять результат.
По моему опыту, эти 2 адреса будут находиться в пределах нескольких строк друг от друга. 3 и более адресов одновременно я не встречал, но такое теоретически возможно.
Адреса стека $0100-$01FF задуманы для хранения байтов, отвечающих за адреса возврата из подпрограмм. При ручном изменении этих байтов на сохранении, например при копировании, цепочка возврата из подпрограмм может нарушиться, что приведет к зависанию.
Но в то же время не все адреса стека используются для возврата из подпрограмм. Обычно адреса для подпрограмм находятся в конце стека, около 1/4 части общего диапазона.
Исключая адреса, которые выделены для подпрограмм, все остальные оставшиеся адреса изначально считаются свободными, чем активно пользуются разработчики игр, поэтому в них также могут находиться динамические данные. Эти данные ты найдешь двоичным поиском или другими способами.
Причина, по которой копирование байтов, включающих адреса стека, между двумя сохранениями не приводит к зависанию, кроется в моменте сохранения. Эмулятор сохраняет состояние игры на 240-й сканлинии (почти в конце кадра), если эмулирует игру в обычном режиме или стоит на паузе при использовании горячей клавиши.
Исключением является сохранение на другой сканлинии при использовании Debugger'а. Номер текущей сканлинии можно посмотреть в Debugger'е на паузе эмулятора.
В этой временной точке кадра чаще всего выполняется бесконечный цикл в ожидании прерывания NMI для последующей отрисовки графики. И если в это время скопировать RAM целиком во второе сохранение, игра просто продолжит выполнять код таким же образом, как она это делает на первом сохранении.
Однако если скопировать RAM лишь частично, включив в копию байты адресов стека, то те данные в других адресах RAM, которые код игры ожидает получить с текущей цепочкой возврата из подпрограмм, будут уже другие, что теоретически может привести к ошибкам.
Несмотря на то, что копирование всей RAM не должно приводить к ошибкам, такое все же случается. Если игра зависает или перезагружается, в первую очередь исключаешь стек из копирования и пробуешь еще раз.
В игре Tecmo Bowl в режиме Season Game, после матча игра записывает статистику этого матча в таблицу. Также есть режим Preseason, в котором статистика во время матча хоть и учитывается в RAM, но по итогу в таблицу не заносится.
В отличие от примера из Battle City, который можно посмотреть на видео, где за продолжение игры/Game Over после подсчета очков отвечает адрес, байт из которого проверяется игрой, в Tecmo Bowl такого простого адреса не существует.
Если воспользоваться двоичным поиском, можно найти адрес стека, в котором находится младший байт адреса возврата из подпрограммы. В зависимости от выбранного режима игры, после матча код возвращается в разные места, что приводит к тому, что дальнейший код либо запишет статистику, либо пропустит эту запись.
Память спрайтов, находящаяся в RAM чаще всего по адресам $0200-$02FF, служит для последующего копирования параметров спрайтов в OAM Memory для отображения их на экране.
В подавляющем большинстве случаев положение одного и того же спрайта в этом диапазоне постоянно меняется, чтобы изменить приоритет спрайта для отображения других (из-за ограничения NES, касаемого 8-ми спрайтов на линии).
Практически невозможно отследить по каким адресам будет находиться один и тот же спрайт в любой игровой момент, поэтому координаты объектов и, возможно, другие параметры спрайта хранятся в конкретных адресах, местоположение которых никогда не изменяется.
Исходя из этого, в 99% случаев можно полностью игнорировать этот диапазон.
В игре Urban Champion время от времени в окне здания появляется мужик, который бросает вниз горшок с цветком. Изображение состоит из двух спрайтов - горшок и цветок.
Координаты X и Y горшка как объекта хранятся не в отдельных адресах, а непосредственно в той области RAM, которая зарезервирована для спрайтов. Абсолютно любой спрайт требует адреса с Y и X координатами, и как раз эти адреса игра использует для хранения координат объекта. В этой игре они всегда расположены по одному и тому же адресу.
Другие игры в некоторых случаях тоже могут явно указывать где будут находиться спрайты, но только какие-то определенные, например цифры таймера или количество HP, отображаемые на экране. Обычно для них резервируются адреса в начале или в конце диапазона для спрайтов. Эти особые спрайты будут всегда отображаться во время игры, даже когда другим не хватает памяти.
Изменяя координаты горшка, будет изменяться координата самого объекта. Это не относится к спрайту цветка, так как игра не рассчитывает на такое ручное вмешательство в RAM. Даже если Y координата спрайта горшка заморожена, спрайт цветка продолжит опускаться.
Это, пожалуй, единственные адреса, которые плохо поддаются двоичному поиску. Байты в адресах с рандомом вычисляются исходя из того, какие байты находятся в других адресах RAM.
Например, в игре Castlevania III - Dracula's Curse, даже создав 2 практически идентичных сохранения, в одном из которых после убийства врага выпадает предмет, поиск конкретных адресов может занять слишком много времени. Усложняет ситуацию то, что в этой игре рандом вычисляется в бесконечном цикле в конце кадра.
Если тебе заранее известно, что за некое действие отвечает рандом, то двоичный поиск для этого не требуется. Достаточно знать расположение адресов с рандомом, а затем поставить на них Read брейкпоинт и изучить код.
Если для схожего случая тебе все же по каким-то причинам нужно найти адреса именно двоичным поиском, понадобятся очень точные сохранения, в которых вычисление шанса выпадения предмета происходит на кадре сохранения, чего можно добиться только с Debugger'ом.