using System;
using System.Collections.Generic;
namespace SceneTests.Components;
///
/// Tests for Component.Clone.cs - how component members are transferred when a
/// GameObject is cloned: value types are copied directly (via the compiled
/// member-copy delegates), reference types are deep-copied through JSON,
/// ICloneable types use their own Clone(), and references to components inside
/// the cloned hierarchy are remapped onto the clones.
///
[TestClass]
[DoNotParallelize]
public class ComponentCloneTest : SceneTest
{
///
/// Value-type properties and fields (and strings) are copied from the
/// original onto the clone, and the copy goes in the right direction - the
/// original is never written to.
///
[TestMethod]
public void ValueMembersAreCopiedToClone()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
comp.Number = 42;
comp.Offset = new Vector3( 1, 2, 3 );
comp.Title = "hello";
var clone = go.Clone();
var cloneComp = clone.GetComponent();
Assert.IsNotNull( cloneComp );
Assert.AreEqual( 42, cloneComp.Number );
Assert.AreEqual( new Vector3( 1, 2, 3 ), cloneComp.Offset );
Assert.AreEqual( "hello", cloneComp.Title );
cloneComp.Number = 7;
Assert.AreEqual( 42, comp.Number );
}
///
/// Mutable reference-type properties like lists must be deep-copied, so
/// mutating the clone's list doesn't change the original.
///
[TestMethod]
public void ListPropertyIsDeepCopied()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
comp.Values = new List { 1, 2, 3 };
var clone = go.Clone();
var cloneComp = clone.GetComponent();
Assert.AreNotSame( comp.Values, cloneComp.Values );
CollectionAssert.AreEqual( comp.Values, cloneComp.Values );
cloneComp.Values.Add( 4 );
Assert.AreEqual( 3, comp.Values.Count );
}
///
/// A member type that declares its own Clone() (ICloneable) is cloned via
/// that method, producing an equal but distinct instance.
///
[TestMethod]
public void ICloneableMemberIsClonedViaCloneMethod()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
comp.Bag = new CloneableBag { Value = 5 };
var clone = go.Clone();
var cloneComp = clone.GetComponent();
Assert.IsNotNull( cloneComp.Bag );
Assert.AreNotSame( comp.Bag, cloneComp.Bag );
Assert.AreEqual( 5, cloneComp.Bag.Value );
}
///
/// Null reference members stay null on the clone - cloning must not
/// fabricate instances.
///
[TestMethod]
public void NullMembersStayNull()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
comp.Title = null;
comp.Bag = null;
var clone = go.Clone();
var cloneComp = clone.GetComponent();
Assert.IsNull( cloneComp.Title );
Assert.IsNull( cloneComp.Bag );
}
///
/// A component property referencing a component inside the cloned hierarchy
/// is remapped to the cloned instance - here a self-reference must point at
/// the clone itself, never back at the original.
///
[TestMethod]
public void ComponentReferenceIsRemappedToClone()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var comp = go.Components.Create();
comp.Buddy = comp;
var clone = go.Clone();
var cloneComp = clone.GetComponent();
Assert.AreSame( cloneComp, cloneComp.Buddy );
Assert.AreNotSame( comp, cloneComp.Buddy );
}
}
///
/// Component whose members cover the different cloning strategies: value-type
/// property and field, string, mutable list, ICloneable class and a component
/// reference.
///
public class CloneDataComponent : Component
{
[Property]
public int Number { get; set; }
[Property]
public Vector3 Offset;
[Property]
public string Title { get; set; }
[Property]
public List Values { get; set; } = new();
[Property]
public CloneableBag Bag { get; set; }
[Property]
public CloneDataComponent Buddy { get; set; }
}
///
/// Plain class implementing ICloneable so the clone system uses its Clone()
/// instead of a JSON round trip.
///
public class CloneableBag : ICloneable
{
public int Value { get; set; }
///
/// Returns a copy of this bag carrying the same value.
///
public object Clone() => new CloneableBag { Value = Value };
}