diff --git a/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/OrganizedListSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/OrganizedListSerializerTest.cs new file mode 100644 index 00000000000..3a86654ce2c --- /dev/null +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/OrganizedListSerializerTest.cs @@ -0,0 +1,245 @@ +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Utility; +using Robust.UnitTesting.Shared; + +namespace Robust.Shared.IntegrationTests.Serialization.TypeSerializers.Custom; + +/// +/// Tests to ensure that properly organizes a very simple list +/// +[TestFixture] +[TestOf(typeof(OrganizedListSerializer<>))] +internal sealed partial class OrganizedListSerializerTest : OurRobustUnitTest +{ + private const string ParentProto = "ProtoA"; + + private const string ChildProto = "ProtoB"; + + private List _key0Values = [2, 4, 8]; + + private List _key1Values = [1, 2, 2, 3, 4, 8]; + + private List _key2Values = [1, 3, 9]; + + private List _key1ValuesB = [1, 2, 3]; + + private List _key2ValuesB = [2, 4, 8]; + + protected override Type[] ExtraComponents => new[] {typeof(TestComponent)}; + + private static readonly string Prototypes = $@" +- type: entity + id: {ParentProto} + components: + - type: Test + listA: + - key: 1 + values: + - 2 + - 4 + - 8 + - key: 2 + values: + - 1 + - 3 + - 9 + listB: + - key: 1 + values: + - 2 + - 4 + - 8 + - key: 2 + values: + - 1 + - 3 + - 9 + +- type: entity + parent: [{ParentProto}] + id: {ChildProto} + components: + - type: Test + listA: + - key: 1 + values: + - 1 + - 2 + - 3 + - key: 0 + values: + - 2 + - 4 + - 8 + listB: + - key: 1 + values: + - 1 + - 2 + - 3 + - key: 2 + values: + - 2 + - 4 + - 8 +"; + + [Test] + public void OrganizedListTest() + { + var serializationManager = IoCManager.Resolve(); + serializationManager.Initialize(); + var prototypeManager = IoCManager.Resolve(); + + // We use entity prototypes since Serialization recognizes them without fuss, and we need complex YAML behavior for this test. + prototypeManager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); + prototypeManager.LoadString(Prototypes); + prototypeManager.ResolveResults(); + + var entityManager = IoCManager.Resolve(); + entityManager.System().CreateMap(out var mapId); + + var coordinates = new MapCoordinates(0, 0, mapId); + + var parentEntity = entityManager.SpawnEntity(ParentProto, coordinates); + var childEntity = entityManager.SpawnEntity(ChildProto, coordinates); + + Assert.That(entityManager.TryGetComponent(parentEntity, out var parentComp)); + Assert.That(entityManager.TryGetComponent(childEntity, out var childComponent)); + + // Instead of 4 items, two of them should've combined into one. + Assert.That(childComponent!.ListA.Count == 3, $"Child Component's AList failed to inherit properly. List contained {childComponent!.ListA.Count} entries."); + + var index = 0; + // Ensure that the lists inherited correctly, and became organized and squashed correctly + foreach (var obj in childComponent.ListA) + { + // Items should be ordered! + Assert.That(obj.Key == index); + switch (obj.Key) + { + case 0: + // Should be an unmodified list. + Assert.That(obj.Values.SequenceEqual(_key0Values)); + break; + case 1: + for (var i = obj.Values.Count - 1; i >= 0; i--) + { + var item = obj.Values[i]; + Assert.That(obj.Values.Count == _key1Values.Count); + for (var j = _key1Values.Count - 1; j >= 0; j--) + { + if (_key1Values[j] != item) + continue; + + _key1Values.RemoveSwap(j); + obj.Values.RemoveSwap(i); + break; + } + } + Assert.That(obj.Values.Count == 0 && _key1Values.Count == 0); + break; + case 2: + Assert.That(obj.Values.SequenceEqual(_key2Values)); + break; + default: + Assert.Fail($"Key was an unexpected value {obj.Key}"); + break; + } + + index++; + } + + // We alternate between keys 1 and 2 :P + index = 1; + // Ensure that this list followed normal push inheritance rules + for (var i = 0; i < childComponent.ListB.Count; i++) + { + // The greatest code known to man :) + var childList = childComponent.ListB[i]; + switch (i) + { + case 0: + Assert.That(childList.Values.Count == _key1ValuesB.Count); + for (var j = 0; j < childList.Values.Count; j++) + { + Assert.That(childList.Values[j] == _key1ValuesB[j]); + } + index++; + break; + case 1: + Assert.That(childList.Values.Count == _key2ValuesB.Count); + for (var j = 0; j < childList.Values.Count; j++) + { + Assert.That(childList.Values[j] == _key2ValuesB[j]); + } + index--; + break; + case 2: + var parentList = parentComp!.ListB[i - 2]; + Assert.That(childList.Values.Count == parentList.Values.Count); + for (var j = 0; j < childList.Values.Count; j++) + { + Assert.That(childList.Values[j] == parentList.Values[j]); + } + index++; + break; + case 3: + parentList = parentComp!.ListB[i - 2]; + Assert.That(childList.Values.Count == parentList.Values.Count); + for (var j = 0; j < childList.Values.Count; j++) + { + Assert.That(childList.Values[j] == parentList.Values[j]); + } + index--; + break; + default: + Assert.Fail($"ListB contained more elements than expected: {childComponent.ListB.Count}"); + break; + } + } + } +} + +[DataDefinition] +public sealed partial class ComplexTestObject : IOrganizeableCollection +{ + [DataField(required: true)] + public int Key; + + [DataField(required: true)] + public List Values = new (); + + public int CompareTo(ComplexTestObject? other) + { + return Key.CompareTo(other?.Key); + } + + public bool Equals(ComplexTestObject? other) + { + return Key.Equals(other?.Key); + } + + public void Insert(ComplexTestObject other) + { + Values.AddRange(other.Values) ; + } +} + +public sealed partial class TestComponent : Component +{ + [DataField(customTypeSerializer: typeof(OrganizedListSerializer))] + [AlwaysPushInheritance] + public List ListA = new (); + + [DataField] + [AlwaysPushInheritance] + public List ListB = new (); +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/OrganizedListSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/OrganizedListSerializer.cs new file mode 100644 index 00000000000..1ede09e2311 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/OrganizedListSerializer.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +/// +/// A highly complex serializer that allows you to serialize a list with complex objects +/// as if it were an organized dictionary. +/// Should be used on a Datafield with the +/// +/// An object which implements +public sealed class OrganizedListSerializer : BaseTypeSerializer, ITypeSerializer, SequenceDataNode> where T : IOrganizeableCollection +{ + public ValidationNode Validate(ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + var list = new List(node.Count); + foreach (var elem in node.Sequence) + { + list.Add(serializationManager.ValidateNode(elem, context)); + } + + return new ValidatedSequenceNode(list); + } + + public List Read(ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate>? instanceProvider = null) + { + if (instanceProvider != null) + Log.Warning($"Provided value to a Read-call for a {nameof(List)}. Ignoring..."); + + var list = new List(node.Count); + + foreach (var sequence in node.Sequence) + { + list.Add(serializationManager.Read(sequence, hookCtx, context)); + } + + for (var i = 0; i < list.Count; i++) + { + var item = list[i]; + var modified = false; + for (var j = list.Count - 1; j > i; j--) + { + var item2 = serializationManager.Read(node.Sequence[j], hookCtx, context); + if (item.Equals(item2)) + { + // Note that this does not organize the nested list items. + // If you deem that necessary, then feel free to write additional logic for that :P + // You'd likely want to do the equality checking at the read step rather than here if you were going to. + list.RemoveAt(j); + item.Insert(item2); + modified = true; + } + } + + if (modified) + list[i] = item; + } + + list.Sort(); + return list; + } + + public DataNode Write(ISerializationManager serializationManager, + List value, + IDependencyCollection dependencies, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + var sequence = new SequenceDataNode(value.Count); + + foreach (var elem in value) + { + sequence.Add(serializationManager.WriteValue(elem, alwaysWrite, context)); + } + + return sequence; + } +} + +/// +/// Interface for an object which can be organized and sorted internally. +/// +/// +public interface IOrganizeableCollection : IComparable, IEquatable +{ + /// + /// Combines these two objects together, typically called by a + /// + /// The other object we are combining into this one. + void Insert(T other); +}