Heroes of Might and Magic IV: tavern bug or patching classic

This short story describes one of the projects carried out by the Equilibris project - an unofficial mod for the game Heroes of Might and Magic IV. From the point of view of both reverse engineering and patching, it is not of particular interest - only the finale turned out to be somewhat funny.



image


As you know, in this series of games in each tavern, the player can only hire one new Hero per week. But…



Bug description: If there was no recruitment in the external tavern, then, starting from the 8th day, you can buy two heroes within two days.



For work, the disassembled file heroes4.exe from the latest official add-on "Winds of War" is used. The tavern operation procedure was found by the team earlier and is located at the address 4705E0. From the whole algorithm of her work, I am interested in the place in which it is determined whether it is possible to hire a Hero in the tavern at the moment, or if waiting is necessary. In the game, this is manifested by the output of the corresponding message:





From a programmatic point of view, this is a new window that is created in the game using the NewWindowCreate (720C80) function (the functions recognized in the disassembler are given their own names). There are several calls to this function in the procedure of the tavern, and the first challenger is a call to the address 470823. With the help of the debugger, I make sure that, in fact, this call creates the desired dialog box. The code that controls this call to NewWindowCreate is above at 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


I buy in the Hero's tavern, then set the "breakpoint" to write to the cell addressed to [ebp + 48h], after which I wait for 7 in-game days. When the tavern is "emptied", the debugger pops up at address 470DFF. Let's see the surrounding code:



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


This small procedure is used to check the number of days the tavern is closed for hire. Note that it is called for every tavern on the map every game day. What is causing the bug? For some reason, the program continues to count the number of days during which there was no Hero hiring in the tavern and after the week in which the tavern was closed (see the counter at 470E06). As a result, we get the following picture. Let the first recruitment of the Hero take place only on the eighth game day. At the entrance to the procedure, the value of the tavern availability flag at [ecx + 48h] will be "1" (the tavern is closed), and the value of the days counter at [ecx + 4Ch] will be "8". However, after the comparison at 470DF9, the control will receive a code at 470DFF, which reopens the tavern for hire! This will reset the day counter.and after hiring the second Hero, the algorithm will work out as the authors intended. But after two in-game weeks, the whole cycle will repeat itself.



The easiest way to fix the bug is to skip counting the days. Let the counter work only when the tavern is closed (which is more logical), and the rest of the time, set it to zero. This is achieved very simply - by changing the transition at address 00470DF7 to the end of the function:



00470DF5                 cmp     dl, al
00470DF7                 jz      short loc_470E09


Now all that remains is to patch the existing code. To do this, look at the original





and modified





options.



As you can see, the desired result can be achieved by replacing 0D with 10 at address 470DF8. A classic of the genre: patch a bug by replacing just one byte!



All Articles