Search This Blog

Tuesday 4 October 2016

A quick "fix" for some of the Might & Magic games (6-8)

I've always been a huge RPG fan. I've played dozens of such games and one of my all time favourites is the Might & Magic series. If you haven't played these yet then stop reading, buy yourself a copy for a few pennies (GOG.com) and give them a try! You won't be disappointed.
Anyway, there's a small flaw in the games 6-8 that has kept me bugging for some time now. There are many spells in the game, some of which are offensive and some defensive. In the latter category, there's one spell (Haste - Fire Magic) that can be really annoying as it puts your characters to 'weak' condition as it wears off. All other defensive spells lack such negative effects, but for some reason Haste was designed this way. As the spell's duration is relatively small, I found myself checking the remaining time every 5 steps, that really 'killed' the gameplay in some sense. Then of course in the worst moment I forgot to check and recast the spell, that resulted in 4 weak characters who could barely inflict any damage.

So I decided to put an end to this annoyance and resorted to WinDbg once again.

The idea was to find & eliminate the code that sets the weak status when the Haste spell wears off. I used MM6. Okay, first of all, I had to find where (and how) in memory the weak status was stored for my characters. As I already had some experience with the structure of saved games, I was not really surprised to see the same layout in memory, when I searched for the 1st and 2nd character's name (Tanis, Cattie):
0:000> !address -c:"s -a %1 %2 Tanis"
00908f35  54 61 6e 69 73 00 6f 00-00 00 00 00 00 00 00 00  Tanis.o.........

0:000> !address -c:"s -a %1 %2 Cattie"

0090a551  43 61 74 74 69 65 00 00-00 00 00 00 00 00 00 00  Cattie..........

Basically, all character stats can be found right after the memory address of their names. 5660 bytes of data is used to represent everything for each character, including stats, skills, spells, inventory and of course conditions. This number comes from the difference of the two addresses (0090a551-00908f35).
Note: the byte preceding the character name is also part of the character descriptor structure, thus the -1 offset below.

To find the address holding the weak condition for Tanis, I saved those 5660 bytes to disk for later comparison.

0:000> .writemem d:\file1.bin 00908f35-1 0090a551-2

Then I cast Haste in the game and quickly spent 2 hours by resting to enforce the evil weak condition.

0:000> .writemem d:\file2.bin 00908f35-1 0090a551-2

After that, a compare tool (I used Total Commander's built-in tool) revealed some differences. 

Note: WinDbg's ".holdmem" command can perform such comparisons, however its output is not verbose enough.


The first diff (4 consecutive bytes) at offset 0x1388 was interesting. Playing around with these values revealed it was a timestamp to indicate when exactly the effect was put in place. Four 0s simply mean you're not 'weak'. Other conditions like 'drunk', 'cursed' etc. are represented equally but at different offsets. So it was time to define a breakpoint for this address to figure out what code alters it when the spell goes off. Just before that, I reloaded my savegame. 
0:000> ba w4 00908f35-1+1388

When the breakpoint was hit,  I checked the surrounding assembly code. 
0:000> u eip-20 L30
MM6+0x889b0:
004889b0 742a            je      MM6+0x889dc (004889dc)
004889b2 399e8c120000    cmp     dword ptr [esi+128Ch],ebx
004889b8 7f22            jg      MM6+0x889dc (004889dc)
004889ba 7c08            jl      MM6+0x889c4 (004889c4)
004889bc 399e88120000    cmp     dword ptr [esi+1288h],ebx
004889c2 7718            ja      MM6+0x889dc (004889dc)
004889c4 8b0d088d9000    mov     ecx,dword ptr [MM6+0x508d08 (00908d08)]
004889ca 898e88130000    mov     dword ptr [esi+1388h],ecx
004889d0 8b150c8d9000    mov     edx,dword ptr [MM6+0x508d0c (00908d0c)]
004889d6 89968c130000    mov     dword ptr [esi+138Ch],edx
004889dc 8b44241c        mov     eax,dword ptr [esp+1Ch]
004889e0 83c004          add     eax,4

<...>

Gotcha'! See the 0x1388 offset up there? I had to circumvent this move operation. For that, I had to replace the opcodes of the move instruction with an equal number of 0x90 values. In which file you might ask. The 'k' command gave me that answer.

0:000> k
 # ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
00 0018fc1c 004879d6 MM6+0x889d0
01 00000000 00000000 MM6+0x879d6


So it was the main game executable, "mm6.exe". I exited the game and changed the bytes with my favourite hex editor.


Now it was time for some testing as I wanted to make sure that all other conditions were still affecting my characters, not to mention that I should be able to get into weak condition in other circumstances, like when waiting for days without regular resting. I was lucky, as there seemed to be no side effects at all. Also, the fix worked for all characters.

From that point on, I can't even express how cool it was to beat the game. ;-) Once again, WinDbg saved the day! Yaaaay!

The same approach can be used to "fix" MM7 & MM8. But I leave that to you as an exercise.