From a09611edc66dc3c79c22f125cb19529a6fac92ca Mon Sep 17 00:00:00 2001 From: NovaRain Date: Wed, 19 Apr 2023 10:35:59 +0800 Subject: [PATCH] Backported the "order to attack" function from 4.x Updated translation ini files. Updated version number. --- artifacts/config_files/Translations.ini | 4 + artifacts/ddraw.ini | 3 +- artifacts/sfall.dat | Bin 43151 -> 44995 bytes .../translations/brazilian_portuguese.ini | 4 + artifacts/translations/chs.ini | 4 + artifacts/translations/cht.ini | 4 + artifacts/translations/french.ini | 4 + artifacts/translations/german.ini | 4 + artifacts/translations/polish.ini | 4 + artifacts/translations/russian.ini | 4 + sfall/FalloutEngine/FunctionOffsets_def.h | 2 + sfall/FalloutEngine/VariableOffsets.h | 1 + sfall/Game/tilemap.cpp | 53 +++++ sfall/Game/tilemap.h | 2 + sfall/Modules/LoadGameHook.cpp | 1 + sfall/Modules/MetaruleExtender.cpp | 6 + sfall/Modules/PartyControl.cpp | 207 +++++++++++++++++- sfall/Modules/PartyControl.h | 3 + sfall/version.h | 4 +- 19 files changed, 309 insertions(+), 5 deletions(-) diff --git a/artifacts/config_files/Translations.ini b/artifacts/config_files/Translations.ini index 25df2315..99d0e08b 100644 --- a/artifacts/config_files/Translations.ini +++ b/artifacts/config_files/Translations.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s cannot pick up the item. AmmoInfoGlovz=Div: DR/%d, DT/%d AmmoInfoYAAM=DT Mod: %d +PartyOrderAttackHuman=I'll take care of it.|Okay, I got it.|Sounds like a plan. +PartyOrderAttackCreature=::Growl:: +PartyOrderAttackRobot=::Beep:: + [AppearanceMod] RaceText=Race StyleText=Style diff --git a/artifacts/ddraw.ini b/artifacts/ddraw.ini index 70f21941..e837cbba 100644 --- a/artifacts/ddraw.ini +++ b/artifacts/ddraw.ini @@ -1,5 +1,5 @@ ;sfall configuration settings -;v3.8.38 +;v3.8.38.1 [Main] ;Set to 1 if you want to use command line arguments to tell sfall to use another ini file @@ -636,6 +636,7 @@ PipboyTimeAnimDelay=50 ;Set to 0 to disable ;Set to 1 to control all critters in combat (allowed only in sfall debugging mode) ;Set to 2 to control all party members +;Set to 3 to let you order party members to attack specified targets after your first attack instead of controlling them directly ControlCombat=0 ;If you want to control only specific critters, uncomment the ControlCombatPIDList line and set a comma delimited list of PIDs ;ControlCombatPIDList=62,89,97,107,160,161 diff --git a/artifacts/sfall.dat b/artifacts/sfall.dat index e2de16b59d1b46a50608f58de6a7d2e85ed2b685..1df3e1b51cd744b3fe851e877730474276deae3a 100644 GIT binary patch delta 1942 zcmeCb$aMHVQ+>s)sUdswZW{=cH7a=*Jz?Hrdfo5Yl&rTGPKxO}Y!y3MoN_M7(?9rP z9P^fL##auXC0B{Rl-0EMYv8kuGO4$#z5lV~_|c?~b&B8eKHe`2)we5-j;TEx{Ct*L zTK1fXCuaYjPTRf2p-eh&|B};YyDop&J>kTJX48oSlT9w_5t42*BwP7q@=cpm112Wf6bqfX zRP`)5BIa|G_ZOv?7vCf=JXgQ#W#8dRo*GtK4uWqs+<*Jc(t!2(DX~Ru6|sLNwd(#T zyCFGK{LZcd?)V~e88$oX%Kvw&*}hdDJo$acO71?5h7_*O{I?gjI^X`8cZ1VF?0*N} z*V5VtUpkfkW@c>Mv@dGyt@~@=^Kuu)T-u>H=}WO^Re||F{)02Ld?vsBbK&Qr)Ai3U z^mwa=yWZOVkpB_$Zeg~tcfJpIJFiWeyvRiHZTpg&>dNi^4wcmTPhwxWY(cQj2A6s} zzS<`yGi{iYpIS%z&g9REx~Vj4){EV5gP&_V^l{5yF89B^aQQQh%GQHv|6|@Id@$2k z^Q(*5bGM_??9cD{X6kc4KTz}G?#C1FSeX-?>Ytx&K6s}5M9oIc^fXQV$TPP;O{|@2 zul3lBJ*u^c%W|#aYipai*ZH@r%UGPMVG_!$USpBvBG<*Tbn$ybxAo8JR)<7d3MrfU;X?x z$MMIv0wMR^ee+B2uG8i^!}?~@a=zcEPkZkR>@QpsI%}C(>SmX)h?u0-XwPr+)~No8 zKX2;Se?hpiM^t)pNk$px_7FXYJd0--NbxOZ{>N{Mb|cDshakEw5s2q8+7x4 zu*~}Zv8@r4eNullemwU;Joa8UE>0_y(L5M^+k=nc&&yin%BS1c zRNQZ1d+d73ol{9^(>x1-lmwlyI%mf7Tx?9MURlcWIt$NCE88AdY5(w!^o3}}w8Xhq z2EhxKxlCP?pZ{LREX?NdTIV#S@{$kr?{u2>OiR0X@7#<_m*)TGGzqhdxxPBNS3z*2 zA%AJM*wmfQK1mM$`nU~*?e26;WxCF?geyEXiut-zo{jyf&#{FeZxrO8$8PWba%$$u z{Q25@0?x1bQu(sw-ljhx=Zly0`P%N(GT)pt*;mY7$HzD9V|nmundiS2gqG!s2|JeY zEv>)vCwTh;kH3q*uimO%eZ1N6wb`|>2}wI^ju={ev(h+X@;=t$n*2VW+qZi8zfHDm zv|IP8%t+qDI8O1)?gv}+=dN5=pa1&u2}OOUIWxZXzrJ=uzkki@{YB4zYTCO_wN#lo zJJfiZ_uWldroU^Ii{94$YPMCFTwAx_^8BU7E1gXKUil z4x#6DH-Fl+yR42=UwTs_c=joY%Y4$)xTngU&)VDGn|ycr7ac5$ zWZ!RccKiL8G5!lL*SqjfS!-m>Q~%HAB-;v(qRe0Ne=Scx)!JS#cS`iGDD%xX3-0~1 zU7}GtrO{{aeZLKEOV4_5KR;)R5UZua;g{a=58hW_i>z`m4SL5n^Yg0Z<;590DqnIp z$}8=1cb)Rn?E6&-E~Dp7HLtjPmiJ$f4%o1=Wa)ePMGtw}L1Js{@76c6#2wx3|F=2O zMdQTseFDEGJZ-pGoAW|mp+9HIJ_XG$tj}LF*RnG#d|c4b5gQ<7Bx4nMp_oT{f7!;q zEtduN-FvX1DezB~O#kr;u01avDOJu^*{|0BL-pkCzw_3&^#1prR9n>eQ{cb3bS*1i z%l7jXx8Cga?SGs$O)nz!ZBd%XMu7**L)aJ?7$g`N7!r$0VlwkeiqaC3Q)3*RLp=Tc zJoVCwav8hW7#L)j8NdLC!VtK^W6U7MlRqujVtT|jS#E_Ilg8@FZYxw(^w%*kurn|) eq@)%n>t&`es4_5s@!E9^3@;fN7!I#zU;qGT*reJ3 delta 52 zcmX?npQ-;M)8, int aWeapon, int a2Or3) returns 1 or 0 FUNC(ai_cap_, 0x4280B4) FUNC(ai_check_drugs_, 0x428480) +FUNC(ai_danger_source_, 0x428F4C) FUNC(ai_have_ammo_, 0x4292D4) FUNC(ai_magic_hands_, 0x428398) FUNC(ai_pick_hit_mode_, 0x429DB4) diff --git a/sfall/FalloutEngine/VariableOffsets.h b/sfall/FalloutEngine/VariableOffsets.h index 71147ca3..af7ff66c 100644 --- a/sfall/FalloutEngine/VariableOffsets.h +++ b/sfall/FalloutEngine/VariableOffsets.h @@ -112,6 +112,7 @@ #define FO_VAR_gdNumOptions 0x5186D8 #define FO_VAR_gIsSteal 0x51D430 #define FO_VAR_glblmode 0x5709D0 +#define FO_VAR_gmouse_3d_action_nums 0x518D1E #define FO_VAR_gmouse_3d_current_mode 0x518D38 #define FO_VAR_gmouse_current_cursor 0x518C0C #define FO_VAR_gmovie_played_list 0x596C78 diff --git a/sfall/Game/tilemap.cpp b/sfall/Game/tilemap.cpp index eb51304d..14966605 100644 --- a/sfall/Game/tilemap.cpp +++ b/sfall/Game/tilemap.cpp @@ -14,6 +14,59 @@ namespace game namespace sf = sfall; +// same as obj_blocking_at_ engine function, but ignores objects of the critter type +static fo::GameObject* __fastcall obj_path_blocking_at(fo::GameObject* source, long tile, long elev) { + if (tile < 0 || tile >= 40000) return nullptr; + + fo::ObjectTable* obj = fo::ptr::objectTable[tile]; + while (obj) { + if (elev == obj->object->elevation) { + fo::GameObject* object = obj->object; + long flags = object->flags; + if (!(flags & (fo::ObjectFlag::Hidden | fo::ObjectFlag::NoBlock)) && source != object) { + char type = object->TypeFid(); + if (type == fo::ObjType::OBJ_TYPE_SCENERY || type == fo::ObjType::OBJ_TYPE_WALL) { + return object; + } + } + } + obj = obj->nextObject; + } + // check for multihex objects on the tile + long direction = 0; + do { + long _tile = fo::func::tile_num_in_direction(tile, direction, 1); + if (_tile >= 0 && _tile < 40000) { + obj = fo::ptr::objectTable[_tile]; + while (obj) { + if (elev == obj->object->elevation) { + fo::GameObject* object = obj->object; + long flags = object->flags; + if (flags & fo::ObjectFlag::MultiHex && !(flags & (fo::ObjectFlag::Hidden | fo::ObjectFlag::NoBlock)) && source != object) { + char type = object->TypeFid(); + if (type == fo::ObjType::OBJ_TYPE_SCENERY || type == fo::ObjType::OBJ_TYPE_WALL) { + return object; + } + } + } + obj = obj->nextObject; + } + } + } while (++direction < 6); + return nullptr; +} + +void __declspec(naked) Tilemap::obj_path_blocking_at_() { + __asm { + push ecx; + push ebx; + mov ecx, eax; + call obj_path_blocking_at; + pop ecx; + retn; + } +} + //static std::vector buildLineTiles; //static bool TileExists(long tile) { diff --git a/sfall/Game/tilemap.h b/sfall/Game/tilemap.h index 20812b54..ebc01761 100644 --- a/sfall/Game/tilemap.h +++ b/sfall/Game/tilemap.h @@ -13,6 +13,8 @@ class Tilemap { public: static void init(); + static void obj_path_blocking_at_(); + static long __fastcall tile_num_beyond(long sourceTile, long targetTile, long maxRange); }; diff --git a/sfall/Modules/LoadGameHook.cpp b/sfall/Modules/LoadGameHook.cpp index e4a4fdfa..9b8e4c87 100644 --- a/sfall/Modules/LoadGameHook.cpp +++ b/sfall/Modules/LoadGameHook.cpp @@ -552,6 +552,7 @@ static void __fastcall CombatInternal(fo::CombatGcsd* gcsd) { // OnCombatEnd AI::AICombatClear(); + PartyControl::OnCombatEnd(); SetInLoop(0, COMBAT); } diff --git a/sfall/Modules/MetaruleExtender.cpp b/sfall/Modules/MetaruleExtender.cpp index 3e64849a..07cab277 100644 --- a/sfall/Modules/MetaruleExtender.cpp +++ b/sfall/Modules/MetaruleExtender.cpp @@ -19,6 +19,7 @@ #include "..\main.h" #include "..\FalloutEngine\Fallout2.h" #include "ExtraSaveSlots.h" +#include "PartyControl.h" #include "MetaruleExtender.h" @@ -41,6 +42,8 @@ enum MetaruleFunction : long { GET_CURRENT_QSAVE_PAGE = 212, GET_CURRENT_QSAVE_SLOT = 213, SET_CURRENT_QSAVE_SLOT = 214, + + PARTY_ORDER_ATTACK = 999, }; /* @@ -67,6 +70,9 @@ static long __fastcall op_metarule3_ext(long metafunc, long* args) { case CLEAR_KEYBOARD_BUFFER: __asm call fo::funcoffs::kb_clear_; break; + case PARTY_ORDER_ATTACK: + PartyControl::OrderAttackPatch(); + break; case GET_CURRENT_SAVE_SLOT: result = ExtraSaveSlots::GetSaveSlot(); break; diff --git a/sfall/Modules/PartyControl.cpp b/sfall/Modules/PartyControl.cpp index ef765cdb..40d6cebd 100644 --- a/sfall/Modules/PartyControl.cpp +++ b/sfall/Modules/PartyControl.cpp @@ -26,6 +26,9 @@ #include "..\FalloutEngine\Fallout2.h" #include "..\Translate.h" +#include "..\Game\objects.h" +#include "..\Game\tilemap.h" + #include "PartyControl.h" namespace sfall @@ -458,6 +461,193 @@ static void __declspec(naked) gdControlUpdateInfo_hook() { } } +//////////////////////////////////////////////////////////////////////////////// + +static bool partyOrderPickTargetLoop; +static std::vector partyOrderAttackMsg; + +struct PartyTarget { + fo::GameObject* member; + fo::GameObject* target; +}; + +static std::vector partyOrderAttackTarget; + +static fo::GameObject* GetMemberTarget(fo::GameObject* member) { + for (std::vector::iterator it = partyOrderAttackTarget.begin(); it != partyOrderAttackTarget.end(); ++it) { + if (it->member->id == member->id) { + fo::GameObject* target = it->target; + if (target && target->critter.IsDead()) it->target = nullptr; + return it->target; + } + } + return nullptr; +} + +static void SetMemberTarget(fo::GameObject* member, fo::GameObject* target) { + for (std::vector::iterator it = partyOrderAttackTarget.begin(); it != partyOrderAttackTarget.end(); ++it) { + if (it->member->id == member->id) { + it->target = target; + return; + } + } + PartyTarget pt = { member, target }; + partyOrderAttackTarget.push_back(pt); +} + +// 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::ptr::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::ptr::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 ((*fo::ptr::combatNumTurns || underObject->critter.combatState) && + game::Objects::is_within_perception(partyMember, underObject, 0) && // HOOK_WITHINPERCEPTION + fo::func::make_path_func(partyMember, partyMember->tile, underObject->tile, 0, 0, game::Tilemap::obj_path_blocking_at_) > 0) + { + underObject->outline = 254 << 8; // flashing red + validTarget = underObject; + } else { + underObject->outline = 10 << 8; // grey + } + fo::func::obj_bound(underObject, &rect); + fo::func::tile_refresh_rect(&rect, *fo::ptr::map_elevation); + targetObject = underObject; + } + } + if (validTarget && *fo::ptr::mouse_buttons == 1) break; // left mouse button + + } while (*fo::ptr::mouse_buttons != 2 && fo::func::get_input() != 27); // 27 - escape code + + if (validTarget && *fo::ptr::mouse_buttons == 1) { + SetMemberTarget(partyMember, validTarget); + validTarget->outline = outlineColor; + + fo::AIcap* cap = fo::func::ai_cap(partyMember); + if (cap->disposition == fo::AIpref::Disposition::DISP_custom) { + cap->attack_who = fo::AIpref::AttackWho::ATKWHO_whomever; + } + + // floating text messages + const char* message; + switch (fo::func::critter_body_type(partyMember)) { + case fo::BodyType::Quadruped: // creatures + message = partyOrderAttackMsg[0].c_str(); + break; + case fo::BodyType::Robotic: + message = partyOrderAttackMsg[1].c_str(); + break; + default: // biped + long max = partyOrderAttackMsg.size() - 1; + long rnd = (max > 2) ? fo::func::roll_random(2, max) : 2; + message = partyOrderAttackMsg[rnd].c_str(); + } + fo::util::PrintFloatText(partyMember, message, cap->color, cap->outline_color, cap->font); + } + + partyOrderPickTargetLoop = false; + *fo::ptr::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_; + } +} + +static void __fastcall SetOrderTarget(fo::GameObject* attacker) { + if (attacker->critter.teamNum || partyOrderAttackTarget.empty()) return; + fo::GameObject* target = GetMemberTarget(attacker); + if (target) attacker->critter.whoHitMe = target; +} + +static void __declspec(naked) combat_ai_hook_target() { + __asm { + push ecx; + mov ecx, eax; + call SetOrderTarget; + pop ecx; + mov eax, esi; + jmp fo::funcoffs::ai_danger_source_; + } +} + +void PartyControl::OrderAttackPatch() { + 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); + + HookCall(0x42B235, combat_ai_hook_target); +} + static void NpcAutoLevelPatch() { npcAutoLevelEnabled = IniReader::GetConfigInt("Misc", "NPCAutoLevel", 0) != 0; if (npcAutoLevelEnabled) { @@ -466,6 +656,10 @@ static void NpcAutoLevelPatch() { } } +void PartyControl::OnCombatEnd() { + partyOrderAttackTarget.clear(); +} + void PartyControl::OnGameLoad() { PartyControlReset(); if (!npcEngineLevelUp) { @@ -476,8 +670,12 @@ void PartyControl::OnGameLoad() { void PartyControl::init() { Mode = IniReader::GetConfigInt("Misc", "ControlCombat", 0); - if (Mode > 2) Mode = 0; - else if (Mode == 1 && !isDebug) Mode = 2; + 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); @@ -522,6 +720,11 @@ void PartyControl::init() { Translate::Get("sfall", "PartyACMsg", "AC:", armorClassMsg, 12); Translate::Get("sfall", "PartyAddictMsg", "Addict", addictMsg, 16); } + + partyOrderAttackMsg.push_back(Translate::Get("sfall", "PartyOrderAttackCreature", "::Growl::", 33)); + partyOrderAttackMsg.push_back(Translate::Get("sfall", "PartyOrderAttackRobot", "::Beep::", 33)); + std::vector msgs = Translate::GetList("sfall", "PartyOrderAttackHuman", "I'll take care of it.|Okay, I got it.", '|', 512); + partyOrderAttackMsg.insert(partyOrderAttackMsg.cend(), msgs.cbegin(), msgs.cend()); } } diff --git a/sfall/Modules/PartyControl.h b/sfall/Modules/PartyControl.h index e6eef609..6badadd0 100644 --- a/sfall/Modules/PartyControl.h +++ b/sfall/Modules/PartyControl.h @@ -27,6 +27,7 @@ public: static void init(); static void OnGameLoad(); + static void OnCombatEnd(); static int __fastcall SwitchHandHook(fo::GameObject* item); @@ -34,6 +35,8 @@ public: // Returns pointer to "real" dude, which is different from "dude_obj" when controlling another critter static fo::GameObject* RealDudeObject(); + + static void OrderAttackPatch(); }; extern bool npcAutoLevelEnabled; diff --git a/sfall/version.h b/sfall/version.h index f07cf3c5..b6aa5e82 100644 --- a/sfall/version.h +++ b/sfall/version.h @@ -25,6 +25,6 @@ #define VERSION_MAJOR 3 #define VERSION_MINOR 8 #define VERSION_BUILD 38 -#define VERSION_REV 0 +#define VERSION_REV 1 -#define VERSION_STRING "3.8.38" +#define VERSION_STRING "3.8.38.1"