Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
949 changes: 496 additions & 453 deletions ChaosMod/Main.cpp

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions ChaosMod/Memory/FiveMCompat.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#include <stdafx.h>

#include "FiveMCompat.h"

#include "Memory.h"
#include "Util/Logging.h"

#include <algorithm>
#include <array>
#include <charconv>

namespace
{
using namespace Memory;
using namespace Memory::FiveMCompat;

constexpr std::array<const wchar_t *, 10> g_FiveMModules = { {
L"gta-net-five.dll",
L"gta-core-five.dll",
L"net-five.dll",
L"adhesive.dll",
L"citizen-resources-client.dll",
L"ros-patches-five.dll",
L"citizen-scripting-gta.dll",
L"citizen-scripting-core.dll",
L"citizen-scripting-lua.dll",
L"asi-five.dll",
} };


// Static compatibility metadata used by low-level wrappers. These are integration-layer offsets
// (delta/index values), not absolute addresses, so they remain safe across ASLR.
constexpr NativeOffsets g_Build3717Offsets { 0x14, 0x20, 3 };
constexpr NativeOffsets g_Build3757Offsets { 0x18, 0x24, 3 };
constexpr NativeOffsets g_DefaultOffsets { 0x0, 0x0, 3 };

int ParseBuildNumber(const std::string &build)
{
if (build.empty())
return 0;

int parsedBuild = 0;
auto [ptr, ec] = std::from_chars(build.data(), build.data() + build.size(), parsedBuild);
if (ec != std::errc() || ptr == build.data())
return 0;

return parsedBuild;
}
}

namespace Memory::FiveMCompat
{
bool IsFiveM()
{
return std::any_of(g_FiveMModules.begin(), g_FiveMModules.end(), [](auto module) {
return GetModuleHandle(module) != nullptr;
});
}

ClientBuild DetectClientBuild()
{
if (!IsFiveM())
return ClientBuild::Unknown;

// FiveM does not expose an official build API through SHV, so reuse the game build string extracted
// by pattern scan and map it to known supported FiveM client builds.
switch (ParseBuildNumber(Memory::GetGameBuild()))
{
case 3717:
return ClientBuild::Build3717;
case 3757:
return ClientBuild::Build3757;
default:
return ClientBuild::Unknown;
}
}

bool IsSupportedClient(ClientBuild clientBuild)
{
return clientBuild == ClientBuild::Build3717 || clientBuild == ClientBuild::Build3757;
}

const char *ToString(ClientBuild clientBuild)
{
switch (clientBuild)
{
case ClientBuild::Build3717:
return "3717";
case ClientBuild::Build3757:
return "3757";
default:
return "unknown";
}
}


const NativeOffsets &GetNativeOffsets(ClientBuild clientBuild)
{
switch (clientBuild)
{
case ClientBuild::Build3717:
return g_Build3717Offsets;
case ClientBuild::Build3757:
return g_Build3757Offsets;
default:
return g_DefaultOffsets;
}
}

void LogCompatibilityStatus()
{
if (!IsFiveM())
{
LOG("Running on GTA V / ScriptHookV host (FiveM modules not detected)");
return;
}

auto clientBuild = DetectClientBuild();
if (IsSupportedClient(clientBuild))
{
const auto &offsets = GetNativeOffsets(clientBuild);
LOG("Compatible client FiveM " << ToString(clientBuild)
<< " detected. Enabling compatible native wrappers and guarded hooks.");
LOG("FiveM native offset profile: WAIT=" << offsets.WaitHandlerDelta
<< ", SCREEN_FADE=" << offsets.ScreenFadeHandlerDelta
<< ", SET_ENTITY_HEALTH args=" << offsets.SetEntityHealthArgCount);
}
else
{
LOG("Warning: FiveM detected but client build is not in the validated list (3717/3757). "
"Falling back to conservative compatibility mode.");
}
}
}
29 changes: 29 additions & 0 deletions ChaosMod/Memory/FiveMCompat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <string>

namespace Memory::FiveMCompat
{
// Explicit list of FiveM client builds the low-level integration layer was validated against.
enum class ClientBuild
{
Unknown = 0,
Build3717,
Build3757,
};

struct NativeOffsets
{
int WaitHandlerDelta = 0;
int ScreenFadeHandlerDelta = 0;
int SetEntityHealthArgCount = 3;
};

bool IsFiveM();
ClientBuild DetectClientBuild();
bool IsSupportedClient(ClientBuild clientBuild);
const char *ToString(ClientBuild clientBuild);
const NativeOffsets &GetNativeOffsets(ClientBuild clientBuild);
void LogCompatibilityStatus();
}

14 changes: 14 additions & 0 deletions ChaosMod/Memory/Hooks/ScriptThreadRunHook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "Memory/Hooks/Hook.h"
#include "Memory/Memory.h"
#include "Memory/FiveMCompat.h"
#include "Memory/Script.h"

#include "Components/Failsafe.h"
Expand Down Expand Up @@ -73,6 +74,19 @@ __int64 HK_rage__scrThread__Run(rage::scrThread *thread)

static bool OnHook()
{
#if defined(CHAOS_DISABLE_INCOMPATIBLE_FIVEM_HOOKS)
if (Memory::FiveMCompat::IsFiveM())
{
auto clientBuild = Memory::FiveMCompat::DetectClientBuild();
if (!Memory::FiveMCompat::IsSupportedClient(clientBuild))
{
LOG("Skipping rage::scrThread::Run hook on unsupported FiveM client build "
<< Memory::FiveMCompat::ToString(clientBuild) << ".");
return true;
}
}
#endif

Handle handle;

handle = Memory::FindPattern(
Expand Down
113 changes: 51 additions & 62 deletions ChaosMod/Memory/Memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Effects/EffectThreads.h"

#include "Memory/Hooks/Hook.h"
#include "Memory/FiveMCompat.h"

#include "Util/Script.h"
#include "Util/Text.h"
Expand Down Expand Up @@ -276,68 +277,56 @@ namespace Memory
return handle.At(2).Into().Get<DWORD64 *>();
}();

static auto fallbackToSHV = []() -> bool
{
const auto isFiveM = []()
{
// FiveM renamed its core modules in newer builds (e.g. b3570 uses adhesive.dll
// and ros-patches-five.dll). Check for a handful of known module names instead of
// only the legacy gta-net-five.dll so we can correctly detect FiveM across builds.
static const std::array<const wchar_t *, 10> modules = { {
L"gta-net-five.dll",
L"gta-core-five.dll",
L"net-five.dll",
L"adhesive.dll",
L"citizen-resources-client.dll",
L"ros-patches-five.dll",
// Additional FiveM modules observed in newer builds.
L"citizen-scripting-gta.dll",
L"citizen-scripting-core.dll",
L"citizen-scripting-lua.dll",
L"asi-five.dll",
} };

return std::any_of(modules.begin(), modules.end(), [](auto module) {
return GetModuleHandle(module) != nullptr;
});
};

bool fallbackToSHV = !globalPtr;

if (fallbackToSHV)
{
LOG("Warning: _globalPtr not found, falling back to SHV's getGlobalPtr");
}
// HACK: Check for the presence some arbitrary module specific to FiveM
// Also check if player is in a mp session if so to check for FiveM sp
else if (isFiveM())
{
bool modeDetermined = false;

auto handle = FindPattern("48 8B 0D ? ? ? ? E8 ? ? ? ? 84 C0 74 09 48 8D 15");
if (handle.IsValid())
{
auto _networkObj = handle.At(2).Into().Get<DWORD64>();
handle = FindPattern("83 B9 ? ? 00 00 05 0F 85 ? ? ? ? E9");
if (handle.IsValid())
{
fallbackToSHV = *reinterpret_cast<DWORD *>(*_networkObj + handle.At(2).Value<WORD>()) == 5;
modeDetermined = true;
}
}

if (!modeDetermined)
{
LOG("Warning: FiveM detected but could not determine mode, switching to fallback for GetGlobalPtr");
fallbackToSHV = true;
}
}

if (fallbackToSHV)
LOG("Warning: FiveM (non-sp) detected, features such as Failsafe will not work!");

return fallbackToSHV;
}();
static auto fallbackToSHV = []() -> bool
{
bool fallbackToSHV = !globalPtr;

if (fallbackToSHV)
{
LOG("Warning: _globalPtr not found, falling back to SHV's getGlobalPtr");
}
// HACK: Check for the presence some arbitrary module specific to FiveM
// Also check if player is in a mp session if so to check for FiveM sp
else if (FiveMCompat::IsFiveM())
{
const auto clientBuild = FiveMCompat::DetectClientBuild();
if (FiveMCompat::IsSupportedClient(clientBuild))
{
LOG("Compatible client FiveM " << FiveMCompat::ToString(clientBuild)
<< " detected for GetGlobalPtr routing.");
}
else
{
LOG("Warning: FiveM client build " << FiveMCompat::ToString(clientBuild)
<< " not explicitly validated for GetGlobalPtr routing.");
}

bool modeDetermined = false;

auto handle = FindPattern("48 8B 0D ? ? ? ? E8 ? ? ? ? 84 C0 74 09 48 8D 15");
if (handle.IsValid())
{
auto _networkObj = handle.At(2).Into().Get<DWORD64>();
handle = FindPattern("83 B9 ? ? 00 00 05 0F 85 ? ? ? ? E9");
if (handle.IsValid())
{
fallbackToSHV = *reinterpret_cast<DWORD *>(*_networkObj + handle.At(2).Value<WORD>()) == 5;
modeDetermined = true;
}
}

if (!modeDetermined)
{
LOG("Warning: FiveM detected but could not determine mode, switching to fallback for GetGlobalPtr");
fallbackToSHV = true;
}
}

if (fallbackToSHV)
LOG("Warning: FiveM (non-sp) detected, features such as Failsafe will not work!");

return fallbackToSHV;
}();

return fallbackToSHV ? getGlobalPtr(globalId) : (&globalPtr[globalId >> 18 & 0x3F][globalId & 0x3FFFF]);
}
Expand Down
35 changes: 31 additions & 4 deletions ChaosMod/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,34 @@
#include "Main.h"

#include "Memory/Memory.h"
#include "Memory/FiveMCompat.h"

#include "Util/CrashHandler.h"


static bool CanRegisterWithCurrentLoader()
{
// Modern FiveM clients can host ASI plugins only when ASI loading is enabled. We cannot force-enable
// that from inside the plugin, so only log capabilities and proceed conservatively.
if (Memory::FiveMCompat::IsFiveM())
{
const auto clientBuild = Memory::FiveMCompat::DetectClientBuild();
if (!Memory::FiveMCompat::IsSupportedClient(clientBuild))
{
LOG("Warning: FiveM ASI host detected with unsupported build "
<< Memory::FiveMCompat::ToString(clientBuild)
<< ". Script registration continues in compatibility mode.");
}
else
{
LOG("Compatible client FiveM " << Memory::FiveMCompat::ToString(clientBuild)
<< " detected during loader attach.");
}
}

return true;
}

BOOL APIENTRY DllMain(HMODULE instance, DWORD reason, LPVOID reserved)
{
switch (reason)
Expand All @@ -17,11 +42,13 @@ BOOL APIENTRY DllMain(HMODULE instance, DWORD reason, LPVOID reserved)

RAW_LOG("Chaos Mod v" MOD_VERSION "\n\n");

scriptRegister(instance, Main::OnRun);

keyboardHandlerRegister(Main::OnKeyboardInput);
if (CanRegisterWithCurrentLoader())
{
scriptRegister(instance, Main::OnRun);
keyboardHandlerRegister(Main::OnKeyboardInput);
}

break;
break;
}
case DLL_PROCESS_DETACH:
if (Main::IsMemoryInitialized())
Expand Down
Loading