diff --git a/Source/Framework/Constants/CreatureConst.cs b/Source/Framework/Constants/CreatureConst.cs index 7d076b25d..b43dc0c3d 100644 --- a/Source/Framework/Constants/CreatureConst.cs +++ b/Source/Framework/Constants/CreatureConst.cs @@ -13,7 +13,8 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - */ + */ +using System; namespace Framework.Constants { @@ -365,6 +366,7 @@ namespace Framework.Constants Other } + [Flags] public enum GroupAIFlags { None = 0, // No creature group behavior diff --git a/Source/Game/Chat/Commands/NPCCommands.cs b/Source/Game/Chat/Commands/NPCCommands.cs index a44a24cde..7e251df9b 100644 --- a/Source/Game/Chat/Commands/NPCCommands.cs +++ b/Source/Game/Chat/Commands/NPCCommands.cs @@ -800,7 +800,7 @@ namespace Game.Chat ulong lowguid = creature.GetSpawnId(); if (creature.GetFormation() != null) { - handler.SendSysMessage("Selected creature is already member of group {0}", creature.GetFormation().GetId()); + handler.SendSysMessage("Selected creature is already member of group {0}", creature.GetFormation().GetLeaderSpawnId()); return false; } @@ -808,21 +808,18 @@ namespace Game.Chat return false; Player chr = handler.GetSession().GetPlayer(); - FormationInfo group_member = new(); - group_member.follow_angle = (creature.GetAngle(chr) - chr.GetOrientation()) * 180 / MathFunctions.PI; - group_member.follow_dist = (float)Math.Sqrt(Math.Pow(chr.GetPositionX() - creature.GetPositionX(), 2) + Math.Pow(chr.GetPositionY() - creature.GetPositionY(), 2)); - group_member.leaderGUID = leaderGUID; - group_member.groupAI = 0; - - FormationMgr.CreatureGroupMap[lowguid] = group_member; + float followAngle = (creature.GetAngle(chr) - chr.GetOrientation()) * 180.0f / MathF.PI; + float followDist = MathF.Sqrt(MathF.Pow(chr.GetPositionX() - creature.GetPositionX(), 2f) + MathF.Pow(chr.GetPositionY() - creature.GetPositionY(), 2f)); + uint groupAI = 0; + FormationMgr.AddFormationMember(lowguid, followAngle, followDist, leaderGUID, groupAI); creature.SearchFormation(); PreparedStatement stmt = DB.World.GetPreparedStatement(WorldStatements.INS_CREATURE_FORMATION); stmt.AddValue(0, leaderGUID); stmt.AddValue(1, lowguid); - stmt.AddValue(2, group_member.follow_dist); - stmt.AddValue(3, group_member.follow_angle); - stmt.AddValue(4, group_member.groupAI); + stmt.AddValue(2, followAngle); + stmt.AddValue(3, followDist); + stmt.AddValue(4, groupAI); DB.World.Execute(stmt); diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index 29ae1c563..9e908d717 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -125,9 +125,9 @@ namespace Game.Entities if (lowguid == 0) return; - var frmdata = FormationMgr.CreatureGroupMap.LookupByKey(lowguid); - if (frmdata != null) - FormationMgr.AddCreatureToGroup(frmdata.leaderGUID, this); + var formationInfo = FormationMgr.GetFormationInfo(lowguid); + if (formationInfo != null) + FormationMgr.AddCreatureToGroup(formationInfo.LeaderSpawnId, this); } public bool IsFormationLeader() @@ -784,17 +784,18 @@ namespace Game.Entities void InitializeMovementAI() { - if (m_formation == null) - GetMotionMaster().Initialize(); - else if (m_formation.GetLeader() == this) + if (m_formation != null) { - m_formation.FormationReset(false); - GetMotionMaster().Initialize(); + if (m_formation.GetLeader() == this) + m_formation.FormationReset(false); + else if (m_formation.IsFormed()) + { + GetMotionMaster().MoveIdle(); //wait the order of leader + return; + } } - else if (m_formation.IsFormed()) - GetMotionMaster().MoveIdle(); //wait the order of leader - else - GetMotionMaster().Initialize(); + + GetMotionMaster().Initialize(); } public static Creature CreateCreature(uint entry, Map map, Position pos, uint vehId = 0) diff --git a/Source/Game/Entities/Creature/CreatureGroups.cs b/Source/Game/Entities/Creature/CreatureGroups.cs index 85174246e..b3a5ae719 100644 --- a/Source/Game/Entities/Creature/CreatureGroups.cs +++ b/Source/Game/Entities/Creature/CreatureGroups.cs @@ -20,48 +20,60 @@ using Game.Maps; using System; using System.Collections.Generic; using Framework.Constants; +using System.Linq; namespace Game.Entities { public class FormationMgr { - public static void AddCreatureToGroup(ulong leaderGuid, Creature member) + static Dictionary _creatureGroupMap = new(); + + public static void AddCreatureToGroup(ulong leaderSpawnId, Creature creature) { - Map map = member.GetMap(); - if (!map) - return; + Map map = creature.GetMap(); - var creatureGroup = map.CreatureGroupHolder.LookupByKey(leaderGuid); - - //Add member to an existing group + var creatureGroup = map.CreatureGroupHolder.LookupByKey(leaderSpawnId); if (creatureGroup != null) { - Log.outDebug(LogFilter.Unit, "Group found: {0}, inserting creature GUID: {1}, Group InstanceID {2}", leaderGuid, member.GetGUID().ToString(), member.GetInstanceId()); - creatureGroup.AddMember(member); + //Add member to an existing group + Log.outDebug(LogFilter.Unit, "Group found: {0}, inserting creature GUID: {1}, Group InstanceID {2}", leaderSpawnId, creature.GetGUID().ToString(), 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 + var bounds = map.GetCreatureBySpawnIdStore().LookupByKey(creature.GetSpawnId()); + foreach (var other in bounds) + { + if (other == creature) + continue; + + if (creatureGroup.HasMember(other)) + creatureGroup.RemoveMember(other); + } + + creatureGroup.AddMember(creature); } - //Create new group else { - Log.outDebug(LogFilter.Unit, "Group not found: {0}. Creating new group.", leaderGuid); - CreatureGroup group = new(leaderGuid); - map.CreatureGroupHolder[leaderGuid] = group; - group.AddMember(member); + //Create new group + Log.outDebug(LogFilter.Unit, "Group not found: {0}. Creating new group.", leaderSpawnId); + CreatureGroup group = new(leaderSpawnId); + map.CreatureGroupHolder[leaderSpawnId] = group; + group.AddMember(creature); } } public static void RemoveCreatureFromGroup(CreatureGroup group, Creature member) { - Log.outDebug(LogFilter.Unit, "Deleting member GUID: {0} from group {1}", group.GetId(), member.GetSpawnId()); + Log.outDebug(LogFilter.Unit, "Deleting member GUID: {0} from group {1}", group.GetLeaderSpawnId(), member.GetSpawnId()); group.RemoveMember(member); if (group.IsEmpty()) { Map map = member.GetMap(); - if (!map) - return; Log.outDebug(LogFilter.Unit, "Deleting group with InstanceID {0}", member.GetInstanceId()); - map.CreatureGroupHolder.Remove(group.GetId()); + Cypher.Assert(map.CreatureGroupHolder.ContainsKey(group.GetLeaderSpawnId()), $"Not registered group {group.GetLeaderSpawnId()} in map {map.GetId()}"); + map.CreatureGroupHolder.Remove(group.GetLeaderSpawnId()); } } @@ -69,11 +81,8 @@ namespace Game.Entities { uint oldMSTime = Time.GetMSTime(); - CreatureGroupMap.Clear(); - //Get group data SQLResult result = DB.World.Query("SELECT leaderGUID, memberGUID, dist, angle, groupAI, point_1, point_2 FROM creature_formations ORDER BY leaderGUID"); - if (result.IsEmpty()) { Log.outInfo(LogFilter.ServerLoading, "Loaded 0 creatures in formations. DB table `creature_formations` is empty!"); @@ -81,110 +90,155 @@ namespace Game.Entities } uint count = 0; - FormationInfo group_member; + List leaderSpawnIds = new(); do { //Load group member data - group_member = new FormationInfo(); - group_member.leaderGUID = result.Read(0); - ulong memberGUID = result.Read(1); - group_member.groupAI = result.Read(4); - group_member.point_1 = result.Read(5); - group_member.point_2 = result.Read(6); + FormationInfo member = new(); + member.LeaderSpawnId = result.Read(0); + ulong memberSpawnId = result.Read(1); + member.FollowDist = 0f; + member.FollowAngle = 0f; + //If creature is group leader we may skip loading of dist/angle - if (group_member.leaderGUID != memberGUID) + if (member.LeaderSpawnId != memberSpawnId) { - group_member.follow_dist = result.Read(2); - group_member.follow_angle = result.Read(3) * MathFunctions.PI / 180; - } - else - { - group_member.follow_dist = 0; - group_member.follow_angle = 0; + member.FollowDist = result.Read(2); + member.FollowAngle = result.Read(3) * MathFunctions.PI / 180; } + member.GroupAI = result.Read(4); + + for (var i = 0; i < 2; ++i) + member.LeaderWaypointIDs[i] = result.Read(5 + i); + // check data correctness { - if (Global.ObjectMgr.GetCreatureData(group_member.leaderGUID) == null) + if (Global.ObjectMgr.GetCreatureData(member.LeaderSpawnId) == null) { - Log.outError(LogFilter.Sql, "creature_formations table leader guid {0} incorrect (not exist)", group_member.leaderGUID); + Log.outError(LogFilter.Sql, $"creature_formations table leader guid {member.LeaderSpawnId} incorrect (not exist)"); continue; } - if (Global.ObjectMgr.GetCreatureData(memberGUID) == null) + if (Global.ObjectMgr.GetCreatureData(memberSpawnId) == null) { - Log.outError(LogFilter.Sql, "creature_formations table member guid {0} incorrect (not exist)", memberGUID); + Log.outError(LogFilter.Sql, $"creature_formations table member guid {memberSpawnId} incorrect (not exist)"); continue; } + + leaderSpawnIds.Add(member.LeaderSpawnId); } - CreatureGroupMap[memberGUID] = group_member; + _creatureGroupMap.Add(memberSpawnId, member); ++count; } while (result.NextRow()); + foreach (ulong leaderSpawnId in leaderSpawnIds) + { + if (!_creatureGroupMap.ContainsKey(leaderSpawnId)) + { + Log.outError(LogFilter.Sql, $"creature_formation contains leader spawn {leaderSpawnId} which is not included on its formation, removing"); + foreach (var itr in _creatureGroupMap.ToList()) + { + if (itr.Value.LeaderSpawnId == leaderSpawnId) + _creatureGroupMap.Remove(itr.Key); + } + } + } + Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creatures in formations in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime)); } - public static Dictionary CreatureGroupMap = new(); + public static FormationInfo GetFormationInfo(ulong spawnId) + { + return _creatureGroupMap.LookupByKey(spawnId); + } + + public static void AddFormationMember(ulong spawnId, float followAng, float followDist, ulong leaderSpawnId, uint groupAI) + { + FormationInfo member = new(); + member.LeaderSpawnId = leaderSpawnId; + member.FollowDist = followDist; + member.FollowAngle = followAng; + member.GroupAI = groupAI; + for (var i = 0; i < 2; ++i) + member.LeaderWaypointIDs[i] = 0; + + _creatureGroupMap.Add(spawnId, member); + } } public class FormationInfo { - public ulong leaderGUID; - public float follow_dist; - public float follow_angle; - public uint groupAI; - public ushort point_1; - public ushort point_2; + public ulong LeaderSpawnId; + public float FollowDist; + public float FollowAngle; + public uint GroupAI; + public uint[] LeaderWaypointIDs = new uint[2]; } public class CreatureGroup { - public CreatureGroup(ulong id) + Creature _leader; + Dictionary _members = new(); + + ulong _leaderSpawnId; + bool _formed; + bool _engaging; + + public CreatureGroup(ulong leaderSpawnId) { - m_groupID = id; + _leaderSpawnId = leaderSpawnId; } public void AddMember(Creature member) { - Log.outDebug(LogFilter.Unit, "CreatureGroup.AddMember: Adding unit GUID: {0}.", member.GetGUID().ToString()); + Log.outDebug(LogFilter.Unit, "CreatureGroup.AddMember: Adding {0}.", member.GetGUID().ToString()); //Check if it is a leader - if (member.GetSpawnId() == m_groupID) + if (member.GetSpawnId() == _leaderSpawnId) { - Log.outDebug(LogFilter.Unit, "Unit GUID: {0} is formation leader. Adding group.", member.GetGUID().ToString()); - m_leader = member; + Log.outDebug(LogFilter.Unit, "{0} is formation leader. Adding group.", member.GetGUID().ToString()); + _leader = member; } - m_members[member] = FormationMgr.CreatureGroupMap.LookupByKey(member.GetSpawnId()); + // formation must be registered at this point + FormationInfo formationInfo = FormationMgr.GetFormationInfo(member.GetSpawnId()); + _members.Add(member, formationInfo); member.SetFormation(this); } public void RemoveMember(Creature member) { - if (m_leader == member) - m_leader = null; + if (_leader == member) + _leader = null; - m_members.Remove(member); + _members.Remove(member); member.SetFormation(null); } public void MemberEngagingTarget(Creature member, Unit target) { - GroupAIFlags groupAI = (GroupAIFlags)FormationMgr.CreatureGroupMap[member.GetSpawnId()].groupAI; + // used to prevent recursive calls + if (_engaging) + return; + + GroupAIFlags groupAI = (GroupAIFlags)FormationMgr.GetFormationInfo(member.GetSpawnId()).GroupAI; if (groupAI == 0) return; - if (member == m_leader) + if (member == _leader) { - if (!groupAI.HasAnyFlag(GroupAIFlags.MembersAssistLeader)) + if (!groupAI.HasFlag(GroupAIFlags.MembersAssistLeader)) return; } - else if (!groupAI.HasAnyFlag(GroupAIFlags.LeaderAssistsMember)) + else if (!groupAI.HasFlag(GroupAIFlags.LeaderAssistsMember)) return; - foreach (var pair in m_members) + _engaging = true; + + foreach (var pair in _members) { Creature other = pair.Key; @@ -195,16 +249,18 @@ namespace Game.Entities if (!other.IsAlive()) continue; - if (((other != m_leader && groupAI.HasAnyFlag(GroupAIFlags.MembersAssistLeader)) || (other == m_leader && groupAI.HasAnyFlag(GroupAIFlags.LeaderAssistsMember))) && other.IsValidAttackTarget(target)) + if (((other != _leader && groupAI.HasFlag(GroupAIFlags.MembersAssistLeader)) || (other == _leader && groupAI.HasFlag(GroupAIFlags.LeaderAssistsMember))) && other.IsValidAttackTarget(target)) other.EngageWithTarget(target); } + + _engaging = false; } public void FormationReset(bool dismiss) { - foreach (var creature in m_members.Keys) + foreach (var creature in _members.Keys) { - if (creature != m_leader && creature.IsAlive()) + if (creature != _leader && creature.IsAlive()) { if (dismiss) creature.GetMotionMaster().Initialize(); @@ -213,37 +269,44 @@ namespace Game.Entities Log.outDebug(LogFilter.Unit, "Set {0} movement for member GUID: {1}", dismiss ? "default" : "idle", creature.GetGUID().ToString()); } } - m_Formed = !dismiss; + + _formed = !dismiss; } public void LeaderMoveTo(Position destination, uint id = 0, WaypointMoveType moveType = 0, bool orientation = false) { //! To do: This should probably get its own movement generator or use WaypointMovementGenerator. //! If the leader's path is known, member's path can be plotted as well using formation offsets. - if (!m_leader) + if (_leader == null) return; - float x = destination.GetPositionX(); - float y = destination.GetPositionY(); - float z = destination.GetPositionZ(); - float pathangle = (float)Math.Atan2(m_leader.GetPositionY() - y, m_leader.GetPositionX() - x); + Position pos = new(destination); + float pathangle = (float)Math.Atan2(_leader.GetPositionY() - pos.GetPositionY(), _leader.GetPositionX() - pos.GetPositionX()); - foreach (var pair in m_members) + foreach (var pair in _members) { Creature member = pair.Key; - if (member == m_leader || !member.IsAlive() || member.GetVictim() || !pair.Value.groupAI.HasAnyFlag((uint)GroupAIFlags.IdleInFormation)) + if (member == _leader || !member.IsAlive() || member.IsEngaged() || !pair.Value.GroupAI.HasAnyFlag((uint)GroupAIFlags.IdleInFormation)) continue; - if (pair.Value.point_1 != 0) - if (m_leader.GetCurrentWaypointInfo().nodeId == pair.Value.point_1 - 1 || m_leader.GetCurrentWaypointInfo().nodeId == pair.Value.point_2 - 1) - pair.Value.follow_angle = (float)Math.PI * 2 - pair.Value.follow_angle; + if (pair.Value.LeaderWaypointIDs[0] != 0) + { + for (var i = 0; i < 2; ++i) + { + if (_leader.GetCurrentWaypointInfo().nodeId == pair.Value.LeaderWaypointIDs[i]) + { + pair.Value.FollowAngle = MathF.PI * 2f - pair.Value.FollowAngle; + break; + } + } + } - float angle = pair.Value.follow_angle; - float dist = pair.Value.follow_dist; + float angle = pair.Value.FollowAngle; + float dist = pair.Value.FollowDist; - float dx = x + (float)Math.Cos(angle + pathangle) * dist; - float dy = y + (float)Math.Sin(angle + pathangle) * dist; - float dz = z; + float dx = pos.GetPositionX() + MathF.Cos(angle + pathangle) * dist; + float dy = pos.GetPositionY() + MathF.Sin(angle + pathangle) * dist; + float dz = pos.GetPositionZ(); GridDefines.NormalizeMapCoord(ref dx); GridDefines.NormalizeMapCoord(ref dy); @@ -253,16 +316,16 @@ namespace Game.Entities Position point = new(dx, dy, dz, destination.GetOrientation()); - member.GetMotionMaster().MoveFormation(id, point, moveType, !member.IsWithinDist(m_leader, dist + 5.0f), orientation); + member.GetMotionMaster().MoveFormation(id, point, moveType, !member.IsWithinDist(_leader, dist + 5.0f), orientation); member.SetHomePosition(dx, dy, dz, pathangle); } } public bool CanLeaderStartMoving() { - foreach (var itr in m_members) + foreach (var itr in _members) { - if (itr.Key != m_leader && itr.Key.IsAlive()) + if (itr.Key != _leader && itr.Key.IsAlive()) { if (itr.Key.IsEngaged() || itr.Key.IsReturningHome()) return false; @@ -272,16 +335,12 @@ namespace Game.Entities return true; } - public Creature GetLeader() { return m_leader; } - public ulong GetId() { return m_groupID; } - public bool IsEmpty() { return m_members.Empty(); } - public bool IsFormed() { return m_Formed; } - public bool IsLeader(Creature creature) { return m_leader == creature; } - - Creature m_leader; - Dictionary m_members = new(); + public Creature GetLeader() { return _leader; } + public ulong GetLeaderSpawnId() { return _leaderSpawnId; } + public bool IsEmpty() { return _members.Empty(); } + public bool IsFormed() { return _formed; } + public bool IsLeader(Creature creature) { return _leader == creature; } - ulong m_groupID; - bool m_Formed; + public bool HasMember(Creature member) { return _members.ContainsKey(member); } } -} +} \ No newline at end of file