Реверс-инжиниринг и замедление «Казаков»



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

  1. Искусственно загрузить ядро процессора, на котором запущена игра
  2. Запускать игру в виртуальной машине с ограниченными ресурсами
  3. Играть не по локальной сети, а по интернету — там задержки побольше

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

Приступаем


Для начала поищем значение настройки скорости с помощью Cheat Engine. Это должен быть либо некий мультипликатор, прямо пропорциональный положению ползунка в меню настроек, либо обратно пропорциональный ему интервал. Довольно быстро находится вот эта ячейка памяти:



Этот интервал равен 0 при максимальной скорости, а комфортная для меня скорость игры соответствует интервалу 20. Изменение значения во время игры тут же отражается на скорости игрового процесса. Хорошо, посмотрим, что там у нас в сетевой игре. Загружаемся, меняем значение в Cheat Engine, и… ничего. Меняется только положение ползунка в настройках. Ладно, посмотрим где этот интервал обрабатывается. Cheat Engine показывает лишь два соседних адреса, на которых происходит чтение ячейки:



Посмотрим на листинг встроенного дизассемблера:



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

После некоторого копания выясняем следующее:

  • dword_718410 — величина интервала согласно настройкам скорости
  • sub_54C1BE — процедура, выполняющая GetTickCount
  • dword_83B1A4 — результат предыдущего GetTickCount
  • loc_4D1ABF — субпроцедура, сравнивающая разницу двух результатов GetTickCount и прыгающая назад, если разница меньше интервала

Ищем ошибку


Ставим точку останова где-нибудь в субпроцедуре сравнения с интервалом. Запускаем одиночную игру и сразу же вылетаем в отладчик. Запускаем игру по сети — точка останова не срабатывает. При этом если поставить точку останова в самом начале функции, то она срабатывает всегда. Выходит, что именно в многопользовательской игре сознательно не выполняется проверка интервала. Дело осталось за малым: Найти ответственное за это разветвление в функции и изменить его так, чтобы всегда выполнялась ветвь с нужной нам субпроцедурой loc_4D1ABF.

Будем идти снизу вверх. Для начала поставим точку останова в субпроцедуре loc_4D1A9C. Бинго! В одиночной игре переменная word_611B60 всегда равна 1, так что условие прыжка не выполняется и управление передаётся сначала в loc_4D1AAA, а оттуда уже нашей субпроцедуре. При игре по сети переменная word_611B60 всегда равна 2, что приводит к прыжку вперёд сразу к loc_4D1AE6 и к концу функции. Чтобы заставить игру всегда передавать управление на ветку с субпроцедурой loc_4D1ABF достаточно заменить инструкцию сравнения cmp edx, 2 на cmp edx, 3. Как два байта об асфальт!


Не всё так просто


Теперь настройка скорости работает и в сетевой игре. К сожалению, без ложки дёгтя не обошлось: Со временем у одного из игроков начинает сильно увеличиваться скорость скроллинга и темп анимации воды. Через какое-то время эффект пропадает и появляется у другого игрока. При этом скорость остальных игровых процессов у обоих одинакова и соответствует установленной в настройках.

Причину такого поведения выяснить мне не удалось, но возникло сильное подозрение на субпроцедуру loc_4D1AAA, вызывающую ProcessMessages. По всей видимости для игры по сети этот вызов не был нужен. Возможно именно из-за выше описанного странного поведения был сделан обход этой субпроцедуры? Во всяком случае попробуем исключить её из ветки, оставив только полезную нам субпроцедуру loc_4D1ABF.

Достаём калькулятор


Итак, что нам нужно сделать:

  1. Заменить прыжок с условием в субпроцедуре loc_4D1A9C на короткий прямой прыжок в субпроцедуру loc_4D1ABF
  2. Изменить смещение обратного прыжка в субпроцедуре loc_4D1ABF, чтобы замкнуть её на себя и не попадать в loc_4D1AAA
  3. Убрать вторую инструкцию прыжка к loc_4D1AAA, находящуюся в блоке сразу после субпроцедуры loc_4D1ABF

С первым пунктом всё понятно: Операционный код короткого прыжка это EB, а нужное нам смещение определяется вычитанием адреса следующего за инструкцией прыжка байта из адреса начала субпроцедуры loc_4D1A9C.

С третьим пунктом ещё проще: Заменяем инструкцию прыжка двумя nop'ами.

Второй же пункт требует вычисления смещения для короткого прыжка назад. К счастью я наткнулся на статью, понятно описывающую алгоритм этого действия, а именно: Вычесть адрес назначения из адреса следующего за инструкцией прыжка байта, затем вычесть 1h, после этого перевести число в бинарный вид (в размере байта), инвертировать и снова перевести в шестнадцатеричную систему. Полученное число и есть нужное нам смещение.

Ну что ж, господа. Патчим!





Результат


Открываем изменённый файл в всё той же всеми любимой программе и видим следующую картину:



Блок, содержащий вызов ProcessMessages никогда не будет исполнен. После выключенной нами проверки на многопользовательскую игру в субпроцедуре loc_4D1A9C управление переходит в нашу субпроцедуру с вызовом GetTickCount и сравнением с интервалом. Если разница меньше интервала, то субпроцедура прыгает обратно в начало самой себя до тех пор, пока интервал не будет соблюдён.

Теперь игра ведёт себя так, как надо. Скорость игры соответствует наименьшей скорости среди игроков, синхронизация не нарушается. Скорость скроллинга тоже поддаётся настройке.

Послесловие


Так как это мой первый опыт реверс-инжиниринга и работы с ассемблером, то скорее всего это не самое элегантное решение. Корень проблемы кроется в использовании функций QueryPerformanceFrequency и QueryPerformanceCounter, на которых основывается тайминг игры. Эти функции вызываются один раз при создании новой игры, задавая тон для всех последующих вычислений с GetTickCount. К сожалению, у меня не получилось повлиять на этот участок программы должным образом, потому что я был слишком ленив, а вокруг вызова выше упомянутых функций творилась какая-то странная арифметика.

UPD: При внимательном анализе быстро становится ясно, что функции QPF и QPC используются не по назначению. Их результаты суммируются, а QPC впоследствии больше нигде не вызывается. Переменная, хранящая результат, впоследствии используется перед вызовом функций, работающих со строками. Так что QPF и QPC здесь используются скорее всего лишь как ГПСЧ в процессе создания случайной карты и/или имени файла карты.

Ссылки
  • Статья на Хабре про реверс-инжиниринг игры Бэтмен, которая подтолкнула меня к действию и дала пару идей
    Спасибо, ID_Daemon!
  • Статья про вычисление смещений для коротких прыжков в ассемблере



UPD: По просьбе VRV была также пропатчена версия «Казаков», доступная через Steam. Созданное им обсуждение находится на форуме LCN.

Патчер для различных версий исполняемого файла «Казаков» доступен по ссылке.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 29

    +5
    Ждем выхода новых казаков, осталось совсем немного. )
      +1
      Судя по сообщениям на форуме не всё так оптимистично с датой релиза.
        +1
        Ну я всё еще оптимистично настроен )
      +7
      Ну нельзя же так, что за всеми любимая татарская программа?)
        +12
        IDA Pro, основным разработчиком которой является на данный момент широко известный в узких кругах татарин — Ильфак Гильфанов.
        +4
        Проще было заNOPать всю loc_4D1A9C и оставить логику как есть, чтобы не мучиться со смещениями :)
        За статью — плюсик, всегда интересно подобное читать.
          +4
          Спасибо. Да, было бы проще, но тогда всё равно будет выполнена loc_4D1AAA вместе с вызовом ProcessMessages, который приводит к багам во время сетевой игры.
            +1
            Конечно же loc_4D1AAA нужно занопать. Имел в виду ее, а написал другое.
            Еще поясните вот этот момент:
            Корень проблемы кроется в использовании функций QueryPerformanceFrequency и QueryPerformanceCounter, на которых основывается тайминг игры. Эти функции вызываются один раз при создании новой игры, задавая тон для всех последующих вычислений с GetTickCount.

            Насколько я знаю, QueryPerformanceFrequency должна запускать единожды, при инициализации приложения, а QueryPerformanceCounter — в каждой итерации игрового цикла. Как это они связаны с GetTickCount?
              +2
              К сожалению, я не имею опыта в разработке игр и не в курсе всех деталей тайминга игрового цикла. Однако про QueryPerformanceFrequency на одном форуме (нем.) писали, что на современных процессорах возвращаемое им значение может быть меньше настоящей частоты.

              Я ставил брейк на вызов QPF и пытался как-то манипулировать значения, но там всё проходит через указатели и методом тыка я там уже не смог ничего решить)

              Xref по QPC показывает только два вызова — один находится рядом с QPF и проходится единожды при открытии новой игры. На счёт второго я не углублялся, возможно он в игровом цикле.

              При этом xref по GetTickCount выдаёт целую простыню вызовов и многие из них в игровом цикле. Как именно это всё взаимодействует мне тоже интересно узнать…
                +3
                К сожалению, я не имею опыта в разработке игр и не в курсе всех деталей тайминга игрового цикла.

                www.koonsolo.com/news/dewitters-gameloop
                  +1
                  Спасибо за ссылку. Однако все примеры таймеров, приведённые в указанной статье используют только GetTickCount. О взаимодействии с QPF и QPC там не упоминается. Интересно, этот метод вообще ещё используется или он уже «вышел из моды»?

                  Согласно документации Microsoft частота, возвращаемая QPF определяется при старте операционной системы и больше не меняется. Но в этом обсуждении упоминаются возможные ошибки имплементации QPF на некоторых ситемах и даётся такой совет:
                  Как правило, timeGetTime лучше всего подходит для тайминга игр — у GetTickCount недостаточно высокое разрешение, а от QPC и RDTSC одни неприятности.
                  Было бы интересно услышать мнение опытного разработчика игр по этому вопросу.
                    0
                    Есть догадка — что на процессорах с изменяющейся частотой(Turbo boost) и получается данный эффект, т.к. QPF возвращает «пониженную» частоту, а потом при нагрузке она возрастает и скорость плывет.
                      0
                      Это не так — QPF всегда возращает фиксированную частоту, не зависящую от частоты процессора. См. мой ответ ниже — по последней ссылке как раз проверялось что QPF не зависит от частоты процессора.
                      +1
                      Недавно копался в игре Deadly Premonition, так там в качестве таймера используется как раз QueryPerformanceCounter. Игра была портирована на ПК не так давно: в 2013 году, так что такой подход тоже имеет право на жизнь. Проблем со скоростью нет, по крайней мере жалоб не встречал.
                    +1
                    зачем ставить бряки, когда можно подсунуть свои QPF/QPC через хуки?
                      +2
                      Не оказался бы от интересных ссылок по теме
                      +2
                      QueryPerfomanceFrequency возвращает не частоту процессора, а частоту таймера который сама ОС выбрала при загрузке (таймеров в архитектуре PC довольно много). Таким образом частота действительно остаётся фиксированной в течении работы ОС, а результат QueryPerfomanceQuery монотонно увеличивающимся.

                      Вот немного ссылок на эту тему:
                      habrahabr.ru/company/intel/blog/260113 — рассказывается о таймерах на PC
                      www.gamedev.ru/code/forum/?id=39166#m4 — очень старая тема, но мне кажется довольно понятно объясняет как работают обе функции
                      www.gamedev.ru/code/forum/?id=142497&page=2#m16 — тоже старая тема, там я выкладывал тестовый код который доказывает что QFP и QFC не зависят от частоты процессора
                +2
                Научите как играть по сети на современных системах, как создать, чтобы тебя увидели, а потом не было разрывов? Кроме скорости, помню были проблемы, что тупо всё дёргалось, и не было плавности.
                  0
                  Сергей Григорович в одной беседе мне рассказывал как они лазили в Windows Kernel, чтобы дбится перформанса… а тут такие речи — как замедлить! (куда мир катится?)
                    +1
                    Я думал настоящими мужики до сих пор используют hiew для патчинга, тем более там встроенный дизассемблер есть.
                      +1
                      А я не привередливый — что под рукой было, тем и патчил)
                      +4
                      Странно, что интерес к игре не пропал после всех приключений.
                        +5
                        «Зачем врач-гинеколог женится?»
                          0
                          А он мужчина или женщина?
                            +1
                            Женившийся гинеколог? Он, соответственно, мужчина.
                            (Не думал, что это настолько обсуждаемая тема — 492 тыс. ответов).
                        +1
                        Автор статьи, Эреб, решил поистине важную задачу и помог нашему сообществу преодолеть проблему со скоростью в игре, которая вела себя неадекватно на современном железе. Благодаря патчу, который он сделал лично для меня, после переписки в ЛС, для последней версии казаков 1,35, выложенных на стиме, мне стало гораздо комфортнее играть!

                        Была создана тема на форуме лиги казаков — http://forum.newlcn.com/viewtopic.php?p=444915#444915

                        Еще раз, спасибо, Эреб.
                          0
                          Пожалуйста, Владимир. Приятно видеть, что результат работы интересен и полезен сообществу.
                          0
                          Люди, а как решить проблему "высасывания" дерева или камня построенными частоколом и стеной?

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое