DEV Community

Cover image for Resurrecting a 25-Year-Old Horror Game: How I Ported 'Fiend' to Modern Systems
Gustavo Lopes
Gustavo Lopes

Posted on • Edited on

Resurrecting a 25-Year-Old Horror Game: How I Ported 'Fiend' to Modern Systems

1 - Introduction

Game restoration is more than just a technical challenge; it is an essential act of preservation. Video games are a unique blend of art and complex engineering, but unlike movies or books, they can become stuck to the configurations they were born on. Just like software, a video game project risks becoming "abandonware" if its fundamental components—such as supporting libraries—become obsolete and are not adequately maintained.

As a passionate computer scientist and an avid gamer, I always dreamed of the chance to dive into and help preserve the source code of older games. That opportunity finally arrived when I came across the source code for the survival horror title, Fiend, originally released in 2001. This game holds a special place for me. Although not a classic or the best of its time, it was one of the first horror games I ever played.

My goal was ambitious: to take source code written for Windows 98/XP and make it run natively on modern 64-bit Linux and Windows 10/11.

This wasn't just a simple "recompile." It required modernizing a lot of components of the codebase. I replaced the outdated audio library, fixed memory corruption bugs that had been sleeping for 20 years, and built a CI/CD pipeline using GitHub Workflows, and more. This ensures that every time I change the code, the game is automatically built and tested, guaranteeing it won't break again in the future.

This post documents the game's revitalization, the technical challenges, and aims to highlight this influential, lost relic that is, in a way, very influential to a major video game company's history.

But first, let's start with some context.

2 - What is "Fiend"?

GripDesign Logo that appears when you boot the game

To understand the restoration, you first have to understand the subject. Fiend is a top-down survival horror game released in 2001 by GripDesign—the alias of Thomas Grip, who would go on to co-found Frictional Games.

If you have played Amnesia: The Dark Descent or SOMA, you can see the DNA of those games right here in Fiend. It features a heavy Lovecraftian atmosphere, limited resources, and a combination of psychological horror and combat.

The story is as follows: you play as Nick Cane, a researcher sent to the small mining town of Lauder. What starts as a routine mineral survey quickly spirals into a nightmare as you uncover an ancient evil buried beneath the town.

If you like to see a full playthrough of the game here is a link for it.

Also, for more information on some of Frictional Games' lesser-known projects, such as Fiend, visit this page, on the official website.

2.1 - The Tech Stack (2001 Era):

The game was a technical marvel for a one-man team in 2001, featuring real-time lighting and particle effects. It was built using:

Language: C (The grandfather of modern systems programming).

Graphics: Allegro 3.9.3 (A famous game programming library from the 90s/2000s).

Audio: FMOD (A powerful sound engine still used today, though the 2001 version is ancient history).

But most impressive of all, alongside the game there is also a Map Editor. The restoration project also includes the restoration of the Map Editor as well.

The map editor open on one of the game's many houses

This isn't just a tool for drawing walls; it is a complete development environment for the game. It allows you to design map layouts, define collision data, place items, and add complex logic like triggers and events. By fixing the editor, I ensured that users can not only play Fiend but also study how it was put together—or even create their own mods and stories (if they so desire).

With the project’s current state being explained, it becomes clear as day as to why the game is “unplayable” in its current state. This boils down to three main factors: the dependence on the closed-source FMOD library, the choice of programming language, and the use of an obsolete game programming framework.

3 - Getting it to boot

And so, let’s get started into the process of revitalizing this game, with the first step being, getting it to run. Surprisingly, the project featured a functional CMake setup. It was designed to automatically detect essential libraries on the user's system, particularly Allegro and FMOD binaries, allowing anyone to clone and build the repository with a single command. Nevertheless, crucial architectural decisions for the project still needed to be made.

The game was originally built on Allegro 3.9.3, a library from the late 90s. While the modern standard is Allegro 5, upgrading to it was impossible without rewriting the entire game. Allegro 5 fundamentally changes how input works, moving from a "polling" system (checking if a key is down right now) to an "event queue" system (waiting for the OS to report a key press). To bridge this gap, I chose Allegro 4.4.3. This version is modern enough to compile on 2025 Linux and Windows, but retains the legacy architecture the code expects, requiring only minor updates to deprecated math functions and updating the CMake to link to the "math.h" header.

These math functions where responsible to calculate the direction of the game's fog and also helped NPCs face the correct direction that they are facing the player when interacted

With the graphics sorted, I hit a wall with the audio. The game relied on an ancient version of FMOD that is incompatible with modern Linux. Rather than getting stuck in "dependency hell," I used a standard development tactic called feature flagging. By wrapping all audio code in pre-processor directives (#ifdef USE_FMOD), I made the sound system optional during the first stages of development.

// Example of how I silenced the audio engine
void PlaySound(int id, int vol, int pan, int freq) {
    #ifdef USE_FMOD
        // Original code: only compiles if FMOD is found
        FSOUND_PlaySound(channel, sound_data[id].sample);
    #else
        // If FMOD is missing, do nothing (game runs silently)
        return; 
    #endif
}
Enter fullscreen mode Exit fullscreen mode

This allowed the compiler to ignore the missing library and build a silent—but functional—version of the game so I could focus on stability.

After this point, the game was actually initializing, but there were still a lot of issues that made it crash on startup. One of them is that I had to hard-code the system locale to "C" (standard English). The game crashed on systems using languages like Portuguese or German because the fscanf function tries to use commas for decimals instead of dots.

// Storing the user's original locale
char *old_locale = setlocale(LC_NUMERIC, NULL);

// Forcing the game to use "." for decimals (English Standard)
setlocale(LC_NUMERIC, "C");

// ... Parsing code runs here ...

// Restoring the user's locale
setlocale(LC_NUMERIC, old_locale);
Enter fullscreen mode Exit fullscreen mode

This fix was important, because a lot of data was being read from .txt files with important information, like for example, the data for the weapons of the game.

This .txt is the file for the revolver

After fixing that (and some logic issues inside the game), I faced the most complex challenge yet: Binary Compatibility. The game saved maps by dumping raw memory structures to disk. In 2001, on 32-bit systems, memory pointers were 4 bytes. On my 64-bit Linux machine, they are 8 bytes.

This size mismatch meant the game was reading garbage data. To fix this, I wrote a custom loader that reads the files field-by-field, manually discarding the old 32-bit pointers to realign the data.

// The Fix: Manual Field Reading & Skipping 32-bit Pointers
fread(&temp_map->num_of_lights, sizeof(int), 1, f);

// On 32-bit Windows, the next 4 bytes were a pointer.
// On 64-bit Linux, we don't need that old address, so we skip it.
fseek(f, 4, SEEK_CUR); // Skip 32-bit pointer
Enter fullscreen mode Exit fullscreen mode

With these fixes, the game was finally playable—albeit silent.

4 - Fixing memory issues

After the initial port, I felt confident to work on the audio. The game compiled, the graphics rendered, and I could walk around the first map. But when I switched the compiler from "Debug" mode to "Release" (turning on optimizations like -O2), the game immediately crashed.

This is a classic problem in C programming. Debug builds often add "padding" around variables that can accidentally hide memory errors. Release builds remove that padding, causing those sleeping bugs—often called "Heisenbugs"—to wake up and crash the application.

To hunt them down, I needed a memory debugger.

4.1 - The Tool Selection: Valgrind vs. AddressSanitizer

My initial strategy for memory checking involved Valgrind, a tool I was already familiar with. It works by executing the program within a virtual environment, thoroughly inspecting every single instruction. While remarkably comprehensive, this approach comes with a severe performance hit, slowing execution by a factor of 20 to 50.

The issue with Valgrind in this context was immediate: its drastic slowdown made it unusable for a real-time game. The resulting frame rate was so poor that I couldn't even navigate the menus to access the very bugs I was trying to find.

Consequently, I switched to AddressSanitizer (ASAN). Rather than virtualizing the CPU, ASAN integrates error-checking code directly into the program during the compilation process. This makes it significantly more efficient, reducing the slowdown to only about 2x. This much faster performance finally allowed me to play the game at a manageable frame rate while ASAN monitored for errors in the background.

The moment I launched the game with ASAN active, the console instantly filled with red text. The 2001 original code was quickly exposed as being full of "ticking time bombs."

4.2 - "Off-by-One" Error and Unitialized Memory

The most severe bug—the one causing the crashes—was hidden in the Line-of-Sight (LOS) system. The game uses an array to track light sources, defined with a size of 18. However, the loop iterating through this array was written to go from 0 to 18.

In C, an array of size 18 has indices 0 through 17. Accessing index 18 means you are reading/writing memory that doesn't belong to that array.

// The Bug: Iterating too far
// 'l' goes up to 18, but array size is 18 (indices 0-17)
if(l < 18) { 
    light_source[l] = ...; // Writes to invalid memory at index 18!
}

// The Fix: Stop at 17
if(l < 17) {
    light_source[l] = ...; 
}
Enter fullscreen mode Exit fullscreen mode

ASAN flagged this as a "Global Buffer Overflow". This single byte of corruption was overwriting adjacent memory, destabilizing the entire engine during map transitions.

A similar bug caused loops to iterate using MAX_OBJECT_NUM instead of the actual number of objects (map->num_of_objects), leading to uninitialized memory access.

// BEFORE (heap buffer overflow):
for (i = 0; i < MAX_OBJECT_NUM<; i++) {
    // Accesses beyond allocated memory!
}

// AFTER (correct bounds):
for (i = 0; i < map->num_of_objects; i++) {
    // Uses dynamic bounds to stay within the allocated heap space
}
Enter fullscreen mode Exit fullscreen mode

4.3 The Dangers of sprintf

The original code heavily relied on sprintf to generate save filenames. This function is dangerous because it doesn't check if the text fits into the destination buffer. ASAN detected multiple stack-buffer overflows where the game tried to write long map names into small variables.

I replaced these unsafe calls with strncpy, which forces a limit on how many characters can be copied, ensuring we never write past the end of the buffer.

// BEFORE (unsafe)
sprintf(temp.map_name, "%s", map->name);

// AFTER (safe)
strncpy(temp.map_name, map->name, sizeof(temp.map_name)-1);
temp.map_name[sizeof(temp.map_name) - 1] = "\0";
Enter fullscreen mode Exit fullscreen mode

4.4 Memory Leaks and Double-Free

Due to the significant memory consumption of storing many in-game images as bitmaps, which would make simultaneous rendering challenging, the game employs an efficient strategy: Allegro's Run-Length Encoding (RLE) Sprites. This method is particularly effective for simpler images that feature large, uniform areas of color. By replacing repeated pixel data, RLE reduces both the size of the image files and the time required to draw them, making the rendering process much faster.

The core issue is twofold: while the game correctly converts the bitmap to RLE and cleans it up, the RLE Sprites are never cleaned up, not even during transitions.

void release_objects(void) {
    for (i = 0; i < num_of_objects; i++) {
        for (j = 0; j < object_info[i].num_of_frames; j++) {

            if (!object_info[i].angles || object_info[i].door) {
                // Single bitmap - free it
                if (object_info[i].pic[j][0].data) {
                    destroy_bitmap(object_info[i].pic[j][0].data);
                }
            } 
            else if (object_info[i].additive) {
                // Additive objects - free angle bitmaps
                for (k = 0; k < num_angles; k++) {
                    if (object_info[i].pic[j][k].data) {
                        destroy_bitmap(object_info[i].pic[j][k].data);
                    }
                }
            } 
            else {
                // Normal objects - free RLE sprites (MEMORY LEAK FIX)
                for (k = 0; k < num_angles; k++) {
                    if (object_info[i].rle_pic[j][k]) {
                        destroy_rle_sprite(object_info[i].rle_pic[j][k]);
                    }
                }
            }

        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Second, in specific scenarios, flawed boolean logic leads to the same memory data being freed multiple times, which results in a Double-Free Error.

// BEFORE (double-free):
// Loading logic:
else if(object_info[i].additive) {
    // Bitmaps NOT freed
    else {
        // Normal objects (includes trans without additive)
        // Bitmaps FREED here
    }
}

// Cleanup logic (MISMATCH):
else if(object_info[i].additive || object_info[i].trans) {
    // Tries to free bitmaps AGAIN!
}

// AFTER (correct):
// Cleanup now matches loading:
else if(object_info[i].additive) {
    // Only additive objects, matches loading
}
Enter fullscreen mode Exit fullscreen mode

4.5 Results

There were some other errors, but it would take a lot more context to explain them, but in short, by the end of this phase, I had fixed:

  • 3 Buffer Overflows
  • 1 Heap Corruption (The LOS bug)
  • 2 Memory Leaks
  • 1 Double-Free Error

With these fixes, the game was now stable in Release mode, and running without a single report from AddressSanitizer.

5 - Replacing FMOD

By this point, the game was playable and stable, but it was completely silent. The final challenge was to restore the atmosphere that makes a horror game scary: the sound.

The game originally used FMOD, but trying to link a 2001 audio library to a 2025 operating system was a dead end. I needed a replacement that was modern, open-source, and easy to integrate. I chose miniaudio.

5.1 - The Choice: Why Miniaudio?

Unlike FMOD, miniaudio simplifies the development process as it only requires a single file (miniaudio.h) to be dropped into the project, eliminating the need for complex installation. Since the game's audio requirements were minimal—solely focused on playback, looping, and volume control—miniaudio was the perfect fit. The library also offers native support for essential features like mixing, volume management, and resource handling across Linux, Windows, and other platforms.

5.2 - The Wrapper: Faking the Old System

With the API issue resolved, the central challenge shifted to integrating the new library without rewriting the existing sound-playing code. To achieve this, I introduced a Wrapper API (audio.c). This wrapper acts as an intermediary, translating calls from the existing game code, which uses generic functions like PlaySound(), into the necessary miniaudio instructions. This strategic approach allowed for a fundamental change in the underlying audio engine while successfully preserving the original game logic.

5.3 - The Crisis: The ".it" vs ".wav" Corruption

Screenshot of the impulse tracker software

The original music assets were stored in the legacy Impulse Tracker (.it) format, dating back to the 1990s. Jeffrey Lim ('Pulse') created Impulse Tracker in 1995 as the spiritual successor to Scream Tracker 3. It quickly gained popularity within the demoscene and independent game development community due to its advanced features, including full 32-channel support, new note effects, and superior sample handling. Since miniaudio lacks support for .it files, all these assets were converted to the standard .wav format.

The change in file format, however, led to a significant issue. The map files, which contain the binary data defining the game levels, had specific references to files ending in the ".it" extension. Initially, I attempted a simple find-and-replace operation within the map files, changing text like "music.it" to "music.wav."

This action caused widespread corruption. Because the ".wav" extension (4 characters) is one character longer than ".it" (3 characters), the replacement shifted every subsequent byte in the binary file by one position. In a densely packed binary file structure, even a single-byte shift turns valid data—such as door triggers—into unusable garbage.

5.4 - The Solution: The Map Editor Tool

I realized I couldn't edit the raw files safely. I had to do it logically. Since I had already restored the Map Editor in Phase 1, I used it to build a migration tool.

Batch replace tool for music file extensions

  1. I wrote a script inside the editor to Load a map (reading the data into correct memory structures).
  2. The script then finds every string ending in .it and safely replaces it with .wav in memory.
  3. Finally, it Saves the map back to disk.

Because the editor knows how to write the file structure correctly, it handled the size difference automatically, preventing the "bit shift" corruption. With this final fix, the game was complete: stable, modernized, and fully audible.

6 - CI/CD - Making the game Cross-Platform

With the game stable and audible, I wanted to ensure it stayed that way. I set up a CI/CD pipeline using GitHub Actions. The goal was simple: every time I push code, a cloud server should automatically compile the game for both Linux and Windows, bundle the assets, and publish a release. Also I wanted to bundle with the game all the required libraries, so that everything the game needed to run was packaged with the final product.

The Linux build was straightforward—just a standard ubuntu-latest container installing necessary packages via apt and compiling the allegro library with only necessary components. The Windows build, however, turned into a nightmare.

6.1 - The Windows Problem

Mingw Installer window

I initially tried to compile the Windows version using modern tools (latest GCC/MinGW). It failed miserably. The game would crash every single time on the title screen.

I realized that Allegro 4 and the 2001 code were tightly coupled to the specific behavior of older compilers and APIs. Achieving the desired outcome required essential changes to the entire build process.

First, I manually installed an older version, MinGW 6.3.0, which I sourced from SourceForge, instead of installing the compiler through the use of the MSYS2’s package manager.

The second major hurdle was the incompatibility of modern Windows SDKs with the specific DirectDraw interfaces used by Fiend. This necessitated tracking down the original DirectX 8 SDK, which was eventually located in the archives of the official Allegro website.

Finally, a custom step was incorporated into the pipeline script. This step forcibly extracts the archaic SDK and injects its necessary header and library files directly into the path recognized by the MinGW compiler.

# A snippet of the "Digital Archaeology" in build.yml
- name: Injecting DX8 into MinGW...
  run: |
    Copy-Item -Path "C:\DX8\include\*" -Destination "C:\mingw32\include" -Recurse -Force
    Copy-Item -Path "C:\DX8\lib\*" -Destination "C:\mingw32\lib" -Recurse -Force
Enter fullscreen mode Exit fullscreen mode

6.2 - The "Text Test" Debugging

I only figured out the problem's root cause after I put together a small, focused test program just for checking out how text rendering worked in Allegro. When compiled with modern GCC, it failed. When compiled with the legacy stack, it worked. This confirmed that the issue wasn't my code—it was the toolchain.

#include <allegro.h>
#include <stdio.h>

int main(void)
{
    printf("Starting Allegro font test\n");
    allegro_message("[LOG] Starting Allegro font test\n");
    allegro_init();
    printf("After init\n");
    install_keyboard();
    set_color_depth(32);

    allegro_message("[LOG] Attempting to set graphics mode...\n");
    if (set_gfx_mode(GFX_AUTODETECT, 640, 480, 0, 0) != 0) {
        allegro_message("[ERROR] Failed to set graphics mode!\n%s", allegro_error);
        return 1;
    }
    allegro_message("[LOG] Graphics mode set successfully.\n");

    allegro_message("[LOG] Using Allegro's built-in font.\n");
    clear_to_color(screen, makecol(0,0,0));
    allegro_message("[LOG] Screen cleared. Attempting to render text...\n");
    textout_centre_ex(screen, font, "Text test in Windows Allegro", 320, 240, makecol(255,255,255), -1);
    allegro_message("[LOG] Text rendering attempted.\n");

    allegro_message("[LOG] Waiting for key press...\n");
    readkey();

    allegro_message("[LOG] Exiting.\n");
    allegro_exit();
    return 0;
}
END_OF_MAIN()
Enter fullscreen mode Exit fullscreen mode

Now, the repository works like a factory.

The repository functions as a build factory. For Linux, it performs a native build using bundled dependencies. Conversely, for Windows, it initiates a legacy environment to build the executable and bundle essential DLLs (such as libgcc and libstdc++).

The pipeline handles the automatic zipping and uploading of these builds as a release. This makes a fully playable, modern-compatible version of Fiend readily available for download, allowing anyone to use it without the need for compilation.

7 - Conclusion

So this conclude the process of how I restored the source code of the game “Fiend”. This was a highly enjoyable project that successfully refreshed my memory on various low-level concepts crucial for many programmers. Although I am sure that I may have missed some other interesting facts, I believe I grasped the most interesting points of the hole process.

To end this post I would like to answer a few questions that you may have about the project

How long did it took you to finish this project?

The project's completion time was approximately 50 days, or about a month and a half. However, as I took some breaks throughout the process, the precise number of working days is unknown.

Where did you get the original source code?

The original, unpatched code, was published by someone on Github, and can be found here. Please note this is the broken version and does not contain my necessary fixes.

Where I can download the game?

The modified source code and game binaries cannot be released at this time because the original 2001 license only allows for the game's distribution "in its original form."

I did find a repository claiming to allow "redistribution and use in source and binary forms, with or without modifications." However, the validity of this claim is uncertain, as it is not an official Frictional Games repository, nor does the individual who posted it appear to be affiliated with Frictional Games.

I have reached out to Thomas Grip (Frictional Games) to request permission to share this modernized port with the world. Until then, this post serves as a record of my journey into making this port.

Fingers crossed that we can bring Fiend back to the public soon! I will share any new project updates in a subsequent post here.

But that's about it, thank you for reading, and have a nice day!

Top comments (0)