Client LuaCsForBarotrauma
PrefabCollection.cs
1 #nullable enable
3 using System;
4 using System.Collections;
5 using System.Collections.Concurrent;
6 using System.Collections.Generic;
7 using System.Diagnostics.CodeAnalysis;
8 using System.Linq;
9 using System.Security.Cryptography;
10 
11 namespace Barotrauma
12 {
13  public class PrefabCollection<T> : IEnumerable<T> where T : notnull, Prefab
14  {
19  {
20  var interfaces = typeof(T).GetInterfaces();
21  implementsVariants = interfaces.Any(i => i.Name.Contains(nameof(IImplementsVariants<T>)));
22  }
23 
28  Action<T, bool>? onAdd,
29  Action<T>? onRemove,
30  Action? onSort,
31  Action<ContentFile>? onAddOverrideFile,
32  Action<ContentFile>? onRemoveOverrideFile) : this()
33  {
34  OnAdd = onAdd;
35  OnRemove = onRemove;
36  OnSort = onSort;
37  OnAddOverrideFile = onAddOverrideFile;
38  OnRemoveOverrideFile = onRemoveOverrideFile;
39  }
40 
44  public PrefabCollection(Action? onSort) : this()
45  {
46  OnSort = onSort;
47  }
48 
53  private readonly Action<T, bool>? OnAdd = null;
54 
60  private readonly Action<T>? OnRemove = null;
61 
65  private readonly Action? OnSort = null;
66 
70  private readonly Action<ContentFile>? OnAddOverrideFile = null;
71 
75  private readonly Action<ContentFile>? OnRemoveOverrideFile = null;
76 
87 #if DEBUG && MODBREAKER
88  private readonly CursedDictionary<Identifier, PrefabSelector<T>> prefabs = new CursedDictionary<Identifier, PrefabSelector<T>>();
89 #else
90  private readonly ConcurrentDictionary<Identifier, PrefabSelector<T>> prefabs = new ConcurrentDictionary<Identifier, PrefabSelector<T>>();
91 #endif
92 
97  private readonly HashSet<ContentFile> overrideFiles = new HashSet<ContentFile>();
98  private ContentFile? topMostOverrideFile = null;
99 
100  private readonly bool implementsVariants;
101 
102  private bool IsPrefabOverriddenByFile(T prefab)
103  {
104  return topMostOverrideFile != null &&
105  topMostOverrideFile.ContentPackage.Index > prefab.ContentFile.ContentPackage.Index;
106  }
107 
108  private class InheritanceTreeCollection
109  {
110  public class Node
111  {
112  public Node(Identifier identifier) { Identifier = identifier; }
113 
114  public readonly Identifier Identifier;
115  public Node? Parent = null;
116  public readonly HashSet<Node> Inheritors = new HashSet<Node>();
117  }
118 
119  private readonly PrefabCollection<T> prefabCollection;
120 
121  public InheritanceTreeCollection(PrefabCollection<T> collection) { prefabCollection = collection; }
122 
123  public readonly Dictionary<Identifier, Node> IdToNode = new Dictionary<Identifier, Node>();
124  public readonly HashSet<Node> RootNodes = new HashSet<Node>();
125 
126  public Node? AddNodeAndInheritors(Identifier id)
127  {
128  if (!prefabCollection.TryGet(id, out T? _, requireInheritanceValid: false)) { return null; }
129 
130  if (!IdToNode.TryGetValue(id, out var node))
131  {
132  node = new Node(id);
133  RootNodes.Add(node);
134  IdToNode.Add(id, node);
135  }
136  else
137  {
138  //if the node already exists, it already contains
139  //all inheritors so let's just return this immediately
140  return node;
141  }
142 
143  var enumerator = prefabCollection.GetEnumerator(requireInheritanceValid: false);
144  while (enumerator.MoveNext())
145  {
146  T p = enumerator.Current;
147  if (p is not IImplementsVariants<T> implementsVariants || implementsVariants.VariantOf != id)
148  {
149  continue;
150  }
151  var inheritorNode = AddNodeAndInheritors(p.Identifier);
152  if (inheritorNode is null) { continue; }
153  RootNodes.Remove(inheritorNode);
154  inheritorNode.Parent = node;
155  node.Inheritors.Add(inheritorNode);
156  }
157  return node;
158  }
159 
160  private static void FindCycles(in Node node, HashSet<Node> uncheckedNodes)
161  {
162  HashSet<Node> checkedNodes = new HashSet<Node>();
163  List<Node> hierarchyPositions = new List<Node>();
164  Node? currNode = node;
165  do
166  {
167  if (!uncheckedNodes.Contains(currNode)) { break; }
168  if (checkedNodes.Contains(currNode))
169  {
170  int index = hierarchyPositions.IndexOf(currNode);
171  throw new Exception("Inheritance cycle detected: "
172  +string.Join(", ", hierarchyPositions.Skip(index).Select(n => n.Identifier)));
173  }
174  checkedNodes.Add(currNode);
175  hierarchyPositions.Add(currNode);
176  currNode = currNode.Parent;
177  } while (currNode != null);
178  uncheckedNodes.RemoveWhere(i => checkedNodes.Contains(i));
179  }
180 
181  public void AddNodesAndInheritors(IEnumerable<Identifier> ids)
182  => ids.ForEach(id => AddNodeAndInheritors(id));
183 
184  public void InvokeCallbacks()
185  {
186  HashSet<Node> uncheckedNodes = IdToNode.Values.ToHashSet();
187  IdToNode.Values.ForEach(v => PrefabCollection<T>.InheritanceTreeCollection.FindCycles(v, uncheckedNodes));
188  void invokeCallbacksForNode(Node node)
189  {
190  if (!prefabCollection.TryGet(node.Identifier, out var p, requireInheritanceValid: false) ||
191  p is not IImplementsVariants<T> prefab) { return; }
192  if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent, requireInheritanceValid: false))
193  {
194  prefab.InheritFrom(parent);
195  prefab.ParentPrefab = parent;
196  }
197  node.Inheritors.ForEach(invokeCallbacksForNode);
198  }
199  RootNodes.ForEach(invokeCallbacksForNode);
200  }
201  }
202 
203  private static bool IsInheritanceValid(T? prefab)
204  {
205  if (prefab == null) { return false; }
206  return
207  prefab is not IImplementsVariants<T> implementsVariants ||
208  (implementsVariants.VariantOf.IsEmpty || (implementsVariants.ParentPrefab != null && IsInheritanceValid(implementsVariants.ParentPrefab)));
209  }
210 
211  private void HandleInheritance(Identifier prefabIdentifier)
212  => HandleInheritance(prefabIdentifier.ToEnumerable());
213 
214  private void HandleInheritance(IEnumerable<Identifier> identifiers)
215  {
216  if (!implementsVariants) { return; }
217  foreach (var id in identifiers)
218  {
219  if (!TryGet(id, out T? prefab, requireInheritanceValid: false)) { continue; }
220  if (prefab is IImplementsVariants<T> implementsVariants && !implementsVariants.VariantOf.IsEmpty)
221  {
222  //reset parent prefab, it'll get set in InvokeCallbacks if the inheritance is valid
223  implementsVariants.ParentPrefab = null;
224  }
225  }
226  InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this);
227  inheritanceTreeCollection.AddNodesAndInheritors(identifiers);
228  inheritanceTreeCollection.InvokeCallbacks();
229  }
230 
234  public IEnumerable<KeyValuePair<Identifier, PrefabSelector<T>>> AllPrefabs
235  {
236  get
237  {
238  foreach (var kvp in prefabs)
239  {
240  var prefab = kvp.Value.ActivePrefab;
241  if (!IsInheritanceValid(prefab)) { continue; }
242  yield return kvp;
243  }
244  }
245  }
246 
252  public T this[Identifier identifier]
253  {
254  get
255  {
257  var prefab = prefabs[identifier].ActivePrefab;
258  if (prefab != null && !IsPrefabOverriddenByFile(prefab) &&
259  IsInheritanceValid(prefab))
260  {
261  return prefab;
262  }
263  throw new IndexOutOfRangeException($"Prefab of identifier \"{identifier}\" cannot be returned because it was overridden by \"{topMostOverrideFile!.Path}\"");
264  }
265  }
266 
267  public T this[string identifier]
268  {
269  get
270  {
271  //this exists because I don't want implicit
272  //string to Identifier conversion for the most
273  //part, but it's useful and fairly safe to do
274  //in this particular instance
275  return this[identifier.ToIdentifier()];
276  }
277  }
278 
285  public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result)
286  {
287  return TryGet(identifier, out result, requireInheritanceValid: true);
288  }
289 
290  private bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result, bool requireInheritanceValid)
291  {
293  if (prefabs.TryGetValue(identifier, out PrefabSelector<T>? selector) && selector.ActivePrefab != null)
294  {
295  result = selector!.ActivePrefab;
296  return !requireInheritanceValid || IsInheritanceValid(result);
297  }
298  else
299  {
300  result = null;
301  return false;
302  }
303  }
304 
305  public bool TryGet(string identifier, out T? result)
306  => TryGet(identifier.ToIdentifier(), out result);
307 
308  public IEnumerable<Identifier> Keys => prefabs.Keys;
309 
316  public T? Find(Predicate<T> predicate)
317  {
319  foreach (var kpv in prefabs)
320  {
321  if (kpv.Value.ActivePrefab is T p && predicate(p))
322  {
323  return p;
324  }
325  }
326  return null;
327  }
328 
334  public bool ContainsKey(Identifier identifier)
335  {
337  return TryGet(identifier, out _);
338  }
339 
340  public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier());
341 
347  public bool IsOverride(T prefab)
348  {
350  if (ContainsKey(prefab.Identifier))
351  {
352  return prefabs[prefab.Identifier].IsOverride(prefab);
353  }
354  return false;
355  }
356 
366  public void Add(T prefab, bool isOverride)
367  {
369  if (prefab.Identifier.IsEmpty)
370  {
371  throw new ArgumentException($"Prefab has no identifier!");
372  }
373 
374  bool selectorExists = prefabs.TryGetValue(prefab.Identifier, out PrefabSelector<T>? selector);
375 
376  //Add to list
377  selector ??= new PrefabSelector<T>();
378 
379  if (prefab is PrefabWithUintIdentifier prefabWithUintIdentifier)
380  {
381  if (!selector.IsEmpty)
382  {
383  prefabWithUintIdentifier.UintIdentifier = (selector.ActivePrefab as PrefabWithUintIdentifier)!.UintIdentifier;
384  }
385  else
386  {
387  using (MD5 md5 = MD5.Create())
388  {
389  prefabWithUintIdentifier.UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(prefab.Identifier, md5);
390 
391  //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
392  T? findCollision()
393  => Find(p =>
394  p.Identifier != prefab.Identifier
395  && p is PrefabWithUintIdentifier otherPrefab
396  && otherPrefab.UintIdentifier == prefabWithUintIdentifier.UintIdentifier);
397  for (T? collision = findCollision(); collision != null; collision = findCollision())
398  {
399  DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})");
400  prefabWithUintIdentifier.UintIdentifier++;
401  }
402  }
403  }
404  }
405  selector.Add(prefab, isOverride);
406 
407  if (!selectorExists)
408  {
409  if (!prefabs.TryAdd(prefab.Identifier, selector)) { throw new Exception($"Failed to add selector for \"{prefab.Identifier}\""); }
410  }
411  OnAdd?.Invoke(prefab, isOverride);
412  HandleInheritance(prefab.Identifier);
413  }
414 
419  public void Remove(T prefab)
420  {
422  OnRemove?.Invoke(prefab);
423  if (!ContainsKey(prefab.Identifier)) { return; }
424  if (!prefabs[prefab.Identifier].Contains(prefab)) { return; }
425  prefabs[prefab.Identifier].Remove(prefab);
426 
427  if (prefabs[prefab.Identifier].IsEmpty)
428  {
429  prefabs.TryRemove(prefab.Identifier, out _);
430  }
431  HandleInheritance(prefab.Identifier);
432  }
433 
437  public void RemoveByFile(ContentFile file)
438  {
440  HashSet<Identifier> clearedIdentifiers = new HashSet<Identifier>();
441  foreach (var kpv in prefabs)
442  {
443  kpv.Value.RemoveByFile(file, OnRemove);
444  if (kpv.Value.IsEmpty) { clearedIdentifiers.Add(kpv.Key); }
445  }
446 
447  foreach (var identifier in clearedIdentifiers)
448  {
449  prefabs.TryRemove(identifier, out _);
450  }
451  RemoveOverrideFile(file);
452  }
453 
457  public void AddOverrideFile(ContentFile file)
458  {
460  if (!overrideFiles.Contains(file))
461  {
462  overrideFiles.Add(file);
463  }
464  OnAddOverrideFile?.Invoke(file);
465  }
466 
470  public void RemoveOverrideFile(ContentFile file)
471  {
473  if (overrideFiles.Contains(file))
474  {
475  overrideFiles.Remove(file);
476  }
477  OnRemoveOverrideFile?.Invoke(file);
478  }
479 
483  public void SortAll()
484  {
486  foreach (var kvp in prefabs)
487  {
488  kvp.Value.Sort();
489  }
490  topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null;
491  OnSort?.Invoke();
492  HandleInheritance(this.Select(p => p.Identifier));
493 
494  var enumerator = GetEnumerator(requireInheritanceValid: false);
495  while (enumerator.MoveNext())
496  {
497  T p = enumerator.Current;
498  if (p is IImplementsVariants<T> implementsVariants && !IsInheritanceValid(p))
499  {
500  DebugConsole.ThrowError(
501  $"Error in content package \"{p.ContentFile.ContentPackage.Name}\": " +
502  $"could not find the prefab \"{implementsVariants.VariantOf}\" the prefab \"{p.Identifier}\" is configured as a variant of.");
503  continue;
504  }
505  }
506  }
507 
512  public IEnumerator<T> GetEnumerator()
513  {
514  return GetEnumerator(requireInheritanceValid: true);
515  }
516 
517  private IEnumerator<T> GetEnumerator(bool requireInheritanceValid)
518  {
520  foreach (var kvp in prefabs)
521  {
522  var prefab = kvp.Value.ActivePrefab;
523  if (prefab == null || IsPrefabOverriddenByFile(prefab)) { continue; }
524  if (requireInheritanceValid && !IsInheritanceValid(prefab)) { continue; }
525  yield return prefab;
526  }
527  }
528 
533  IEnumerator IEnumerable.GetEnumerator()
534  {
535  return GetEnumerator(requireInheritanceValid: true);
536  }
537  }
538 }
Base class for content file types, which are loaded from filelist.xml via reflection....
Definition: ContentFile.cs:23
readonly ContentPackage ContentPackage
Definition: ContentFile.cs:136
void Add(T prefab, bool isOverride)
Add a prefab to the collection. If not marked as an override, fail if a prefab with the same identifi...
PrefabCollection(Action< T, bool >? onAdd, Action< T >? onRemove, Action? onSort, Action< ContentFile >? onAddOverrideFile, Action< ContentFile >? onRemoveOverrideFile)
Constructor with OnAdd and OnRemove callbacks provided.
IEnumerable< KeyValuePair< Identifier, PrefabSelector< T > > > AllPrefabs
AllPrefabs exposes all prefabs instead of just the active ones.
T? Find(Predicate< T > predicate)
Finds the first active prefab that returns true given the predicate, or null if no such prefab is fou...
void SortAll()
Sorts all prefabs in the collection based on the content package load order.
IEnumerator< T > GetEnumerator()
GetEnumerator implementation to enable foreach
bool ContainsKey(string k)
PrefabCollection(Action? onSort)
Constructor with only the OnSort callback provided.
void AddOverrideFile(ContentFile file)
Adds an override file to the collection.
bool IsOverride(T prefab)
Determines whether a prefab is implemented as an override or not.
IEnumerable< Identifier > Keys
void RemoveOverrideFile(ContentFile file)
Removes an override file from the collection.
void Remove(T prefab)
Removes a prefab from the collection.
bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result)
Returns true if a prefab with the identifier exists, false otherwise.
PrefabCollection()
Default constructor.
bool ContainsKey(Identifier identifier)
Returns true if a prefab with the given identifier exists, false otherwise.
bool TryGet(string identifier, out T? result)
void RemoveByFile(ContentFile file)
Removes all prefabs that were loaded from a certain file.
static void DisallowCallFromConstructor()
Definition: Prefab.cs:19
Prefab that has a property serves as a deterministic hash of a prefab's identifier....