- PVSM.RU - https://www.pvsm.ru -
В известной игре «Казаки: Снова Война» [1] присутствует баг, сводящий удовольствие от сетевой игры к нулю: Нечеловеческая скорость игрового процесса на современных компьютерах. При этом изменение скорости игры в настройках, прекрасно работающее в режиме одиночной игры, никак не влияет на происходящее в игре по сети. Этот вопрос обсуждается на множестве форумов, но самые популярные советы это:
Первые два варианта приводят к тому, что игра идёт медленно, но с рывками. Качество звука при этом тоже падает. Третий вариант вообще без комментариев.
Для начала поищем значение настройки скорости с помощью Cheat Engine [2]. Это должен быть либо некий мультипликатор, прямо пропорциональный положению ползунка в меню настроек, либо обратно пропорциональный ему интервал. Довольно быстро находится вот эта ячейка памяти:
Этот интервал равен 0 при максимальной скорости, а комфортная для меня скорость игры соответствует интервалу 20. Изменение значения во время игры тут же отражается на скорости игрового процесса. Хорошо, посмотрим, что там у нас в сетевой игре. Загружаемся, меняем значение в Cheat Engine, и… ничего. Меняется только положение ползунка в настройках. Ладно, посмотрим где этот интервал обрабатывается. Cheat Engine показывает лишь два соседних адреса, на которых происходит чтение ячейки:
Посмотрим на листинг встроенного дизассемблера:
Навскидку можно сказать, что интервал удваивается и сравнивается с чем-то, и если это что-то меньше удвоенного интервала, то совершается переход куда-то назад. Отлично, запускаем всеми любимую для таких дел татарскую программу и видим такую картину в самом конце одной длинной функции:
После некоторого копания выясняем следующее:
Ставим точку останова где-нибудь в субпроцедуре сравнения с интервалом. Запускаем одиночную игру и сразу же вылетаем в отладчик. Запускаем игру по сети — точка останова не срабатывает. При этом если поставить точку останова в самом начале функции, то она срабатывает всегда. Выходит, что именно в многопользовательской игре сознательно не выполняется проверка интервала. Дело осталось за малым: Найти ответственное за это разветвление в функции и изменить его так, чтобы всегда выполнялась ветвь с нужной нам субпроцедурой 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.
Итак, что нам нужно сделать:
С первым пунктом всё понятно: Операционный код короткого прыжка это EB, а нужное нам смещение определяется вычитанием адреса следующего за инструкцией прыжка байта из адреса начала субпроцедуры loc_4D1A9C.
С третьим пунктом ещё проще: Заменяем инструкцию прыжка двумя nop'ами.
Второй же пункт требует вычисления смещения для короткого прыжка назад. К счастью я наткнулся на статью [3], понятно описывающую алгоритм этого действия, а именно: Вычесть адрес назначения из адреса следующего за инструкцией прыжка байта, затем вычесть 1h, после этого перевести число в бинарный вид (в размере байта), инвертировать и снова перевести в шестнадцатеричную систему. Полученное число и есть нужное нам смещение.
Ну что ж, господа. Патчим!
Открываем изменённый файл в всё той же всеми любимой программе и видим следующую картину:
Блок, содержащий вызов ProcessMessages никогда не будет исполнен. После выключенной нами проверки на многопользовательскую игру в субпроцедуре loc_4D1A9C управление переходит в нашу субпроцедуру с вызовом GetTickCount и сравнением с интервалом. Если разница меньше интервала, то субпроцедура прыгает обратно в начало самой себя до тех пор, пока интервал не будет соблюдён.
Теперь игра ведёт себя так, как надо. Скорость игры соответствует наименьшей скорости среди игроков, синхронизация не нарушается. Скорость скроллинга тоже поддаётся настройке.
Так как это мой первый опыт реверс-инжиниринга и работы с ассемблером, то скорее всего это не самое элегантное решение. Корень проблемы кроется в использовании функций QueryPerformanceFrequency и QueryPerformanceCounter, на которых основывается тайминг игры. Эти функции вызываются один раз при создании новой игры, задавая тон для всех последующих вычислений с GetTickCount. К сожалению, у меня не получилось повлиять на этот участок программы должным образом.
Спасибо, ID_Daemon [5]!
Автор: Ereb
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/asm/112018
Ссылки в тексте:
[1] «Казаки: Снова Война»: https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D0%B7%D0%B0%D0%BA%D0%B8:_%D0%A1%D0%BD%D0%BE%D0%B2%D0%B0_%D0%B2%D0%BE%D0%B9%D0%BD%D0%B0
[2] Cheat Engine: http://www.cheatengine.org/
[3] статью: http://thestarman.pcministry.com/asm/2bytejumps.htm#REV
[4] Статья на Хабре: https://habrahabr.ru/post/266385/
[5] ID_Daemon: https://habrahabr.ru/users/id_daemon/
[6] Источник: https://habrahabr.ru/post/277067/
Нажмите здесь для печати.