From 9e403e0e6b2265ca9c44a08eac83108707358b0d Mon Sep 17 00:00:00 2001 From: NovaRain Date: Thu, 21 Jan 2021 12:59:45 +0800 Subject: [PATCH] Added a new mode to NPC combat control mod (WIP) --- artifacts/config_files/sfall-mods.ini | 1 + artifacts/mods/gl_partycontrol.int | Bin 4388 -> 4440 bytes artifacts/mods/gl_partycontrol.ssl | 33 ++++--- artifacts/sfall.dat | Bin 156523 -> 158410 bytes sfall/CheckAddress.cpp | 1 + sfall/FalloutEngine/EngineUtils.cpp | 6 +- sfall/FalloutEngine/Enums.h | 73 +++++++++++++++ sfall/Modules/BugFixes.cpp | 2 +- sfall/Modules/MetaruleExtender.cpp | 5 + sfall/Modules/PartyControl.cpp | 126 ++++++++++++++++++++++++++ sfall/Modules/PartyControl.h | 2 + 11 files changed, 234 insertions(+), 15 deletions(-) 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 c2a36601db2575ab996d81aedaf72ab988f9a8fe..56797cbd94744f237b2f34dda9774bad54a95635 100644 GIT binary patch delta 1075 zcmZ3YbVF&v#Ci_~6u@QzKxi%q#lRp?FEH$qrzo^nTzYIx21S}q&nVOS= zq?#ugVv+CUJjSlgc}#a0nb=G=`>?!YoP2cjUnR9&mrQ-WW&P-DHOgih{tI&`2n986T8>s zAAEXD98Hro_zjpioF+%{t1+?Pn%u;%!o>b*@(O-ADGn=;dIol=8(166Aui?6n*4xY zkBK8_vVedX6Gzo#4FNqSjwO>L1bmn{K22UGV5!U2lmyl*4<^~6JP9z51Im*G^EjK5 zCOh!R32`0*a~T_iAeL}Wo9x48z{I(3avj?YA+A2KEL(#R#JgN2lQr05+;|s(g&7)* zz%*lnF+?*rQ-ef{lU><7&D@^U=Q|tyzJi8|UVV7Xyj+iXQ5yr&h zF*%PThlxjH@*NHjChoq;YMcdJJTDqpKzeyzOkTjLBg8uk#9`oqxrtX{@;y!ym@qrc zBfK$_Rk)(0_!A(S`4doe@hzJ?gUg7CZ`tHCTscg9ZIkbCXiiS%mgnMq1JS^DZgLN| zS^!iZ%&QEr$l#Z1kZ5p)d6r)YmuJDYGBiMw3xC37CY~4}{&QeqkQcydkblwS0-hKl z0f-wxp$!%m;G2ApM@&p0p<#a`!vRJ{28M;OkQQ*8%*8uJNYDXn5OaeOBv1vOP2RvO dqb4YVhzCItsM8o4j6kM*P;bMwoPBt*dC<(a8D zDM+f>ic1oUN*MnC|NozXfw6&Mb2-yxMkYRs%|0ye7=_pt9RLLfV}nqGG>FAkHaUmW zfQhYd@;uHNLhN}UF$T5rWqTIA)47AHApn*H%Ng* z*$*}Ff@lUNm?|)jrGXWucJeDO118Q{lV!Lim^dsZn{kIRacWHNA zsxI!f$vb$Bn7G>}zv0bc;!d0Vhg)-UGoL&c*BOWg?sb#b@Tmnr^})Q#&|nU6KKHK% zi3V4gXSqM&@+{a^h6ZCu`0+SQR^pEl;#mh426+J-=sZo6C-BDz@j~1P3T=q+yUG9f z#l(0W8um9b9AIQ*U|0wXXMz%ItbFEv?%U5`m1XmSL*0h55x 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 0d7c96fa722b4989eb873f56c26f9e231ddc04e8..8a0cd91260d071f5be2a6b38dd2cc7809e13df05 100644 GIT binary patch delta 1992 zcmaETjPul0&iaa5Q>PgAJuwq-z1^_!B;QeH3AJGN)!G&XCc2jzrb=CYKBLB9LWx}b zfk5Vv#!I3;aYz1(FTHn{QD(=AWgf4-u1{Kdoc`aH<#{Rf4HHpp?>eCXv6y2+NTOR zvEqkKXZ_e4dsA|9W$(|MJ@?Ae4g}RHT*_msHx*2%IcK=~UW4oBz>kSfikIE{wMKYL z(3gwOe`HqND?Q2ha$UcBS0nHRQkE4)dPzw;zp z`1qlP;>BuemM0db@2&U$TB7@;{@eY z{u&T0);g!^O6L5Ci%r+fl(qX(IS(Ea)ifxmeiyttePUNn!J4yw+|G%v4M`F7`+2p{ z>-t-5i$}(*FW1k!q0h9uzgY2VNuBPj=dZQa&Re@`?v2D(-#gB4Rn1eYdm`nrw_-Zm zw4i;K;UWiI<1c=Yi4whZm09EMN}1@aDmTlFvX33JTrB5opWOE~=bEvM46{UC@t--F z7vEpvk>#2FVxReG*KYY^7mW4w|9aoQ|BHA3yu)Q*bJxC~e9mswpZe*s|5%-3AO5Yl zH8o^o(PJ}#&@`j#HLpb$1j@3<)ZI z>R6ogSkjaEqp;6Q_fKM0-%lQDb9ngj?d_-U%B<$Tx6Zj=`fq!!L|NT|S2a7n^m|TP zb}o9>2Fo@1@^wGgax(Ap-59o~zVf!p`{UAT4Q$*(FTg>3y7jh`#!^}wxM^>#^WI2(=PwblEeYZ0Yj(90)J*zXGs-r#Oyvxh_ z`n?JJvVW{Ch@E)-c+H;JR}lhN0u)(Q10~o_G87er2Pr%gI4{jP*Wc~OTtR(-6E%)a z>Yt{W_(V@yxGE%4R{!Xo0TYT649z=qvA8!Q&V6KN~kGXS0Mh zT3bG!VD9z&X6>}}Rr@xkaJ?*)){d*OI$?TpWxZL+;$@Zd(k=5>=}u32R%p+tRGY)7 zrBKi><+JtFSC!MtBWoR}=1kmw`JqDC`oohSYM(l^^RmILXRE@jg4PB_z7yz;sI0wm zEhfzK@bAhUr#C-8z1e$fmDJp$m)}&n*y*25`@mix*RrI;9DZaMcUc1uv8+{>v!mCM$@I-d6Y^QS;VL1zBSZw)Oj)pPGZ=X&tw zpW^BNH;wNXT=45_y{~ec`LitF#d$wvo(EnJ&+Wf2{m9nqamB-mS+Zq&4~w$cZ8iOJ zbmb-Mb*uL$y;c5jV1fDB&9PhF`i9szN&JrTjIX~iJ>%WB{5jDb^;R->RIGN(tgeY# z9>28ULH0eidFw5gZMew3XC22xt-o_pwbpq`9gE%1`F&n`+1>l!?4C!O9X++I^iJQB z4fFDK9^I<-F1fU(^zE0=C0%1x!``eLKUVd=j#|7z{1X-(O`qFP2GgLwG@&=0oY2OG$eoUT{~&B{H03de44jk$}w&mh*O*TxZmn7Cx75yPJFC?-M84 zjOEOGo?d$>+;zzR?uOgEhXra5zRmr=?f(C*`=t-p@Be?h>TG?j*}rx7<3E?yOaC)} z{fGO#$)-sSEsR^33X?)O7#J8N7#J85i%McL^Gb@+5|dM79Gycv{rx=k(u#5!yVw{Q zn3x&B0Efa5xI%6gkmBjZNlaQyyzJBGCo$ 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;