Decompiling 1997's MTG Shandalar to ANSI C with Ghidra and AI

Tags: s30 programming

The Original MTG Deckbuilding Game

Back in 1997 Microprose released Magic the Gathering for PC aka Shandalar. It's a very unique game, even for its time. It was part RPG, part deckbuilder and part roguelike. It let you wander a fantasy overworld, gamble cards on ante matches and acquire the Power Nine in deadly dungeons.

For nearly 30 years even after numerous fan updates and improvements the source code has been unavailable.

Recently I bought an original Microprose CD off eBay with the goal to reverse engineer the game and recreate, as much as possible, the original source code. Using a combination of Ghidra and modern AI, I was able to convert the MAGIC.exe binary and .DLLs into fully compilable ANSI C99 source code.

The codebase helped me answer decades-old questions, countered "common knowledge" about how the AI worked and even uncovered test tools written by the original team.

The Decompilation Pipeline: Ghidra + AI

Decompiling a 90's Win32/DirectX 2 game is messy. The raw decompilation is full of unnamed pointers, mangled global variables and inscrutable function names.

As additional cleanup steps were added, I ended up with a 4-step pipeline that I ran across the entire codebase. A combination of Ghidra settings and post export scripts (in scripts/) renamed symbols across the codebase, identified subsystems and grouped related functions into individual files.

  • 1. ANSI C Exporter Configuration: Automated Ghidra headlessly across all seven binaries, forcing the C decompiler into strict ANSI C mode with standard block comments and exported type definitions.
  • 2. String & Source Archaeology: Scanned the binaries for leftover 1990s assert() file paths and traced cross-references to automatically reconstruct Sid Meier and Ned Way’s original source file structure.
  • 3. Automated Two-Way Symbol Renaming: Extracted string contexts and API call graphs into CSV symbol maps, using custom Ghidra scripts to rename over 1,650 functions and 268 global game-state variables.
  • 4. C99 Type & Variable Normalization: Replaced raw Ghidra pseudo-types (undefined4, byte) and decompiler artifact variables (unaff_EBP, iVar1) with modern fixed-width C99 types and readable identifiers.

For this workflow, the AI was incredible. Ghidra was able to identify functions from the decompiled assembly, but the AI did the bulk of the renaming, working backwards and incrementally deciphering a function's effect.

Before/After Example

Here's an example of what the initial source looks like vs after several rounds of AI, it's pretty readable!

Before

undefined4 FUN_004d1cc4(int param_1, int param_2, int param_3)
{
  int iVar1;
  undefined4 uVar2;

  if (param_3 == 0x73) {
    uVar2 = (*(uint *)(DAT_006a5f3c + param_2 * 0x120 + param_1 * 0x5b20) &
             0x20010) == 0;
  }
  else {
    if (param_3 == 0x6d) {
      *(uint *)(DAT_006a5f3c + param_2 * 0x120 + param_1 * 0x5b20) =
        *(uint *)(DAT_006a5f3c + param_2 * 0x120 + param_1 * 0x5b20) | 0x10;
    }
    if (param_3 == 0x72) {
      iVar1 = FUN_004b50c0(param_1);
      FUN_004cc56d(param_1, param_1, param_2, param_1, iVar1,
                   s_Sinbad_draws____0052e914, 0);
      if ((*(byte *)(DAT_004ff594 + *(int *)(DAT_006a5f34 + iVar1 * 0x120 +
                                             param_1 * 0x5b20) * 0x34) & 1) == 0) {
        FUN_0044913a(param_1, iVar1);
        *(int *)(DAT_006a5f34 + iVar1 * 0x120 + param_1 * 0x5b20) = -1;
        *(int *)(DAT_0068ecb0 + param_1 * 4) = *(int *)(DAT_0068ecb0 + param_1
                                                        * 4) + -1;
        if (DAT_0066aaf4 != 1) {
          FUN_00472180(0x18);
        }
      }
    }
    uVar2 = 0;
  }
  return uVar2;

After

/*
 * FUNCTION: Card_Sinbad_Draw
 *
 * DESCRIPTION:
 *   Executes the activated draw ability for Sinbad.
 *
 * PROCEDURAL STEPS:
 *   1. Check if card is untapped and free of summoning sickness (Event 0x73).
 *   2. Tap the permanent to pay activation cost (Event 0x6D).
 *   3. Draw top card from active library (Event 0x72).
 *   4. Check card color identity: if not a land (ColorMask & 1 == 0), discard
 it.
*/
bool Card_Sinbad_Draw(int player, int card_index, int event_code)
{
  int drawn_slot;
  bool can_activate;

  /* Event 0x73: Validate if ability can be activated */
  if (event_code == EVENT_CAN_ACTIVATE) {
    can_activate = (*(uint32_t *)(&g_CardSlot_Flags + card_index * 0x120 +
                                  player * 0x5b20) & (FLAG_TAPPED | FLAG_SUMMONING_SICK)) == 0;
  }
  else {
    /* Event 0x6D: Pay activation cost (Tap card) */
    if (event_code == EVENT_PAY_COST) {
      *(uint32_t *)(&g_CardSlot_Flags + card_index * 0x120 + player *
                    0x5b20) |= FLAG_TAPPED;
    }

    /* Event 0x72: Resolve ability effect */
    if (event_code == EVENT_RESOLVE_TRIGGER) {
      drawn_slot = Magic_ExecuteDrawPhase(player);
      Ai_LogAction(player, card_index, drawn_slot, "Sinbad draws...");

      /* Check if drawn card is NOT a Land (Bit 0 in color table
         indicates Land) */
      if (((&g_MasterCardColorTable)[*(int *)(&g_CardSlot_CardId +
                                              drawn_slot * 0x120 + player * 0x5b20) * 0x34] & COLOR_LAND) == 0) {
        Pic_DisplayDiscardAnimation(player, drawn_slot);

        /* Discard drawn card to graveyard */
        *(int *)(&g_CardSlot_CardId + drawn_slot * 0x120 + player *
                 0x5b20) = -1;
        g_ActivePlayerSpellPriority[player]--;

        if (g_IsAiThinking != 1) {
          Magic_UpkeepPhase(PHASE_DISCARD_TRIGGER);
        }
      }
    }
    can_activate = false;
  }
  return can_activate;
}

Mysteries Answered

  • Does the Enemy AI Cheat? Myth: The AI can see your hand Reality: No. The AI will try to infer what is in your hand, but there are explicit restrictions that prevent the AI from seeing the players hand or library.
  • How are Starting Decks Generated? Using a combination of known poor cards but also including an "anchor card" that was stronger so the player could build a deck around it. Often these anchor cards were good creatures like Serra Angel or Sengir Vampire.
  • Proprietary Asset Formats When I started my 30th Anniversary edition, I spent a lot of time reading the excellent series of articles on the .PIC format on canadianavenger.io so I could extract the original artwork. I wrote a set of tools to help encode/decode .PIC and .SPR files, but encoding .SPR files from .PNG didn't work initially. Now, with the full source I get pixel perfect encoding/decoding!

Internal Test Harnesses

One of the coolest discoveries in the binary was several internal test applications. These appear to have been custom test beds written during development to prototype game mechanics and UI flows before the main campaign was stitched together.

To get the decompiled codebase running on modern systems:

  • Test Harnesses: Built a lightweight SDL2 shim to mock the legacy graphics/sound calls. You can build with `make sidtest`
  • Full Game: Runs cleanly under Wine, bridging legacy Win32/DirectX APIs to modern OSes.

Final Words & What's Next

What started as a curiosity with an eBay CD-ROM turned into a software archaeology project. We now have a readable, compilable ANSI C99 codebase that can be read by others. This opens up a lot of options for modding, bug fixing and derivative games.

In my case this will help me understand, in detail, how the original worked and how I can preserve that in the 30th Anniversary Edition I'm building.

Additionally, getting an unmaintained 1997 DirectX 2 game to decompile into 3,500+ functions of 100% valid ANSI C99 with zero compiler errors and authentic subsystem structures shows how powerful Ghidra and modern AI can be for software preservation.

If you want to explore the source, inspect the AI routines, or build the engine yourself:

Run the original test harnesses with make sidtest

Date: 2026-08-31

Author: Ben