In September 2020, Spelunky 2 was released by Mossmouth and BlitWorks. As a fan of the original Spelunky HD, I was immediately hooked on how Spelunky 2’s uncompromising difficulty gave a constant thrill to exploring the game’s randomly-generated caverns.

As I learned to play the game, I discovered how Spelunky 2 offers a number of hidden ways to make the game even harder. One of these methods involves obtaining the “True Crown”, a hat with the appearance of a medieval jester’s cap. When picked up, the True Crown makes the player teleport the direction they are facing every 22 seconds. While wearing the True Crown, it’s easy to lose track of the teleport timing and accidentally teleport into an enemy, a trap, or even a wall tile (which “telefrags” the player, killing them instantly).

The True Crown

Each teleport also gives the player 22 bombs, which makes the True Crown viable for maximizing the amount of treasure you can obtain. With an effectively unlimited supply of bombs, a skilled player can carpet-bomb every area of a level, releasing all the gems and gold trapped inside the walls. For this reason, the True Crown sees a lot of use during high-level Spelunky 2 play.

I set out to provide a tool that players could use when equipped with the True Crown. This application would play audio cues to the player to remind them that they were about to teleport. Since the True Crown looks like a medieval jester’s hat, I decided to call my application JesterCap.

Design Considerations

A simple timer application that plays a sound every 22 seconds would not have been a sufficient solution for a True Crown timer. For instance, whenever the player paused or transitioned to a new level, they would have to recalibrate the timer. Spelunky 2 is a stressful game and I didn’t want players having to worry about performing an additional task.

The following events would have to be automatically detected by reading the game’s memory:

  • Picking up the True Crown
  • Pausing / unpausing the game
  • Transitioning to a new level

My application would be limited to PC players only, since I would not be able to run my application on a console such as the PS4. Also, the only supported operating system for PC is Windows, meaning that I could rely on the availability of Windows APIs to read the game’s memory.

Since I was limited to Windows APIs and did not have to worry about other platforms, I decided that JesterCap would be a C# / .NET application.

Reverse-Engineering

Because Spelunky 2 does not have an official modding API, I knew I would have to rely on directly reading memory values from the Spelunky 2 process. I had to find the precise locations of the relevant memory values to be read, known as “offsets”.

Cheat Engine (abbreviated “CE”) is a utility that attaches to a running program and allows you to read or write its memory values. I found that in order to use CE with Spelunky 2 I had to edit the CE settings to use page exceptions and the VEH debugger (under the Debugger Options section of the settings) because other exception types caused crashes.

As a proof-of-concept, I wanted to find the memory address of the current score. I was able to do this with the following steps:

  • Start up Spelunky 2 and begin a run.
  • Attach CE to the Spelunky 2 process.
  • Pick up some treasure in Spelunky 2 so that the score is not 0.
  • Enter the current score in the “Value” textbox.
    Searching for the current score
  • Click “First Scan” to list all the locations in memory that currently contain the specified value.
  • Repeat the following steps until the list only contains memory offsets which are changing with the score:
    • Pick up more gold in Spelunky 2 to change the score
    • Enter the new score into the “Value” textbox and click “Next Scan”

Eventually I was left with these locations in memory:

Score Offsets

Offsets are sometimes notated with a hexadecimal number after the process name. Here, the process name is Spel2.exe so we actually have 3 offsets to choose from.

I could technically have used any of these 3 score offsets, since they all seemed to update when gold is picked up. However, one of these offsets had a distinguishing feature that makes it slightly preferable: only one offset updates when gold is picked up, while the others update every frame regardless of whether or not the score value was changed.

To find out when the memory values at these offsets get updated, I right-clicked on each offset and clicked “Find out what writes to this address”. For each offset, I noticed one of two things:

  1. The debugger will immediately pick up a write operation, and the count on the write operation will begin ticking up. These addresses update every frame.
  2. The debugger will not pick up any write operations until the score actually changes - one write operation per score change. This address is only updating when the score changes, and is the one I decided to use.

I now had a static offset that pointed to the current score. However, when I tried to use Cheat Engine to edit the value of this offset to get 99999999 gold, it would immediately be reset to its old value once more gold was picked up, indicating that this value was the result of some larger calculation.

To try and ascertain the nature of this larger calculation, I delved deep into the debugger. I started with the write operation that changed the score value and opened Cheat Engine’s memory viewer, which allowed me to view the write operation in the context of the game’s assembly code.

Cheat Engine Memory Viewer showing some Spelunky 2 assembly code

I spent some time unravelling this assembly, and discovered that upon picking up new treasure, the current score is recalculated as a sum of each player’s score (even when playing the game in single-player). I found that each player has their own data structure that contains their current score, health, items, and more. I knew I’d need this data structure to determine when the True Crown was picked up, so I used the “Dissect Data/Structures” functionality in Cheat Engine to try and pick apart exactly which pieces of data were being stored.

Cheat Engine Data Structure screen showing Spelunky 2 player data

By playing the game and observing how each field of this data structure changed, I was able to make some informed guesses about what each memory offset represented. For instance, I noticed that proceeding to the next stage incremented a particular value, so I was able to identify that value as the location of the current stage.

By following the methodology of playing the game and examining how the memory changed, I was able to determine the relevant fields for my application:

  • Number of items picked up
  • Item ID of each equipped item
  • Frame count of the level (not including paused time)

With these values, I was now able to build the application.

Process Hooking

My application would need to be able to read these values directly from Spelunky 2. The first step was to create a static class that would detect whether or not a Spelunky 2 process had started:

/**
 * Responsible for locating the Spelunky 2 process and exposing a handle
 * to it.
 */

using System;
using System.Diagnostics;

namespace JesterCap
{   
    public static class ProcessSearcher
    {
        public const string SPELUNKY_PROCESS_NAME = "Spel2";

        private static Process GetSpelunkyProcess()
        {
            Process[] matchingProcesses = Process.GetProcessesByName(SPELUNKY_PROCESS_NAME);
            if (matchingProcesses.Length > 0)
            {
                return matchingProcesses[0];
            }
            else
            {
                return null;
            }
        }
    }
}

I then called this function repeatedly on a timer so that my application would continuously poll Windows to determine whether or not a Spelunky 2 process had started. (This timer code is omitted for brevity.)

Now that I had a handle to the Spelunky 2 process, I’d read the memory from its offsets in another static class. The act of reading memory from another process could be accomplished with external functions provided by kernel32.dll, which is a dynamically-linked library provided by the Windows operating system. I wrapped the functions I would need from kernel32.dll in my own static C# class for ease of use:

/**
 * Responsible for wrapping kernel32.dll. This DLL provides functionality
 * that allows us to read memory from running processes.
 */

using System;
using System.Runtime.InteropServices;

namespace JesterCap
{
    public static class KernelDll
    {
        [DllImport("kernel32.dll")]
        public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

        [DllImport("kernel32.dll")]
        public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesRead);

    }
}

Finally, I was able to use these wrapped kernel32.dll methods (now accessible as KernelDll.OpenProcess and KernelDll.ReadProcessMemory) in another static class that would take the handle to Spelunky 2 process and read the memory values at specific offsets from that handle:

/**
* Responsible for reading memory from the Spelunky process.
*/

using System;
using System.Diagnostics;

namespace JesterCap
{
    public static class ProcessReader
    {
        private const int PROCESS_WM_READ = 0x0010;

        // These offsets need to be updated with each new version of Spelunky 2.
        // Current as of 1.25.0b (2021-11-16)
        private const int OFFSET_GAME_DATA = 0x22DB7F08;
        private const int OFFSET_SCORE = 0x22DC3884;
        private const int OFFSET_FRAME_COUNT = 0xEE4;      // the number of frames spent in the current level (unpaused)
        private const int OFFSET_NUM_ITEMS = 0X77E;        // the number of items picked up
        private const int OFFSET_FIRST_ITEM_ID = 0x784;    // the first item ID in memory
        private const int SIZE_ITEM_STRUCT = 0x14;         // the space between each item ID

        private const int POINTER_SIZE = 8;

        public static Process SpelunkyProcess = null;
        public static IntPtr processHandle;
        private static IntPtr baseAddress;

        public static void LoadProcess(Process process)
        {
            SpelunkyProcess = process;
            baseAddress = process.MainModule.BaseAddress;
            processHandle = KernelDll.OpenProcess(PROCESS_WM_READ, false, process.Id);
        }

        private static byte[] ReadMemoryIntoBuffer(IntPtr address, int numBytes)
        {
            int bytesRead = 0;
            byte[] buffer = new byte[numBytes];
            KernelDll.ReadProcessMemory(processHandle, address, buffer, numBytes, ref bytesRead);
            return buffer;
        }

        public static IntPtr GetGamePointer()
        {
            byte[] buffer = ReadMemoryIntoBuffer(baseAddress + OFFSET_GAME_DATA, POINTER_SIZE);
            return (IntPtr)BitConverter.ToInt64(buffer, 0);
        }

        public static int GetScore()
        {
            byte[] buffer = ReadMemoryIntoBuffer(baseAddress + OFFSET_SCORE, 4);
            return BitConverter.ToInt32(buffer, 0);
        }

        public static int GetFrameCount(IntPtr gamePointer)
        {
            byte[] buffer = ReadMemoryIntoBuffer(gamePointer + OFFSET_FRAME_COUNT, 4);
            return BitConverter.ToInt32(buffer, 0);
        }

        public static byte GetNumItems(IntPtr gamePointer)
        {
            byte[] buffer = ReadMemoryIntoBuffer(gamePointer + OFFSET_NUM_ITEMS, 1);
            return buffer[0];
        }

        public static int GetItemId(IntPtr gamePointer, int itemIndex)
        {
            int totalOffset = OFFSET_FIRST_ITEM_ID + itemIndex * SIZE_ITEM_STRUCT;
            byte[] buffer = ReadMemoryIntoBuffer(gamePointer + totalOffset, 4);
            return BitConverter.ToInt32(buffer, 0);
        }
    }
}

I put together a simple WinForms GUI to expose this functionality to the user. I also implemented a number of other useful features:

  • Automatic detection of Spelunky 2 process by polling Windows
  • Automatic detection of True Crown pickup time by polling the Spelunky 2 process
  • User-configurable alert sound and alert timings before teleport

Finally, I open-sourced JesterCap on GitHub so that other people could contribute to its development.

A screenshot of the completed JesterCap application

Release and Maintenance

After releasing JesterCap, I reached out to TwiggleSoft, a prominent Spelunky 2 streamer known for setting high scores. Shortly after he started using JesterCap, TwiggleSoft was able to set a new world record! This record has since been surpassed, but I am very proud of my part in this achievement.

Because I open-sourced the project, I ended up collaborating directly with JesterCap users. At one point I added a document detailing how to reverse-engineer Spelunky 2 to obtain the offsets, and having this documentation available empowered other contributors to submit pull requests. Since the memory offsets changed with each new Spelunky 2 update, it was nice to have other people who were able to update these offsets for me (although I would always aim to test JesterCap against the current build of the game). At the time of writing, JesterCap has releases that support 13 discrete versions of Spelunky 2.

One of the most memorable bugs reported by a user was that JesterCap would crash if your Windows region was set to certain European countries. I had added a feature where the user could configure when they wished to receive the audio alert (e.g. 1, 2.5, or 5 seconds before teleporting). The culprit turned out to be the decimal parsing in this feature; in certain regions, a comma was used instead of a period as a decimal point (e.g. 2,5 instead of 2.5). The problem was easily fixed once identified, but I remember it fondly because I would likely not have had the chance to fix this bug had I not open-sourced the project and collaborated with people from other countries.

I was also fortunate that JesterCap attracted the attention of wonderful people in the open-source community, who then made their own contributions to the project. JesterCap is the first project I’ve open-sourced, and I’ll certainly be considering it for future projects. While developing the application, I had to rely on my knowledge of some very low-level computing concepts to successfully reverse-engineer Spelunky 2. In closing, JesterCap has been a great opportunity to hone my application development and maintenance skills.

Acknowledgements

  • All collaborators on the JesterCap project (GitHub link)
  • TwiggleSoft (for trying out JesterCap)
  • Laura McLean (for proofreading and editing)

See Also