PrototypeSaveTest.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #nullable enable
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Content.Shared.Coordinates;
  5. using Robust.Shared.GameObjects;
  6. using Robust.Shared.IoC;
  7. using Robust.Shared.Map;
  8. using Robust.Shared.Map.Components;
  9. using Robust.Shared.Physics;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Serialization;
  12. using Robust.Shared.Serialization.Manager;
  13. using Robust.Shared.Serialization.Markdown;
  14. using Robust.Shared.Serialization.Markdown.Mapping;
  15. using Robust.Shared.Serialization.Markdown.Validation;
  16. using Robust.Shared.Serialization.Markdown.Value;
  17. using Robust.Shared.Serialization.TypeSerializers.Interfaces;
  18. namespace Content.IntegrationTests.Tests;
  19. /// <summary>
  20. /// This test ensure that when an entity prototype is spawned into an un-initialized map, its component data is not
  21. /// modified during init. I.e., when the entity is saved to the map, its data is simply the default prototype data (ignoring transform component).
  22. /// </summary>
  23. /// <remarks>
  24. /// If you are here because this test is failing on your PR, then one easy way of figuring out how to fix the prototype is to just
  25. /// spawn it into a new empty map and seeing what the map yml looks like.
  26. /// </remarks>
  27. [TestFixture]
  28. public sealed class PrototypeSaveTest
  29. {
  30. [Test]
  31. public async Task UninitializedSaveTest()
  32. {
  33. await using var pair = await PoolManager.GetServerClient();
  34. var server = pair.Server;
  35. var entityMan = server.ResolveDependency<IEntityManager>();
  36. var prototypeMan = server.ResolveDependency<IPrototypeManager>();
  37. var seriMan = server.ResolveDependency<ISerializationManager>();
  38. var compFact = server.ResolveDependency<IComponentFactory>();
  39. var mapSystem = server.System<SharedMapSystem>();
  40. var prototypes = new List<EntityPrototype>();
  41. EntityUid uid;
  42. await pair.CreateTestMap(false, "FloorSteel"); // Wires n such disable ambiance while under the floor
  43. var mapId = pair.TestMap.MapId;
  44. var grid = pair.TestMap.Grid;
  45. await server.WaitRunTicks(5);
  46. //Generate list of non-abstract prototypes to test
  47. foreach (var prototype in prototypeMan.EnumeratePrototypes<EntityPrototype>())
  48. {
  49. if (prototype.Abstract)
  50. continue;
  51. if (pair.IsTestPrototype(prototype))
  52. continue;
  53. // Yea this test just doesn't work with this, it parents a grid to another grid and causes game logic to explode.
  54. if (prototype.Components.ContainsKey("MapGrid"))
  55. continue;
  56. // Currently mobs and such can't be serialized, but they aren't flagged as serializable anyways.
  57. if (!prototype.MapSavable)
  58. continue;
  59. if (prototype.SetSuffix == "DEBUG")
  60. continue;
  61. prototypes.Add(prototype);
  62. }
  63. var context = new TestEntityUidContext();
  64. await server.WaitAssertion(() =>
  65. {
  66. Assert.That(!mapSystem.IsInitialized(mapId));
  67. var testLocation = grid.Owner.ToCoordinates();
  68. Assert.Multiple(() =>
  69. {
  70. //Iterate list of prototypes to spawn
  71. foreach (var prototype in prototypes)
  72. {
  73. uid = entityMan.SpawnEntity(prototype.ID, testLocation);
  74. context.Prototype = prototype;
  75. // get default prototype data
  76. Dictionary<string, MappingDataNode> protoData = new();
  77. try
  78. {
  79. context.WritingReadingPrototypes = true;
  80. foreach (var (compType, comp) in prototype.Components)
  81. {
  82. context.WritingComponent = compType;
  83. protoData.Add(compType, seriMan.WriteValueAs<MappingDataNode>(comp.Component.GetType(), comp.Component, alwaysWrite: true, context: context));
  84. }
  85. context.WritingComponent = string.Empty;
  86. context.WritingReadingPrototypes = false;
  87. }
  88. catch (Exception e)
  89. {
  90. Assert.Fail($"Failed to convert prototype {prototype.ID} into yaml. Exception: {e.Message}");
  91. continue;
  92. }
  93. var comps = new HashSet<IComponent>(entityMan.GetComponents(uid));
  94. var compNames = new HashSet<string>(comps.Count);
  95. foreach (var component in comps)
  96. {
  97. var compType = component.GetType();
  98. var compName = compFact.GetComponentName(compType);
  99. compNames.Add(compName);
  100. if (compType == typeof(MetaDataComponent) || compType == typeof(TransformComponent) || compType == typeof(FixturesComponent))
  101. continue;
  102. MappingDataNode compMapping;
  103. try
  104. {
  105. context.WritingComponent = compName;
  106. compMapping = seriMan.WriteValueAs<MappingDataNode>(compType, component, alwaysWrite: true, context: context);
  107. }
  108. catch (Exception e)
  109. {
  110. Assert.Fail($"Failed to serialize {compName} component of entity prototype {prototype.ID}. Exception: {e.Message}");
  111. continue;
  112. }
  113. if (protoData.TryGetValue(compName, out var protoMapping))
  114. {
  115. var diff = compMapping.Except(protoMapping);
  116. if (diff != null && diff.Children.Count != 0)
  117. Assert.Fail($"Prototype {prototype.ID} modifies component on spawn: {compName}. Modified yaml:\n{diff}");
  118. }
  119. else
  120. {
  121. Assert.Fail($"Prototype {prototype.ID} gains a component on spawn: {compName}");
  122. }
  123. }
  124. // An entity may also remove components on init -> check no components are missing.
  125. foreach (var (compType, comp) in prototype.Components)
  126. {
  127. Assert.That(compNames, Does.Contain(compType), $"Prototype {prototype.ID} removes component {compType} on spawn.");
  128. }
  129. if (!entityMan.Deleted(uid))
  130. entityMan.DeleteEntity(uid);
  131. }
  132. });
  133. });
  134. await pair.CleanReturnAsync();
  135. }
  136. public sealed class TestEntityUidContext : ISerializationContext,
  137. ITypeSerializer<EntityUid, ValueDataNode>
  138. {
  139. public SerializationManager.SerializerProvider SerializerProvider { get; }
  140. public bool WritingReadingPrototypes { get; set; }
  141. public string WritingComponent = string.Empty;
  142. public EntityPrototype? Prototype;
  143. public TestEntityUidContext()
  144. {
  145. SerializerProvider = new();
  146. SerializerProvider.RegisterSerializer(this);
  147. }
  148. ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
  149. ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
  150. {
  151. return new ValidatedValueNode(node);
  152. }
  153. public DataNode Write(ISerializationManager serializationManager, EntityUid value,
  154. IDependencyCollection dependencies, bool alwaysWrite = false,
  155. ISerializationContext? context = null)
  156. {
  157. if (WritingComponent != "Transform" && Prototype?.HideSpawnMenu == false)
  158. {
  159. // Maybe this will be necessary in the future, but at the moment it just indicates that there is some
  160. // issue, like a non-nullable entityUid data-field. If a component MUST have an entity uid to work with,
  161. // then the prototype very likely has to be a no-spawn entity that is never meant to be directly spawned.
  162. Assert.Fail($"Uninitialized entities should not be saving entity Uids. Component: {WritingComponent}. Prototype: {Prototype.ID}");
  163. }
  164. return new ValueDataNode(value.ToString());
  165. }
  166. EntityUid ITypeReader<EntityUid, ValueDataNode>.Read(ISerializationManager serializationManager,
  167. ValueDataNode node,
  168. IDependencyCollection dependencies,
  169. SerializationHookContext hookCtx,
  170. ISerializationContext? context, ISerializationManager.InstantiationDelegate<EntityUid>? instanceProvider)
  171. {
  172. return EntityUid.Parse(node.Value);
  173. }
  174. }
  175. }