Client LuaCsForBarotrauma
Entity.cs
1 using Microsoft.Xna.Framework;
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using Barotrauma.IO;
6 using System.Linq;
7 using System.Text;
9 
10 namespace Barotrauma
11 {
12  abstract class Entity : ISpatialEntity
13  {
14  public const ushort NullEntityID = 0;
15  public const ushort EntitySpawnerID = ushort.MaxValue;
16  public const ushort RespawnManagerID = ushort.MaxValue - 1;
17  public const ushort DummyID = ushort.MaxValue - 2;
18 
19  public const ushort ReservedIDStart = ushort.MaxValue - 3;
20 
21  public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values
22 
23  private static readonly Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
24  public static IReadOnlyCollection<Entity> GetEntities()
25  {
26  return dictionary.Values;
27  }
28 
29  public static int EntityCount => dictionary.Count;
30 
31  public static EntitySpawner Spawner;
32 
33  protected AITarget aiTarget;
34 
35  public bool Removed { get; private set; }
36 
37  public bool IdFreed { get; private set; }
38 
43  public readonly ushort ID;
44 
45  public virtual Vector2 SimPosition => Vector2.Zero;
46 
47  public virtual Vector2 Position => Vector2.Zero;
48 
49  public virtual Vector2 WorldPosition => Submarine == null ? Position : Submarine.Position + Position;
50 
51  public virtual Vector2 DrawPosition => Submarine == null ? Position : Submarine.DrawPosition + Position;
52 
53  public Submarine Submarine { get; set; }
54 
56 
60  public bool InDetectable
61  {
62  get
63  {
64  if (aiTarget != null)
65  {
66  return aiTarget.InDetectable;
67  }
68  return false;
69  }
70  set
71  {
72  if (aiTarget != null)
73  {
74  aiTarget.InDetectable = value;
75  }
76  }
77  }
78 
79  public double SpawnTime => spawnTime;
80  private readonly double spawnTime;
81 
82  private static UInt64 creationCounter = 0;
83  private readonly static object creationCounterMutex = new object();
84 
85  public readonly string CreationStackTrace;
86  public readonly UInt64 CreationIndex;
87  public string ErrorLine
88  => $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}";
89 
90  public Entity(Submarine submarine, ushort id)
91  {
92  this.Submarine = submarine;
93  spawnTime = Timing.TotalTime;
94 
95  if (dictionary.Count >= MaxEntityCount)
96  {
97  Dictionary<Identifier, int> entityCounts = new Dictionary<Identifier, int>();
98  foreach (var entity in dictionary)
99  {
100  if (entity.Value is MapEntity me)
101  {
102  if (entityCounts.ContainsKey(me.Prefab.Identifier))
103  {
104  entityCounts[me.Prefab.Identifier]++;
105  }
106  else
107  {
108  entityCounts[me.Prefab.Identifier] = 1;
109  }
110  }
111  }
112  string errorMsg = $"Maximum amount of entities ({MaxEntityCount}) exceeded! Largest numbers of entities: " +
113  string.Join(", ", entityCounts.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}: {kvp.Value}"));
114  throw new Exception(errorMsg);
115  }
116 
117  //give a unique ID
118  ID = DetermineID(id, submarine);
119 
120  if (dictionary.ContainsKey(ID))
121  {
122  throw new Exception($"ID {ID} is taken by {dictionary[ID]}");
123  }
124 
125  dictionary.Add(ID, this);
126 
127  CreationStackTrace = "";
128 #if DEBUG
129  var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
130  var frames = st.GetFrames();
131  int frameCount = 0;
132  foreach (var frame in frames)
133  {
134  string fileName = frame.GetFileName();
135  if ((fileName?.Contains("BarotraumaClient") ?? false) || (fileName?.Contains("BarotraumaServer") ?? false)) { break; }
136 
137  fileName = Path.GetFileNameWithoutExtension(fileName);
138  int fileLineNumber = frame.GetFileLineNumber();
139 
140  if (fileName.IsNullOrEmpty() || fileLineNumber <= 0) { continue; }
141 
142  CreationStackTrace += $"{fileName}@{fileLineNumber}; ";
143  }
144 #endif
145  #warning TODO: consider removing this mutex, entity creation probably shouldn't be multithreaded
146  lock (creationCounterMutex)
147  {
148  CreationIndex = creationCounter;
149  creationCounter++;
150  }
151  }
152 
153  protected virtual ushort DetermineID(ushort id, Submarine submarine)
154  {
155  return id != NullEntityID
156  ? id
157  : FindFreeId(submarine == null ? (ushort)1 : submarine.IdOffset);
158  }
159 
160  private static ushort FindFreeId(ushort idOffset)
161  {
162  if (dictionary.Count >= MaxEntityCount)
163  {
164  throw new Exception($"Maximum amount of entities ({MaxEntityCount}) reached!");
165  }
166 
167  ushort id = idOffset;
168  while (id < ReservedIDStart)
169  {
170  if (!dictionary.ContainsKey(id)) { break; }
171  id++;
172  };
173  return id;
174  }
175 
180  public static int FindFreeIdBlock(int minBlockSize)
181  {
182  int currentBlockSize = 0;
183  for (int i = 1; i < ReservedIDStart; i++)
184  {
185  if (dictionary.ContainsKey((ushort)i))
186  {
187  currentBlockSize = 0;
188  }
189  else
190  {
191  currentBlockSize++;
192  if (currentBlockSize >= minBlockSize)
193  {
194  return i - (currentBlockSize-1);
195  }
196  }
197  }
198  return 0;
199  }
200 
204  public static Entity FindEntityByID(ushort ID)
205  {
206  Entity matchingEntity;
207  dictionary.TryGetValue(ID, out matchingEntity);
208 
209  return matchingEntity;
210  }
211 
212  public static void RemoveAll()
213  {
214  List<Entity> list = new List<Entity>(dictionary.Values);
215  foreach (Entity e in list)
216  {
217  try
218  {
219  e.Remove();
220  }
221  catch (Exception exception)
222  {
223  DebugConsole.ThrowError($"Error while removing entity \"{e}\"", exception);
224  GameAnalyticsManager.AddErrorEventOnce(
225  $"Entity.RemoveAll:Exception{e}",
226  GameAnalyticsManager.ErrorSeverity.Error,
227  $"Error while removing entity \"{e} ({exception.Message})\n{exception.StackTrace.CleanupStackTrace()}");
228  }
229  }
230  StringBuilder errorMsg = new StringBuilder();
231  if (dictionary.Count > 0)
232  {
233  errorMsg.AppendLine("Some entities were not removed in Entity.RemoveAll:");
234  foreach (Entity e in dictionary.Values)
235  {
236  errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.ID + ")");
237  }
238  }
239  if (Item.ItemList.Count > 0)
240  {
241  errorMsg.AppendLine("Some items were not removed in Entity.RemoveAll:");
242  foreach (Item item in Item.ItemList)
243  {
244  errorMsg.AppendLine(" - " + item.Name + "(ID " + item.ID + ")");
245  }
246 
247  var items = new List<Item>(Item.ItemList);
248  foreach (Item item in items)
249  {
250  try
251  {
252  item.Remove();
253  }
254  catch (Exception exception)
255  {
256  DebugConsole.ThrowError($"Error while removing item \"{item}\"", exception);
257  }
258  }
259  Item.ItemList.Clear();
260  }
261  if (Character.CharacterList.Count > 0)
262  {
263  errorMsg.AppendLine("Some characters were not removed in Entity.RemoveAll:");
264  foreach (Character character in Character.CharacterList)
265  {
266  errorMsg.AppendLine(" - " + character.Name + "(ID " + character.ID + ")");
267  }
268 
269  var characters = new List<Character>(Character.CharacterList);
270  foreach (Character character in characters)
271  {
272  try
273  {
274  character.Remove();
275  }
276  catch (Exception exception)
277  {
278  DebugConsole.ThrowError($"Error while removing character \"{character}\"", exception);
279  }
280  }
281  Character.CharacterList.Clear();
282  }
283 
284  if (!string.IsNullOrEmpty(errorMsg.ToString()))
285  {
286  foreach (string errorLine in errorMsg.ToString().Split('\n'))
287  {
288  DebugConsole.ThrowError(errorLine);
289  }
290  GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.ToString());
291  }
292 
293  dictionary.Clear();
294  Hull.EntityGrids.Clear();
295  Spawner?.Reset();
296  Items.Components.Projectile.ResetSpreadCounter();
297  }
298 
302  public void FreeID()
303  {
304  if (IdFreed) { return; }
305  DebugConsole.Log($"Removing entity {ToString()} ({ID}) from entity dictionary.");
306  if (!dictionary.TryGetValue(ID, out Entity existingEntity))
307  {
308  DebugConsole.ThrowError($"Entity {ToString()} ({ID}) not present in entity dictionary.");
309  GameAnalyticsManager.AddErrorEventOnce(
310  $"Entity.FreeID:EntityNotFound{ID}",
311  GameAnalyticsManager.ErrorSeverity.Error,
312  $"Entity {ToString()} ({ID}) not present in entity dictionary.\n{Environment.StackTrace.CleanupStackTrace()}");
313  }
314  else if (existingEntity != this)
315  {
316  DebugConsole.ThrowError($"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})");
317  GameAnalyticsManager.AddErrorEventOnce("Entity.FreeID:EntityMismatch" + ID,
318  GameAnalyticsManager.ErrorSeverity.Error,
319  $"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})");
320  }
321  else
322  {
323  dictionary.Remove(ID);
324  }
325  IdFreed = true;
326  }
327 
328  public virtual void Remove()
329  {
330  FreeID();
331  Removed = true;
332  }
333 
334  public static void DumpIds(int count, string filename)
335  {
336  List<Entity> entities = dictionary.Values.OrderByDescending(e => e.ID).ToList();
337 
338  count = Math.Min(entities.Count, count);
339 
340  List<string> lines = new List<string>();
341  for (int i = 0; i < count; i++)
342  {
343  lines.Add($"{entities[i].ID}: {entities[i]}");
344  DebugConsole.ThrowError($"{entities[i].ID}: {entities[i]}");
345  }
346 
347  if (!string.IsNullOrWhiteSpace(filename))
348  {
349  File.WriteAllLines(filename, lines);
350  }
351  }
352  }
353 }
bool InDetectable
Should be reset to false each frame and kept indetectable by e.g. a status effect.
const ushort DummyID
Definition: Entity.cs:17
const ushort ReservedIDStart
Definition: Entity.cs:19
virtual void Remove()
Definition: Entity.cs:328
AITarget AiTarget
Definition: Entity.cs:55
static EntitySpawner Spawner
Definition: Entity.cs:31
virtual Vector2 WorldPosition
Definition: Entity.cs:49
virtual Vector2 DrawPosition
Definition: Entity.cs:51
Entity(Submarine submarine, ushort id)
Definition: Entity.cs:90
static int FindFreeIdBlock(int minBlockSize)
Finds a contiguous block of free IDs of at least the given size
Definition: Entity.cs:180
static void DumpIds(int count, string filename)
Definition: Entity.cs:334
static void RemoveAll()
Definition: Entity.cs:212
const ushort RespawnManagerID
Definition: Entity.cs:16
const ushort EntitySpawnerID
Definition: Entity.cs:15
static IReadOnlyCollection< Entity > GetEntities()
Definition: Entity.cs:24
virtual Vector2 SimPosition
Definition: Entity.cs:45
static int EntityCount
Definition: Entity.cs:29
virtual Vector2 Position
Definition: Entity.cs:47
const ushort NullEntityID
Definition: Entity.cs:14
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
void FreeID()
Removes the entity from the entity dictionary and frees up the ID it was using.
Definition: Entity.cs:302
const ushort MaxEntityCount
Definition: Entity.cs:21
readonly string CreationStackTrace
Definition: Entity.cs:85
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
AITarget aiTarget
Definition: Entity.cs:33
string ErrorLine
Definition: Entity.cs:88
bool InDetectable
Indetectable characters can't be spotted by AIs and aren't visible on the sonar or health scanner.
Definition: Entity.cs:61
readonly UInt64 CreationIndex
Definition: Entity.cs:86
virtual ushort DetermineID(ushort id, Submarine submarine)
Definition: Entity.cs:153
double SpawnTime
Definition: Entity.cs:79
static readonly List< EntityGrid > EntityGrids
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
static readonly List< Item > ItemList