From d21225bd9293a189130995202e87dc1f04d88a63 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 27 May 2024 15:35:25 +0200 Subject: [PATCH] WorldMapHealingRatePatch & set_world_map_heal_time metarule (#539) Fixes bug: healing time on worldmap is tied to real time clock, instead of game time. --- artifacts/scripting/functions.yml | 8 +++ artifacts/scripting/headers/sfall.h | 1 + sfall/Modules/Scripting/Handlers/Metarule.cpp | 1 + sfall/Modules/Scripting/Handlers/Worldmap.cpp | 4 ++ sfall/Modules/Scripting/Handlers/Worldmap.h | 2 + sfall/Modules/Worldmap.cpp | 51 ++++++++++++++++++- sfall/Modules/Worldmap.h | 1 + 7 files changed, 66 insertions(+), 2 deletions(-) diff --git a/artifacts/scripting/functions.yml b/artifacts/scripting/functions.yml index 1ab1429f..af7216bf 100644 --- a/artifacts/scripting/functions.yml +++ b/artifacts/scripting/functions.yml @@ -1427,6 +1427,14 @@ detail: void set_rest_heal_time(int time) doc: 'Sets the time interval in minutes for healing during resting. The default is 180. Note: The interval will be reset each time the player reloads the game.' macro: sfall.h + - name: set_worldmap_heal_time + detail: void set_worldmap_heal_time(int time) + doc: | + Sets the time interval in minutes for healing during worldmap travel. + - passing 0 will revert to 1 second per real time (vanilla behavior) + - passing -1 will disable healing during travel + - the interval will be reset each time the player reloads the game + macro: sfall.h - name: set_rest_mode detail: void set_rest_mode(int flags) doc: 'Sets the bit flags for the rest mode (see `RESTMODE_*` constants in **sfall.h**). Passing 0 will reset the rest mode. It will also be reset each time the player reloads the game.' diff --git a/artifacts/scripting/headers/sfall.h b/artifacts/scripting/headers/sfall.h index 2f299c70..8cf5b2b8 100644 --- a/artifacts/scripting/headers/sfall.h +++ b/artifacts/scripting/headers/sfall.h @@ -395,6 +395,7 @@ #define set_unique_id(obj) sfall_func1("set_unique_id", obj) #define set_unjam_locks_time(time) sfall_func1("set_unjam_locks_time", time) #define set_window_flag(winID, flag, value) sfall_func3("set_window_flag", winID, flag, value) +#define set_worldmap_heal_time(time) sfall_func1("set_worldmap_heal_time", time) #define show_win sfall_func0("show_window") #define show_window(winName) sfall_func1("show_window", winName) #define signal_close_game sfall_func0("signal_close_game") diff --git a/sfall/Modules/Scripting/Handlers/Metarule.cpp b/sfall/Modules/Scripting/Handlers/Metarule.cpp index e7e205d4..b84e8603 100644 --- a/sfall/Modules/Scripting/Handlers/Metarule.cpp +++ b/sfall/Modules/Scripting/Handlers/Metarule.cpp @@ -151,6 +151,7 @@ static const SfallMetarule metarules[] = { {"set_outline", mf_set_outline, 2, 2, -1, {ARG_OBJECT, ARG_INT}}, {"set_quest_failure_value", mf_set_quest_failure_value, 2, 2, -1, {ARG_INT, ARG_INT}}, {"set_rest_heal_time", mf_set_rest_heal_time, 1, 1, -1, {ARG_INT}}, + {"set_worldmap_heal_time", mf_set_worldmap_heal_time, 1, 1, -1, {ARG_INT}}, {"set_rest_mode", mf_set_rest_mode, 1, 1, -1, {ARG_INT}}, {"set_scr_name", mf_set_scr_name, 0, 1, -1, {ARG_STRING}}, {"set_selectable_perk_npc", mf_set_selectable_perk_npc, 5, 5, -1, {ARG_OBJECT, ARG_STRING, ARG_INT, ARG_INT, ARG_STRING}}, diff --git a/sfall/Modules/Scripting/Handlers/Worldmap.cpp b/sfall/Modules/Scripting/Handlers/Worldmap.cpp index aa887c06..0dd1b7af 100644 --- a/sfall/Modules/Scripting/Handlers/Worldmap.cpp +++ b/sfall/Modules/Scripting/Handlers/Worldmap.cpp @@ -202,6 +202,10 @@ void mf_set_rest_heal_time(OpcodeContext& ctx) { Worldmap::SetRestHealTime(ctx.arg(0).rawValue()); } +void mf_set_worldmap_heal_time(OpcodeContext& ctx) { + Worldmap::SetWorldMapHealTime(ctx.arg(0).rawValue()); +} + void mf_set_rest_mode(OpcodeContext& ctx) { Worldmap::SetRestMode(ctx.arg(0).rawValue()); } diff --git a/sfall/Modules/Scripting/Handlers/Worldmap.h b/sfall/Modules/Scripting/Handlers/Worldmap.h index add620d6..a7442e5b 100644 --- a/sfall/Modules/Scripting/Handlers/Worldmap.h +++ b/sfall/Modules/Scripting/Handlers/Worldmap.h @@ -50,6 +50,8 @@ void mf_get_map_enter_position(OpcodeContext&); void mf_set_rest_heal_time(OpcodeContext&); +void mf_set_worldmap_heal_time(OpcodeContext&); + void mf_set_rest_mode(OpcodeContext&); void mf_set_rest_on_map(OpcodeContext&); diff --git a/sfall/Modules/Worldmap.cpp b/sfall/Modules/Worldmap.cpp index c4bcb73c..860814ff 100644 --- a/sfall/Modules/Worldmap.cpp +++ b/sfall/Modules/Worldmap.cpp @@ -61,6 +61,11 @@ static DWORD worldMapTicks; static DWORD WorldMapEncounterRate; +static constexpr long WorldMapHealingDefaultInterval = 0; +// Healing interval in game time +static long worldMapHealingInterval = WorldMapHealingDefaultInterval; +static DWORD worldMapLastHealTime; + static double tickFract = 0.0; static double mapMultiMod = 1.0; static float scriptMapMulti = 1.0f; @@ -454,6 +459,40 @@ static void WorldmapFpsPatch() { } } +// Original function checks how many system Ticks (not game ticks) passed since last WM loop iteration and if more than 1000 ms, +// applies one healing to the whole party according to their healing rate. +// So we hijack it and return number bigger than 1000 to force the event, or 0 to skip it. +static DWORD __fastcall wmWorldMap_HealingTimeElapsed(DWORD elapsedTocks) { + if (worldMapHealingInterval == 0) return elapsedTocks; + if (worldMapHealingInterval > 0 && ((long)(fo::var::fallout_game_time - worldMapLastHealTime) > worldMapHealingInterval)) { + worldMapLastHealTime = fo::var::fallout_game_time; + return 10000; // force healing + } + return 0; // skip healing +} + +static void __declspec(naked) wmWorldMap_elapsed_tocks_hook() { + __asm { + push ecx; + call fo::funcoffs::elapsed_tocks_; + mov ecx, eax; + call wmWorldMap_HealingTimeElapsed; + pop ecx; + retn; + } +} + +static void WorldmapHealingRatePatch() { + dlogr("Applying world map healing rate patch.", DL_INIT); + HookCall(0x4C0085, wmWorldMap_elapsed_tocks_hook); + LoadGameHook::OnGameModeChange() += [](DWORD state) { + if (InWorldMap()) worldMapLastHealTime = fo::var::fallout_game_time; + }; + LoadGameHook::OnGameReset() += []() { + worldMapHealingInterval = WorldMapHealingDefaultInterval; + }; +} + static void PathfinderFixInit() { //if (IniReader::GetConfigInt("Misc", "PathfinderFix", 0)) { dlogr("Applying Pathfinder patch.", DL_INIT); @@ -585,6 +624,13 @@ void Worldmap::SetRestHealTime(DWORD minutes) { } } +void Worldmap::SetWorldMapHealTime(long minutes) { + worldMapHealingInterval = minutes >= 0 + ? minutes * 60 * 10 + : -1; + worldMapLastHealTime = fo::var::fallout_game_time; +} + void Worldmap::SetRestMode(DWORD mode) { RestRestore(); // restore default @@ -601,8 +647,8 @@ void Worldmap::SetRestMode(DWORD mode) { SafeWrite32(0x42E588, 0x94); // jmp 0x42E620 } if (mode & 4) { // bit3 - disable healing during resting - SafeWrite16(0x499FD4, 0x9090); - SafeWrite16(0x499E93, 0x8FEB); // jmp 0x499E24 + SafeWrite16(0x499FD4, 0x9090); // Check4Health_ + SafeWrite16(0x499E93, 0x8FEB); // TimedRest_: jmp 0x499E24 } } @@ -691,6 +737,7 @@ void Worldmap::init() { MapLimitsPatches(); WorldmapFpsPatch(); PipBoyAutomapsPatch(); + WorldmapHealingRatePatch(); // Add a flashing icon to the Horrigan encounter HookCall(0x4C071C, wmRndEncounterOccurred_hook); diff --git a/sfall/Modules/Worldmap.h b/sfall/Modules/Worldmap.h index d2b079be..6e76df09 100644 --- a/sfall/Modules/Worldmap.h +++ b/sfall/Modules/Worldmap.h @@ -36,6 +36,7 @@ public: static void SetCarInterfaceArt(DWORD artIndex); static void SetRestHealTime(DWORD minutes); + static void SetWorldMapHealTime(long minutes); static void SetRestMode(DWORD mode); static void SetRestMapLevel(int mapId, long elev, bool canRest); static long __fastcall GetRestMapLevel(long elev, int mapId);