using System; using System.Collections.Generic; using System.Linq; using System.Threading; using ACE.Entity.Enum; using ACE.Server.Physics.Managers; using ACE.Server.WorldObjects; namespace ACE.Server.Physics.Common { /// /// Player visibility tracking /// Keeps track of which objects are currently known / visible to a player /// public class ObjectMaint { /// /// The client automatically removes known objects if they remain outside visibility for this amount of time /// public const float DestructionTime = 25.0f; private static readonly ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); /// /// The owner of this ObjectMaint instance /// This is who we are tracking object visibility for, ie. a Player /// public PhysicsObj PhysicsObj { get; } /// /// This list of objects that are known to a player /// /// /// /// - When an object enters PVS / VisibleCell range of a player, /// it is added to this list, and the VisibleObject list /// /// - When an object exits PVS / VisibleCell range of a player, /// it is removed from the VisibleObject list, and added to the destruction queue. /// if it remains outside PVS for DestructionTime, the client automatically culls the object, /// and it is removed from this list of known objects. /// /// - This is only maintained for players. /// private Dictionary KnownObjects { get; } = new Dictionary(); /// /// This list of objects that are currently within PVS / VisibleCell range /// only maintained for players /// private Dictionary VisibleObjects { get; } = new Dictionary(); /// /// Objects that were previously visible to the client, /// but have been outside the PVS for less than 25 seconds /// only maintained for players /// private Dictionary DestructionQueue { get; } = new Dictionary(); /// /// A list of players that currently know about this object /// This is maintained for all server-spawned WorldObjects, and is used for broadcasting /// private Dictionary KnownPlayers { get; } = new Dictionary(); /// /// For monster and CombatPet FindNextTarget /// - for monsters, contains players and combat pets /// - for combat pets, contains monsters /// private Dictionary VisibleTargets { get; } = new Dictionary(); /// /// Handles monsters targeting things they would not normally target /// - For faction mobs, retaliate against same-faction players and combat pets /// - For regular monsters, retaliate against faction mobs /// - For regular monsters that do *not* have a FoeType, retaliate against monsters that are foes with this creature /// private Dictionary RetaliateTargets { get; } = new Dictionary(); // Client structures - // When client unloads a cell/landblock, but still knows about objects in those cells? //public Dictionary LostCellTable; //public List NullObjectTable; //public Dictionary WeenieObjectTable; //public List NullWeenieObjectTable; public ObjectMaint() { } public ObjectMaint(PhysicsObj obj) { PhysicsObj = obj; } public PhysicsObj GetKnownObject(uint objectGuid) { rwLock.EnterReadLock(); try { KnownObjects.TryGetValue(objectGuid, out var obj); return obj; } finally { rwLock.ExitReadLock(); } } public int GetKnownObjectsCount() { rwLock.EnterReadLock(); try { return KnownObjects.Count; } finally { rwLock.ExitReadLock(); } } public bool KnownObjectsContainsKey(uint guid) { rwLock.EnterReadLock(); try { return KnownObjects.ContainsKey(guid); } finally { rwLock.ExitReadLock(); } } public bool KnownObjectsContainsValue(PhysicsObj value) { rwLock.EnterReadLock(); try { return KnownObjects.Values.Contains(value); } finally { rwLock.ExitReadLock(); } } public List> GetKnownObjectsWhere(Func, bool> predicate) { rwLock.EnterReadLock(); try { return KnownObjects.Where(predicate).ToList(); } finally { rwLock.ExitReadLock(); } } public List GetKnownObjectsValues() { rwLock.EnterReadLock(); try { return KnownObjects.Values.ToList(); } finally { rwLock.ExitReadLock(); } } public List GetKnownObjectsValuesWhere(Func predicate) { rwLock.EnterReadLock(); try { return KnownObjects.Values.Where(predicate).ToList(); } finally { rwLock.ExitReadLock(); } } /// /// Adds an object to the list of known objects /// only maintained for players /// /// true if previously an unknown object public bool AddKnownObject(PhysicsObj obj) { rwLock.EnterWriteLock(); try { if (KnownObjects.ContainsKey(obj.ID)) return false; KnownObjects.TryAdd(obj.ID, obj); // maintain KnownPlayers for both parties if (obj.IsPlayer) AddKnownPlayer(obj); obj.ObjMaint.AddKnownPlayer(PhysicsObj); return true; } finally { rwLock.ExitWriteLock(); } } /// /// Adds a list of objects to the known objects list /// only maintained for players /// /// A list of currently visible objects /// The list of visible objects that were previously unknown private List AddKnownObjects(IEnumerable objs) { var newObjs = new List(); foreach (var obj in objs) { if (AddKnownObject(obj)) newObjs.Add(obj); } return newObjs; } public bool RemoveKnownObject(PhysicsObj obj, bool inversePlayer = true) { rwLock.EnterWriteLock(); try { var result = KnownObjects.Remove(obj.ID, out _); if (inversePlayer && PhysicsObj.IsPlayer) obj.ObjMaint.RemoveKnownPlayer(PhysicsObj); return result; } finally { rwLock.ExitWriteLock(); } } public int GetVisibleObjectsCount() { rwLock.EnterReadLock(); try { return VisibleObjects.Count; } finally { rwLock.ExitReadLock(); } } public bool VisibleObjectsContainsKey(uint key) { rwLock.EnterReadLock(); try { return VisibleObjects.ContainsKey(key); } finally { rwLock.ExitReadLock(); } } public List> GetVisibleObjectsWhere(Func, bool> predicate) { rwLock.EnterReadLock(); try { return VisibleObjects.Where(predicate).ToList(); } finally { rwLock.ExitReadLock(); } } public List GetVisibleObjectsValues() { rwLock.EnterReadLock(); try { return VisibleObjects.Values.ToList(); } finally { rwLock.ExitReadLock(); } } public List GetVisibleObjectsValuesWhere(Func predicate) { rwLock.EnterReadLock(); try { return VisibleObjects.Values.Where(predicate).ToList(); } finally { rwLock.ExitReadLock(); } } public List GetVisibleObjectsValuesOfTypeCreature() { rwLock.EnterReadLock(); try { return VisibleObjects.Values.Select(v => v.WeenieObj.WorldObject).OfType().ToList(); } finally { rwLock.ExitReadLock(); } } /// /// Returns a list of objects that are currently visible from a cell /// public List GetVisibleObjects(ObjCell cell, VisibleObjectType type = VisibleObjectType.All) { rwLock.EnterReadLock(); try { if (PhysicsObj.CurLandblock == null || cell == null) return new List(); // use PVS / VisibleCells for EnvCells not seen outside // (mostly dungeons, also some large indoor areas ie. caves) if (cell is EnvCell envCell) return GetVisibleObjects(envCell, type); // use current landblock + adjacents for outdoors, // and envcells seen from outside (all buildings) var visibleObjs = PhysicsObj.CurLandblock.GetServerObjects(true); return ApplyFilter(visibleObjs, type).Where(i => i.ID != PhysicsObj.ID && (!(i.CurCell is EnvCell indoors) || indoors.SeenOutside)).ToList(); } finally { rwLock.ExitReadLock(); } } /// /// Returns a list of objects that are currently visible from a dungeon cell /// private List GetVisibleObjects(EnvCell cell, VisibleObjectType type) { var visibleObjs = new List(); // add objects from current cell cell.AddObjectListTo(visibleObjs); // add objects from visible cells foreach (var envCell in cell.VisibleCells.Values) { if (envCell != null) envCell.AddObjectListTo(visibleObjs); } // if SeenOutside, add objects from outdoor landblock if (cell.SeenOutside) { var outsideObjs = PhysicsObj.CurLandblock.GetServerObjects(true).Where(i => !(i.CurCell is EnvCell indoors) || indoors.SeenOutside); visibleObjs.AddRange(outsideObjs); } return ApplyFilter(visibleObjs, type).Where(i => !i.DatObject && i.ID != PhysicsObj.ID).Distinct().ToList(); } public enum VisibleObjectType { All, Players, AttackTargets }; private IEnumerable ApplyFilter(List objs, VisibleObjectType type) { IEnumerable results = objs; if (type == VisibleObjectType.Players) { results = objs.Where(i => i.IsPlayer); } else if (type == VisibleObjectType.AttackTargets) { if (PhysicsObj.WeenieObj.IsCombatPet) results = objs.Where(i => i.WeenieObj.IsMonster && i.WeenieObj.PlayerKillerStatus != PlayerKillerStatus.PK); // combat pets cannot attack pk-only creatures (ie. faction banners) else if (PhysicsObj.WeenieObj.IsFactionMob) results = objs.Where(i => i.IsPlayer || i.WeenieObj.IsCombatPet || i.WeenieObj.IsMonster && !i.WeenieObj.SameFaction(PhysicsObj)); else { // adding faction mobs here, even though they are retaliate-only, for inverse visible targets results = objs.Where(i => i.IsPlayer || i.WeenieObj.IsCombatPet && PhysicsObj.WeenieObj.PlayerKillerStatus != PlayerKillerStatus.PK || i.WeenieObj.IsFactionMob || i.WeenieObj.PotentialFoe(PhysicsObj)); } } return results; } public static bool InitialClamp = true; public static float InitialClamp_Dist = 112.5f; public static float InitialClamp_DistSq = InitialClamp_Dist * InitialClamp_Dist; /// /// Adds an object to the list of visible objects /// only maintained for players /// /// TRUE if object was previously not visible, and added to the visible list public bool AddVisibleObject(PhysicsObj obj) { rwLock.EnterWriteLock(); try { if (VisibleObjects.ContainsKey(obj.ID)) return false; if (InitialClamp && !KnownObjects.ContainsKey(obj.ID)) { var distSq = PhysicsObj.Position.Distance2DSquared(obj.Position); if (distSq > InitialClamp_DistSq) return false; } //Console.WriteLine($"{PhysicsObj.Name}.AddVisibleObject({obj.Name})"); VisibleObjects.TryAdd(obj.ID, obj); if (obj.WeenieObj.IsMonster) obj.ObjMaint.AddVisibleTarget(PhysicsObj, false); return true; } finally { rwLock.ExitWriteLock(); } } /// /// Add a list of visible objects - maintains both known and visible objects /// only maintained for players /// public List AddVisibleObjects(ICollection objs) { rwLock.EnterWriteLock(); try { var visibleAdded = new List(); foreach (var obj in objs) { if (AddVisibleObject(obj)) visibleAdded.Add(obj); } RemoveObjectsToBeDestroyed(objs); return AddKnownObjects(visibleAdded); } finally { rwLock.ExitWriteLock(); } } /// /// Removes an object from the visible objects list /// only run for players /// and also removes the player from the object's visible targets list /// public bool RemoveVisibleObject(PhysicsObj obj, bool inverseTarget = true) { rwLock.EnterWriteLock(); try { var removed = VisibleObjects.Remove(obj.ID, out _); if (inverseTarget) obj.ObjMaint.RemoveVisibleTarget(PhysicsObj); return removed; } finally { rwLock.ExitWriteLock(); } } public int GetDestructionQueueCount() { rwLock.EnterReadLock(); try { return DestructionQueue.Count; } finally { rwLock.ExitReadLock(); } } public Dictionary GetDestructionQueueCopy() { rwLock.EnterReadLock(); try { var result = new Dictionary(); foreach (var kvp in DestructionQueue) result[kvp.Key] = kvp.Value; return result; } finally { rwLock.ExitReadLock(); } } /// /// Adds an object to the destruction queue /// Called when an object exits the PVS range /// only maintained for players /// public bool AddObjectToBeDestroyed(PhysicsObj obj) { rwLock.EnterWriteLock(); try { RemoveVisibleObject(obj); if (DestructionQueue.ContainsKey(obj)) return false; DestructionQueue.TryAdd(obj, PhysicsTimer.CurrentTime + DestructionTime); return true; } finally { rwLock.ExitWriteLock(); } } /// /// Adds a list of objects to the destruction queue /// only maintained for players /// public List AddObjectsToBeDestroyed(List objs) { rwLock.EnterWriteLock(); try { var queued = new List(); foreach (var obj in objs) { if (AddObjectToBeDestroyed(obj)) queued.Add(obj); } return queued; } finally { rwLock.ExitWriteLock(); } } /// /// Removes an object from the destruction queue if it has been invisible for less than 25s /// this is only used for players /// public bool RemoveObjectToBeDestroyed(PhysicsObj obj) { rwLock.EnterWriteLock(); try { double time = -1; DestructionQueue.TryGetValue(obj, out time); if (time != -1 && time > PhysicsTimer.CurrentTime) { DestructionQueue.Remove(obj, out _); return true; } return false; } finally { rwLock.ExitWriteLock(); } } /// /// Removes objects from the destruction queue when they re-enter visibility within 25s /// this is only used for players /// private void RemoveObjectsToBeDestroyed(IEnumerable objs) { foreach (var obj in objs) RemoveObjectToBeDestroyed(obj); } /// /// Removes any objects that have been in the destruction queue for more than 25s /// this is only used for players /// public List DestroyObjects() { rwLock.EnterWriteLock(); try { // find the list of objects that have been in the destruction queue > 25s var expiredObjs = DestructionQueue.Where(kvp => kvp.Value <= PhysicsTimer.CurrentTime) .Select(kvp => kvp.Key).ToList(); // remove expired objects from all lists foreach (var expiredObj in expiredObjs) RemoveObject(expiredObj); return expiredObjs; } finally { rwLock.ExitWriteLock(); } } /// /// Removes an object after it has expired from the destruction queue, or it has been destroyed /// public void RemoveObject(PhysicsObj obj, bool inverse = true) { rwLock.EnterWriteLock(); try { if (obj == null) return; RemoveKnownObject(obj, inverse); RemoveVisibleObject(obj, inverse); DestructionQueue.Remove(obj, out _); if (obj.IsPlayer) RemoveKnownPlayer(obj); RemoveVisibleTarget(obj); RemoveRetaliateTarget(obj); } finally { rwLock.ExitWriteLock(); } } public int GetKnownPlayersCount() { rwLock.EnterReadLock(); try { return KnownPlayers.Count; } finally { rwLock.ExitReadLock(); } } public List> GetKnownPlayersWhere(Func, bool> predicate) { rwLock.EnterReadLock(); try { return KnownPlayers.Where(predicate).ToList(); } finally { rwLock.ExitReadLock(); } } public List GetKnownPlayersValues() { rwLock.EnterReadLock(); try { return KnownPlayers.Values.ToList(); } finally { rwLock.ExitReadLock(); } } public List GetKnownPlayersValuesAsPlayer() { rwLock.EnterReadLock(); try { return KnownPlayers.Values.Select(v => v.WeenieObj.WorldObject).OfType().ToList(); } finally { rwLock.ExitReadLock(); } } public List GetKnownObjectsValuesAsCreature() { rwLock.EnterReadLock(); try { return KnownObjects.Values.Select(v => v.WeenieObj.WorldObject).OfType().ToList(); } finally { rwLock.ExitReadLock(); } } /// /// Adds a player who currently knows about this object /// - this is maintained for all server-spawned objects /// /// when an object broadcasts, it sends network messages /// to all players who currently know about this object /// /// true if previously an unknown object private bool AddKnownPlayer(PhysicsObj obj) { // only tracking players who know about this object if (!obj.IsPlayer) { Console.WriteLine($"{PhysicsObj.Name}.ObjectMaint.AddKnownPlayer({obj.Name}): tried to add a non-player"); return false; } if (PhysicsObj.DatObject) { Console.WriteLine($"{PhysicsObj.Name}.ObjectMaint.AddKnownPlayer({obj.Name}): tried to add player for dat object"); return false; } //Console.WriteLine($"{PhysicsObj.Name} ({PhysicsObj.ID:X8}).ObjectMaint.AddKnownPlayer({obj.Name})"); // TryAdd for existing keys still modifies collection? if (KnownPlayers.ContainsKey(obj.ID)) return false; KnownPlayers.TryAdd(obj.ID, obj); return true; } /// /// Adds a list of players known to this object /// public List AddKnownPlayers(IEnumerable objs) { rwLock.EnterWriteLock(); try { var newObjs = new List(); foreach (var obj in objs) { if (AddKnownPlayer(obj)) newObjs.Add(obj); } return newObjs; } finally { rwLock.ExitWriteLock(); } } /// /// Removes a known player for this object /// public bool RemoveKnownPlayer(PhysicsObj obj) { //Console.WriteLine($"{PhysicsObj.Name} ({PhysicsObj.ID:X8}).ObjectMaint.RemoveKnownPlayer({obj.Name})"); rwLock.EnterReadLock(); try { return KnownPlayers.Remove(obj.ID, out _); } finally { rwLock.ExitReadLock(); } } public int GetVisibleTargetsCount() { rwLock.EnterReadLock(); try { return VisibleTargets.Count; } finally { rwLock.ExitReadLock(); } } public int GetRetaliateTargetsCount() { rwLock.EnterReadLock(); try { return RetaliateTargets.Count; } finally { rwLock.ExitReadLock(); } } public bool VisibleTargetsContainsKey(uint key) { rwLock.EnterReadLock(); try { return VisibleTargets.ContainsKey(key); } finally { rwLock.ExitReadLock(); } } public bool RetaliateTargetsContainsKey(uint key) { rwLock.EnterReadLock(); try { return RetaliateTargets.ContainsKey(key); } finally { rwLock.ExitReadLock(); } } public List GetVisibleTargetsValues() { rwLock.EnterReadLock(); try { return VisibleTargets.Values.ToList(); } finally { rwLock.ExitReadLock(); } } public List GetRetaliateTargetsValues() { rwLock.EnterReadLock(); try { return RetaliateTargets.Values.ToList(); } finally { rwLock.ExitReadLock(); } } public List GetVisibleTargetsValuesOfTypeCreature() { rwLock.EnterReadLock(); try { return VisibleTargets.Values.Select(v => v.WeenieObj.WorldObject).OfType().ToList(); } finally { rwLock.ExitReadLock(); } } /// /// For monster and CombatPet FindNextTarget /// - for monsters, contains players and combat pets /// - for combat pets, contains monsters /// private bool AddVisibleTarget(PhysicsObj obj, bool clamp = true, bool foeType = false) { if (PhysicsObj.WeenieObj.IsCombatPet) { // only tracking monsters if (!obj.WeenieObj.IsMonster) { Console.WriteLine($"{PhysicsObj.Name}.ObjectMaint.AddVisibleTarget({obj.Name}): tried to add a non-monster"); return false; } } else if (PhysicsObj.WeenieObj.IsFactionMob) { // only tracking players, combat pets, and monsters of differing faction if (!obj.IsPlayer && !obj.WeenieObj.IsCombatPet && (!obj.WeenieObj.IsMonster || PhysicsObj.WeenieObj.SameFaction(obj))) { Console.WriteLine($"{PhysicsObj.Name}.ObjectMaint.AddVisibleTarget({obj.Name}): tried to add a non-player / non-combat pet / non-opposing faction mob"); return false; } } else { // handle special case: // we want to select faction mobs for monsters inverse targets, // but not add to the original monster if (obj.WeenieObj.IsFactionMob) { obj.ObjMaint.AddVisibleTarget(PhysicsObj); return false; } // handle special case: // if obj has a FoeType of this creature, and this creature doesn't have a FoeType for obj, // we only want to perform the inverse if (obj.WeenieObj.FoeType != null && obj.WeenieObj.FoeType == PhysicsObj.WeenieObj.WorldObject?.CreatureType && (PhysicsObj.WeenieObj.FoeType == null || obj.WeenieObj.WorldObject != null && PhysicsObj.WeenieObj.FoeType != obj.WeenieObj.WorldObject.CreatureType)) { obj.ObjMaint.AddVisibleTarget(PhysicsObj); return false; } // only tracking players and combat pets if (!obj.IsPlayer && !obj.WeenieObj.IsCombatPet && PhysicsObj.WeenieObj.FoeType == null) { Console.WriteLine($"{PhysicsObj.Name}.ObjectMaint.AddVisibleTarget({obj.Name}): tried to add a non-player / non-combat pet"); return false; } } if (PhysicsObj.DatObject) { Console.WriteLine($"{PhysicsObj.Name}.ObjectMaint.AddVisibleTarget({obj.Name}): tried to add player for dat object"); return false; } if (clamp && InitialClamp && obj.IsPlayer && !obj.ObjMaint.KnownObjects.ContainsKey(obj.ID)) { var distSq = PhysicsObj.Position.Distance2DSquared(obj.Position); if (distSq > InitialClamp_DistSq) return false; } // TryAdd for existing keys still modifies collection? if (VisibleTargets.ContainsKey(obj.ID)) return false; //Console.WriteLine($"{PhysicsObj.Name} ({PhysicsObj.ID:X8}).ObjectMaint.AddVisibleTarget({obj.Name})"); VisibleTargets.Add(obj.ID, obj); // maintain inverse for monsters / combat pets if (!obj.IsPlayer) obj.ObjMaint.AddVisibleTarget(PhysicsObj); return true; } /// /// For monster and CombatPet FindNextTarget /// - for monsters, contains players and combat pets /// - for combat pets, contains monsters /// public List AddVisibleTargets(IEnumerable objs) { rwLock.EnterWriteLock(); try { var visibleAdded = new List(); foreach (var obj in objs) { if (AddVisibleTarget(obj)) visibleAdded.Add(obj); } return AddKnownPlayers(visibleAdded.Where(o => o.IsPlayer)); } finally { rwLock.ExitWriteLock(); } } private bool RemoveVisibleTarget(PhysicsObj obj) { //Console.WriteLine($"{PhysicsObj.Name} ({PhysicsObj.ID:X8}).ObjectMaint.RemoveVisibleTarget({obj.Name})"); return VisibleTargets.Remove(obj.ID); } public List GetVisibleObjectsDist(ObjCell cell, VisibleObjectType type) { rwLock.EnterReadLock(); try { var visibleObjs = GetVisibleObjects(cell, type); var dist = new List(); foreach (var obj in visibleObjs) { var distSq = PhysicsObj.Position.Distance2DSquared(obj.Position); if (distSq <= InitialClamp_DistSq) dist.Add(obj); } return dist; } finally { rwLock.ExitReadLock(); } } /// /// Adds a retaliate target for a monster that it would not normally attack /// public void AddRetaliateTarget(PhysicsObj obj) { rwLock.EnterWriteLock(); try { if (RetaliateTargets.ContainsKey(obj.ID)) { //Console.WriteLine($"{PhysicsObj.Name}.AddRetaliateTarget({obj.Name}) - retaliate target already exists"); return; } //Console.WriteLine($"{PhysicsObj.Name}.AddRetaliateTarget({obj.Name})"); RetaliateTargets.Add(obj.ID, obj); // we're going to add retaliate targets to the list of visible targets as well, // so that we don't have to traverse both VisibleTargets and RetaliateTargets // in all of the logic based on VisibleTargets if (VisibleTargets.ContainsKey(obj.ID)) { //Console.WriteLine($"{PhysicsObj.Name}.AddRetaliateTarget({obj.Name}) - visible target already exists"); return; } VisibleTargets.Add(obj.ID, obj); } finally { rwLock.ExitWriteLock(); } } /// /// Called when a monster goes back to sleep /// public void ClearRetaliateTargets() { rwLock.EnterWriteLock(); try { // remove retaliate targets from visible targets foreach (var retaliateTarget in RetaliateTargets) VisibleTargets.Remove(retaliateTarget.Key); RetaliateTargets.Clear(); } finally { rwLock.ExitWriteLock(); } } private bool RemoveRetaliateTarget(PhysicsObj obj) { return RetaliateTargets.Remove(obj.ID); } /// /// Clears all of the ObjMaint tables for an object /// private void RemoveAllObjects() { KnownObjects.Clear(); VisibleObjects.Clear(); DestructionQueue.Clear(); KnownPlayers.Clear(); VisibleTargets.Clear(); RetaliateTargets.Clear(); } /// /// The destructor cleans up all ObjMaint references /// to this PhysicsObj /// public void DestroyObject() { rwLock.EnterWriteLock(); try { foreach (var obj in KnownObjects.Values) obj.ObjMaint.RemoveObject(PhysicsObj); // we are maintaining the inverses here, // so passing false to iterate with modifying these collections foreach (var obj in KnownPlayers.Values) obj.ObjMaint.RemoveObject(PhysicsObj, false); foreach (var obj in VisibleTargets.Values) obj.ObjMaint.RemoveObject(PhysicsObj, false); foreach (var obj in RetaliateTargets.Values) obj.ObjMaint.RemoveObject(PhysicsObj, false); RemoveAllObjects(); ServerObjectManager.RemoveServerObject(PhysicsObj); } finally { rwLock.ExitWriteLock(); } } } }