From 420ec2433af0e2fbeb3543eddbd4d54e7e3a55aa Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 7 Apr 2023 01:21:56 +0100 Subject: [PATCH 01/12] feat: add Component move/copy As usual, experimental API - subject to change. --- CHANGELOG.md | 2 + .../Assets/Tests/ComponentTests.cs | 132 ++++++++++- .../Assets/Tests/GameObjectTests.cs | 197 ++++++++++++++++ X10D.Unity/X10D.Unity.csproj | 15 ++ X10D.Unity/src/ComponentExtensions.cs | 214 +++++++++++++++++- X10D.Unity/src/ExceptionMessages.Designer.cs | 80 +++++++ X10D.Unity/src/ExceptionMessages.resx | 34 +++ X10D.Unity/src/GameObjectExtensions.cs | 180 +++++++++++++++ 8 files changed, 852 insertions(+), 2 deletions(-) create mode 100644 X10D.Unity/src/ExceptionMessages.Designer.cs create mode 100644 X10D.Unity/src/ExceptionMessages.resx diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de5308..343b6dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - X10D: Added `TextWriter.WriteLineNoAlloc(uint[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(long[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan[, IFormatProvider]])`. +- X10D.Unity: Added `Component.CopyTo(GameObject)` and `Component.MoveTo(GameObject)`. +- X10D.Unity: Added `GameObject.CopyComponent(GameObject)` and `GameObject.MoveComponent(GameObject)`. ### Changed - X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`. diff --git a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs index e6c1431..615f16e 100644 --- a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs @@ -1,14 +1,77 @@ #nullable enable +using System; using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using Object = UnityEngine.Object; namespace X10D.Unity.Tests { public class ComponentTests { + [UnityTest] + public IEnumerator CopyTo_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10.0f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + sourceComponent.CopyTo(target); + + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyTo_ShouldThrowArgumentNullException_GivenNullComponent() + { + var target = new GameObject(); + Rigidbody rigidbody = null!; + + Assert.Throws(() => rigidbody.CopyTo(target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyTo_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + GameObject target = null!; + + Assert.Throws(() => rigidbody.CopyTo(target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator CopyTo_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => rigidbody.CopyTo(target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + [UnityTest] public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() { @@ -16,13 +79,80 @@ namespace X10D.Unity.Tests var rigidbody = parent.AddComponent(); var child = new GameObject(); - child.transform.SetParent(parent.transform); child.AddComponent(); + yield return null; + Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly(); Assert.That(components.Length, Is.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); + Object.Destroy(parent); + Object.Destroy(child); + } + + [UnityTest] + public IEnumerator MoveTo_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + sourceComponent.MoveTo(target); + + // effects of Destroy only take place at end of frame + yield return null; + + Assert.That(sourceComponent == null); + Assert.That(source.TryGetComponent(out Rigidbody _), Is.False); + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + } + + [UnityTest] + public IEnumerator MoveTo_ShouldThrowArgumentNullException_GivenNullComponent() + { + var target = new GameObject(); + Rigidbody rigidbody = null!; + + Assert.Throws(() => rigidbody.MoveTo(target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveTo_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + GameObject target = null!; + + Assert.Throws(() => rigidbody.MoveTo(target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator MoveTo_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + var rigidbody = source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => rigidbody.MoveTo(target)); + + Object.Destroy(source); + Object.Destroy(target); yield break; } } diff --git a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs index fef5a66..10ab455 100644 --- a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs @@ -1,14 +1,107 @@ #nullable enable +using System; using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using Object = UnityEngine.Object; namespace X10D.Unity.Tests { public class GameObjectTests { + [UnityTest] + public IEnumerator CopyComponent_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10.0f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + source.CopyComponent(target); + + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullComponentType() + { + var source = new GameObject(); + var target = new GameObject(); + Type componentType = null!; + + Assert.Throws(() => source.CopyComponent(componentType, target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullGameObject() + { + var target = new GameObject(); + GameObject source = null!; + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + GameObject target = null!; + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowInvalidOperationException_GivenInvalidComponent() + { + var source = new GameObject(); + var target = new GameObject(); + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator CopyComponent_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => source.CopyComponent(target)); + Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + [UnityTest] public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() { @@ -23,6 +116,8 @@ namespace X10D.Unity.Tests Assert.That(components.Length, Is.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); + Object.Destroy(parent); + Object.Destroy(child); yield break; } @@ -58,6 +153,103 @@ namespace X10D.Unity.Tests first.LookAt(Vector3.right); Assert.That(firstTransform.rotation, Is.EqualTo(expected)); + Object.Destroy(first); + Object.Destroy(second); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldCopyComponent_GivenComponent() + { + var source = new GameObject(); + var sourceComponent = source.AddComponent(); + sourceComponent.mass = 10.0f; + sourceComponent.useGravity = false; + + var target = new GameObject(); + source.MoveComponent(target); + + // effects of Destroy only take place at end of frame + yield return null; + + Assert.That(sourceComponent == null); + Assert.That(source.TryGetComponent(out Rigidbody _), Is.False); + Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); + Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); + Assert.That(targetComponent.useGravity, Is.False); + + Object.Destroy(source); + Object.Destroy(target); + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullComponentType() + { + var source = new GameObject(); + var target = new GameObject(); + Type componentType = null!; + + Assert.Throws(() => source.MoveComponent(componentType, target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullGameObject() + { + var target = new GameObject(); + GameObject source = null!; + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullTarget() + { + var source = new GameObject(); + GameObject target = null!; + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowInvalidOperationException_GivenInvalidComponent() + { + var source = new GameObject(); + var target = new GameObject(); + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); + yield break; + } + + [UnityTest] + public IEnumerator MoveComponent_ShouldThrowInvalidOperationException_GivenDuplicate() + { + var source = new GameObject(); + source.AddComponent(); + + var target = new GameObject(); + target.AddComponent(); + + Assert.Throws(() => source.MoveComponent(target)); + Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); + + Object.Destroy(source); + Object.Destroy(target); yield break; } @@ -82,6 +274,9 @@ namespace X10D.Unity.Tests Assert.That(child.layer, Is.EqualTo(layer)); Assert.That(grandChild.layer, Is.EqualTo(layer)); + Object.Destroy(parent); + Object.Destroy(child); + Object.Destroy(grandChild); yield break; } @@ -103,6 +298,8 @@ namespace X10D.Unity.Tests second.SetParent(first); Assert.That(second.transform.parent, Is.EqualTo(first.transform)); + Object.Destroy(first); + Object.Destroy(second); yield break; } } diff --git a/X10D.Unity/X10D.Unity.csproj b/X10D.Unity/X10D.Unity.csproj index f99bf2e..90caf27 100644 --- a/X10D.Unity/X10D.Unity.csproj +++ b/X10D.Unity/X10D.Unity.csproj @@ -70,4 +70,19 @@ + + + ResXFileCodeGenerator + ExceptionMessages.Designer.cs + + + + + + True + True + ExceptionMessages.resx + + + diff --git a/X10D.Unity/src/ComponentExtensions.cs b/X10D.Unity/src/ComponentExtensions.cs index 6ad7388..93afe4d 100644 --- a/X10D.Unity/src/ComponentExtensions.cs +++ b/X10D.Unity/src/ComponentExtensions.cs @@ -1,4 +1,7 @@ -using UnityEngine; +using System.Globalization; +using System.Reflection; +using UnityEngine; +using Object = UnityEngine.Object; namespace X10D.Unity; @@ -7,6 +10,95 @@ namespace X10D.Unity; /// public static class ComponentExtensions { + /// + /// Copies the component to another game object. + /// + /// The component to copy. + /// The game object to which the component will be copied. + /// The type of the component to copy. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void CopyTo(this T component, GameObject target) + where T : Component + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (target.TryGetComponent(out T targetComponent)) + { + string message = ExceptionMessages.ComponentAlreadyExists; + message = string.Format(CultureInfo.CurrentCulture, message, target.name, typeof(T).Name); + throw new InvalidOperationException(message); + } + + targetComponent = target.AddComponent(); + + var typeInfo = typeof(T).GetTypeInfo(); + CopyFields(typeInfo, component, targetComponent); + CopyProperties(typeInfo, component, targetComponent); + } + + /// + /// Copies the component to another game object. + /// + /// The component to copy. + /// The game object to which the component will be copied. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of the same type. + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void CopyTo(this Component component, GameObject target) + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + var componentType = component.GetType(); + if (target.TryGetComponent(componentType, out Component targetComponent)) + { + string message = ExceptionMessages.ComponentAlreadyExists; + message = string.Format(CultureInfo.CurrentCulture, message, target.name, componentType.Name); + throw new InvalidOperationException(message); + } + + targetComponent = target.AddComponent(componentType); + + var typeInfo = componentType.GetTypeInfo(); + CopyFields(typeInfo, component, targetComponent); + CopyProperties(typeInfo, component, targetComponent); + } + /// /// Returns an array of components of the specified type, excluding components that live on the object to which this /// component is attached. @@ -18,4 +110,124 @@ public static class ComponentExtensions { return component.gameObject.GetComponentsInChildrenOnly(); } + + /// + /// Moves the component to another game object. + /// + /// The component to move. + /// The game object to which the component will be moved. + /// The type of the component to move. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveTo(this T component, GameObject target) + where T : Component + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + component.CopyTo(target); + Object.Destroy(component); + } + + /// + /// Moves the component to another game object. + /// + /// The component to move. + /// The game object to which the component will be moved. + /// + /// is . + /// -or- + /// is . + /// + /// + /// already has a component of the same type. + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveTo(this Component component, GameObject target) + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + component.CopyTo(target); + Object.Destroy(component); + } + + private static void CopyFields(TypeInfo typeInfo, T component, T targetComponent) + where T : Component + { + foreach (FieldInfo field in typeInfo.DeclaredFields) + { + if (field.IsStatic) + { + continue; + } + + object fieldValue = GetNewReferences(component, targetComponent, field.GetValue(component)); + field.SetValue(targetComponent, fieldValue); + } + } + + private static void CopyProperties(TypeInfo typeInfo, T component, T targetComponent) + where T : Component + { + foreach (PropertyInfo property in typeInfo.DeclaredProperties) + { + if (!property.CanRead || !property.CanWrite) + { + continue; + } + + MethodInfo getMethod = property.GetMethod; + MethodInfo setMethod = property.SetMethod; + if (getMethod.IsStatic || setMethod.IsStatic) + { + continue; + } + + object propertyValue = GetNewReferences(component, targetComponent, property.GetValue(component)); + property.SetValue(targetComponent, propertyValue); + } + } + + private static object GetNewReferences(T component, T targetComponent, object value) + where T : Component + { + if (ReferenceEquals(value, component)) + { + value = targetComponent; + } + else if (ReferenceEquals(value, component.gameObject)) + { + value = targetComponent.gameObject; + } + + return value; + } } diff --git a/X10D.Unity/src/ExceptionMessages.Designer.cs b/X10D.Unity/src/ExceptionMessages.Designer.cs new file mode 100644 index 0000000..954a74a --- /dev/null +++ b/X10D.Unity/src/ExceptionMessages.Designer.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace X10D.Unity { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ExceptionMessages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ExceptionMessages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("X10D.Unity.src.ExceptionMessages", typeof(ExceptionMessages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The game object {0} already has a component of type {1}.. + /// + internal static string ComponentAlreadyExists { + get { + return ResourceManager.GetString("ComponentAlreadyExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The game object {0} does not have a component of type {1}.. + /// + internal static string ComponentDoesNotExist { + get { + return ResourceManager.GetString("ComponentDoesNotExist", resourceCulture); + } + } + } +} diff --git a/X10D.Unity/src/ExceptionMessages.resx b/X10D.Unity/src/ExceptionMessages.resx new file mode 100644 index 0000000..ec27743 --- /dev/null +++ b/X10D.Unity/src/ExceptionMessages.resx @@ -0,0 +1,34 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + + The game object {0} does not have a component of type {1}. + + + + The game object {0} already has a component of type {1}. + + \ No newline at end of file diff --git a/X10D.Unity/src/GameObjectExtensions.cs b/X10D.Unity/src/GameObjectExtensions.cs index 97d03d3..1ede330 100644 --- a/X10D.Unity/src/GameObjectExtensions.cs +++ b/X10D.Unity/src/GameObjectExtensions.cs @@ -1,4 +1,6 @@ +using System.Globalization; using UnityEngine; +using Object = UnityEngine.Object; namespace X10D.Unity; @@ -7,6 +9,90 @@ namespace X10D.Unity; /// public static class GameObjectExtensions { + /// + /// Copies the component of the specified type from one game object to another. + /// + /// The game object from which to copy the component. + /// The game object to which the component will be copied. + /// The type of the component to copy. + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + public static void CopyComponent(this GameObject gameObject, GameObject target) + where T : Component + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(out T sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, typeof(T).Name); + throw new InvalidOperationException(message); + } + + sourceComponent.CopyTo(target); + } + + /// + /// Copies the component of the specified type from one game object to another. + /// + /// The game object from which to copy the component. + /// The type of the component to copy. + /// The game object to which the component will be copied. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + public static void CopyComponent(this GameObject gameObject, Type componentType, GameObject target) + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(componentType, out Component sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, componentType.Name); + throw new InvalidOperationException(message); + } + + sourceComponent.CopyTo(target); + } + /// /// Returns an array of components of the specified type, excluding components that live on this game object. /// @@ -171,6 +257,100 @@ public static class GameObjectExtensions gameObject.transform.LookAt(target, worldUp); } + /// + /// Moves the component of the specified type from one game object to another. + /// + /// The game object from which to move the component. + /// The game object to which the component will be moved. + /// The type of the component to copy. + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveComponent(this GameObject gameObject, GameObject target) + where T : Component + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(out T sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, typeof(T).Name); + throw new InvalidOperationException(message); + } + + sourceComponent.MoveTo(target); + Object.Destroy(sourceComponent); + } + + /// + /// Moves the component of the specified type from one game object to another. + /// + /// The game object from which to move the component. + /// The type of the component to copy. + /// The game object to which the component will be moved. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// does not have a component of type . + /// -or- + /// already has a component of type . + /// + /// + /// This method will destroy the component on the source game object, creating a new instance on the target. Use with + /// caution. + /// + public static void MoveComponent(this GameObject gameObject, Type componentType, GameObject target) + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!gameObject.TryGetComponent(componentType, out Component sourceComponent)) + { + string message = ExceptionMessages.ComponentDoesNotExist; + message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, componentType.Name); + throw new InvalidOperationException(message); + } + + sourceComponent.MoveTo(target); + Object.Destroy(sourceComponent); + } + /// /// Sets the new layer of this game object and its children, recursively. /// From dc6d984fa865c2a82fa40fa41c2da7509c88adcc Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 7 Apr 2023 01:34:08 +0100 Subject: [PATCH 02/12] test: suppress ReSharper.Unity.InefficientPropertyAccess This is false positive being thrown by the analyzer. The values are - in fact - changing before being read each time. --- X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs index 10ab455..c6b352a 100644 --- a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections; +using System.Diagnostics.CodeAnalysis; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -122,6 +123,7 @@ namespace X10D.Unity.Tests } [UnityTest] + [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")] public IEnumerator LookAt_ShouldRotateSameAsTransform() { var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}}; @@ -281,6 +283,7 @@ namespace X10D.Unity.Tests } [UnityTest] + [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")] public IEnumerator SetParent_ShouldSetParent() { var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}}; From ad2d33aa88c7c6178d9ae4ea735775f995d815d7 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 7 Apr 2023 01:34:34 +0100 Subject: [PATCH 03/12] style(test): use constraint API for length check --- X10D.Unity.Tests/Assets/Tests/ComponentTests.cs | 2 +- X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs index 615f16e..c1b30f5 100644 --- a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs @@ -84,7 +84,7 @@ namespace X10D.Unity.Tests yield return null; Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly(); - Assert.That(components.Length, Is.EqualTo(1)); + Assert.That(components, Has.Length.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); Object.Destroy(parent); diff --git a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs index c6b352a..c555174 100644 --- a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs @@ -114,7 +114,7 @@ namespace X10D.Unity.Tests child.AddComponent(); Rigidbody[] components = parent.GetComponentsInChildrenOnly(); - Assert.That(components.Length, Is.EqualTo(1)); + Assert.That(components, Has.Length.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); Object.Destroy(parent); From a4a1d3b13a28de5fcd15452b210c01e0a4d96a64 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 7 Apr 2023 13:09:07 +0100 Subject: [PATCH 04/12] fix: only copy Unity-serialized members --- X10D.Unity/src/ComponentExtensions.cs | 32 ++++++--------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/X10D.Unity/src/ComponentExtensions.cs b/X10D.Unity/src/ComponentExtensions.cs index 93afe4d..9a7dafc 100644 --- a/X10D.Unity/src/ComponentExtensions.cs +++ b/X10D.Unity/src/ComponentExtensions.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Reflection; using UnityEngine; +using X10D.Reflection; using Object = UnityEngine.Object; namespace X10D.Unity; @@ -52,7 +53,6 @@ public static class ComponentExtensions var typeInfo = typeof(T).GetTypeInfo(); CopyFields(typeInfo, component, targetComponent); - CopyProperties(typeInfo, component, targetComponent); } /// @@ -96,7 +96,6 @@ public static class ComponentExtensions var typeInfo = componentType.GetTypeInfo(); CopyFields(typeInfo, component, targetComponent); - CopyProperties(typeInfo, component, targetComponent); } /// @@ -184,7 +183,12 @@ public static class ComponentExtensions { foreach (FieldInfo field in typeInfo.DeclaredFields) { - if (field.IsStatic) + if (field.IsStatic || !field.IsPublic && !field.HasCustomAttribute()) + { + continue; + } + + if (field.HasCustomAttribute()) { continue; } @@ -194,28 +198,6 @@ public static class ComponentExtensions } } - private static void CopyProperties(TypeInfo typeInfo, T component, T targetComponent) - where T : Component - { - foreach (PropertyInfo property in typeInfo.DeclaredProperties) - { - if (!property.CanRead || !property.CanWrite) - { - continue; - } - - MethodInfo getMethod = property.GetMethod; - MethodInfo setMethod = property.SetMethod; - if (getMethod.IsStatic || setMethod.IsStatic) - { - continue; - } - - object propertyValue = GetNewReferences(component, targetComponent, property.GetValue(component)); - property.SetValue(targetComponent, propertyValue); - } - } - private static object GetNewReferences(T component, T targetComponent, object value) where T : Component { From f6847315a16fa4d422468adc160381aa78cae086 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 12:44:53 +0100 Subject: [PATCH 05/12] feat: add Progress.OnProgressChanged Provides a mechanism to wrap the ProgressChanged event of a Progress as an IObservable. --- CHANGELOG.md | 3 + X10D.Tests/X10D.Tests.csproj | 1 + X10D.Tests/src/Reactive/ProgressTests.cs | 67 ++++++++++++++++ X10D/src/Reactive/ObservableDisposer.cs | 48 ++++++++++++ X10D/src/Reactive/ProgressExtensions.cs | 97 ++++++++++++++++++++++++ X10D/src/Reactive/ProgressObservable.cs | 40 ++++++++++ 6 files changed, 256 insertions(+) create mode 100644 X10D.Tests/src/Reactive/ProgressTests.cs create mode 100644 X10D/src/Reactive/ObservableDisposer.cs create mode 100644 X10D/src/Reactive/ProgressExtensions.cs create mode 100644 X10D/src/Reactive/ProgressObservable.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 343b6dd..643123d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 4.0.0 - [Unreleased] ### Added + - X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`. - X10D: Added math-related extension methods for `BigInteger`. - X10D: Added `Span.Replace(T, T)`. - X10D: Added `CountDigits` for integer types. +- X10D: Added `Progress.OnProgressChanged([T])`; - X10D: Added `TextWriter.WriteNoAlloc(int[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteNoAlloc(uint[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteNoAlloc(long[, ReadOnlySpan[, IFormatProvider]])`. @@ -24,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - X10D.Unity: Added `GameObject.CopyComponent(GameObject)` and `GameObject.MoveComponent(GameObject)`. ### Changed + - X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`. ## [3.2.0] - 2023-04-03 diff --git a/X10D.Tests/X10D.Tests.csproj b/X10D.Tests/X10D.Tests.csproj index d763c8c..00ea605 100644 --- a/X10D.Tests/X10D.Tests.csproj +++ b/X10D.Tests/X10D.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/X10D.Tests/src/Reactive/ProgressTests.cs b/X10D.Tests/src/Reactive/ProgressTests.cs new file mode 100644 index 0000000..fce2683 --- /dev/null +++ b/X10D.Tests/src/Reactive/ProgressTests.cs @@ -0,0 +1,67 @@ +using NUnit.Framework; +using X10D.Reactive; + +namespace X10D.Tests.Reactive; + +[TestFixture] +public class ProgressTests +{ + [Test] + public void OnProgressChanged_ShouldCallCompletionDelegate_GivenCompletionValue() + { + var subscriberWasCalled = false; + var completionWasCalled = false; + + var progress = new Progress(); + progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true, () => completionWasCalled = true); + + ((IProgress)progress).Report(0.5f); + ((IProgress)progress).Report(1.0f); + + Thread.Sleep(1000); + Assert.That(subscriberWasCalled); + Assert.That(completionWasCalled); + } + + [Test] + public void OnProgressChanged_ShouldCallSubscribers_OnProgressChanged() + { + var subscriberWasCalled = false; + + var progress = new Progress(); + progress.OnProgressChanged().Subscribe(_ => subscriberWasCalled = true); + + ((IProgress)progress).Report(0.5f); + + Thread.Sleep(1000); + Assert.That(subscriberWasCalled); + } + + [Test] + public void OnProgressChanged_ShouldCallSubscribers_OnProgressChanged_GivenCompletionValue() + { + var subscriberWasCalled = false; + + var progress = new Progress(); + progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true); + + ((IProgress)progress).Report(0.5f); + + Thread.Sleep(1000); + Assert.That(subscriberWasCalled); + } + + [Test] + public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgress() + { + Progress progress = null!; + Assert.Throws(() => progress.OnProgressChanged()); + } + + [Test] + public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgressAndCompletionValue() + { + Progress progress = null!; + Assert.Throws(() => progress.OnProgressChanged(1.0f)); + } +} diff --git a/X10D/src/Reactive/ObservableDisposer.cs b/X10D/src/Reactive/ObservableDisposer.cs new file mode 100644 index 0000000..9073ff9 --- /dev/null +++ b/X10D/src/Reactive/ObservableDisposer.cs @@ -0,0 +1,48 @@ +namespace X10D.Reactive; + +/// +/// Represents a disposable that removes an observer from a collection of observers. +/// +internal readonly struct ObservableDisposer : IDisposable +{ + private readonly HashSet> _observers; + private readonly IObserver _observer; + private readonly Action? _additionalAction; + + /// + /// Initializes a new instance of the struct. + /// + /// A collection of observers from which to remove the specified observer. + /// The observer to remove from the collection. + /// The additional action to run on dispose. + public ObservableDisposer(HashSet> observers, IObserver observer, Action? additionalAction) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(observers); + ArgumentNullException.ThrowIfNull(observer); +#else + if (observers is null) + { + throw new ArgumentNullException(nameof(observers)); + } + + if (observer is null) + { + throw new ArgumentNullException(nameof(observer)); + } +#endif + + _observers = observers; + _observer = observer; + _additionalAction = additionalAction; + } + + /// + /// Removes the observer from the collection of observers. + /// + public void Dispose() + { + _observers.Remove(_observer); + _additionalAction?.Invoke(); + } +} diff --git a/X10D/src/Reactive/ProgressExtensions.cs b/X10D/src/Reactive/ProgressExtensions.cs new file mode 100644 index 0000000..6128a72 --- /dev/null +++ b/X10D/src/Reactive/ProgressExtensions.cs @@ -0,0 +1,97 @@ +namespace X10D.Reactive; + +/// +/// Provides extension methods for . +/// +public static class ProgressExtensions +{ + /// + /// Wraps the event of the current in an + /// object. + /// + /// The progress whose event to wrap. + /// The type of progress update value. + /// + /// An object that wraps the event of the current + /// . + /// + /// is . + public static IObservable OnProgressChanged(this Progress progress) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(progress); +#else + if (progress is null) + { + throw new ArgumentNullException(nameof(progress)); + } +#endif + + var progressObservable = new ProgressObservable(); + + void ProgressChangedHandler(object? sender, T args) + { + IObserver[] observers = progressObservable.Observers; + + for (var index = 0; index < observers.Length; index++) + { + observers[index].OnNext(args); + } + } + + progress.ProgressChanged += ProgressChangedHandler; + progressObservable.OnDispose = () => progress.ProgressChanged -= ProgressChangedHandler; + + return progressObservable; + } + + /// + /// Wraps the event of the current in an + /// object, and completes the observable when the progress reaches the specified value. + /// + /// The progress whose event to wrap. + /// The value that indicates completion. + /// The type of progress update value. + /// + /// An object that wraps the event of the current + /// . + /// + /// is . + public static IObservable OnProgressChanged(this Progress progress, T completeValue) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(progress); +#else + if (progress is null) + { + throw new ArgumentNullException(nameof(progress)); + } +#endif + + var progressObservable = new ProgressObservable(); + var comparer = EqualityComparer.Default; + + void ProgressChangedHandler(object? sender, T args) + { + IObserver[] observers = progressObservable.Observers; + + for (var index = 0; index < observers.Length; index++) + { + observers[index].OnNext(args); + } + + if (comparer.Equals(args, completeValue)) + { + for (var index = 0; index < observers.Length; index++) + { + observers[index].OnCompleted(); + } + } + } + + progress.ProgressChanged += ProgressChangedHandler; + progressObservable.OnDispose = () => progress.ProgressChanged -= ProgressChangedHandler; + + return progressObservable; + } +} diff --git a/X10D/src/Reactive/ProgressObservable.cs b/X10D/src/Reactive/ProgressObservable.cs new file mode 100644 index 0000000..29a8243 --- /dev/null +++ b/X10D/src/Reactive/ProgressObservable.cs @@ -0,0 +1,40 @@ +namespace X10D.Reactive; + +/// +/// Represents a concrete implementation of that tracks progress of a . +/// +internal sealed class ProgressObservable : IObservable +{ + private readonly HashSet> _observers = new(); + + /// + /// Gets the observers. + /// + /// The observers. + public IObserver[] Observers + { + get => _observers.ToArray(); + } + + internal Action? OnDispose { get; set; } + + /// + /// Subscribes the specified observer to the progress tracker. + /// + /// The observer. + /// An object which can be disposed to unsubscribe from progress tracking. + public IDisposable Subscribe(IObserver observer) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(observer); +#else + if (observer is null) + { + throw new ArgumentNullException(nameof(observer)); + } +#endif + + _observers.Add(observer); + return new ObservableDisposer(_observers, observer, OnDispose); + } +} From 844f697754e80ce18ed7de3c575031e0aa58725b Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 12:45:18 +0100 Subject: [PATCH 06/12] style: remove unused using directives --- X10D/src/Collections/SpanExtensions.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/X10D/src/Collections/SpanExtensions.cs b/X10D/src/Collections/SpanExtensions.cs index 1aee61d..ac2fd2b 100644 --- a/X10D/src/Collections/SpanExtensions.cs +++ b/X10D/src/Collections/SpanExtensions.cs @@ -1,9 +1,4 @@ -#if NET5_0_OR_GREATER -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -#endif - -namespace X10D.Collections; +namespace X10D.Collections; /// /// Extension methods for and From 77ab429f72eed06c6f922e85c9089ecf25d4924d Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 13:26:38 +0100 Subject: [PATCH 07/12] refactor: remove Copy/Move component functionality This may be returned at a later point. --- .../Assets/Tests/ComponentTests.cs | 127 ------------ .../Assets/Tests/GameObjectTests.cs | 187 ----------------- X10D.Unity/src/ComponentExtensions.cs | 196 +----------------- X10D.Unity/src/GameObjectExtensions.cs | 180 ---------------- 4 files changed, 1 insertion(+), 689 deletions(-) diff --git a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs index c1b30f5..56c8dee 100644 --- a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs @@ -1,6 +1,5 @@ #nullable enable -using System; using System.Collections; using NUnit.Framework; using UnityEngine; @@ -11,67 +10,6 @@ namespace X10D.Unity.Tests { public class ComponentTests { - [UnityTest] - public IEnumerator CopyTo_ShouldCopyComponent_GivenComponent() - { - var source = new GameObject(); - var sourceComponent = source.AddComponent(); - sourceComponent.mass = 10.0f; - sourceComponent.useGravity = false; - - var target = new GameObject(); - sourceComponent.CopyTo(target); - - Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); - Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); - Assert.That(targetComponent.useGravity, Is.False); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator CopyTo_ShouldThrowArgumentNullException_GivenNullComponent() - { - var target = new GameObject(); - Rigidbody rigidbody = null!; - - Assert.Throws(() => rigidbody.CopyTo(target)); - - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator CopyTo_ShouldThrowArgumentNullException_GivenNullTarget() - { - var source = new GameObject(); - var rigidbody = source.AddComponent(); - GameObject target = null!; - - Assert.Throws(() => rigidbody.CopyTo(target)); - - Object.Destroy(source); - yield break; - } - - [UnityTest] - public IEnumerator CopyTo_ShouldThrowInvalidOperationException_GivenDuplicate() - { - var source = new GameObject(); - var rigidbody = source.AddComponent(); - - var target = new GameObject(); - target.AddComponent(); - - Assert.Throws(() => rigidbody.CopyTo(target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - [UnityTest] public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() { @@ -90,70 +28,5 @@ namespace X10D.Unity.Tests Object.Destroy(parent); Object.Destroy(child); } - - [UnityTest] - public IEnumerator MoveTo_ShouldCopyComponent_GivenComponent() - { - var source = new GameObject(); - var sourceComponent = source.AddComponent(); - sourceComponent.mass = 10f; - sourceComponent.useGravity = false; - - var target = new GameObject(); - sourceComponent.MoveTo(target); - - // effects of Destroy only take place at end of frame - yield return null; - - Assert.That(sourceComponent == null); - Assert.That(source.TryGetComponent(out Rigidbody _), Is.False); - Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); - Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); - Assert.That(targetComponent.useGravity, Is.False); - - Object.Destroy(source); - Object.Destroy(target); - } - - [UnityTest] - public IEnumerator MoveTo_ShouldThrowArgumentNullException_GivenNullComponent() - { - var target = new GameObject(); - Rigidbody rigidbody = null!; - - Assert.Throws(() => rigidbody.MoveTo(target)); - - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator MoveTo_ShouldThrowArgumentNullException_GivenNullTarget() - { - var source = new GameObject(); - var rigidbody = source.AddComponent(); - GameObject target = null!; - - Assert.Throws(() => rigidbody.MoveTo(target)); - - Object.Destroy(source); - yield break; - } - - [UnityTest] - public IEnumerator MoveTo_ShouldThrowInvalidOperationException_GivenDuplicate() - { - var source = new GameObject(); - var rigidbody = source.AddComponent(); - - var target = new GameObject(); - target.AddComponent(); - - Assert.Throws(() => rigidbody.MoveTo(target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } } } diff --git a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs index c555174..dca113b 100644 --- a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs @@ -1,6 +1,5 @@ #nullable enable -using System; using System.Collections; using System.Diagnostics.CodeAnalysis; using NUnit.Framework; @@ -12,97 +11,6 @@ namespace X10D.Unity.Tests { public class GameObjectTests { - [UnityTest] - public IEnumerator CopyComponent_ShouldCopyComponent_GivenComponent() - { - var source = new GameObject(); - var sourceComponent = source.AddComponent(); - sourceComponent.mass = 10.0f; - sourceComponent.useGravity = false; - - var target = new GameObject(); - source.CopyComponent(target); - - Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); - Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); - Assert.That(targetComponent.useGravity, Is.False); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullComponentType() - { - var source = new GameObject(); - var target = new GameObject(); - Type componentType = null!; - - Assert.Throws(() => source.CopyComponent(componentType, target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullGameObject() - { - var target = new GameObject(); - GameObject source = null!; - - Assert.Throws(() => source.CopyComponent(target)); - Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); - - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator CopyComponent_ShouldThrowArgumentNullException_GivenNullTarget() - { - var source = new GameObject(); - GameObject target = null!; - - Assert.Throws(() => source.CopyComponent(target)); - Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); - - Object.Destroy(source); - yield break; - } - - [UnityTest] - public IEnumerator CopyComponent_ShouldThrowInvalidOperationException_GivenInvalidComponent() - { - var source = new GameObject(); - var target = new GameObject(); - - Assert.Throws(() => source.CopyComponent(target)); - Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator CopyComponent_ShouldThrowInvalidOperationException_GivenDuplicate() - { - var source = new GameObject(); - source.AddComponent(); - - var target = new GameObject(); - target.AddComponent(); - - Assert.Throws(() => source.CopyComponent(target)); - Assert.Throws(() => source.CopyComponent(typeof(Rigidbody), target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - [UnityTest] public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() { @@ -160,101 +68,6 @@ namespace X10D.Unity.Tests yield break; } - [UnityTest] - public IEnumerator MoveComponent_ShouldCopyComponent_GivenComponent() - { - var source = new GameObject(); - var sourceComponent = source.AddComponent(); - sourceComponent.mass = 10.0f; - sourceComponent.useGravity = false; - - var target = new GameObject(); - source.MoveComponent(target); - - // effects of Destroy only take place at end of frame - yield return null; - - Assert.That(sourceComponent == null); - Assert.That(source.TryGetComponent(out Rigidbody _), Is.False); - Assert.That(target.TryGetComponent(out Rigidbody targetComponent)); - Assert.That(targetComponent.mass, Is.EqualTo(10.0f)); - Assert.That(targetComponent.useGravity, Is.False); - - Object.Destroy(source); - Object.Destroy(target); - } - - [UnityTest] - public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullComponentType() - { - var source = new GameObject(); - var target = new GameObject(); - Type componentType = null!; - - Assert.Throws(() => source.MoveComponent(componentType, target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullGameObject() - { - var target = new GameObject(); - GameObject source = null!; - - Assert.Throws(() => source.MoveComponent(target)); - Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); - - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator MoveComponent_ShouldThrowArgumentNullException_GivenNullTarget() - { - var source = new GameObject(); - GameObject target = null!; - - Assert.Throws(() => source.MoveComponent(target)); - Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); - - Object.Destroy(source); - yield break; - } - - [UnityTest] - public IEnumerator MoveComponent_ShouldThrowInvalidOperationException_GivenInvalidComponent() - { - var source = new GameObject(); - var target = new GameObject(); - - Assert.Throws(() => source.MoveComponent(target)); - Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - - [UnityTest] - public IEnumerator MoveComponent_ShouldThrowInvalidOperationException_GivenDuplicate() - { - var source = new GameObject(); - source.AddComponent(); - - var target = new GameObject(); - target.AddComponent(); - - Assert.Throws(() => source.MoveComponent(target)); - Assert.Throws(() => source.MoveComponent(typeof(Rigidbody), target)); - - Object.Destroy(source); - Object.Destroy(target); - yield break; - } - [UnityTest] public IEnumerator SetLayerRecursively_ShouldSetLayerRecursively() { diff --git a/X10D.Unity/src/ComponentExtensions.cs b/X10D.Unity/src/ComponentExtensions.cs index 9a7dafc..6ad7388 100644 --- a/X10D.Unity/src/ComponentExtensions.cs +++ b/X10D.Unity/src/ComponentExtensions.cs @@ -1,8 +1,4 @@ -using System.Globalization; -using System.Reflection; -using UnityEngine; -using X10D.Reflection; -using Object = UnityEngine.Object; +using UnityEngine; namespace X10D.Unity; @@ -11,93 +7,6 @@ namespace X10D.Unity; /// public static class ComponentExtensions { - /// - /// Copies the component to another game object. - /// - /// The component to copy. - /// The game object to which the component will be copied. - /// The type of the component to copy. - /// - /// is . - /// -or- - /// is . - /// - /// - /// already has a component of type . - /// - /// - /// This method will destroy the component on the source game object, creating a new instance on the target. Use with - /// caution. - /// - public static void CopyTo(this T component, GameObject target) - where T : Component - { - if (component == null) - { - throw new ArgumentNullException(nameof(component)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (target.TryGetComponent(out T targetComponent)) - { - string message = ExceptionMessages.ComponentAlreadyExists; - message = string.Format(CultureInfo.CurrentCulture, message, target.name, typeof(T).Name); - throw new InvalidOperationException(message); - } - - targetComponent = target.AddComponent(); - - var typeInfo = typeof(T).GetTypeInfo(); - CopyFields(typeInfo, component, targetComponent); - } - - /// - /// Copies the component to another game object. - /// - /// The component to copy. - /// The game object to which the component will be copied. - /// - /// is . - /// -or- - /// is . - /// - /// - /// already has a component of the same type. - /// - /// - /// This method will destroy the component on the source game object, creating a new instance on the target. Use with - /// caution. - /// - public static void CopyTo(this Component component, GameObject target) - { - if (component == null) - { - throw new ArgumentNullException(nameof(component)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - var componentType = component.GetType(); - if (target.TryGetComponent(componentType, out Component targetComponent)) - { - string message = ExceptionMessages.ComponentAlreadyExists; - message = string.Format(CultureInfo.CurrentCulture, message, target.name, componentType.Name); - throw new InvalidOperationException(message); - } - - targetComponent = target.AddComponent(componentType); - - var typeInfo = componentType.GetTypeInfo(); - CopyFields(typeInfo, component, targetComponent); - } - /// /// Returns an array of components of the specified type, excluding components that live on the object to which this /// component is attached. @@ -109,107 +18,4 @@ public static class ComponentExtensions { return component.gameObject.GetComponentsInChildrenOnly(); } - - /// - /// Moves the component to another game object. - /// - /// The component to move. - /// The game object to which the component will be moved. - /// The type of the component to move. - /// - /// is . - /// -or- - /// is . - /// - /// - /// already has a component of type . - /// - /// - /// This method will destroy the component on the source game object, creating a new instance on the target. Use with - /// caution. - /// - public static void MoveTo(this T component, GameObject target) - where T : Component - { - if (component == null) - { - throw new ArgumentNullException(nameof(component)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - component.CopyTo(target); - Object.Destroy(component); - } - - /// - /// Moves the component to another game object. - /// - /// The component to move. - /// The game object to which the component will be moved. - /// - /// is . - /// -or- - /// is . - /// - /// - /// already has a component of the same type. - /// - /// - /// This method will destroy the component on the source game object, creating a new instance on the target. Use with - /// caution. - /// - public static void MoveTo(this Component component, GameObject target) - { - if (component == null) - { - throw new ArgumentNullException(nameof(component)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - component.CopyTo(target); - Object.Destroy(component); - } - - private static void CopyFields(TypeInfo typeInfo, T component, T targetComponent) - where T : Component - { - foreach (FieldInfo field in typeInfo.DeclaredFields) - { - if (field.IsStatic || !field.IsPublic && !field.HasCustomAttribute()) - { - continue; - } - - if (field.HasCustomAttribute()) - { - continue; - } - - object fieldValue = GetNewReferences(component, targetComponent, field.GetValue(component)); - field.SetValue(targetComponent, fieldValue); - } - } - - private static object GetNewReferences(T component, T targetComponent, object value) - where T : Component - { - if (ReferenceEquals(value, component)) - { - value = targetComponent; - } - else if (ReferenceEquals(value, component.gameObject)) - { - value = targetComponent.gameObject; - } - - return value; - } } diff --git a/X10D.Unity/src/GameObjectExtensions.cs b/X10D.Unity/src/GameObjectExtensions.cs index 1ede330..97d03d3 100644 --- a/X10D.Unity/src/GameObjectExtensions.cs +++ b/X10D.Unity/src/GameObjectExtensions.cs @@ -1,6 +1,4 @@ -using System.Globalization; using UnityEngine; -using Object = UnityEngine.Object; namespace X10D.Unity; @@ -9,90 +7,6 @@ namespace X10D.Unity; /// public static class GameObjectExtensions { - /// - /// Copies the component of the specified type from one game object to another. - /// - /// The game object from which to copy the component. - /// The game object to which the component will be copied. - /// The type of the component to copy. - /// - /// is . - /// -or- - /// is . - /// - /// - /// does not have a component of type . - /// -or- - /// already has a component of type . - /// - public static void CopyComponent(this GameObject gameObject, GameObject target) - where T : Component - { - if (gameObject == null) - { - throw new ArgumentNullException(nameof(gameObject)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (!gameObject.TryGetComponent(out T sourceComponent)) - { - string message = ExceptionMessages.ComponentDoesNotExist; - message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, typeof(T).Name); - throw new InvalidOperationException(message); - } - - sourceComponent.CopyTo(target); - } - - /// - /// Copies the component of the specified type from one game object to another. - /// - /// The game object from which to copy the component. - /// The type of the component to copy. - /// The game object to which the component will be copied. - /// - /// is . - /// -or- - /// is . - /// -or- - /// is . - /// - /// - /// does not have a component of type . - /// -or- - /// already has a component of type . - /// - public static void CopyComponent(this GameObject gameObject, Type componentType, GameObject target) - { - if (gameObject == null) - { - throw new ArgumentNullException(nameof(gameObject)); - } - - if (componentType is null) - { - throw new ArgumentNullException(nameof(componentType)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (!gameObject.TryGetComponent(componentType, out Component sourceComponent)) - { - string message = ExceptionMessages.ComponentDoesNotExist; - message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, componentType.Name); - throw new InvalidOperationException(message); - } - - sourceComponent.CopyTo(target); - } - /// /// Returns an array of components of the specified type, excluding components that live on this game object. /// @@ -257,100 +171,6 @@ public static class GameObjectExtensions gameObject.transform.LookAt(target, worldUp); } - /// - /// Moves the component of the specified type from one game object to another. - /// - /// The game object from which to move the component. - /// The game object to which the component will be moved. - /// The type of the component to copy. - /// - /// is . - /// -or- - /// is . - /// - /// - /// does not have a component of type . - /// -or- - /// already has a component of type . - /// - /// - /// This method will destroy the component on the source game object, creating a new instance on the target. Use with - /// caution. - /// - public static void MoveComponent(this GameObject gameObject, GameObject target) - where T : Component - { - if (gameObject == null) - { - throw new ArgumentNullException(nameof(gameObject)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (!gameObject.TryGetComponent(out T sourceComponent)) - { - string message = ExceptionMessages.ComponentDoesNotExist; - message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, typeof(T).Name); - throw new InvalidOperationException(message); - } - - sourceComponent.MoveTo(target); - Object.Destroy(sourceComponent); - } - - /// - /// Moves the component of the specified type from one game object to another. - /// - /// The game object from which to move the component. - /// The type of the component to copy. - /// The game object to which the component will be moved. - /// - /// is . - /// -or- - /// is . - /// -or- - /// is . - /// - /// - /// does not have a component of type . - /// -or- - /// already has a component of type . - /// - /// - /// This method will destroy the component on the source game object, creating a new instance on the target. Use with - /// caution. - /// - public static void MoveComponent(this GameObject gameObject, Type componentType, GameObject target) - { - if (gameObject == null) - { - throw new ArgumentNullException(nameof(gameObject)); - } - - if (componentType is null) - { - throw new ArgumentNullException(nameof(componentType)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (!gameObject.TryGetComponent(componentType, out Component sourceComponent)) - { - string message = ExceptionMessages.ComponentDoesNotExist; - message = string.Format(CultureInfo.CurrentCulture, message, gameObject.name, componentType.Name); - throw new InvalidOperationException(message); - } - - sourceComponent.MoveTo(target); - Object.Destroy(sourceComponent); - } - /// /// Sets the new layer of this game object and its children, recursively. /// From 0b5bb074c8b33746b56bd44ac2a565d2e8a797d0 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 13:38:56 +0100 Subject: [PATCH 08/12] refactor(test): remove IEnumerator tests Use synchronous NUnit tests --- .../Assets/Tests/GameObjectTests.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs index dca113b..565d455 100644 --- a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs @@ -1,18 +1,16 @@ #nullable enable -using System.Collections; using System.Diagnostics.CodeAnalysis; using NUnit.Framework; using UnityEngine; -using UnityEngine.TestTools; using Object = UnityEngine.Object; namespace X10D.Unity.Tests { public class GameObjectTests { - [UnityTest] - public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() + [Test] + public void GetComponentsInChildrenOnly_ShouldIgnoreParent() { var parent = new GameObject(); parent.AddComponent(); @@ -27,12 +25,11 @@ namespace X10D.Unity.Tests Object.Destroy(parent); Object.Destroy(child); - yield break; } - [UnityTest] + [Test] [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")] - public IEnumerator LookAt_ShouldRotateSameAsTransform() + public void LookAt_ShouldRotateSameAsTransform() { var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}}; var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}}; @@ -65,11 +62,10 @@ namespace X10D.Unity.Tests Object.Destroy(first); Object.Destroy(second); - yield break; } - [UnityTest] - public IEnumerator SetLayerRecursively_ShouldSetLayerRecursively() + [Test] + public void SetLayerRecursively_ShouldSetLayerRecursively() { var parent = new GameObject(); var child = new GameObject(); @@ -92,12 +88,11 @@ namespace X10D.Unity.Tests Object.Destroy(parent); Object.Destroy(child); Object.Destroy(grandChild); - yield break; } - [UnityTest] + [Test] [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")] - public IEnumerator SetParent_ShouldSetParent() + public void SetParent_ShouldSetParent() { var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}}; var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}}; @@ -116,7 +111,6 @@ namespace X10D.Unity.Tests Object.Destroy(first); Object.Destroy(second); - yield break; } } } From a8ebe9c9021caf70003b3fffab35b7883cfca73f Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 13:42:12 +0100 Subject: [PATCH 09/12] fix: fix issue with GetComponentsInChildrenOnly checking wrong Transform --- X10D.Unity/src/GameObjectExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/X10D.Unity/src/GameObjectExtensions.cs b/X10D.Unity/src/GameObjectExtensions.cs index 97d03d3..23d7f3c 100644 --- a/X10D.Unity/src/GameObjectExtensions.cs +++ b/X10D.Unity/src/GameObjectExtensions.cs @@ -15,6 +15,7 @@ public static class GameObjectExtensions /// An array representing the child components. public static T[] GetComponentsInChildrenOnly(this GameObject gameObject) { + Transform rootTransform = gameObject.transform; var components = new List(gameObject.GetComponentsInChildren()); for (var index = 0; index < components.Count; index++) @@ -26,7 +27,7 @@ public static class GameObjectExtensions continue; } - if (childComponent.transform.parent != gameObject.transform) + if (childComponent.transform == rootTransform) { components.RemoveAt(index); index--; From f4d6c9083b5f3cce4ee35401c08c7e15a405444b Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 13:44:37 +0100 Subject: [PATCH 10/12] refactor(test): remove lingering IEnumerator test Remant from 0b5bb074c8b33746b56bd44ac2a565d2e8a797d0 --- X10D.Unity.Tests/Assets/Tests/ComponentTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs index 56c8dee..b7e4c69 100644 --- a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs @@ -10,8 +10,8 @@ namespace X10D.Unity.Tests { public class ComponentTests { - [UnityTest] - public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() + [Test] + public void GetComponentsInChildrenOnly_ShouldIgnoreParent() { var parent = new GameObject(); var rigidbody = parent.AddComponent(); @@ -19,8 +19,6 @@ namespace X10D.Unity.Tests var child = new GameObject(); child.AddComponent(); - yield return null; - Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly(); Assert.That(components, Has.Length.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); From 98cd96d5cbf1b57ea026e3c9afed533e5bde00f1 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 13:45:39 +0100 Subject: [PATCH 11/12] fix(test): fix malformed test The child was not being assigned a new parent, causing GetComponentsInChildrenOnly to return empty array and the subsequent line: Assert.That(components, Has.Length.EqualTo(1)); was resulting in a test fail. --- X10D.Unity.Tests/Assets/Tests/ComponentTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs index b7e4c69..126e435 100644 --- a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs @@ -17,6 +17,7 @@ namespace X10D.Unity.Tests var rigidbody = parent.AddComponent(); var child = new GameObject(); + child.transform.SetParent(parent.transform); child.AddComponent(); Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly(); From b2e4092ca7b77716624fff679dba0ab8588941bd Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 10 Apr 2023 13:50:44 +0100 Subject: [PATCH 12/12] [ci skip] docs: remove addition of component copy/move --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 643123d..6cdf854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - X10D: Added `TextWriter.WriteLineNoAlloc(uint[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(long[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan[, IFormatProvider]])`. -- X10D.Unity: Added `Component.CopyTo(GameObject)` and `Component.MoveTo(GameObject)`. -- X10D.Unity: Added `GameObject.CopyComponent(GameObject)` and `GameObject.MoveComponent(GameObject)`. ### Changed