Извлекаем уровни из Super Mario Bros с помощью Python |
||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2018-07-06 12:57 Введение Для нового проекта мне понадобилось извлечь данные уровней из классической видеоигры 1985 года Super Mario Bros (SMB). Если конкретнее, то я хотел извлечь фоновую графику каждого уровня игры без интерфейса, подвижных спрайтов и т.п. Разумеется, я просто мог склеить изображения из игры и, возможно, автоматизировать процесс с помощью техник машинного зрения. Но мне показался более интересным описанный ниже метод, позволяющий исследовать те элементы уровней, которые нельзя получить с помощью скриншотов. Анализ исходного кода Реверс-инжиниринг любой программы намного проще, если есть её исходный код, а у нас имеются исходники SMB в виде 17 тысяч строк ассемблерного кода 6502 (процессора NES), опубликованных doppelganger. Поскольку Nintendo так и не выпустила официального релиза исходников, код был создан дизассемблированием машинного кода SMB, с мучительной расшифровкой значения каждой части, добавлением комментариев и осмысленных символьных названий. Выполнив быстрый поиск по файлу, я нашёл нечто, похожее на нужные нам данные уровней: AreaParserCore Процедура выполняет запись в MetatileBuffer : раздел памяти длиной 13 байтов, представляющий собой один столбец блоков в уровне, каждый байт которого обозначает отдельный блок. Метатайл (metatile) — это блок 16x16, из которого составляются фоны игры SMB:Уровень с прямоугольниками, описанными вокруг метатайлов Они называются метатайлами, потому что каждый состоит из четырёх тайлов размером 8x8 пикселей, но подробнее об этом ниже. То, что декодер работает с заранее заданными объектами, объясняет маленький размер уровня: данные уровня должны ссылаться только на типы объектов и их расположение, например «расположить трубу в точке (20, 16), ряд блоков в точке (10, 5), …». Однако это означает, что для превращения сырых данных уровня в метатайлы требуется много кода. Портирование такого объёма кода для создания собственного распаковщика уровней заняло бы слишком много времени, поэтому давайте попробуем другой подход. py65emu Если бы у нас был интерфейс между Python и языком ассемблера 6502, мы могли бы вызывать подпроцедуру
После этого мы можем выполнять отдельные инструкции с помощью метода cpu.step() , исследовать память с помощью mmu.read() , изучать регистры машины с помощью cpu.r.a , cpu.r.pc и т.д. Кроме того, мы можем выполнять запись в память с помощью mmu.write() .Стоит заметить, что это всего лишь эмулятор процессора NES: он не эмулирует другие аппаратные части, такие как PPU (Picture Processing Unit), поэтому его нельзя использовать для эмуляции всей игры. Однако его должно быть достаточно для вызова подпроцедуры парсинга, потому что она не использует никаких других аппаратных устройств, кроме ЦП и памяти. План заключается в том, чтобы настроить ЦП так, как показано выше, а затем для каждого столбца уровня инициализировать разделы памяти с входными значениями, требуемыми для AreaParserCore , вызвать AreaParserCore , а затем считать обратно данные столбца. После завершения этих операций мы используем Python для сборки результата в готовое изображение.Но перед этим нам нужно скомпилировать листинг на ассемблере в машинный код. x816 Как указано в исходном коде, ассемблер компилируется с помощью x816. x816 — это ассемблер 6502 под MS-DOS, используемый сообществом разработчиков самодельных игр (homebrew) для NES и ROM-хакеров. Он замечательно работает в DOSBox. Наряду с ROM программы, который необходим для py65emu, ассемблер x816 создаёт символьный файл, привязывающий символы к расположению их в памяти в адресном пространстве ЦП. Вот фрагмент файла:
Подпроцедуры Как сказано в представленном выше плане, мы хотим научиться вызывать подпроцедуру
Инструкция jsr (jump to subroutine, «перейти к подпроцедуре») добавляет регистр PC в стек и присваивает ему значение адреса, на который ссылается WritePPUReg1 . Регистр PC сообщает процессору адрес следующей загружаемой инструкции, чтобы следующая инструкция, выполняемая после инструкции jsr , была первой строкой WritePPUReg1 .В конце подпроцедуры выполняется инструкция rts (return from subroutine, «возврат из подпроцедуры»). Эта команда удаляет сохранённое значение из стека и сохраняет его в регистре PC, что заставляет ЦП выполнить инструкцию, следующую за вызовом jsr .Замечательное свойство подпроцедур заключается в том, что можно создавать встроенные вызовы, то есть вызовы подпроцедур внутри подпроцедур. Адреса возврата будут добавляться в стек и извлекаться в правильном порядке, таким же образом, как это происходит с вызовами функций в языках высокого уровня. Вот код для выполнения подпроцедуры из Python:
Код сохраняет текущее значение регистра указателя стека ( s ), эмулирует вызов jsr , а затем выполняет инструкции, пока стек не вернётся к исходной высоте, что происходит только после возврата первой подпроцедуры. Это будет полезно, потому что теперь у нас есть способ прямого вызова подпроцедур 6502 из Python.Однако мы кое о чём забыли: как передавать входные значения для этой подпроцедуры? Нам нужно сообщить процедуре, какой уровень мы хотим отрендерить и какой столбец нам нужно парсить. В отличие от функций в языках высокого уровня, подпроцедуры языка ассемблера 6502 не могут получать явно заданных входных данных. Вместо этого входные данные передаются заданием мест в памяти, находящихся где-то перед вызовом, которые затем считываются внутри вызова подпроцедуры. Если учесть размер AreaParserCore , то обратная разработка необходимых входных данных простым изучением исходного кода будет очень сложной и подверженной ошибкам.Valgrind для NES? Чтобы найти способ для определения входных значений
Здесь мы обернули блок управления памятью (MMU) py65emu. Этот класс содержит массив _uninitialized , элементы которого сообщают нам, выполнялась ли когда-нибудь запись в соответствующий байт эмулируемой ОЗУ. В случае возникновения неинициализированного считывания выводится адрес неверной операции считывания и имя соответствующего символа.Вот какие результаты даёт MMU в обёртке при вызове execute_subroutine(sym_file['AREAPARSERCORE']) :Uninitialized read! 0x0728 (BACKLOADINGFLAG): Поискав по коду, можно увидеть, что многие из этих значений задаются подпроцедурой InitializeArea , поэтому давайте снова запустим скрипт, вызывая сначала эту функцию. Повторяя этот процесс, мы придём к следующей последовательности вызовов, которая требует задания только номера мира и номера области:
Код записывает первые 48 столбцов уровня World 1-1 в metatile_data , используя подпроцедуру IncrementColumnPos для увеличения внутренних переменных, необходимых для отслеживания текущего столбца.А вот содержимое metatile_data , наложенное на скриншоты из игры (байты со значением 0 не показаны):Очевидно, что metatile_data явно соответствует информации фона.Графика метатайлов (Чтобы посмотреть конечный результат, можно сразу перейти к разделу «Соединяем всё вместе».) MetatileGraphics_Low: Сочетание этих двух массивов даёт нам 16-битный адрес, который в нашем примере указывает на Palette1_Mtiles :Palette1_MTiles: При умножении индекса метатайла на 4 он становится индексом этого массива. Данные форматированы в 4 записи на строку, поэтому наш пример метатайла ссылается на двадцатую строку, помеченную комментарием cracked rock terrain .Четыре записи этой строки на самом деле являются идентификаторами тайлов: каждый метатайл состоит из четырёх тайлов размером 8x8 пикселей, выстроенных в следующем порядке — верхний левый, нижний левый, верхний правый и нижний правый. Эти идентификаторы передаются непосредственно в PPU консоли NES. Идентификатор ссылается на 16 байтов данных в CHR-ROM консоли, а каждая запись начинается с адреса 0x1000 + 16 * <идентификатор тайла> :0x1000 + 16 * 0xb4: 0b01111111 0x1000 + 16 * 0xb5: 0b11011110 CHR-ROM — это фрагмент памяти read-only, к которому может получать доступ только PPU. Он отделён от PRG-ROM, в котором хранится код программы. Поэтому приведённые выше данные отсутствуют в исходном коде и их нужно получать из дампа ROM игры. 16 байта для каждого тайла составляют 2-битный тайл размером 8x8: первый бит — это первые 8 байтов, а второй — вторые 8 байтов: 21111111 13211112 Выполняем привязку этих данных к палитре 1: …и объединяем куски: Наконец мы получили отрендеренный тайл. Соединяем всё вместе Повторив эту процедуру для каждого метатайла, мы получим полностью отрендеренный уровень. И благодаря этому нам удалось извлечь графику уровней SMB с помощью Python!Источник: habr.com Комментарии: |
|