Hacking Cosmic Fantasy 2 save RAM out of spite

This article is about editing the save RAM for Cosmic Fantasy 2 on the TurboGrafx-16 CD. It may work for the PC Engine CD version but I'm not testing it. There is a link to a spreadsheet with the addresses I found at the bottom if you want to cut to the chase. Everything up until then is a walkthrough of the steps I took to figure it out. Keep reading, scroll to the bottom, it doesn't matter which you choose.

As with previous similar articles, I'll note that I did not pirate this game:

Cosmic Fantasy 2

I bought it on xmas eve 1993 for $19.99 (before the whopping 10% employee discount).

I was working that day for sure. I believe, but am not positive, that the mall closed at 6:00 PM. It could have been 8:00. Whatever the case, it was slow in the afternoon. Slow enough to buy myself a last minute gift at 4:16 apparently. In the 6 seasons I worked retail I didn't miss an xmas eve. When I was a peon there was no choice. When I was a manager I felt obligated to. It was always a day that grew slower as the hours passed. Shoppers usually taper off around noon. By 4:16 it was probably just me and employee #1887 in the store (RIP old friend).

In another article I recalled that our store cleared out all TurboGrafx-16 games for $4.99 in late 1993. This receipt conflicts with that memory, it's more likely the event I'm thinking of was very early 1994. That would make a lot more sense. Clearing out a system right before the holidays is foolish. After all, you might earn $15 more on a sale.

I liked Cosmic Fantasy 2 at the time. There weren't many other games with that art style in 1993. I assume I played it during the winter of 1994. That would mean I'm writing this exactly 30 years later. I re-played many 16-bit RPGs but not this one. It was fine the first time around but is way too grindy. I re-played Phantasy Star II if that gives you any idea how bad it is.

The title of this is halfway based on my feelings toward playing Cosmic Fantasy 2 again. I will only do so if it can be made significantly easier.

The title is also halfway based on a recent event.

For many years I've been a fan of Limited Run Games. I bought something in the range of 40-50 games from them and even played most. Of course there are 3 variants of Ys Origin and 4 variants of Night Trap in that total. I've been 100% satisfied with them until this moment.

As much as I like them, I don't trust them with my credit card number. I don't really trust anyone with my credit card number and limit where I'll store it. This is where services like Apple Pay, Paypal, Google Pay are nice. I don't trust them either, but they limit the exposure. Until now they were all accepted by Limited Run Games.

In January 2024 they put up pre-orders for a Switch port of Cosmic Fantasy and Cosmic Fantasy 2. How could I possibly resist that? Around the same time they changed their payment backend to something called "Purple Dot", I've never heard of them before either. They only work if you give them your credit card number. Extremely common payment methods like the three aforementioned ones or Venmo or literally anything else are not accepted. So I guess I'm not pre-ordering this game, or anything else again. Maybe they'll fix that. I already unsubscribed from their mailing list so I might not find out.

I blame Embracer. I can't think of a company or IP whose products are better since being acquired by them.

Look, I'm not an expert on much. One thing that I am an expert on though is technology migrations. It's not on purpose but most of my >25 years of development experience is in technology migrations. The very first rule is to never migrate to something with fewer features than what you currently have.

So these are the two sources for the spite I'm feeling today. Cosmic Fantasy 2 grinding and Limited Run Games ruined my usual mellow demeanor. Also I enjoy making Seinfeld references so that's three things contributing to the title.

Let's get some background stuff out of the way.

Old-school games with a save feature typically used an internal battery built into the cartridge. Think Legend of Zelda or Phantasy Star. For early CD systems, the save RAM (SRAM) was built into the console itself. The Sega CD and Saturn had optional external memory cards to expand or backup the available data. The PlayStation went with external memory cards only. You can call that a cash grab but it really is preferable. TurboGrafx-16 CD owners know the pain of having to delete an old save to make room for a new one. Maybe hundreds of hours of progress in a game lost because you want to try something new. Back then it was impossible to (easily) backup or edit the games saved in the TurboGrafx-16 CD SRAM. I'd estimate that most TurboGrafx-16 CD fans aren't using original hardware today. Myself included, we're all using something where the SRAM can be trivially accessed because it's in a file likely called "game-name.srm". This is a long way of explaining why this is being done in an emulator when I own the original game & necessary hardware.

SRAM hacking is more portable than save state hacking. Save states produced by emulators can vary, meaning values you care about aren't always in the same address. SRAM is filled by the game itself, the emulator can't mess with the format. An SRAM save created by one emulator should look the same as any other. This would include FPGA systems like the Analogue TurboGrafx-16 CD clone.

The complication is the checksum. Most games that support save battery backups utilize a checksum algorithm to prevent corruption. So in addition to saving the game state they include an extra computed value to verify the contents haven't been corrupted. The aforementioned J.B. Harold Murder Club, for example, subtracts all the bytes in the save game from 0x0000 and stores the result.

The first phase of this investigation will be whether Cosmic Fantasy 2 uses a checksum. If so, we then need to figure out how it works.

We'll start by looking for some known values in the save game. When the game starts, Van has 36 HP and 19 MP.

Initial HP and MP in Cosmic Fantasy 2

Let's see what the initial save ram looks like.

Initial SRAM in Cosmic Fantasy 2

Yaaaaawn. This is starting to look a lot like J.B. Harold Murder Club. I already wonder if NEC included an example checksum algorithm in their developer documentation.

See in save game 1 the value I suspect is the checksum is 0xAF10. Really 0x10AF though because of endianness. In save game 2 the suspected checksum is 0x10AE, a difference of just 1. The data in the save games is the same except for a 1 byte difference in the save game title itself. So far this is just like J.B. Harold Murder Club.

After saving the game in slot 1, doing nothing else, there are some more differences:

Cosmic Fantasy 2 save ram after first save

Now, I didn't manually comb through those save games to figure these out. I wrote just a little bit of code to do that. The code being imported is all here.

	
import java.util.ArrayList;

import com.huguesjohnson.dubbel.file.FileUtils;
import com.huguesjohnson.dubbel.util.ByteComparer;
import com.huguesjohnson.dubbel.util.ByteComparerResult;
import com.huguesjohnson.dubbel.util.ChecksumUtils;

public class PCESRAMSandbox{

 public static void main(String[] args){
  String path1= "/home/[YOUR_USER]/.config/retroarch/saves/Cosmic Fantasy 2 (USA).1st-save.srm ";
  int startByte1=22;
  String path2= "/home/[YOUR_USER]/.config/retroarch/saves/Cosmic Fantasy 2 (USA).1st-save.srm ";
  int startByte2=376;
  int length=347-startByte1;
  
  try{
   ArrayList<ByteComparerResult> result=ByteComparer.compare(path1,startByte1,path2,startByte2,length);
   StringBuffer sb=new StringBuffer( "Differences\n ");
   for(ByteComparerResult bcr:result){
    sb.append(bcr.getFile1LineHex());
    sb.append( ": ");
    sb.append(bcr.getFile1ValueHex());
    sb.append( "  ");
    sb.append(bcr.getFile2LineHex());
    sb.append( ": ");
    sb.append(bcr.getFile2ValueHex());
    sb.append( "\n ");
   }
   System.out.println(sb.toString());
   byte[] b1=FileUtils.readBytes(path1,startByte1,length);
   byte[] b2=FileUtils.readBytes(path2,startByte2,length);
   int sum1=ChecksumUtils.sumBytes(b1);
   int sum2=ChecksumUtils.sumBytes(b2);
   System.out.println( "sum of save1=0x "+Integer.toHexString(sum1));
   System.out.println( "sum of save2=0x "+Integer.toHexString(sum2));
   int sub1=ChecksumUtils.sumBytes(b1);
   int sumb=ChecksumUtils.sumBytes(b2);
   System.out.println( "\nsub of save1=0x "+Integer.toHexString(sub1));
   System.out.println( "sub of save2=0x "+Integer.toHexString(sub2));
   int xor1=ChecksumUtils.xorBytes(b1);
   int xor2=ChecksumUtils.xorBytes(b2);
   System.out.println( "\nxor of save1=0x "+Integer.toHexString(xor1));
   System.out.println( "xor of save2=0x "+Integer.toHexString(xor2));
   
  }catch(Exception x){
   x.printStackTrace();
  }
 }
}

Running this produces:

	
Differences
0x1E:0x31 0x180:0x32
0x145:0x1A 0x2A7:0x0
0x146:0xB 0x2A8:0x0
0x147:0x40 0x2A9:0xFFFFFFFF
0x149:0x1 0x2AB:0x0

sum of save1=0x3b5
sum of save2=0x34f

sub of save1=0xfffffc4b
sub of save2=0xfffffcb1

xor of save1=0xb
xor of save2=0xffffffa7
OK, so let's do a sanity check...
	
(0x31-0x32)+(0x1A-0x00)+(0x0B-0x00)+(0x40-0xFF)+(0x01-0x00)=-(0x9A)
0x10AE−0x1148=-(0x9A)

So there you go. The checksum is being calculated via subtracting bits from some base value. In J.B. Harold Murder Club that base value was 0x0000. Is that the case here? I don't think so. If so the first checksum should be 0x4AFD and the second should be 0xFCB1 (plus or minus 1 based on my ability to compute two's compliment).

Let's test this theory out. First we'll see if the checksum is even used at all:

Cosmic Fantasy 2 load error

OK, it is.

Now we'll boost the HP and MP values by 1. One set of values is the max value and the other set is the current value. I don't care which is which so I'm bumping each by 1. This means I need to subtract 4 from the checksum. Save the SRAM, reload, and...

Successfully editing a Cosmic Fantasy 2 save state

At this point I know I can hack the SRAM for Cosmic Fantasy 2. I'm still curious what the base value is for the checksum, or which bits are used to compute it. I can proceed for the moment without those answers. Let's look for the rest of the stats:

Van statistics

Power, agility, and wisdom look straightforward. Attack and defense look like they have base values and the value on the screen is the base value + equipment value. XP might be that block of zeroes. It would be odd if the next XP value was stored, I think it could just be a display value there. Starting gold is 100 (0x64) which I don't see anywhere.

Alright, let's adjust these:

Updated statistics

OK, that all worked. I'm optimistic we'll be able to make this game a lot easier. It definitely can't get much more difficult. I nearly had a game over in the very first random battle. After cranking up the base attack value, things indeed got easier:

Attack raised

That was great for a moment. Then I discovered something horrible about this game. When you gain a level it doesn't boost your current statistics. It replaces them with, what I assume are, hard-coded values for each level. Look at this nonsense:

Statistics reverted after gaining a level

The statistics I upgraded are gone. Boo. So I guess the first thing to do is get Van to his maximum level. If he can't gain levels then maybe we can edit everything else without losing the values.

The thing confusing me right now is it really, really looks like current and next level XP are stored as decimal:

Is XP stored in decimal?

Let's try messing with those... if this works, it will be the first 8/16-bit RPG I've seen do this.

Gaining a level

So this is cool. After bumping Van's XP from 106 to 206 (because that's an easy checksum change) he gains a level immediately upon load. That's really convenient. Let's just assume the XP goes over 3 bytes and bump it by 100K:

Up to level 20

Now we're in business. What value will this finally break at?

Up to level 50

It looks like level 50 is the practical limit. Something bad will probably happen if I get over 999,999 XP. Since level 48 is in the 500K range you must reach 50 somewhere before 600K. I don't recall what the limit is from playing it. Also most of the stats are 2 bytes so they could probably go all the way to 65535 in theory.

If having XP as decimal wasn't confusing enough... HP & MP are stored in little-endian byte order while the other stats are in big-endian byte order. I have never despised this game more.

Anyway, I'm going to knock down the XP and boost the max HP & MP. I still haven't figured out how gold is stored but I'm ready to play this game now.

First boss fight is much easier

Alright, let's see how much more I can figure out before my attention span expires. Oh, bad times, I forgot about the "unwinnable" fight at the beginning:

First fight against Wizda

It looks like the developers prepared for this. You can bring her HP down to zero (which takes a while) and she will still win.

Wizda wins anyway

I feel like this next picture is self-explanatory:

Testing out inventory editing

This is a late game accessory, not a usable item, as I recall. It at least shows where inventory is stored. We'll probably figure out some of the item codes as we go along. We can sell this item to quickly build a little nest egg:

Selling Zartam

Neat, that item isn't removed from your inventory after selling it:

Zartam still in inventory

Maybe I can punt on editing the gold value since I can sell this item endlessly. For the moment, it helps to buy everything in town and figure out some more item codes:

Looking for more item codes

TL;DR - the item codes match the order you'll see the names in Track 2 of the CD.

Also, I'm an idiot. Gold was trivial to find all along. I was looking for the hex value. Like XP, it's stored as decimal:

Gold in save RAM

Soon Annie joins the team. Her statistics are stored the same as Van's:

Annie level 50

From here it should be easy. The last thing I want to figure out is the equipment, which looks to be separate from the inventory. For that, I'll use the same little compare program. I'll change Van's equipment and save in the 2nd slot.

	
Differences
0xC8:0x0 0x22A:0x2
0xCA:0x0 0x22C:0x1
0xCB:0x0 0x22D:0x1

OK, so these values appear to map to an index of an equipable item.

Equipment slots

I can't stress enough how weird the save RAM for Cosmic Fantasy 2 is compared to similar games of the era.

The bytes before then are the equipable items themselves. Maybe this helps...

Equipment

Alright, I've figured out everything we need to make this game a (relative) breeze. There are still a million battles but at least level grinding isn't needed.

There are still a couple things to look for... although I probably won't:

My goal was to make the game easy and that is done. Will I replay it now? No. Will I build a UI to make editing Cosmic Fantasy save RAM easy for others? Maybe, if I ever want to learn a new UI framework.


Spreadsheet with all the addresses and values I figured out

This is probably what you were interested in all along.

There's a 1% chance I've added things to it that are not discussed in this article.



Related