Эта короткая история описывает одну из работ, проведенную в рамках проекта «Equilibris» — неофициального мода для игры «Heroes of Might and Magic IV». С точки зрения как реверс-инжиниринга, так и патчинга она не представляет особого интереса — несколько забавным оказался только лишь финал.

image

Как известно, в данной серии игр в каждой таверне игрок может нанимать лишь одного нового Героя в неделю. Однако…

Описание бага: Если во внешней таверне не было найма, то, начиная с 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. Классика жанра: пропатчить баг, заменив всего лишь один байт!