I'm curious what kind of computer wizardry is required to do stuff like that.
In this case, it was a matter of unpacking and tearing open the executable file with a disassembler (IDA, in this case).
I looked for the file write functions (_fwrite, in this case) and looked at what segments of code were calling that. I saw a piece of code that was going through a data structure that was the size of the saved game, did a rough transcription of what it was doing in LINQPad (basically a nice way of running .NET code without creating a new project) and voila!
Here's an explanation of the checksum code:
mov al, [si-4780h] - Retrieves a byte and stores it into the al register.
add [bp+checksum1], al - Adds the retrieved value to the first byte of the checksum.
mov al, [bp+checksum1] - Moves the first byte of the checksum into the al register.
cbw - Expands al into a word and stores it into the ax register.
push ax - Pushes the ax register onto the stack.
mov al, [bp+checksum1] - Moves the first byte of the checksum into the al register.
cbw - Expands al into a word and stores it into the ax register.
mov dx, ax - Loads the contents of the ax register into the dx register.
pop ax - Pops the top value off of the stack and stores it in the ax register.
imul dx - Multiplies the value in the ax register by dx and stores the result in the ax register.
mov [bp+var_2], al - Stores the contents of the al register into a temporary variable in memory.
mov al, [bp+checksum1] - Loads the al register with the first byte of the checksum.
sub al, [bp+var_2] - Subtracts the contents of the al register with the contents of the temporary variable.
mov dl, [bp+checksum2] - Pushes the second byte of the checksum into the dl register.
add dl, al - Adds the value in the al register to the value in the dl register, then stores the output to the dl register.
mov [bp+checksum2], dl - Stores the dl register to the memory location holding the second checksum byte.
Here's a couple of core concepts:
Registers: These are tiny bits of memory within the processor. Put simply, whenever a mathematical operation is done, the numbers are typically loaded into registers, calculated, and the resulting output is moved from a register back into memory. While you can't store a whole lot of data in registers, because they're located in the processor core, reading and writing to them is extremely fast.
The two registers in the above code are ax and dx. These can hold 16-bit (0 to 65535) values. The upper and lower bytes of these registers (8-bits each, or 0 to 255) can be addressed individually using ah/dh and al/dl, respectively.
Stack: This is a section of memory set aside for some processor operations. The name is apt, as it simulates having a stack of objects. You can place (push) an arbitrary number of objects on the stack and remove (pop) them in the reverse order that you put them on (you can technically bypass this rule, but that's beyond the scope of this post). This is known as LIFO, or last-in, first-out.
There is one important bit to keep in mind. First, as I mentioned, the al register holds values from 0-255. What happens, then, when the value exceeds that? There's no checks to make sure that it never exceeds 255, after all. Basically, the value wraps around. If you take 255+1, you'll get 0. The same wrap-around can happen when we hit the subtraction a bit later on. The same thing occurs with the multiplication instruction (imul) as well.
There is one other noteworthy thing. It looks like the cbw instruction isn't necessary. After all, couldn't you just push al and be done with it? Unfortunately, no. The 8086 processor can only push 16-bit words, so you have to push ax. The game was written in C and presumably uses chars to hold the checksum values, and cbw is the cleanest way to preserve the sign prior to pushing the value to the stack (if you want to learn more about how signed values--i.e. ones that can either be positive or negative--are stored, look up
two's complement).
So...yeah! That's how the saved game checksumming in Dungeons of the Unforgiven works. Woo.
Edit: Here's the C# equivalent of the above assembly code (assuming that cs0 and cs1 are bytes and i is one byte of file input):
cs0 += i;
cs1 += (byte)(cs0 - cs0 * cs0);
It's funny how verbose things appear to be when you dive down to a much lower level.
As of recent I've been more interested in learning programming for fun, but apparently I'm dumb enough to find even installing python to be challenging, so that's where I'm at right now, lol.
A lot of those languages tend to have fairly unintuitive Windows installers, honestly. Some of them (and, if I remember correctly, Python is in this camp) are fairly easy to install, but actually being able to use the interpreter is a different story, since it doesn't usually set paths and stuff up for you.
One thing you may want to look into is
Microsoft Visual Studio. I seem to remember it supporting Python fairly well. One caveat is that VS is heavily project-based, so you won't be able to just pop open a script and go, but it's easily one of the best programming suites out there.
reverse engineering is still beyond me. best i ever accomplished was bypassing a cd key in an obscure game, but there was a secondary copy protection involving missing content causing crashes lol. all i did was figure out the correct change to the jump instruction to bypass the check window entirely.
That's basically how I "fixed" Milk Chan's old O2Jam server. The server software that he was using had a bunch of highly annoying nag screens that would pop up, so I used OllyDbg to locate the offending segments of code and disabled them.
I don't remember if I jmp'd past them or replaced the calls with nop's. It's been a while.