2a1b

Dave Ball / nzdjb (he/him)

About me

Husband, Dad, Computer Wrangler in Te Whanganui-a-Tara, Aotearoa.

Me elsewhere

Reverse Engineering Slay The Spire

Dave @ 2025-09-03

In this article, I describe the steps I took to reverse engineer the save game code of Slay the Spire to unlock an achievement more quickly.

What is reverse engineering?

Reverse engineering is the process of taking a product and observing its behaviour and deconstructing it to understand the how it works. This is often done with the aim of reproducing parts of it in another product or modifying the behaviour of the product in some way.

The game

Slay the Spire (2019) is a deck builder game developed and published by Mega Crit. In it, your goal is to climb the Spire by defeating a variety of enemies. To do so, you must assemble a deck of cards that are used in combat.

The goal

One of the most fun things to do in a deck builder game is to build decks that synergise in different ways to produce interesting effects. Unfortunately, the randomness involved in availability of cards can make this quite frustrating. If I could start the game with the deck the way we wanted it, I could get straight to the fun part without the frustration!

The problem

But how can I make that happen? An obvious answer is to create a saved game and then manipulate it to put the game into the state I want. If I start the game and save as soon as possible, I should have a fairly basic save file that will provide a good base to work from.

Starting a game as the Ironclad class and saving after the initial encounter provided me with a file called IRONCLAD.autosave. Opening this, I saw what looked like a base64 string. Loading the file in CyberChef and running From Base64 gave an unreadable response, so I had to dig further.

Reverse engineering the game

In the game directory, there are several interesting files:

  • SlayTheSpire.exe (364KB)
  • desktop-1.0.jar (356,531KB)
  • mod-uploader.jar (1,600KB)
  • mts-launcher.jar (1,250KB)

The presence of the jar (Java Archive) files suggested that this game was written in Java (or another JRE language). Given the small size of SlayTheSpire.exe and comparatively large size of desktop-1.0.jar, it's likely that the exe is a native wrapper for starting the jar, which would then contain the game logic.

I validated this theory by exploring the contents of the jar. jar files are zip archives structured in a particular way, so they can be extracted them with an archiving tool like 7-Zip.

After extracting the files and searching for filenames containing 'save', I found com/megacrit/cardcrawl/saveAndContinue/SaveFileObfuscator.class, which sounded very promising. This is a Java class file which contains byte-code, which is run on a JVM (Java Virtual Machine). This can usually be decompiled into readable Java fairly easily.

In this particular case, I opened it in VSCode and used the Language Support for Java(TM) by Red Hat extension to decompile the class file and was shown the following:

 // Source code is unavailable, and was generated by the Fernflower decompiler.
package com.megacrit.cardcrawl.saveAndContinue;

import org.apache.commons.codec.binary.Base64;

public class SaveFileObfuscator {
   public static final String key = "key";

   public static String encode(String s, String key) {
      return base64Encode(xorWithKey(s.getBytes(), key.getBytes()));
   }

   public static String decode(String s, String key) {
      return new String(xorWithKey(base64Decode(s), key.getBytes()));
   }

   private static byte[] xorWithKey(byte[] a, byte[] key) {
      byte[] out = new byte[a.length];

      for(int i = 0; i < a.length; ++i) {
         out[i] = (byte)(a[i] ^ key[i % key.length]);
      }

      return out;
   }

   private static byte[] base64Decode(String s) {
      return Base64.decodeBase64(s);
   }

   private static String base64Encode(byte[] bytes) {
      return new String(Base64.encodeBase64(bytes));
   }

   public static boolean isObfuscated(String data) {
      return !data.contains("{");
   }
}

This shows a class with three public methods, encode, decode and isObfuscated as well as a public field key. The interesting one is decode which does the following:

  1. Takes a string s and a string key
  2. Base64 decodes s into a byte array
  3. Turns key into a byte array
  4. Passes both byte arrays into xorWithKey, which xors them together for the length of s, repeating key as necessary
  5. Creates a string from the resulting byte array

Seeing this, I went back to CyberChef and added an xor step to the recipe, using "key" as the key. This output a JSON document containing the card list, as well as many other parts of the game state.

Putting it together

Using this recipe, I was able to decode the save file. After modifying it as needed, I could run the edited JSON through a recipe comprised of the steps in reverse order to produce a save file that the game would open1.

Summary

Deobfuscating save files required a small amount of effort, but provided a huge payoff in ability to control the game state. This approach can likely be applied to many other games.

  1. I later discovered that the game will happily open an unobfuscated save file, so this step was unnecessary.


Reverse Engineering MOLEK-SYNTEZ

Dave @ 2025-06-07

In this article, I describe the steps I took to reverse engineer the Solitaire portion of MOLEK-SYNTEZ to unlock an achievement more quickly.

What is reverse engineering?

Reverse engineering is the process of taking a product and observing its behaviour and deconstructing it to understand the how it works. This is often done with the aim of reproducing parts of it in another product or modifying the behaviour of the product in some way.

The game

MOLEK-SYNTEZ (2019) is a puzzle game developed and published by Zachtronics. In it, you program a molecular synthesizer to transform various precursor molecules into the desired output molecules.

As with many Zachtronics games, there is also a Solitaire game included.

The achievement

Charlatan: Win 100 games of solitaire.

Winning the Solitaire is not especially hard, but takes a while. After winning around 20 something games, I got bored and decided to see if I could speed things up.

Failed approaches

Typically, I'd use Cheat Engine or Saved Game Modification for this sort of thing.

Cheat Engine

Cheat Engine allows you to repeatedly scan the memory of a process for particular values. By changing the value in game and rescanning, you can narrow down the list of memory addresses to identify the address that stores the value. Once this is done, you can then change the value at that address to affect the state of the game.

Unfortunately, after identifying the address for the number of solitaire wins, modifying it didn't impact the achievement and the number was reset to the real value after another win was added.

Saved Game Modification

Another approach for modifying a game's state is to decode and modify the save files.

However, in this case, the number of solitaire wins didn't seem to be stored in any of the save files. I also checked the registry, but wasn't able to find it there either.

Reverse engineering the game

The first step to finding out how the game works is to try to decompile it. For that, it helps to know what kind of executable we're working with.

Using the file command, we can find that MOLEK-SYNTEZ.exe is a .Net assembly. .Net languages are compiled to the Common Intermediate Language, which can be decompiled fairly easily to readable C# code. My tool of choice for this is dnSpy, which supports debugging as well as decompilation.

On opening the binary and decompiling it, I found that it was partially obfuscated. All the variables, most of the method calls and some of the class names had been replaced with indecipherable strings. Fortunately, there's a very good tool for deobfuscating .Net called de4dot. With the help of this, I was able to return the code to a fairly readable state.

While de4dot can't recover obfuscated symbols, a lot of the classes in the binary hadn't had their names obfuscated. In particular, these looked promising: SolitaireAnimation, SolitaireItem, SolitaireScreen and SolitaireState.

I also noticed that the game used a library called Steamworks.NET, which is used for interacting with Steam for tasks including setting achievements. Looking at the Solitaire classes, I found that the library was used in SolitaireScreen, with a class called SteamUserStats. Static methods from this class were used several times in one part of the code, which looked like this:

if (this.method_0().method_0() || GClass74.smethod_14((GEnum122)3, Key.F11))
{
  GClass5.smethod_2().sound_20.smethod_1();
  GameLogic.gameLogic_0.gclass116_0.method_7(this.method_0().bool_0);
  if (!this.method_0().bool_0)
  {
    SteamUserStats.SetAchievement("NO_CHEAT");
    SteamUserStats.StoreStats();
  }
  if (GClass102.bool_3 && this.method_0().method_0() && SteamUserStats.GetStat("SOLITAIRE", out this.int_0))
  {
    this.int_0++;
    SteamUserStats.SetStat("SOLITAIRE", this.int_0);
    SteamUserStats.StoreStats();
    if (this.int_0 <= 1)
    {
      SteamUserStats.IndicateAchievementProgress("SOLITAIRE_1", (uint)this.int_0, 1U);
    }
    else if (this.int_0 <= 10)
    {
      SteamUserStats.IndicateAchievementProgress("SOLITAIRE_2", (uint)this.int_0, 10U);
    }
    else if (this.int_0 <= 100)
    {
      SteamUserStats.IndicateAchievementProgress("SOLITAIRE_3", (uint)this.int_0, 100U);
    }
  }
  this.method_0().genum116_0 = (GEnum116)2;
  this.method_0().float_1 = 0f;
}

In particular, these five lines stood out:

if (GClass102.bool_3 && this.method_0().method_0() && SteamUserStats.GetStat("SOLITAIRE", out this.int_0))
{
  this.int_0++;
  SteamUserStats.SetStat("SOLITAIRE", this.int_0);
  SteamUserStats.StoreStats();

This code is retrieving the value of the "SOLITAIRE" stat, incrementing it and storing it back. I set a breakpoint on the post-increment line and attached the debugger to my game process. After winning a game of Solitaire, the game froze and dnSpy showed me I was at the post-increment line.

From there, I opened the Locals tab in dnSpy and found this.int_0. Unfortunately, changing the value directly in the Locals tab gave an "Internal debugger error", but I was able to work around this by opening the address in a Memory tab and changing the hex value directly.

After that, I continued the process. My edited Win Count was displayed in the game and I was given the achievement. Hooray!

Summary

Decompiling and debugging the game was quite a lot more work than fiddling values through Cheat Engine, but it provided quite a lot more control over the process, which was what was needed in this situation.