Backported the "order to attack" function from 4.x

Updated translation ini files.
Updated version number.
This commit is contained in:
NovaRain
2023-04-19 10:35:59 +08:00
parent 291370a96c
commit a09611edc6
19 changed files with 309 additions and 5 deletions
+4
View File
@@ -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
+2 -1
View File
@@ -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
Binary file not shown.
@@ -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
+4
View File
@@ -14,6 +14,10 @@ NPCPickupFail=%s
AmmoInfoGlovz=Div: DR/%d, DT/%d
AmmoInfoYAAM=DT Mod: %d
PartyOrderAttackHuman=我会对付他的。|好,我知道了。|好主意。
PartyOrderAttackCreature=::吼吼::
PartyOrderAttackRobot=::哔::
[AppearanceMod]
RaceText=种族
StyleText=风格
+4
View File
@@ -14,6 +14,10 @@ NPCPickupFail=%s
AmmoInfoGlovz=Div: DR/%d, DT/%d
AmmoInfoYAAM=DT Mod: %d
PartyOrderAttackHuman=我會對付他的。|好,我知道了。|好主意。
PartyOrderAttackCreature=::吼吼::
PartyOrderAttackRobot=::嗶::
[AppearanceMod]
RaceText=種族
StyleText=風格
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -14,6 +14,10 @@ NPCPickupFail=%s
AmmoInfoGlovz=Дел.: СУ/%d, ПУ/%d
AmmoInfoYAAM=Изм. ПУ: %d
PartyOrderAttackHuman=Хорошо, я им займусь.|Цель ясна.|Действую по плану.
PartyOrderAttackCreature=::Гррр::
PartyOrderAttackRobot=::Биип::
[AppearanceMod]
RaceText=Раса
StyleText=Стиль
@@ -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<eax>, int aWeapon<edx>, int a2Or3<ebx>) 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)
+1
View File
@@ -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
+53
View File
@@ -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<int> buildLineTiles;
//static bool TileExists(long tile) {
+2
View File
@@ -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);
};
+1
View File
@@ -552,6 +552,7 @@ static void __fastcall CombatInternal(fo::CombatGcsd* gcsd) {
// OnCombatEnd
AI::AICombatClear();
PartyControl::OnCombatEnd();
SetInLoop(0, COMBAT);
}
+6
View File
@@ -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;
+205 -2
View File
@@ -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<std::string> partyOrderAttackMsg;
struct PartyTarget {
fo::GameObject* member;
fo::GameObject* target;
};
static std::vector<PartyTarget> partyOrderAttackTarget;
static fo::GameObject* GetMemberTarget(fo::GameObject* member) {
for (std::vector<PartyTarget>::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<PartyTarget>::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<std::string> msgs = Translate::GetList("sfall", "PartyOrderAttackHuman", "I'll take care of it.|Okay, I got it.", '|', 512);
partyOrderAttackMsg.insert(partyOrderAttackMsg.cend(), msgs.cbegin(), msgs.cend());
}
}
+3
View File
@@ -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;
+2 -2
View File
@@ -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"