As I’ve mentioned in a previous post, I’ve been working on a fan translation for a Sega Saturn game called Magic School Lunar! As I get closer to the end of the process I’ve been doing more playtesting, but since I’m only testing the script at this point I found myself wanting to cut out the parts of the game I don’t really need to be testing right now. This game has an infamously high random battle rate; at least half my time playing the game is just going through battles1, and there’s no reason to waste my time with any of that when I just want to check the script in-game.
I already know how to turn the encounter rate off, but that wouldn’t solve boss battles. I’d need to be able to fight those for real, and since I’d be skipping all of the random battles I’d be going into each boss with underpowered level 1 characters.
There are probably other ways to have tackled this, but I decided to go about it by working out the game’s save file format so that I could just start the game giving every character maxed out stats. Being able to edit the save files further down the line might turn out to be handy.
The Saturn, like other CD-based systems, uses a memory card2 format with multiple saves per memory card, so I started by extracting the individual game save3 as its own file so that I didn’t need to worry about running into anything unrelated from other saves on the same cards. I made a save game right at the start of the game, noted down my party’s stats, then started digging in the file to see if I could find them.
…but I didn’t, which surprised me. I started out looking for key stats like attack and defense, but none of the primary stats I wanted seemed to be present in the save game except HP and MP. I was a little puzzled, but as I tried searching for other stats I realized what was happening: the only value I could find was the character’s total experience points… and that was also the only thing I needed to find.
Magic School Lunar! has no way for your character to raise their stats outside of gaining levels, and no way to gain levels outside of gaining experience. It wasn’t storing the rest of those stats because it didn’t need to: given just your current experience pool, the game can calculate what level you’re at by checking where that puts you on the level curve, and then look up what your stats should be at that level. I wasn’t finding the rest of those stats since they never really exist outside of memory!
I’d been hoping to just put all my stats up to the maximum, so this wasn’t quite what I was after, but it’s good enough4. I could just give everyone the maximum experience to bump them up to the highest possible level, and that’ll be plenty5. So I edited one character’s XP, loaded it in the game, and… it didn’t work.

I wasn’t all that surprised, though. Save game formats often have some kind of integrity protection feature, either to protect against cheating or corruption, so I’d been expecting something like that here too. For a game like this it would almost certainly be using a simple checksum, which is usually just handled by literally summing all of the bytes in the data and storing that somewhere in the file. It’s not a terribly secure hashing routine, but for something like this it doesn’t need to be.
So I tried loading the save, sitting for a minute, and then saving again. I figured that changing nothing else, not even moving my characters, should minimize the number of unrelated changes in the file and help me isolate the checksum. Unfortunately, as it turns out, it did that a little too well - the new save only had four bytes different, and I quickly identified those bytes not as a checksum but as a 32-bit integer at the start of the save tracking your playtime in seconds from the start of the game. It’s useful knowing that’s not the data I wanted, I suppose, but why didn’t the checksum change?
So I tried something different. I went out to fight a few battles, letting exactly one of my characters gain a few experience points. I then tried comparing that save to the original one to see what changed. Obviously I had a lot more values to sift through, but I started narrowing it down. I also started producing some values to compare it to. Betting that this is probably a simple checksum format, I tried actually calculating the checksum for a few different possible slices of data and comparing them in 16-bit and 32-bit integer formats6 to see if any of those values that had changed between the saves resembled them.
This took me a little longer than I’d expected, mostly because I made a bad assumption going in. Given the amount of data being summed here (about 1012 bytes), I presumed we were looking at at least a 16-bit value. Maybe it would reserve a full four bytes, even if it wouldn’t need that much space. But as I started looking at potential hashes for the game, I noticed something. There was one byte that did resemble the checksums I’d found… just one byte.
As it turns out, Magic School Lunar! really has reserved a single byte for the checksum. It calculates a full sum over (most of) the save file, but then clips it down to only the least significant byte. So, for example, if the sum of all bytes is 18279 (0x4767), then it takes only the last byte of that number (0x67). The checksum also doesn’t cover the entire file; it seems to exclude the first eight bytes, which act as a sort of header. That header includes the playtime (which I’d already discovered wasn’t checksummed!), the current chapter and average party level7, and the checksum itself. Once I started checksumming everything past the first eight bytes, I was able to produce a checksum that matched what the game itself did and ensured it would be happy to accept my changes.

So I tested it out and confirmed that, yes, the game was happy to accept my changes now! As it happens, a single character starts the game with the maximum amount of experience8, so I even knew the maximum experience I could assign everyone else to max their stats out and make them as powerful as I could.

To make these tweaks easier on myself, I threw together a quick and dirty save editor in Python. I’ve also typed up my notes on where everything is located in the save file in case anyone else finds that useful in the future. I’ve put it up on Codeberg, and in the meantime I’m back to testing now that I never have to worry about winning a battle again.
-
And that’s after I’ve adjusted the encounter rate from the original game to make it more fun to play!↩
-
The Saturn actually also comes with builtin save memory, which is surprisingly convenient. It’s only 32KB, but for the era that’s spacious enough that I think a lot of Saturn owners never bothered buying an external memory card.↩
-
Extracted Saturn saves are usually handled in a format called BUP, which was created by the Saturn community and which is supported by a number of different tools now. I was working with BUP files, but note that all of my references to offsets in this post assume there’s no BUP header at the start of the file. If you’re reading along with your own save in BUP format and would like to compare, just add 0x40 to any offset I mention.↩
-
I suppose I could also have edited the stats table in the game to max out those stats, but I’m trying to avoid making too many code/data changes that are unrelated to the translation just to make sure I don’t confuse bugs from my debug settings for bugs in the translation.↩
-
My first testing playthrough, where I left battles on, I finished the game at level 41. Since level 99 is the cap, I should be able to easily stomp every boss at this level without needing to fight a single random battle.↩
-
ImHex’s builtin checksum feature was pretty useful for this, since it updates in realtime as you select data in the file. Unfortunately it also seems to be very crash-prone, so it took me a little longer than it might have otherwise.↩
-
The chapter and average party level are just for display in the save loading screen; changing them does nothing else. They’re followed by what looks like a padding byte, since it’s always been 0 in every save I’ve checked.↩
-
For a brief period, the characters are joined by an extremely powerful sorceror named Glen. He starts the game maxed out at level 99 and easily stomps out any enemy he runs into, just to emphasize how much more powerful he is than anyone else.↩