From a90b59946da09110703c78b5fe8ce4743feb68b8 Mon Sep 17 00:00:00 2001 From: caxanga334 <10157643+caxanga334@users.noreply.github.com> Date: Sat, 7 Dec 2024 21:26:16 -0300 Subject: [PATCH] Big TF2 Update Implemented FindHealth task (missed this one for too long lol). Fixed some crashes caused by the TF2 bot. Improvements to the TF2 bot behavior (on CTF maps) Medic bot still dumb. Needs major overhaul. --- extension/bot/basebot.cpp | 5 + extension/bot/basebot.h | 3 +- extension/bot/interfaces/event_listener.h | 12 +- extension/bot/interfaces/inventory.cpp | 50 +++-- extension/bot/interfaces/inventory.h | 13 +- extension/bot/interfaces/movement.cpp | 1 + extension/bot/interfaces/path/basepath.cpp | 17 +- .../bot/interfaces/path/chasenavigator.cpp | 126 +++++++++++++ .../bot/interfaces/path/chasenavigator.h | 174 ++++++++++++++++++ .../bot/interfaces/path/meshnavigator.cpp | 10 +- extension/bot/interfaces/path/meshnavigator.h | 89 ++++++++- extension/bot/interfaces/sensor.cpp | 15 +- extension/bot/interfaces/tasks.h | 12 +- .../tasks/engineer/tf2bot_engineer_nest.cpp | 24 +-- .../tf2/tasks/engineer/tf2bot_engineer_nest.h | 8 +- .../tasks/medic/tf2bot_medic_main_task.cpp | 21 +++ .../tf2/tasks/medic/tf2bot_medic_main_task.h | 2 + .../bot/tf2/tasks/scenario/tf2bot_map_ctf.cpp | 6 + ...tf2bot_task_sniper_move_to_sniper_spot.cpp | 4 +- .../tf2bot_task_sniper_move_to_sniper_spot.h | 2 +- .../sniper/tf2bot_task_sniper_snipe_area.cpp | 8 +- extension/bot/tf2/tasks/tf2bot_attack.cpp | 93 ++++++++++ extension/bot/tf2/tasks/tf2bot_attack.h | 31 ++++ extension/bot/tf2/tasks/tf2bot_dead.cpp | 14 ++ extension/bot/tf2/tasks/tf2bot_dead.h | 14 ++ .../bot/tf2/tasks/tf2bot_find_ammo_task.cpp | 14 +- .../bot/tf2/tasks/tf2bot_find_health_task.cpp | 51 ++++- .../bot/tf2/tasks/tf2bot_find_health_task.h | 3 +- extension/bot/tf2/tasks/tf2bot_maintask.cpp | 19 +- extension/bot/tf2/tasks/tf2bot_maintask.h | 2 + extension/bot/tf2/tasks/tf2bot_roam.cpp | 13 ++ extension/bot/tf2/tf2bot.cpp | 12 +- extension/bot/tf2/tf2bot_upgrades.cpp | 5 +- extension/concommands_bots.cpp | 6 + extension/manager.cpp | 5 + extension/mods/tf2/teamfortress2_shareddefs.h | 1 + extension/mods/tf2/teamfortress2mod.cpp | 31 ++++ 37 files changed, 827 insertions(+), 89 deletions(-) create mode 100644 extension/bot/interfaces/path/chasenavigator.cpp create mode 100644 extension/bot/interfaces/path/chasenavigator.h create mode 100644 extension/bot/tf2/tasks/tf2bot_attack.cpp create mode 100644 extension/bot/tf2/tasks/tf2bot_attack.h create mode 100644 extension/bot/tf2/tasks/tf2bot_dead.cpp create mode 100644 extension/bot/tf2/tasks/tf2bot_dead.h diff --git a/extension/bot/basebot.cpp b/extension/bot/basebot.cpp index 1a9c553..749eda9 100644 --- a/extension/bot/basebot.cpp +++ b/extension/bot/basebot.cpp @@ -254,6 +254,11 @@ bool CBaseBot::IsAbleToBreak(edict_t* entity) return false; } +bool CBaseBot::IsAlive() const +{ + return UtilHelpers::IsEntityAlive(this->GetIndex()); +} + void CBaseBot::RegisterInterface(IBotInterface* iface) { m_interfaces.push_back(iface); diff --git a/extension/bot/basebot.h b/extension/bot/basebot.h index 5125b90..079fd70 100644 --- a/extension/bot/basebot.h +++ b/extension/bot/basebot.h @@ -90,6 +90,7 @@ class CBaseBot : public CBaseExtPlayer, public IEventListener virtual bool IsRangeLessThan(edict_t* edict, const float range) const; virtual bool IsAbleToBreak(edict_t* entity); + virtual bool IsAlive() const; IBotController* GetController() const { return m_controller; } @@ -182,7 +183,7 @@ class CBaseBot : public CBaseExtPlayer, public IEventListener // makes the bot hold their fire for the given time in seconds inline void DontAttackEnemies(const float time) { m_holdfire_time.Start(time); } - bool IsLineOfFireClear(const Vector& to) const; + virtual bool IsLineOfFireClear(const Vector& to) const; inline const Vector& GetHomePos() const { return m_homepos; } void SetHomePos(const Vector& home) { m_homepos = home; } diff --git a/extension/bot/interfaces/event_listener.h b/extension/bot/interfaces/event_listener.h index 4a774a2..3b83845 100644 --- a/extension/bot/interfaces/event_listener.h +++ b/extension/bot/interfaces/event_listener.h @@ -52,8 +52,8 @@ class IEventListener virtual void OnLostSight(edict_t* subject); // when the bot loses sight of an entity virtual void OnSound(edict_t* source, const Vector& position, SoundType type, const int volume); // when the bot hears an entity virtual void OnRoundStateChanged(); // When the round state changes (IE: round start,end, freeze time end, setup time end, etc...) - virtual void OnFlagTaken(CBaseEntity* flag); // CTF: Flag was stolen - virtual void OnFlagDropped(CBaseEntity* flag); // CTF: Flag was dropped + virtual void OnFlagTaken(CBaseEntity* player); // CTF: Flag was stolen + virtual void OnFlagDropped(CBaseEntity* player); // CTF: Flag was dropped virtual void OnControlPointCaptured(CBaseEntity* point); // When a control point is captured virtual void OnControlPointLost(CBaseEntity* point); // When a control point is lost virtual void OnControlPointContested(CBaseEntity* point); // When a control point is under siege @@ -241,7 +241,7 @@ inline void IEventListener::OnRoundStateChanged() } } -inline void IEventListener::OnFlagTaken(CBaseEntity* flag) +inline void IEventListener::OnFlagTaken(CBaseEntity* player) { auto vec = GetListenerVector(); @@ -249,12 +249,12 @@ inline void IEventListener::OnFlagTaken(CBaseEntity* flag) { for (auto listener : *vec) { - listener->OnFlagTaken(flag); + listener->OnFlagTaken(player); } } } -inline void IEventListener::OnFlagDropped(CBaseEntity* flag) +inline void IEventListener::OnFlagDropped(CBaseEntity* player) { auto vec = GetListenerVector(); @@ -262,7 +262,7 @@ inline void IEventListener::OnFlagDropped(CBaseEntity* flag) { for (auto listener : *vec) { - listener->OnFlagDropped(flag); + listener->OnFlagDropped(player); } } } diff --git a/extension/bot/interfaces/inventory.cpp b/extension/bot/interfaces/inventory.cpp index bf36503..f5410d0 100644 --- a/extension/bot/interfaces/inventory.cpp +++ b/extension/bot/interfaces/inventory.cpp @@ -29,6 +29,13 @@ void IInventory::Reset() m_weapons.clear(); m_weaponSwitchCooldown.Invalidate(); m_updateWeaponsTimer.Start(UPDATE_WEAPONS_INTERVAL_AFTER_RESET); + + edict_t* weapon = GetBot()->GetActiveWeapon(); + + if (weapon != nullptr) + { + m_cachedActiveWeapon = std::make_shared(weapon); + } } void IInventory::Update() @@ -38,6 +45,13 @@ void IInventory::Update() m_updateWeaponsTimer.Start(UPDATE_WEAPONS_INTERVAL); BuildInventory(); } + + edict_t* weapon = GetBot()->GetActiveWeapon(); + + if (weapon != nullptr && m_cachedActiveWeapon->GetEdict() != weapon) + { + m_cachedActiveWeapon = std::make_shared(weapon); + } } void IInventory::Frame() @@ -48,10 +62,10 @@ bool IInventory::HasWeapon(std::string classname) { for (auto& weapon : m_weapons) { - if (!weapon.IsValid()) + if (!weapon->IsValid()) continue; - const char* clname = weapon.GetBaseCombatWeapon().GetClassname(); + const char* clname = weapon->GetBaseCombatWeapon().GetClassname(); if (clname != nullptr) { @@ -84,7 +98,7 @@ void IInventory::BuildInventory() if (!weapon || weapon->IsFree() || weapon->GetIServerEntity() == nullptr) continue; - m_weapons.emplace_back(weapon); + m_weapons.emplace_back(new CBotWeapon(weapon)); } } @@ -107,34 +121,34 @@ void IInventory::SelectBestWeaponForThreat(const CKnownEntity* threat) const CBotWeapon* best = nullptr; int priority = std::numeric_limits::min(); - ForEveryWeapon([&bot, &priority, &rangeToThreat, &best](const CBotWeapon& weapon) { + ForEveryWeapon([&bot, &priority, &rangeToThreat, &best](const CBotWeapon* weapon) { // weapon must be usable against enemies - if (!weapon.GetWeaponInfo()->IsCombatWeapon()) + if (!weapon->GetWeaponInfo()->IsCombatWeapon()) { return; } // Must have ammo - if (weapon.GetBaseCombatWeapon().GetClip1() == 0 && bot->GetAmmoOfIndex(weapon.GetBaseCombatWeapon().GetPrimaryAmmoType()) == 0) + if (weapon->GetBaseCombatWeapon().GetClip1() == 0 && bot->GetAmmoOfIndex(weapon->GetBaseCombatWeapon().GetPrimaryAmmoType()) == 0) { return; } - if (rangeToThreat > weapon.GetWeaponInfo()->GetAttackInfo(WeaponInfo::PRIMARY_ATTACK).GetMaxRange()) + if (rangeToThreat > weapon->GetWeaponInfo()->GetAttackInfo(WeaponInfo::PRIMARY_ATTACK).GetMaxRange()) { return; // outside max range } - else if (rangeToThreat < weapon.GetWeaponInfo()->GetAttackInfo(WeaponInfo::PRIMARY_ATTACK).GetMinRange()) + else if (rangeToThreat < weapon->GetWeaponInfo()->GetAttackInfo(WeaponInfo::PRIMARY_ATTACK).GetMinRange()) { return; // too close } - int currentprio = weapon.GetWeaponInfo()->GetPriority(); + int currentprio = weapon->GetWeaponInfo()->GetPriority(); if (currentprio > priority) { - best = &weapon; + best = weapon; priority = currentprio; } }); @@ -150,7 +164,7 @@ void IInventory::SelectBestWeaponForThreat(const CKnownEntity* threat) } } -const CBotWeapon* IInventory::GetActiveBotWeapon() +std::shared_ptr IInventory::GetActiveBotWeapon() { edict_t* weapon = GetBot()->GetActiveWeapon(); @@ -159,13 +173,19 @@ const CBotWeapon* IInventory::GetActiveBotWeapon() return nullptr; } - int index = gamehelpers->IndexOfEdict(weapon); + if (m_cachedActiveWeapon) + { + if (m_cachedActiveWeapon->GetEdict() == weapon) + { + return m_cachedActiveWeapon; + } + } - for (auto& weapon : m_weapons) + for (auto& weaponptr : m_weapons) { - if (weapon.GetIndex() == index) + if (weaponptr->GetEdict() == weapon) { - return &weapon; + return weaponptr; } } diff --git a/extension/bot/interfaces/inventory.h b/extension/bot/interfaces/inventory.h index bc09245..7d7179a 100644 --- a/extension/bot/interfaces/inventory.h +++ b/extension/bot/interfaces/inventory.h @@ -31,20 +31,20 @@ class IInventory : public IBotInterface /** * @brief Runs a function on every valid bot weapon - * @tparam T a class with operator() overload with 1 parameter: (const CBotWeapon& weapon) + * @tparam T a class with operator() overload with 1 parameter: (const CBotWeapon* weapon) * @param functor function to run on every valid weapon */ template inline void ForEveryWeapon(T functor) const { - for (const CBotWeapon& weapon : m_weapons) + for (auto& weapon : m_weapons) { - if (!weapon.IsValid()) + if (!weapon->IsValid()) { continue; } - functor(weapon); + functor(weapon.get()); } } @@ -62,10 +62,11 @@ class IInventory : public IBotInterface virtual void SelectBestWeaponForThreat(const CKnownEntity* threat); - virtual const CBotWeapon* GetActiveBotWeapon(); + virtual std::shared_ptr GetActiveBotWeapon(); private: - std::vector m_weapons; + std::vector> m_weapons; + std::shared_ptr m_cachedActiveWeapon; protected: CountdownTimer m_updateWeaponsTimer; diff --git a/extension/bot/interfaces/movement.cpp b/extension/bot/interfaces/movement.cpp index fe3d644..cd537c6 100644 --- a/extension/bot/interfaces/movement.cpp +++ b/extension/bot/interfaces/movement.cpp @@ -482,6 +482,7 @@ bool IMovement::IsGap(const Vector& pos, const Vector& forward) * @param fraction trace result fraction * @param now When true, check if the bot is able to move right now. Otherwise check if the bot is able to move in the future * (ie: blocked by an entity that can be destroyed) + * @param obstacle The obstacle entity will be stored here. * @return true if the bot can walk, false otherwise */ bool IMovement::IsPotentiallyTraversable(const Vector& from, const Vector& to, float* fraction, const bool now, CBaseEntity** obstacle) diff --git a/extension/bot/interfaces/path/basepath.cpp b/extension/bot/interfaces/path/basepath.cpp index dec2333..cd50329 100644 --- a/extension/bot/interfaces/path/basepath.cpp +++ b/extension/bot/interfaces/path/basepath.cpp @@ -533,6 +533,8 @@ bool CPath::ProcessGroundPath(CBaseBot* bot, const Vector& start, std::shared_pt // Sometimes there is a railing between the drop and the ground + +#if PROBLEMATIC trace::CTraceFilterNoNPCsOrPlayers filter(bot->GetEntity(), COLLISION_GROUP_NONE); trace_t result; Vector mins(-halfWidth, -halfWidth, mover->GetStepHeight()); @@ -547,9 +549,22 @@ bool CPath::ProcessGroundPath(CBaseBot* bot, const Vector& start, std::shared_pt seg2->CopySegment(to.get()); seg2->goal = startDrop + Vector(0.0f, 0.0f, mover->GetStepHeight()); seg2->type = AIPath::SegmentType::SEGMENT_CLIMB_UP; - pathinsert.emplace(newSegment, std::move(seg2), false); + + std::shared_ptr jumpRunSegment = CreateNewSegment(); + jumpRunSegment->CopySegment(from.get()); + jumpRunSegment->type = AIPath::SegmentType::SEGMENT_GROUND; + + Vector runDir = (jumpRunSegment->goal - result.endpos); + runDir.z = 0.0f; + runDir.NormalizeInPlace(); + jumpRunSegment->goal = result.endpos + (runDir * (mover->GetHullWidth() * 1.2f)); + + pathinsert.emplace(seg2, jumpRunSegment, false); + pathinsert.emplace(newSegment, seg2, false); } +#endif + pathinsert.emplace(to, newSegment, true); } } diff --git a/extension/bot/interfaces/path/chasenavigator.cpp b/extension/bot/interfaces/path/chasenavigator.cpp new file mode 100644 index 0000000..7ac2e69 --- /dev/null +++ b/extension/bot/interfaces/path/chasenavigator.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include "chasenavigator.h" + +#undef min +#undef max +#undef clamp + +CChaseNavigator::CChaseNavigator(SubjectLeadType leadType, const float leadRadius, const float lifeTime) +{ + m_leadtype = leadType; + m_leadRadius = leadRadius; + m_lifetimeduration = lifeTime; + m_maxpathlength = 0.0f; + m_subject = nullptr; + m_failTimer.Invalidate(); + m_lifeTimer.Invalidate(); + m_throttleTimer.Invalidate(); +} + +CChaseNavigator::~CChaseNavigator() +{ +} + +void CChaseNavigator::Invalidate() +{ + m_lifeTimer.Invalidate(); + m_throttleTimer.Invalidate(); + + CMeshNavigator::Invalidate(); +} + +Vector CChaseNavigator::PredictSubjectPosition(CBaseBot* bot, CBaseEntity* subject) const +{ + auto mover = bot->GetMovementInterface(); + entities::HBaseEntity sbjEntity(subject); + + const Vector subjectPos = sbjEntity.GetAbsOrigin(); + + Vector to = (subjectPos - bot->GetAbsOrigin()); + to.z = 0.0f; + float flRangeSq = to.LengthSqr(); + + // don't lead if subject is very far away + float flLeadRadiusSq = GetLeadRadius(); + flLeadRadiusSq *= flLeadRadiusSq; + if (flRangeSq > flLeadRadiusSq) + return subjectPos; + + // Normalize in place + float range = sqrt(flRangeSq); + to /= (range + 0.0001f); // avoid divide by zero + + // estimate time to reach subject, assuming maximum speed + float leadTime = 0.5f + (range / (mover->GetMovementSpeed() + 0.0001f)); + + // estimate amount to lead the subject + Vector lead = leadTime * sbjEntity.GetAbsVelocity(); + lead.z = 0.0f; + + if (DotProduct(to, lead) < 0.0f) + { + // the subject is moving towards us - only pay attention + // to his perpendicular velocity for leading + Vector2D to2D = to.AsVector2D(); + to2D.NormalizeInPlace(); + + Vector2D perp(-to2D.y, to2D.x); + + float enemyGroundSpeed = lead.x * perp.x + lead.y * perp.y; + + lead.x = enemyGroundSpeed * perp.x; + lead.y = enemyGroundSpeed * perp.y; + } + + // compute our desired destination + Vector pathTarget = subjectPos + lead; + + // validate this destination + + // don't lead through walls + if (lead.LengthSqr() > 36.0f) + { + float fraction = 0.0f; + + if (!mover->IsPotentiallyTraversable(subjectPos, pathTarget, &fraction, true, nullptr)) + { + // tried to lead through an unwalkable area - clip to walkable space + pathTarget = subjectPos + fraction * (pathTarget - subjectPos); + } + } + + // don't lead over cliffs + CNavArea* leadArea = nullptr; + + leadArea = TheNavMesh->GetNearestNavArea(pathTarget); + + if (leadArea == nullptr || leadArea->GetZ(pathTarget.x, pathTarget.y) < pathTarget.z - mover->GetMaxJumpHeight()) + { + // would fall off a cliff + return subjectPos; + } + + return pathTarget; +} + +bool CChaseNavigator::IsRepathNeeded(CBaseBot* bot, CBaseEntity* subject) +{ + entities::HBaseEntity sbjEntity(subject); + + // the closer we get, the more accurate our path needs to be + Vector to = (sbjEntity.GetAbsOrigin() - bot->GetAbsOrigin()); + + constexpr float minTolerance = 0.0f; + constexpr float toleranceRate = 0.33f; + + float tolerance = minTolerance + toleranceRate * to.Length(); + + return (sbjEntity.GetAbsOrigin() - GetEndPosition()).IsLengthGreaterThan(tolerance); +} + +void CChaseNavigator::Update(CBaseBot* bot) +{ + CMeshNavigator::Update(bot); +} diff --git a/extension/bot/interfaces/path/chasenavigator.h b/extension/bot/interfaces/path/chasenavigator.h new file mode 100644 index 0000000..56f9bff --- /dev/null +++ b/extension/bot/interfaces/path/chasenavigator.h @@ -0,0 +1,174 @@ +#ifndef NAVBOT_CHASE_MESH_NAVIGATOR_H_ +#define NAVBOT_CHASE_MESH_NAVIGATOR_H_ + +#include +#include +#include +#include "meshnavigator.h" + +/** + * @brief Navigator designed for chasing a moving target. + */ +class CChaseNavigator : public CMeshNavigator +{ +public: + enum SubjectLeadType : unsigned int + { + LEAD_SUBJECT = 0U, // The path will try to lead the target entity + DONT_LEAD_SUBJECT, // The path won't try to lead the target entity, it will just move to the entity position itself + }; + + /** + * @brief A navigator designed for chasing entities. + * @param leadType Subject lead type. + * @param leadRadius Lead How far ahead of the subject the bot should try to move to. + */ + CChaseNavigator(SubjectLeadType leadType = LEAD_SUBJECT, const float leadRadius = 512.0f, const float lifeTime = -1.0f); + ~CChaseNavigator() override; + + void Invalidate() override; + + /** + * @brief Moves the bot along the path. + * @param bot Bot that will use this path. + * @param subject Subject the bot will chase. + * @param costFunc Path cost functor. + * @param predictedSubjectPosition Optional: If set the navigator won't calculate a predicted subject position and will use this instead. + */ + template + void Update(CBaseBot* bot, CBaseEntity* subject, CF& costFunc, Vector* predictedSubjectPosition = nullptr); + + virtual Vector PredictSubjectPosition(CBaseBot* bot, CBaseEntity* subject) const; + + // How far ahead of the subject the bot should try to move to. + void SetLeadRadius(float radius) { m_leadRadius = radius; } + float GetLeadRadius() const { return m_leadRadius; } + void SetPathLifeTimeDuration(float lt) { m_lifetimeduration = lt; } + float GetPathLifeTimeDuration() const { return m_lifetimeduration; } + +protected: + virtual bool IsRepathNeeded(CBaseBot* bot, CBaseEntity* subject); + +private: + SubjectLeadType m_leadtype; + CHandle m_subject; // subject from the last valid path (tracks target entity changes) + CountdownTimer m_failTimer; // path failed timer + CountdownTimer m_lifeTimer; // maximum path life + CountdownTimer m_throttleTimer; // prevent quick repaths + float m_leadRadius; + float m_lifetimeduration; + float m_maxpathlength; + + void Update(CBaseBot* bot) override; + + template + void RefreshPath(CBaseBot* bot, CBaseEntity* subject, CF& costFunc, Vector* predictedSubjectPosition = nullptr); +}; + +template +inline void CChaseNavigator::Update(CBaseBot* bot, CBaseEntity* subject, CF& costFunc, Vector* predictedSubjectPosition) +{ + RefreshPath(bot, subject, costFunc, predictedSubjectPosition); + CMeshNavigator::Update(bot); +} + +template +inline void CChaseNavigator::RefreshPath(CBaseBot* bot, CBaseEntity* subject, CF& costFunc, Vector* predictedSubjectPosition) +{ + auto mover = bot->GetMovementInterface(); + + if (IsValid() && mover->IsOnLadder()) + { + m_throttleTimer.Start(0.5f); // don't repath while using ladders + return; + } + + if (subject == nullptr) + { + return; + } + + if (!m_failTimer.IsElapsed()) + { + return; + } + + if (subject != m_subject.Get()) + { + // new chase target, refresh path + + if (bot->IsDebugging(BOTDEBUG_PATH)) + { + bot->DebugPrintToConsole(255, 165, 0, "%s CChaseNavigator target subject changed from %p to %p!\n", m_subject.Get(), subject); + } + + Invalidate(); + + m_failTimer.Invalidate(); + } + + if (IsValid() && !m_throttleTimer.IsElapsed()) + { + // don't repath too frequently while the current path is valid + return; + } + + if (!IsValid() || IsRepathNeeded(bot, subject)) + { + entities::HBaseEntity sbjEntity(subject); + bool foundpath = false; + Vector pathGoal = sbjEntity.GetAbsOrigin(); + + if (m_leadtype == LEAD_SUBJECT) + { + // if a position was given, use it, otherwise calculate it. + pathGoal = predictedSubjectPosition != nullptr ? *predictedSubjectPosition : PredictSubjectPosition(bot, subject); + foundpath = this->ComputePathToPosition(bot, pathGoal, costFunc, m_maxpathlength); + } + else // don't lead subject + { + foundpath = this->ComputePathToPosition(bot, pathGoal, costFunc, m_maxpathlength); + } + + if (foundpath) + { + if (bot->IsDebugging(BOTDEBUG_PATH)) + { + bot->DebugPrintToConsole(255, 165, 0, "%s: CChaseNavigator::RefreshPath REPATH!\n", bot->GetDebugIdentifier()); + } + + m_subject = subject; // remember the subject of the last valid path + m_throttleTimer.Start(0.5f); // don't repath frequently (unless the path becomes invalid) + + if (m_lifetimeduration > 0.9f) + { + m_lifeTimer.Start(m_lifetimeduration); + } + else + { + m_lifeTimer.Invalidate(); + } + } + else + { + Invalidate(); + Vector subjectPos = sbjEntity.GetAbsOrigin(); + + // path to subject failed - try again later, time is based on distance. + float time = (bot->GetRangeTo(subjectPos) * 0.005f); + time = std::min(time, 3.0f); // allow a maximum fail time of 3 seconds. + + m_failTimer.Start(time); + + if (bot->IsDebugging(BOTDEBUG_PATH)) + { + bot->DebugPrintToConsole(255, 165, 0, "%s: CChaseNavigator::RefreshPath REPATH FAILED!\n", bot->GetDebugIdentifier()); + NDebugOverlay::EntityBounds(subject, 255, 0, 0, 150, 2.0f); + Vector start = bot->GetAbsOrigin(); + NDebugOverlay::HorzArrow(start, pathGoal, 8.0f, 255, 0, 0, 255, true, 2.0f); + } + } + } +} + +#endif // !NAVBOT_CHASE_MESH_NAVIGATOR_H_ diff --git a/extension/bot/interfaces/path/meshnavigator.cpp b/extension/bot/interfaces/path/meshnavigator.cpp index b2bac2c..44a3a19 100644 --- a/extension/bot/interfaces/path/meshnavigator.cpp +++ b/extension/bot/interfaces/path/meshnavigator.cpp @@ -3,9 +3,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -1385,3 +1382,10 @@ bool CMeshNavigator::LadderUpdate(CBaseBot* bot) return false; } + +bool CMeshNavigatorAutoRepath::IsRepathNeeded(const Vector& goal) +{ + float tolerance = GetGoalTolerance() * GetGoalTolerance(); + + return (goal - m_lastGoal).LengthSqr() > tolerance; +} diff --git a/extension/bot/interfaces/path/meshnavigator.h b/extension/bot/interfaces/path/meshnavigator.h index 5eadf63..4e85f96 100644 --- a/extension/bot/interfaces/path/meshnavigator.h +++ b/extension/bot/interfaces/path/meshnavigator.h @@ -2,10 +2,9 @@ #define SMNAV_BOT_NAV_MESH_NAVIGATOR_H_ #pragma once +#include #include "basepath.h" -class CBaseBot; - // Nav Mesh navigator class CMeshNavigator : public CPath { @@ -44,6 +43,7 @@ class CMeshNavigator : public CPath void SetBot(CBaseBot* bot) { m_me = bot; } // Bot using this navigator, may be NULL CBaseBot* GetBot() const { return m_me; } + // Waiting for obstacles bool IsWaitingForSomething() { return !m_waitTimer.IsElapsed(); } private: @@ -57,4 +57,89 @@ class CMeshNavigator : public CPath float m_skipAheadDistance; }; +/** + * @brief Nav Mesh navigator that automatically recalculates the path when needed + */ +class CMeshNavigatorAutoRepath : public CMeshNavigator +{ +public: + CMeshNavigatorAutoRepath(float repathInterval = 0.5f) + { + m_repathinterval = repathInterval; + m_repathTimer.Invalidate(); + } + + void Invalidate() override + { + m_repathTimer.Invalidate(); + } + + template + void Update(CBaseBot* bot, const Vector& goal, CF& costFunctor); + +private: + float m_repathinterval; + CountdownTimer m_repathTimer; // Time until next repath + CountdownTimer m_failTimer; // Time to wait if the path failed + Vector m_lastGoal; // goal from the last valid path + + template + void RefreshPath(CBaseBot* bot, const Vector& goal, CF& costFunctor); + + bool IsRepathNeeded(const Vector& goal); + + void Update(CBaseBot* bot) override + { + CMeshNavigator::Update(bot); + } +}; + +template +inline void CMeshNavigatorAutoRepath::Update(CBaseBot* bot, const Vector& goal, CF& costFunctor) +{ + // Refresh path if needed + RefreshPath(bot, goal, costFunctor); + + // Move bot along path + CMeshNavigator::Update(bot); +} + +template +inline void CMeshNavigatorAutoRepath::RefreshPath(CBaseBot* bot, const Vector& goal, CF& costFunctor) +{ + if (IsValid() && !m_repathTimer.IsElapsed()) + { + return; + } + + auto mover = bot->GetMovementInterface(); + + // Don't repath on these conditions but also force a repath as soon as possible. + if (mover->IsOnLadder() || mover->IsControllingMovements()) + { + m_repathTimer.Invalidate(); + return; + } + + if (!m_failTimer.IsElapsed()) + { + return; + } + + if (!IsValid() || IsRepathNeeded(goal)) + { + bool foundpath = this->ComputePathToPosition(bot, goal, costFunctor); + + if (!foundpath) + { + Invalidate(); + m_failTimer.Start(1.0f); // Wait one second before repath + } + else + { + m_repathTimer.Start(m_repathinterval); + } + } +} + #endif // !SMNAV_BOT_NAV_MESH_NAVIGATOR_H_ diff --git a/extension/bot/interfaces/sensor.cpp b/extension/bot/interfaces/sensor.cpp index 03118dd..ffacd1a 100644 --- a/extension/bot/interfaces/sensor.cpp +++ b/extension/bot/interfaces/sensor.cpp @@ -397,8 +397,21 @@ std::shared_ptr ISensor::GetPrimaryKnownThreat(const bool on if (m_knownlist.empty()) return nullptr; + // cached threat from the last call if (m_primarythreatcache) - return m_primarythreatcache; + { + // only visible and primary threat is visible right now. + if (onlyvisible && m_primarythreatcache->IsVisibleNow()) + { + return m_primarythreatcache; + } + else if (!onlyvisible) // allow non visible and we have a cached threat. + { + return m_primarythreatcache; + } + + // if we want only visible threat and the cache is not visible, allow the code below to run and update the cache + } std::shared_ptr primarythreat = nullptr; diff --git a/extension/bot/interfaces/tasks.h b/extension/bot/interfaces/tasks.h index c89da2d..8b4ac99 100644 --- a/extension/bot/interfaces/tasks.h +++ b/extension/bot/interfaces/tasks.h @@ -574,8 +574,8 @@ class AITask : public IEventListener, public IDecisionQuery virtual TaskEventResponseResult OnLostSight(BotClass* bot, edict_t* subject) { return TryContinue(); } virtual TaskEventResponseResult OnSound(BotClass* bot, edict_t* source, const Vector& position, SoundType type, const int volume) { return TryContinue(); } virtual TaskEventResponseResult OnRoundStateChanged(BotClass* bot) { return TryContinue(); } - virtual TaskEventResponseResult OnFlagTaken(BotClass* bot, CBaseEntity* flag) { return TryContinue(); } - virtual TaskEventResponseResult OnFlagDropped(BotClass* bot, CBaseEntity* flag) { return TryContinue(); } + virtual TaskEventResponseResult OnFlagTaken(BotClass* bot, CBaseEntity* player) { return TryContinue(); } + virtual TaskEventResponseResult OnFlagDropped(BotClass* bot, CBaseEntity* player) { return TryContinue(); } virtual TaskEventResponseResult OnControlPointCaptured(BotClass* bot, CBaseEntity* point) { return TryContinue(); } virtual TaskEventResponseResult OnControlPointLost(BotClass* bot, CBaseEntity* point) { return TryContinue(); } virtual TaskEventResponseResult OnControlPointContested(BotClass* bot, CBaseEntity* point) { return TryContinue(); } @@ -1036,14 +1036,14 @@ class AITask : public IEventListener, public IDecisionQuery PROPAGATE_TASK_EVENT_WITH_NO_ARGS(OnRoundStateChanged); } - void OnFlagTaken(CBaseEntity* flag) override final + void OnFlagTaken(CBaseEntity* player) override final { - PROPAGATE_TASK_EVENT_WITH_1_ARGS(OnFlagTaken, flag); + PROPAGATE_TASK_EVENT_WITH_1_ARGS(OnFlagTaken, player); } - void OnFlagDropped(CBaseEntity* flag) override final + void OnFlagDropped(CBaseEntity* player) override final { - PROPAGATE_TASK_EVENT_WITH_1_ARGS(OnFlagDropped, flag); + PROPAGATE_TASK_EVENT_WITH_1_ARGS(OnFlagDropped, player); } void OnControlPointCaptured(CBaseEntity* point) override final diff --git a/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.cpp b/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.cpp index 81c8db3..662d2cb 100644 --- a/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.cpp +++ b/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.cpp @@ -120,14 +120,14 @@ AITask* CTF2BotEngineerNestTask::NestTask(CTF2Bot* me) if (me->GetMyTeleporterEntrance() == nullptr) { - if (FindSpotToBuildTeleEntrance(me, wpt)) + if (FindSpotToBuildTeleEntrance(me, &wpt)) { return new CTF2BotEngineerBuildObjectTask(CTF2BotEngineerBuildObjectTask::OBJECT_TELEPORTER_ENTRANCE, wpt); } } else if (me->GetMySentryGun() == nullptr) { - if (FindSpotToBuildSentryGun(me, wpt)) + if (FindSpotToBuildSentryGun(me, &wpt)) { return new CTF2BotEngineerBuildObjectTask(CTF2BotEngineerBuildObjectTask::OBJECT_SENTRYGUN, wpt); } @@ -135,7 +135,7 @@ AITask* CTF2BotEngineerNestTask::NestTask(CTF2Bot* me) else if (me->GetMyDispenser() == nullptr) { // Search for nearby waypoints - if (FindSpotToBuildDispenser(me, wpt)) + if (FindSpotToBuildDispenser(me, &wpt)) { return new CTF2BotEngineerBuildObjectTask(CTF2BotEngineerBuildObjectTask::OBJECT_DISPENSER, wpt); } @@ -148,7 +148,7 @@ AITask* CTF2BotEngineerNestTask::NestTask(CTF2Bot* me) } else if (me->GetMyTeleporterExit() == nullptr) { - if (FindSpotToBuildTeleExit(me, wpt)) + if (FindSpotToBuildTeleExit(me, &wpt)) { return new CTF2BotEngineerBuildObjectTask(CTF2BotEngineerBuildObjectTask::OBJECT_TELEPORTER_EXIT, wpt); } @@ -188,7 +188,7 @@ AITask* CTF2BotEngineerNestTask::NestTask(CTF2Bot* me) return nullptr; } -bool CTF2BotEngineerNestTask::FindSpotToBuildSentryGun(CTF2Bot* me, CTFWaypoint* out) +bool CTF2BotEngineerNestTask::FindSpotToBuildSentryGun(CTF2Bot* me, CTFWaypoint** out) { std::vector spots; auto& sentryWaypoints = CTeamFortress2Mod::GetTF2Mod()->GetAllSentryWaypoints(); @@ -206,11 +206,11 @@ bool CTF2BotEngineerNestTask::FindSpotToBuildSentryGun(CTF2Bot* me, CTFWaypoint* return false; } - out = librandom::utils::GetRandomElementFromVector(spots); + *out = librandom::utils::GetRandomElementFromVector(spots); return true; } -bool CTF2BotEngineerNestTask::FindSpotToBuildDispenser(CTF2Bot* me, CTFWaypoint* out) +bool CTF2BotEngineerNestTask::FindSpotToBuildDispenser(CTF2Bot* me, CTFWaypoint** out) { std::vector spots; auto& dispenserWaypoints = CTeamFortress2Mod::GetTF2Mod()->GetAllDispenserWaypoints(); @@ -238,11 +238,11 @@ bool CTF2BotEngineerNestTask::FindSpotToBuildDispenser(CTF2Bot* me, CTFWaypoint* return false; } - out = librandom::utils::GetRandomElementFromVector(spots); + *out = librandom::utils::GetRandomElementFromVector(spots); return true; } -bool CTF2BotEngineerNestTask::FindSpotToBuildTeleEntrance(CTF2Bot* me, CTFWaypoint* out) +bool CTF2BotEngineerNestTask::FindSpotToBuildTeleEntrance(CTF2Bot* me, CTFWaypoint** out) { std::vector spots; auto& teleentranceWaypoints = CTeamFortress2Mod::GetTF2Mod()->GetAllTeleEntranceWaypoints(); @@ -260,11 +260,11 @@ bool CTF2BotEngineerNestTask::FindSpotToBuildTeleEntrance(CTF2Bot* me, CTFWaypoi return false; } - out = librandom::utils::GetRandomElementFromVector(spots); + *out = librandom::utils::GetRandomElementFromVector(spots); return true; } -bool CTF2BotEngineerNestTask::FindSpotToBuildTeleExit(CTF2Bot* me, CTFWaypoint* out) +bool CTF2BotEngineerNestTask::FindSpotToBuildTeleExit(CTF2Bot* me, CTFWaypoint** out) { std::vector spots; auto& teleexitWaypoints = CTeamFortress2Mod::GetTF2Mod()->GetAllTeleExitWaypoints(); @@ -282,7 +282,7 @@ bool CTF2BotEngineerNestTask::FindSpotToBuildTeleExit(CTF2Bot* me, CTFWaypoint* return false; } - out = librandom::utils::GetRandomElementFromVector(spots); + *out = librandom::utils::GetRandomElementFromVector(spots); return true; } diff --git a/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.h b/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.h index 3eefc7b..eeeef34 100644 --- a/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.h +++ b/extension/bot/tf2/tasks/engineer/tf2bot_engineer_nest.h @@ -37,11 +37,11 @@ class CTF2BotEngineerNestTask : public AITask Vector m_goal; AITask* NestTask(CTF2Bot* me); - bool FindSpotToBuildSentryGun(CTF2Bot* me, CTFWaypoint* out); - bool FindSpotToBuildDispenser(CTF2Bot* me, CTFWaypoint* out); + bool FindSpotToBuildSentryGun(CTF2Bot* me, CTFWaypoint** out); + bool FindSpotToBuildDispenser(CTF2Bot* me, CTFWaypoint** out); bool FindSpotToBuildDispenser(CTF2Bot* me, Vector& out); - bool FindSpotToBuildTeleEntrance(CTF2Bot* me, CTFWaypoint* out); - bool FindSpotToBuildTeleExit(CTF2Bot* me, CTFWaypoint* out); + bool FindSpotToBuildTeleEntrance(CTF2Bot* me, CTFWaypoint** out); + bool FindSpotToBuildTeleExit(CTF2Bot* me, CTFWaypoint** out); bool GetRandomDispenserSpot(CTF2Bot* me, const Vector& start, Vector& out); }; diff --git a/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.cpp b/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.cpp index f740021..f98c8eb 100644 --- a/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.cpp +++ b/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "tf2bot_medic_retreat_task.h" #include "tf2bot_medic_revive_task.h" #include "tf2bot_medic_main_task.h" @@ -92,6 +93,21 @@ TaskResult CTF2BotMedicMainTask::OnTaskResume(CTF2Bot* bot, AITask CTF2BotMedicMainTask::OnFlagTaken(CTF2Bot* bot, CBaseEntity* player) +{ + if (bot->GetEntity() == player) + { + auto mod = CTeamFortress2Mod::GetTF2Mod(); + + if (mod->GetCurrentGameMode() == TeamFortress2::GameModeType::GM_CTF) + { + return TryPauseFor(new CTF2BotCTFDeliverFlagTask, PRIORITY_HIGH, "I got the flag, delivering it!"); + } + } + + return TryContinue(); +} + bool CTF2BotMedicMainTask::IsCurrentPatientValid() { if (m_patient.Get() == nullptr) @@ -148,6 +164,11 @@ bool CTF2BotMedicMainTask::LookForPatients(CTF2Bot* me) return; } + if (tf2lib::GetPlayerClassType(client) == TeamFortress2::TFClass_Medic && tf2lib::GetPlayerHealthPercentage(client) >= 0.92f) + { + return; // temporary until a better solution is made + } + float distance = me->GetRangeTo(entity); if (tf2lib::GetPlayerHealthPercentage(client) >= 0.9f) diff --git a/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.h b/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.h index fa1fae5..67a2c79 100644 --- a/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.h +++ b/extension/bot/tf2/tasks/medic/tf2bot_medic_main_task.h @@ -17,6 +17,8 @@ class CTF2BotMedicMainTask : public AITask TaskResult OnTaskUpdate(CTF2Bot* bot) override; TaskResult OnTaskResume(CTF2Bot* bot, AITask* pastTask) override; + TaskEventResponseResult OnFlagTaken(CTF2Bot* bot, CBaseEntity* player) override; + // medics never attack QueryAnswerType ShouldAttack(CBaseBot* me, const CKnownEntity* them) override { return ANSWER_NO; } diff --git a/extension/bot/tf2/tasks/scenario/tf2bot_map_ctf.cpp b/extension/bot/tf2/tasks/scenario/tf2bot_map_ctf.cpp index d32eac2..3ada677 100644 --- a/extension/bot/tf2/tasks/scenario/tf2bot_map_ctf.cpp +++ b/extension/bot/tf2/tasks/scenario/tf2bot_map_ctf.cpp @@ -4,6 +4,7 @@ #include #include #include "bot/tf2/tf2bot.h" +#include #include "tf2bot_map_ctf.h" TaskResult CTF2BotCTFMonitorTask::OnTaskUpdate(CTF2Bot* bot) @@ -22,6 +23,11 @@ TaskResult CTF2BotCTFMonitorTask::OnTaskUpdate(CTF2Bot* bot) } } + if (randomgen->GetRandomInt(0, 1) == 1) + { + return PauseFor(new CTF2BotRoamTask(), "No flag to deliver or fetch, roaming!"); + } + return Continue(); } diff --git a/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.cpp b/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.cpp index 0a13120..b3d5f49 100644 --- a/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.cpp +++ b/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.cpp @@ -65,10 +65,10 @@ void CTF2BotSniperMoveToSnipingSpotTask::FindSniperSpot(CTF2Bot* bot) { // TO-DO: Add game mode sniping spots - GetRandomSnipingSpot(bot, m_goal); + GetRandomSnipingSpot(bot); } -void CTF2BotSniperMoveToSnipingSpotTask::GetRandomSnipingSpot(CTF2Bot* bot, Vector& out) +void CTF2BotSniperMoveToSnipingSpotTask::GetRandomSnipingSpot(CTF2Bot* bot) { std::vector spots; auto& thewaypoints = CTeamFortress2Mod::GetTF2Mod()->GetAllSniperWaypoints(); diff --git a/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.h b/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.h index 0503c49..18d3f2a 100644 --- a/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.h +++ b/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_move_to_sniper_spot.h @@ -23,7 +23,7 @@ class CTF2BotSniperMoveToSnipingSpotTask : public AITask bool m_sniping; void FindSniperSpot(CTF2Bot* bot); - void GetRandomSnipingSpot(CTF2Bot* bot, Vector& out); + void GetRandomSnipingSpot(CTF2Bot* bot); }; #endif // !NAVBOT_TF2_TASK_SNIPER_MOVE_TO_SPOT_H_ diff --git a/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_snipe_area.cpp b/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_snipe_area.cpp index 2c09523..6c3aaa9 100644 --- a/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_snipe_area.cpp +++ b/extension/bot/tf2/tasks/sniper/tf2bot_task_sniper_snipe_area.cpp @@ -167,10 +167,10 @@ void CTF2BotSniperSnipeAreaTask::BuildLookPoints(CTF2Bot* me) { if (m_waypoint != nullptr) { - // This should never return NULL for most entities, especially players. - Vector* view_ofs = entprops->GetPointerToEntData(me->GetEntity(), Prop_Data, "m_vecviewoffset"); - // use the waypoint origin at eye level as a start point so aim angles are consistent - Vector start = m_waypoint->GetOrigin() + *view_ofs; + // Use waypoint origin but the eye's Z coordinate. + Vector eyePos = me->GetEyeOrigin(); + Vector start = m_waypoint->GetOrigin(); + start.z = eyePos.z; m_waypoint->ForEveryAngle([this, &me, &start](const QAngle& angle) { Vector forward; diff --git a/extension/bot/tf2/tasks/tf2bot_attack.cpp b/extension/bot/tf2/tasks/tf2bot_attack.cpp new file mode 100644 index 0000000..bbc6649 --- /dev/null +++ b/extension/bot/tf2/tasks/tf2bot_attack.cpp @@ -0,0 +1,93 @@ +#include + +#include +#include +#include +#include +#include +#include +#include "tf2bot_attack.h" + +CTF2BotAttackTask::CTF2BotAttackTask(CBaseEntity* entity, const float escapeTime, const float maxChaseTime) +{ + m_target = entity; + m_chaseDuration = maxChaseTime; + m_escapeDuration = escapeTime; +} + +TaskResult CTF2BotAttackTask::OnTaskStart(CTF2Bot* bot, AITask* pastTask) +{ + if (m_target.Get() == nullptr) + { + return Done("NULL target entity!"); + } + + m_expireTimer.Start(m_chaseDuration); + m_escapeTimer.Start(m_escapeDuration); + + return Continue(); +} + +TaskResult CTF2BotAttackTask::OnTaskUpdate(CTF2Bot* bot) +{ + if (m_expireTimer.IsElapsed()) + { + return Done("This is taking too much time! Giving up!"); + } + + if (bot->GetHealthPercentage() <= 0.30f) + { + return Done("Backing off, low health!"); + } + + CBaseEntity* pEntity = m_target.Get(); + + if (pEntity == nullptr) + { + return Done("Target is NULL!"); + } + + int index = gamehelpers->EntityToBCompatRef(m_target.Get()); + + if (!UtilHelpers::IsEntityAlive(index)) + { + return Done("Target is dead!"); + } + + auto known = bot->GetSensorInterface()->GetKnown(pEntity); + auto threat = bot->GetSensorInterface()->GetPrimaryKnownThreat(true); + + // if sensor lost track of it, it's gone. + if (known.get() == nullptr) + { + return Done("Target has escaped me!"); + } + + CTF2BotPathCost cost(bot); + m_nav.Update(bot, pEntity, cost, nullptr); + + if (threat && known && threat.get() == known.get()) + { + // don't use combat look priority in case another more important threat is visible to us + bot->GetControlInterface()->AimAt(pEntity, IPlayerController::LOOK_DANGER, 0.25f, "Looking at target entity!"); + } + + return Continue(); +} + +TaskResult CTF2BotAttackTask::OnTaskResume(CTF2Bot* bot, AITask* pastTask) +{ + CBaseEntity* pEntity = m_target.Get(); + + if (pEntity == nullptr) + { + return Done("Target is NULL!"); + } + + if (!bot->GetSensorInterface()->IsAbleToSee(pEntity)) + { + return Done("Target has escaped me!"); + } + + return Continue(); +} diff --git a/extension/bot/tf2/tasks/tf2bot_attack.h b/extension/bot/tf2/tasks/tf2bot_attack.h new file mode 100644 index 0000000..256ba54 --- /dev/null +++ b/extension/bot/tf2/tasks/tf2bot_attack.h @@ -0,0 +1,31 @@ +#ifndef NAVBOT_TF2BOT_ATTACK_TASK_H_ +#define NAVBOT_TF2BOT_ATTACK_TASK_H_ + +#include + +class CTF2Bot; + +class CTF2BotAttackTask : public AITask +{ +public: + CTF2BotAttackTask(CBaseEntity* entity, const float escapeTime = 5.0f, const float maxChaseTime = 60.0f); + + TaskResult OnTaskStart(CTF2Bot* bot, AITask* pastTask) override; + TaskResult OnTaskUpdate(CTF2Bot* bot) override; + TaskResult OnTaskResume(CTF2Bot* bot, AITask* pastTask) override; + + // Always allow weapon switches and attacking + QueryAnswerType ShouldAttack(CBaseBot* me, const CKnownEntity* them) override { return ANSWER_YES; } + QueryAnswerType ShouldSwitchToWeapon(CBaseBot* me, const CBotWeapon* weapon) override { return ANSWER_YES; } + + const char* GetName() const override { return "Attack"; } +private: + CHandle m_target; + CChaseNavigator m_nav; + CountdownTimer m_escapeTimer; // timer to give up if the bot doesn't have LOS + CountdownTimer m_expireTimer; // timer to give up if this ends up taking too much time + float m_escapeDuration; + float m_chaseDuration; +}; + +#endif // !NAVBOT_TF2BOT_ATTACK_TASK_H_ diff --git a/extension/bot/tf2/tasks/tf2bot_dead.cpp b/extension/bot/tf2/tasks/tf2bot_dead.cpp new file mode 100644 index 0000000..3bb8524 --- /dev/null +++ b/extension/bot/tf2/tasks/tf2bot_dead.cpp @@ -0,0 +1,14 @@ +#include +#include +#include "tf2bot_maintask.h" +#include "tf2bot_dead.h" + +TaskResult CTF2BotDeadTask::OnTaskUpdate(CTF2Bot* bot) +{ + if (bot->IsAlive()) + { + return SwitchTo(new CTF2BotMainTask, "I'm alive!"); + } + + return Continue(); +} diff --git a/extension/bot/tf2/tasks/tf2bot_dead.h b/extension/bot/tf2/tasks/tf2bot_dead.h new file mode 100644 index 0000000..050d5fa --- /dev/null +++ b/extension/bot/tf2/tasks/tf2bot_dead.h @@ -0,0 +1,14 @@ +#ifndef NAVBOT_TF2BOT_DEAD_TASK_H_ +#define NAVBOT_TF2BOT_DEAD_TASK_H_ + +class CTF2Bot; + +class CTF2BotDeadTask : public AITask +{ +public: + TaskResult OnTaskUpdate(CTF2Bot* bot) override; + + const char* GetName() const override { return "Dead"; } +}; + +#endif // !NAVBOT_TF2BOT_DEAD_TASK_H_ diff --git a/extension/bot/tf2/tasks/tf2bot_find_ammo_task.cpp b/extension/bot/tf2/tasks/tf2bot_find_ammo_task.cpp index 091bc8a..2707630 100644 --- a/extension/bot/tf2/tasks/tf2bot_find_ammo_task.cpp +++ b/extension/bot/tf2/tasks/tf2bot_find_ammo_task.cpp @@ -87,13 +87,17 @@ TaskResult CTF2BotFindAmmoTask::OnTaskUpdate(CTF2Bot* bot) TaskEventResponseResult CTF2BotFindAmmoTask::OnMoveToFailure(CTF2Bot* bot, CPath* path, IEventListener::MovementFailureType reason) { - bot->GetMovementInterface()->ClearStuckStatus("Repath!"); - m_repathtimer.Start(0.5f); + // don't clear stuck status here, can cause bots to get stuck forever - CTF2BotPathCost cost(bot); - if (!m_nav.ComputePathToPosition(bot, m_sourcepos, cost)) + if (m_repathtimer.IsElapsed()) { - return TryDone(PRIORITY_HIGH, "Failed to build a path to the ammo source!"); + m_repathtimer.Start(0.5f); + + CTF2BotPathCost cost(bot); + if (!m_nav.ComputePathToPosition(bot, m_sourcepos, cost)) + { + return TryDone(PRIORITY_HIGH, "Failed to build a path to the ammo source!"); + } } return TryContinue(); diff --git a/extension/bot/tf2/tasks/tf2bot_find_health_task.cpp b/extension/bot/tf2/tasks/tf2bot_find_health_task.cpp index ae4aeb0..7bd2ca4 100644 --- a/extension/bot/tf2/tasks/tf2bot_find_health_task.cpp +++ b/extension/bot/tf2/tasks/tf2bot_find_health_task.cpp @@ -12,12 +12,51 @@ TaskResult CTF2BotFindHealthTask::OnTaskStart(CTF2Bot* bot, AITask* pastTask) { - return TaskResult(); + m_type = FindSource(bot); + + if (m_type == HealthSource::NONE) + { + return Done("Failed to find a health source!"); + } + + CTF2BotPathCost cost(bot); + if (!m_nav.ComputePathToPosition(bot, m_sourcepos, cost)) + { + return Done("Failed to build a path to the Health source!"); + } + + return Continue(); } TaskResult CTF2BotFindHealthTask::OnTaskUpdate(CTF2Bot* bot) { - return TaskResult(); + if (!IsSourceStillValid(bot)) + return Done("Health Source is invalid!"); + + if (m_reached && m_failsafetimer.IsElapsed()) + { + return Done("Health collected!"); + } + + if (bot->GetHealthPercentage() >= 0.98f) + { + return Done("I'm at full health!"); + } + + UpdateSourcePosition(); + + // if the bot is this close to the dispenser, stop moving + static constexpr auto DISPENSER_TOUCH_RANGE = 64.0f; + + if (m_type == HealthSource::DISPENSER && bot->GetRangeTo(m_sourcepos) < DISPENSER_TOUCH_RANGE) + { + return Continue(); + } + + CTF2BotPathCost cost(bot); + m_nav.Update(bot, m_sourcepos, cost); + + return Continue(); } CTF2BotFindHealthTask::HealthSource CTF2BotFindHealthTask::FindSource(CTF2Bot* me) @@ -98,10 +137,10 @@ CTF2BotFindHealthTask::HealthSource CTF2BotFindHealthTask::FindSource(CTF2Bot* m if (!evaluatedispenser(edict)) continue; - distance_mul = 0.75f; // prefer dispensers over ammopacks + distance_mul = 0.75f; // prefer dispensers over health kits currentsource = HealthSource::DISPENSER; } - else if (strncasecmp(classname, "item_ammopack", 13) == 0) + else if (strncasecmp(classname, "item_healthkit", 14) == 0) { if (!evaluateammopack(edict)) continue; @@ -145,10 +184,10 @@ CTF2BotFindHealthTask::HealthSource CTF2BotFindHealthTask::FindSource(CTF2Bot* m if (me->IsDebugging(BOTDEBUG_TASKS)) { - me->DebugPrintToConsole(BOTDEBUG_TASKS, 153, 156, 255, "%s Found Ammo Source <%i> at %3.2f, %3.2f, %3.2f", me->GetDebugIdentifier(), static_cast(source), + me->DebugPrintToConsole(BOTDEBUG_TASKS, 153, 156, 255, "%s Found Health Source Source <%i> at %3.2f, %3.2f, %3.2f \n", me->GetDebugIdentifier(), static_cast(source), m_sourcepos.x, m_sourcepos.y, m_sourcepos.z); - NDebugOverlay::Text(m_sourcepos, "Ammo Source!", false, 10.0f); + NDebugOverlay::Text(m_sourcepos, "Health Source!", false, 10.0f); NDebugOverlay::Sphere(m_sourcepos, 32.0f, 153, 156, 255, true, 10.0f); } diff --git a/extension/bot/tf2/tasks/tf2bot_find_health_task.h b/extension/bot/tf2/tasks/tf2bot_find_health_task.h index 6b18050..d1c6792 100644 --- a/extension/bot/tf2/tasks/tf2bot_find_health_task.h +++ b/extension/bot/tf2/tasks/tf2bot_find_health_task.h @@ -23,9 +23,8 @@ class CTF2BotFindHealthTask : public AITask const char* GetName() const override { return "FindHealth"; } private: - CountdownTimer m_repathtimer; CountdownTimer m_failsafetimer; - CMeshNavigator m_nav; + CMeshNavigatorAutoRepath m_nav; HealthSource m_type; bool m_reached; CBaseHandle m_sourceentity; diff --git a/extension/bot/tf2/tasks/tf2bot_maintask.cpp b/extension/bot/tf2/tasks/tf2bot_maintask.cpp index eec1563..f62763c 100644 --- a/extension/bot/tf2/tasks/tf2bot_maintask.cpp +++ b/extension/bot/tf2/tasks/tf2bot_maintask.cpp @@ -10,6 +10,7 @@ #include #include #include "bot/tf2/tf2bot.h" +#include "tf2bot_dead.h" #include "tf2bot_taunting.h" #include "tf2bot_tactical.h" #include "tf2bot_maintask.h" @@ -102,6 +103,11 @@ Vector CTF2BotMainTask::GetTargetAimPos(CBaseBot* me, edict_t* entity, CBaseExtP return aimat; } +TaskEventResponseResult CTF2BotMainTask::OnKilled(CTF2Bot* bot, const CTakeDamageInfo& info) +{ + return TrySwitchTo(new CTF2BotDeadTask, PRIORITY_MANDATORY, "I am dead!"); +} + void CTF2BotMainTask::FireWeaponAtEnemy(CTF2Bot* me, const CKnownEntity* threat) { if (me->GetPlayerInfo()->IsDead()) @@ -153,6 +159,12 @@ void CTF2BotMainTask::FireWeaponAtEnemy(CTF2Bot* me, const CKnownEntity* threat) Vector top = origin; top.z += max.z - 1.0f; + // basic general direction check + if (!me->IsLookingTowards(center, 0.94f)) + { + return; + } + if (!me->IsLineOfFireClear(origin)) { if (!me->IsLineOfFireClear(center)) @@ -213,12 +225,13 @@ void CTF2BotMainTask::UpdateLook(CTF2Bot* me, const CKnownEntity* threat) void CTF2BotMainTask::InternalAimAtEnemyPlayer(CTF2Bot* me, CBaseExtPlayer* player, Vector& result) { - const CBotWeapon* myweapon = me->GetInventoryInterface()->GetActiveBotWeapon(); + auto myweapon = me->GetInventoryInterface()->GetActiveBotWeapon(); - if (!myweapon) // how? + // inventory does't update on every frame, this check is important. generally happens post spawn. + if (myweapon.get() == nullptr) { #ifdef EXT_DEBUG - smutils->LogError(myself, "CTF2BotMainTask::InternalAimAtEnemyPlayer(CTF2Bot* me, CBaseExtPlayer* player, Vector& result) -- CBaseBot::GetActiveBotWeapon() is NULL!"); + Warning("%s CTF2BotMainTask::InternalAimAtEnemyPlayer -- GetActiveBotWeapon() is NULL!", me->GetDebugIdentifier()); #endif // EXT_DEBUG result = player->WorldSpaceCenter(); diff --git a/extension/bot/tf2/tasks/tf2bot_maintask.h b/extension/bot/tf2/tasks/tf2bot_maintask.h index d964616..be03cbc 100644 --- a/extension/bot/tf2/tasks/tf2bot_maintask.h +++ b/extension/bot/tf2/tasks/tf2bot_maintask.h @@ -25,6 +25,8 @@ class CTF2BotMainTask : public AITask std::shared_ptr SelectTargetThreat(CBaseBot* me, std::shared_ptr threat1, std::shared_ptr threat2) override; Vector GetTargetAimPos(CBaseBot* me, edict_t* entity, CBaseExtPlayer* player = nullptr) override; + TaskEventResponseResult OnKilled(CTF2Bot* bot, const CTakeDamageInfo& info) override; + const char* GetName() const override { return "MainTask"; } private: diff --git a/extension/bot/tf2/tasks/tf2bot_roam.cpp b/extension/bot/tf2/tasks/tf2bot_roam.cpp index 791f0d1..9089573 100644 --- a/extension/bot/tf2/tasks/tf2bot_roam.cpp +++ b/extension/bot/tf2/tasks/tf2bot_roam.cpp @@ -7,6 +7,7 @@ #include #include #include "tf2bot_roam.h" +#include "tf2bot_attack.h" class RoamCollector : public INavAreaCollector { @@ -65,6 +66,18 @@ TaskResult CTF2BotRoamTask::OnTaskUpdate(CTF2Bot* bot) return Done("Goal Reached!"); } + auto threat = bot->GetSensorInterface()->GetPrimaryKnownThreat(true); + + if (threat) + { + if (randomgen->GetRandomInt(0, 100) >= 80) + { + m_repathtimer.Invalidate(); + m_nav.Invalidate(); + return PauseFor(new CTF2BotAttackTask(threat->GetEntity()), "Attacking visible threat!"); + } + } + if (m_repathtimer.IsElapsed()) { m_repathtimer.Start(randomgen->GetRandomReal(1.0f, 3.0f)); diff --git a/extension/bot/tf2/tf2bot.cpp b/extension/bot/tf2/tf2bot.cpp index 4c234a9..dea8bdc 100644 --- a/extension/bot/tf2/tf2bot.cpp +++ b/extension/bot/tf2/tf2bot.cpp @@ -237,27 +237,27 @@ bool CTF2Bot::IsAmmoLow() const bool haslowammoweapon = false; - GetInventoryInterface()->ForEveryWeapon([this, &haslowammoweapon](const CBotWeapon& weapon) { + GetInventoryInterface()->ForEveryWeapon([this, &haslowammoweapon](const CBotWeapon* weapon) { if (haslowammoweapon) return; - if (!weapon.GetWeaponInfo()->IsCombatWeapon()) + if (!weapon->GetWeaponInfo()->IsCombatWeapon()) { return; // don't bother with ammo for non combat weapons } - if (weapon.GetWeaponInfo()->HasLowPrimaryAmmoThreshold()) + if (weapon->GetWeaponInfo()->HasLowPrimaryAmmoThreshold()) { - if (GetAmmoOfIndex(weapon.GetBaseCombatWeapon().GetPrimaryAmmoType()) < weapon.GetWeaponInfo()->GetLowPrimaryAmmoThreshold()) + if (GetAmmoOfIndex(weapon->GetBaseCombatWeapon().GetPrimaryAmmoType()) < weapon->GetWeaponInfo()->GetLowPrimaryAmmoThreshold()) { haslowammoweapon = true; return; } } - if (weapon.GetWeaponInfo()->HasLowSecondaryAmmoThreshold()) + if (weapon->GetWeaponInfo()->HasLowSecondaryAmmoThreshold()) { - if (GetAmmoOfIndex(weapon.GetBaseCombatWeapon().GetSecondaryAmmoType()) < weapon.GetWeaponInfo()->GetLowSecondaryAmmoThreshold()) + if (GetAmmoOfIndex(weapon->GetBaseCombatWeapon().GetSecondaryAmmoType()) < weapon->GetWeaponInfo()->GetLowSecondaryAmmoThreshold()) { haslowammoweapon = true; return; diff --git a/extension/bot/tf2/tf2bot_upgrades.cpp b/extension/bot/tf2/tf2bot_upgrades.cpp index 3dd74cc..6d1e6cb 100644 --- a/extension/bot/tf2/tf2bot_upgrades.cpp +++ b/extension/bot/tf2/tf2bot_upgrades.cpp @@ -303,11 +303,10 @@ void CTF2BotUpgradeManager::FilterUpgrades() return; } - // Collect weapon item definition indexes std::vector myweaponindexes; - m_me->GetInventoryInterface()->ForEveryWeapon([&myweaponindexes](const CBotWeapon& weapon) { - myweaponindexes.push_back(weapon.GetWeaponEconIndex()); + m_me->GetInventoryInterface()->ForEveryWeapon([&myweaponindexes](const CBotWeapon* weapon) { + myweaponindexes.push_back(weapon->GetWeaponEconIndex()); }); auto start = std::remove_if(m_tobuylist.begin(), m_tobuylist.end(), [&myweaponindexes](const TF2BotUpgradeInfo_t* upgradeinfo) { diff --git a/extension/concommands_bots.cpp b/extension/concommands_bots.cpp index fa5bfc2..8e53dec 100644 --- a/extension/concommands_bots.cpp +++ b/extension/concommands_bots.cpp @@ -32,6 +32,12 @@ CON_COMMAND(sm_navbot_kick, "Removes a Nav Bot from the game.") return; } + if (strncasecmp(args[1], "all", 3) == 0) + { + extmanager->RemoveAllBots("Nav Bot: Kicked by admin command."); + return; + } + std::string targetname(args[1]); extmanager->ForEachBot([&targetname](CBaseBot* bot) { auto gp = playerhelpers->GetGamePlayer(bot->GetIndex()); diff --git a/extension/manager.cpp b/extension/manager.cpp index 08f5363..c758123 100644 --- a/extension/manager.cpp +++ b/extension/manager.cpp @@ -368,6 +368,11 @@ CBaseBot* CExtManager::AttachBotInstanceToEntity(edict_t* entity) void CExtManager::RemoveRandomBot(const char* message) { + if (m_bots.empty()) + { + return; + } + auto& botptr = m_bots[randomgen->GetRandomInt(0U, m_bots.size() - 1)]; auto player = playerhelpers->GetGamePlayer(botptr->GetIndex()); player->Kick(message); diff --git a/extension/mods/tf2/teamfortress2_shareddefs.h b/extension/mods/tf2/teamfortress2_shareddefs.h index b025eb1..16f3ee0 100644 --- a/extension/mods/tf2/teamfortress2_shareddefs.h +++ b/extension/mods/tf2/teamfortress2_shareddefs.h @@ -445,6 +445,7 @@ namespace TeamFortress2 enum TFFlagEvent { + TF_FLAGEVENT_INVALID = 0, // the game doesn't use this, this is in case we can't get the correct flag event TF_FLAGEVENT_PICKEDUP = 1, TF_FLAGEVENT_CAPTURED, TF_FLAGEVENT_DEFENDED, diff --git a/extension/mods/tf2/teamfortress2mod.cpp b/extension/mods/tf2/teamfortress2mod.cpp index 55786a4..233725d 100644 --- a/extension/mods/tf2/teamfortress2mod.cpp +++ b/extension/mods/tf2/teamfortress2mod.cpp @@ -117,6 +117,7 @@ CTeamFortress2Mod::CTeamFortress2Mod() : CBaseMod() ListenForGameEvent("controlpoint_initialized"); ListenForGameEvent("mvm_begin_wave"); ListenForGameEvent("mvm_wave_complete"); + ListenForGameEvent("teamplay_flag_event"); } CTeamFortress2Mod::~CTeamFortress2Mod() @@ -168,6 +169,36 @@ void CTeamFortress2Mod::FireGameEvent(IGameEvent* event) m_bInSetup = false; return; } + + if (strncasecmp(name, "teamplay_flag_event", 19) == 0) + { + edict_t* edict = gamehelpers->EdictOfIndex(event->GetInt("player", -1)); + IServerEntity* serverent = edict->GetIServerEntity(); + + if (serverent == nullptr) + { + return; + } + + CBaseEntity* entity = serverent->GetBaseEntity(); + + TeamFortress2::TFFlagEvent flagevent = static_cast(event->GetInt("eventtype", 0)); + + if (flagevent == TeamFortress2::TF_FLAGEVENT_PICKEDUP) + { + extmanager->ForEachBot([&entity](CBaseBot* bot) { + bot->OnFlagTaken(entity); + }); + } + else if (flagevent == TeamFortress2::TF_FLAGEVENT_DROPPED) + { + extmanager->ForEachBot([&entity](CBaseBot* bot) { + bot->OnFlagDropped(entity); + }); + } + + return; + } } }