diff --git a/FearTheTimeloop/dllmain.cpp b/FearTheTimeloop/dllmain.cpp new file mode 100644 index 0000000..8851daf --- /dev/null +++ b/FearTheTimeloop/dllmain.cpp @@ -0,0 +1,385 @@ +#include "CommonHeaders.h" +#include "UEngine.hpp" +#include "UETools.hpp" +#include "UEvars.hpp" +#include "Logger.hpp" +#include "SDK/Basic.hpp" +#include "SDK/Engine_classes.hpp" +#include "SDK/W_FullUI_classes.hpp" +#include "SDK/CBP_James_Cooper_classes.hpp" +#include "SDK/BPC_HealthSystem_classes.hpp" + +using namespace SDK; + +// Constants +const std::string PLUGIN_NAME = "FearTheTimeloop"; +const std::string PLUGIN_LOG = PLUGIN_NAME + ".log"; + +// Logger +std::shared_ptr logger; + +// Screen informations +static int screenWidth = GetSystemMetrics(SM_CXSCREEN); +static int screenHeight = GetSystemMetrics(SM_CYSCREEN); +static float aspectRatio = (float)screenWidth / screenHeight; +float baseAspect = 1.777778f; + +// Plugin states +static bool AOBScanDone = false; +static bool g_Console = false; +static bool g_fix_enabled = false; +static bool g_fov_fix_enabled = false; +static bool g_camera_fix_enabled = false; +static bool g_ultrawide_fix_enabled = false; +static bool g_HUD_fix_enabled = false; +static bool g_Camera_fix_enabled = false; +static bool g_Vignetting_fix_enabled = false; +static bool g_Fog_fix_enabled = false; +static bool g_TimeDilation_fix_enabled = false; +static bool g_GodMode_fix_enabled = false; +static int g_AdditionalFOVValue = 0; +static float g_cameraDistanceMultiplier = 1.f; +static float g_WorldTimeDilationValue = 1.f; +static float g_AITimeDilationValue = 1.f; +static int g_HUDOffsets = 0.f; +static bool user_inputs_logged = false; + +// Shared values +static float g_FOV_In = 90.f; +static float g_FOV_Out = 90.f; +static float g_Camera_In = 120; +static float g_Camera_Out = 120; + +// AOB Scan pointers +static uint8_t* FOVaddress = nullptr; +static uint8_t* Cameraaddress = nullptr; +static uint8_t* Vignettingaddress = nullptr; +static uint8_t* Fogaddress = nullptr; +static uint8_t* WorldTimedilationaddress = nullptr; + +// Hooking +static SafetyHookMid FOVHook{}; +static SafetyHookMid CameraHook{}; +static SafetyHookMid PEHook{}; +static SafetyHookMid WorldTimeDilationHook{}; + +// Prototypes +static void FOVFixEnabled(); +static void CameraFixEnabled(); +static void HUDFixEnabled(const std::vector& widgets); +static void LogHUD(GameFixes fix); +static void DOFFixEnabled(); +static void CAFixEnabled(); +static void VignettingFixEnabled(); +static void FogFixEnabled(); +static void EnableConsole(); +static void EnableCheats(Cheat cheat); +static void ProcessEvent(); + +// Unreal Engine widgets +static UW_FullUI_C* g_FullUI = nullptr; +static UUserWidget* g_MainMenu = nullptr; + +extern "C" __declspec(dllexport) void SetFixEnabled(bool enabled, bool init) { + g_fix_enabled = enabled; + if (!AOBScanDone) { // Unreal Engine 5.5.4 + logger->info("--------------- AOB scan started ---------------"); + constexpr auto FOVStringObfuscated = make_obfuscated<0xF3>("77 ?? 48 ?? ?? FF 90 ?? ?? ?? ?? F3 0F ?? ?? ?? 48 83 ?? ?? C3"); + constexpr auto CameraStringObfuscated = make_obfuscated<0xF3>("F3 0F 10 ?? ?? ?? ?? ?? F2 0F 10 ?? ?? ?? F2 0F 10 ?? ?? ?? 0F ?? ?? 0F"); + constexpr auto VignettingStringObfuscated = make_obfuscated<0x7D>("8B ?? 83 ?? ?? 7D ?? 44 89"); + constexpr auto FogStringObfuscated = make_obfuscated<0x75>("74 ?? 48 8B ?? ?? ?? ?? ?? 83 ?? ?? ?? 75 ?? 40 ?? ?? EB ?? 40 ?? ?? 48"); + constexpr auto WorldTimeDilationStringObfuscated = make_obfuscated<0x59>("F3 0F 10 ?? ?? ?? ?? ?? F3 0F 59 ?? ?? ?? ?? ?? F3 0F 59 ?? ?? ?? ?? ?? C3"); + + using AOBScan::Make; + using OffsetScan::Make; + // Prepare all data for scanning + std::vector signatures = { + Make(&FOVaddress, FOVStringObfuscated, "FOV"), + Make(&Cameraaddress, CameraStringObfuscated, "Camera distance"), + Make(&Vignettingaddress, VignettingStringObfuscated, "Vignetting"), + Make(&Fogaddress, FogStringObfuscated, "Fog"), + Make(&WorldTimedilationaddress, WorldTimeDilationStringObfuscated, "World time dilation"), + }; + // Scan all signature in a batch + Memory::AOBScanBatch(signatures, logger); + + if (FOVaddress && Cameraaddress && Vignettingaddress && Fogaddress && WorldTimedilationaddress) + logger->info("All AOB signatures found. Ready to patch..."); + + if (!GObjectsaddress || !AppendStringaddress || !ProcessEventaddress) { + logger->info("------------ UEngine offsets search ------------"); + uint8_t* baseModule = reinterpret_cast(GetModuleHandleA(nullptr)); // Get game base address + + constexpr auto GObjetcsStringObfuscated = make_obfuscated<0x8D>("48 8B ?? ?? ?? ?? ?? 48 8B ?? ?? 48 8D ?? ?? EB ?? 33"); + constexpr auto AppendStringStringObfuscated = make_obfuscated<0x80>("48 89 ?? ?? ?? 48 89 ?? ?? ?? 57 48 83 ?? ?? 80 3D ?? ?? ?? ?? ?? 48 ?? F2 8B ?? 48 ?? ?? 74 ?? 4C 8D ?? ?? ?? ?? ?? EB ?? 48 8D ?? ?? ?? ?? ?? E8 ?? ?? ?? ?? 4C"); + constexpr auto ProcessEventStringObfuscated = make_obfuscated<0x56>("40 ?? 56 57 41 ?? 41 ?? 41 ?? 41 ?? 48 81 ?? ?? ?? ?? ?? 48 8D ?? ?? ?? 48 89 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 ?? ?? 48 89 ?? ?? ?? ?? ?? 4D"); + + // Prepare all data for scanning + std::vector UEoffsetsScans = { + Make(&GObjectsaddress, GObjetcsStringObfuscated, "GObjects", OffsetCalcType::GetOffsetFromOpcode, &Offsets::GObjects, 0x3), + Make(&AppendStringaddress, AppendStringStringObfuscated, "AppendString", OffsetCalcType::UE_CalculateOffset, &Offsets::AppendString), + Make(&ProcessEventaddress, ProcessEventStringObfuscated, "ProcessEvent", OffsetCalcType::UE_CalculateOffset, &Offsets::ProcessEvent) + }; + // Retrieve all Unreal Engine offsets in a batch + Memory::OffsetScanBatch(UEoffsetsScans, baseModule, logger, ""); + } + logger->info("-------------- Fixes initialisation -------------"); + AOBScanDone = true; + } + + if (!init && FOVaddress) FOVFixEnabled(); + if (!init && Cameraaddress) CameraFixEnabled(); + if (!init) HUDFixEnabled({ g_FullUI, g_MainMenu }); + if (!init && Vignettingaddress) VignettingFixEnabled(); + if (!init && Fogaddress) FogFixEnabled(); + if (!init && WorldTimedilationaddress) { + EnableCheats(Cheat::TimeDilation); + EnableCheats(Cheat::GodMode); + } + ProcessEvent(); +} + +// Setters for Reshade addon call +extern "C" __declspec(dllexport) void SetFixesEnabled(GameFixes fix, bool enabled) { // Set each fix individually + if (fix == GameFixes::DevConsole) { g_Console = enabled; EnableConsole(); } + if (fix == GameFixes::FOV) { g_fov_fix_enabled = enabled; FOVFixEnabled(); } + if (fix == GameFixes::Camera) { g_camera_fix_enabled = enabled; CameraFixEnabled(); } + if (fix == GameFixes::HUD) { g_HUD_fix_enabled = enabled; HUDFixEnabled({ g_FullUI, g_MainMenu }); LogHUD(GameFixes::HUD); } + if (fix == GameFixes::UltraWide) { g_ultrawide_fix_enabled = enabled; HUDFixEnabled({ g_FullUI }); LogHUD(GameFixes::UltraWide); } + if (fix == GameFixes::Vignetting) { g_Vignetting_fix_enabled = enabled; VignettingFixEnabled(); } + if (fix == GameFixes::Fog) { g_Fog_fix_enabled = enabled; FogFixEnabled(); } + if (fix == GameFixes::TimeDilation) { g_TimeDilation_fix_enabled = enabled; EnableCheats(Cheat::TimeDilation); } + if (fix == GameFixes::GodMode) { g_GodMode_fix_enabled = enabled; EnableCheats(Cheat::GodMode); } +} + +extern "C" __declspec(dllexport) void SetValues(GameSetting setting, float value) { + if (setting == GameSetting::FOV) g_AdditionalFOVValue = (int)(value); + if (setting == GameSetting::CameraDistance) g_cameraDistanceMultiplier = value; + if (setting == GameSetting::WorldTimeDilation) g_WorldTimeDilationValue = value; + if (setting == GameSetting::AITimeDilation) { + g_AITimeDilationValue = value; + EnableCheats(Cheat::None); + } + if (setting == GameSetting::HUD) { + g_HUDOffsets = ((int)value * screenWidth) / 100; + HUDFixEnabled({ g_FullUI, g_MainMenu }); + } +} +// Getters for Reshade addon call +extern "C" __declspec(dllexport) void GetGameInfos(GameInfos* infos) { + if (!infos) return; + + infos->FOVIn = g_FOV_In; + infos->FOVOut = g_FOV_Out; + infos->cameraIn = g_Camera_In; + infos->cameraOut = g_Camera_Out; + infos->consoleEnabled = g_Console_Enabled; +} +// Hook injections +static APawn* g_Enemy = nullptr; +static ACBP_James_Cooper_C* g_James = nullptr; +static void ProcessEvent() { + if (!PEHook && ProcessEventaddress) { + PEHook = safetyhook::create_mid(ProcessEventaddress + 0xc, + [](SafetyHookContext& ctx) { + UObject* object = (UObject*)ctx.rcx; + UFunction* func = (UFunction*)ctx.rdx; + + if (object && func) { + std::string funcName = func->GetName(); + std::string objectName = object->GetName(); + + if (object && func && object->IsA(APawn::StaticClass())) { + APawn* pawn = (APawn*)object; + + if (funcName.contains("ReceiveTick")) { // Enable cheats based on Tick + if (!pawn->IsPlayerControlled() && pawn->Class->GetName().contains("BP_Zombie_C")) { + g_Enemy = pawn; + EnableCheats(Cheat::None); + } + } + } + + if (objectName.contains("James_Cooper_C") && funcName == "IsPlayerControlled") { // Enable cheats based on Tick + if (object->IsA(ACBP_James_Cooper_C::StaticClass())) { + g_James = (ACBP_James_Cooper_C*)object; + EnableCheats(Cheat::None); + } + } + if (objectName.contains("QuickMenu") || objectName.contains("DeathScreen")) { + if (funcName == "OnDeath" || funcName == "OnInputEnabled") { + g_Enemy = nullptr; + g_James = nullptr; + g_MainMenu = nullptr; + } + logger->debug("{} {}", objectName, funcName); + } + + if (objectName.contains("W_FullUI_C")) { + if (funcName == "Construct") g_FullUI = (UW_FullUI_C*)object; + if (funcName == "Destruct") g_FullUI = nullptr; + } + if (objectName.contains("W_MainMenu_C") && object->IsA(UUserWidget::StaticClass())) { + if (funcName == "Construct") { + g_MainMenu = (UUserWidget*)object; + HUDFixEnabled({ g_MainMenu }); + } + if (funcName == "Destruct") g_MainMenu = nullptr; + } + // This will be our Tick to apply HUD fix + if (objectName.contains("BPC_InteractionHandler") && funcName.contains("ReceiveTick")) + HUDFixEnabled({ g_FullUI }); + } + }); + } +} +// HUD fix hook UUserWidget -> AddToViewPort to log and know which HUD widget will be targeted +static void HUDFixEnabled(const std::vector& widgets) { + if (!g_fix_enabled || !(g_HUD_fix_enabled || g_ultrawide_fix_enabled)) return; + + for (auto* widget : widgets) { + if (!widget) continue; + + UWidget* root = widget->WidgetTree->RootWidget; + if (!root || !root->IsA(UCanvasPanel::StaticClass())) continue; + auto* panel = (UCanvasPanel*)root; + if (!panel) continue; + + for (int i = 0; i < panel->Slots.Num(); i++) { + UPanelSlot* slotBase = panel->Slots[i]; + if (!slotBase) continue; + UWidget* content = slotBase->Content; + if (!content) continue; + // Pillarboxing + if (content->GetName().contains("AspectRation") || content->GetName().contains("Fader")) { + content->SetVisibility(g_ultrawide_fix_enabled ? ESlateVisibility::Collapsed : ESlateVisibility::Visible); + continue; + } + // Set offsets to all other widgets + if (slotBase->IsA(UCanvasPanelSlot::StaticClass())) { + UCanvasPanelSlot* slot = (UCanvasPanelSlot*)slotBase; + if (!slot) continue; + + FMargin slotOffsets = slot->GetOffsets(); + slotOffsets.Left = g_HUD_fix_enabled ? g_HUDOffsets : 0.f; + slotOffsets.Right = g_HUD_fix_enabled ? g_HUDOffsets : 0.f; + slot->SetOffsets(slotOffsets); + } + } + } +} + +static void LogHUD(GameFixes fix) { + if (fix == GameFixes::HUD) logger->info("HUD fix {}", g_HUD_fix_enabled ? "enabled" : "disabled"); + if (fix == GameFixes::UltraWide) logger->info("Ultrawide fix {}", g_ultrawide_fix_enabled ? "enabled" : "disabled"); +} + +static void FOVFixEnabled() { + if (g_fix_enabled && g_fov_fix_enabled && FOVaddress) { + if (!FOVHook) { // Hook only once + FOVHook = safetyhook::create_mid(FOVaddress + 0x10, + [](SafetyHookContext& ctx) { + g_FOV_In = ctx.xmm0.f32[0]; + ctx.xmm0.f32[0] += (g_fix_enabled && g_fov_fix_enabled ? g_AdditionalFOVValue : 0); + g_FOV_Out = ctx.xmm0.f32[0]; + }); + } + } + logger->info("FOV fix {}", g_fov_fix_enabled ? "enabled" : "disabled"); +} + +static void CameraFixEnabled() { + if (g_fix_enabled && g_camera_fix_enabled && Cameraaddress) { + if (!CameraHook) { // Hook only once + CameraHook = safetyhook::create_mid(Cameraaddress + 0x8, + [](SafetyHookContext& ctx) { + g_Camera_In = ctx.xmm0.f32[0]; + ctx.xmm0.f32[0] *= g_cameraDistanceMultiplier; + g_Camera_Out = ctx.xmm0.f32[0]; + }); + } + else CameraHook.enable(); + } + if (!(g_fix_enabled && g_camera_fix_enabled) && Cameraaddress && CameraHook) + CameraHook.disable(); + + logger->info("Camera distance fix {}", g_fov_fix_enabled ? "enabled" : "disabled"); +} + +// Cheats +static double g_HealthReducePerSecond = 0; +static bool bIsReadHealthReducePerSecond = false; +static void EnableCheats(Cheat cheat) { + if (WorldTimedilationaddress && !WorldTimeDilationHook) { + WorldTimeDilationHook = safetyhook::create_mid(WorldTimedilationaddress + 0x10, + [](SafetyHookContext& ctx) { + // From AWorldSettings retrieved from world->K2_GetWorldSettings() + ctx.xmm0.f32[0] *= g_TimeDilation_fix_enabled ? g_WorldTimeDilationValue : 1.f; + }); + } + // Enemies time dilation + if (g_Enemy) g_Enemy->CustomTimeDilation = g_TimeDilation_fix_enabled ? g_AITimeDilationValue : 1.f; + // Player cheats + if (g_James && g_James->Class) { + double maxHealth = g_James->BPC_HealthSystem->m_MaxHealth; + double currentHealth = 0; + if (maxHealth > 0) { + if (!bIsReadHealthReducePerSecond) { + g_HealthReducePerSecond = g_James->BPC_HealthSystem->m_HealthReducePerSecond; + bIsReadHealthReducePerSecond = true; + } + g_James->BPC_HealthSystem->IsInvincible = g_GodMode_fix_enabled; + g_James->BPC_HealthSystem->m_HealthReducePerSecond = g_GodMode_fix_enabled ? 0 : g_HealthReducePerSecond; + g_James->BPC_HealthSystem->ReduceHealthFromStart = !g_GodMode_fix_enabled; + currentHealth = g_James->BPC_HealthSystem->m_CurrentHealth; + g_James->BPC_HealthSystem->m_CurrentHealth = g_GodMode_fix_enabled ? maxHealth : currentHealth; + } + } + + if (cheat == Cheat::TimeDilation) logger->info("Time dilation cheat {}", g_TimeDilation_fix_enabled ? "enabled" : "disabled"); + if (cheat == Cheat::GodMode) logger->info("God mode cheat {}", g_GodMode_fix_enabled ? "enabled" : "disabled"); +} + +// Memory patch fixes +static void VignettingFixEnabled() { + if (g_fix_enabled && g_Vignetting_fix_enabled && Vignettingaddress) + Memory::PatchBytes(Vignettingaddress, "\x31\xC9", 2); // xor ecx,ecx r.Tonemapper.Quality=0 + if (!(g_fix_enabled && g_Vignetting_fix_enabled) && Vignettingaddress) + Memory::RestoreBytes(Vignettingaddress); + + logger->info("Vignetting fix {}", g_Vignetting_fix_enabled ? "enabled" : "disabled"); +} + +static void FogFixEnabled() { + if (g_fix_enabled && g_Fog_fix_enabled && Fogaddress) + Memory::PatchBytes(Fogaddress, "\xEB", 1); // jmp -> r.Fog 0 + if (!(g_fix_enabled && g_Fog_fix_enabled) && Fogaddress) + Memory::RestoreBytes(Fogaddress); + + logger->info("Fog fix {}", g_Fog_fix_enabled ? "enabled" : "disabled"); +} +// UE Console creation +static void EnableConsole() { + if (g_Console_Enabled || !g_Console || !GObjectsaddress || !AppendStringaddress || !ProcessEventaddress) { + if (!g_Console && !user_inputs_logged) { + logger->info("------------------ User inputs ------------------"); + user_inputs_logged = true; + } + return; + } + + logger->info("-------------- Console re-enabling --------------"); + ReactivateDevConsole(logger); +} +// Standard dll entry +BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID) { + if (reason == DLL_PROCESS_ATTACH) { + logger = InitializeLogger("Fear the timeloop", PLUGIN_LOG); + logger->info("Plugin {} loaded.", PLUGIN_NAME); + } + else if (reason == DLL_PROCESS_DETACH) { + logger->info("Plugin {} unloaded.", PLUGIN_NAME); + spdlog::drop_all(); + } + return TRUE; +} \ No newline at end of file