IRobustCloneable and generator support (#5692)

* Add IRobustCloneable and check for it in compnet generator.

* Redo compnetgenerator support; add test

* Disconnect client at end of test

* Actually test for client entities

* Cleanup

* Cleanup 2
This commit is contained in:
Tayrtahn
2025-06-27 14:37:43 -04:00
committed by GitHub
parent bd0579ed6d
commit a45b72a1c5
3 changed files with 199 additions and 1 deletions

View File

@@ -35,6 +35,7 @@ namespace Robust.Shared.CompNetworkGenerator
private const string GlobalDictionaryName = "global::System.Collections.Generic.Dictionary<TKey, TValue>";
private const string GlobalHashSetName = "global::System.Collections.Generic.HashSet<T>";
private const string GlobalListName = "global::System.Collections.Generic.List<T>";
private const string GlobalIRobustCloneableName = "global::Robust.Shared.Serialization.IRobustCloneable";
private static readonly SymbolDisplayFormat FullNullableFormat =
FullyQualifiedFormat.WithMiscellaneousOptions(IncludeNullableReferenceTypeModifier);
@@ -375,7 +376,39 @@ namespace Robust.Shared.CompNetworkGenerator
stateFields.Append($@"
public {networkedType} {name} = default!;");
if (IsCloneType(type))
if (ImplementsInterface(type, GlobalIRobustCloneableName))
{
getField = $"component.{name}";
cast = $"({castString})";
var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString;
if (nullable)
{
handleStateSetters.Append($@"
component.{name} = state.{name} == null ? null! : state.{name}.Clone();");
deltaHandleFields.Append($@"
var {name}Value = {cast} {fieldHandleValue};
if ({name}Value == null)
component.{name} = null!;
else
component.{name} = {nullCast}({name}Value.Clone());");
shallowClone.Append($@"
{name} = this.{name},");
deltaApply.Add($"fullState.{name} = {name} == null ? null! : {name}.Clone();");
}
else
{
handleStateSetters.Append($@"
component.{name} = state.{name}.Clone();");
deltaHandleFields.Append($@"
component.{name} = {cast}({fieldHandleValue}.Clone());");
shallowClone.Append($@"
{name} = this.{name},");
deltaApply.Add($"fullState.{name} = {name}.Clone();");
}
}
else if (IsCloneType(type))
{
getField = $"component.{name}";
cast = $"({castString})";
@@ -758,5 +791,19 @@ public partial class {componentName}{deltaInterface}
_ => false
};
}
private static bool ImplementsInterface(ITypeSymbol type, string interfaceName)
{
foreach (var interfaceType in type.AllInterfaces)
{
if (interfaceType.ToDisplayString(FullyQualifiedFormat).Contains(interfaceName)
|| interfaceType.ConstructedFrom.ToDisplayString(FullyQualifiedFormat).Contains(interfaceName))
{
return true;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,19 @@
namespace Robust.Shared.Serialization;
/// <summary>
/// Implementers of this interface will have their <see cref="Clone"/> method
/// called when generating component states. This can be useful for reference types
/// used as datafields to make copies of values instead of references.
/// </summary>
/// <typeparam name="T">
/// Type returned by the <see cref="Clone"/> method.
/// This should probably be the same Type as the implementer.
/// </typeparam>
public interface IRobustCloneable<T>
{
/// <summary>
/// Returns a new instance of <typeparamref name="T"/> with the same values as this instance.
/// </summary>
/// <returns>A new instance of <typeparamref name="T"/> with the same values as this instance.</returns>
T Clone();
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[Serializable, NetSerializable]
[DataDefinition]
public sealed partial class RobustCloneableTestClass : IRobustCloneable<RobustCloneableTestClass>
{
[DataField]
public int IntValue;
public RobustCloneableTestClass Clone()
{
return new RobustCloneableTestClass
{
IntValue = IntValue
};
}
}
[Serializable, NetSerializable]
[DataDefinition]
public partial struct RobustCloneableTestStruct : IRobustCloneable<RobustCloneableTestStruct>
{
[DataField]
public int IntValue;
public RobustCloneableTestStruct Clone()
{
return new RobustCloneableTestStruct
{
IntValue = IntValue
};
}
}
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class RobustCloneableTestComponent : Component
{
[DataField, AutoNetworkedField]
public RobustCloneableTestClass TestClass = new();
[DataField, AutoNetworkedField]
public RobustCloneableTestStruct TestStruct = new();
[DataField, AutoNetworkedField]
public RobustCloneableTestStruct? NullableTestStruct;
}
public sealed class RobustCloneableTest() : RobustIntegrationTest
{
[Test]
public async Task TestClone()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(server.WaitIdleAsync(), client.WaitIdleAsync());
var sEntMan = server.EntMan;
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var cEntMan = client.EntMan;
var cNetMan = client.ResolveDependency<IClientNetManager>();
MapId mapId = default;
await server.WaitPost(() =>
{
server.System<SharedMapSystem>().CreateMap(out mapId);
var coords = new MapCoordinates(0, 0, mapId);
var uid = sEntMan.SpawnEntity(null, coords);
var comp = sEntMan.EnsureComponent<RobustCloneableTestComponent>(uid);
comp.TestClass.IntValue = 50;
comp.TestStruct.IntValue = 60;
comp.NullableTestStruct = new() { IntValue = 70 };
});
// Connect client.
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() => cNetMan.ClientConnect(null!, 0, null!));
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
await RunTicks();
EntityUid player = default;
await server.WaitPost(() =>
{
var coords = new MapCoordinates(0, 0, mapId);
player = sEntMan.SpawnEntity(null, coords);
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
sPlayerMan.JoinGame(session);
});
await RunTicks();
await server.WaitAssertion(() =>
{
Assert.That(cNetMan.IsConnected, Is.True);
var ents = cEntMan.AllEntities<RobustCloneableTestComponent>().ToList();
Assert.That(ents, Has.Count.EqualTo(1));
var testEnt = ents[0];
Assert.That(testEnt.Comp.TestClass.IntValue, Is.EqualTo(50));
Assert.That(testEnt.Comp.TestStruct.IntValue, Is.EqualTo(60));
Assert.That(testEnt.Comp.NullableTestStruct, Is.Not.Null);
Assert.That(testEnt.Comp.NullableTestStruct!.Value.IntValue, Is.EqualTo(70));
});
// Disconnect client
await client.WaitPost(() => cNetMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}