diff --git a/artifacts/config_files/sfall-mods.ini b/artifacts/config_files/sfall-mods.ini index 350bd58c..93d8ec8b 100644 --- a/artifacts/config_files/sfall-mods.ini +++ b/artifacts/config_files/sfall-mods.ini @@ -35,6 +35,7 @@ MotionScanner=0 ;Set to 0 to disable ;Set to 1 to control all critters in combat ;Set to 2 to control all party members +;Set to 3 to order party members to attack a specified target instead of controlling them by yourself Mode=0 ;If you want to control only specific critters, uncomment the PIDList line and set a comma delimited list of PIDs ;PIDList=62,89,97,107,160,161 diff --git a/artifacts/mods/gl_partycontrol.int b/artifacts/mods/gl_partycontrol.int index c2a36601..56797cbd 100644 Binary files a/artifacts/mods/gl_partycontrol.int and b/artifacts/mods/gl_partycontrol.int differ diff --git a/artifacts/mods/gl_partycontrol.ssl b/artifacts/mods/gl_partycontrol.ssl index 40d77521..c852c649 100644 --- a/artifacts/mods/gl_partycontrol.ssl +++ b/artifacts/mods/gl_partycontrol.ssl @@ -12,12 +12,19 @@ */ #include "..\headers\define.h" -#include "..\headers\command.h" +//#include "..\headers\command.h" #include "main.h" #define OBJ_DATA_LIGHT_DISTANCE (0x6C) #define OBJ_DATA_LIGHT_INTENSITY (0x70) +procedure start; +procedure AllowControl(variable pid); +procedure CombatTurn_Handler; +procedure GameModeChange_Handler; +procedure InventoryMove_Handler; +procedure InvenWield_Handler; + variable controlMode, noCheckArray, @@ -32,7 +39,7 @@ procedure AllowControl(variable pid) begin return false; end -procedure combatturn_handler begin +procedure CombatTurn_Handler begin variable status := get_sfall_arg, critter := get_sfall_arg, @@ -98,7 +105,7 @@ procedure combatturn_handler begin end end -procedure gamemodechange_handler begin +procedure GameModeChange_Handler begin if (inControl and not(get_game_mode BWAND COMBAT)) then begin inControl := false; npcControl := 0; @@ -106,7 +113,7 @@ procedure gamemodechange_handler begin end end -procedure inventorymove_handler begin +procedure InventoryMove_Handler begin if (npcControl and get_sfall_arg == 3) then begin // armor slot if (obj_pid(dude_obj) == PID_MARCUS or proto_data(obj_pid(dude_obj), cr_body_type) != CR_BODY_BIPED) then begin display_msg(message_str_game(GAME_MSG_PROTO, 675)); @@ -115,8 +122,9 @@ procedure inventorymove_handler begin end end -procedure invenwield_handler begin - variable critter := get_sfall_arg, +procedure InvenWield_Handler begin + variable + critter := get_sfall_arg, item := get_sfall_arg, slot := get_sfall_arg, event := get_sfall_arg; @@ -135,7 +143,10 @@ procedure start begin set_perk_level(PERK_gecko_skinning_perk, 999); // prevent it from appearing in the perk selection window controlMode := GetConfig("CombatControl", "Mode", 0); - if (controlMode > 2) then controlMode := 0; + if (controlMode >= 3) then begin + if (controlMode == 3) then metarule3(999, 0, 0, 0); // enable feature: order party members to attack a specified target + controlMode := 0; + end if (controlMode > 0) then begin displayName := GetConfig("CombatControl", "DisplayName", 0); @@ -160,10 +171,10 @@ procedure start begin perksList := GetConfigListInt("CombatControl", "PerksList"); fix_array(perksList); - register_hook_proc(HOOK_COMBATTURN, combatturn_handler); - register_hook_proc(HOOK_GAMEMODECHANGE, gamemodechange_handler); - register_hook_proc(HOOK_INVENTORYMOVE, inventorymove_handler); - register_hook_proc(HOOK_INVENWIELD, invenwield_handler); + register_hook_proc(HOOK_COMBATTURN, CombatTurn_Handler); + register_hook_proc(HOOK_GAMEMODECHANGE, GameModeChange_Handler); + register_hook_proc(HOOK_INVENTORYMOVE, InventoryMove_Handler); + register_hook_proc(HOOK_INVENWIELD, InvenWield_Handler); end end end diff --git a/artifacts/sfall.dat b/artifacts/sfall.dat index 0d7c96fa..8a0cd912 100644 Binary files a/artifacts/sfall.dat and b/artifacts/sfall.dat differ diff --git a/sfall/CheckAddress.cpp b/sfall/CheckAddress.cpp index 9d7b6a74..1d6e542a 100644 --- a/sfall/CheckAddress.cpp +++ b/sfall/CheckAddress.cpp @@ -86,6 +86,7 @@ static std::vector hackAddr = { // module: PartyControl {0x422BDE, 1}, {0x4229EC, 1}, {0x49EB09, 1}, {0x49EB0A, 4}, {0x458242, 1}, {0x458243, 4}, {0x458326, 1}, {0x458327, 4}, {0x42E25B, 1}, {0x42E25C, 4}, // hookcalls + {0x44C4A7, 1}, {0x44C4A8, 4}, {0x44C75F, 1}, {0x44C760, 4}, {0x44C69A, 1}, {0x44C69B, 4}, {0x44B830, 1}, {0x44B831, 4}, // hookcalls for party order attack // module: Perks {0x43C77C, 1}, {0x43C77D, 4}, // hookcall {0x478AC4, 1}, {0x478AC5, 4}, {0x496823, 5}, diff --git a/sfall/FalloutEngine/EngineUtils.cpp b/sfall/FalloutEngine/EngineUtils.cpp index 41b3db47..c46102c0 100644 --- a/sfall/FalloutEngine/EngineUtils.cpp +++ b/sfall/FalloutEngine/EngineUtils.cpp @@ -186,10 +186,10 @@ 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) { - size_t patryCount = fo::var::partyMemberMaxCount; - if (patryCount) { + size_t partyCount = fo::var::partyMemberMaxCount; + if (partyCount) { DWORD* memberPids = fo::var::partyMemberPidList; // pids from party.txt - for (size_t i = 0; i < patryCount; i++) { + for (size_t i = 0; i < partyCount; i++) { if (memberPids[i] == pid) return i + 1; } } diff --git a/sfall/FalloutEngine/Enums.h b/sfall/FalloutEngine/Enums.h index 19d92a3f..c5cea74b 100644 --- a/sfall/FalloutEngine/Enums.h +++ b/sfall/FalloutEngine/Enums.h @@ -802,6 +802,79 @@ namespace WinFlags { }; } +namespace AIpref { + enum distance : long + { + stay_close = 0, + charge = 1, + snipe = 2, + on_your_own = 3, + stay = 4 + }; + + enum disposition : long + { + none = -1, + custom = 0, + coward = 1, + defensive = 2, + aggressive = 3, + berserk = 4 + }; + + enum class attack_who : long + { + whomever_attacking_me = 0, + strongest = 1, + weakest = 2, + whomever = 3, + closest = 4, + }; + + enum class run_away_mode : long + { + none = -1, // get the value from the cap.min_hp (in cai_get_min_hp_) + coward = 0, // 0% + finger_hurts = 1, // 25% of the lost amount of health + bleeding = 2, // 40% of the lost amount of health + not_feeling_good = 3, // 60% of the lost amount of health + tourniquet = 4, // 75% of the lost amount of health + never = 5 // 100% + }; + + enum class weapon_pref : long + { + no_pref = 0, + melee = 1, + melee_over_ranged = 2, + ranged_over_melee = 3, + ranged = 4, + unarmed = 5, + unarmed_over_thrown = 6, // not available in party member control panel + random = 7 // not available in party member control panel + }; + + enum class area_attack_mode : long + { + no_pref = -1, // special logic for NPC (not available in party member control panel) + always = 0, + sometimes = 1, // use random value from cap.secondary_freq + be_sure = 2, // 85% hit chance + be_careful = 3, // 50% hit chance + be_absolutely_sure = 4, // 95% hit chance + }; + + enum class chem_use_mode : long + { + clean = 0, + stims_when_hurt_little = 1, + stims_when_hurt_lots = 2, + sometimes = 3, + anytime = 4, + always = 5, + }; +} + enum QueueType : long { drug_effect_event = 0, // critter use drug diff --git a/sfall/Modules/BugFixes.cpp b/sfall/Modules/BugFixes.cpp index 5f3e719a..4ce9ee73 100644 --- a/sfall/Modules/BugFixes.cpp +++ b/sfall/Modules/BugFixes.cpp @@ -3598,7 +3598,7 @@ void BugFixes::init() // Fix broken Print() script function HookCall(0x461AD4, (void*)fo::funcoffs::windowOutput_); - // Fix for the flags of non-door objects being set/unset when opening/closing objects (e.g. using obj_close/open functions) + // Fix for the flags of non-door objects being set/unset when using obj_close/open functions MakeCall(0x49CBF7, check_door_state_hack_close, 2); MakeCall(0x49CB30, check_door_state_hack_open, 1); } diff --git a/sfall/Modules/MetaruleExtender.cpp b/sfall/Modules/MetaruleExtender.cpp index 1b8b9170..102d541e 100644 --- a/sfall/Modules/MetaruleExtender.cpp +++ b/sfall/Modules/MetaruleExtender.cpp @@ -19,6 +19,7 @@ #include "..\main.h" #include "..\FalloutEngine\Fallout2.h" #include "LoadGameHook.h" +#include "PartyControl.h" #include "MetaruleExtender.h" @@ -35,6 +36,7 @@ static bool HorriganEncounterDisabled = false; enum class MetaruleFunction : long { SET_HORRIGAN_ENCOUNTER = 200, // sets the number of days for the Frank Horrigan encounter or disable encounter CLEAR_KEYBOARD_BUFFER = 201, // clears the keyboard input buffer, should be used in the HOOK_KEYPRESS hook to clear keyboard events in some cases + EXT_PARTY_ORDER_ATTACK = 999 }; /* @@ -61,6 +63,9 @@ static long __fastcall op_metarule3_ext(long metafunc, long* args) { case MetaruleFunction::CLEAR_KEYBOARD_BUFFER: __asm call fo::funcoffs::kb_clear_; break; + case MetaruleFunction::EXT_PARTY_ORDER_ATTACK: + PartyControl::MemberOrderAttackPatch(); + break; default: fo::func::debug_printf("\nOPCODE ERROR: metarule3(%d, ...) - metarule function number does not exist.\n > Script: %s, procedure %s.", metafunc, fo::var::currentProgram->fileName, fo::func::findCurrentProc(fo::var::currentProgram)); diff --git a/sfall/Modules/PartyControl.cpp b/sfall/Modules/PartyControl.cpp index 9e130aa1..51c9e542 100644 --- a/sfall/Modules/PartyControl.cpp +++ b/sfall/Modules/PartyControl.cpp @@ -23,6 +23,7 @@ #include "HookScripts.h" #include "LoadGameHook.h" #include "ScriptExtender.h" +//#include "Objects.h" #include "PartyControl.h" @@ -271,6 +272,9 @@ static void SetCurrentDude(fo::GameObject* npc) { } if (!exist) __asm call fo::funcoffs::critter_sneak_check_; } + + //if (!isPartyMember) Objects::SetObjectUniqueID(npc); + fo::func::intface_redraw(); } @@ -622,6 +626,126 @@ static void __declspec(naked) gdControlUpdateInfo_hook() { } } +static bool partyOrderPickTargetLoop; +static char partyOrderAttackMsg[33]; + +// disables the display of the hit chance value when picking a target +static void __declspec(naked) gmouse_bk_process_hack() { + __asm { + mov edx, -1; // mode + mov eax, ds:[FO_VAR_gmouse_3d_current_mode]; + test partyOrderPickTargetLoop, 0xFF; + cmovnz eax, edx; + retn; + } +} + +static void __fastcall action_attack_to(long unused, fo::GameObject* partyMember) { + fo::func::gmouse_set_cursor(20); + fo::func::gmouse_3d_set_mode(2); + + fo::GameObject* targetObject = nullptr; + fo::GameObject* validTarget = nullptr; + + long outlineColor; // backup color + fo::BoundRect rect; + partyOrderPickTargetLoop = true; + + do { + fo::GameObject* underObject = fo::func::object_under_mouse(1, 0, fo::var::map_elevation); + + if (targetObject && targetObject != underObject) { + targetObject->outline = outlineColor; + fo::func::obj_bound(targetObject, &rect); + if (!outlineColor) { + rect.x--; + rect.y--; + rect.offx += 2; + rect.offy += 2; + } + fo::func::tile_refresh_rect(&rect, fo::var::map_elevation); + targetObject = validTarget = nullptr; + } + if (underObject && underObject != targetObject && underObject->IsCritter() && underObject->critter.teamNum != partyMember->critter.teamNum) { + if (underObject->critter.IsNotDead()) { + outlineColor = underObject->outline; + + if (underObject->critter.combatState) { + underObject->outline = 254 << 8; + validTarget = underObject; + } else { + underObject->outline = 10 << 8; + } + fo::func::obj_bound(underObject, &rect); + fo::func::tile_refresh_rect(&rect, fo::var::map_elevation); + targetObject = underObject; + } + } + if (validTarget && *(DWORD*)FO_VAR_mouse_buttons == 1) break; // left mouse button + + } while (*(DWORD*)FO_VAR_mouse_buttons != 2 && fo::func::get_input() != 27); // 27 - escape code + + if (validTarget && *(DWORD*)FO_VAR_mouse_buttons == 1) { + partyMember->critter.whoHitMe = validTarget; + validTarget->outline = outlineColor; + + fo::AIcap* cap = fo::func::ai_cap(partyMember); + if (cap->disposition == fo::AIpref::disposition::custom) { + cap->attack_who = (long)fo::AIpref::attack_who::whomever; + } + std::strcpy((char*)FO_VAR_attack_str, partyOrderAttackMsg); + fo::func::ai_print_msg(partyMember, 0); // float message + } + + partyOrderPickTargetLoop = false; + //*(DWORD*)FO_VAR_mouse_buttons = 0; + + fo::func::gmouse_set_cursor(0); + fo::func::gmouse_3d_set_mode(1); +} + +static void __declspec(naked) gmouse_handle_event_hook() { + __asm { + test ds:[FO_VAR_combat_state], 1; + jnz action_attack_to; + jmp fo::funcoffs::action_talk_to_; + } +} + +static void __declspec(naked) gmouse_handle_event_hack() { + __asm { + test ds:[FO_VAR_combat_state], 1; + jnz pick; + retn; +pick: + mov eax, edi; // critter + call fo::funcoffs::isPartyMember_; + test eax, eax; + jz end; + mov bl, 1; + mov eax, 5; + mov [esp + 4], eax; // actionMenuList + mov word ptr ds:[FO_VAR_gmouse_3d_action_nums][5*2], 81; // index in INTRFACE.LST (ACTIONI.FRM) +end: + or eax, 1; + retn; + } +} + +static void __declspec(naked) gmouse_handle_event_hook_restore() { + __asm { + mov word ptr ds:[FO_VAR_gmouse_3d_action_nums][5*2], 263; // index in INTRFACE.LST (TALKN.FRM) + jmp fo::funcoffs::map_enable_bk_processes_; + } +} + +void PartyControl::MemberOrderAttackPatch() { + MakeCall(0x44C4A7, gmouse_handle_event_hack, 2); + HookCall(0x44C75F, gmouse_handle_event_hook); + HookCall(0x44C69A, gmouse_handle_event_hook_restore); + MakeCall(0x44B830, gmouse_bk_process_hack); +} + static void NpcAutoLevelPatch() { npcAutoLevelEnabled = GetConfigInt("Misc", "NPCAutoLevel", 0) != 0; if (npcAutoLevelEnabled) { @@ -663,6 +787,8 @@ void PartyControl::init() { Translate("sfall", "PartyAddictMsg", "Addict", addictMsg, 16); dlogr(" Done", DL_INIT); } + + Translate("sfall", "PartyOrderAttack", "I'll take care of it.", partyOrderAttackMsg, 33); } void PartyControl::exit() { diff --git a/sfall/Modules/PartyControl.h b/sfall/Modules/PartyControl.h index 97424d39..9f1fb27b 100644 --- a/sfall/Modules/PartyControl.h +++ b/sfall/Modules/PartyControl.h @@ -37,6 +37,8 @@ public: // Returns pointer to "real" dude, which is different from "dude_obj" when controlling another critter static fo::GameObject* RealDudeObject(); + + static void MemberOrderAttackPatch(); }; extern bool npcAutoLevelEnabled;