Unstable 0.17.0.0
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public interface IImplementsVariants<T> where T : Prefab
|
||||
{
|
||||
public Identifier VariantOf { get; }
|
||||
|
||||
public void InheritFrom(T parent);
|
||||
}
|
||||
|
||||
public static class VariantExtensions
|
||||
{
|
||||
public static ContentXElement CreateVariantXML(this ContentXElement variantElement, ContentXElement baseElement)
|
||||
{
|
||||
#warning TODO: fix %ModDir% instances in the base element such that they become %ModDir:BaseMod% if necessary
|
||||
return variantElement.Element.CreateVariantXML(baseElement.Element).FromPackage(variantElement.ContentPackage);
|
||||
}
|
||||
|
||||
public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement)
|
||||
{
|
||||
XElement newElement = new XElement(variantElement.Name);
|
||||
newElement.Add(baseElement.Attributes());
|
||||
newElement.Add(baseElement.Elements());
|
||||
|
||||
ReplaceElement(newElement, variantElement);
|
||||
|
||||
void ReplaceElement(XElement element, XElement replacement)
|
||||
{
|
||||
List<XElement> elementsToRemove = new List<XElement>();
|
||||
foreach (XAttribute attribute in replacement.Attributes())
|
||||
{
|
||||
ReplaceAttribute(element, attribute);
|
||||
}
|
||||
foreach (XElement replacementSubElement in replacement.Elements())
|
||||
{
|
||||
int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement);
|
||||
System.Diagnostics.Debug.Assert(index > -1);
|
||||
|
||||
int i = 0;
|
||||
bool matchingElementFound = false;
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
if (i == index)
|
||||
{
|
||||
if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements)
|
||||
{
|
||||
//if the replacement is empty (no attributes or child elements)
|
||||
//remove the element from the variant
|
||||
elementsToRemove.Add(subElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceElement(subElement, replacementSubElement);
|
||||
}
|
||||
matchingElementFound = true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (!matchingElementFound)
|
||||
{
|
||||
element.Add(replacementSubElement);
|
||||
}
|
||||
}
|
||||
elementsToRemove.ForEach(e => e.Remove());
|
||||
}
|
||||
|
||||
void ReplaceAttribute(XElement element, XAttribute newAttribute)
|
||||
{
|
||||
XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||
if (existingAttribute == null)
|
||||
{
|
||||
element.Add(newAttribute);
|
||||
return;
|
||||
}
|
||||
float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value);
|
||||
if (newAttribute.Value.StartsWith('*'))
|
||||
{
|
||||
string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1);
|
||||
float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier);
|
||||
if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.'))
|
||||
{
|
||||
existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingAttribute.Value = ((int)(value * multiplier)).ToString();
|
||||
}
|
||||
}
|
||||
else if (newAttribute.Value.StartsWith('+'))
|
||||
{
|
||||
string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1);
|
||||
float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition);
|
||||
if (additionStr.Contains('.') || existingAttribute.Value.Contains('.'))
|
||||
{
|
||||
existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingAttribute.Value = ((int)(value + addition)).ToString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
existingAttribute.Value = newAttribute.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return newElement;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public interface IPrefab
|
||||
{
|
||||
string OriginalName { get; }
|
||||
string Identifier { get; }
|
||||
string FilePath { get; }
|
||||
ContentPackage ContentPackage { get; }
|
||||
}
|
||||
|
||||
public interface IHasUintIdentifier
|
||||
{
|
||||
uint UIntIdentifier { get; set; }
|
||||
}
|
||||
|
||||
public static class PrefabExtensions
|
||||
{
|
||||
public static void CalculatePrefabUIntIdentifier<T>(this T prefab, PrefabCollection<T> prefabs) where T : class, IPrefab, IHasUintIdentifier, IDisposable
|
||||
{
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
|
||||
|
||||
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
|
||||
var collision = prefabs.Find(p => p.Identifier != prefab.Identifier && p.UIntIdentifier == prefab.UIntIdentifier);
|
||||
if (collision != null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})");
|
||||
collision.UIntIdentifier++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public abstract class Prefab : IDisposable
|
||||
{
|
||||
public readonly static ImmutableHashSet<Type> Types;
|
||||
static Prefab()
|
||||
{
|
||||
Types = ReflectionUtils.GetDerivedNonAbstract<Prefab>().ToImmutableHashSet();
|
||||
}
|
||||
|
||||
private static bool potentialCallFromConstructor = false;
|
||||
public static void DisallowCallFromConstructor()
|
||||
{
|
||||
if (!potentialCallFromConstructor) { return; }
|
||||
StackTrace st = new StackTrace(skipFrames: 2, fNeedFileInfo: false);
|
||||
for (int i = st.FrameCount-1; i >= 0; i--)
|
||||
{
|
||||
if (st.GetFrame(i)?.GetMethod() is {IsConstructor: true, DeclaringType: { } declaringType}
|
||||
&& Types.Contains(declaringType))
|
||||
{
|
||||
throw new Exception("Called disallowed method from within a prefab's constructor!");
|
||||
}
|
||||
}
|
||||
potentialCallFromConstructor = false;
|
||||
}
|
||||
|
||||
public readonly Identifier Identifier;
|
||||
public readonly ContentFile ContentFile;
|
||||
|
||||
public ContentPackage? ContentPackage => ContentFile?.ContentPackage;
|
||||
public ContentPath FilePath => ContentFile.Path;
|
||||
|
||||
public Prefab(ContentFile file, Identifier identifier)
|
||||
{
|
||||
potentialCallFromConstructor = true;
|
||||
ContentFile = file;
|
||||
Identifier = identifier;
|
||||
if (Identifier.IsEmpty) { throw new ArgumentException($"Error creating {GetType().Name}: Identifier cannot be empty"); }
|
||||
}
|
||||
|
||||
public Prefab(ContentFile file, ContentXElement element)
|
||||
{
|
||||
potentialCallFromConstructor = true;
|
||||
ContentFile = file;
|
||||
Identifier = DetermineIdentifier(element!);
|
||||
if (Identifier.IsEmpty) { throw new ArgumentException($"Error creating {GetType().Name}: Identifier cannot be empty"); }
|
||||
}
|
||||
|
||||
protected virtual Identifier DetermineIdentifier(XElement element)
|
||||
{
|
||||
return element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,70 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public class PrefabCollection<T> : IEnumerable<T> where T : class, IPrefab, IDisposable
|
||||
public class PrefabCollection<T> : IEnumerable<T> where T : notnull, Prefab
|
||||
{
|
||||
/// <summary>
|
||||
/// Default constructor.
|
||||
/// </summary>
|
||||
public PrefabCollection()
|
||||
{
|
||||
var interfaces = typeof(T).GetInterfaces();
|
||||
implementsVariants = interfaces.Any(i => i.Name.Contains(nameof(IImplementsVariants<T>)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with OnAdd and OnRemove callbacks provided.
|
||||
/// </summary>
|
||||
public PrefabCollection(
|
||||
Action<T, bool>? onAdd,
|
||||
Action<T>? onRemove,
|
||||
Action? onSort,
|
||||
Action<ContentFile>? onAddOverrideFile,
|
||||
Action<ContentFile>? onRemoveOverrideFile) : this()
|
||||
{
|
||||
OnAdd = onAdd;
|
||||
OnRemove = onRemove;
|
||||
OnSort = onSort;
|
||||
OnAddOverrideFile = onAddOverrideFile;
|
||||
OnRemoveOverrideFile = onRemoveOverrideFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to be called when calling Add(T prefab, bool override).
|
||||
/// If provided, the method is called only if Add succeeds.
|
||||
/// </summary>
|
||||
private readonly Action<T, bool>? OnAdd = null;
|
||||
|
||||
/// <summary>
|
||||
/// Method to be called when calling Remove(T prefab).
|
||||
/// If provided, the method is called before success
|
||||
/// or failure can be determined within the body of Remove.
|
||||
/// </summary>
|
||||
private readonly Action<T>? OnRemove = null;
|
||||
|
||||
/// <summary>
|
||||
/// Method to be called when calling SortAll().
|
||||
/// </summary>
|
||||
private readonly Action? OnSort = null;
|
||||
|
||||
/// <summary>
|
||||
/// Method to be called when calling AddOverrideFile(ContentFile file).
|
||||
/// </summary>
|
||||
private readonly Action<ContentFile>? OnAddOverrideFile = null;
|
||||
|
||||
/// <summary>
|
||||
/// Method to be called when calling RemoveOverrideFile(ContentFile file).
|
||||
/// </summary>
|
||||
private readonly Action<ContentFile>? OnRemoveOverrideFile = null;
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary containing all prefabs of the same type.
|
||||
/// Key is the identifier.
|
||||
@@ -18,12 +75,132 @@ namespace Barotrauma
|
||||
/// The last element of the list is the prefab that is effectively used
|
||||
/// (hereby called "active prefab")
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, List<T>> prefabs = new Dictionary<string, List<T>>();
|
||||
#if DEBUG && MODBREAKER
|
||||
private readonly CursedDictionary<Identifier, PrefabSelector<T>> prefabs = new CursedDictionary<Identifier, PrefabSelector<T>>();
|
||||
#else
|
||||
private readonly ConcurrentDictionary<Identifier, PrefabSelector<T>> prefabs = new ConcurrentDictionary<Identifier, PrefabSelector<T>>();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Collection of content files that override all previous prefabs
|
||||
/// i.e. anything set to load before these effectively doesn't exist
|
||||
/// </summary>
|
||||
private readonly HashSet<ContentFile> overrideFiles = new HashSet<ContentFile>();
|
||||
private ContentFile? topMostOverrideFile = null;
|
||||
|
||||
private readonly bool implementsVariants;
|
||||
|
||||
private bool IsPrefabOverriddenByFile(T prefab)
|
||||
{
|
||||
return topMostOverrideFile != null &&
|
||||
topMostOverrideFile.ContentPackage.Index > prefab.ContentFile.ContentPackage.Index;
|
||||
}
|
||||
|
||||
private class InheritanceTreeCollection
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public Node(Identifier identifier) { Identifier = identifier; }
|
||||
|
||||
public readonly Identifier Identifier;
|
||||
public Node? Parent = null;
|
||||
public readonly HashSet<Node> Inheritors = new HashSet<Node>();
|
||||
}
|
||||
|
||||
private readonly PrefabCollection<T> prefabCollection;
|
||||
|
||||
public InheritanceTreeCollection(PrefabCollection<T> collection) { prefabCollection = collection; }
|
||||
|
||||
public readonly Dictionary<Identifier, Node> IdToNode = new Dictionary<Identifier, Node>();
|
||||
public readonly HashSet<Node> RootNodes = new HashSet<Node>();
|
||||
|
||||
public Node? AddNodeAndInheritors(Identifier id)
|
||||
{
|
||||
if (!prefabCollection.TryGet(id, out T? prefab)) { return null; }
|
||||
|
||||
if (!IdToNode.TryGetValue(id, out var node))
|
||||
{
|
||||
node = new Node(id);
|
||||
RootNodes.Add(node);
|
||||
IdToNode.Add(id, node);
|
||||
}
|
||||
else
|
||||
{
|
||||
//if the node already exists, it already contains
|
||||
//all inheritors so let's just return this immediately
|
||||
return node;
|
||||
}
|
||||
|
||||
prefabCollection
|
||||
.Cast<IImplementsVariants<T>>()
|
||||
.Where(p => p.VariantOf == id)
|
||||
.Cast<T>()
|
||||
.ForEach(p =>
|
||||
{
|
||||
var inheritorNode = AddNodeAndInheritors(p.Identifier);
|
||||
if (inheritorNode is null) { return; }
|
||||
RootNodes.Remove(inheritorNode);
|
||||
inheritorNode.Parent = node;
|
||||
node.Inheritors.Add(inheritorNode);
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private void FindCycles(in Node node, HashSet<Node> uncheckedNodes)
|
||||
{
|
||||
HashSet<Node> checkedNodes = new HashSet<Node>();
|
||||
List<Node> hierarchyPositions = new List<Node>();
|
||||
Node? currNode = node;
|
||||
do
|
||||
{
|
||||
if (!uncheckedNodes.Contains(currNode)) { break; }
|
||||
if (checkedNodes.Contains(currNode))
|
||||
{
|
||||
int index = hierarchyPositions.IndexOf(currNode);
|
||||
throw new Exception("Inheritance cycle detected: "
|
||||
+string.Join(", ", hierarchyPositions.Skip(index).Select(n => n.Identifier)));
|
||||
}
|
||||
checkedNodes.Add(currNode);
|
||||
hierarchyPositions.Add(currNode);
|
||||
currNode = currNode.Parent;
|
||||
} while (currNode != null);
|
||||
uncheckedNodes.RemoveWhere(i => checkedNodes.Contains(i));
|
||||
}
|
||||
|
||||
public void AddNodesAndInheritors(IEnumerable<Identifier> ids)
|
||||
=> ids.ForEach(id => AddNodeAndInheritors(id));
|
||||
|
||||
public void InvokeCallbacks()
|
||||
{
|
||||
HashSet<Node> uncheckedNodes = IdToNode.Values.ToHashSet();
|
||||
IdToNode.Values.ForEach(v => FindCycles(v, uncheckedNodes));
|
||||
void invokeCallbacksForNode(Node node)
|
||||
{
|
||||
if (!prefabCollection.TryGet(node.Identifier, out var p) ||
|
||||
!(p is IImplementsVariants<T> prefab)) { return; }
|
||||
if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent)) { prefab.InheritFrom(parent!); }
|
||||
node.Inheritors.ForEach(invokeCallbacksForNode);
|
||||
}
|
||||
RootNodes.ForEach(invokeCallbacksForNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInheritance(Identifier prefabIdentifier)
|
||||
=> HandleInheritance(prefabIdentifier.ToEnumerable());
|
||||
|
||||
private void HandleInheritance(IEnumerable<Identifier> identifiers)
|
||||
{
|
||||
if (!implementsVariants) { return; }
|
||||
InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this);
|
||||
inheritanceTreeCollection.AddNodesAndInheritors(identifiers);
|
||||
inheritanceTreeCollection.InvokeCallbacks();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AllPrefabs exposes all prefabs instead of just the active ones.
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, List<T>>> AllPrefabs
|
||||
public IEnumerable<KeyValuePair<Identifier, PrefabSelector<T>>> AllPrefabs
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -35,58 +212,107 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the active prefab with the identifier.
|
||||
/// Returns the active prefab with the given identifier.
|
||||
/// </summary>
|
||||
/// <param name="identifier">Prefab identifier</param>
|
||||
/// <returns>Active prefab with the identifier</returns>
|
||||
/// <returns>Active prefab with the given identifier</returns>
|
||||
public T this[Identifier identifier]
|
||||
{
|
||||
get
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
var prefab = prefabs[identifier].ActivePrefab;
|
||||
if (prefab != null && !IsPrefabOverriddenByFile(prefab))
|
||||
{
|
||||
return prefab;
|
||||
}
|
||||
throw new IndexOutOfRangeException($"Prefab of identifier \"{identifier}\" cannot be returned because it was overridden by \"{topMostOverrideFile!.Path}\"");
|
||||
}
|
||||
}
|
||||
|
||||
public T this[string identifier]
|
||||
{
|
||||
get { return prefabs[identifier].Last(); }
|
||||
get
|
||||
{
|
||||
//this exists because I don't want implicit
|
||||
//string to Identifier conversion for the most
|
||||
//part, but it's useful and fairly safe to do
|
||||
//in this particular instance
|
||||
return this[identifier.ToIdentifier()];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a prefab with the identifier exists, false otherwise.
|
||||
/// </summary>
|
||||
/// <param name="identifier">Prefab identifier</param>
|
||||
/// <param name="result">The matching prefab (if one is found)</param>
|
||||
/// <returns>Whether a prefab with the identifier exists or not</returns>
|
||||
public bool TryGet(Identifier identifier, out T? result)
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
if (prefabs.TryGetValue(identifier, out PrefabSelector<T>? selector))
|
||||
{
|
||||
result = selector!.ActivePrefab;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGet(string identifier, out T? result)
|
||||
=> TryGet(identifier.ToIdentifier(), out result);
|
||||
|
||||
public IEnumerable<Identifier> Keys => prefabs.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first active prefab that returns true given the predicate,
|
||||
/// or null if no such prefab is found.
|
||||
/// </summary>
|
||||
/// <param name="predicate">Predicate to perform the search with.</param>
|
||||
/// <returns></returns>
|
||||
public T Find(Predicate<T> predicate)
|
||||
public T? Find(Predicate<T> predicate)
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
foreach (var kpv in prefabs)
|
||||
{
|
||||
if (predicate(kpv.Value.Last()))
|
||||
if (kpv.Value.ActivePrefab is T p && predicate(p))
|
||||
{
|
||||
return kpv.Value.Last();
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a prefab with the identifier exists, false otherwise.
|
||||
/// Returns true if a prefab with the given identifier exists, false otherwise.
|
||||
/// </summary>
|
||||
/// <param name="identifier">Prefab identifier</param>
|
||||
/// <returns>Whether a prefab with the identifier exists or not</returns>
|
||||
public bool ContainsKey(string identifier)
|
||||
/// <returns>Whether a prefab with the given identifier exists or not</returns>
|
||||
public bool ContainsKey(Identifier identifier)
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
return prefabs.ContainsKey(identifier);
|
||||
}
|
||||
|
||||
public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier());
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a prefab with the identifier exists, false otherwise.
|
||||
/// Determines whether a prefab is implemented as an override or not.
|
||||
/// </summary>
|
||||
/// <param name="identifier">Prefab identifier</param>
|
||||
/// <param name="prefab">The matching prefab (if one is found)</param>
|
||||
/// <returns>Whether a prefab with the identifier exists or not</returns>
|
||||
public bool TryGetValue(string identifier, out T prefab)
|
||||
/// <param name="prefab">Prefab in this collection</param>
|
||||
/// <returns>Whether a prefab is implemented as an override or not</returns>
|
||||
public bool IsOverride(T prefab)
|
||||
{
|
||||
if (!ContainsKey(identifier))
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
if (ContainsKey(prefab.Identifier))
|
||||
{
|
||||
prefab = default;
|
||||
return false;
|
||||
return prefabs[prefab.Identifier].IsOverride(prefab);
|
||||
}
|
||||
prefab = this[identifier];
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,34 +326,51 @@ namespace Barotrauma
|
||||
/// <param name="isOverride">Is marked as override</param>
|
||||
public void Add(T prefab, bool isOverride)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prefab.Identifier))
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
if (prefab.Identifier.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"Prefab \"{prefab.OriginalName}\" has no identifier!");
|
||||
throw new ArgumentException($"Prefab has no identifier!");
|
||||
}
|
||||
|
||||
bool basePrefabExists = prefabs.TryGetValue(prefab.Identifier, out List<T> list);
|
||||
|
||||
//Handle bad overrides and duplicates
|
||||
if (basePrefabExists && !isOverride)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to add the prefab \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T)}): a prefab with the same identifier already exists; try overriding\n{Environment.StackTrace}");
|
||||
return;
|
||||
}
|
||||
bool selectorExists = prefabs.TryGetValue(prefab.Identifier, out PrefabSelector<T>? selector);
|
||||
|
||||
//Add to list
|
||||
if (!basePrefabExists)
|
||||
selector ??= new PrefabSelector<T>();
|
||||
|
||||
if (prefab is PrefabWithUintIdentifier prefabWithUintIdentifier)
|
||||
{
|
||||
list = new List<T>();
|
||||
if (!selector.IsEmpty)
|
||||
{
|
||||
prefabWithUintIdentifier.UintIdentifier = (selector.ActivePrefab as PrefabWithUintIdentifier)!.UintIdentifier;
|
||||
}
|
||||
else
|
||||
{
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
prefabWithUintIdentifier.UintIdentifier = ToolBox.IdentifierToUint32Hash(prefab.Identifier, md5);
|
||||
|
||||
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
|
||||
T? findCollision()
|
||||
=> Find(p =>
|
||||
p.Identifier != prefab.Identifier
|
||||
&& p is PrefabWithUintIdentifier otherPrefab
|
||||
&& otherPrefab.UintIdentifier == prefabWithUintIdentifier.UintIdentifier);
|
||||
for (T? collision = findCollision(); collision != null; collision = findCollision())
|
||||
{
|
||||
DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})");
|
||||
prefabWithUintIdentifier.UintIdentifier++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selector.Add(prefab, isOverride);
|
||||
|
||||
list.Add(prefab);
|
||||
|
||||
Sort(list);
|
||||
|
||||
if (!basePrefabExists)
|
||||
if (!selectorExists)
|
||||
{
|
||||
prefabs.Add(prefab.Identifier, list);
|
||||
if (!prefabs.TryAdd(prefab.Identifier, selector)) { throw new Exception($"Failed to add selector for \"{prefab.Identifier}\""); }
|
||||
}
|
||||
OnAdd?.Invoke(prefab, isOverride);
|
||||
HandleInheritance(prefab.Identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,62 +379,63 @@ namespace Barotrauma
|
||||
/// <param name="prefab">Prefab</param>
|
||||
public void Remove(T prefab)
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
OnRemove?.Invoke(prefab);
|
||||
if (!ContainsKey(prefab.Identifier)) { return; }
|
||||
if (!prefabs[prefab.Identifier].Contains(prefab)) { return; }
|
||||
if (prefabs[prefab.Identifier].IndexOf(prefab)==0)
|
||||
{
|
||||
prefabs[prefab.Identifier][0] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
prefabs[prefab.Identifier].Remove(prefab);
|
||||
}
|
||||
prefab.Dispose();
|
||||
prefabs[prefab.Identifier].Remove(prefab);
|
||||
|
||||
if (prefabs[prefab.Identifier].Count <= 0 ||
|
||||
(prefabs[prefab.Identifier].Count == 1 && prefabs[prefab.Identifier][0] == null))
|
||||
if (prefabs[prefab.Identifier].IsEmpty)
|
||||
{
|
||||
prefabs.Remove(prefab.Identifier);
|
||||
prefabs.TryRemove(prefab.Identifier, out _);
|
||||
}
|
||||
HandleInheritance(prefab.Identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all prefabs that were loaded from a certain file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">File path</param>
|
||||
public void RemoveByFile(string filePath)
|
||||
public void RemoveByFile(ContentFile file)
|
||||
{
|
||||
List<T> prefabsToRemove = new List<T>();
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
HashSet<Identifier> clearedIdentifiers = new HashSet<Identifier>();
|
||||
foreach (var kpv in prefabs)
|
||||
{
|
||||
foreach (var prefab in kpv.Value)
|
||||
{
|
||||
if (prefab != null && prefab.FilePath == filePath)
|
||||
{
|
||||
prefabsToRemove.Add(prefab);
|
||||
}
|
||||
}
|
||||
kpv.Value.RemoveByFile(file, OnRemove);
|
||||
if (kpv.Value.IsEmpty) { clearedIdentifiers.Add(kpv.Key); }
|
||||
}
|
||||
|
||||
foreach (var prefab in prefabsToRemove)
|
||||
foreach (var identifier in clearedIdentifiers)
|
||||
{
|
||||
Remove(prefab);
|
||||
prefabs.TryRemove(identifier, out _);
|
||||
}
|
||||
RemoveOverrideFile(file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts a list of prefabs based on the content package load order.
|
||||
/// Adds an override file to the collection.
|
||||
/// </summary>
|
||||
/// <param name="list">List of prefabs</param>
|
||||
private void Sort(List<T> list)
|
||||
public void AddOverrideFile(ContentFile file)
|
||||
{
|
||||
if (list.Count <= 1) { return; }
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
if (!overrideFiles.Contains(file))
|
||||
{
|
||||
overrideFiles.Add(file);
|
||||
}
|
||||
OnAddOverrideFile?.Invoke(file);
|
||||
}
|
||||
|
||||
var newList = list.Skip(1)
|
||||
.OrderByDescending(p => GameMain.Config.EnabledRegularPackages.IndexOf(p.ContentPackage)).ToList();
|
||||
|
||||
list.RemoveRange(1, list.Count - 1);
|
||||
list.AddRange(newList);
|
||||
/// <summary>
|
||||
/// Removes an override file from the collection.
|
||||
/// </summary>
|
||||
public void RemoveOverrideFile(ContentFile file)
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
if (overrideFiles.Contains(file))
|
||||
{
|
||||
overrideFiles.Remove(file);
|
||||
}
|
||||
OnRemoveOverrideFile?.Invoke(file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -199,10 +443,14 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public void SortAll()
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
foreach (var kvp in prefabs)
|
||||
{
|
||||
Sort(kvp.Value);
|
||||
kvp.Value.Sort();
|
||||
}
|
||||
topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null;
|
||||
OnSort?.Invoke();
|
||||
HandleInheritance(this.Select(p => p.Identifier));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -211,9 +459,14 @@ namespace Barotrauma
|
||||
/// <returns>IEnumerator</returns>
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
Prefab.DisallowCallFromConstructor();
|
||||
foreach (var kpv in prefabs)
|
||||
{
|
||||
yield return kpv.Value.Last();
|
||||
var prefab = kpv.Value.ActivePrefab;
|
||||
if (prefab != null && !IsPrefabOverriddenByFile(prefab))
|
||||
{
|
||||
yield return prefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public class PrefabSelector<T> : IEnumerable<T> where T : notnull, Prefab
|
||||
{
|
||||
public T? BasePrefab
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (overrides) { return basePrefabInternal; }
|
||||
}
|
||||
}
|
||||
|
||||
public T? ActivePrefab
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (overrides) { return activePrefabInternal; }
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(T prefab, bool isOverride)
|
||||
{
|
||||
lock (overrides) { AddInternal(prefab, isOverride); }
|
||||
}
|
||||
|
||||
public void RemoveIfContains(T prefab)
|
||||
{
|
||||
lock (overrides) { RemoveIfContainsInternal(prefab); }
|
||||
}
|
||||
|
||||
public void Remove(T prefab)
|
||||
{
|
||||
lock (overrides) { RemoveInternal(prefab); }
|
||||
}
|
||||
|
||||
public void RemoveByFile(ContentFile file, Action<T>? callback = null)
|
||||
{
|
||||
lock (overrides) { RemoveByFileInternal(file, callback); }
|
||||
}
|
||||
|
||||
public void Sort()
|
||||
{
|
||||
lock (overrides) { SortInternal(); }
|
||||
}
|
||||
|
||||
public bool IsEmpty
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (overrides) { return isEmptyInternal; }
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(T prefab)
|
||||
{
|
||||
lock (overrides) { return ContainsInternal(prefab); }
|
||||
}
|
||||
|
||||
public bool IsOverride(T prefab)
|
||||
{
|
||||
lock (overrides) { return IsOverrideInternal(prefab); }
|
||||
}
|
||||
|
||||
|
||||
#region Underlying implementations of the public methods, done separately to avoid nested locking
|
||||
private T? basePrefabInternal;
|
||||
private readonly List<T> overrides = new List<T>();
|
||||
|
||||
private T? activePrefabInternal => overrides.Any() ? overrides.First() : basePrefabInternal;
|
||||
|
||||
private void AddInternal(T prefab, bool isOverride)
|
||||
{
|
||||
if (isOverride)
|
||||
{
|
||||
if (overrides.Contains(prefab)) { throw new InvalidOperationException($"Duplicate prefab in PrefabSelector ({typeof(T)}, {prefab.Identifier}, {prefab.ContentFile.ContentPackage.Name})"); }
|
||||
overrides.Add(prefab);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (BasePrefab != null)
|
||||
{
|
||||
string prefabName
|
||||
= prefab is MapEntityPrefab mapEntityPrefab
|
||||
? $"\"{mapEntityPrefab.OriginalName}\", \"{prefab.Identifier}\""
|
||||
: $"\"{prefab.Identifier}\"";
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to add the prefab {prefabName} ({prefab.GetType()}) from \"{prefab.ContentPackage?.Name ?? "[NULL]"}\" ({prefab.ContentPackage?.Dir ?? ""}): "
|
||||
+ $"a prefab with the same identifier from \"{ActivePrefab!.ContentPackage?.Name ?? "[NULL]"}\" ({ActivePrefab!.ContentPackage?.Dir ?? ""}) already exists; try overriding");
|
||||
}
|
||||
basePrefabInternal = prefab;
|
||||
}
|
||||
SortInternal();
|
||||
}
|
||||
|
||||
private void RemoveIfContainsInternal(T prefab)
|
||||
{
|
||||
if (!ContainsInternal(prefab)) { return; }
|
||||
RemoveInternal(prefab);
|
||||
}
|
||||
|
||||
private void RemoveInternal(T prefab)
|
||||
{
|
||||
if (basePrefabInternal == prefab) { basePrefabInternal = null; }
|
||||
else if (overrides.Contains(prefab)) { overrides.Remove(prefab); }
|
||||
else { throw new InvalidOperationException($"Can't remove prefab from PrefabSelector ({typeof(T)}, {prefab.Identifier}, {prefab.ContentFile.ContentPackage.Name})"); }
|
||||
prefab.Dispose();
|
||||
SortInternal();
|
||||
}
|
||||
|
||||
private void RemoveByFileInternal(ContentFile file, Action<T>? callback)
|
||||
{
|
||||
for (int i = overrides.Count-1; i >= 0; i--)
|
||||
{
|
||||
var prefab = overrides[i];
|
||||
if (prefab.ContentFile == file)
|
||||
{
|
||||
RemoveInternal(prefab);
|
||||
callback?.Invoke(prefab);
|
||||
}
|
||||
}
|
||||
|
||||
if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file)
|
||||
{
|
||||
RemoveInternal(basePrefabInternal);
|
||||
callback?.Invoke(p);
|
||||
}
|
||||
}
|
||||
|
||||
private void SortInternal()
|
||||
{
|
||||
overrides.Sort((p1, p2) => (p1.ContentPackage?.Index ?? int.MaxValue) - (p2.ContentPackage?.Index ?? int.MaxValue));
|
||||
}
|
||||
|
||||
private bool isEmptyInternal => basePrefabInternal is null && !overrides.Any();
|
||||
|
||||
private bool ContainsInternal(T prefab) => basePrefabInternal == prefab || overrides.Contains(prefab);
|
||||
|
||||
private int IndexOfInternal(T prefab) => basePrefabInternal == prefab
|
||||
? overrides.Count
|
||||
: overrides.IndexOf(prefab);
|
||||
|
||||
private bool IsOverrideInternal(T prefab) => IndexOfInternal(prefab) > 0;
|
||||
#endregion
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
T? basePrefab;
|
||||
ImmutableArray<T> overrideClone;
|
||||
lock (overrides)
|
||||
{
|
||||
basePrefab = basePrefabInternal;
|
||||
overrideClone = overrides.ToImmutableArray();
|
||||
}
|
||||
if (basePrefab != null) { yield return basePrefab; }
|
||||
foreach (T prefab in overrideClone)
|
||||
{
|
||||
yield return prefab;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefab that has a property serves as a deterministic hash of
|
||||
/// a prefab's identifier. This member is filled automatically
|
||||
/// by PrefabCollection.Add. Required for GetRandom to work on
|
||||
/// arbitrary Prefab enumerables, recommended for network synchronization.
|
||||
/// </summary>
|
||||
public abstract class PrefabWithUintIdentifier : Prefab
|
||||
{
|
||||
public UInt32 UintIdentifier { get; set; }
|
||||
|
||||
protected PrefabWithUintIdentifier(ContentFile file, Identifier identifier) : base(file, identifier) { }
|
||||
|
||||
protected PrefabWithUintIdentifier(ContentFile file, ContentXElement element) : base(file, element) { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user