c++ – Modding API for a game


This will be in a dll injected into a game’s process. The goal is to have a nice api to make it easier to develop mods. The api provides access to game variables, functions and events such as onDraw, onGameExit.

Below is a console app minimal working example that hopefully captures what I am trying to do:

#include <string>
#include "GameAPI.h"

struct Module {
    int x = 0;
    int y = 0;

    void enable() {
        //subscribe to events
        GameAPI::onDraw = std::bind(&Module::draw, this);
    }
    void disable() {
        //unsubscribe from events
        GameAPI::onDraw = nullptr;
    }    
    void draw() {
        auto hp = GameAPI::getPlayerHP();
        std::string hpstr = "HP: " + std::to_string(hp);
        GameAPI::drawString(x, y, hpstr);
    }
};

int main()
{ 
    GameAPI::installPatches();
    Module drawHpMod{ 33, 77 };     
    drawHpMod.enable();

    //simulate a few draw calls
    //normally the game would call game_daw in GameAPI.cpp where onDraw is raised
    GameAPI::onDraw();
    GameAPI::onDraw();
    GameAPI::onDraw();

    drawHpMod.disable();
    GameAPI::uninstallPatches();
}

GameAPI.h

#pragma once
#include <functional>
namespace GameAPI {
    void installPatches();
    void uninstallPatches();

    // variables
    extern int* vitality;
    
    // functions
    int getPlayerHP();
    void drawString(int x, int y, const std::string& str);
    
    // events - these will be proper events, probably boost signals
    extern std::function<void()> onDraw;
}

GameAPI.cpp

#include "GameAPI.h"
#include "Func.h"
#include "Var.h"

int* GameAPI::vitality = Var<int*>{ "N/A", 111 };
static auto game_getPlayerHP = Func<int __fastcall (int vitality, int unk)>{ "N/A", 222 };
static auto game_drawString = Func<void __stdcall (int x, int y, const char* str)>{ "N/A", 333 };
std::function<void()> GameAPI::onDraw = nullptr;
//Patch patch;

void GameAPI::installPatches()
{
    //apply patches to the game code
    //once the patches are installed the game will call
    //game_draw, game_update, game_exit etc...
    //For example:
    //patch = Patch((DWORD)game_draw_callback, Type::Jump, dllName, offset);
    //patch.install();
}

void GameAPI::uninstallPatches()
{
    //restore the original game code
    //patch.uninstall();
}

int GameAPI::getPlayerHP()
{
    return game_getPlayerHP(*vitality, 1);
}

void GameAPI::drawString(int x, int y, const std::string& str)
{
    game_drawString(x, y, str.c_str());
}

void game_draw() 
{
    if(GameAPI::onDraw) GameAPI::onDraw();
}

void __declspec(naked) game_draw_callback()
{
    __asm
    {
        pushad
        call game_draw
        popad
        pop EBP
        pop EBX
        add ESP, 8
        ret 4
    }
}

Var.h

#pragma once
#include "DllUtil.h"

template <class R>
struct Var {
private:
    R value = 0;

public:
    Var(const char* dll, int offset) {
        DWORD address = DllUtil::GetAddress(dll, offset);
        value = reinterpret_cast<R>(address);
    }
    operator R() { return value; }
};

Func.h

#pragma once
#include "DllUtil.h"

template<class R, class ... Args> class Func;

template<class R, class ... Args>
class Func<R __fastcall (Args... args)> {
    R(__fastcall* fPtr)(Args...) = 0;

public:
    Func(const char* dllName, int offset) {
        DWORD address = DllUtil::GetAddress(dllName, offset);
        fPtr = reinterpret_cast<R(__fastcall*)(Args...)>(address);
    }

    R operator() (Args... args) {
        return fPtr(args...);
    };
};

template<class R, class ... Args>
class Func<R __stdcall (Args... args)> {
    R(__stdcall* fPtr)(Args...) = 0;

public:
    Func(const char* dllName, int offset) {
        DWORD address = DllUtil::GetAddress(dllName, offset);
        fPtr = reinterpret_cast<R(__stdcall*)(Args...)>(address);
    }

    R operator() (Args... args) {
        return fPtr(args...);
    };
};

DllUtil.h

#pragma once
#include <wtypes.h>
#include <iostream>
namespace DllUtil {
    int mock_game_vitality = 17;
    int __fastcall mock_game_getPlayerHP(int vitality, int unk) {
        return vitality * unk * 10;
    }
    void __stdcall mock_game_drawString(int x, int y, const char* str) {
        std::cout << "{" << x << "," << y << "} " << str << std::endl;
    }
    DWORD GetAddress(const char* dllName, int offset) {
        //mock implementation. 
        //This would return the address of the variables and functions in the actual game
        if (offset == 111) return (DWORD)&mock_game_vitality;
        if (offset == 222) return (DWORD)&mock_game_getPlayerHP;
        if (offset == 333) return (DWORD)&mock_game_drawString;
        return 0;
    }
}

Some of my concerns are:

  • Patches, callbacks and events in GameAPI. A patch modifies the game code such that the game calls (in the example above) game_draw_callback, which calls game_draw where the onDraw event is raised. All that setup plus keeping track of patches is maybe too much responsibility for GameAPI.
  • Having Func and Var is maybe unnecessarily complicated. For Func I like the syntax slightly more than the function pointer syntax though, I think it’s easier to read.

Additional information: This is for a specific game, it’s not meant to be a general solution, most of the game’s state is global, the game is a 32 bit application.