[C++] Toggle-able persistent skin changer

Joined
Jul 30, 2023
Messages
5
Reaction score
4
Salutations, niggas.

I am GUWOP, coming from the trap, putting forward code for those who may find use for it.

Below, I attach the source code for a SA:MP skin changer, it automatically re-applies itself upon death, and can be toggled off, in which instance it'll reset to the skin you were wearing before.

The following code is for SA:MP 0.3.7-R5 (MD5 5BA5F0BE7AF99DFD03FB39E88A970A2B) and gta_sa (MD5 170B3A9108687B26DA2D8901C6948A18).

First, we need some state for our skin changer. I will provide a stripped down version of my context class.

Code:
class CPlayerController;
class CPlayer;
typedef CPlayer* (__thiscall* GetPlayerFn_t)(CPlayerController*);

enum class DebugMode : bool
{
    On = true,
    Off = false
};

enum class SkinChangerMode : bool
{
    On = true,
    Off = false
};

enum class UpdateSkinMode : uint8_t
{
    None = 0,
    Backup = 1,
    New = 2
};

class CData
{
public:
    CData();

    HMODULE GetSampDll() const;
    HMODULE GetGtaExe() const;

    // allows you extra commands and unlocks some functions
    void SetDebugMode(DebugMode);
    DebugMode GetDebugMode() const;

    //  skin changer functionality
    void SetSkinChangerMode(SkinChangerMode);
    SkinChangerMode GetSkinChangerMode() const;
    void SetBackupSkin(int);
    int GetBackupSkin();
    void SetSkin(int);
    int GetSkin() const;
    void UpdateSkinIfNeeded();

    CPlayer* GetPlayer(CPlayerController*) const;
    CPlayer* GetLocalPlayer() const;

private:
    HMODULE m_hmSampDll;
    HMODULE m_hmGtaExe;
    int* m_iDebugMode;
    SkinChangerMode m_skinChangerMode;
    int m_iSkin;
    int m_iBackupSkin;
    UpdateSkinMode m_updateSkinMode;
    GetPlayerFn_t m_fnGetPlayer;
    CPlayerController* m_pLocalPlayerController;
};

Now, the code for the class
Code:
// Offsets
static constexpr DWORD g_debugMode = 0x26DFE8;
static constexpr DWORD g_fnGetPlayer = 0x1010;
static constexpr DWORD g_localPlayer = 0x26EBAC;

CData::CData() 
{
    m_hmSampDll = GetModuleHandle(L"samp");
    m_hmGtaExe = GetModuleHandle(L"gta_sa");
    m_iDebugMode = (int*)((DWORD)(m_hmSampDll)+g_debugMode);
    m_skinChangerMode = SkinChangerMode::Off;
    m_iSkin = -1;
    m_iBackupSkin = -1;
    m_updateSkinMode = UpdateSkinMode::None;
    m_fnGetPlayer = (GetPlayerFn_t)((DWORD)(m_hmSampDll)+g_fnGetPlayer);
    m_pLocalPlayerController = *(CPlayerController**)((DWORD)(m_hmSampDll)+g_localPlayer);
}

HMODULE CData::GetSampDll() const
{
    return m_hmSampDll;
}

HMODULE CData::GetGtaExe() const
{
    return m_hmGtaExe;
}

CContext* CData::GetContext() const
{
    return m_pContext;
}

void CData::SetDebugMode(DebugMode s)
{
    *m_iDebugMode = (s == DebugMode::On) ? 1 : 0;
}

DebugMode CData::GetDebugMode() const
{
    if ((*m_iDebugMode) > 0)
        return DebugMode::On;

    return DebugMode::Off;
}

void CData::SetSkinChangerMode(SkinChangerMode s)
{
    SkinChangerMode modeOld = m_skinChangerMode;
    m_skinChangerMode = s;
    if (m_skinChangerMode != modeOld)
    {
        if (modeOld == SkinChangerMode::On)
            m_updateSkinMode = UpdateSkinMode::Backup;
        else
            m_updateSkinMode = UpdateSkinMode::New;
    }
}

SkinChangerMode CData::GetSkinChangerMode() const
{
    return m_skinChangerMode;
}

void CData::SetBackupSkin(int skin)
{
    m_iBackupSkin = skin;
}

int CData::GetBackupSkin()
{
    return m_iBackupSkin;
}

void CData::SetSkin(int skin)
{
    m_iSkin = skin;
    if (GetSkinChangerMode() == SkinChangerMode::On)
        m_updateSkinMode = UpdateSkinMode::New;
}

int CData::GetSkin() const
{
    return m_iSkin;
}

void CData::UpdateSkinIfNeeded()
{
    static constexpr DWORD g_setPlayerSkin = 0x68D00;
    if (m_updateSkinMode != UpdateSkinMode::None)
    {
        int skin = (m_updateSkinMode == UpdateSkinMode::Backup) ? m_iBackupSkin : m_iSkin;
        // we use this for a return address
        // we could also just encode our intent in skin instead to make sure we are looking at our own call...
        // but that isn't really worth it.
        typedef void(__cdecl* SetPlayerSkinFn_t)(const char*);
        static SetPlayerSkinFn_t fnSetPlayerSkin = (SetPlayerSkinFn_t)((DWORD)(m_hmSampDll)+g_setPlayerSkin);
        fnSetPlayerSkin(std::to_string(skin).c_str());

#if _DEBUG
        WriteToChat("{ffff00}SkinChanger: {ff00ff}Updated skin to {ffffff}%d", skin);
#endif
        
        m_updateSkinMode = UpdateSkinMode::None;
    }
}

CPlayer* CData::GetPlayer(CPlayerController* pPlayerController) const
{
    return m_fnGetPlayer(pPlayerController);
}

CPlayer* CData::GetLocalPlayer() const
{
    return GetPlayer(m_pLocalPlayerController);
}

Now using this logic in some hooks which handle it
Code:
static constexpr DWORD g_localPlayerSkinRetaddrs[] = { 0x3d37, 0x192e9 };
static constexpr DWORD g_localPlayerSkinBackupIgnoreRetaddrs[] = { 0x68D6A }; 
typedef int(__thiscall* SetPlayerSkinHook_t)(CPlayer*, int);
static SetPlayerSkinHook_t g_ogSetPlayerSkinHook = nullptr;
static int __fastcall SetPlayerSkinHook(CPlayer* self, DWORD, int skin)
{
    // these are probably a bit pedantic... just wanted to make sure there's no oddities
    if (IS_RETADDR_INSIDE(g_localPlayerSkinRetaddrs) && (self == g_data.GetLocalPlayer()))
    {
        // don't update backup if we are setting skin ourselves
        if (!IS_RETADDR_INSIDE(g_localPlayerSkinBackupIgnoreRetaddrs))
            g_data.SetBackupSkin(skin); 

        if (g_data.GetSkinChangerMode() == SkinChangerMode::On)
            skin = g_data.GetSkin();
    }

    Hook::Remove(SetPlayerSkinHook);
    int r = g_ogSetPlayerSkinHook(self, skin);
    Hook::Reinstall(SetPlayerSkinHook);
    
    return r;
}

typedef int(__cdecl* GtaSaEveryFrame_t)(int, int, int);
static GtaSaEveryFrame_t g_ogGtaSaEveryFrame = nullptr;
int __cdecl GtaSaEveryFrameHook(int a, int b, int c)
{
    // events
    g_data.UpdateSkinIfNeeded();

    // whatever else u be doing cuh... removed for obv reasons

    Hook::Remove(GtaSaEveryFrameHook);
    int r = g_ogGtaSaEveryFrame(a, b, c);
    Hook::Reinstall(GtaSaEveryFrameHook);

    return r;
}

static constexpr DWORD g_fnGtaSaEveryFrame = 0x7F99B0; // can't disassemble in IDA, try ghidra decompiler
                                                        // it's also stored inside "dword_C97B24 + 152" but
                                                        // going there after, the fp is null
static constexpr DWORD g_fnSetPlayerSkin = 0xaff50;
void Hooks::Install()
{
    Hook::Install((void*)((DWORD)(g_data.GetGtaExe()) + g_fnGtaSaEveryFrame), GtaSaEveryFrameHook, (void**)&g_ogGtaSaEveryFrame);
    Hook::Install((void*)((DWORD)(g_data.GetSampDll()) + g_fnSetPlayerSkin), SetPlayerSkinHook, (void**)&g_ogSetPlayerSkinHook);
}

And a simple example of how to set it up:
Code:
    g_data.SetDebugMode(DebugMode::On);
    g_data.SetSkinChangerMode(SkinChangerMode::On);
    g_data.SetSkin(294);
    Hooks::Install();

Kind Regards,
GUWOP.
 
Top