0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Как EA усложнили нам жизнь, или как мы чинили баг 12-летней давности

Как EA усложнили нам жизнь, или как мы чинили баг 12-летней давности

Иногда в программы закрадываются баги. Причем закрадываются так, что обнаружить их получается лишь через много-много лет после выпуска, когда чинить их уже нерентабельно. Иногда такие баги оказываются слишком критическими, чтобы их игнорировать. Поэтому под катом я расскажу, как мы устраняли один такой критический баг в одной старенькой гонялке. А заодно наглядно продемонстрирую, чем плох float, какие могут быть последствия и как с этим бороться.

Лирика

Речь пойдет об игре Need for Speed: Most Wanted. Эта игра очень популярна и крайне любима многими геймерами-энтузиастами, и многими считается чуть ли не лучшей в серии. Даже если вы не азартный игрок, то наверняка слышали об этой игре в частности или о серии в целом. Но обо всем по порядку.

Я – спидранер. Прохожу игры на скорость. Довелось мне быть одним из первопроходцев в деле скоростного прохождения гоночных игр, поэтому я заодно являюсь одним из «глобальных модераторов» спидран-коммьюнити серии NFS. Участь со мной разделил чех под ником Ewil.

Почему участь? Потому что в один прекрасный момент к нам в дискорд-сервер пришел человек и заявил, что все наши рекорды трасс неправильны, и что мы – нубы. Скрепя сердце, подавляя багет от, как казалось, необоснованного обвинения и борясь с языковым барьером (английским этот человек владеет на очень плохом уровне), мы начали разбираться, что же не так с нашими рекордами. Из обрывков речи мы поняли, что в игре есть некий «timebug», который делает IGT неправильным. Ewil пересмотрел некоторые записи и руками пересчитал время. Оказалось, нам не врали. На записях IGT резко отличалось от RTA . Это были не пара миллисекунд, которые тоже могу решить исход рекорда, а местами разница доходила даже до нескольких секунд(!).

Мы начали искать причину и последствия этого явления. Еще задолго до этого я в личных интересах пытался «вытащить» из игры IGT. Моя попытка не увенчалась успехом, и я как-то забил на эту идею. Информации в интернете мы найти не смогли, за исключением какой-то английской странички с очень коротким описанием, без какой-либо доказательной базы. Поиск по ютубу также не принес результатов, но были найдены записи, которые гласили «No TimeBug».

Чуть позже я познакомился со SpeedyHeart, и она мне подсказала, что в игре время считается как float. Тут все начало проясняться, и мы медленно переходим от унылого вступления к лютому экшону!

Как это работает

Вооружившись Cheat Engine, OllyDbg, DxWND и NFS: MostWanted версии 1.3, я полез рыться в памяти. Выкопал я примерно вот что (картинка кликабельна):

Нас интересуют последние три адреса. Они хранят в себе IGT для разных ситуаций. Почему они float – одному Блэк Боксу известно… Но так не делают! Float, у него же точность, как у дробовика, а может и того хуже.

Собственно, немного о самих таймерах. Таймеры хранят время в секундах, т. е. целая часть – количество полных секунд. Два из этих таймеров, а именно Global IGT и Race IGT, периодически обнуляются. Для Global IGT это происходит в момент выхода в главное меню, а Race IGT обнуляется при рестарте гонки. Подсчет IGT производится через Global IGT, и в какой-то момент времени ему уже не хватает точности. Из-за этого время считается неправильно.

На этой стадии меня заинтересовали несколько вопросов:

  1. Раз уж есть разница во времени, то отличается ли геймплей с багом и без? Логично предположить, что если IGT ускоряется, то и в целом игра должна становиться «быстрее»
  2. Какие рамки у этого бага? Как он будет себя вести при разных значениях таймера, и как на это будет реагировать игра.

Ответ на вопрос номер 1 был найден крайне быстро. Я просто взял и изменил показания Global IGT на 300000.0 и получил то, что получил. Время ускорилось почти в два раза(!), однако на физике и поведении игры это никак не отразилось. Прикола ради я тыркал и другие таймеры, но они, почему-то, ни на что не влияют. Собственно, если бы с ускорением времени ускорялся и геймплэй, то в нашем мире спидранерства это считается вполне законным. Все таки мы любим баги. Но такой расклад никого не устроил.

Я пошел немного глубже и нашел ответ на вопрос 2. Как только Global IGT достигает отметки в 524288 время в игре полностью останавливается. Это немного смущает игру, и она начинает плохо себя вести делать интересные вещи. Например, не дает начать гонку после рестарта, намертво блокируя игру (выйти из нее можно только через диспетчер задач или Alt+F4). Отрыв/отставание от соперников перестает работать. А если проиграть гонку, то игра отправляет вас в свободное плавание по миру.

Float — ад перфекциониста.

Хоть и не в прямом смысле, но все же. Для наглядной демонстрации я написал небольшую программу, которая поможет мне оценить всю печальность происходящего.

Перед непосредственно кодом, распишу немного про цели. Известно, что игра «блокирует» цикл обновления физики на 120 раз в секунду (опять же, спасибо SpeedyHeart за информацию). Однако vsync обрубает обновление физики еще сильнее, до 60 раз в секунду. Соответственно, мы просто возьмем float-переменную и будем циклически туда добавлять 1/60 секунды. Потом мы посчитаем, за сколько шагов мы добились результата, и за сколько шагов мы должны были добиться этого результата. Также будем делать все циклически для разных случайных величин и считать среднюю погрешность в рассчетах. Любые отклонения в 2 и менее шагов (33мс) мы будем считать незначительными, потому что игра показывает время до сотых секунды.

Меняя значения START_TIME и TEST_TIME мы можем получить необходимую нам статистику. В целом, пока START_TIME не превышает 15 минут, то обычный 2-х минутный заезд окажется «свободным» от бага. Разница остается не критичной в рамках игры, 1-2 кадра:

16 Минут же оказались «критической точкой», когда время беспощадно плывет:

Интересен так же тот момент, что в зависимости от START_TIME будет меняться «сторона» ошибки. Например, после полутора часового непрерывного геймплея время начнет течь медленнее, чем должно:

Поигравшись со значениями еще немного я оценил, что в получившейся программе «время» течет примерно так же, как в игре. Это было подтверждено практически – любые рекорды, записанные «из главного меню» в течение первых 15 минут геймплея были чистыми. При START_TIME близкому к 300000 секунд количество шагов было почти в два раза меньше, чем ожидалось. При START_TIME, большем магической константы 524288 программа переставала работать. Все это подтверждало, что процесс подсчета времени был скопирован верно.

Устраняем нежелательное поведение

Теперь, когда известна проблема и ее поведение, можно ее устранить. Нужно лишь перезаписывать Global IGT всякий раз, когда игрок начинает заезд заново. Это можно узнать довольно просто – в этот момент обнуляется Race IGT. Но тут есть проблема.

Есть два издания игры: NFS: Most Wanted и NFS: Most Wanted Black Edition. Второе издание включает в себя две дополнительные машины и две трассы, да 69-ое испытание. Но, технически, это две совершенно разные игры! Их запускаемые файлы отличаются. Помимо этого, есть патч 1.3… Который отличается для каждого издания. В итоге у нас есть 4 разных версии игры, которые надо поддерживать. Этот факт делает «правильный» путь чрезмерно сложным и неоправданным. По-хорошему, нужно слегка подправить запускаемый файл и обнулять счетчик там, но… Править 4 разных экзешника, которые еще и запакованы, да защищены от отладки… Лучше просто напишем простую программку, которая будет в реалтайме отслеживать состояние таймеров и обнулять их при необходимости. Писать будем на C#.

Вот такую архитектурку я набросал. GameProcess – это вспомогательный класс, который упрощает доступ к чтению-записи памяти процесса. GameHolder – сердце программы. Он будет инициализировать GameProcess, а при «подцепе» процесса будет определять версию игры и создавать необходимый экземпляр наследника Game. Поскольку логика «фикса» не отличается от версии к версии, то ее лучше вынести в один класс.

Как же нам определить версию? Просто – по размеру основного модуля. Я специально реализовал проперти ImageSize. А чтобы не захламлять код магическими константами, запилим enum:

Остальные версии добавим по мере их попадания ко мне в руки.

isUnknown отвечает за тот факт, удалось ли нам определить версию или нет. Из всего класса нам интересен только метод Refresh, вот он:

Логика фикса вышла совсем простенькой:

Дело осталось за малым: реализовать версию, выставив в конструкторе необходиме значения соответствующим protected-переменным. В мэйне же просто кидаем цикл обновления в отдельный трэд и забываем про него. Ах да, из-за особенностей карточек Nvidia и особенностей реализации установщика игр NFS мы будет принимать на вход имя процесса, чтобы была возможность кастомизации.

На этом фикс заканчивается. Компилируем, запускаем и забываем о таймбаге, yay! ^_^ Картинка кликабельна.

На самом деле, никуда этот баг не денется. Если один заезд физически не уложится в рамки 15 минут, то тут уже ничего не поделаешь. Но таких заездов в игре аж один, и тот от полиции.
Полные исходники на гитхабе.

Summary

Вот так один маленький баг нехило подпортил нам жизнь. А ведь его можно было избежать, если бы Black Box использовали в свое время double, но нет. Кстати, это яркий пример того, как «написанное однажды» выливается в кучу неулавливаемых/перекатывающихся багов. Timebug присутствовал в каждой игре от Black Box ever since. В Carbon, ProStreet и даже Undercover. В последнем они поменяли логику подсчета IGT, но эти три таймера там все так же присутствуют, и ошибки округления приводят к странным последствиям. SpeedyHeart обещала сделать видео-обзор всей найденой в процессе информации, так что ждем-с.

Чему меня научила эта ситуация? Не знаю. Я и так понимал, что использовать float для серьезных вычислений – идея сомнительная. Но теперь я лучше представляю, как именно все это будет работать на практике. Однако забавно получилось, что такая серьезная компания могла допустить такую серьезную ошибку, да еще и несколько лет подряд не замечать ее.

Мне кажется, что для данной задачи (подсчет IGT) нужно использовать такой путь: ставить timestamp в начале заезда, а потом вычитать из текущего времени. Причем арифметических операций стоит избегать, даже над целыми числами. 1/60 секунды это 16,(6) миллисекунд, поэтому в случае целого числа мы будем наивно откидывать 0,(6) при каждом сложении, что приведет к неточностям в подсчете.

В некоем обозримом будущем я постараюсь написать фикс и на другие версии. На этом у меня все, спасибо за внимание.

UPD: Поправил ссылку на гитхаб всвязи с переездом на новое имя.
UPD2: Выкатил вторую часть, интересующиеся могут прочитать.

⇡#Спорткары легкого поведения

Первым делом создатели Burnout растоптали один из главных стимулов к прохождению. Отныне все машины, за исключением десяти болидов, принадлежащих гонщикам из списка самых разыскиваемых, доступны пользователю с самого начала игры. Не нужно их покупать или завоевывать в нелегких поединках — достаточно проехать мимо гудящего аудиосистемой и слепящего фарами автомобиля, припаркованного где-нибудь в укромном месте. Если даже такой немудрёный вариант кажется слишком муторным, позволяется за 149 рублей купить в Origin доступ ко всему под милым названием «Most Wanted Время Saver Pack» (цитата дословная). Осталось только показывать финальные ролики за деньги, а также ввести тарифы на заказные убийства «боссов» в экшенах.

Читать еще:  Need for Speed (2015)

Однако сами гоночные снаряды вышли просто шикарными, а их ассортимент включает в себя все, что только можно представить. Criterion совмещает несовместимое, умещая в одной игре самые разные классы и марки машин. Если только вы не поклоняетесь исключительно американским танкам, то обязательно найдете себе что-нибудь по душе. А даже если и поклоняетесь, огромный Ford F-150 легко их заменит. «Бешеная блоха» Ariel Atom и её собрат Caterham R500 Superlight, большие «мускулы» Ford Mustang и Dodge Challenger, агрессивный хот-хэтч Ford Focus RS500, смелый концепт Audi A1 Clubsport Quattro, астрономически быстрые и настолько же дорогие Pagani Huayra, Lamborghini Aventador и Bugatti Veyron SuperSport, раллийная классика Lancia Delta Integrale… есть даже отечественная Marussia B2!

Список можно продолжать еще долго, ведь он насчитывает 42 модели. Машины прорисованы настолько хорошо, что создают неожиданный эффект — как будто ты очутился среди коллекции игрушечных моделек. Хочется повертеть каждую в руках, потрогать, осмотреть, а потом перейти к следующей. И так пока весь набор не подойдет к концу или процесс не надоест. Обратная сторона этого эффекта — поверхностность. Едва проедешь одну-две гонки за рулем классического Porsche 911 Turbo, как за очередным поворотом взгляд падает на заманчиво сверкающий Ford GT. Словом, гаража как такового нет, есть лишь набор игрушечных машинок, которые на минуту снимаешь с полки, а потом возвращаешь обратно.

Этот поверхностный подход закрепляется организацией гонок. Нашей целью в игре является победа над гонщиками из списка Most Wanted. Точнее, не гонщиками, а тачками — какого-либо намека на сюжет здесь нет и в помине. Конечно, для автомобильной аркады это дело десятое, однако вспомните, насколько хорошо вписалась в общую картину простенькая история в Most Wanted образца 2005 года. Она обеспечивала надлежащий подтекст происходящему, давая игроку дополнительную мотивацию двигаться к развязке. Здесь же все для машин и ради машин.

Чтобы получить право вызвать очередного конкурента на поединок, необходимо набрать определенное количество Speed Points, выдаваемых за победы в гонках. На любой автомобиль приходится всего пять заездов, каждый из которых не только приносит пресловутые очки, но и позволяет оснастить болид каким-нибудь улучшением. Тюнинг примитивен донельзя. Три разновидности шин, короткая/длинная КПП (для достижения наилучшего ускорения или максимальной скорости соответственно), легкий или прочный кузов, два варианта нитро — и все. Да и не совсем понятно, к чему плясать и пытаться выжать из той же EVO X то, что Nissan GT-R умеет в базе, ведь ограничений на выбор оружия победы в гонках с Most Wanted нет.

Need For Speed: High Stakes, PSone, PC, 1999

Старые аркадные гонки для PSone и ПК от Electronics Arts, 99 года выпуска, без лутбоксов, микротранзакций и прочего бреда. В версию для PC не играл, помню только консольный вариант. Кстати, разработкой для двух платформ занимались разные студии EA.

Эта часть является классическим продолжением серии прошлого века с заездами за городом, мимо лесов, гор, океанов, европейских городков. Помимо, более и менее, доступных марок автомобилей, есть и зверские спорткары типа Lamborghini, Ferrari. Причем на этот раз средства передвижение не обладают каким-то бронебойным покрытием, когда после дикого столкновения на тачке не остается не то что вмятины, но и не одной царапины, и даже пылинки. Разбить машину вдребезги на манер Burnout’а все еще нельзя (ведь не должны же владельцы лицензии видеть, как их серия автомобилей превращается в кусок паленого железа), но система повреждений присутствует. Мнется корпус, а изношенная модель отбрасывает

красивые искры по пути к финишу.

Но тут дело не только в добавлении визуального реализма, все это так же оказывает влияние на управление: сбрасывается скорость, чето вечно скрипит, как на пассажирском Пазике, тачка хуже входит в поворот. Исправить неполадки можно, отремонтировав автомобиль в гараже. Помимо получения медалей за первое место, теперь есть возможность зарабатывать деньги, которые будут тратиться как на ремонт, так и на добавлении различных модификаций, улучающие двигатель и «толстокожесть» выбранного транспорта. Есть обычные заезды, но теперь появился режим карьеры с несколькими вариантами заработка на еду и новую машину. Самый забавный — High Stakes: только два соперника, проиграл — отдавай тачку, выиграл — забирай машину соперника. Все по взрослому. Но и, конечно, классические гоночки, с несколькими соперниками, аркада и т. д. Полиция тоже никуда не делась, при желании можно попробовать свои силы в игре за сотрудников, только вместо одного полицейского, теперь в Вашем распоряжении целых отряд преследования, между которыми можно переключаться прямо на ходу.

Версия для PC

Управление осталось на аркадном уровне, оно и к лучше. Глюков нет, кататься приятно — ну и ладно.

Вот чего Electronic Arts больше не сможет повторить (или может, но не хочет), так это отменный саундтрек. Отличные композиции, записанные специально для игры.

Игрушка подойдет для запуска на смартфонах с использованием эмулятора. Но если хочется аналогов посерьезнее, то есть Hot Pursuit 2010, на который ценник в стиме, кажется, навечно застыл в скидках (сколько раз не захожу все время вижу ее в акциях, стоит около 100 р.). Скачиваем, вырубаем музыку, включаем нормальный плейлист из High Stakes и вперед.

⇡#Коллективный бедлам

Исправить ситуацию могла бы сетевая игра, благо красивая графика и приятная физика поведения машин у Most Wanted есть. Однако и здесь создатели решили поступить по-своему, сделав «не так, как раньше». Нам предлагается участвовать в серии из пяти заездов, в которые входят как обычные гонки, так и различные испытания. Среди десятка других игроков вы оказываетесь в точно таком же Фэйрхевене, как и в одиночной игре, а на карте мерцает маркер, показывающий точку начала события.

Пока на месте не соберутся все, заезд не начнется — представляете, каково это, когда кругом сплошь случайные люди? Один может отойти по делам, оставив игру включенной, другой будет кататься по городу в свое удовольствие… конечно, система предусматривает принудительный старт через несколько минут после того, как обозначилось место встречи, однако между гонками все равно проходит много пустого, ничем не заполненного времени.

Старт заезда в Most Wanted — это только начало в череде сюрпризов. В нарезании кругов или спринте все еще более-менее хорошо (за исключением изредка дергающихся и проваливающихся в землю соперников), но вот состязания стоит упомянуть отдельно. Они здесь так же неуместны, как фингал под глазом перед фотографированием на паспорт. К примеру, в одном из них побеждает тот, кто больше раз сможет разминуться с другими игроками внутри лежащей на стройплощадке трубы. Постановка задачи, как и в большинстве других испытаний, настолько туманна, что сначала даже не понимаешь, что от тебя требуется (таймер тем временем тикает). А когда, наконец, осознаешь, то начинаешь смеяться над абсурдностью происходящего — в трубе царит броуновское движение и постоянные столкновения, а особо толстые тролли просто ставят машину поперек и уходят… Организация играющих в мультиплеере отсутствует напрочь, а состязания слишком примитивны, так что рассчитывать на приятное времяпровождение не приходится. Почему нельзя было сделать все по-старому и как следует, вместо того чтобы пытаться всучить нам свежеизобретённый велосипед с вырванными спицами и торчащей над рамой трубой вместо седла?

От Most Wanted 2012 можно получить удовольствие (только в «сингле», уважаемые читатели), если воспринимать его как самостоятельный проект, никак не связанный с NFS. Это красивая игра про красивые автомобили, которые хочется перепробовать один за другим, пока не даст о себе знать общая незатейливость происходящего. Впрочем, трудно не обращать внимания на глядящие отовсюду логотипы Need for Speed и Most Wanted, которые сулят издателю солидные тиражи и большие прибыли. Правда, такой трюк с покупателями обычно срабатывает только один раз.

Компромисс для любителей

Удивительно, но Criterion Games выполнила собственное обещание и представила на PS Vita практически такую же игру, что и на консолях/ПК. Тот же самый город Фэйрхевен со всеми его секретами и гоночными событиями, абсолютно идентичное управление, сетевые возможности (зачем-то с авторизацией в местном Origin), даже российская локализация — всё как в домашних версиях.

Однако наличествует и ряд отрицательных моментов. Прежде всего, пришлось серьёзно пожертвовать графикой и детализацией — Most Wanted выглядит немногим лучше товарок по жанру с PSP. Машинам явно недостаёт полигонов, а городские достопримечательности размыты и не производят совершенно никакого впечатления — всё это расплата за огромный открытый мир. Кроме того, и так немноголюдный Фэйрхевен стал ещё более пустым — игрока преследуют в лучшем случае пять копов (ни о какой дюжине и речи не идёт), да и участников самих гонок будто бы всегда меньше.

Портят слабый визуальный ряд и местные режиссёрские приёмчики вроде вертящейся вокруг места старта гонки камеры . Смотрится это ну совсем странно, с такими-то торчащими пикселями и «лесенками».

Ещё один нюанс связан с маленьким экраном «Виты». Игра работает с хорошей частотой смены кадров (и это безусловный плюс), автомобили несутся с огромной скоростью по урбанистическим просторам, но некоторые мелкие элементы окружающего мира вроде бордюров, выступов или столбиков порой сложно разглядеть — масштаб объектов явно рассчитан на телевизор или хотя бы монитор. В результате происходят неприятные столкновения и аварии, которые, по идее, не случились бы на дисплее с большей диагональю.

Аналогичная ситуация с интерфейсом. Надписи, менюшки и обозначения слишком маленькие; разглядеть их, конечно, не составляет особого труда, но на портативной консоли шрифты подобного размера совсем не смотрятся. И некрасиво, и не слишком функционально.

Получился несколько компромиссный порт. Играть, в общем-то, можно и где-то даже увлекательно, но чувствуется, что предлагают не совсем полноценное блюдо. Навевает мысли: дескать, что хуже — портативные игры «по мотивам» больших проектов (со всеми известными недостатками) или вещи вроде Most Wanted на Vita с ампутированными элементами, но в канонически верном виде.

Most Wanted полностью переведена на русский язык, однако стоит придержать радостные восклицания. К немногочисленным текстовым элементам претензий нет (глаз режет только дурацкое «Вы: превзойти машину Most Wanted» после победы над одним из гонщиков в списке), а вот с полицейскими всё хуже. Такое ощущение, что переговоры копов по радио переводили и озвучивали люди, знающие наш язык как второй, — они вроде бы понимают смысл сказанного, но на практике часто получается нелепо. Впечатление только усиливается, когда служители порядка проговаривают свой немногочисленный запас реплик десятки раз на протяжении игры.

Оценка локализации: 7

Достоинства:

  • внушительный автопарк из 42 моделей — от грузных внедорожников и спортивной классики до стремительных суперкаров;
  • вылизанные до блеска модели машин, динамическая смена дня и ночи, симпатичное окружение;
  • зрелищная, отзывчивая аркадная физика;
  • приятный саундтрек.

Недостатки:

  • суматошный и бестолковый мультиплеер;
  • уход от полиции на машинах начального уровня может стать настоящей пыткой;
  • отсутствие штрафов за поимку копами делает погони практически бессмысленными;
  • повторяющиеся реплики служителей закона быстро надоедают;
  • «работать» на приобретение новых машин не приходится, а сами автомобили забываются так же быстро, как достаются;
  • оторваться от гонщиков Most Wanted невозможно, зато они сами с легкостью растворяются за горизонтом;
  • чувства прогресса и какой-то весомой мотивации по ходу прохождения не возникает: что первый конкурент из списка, что десятый — всё одно.

Изменить параметры электропитания

По умолчанию в компьютере установлен сбалансированный режим электропитания, который, а в некоторых ноутбуках, в целях увеличения времени работы, и вовсе установлена экономия энергии.

Это не дает компьютеру раскрыть свой потенциал в Need for Speed: Most Wanted (2005) полностью, поэтому первым делом нужно открыть панель управления, которую можно найти с помощью поиска. После нужно сделать следующее:

  • Выбрать режим просмотра «Мелкие значки»;
  • Кликнуть на «Электропитание»;
  • На экране найти опцию «Настройка схемы электропитания», кликнуть на нее;
  • Кликнуть на «Изменить дополнительные параметры питания»;
  • В открывшемся окне найти выпадающий список;
  • В списке выбрать «Высокая производительность»;
  • Нажать кнопку «Применить», затем кликнуть «ОК».

В игре Need for Speed: Most Wanted [2005] от EA можно поучаствовать в гонке от спонсора, компании «Бургер кинг», если ввести burgerking в меню «Нажмите для продолжения»

Это обычный спринт, во время которого ты катаешься на BMW M3 GTR

Читать еще:  Need for Speed: Most Wanted (2012)

БК пришел в Россию только в 2010 году, через 5 лет после выхода игры. Кстати, я узнал об этой пасхалке только в 2019 году.

После победы можно получить детали от «старьевщика».

Пишите, какие еще рекламные интеграции вы знаете.

Как EA усложнили нам жизнь, или как мы чинили баг 12-летней давности

Иногда в программы закрадываются баги. Причем закрадываются так, что обнаружить их получается лишь через много-много лет после выпуска, когда чинить их уже нерентабельно. Иногда такие баги оказываются слишком критическими, чтобы их игнорировать. Поэтому под катом я расскажу, как мы устраняли один такой критический баг в одной старенькой гонялке. А заодно наглядно продемонстрирую, чем плох float, какие могут быть последствия и как с этим бороться.

Лирика

Речь пойдет об игре Need for Speed: Most Wanted. Эта игра очень популярна и крайне любима многими геймерами-энтузиастами, и многими считается чуть ли не лучшей в серии. Даже если вы не азартный игрок, то наверняка слышали об этой игре в частности или о серии в целом. Но обо всем по порядку.

Я – спидранер. Прохожу игры на скорость. Довелось мне быть одним из первопроходцев в деле скоростного прохождения гоночных игр, поэтому я заодно являюсь одним из «глобальных модераторов» спидран-коммьюнити серии NFS. Участь со мной разделил чех под ником Ewil.

Почему участь? Потому что в один прекрасный момент к нам в дискорд-сервер пришел человек и заявил, что все наши рекорды трасс неправильны, и что мы – нубы. Скрепя сердце, подавляя багет от, как казалось, необоснованного обвинения и борясь с языковым барьером (английским этот человек владеет на очень плохом уровне), мы начали разбираться, что же не так с нашими рекордами. Из обрывков речи мы поняли, что в игре есть некий «timebug», который делает IGT неправильным. Ewil пересмотрел некоторые записи и руками пересчитал время. Оказалось, нам не врали. На записях IGT резко отличалось от RTA . Это были не пара миллисекунд, которые тоже могу решить исход рекорда, а местами разница доходила даже до нескольких секунд(!).

Мы начали искать причину и последствия этого явления. Еще задолго до этого я в личных интересах пытался «вытащить» из игры IGT. Моя попытка не увенчалась успехом, и я как-то забил на эту идею. Информации в интернете мы найти не смогли, за исключением какой-то английской странички с очень коротким описанием, без какой-либо доказательной базы. Поиск по ютубу также не принес результатов, но были найдены записи, которые гласили «No TimeBug».

Чуть позже я познакомился со SpeedyHeart, и она мне подсказала, что в игре время считается как float. Тут все начало проясняться, и мы медленно переходим от унылого вступления к лютому экшону!

Как это работает

Вооружившись Cheat Engine, OllyDbg, DxWND и NFS: MostWanted версии 1.3, я полез рыться в памяти. Выкопал я примерно вот что (картинка кликабельна):

Нас интересуют последние три адреса. Они хранят в себе IGT для разных ситуаций. Почему они float – одному Блэк Боксу известно… Но так не делают! Float, у него же точность, как у дробовика, а может и того хуже.

Собственно, немного о самих таймерах. Таймеры хранят время в секундах, т. е. целая часть – количество полных секунд. Два из этих таймеров, а именно Global IGT и Race IGT, периодически обнуляются. Для Global IGT это происходит в момент выхода в главное меню, а Race IGT обнуляется при рестарте гонки. Подсчет IGT производится через Global IGT, и в какой-то момент времени ему уже не хватает точности. Из-за этого время считается неправильно.

На этой стадии меня заинтересовали несколько вопросов:

  1. Раз уж есть разница во времени, то отличается ли геймплей с багом и без? Логично предположить, что если IGT ускоряется, то и в целом игра должна становиться «быстрее»
  2. Какие рамки у этого бага? Как он будет себя вести при разных значениях таймера, и как на это будет реагировать игра.

Ответ на вопрос номер 1 был найден крайне быстро. Я просто взял и изменил показания Global IGT на 300000.0 и получил то, что получил. Время ускорилось почти в два раза(!), однако на физике и поведении игры это никак не отразилось. Прикола ради я тыркал и другие таймеры, но они, почему-то, ни на что не влияют. Собственно, если бы с ускорением времени ускорялся и геймплэй, то в нашем мире спидранерства это считается вполне законным. Все таки мы любим баги. Но такой расклад никого не устроил.

Я пошел немного глубже и нашел ответ на вопрос 2. Как только Global IGT достигает отметки в 524288 время в игре полностью останавливается. Это немного смущает игру, и она начинает плохо себя вести делать интересные вещи. Например, не дает начать гонку после рестарта, намертво блокируя игру (выйти из нее можно только через диспетчер задач или Alt+F4). Отрыв/отставание от соперников перестает работать. А если проиграть гонку, то игра отправляет вас в свободное плавание по миру.

Float — ад перфекциониста.

Хоть и не в прямом смысле, но все же. Для наглядной демонстрации я написал небольшую программу, которая поможет мне оценить всю печальность происходящего.

Перед непосредственно кодом, распишу немного про цели. Известно, что игра «блокирует» цикл обновления физики на 120 раз в секунду (опять же, спасибо SpeedyHeart за информацию). Однако vsync обрубает обновление физики еще сильнее, до 60 раз в секунду. Соответственно, мы просто возьмем float-переменную и будем циклически туда добавлять 1/60 секунды. Потом мы посчитаем, за сколько шагов мы добились результата, и за сколько шагов мы должны были добиться этого результата. Также будем делать все циклически для разных случайных величин и считать среднюю погрешность в рассчетах. Любые отклонения в 2 и менее шагов (33мс) мы будем считать незначительными, потому что игра показывает время до сотых секунды.

Меняя значения START_TIME и TEST_TIME мы можем получить необходимую нам статистику. В целом, пока START_TIME не превышает 15 минут, то обычный 2-х минутный заезд окажется «свободным» от бага. Разница остается не критичной в рамках игры, 1-2 кадра:

16 Минут же оказались «критической точкой», когда время беспощадно плывет:

Интересен так же тот момент, что в зависимости от START_TIME будет меняться «сторона» ошибки. Например, после полутора часового непрерывного геймплея время начнет течь медленнее, чем должно:

Поигравшись со значениями еще немного я оценил, что в получившейся программе «время» течет примерно так же, как в игре. Это было подтверждено практически – любые рекорды, записанные «из главного меню» в течение первых 15 минут геймплея были чистыми. При START_TIME близкому к 300000 секунд количество шагов было почти в два раза меньше, чем ожидалось. При START_TIME, большем магической константы 524288 программа переставала работать. Все это подтверждало, что процесс подсчета времени был скопирован верно.

Устраняем нежелательное поведение

Теперь, когда известна проблема и ее поведение, можно ее устранить. Нужно лишь перезаписывать Global IGT всякий раз, когда игрок начинает заезд заново. Это можно узнать довольно просто – в этот момент обнуляется Race IGT. Но тут есть проблема.

Есть два издания игры: NFS: Most Wanted и NFS: Most Wanted Black Edition. Второе издание включает в себя две дополнительные машины и две трассы, да 69-ое испытание. Но, технически, это две совершенно разные игры! Их запускаемые файлы отличаются. Помимо этого, есть патч 1.3… Который отличается для каждого издания. В итоге у нас есть 4 разных версии игры, которые надо поддерживать. Этот факт делает «правильный» путь чрезмерно сложным и неоправданным. По-хорошему, нужно слегка подправить запускаемый файл и обнулять счетчик там, но… Править 4 разных экзешника, которые еще и запакованы, да защищены от отладки… Лучше просто напишем простую программку, которая будет в реалтайме отслеживать состояние таймеров и обнулять их при необходимости. Писать будем на C#.

Вот такую архитектурку я набросал. GameProcess – это вспомогательный класс, который упрощает доступ к чтению-записи памяти процесса. GameHolder – сердце программы. Он будет инициализировать GameProcess, а при «подцепе» процесса будет определять версию игры и создавать необходимый экземпляр наследника Game. Поскольку логика «фикса» не отличается от версии к версии, то ее лучше вынести в один класс.

Как же нам определить версию? Просто – по размеру основного модуля. Я специально реализовал проперти ImageSize. А чтобы не захламлять код магическими константами, запилим enum:

Остальные версии добавим по мере их попадания ко мне в руки.

isUnknown отвечает за тот факт, удалось ли нам определить версию или нет. Из всего класса нам интересен только метод Refresh, вот он:

Логика фикса вышла совсем простенькой:

Дело осталось за малым: реализовать версию, выставив в конструкторе необходиме значения соответствующим protected-переменным. В мэйне же просто кидаем цикл обновления в отдельный трэд и забываем про него. Ах да, из-за особенностей карточек Nvidia и особенностей реализации установщика игр NFS мы будет принимать на вход имя процесса, чтобы была возможность кастомизации.

На этом фикс заканчивается. Компилируем, запускаем и забываем о таймбаге, yay! ^_^ Картинка кликабельна.

На самом деле, никуда этот баг не денется. Если один заезд физически не уложится в рамки 15 минут, то тут уже ничего не поделаешь. Но таких заездов в игре аж один, и тот от полиции.
Полные исходники на гитхабе.

Summary

Вот так один маленький баг нехило подпортил нам жизнь. А ведь его можно было избежать, если бы Black Box использовали в свое время double, но нет. Кстати, это яркий пример того, как «написанное однажды» выливается в кучу неулавливаемых/перекатывающихся багов. Timebug присутствовал в каждой игре от Black Box ever since. В Carbon, ProStreet и даже Undercover. В последнем они поменяли логику подсчета IGT, но эти три таймера там все так же присутствуют, и ошибки округления приводят к странным последствиям. SpeedyHeart обещала сделать видео-обзор всей найденой в процессе информации, так что ждем-с.

Чему меня научила эта ситуация? Не знаю. Я и так понимал, что использовать float для серьезных вычислений – идея сомнительная. Но теперь я лучше представляю, как именно все это будет работать на практике. Однако забавно получилось, что такая серьезная компания могла допустить такую серьезную ошибку, да еще и несколько лет подряд не замечать ее.

Мне кажется, что для данной задачи (подсчет IGT) нужно использовать такой путь: ставить timestamp в начале заезда, а потом вычитать из текущего времени. Причем арифметических операций стоит избегать, даже над целыми числами. 1/60 секунды это 16,(6) миллисекунд, поэтому в случае целого числа мы будем наивно откидывать 0,(6) при каждом сложении, что приведет к неточностям в подсчете.

В некоем обозримом будущем я постараюсь написать фикс и на другие версии. На этом у меня все, спасибо за внимание.

UPD: Поправил ссылку на гитхаб всвязи с переездом на новое имя.
UPD2: Выкатил вторую часть, интересующиеся могут прочитать.

⇡#Страсти на дороге

Но до того, как мы встретимся с «лучшими гонщиками Фэйрхевена», придется протолкаться сквозь толпу других желающих и свору полицейских. Фантазии разработчиков хватило всего на четыре вида гонок: кольцевой заезд, спринт от точки А до точки Б, отрыв от копов за требуемое время и прохват, в котором необходимо достичь заданной средней скорости. Самым интересным и увлекательным получился последний режим, потому что в остальных впечатление портит поведение компьютерных противников.

Начать с того, что полицейские в этом городе очень, очень упорные, если не сказать — упертые. Их поведение диктуется не только физикой, которой подчиняется и ваша машина, но и безжалостными скриптами, причем последние явно превалируют. Копы появляются чуть ли не прямо за спиной, стремительно покрывают огромные расстояния и вообще ведут себя нагло. Сбежать от них на машинах начального уровня очень проблематично, а когда это все-таки происходит, то благодарить обычно нужно не свои навыки, а удачу.

Вообще, все эти прятки с казенными машинами не имеют особого смысла, поскольку единственным штрафом за поимку является потеря Speed Points, заработанных в ходе организации дорожных беспорядков. За первое место в одной гонке дают столько же очков, сколько за три-четыре длительные погони, — поэтому, если вам надоело стряхивать скриптовых болванчиков с хвоста, лучше остановиться и подставить запястья для наручников. Честное слово, даже в бородатой Hot Pursuit и её сиквеле 2002 года выпуска было интересней тягаться с полицией.

Читать еще:  Need for Speed: Most Wanted "Hot Pursuit soundtrack"

Другие претензии аналогичны тем, которые предъявлялись служителям порядка в Need for Speed: The Run — сверкающий сине-красными огнями Corvette может запросто обойти вашу McLaren MP4-12C, несущуюся на предельной скорости, а затем выбросить шипы прямо перед носом. Такие условности еще можно было бы простить, если бы аналогичные меры применялись и по отношению к кремниевым водилам. Однако создается ощущение, что балаган вращается только вокруг нашей персоны, а все остальные просто работают на подтанцовке.

Вообще, искусственному интеллекту здесь дается большая фора. Стоит только впечатать оппонента в отбойник или удачно познакомить его с едущим навстречу фургоном, как он тут же возрождается у вас за спиной. А вот если на его месте оказываетесь вы, то виртуальный оператор сначала во всех подробностях просмакует аварию, показав ее с нескольких ракурсов, и только потом ваша машина будет снова на дороге. Это особенно заметно во время заездов Most Wanted, где одно такое столкновение обычно чревато перезапуском гонки. При этом все, что могут соперники, — это агрессивно идти на таран, нагло пользуясь своим преимуществом в скорости возрождения. Такие моменты только вызывают раздражение, а не разжигают дух соперничества.

Как EA усложнили нам жизнь, или как мы чинили баг 12-летней давности

Иногда в программы закрадываются баги. Причем закрадываются так, что обнаружить их получается лишь через много-много лет после выпуска, когда чинить их уже нерентабельно. Иногда такие баги оказываются слишком критическими, чтобы их игнорировать. Поэтому под катом я расскажу, как мы устраняли один такой критический баг в одной старенькой гонялке. А заодно наглядно продемонстрирую, чем плох float, какие могут быть последствия и как с этим бороться.

Лирика

Речь пойдет об игре Need for Speed: Most Wanted. Эта игра очень популярна и крайне любима многими геймерами-энтузиастами, и многими считается чуть ли не лучшей в серии. Даже если вы не азартный игрок, то наверняка слышали об этой игре в частности или о серии в целом. Но обо всем по порядку.

Я – спидранер. Прохожу игры на скорость. Довелось мне быть одним из первопроходцев в деле скоростного прохождения гоночных игр, поэтому я заодно являюсь одним из «глобальных модераторов» спидран-коммьюнити серии NFS. Участь со мной разделил чех под ником Ewil.

Почему участь? Потому что в один прекрасный момент к нам в дискорд-сервер пришел человек и заявил, что все наши рекорды трасс неправильны, и что мы – нубы. Скрепя сердце, подавляя багет от, как казалось, необоснованного обвинения и борясь с языковым барьером (английским этот человек владеет на очень плохом уровне), мы начали разбираться, что же не так с нашими рекордами. Из обрывков речи мы поняли, что в игре есть некий «timebug», который делает IGT неправильным. Ewil пересмотрел некоторые записи и руками пересчитал время. Оказалось, нам не врали. На записях IGT резко отличалось от RTA . Это были не пара миллисекунд, которые тоже могу решить исход рекорда, а местами разница доходила даже до нескольких секунд(!).

Мы начали искать причину и последствия этого явления. Еще задолго до этого я в личных интересах пытался «вытащить» из игры IGT. Моя попытка не увенчалась успехом, и я как-то забил на эту идею. Информации в интернете мы найти не смогли, за исключением какой-то английской странички с очень коротким описанием, без какой-либо доказательной базы. Поиск по ютубу также не принес результатов, но были найдены записи, которые гласили «No TimeBug».

Чуть позже я познакомился со SpeedyHeart, и она мне подсказала, что в игре время считается как float. Тут все начало проясняться, и мы медленно переходим от унылого вступления к лютому экшону!

Как это работает

Вооружившись Cheat Engine, OllyDbg, DxWND и NFS: MostWanted версии 1.3, я полез рыться в памяти. Выкопал я примерно вот что (картинка кликабельна):

Нас интересуют последние три адреса. Они хранят в себе IGT для разных ситуаций. Почему они float – одному Блэк Боксу известно… Но так не делают! Float, у него же точность, как у дробовика, а может и того хуже.

Собственно, немного о самих таймерах. Таймеры хранят время в секундах, т. е. целая часть – количество полных секунд. Два из этих таймеров, а именно Global IGT и Race IGT, периодически обнуляются. Для Global IGT это происходит в момент выхода в главное меню, а Race IGT обнуляется при рестарте гонки. Подсчет IGT производится через Global IGT, и в какой-то момент времени ему уже не хватает точности. Из-за этого время считается неправильно.

На этой стадии меня заинтересовали несколько вопросов:

  1. Раз уж есть разница во времени, то отличается ли геймплей с багом и без? Логично предположить, что если IGT ускоряется, то и в целом игра должна становиться «быстрее»
  2. Какие рамки у этого бага? Как он будет себя вести при разных значениях таймера, и как на это будет реагировать игра.

Ответ на вопрос номер 1 был найден крайне быстро. Я просто взял и изменил показания Global IGT на 300000.0 и получил то, что получил. Время ускорилось почти в два раза(!), однако на физике и поведении игры это никак не отразилось. Прикола ради я тыркал и другие таймеры, но они, почему-то, ни на что не влияют. Собственно, если бы с ускорением времени ускорялся и геймплэй, то в нашем мире спидранерства это считается вполне законным. Все таки мы любим баги. Но такой расклад никого не устроил.

Я пошел немного глубже и нашел ответ на вопрос 2. Как только Global IGT достигает отметки в 524288 время в игре полностью останавливается. Это немного смущает игру, и она начинает плохо себя вести делать интересные вещи. Например, не дает начать гонку после рестарта, намертво блокируя игру (выйти из нее можно только через диспетчер задач или Alt+F4). Отрыв/отставание от соперников перестает работать. А если проиграть гонку, то игра отправляет вас в свободное плавание по миру.

Float — ад перфекциониста.

Хоть и не в прямом смысле, но все же. Для наглядной демонстрации я написал небольшую программу, которая поможет мне оценить всю печальность происходящего.

Перед непосредственно кодом, распишу немного про цели. Известно, что игра «блокирует» цикл обновления физики на 120 раз в секунду (опять же, спасибо SpeedyHeart за информацию). Однако vsync обрубает обновление физики еще сильнее, до 60 раз в секунду. Соответственно, мы просто возьмем float-переменную и будем циклически туда добавлять 1/60 секунды. Потом мы посчитаем, за сколько шагов мы добились результата, и за сколько шагов мы должны были добиться этого результата. Также будем делать все циклически для разных случайных величин и считать среднюю погрешность в рассчетах. Любые отклонения в 2 и менее шагов (33мс) мы будем считать незначительными, потому что игра показывает время до сотых секунды.

Меняя значения START_TIME и TEST_TIME мы можем получить необходимую нам статистику. В целом, пока START_TIME не превышает 15 минут, то обычный 2-х минутный заезд окажется «свободным» от бага. Разница остается не критичной в рамках игры, 1-2 кадра:

16 Минут же оказались «критической точкой», когда время беспощадно плывет:

Интересен так же тот момент, что в зависимости от START_TIME будет меняться «сторона» ошибки. Например, после полутора часового непрерывного геймплея время начнет течь медленнее, чем должно:

Поигравшись со значениями еще немного я оценил, что в получившейся программе «время» течет примерно так же, как в игре. Это было подтверждено практически – любые рекорды, записанные «из главного меню» в течение первых 15 минут геймплея были чистыми. При START_TIME близкому к 300000 секунд количество шагов было почти в два раза меньше, чем ожидалось. При START_TIME, большем магической константы 524288 программа переставала работать. Все это подтверждало, что процесс подсчета времени был скопирован верно.

Устраняем нежелательное поведение

Теперь, когда известна проблема и ее поведение, можно ее устранить. Нужно лишь перезаписывать Global IGT всякий раз, когда игрок начинает заезд заново. Это можно узнать довольно просто – в этот момент обнуляется Race IGT. Но тут есть проблема.

Есть два издания игры: NFS: Most Wanted и NFS: Most Wanted Black Edition. Второе издание включает в себя две дополнительные машины и две трассы, да 69-ое испытание. Но, технически, это две совершенно разные игры! Их запускаемые файлы отличаются. Помимо этого, есть патч 1.3… Который отличается для каждого издания. В итоге у нас есть 4 разных версии игры, которые надо поддерживать. Этот факт делает «правильный» путь чрезмерно сложным и неоправданным. По-хорошему, нужно слегка подправить запускаемый файл и обнулять счетчик там, но… Править 4 разных экзешника, которые еще и запакованы, да защищены от отладки… Лучше просто напишем простую программку, которая будет в реалтайме отслеживать состояние таймеров и обнулять их при необходимости. Писать будем на C#.

Вот такую архитектурку я набросал. GameProcess – это вспомогательный класс, который упрощает доступ к чтению-записи памяти процесса. GameHolder – сердце программы. Он будет инициализировать GameProcess, а при «подцепе» процесса будет определять версию игры и создавать необходимый экземпляр наследника Game. Поскольку логика «фикса» не отличается от версии к версии, то ее лучше вынести в один класс.

Как же нам определить версию? Просто – по размеру основного модуля. Я специально реализовал проперти ImageSize. А чтобы не захламлять код магическими константами, запилим enum:

Остальные версии добавим по мере их попадания ко мне в руки.

isUnknown отвечает за тот факт, удалось ли нам определить версию или нет. Из всего класса нам интересен только метод Refresh, вот он:

Логика фикса вышла совсем простенькой:

Дело осталось за малым: реализовать версию, выставив в конструкторе необходиме значения соответствующим protected-переменным. В мэйне же просто кидаем цикл обновления в отдельный трэд и забываем про него. Ах да, из-за особенностей карточек Nvidia и особенностей реализации установщика игр NFS мы будет принимать на вход имя процесса, чтобы была возможность кастомизации.

На этом фикс заканчивается. Компилируем, запускаем и забываем о таймбаге, yay! ^_^ Картинка кликабельна.

На самом деле, никуда этот баг не денется. Если один заезд физически не уложится в рамки 15 минут, то тут уже ничего не поделаешь. Но таких заездов в игре аж один, и тот от полиции.
Полные исходники на гитхабе.

Summary

Вот так один маленький баг нехило подпортил нам жизнь. А ведь его можно было избежать, если бы Black Box использовали в свое время double, но нет. Кстати, это яркий пример того, как «написанное однажды» выливается в кучу неулавливаемых/перекатывающихся багов. Timebug присутствовал в каждой игре от Black Box ever since. В Carbon, ProStreet и даже Undercover. В последнем они поменяли логику подсчета IGT, но эти три таймера там все так же присутствуют, и ошибки округления приводят к странным последствиям. SpeedyHeart обещала сделать видео-обзор всей найденой в процессе информации, так что ждем-с.

Чему меня научила эта ситуация? Не знаю. Я и так понимал, что использовать float для серьезных вычислений – идея сомнительная. Но теперь я лучше представляю, как именно все это будет работать на практике. Однако забавно получилось, что такая серьезная компания могла допустить такую серьезную ошибку, да еще и несколько лет подряд не замечать ее.

Мне кажется, что для данной задачи (подсчет IGT) нужно использовать такой путь: ставить timestamp в начале заезда, а потом вычитать из текущего времени. Причем арифметических операций стоит избегать, даже над целыми числами. 1/60 секунды это 16,(6) миллисекунд, поэтому в случае целого числа мы будем наивно откидывать 0,(6) при каждом сложении, что приведет к неточностям в подсчете.

В некоем обозримом будущем я постараюсь написать фикс и на другие версии. На этом у меня все, спасибо за внимание.

UPD: Поправил ссылку на гитхаб всвязи с переездом на новое имя.
UPD2: Выкатил вторую часть, интересующиеся могут прочитать.

Ссылка на основную публикацию
Статьи c упоминанием слов:
Adblock
detector