using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; using Barotrauma.Networking; namespace Barotrauma { abstract class Entity : ISpatialEntity { public const ushort NullEntityID = 0; public const ushort EntitySpawnerID = ushort.MaxValue; public const ushort RespawnManagerID = ushort.MaxValue - 1; public const ushort DummyID = ushort.MaxValue - 2; public const ushort ReservedIDStart = ushort.MaxValue - 3; public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values private static readonly Dictionary dictionary = new Dictionary(); public static IReadOnlyCollection GetEntities() { return dictionary.Values; } public static int EntityCount => dictionary.Count; public static EntitySpawner Spawner; protected AITarget aiTarget; public bool Removed { get; private set; } public bool IdFreed { get; private set; } /// /// Unique, but non-persistent identifier. /// Stays the same if the entities are created in the exactly same order, but doesn't persist e.g. between the rounds. /// public readonly ushort ID; public virtual Vector2 SimPosition => Vector2.Zero; public virtual Vector2 Position => Vector2.Zero; public virtual Vector2 WorldPosition => Submarine == null ? Position : Submarine.Position + Position; public virtual Vector2 DrawPosition => Submarine == null ? Position : Submarine.DrawPosition + Position; public Submarine Submarine { get; set; } public AITarget AiTarget => aiTarget; /// /// Indetectable characters can't be spotted by AIs and aren't visible on the sonar or health scanner. /// public bool InDetectable { get { if (aiTarget != null) { return aiTarget.InDetectable; } return false; } set { if (aiTarget != null) { aiTarget.InDetectable = value; } } } public double SpawnTime => spawnTime; private readonly double spawnTime; private static UInt64 creationCounter = 0; private readonly static object creationCounterMutex = new object(); public readonly string CreationStackTrace; public readonly UInt64 CreationIndex; public string ErrorLine => $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}"; public Entity(Submarine submarine, ushort id) { this.Submarine = submarine; spawnTime = Timing.TotalTime; if (dictionary.Count >= MaxEntityCount) { Dictionary entityCounts = new Dictionary(); foreach (var entity in dictionary) { if (entity.Value is MapEntity me) { if (entityCounts.ContainsKey(me.Prefab.Identifier)) { entityCounts[me.Prefab.Identifier]++; } else { entityCounts[me.Prefab.Identifier] = 1; } } } string errorMsg = $"Maximum amount of entities ({MaxEntityCount}) exceeded! Largest numbers of entities: " + string.Join(", ", entityCounts.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}: {kvp.Value}")); throw new Exception(errorMsg); } //give a unique ID ID = DetermineID(id, submarine); if (dictionary.ContainsKey(ID)) { throw new Exception($"ID {ID} is taken by {dictionary[ID]}"); } dictionary.Add(ID, this); CreationStackTrace = ""; #if DEBUG var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true); var frames = st.GetFrames(); int frameCount = 0; foreach (var frame in frames) { string fileName = frame.GetFileName(); if ((fileName?.Contains("BarotraumaClient") ?? false) || (fileName?.Contains("BarotraumaServer") ?? false)) { break; } fileName = Path.GetFileNameWithoutExtension(fileName); int fileLineNumber = frame.GetFileLineNumber(); if (fileName.IsNullOrEmpty() || fileLineNumber <= 0) { continue; } CreationStackTrace += $"{fileName}@{fileLineNumber}; "; } #endif #warning TODO: consider removing this mutex, entity creation probably shouldn't be multithreaded lock (creationCounterMutex) { CreationIndex = creationCounter; creationCounter++; } } protected virtual ushort DetermineID(ushort id, Submarine submarine) { return id != NullEntityID ? id : FindFreeId(submarine == null ? (ushort)1 : submarine.IdOffset); } private static ushort FindFreeId(ushort idOffset) { if (dictionary.Count >= MaxEntityCount) { throw new Exception($"Maximum amount of entities ({MaxEntityCount}) reached!"); } ushort id = idOffset; while (id < ReservedIDStart) { if (!dictionary.ContainsKey(id)) { break; } id++; }; return id; } /// /// Finds a contiguous block of free IDs of at least the given size /// /// The first ID in the found block, or zero if none are found public static int FindFreeIdBlock(int minBlockSize) { int currentBlockSize = 0; for (int i = 1; i < ReservedIDStart; i++) { if (dictionary.ContainsKey((ushort)i)) { currentBlockSize = 0; } else { currentBlockSize++; if (currentBlockSize >= minBlockSize) { return i - (currentBlockSize-1); } } } return 0; } /// /// Find an entity based on the ID /// public static Entity FindEntityByID(ushort ID) { Entity matchingEntity; dictionary.TryGetValue(ID, out matchingEntity); return matchingEntity; } public static void RemoveAll() { List list = new List(dictionary.Values); foreach (Entity e in list) { try { e.Remove(); } catch (Exception exception) { DebugConsole.ThrowError($"Error while removing entity \"{e}\"", exception); GameAnalyticsManager.AddErrorEventOnce( $"Entity.RemoveAll:Exception{e}", GameAnalyticsManager.ErrorSeverity.Error, $"Error while removing entity \"{e} ({exception.Message})\n{exception.StackTrace.CleanupStackTrace()}"); } } StringBuilder errorMsg = new StringBuilder(); if (dictionary.Count > 0) { errorMsg.AppendLine("Some entities were not removed in Entity.RemoveAll:"); foreach (Entity e in dictionary.Values) { errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.ID + ")"); } } if (Item.ItemList.Count > 0) { errorMsg.AppendLine("Some items were not removed in Entity.RemoveAll:"); foreach (Item item in Item.ItemList) { errorMsg.AppendLine(" - " + item.Name + "(ID " + item.ID + ")"); } var items = new List(Item.ItemList); foreach (Item item in items) { try { item.Remove(); } catch (Exception exception) { DebugConsole.ThrowError($"Error while removing item \"{item}\"", exception); } } Item.ItemList.Clear(); } if (Character.CharacterList.Count > 0) { errorMsg.AppendLine("Some characters were not removed in Entity.RemoveAll:"); foreach (Character character in Character.CharacterList) { errorMsg.AppendLine(" - " + character.Name + "(ID " + character.ID + ")"); } var characters = new List(Character.CharacterList); foreach (Character character in characters) { try { character.Remove(); } catch (Exception exception) { DebugConsole.ThrowError($"Error while removing character \"{character}\"", exception); } } Character.CharacterList.Clear(); } if (!string.IsNullOrEmpty(errorMsg.ToString())) { foreach (string errorLine in errorMsg.ToString().Split('\n')) { DebugConsole.ThrowError(errorLine); } GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.ToString()); } dictionary.Clear(); Hull.EntityGrids.Clear(); Spawner?.Reset(); Items.Components.Projectile.ResetSpreadCounter(); } /// /// Removes the entity from the entity dictionary and frees up the ID it was using. /// public void FreeID() { if (IdFreed) { return; } DebugConsole.Log($"Removing entity {ToString()} ({ID}) from entity dictionary."); if (!dictionary.TryGetValue(ID, out Entity existingEntity)) { DebugConsole.ThrowError($"Entity {ToString()} ({ID}) not present in entity dictionary."); GameAnalyticsManager.AddErrorEventOnce( $"Entity.FreeID:EntityNotFound{ID}", GameAnalyticsManager.ErrorSeverity.Error, $"Entity {ToString()} ({ID}) not present in entity dictionary.\n{Environment.StackTrace.CleanupStackTrace()}"); } else if (existingEntity != this) { DebugConsole.ThrowError($"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})"); GameAnalyticsManager.AddErrorEventOnce("Entity.FreeID:EntityMismatch" + ID, GameAnalyticsManager.ErrorSeverity.Error, $"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})"); } else { dictionary.Remove(ID); } IdFreed = true; } public virtual void Remove() { FreeID(); Removed = true; } public static void DumpIds(int count, string filename) { List entities = dictionary.Values.OrderByDescending(e => e.ID).ToList(); count = Math.Min(entities.Count, count); List lines = new List(); for (int i = 0; i < count; i++) { lines.Add($"{entities[i].ID}: {entities[i]}"); DebugConsole.ThrowError($"{entities[i].ID}: {entities[i]}"); } if (!string.IsNullOrWhiteSpace(filename)) { File.WriteAllLines(filename, lines); } } } }