[Ненормальное программирование, Assembler, Игры и игровые приставки] Перепрограммирование GameBoy за счёт бага в Pokemon Yellow (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
18-Май-2021 12:31

Pokemon Yellow - это карманная вселенная со своими правилами. В ней можно покупать и продавать предметы, тренировать покемонов, побеждать других тренеров — но нельзя менять правила самой игры. Нельзя построить себе дом, поменять музыку или даже переодеться. Точнее, так было задумано. На самом деле есть последовательность валидных команд (типа перемещения из одного места в другое и манипуляций с предметами), которая позволяет превратить игру в Pacman, тетрис, Pong, MIDI-проигрыватель и что угодно ещё.Существует спидран Felipe Lopes de Freitas (p4wn3r), в котором Pokemon Yellow проходится за 1 минуту 36 секунд. Этот спидран основан на следующем хаке: в норме инвентарь игрока ограничен 20 предметами. Но есть баг, который позволяет игнорировать это ограничение и обращаться с памятью, расположенной сразу после инвентаря, как будто бы это был список предметов. Соответственно, стандартные манипуляции с предметами позволяют эту память переписывать. Спидраннер использует эту возможность, чтобы заставить дверь из стартовой комнаты переносить его на финальную локацию, в которой ему остаётся только выслушать поздравления.Когда я впервые увидел этот спидран и понял, что памятью Gameboy можно манипулировать с помощью одного только списка предметов, безо всяких внешних инструментов, я решил посмотреть, получится ли у меня усовершенствовать приёмы p4wn3r. Вот что получилось:Извините, данный ресурс не поддреживается. :( Gameboy — это восьмибитный компьютер. Соответственно, всё происходящее в игре — результат того, что исполняется некая последовательность однобайтовых команд. Например, последовательность[62 16 37 224 47 240 37 230 15 55] в местных машинных кодах означает, что надо выяснить, какие кнопки нажаты в данный момент, и записать результат в регистр A. Можно создать программу, которая будет читать ввод с кнопок и записывать куда-нибудь в память исполняемый код, а потом передавать ему управление. Если удастся каким-то образом засунуть такую программу (назовём её программой ввода) в память и заставить Gameboy её исполнить — то я выиграл, потому что теперь я могу исполнять произвольный код (скажем, тетрис) вместо Pokemon Yellow. Во-первых, как написать такую программу? Восьмибитными числами представлены не только правила, но и состояние игры (инвентарь, список покемонов, имя главного героя и т.п.) Инвентарь хранится вот так:
item-one-id         (0-255)
item-one-quantity   (0-255)
item-two-id         (0-255)
item-two-quantity   (0-255)
.
.
.
Упомянутая выше подпрограмма для чтения кнопок хранится в той же последовательности битов, что и следующий набор предметов:
lemonade     x16
guard spec.  x224
leaf stone   x240
guard spec.  x230
parlyz heal  x55
Так что если нам удастся собрать нужные предметы в нужном количестве и порядке — то получится код программы ввода. Правда, его с самого начала нужно писать так, чтобы он состоял только из валидных ID и количеств предметов. Это не так-то просто, потому что предметов в игре немного, а многие машинные инструкции состоят из 2-3 битов. Та версия программы, на которой я остановился, состоит из 92 бит; примерно половина из них не делает ничего полезного и вставлена только для того, чтоб код был ещё и корректным списком предметов.Записать код в память — это только полдела, потому что надо ещё заставить игру его исполнить. К счастью для нас, недалеко от инвентаря есть указатель на функцию, которую игра регулярно вызывает, чтобы обсчитывать эффект отравления и прочие штуки, срабатывающие по таймеру. Без него ничего бы не получилось, а с ним мы можем исполнить код по произвольному адресу, положив нужные предметы в соответствующую ячейку инвентаря.Поехали!Я начинаю с того, что называю своего противника Lp/k. Эти символы в своё время превратятся в предметы и будут перемещены на указатель функции, чтобы я мог исполнить программу ввода. Но сперва её надо записать, так что я начинаю забег так же, как и p4wn3r: перезапускаю игру при сохранении, чтобы сломать список покемонов. После этого я меняю местами 8 и 10 покемона, что ломает список предметов и позволяет мне манипулировать предметами после 20-го (то есть соответствующей областью памяти). С помощью этих манипуляций я выставляю скорость текста на максимум и переключаю свою дверь на магазин Celadon Dept. store. p4wn3r в этот момент переключил её на “Зал славы” и выиграл, но мне неинтересен спидран как таковой.Поэтому я не останавливаюсь, а выкладываю из своего поломанного инвентаря в сундук множество глитчевых предметов 0x00, которые мне ещё пригодятся. Потом я забираю из сундука зелье, что вызывает переполнение счётчика предметов с 0xFF на 0x00 и восстанавливает мой инвентарь. Зелье при этом, правда, исчезает. После этого я забираю 255 0x00-предметов и отправляюсь в магазин. Там я продаю 2 0x00 за 414925 монет каждый, что позволяет мне купить следующие предметы:
+-------------------+----------+
|##| Item           | Quantity |
+--+----------------+----------+
|1 | TM02           |  98      |
|2 | TM37           |  71      |
|3 | TM05           |   1      |
|4 | TM09           |   1      |
|5 | burn-heal      |  12      |
|6 | ice-heal       |  55      |
|7 | parlyz-heal    |  99      |
|8 | parlyz-heal    |  55      |
|9 | TM18           |   1      |
|10| fire-stone     |  23      |
|11| water-stone    |  29      |
|12| x-accuracy     |  58      |
|13| guard-spec     |  99      |
|14| guard-spec     |  24      |
|15| lemonade       |  16      |
|16| TM13           |   1      |
+--+----------------+----------+
Эти предметы я раскладываю в инвентаре в таком порядке, чтобы получилась моя первая программа ввода. Первая — потому что написать окончательную версию программы ввода внутри игры у меня не получилось. Та, которую я собираю из предметов, умеет читать только кнопки A, B, start и select. Каждый фрейм игры она пишет 4 бита в определённую область памяти; собрав 200 с лишним байт, она исполняет то, что написала. С помощью этой программы я пишу следующую, которая читает уже все 8 кнопок и пишет произвольное число байтов в произвольную область памяти, а потом исполняет код по произвольному адресу. Эта программа, в свою очередь, используется для загрузки третьей, которая делает всё то же самое, но ещё и выводит записываемый код на экран.Закончив с кодом первой программы, я отправляюсь в Celadon mansion (это не критично, мне просто нравится локация). Там я снова меняю местами сломанных покемонов и ломаю инвентарь. Проскроллив до имени своего противника, я выкидываю предметы до тех пор, пока имя не окажется на указателе функции. Как только я закрываю меню, программа ввода исполняется, и я могу заливать следующую программу.ИнфраструктураВсё видео было записано ботами, а сам я к Gameboy (точнее, к эмулятору) даже не притрагивался. Ниже — краткое описание инфраструктуры, которую я создал для этого проекта. Исходники доступны в http://hg.bortreb.com/vba-clojureПрежде всего, мне нужен был программный доступ к эмулятору, так что я скачал vba-rerecording. Поверх него я написал низкоуровневый интерфейс на C, к которому я смогу обращаться из Java через JNI. Этот интерфейс позволяет обращаться к базовым функциям эмулятора: прогнать один фрейм игры или один тик часов Gameboy, обратиться к любому адресу памяти или регистру. Кроме того, он позволяет выгрузить состояние эмулятора в объект Java или загрузить его обратно.Поверх JNI я написал обёртку на clojure, так что в итоге у меня получился функциональный интерфейс к vba-rerecording. Этот интерфейс работает с состоянием эмулятора как с иммутабельным объектом и позволяет делать в функциональном стиле всё то, что интерфейс на C позволял делать в императивном. С помощью этого интерфейса я написал функции, которые принимают на вход состояние эмулятора, находят и исполняют последовательность команд, которая приведёт к желаемому эффекту. Из этих функций я собрал высокоуровневые функции, которые выполняют в игре задачи типа перемещения и покупки предметов. Вот, например, код для перемещения кратчайшим путём из магазина покемонов в Viridian City в лабораторию Оака:
(defn-memo viridian-store->oaks-lab
  ([] (viridian-store->oaks-lab
       (get-oaks-parcel)))
  ([script]
     (->> script
          (walk [↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
                 ← ← ← ← ← ← ← ← ←
                 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
                 ← ←
                 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
                 ↓ ↓ ↓ ↓ ↓ ↓ ↓
                 → → → → → → → →
                 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
                 ← ← ← ← ←
                 ↓ ↓ ↓ ↓
                 ])
          (walk-thru-grass
           [↓ ↓ ↓ ↓ ↓ ↓ ↓])
          (walk [↓ ↓ ← ↓ ↓ ↓ ←
                 ↓ ↓ ↓ ↓ ↓ ↓
                 → → → ↑])
          (do-nothing 1))))
А вот функция, которая находит кратчайшую последовательность команд для переноса нужных предметов в сундук:
(defn-memo hacking-10
  ([] (hacking-10 (hacking-9)))
  ([script]
     (->> script
          begin-deposit
          (deposit-held-item 17 230)
          (deposit-held-item-named :parlyz-heal 55)
          (deposit-held-item 14 178)
          (deposit-held-item-named :water-stone 29)
          (deposit-held-item 14 32)
          (deposit-held-item-named :TM18 1)
          (deposit-held-item 13 1)
          (deposit-held-item 13 191)
          (deposit-held-item-named :TM02 98)
          (deposit-held-item-named :TM09 1)
          close-menu)))
Вооружившись этими инструментами, я составил программу ввода, которая может быть представлена в инвентаре. Это было непросто, потому что многие полезные коды не соответствуют никакому предмету, а количество предметов не может быть больше 99. Итак, вот что у меня получилось:Программа ввода(defn pc-item-writer-program  []  (let [;;limit 75        limit 201 ;; (item-hack 201 is the smallest I could make this.)         [target-high target-low] (disect-bytes-2 pokemon-list-start)]    (flatten     [[0x00  ;; (item-hack) no-op (can't buy repel (1E) at celadon)       0x1E  ;; load limit into E       limit       0x3F  ;; (item-hack) set carry flag no-op       ;; load 2 into C.       0x0E   ;; C == 1 means input-first nybble       0x04   ;; C == 0 means input-second nybble       0x21 ;; load target into HL       target-low       target-high       0x37 ;; (item-hack) set carry flag no-op       0x00 ;; (item-hack) no-op       0x37 ;; (item-hack) set carry flag no-op       0x00 ;; (item-hack) no-op       0xF3 ;; disable interrupts       ;; Input Section       0x3E ;; load 0x20 into A, to measure buttons       0x10        0x00 ;; (item-hack) no-op       0xE0 ;; load A into [FF00]       0x00       0xF0 ;; load 0xFF00 into A to get       0x00 ;; button presses              0xE6       0x0F ;; select bottom four bits of A       0x37 ;; (item-hack) set carry flag no-op       0x00 ;; (item-hack) no-op       0xB8 ;; see if input is different (CP A B)       0x00 ;; (item-hack) (INC SP)       0x28 ;; repeat above steps if input is not different       ;; (jump relative backwards if B != A)       0xED ;; (literal -19) (item-hack) -19 == egg bomb (TM37)       0x47 ;; load A into B              0x0D ;; dec C       0x37 ;; (item-hack) set-carry flag       ;; branch based on C:       0x20 ;; JR NZ       23 ;; skip "input second nybble" and "jump to target" below       ;; input second nybble       0x0C ;; inc C       0x0C ;; inc C       0x00 ;; (item-hack) no-op       0xE6 ;; select bottom bits       0x0F       0x37 ;; (item-hack) set-carry flag no-op       0x00 ;; (item-hack) no-op       0xB2 ;; (OR A D) -> A       0x22 ;; (do (A -> (HL)) (INC HL))       0x1D ;; (DEC E)       0x00 ;; (item-hack)        0x20 ;; jump back to input section if not done       0xDA ;; literal -36 == TM 18 (counter)       0x01 ;; (item-hack) set BC to literal (no-op)       ;; jump to target       0x00  ;; (item-hack) these two bytes can be anything.       0x01        0x00   ;; (item-hack) no-op       0xBF   ;; (CP A A) ensures Z       0xCA   ;; (item-hack) jump if Z       target-low       target-high       0x01   ;; (item-hack) will never be reached.       ;; input first nybble       0x00       0xCB       0x37  ;; swap nybbles on A       0x57  ;; A -> D       0x37  ;; (item-hack) set carry flag no-op       0x18  ;; relative jump backwards       0xCD  ;; literal -51 == TM05; go back to input section       0x01  ;; (item-hack) will never reach this instruction       ]      (repeat 8 [0x00 0x01]);; these can be anything      [;; jump to actual program       0x00       0x37  ;; (item-hack) set carry flag no-op       0x2E  ;; 0x3A -> L       0x3A       0x00  ;; (item-hack) no-op       0x26  ;; 0xD5 -> L       0xD5         0x01  ;; (item-hack) set-carry BC       0x00  ;; (item-hack) these can be anything       0x01         0x00       0xE9 ;; jump to (HL)Мне особенно пригодились глитч-предметы 0x00 и 0xFF. 0x00 стоит почти половину максимально возможного числа денег, и всего двух хватило на то, чтоб купить все необходимые предметы. К тому же 0x00 — это NO-OP, который я могу вставлять куда угодно, чтобы подогнать программу под требования инвентаря. 0xFF имеет другое полезное свойство. В норме игра объединяет стеки предметов: если купить, например, покеболл, а потом ещё один покеболл, то инвентарь будет выглядеть вот так:
pokeball x2
Но если где-то перед тем в списке предметов есть 0xFF, то эта функция отключается, что позволяет мне получить вот такой инвентарь:
pokeball x1
pokeball x1
pokeball x1
Так что я вставляю в самое начало инвентаря 0xFF и больше об этом не беспокоюсь. Полезная нагрузка, которую я залил в Gameboy, — это тоже последовательность программ. Я создал упрощённый формат MIDI, имплементировал его в машинных кодах GameBoy, и перевёл в свой формат мелодию с http://www.everyponysings.com/. В полезной нагрузке последней программы ввода содержится как сам MIDI-файл, так и интерпретатор. Картинка устроена так же — я перевёл PNG-файл в Gameboy-совместимый формат и добавил код, который её показывает. И картинку, и мелодию загружает последняя программа ввода, так что исходники можно увидеть на экране эмулятора (или скачать с http://hg.bortreb.com/vba-clojure).
===========
Источник:
habr.com
===========

===========
Автор оригинала: Robert McIntyre
===========
Похожие новости: Теги для поиска: #_nenormalnoe_programmirovanie (Ненормальное программирование), #_assembler, #_igry_i_igrovye_pristavki (Игры и игровые приставки), #_gameboy, #_speedrun, #_pokemon, #_haki (хаки), #_blog_kompanii_timeweb (
Блог компании Timeweb
)
, #_nenormalnoe_programmirovanie (
Ненормальное программирование
)
, #_assembler, #_igry_i_igrovye_pristavki (
Игры и игровые приставки
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 12:44
Часовой пояс: UTC + 5