О чём пойдёт речь
Поиск ошибок в ПО бывает очень разным. Я занимаюсь поиском ошибок в исходных кодах, в бинарных файлах, в больших комплексах программ и даже в каких-то железках. Но есть область, в которой я практически никогда ничем не занимался — поиск ошибок в компьютерных играх.
Люди тратят уйму времени, чтобы найти ошибки в играх. Это могут быть логические, математические и даже программистские ошибки. Кто-то делает это, чтобы посмотреть на игру под новым углом, а кто-то пытается пройти игру хоть на несколько секунд быстрее, чем любой другой игрок. Мне кажется именно сообщество спидранеров (людей, проходящих игры на скорость) внесло огромный вклад в дело поиска игровых глюков и ошибок. Но экономить время при прохождении мы будем в другой раз, сегодня просто рассказ о поиске ошибки в игровой логике.
В сентябре 2015 года компания Нинтендо выпустила Super Mario Maker — платформер про известного сантехника Марио. Одной из ключевых особенностей игры стала возможность пользователям самостоятельно создавать уровни (здесь они называются курсы) и делиться ими с другими игроками. Кто-то уровни создаёт, отслеживает процент успешных прохождений, а кто-то, собственно, проходит эти уровни. Именно в этот момент у игроков-исследователей зачесались руки — а можно ли опубликовать непроходимый уровень?
Рисовать что-то такое нет смысла, уровень хоть и действительно непроходимый, но и опубликовать мы его не сможем.
Перед Нинтендо стояла довольно интересная задача — как не допустить «захламления» библиотеки пустыми и непроходимыми уровнями. И они с ней хорошо справились. Решение довольно элегантное и не раз применялось в играх с подобной творческой концепцией: уровень нужно пройти самому перед публикацией и загрузкой на сервер. Причём один раз от начала и по разу от каждого чекпоинта. Фактически, задача сводится к тому, что необходимо разработать курс, который автор сможет пройти во время публикации, но не после неё.
Попытка номер 1. Кодовый замок
В игре множество объектов, которые взаимодействуют друг с другом по известным правилам. Можно попробовать воспроизвести что-то похожее на кодовый замок.
Для примера можно взять такую заготовку:
Купа (1) не может проходить свозь монстров на её пути (3), за исключением привидений (2). Монстры, находясь на головах друг у друга, не разбегаются и стоят на месте. Если Марио ударит кирпичный блок (4), то нижний монстр из стопки умрёт, опустив привидение на один блок ниже. Если привидение будет на одной линии с купой, та сможет пройти дальше. Автор уровня знает количество монстров до каждого привидения, и он знает сколько раз нужно ударить каждый блок, чтобы высвободить путь для купы. Как только купа пройдёт мимо всех монстров, она активирует триггер и освободит проход для Марио.
Этот пример приведён только для наглядности — в реальных попытках сделать кодовый замок все было сложнее. Игрок не видит монстров, уровень разбит на независимые сегменты, в каждом из которых нужно знать сколько раз и в какой кирпич биться. И только точное прохождение всех сегментов открывает проход к выходу с курса.
К сожалению, данный подход имеет ряд существенных минусов:
- Какой-то
задротэнтузиаст может потратить много времени и перебрать всё количественно. - Удачливый игрок просто ударит выбранные наугад блоки и, ВНЕЗАПНО, это будут те самые блоки, которые надо ударить то самое количество раз.
- Игроки могут просто скачать курс и открыть его в редакторе. После изучения, при должном усердии, они узнают «код».
Продолжим поиск способа загрузки непроходимого уровня.
Попытка номер 2. Красные монеты
Пользователь ReflectivistFox опубликовал видео The Impossible Level в котором предложил интересную идею для публикации непроходимого уровня. Правда, есть существенное ограничение: его курс проходим, если играть его с начала и до выхода, и АБСОЛЮТНО-ТОЧНО-ПОЛНОСТЬЮ-СОВСЕМ непроходим, если воспользоваться чекпоинтом. Казалось бы, при таких ограничениях уже не так интересно рассматривать концепцию уровня, но на самом деле даже такой шаг очень важен. Вы ещё помните, что во время загрузки уровня его нужно пройти не только от начала, но и от каждого чекпоинта? ReflectivistFox преодолел половину ограничения — его уровень можно пройти во время загрузки, но нельзя в обычном режиме.
Ключевой идеей является использование игрового элемента «красная монета» (сразу уточню, что игроки называют их и «красными», и «розовыми». Красными они были в предыдущих играх серии Марио, а розовыми стали в Super Mario Maker. Принцип их работы одинаков, поэтому здесь и далее я буду использовать только «красный»). В обычных уровнях красные монеты используются как части собираемого ключа. Автор курса располагает несколько таких монет в труднодоступных местах. Как только Марио собирает их все (игрок знает точное количество монет), он получает ключ от специальной двери, за которой могут быть какие-то бонусы или проход дальше по уровню.
Важной особенностью является то, что эти на эти монеты по-особенному влияют чекпоинты. Чекпоинт сохраняет игру, когда Марио активирует его. Если игрок проигрывает, персонаж погибает, то происходит загрузка с последнего чекпоинта. Все игровые элементы восстанавливаются: враги, бонусы и прочее. Но не красные монеты. Они не восстанавливаются, хотя сохраняется количество собранных монет. И это не вся особенность. Если собрать все красные монеты и проиграть, то поведение меняется — счетчик обнулится, а монеты будут на своих исходных местах.
ReflectivistFox в своём уровне использовал две особенности игры.
Обратите внимание, что чекпоинт (красный флаг в центре) наполовину закрыт блоком. Такая установка создаёт интересный эффект — активировать его можно, добравшись до верхнего блока, а при загрузке игра не сможет создать персонажа внутри блока, и он «выпадет» ниже, возле дверей и динозавра.
Таким образом, игрок, который загрузится с чекпоинта, попадёт на альтернативный маршрут. На этом маршруте его ждёт следующее:
Тут много элементов и коротко очень сложно описать, поэтому я выделю только самые важные вещи.
Марио появляется из двери и его взгляд направлен так же, как на картинке, а добраться ему надо до двери справа (4). Для этого ему нужно пройти по стрелочкам влево, подняться наверх и по блокам сверху пройти вправо (на скриншоте путь вверх и вправо проходит совсем по границе, но более удачного кадра не сделать). Специальная конструкция объектов (1) запрещает персонажу поворачиваться, если он это сделает, то пропадёт объект (2), и проход закроет подвижная стенка над ним. Все остальные способы завязаны на том, чтобы дракончик воспользовался блоком POW (3) — он будет отталкиваться и двигаться спиной, что позволит попасть влево без раннего провоцирования группы (1). К сожалению, перемещение блока POW (3) делает невозможным использование двери (4). Чтобы войти в дверь игроку нужно расположить что-то под дверью, чтобы быть с ней на одной горизонтали. Игрок может попробовать ловкостью рук забрать блок POW (3) и пронести его до двери, но автор позаботился и об этом — вертикальная часть маршрута слишком высока, чтобы ее можно было преодолеть обычным прыжком. Все варианты, которыми располагает игрок, приведут к взрыву POW или невозможности донести этот блок до двери (4). Именно с такой неразрешимой ситуаций сталкивается игрок, который решит пройти этот уровень от чекпоинта. А как же сам автор прошёл его при публикации?
Обратите внимание на блок (пустой), который я отметил цифрой 5. Изначально на этом месте находится красная монета. Во время прохождения, до чекпоинта, игрок не может пройти мимо и обязательно заберёт эту монету, и именно поэтому в данном месте сейчас пусто. Во время проверки игра не может определить, какие красные монеты игрок собрал, поэтому она, загружая состояние игры в чекпоинте, восстанавливает все красные монеты, давая возможность зазевавшемуся игроку собрать их все, если требуется. Вторая красная монета вмурована в стену и её невозможно собрать, тем самым обеспечивается невозможность перезагрузить счётчик монет, и поэтому во время игры на месте блока (5) монеты не будет, когда она будет нужна. А во время публикации, она там будет гарантированно — что позволяет воспользоваться ей вместо блока POW (3) для движения влево и спокойно выйти в дверь (4). А там уже и выход с курса рядом.
Итог этой попытки:
- Уровень можно пройти, если не пользоваться чекпоинтом вообще. Поэтому такой способ нам не подходит.
- Прохождение уровня во время публикации и во время игры всё же имеют геймплейные отличия, а значит надо продолжать искать.
Не унываем, продолжаем и ищем.
Попытка номер 3. Странный гриб
Разница была найдена! Это, так называемый, «странный гриб». С вероятностью в 1% странный гриб появляется вместо обычного.
Вот так выглядит странный гриб.
А вот Марио после, хм… использования гриба. Странный Марио отличается от обычного существенно более высокими и дальними прыжками. Кроме того, если Марио съест сначала обычный гриб, а потом странный, то Марио станет странным. Если поступить наоборот, то он всё равно останется странным — обычный гриб не окажет никакого эффекта.
Если бы все было так просто, то любой мог бы создать уровень вроде такого:
Длина рва специально рассчитана так, что обычный Марио его не перепрыгнет, а странный — легко. Достаточно переигрывать его раз за разом, пока не выпадет странный гриб. К сожалению, данный способ нам не подходит, так как он даёт целый 1% для вероятности прохождения. К счастью, Нинтендо уровень тоже не устраивает из-за того, что в 99% он непроходим и они решили исправить такую ситуацию. Во время публикации странные грибы не будут появляться никогда.
Вот оно! Нам нужно просто инвертировать логику. Если во время обычного прохождения гриб может выпасть, а во время публикации — нет, то нужно довести все до абсолюта. Нужно принудить гриб появиться, и тогда в обычном режиме всегда будет играть странный Марио, а во время публикации — обычный.
Вот так может выглядеть целая “грибная ферма”, которая принудит Марио собрать около 200 грибов (ограничение в 100 блоков одного типа, но можно разместить 100 в основном мире и ещё 100 в дополнительном).
А вот пример “ловушки” для странного Марио. Из-за более высокого прыжка, он не сможет свернуть в ответвление слева и будет заперт до окончания таймера (если быть точным, то там хитрая система из бомбы и препятствий, которые принуждают Марио прыгнуть выше, и для странного Марио этого достаточно чтобы запереться, а обычный просто активирует бомбу и может выйти с курса).
Собираем вместе и подводим итог:
- Очень хорошая попытка, которая с просто огромной вероятностью не даст пройти игру в обычном режиме.
- И все-таки есть вероятность в 13,4% (0,99^200 ~ 0,134, если мы считаем, что все генерируется с равномерной вероятностью), что странный гриб не выпадет, и уровень будет все-таки пройден. А даже если и не равномерно, то вероятность все равно есть, и уровень потенциально всё же может быть пройден.
Что же делать? Последний шанс.
Попытка номер 4. Блоки с двойной сущностью
Вы помните первую картинку к этой статье? Я повторю её:
Этот уровень был реально опубликован среди прочих уровней. И может быть опубликован ещё.
Нинтендо хорошо постаралась, когда придумывала логику проверки уровней, но так было не всегда. Когда игра только вышла, в ней были баги. И если мы воспользуемся одним из них, то сможем выполнить задуманное.
Сначала надо сделать вайп всех данных на Wii U и установить Super Mario Maker v1.0. Делать это надо в режиме офлайн, чтобы игра не обновилась. И можно будет играть в игру, какой она была на момент старта.
Теперь надо рассказать о блоках с двойной сущностью. Это глюк, который позволял блокам выглядеть одним образом, при этом по факту являться совсем другим. Чтобы создать такой надо было взять один из трёх видов блоков — монетку, облачко или сплошной непроходимый блок, поместить на карту, а поверх расположить другой блок из того же набора, но двигающийся по маршруту, после чего убрать маршрут (у блоков с маршрутом изначально есть небольшой путь).
Накладываем на облачко сплошной блок с маршрутом.
Такие блоки схлопнутся и будут вести себя как изначальный блок, а выглядеть — как наложенный. Спустя некоторое время Нинтендо запатчили это, чем поломали все уровни, которые использовали этот баг, потому что починка фактически из двойных блоков оставила только наложенные.
Блоки (1) только выглядят сплошными, а логически это обычные монетки.
После того, как такой уровень создан, не обязательно публиковать его сразу. Точнее попробовать стоит — игра предложит пройти наш курс, что мы легко сделаем. А вот отправить его на сервера не удастся, мы же не включили интернет. Но всё равно уровень будет помечен как пройденный и отправится на сервера без дополнительных проверок. Даже после обновления игры до актуальной версии. В которой курс уже невозможно пройти.
Готово! Вот он — загруженный и абсолютно точно никак и ни при каких обстоятельствах не проходимый уровень. С четвертой попытки успех был достигнут. На этом и завершается наше небольшое путешествие в мир исследования игры Super Mario Maker.
Заключение
Спасибо, что дочитали эту простыню текста с картинками. Я постарался доступно рассказать о процессе поиска ошибок в логике работы приложения. В данном случае — это игра, но общие подходы сохраняются, если их переносить на системное и прикладное ПО. Код может быть написан хорошо, но это не означает, что и работать все будет так, как задумано. Логические уязвимости очень опасны и сложнообнаружимы, но этим и интересны.
Всем хороших игр, уязвимостей и добра!
Источник