Skip to content

Commit 45f3caa

Browse files
Fastpath EntityManager.IsDefault (space-wizards#6735)
* Slim it down Had the spawn mess included. * fixes * build fix
1 parent 9cd8401 commit 45f3caa

9 files changed

Lines changed: 461 additions & 23 deletions

File tree

Robust.Serialization.Generator/Generator.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ private static (string, string)? GenerateForDataDefinition(
198198
199199
{{GetWriter(definition)}}
200200
201+
{{GetEquality(definition)}}
202+
201203
{{GetValidator(definition)}}
202204
203205
{{GetFieldDefinitions(definition)}}
@@ -1091,6 +1093,95 @@ public static void Write(
10911093
""";
10921094
}
10931095

1096+
private static string GetEquality(DataDefinition definition)
1097+
{
1098+
var builder = new StringBuilder();
1099+
1100+
if (definition.GetFirstDataDefinitionBaseType() is { } baseType)
1101+
{
1102+
builder.AppendLine($$"""
1103+
if (!{{baseType.ToDisplayString()}}.AreEqual(left, right, serialization, context))
1104+
return false;
1105+
""");
1106+
}
1107+
1108+
foreach (var field in definition.Fields)
1109+
{
1110+
if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default))
1111+
continue;
1112+
1113+
if (field.Attribute.ReadOnly)
1114+
continue;
1115+
1116+
var fieldType = field.Type.ToDisplayString();
1117+
if (IsMultidimensionalArray(field.Type))
1118+
fieldType = fieldType.Replace("*", "");
1119+
1120+
if (field.Type.NullableAnnotation == NullableAnnotation.Annotated &&
1121+
!fieldType.EndsWith("?"))
1122+
{
1123+
fieldType += "?";
1124+
}
1125+
1126+
var equalityExpression = GetFieldEqualityExpression(field, fieldType);
1127+
1128+
builder.AppendLine($$"""
1129+
if (!{{equalityExpression}})
1130+
return false;
1131+
""");
1132+
}
1133+
1134+
builder.AppendLine("return true;");
1135+
1136+
return $$"""
1137+
public static bool AreEqual(
1138+
{{definition.GenericTypeName}} left,
1139+
{{definition.GenericTypeName}} right,
1140+
ISerializationManager serialization,
1141+
ISerializationContext? context = null)
1142+
{
1143+
{{builder}}
1144+
}
1145+
""";
1146+
}
1147+
1148+
private static string GetFieldEqualityExpression(DataField field, string fieldType)
1149+
{
1150+
var fieldName = field.Symbol.Name;
1151+
1152+
if (TryGetFastHashSetEquality(field.Type, $"left.{fieldName}", $"right.{fieldName}", out var hashSetEquality))
1153+
return hashSetEquality;
1154+
1155+
if (CanFieldUseDirectEquality(field))
1156+
return $"EqualityComparer<{fieldType}>.Default.Equals(left.{fieldName}, right.{fieldName})";
1157+
1158+
return $"serialization.DataFieldEquals<{fieldType}>(left.{fieldName}, right.{fieldName}, context)";
1159+
}
1160+
1161+
private static bool CanFieldUseDirectEquality(DataField field)
1162+
{
1163+
return CanTypeBeCopiedByValue(field.Type);
1164+
}
1165+
1166+
private static bool TryGetFastHashSetEquality(
1167+
ITypeSymbol type,
1168+
string leftAccess,
1169+
string rightAccess,
1170+
[NotNullWhen(true)] out string? equality)
1171+
{
1172+
equality = null;
1173+
1174+
if (type.WithNullableAnnotation(NullableAnnotation.None) is not INamedTypeSymbol namedType ||
1175+
!IsGenericCollectionType(namedType, "HashSet", 1))
1176+
{
1177+
return false;
1178+
}
1179+
1180+
equality =
1181+
$"ReferenceEquals({leftAccess}, {rightAccess}) || ({leftAccess} != null && {rightAccess} != null && {leftAccess}.SetEquals({rightAccess}))";
1182+
return true;
1183+
}
1184+
10941185
private static string GetFieldDefinitions(DataDefinition definition)
10951186
{
10961187
var builder = new StringBuilder();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Collections.Generic;
2+
using NUnit.Framework;
3+
using Robust.Shared.GameObjects;
4+
using Robust.Shared.Map;
5+
using Robust.Shared.Serialization.Manager;
6+
using Robust.Shared.Spawners;
7+
using Robust.UnitTesting.Server;
8+
9+
namespace Robust.UnitTesting.Shared.GameObjects;
10+
11+
[TestFixture]
12+
internal sealed partial class EntityManagerIsDefaultTests
13+
{
14+
private const string PrototypeId = "IsDefaultTestEntity";
15+
16+
private const string Prototypes = $"""
17+
- type: entity
18+
id: {PrototypeId}
19+
name: test entity
20+
description: test description
21+
components:
22+
- type: TimedDespawn
23+
lifetime: 5
24+
""";
25+
26+
[Test]
27+
public void IsDefaultComparesAgainstPrototypeData()
28+
{
29+
var sim = RobustServerSimulation
30+
.NewSimulation()
31+
.RegisterComponents(factory =>
32+
{
33+
factory.RegisterClass<TimedDespawnComponent>();
34+
factory.RegisterClass<PlacementReplacementComponent>();
35+
})
36+
.RegisterPrototypes(prototypes =>
37+
{
38+
prototypes.LoadString(Prototypes);
39+
prototypes.ResolveResults();
40+
})
41+
.InitializeInstance();
42+
43+
var entMan = (EntityManager) sim.Resolve<IEntityManager>();
44+
var map = sim.CreateMap().Uid;
45+
var coords = new EntityCoordinates(map, default);
46+
var entity = entMan.SpawnEntity(PrototypeId, coords);
47+
48+
Assert.That(entMan.IsDefault(entity), Is.True);
49+
50+
entMan.GetComponent<TimedDespawnComponent>(entity).Lifetime = 10;
51+
Assert.That(entMan.IsDefault(entity), Is.False);
52+
Assert.That(entMan.IsDefault(entity, new HashSet<string> { "TimedDespawn" }), Is.True);
53+
54+
entMan.GetComponent<TimedDespawnComponent>(entity).Lifetime = 5;
55+
entMan.AddComponent<PlacementReplacementComponent>(entity);
56+
Assert.That(entMan.IsDefault(entity), Is.False);
57+
}
58+
59+
[Test]
60+
public void DataFieldEqualsComparesHashSetsAsSets()
61+
{
62+
var sim = RobustServerSimulation
63+
.NewSimulation()
64+
.InitializeInstance();
65+
66+
var serialization = sim.Resolve<ISerializationManager>();
67+
68+
Assert.That(serialization.DataFieldEquals(
69+
new HashSet<int> { 1, 2, 3 },
70+
new HashSet<int> { 3, 2, 1 }), Is.True);
71+
72+
Assert.That(serialization.DataFieldEquals(
73+
new HashSet<int> { 1, 2, 3 },
74+
new HashSet<int> { 1, 2, 4 }), Is.False);
75+
}
76+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using NUnit.Framework;
2+
using Robust.Shared.Serialization.Manager.Attributes;
3+
4+
namespace Robust.UnitTesting.Shared.Serialization;
5+
6+
[TestFixture]
7+
internal sealed class EqualityTest : OurSerializationTest
8+
{
9+
[Test]
10+
public void DataFieldEqualsUsesRuntimeDataDefinition()
11+
{
12+
EqualityBase left = new EqualityDerived { BaseValue = 1, DerivedValue = 2 };
13+
EqualityBase right = new EqualityDerived { BaseValue = 1, DerivedValue = 3 };
14+
15+
Assert.That(Serialization.DataFieldEquals(left, right), Is.False);
16+
17+
((EqualityDerived) right).DerivedValue = 2;
18+
Assert.That(Serialization.DataFieldEquals(left, right), Is.True);
19+
}
20+
21+
[Test]
22+
public void DataFieldEqualsUsesSerializedFieldsInsteadOfObjectEquals()
23+
{
24+
var left = new EqualityOverride { Value = 1 };
25+
var right = new EqualityOverride { Value = 2 };
26+
27+
Assert.That(left.Equals(right), Is.True);
28+
Assert.That(Serialization.DataFieldEquals(left, right), Is.False);
29+
Assert.That(Serialization.DataFieldEquals<EqualityOverride[]>([left], [right]), Is.False);
30+
}
31+
}
32+
33+
[DataDefinition]
34+
public abstract partial class EqualityBase
35+
{
36+
[DataField]
37+
public int BaseValue;
38+
}
39+
40+
[DataDefinition]
41+
public sealed partial class EqualityDerived : EqualityBase
42+
{
43+
[DataField]
44+
public int DerivedValue;
45+
}
46+
47+
[DataDefinition]
48+
public sealed partial class EqualityOverride
49+
{
50+
[DataField]
51+
public int Value;
52+
53+
public override bool Equals(object? obj)
54+
{
55+
return obj is EqualityOverride;
56+
}
57+
58+
public override int GetHashCode()
59+
{
60+
return 0;
61+
}
62+
}

Robust.Shared/GameObjects/EntityManager.cs

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,11 @@ public bool IsDefault(EntityUid uid, ICollection<string>? ignoredComps = null)
180180
return false;
181181
}
182182

183-
var protoData = PrototypeManager.GetPrototypeData(prototype);
184183
var comps = _entCompIndex[uid];
184+
var hasIgnoredComps = ignoredComps is { Count: > 0 };
185185

186186
// Fast check if the component counts match.
187-
// Note that transform and metadata are not included in the prototype data.
188-
if (protoData.Count + 2 != comps.Count)
187+
if (prototype.Components.Count + 2 != comps.Count)
189188
return false;
190189

191190
foreach (var component in comps)
@@ -195,34 +194,19 @@ public bool IsDefault(EntityUid uid, ICollection<string>? ignoredComps = null)
195194

196195
var compType = component.GetType();
197196

198-
if (compType == typeof(TransformComponent) || compType == typeof(MetaDataComponent))
197+
if (compType == _xformReg.Type || compType == _metaReg.Type)
199198
continue;
200199

201-
var compName = _componentFactory.GetComponentName(compType);
200+
var compReg = _componentFactory.GetRegistration(compType);
202201

203-
if (ignoredComps?.Contains(compName) == true)
202+
if (hasIgnoredComps && ignoredComps!.Contains(compReg.Name))
204203
continue;
205204

206205
// If the component isn't on the prototype then it's custom.
207-
if (!protoData.TryGetValue(compName, out var protoMapping))
206+
if (!prototype.Components.TryGetValue(compReg.Name, out var protoEntry))
208207
return false;
209208

210-
MappingDataNode compMapping;
211-
try
212-
{
213-
compMapping = _serManager.WriteValueAs<MappingDataNode>(compType, component, alwaysWrite: true, context: _context);
214-
}
215-
catch (Exception e)
216-
{
217-
_sawmill.Error($"Failed to serialize {compName} component of entity prototype {prototype.ID}. Exception: {e.Message}");
218-
#if !EXCEPTION_TOLERANCE
219-
throw;
220-
#else
221-
return false;
222-
#endif
223-
}
224-
225-
if (compMapping.AnyExcept(protoMapping))
209+
if (!_serManager.DataFieldEquals(compReg.Type, component, protoEntry.Component, _context))
226210
return false;
227211
}
228212

Robust.Shared/Serialization/ISerializationGenerated.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ static virtual void Write(
6464
throw new NotImplementedException();
6565
}
6666

67+
[Obsolete("Used only in serialization source generation internally")]
68+
static virtual bool AreEqual(
69+
T left,
70+
T right,
71+
ISerializationManager serialization,
72+
ISerializationContext? context = null)
73+
{
74+
throw new NotImplementedException();
75+
}
76+
6777
/// <seealso cref="ISerializationManager.ValidateNode"/>
6878
[Obsolete("Use ISerializationManager.ValidateNode instead")]
6979
static virtual void Validate(

Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal sealed class DataDefinition<T> : DataDefinition where T : ISerializatio
3131
internal readonly PopulateDelegateSignature<T> Populate;
3232
internal readonly SerializeDelegateSignature<T> Serialize;
3333
internal readonly CopyDelegateSignature<T> CopyTo;
34+
internal readonly EqualDelegateSignature<T> AreEqual;
3435

3536
internal override PopulateDelegateSignature<object> PopulateObj { get; init; }
3637
#pragma warning restore CS0618
@@ -95,6 +96,7 @@ internal DataDefinition(SerializationManager manager, bool isRecord)
9596
Populate = T.Read;
9697
Serialize = T.Write;
9798
CopyTo = (source, ref target, ctx, context) => source.Copy(ref target, manager, ctx, context); // TODO source gen this one too!
99+
AreEqual = T.AreEqual;
98100
FieldValidators = T.Validate;
99101
InstantiateObj = T.StaticInstantiateObject;
100102
#pragma warning restore CS0618

Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ internal delegate void SerializeDelegateSignature<T>(
3434
ImmutableDictionary<string, object?> defaultValues
3535
);
3636

37+
[Obsolete("Used only in source generation")]
38+
internal delegate bool EqualDelegateSignature<T>(
39+
T left,
40+
T right,
41+
ISerializationManager serialization,
42+
ISerializationContext? context
43+
);
44+
3745
[Obsolete("Used only in source generation")]
3846
internal delegate void ValidateAllFieldsDelegate(
3947
Dictionary<string, ValidationNode> nodes,

Robust.Shared/Serialization/Manager/ISerializationManager.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,21 @@ T Read<T, TNode, TReader>(
266266

267267
#endregion
268268

269+
#region Equality
270+
271+
/// <summary>
272+
/// Compares serialized data fields structurally, including generated data definitions and collections.
273+
/// </summary>
274+
bool DataFieldEquals<T>(T left, T right, ISerializationContext? context = null);
275+
276+
/// <summary>
277+
/// Compares serialized data fields structurally when their type is only known at runtime.
278+
/// </summary>
279+
[PreferGenericVariant]
280+
bool DataFieldEquals(Type type, object? left, object? right, ISerializationContext? context = null);
281+
282+
#endregion
283+
269284
#region Copy
270285

271286
/// <summary>

0 commit comments

Comments
 (0)