/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*/
#include "CreatureGroups.h"
#include "Creature.h"
#include "CreatureAI.h"
#include "DatabaseEnv.h"
#include "Log.h"
#include "Map.h"
#include "MapUtils.h"
#include "MotionMaster.h"
#include "MovementGenerator.h"
#include "ObjectMgr.h"
#include "ZoneScript.h"
FormationMgr::FormationMgr() = default;
FormationMgr::~FormationMgr() = default;
FormationMgr* FormationMgr::instance()
{
static FormationMgr instance;
return &instance;
}
void FormationMgr::AddCreatureToGroup(ObjectGuid::LowType leaderSpawnId, Creature* creature)
{
Map* map = creature->GetMap();
auto [itr, isNew] = map->CreatureGroupHolder.try_emplace(leaderSpawnId, nullptr);
if (!isNew)
{
//Add member to an existing group
TC_LOG_DEBUG("entities.unit", "Group found: {}, inserting creature {}, Group InstanceID {}", leaderSpawnId, creature->GetGUID(), creature->GetInstanceId());
// With dynamic spawn the creature may have just respawned
// we need to find previous instance of creature and delete it from the formation, as it'll be invalidated
auto bounds = Trinity::Containers::MapEqualRange(map->GetCreatureBySpawnIdStore(), creature->GetSpawnId());
for (auto const& [spawnId, other] : bounds)
{
if (other == creature)
continue;
if (itr->second->HasMember(other))
itr->second->RemoveMember(other);
}
}
else
{
//Create new group
TC_LOG_DEBUG("entities.unit", "Group not found: {}. Creating new group.", leaderSpawnId);
itr->second = new CreatureGroup(leaderSpawnId);
}
itr->second->AddMember(creature);
}
void FormationMgr::RemoveCreatureFromGroup(CreatureGroup* group, Creature* member)
{
ObjectGuid::LowType leaderSpawnId = group->GetLeaderSpawnId();
TC_LOG_DEBUG("entities.unit", "Deleting member pointer to GUID: {} from group {}", leaderSpawnId, member->GetSpawnId());
group->RemoveMember(member);
// If removed member was alive we need to check if we have any other alive members
// if not - fire OnCreatureGroupDepleted
if (ZoneScript* script = member->GetZoneScript())
if (member->IsAlive() && !group->HasAliveMembers())
script->OnCreatureGroupDepleted(group);
if (group->IsEmpty())
{
if (leaderSpawnId)
{
Map* map = member->GetMap();
TC_LOG_DEBUG("entities.unit", "Deleting group with InstanceID {}", map->GetInstanceId());
std::size_t erased = map->CreatureGroupHolder.erase(leaderSpawnId);
ASSERT(erased, "Not registered group " UI64FMTD " in map %u", leaderSpawnId, map->GetId());
}
delete group;
}
}
void FormationMgr::LoadCreatureFormations()
{
uint32 oldMSTime = getMSTime();
//Get group data
QueryResult result = WorldDatabase.Query("SELECT leaderGUID, memberGUID, dist, angle, groupAI, point_1, point_2 FROM creature_formations ORDER BY leaderGUID");
if (!result)
{
TC_LOG_INFO("server.loading", ">> Loaded 0 creatures in formations. DB table `creature_formations` is empty!");
return;
}
uint32 count = 0;
std::unordered_set leaderSpawnIds;
do
{
Field* fields = result->Fetch();
//Load group member data
ObjectGuid::LowType leaderSpawnId = fields[0].GetUInt64();
ObjectGuid::LowType memberSpawnId = fields[1].GetUInt64();
// check data correctness
{
if (!sObjectMgr->GetCreatureData(leaderSpawnId))
{
TC_LOG_ERROR("sql.sql", "creature_formations table leader guid {} incorrect (not exist)", leaderSpawnId);
continue;
}
if (!sObjectMgr->GetCreatureData(memberSpawnId))
{
TC_LOG_ERROR("sql.sql", "creature_formations table member guid {} incorrect (not exist)", memberSpawnId);
continue;
}
leaderSpawnIds.insert(leaderSpawnId);
}
FormationInfo& member = _creatureGroupMap[memberSpawnId];
member.LeaderSpawnId = leaderSpawnId;
member.FollowDist = 0.f;
member.FollowAngle = 0.f;
//If creature is group leader we may skip loading of dist/angle
if (member.LeaderSpawnId != memberSpawnId)
{
member.FollowDist = fields[2].GetFloat();
member.FollowAngle = fields[3].GetFloat() * float(M_PI) / 180.0f;
}
member.GroupAI = fields[4].GetUInt32();
for (uint8 i = 0; i < 2; ++i)
member.LeaderWaypointIDs[i] = fields[5 + i].GetUInt16();
++count;
} while (result->NextRow());
for (ObjectGuid::LowType leaderSpawnId : leaderSpawnIds)
{
if (!_creatureGroupMap.contains(leaderSpawnId))
{
TC_LOG_ERROR("sql.sql", "creature_formation contains leader spawn {} which is not included on its formation, removing", leaderSpawnId);
for (auto itr = _creatureGroupMap.begin(); itr != _creatureGroupMap.end();)
{
if (itr->second.LeaderSpawnId == leaderSpawnId)
{
itr = _creatureGroupMap.erase(itr);
continue;
}
++itr;
}
}
}
TC_LOG_INFO("server.loading", ">> Loaded {} creatures in formations in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
}
FormationInfo* FormationMgr::GetFormationInfo(ObjectGuid::LowType spawnId)
{
return Trinity::Containers::MapGetValuePtr(_creatureGroupMap, spawnId);
}
void FormationMgr::AddFormationMember(ObjectGuid::LowType spawnId, float followAng, float followDist, ObjectGuid::LowType leaderSpawnId, uint32 groupAI)
{
FormationInfo& member = _creatureGroupMap[spawnId];
member.LeaderSpawnId = leaderSpawnId;
member.FollowDist = followDist;
member.FollowAngle = followAng;
member.GroupAI = groupAI;
for (uint8 i = 0; i < 2; ++i)
member.LeaderWaypointIDs[i] = 0;
}
CreatureGroup::CreatureGroup(ObjectGuid::LowType leaderSpawnId) : _leader(nullptr), _members(), _leaderSpawnId(leaderSpawnId), _formed(false), _engaging(false)
{
}
CreatureGroup::~CreatureGroup() = default;
void CreatureGroup::AddMember(Creature* member)
{
TC_LOG_DEBUG("entities.unit", "CreatureGroup::AddMember: Adding unit {}.", member->GetGUID());
//Check if it is a leader
if ((_leaderSpawnId && member->GetSpawnId() == _leaderSpawnId)
|| (!_leaderSpawnId && !_leader)) // in formations made of tempsummons first member to be added is leader
{
TC_LOG_DEBUG("entities.unit", "Unit {} is formation leader. Adding group.", member->GetGUID());
_leader = member;
}
_members.emplace(member, sFormationMgr->GetFormationInfo(member->GetSpawnId()));
member->SetFormation(this);
}
void CreatureGroup::RemoveMember(Creature* member)
{
if (_leader == member)
_leader = nullptr;
_members.erase(member);
member->SetFormation(nullptr);
}
void CreatureGroup::MemberEngagingTarget(Creature* member, Unit* target)
{
// used to prevent recursive calls
if (_engaging)
return;
FormationInfo const* formationInfo = Trinity::Containers::MapGetValuePtr(_members, member);
if (!formationInfo)
return;
uint32 groupAI = formationInfo->GroupAI;
if (!groupAI)
return;
if (member == _leader)
{
if (!(groupAI & FLAG_MEMBERS_ASSIST_LEADER))
return;
}
else if (!(groupAI & FLAG_LEADER_ASSISTS_MEMBER))
return;
_engaging = true;
for (auto const& [other, _] : _members)
{
if (other == member)
continue;
if (!other->IsAlive())
continue;
if (((other != _leader && (groupAI & FLAG_MEMBERS_ASSIST_LEADER)) || (other == _leader && (groupAI & FLAG_LEADER_ASSISTS_MEMBER))) && other->IsValidAttackTarget(target))
other->EngageWithTarget(target);
}
_engaging = false;
}
void CreatureGroup::FormationReset(bool dismiss)
{
for (auto const& [member, _] : _members)
{
if (member != _leader && member->IsAlive())
{
if (dismiss)
member->GetMotionMaster()->Remove(FORMATION_MOTION_TYPE, MOTION_SLOT_DEFAULT);
else
member->GetMotionMaster()->MoveIdle();
TC_LOG_DEBUG("entities.unit", "CreatureGroup::FormationReset: Set {} movement for member {}", dismiss ? "default" : "idle", member->GetGUID());
}
}
_formed = !dismiss;
}
void CreatureGroup::LeaderStartedMoving()
{
if (!_leader)
return;
for (auto const& [member, formationInfo] : _members)
{
if (member == _leader || !member->IsAlive() || member->IsEngaged() || !formationInfo || !(formationInfo->GroupAI & FLAG_IDLE_IN_FORMATION))
continue;
float angle = formationInfo->FollowAngle + float(M_PI); // for some reason, someone thought it was a great idea to invert relativ angles...
float dist = formationInfo->FollowDist;
if (member->GetMotionMaster()->GetCurrentMovementGeneratorType(MOTION_SLOT_DEFAULT) != FORMATION_MOTION_TYPE)
member->GetMotionMaster()->MoveFormation(_leader, dist, angle, formationInfo->LeaderWaypointIDs[0], formationInfo->LeaderWaypointIDs[1]);
}
}
bool CreatureGroup::CanLeaderStartMoving() const
{
for (auto const& [member, _] : _members)
{
if (member != _leader && member->IsAlive())
{
if (member->IsEngaged() || member->IsReturningHome())
return false;
}
}
return true;
}
bool CreatureGroup::HasAliveMembers() const
{
return std::ranges::any_of(_members, [](Creature const* member) { return member->IsAlive(); }, Trinity::Containers::MapKey);
}
bool CreatureGroup::LeaderHasStringId(std::string_view id) const
{
if (_leader)
return _leader->HasStringId(id);
if (CreatureData const* leaderCreatureData = sObjectMgr->GetCreatureData(_leaderSpawnId))
{
if (leaderCreatureData->StringId == id)
return true;
if (ASSERT_NOTNULL(sObjectMgr->GetCreatureTemplate(leaderCreatureData->id))->StringId == id)
return true;
}
return false;
}