prototype composition (#3070)

* prototype composition

* oops

* better

* fixes build

* should fix

* misc fixes & circle test

* moar circle checks

* adds tests

Co-authored-by: Paul <ritter.paul1@gmail.com>
This commit is contained in:
Paul Ritter
2022-08-01 14:22:46 +02:00
committed by GitHub
parent 92d40cded3
commit 752d4dfe8b
14 changed files with 587 additions and 98 deletions

View File

@@ -5,6 +5,7 @@ using System.Text.RegularExpressions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Robust.Server.Bql
{
@@ -268,7 +269,9 @@ namespace Robust.Server.Bql
if ((metaData.EntityPrototype?.ID == name) ^ isInverted)
return true;
return (metaData.EntityPrototype?.Parent == name) ^ isInverted; // Damn, can't actually do recursive check here.
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
return metaData.EntityPrototype != null && prototypeManager.EnumerateParents<EntityPrototype>(metaData.EntityPrototype.ID).Any(x => x.Name == name) ^ isInverted;
});
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Linguini.Bundle.Errors;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Localization;
@@ -56,70 +57,72 @@ namespace Robust.Shared.Localization
string? suffix = null;
Dictionary<string, string>? attributes = null;
_prototype.TryIndex<EntityPrototype>(prototypeId, out var prototype);
var locId = prototype?.CustomLocalizationID ?? $"ent-{prototypeId}";
if (TryGetMessage(locId, out var bundle, out var msg))
foreach (var prototype in _prototype.EnumerateParents<EntityPrototype>(prototypeId, true))
{
// Localization override exists.
var msgAttrs = msg.Attributes;
var locId = prototype?.CustomLocalizationID ?? $"ent-{prototypeId}";
if (name == null && msg.Value != null)
if (TryGetMessage(locId, out var bundle, out var msg))
{
// Only set name if the value isn't empty.
// So that you can override *only* a desc/suffix.
name = bundle.FormatPattern(msg.Value, null, out var fmtErr);
WriteWarningForErrs(fmtErr, locId);
// Localization override exists.
var msgAttrs = msg.Attributes;
if (name == null && msg.Value != null)
{
// Only set name if the value isn't empty.
// So that you can override *only* a desc/suffix.
name = bundle.FormatPattern(msg.Value, null, out var fmtErr);
WriteWarningForErrs(fmtErr, locId);
}
if (msgAttrs.Count != 0)
{
foreach (var (attrId, pattern) in msg.Attributes)
{
var attrib = attrId.ToString();
if (attrib.Equals("desc")
|| attrib.Equals("suffix"))
continue;
attributes ??= new Dictionary<string, string>();
if (!attributes.ContainsKey(attrib))
{
var value = bundle.FormatPattern(pattern, null, out var errors);
WriteWarningForErrs(errors, locId);
attributes[attrib] = value;
}
}
var allErrors = new List<FluentError>();
if (desc == null
&& bundle.TryGetMsg(locId, "desc", null, out var err1, out desc))
{
allErrors.AddRange(err1);
}
if (suffix == null
&& bundle.TryGetMsg(locId, "suffix", null, out var err, out suffix))
{
allErrors.AddRange(err);
}
WriteWarningForErrs(allErrors, locId);
}
}
if (msgAttrs.Count != 0)
{
foreach (var (attrId, pattern) in msg.Attributes)
{
var attrib = attrId.ToString();
if (attrib.Equals("desc")
|| attrib.Equals("suffix"))
continue;
name ??= prototype?.SetName;
desc ??= prototype?.SetDesc;
suffix ??= prototype?.SetSuffix;
if (prototype?.LocProperties != null && prototype.LocProperties.Count != 0)
{
foreach (var (attrib, value) in prototype.LocProperties)
{
attributes ??= new Dictionary<string, string>();
if (!attributes.ContainsKey(attrib))
{
var value = bundle.FormatPattern(pattern, null, out var errors);
WriteWarningForErrs(errors, locId);
attributes[attrib] = value;
}
}
var allErrors = new List<FluentError>();
if (desc == null
&& bundle.TryGetMsg(locId, "desc", null, out var err1, out desc))
{
allErrors.AddRange(err1);
}
if (suffix == null
&& bundle.TryGetMsg(locId, "suffix", null, out var err, out suffix))
{
allErrors.AddRange(err);
}
WriteWarningForErrs(allErrors, locId);
}
}
name ??= prototype?.SetName;
desc ??= prototype?.SetDesc;
suffix ??= prototype?.SetSuffix;
if (prototype?.LocProperties != null && prototype.LocProperties.Count != 0)
{
foreach (var (attrib, value) in prototype.LocProperties)
{
attributes ??= new Dictionary<string, string>();
if (!attributes.ContainsKey(attrib))
{
attributes[attrib] = value;
}
}
}

View File

@@ -14,6 +14,7 @@ using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Prototypes
@@ -136,8 +137,8 @@ namespace Robust.Shared.Prototypes
/// The prototype we inherit from.
/// </summary>
[ViewVariables]
[ParentDataFieldAttribute(typeof(AbstractPrototypeIdSerializer<EntityPrototype>))]
public string? Parent { get; private set; }
[ParentDataFieldAttribute(typeof(AbstractPrototypeIdArraySerializer<EntityPrototype>))]
public string[]? Parents { get; }
[ViewVariables]
[NeverPushInheritance]

View File

@@ -27,7 +27,7 @@ namespace Robust.Shared.Prototypes
public interface IInheritingPrototype
{
string? Parent { get; }
string[]? Parents { get; }
bool Abstract { get; }
}

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Utility;
namespace Robust.Shared.Prototypes;
public sealed class MultiRootInheritanceGraph<T> where T : notnull
{
private readonly HashSet<T> _rootNodes = new();
private readonly Dictionary<T, HashSet<T>> _edges = new();
private readonly Dictionary<T, T[]> _parents = new();
public bool Add(T id) => _rootNodes.Add(id);
public IReadOnlySet<T> RootNodes => _rootNodes;
public IReadOnlySet<T>? GetChildren(T id) => _edges.GetValueOrDefault(id);
public bool TryGetChildren(T id, [NotNullWhen(true)] out IReadOnlySet<T>? set)
{
set = GetChildren(id);
return set != null;
}
public T[]? GetParents(T id) => _parents.GetValueOrDefault(id);
public bool TryGetParents(T id, [NotNullWhen(true)] out T[]? parents)
{
parents = GetParents(id);
return parents != null;
}
public void Add(T id, T[] parents)
{
//check for circular inheritance
foreach (var parent in parents)
{
var parentsL1 = GetParents(parent);
if(parentsL1 == null) continue;
var queue = new Queue<T>(parentsL1);
while (queue.TryDequeue(out var parentL1))
{
if (EqualityComparer<T>.Default.Equals(parentL1,id))
throw new InvalidOperationException(
$"Circular Inheritance detected for id \"{id}\" and parent \"{parent}\"");
var parentsL2 = GetParents(parentL1);
if (parentsL2 != null)
{
foreach (var parentL3 in parentsL2)
{
queue.Enqueue(parentL3);
}
}
}
}
_rootNodes.Remove(id);
foreach (var parent in parents)
{
var edges = _edges.GetOrNew(parent);
edges.Add(id);
_parents[id] = parents;
if (!_parents.ContainsKey(parent))
_rootNodes.Add(parent);
}
}
public bool Remove(T id, bool force = false)
{
if (!force && _edges.ContainsKey(id)) throw new InvalidOperationException("Cannot remove node that has remaining children");
var result = _rootNodes.Remove(id);
if (_parents.TryGetValue(id, out var parents))
{
result = true;
foreach (var parent in parents)
{
_edges[parent].Remove(id);
}
_parents.Remove(id);
}
if (force)
{
if (_edges.TryGetValue(id, out var children))
{
foreach (var child in children)
{
var childParents = _parents[child];
var newParents = new T[childParents.Length - 1];
var i = 0;
foreach (var childParent in childParents)
{
if(Equals(childParent, id)) continue;
newParents[i++] = childParent;
}
if (newParents.Length == 0)
{
_rootNodes.Add(child);
_parents.Remove(child);
}
else
{
_parents[child] = newParents;
}
}
}
}
return result;
}
}

View File

@@ -57,6 +57,16 @@ namespace Robust.Shared.Prototypes
/// </exception>
IEnumerable<IPrototype> EnumeratePrototypes(string variant);
/// <summary>
/// Returns an IEnumerable to iterate all parents of a prototype of a certain type.
/// </summary>
IEnumerable<T> EnumerateParents<T>(string id, bool includeSelf = false) where T : class, IPrototype, IInheritingPrototype;
/// <summary>
/// Returns an IEnumerable to iterate all parents of a prototype of a certain type.
/// </summary>
IEnumerable<IPrototype> EnumerateParents(Type type, string id, bool includeSelf = false);
/// <summary>
/// Index for a <see cref="IPrototype"/> by ID.
/// </summary>
@@ -224,7 +234,7 @@ namespace Robust.Shared.Prototypes
private readonly Dictionary<Type, Dictionary<string, IPrototype>> _prototypes = new();
private readonly Dictionary<Type, Dictionary<string, MappingDataNode>> _prototypeResults = new();
private readonly Dictionary<Type, PrototypeInheritanceTree> _inheritanceTrees = new();
private readonly Dictionary<Type, MultiRootInheritanceGraph<string>> _inheritanceTrees = new();
private readonly HashSet<string> _ignoredPrototypeTypes = new();
@@ -269,6 +279,67 @@ namespace Robust.Shared.Prototypes
return EnumeratePrototypes(GetVariantType(variant));
}
public IEnumerable<T> EnumerateParents<T>(string id, bool includeSelf = false) where T : class, IPrototype, IInheritingPrototype
{
if (!_hasEverBeenReloaded)
{
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
if(!TryIndex<T>(id, out var prototype))
yield break;
if (includeSelf) yield return prototype;
if (prototype.Parents == null) yield break;
var queue = new Queue<string>(prototype.Parents);
while (queue.TryDequeue(out var prototypeId))
{
if(!TryIndex<T>(prototypeId, out var parent))
yield break;
yield return parent;
if (parent.Parents == null) continue;
foreach (var parentId in parent.Parents)
{
queue.Enqueue(parentId);
}
}
}
public IEnumerable<IPrototype> EnumerateParents(Type type, string id, bool includeSelf = false)
{
if (!_hasEverBeenReloaded)
{
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
if (!type.IsAssignableTo(typeof(IInheritingPrototype)))
{
throw new InvalidOperationException("The provided prototype type is not an inheriting prototype");
}
if(!TryIndex(type, id, out var prototype))
yield break;
if (includeSelf) yield return prototype;
var iPrototype = (IInheritingPrototype)prototype;
if (iPrototype.Parents == null) yield break;
var queue = new Queue<string>(iPrototype.Parents);
while (queue.TryDequeue(out var prototypeId))
{
if (!TryIndex(type, id, out var parent))
continue;
yield return parent;
iPrototype = (IInheritingPrototype)parent;
if (iPrototype.Parents == null) continue;
foreach (var parentId in iPrototype.Parents)
{
queue.Enqueue(parentId);
}
}
}
public T Index<T>(string id) where T : class, IPrototype
{
if (!_hasEverBeenReloaded)
@@ -340,23 +411,42 @@ namespace Robust.Shared.Prototypes
continue;
}
var tree = _inheritanceTrees[type];
var processQueue = new Queue<string>();
foreach (var id in prototypes[type])
{
if (!pushed.ContainsKey(type))
pushed[type] = new HashSet<string>();
if (pushed[type].Contains(id))
processQueue.Enqueue(id);
}
while(processQueue.TryDequeue(out var id))
{
var pushedSet = pushed.GetOrNew(type);
if (tree.TryGetParents(id, out var parents))
{
continue;
var nonPushedParent = false;
foreach (var parent in parents)
{
//our parent has been reloaded and has not been added to the pushedSet yet
if (prototypes[type].Contains(parent) && !pushedSet.Contains(parent))
{
//we re-queue ourselves at the end of the queue
processQueue.Enqueue(id);
nonPushedParent = true;
break;
}
}
if(nonPushedParent) continue;
foreach (var parent in parents)
{
PushInheritance(type, id, parent);
}
}
var set = new HashSet<string>();
set.Add(id);
PushInheritance(type, id, _inheritanceTrees[type].GetParent(id), set);
foreach (var changedId in set)
{
TryReadPrototype(type, changedId, _prototypeResults[type][changedId]);
}
pushed[type].UnionWith(set);
TryReadPrototype(type, id, _prototypeResults[type][id]);
pushedSet.Add(id);
}
}
@@ -397,10 +487,31 @@ namespace Robust.Shared.Prototypes
types.Sort(SortPrototypesByPriority);
foreach (var type in types)
{
if(_inheritanceTrees.TryGetValue(type, out var tree)){
foreach (var baseNode in tree.BaseNodes)
if(_inheritanceTrees.TryGetValue(type, out var tree))
{
var processed = new HashSet<string>();
var workList = new Queue<string>(tree.RootNodes);
while (workList.TryDequeue(out var id))
{
PushInheritance(type, baseNode);
processed.Add(id);
if (tree.TryGetParents(id, out var parents))
{
foreach (var parent in parents)
{
PushInheritance(type, id, parent);
}
}
if (tree.TryGetChildren(id, out var children))
{
foreach (var child in children)
{
var childParents = tree.GetParents(child)!;
if(childParents.All(p => processed.Contains(p)))
workList.Enqueue(child);
}
}
}
}
@@ -425,27 +536,10 @@ namespace Robust.Shared.Prototypes
}
}
private void PushInheritance(Type type, string id, string? parent = null, HashSet<string>? changed = null)
{
if (parent != null)
{
PushInheritanceWithoutRecursion(type, id, parent, changed);
}
if(!_inheritanceTrees[type].HasId(id)) return;
foreach (var child in _inheritanceTrees[type].Children(id))
{
PushInheritance(type, child, id, changed);
}
}
private void PushInheritanceWithoutRecursion(Type type, string id, string parent,
HashSet<string>? changed = null)
private void PushInheritance(Type type, string id, string parent)
{
_prototypeResults[type][id] = _serializationManager.PushCompositionWithGenericNode(type,
new[] { _prototypeResults[type][parent] }, _prototypeResults[type][id]);
changed?.Add(id);
}
/// <inheritdoc />
@@ -624,12 +718,13 @@ namespace Robust.Shared.Prototypes
if (_inheritanceTrees.TryGetValue(type, out var tree))
{
tree.RemoveId(id);
tree.Remove(id, true);
}
if (_prototypes.TryGetValue(type, out var prototypeIds))
{
prototypeIds.Remove(id);
_prototypeResults[type].Remove(id);
}
}
}
@@ -677,8 +772,15 @@ namespace Robust.Shared.Prototypes
_prototypeResults[prototypeType][idNode.Value] = datanode;
if (prototypeType.IsAssignableTo(typeof(IInheritingPrototype)))
{
datanode.TryGet<ValueDataNode>(ParentDataFieldAttribute.Name, out var parentNode);
_inheritanceTrees[prototypeType].AddId(idNode.Value, parentNode?.Value, true);
if (datanode.TryGet(ParentDataFieldAttribute.Name, out var parentNode))
{
var parents = _serializationManager.Read<string[]>(parentNode);
_inheritanceTrees[prototypeType].Add(idNode.Value, parents);
}
else
{
_inheritanceTrees[prototypeType].Add(idNode.Value);
}
}
if (changed == null) continue;
@@ -872,7 +974,7 @@ namespace Robust.Shared.Prototypes
_prototypes[type] = new Dictionary<string, IPrototype>();
_prototypeResults[type] = new Dictionary<string, MappingDataNode>();
if (typeof(IInheritingPrototype).IsAssignableFrom(type))
_inheritanceTrees[type] = new PrototypeInheritanceTree();
_inheritanceTrees[type] = new MultiRootInheritanceGraph<string>();
}
}

View File

@@ -0,0 +1,63 @@
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
public sealed class AbstractPrototypeIdArraySerializer<TPrototype> : PrototypeIdArraySerializer<TPrototype> where TPrototype : class, IPrototype, IInheritingPrototype
{
protected override PrototypeIdSerializer<TPrototype> PrototypeSerializer =>
new AbstractPrototypeIdSerializer<TPrototype>();
}
[Virtual]
public class PrototypeIdArraySerializer<TPrototype> : ITypeSerializer<string[], SequenceDataNode>,
ITypeSerializer<string[], ValueDataNode> where TPrototype : class, IPrototype
{
protected virtual PrototypeIdSerializer<TPrototype> PrototypeSerializer => new();
public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
return new ValidatedSequenceNode(node.Select(x =>
x is ValueDataNode valueDataNode
? PrototypeSerializer.Validate(serializationManager, valueDataNode, dependencies, context)
: new ErrorNode(x, $"Cannot cast node {x} to ValueDataNode.")).ToList());
}
public string[] Read(ISerializationManager serializationManager, SequenceDataNode node, IDependencyCollection dependencies,
bool skipHook, ISerializationContext? context = null, string[]? value = default)
{
return node.Select(x =>
PrototypeSerializer.Read(serializationManager, (ValueDataNode)x, dependencies, skipHook, context))
.ToArray();
}
public DataNode Write(ISerializationManager serializationManager, string[] value, bool alwaysWrite = false,
ISerializationContext? context = null)
{
return serializationManager.WriteValue(value, alwaysWrite, context);
}
public string[] Copy(ISerializationManager serializationManager, string[] source, string[] target, bool skipHook,
ISerializationContext? context = null)
{
return serializationManager.Copy(source, target, context, skipHook)!;
}
public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null) =>
PrototypeSerializer.Validate(serializationManager, node, dependencies, context);
public string[] Read(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies,
bool skipHook, ISerializationContext? context = null, string[]? value = default) =>
new[] { PrototypeSerializer.Read(serializationManager, node, dependencies, skipHook, context) };
}

View File

@@ -14,7 +14,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
{
public sealed class AbstractPrototypeIdDictionarySerializer<TValue, TPrototype> : PrototypeIdDictionarySerializer<TValue,
TPrototype> where TPrototype : class, IPrototype
TPrototype> where TPrototype : class, IPrototype, IInheritingPrototype
{
protected override PrototypeIdSerializer<TPrototype> PrototypeSerializer =>
new AbstractPrototypeIdSerializer<TPrototype>();

View File

@@ -10,7 +10,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List
{
public sealed class AbstractPrototypeIdListSerializer<T> : PrototypeIdListSerializer<T> where T : class, IPrototype
public sealed class AbstractPrototypeIdListSerializer<T> : PrototypeIdListSerializer<T> where T : class, IPrototype, IInheritingPrototype
{
protected override PrototypeIdSerializer<T> PrototypeSerializer => new AbstractPrototypeIdSerializer<T>();
}

View File

@@ -9,7 +9,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype
{
public sealed class AbstractPrototypeIdSerializer<TPrototype> : PrototypeIdSerializer<TPrototype>
where TPrototype : class, IPrototype
where TPrototype : class, IPrototype, IInheritingPrototype
{
public override ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)

View File

@@ -11,7 +11,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set
{
public sealed class AbstractPrototypeIdHashSetSerializer<TPrototype> : PrototypeIdHashSetSerializer<TPrototype>
where TPrototype : class, IPrototype
where TPrototype : class, IPrototype, IInheritingPrototype
{
protected override PrototypeIdSerializer<TPrototype> PrototypeSerializer =>
new AbstractPrototypeIdSerializer<TPrototype>();

View File

@@ -0,0 +1,107 @@
using System;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.Prototypes;
namespace Robust.UnitTesting.Shared.Prototypes;
[TestFixture]
public sealed class MultiRootGraphTest
{
private const string Id1 = "id1";
private const string Id2 = "id2";
private const string Id3 = "id3";
private const string Id4 = "id4";
[Test]
public void AddAndRemoveRoot()
{
var graph = new MultiRootInheritanceGraph<string>();
graph.Add(Id1);
Assert.That(graph.RootNodes.Count, Is.EqualTo(1));
Assert.That(graph.RootNodes.Contains(Id1));
}
[Test]
public void AddAndRemoveRootAndChild()
{
var graph = new MultiRootInheritanceGraph<string>();
graph.Add(Id3, new []{Id1});
var children = graph.GetChildren(Id1);
Assert.That(children, Is.Not.Null);
Assert.That(children!.Count, Is.EqualTo(1));
Assert.That(children.Contains(Id3));
var parents = graph.GetParents(Id3);
Assert.That(parents, Is.Not.Null);
Assert.That(parents!.Count, Is.EqualTo(1));
Assert.That(parents.Contains(Id1));
}
[Test]
public void AddTwoParentsRemoveOne()
{
var graph = new MultiRootInheritanceGraph<string>();
graph.Add(Id3, new []{Id1, Id2});
var parents = graph.GetParents(Id3);
Assert.That(parents, Is.Not.Null);
Assert.That(parents!.Count, Is.EqualTo(2));
Assert.That(parents.Contains(Id1));
Assert.That(parents.Contains(Id2));
var children = graph.GetChildren(Id1);
Assert.That(children, Is.Not.Null);
Assert.That(children!.Count, Is.EqualTo(1));
Assert.That(children.Contains(Id3));
children = graph.GetChildren(Id2);
Assert.That(children, Is.Not.Null);
Assert.That(children!.Count, Is.EqualTo(1));
Assert.That(children.Contains(Id3));
Assert.That(graph.RootNodes.Count, Is.EqualTo(2));
Assert.That(graph.RootNodes.Contains(Id1));
Assert.That(graph.RootNodes.Contains(Id2));
}
[Test]
public void OneParentTwoChildrenRemoveParent()
{
var graph = new MultiRootInheritanceGraph<string>();
graph.Add(Id3, new []{Id1});
graph.Add(Id4, new []{Id1});
var parents = graph.GetParents(Id3);
Assert.That(parents, Is.Not.Null);
Assert.That(parents!.Count, Is.EqualTo(1));
Assert.That(parents.Contains(Id1));
parents = graph.GetParents(Id4);
Assert.That(parents, Is.Not.Null);
Assert.That(parents!.Count, Is.EqualTo(1));
Assert.That(parents.Contains(Id1));
var children = graph.GetChildren(Id1);
Assert.That(children, Is.Not.Null);
Assert.That(children!.Count, Is.EqualTo(2));
Assert.That(children.Contains(Id3));
Assert.That(children.Contains(Id4));
Assert.That(graph.RootNodes.Count, Is.EqualTo(1));
Assert.That(graph.RootNodes.Contains(Id1));
}
[Test]
public void CircleCheckTest()
{
var graph = new MultiRootInheritanceGraph<string>();
graph.Add(Id1, new []{Id2});
Assert.Throws<InvalidOperationException>(() => graph.Add(Id2, new []{Id1}));
graph.Add(Id2, new[] { Id3 });
Assert.Throws<InvalidOperationException>(() => graph.Add(Id3, new[] { Id1 }));
}
}

View File

@@ -1,3 +1,4 @@
using System;
using JetBrains.Annotations;
using NUnit.Framework;
using Robust.Server.GameObjects;
@@ -7,6 +8,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
namespace Robust.UnitTesting.Shared.Prototypes
{
@@ -114,6 +116,42 @@ namespace Robust.UnitTesting.Shared.Prototypes
Assert.That(prototype.Name, Is.EqualTo(LoadStringTestDummyId));
}
[Test]
public void TestCircleException()
{
string GenerateCircleTestPrototype(string id, string parent)
{
return $@"- type: circle
id: {id}
parent: {parent}";
}
manager.RegisterType(typeof(CircleTestPrototype));
var directCircle = $@"{GenerateCircleTestPrototype("1", "2")}
{GenerateCircleTestPrototype("2", "1")}";
Assert.Throws<PrototypeLoadException>(() => manager.LoadString(directCircle));
manager.RemoveString(directCircle);
var indirectCircle = $@"{GenerateCircleTestPrototype("1", "2")}
{GenerateCircleTestPrototype("2", "3")}
{GenerateCircleTestPrototype("3", "1")}";
Assert.Throws<PrototypeLoadException>(() => manager.LoadString(indirectCircle));
}
[Prototype("circle")]
private sealed class CircleTestPrototype : IPrototype, IInheritingPrototype
{
[IdDataField()]
public string ID { get; } = default!;
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<CircleTestPrototype>))]
public string[]? Parents { get; }
[AbstractDataField]
public bool Abstract { get; }
}
public enum YamlTestEnum : byte
{
Foo,

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
using NUnit.Framework;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown.Mapping;
using TerraFX.Interop.Windows;
namespace Robust.UnitTesting.Shared.Serialization;
[TestFixture]
public sealed class CompositionTest : SerializationTest
{
[DataDefinition]
private sealed class CompositionTestClass
{
[DataField("f1")] public int ChildValue;
[DataField("f2")] public int Parent1Value;
[DataField("f3")] public int Parent2Value;
[DataField("f4"), NeverPushInheritance]
public int NeverPushValueParent1;
[DataField("f5"), NeverPushInheritance]
public int NeverPushValueParent2;
}
[Test]
public void TestPushComposition()
{
var child = new MappingDataNode { { "f1", "1" } };
var parent1 = new MappingDataNode
{
{ "f1", "2" },
{ "f2", "1" },
{ "f4", "1" }
};
var parent2 = new MappingDataNode
{
{ "f1", "3" },
{ "f2", "2" },
{ "f3", "1" },
{ "f5", "1" }
};
var finalMapping = Serialization.PushComposition<CompositionTestClass, MappingDataNode>(new[] { parent1, parent2 }, child);
var val = Serialization.Read<CompositionTestClass>(finalMapping);
Assert.That(val.ChildValue, Is.EqualTo(1));
Assert.That(val.Parent1Value, Is.EqualTo(1));
Assert.That(val.Parent2Value, Is.EqualTo(1));
Assert.That(val.NeverPushValueParent1, Is.EqualTo(0));
Assert.That(val.NeverPushValueParent2, Is.EqualTo(0));
}
}