From 3b6435d8a4f966f1c17eb943d592d48221e17882 Mon Sep 17 00:00:00 2001 From: NovaRain Date: Wed, 7 Jun 2023 21:48:54 +0800 Subject: [PATCH] Backported HOOK_COMBATTURN from 4.2 * using pre-4.2.8 codebase Replaced ControlCombat option with PartyOrderToAttack. Updated documents. --- artifacts/scripting/headers/sfall.h | 2 +- artifacts/scripting/hookscripts.md | 18 + sfall/FalloutEngine/EngineUtils.cpp | 12 + sfall/FalloutEngine/EngineUtils.h | 2 + sfall/FalloutEngine/FunctionOffsets_def.h | 2 + sfall/FalloutEngine/Functions_def.h | 1 + sfall/FalloutEngine/VarPointers_def.h | 1 + sfall/Modules/HookScripts.cpp | 2 +- sfall/Modules/HookScripts.h | 2 +- sfall/Modules/HookScripts/CombatHs.cpp | 115 ++++++ sfall/Modules/HookScripts/CombatHs.h | 1 + sfall/Modules/PartyControl.cpp | 326 +++++++++--------- sfall/Modules/PartyControl.h | 3 + sfall/Modules/Scripting/Handlers/Metarule.cpp | 1 + sfall/Modules/Scripting/Handlers/Objects.cpp | 14 + sfall/Modules/Scripting/Handlers/Objects.h | 2 + 16 files changed, 337 insertions(+), 167 deletions(-) diff --git a/artifacts/scripting/headers/sfall.h b/artifacts/scripting/headers/sfall.h index 0b01db33..b162d18a 100644 --- a/artifacts/scripting/headers/sfall.h +++ b/artifacts/scripting/headers/sfall.h @@ -53,7 +53,7 @@ #define HOOK_INVENTORYMOVE (24) #define HOOK_INVENWIELD (25) #define HOOK_ADJUSTFID (26) -//#define HOOK_COMBATTURN (27) // unimplemented +#define HOOK_COMBATTURN (27) #define HOOK_CARTRAVEL (28) #define HOOK_SETGLOBALVAR (29) #define HOOK_RESTTIMER (30) diff --git a/artifacts/scripting/hookscripts.md b/artifacts/scripting/hookscripts.md index 4ae39022..bcb547db 100644 --- a/artifacts/scripting/hookscripts.md +++ b/artifacts/scripting/hookscripts.md @@ -584,6 +584,24 @@ int ret0 - overrides the calculated FID with provided value ------------------------------------------- +#### `HOOK_COMBATTURN (hs_combatturn.int)` + +Runs before and after each turn in combat (for both PC and NPC). + +``` +int arg0 - event type: + 1 - start of turn + 0 - normal end of turn + -1 - combat ends abruptly (by script or by pressing Enter during PC turn) + -2 - combat ends normally (hook always runs at the end of combat) +Critter arg1 - critter doing the turn +bool arg2 - 1 at the start/end of the player's turn after loading a game saved in combat mode, 0 otherwise + +int ret0 - pass 1 at the start of turn to skip the turn, pass -1 at the end of turn to force end of combat +``` + +------------------------------------------- + #### `HOOK_CARTRAVEL (hs_cartravel.int)` Runs continuously during worldmap travel on car. diff --git a/sfall/FalloutEngine/EngineUtils.cpp b/sfall/FalloutEngine/EngineUtils.cpp index b827c850..506e1528 100644 --- a/sfall/FalloutEngine/EngineUtils.cpp +++ b/sfall/FalloutEngine/EngineUtils.cpp @@ -266,6 +266,18 @@ long __fastcall IsRadInfluence() { return 0; } +void ToggleNpcFlag(fo::GameObject* npc, long flag, bool set) { + fo::Proto* proto; + if (GetProto(npc->protoId, &proto)) { + long bit = (1 << flag); + if (set) { + proto->critter.critterFlags |= bit; + } else { + proto->critter.critterFlags &= ~bit; + } + } +} + // Returns the position of party member in the existing table (1 is added to the index position) long IsPartyMemberByPid(long pid) { size_t partyCount = *fo::ptr::partyMemberMaxCount; diff --git a/sfall/FalloutEngine/EngineUtils.h b/sfall/FalloutEngine/EngineUtils.h index 46893324..88d052f5 100644 --- a/sfall/FalloutEngine/EngineUtils.h +++ b/sfall/FalloutEngine/EngineUtils.h @@ -107,6 +107,8 @@ fo::QueueRadiationData* __fastcall GetRadiationEvent(long type); // Checks whether the player is under the influence of negative effects of radiation long __fastcall IsRadInfluence(); +void ToggleNpcFlag(fo::GameObject* npc, long flag, bool set); + // Returns the position of party member in the existing table (begins from 1) long IsPartyMemberByPid(long pid); diff --git a/sfall/FalloutEngine/FunctionOffsets_def.h b/sfall/FalloutEngine/FunctionOffsets_def.h index cd4db600..d97e0e00 100644 --- a/sfall/FalloutEngine/FunctionOffsets_def.h +++ b/sfall/FalloutEngine/FunctionOffsets_def.h @@ -112,8 +112,10 @@ FUNC(combat_check_bad_shot_, 0x426614) FUNC(combat_ctd_init_, 0x422EC4) FUNC(combat_delete_critter_, 0x426DDC) FUNC(combat_input_, 0x4227F4) +FUNC(combat_over_, 0x421EFC) FUNC(combat_safety_invalidate_weapon_, 0x4213E8) FUNC(combat_safety_invalidate_weapon_func_, 0x4213FC) +FUNC(combat_sequence_, 0x422580) FUNC(combat_should_end_, 0x422C60) FUNC(combat_turn_, 0x42299C) FUNC(combat_turn_run_, 0x4227DC) diff --git a/sfall/FalloutEngine/Functions_def.h b/sfall/FalloutEngine/Functions_def.h index 8e7e92c2..4d470a8a 100644 --- a/sfall/FalloutEngine/Functions_def.h +++ b/sfall/FalloutEngine/Functions_def.h @@ -85,6 +85,7 @@ WRAP_WATCOM_FUNC4(BYTE*, art_lock, long, frmId, DWORD*, lockPtr, long*, widthOut WRAP_WATCOM_FUNC1(long, art_ptr_unlock, DWORD, lockId) WRAP_WATCOM_FUNC2(long, barter_compute_value, fo::GameObject*, source, fo::GameObject*, target) WRAP_WATCOM_FUNC1(long, block_for_tocks, long, ticks) +WRAP_WATCOM_FUNC2(long, combat_turn, fo::GameObject*, critter, long, isDudeTurn) // Perform combat turn for a given critter WRAP_WATCOM_FUNC1(long, critter_body_type, fo::GameObject*, critter) WRAP_WATCOM_FUNC1(long, critter_is_dead, fo::GameObject*, critter) WRAP_WATCOM_FUNC1(long, critter_kill_count_type, fo::GameObject*, critter) diff --git a/sfall/FalloutEngine/VarPointers_def.h b/sfall/FalloutEngine/VarPointers_def.h index c99209a6..ac48dd8d 100644 --- a/sfall/FalloutEngine/VarPointers_def.h +++ b/sfall/FalloutEngine/VarPointers_def.h @@ -19,6 +19,7 @@ PTR_(carCurrentArea, DWORD) PTR_(carGasAmount, long) // from 0 to 80000 PTR_(cmap, fo::PALETTE) // array of 256 PALETTE, palette without gamma PTR_(colorTable, DWORD) +PTR_(combat_end_due_to_load, DWORD) PTR_(combat_free_move, DWORD) PTR_(combat_list, fo::GameObject**) // dynamic array PTR_(combat_obj, fo::GameObject*) diff --git a/sfall/Modules/HookScripts.cpp b/sfall/Modules/HookScripts.cpp index 0afffa0c..3cb5cfd2 100644 --- a/sfall/Modules/HookScripts.cpp +++ b/sfall/Modules/HookScripts.cpp @@ -89,7 +89,7 @@ static HooksInjectInfo injectHooks[] = { {HOOK_INVENTORYMOVE, Inject_InventoryMoveHook, false}, {HOOK_INVENWIELD, Inject_InvenWieldHook, false}, {HOOK_ADJUSTFID, nullptr, true}, // always embedded to the engine - {-1, nullptr, false}, // dummy + {HOOK_COMBATTURN, Inject_CombatTurnHook, false}, {HOOK_CARTRAVEL, Inject_CarTravelHook, false}, {HOOK_SETGLOBALVAR, Inject_SetGlobalVarHook, false}, {HOOK_RESTTIMER, Inject_RestTimerHook, false}, diff --git a/sfall/Modules/HookScripts.h b/sfall/Modules/HookScripts.h index ae03c6f2..aedb5cef 100644 --- a/sfall/Modules/HookScripts.h +++ b/sfall/Modules/HookScripts.h @@ -52,7 +52,7 @@ enum HookType HOOK_INVENTORYMOVE = 24, HOOK_INVENWIELD = 25, HOOK_ADJUSTFID = 26, -// HOOK_COMBATTURN = 27, // unimplemented + HOOK_COMBATTURN = 27, HOOK_CARTRAVEL = 28, HOOK_SETGLOBALVAR = 29, HOOK_RESTTIMER = 30, diff --git a/sfall/Modules/HookScripts/CombatHs.cpp b/sfall/Modules/HookScripts/CombatHs.cpp index 3997fd30..6f2abf8f 100644 --- a/sfall/Modules/HookScripts/CombatHs.cpp +++ b/sfall/Modules/HookScripts/CombatHs.cpp @@ -321,6 +321,107 @@ skip: } } +static long combatTurnResult = 0; + +// hooks combat_turn function +static long __fastcall CombatTurnHook_Script(fo::GameObject* critter, long dudeBegin) { + BeginHook(); + argCount = 3; + + args[0] = 1; // turn begin + args[1] = (DWORD)critter; // who begins turn + args[2] = dudeBegin; // true - dude begins/ends turn after loading a game saved in combat mode + + RunHookScript(HOOK_COMBATTURN); // Start of turn + + combatTurnResult = 0; + if (cRet > 0 && rets[0] == 1) { // skip turn + goto endHook; // exit hook + } + + // set_sfall_return is not used, proceed normally + combatTurnResult = fo::func::combat_turn(critter, dudeBegin); + args[0] = combatTurnResult; + + if (*fo::ptr::combat_end_due_to_load && combatTurnResult == -1) goto endHook; // don't run end of turn hook when the game was loaded during the combat + + //cRet = 0; // reset number of return values + RunHookScript(HOOK_COMBATTURN); // End of turn + + if (cRet > 0 && rets[0] == -1) combatTurnResult = -1; // override result of turn + +endHook: + EndHook(); + return combatTurnResult; +} + +static void __declspec(naked) CombatTurnHook() { + __asm { + push ecx; + mov ecx, eax; + call CombatTurnHook_Script; // edx - dudeBegin + pop ecx; + retn; + } +} + +static void __declspec(naked) CombatTurnHook_End() { + if (combatTurnResult >= 0) { + BeginHook(); + argCount = 3; + + args[0] = -2; // combat ended normally + args[1] = fo::var::getInt(FO_VAR_combat_turn_obj); + args[2] = 0; + + RunHookScript(HOOK_COMBATTURN); + EndHook(); + } + __asm jmp fo::funcoffs::combat_over_; +} + +// hack to exit from combat_add_noncoms function without crashing when you load game during PM/NPC turn +static long countCombat = 0; +static void __declspec(naked) CombatTurnHook_AddNoncoms() { + __asm { + push ecx; + mov ecx, eax; + call CombatTurnHook_Script; // edx - dudeBegin + pop ecx; + cmp eax, -1; + je endCombat; + retn; +endCombat: + mov ecx, [esp + 4]; // list + xor edx, edx; + cmp ds:[FO_VAR_combat_end_due_to_load], edx; + jz skip; + mov eax, ds:[FO_VAR_list_com]; + test eax, eax; + jz skip; + mov countCombat, eax; +skip: + mov ds:[FO_VAR_list_com], edx; // edx = 0 + retn; + } +} + +static void __declspec(naked) combat_hook_fix_load() { + static const DWORD combat_hook_end_combat = 0x422E91; + __asm { + call fo::funcoffs::combat_sequence_; + mov eax, countCombat; + test eax, eax; + jnz forceEndCombat; + retn; +forceEndCombat: + mov ds:[FO_VAR_list_com], eax; + mov countCombat, 0; + add esp, 4; + jmp combat_hook_end_combat; + } +} + fo::GameObject* __fastcall ComputeExplosionOnExtrasHook_Script(fo::GameObject* object, DWORD isCheck, DWORD checkTile, fo::ComputeAttackResult* ctdSource, DWORD isThrowing, fo::GameObject* who) { fo::GameObject* result = object; @@ -587,6 +688,19 @@ void Inject_AmmoCostHook() { HookCall(0x423A7C, AmmoCostHook); // compute_attack_ } +void Inject_CombatTurnHook() { + HookCall(0x422354, CombatTurnHook_AddNoncoms); + const DWORD combatTurnHkAddr[] = {0x422D87, 0x422E20}; + HookCalls(CombatTurnHook, combatTurnHkAddr); + const DWORD combatOverHkAddr[] = { + 0x422E85, // combat_ + 0x422196 // combat_over_from_load_ + }; + HookCalls(CombatTurnHook_End, combatOverHkAddr); + + HookCall(0x422E4D, combat_hook_fix_load); +} + void Inject_OnExplosionHook() { HookCall(0x423D70, ComputeExplosionOnExtrasHook); } @@ -622,6 +736,7 @@ void InitCombatHookScripts() { HookScripts::LoadHookScript("hs_findtarget", HOOK_FINDTARGET); HookScripts::LoadHookScript("hs_itemdamage", HOOK_ITEMDAMAGE); HookScripts::LoadHookScript("hs_ammocost", HOOK_AMMOCOST); + HookScripts::LoadHookScript("hs_combatturn", HOOK_COMBATTURN); HookScripts::LoadHookScript("hs_onexplosion", HOOK_ONEXPLOSION); HookScripts::LoadHookScript("hs_targetobject", HOOK_TARGETOBJECT); HookScripts::LoadHookScript("hs_bestweapon", HOOK_BESTWEAPON); diff --git a/sfall/Modules/HookScripts/CombatHs.h b/sfall/Modules/HookScripts/CombatHs.h index 809254c4..ad4a92aa 100644 --- a/sfall/Modules/HookScripts/CombatHs.h +++ b/sfall/Modules/HookScripts/CombatHs.h @@ -14,6 +14,7 @@ void Inject_CombatDamageHook(); void Inject_FindTargetHook(); void Inject_ItemDamageHook(); void Inject_AmmoCostHook(); +void Inject_CombatTurnHook(); void Inject_OnExplosionHook(); void Inject_TargetObjectHook(); void Inject_BestWeaponHook(); diff --git a/sfall/Modules/PartyControl.cpp b/sfall/Modules/PartyControl.cpp index e61d054f..1fae8f99 100644 --- a/sfall/Modules/PartyControl.cpp +++ b/sfall/Modules/PartyControl.cpp @@ -16,12 +16,6 @@ * along with this program. If not, see . */ - -/* - KNOWN ISSUES with party control - - doesn't work with NPC's wearing armor mod, armor won't change when you change it from critter's inventory -*/ - #include "..\main.h" #include "..\FalloutEngine\Fallout2.h" #include "..\Translate.h" @@ -37,11 +31,22 @@ namespace sfall bool npcAutoLevelEnabled; bool npcEngineLevelUp = true; -static DWORD Mode; static bool isControllingNPC = false; static char skipCounterAnim; -static std::vector Chars; + static int delayedExperience; +static bool switchHandHookInjected = false; + +struct WeaponStateSlot { + long npcID; + bool leftIsCopy; + bool rightIsCopy; + fo::ItemButtonItem leftSlot; + fo::ItemButtonItem rightSlot; + + WeaponStateSlot() : leftIsCopy(false), rightIsCopy(false) {} +}; +std::vector weaponState; static struct DudeState { fo::GameObject* obj_dude; @@ -59,33 +64,14 @@ static struct DudeState { DWORD itemCurrentItem; fo::ItemButtonItem itemButtonItems[2]; long perkLevelDataList[fo::PERK_count]; - //long addictGvar[8]; + long addictGvar[8]; long tag_skill[4]; //DWORD bbox_sneak; + bool isSaved; - DudeState() : obj_dude(nullptr) {} + DudeState() : obj_dude(nullptr), isSaved(false) {} } realDude; -static bool __stdcall IsInPidList(fo::GameObject* obj) { - int pid = obj->protoId & 0xFFFFFF; - for (std::vector::iterator it = Chars.begin(); it != Chars.end(); ++it) { - if (*it == pid) { - return true; - } - } - return false; -} - -static void __stdcall SetInventoryCheck(bool skip) { - if (skip) { - SafeWrite16(0x46E7CD, 0x9090); // Inventory check - SafeWrite32(0x46E7CF, 0x90909090); - } else { - SafeWrite16(0x46E7CD, 0x850F); // Inventory check - SafeWrite32(0x46E7CF, 0x4B1); - } -} - // saves the state of PC before moving control to NPC static void SaveRealDudeState() { realDude.obj_dude = *fo::ptr::obj_dude; @@ -103,6 +89,12 @@ static void SaveRealDudeState() { realDude.sneak_working = *fo::ptr::sneak_working; fo::util::SkillGetTags(realDude.tag_skill, 4); + for (int i = 0; i < 6; i++) realDude.addictGvar[i] = (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[i].addictGvar]; + realDude.addictGvar[6] = (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[7].addictGvar]; + realDude.addictGvar[7] = (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[8].addictGvar]; + + realDude.isSaved = true; + if (skipCounterAnim == 1) { skipCounterAnim++; SafeWrite8(0x422BDE, 0); // no animate @@ -111,7 +103,7 @@ static void SaveRealDudeState() { } // take control of the NPC -static void TakeControlOfNPC(fo::GameObject* npc) { +static void SetCurrentDude(fo::GameObject* npc) { if (isDebug) fo::func::debug_printf("\n[SFALL] Take control of critter."); // remove skill tags @@ -149,6 +141,43 @@ static void TakeControlOfNPC(fo::GameObject* npc) { } else { *fo::ptr::itemCurrentItem = fo::HandSlot::Right; } + // restore selected weapon mode + size_t count = weaponState.size(); + for (size_t i = 0; i < count; i++) { + if (weaponState[i].npcID == npc->id) { + bool isMatch = false; + if (weaponState[i].leftIsCopy) { + fo::GameObject* item = fo::func::inven_left_hand(npc); + if (item && item->protoId == weaponState[i].leftSlot.item->protoId) { + std::memcpy(&fo::ptr::itemButtonItems[0], &weaponState[i].leftSlot, 0x14); + isMatch = true; + } + weaponState[i].leftIsCopy = false; + } + if (weaponState[i].rightIsCopy) { + fo::GameObject* item = fo::func::inven_right_hand(npc); + if (item && item->protoId == weaponState[i].rightSlot.item->protoId) { + std::memcpy(&fo::ptr::itemButtonItems[1], &weaponState[i].rightSlot, 0x14); + isMatch = true; + } + weaponState[i].rightIsCopy = false; + } + if (!isMatch) { + if (i < count - 1) weaponState[i] = weaponState.back(); + weaponState.pop_back(); + } + break; + } + } + + bool isAddict = false; + for (int i = 0; i < 9; i++) (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[i].addictGvar] = 0; + for (int i = 0; i < 9; i++) { + if (!fo::util::CheckAddictByPid(npc, fo::ptr::drugInfoList[i].itemPid)) continue; + (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[i].addictGvar] = 1; + isAddict = true; + } + fo::util::ToggleNpcFlag(npc, 4, isAddict); // for show/hide addiction box (fix bug) // switch main dude_obj pointers - this should be done last! *fo::ptr::combat_turn_obj = npc; @@ -159,7 +188,8 @@ static void TakeControlOfNPC(fo::GameObject* npc) { isControllingNPC = true; delayedExperience = 0; - SetInventoryCheck(true); + + //if (!isPartyMember) Objects::SetObjectUniqueID(npc); fo::func::intface_redraw(); } @@ -192,6 +222,10 @@ static void RestoreRealDudeState(bool redraw = true) { fo::func::stat_pc_add_experience(delayedExperience); } + for (int i = 0; i < 6; i++) (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[i].addictGvar] = realDude.addictGvar[i]; + (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[7].addictGvar] = realDude.addictGvar[6]; + (*fo::ptr::game_global_vars)[fo::ptr::drugInfoList[8].addictGvar] = realDude.addictGvar[7]; + if (redraw) { if (skipCounterAnim == 2) { skipCounterAnim--; @@ -200,72 +234,12 @@ static void RestoreRealDudeState(bool redraw = true) { fo::func::intface_redraw(); } - SetInventoryCheck(false); + realDude.isSaved = false; isControllingNPC = false; if (isDebug) fo::func::debug_printf("\n[SFALL] Restore control to dude.\n"); } -static void __stdcall CenterScreenOnDude() { - using namespace fo::Fields; - __asm { - mov edx, 3; - mov eax, ds:[FO_VAR_obj_dude]; - mov eax, [eax + tile]; - call fo::funcoffs::tile_scroll_to_; - } -} - -static long __stdcall CombatTurn(fo::GameObject* obj) { - __asm { - mov eax, obj; - call fo::funcoffs::combat_turn_; - } -} - -// return values: 0 - use vanilla handler, 1 - skip vanilla handler, return 0 (normal status), -1 - skip vanilla, return -1 (game ended) -static long __stdcall CombatWrapperInner(fo::GameObject* obj) { - if (obj == *fo::ptr::obj_dude) { - if (*fo::ptr::combatNumTurns != 0) { - CenterScreenOnDude(); - } - } else if ((Chars.size() == 0 || IsInPidList(obj)) && (Mode == 1 || fo::func::isPartyMember(obj))) { - // save "real" dude state - SaveRealDudeState(); - TakeControlOfNPC(obj); - CenterScreenOnDude(); - - // Do combat turn - long turnResult = CombatTurn(obj); - - // restore state - if (isControllingNPC) { // if game was loaded during turn, PartyControlReset() was called and already restored state - RestoreRealDudeState(); - } - // -1 means that combat ended during turn - return (turnResult == -1) ? -1 : 1; - } - return 0; -} - - -// this hook fixes NPCs art switched to main dude art after inventory screen closes -static void __declspec(naked) FidChangeHook() { - __asm { - cmp isControllingNPC, 0; - je skip; - push eax; - mov eax, [eax + 0x20]; // current fid - and eax, 0xFFFF0FFF; - and edx, 0x0000F000; - or edx, eax; // only change one octet with weapon type - pop eax; -skip: - call fo::funcoffs::obj_change_fid_; - retn; - } -} - static void __stdcall DisplayCantDoThat() { fo::func::display_print(fo::util::GetMessageStr(fo::ptr::proto_main_msg_file, 675)); // I Can't do that } @@ -313,42 +287,6 @@ skip: } } -static void __declspec(naked) CombatWrapper_v2() { - __asm { - sub esp, 4; - pushad; - push eax; - call CombatWrapperInner; - mov [esp + 32], eax; - popad; - add esp, 4; - cmp [esp - 4], 0; - je gonormal; - cmp [esp - 4], -1; - je combatend; - xor eax, eax; - retn; -combatend: - mov eax, -1; // don't continue combat, as the game was loaded - retn; -gonormal: - jmp fo::funcoffs::combat_turn_; - } -} - -// hack to exit from combat_add_noncoms function without crashing when you load game during PM/NPC turn -static void __declspec(naked) combat_add_noncoms_hook() { - __asm { - call CombatWrapper_v2; - inc eax; - jnz end; // jump if return value != -1 - mov ds:[FO_VAR_list_com], eax; // eax = 0 - mov ecx, [esp + 4]; // list -end: - retn; - } -} - static void __declspec(naked) stat_pc_add_experience_hook() { __asm { cmp isControllingNPC, 0; @@ -423,12 +361,101 @@ static void PartyControlReset() { } } realDude.obj_dude = nullptr; + realDude.isSaved = false; + weaponState.clear(); } bool PartyControl::IsNpcControlled() { return isControllingNPC; } +static bool CopyItemSlots(WeaponStateSlot &element, bool isSwap) { + bool isCopy = false; + if (fo::ptr::itemButtonItems[0 + isSwap].itsWeapon && fo::ptr::itemButtonItems[0 + isSwap].item) { + std::memcpy(&element.leftSlot, &fo::ptr::itemButtonItems[0 + isSwap], 0x14); + element.leftIsCopy = isCopy = true; + } + if (fo::ptr::itemButtonItems[1 - isSwap].itsWeapon && fo::ptr::itemButtonItems[1 - isSwap].item) { + std::memcpy(&element.rightSlot, &fo::ptr::itemButtonItems[1 - isSwap], 0x14); + element.rightIsCopy = isCopy = true; + } + if (isSwap && isCopy) { + if (element.leftIsCopy) { + element.leftSlot.primaryAttack = fo::AttackType::ATKTYPE_LWEAPON_PRIMARY; + element.leftSlot.secondaryAttack = fo::AttackType::ATKTYPE_LWEAPON_SECONDARY; + } + if (element.rightIsCopy) { + element.rightSlot.primaryAttack = fo::AttackType::ATKTYPE_RWEAPON_PRIMARY; + element.rightSlot.secondaryAttack = fo::AttackType::ATKTYPE_RWEAPON_SECONDARY; + } + } + return isCopy; +} + +static void SaveWeaponMode(bool isSwap) { + for (size_t i = 0; i < weaponState.size(); i++) { + if (weaponState[i].npcID == (*fo::ptr::obj_dude)->id) { + CopyItemSlots(weaponState[i], isSwap); + return; + } + } + WeaponStateSlot wState; + if (CopyItemSlots(wState, isSwap)) { + wState.npcID = (*fo::ptr::obj_dude)->id; + weaponState.push_back(wState); + } +} + +// Moves the weapon from the active left slot to the right slot and saves the selected weapon mode for NPC +static void NPCWeaponTweak() { + bool isSwap = false; + if (*fo::ptr::itemCurrentItem == fo::HandSlot::Left) { + // set active left item to right slot + fo::GameObject* lItem = fo::func::inven_left_hand(*fo::ptr::obj_dude); + if (lItem) { + isSwap = true; + fo::GameObject* rItem = fo::func::inven_right_hand(*fo::ptr::obj_dude); + lItem->flags &= ~fo::ObjectFlag::Left_Hand; + lItem->flags |= fo::ObjectFlag::Right_Hand; + if (rItem) { + rItem->flags &= ~fo::ObjectFlag::Right_Hand; + rItem->flags |= fo::ObjectFlag::Left_Hand; + } + } + } + SaveWeaponMode(isSwap); +} + +void PartyControl::SwitchToCritter(fo::GameObject* critter) { + if (skipCounterAnim == 2 && critter && critter == realDude.obj_dude) { + skipCounterAnim--; + SafeWrite8(0x422BDE, 1); // restore + } + + if (isControllingNPC) { + NPCWeaponTweak(); + if (critter == nullptr || critter == realDude.obj_dude) RestoreRealDudeState(critter != nullptr); // return control to dude + } + if (critter && critter != PartyControl::RealDudeObject()) { + if (!isControllingNPC && !realDude.isSaved) { + SaveRealDudeState(); + } + SetCurrentDude(critter); + + if (!switchHandHookInjected) { + switchHandHookInjected = true; + //if (!HookScripts::IsInjectHook(HOOK_INVENTORYMOVE)) Inject_SwitchHandHook(); + + HookCall(0x49EB09, proto_name_hook); + + // Gets dude perks and traits from script while controlling another NPC + // WARNING: Handling dude perks/traits in the engine code while controlling another NPC remains impossible, this requires serious hacking of the engine code + HookCall(0x458242, GetRealDudePerk); // op_has_trait_ + HookCall(0x458326, GetRealDudeTrait); // op_has_trait_ + } + } +} + fo::GameObject* PartyControl::RealDudeObject() { return realDude.obj_dude != nullptr ? realDude.obj_dude @@ -683,43 +710,14 @@ void PartyControl::OnGameLoad() { } void PartyControl::init() { - Mode = IniReader::GetConfigInt("Misc", "ControlCombat", 0); - if (Mode >= 3) { - if (Mode == 3) PartyControl::OrderAttackPatch(); - Mode = 0; - } else if (Mode == 1 && !isDebug) { - Mode = 2; - } - - if (Mode > 0) { - dlog("Setting up party control.", DL_INIT); - std::vector pidList = IniReader::GetConfigList("Misc", "ControlCombatPIDList", "", 512); - size_t countPids = pidList.size(); - if (countPids) { - Chars.resize(countPids); - for (size_t i = 0; i < countPids; i++) { - Chars[i] = (WORD)atoi(pidList[i].c_str()); - } - } - dlog_f(" Mode %d, Chars read: %d.\n", DL_INIT, Mode, countPids); - - HookCall(0x46EBEE, FidChangeHook); - - HookCall(0x422354, combat_add_noncoms_hook); - const DWORD combatWrapperAddr[] = {0x422D87, 0x422E20}; - HookCalls(CombatWrapper_v2, combatWrapperAddr); - - HookCall(0x454218, stat_pc_add_experience_hook); // call inside op_give_exp_points_hook - const DWORD pcFlagToggleAddr[] = {0x4124F1, 0x41279A}; - HookCalls(pc_flag_toggle_hook, pcFlagToggleAddr); - HookCall(0x49EB09, proto_name_hook); - - // Gets dude perks and traits from script while controlling another NPC - // WARNING: Handling dude perks/traits in the engine code while controlling another NPC remains impossible, this requires serious hacking of the engine code - HookCall(0x458242, GetRealDudePerk); // op_has_trait_ - HookCall(0x458326, GetRealDudeTrait); // op_has_trait_ + if (IniReader::GetConfigInt("Misc", "PartyOrderToAttack", 0)) { + dlogr("Applying order to attack patch for party members.", DL_INIT); + PartyControl::OrderAttackPatch(); } + HookCall(0x454218, stat_pc_add_experience_hook); // call inside op_give_exp_points_hook + const DWORD pcFlagToggleAddr[] = {0x4124F1, 0x41279A}; + HookCalls(pc_flag_toggle_hook, pcFlagToggleAddr); MakeCall(0x45F47C, intface_toggle_items_hack); const DWORD switchHandAddr[] = {0x4712E3, 0x47136D}; // left slot, right slot HookCalls(inven_pickup_hook, switchHandAddr); // will be overwritten if HOOK_INVENTORYMOVE is injected diff --git a/sfall/Modules/PartyControl.h b/sfall/Modules/PartyControl.h index 6badadd0..425dee0c 100644 --- a/sfall/Modules/PartyControl.h +++ b/sfall/Modules/PartyControl.h @@ -33,6 +33,9 @@ public: static bool IsNpcControlled(); + // Take control of given NPC or switch back to "Real" dude if nullptr is passed + static void SwitchToCritter(fo::GameObject* critter); + // Returns pointer to "real" dude, which is different from "dude_obj" when controlling another critter static fo::GameObject* RealDudeObject(); diff --git a/sfall/Modules/Scripting/Handlers/Metarule.cpp b/sfall/Modules/Scripting/Handlers/Metarule.cpp index dda564f8..2f8acf1e 100644 --- a/sfall/Modules/Scripting/Handlers/Metarule.cpp +++ b/sfall/Modules/Scripting/Handlers/Metarule.cpp @@ -127,6 +127,7 @@ static const SfallMetarule metarules[] = { {"set_car_intface_art", mf_set_car_intface_art, 1, 1, -1, {ARG_INT}}, {"set_combat_free_move", mf_set_combat_free_move, 1, 1, -1, {ARG_INT}}, {"set_cursor_mode", mf_set_cursor_mode, 1, 1, -1, {ARG_INT}}, + {"set_dude_obj", mf_set_dude_obj, 1, 1, -1, {ARG_INT}}, {"set_flags", mf_set_flags, 2, 2, -1, {ARG_OBJECT, ARG_INT}}, {"set_iface_tag_text", mf_set_iface_tag_text, 3, 3, -1, {ARG_INT, ARG_STRING, ARG_INT}}, {"set_ini_setting", mf_set_ini_setting, 2, 2, -1, {ARG_STRING, ARG_INTSTR}}, diff --git a/sfall/Modules/Scripting/Handlers/Objects.cpp b/sfall/Modules/Scripting/Handlers/Objects.cpp index 2e06e80e..a0d73482 100644 --- a/sfall/Modules/Scripting/Handlers/Objects.cpp +++ b/sfall/Modules/Scripting/Handlers/Objects.cpp @@ -372,6 +372,20 @@ void mf_outlined_object(OpcodeContext& ctx) { ctx.setReturn(*fo::ptr::outlined_object); } +void mf_set_dude_obj(OpcodeContext& ctx) { + auto obj = ctx.arg(0).object(); + if (obj == nullptr || obj->IsCritter()) { + //if (!InCombat && obj && obj != PartyControl::RealDudeObject()) { + // ctx.printOpcodeError("%s() - controlling of the critter is only allowed in combat mode.", ctx.getMetaruleName()); + //} else { + PartyControl::SwitchToCritter(obj); + //} + } else { + ctx.printOpcodeError("%s() - the object is not a critter.", ctx.getMetaruleName()); + ctx.setReturn(-1); + } +} + void mf_real_dude_obj(OpcodeContext& ctx) { ctx.setReturn(PartyControl::RealDudeObject()); } diff --git a/sfall/Modules/Scripting/Handlers/Objects.h b/sfall/Modules/Scripting/Handlers/Objects.h index e09ceeb1..56f3496f 100644 --- a/sfall/Modules/Scripting/Handlers/Objects.h +++ b/sfall/Modules/Scripting/Handlers/Objects.h @@ -69,6 +69,8 @@ void mf_get_flags(OpcodeContext&); void mf_outlined_object(OpcodeContext&); +void mf_set_dude_obj(OpcodeContext&); + void mf_real_dude_obj(OpcodeContext&); void mf_car_gas_amount(OpcodeContext&);