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 e2de16b5..1df3e1b5 100644 Binary files a/artifacts/sfall.dat and b/artifacts/sfall.dat differ diff --git a/artifacts/translations/brazilian_portuguese.ini b/artifacts/translations/brazilian_portuguese.ini index d4db5f56..a4ab998d 100644 --- a/artifacts/translations/brazilian_portuguese.ini +++ b/artifacts/translations/brazilian_portuguese.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s n AmmoInfoGlovz=Div: DR/%d, DT/%d AmmoInfoYAAM=DT Mod: %d +PartyOrderAttackHuman=Eu tomarei conta disso.|Ok, eu entendi.|Soa como um plano. +PartyOrderAttackCreature=::Rosnado:: +PartyOrderAttackRobot=::Beep:: + [AppearanceMod] RaceText=Raça StyleText=Estilo diff --git a/artifacts/translations/chs.ini b/artifacts/translations/chs.ini index 73517700..3ba3e8a3 100644 --- a/artifacts/translations/chs.ini +++ b/artifacts/translations/chs.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s AmmoInfoGlovz=Div: DR/%d, DT/%d AmmoInfoYAAM=DT Mod: %d +PartyOrderAttackHuman=ÎÒ»á¶Ô¸¶ËûµÄ¡£|ºÃ£¬ÎÒÖªµÀÁË¡£|ºÃÖ÷Òâ¡£ +PartyOrderAttackCreature=::ºðºð:: +PartyOrderAttackRobot=::ßÙ:: + [AppearanceMod] RaceText=ÖÖ×å StyleText=·ç¸ñ diff --git a/artifacts/translations/cht.ini b/artifacts/translations/cht.ini index 54981c9b..ce007d73 100644 --- a/artifacts/translations/cht.ini +++ b/artifacts/translations/cht.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s AmmoInfoGlovz=Div: DR/%d, DT/%d AmmoInfoYAAM=DT Mod: %d +PartyOrderAttackHuman=§Ú·|¹ï¥I¥Lªº¡C|¦n¡A§Úª¾¹D¤F¡C|¦n¥D·N¡C +PartyOrderAttackCreature=::§q§q:: +PartyOrderAttackRobot=::¹Í:: + [AppearanceMod] RaceText=ºØ±Ú StyleText=­·®æ diff --git a/artifacts/translations/french.ini b/artifacts/translations/french.ini index 0c949076..e57e8efb 100644 --- a/artifacts/translations/french.ini +++ b/artifacts/translations/french.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s ne peux pas prendre cet objet. AmmoInfoGlovz=Div. : RD/%d, SD/%d AmmoInfoYAAM=Mod. SD : %d +PartyOrderAttackHuman=Je m'en occupe.|Okay, je fais ça.|Bonne idée. +PartyOrderAttackCreature=::Grogne:: +PartyOrderAttackRobot=::Biip:: + [AppearanceMod] RaceText=Race StyleText=Style diff --git a/artifacts/translations/german.ini b/artifacts/translations/german.ini index 7fec334e..bcdefa8c 100644 --- a/artifacts/translations/german.ini +++ b/artifacts/translations/german.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s kann das Objekt nicht nehmen. AmmoInfoGlovz=Div: RV/%d, SV/%d AmmoInfoYAAM=SV-Mod: %d +PartyOrderAttackHuman=Ich kümmere mich darum.|Wird gemacht.|Geht klar. +PartyOrderAttackCreature=::Grrrr:: +PartyOrderAttackRobot=::Piep:: + [AppearanceMod] RaceText=Geschlecht StyleText=Stil diff --git a/artifacts/translations/polish.ini b/artifacts/translations/polish.ini index ac4ac250..bf806be4 100644 --- a/artifacts/translations/polish.ini +++ b/artifacts/translations/polish.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s nie mo AmmoInfoGlovz=Dz.: OO/%d, PO/%d AmmoInfoYAAM=Mod. PO: %d +PartyOrderAttackHuman=Zajmê siê tym.|Okej, zrozumia³em.|No to mamy plan. +PartyOrderAttackCreature=::Warkniêcie:: +PartyOrderAttackRobot=::Bip:: + [AppearanceMod] RaceText=Rasa StyleText=Styl diff --git a/artifacts/translations/russian.ini b/artifacts/translations/russian.ini index a3b9e41d..3994ed94 100644 --- a/artifacts/translations/russian.ini +++ b/artifacts/translations/russian.ini @@ -14,6 +14,10 @@ NPCPickupFail=%s AmmoInfoGlovz=Äåë.: ÑÓ/%d, ÏÓ/%d AmmoInfoYAAM=Èçì. ÏÓ: %d +PartyOrderAttackHuman=Õîðîøî, ÿ èì çàéìóñü.|Öåëü ÿñíà.|Äåéñòâóþ ïî ïëàíó. +PartyOrderAttackCreature=::Ãððð:: +PartyOrderAttackRobot=::Áèèï:: + [AppearanceMod] RaceText=Ðàñà StyleText=Ñòèëü diff --git a/sfall/FalloutEngine/FunctionOffsets_def.h b/sfall/FalloutEngine/FunctionOffsets_def.h index be8416be..2a59069e 100644 --- a/sfall/FalloutEngine/FunctionOffsets_def.h +++ b/sfall/FalloutEngine/FunctionOffsets_def.h @@ -41,6 +41,7 @@ FUNC(SexWindow_, 0x437664) FUNC(_word_wrap_, 0x4BC6F0) FUNC(action_get_an_object_, 0x412134) FUNC(action_loot_container_, 0x4123E8) +FUNC(action_talk_to_, 0x413330) FUNC(action_use_an_item_on_object_, 0x411F2C) FUNC(action_use_skill_on_, 0x41255C) FUNC(add_bar_box_, 0x4616F0) @@ -49,6 +50,7 @@ FUNC(adjust_fid_, 0x4716E8) FUNC(ai_can_use_weapon_, 0x4298EC) // (GameObject *aCritter, 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"