From 4f342176258d56d9228b1f7bc1c7a2d2bd1d4d28 Mon Sep 17 00:00:00 2001 From: NovaRain Date: Sat, 26 Nov 2022 09:57:01 +0800 Subject: [PATCH] Backported game hooks from 4.0.x * COMBATTURN is not implemented. Updated documents. --- artifacts/scripting/headers/define_extra.h | 2 +- artifacts/scripting/headers/sfall.h | 4 + artifacts/scripting/hookscripts.md | 42 +++++ sfall/FalloutEngine/FunctionOffsets_def.h | 1 + sfall/FalloutEngine/Functions_def.h | 7 +- sfall/Modules/HookScripts.cpp | 6 +- sfall/Modules/HookScripts.h | 8 +- sfall/Modules/HookScripts/Common.cpp | 24 ++- sfall/Modules/HookScripts/MiscHs.cpp | 180 +++++++++++++++++++++ sfall/Modules/HookScripts/MiscHs.h | 3 + sfall/Modules/Karma.cpp | 27 +--- sfall/Modules/Karma.h | 4 + 12 files changed, 276 insertions(+), 32 deletions(-) diff --git a/artifacts/scripting/headers/define_extra.h b/artifacts/scripting/headers/define_extra.h index 5e6f722d..beee1b96 100644 --- a/artifacts/scripting/headers/define_extra.h +++ b/artifacts/scripting/headers/define_extra.h @@ -146,7 +146,7 @@ //#define RMOBJ_AI_USE_DRUG_ON_2 4360176 // same as RMOBJ_AI_USE_DRUG_ON (obsolete, use only for sfall before 4.3.1/3.8.31) #define RMOBJ_BARTER_ARMOR 4675656 // removing armor from NPC's slot before entering the barter screen #define RMOBJ_BARTER_WEAPON 4675722 // removing weapon from NPC's slot before entering the barter screen -#define RMOBJ_INVEN_DROP_CAPS 4667295 // if money/caps are dropped manually by the player from the inventory screen +#define RMOBJ_INVEN_DROP_CAPS 4667295 // if multiple money/caps are dropped manually by the player from the inventory screen #define RMOBJ_DROP_INTO_CONTAINER 4678833 // when dropping items into a container item (bag/backpack) // old defines #define RMOBJ_RM_MULT_OBJS RMOBJ_ITEM_REMOVED_MULTI diff --git a/artifacts/scripting/headers/sfall.h b/artifacts/scripting/headers/sfall.h index c4f20d40..8a6344df 100644 --- a/artifacts/scripting/headers/sfall.h +++ b/artifacts/scripting/headers/sfall.h @@ -52,6 +52,10 @@ #define HOOK_INVENTORYMOVE (24) #define HOOK_INVENWIELD (25) #define HOOK_ADJUSTFID (26) +//#define HOOK_COMBATTURN (27) // unimplemented +#define HOOK_CARTRAVEL (28) +#define HOOK_SETGLOBALVAR (29) +#define HOOK_RESTTIMER (30) #define HOOK_GAMEMODECHANGE (31) // Valid arguments to list_begin diff --git a/artifacts/scripting/hookscripts.md b/artifacts/scripting/hookscripts.md index 6e157804..435bced1 100644 --- a/artifacts/scripting/hookscripts.md +++ b/artifacts/scripting/hookscripts.md @@ -582,6 +582,48 @@ int ret0 - overrides the calculated FID with provided value ------------------------------------------- +#### `HOOK_CARTRAVEL (hs_cartravel.int)` + +Runs continuously during worldmap travel on car. + +``` +int arg0 - vanilla car speed (between 3 and 8 "steps") +int arg1 - vanilla fuel consumption (100 and below) + +int ret0 - car speed override (pass -1 if you just want to override fuel consumption) +int ret1 - fuel consumption override +``` + +------------------------------------------- + +#### `HOOK_SETGLOBALVAR (hs_setglobalvar.int)` + +Runs when setting the value of a global variable. + +``` +int arg0 - the index number of the global variable being set +int arg1 - the set value of the global variable + +int ret0 - overrides the value of the global variable +``` + +------------------------------------------- + +#### `HOOK_RESTTIMER (hs_resttimer.int)` + +Runs continuously while the player is resting (using pipboy alarm clock). + +``` +int arg0 - the game time in ticks +int arg1 - event type: 1 - when the resting ends normally, -1 - when pressing ESC to cancel the timer, 0 - otherwise +int arg2 - the hour part of the length of resting time +int arg3 - the minute part of the length of resting time + +int ret0 - pass 1 to interrupt the resting, pass 0 to continue the rest if it was interrupted by pressing ESC key +``` + +------------------------------------------- + #### `HOOK_GAMEMODECHANGE (hs_gamemodechange.int)` Runs once every time when the game mode was changed, like opening/closing the inventory, character screen, pipboy, etc. diff --git a/sfall/FalloutEngine/FunctionOffsets_def.h b/sfall/FalloutEngine/FunctionOffsets_def.h index 0347d369..ad052ca0 100644 --- a/sfall/FalloutEngine/FunctionOffsets_def.h +++ b/sfall/FalloutEngine/FunctionOffsets_def.h @@ -732,6 +732,7 @@ FUNC(windowOutput_, 0x4B80A4) FUNC(windowShow_, 0x4B7648) FUNC(windowWidth_, 0x4B7734) FUNC(windowWrapLineWithSpacing_, 0x4B8854) +FUNC(wmCarUseGas_, 0x4C4DA4) FUNC(wmDrawCursorStopped_, 0x4C41EC) FUNC(wmEvalTileNumForPlacement_, 0x4C1A64) FUNC(wmFindCurSubTileFromPos_, 0x4C0C00) diff --git a/sfall/FalloutEngine/Functions_def.h b/sfall/FalloutEngine/Functions_def.h index 7669ea32..b041e645 100644 --- a/sfall/FalloutEngine/Functions_def.h +++ b/sfall/FalloutEngine/Functions_def.h @@ -58,8 +58,6 @@ WRAP_WATCOM_FFUNC4(void, register_object_call, long*, target, long*, source, voi WRAP_WATCOM_FFUNC3(long, scr_get_local_var, long, sid, long, varId, long*, value) WRAP_WATCOM_FFUNC3(long, scr_set_local_var, long, sid, long, varId, long, value) WRAP_WATCOM_FFUNC6(long, text_object_create, fo::GameObject*, object, const char*, text, long, font, long, colorText, long, colorOutline, fo::BoundRect*, rect) -WRAP_WATCOM_FFUNC3(long, tile_coord, long, tile, long*, outX, long*, outY) // the fourth argument of the function is not used -WRAP_WATCOM_FFUNC3(long, tile_num_in_direction, long, tile, long, rotation, long, distance) WRAP_WATCOM_FFUNC8(void, trans_cscale, void*, fromBuff, long, width, long, height, long, fromPitch, void*, toBuff, long, toWidth, long, toHeight, long, toPitch) WRAP_WATCOM_FFUNC3(void, win_clip, fo::Window*, window, fo::RectList**, rects, void*, buffer) WRAP_WATCOM_FFUNC6(void, win_print, long, winID, const char*, text, long, textWidth, long, xPos, long, yPos, long, colorFlags) @@ -99,6 +97,7 @@ WRAP_WATCOM_FUNC2(void, executeProcedure, fo::Program*, sptr, long, procNum) WRAP_WATCOM_FUNC1(long, folder_print_line, const char*, text) WRAP_WATCOM_FUNC1(const char*, findCurrentProc, fo::Program*, program) // Returns the name of current procedure by program pointer WRAP_WATCOM_FUNC1(long, FMtext_width, const char*, text) +WRAP_WATCOM_FUNC1(long, game_get_global_var, long, globalVar) WRAP_WATCOM_FUNC0(long, get_input) // Searches for message ID in given message file and places result in result argument WRAP_WATCOM_FUNC3(const char*, getmsg, const fo::MessageList*, fileAddr, fo::MessageNode*, result, long, messageId) @@ -222,9 +221,11 @@ WRAP_WATCOM_FUNC2(void, skill_set_tags, long*, tags, long, num) WRAP_WATCOM_FUNC2(long, stat_level, fo::GameObject*, critter, long, statId) WRAP_WATCOM_FUNC1(void, stat_pc_add_experience, long, amount) // Adds experience points to PC WRAP_WATCOM_FUNC1(long, text_font, long, fontNum) +WRAP_WATCOM_FUNC3(long, tile_coord, long, tile, long*, outX, long*, outY) // the fourth argument of the function is not used WRAP_WATCOM_FUNC2(long, tile_dist, long, scrTile, long, dstTile) WRAP_WATCOM_FUNC2(long, tile_dir, long, scrTile, long, dstTile) WRAP_WATCOM_FUNC2(long, tile_idistance, long, sourceTile, long, targetTile) +WRAP_WATCOM_FUNC3(long, tile_num_in_direction, long, tile, long, rotation, long, distance) WRAP_WATCOM_FUNC1(long, tile_on_edge, long, tile) WRAP_WATCOM_FUNC0(void, tile_refresh_display) // Redraws the whole screen WRAP_WATCOM_FUNC2(void, tile_refresh_rect, fo::BoundRect*, boundRect, long, elevation) // Redraws the given rectangle on screen @@ -237,7 +238,9 @@ WRAP_WATCOM_FUNC1(BYTE*, win_get_buf, DWORD, winRef) WRAP_WATCOM_FUNC1(void, win_hide, DWORD, winRef) WRAP_WATCOM_FUNC1(void, win_show, DWORD, winRef) WRAP_WATCOM_FUNC0(long, windowWidth) +WRAP_WATCOM_FUNC1(void, wmCarUseGas, long, gasAmount) WRAP_WATCOM_FUNC1(long, wmEvalTileNumForPlacement, long, tile) +WRAP_WATCOM_FUNC0(void, wmPartyWalkingStep) WRAP_WATCOM_FUNC1(void, wmRefreshInterfaceOverlay, long, isRedraw) /* Database functions */ diff --git a/sfall/Modules/HookScripts.cpp b/sfall/Modules/HookScripts.cpp index e4f87a93..88338604 100644 --- a/sfall/Modules/HookScripts.cpp +++ b/sfall/Modules/HookScripts.cpp @@ -90,9 +90,9 @@ static HooksInjectInfo injectHooks[] = { {HOOK_INVENWIELD, Inject_InvenWieldHook, false}, {HOOK_ADJUSTFID, nullptr, true}, // always embedded to the engine {-1, nullptr, false}, // dummy - {-1, nullptr, false}, // dummy - {-1, nullptr, false}, // dummy - {-1, nullptr, false}, // dummy + {HOOK_CARTRAVEL, Inject_CarTravelHook, false}, + {HOOK_SETGLOBALVAR, Inject_SetGlobalVarHook, false}, + {HOOK_RESTTIMER, Inject_RestTimerHook, false}, {HOOK_GAMEMODECHANGE, nullptr, true}, // always embedded to the engine }; diff --git a/sfall/Modules/HookScripts.h b/sfall/Modules/HookScripts.h index caab6edd..0e4b4afc 100644 --- a/sfall/Modules/HookScripts.h +++ b/sfall/Modules/HookScripts.h @@ -51,8 +51,12 @@ enum HookType HOOK_WITHINPERCEPTION = 23, HOOK_INVENTORYMOVE = 24, HOOK_INVENWIELD = 25, - HOOK_ADJUSTFID = 26, // 4.x backport - HOOK_GAMEMODECHANGE = 31, // 4.x backport + HOOK_ADJUSTFID = 26, + HOOK_COMBATTURN = 27, // unimplemented + HOOK_CARTRAVEL = 28, + HOOK_SETGLOBALVAR = 29, + HOOK_RESTTIMER = 30, + HOOK_GAMEMODECHANGE = 31, HOOK_COUNT }; diff --git a/sfall/Modules/HookScripts/Common.cpp b/sfall/Modules/HookScripts/Common.cpp index ba6d3e16..2c3fba4c 100644 --- a/sfall/Modules/HookScripts/Common.cpp +++ b/sfall/Modules/HookScripts/Common.cpp @@ -62,6 +62,20 @@ void __stdcall HookCommon::SetHSReturn(DWORD value) { } } +// List of hooks that are not allowed to be called recursively +static bool CheckRecursiveHooks(DWORD hook) { + if (hook == currentRunHook) { + switch (hook) { + case HOOK_SETGLOBALVAR: +// case HOOK_SETLIGHTING: + return true; + default: + if (isDebug) fo::func::debug_printf("\nWARNING: A recursive hook with ID %d was running.", hook); + } + } + return false; +} + void __stdcall BeginHook() { if (callDepth && callDepth <= maxDepth) { // save all values of the current hook if another hook was called during the execution of the current hook @@ -97,10 +111,12 @@ static void __stdcall RunSpecificHookScript(HookScript *hook) { void __stdcall RunHookScript(DWORD hook) { cRet = 0; if (!hooks[hook].empty()) { - if (callDepth > 8) { - fo::func::debug_printf("\n[SFALL] The hook ID: %d cannot be executed.", hook); - dlog_f("The hook %d cannot be executed due to exceeding depth limit\n", DL_MAIN, hook); - return; + if (callDepth > 1) { + if (CheckRecursiveHooks(hook) || callDepth > 8) { + fo::func::debug_printf("\n[SFALL] The hook ID: %d cannot be executed.", hook); + dlog_f("The hook %d cannot be executed due to exceeding depth limit or disallowed recursive calls\n", DL_MAIN, hook); + return; + } } currentRunHook = hook; size_t hooksCount = hooks[hook].size(); diff --git a/sfall/Modules/HookScripts/MiscHs.cpp b/sfall/Modules/HookScripts/MiscHs.cpp index 980503e0..58f19b74 100644 --- a/sfall/Modules/HookScripts/MiscHs.cpp +++ b/sfall/Modules/HookScripts/MiscHs.cpp @@ -226,6 +226,167 @@ static void __declspec(naked) PerceptionSearchTargetHook() { } } +static const long maxGasAmount = 80000; + +static void CarTravelHook_Script() { + BeginHook(); + argCount = 2; + + // calculate vanilla speed + int carSpeed = 3; + if (fo::func::game_get_global_var(fo::GVAR_CAR_BLOWER)) { + carSpeed += 1; + } + if (fo::func::game_get_global_var(fo::GVAR_NEW_RENO_CAR_UPGRADE)) { + carSpeed += 1; + } + if (fo::func::game_get_global_var(fo::GVAR_NEW_RENO_SUPER_CAR)) { + carSpeed += 3; + } + // calculate vanilla fuel consumption + int originalGas = *fo::ptr::carGasAmount; + *fo::ptr::carGasAmount = maxGasAmount; + fo::func::wmCarUseGas(100); + int consumption = maxGasAmount - *fo::ptr::carGasAmount; + + // run script + args[0] = carSpeed; + args[1] = consumption; + RunHookScript(HOOK_CARTRAVEL); + + // override travel speed + if (cRet > 0 && static_cast(rets[0]) >= 0) { + carSpeed = rets[0]; + } + // move car on map + for (int i = 0; i < carSpeed; ++i) { + fo::func::wmPartyWalkingStep(); + } + + // override fuel consumption + if (cRet > 1) { + consumption = static_cast(rets[1]); + } + // consume fuel + *fo::ptr::carGasAmount = max(0, originalGas - consumption); // Note: the reverse breaks the result in VS2010 + + EndHook(); +} + +static void __declspec(naked) CarTravelHack() { + static const DWORD CarTravelHack_back = 0x4BFF43; + __asm { + pushad; + call CarTravelHook_Script; + popad; + jmp CarTravelHack_back; + } +} + +static long __fastcall GlobalVarHook_Script(register DWORD number, register int value) { + int old = (*fo::ptr::game_global_vars)[number]; + + if (IsGameLoaded() && HookScripts::HookHasScript(HOOK_SETGLOBALVAR)) { // IsGameLoaded - don't execute hook until loading sfall scripts + BeginHook(); + argCount = 2; + args[0] = number; + args[1] = value; + + RunHookScript(HOOK_SETGLOBALVAR); + if (cRet > 0) value = rets[0]; + EndHook(); + } + if (number == fo::GVAR_PLAYER_REPUTATION && displayKarmaChanges) { + int diff = value - old; + if (diff != 0) Karma::DisplayKarma(diff); + } + return value; +} + +static void __declspec(naked) SetGlobalVarHook() { + __asm { + push eax; + push ecx; + push edx; + mov ecx, eax; // number + call GlobalVarHook_Script; // edx - value + pop edx; + pop ecx; + cmp eax, edx; // if return value != set value + cmovne edx, eax; + pop eax; + jmp fo::funcoffs::game_set_global_var_; + } +} + +static int restTicks; + +static long __fastcall RestTimerHook_Script(DWORD hours, DWORD minutes, DWORD gameTime, DWORD addrHook) { + addrHook -= 5; + + BeginHook(); + argCount = 4; + + args[0] = gameTime; + args[2] = hours; + args[3] = minutes; + + if (addrHook == 0x499CA1 || addrHook == 0x499B63) { + args[0] = restTicks; + args[1] = -1; + } else { + restTicks = args[0]; + args[1] = (addrHook == 0x499DF2 || (args[2] == 0 && addrHook == 0x499BE0)) ? 1 : 0; + } + RunHookScript(HOOK_RESTTIMER); + + long result = -1; + if (cRet > 0) { + result = (rets[0] != 0) ? 1 : 0; + } + EndHook(); + + return result; +} + +static void __declspec(naked) RestTimerLoopHook() { + __asm { + pushadc; + mov edx, [esp + 16 + 0x44]; // minutes_ + mov ecx, [esp + 16 + 0x40]; // hours_ + push [esp + 12]; // addrHook + push eax; // gameTime + call RestTimerHook_Script; + pop ecx; + pop edx; + test eax, eax; // result >= 0 + cmovge edi, eax; // return 1 to interrupt resting + pop eax; + jmp fo::funcoffs::set_game_time_; + } +} + +static void __declspec(naked) RestTimerEscapeHook() { + __asm { + mov edi, 1; // engine code + cmp eax, 0x1B; // ESC ASCII code + jnz skip; + pushadc; + mov edx, [esp + 16 + 0x44]; // minutes_ + mov ecx, [esp + 16 + 0x40]; // hours_ + push [esp + 12]; // addrHook + push eax; // gameTime + call RestTimerHook_Script; + pop ecx; + pop edx; + test eax, eax; // result >= 0 + cmovge edi, eax; // return 0 for cancel ESC key + pop eax; +skip: + retn; + } +} + void Inject_BarterPriceHook() { const DWORD barterPriceHkAddr[] = { 0x474D4C, // barter_attempt_transaction_ (offers button) @@ -261,11 +422,30 @@ void Inject_WithinPerceptionHook() { HookCall(0x458403, PerceptionRangeHearHook); } +void Inject_CarTravelHook() { + MakeJump(0x4BFEF1, CarTravelHack); + BlockCall(0x4BFF6E); // vanilla wmCarUseGas(100) call +} + +void Inject_SetGlobalVarHook() { + HookCall(0x455A6D, SetGlobalVarHook); +} + +void Inject_RestTimerHook() { + const DWORD restTimerLoopHkAddr[] = {0x499B4B, 0x499BE0, 0x499D2C, 0x499DF2}; + HookCalls(RestTimerLoopHook, restTimerLoopHkAddr); + const DWORD restTimerEscHkAddr[] = {0x499B63, 0x499CA1}; + MakeCalls(RestTimerEscapeHook, restTimerEscHkAddr); +} + void InitMiscHookScripts() { HookScripts::LoadHookScript("hs_barterprice", HOOK_BARTERPRICE); HookScripts::LoadHookScript("hs_useskill", HOOK_USESKILL); HookScripts::LoadHookScript("hs_steal", HOOK_STEAL); HookScripts::LoadHookScript("hs_withinperception", HOOK_WITHINPERCEPTION); + HookScripts::LoadHookScript("hs_cartravel", HOOK_CARTRAVEL); + HookScripts::LoadHookScript("hs_setglobalvar", HOOK_SETGLOBALVAR); + HookScripts::LoadHookScript("hs_resttimer", HOOK_RESTTIMER); } } diff --git a/sfall/Modules/HookScripts/MiscHs.h b/sfall/Modules/HookScripts/MiscHs.h index c267a138..e6f26297 100644 --- a/sfall/Modules/HookScripts/MiscHs.h +++ b/sfall/Modules/HookScripts/MiscHs.h @@ -9,6 +9,9 @@ void Inject_BarterPriceHook(); void Inject_UseSkillHook(); void Inject_StealCheckHook(); void Inject_WithinPerceptionHook(); +void Inject_CarTravelHook(); +void Inject_SetGlobalVarHook(); +void Inject_RestTimerHook(); long PerceptionRangeHook_Invoke(fo::GameObject* watcher, fo::GameObject* target, long type, long result); diff --git a/sfall/Modules/Karma.cpp b/sfall/Modules/Karma.cpp index e3129993..6036cb12 100644 --- a/sfall/Modules/Karma.cpp +++ b/sfall/Modules/Karma.cpp @@ -23,6 +23,8 @@ #include "..\FalloutEngine\Fallout2.h" #include "..\Translate.h" +#include "HookScripts.h" + #include "Karma.h" namespace sfall @@ -37,6 +39,7 @@ static std::vector karmaFrms; static char karmaGainMsg[128]; static char karmaLossMsg[128]; +bool displayKarmaChanges; static DWORD __stdcall DrawCard() { int reputation = (*fo::ptr::game_global_vars)[fo::GVAR_PLAYER_REPUTATION]; @@ -64,7 +67,7 @@ skip: } } -static void __stdcall SetKarma(int value) { +void Karma::DisplayKarma(int value) { char buf[128]; if (value > 0) { sprintf_s(buf, karmaGainMsg, value); @@ -74,29 +77,13 @@ static void __stdcall SetKarma(int value) { fo::func::display_print(buf); } -static void __declspec(naked) SetGlobalVarWrapper() { - __asm { - test eax, eax; // Gvar number - jnz end; - pushadc; - call fo::funcoffs::game_get_global_var_; - sub edx, eax; // value -= old - jz skip; - push edx; - call SetKarma; -skip: - popadc; -end: - jmp fo::funcoffs::game_set_global_var_; - } -} - static void ApplyDisplayKarmaChangesPatch() { - if (IniReader::GetConfigInt("Misc", "DisplayKarmaChanges", 0)) { + displayKarmaChanges = IniReader::GetConfigInt("Misc", "DisplayKarmaChanges", 0) != 0; + if (displayKarmaChanges) { dlog("Applying display karma changes patch.", DL_INIT); Translate::Get("sfall", "KarmaGain", "You gained %d karma.", karmaGainMsg); Translate::Get("sfall", "KarmaLoss", "You lost %d karma.", karmaLossMsg); - HookCall(0x455A6D, SetGlobalVarWrapper); + HookScripts::InjectingHook(HOOK_SETGLOBALVAR); dlogr(" Done", DL_INIT); } } diff --git a/sfall/Modules/Karma.h b/sfall/Modules/Karma.h index 4c70f152..741c9c34 100644 --- a/sfall/Modules/Karma.h +++ b/sfall/Modules/Karma.h @@ -25,6 +25,10 @@ class Karma { public: static const char* name() { return "Karma"; } static void init(); + + static void DisplayKarma(int value); }; +extern bool displayKarmaChanges; + }