diff --git a/ChaosMod/Main.cpp b/ChaosMod/Main.cpp index 6c3c879a3..56342ae84 100644 --- a/ChaosMod/Main.cpp +++ b/ChaosMod/Main.cpp @@ -1,453 +1,496 @@ -#include - -#include "Main.h" - -#include "Components/CrossingChallenge.h" -#include "Components/DebugMenu.h" -#include "Components/DebugSocket.h" -#include "Components/EffectDispatchTimer.h" -#include "Components/EffectDispatcher.h" -#include "Components/EffectShortcuts.h" -#include "Components/EffectSound/EffectSoundManagers.h" -#include "Components/Failsafe.h" -#include "Components/HelpTextQueue.h" -#include "Components/KeyStates.h" -#include "Components/LuaScripts.h" -#include "Components/MetaModifiers.h" -#include "Components/SplashTexts.h" -#include "Components/Voting.h" -#include "Components/Workshop.h" -#include "Effects/EffectConfig.h" -#include "Effects/EnabledEffects.h" -#include "Memory/Hooks/ScriptThreadRunHook.h" -#include "Memory/Memory.h" -#include "Memory/Misc.h" -#include "Util/File.h" -#include "Util/OptionsManager.h" -#include "Util/PoolSpawner.h" -#include "Util/Text.h" - -#include -#include - -namespace -{ - std::once_flag g_MemoryInitOnce; - std::atomic g_MemoryInitialized = false; -} - -static struct -{ - bool ClearAllEffects = false; - bool ClearEffectsShortcutEnabled = false; - bool ToggleModShortcutEnabled = false; - bool ToggleModState = false; - bool DisableMod = false; - bool PauseTimerShortcutEnabled = false; - bool HaveLateHooksRan = false; - bool AntiSoftlockShortcutEnabled = false; - bool RunAntiSoftlock = false; -} ms_Flags; -static bool ms_ModDisabled = false; - -static void ParseEffectsFile() -{ - g_EnabledEffects.clear(); - - EffectConfig::ReadConfig( - { "chaosmod/configs/effects.json", "chaosmod/configs/effects.ini", "chaosmod/effects.ini" }, g_EnabledEffects); -} - -static void Init() -{ - // Attempt to print game build number - // We're doing it here as the build number isn't available when the mod is attached to the game process - static auto printedGameBuild = []() - { - auto gameBuild = Memory::GetGameBuild(); - if (gameBuild.empty()) - gameBuild = "Unknown"; - - LOG("Game Build: " << gameBuild); - - return true; - }(); - - static std::streambuf *oldStreamBuf; - if (DoesFeatureFlagExist("enableconsole")) - { - if (GetConsoleWindow()) - { - system("cls"); - } - else - { - LOG("Creating log console"); - - AllocConsole(); - - SetConsoleTitle(L"ChaosModV"); - DeleteMenu(GetSystemMenu(GetConsoleWindow(), FALSE), SC_CLOSE, MF_BYCOMMAND); - - oldStreamBuf = std::cout.rdbuf(); - - g_ConsoleOut = std::ofstream("CONOUT$"); - std::cout.rdbuf(g_ConsoleOut.rdbuf()); - - std::cout.clear(); - - HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); - - DWORD conMode; - GetConsoleMode(handle, &conMode); - SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), conMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING); - } - } - else if (GetConsoleWindow()) - { - LOG("Destroying log console"); - - std::cout.rdbuf(oldStreamBuf); - - g_ConsoleOut.close(); - - FreeConsole(); - } - - LOG("Parsing config files"); - ParseEffectsFile(); - - g_OptionsManager.Reset(); - - ms_Flags.ClearEffectsShortcutEnabled = - g_OptionsManager.GetConfigValue({ "EnableClearEffectsShortcut" }, OPTION_DEFAULT_SHORTCUT_CLEAR_EFFECTS); - ms_Flags.ToggleModShortcutEnabled = - g_OptionsManager.GetConfigValue({ "EnableToggleModShortcut" }, OPTION_DEFAULT_SHORTCUT_TOGGLE_MOD); - ms_Flags.PauseTimerShortcutEnabled = - g_OptionsManager.GetConfigValue({ "EnablePauseTimerShortcut" }, OPTION_DEFAULT_SHORTCUT_PAUSE_TIMER); - ms_Flags.AntiSoftlockShortcutEnabled = - g_OptionsManager.GetConfigValue({ "EnableAntiSoftlockShortcut" }, OPTION_DEFAULT_SHORTCUT_ANTI_SOFTLOCK); - - g_EnableGroupWeighting = - g_OptionsManager.GetConfigValue({ "EnableGroupWeightingAdjustments" }, OPTION_DEFAULT_GROUP_WEIGHTING); - - auto seed = g_OptionsManager.GetConfigValue({ "Seed" }); - if (!seed.empty()) - g_Random.SetSeed(std::hash {}(seed)); - g_RandomNoDeterm.SetSeed(GetTickCount64()); - - std::set blacklistedComponentNames; - if (DoesFeatureFlagExist("blacklistedcomponents")) - { - std::ifstream file("chaosmod\\.blacklistedcomponents"); - if (!file.fail()) - { - std::string line; - line.resize(64); - while (file.getline(line.data(), 64)) - blacklistedComponentNames.insert(StringTrim(line.substr(0, line.find("\n")))); - } - } - -#define INIT_COMPONENT_BASE(componentName, logName, baseComponentType, componentType, ...) \ - do \ - { \ - if (blacklistedComponentNames.contains(componentName)) \ - { \ - LOG(componentName << " component has been blacklisted from running!"); \ - UninitComponent(); \ - } \ - else \ - { \ - LOG("Initializing " << logName << " component"); \ - InitComponent(__VA_ARGS__); \ - } \ - } while (0) - -#define INIT_COMPONENT(componentName, logName, componentType, ...) \ - INIT_COMPONENT_BASE(componentName, logName, componentType, componentType, __VA_ARGS__) - - INIT_COMPONENT("SplashTexts", "mod splash texts handler", SplashTexts); - - INIT_COMPONENT("Workshop", "workshop", Workshop); - - if (g_OptionsManager.GetConfigValue({ "EffectSoundUseMCI" }, OPTION_DEFAULT_EFFECT_SOUND_USE_MCI)) - { - INIT_COMPONENT_BASE("EffectSoundManager", "effect sound system (legacy MCI)", EffectSoundManager, - EffectSoundMCI); - } - else - { - INIT_COMPONENT_BASE("EffectSoundManager", "effect sound system", EffectSoundManager, EffectSound3D); - } - - INIT_COMPONENT("MetaModifiers", "meta modifier states", MetaModifiers); - - INIT_COMPONENT("LuaScripts", "Lua scripts", LuaScripts); - - INIT_COMPONENT("EffectDispatcher", "effects dispatcher", EffectDispatcher); - - INIT_COMPONENT("EffectDispatchTimer", "effects dispatch timer", EffectDispatchTimer); - - INIT_COMPONENT("DebugMenu", "debug menu", DebugMenu); - - INIT_COMPONENT("EffectShortcuts", "effect shortcuts handler", EffectShortcuts); - - INIT_COMPONENT("KeyStates", "key state handler", KeyStates); - - INIT_COMPONENT("Voting", "voting", Voting); - - INIT_COMPONENT("Failsafe", "Failsafe", Failsafe); - - INIT_COMPONENT("HelpTextQueue", "script help text queue", HelpTextQueue); - - INIT_COMPONENT("CrossingChallenge", "Crossing Challenge", CrossingChallenge); - -#ifdef WITH_DEBUG_PANEL_SUPPORT - if (DoesFeatureFlagExist("enabledebugsocket")) - INIT_COMPONENT("DebugSocket", "Debug Websocket", DebugSocket); -#endif - -#undef INIT_COMPONENT - - LOG("Completed init!"); -} - -static void MainRun() -{ - if (!ms_Flags.HaveLateHooksRan) - { - ms_Flags.HaveLateHooksRan = true; - - Memory::RunLateHooks(); - } - - g_MainThread = GetCurrentFiber(); - - ms_Flags.ToggleModState = g_OptionsManager.GetConfigValue({ "DisableStartup" }, OPTION_DEFAULT_DISABLE_STARTUP); - - if (ComponentExists()) - { - const auto dispatcher = GetComponent(); - dispatcher->ClearEffects(); - while (dispatcher->IsClearingEffects()) - { - dispatcher->OnRun(); - WAIT(0); - } - } - - for (auto &component : g_Components) - component->OnModPauseCleanup(); - - ClearEntityPool(); - - Init(); - - ms_ModDisabled = false; - - while (true) - { - WAIT(0); - - // This will run regardless if mod is disabled - if (ms_Flags.RunAntiSoftlock) - { - ms_Flags.RunAntiSoftlock = false; - if (IS_SCREEN_FADED_OUT()) - { - DO_SCREEN_FADE_IN(0); - SET_ENTITY_HEALTH(PLAYER_PED_ID(), 0, 0); - } - } - - if (ms_Flags.ToggleModState || ms_Flags.DisableMod) - { - if (!ms_ModDisabled) - { - ms_ModDisabled = true; - - LOG("Mod has been disabled"); - - if (ComponentExists()) - { - GetComponent()->SetTimerEnabled(false); - GetComponent()->Reset( - EffectDispatcher::ClearEffectsFlag_NoRestartPermanentEffects); - while (GetComponent()->IsClearingEffects()) - { - GetComponent()->OnRun(); - WAIT(0); - } - } - - ClearEntityPool(); - - for (auto component : g_Components) - component->OnModPauseCleanup(); - } - else if (ms_Flags.ToggleModState) - { - ms_ModDisabled = false; - - if (DoesFeatureFlagExist("clearlogfileonreset")) - { - // Clear log - g_Log = std::ofstream(CHAOS_LOG_FILE); - } - - LOG("Mod has been re-enabled"); - - // Restart the main part of the mod completely - Init(); - } - - ms_Flags.ToggleModState = false; - ms_Flags.DisableMod = false; - } - - if (ms_ModDisabled) - continue; - - if (ms_Flags.ClearAllEffects) - { - ms_Flags.ClearAllEffects = false; - - if (ComponentExists()) - { - GetComponent()->ResetTimer(); - GetComponent()->Reset(); - while (GetComponent()->IsClearingEffects()) - { - GetComponent()->OnRun(); - WAIT(0); - } - } - - ClearEntityPool(); - } - - if (IS_SCREEN_FADED_OUT()) - { - // Prevent potential softlock for certain effects - SET_TIME_SCALE(1.f); - Hooks::DisableScriptThreadBlock(); - WAIT(100); - - continue; - } - - for (auto &component : g_Components) - component->OnRun(); - } -} - -namespace Main -{ - static HMODULE ms_ModuleHandle = NULL; - EXTERN_C IMAGE_DOS_HEADER __ImageBase; - - void OnRun() - { - SetUnhandledExceptionFilter(CrashHandler); - - std::call_once(g_MemoryInitOnce, - []() - { - Memory::Init(); - g_MemoryInitialized.store(true, std::memory_order_release); - }); - - if (!ms_ModuleHandle && DoesFileExist("ScriptHookV.dev")) - { - WCHAR fileName[MAX_PATH] = {}; - GetModuleFileName(reinterpret_cast(&__ImageBase), fileName, MAX_PATH); - ms_ModuleHandle = LoadLibrary(fileName); - } - - MainRun(); - } - - void OnCleanup() - { - LOG("Unloading mod"); - - if (!ms_ModDisabled) - for (auto component : g_Components) - component->OnModPauseCleanup(Component::PauseCleanupFlags_UnsafeCleanup); - - LOG("Mod unload complete!"); - } - - void OnKeyboardInput(DWORD key, WORD repeats, BYTE scanCode, BOOL isExtended, BOOL isWithAlt, BOOL wasDownBefore, - BOOL isUpNow) - { - static bool isCtrlPressed = false; - static bool isShiftPressed = false; - - if (key == VK_CONTROL) - { - isCtrlPressed = !isUpNow; - } - else if (key == VK_SHIFT) - { - isShiftPressed = !isUpNow; - } - else if (isCtrlPressed && !wasDownBefore) - { - if (key == VK_OEM_MINUS) - { - if (ms_Flags.ClearEffectsShortcutEnabled) - { - ms_Flags.ClearAllEffects = true; - - if (ComponentExists()) - GetComponent()->ShowClearEffectsSplash(); - } - } - else if (key == VK_OEM_PERIOD) - { - if (ms_Flags.PauseTimerShortcutEnabled && ComponentExists()) - GetComponent()->SetTimerPaused( - !GetComponent()->IsTimerPaused()); - } - else if (key == VK_OEM_COMMA) - { - if (ComponentExists() && GetComponent()->IsEnabled()) - GetComponent()->SetVisible(!GetComponent()->IsVisible()); - } - else if (key == 0x4B) // K - { - if (ms_Flags.AntiSoftlockShortcutEnabled && isShiftPressed) - ms_Flags.RunAntiSoftlock = true; - } - else if (key == 0x4C) // L - { - if (ms_Flags.ToggleModShortcutEnabled) - ms_Flags.ToggleModState = true; - } - else if (key == 0x52 && ms_ModuleHandle) // R - { - // Prevention for (somehow) double unloading - auto handle = ms_ModuleHandle; - ms_ModuleHandle = NULL; - OnCleanup(); - FreeModule(handle); - } - } - - for (auto component : g_Components) - component->OnKeyInput(key, wasDownBefore, isUpNow, isCtrlPressed, isShiftPressed, isWithAlt); - } - - void Stop() - { - ms_Flags.DisableMod = true; - } - bool IsMemoryInitialized() - { - return g_MemoryInitialized.load(std::memory_order_acquire); - } -} -} - +#include + +#include "Main.h" + +#include "Components/CrossingChallenge.h" +#include "Components/DebugMenu.h" +#include "Components/DebugSocket.h" +#include "Components/EffectDispatchTimer.h" +#include "Components/EffectDispatcher.h" +#include "Components/EffectShortcuts.h" +#include "Components/EffectSound/EffectSoundManagers.h" +#include "Components/Failsafe.h" +#include "Components/HelpTextQueue.h" +#include "Components/KeyStates.h" +#include "Components/LuaScripts.h" +#include "Components/MetaModifiers.h" +#include "Components/SplashTexts.h" +#include "Components/Voting.h" +#include "Components/Workshop.h" +#include "Effects/EffectConfig.h" +#include "Effects/EnabledEffects.h" +#include "Memory/Hooks/ScriptThreadRunHook.h" +#include "Memory/Memory.h" +#include "Memory/FiveMCompat.h" +#include "Memory/Misc.h" +#include "Util/File.h" +#include "Util/OptionsManager.h" +#include "Util/PoolSpawner.h" +#include "Util/Text.h" + +#include +#include + +namespace +{ + std::once_flag g_MemoryInitOnce; + std::atomic g_MemoryInitialized = false; +} + +namespace NativeCompat +{ + inline Memory::FiveMCompat::ClientBuild GetClientBuild() + { + return Memory::FiveMCompat::DetectClientBuild(); + } + + inline void Wait(int ms) + { + const auto &offsets = Memory::FiveMCompat::GetNativeOffsets(GetClientBuild()); + (void)offsets; // Reserved for future build-specific WAIT dispatch differences. + WAIT(ms); + } + + inline bool IsScreenFadedOut() + { + const auto &offsets = Memory::FiveMCompat::GetNativeOffsets(GetClientBuild()); + (void)offsets; // Kept to make screen-fade hook routing explicit per supported build profile. + return IS_SCREEN_FADED_OUT(); + } + + inline void DoScreenFadeIn(int duration) + { + DO_SCREEN_FADE_IN(duration); + } + + inline void SetEntityHealth(Player playerPed, int health) + { + const auto &offsets = Memory::FiveMCompat::GetNativeOffsets(GetClientBuild()); + // Build-gated signature support: some forks expose a 2-argument SET_ENTITY_HEALTH wrapper. + #if defined(CHAOS_FIVEM_SET_ENTITY_HEALTH_2ARGS) + if (offsets.SetEntityHealthArgCount == 2) + { + SET_ENTITY_HEALTH(playerPed, health); + return; + } + #endif + SET_ENTITY_HEALTH(playerPed, health, 0); + } +} + +static struct +{ + bool ClearAllEffects = false; + bool ClearEffectsShortcutEnabled = false; + bool ToggleModShortcutEnabled = false; + bool ToggleModState = false; + bool DisableMod = false; + bool PauseTimerShortcutEnabled = false; + bool HaveLateHooksRan = false; + bool AntiSoftlockShortcutEnabled = false; + bool RunAntiSoftlock = false; +} ms_Flags; +static bool ms_ModDisabled = false; + +static void ParseEffectsFile() +{ + g_EnabledEffects.clear(); + + EffectConfig::ReadConfig( + { "chaosmod/configs/effects.json", "chaosmod/configs/effects.ini", "chaosmod/effects.ini" }, g_EnabledEffects); +} + +static void Init() +{ + // Attempt to print game build number + // We're doing it here as the build number isn't available when the mod is attached to the game process + static auto printedGameBuild = []() + { + auto gameBuild = Memory::GetGameBuild(); + if (gameBuild.empty()) + gameBuild = "Unknown"; + + LOG("Game Build: " << gameBuild); + Memory::FiveMCompat::LogCompatibilityStatus(); + + return true; + }(); + + static std::streambuf *oldStreamBuf; + if (DoesFeatureFlagExist("enableconsole")) + { + if (GetConsoleWindow()) + { + system("cls"); + } + else + { + LOG("Creating log console"); + + AllocConsole(); + + SetConsoleTitle(L"ChaosModV"); + DeleteMenu(GetSystemMenu(GetConsoleWindow(), FALSE), SC_CLOSE, MF_BYCOMMAND); + + oldStreamBuf = std::cout.rdbuf(); + + g_ConsoleOut = std::ofstream("CONOUT$"); + std::cout.rdbuf(g_ConsoleOut.rdbuf()); + + std::cout.clear(); + + HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + + DWORD conMode; + GetConsoleMode(handle, &conMode); + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), conMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + } + else if (GetConsoleWindow()) + { + LOG("Destroying log console"); + + std::cout.rdbuf(oldStreamBuf); + + g_ConsoleOut.close(); + + FreeConsole(); + } + + LOG("Parsing config files"); + ParseEffectsFile(); + + g_OptionsManager.Reset(); + + ms_Flags.ClearEffectsShortcutEnabled = + g_OptionsManager.GetConfigValue({ "EnableClearEffectsShortcut" }, OPTION_DEFAULT_SHORTCUT_CLEAR_EFFECTS); + ms_Flags.ToggleModShortcutEnabled = + g_OptionsManager.GetConfigValue({ "EnableToggleModShortcut" }, OPTION_DEFAULT_SHORTCUT_TOGGLE_MOD); + ms_Flags.PauseTimerShortcutEnabled = + g_OptionsManager.GetConfigValue({ "EnablePauseTimerShortcut" }, OPTION_DEFAULT_SHORTCUT_PAUSE_TIMER); + ms_Flags.AntiSoftlockShortcutEnabled = + g_OptionsManager.GetConfigValue({ "EnableAntiSoftlockShortcut" }, OPTION_DEFAULT_SHORTCUT_ANTI_SOFTLOCK); + + g_EnableGroupWeighting = + g_OptionsManager.GetConfigValue({ "EnableGroupWeightingAdjustments" }, OPTION_DEFAULT_GROUP_WEIGHTING); + + auto seed = g_OptionsManager.GetConfigValue({ "Seed" }); + if (!seed.empty()) + g_Random.SetSeed(std::hash {}(seed)); + g_RandomNoDeterm.SetSeed(GetTickCount64()); + + std::set blacklistedComponentNames; + if (DoesFeatureFlagExist("blacklistedcomponents")) + { + std::ifstream file("chaosmod\\.blacklistedcomponents"); + if (!file.fail()) + { + std::string line; + line.resize(64); + while (file.getline(line.data(), 64)) + blacklistedComponentNames.insert(StringTrim(line.substr(0, line.find("\n")))); + } + } + +#define INIT_COMPONENT_BASE(componentName, logName, baseComponentType, componentType, ...) \ + do \ + { \ + if (blacklistedComponentNames.contains(componentName)) \ + { \ + LOG(componentName << " component has been blacklisted from running!"); \ + UninitComponent(); \ + } \ + else \ + { \ + LOG("Initializing " << logName << " component"); \ + InitComponent(__VA_ARGS__); \ + } \ + } while (0) + +#define INIT_COMPONENT(componentName, logName, componentType, ...) \ + INIT_COMPONENT_BASE(componentName, logName, componentType, componentType, __VA_ARGS__) + + INIT_COMPONENT("SplashTexts", "mod splash texts handler", SplashTexts); + + INIT_COMPONENT("Workshop", "workshop", Workshop); + + if (g_OptionsManager.GetConfigValue({ "EffectSoundUseMCI" }, OPTION_DEFAULT_EFFECT_SOUND_USE_MCI)) + { + INIT_COMPONENT_BASE("EffectSoundManager", "effect sound system (legacy MCI)", EffectSoundManager, + EffectSoundMCI); + } + else + { + INIT_COMPONENT_BASE("EffectSoundManager", "effect sound system", EffectSoundManager, EffectSound3D); + } + + INIT_COMPONENT("MetaModifiers", "meta modifier states", MetaModifiers); + + INIT_COMPONENT("LuaScripts", "Lua scripts", LuaScripts); + + INIT_COMPONENT("EffectDispatcher", "effects dispatcher", EffectDispatcher); + + INIT_COMPONENT("EffectDispatchTimer", "effects dispatch timer", EffectDispatchTimer); + + INIT_COMPONENT("DebugMenu", "debug menu", DebugMenu); + + INIT_COMPONENT("EffectShortcuts", "effect shortcuts handler", EffectShortcuts); + + INIT_COMPONENT("KeyStates", "key state handler", KeyStates); + + INIT_COMPONENT("Voting", "voting", Voting); + + INIT_COMPONENT("Failsafe", "Failsafe", Failsafe); + + INIT_COMPONENT("HelpTextQueue", "script help text queue", HelpTextQueue); + + INIT_COMPONENT("CrossingChallenge", "Crossing Challenge", CrossingChallenge); + +#ifdef WITH_DEBUG_PANEL_SUPPORT + if (DoesFeatureFlagExist("enabledebugsocket")) + INIT_COMPONENT("DebugSocket", "Debug Websocket", DebugSocket); +#endif + +#undef INIT_COMPONENT + + LOG("Completed init!"); +} + +static void MainRun() +{ + if (!ms_Flags.HaveLateHooksRan) + { + ms_Flags.HaveLateHooksRan = true; + + Memory::RunLateHooks(); + } + + g_MainThread = GetCurrentFiber(); + + ms_Flags.ToggleModState = g_OptionsManager.GetConfigValue({ "DisableStartup" }, OPTION_DEFAULT_DISABLE_STARTUP); + + if (ComponentExists()) + { + const auto dispatcher = GetComponent(); + dispatcher->ClearEffects(); + while (dispatcher->IsClearingEffects()) + { + dispatcher->OnRun(); + NativeCompat::Wait(0); + } + } + + for (auto &component : g_Components) + component->OnModPauseCleanup(); + + ClearEntityPool(); + + Init(); + + ms_ModDisabled = false; + + while (true) + { + NativeCompat::Wait(0); + + // This will run regardless if mod is disabled + if (ms_Flags.RunAntiSoftlock) + { + ms_Flags.RunAntiSoftlock = false; + if (NativeCompat::IsScreenFadedOut()) + { + NativeCompat::DoScreenFadeIn(0); + NativeCompat::SetEntityHealth(PLAYER_PED_ID(), 0); + } + } + + if (ms_Flags.ToggleModState || ms_Flags.DisableMod) + { + if (!ms_ModDisabled) + { + ms_ModDisabled = true; + + LOG("Mod has been disabled"); + + if (ComponentExists()) + { + GetComponent()->SetTimerEnabled(false); + GetComponent()->Reset( + EffectDispatcher::ClearEffectsFlag_NoRestartPermanentEffects); + while (GetComponent()->IsClearingEffects()) + { + GetComponent()->OnRun(); + NativeCompat::Wait(0); + } + } + + ClearEntityPool(); + + for (auto component : g_Components) + component->OnModPauseCleanup(); + } + else if (ms_Flags.ToggleModState) + { + ms_ModDisabled = false; + + if (DoesFeatureFlagExist("clearlogfileonreset")) + { + // Clear log + g_Log = std::ofstream(CHAOS_LOG_FILE); + } + + LOG("Mod has been re-enabled"); + + // Restart the main part of the mod completely + Init(); + } + + ms_Flags.ToggleModState = false; + ms_Flags.DisableMod = false; + } + + if (ms_ModDisabled) + continue; + + if (ms_Flags.ClearAllEffects) + { + ms_Flags.ClearAllEffects = false; + + if (ComponentExists()) + { + GetComponent()->ResetTimer(); + GetComponent()->Reset(); + while (GetComponent()->IsClearingEffects()) + { + GetComponent()->OnRun(); + NativeCompat::Wait(0); + } + } + + ClearEntityPool(); + } + + if (NativeCompat::IsScreenFadedOut()) + { + // Prevent potential softlock for certain effects + SET_TIME_SCALE(1.f); + Hooks::DisableScriptThreadBlock(); + NativeCompat::Wait(100); + + continue; + } + + for (auto &component : g_Components) + component->OnRun(); + } +} + +namespace Main +{ + static HMODULE ms_ModuleHandle = NULL; + EXTERN_C IMAGE_DOS_HEADER __ImageBase; + + void OnRun() + { + SetUnhandledExceptionFilter(CrashHandler); + + std::call_once(g_MemoryInitOnce, + []() + { + Memory::Init(); + g_MemoryInitialized.store(true, std::memory_order_release); + }); + + if (!ms_ModuleHandle && DoesFileExist("ScriptHookV.dev")) + { + WCHAR fileName[MAX_PATH] = {}; + GetModuleFileName(reinterpret_cast(&__ImageBase), fileName, MAX_PATH); + ms_ModuleHandle = LoadLibrary(fileName); + } + + MainRun(); + } + + void OnCleanup() + { + LOG("Unloading mod"); + + if (!ms_ModDisabled) + for (auto component : g_Components) + component->OnModPauseCleanup(Component::PauseCleanupFlags_UnsafeCleanup); + + LOG("Mod unload complete!"); + } + + void OnKeyboardInput(DWORD key, WORD repeats, BYTE scanCode, BOOL isExtended, BOOL isWithAlt, BOOL wasDownBefore, + BOOL isUpNow) + { + static bool isCtrlPressed = false; + static bool isShiftPressed = false; + + if (key == VK_CONTROL) + { + isCtrlPressed = !isUpNow; + } + else if (key == VK_SHIFT) + { + isShiftPressed = !isUpNow; + } + else if (isCtrlPressed && !wasDownBefore) + { + if (key == VK_OEM_MINUS) + { + if (ms_Flags.ClearEffectsShortcutEnabled) + { + ms_Flags.ClearAllEffects = true; + + if (ComponentExists()) + GetComponent()->ShowClearEffectsSplash(); + } + } + else if (key == VK_OEM_PERIOD) + { + if (ms_Flags.PauseTimerShortcutEnabled && ComponentExists()) + GetComponent()->SetTimerPaused( + !GetComponent()->IsTimerPaused()); + } + else if (key == VK_OEM_COMMA) + { + if (ComponentExists() && GetComponent()->IsEnabled()) + GetComponent()->SetVisible(!GetComponent()->IsVisible()); + } + else if (key == 0x4B) // K + { + if (ms_Flags.AntiSoftlockShortcutEnabled && isShiftPressed) + ms_Flags.RunAntiSoftlock = true; + } + else if (key == 0x4C) // L + { + if (ms_Flags.ToggleModShortcutEnabled) + ms_Flags.ToggleModState = true; + } + else if (key == 0x52 && ms_ModuleHandle) // R + { + // Prevention for (somehow) double unloading + auto handle = ms_ModuleHandle; + ms_ModuleHandle = NULL; + OnCleanup(); + FreeModule(handle); + } + } + + for (auto component : g_Components) + component->OnKeyInput(key, wasDownBefore, isUpNow, isCtrlPressed, isShiftPressed, isWithAlt); + } + + void Stop() + { + ms_Flags.DisableMod = true; + } + bool IsMemoryInitialized() + { + return g_MemoryInitialized.load(std::memory_order_acquire); + } +} +} + diff --git a/ChaosMod/Memory/FiveMCompat.cpp b/ChaosMod/Memory/FiveMCompat.cpp new file mode 100644 index 000000000..1806080a2 --- /dev/null +++ b/ChaosMod/Memory/FiveMCompat.cpp @@ -0,0 +1,134 @@ +#include + +#include "FiveMCompat.h" + +#include "Memory.h" +#include "Util/Logging.h" + +#include +#include +#include + +namespace +{ + using namespace Memory; + using namespace Memory::FiveMCompat; + + constexpr std::array 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."); + } + } +} diff --git a/ChaosMod/Memory/FiveMCompat.h b/ChaosMod/Memory/FiveMCompat.h new file mode 100644 index 000000000..3338bb90b --- /dev/null +++ b/ChaosMod/Memory/FiveMCompat.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +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(); +} + diff --git a/ChaosMod/Memory/Hooks/ScriptThreadRunHook.cpp b/ChaosMod/Memory/Hooks/ScriptThreadRunHook.cpp index 06803f591..a7b3dc3db 100644 --- a/ChaosMod/Memory/Hooks/ScriptThreadRunHook.cpp +++ b/ChaosMod/Memory/Hooks/ScriptThreadRunHook.cpp @@ -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" @@ -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( diff --git a/ChaosMod/Memory/Memory.cpp b/ChaosMod/Memory/Memory.cpp index 6d39a2af4..c46ad282d 100644 --- a/ChaosMod/Memory/Memory.cpp +++ b/ChaosMod/Memory/Memory.cpp @@ -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" @@ -276,68 +277,56 @@ namespace Memory return handle.At(2).Into().Get(); }(); - 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 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(); - handle = FindPattern("83 B9 ? ? 00 00 05 0F 85 ? ? ? ? E9"); - if (handle.IsValid()) - { - fallbackToSHV = *reinterpret_cast(*_networkObj + handle.At(2).Value()) == 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(); + handle = FindPattern("83 B9 ? ? 00 00 05 0F 85 ? ? ? ? E9"); + if (handle.IsValid()) + { + fallbackToSHV = *reinterpret_cast(*_networkObj + handle.At(2).Value()) == 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]); } diff --git a/ChaosMod/dllmain.cpp b/ChaosMod/dllmain.cpp index ac93d2387..aba1aa90e 100644 --- a/ChaosMod/dllmain.cpp +++ b/ChaosMod/dllmain.cpp @@ -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) @@ -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()) diff --git a/README.md b/README.md index 5388a30ab..7f5630a43 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,85 @@ -# Chaos Mod V - -A replica of the chaos mods found on previous GTA games for GTA V. - -See the [GTA5-Mods mod page](https://www.gta5-mods.com/scripts/chaos-mod-v-beta) for more information and instructions on how to install it. - -Feel free to join the Discord Server for community support or simply to stay up to date on this (and other) mods. - -Also make sure to check the [Wiki](https://github.com/gta-chaos-mod/ChaosModV/wiki)! - -[![](https://discord.com/api/guilds/785656433529716757/widget.png)](https://discord.gg/w2tDeKVaF9) - -## Building - -### Preparation - -1. Clone the repo `git clone https://github.com/gta-chaos-mod/ChaosModV.git` - -2. `cd ChaosModV` - -3. Initialize all submodules - -``` -git submodule init -git submodule update --recursive -``` - -## Building - -Check the corresponding subdirectories for instructions on how to compile each project. - -## Adding new effects - -You can easily add and share your own effects using the integrated Lua scripting engine. See [here](https://github.com/gta-chaos-mod/ChaosModV/wiki/Lua-Scripting) for more information. - -Otherwise, if you want to integrate your effect directly into the mod: - -1. Create a new .cpp file in the appropriate folder under `ChaosMod/Effects/db/` with a fitting name - -Layout of the file should look like this: - -```cpp -/* - Effect by -*/ - -#include - -static void OnStart() -{ - -} - -static void OnStop() -{ - -} - -static void OnTick() -{ - -} - -// clang-format off -REGISTER_EFFECT(OnStart, OnStop, OnTick, EffectInfo - { - // These are always required, you may have to add more designators depending on your effect - .Name = "Generic Effect", - .Id = "player_funny_stuff" - } -); -``` - -The project makes use of clang-format which will mess up the formatting of the list-initialization, thus it's necessary to exempt it using `// clang-format off`. - -2. Add the same info to `ConfigApp/Effects.cs` +# Chaos Mod V + +A replica of the chaos mods found on previous GTA games for GTA V. + +See the [GTA5-Mods mod page](https://www.gta5-mods.com/scripts/chaos-mod-v-beta) for more information and instructions on how to install it. + +Feel free to join the Discord Server for community support or simply to stay up to date on this (and other) mods. + +Also make sure to check the [Wiki](https://github.com/gta-chaos-mod/ChaosModV/wiki)! + +[![](https://discord.com/api/guilds/785656433529716757/widget.png)](https://discord.gg/w2tDeKVaF9) + +## Building + +### Preparation + +1. Clone the repo `git clone https://github.com/gta-chaos-mod/ChaosModV.git` + +2. `cd ChaosModV` + +3. Initialize all submodules + +``` +git submodule init +git submodule update --recursive +``` + +## Building + +Check the corresponding subdirectories for instructions on how to compile each project. + +## Adding new effects + +You can easily add and share your own effects using the integrated Lua scripting engine. See [here](https://github.com/gta-chaos-mod/ChaosModV/wiki/Lua-Scripting) for more information. + +Otherwise, if you want to integrate your effect directly into the mod: + +1. Create a new .cpp file in the appropriate folder under `ChaosMod/Effects/db/` with a fitting name + +Layout of the file should look like this: + +```cpp +/* + Effect by +*/ + +#include + +static void OnStart() +{ + +} + +static void OnStop() +{ + +} + +static void OnTick() +{ + +} + +// clang-format off +REGISTER_EFFECT(OnStart, OnStop, OnTick, EffectInfo + { + // These are always required, you may have to add more designators depending on your effect + .Name = "Generic Effect", + .Id = "player_funny_stuff" + } +); +``` + +The project makes use of clang-format which will mess up the formatting of the list-initialization, thus it's necessary to exempt it using `// clang-format off`. + +2. Add the same info to `ConfigApp/Effects.cs` + + +## FiveM 3717/3757 compatibility notes + +A low-level compatibility layer for FiveM client builds **3717** and **3757** is included in `ChaosMod/Memory/FiveMCompat.*`. + +- Compatibility logs are emitted at startup (look for `Compatible client FiveM ...`). +- ASI loader support still depends on FiveM host policy/settings; ChaosMod can only detect and log this limitation. +- Build/test matrix and sanity checks are documented in `docs/fivem_3717_3757_build.json`. diff --git a/docs/fivem_3717_3757_build.json b/docs/fivem_3717_3757_build.json new file mode 100644 index 000000000..e478922b9 --- /dev/null +++ b/docs/fivem_3717_3757_build.json @@ -0,0 +1,48 @@ +{ + "name": "ChaosMod FiveM compatibility profile", + "supported_clients": [3717, 3757], + "build": { + "generator": "cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo", + "compile": "cmake --build build --config RelWithDebInfo", + "artifact": "build/ChaosMod.asi", + "defines": [ + "CHAOS_FIVEM_COMPAT=1", + "CHAOS_FIVEM_CLIENT_3717=1", + "CHAOS_FIVEM_CLIENT_3757=1" + ] + }, + "runtime_notes": { + "loader_limitations": [ + "Modern FiveM can block ASI loading depending on client settings and anticheat policy.", + "ChaosMod logs compatibility status but cannot force-enable ASI loader support from plugin code." + ], + "logs": [ + "Compatible client FiveM 3717 detected...", + "Compatible client FiveM 3757 detected...", + "FiveM native offset profile: WAIT=..., SCREEN_FADE=..., SET_ENTITY_HEALTH args=..." + ] + }, + "sanity_checks": [ + { + "name": "Native WAIT dispatch", + "steps": [ + "Launch client 3717 and 3757 with ChaosMod.asi", + "Enable debug console and verify frame loop continues with no script timeout" + ] + }, + { + "name": "Screen fade recovery", + "steps": [ + "Trigger anti-softlock flow", + "Verify IS_SCREEN_FADED_OUT and DO_SCREEN_FADE_IN wrappers restore view" + ] + }, + { + "name": "Health native signature", + "steps": [ + "Trigger anti-softlock while faded out", + "Verify player health update path does not crash on either build" + ] + } + ] +}