Эта короткая история описывает одну из работ, проведенную в рамках проекта «Equilibris» — неофициального мода для игры «Heroes of Might and Magic IV». С точки зрения как реверс-инжиниринга, так и патчинга она не представляет особого интереса — несколько забавным оказался только лишь финал.
Как известно, в данной серии игр в каждой таверне игрок может нанимать лишь одного нового Героя в неделю. Однако…
Описание бага: Если во внешней таверне не было найма, то, начиная с 8-го дня, можно купить двух героев в течение двух дней.
Для работы используется дизассемблированный файл heroes4.exe из последнего официального аддона «Winds of War». Процедура работы таверны найдена командой ранее и расположена по адресу 4705E0. Из всего алгоритма ее работы меня интересует место, в котором определяется, можно ли в данный момент нанять в таверне Героя, либо необходимо ожидание. В игре это проявляется выводом соответствующего сообщения:
С программной точки зрения это новое окно, которое в игре создается с помощью функции NewWindowCreate (720C80) (распознанным в дизассемблере функциям даны собственные имена). В процедуре таверны несколько вызовов этой функции, и первым претендентом является вызов по адресу 470823. С помощью отладчика убеждаюсь, что, действительно, этот вызов создает искомое диалоговое окно. Код, управляющий этим вызовом NewWindowCreate, находится выше — по адресу 470645:
00470638 call HeroesPricesInTavern_Lost
0047063D mov al, [ebp+48h] // 0 – если таверна работает; 1 – если героя нанять нельзя (ждешь 7 дней).
00470640 add esp, 8
00470643 test al, al
00470645 jz loc_470866 // Если таверна работает, пропустить вывод сообщения по адресу 470823
Покупаю в таверне Героя, затем устанавливаю «бряк» на запись на ячейку, адресуемую [ebp+48h], после чего жду 7 игровых дней. Когда таверна «освобождается», отладчик всплывает по адресу 470DFF. Давайте посмотрим окружающий код:
00470DF0 TavernCountDays proc near
00470DF0 mov dl, [ecx+48h] // ECX+48h – флаг работы таверны:
DL=0 – если таверна работает;
DL=1 – если Героя нанять нельзя (ждешь 7 дней)
00470DF3 xor eax, eax
00470DF5 cmp dl, al
00470DF7 jz short loc_470E06
00470DF9 cmp dword ptr [ecx+4Ch], 7 // В [ECX+4Ch] - число дней с момента найма последнего героя в таверне. Если меньше 7 – выходим.
00470DFD jl short loc_470E06
00470DFF mov [ecx+48h], al // Таверна работает (AL=0)
00470E02 mov [ecx+4Ch], eax // Обнулить число дней
00470E05 retn
00470E06
00470E06 loc_470E06:
00470E06
00470E06 inc dword ptr [ecx+4Ch] // Увеличить число дней с момента найма последнего Героя в таверне
00470E09 retn
00470E09 TavernCountDays endp
Эта небольшая процедура служит для проверки числа дней, в которые таверна закрыта для найма. Замечу, что она вызывается для каждой таверны на карте в каждый игровой день. Что же порождает баг? Программа зачем-то продолжает вести подсчет числа дней, в течении которых в таверне не было найма Героя и по истечении недели, в которую таверна была закрыта (см. счетчик по адресу 470E06). В результате получаем следующую картину. Пусть первый найм Героя происходит только на восьмой игровой день. На входе в процедуру значение флага доступности таверны по адресу [ecx+48h] будет равно «1» (таверна закрыта), а значение счетчика дней по адресу [ecx+4Ch] будет равно «8». Однако при этом, после сравнения по адресу 470DF9, управление получит код по адресу 470DFF, вновь открывающий таверну для найма! При этом счетчик дней сбросится, и после найма второго Героя алгоритм уже отработает, как задумывали авторы. Но через две игровых недели весь цикл повторится.
Самый простой способ пофиксить баг – отказаться от подобного подсчета дней. Пусть счетчик работает только тогда, когда таверна закрыта (что логичнее), а в остальное время зададим его равным нулю. Это достигается очень просто — изменением перехода по адресу 00470DF7 в конец функции:
00470DF5 cmp dl, al
00470DF7 jz short loc_470E09
Теперь остается лишь пропатчить имеющийся код. Для этого смотрим исходный
и измененный
варианты.
Как видно, необходимого результата можно достичь, заменив 0D на 10 по адресу 470DF8. Классика жанра: пропатчить баг, заменив всего лишь один байт!
Автор: Максим