From 82d64985c8df5e33b663b0354f6358fa0e385bef Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 27 Nov 2025 02:57:21 -0800 Subject: [PATCH 01/13] Refactor test files by removing unused imports and adding new executor classes for material reflection and game object creation --- .../Editor/RefTypes/AssetObjectRefTests.cs | 1 - .../Editor/RefTypes/GameObjectRefTests.cs | 1 - .../Tests/Editor/RefTypes/ObjectRefTests.cs | 1 - .../Tests/Editor/ReflectionConverter.meta | 8 ++ .../MaterialReflectionConverterTests.cs | 70 ++++++++++ .../MaterialReflectionConverterTests.cs.meta | 11 ++ .../Editor/Tool/Assets/AssetsMaterialTests.cs | 2 - .../Utils/Executor/AddComponentExecutor.cs | 79 +++++++++++ .../Executor/AddComponentExecutor.cs.meta | 11 ++ .../Executor/CreateGameObjectExecutor.cs | 128 ++++++++++++++++++ .../Executor/CreateGameObjectExecutor.cs.meta | 11 ++ 11 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs.meta diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/AssetObjectRefTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/AssetObjectRefTests.cs index 5e8af681..6cde686b 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/AssetObjectRefTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/AssetObjectRefTests.cs @@ -10,7 +10,6 @@ #nullable enable using System.Linq; -using com.IvanMurzak.ReflectorNet.Utils; using com.IvanMurzak.Unity.MCP.Runtime.Data; using NUnit.Framework; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/GameObjectRefTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/GameObjectRefTests.cs index 192b5dff..7ba0eae7 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/GameObjectRefTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/GameObjectRefTests.cs @@ -10,7 +10,6 @@ #nullable enable using System.Linq; -using com.IvanMurzak.ReflectorNet.Utils; using com.IvanMurzak.Unity.MCP.Runtime.Data; using NUnit.Framework; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/ObjectRefTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/ObjectRefTests.cs index 239383d5..400faac3 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/ObjectRefTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/RefTypes/ObjectRefTests.cs @@ -10,7 +10,6 @@ #nullable enable using System.Linq; -using com.IvanMurzak.ReflectorNet.Utils; using com.IvanMurzak.Unity.MCP.Runtime.Data; using NUnit.Framework; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter.meta new file mode 100644 index 00000000..accac5db --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4d4ff4715a39688429a175dfdb82fd13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs new file mode 100644 index 00000000..4dffffb1 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs @@ -0,0 +1,70 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable +using System.Collections; +using System.Linq.Expressions; +using System.Text.Json; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.Unity.MCP.Editor.API; +using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests +{ + public partial class MaterialReflectionConverterTests + { + [UnitySetUp] + public IEnumerator SetUp() + { + Debug.Log($"[{nameof(DemoTest)}] SetUp"); + yield return null; + } + [UnityTearDown] + public IEnumerator TearDown() + { + Debug.Log($"[{nameof(DemoTest)}] TearDown"); + yield return null; + } + + [UnityTest] + public IEnumerator Always_Valid_Test() + { + var goName = "DemoGO"; + var goRef = new Runtime.Data.GameObjectRef() { Name = goName }; + var materialEx = new CreateMaterialExecutor( + materialName: "TestMaterial__.mat", + shaderName: "Standard", + "Assets", "Unity-MCP-Test", "Materials" + ); + + materialEx + .AddChild(new CreateGameObjectExecutor(goName)) + .AddChild(new AddComponentExecutor(goRef)) + .AddChild(new CallToolExecutor( + toolMethod: typeof(Tool_GameObject).GetMethod(nameof(Tool_GameObject.Modify)), + json: JsonTestUtils.Fill(@"{ + ""gameObjectRefs"": ""{gameObjectRefs}"", + ""gameObjectDiffs"": ""{gameObjectDiffs}"" + }", + new System.Collections.Generic.Dictionary + { + { "{gameObjectRefs}", new Runtime.Data.GameObjectRef[] { goRef } }, + { "{gameObjectDiffs}", new SerializedMemberList() } + })) + ) + .AddChild(new ValidateToolResultExecutor()) + .Execute(); + yield return null; + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta new file mode 100644 index 00000000..17aa90e1 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69646e6142b6a204c8378c53c0ff5afb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs index dd309289..35c2bea4 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs @@ -10,10 +10,8 @@ #nullable enable using System.Collections.Generic; -using com.IvanMurzak.McpPlugin.Common; using com.IvanMurzak.Unity.MCP.Editor.API; using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; -using com.IvanMurzak.Unity.MCP.Runtime.Data; using NUnit.Framework; using UnityEditor; using UnityEngine; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs new file mode 100644 index 00000000..8744f3dd --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs @@ -0,0 +1,79 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable +using System; +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using com.IvanMurzak.Unity.MCP.Runtime.Extensions; +using UnityEditor; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils +{ + public class AddComponentExecutor : LazyNodeExecutor where T : UnityEngine.Component + { + protected readonly GameObjectRef _gameObjectRef; + + public T? Component { get; protected set; } + public GameObject? GameObject { get; protected set; } + + public AddComponentExecutor(GameObjectRef gameObjectRef) : base() + { + _gameObjectRef = gameObjectRef ?? throw new ArgumentNullException(nameof(gameObjectRef)); + + SetAction(() => + { + var componentTypeName = typeof(T).Name; + Debug.Log($"Adding component {componentTypeName} to GameObject"); + + // Find the GameObject + GameObject = _gameObjectRef.FindGameObject(out var error); + + if (error != null) + { + Debug.LogError($"Error finding GameObject: {error}"); + return null; + } + + if (GameObject == null) + { + Debug.LogError("GameObject not found."); + return null; + } + + // Add the component + Component = GameObject.AddComponent(); + + if (Component == null) + { + Debug.LogWarning($"Component of type {componentTypeName} could not be added to GameObject '{GameObject.name}'."); + return null; + } + + EditorUtility.SetDirty(GameObject); + + Debug.Log($"Added component {componentTypeName} to GameObject '{GameObject.name}' (Component InstanceID: {Component.GetInstanceID()})"); + + return Component; + }); + } + + protected override void PostExecute(object? input) + { + if (Component != null && GameObject != null) + { + Debug.Log($"Removing component {typeof(T).Name} from GameObject: {GameObject.name}"); + UnityEngine.Object.DestroyImmediate(Component); + Component = null; + } + base.PostExecute(input); + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs.meta new file mode 100644 index 00000000..5d7a2560 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/AddComponentExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44e57e96a9591724d8c692ca008350ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs new file mode 100644 index 00000000..89222d0c --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs @@ -0,0 +1,128 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable +using System; +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using com.IvanMurzak.Unity.MCP.Runtime.Extensions; +using UnityEditor; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils +{ + public class CreateGameObjectExecutor : LazyNodeExecutor + { + protected readonly string _name; + protected readonly GameObjectRef? _parentGameObjectRef; + protected readonly Vector3? _position; + protected readonly Vector3? _rotation; + protected readonly Vector3? _scale; + protected readonly bool _isLocalSpace; + protected readonly int _primitiveType; + + public GameObjectRef? GameObjectRef { get; protected set; } + public GameObject? GameObject { get; protected set; } + + public CreateGameObjectExecutor( + string name, + GameObjectRef? parentGameObjectRef = null, + Vector3? position = null, + Vector3? rotation = null, + Vector3? scale = null, + bool isLocalSpace = false, + int primitiveType = -1) : base() + { + _name = name ?? throw new ArgumentNullException(nameof(name)); + _parentGameObjectRef = parentGameObjectRef; + _position = position; + _rotation = rotation; + _scale = scale; + _isLocalSpace = isLocalSpace; + _primitiveType = primitiveType; + + SetAction(() => + { + Debug.Log($"Creating GameObject: {_name}"); + + // Find parent if provided + GameObject? parentGo = null; + if (_parentGameObjectRef?.IsValid ?? false) + { + parentGo = _parentGameObjectRef.FindGameObject(out var error); + if (error != null) + { + Debug.LogError($"Error finding parent GameObject: {error}"); + return null; + } + } + + // Create GameObject based on primitive type + GameObject = _primitiveType switch + { + 0 => GameObject.CreatePrimitive(PrimitiveType.Cube), + 1 => GameObject.CreatePrimitive(PrimitiveType.Sphere), + 2 => GameObject.CreatePrimitive(PrimitiveType.Capsule), + 3 => GameObject.CreatePrimitive(PrimitiveType.Cylinder), + 4 => GameObject.CreatePrimitive(PrimitiveType.Plane), + 5 => GameObject.CreatePrimitive(PrimitiveType.Quad), + _ => new GameObject() + }; + + GameObject.name = _name; + + // Set parent if provided + if (parentGo != null) + { + GameObject.transform.SetParent(parentGo.transform, false); + } + + // Set transform properties + var pos = _position ?? Vector3.zero; + var rot = _rotation ?? Vector3.zero; + var scl = _scale ?? Vector3.one; + + if (_isLocalSpace) + { + GameObject.transform.localPosition = pos; + GameObject.transform.localEulerAngles = rot; + GameObject.transform.localScale = scl; + } + else + { + GameObject.transform.position = pos; + GameObject.transform.eulerAngles = rot; + GameObject.transform.localScale = scl; + } + + // Create GameObjectRef + GameObjectRef = new GameObjectRef(GameObject.GetInstanceID()); + + EditorUtility.SetDirty(GameObject); + EditorApplication.RepaintHierarchyWindow(); + + Debug.Log($"Created GameObject: {_name} (InstanceID: {GameObject.GetInstanceID()})"); + + return GameObject; + }); + } + + protected override void PostExecute(object? input) + { + if (GameObject != null) + { + Debug.Log($"Destroying GameObject: {GameObject.name}"); + UnityEngine.Object.DestroyImmediate(GameObject); + GameObject = null; + GameObjectRef = null; + } + base.PostExecute(input); + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs.meta new file mode 100644 index 00000000..de0bced2 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateGameObjectExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 551518b6adf6d5246a7161c4e720d07d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From e1c910f8d3d67af1a2dcb3364a4959ff1394440a Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 27 Nov 2025 21:11:58 -0800 Subject: [PATCH 02/13] Enhance JSON converters to support AssetPath and AssetGuid properties; improve logging in UnityEngine_GameObject_ReflectionConvertor --- .../Types/AssetObjectRefConverter.cs | 2 +- .../Types/GameObjectRefConverter.cs | 12 ++++ .../Base/UnityArrayReflectionConvertor.cs | 64 +++++++++++++++++++ ...tyEngine_GameObject_ReflectionConvertor.cs | 31 +++++++-- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/AssetObjectRefConverter.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/AssetObjectRefConverter.cs index b6855074..0ebd00b6 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/AssetObjectRefConverter.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/AssetObjectRefConverter.cs @@ -51,7 +51,7 @@ public class AssetObjectRefConverter : JsonConverter assetObjectRef.AssetGuid = reader.GetString(); break; default: - throw new JsonException($"Unexpected property name: {propertyName}. " + throw new JsonException($"[AssetObjectRefConverter] Unexpected property name: {propertyName}. " + $"Expected {AssetObjectRef.AssetObjectRefProperty.All.JoinEnclose()}."); } } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/GameObjectRefConverter.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/GameObjectRefConverter.cs index e914f2fd..40ca8a99 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/GameObjectRefConverter.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/JsonConverters/Types/GameObjectRefConverter.cs @@ -50,6 +50,12 @@ public class GameObjectRefConverter : JsonConverter case GameObjectRef.GameObjectRefProperty.Name: result.Name = reader.GetString(); break; + case AssetObjectRef.AssetObjectRefProperty.AssetPath: + result.AssetPath = reader.GetString(); + break; + case AssetObjectRef.AssetObjectRefProperty.AssetGuid: + result.AssetGuid = reader.GetString(); + break; default: throw new JsonException($"Unexpected property name: {propertyName}. " + $"Expected {GameObjectRef.GameObjectRefProperty.All.JoinEnclose()}."); @@ -83,6 +89,12 @@ public override void Write(Utf8JsonWriter writer, GameObjectRef value, JsonSeria if (!string.IsNullOrEmpty(value.Name)) writer.WriteString(GameObjectRef.GameObjectRefProperty.Name, value.Name); + if (!string.IsNullOrEmpty(value.AssetPath)) + writer.WriteString(AssetObjectRef.AssetObjectRefProperty.AssetPath, value.AssetPath); + + if (!string.IsNullOrEmpty(value.AssetGuid)) + writer.WriteString(AssetObjectRef.AssetObjectRefProperty.AssetGuid, value.AssetGuid); + writer.WriteEndObject(); } } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityArrayReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityArrayReflectionConvertor.cs index ce3dad08..108e8037 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityArrayReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityArrayReflectionConvertor.cs @@ -13,8 +13,11 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; +using System.Text.Json; using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Convertor; +using com.IvanMurzak.ReflectorNet.Model; using UnityEngine; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -22,6 +25,67 @@ namespace com.IvanMurzak.McpPlugin.Common.Reflection.Convertor { public partial class UnityArrayReflectionConvertor : ArrayReflectionConvertor { + public override object? Deserialize( + Reflector reflector, + SerializedMember data, + Type? fallbackType = null, + string? fallbackName = null, + int depth = 0, + StringBuilder? stringBuilder = null, + ILogger? logger = null) + { + if (fallbackType == null || !fallbackType.IsArray) + return base.Deserialize(reflector, data, fallbackType, fallbackName, depth, stringBuilder, logger); + + var elementType = fallbackType.GetElementType(); + if (elementType == null) + return base.Deserialize(reflector, data, fallbackType, fallbackName, depth, stringBuilder, logger); + + if (data.valueJsonElement == null || data.valueJsonElement.Value.ValueKind != JsonValueKind.Array) + return base.Deserialize(reflector, data, fallbackType, fallbackName, depth, stringBuilder, logger); + + var jsonArray = data.valueJsonElement.Value; + var length = jsonArray.GetArrayLength(); + var array = Array.CreateInstance(elementType, length); + + int index = 0; + foreach (var element in jsonArray.EnumerateArray()) + { + SerializedMember? member = null; + if (element.ValueKind == JsonValueKind.Object && + ( + element.TryGetProperty(nameof(SerializedMember.typeName), out _) || + element.TryGetProperty(nameof(SerializedMember.fields), out _) || + element.TryGetProperty(nameof(SerializedMember.props), out _)) + ) + { + try + { + member = JsonSerializer.Deserialize(element.GetRawText()); + if (member != null && element.TryGetProperty(nameof(SerializedMember.valueJsonElement), out var valueProp)) + { + member.valueJsonElement = valueProp; + } + } + catch { } + } + + if (member == null) + { + member = new SerializedMember + { + valueJsonElement = element + }; + } + + var value = reflector.Deserialize(member, elementType, null, depth + 1, stringBuilder, logger); + array.SetValue(value, index); + index++; + } + + return array; + } + public override IEnumerable? GetSerializableFields(Reflector reflector, Type objType, BindingFlags flags, ILogger? logger = null) => objType.GetFields(flags) .Where(field => field.GetCustomAttribute() == null) diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs index 99db7cab..11fee955 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs @@ -148,13 +148,31 @@ protected override bool SetValue( { var padding = StringUtils.GetPadding(depth); - if (logger?.IsEnabled(LogLevel.Warning) == true) - logger.LogWarning($"{padding}Cannot set value for '{type.GetTypeShortName()}'. This type is not supported for setting values. Maybe did you want to set a field or a property? If so, set the value in the '{nameof(SerializedMember.fields)}' or '{nameof(SerializedMember.props)}' property instead."); + if (logger?.IsEnabled(LogLevel.Trace) == true) + logger.LogTrace($"{padding}Set value type='{type.GetTypeName(pretty: true)}'. Convertor='{GetType().GetTypeShortName()}'."); - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Warning] Cannot set value for '{type.GetTypeName(pretty: false)}'. This type is not supported for setting values. Maybe did you want to set a field or a property? If so, set the value in the '{nameof(SerializedMember.fields)}' or '{nameof(SerializedMember.props)}' property instead."); + try + { + obj = value + .ToGameObjectRef( + reflector: reflector, + suppressException: false, + depth: depth, + stringBuilder: stringBuilder, + logger: logger) + .FindGameObject(); + return true; + } + catch (Exception ex) + { + if (logger?.IsEnabled(LogLevel.Error) == true) + logger.LogError(ex, $"{padding}[Error] Failed to deserialize value for type '{type.GetTypeName(pretty: false)}'. Convertor: {GetType().GetTypeShortName()}. Exception: {ex.Message}"); + + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Error] Failed to set value for type '{type.GetTypeName(pretty: false)}'. Convertor: {GetType().GetTypeShortName()}. Exception: {ex.Message}"); - return false; + return false; + } } protected override bool TryPopulateField( @@ -167,6 +185,9 @@ protected override bool TryPopulateField( BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, ILogger? logger = null) { + if (logger?.IsEnabled(LogLevel.Information) == true) + logger.LogInformation($"[UnityEngine_GameObject_ReflectionConvertor] TryPopulateField called for obj type: {obj?.GetType().FullName}, field: {fieldValue.name}"); + var padding = StringUtils.GetPadding(depth); var go = obj as UnityEngine.GameObject; if (go == null) From d3a946576d0d96c857f089c24dfd976ec4e9baf9 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 27 Nov 2025 21:16:43 -0800 Subject: [PATCH 03/13] Add executors for creating prefabs, scriptable objects, and textures; implement dynamic call tool executor --- .../Utils/Executor/CreatePrefabExecutor.cs | 57 +++++++++++++++++++ .../Executor/CreatePrefabExecutor.cs.meta | 11 ++++ .../CreateScriptableObjectExecutor.cs | 34 +++++++++++ .../CreateScriptableObjectExecutor.cs.meta | 11 ++++ .../Utils/Executor/CreateTextureExecutor.cs | 46 +++++++++++++++ .../Executor/CreateTextureExecutor.cs.meta | 11 ++++ .../Utils/Executor/DynamicCallToolExecutor.cs | 47 +++++++++++++++ .../Executor/DynamicCallToolExecutor.cs.meta | 11 ++++ 8 files changed, 228 insertions(+) create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs.meta diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs new file mode 100644 index 00000000..37a413cd --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs @@ -0,0 +1,57 @@ +#nullable enable +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using com.IvanMurzak.Unity.MCP.Runtime.Extensions; +using UnityEditor; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils +{ + public class CreatePrefabExecutor : BaseCreateAssetExecutor + { + private readonly GameObjectRef? _sourceGameObjectRef; + + public CreatePrefabExecutor(string assetName, GameObjectRef? sourceGameObjectRef = null, params string[] folders) : base(assetName, folders) + { + _sourceGameObjectRef = sourceGameObjectRef; + + SetAction((input) => + { + Debug.Log($"Creating Prefab: {AssetPath}"); + + GameObject? sourceGo = null; + + if (_sourceGameObjectRef?.IsValid ?? false) + { + sourceGo = _sourceGameObjectRef.FindGameObject(out var error); + if (error != null) Debug.LogError(error); + } + else if (input is GameObject go) + { + sourceGo = go; + } + + if (sourceGo == null) + { + Debug.LogError("Source GameObject for Prefab creation not found."); + return null; + } + + PrefabUtility.SaveAsPrefabAsset(sourceGo, AssetPath); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + + Asset = AssetDatabase.LoadAssetAtPath(AssetPath); + + if (Asset == null) + { + Debug.LogError($"Failed to load created Prefab at {AssetPath}"); + } + else + { + Debug.Log($"Created Prefab: {AssetPath}"); + } + + return Asset; + }); + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs.meta new file mode 100644 index 00000000..09dc1ae7 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7136ca47ab8eb343974af737b10d784 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs new file mode 100644 index 00000000..5609225b --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs @@ -0,0 +1,34 @@ +using UnityEditor; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils +{ + public class CreateScriptableObjectExecutor : BaseCreateAssetExecutor where T : ScriptableObject + { + public CreateScriptableObjectExecutor(string assetName, params string[] folders) : base(assetName, folders) + { + SetAction(() => + { + Debug.Log($"Creating ScriptableObject: {AssetPath}"); + + var so = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(so, AssetPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + + Asset = AssetDatabase.LoadAssetAtPath(AssetPath); + + if (Asset == null) + { + Debug.LogError($"Failed to load created ScriptableObject at {AssetPath}"); + } + else + { + Debug.Log($"Created ScriptableObject: {AssetPath}"); + } + + return Asset; + }); + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs.meta new file mode 100644 index 00000000..e354fc76 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 03d5935c58ea632429c76fb4b520e058 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs new file mode 100644 index 00000000..f3f910d7 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs @@ -0,0 +1,46 @@ +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils +{ + public class CreateTextureExecutor : BaseCreateAssetExecutor + { + public CreateTextureExecutor(string assetName, params string[] folders) : base(assetName, folders) + { + SetAction(() => + { + Debug.Log($"Creating Texture: {AssetPath}"); + + var texture = new Texture2D(64, 64); + // Fill with some color + for (int x = 0; x < 64; x++) + { + for (int y = 0; y < 64; y++) + { + texture.SetPixel(x, y, Color.red); + } + } + texture.Apply(); + + var bytes = texture.EncodeToPNG(); + File.WriteAllBytes(AssetPath, bytes); + + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + + Asset = AssetDatabase.LoadAssetAtPath(AssetPath); + + if (Asset == null) + { + Debug.LogError($"Failed to load created texture at {AssetPath}"); + } + else + { + Debug.Log($"Created Texture: {AssetPath}"); + } + + return Asset; + }); + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs.meta new file mode 100644 index 00000000..c4affc68 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2069b2fd4431c94f80b96966d7f0547 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs new file mode 100644 index 00000000..6399d19b --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using com.IvanMurzak.McpPlugin; +using com.IvanMurzak.McpPlugin.Common.Model; +using com.IvanMurzak.ReflectorNet; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils +{ + public class DynamicCallToolExecutor : LazyNodeExecutor + { + public DynamicCallToolExecutor(MethodInfo toolMethod, Func jsonProvider, Reflector? reflector = null) : this( + toolName: toolMethod.GetCustomAttribute()?.Name + ?? throw new ArgumentException("Tool method must have a McpPluginTool attribute."), + jsonProvider: jsonProvider, + reflector: reflector) + { + } + + public DynamicCallToolExecutor(string toolName, Func jsonProvider, Reflector? reflector = null) : base() + { + if (toolName == null) throw new ArgumentNullException(nameof(toolName)); + if (jsonProvider == null) throw new ArgumentNullException(nameof(jsonProvider)); + + reflector ??= McpPlugin.McpPlugin.Instance!.McpManager.Reflector ?? + throw new ArgumentNullException(nameof(reflector), "Reflector cannot be null. Ensure McpPlugin is initialized before using this executor."); + + SetAction(() => + { + var json = jsonProvider(); + Debug.Log($"{toolName} Started with JSON:\n{JsonTestUtils.Prettify(json)}"); + + var parameters = JsonSerializer.Deserialize>(json, reflector.JsonSerializerOptions); + var request = new RequestCallTool(toolName, parameters!); + + var task = McpPlugin.McpPlugin.Instance!.McpManager.ToolManager!.RunCallTool(request); + var result = task.Result; + + Debug.Log($"{toolName} Completed"); + + return result; + }); + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs.meta new file mode 100644 index 00000000..d6c35a53 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 09059133b17395a49be8ca7a27b6abc3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 505ddcd81f0ecb8a6b2f858bc9f8fbba78d403be Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 27 Nov 2025 21:16:55 -0800 Subject: [PATCH 04/13] Add data population test scripts and related assets for improved testing coverage --- .../Scripts/DataPopulationTestScript.cs | 20 +++ .../Scripts/DataPopulationTestScript.cs.meta | 11 ++ .../DataPopulationTestScriptableObject.cs | 10 ++ ...DataPopulationTestScriptableObject.cs.meta | 11 ++ .../DataPopulationTests.cs | 147 ++++++++++++++++++ .../DataPopulationTests.cs.meta | 11 ++ .../MaterialReflectionConverterTests.cs | 58 +++---- 7 files changed, 239 insertions(+), 29 deletions(-) create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs.meta diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs new file mode 100644 index 00000000..6ddf7faa --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.TestFiles +{ + public class DataPopulationTestScript : MonoBehaviour + { + public Material materialField; + public GameObject gameObjectField; + public Texture2D textureField; + public Sprite spriteField; + public ScriptableObject scriptableObjectField; + public GameObject prefabField; + + public Material[] materialArray; + public GameObject[] gameObjectArray; + + public int intField; + public string stringField; + } +} diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs.meta b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs.meta new file mode 100644 index 00000000..07f3a9ac --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc89d6a3123d605488671f413ddfb193 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs new file mode 100644 index 00000000..167e0515 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.TestFiles +{ + [CreateAssetMenu(fileName = "DataPopulationTestScriptableObject", menuName = "Tests/DataPopulationTestScriptableObject")] + public class DataPopulationTestScriptableObject : ScriptableObject + { + public int value; + } +} diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs.meta b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs.meta new file mode 100644 index 00000000..b2262720 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5637e27532d5fcc48b7dead4b8afac1b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs new file mode 100644 index 00000000..d95cb272 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs @@ -0,0 +1,147 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.Unity.MCP.Editor.API; +using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using com.IvanMurzak.Unity.MCP.TestFiles; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests +{ + public class DataPopulationTests + { + [UnityTest] + public IEnumerator Populate_All_Types_Test() + { + // Executors for creating assets + var materialEx = new CreateMaterialExecutor("TestMaterial.mat", "Standard", "Assets", "Unity-MCP-Test", "DataPopulation"); + var textureEx = new CreateTextureExecutor("TestTexture.png", "Assets", "Unity-MCP-Test", "DataPopulation"); + var soEx = new CreateScriptableObjectExecutor("TestSO.asset", "Assets", "Unity-MCP-Test", "DataPopulation"); + + var prefabSourceGoEx = new CreateGameObjectExecutor("PrefabSource"); + var prefabEx = new CreatePrefabExecutor("TestPrefab.prefab", null, "Assets", "Unity-MCP-Test", "DataPopulation"); + + // Target GameObject + var targetGoName = "TargetGO"; + var targetGoRef = new GameObjectRef() { Name = targetGoName }; + var targetGoEx = new CreateGameObjectExecutor(targetGoName); + var addCompEx = new AddComponentExecutor(targetGoRef); + + // Validation Executor + var validateEx = new LazyNodeExecutor(); + validateEx.SetAction((input) => + { + var comp = addCompEx.Component; + Assert.IsNotNull(comp, "Component should exist"); + + Assert.AreEqual(42, comp!.intField, "intField not populated"); + Assert.AreEqual("Hello World", comp.stringField, "stringField not populated"); + + Assert.IsNotNull(comp.materialField, "Material should be populated"); + Assert.AreEqual(materialEx.Asset!.name, comp.materialField.name); + + Assert.IsNotNull(comp.gameObjectField, "GameObject should be populated"); + Assert.AreEqual(targetGoEx.GameObject!.name, comp.gameObjectField.name); + + Assert.IsNotNull(comp.textureField, "Texture should be populated"); + Assert.AreEqual(textureEx.Asset!.name, comp.textureField.name); + + Assert.IsNotNull(comp.scriptableObjectField, "SO should be populated"); + Assert.AreEqual(soEx.Asset!.name, comp.scriptableObjectField.name); + + Assert.IsNotNull(comp.prefabField, "Prefab should be populated"); + Assert.AreEqual(prefabEx.Asset!.name, comp.prefabField.name); + + Assert.IsNotNull(comp.materialArray, "Material array should be populated"); + Assert.AreEqual(2, comp!.materialArray!.Length); + Assert.AreEqual(materialEx.Asset.name, comp.materialArray[0].name); + + Assert.IsNotNull(comp.gameObjectArray, "GameObject array should be populated"); + Assert.AreEqual(2, comp.gameObjectArray!.Length); + }); + + // Chain creation + var modifyEx = new DynamicCallToolExecutor( + typeof(Tool_GameObject).GetMethod(nameof(Tool_GameObject.Modify)), + () => + { + var reflector = McpPlugin.McpPlugin.Instance!.McpManager.Reflector; + + var matRef = new AssetObjectRef() { AssetPath = materialEx.AssetPath }; + var texRef = new AssetObjectRef() { AssetPath = textureEx.AssetPath }; + var soRef = new AssetObjectRef() { AssetPath = soEx.AssetPath }; + var prefabRef = new AssetObjectRef() { AssetPath = prefabEx.AssetPath }; + var goRef = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + + var goModification = SerializedMember.FromValue( + reflector: reflector, + name: "TargetGO", + type: typeof(GameObject), + value: new GameObjectRef(targetGoEx.GameObject!.GetInstanceID()) + ); + + var componentModification = SerializedMember.FromValue( + reflector: reflector, + name: "DataPopulationTestScript", + type: typeof(DataPopulationTestScript), + value: new ComponentRef(addCompEx.Component!.GetInstanceID()) + ); + + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialField", type: typeof(Material), value: matRef)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectField", type: typeof(GameObject), value: goRef)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "textureField", type: typeof(Texture2D), value: texRef)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectField", type: typeof(DataPopulationTestScriptableObject), value: soRef)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "prefabField", type: typeof(GameObject), value: prefabRef)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "intField", type: typeof(int), value: 42)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "stringField", type: typeof(string), value: "Hello World")); + + var matRefArrayItem = new ObjectRef(materialEx.Asset!.GetInstanceID()); + var goRefArrayItem = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var prefabRefArrayItem = new ObjectRef(prefabEx.Asset!.GetInstanceID()); + + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialArray", type: typeof(Material[]), value: new object[] { matRefArrayItem, matRefArrayItem })); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectArray", type: typeof(GameObject[]), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + + goModification.AddField(componentModification); + + var gameObjectDiffs = new SerializedMemberList { goModification }; + + var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true }; + var gameObjectRefsJson = System.Text.Json.JsonSerializer.Serialize(new GameObjectRef[] { targetGoRef }, options); + var gameObjectDiffsJson = System.Text.Json.JsonSerializer.Serialize(gameObjectDiffs, options); + + var json = JsonTestUtils.Fill(@"{ + ""gameObjectRefs"": {gameObjectRefs}, + ""gameObjectDiffs"": {gameObjectDiffs} + }", + new Dictionary + { + { "{gameObjectRefs}", gameObjectRefsJson }, + { "{gameObjectDiffs}", gameObjectDiffsJson } + }); + + Debug.Log($"[DataPopulationTests] JSON Input: {json}"); + return json; + } + ); + + modifyEx.AddChild(validateEx); + addCompEx.AddChild(modifyEx); + targetGoEx.AddChild(addCompEx); + + materialEx + .Nest(textureEx) + .Nest(soEx) + .Nest(prefabSourceGoEx) + .Nest(prefabEx) + .Nest(targetGoEx); + + materialEx.Execute(); + yield return null; + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs.meta new file mode 100644 index 00000000..1feca592 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1711b8568120caa42b713e8907ca8542 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs index 4dffffb1..03cb69da 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs @@ -36,35 +36,35 @@ public IEnumerator TearDown() yield return null; } - [UnityTest] - public IEnumerator Always_Valid_Test() - { - var goName = "DemoGO"; - var goRef = new Runtime.Data.GameObjectRef() { Name = goName }; - var materialEx = new CreateMaterialExecutor( - materialName: "TestMaterial__.mat", - shaderName: "Standard", - "Assets", "Unity-MCP-Test", "Materials" - ); + // [UnityTest] + // public IEnumerator Always_Valid_Test() + // { + // var goName = "DemoGO"; + // var goRef = new Runtime.Data.GameObjectRef() { Name = goName }; + // var materialEx = new CreateMaterialExecutor( + // materialName: "TestMaterial__.mat", + // shaderName: "Standard", + // "Assets", "Unity-MCP-Test", "Materials" + // ); - materialEx - .AddChild(new CreateGameObjectExecutor(goName)) - .AddChild(new AddComponentExecutor(goRef)) - .AddChild(new CallToolExecutor( - toolMethod: typeof(Tool_GameObject).GetMethod(nameof(Tool_GameObject.Modify)), - json: JsonTestUtils.Fill(@"{ - ""gameObjectRefs"": ""{gameObjectRefs}"", - ""gameObjectDiffs"": ""{gameObjectDiffs}"" - }", - new System.Collections.Generic.Dictionary - { - { "{gameObjectRefs}", new Runtime.Data.GameObjectRef[] { goRef } }, - { "{gameObjectDiffs}", new SerializedMemberList() } - })) - ) - .AddChild(new ValidateToolResultExecutor()) - .Execute(); - yield return null; - } + // materialEx + // .AddChild(new CreateGameObjectExecutor(goName)) + // .AddChild(new AddComponentExecutor(goRef)) + // .AddChild(new CallToolExecutor( + // toolMethod: typeof(Tool_GameObject).GetMethod(nameof(Tool_GameObject.Modify)), + // json: JsonTestUtils.Fill(@"{ + // ""gameObjectRefs"": ""{gameObjectRefs}"", + // ""gameObjectDiffs"": ""{gameObjectDiffs}"" + // }", + // new System.Collections.Generic.Dictionary + // { + // { "{gameObjectRefs}", new Runtime.Data.GameObjectRef[] { goRef } }, + // { "{gameObjectDiffs}", new SerializedMemberList() } + // })) + // ) + // .AddChild(new ValidateToolResultExecutor()) + // .Execute(); + // yield return null; + // } } } From 64c65d76eb5ac8d4922268dd2e7f71af16e0fdf7 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 28 Nov 2025 03:25:13 -0800 Subject: [PATCH 05/13] Refactor UnityArrayReflectionConvertor by removing unused Deserialize method to streamline code --- .../netstandard2.1/McpPlugin.Common.dll | Bin 44032 -> 44032 bytes .../netstandard2.1/McpPlugin.dll | Bin 139776 -> 139776 bytes .../ReflectorNet.dll | Bin 151040 -> 150528 bytes .../Base/UnityArrayReflectionConvertor.cs | 64 ------------------ 4 files changed, 64 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.McpPlugin/netstandard2.1/McpPlugin.Common.dll b/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.McpPlugin/netstandard2.1/McpPlugin.Common.dll index da68ab13b8f904b43c34cf1497d514d10dc31ff4..2740338bc7e1fb934aa5aa2c5ef14815ca700cc2 100644 GIT binary patch delta 313 zcmZp;!PIbrX+j4}@7J=fjXg5yj4YE^rdu+zFfeR>nx3vEP=531og-!qj@?nSOxy3g z0)O*r10>p}y7UyW#Rt?j?wCAd zL7@UvFy#zX5U5%}+{AJAd@jRpZ$l~qHft`7XXFGr6bx84Ph7N|8R#&}W#x?FCJcHE z1`OH^sSK73X+UTMgq94342eK&%8&%4K|C`cY0hBGU;-pf87zQwGD8ZG1yYp=ghoI) YBcM6SKzSpexXEPp6^0-yRvc#r0L|}NApigX delta 313 zcmZp;!PIbrX+j6f>Y3f2H}=S+Gcr$JnQqC<%)qqyX?nVvz`f#SktcMLedjBmcG>pW z)p>JFp8^Y`@#fk|);yNRdIow13=GDM3_u2uqn%`GU}9lno@$h2Xr5?hm}+F4XkeaZ zYLRG>oN8`jk(vUeQY=zTH-DVx%*Ya5A@y~##ex8Vv|p0VGfsIqEI)NDxi4^wLS~$*n7?Sd&0WT9eM=KP13d!<24hABAOpzJPPI%+GcvL?Of)o2O0+aI zvotq0F*mg^Nlr0MG)zo1GBz?uHa9Xg+5UisDTbS+j{SA}bO#Bh0D(!ej`w$VxHxXv z`cT?=eS_`v1rkij3Q*C9Fj0{D%KuuO>t8KcdOh*T%k9dNOsR~VFt@X8pD4vt&CF=B zomY;@oiW^mL65C0 delta 340 zcmZoT!qISqV?qZ@v60vB#-6P`j8cw_%+r@UGWv=$Gavw$Hi*r{z|6q3o!f~~akfBy z`>soN8`jk(vUeQY=zTw?E)vis5GQXkWB@x`PB$fWV8(DGRw~5J1DQs7kWJ+b^gt?u0`$Q?GYGy{` z?YweK?u_Ba40;R(4B8Ay45mQZguw!c%^6Y|jDRABKvp7y8IT6?je%?fphy}}r3H{} j!H^6THwWuUWk`YYQh?$q45rgp%QG1QEx9Ytq|5{WixpPj diff --git a/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.ReflectorNet/ReflectorNet.dll b/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.ReflectorNet/ReflectorNet.dll index 90042ba626320972ea9548830ac5c3c107f9babd..778213f4f9c2101cbc22c2eec8ec115d57b0e365 100644 GIT binary patch delta 57667 zcmcG%2YggT_dY&z@7>*GlWcZVHl1wBCQBMAln^=yQlv^35fBs*UDzNkgbg5Ip?aw{ zieLj21gxN-h!q82^%V<(VDDlt_TTGYW1+EKrm=R}g|2rzZlAK{sisKM|b z>!}~-*(nj!KK{DnpTbP6&dk~PA)N6w{ktfwcJ)&(QuxeWtF*nGf36QOm!*EjnYp?n z^mo^dgnAwmV}%8Z`KOyYmIs<(7@;u-o3 zXIrl6dz@W)r2f0J3Y;yI9_2slUnh-0N&n=ID7iIx5%Q@i=}23p^g+5X#f$W(l({@| z*NjvX=P~-6v>a~JH>I6PFM*w&JD_Hfl2j1k?T4Jxlf#%f(p#kOOfTh``iJS$_1cUm zeQZVwx9f8ZP{CbSL*9&sol#m*yaf(*=^thk$7K=6M)E-dXeiFs zvzlb^c)dfDoXi9wZV}aMsd^Gsw|J5zXO2D>oQeASCOO=xKZ`U;|D;JpTrSnRheW1G zMz>y+nZc9wo|!p3MZYdHD=x1gOTNAlEUEg_U`g9`Dl?9gKV0MBtr1x#d4b+OyBEJn zzd3s#FVsKCJ{VU737(#4q)2l{A1|_aOY}WCAM=*_9l3{4=}Px7r2E{1kjCd7Pj3x& zt68mG3p13OCP>}IYFsJ-_^0Sslt2e6U#^N zUi$Op-C%;P;xfKu*Tjl;8t<{|zE*J*Xx{d@`UmYCemI$X+L7maBPpI^KC(2tWRm&F zvhb4O<|8x1OS+qnEDtYfX+E+dyd>9rwmRxVfQxR(3f>+tJigKaG!p0ht|7Z?oi74I{lB1>Hbl0nU`~BHG3J->L(jue*^qZ z1H7OCKG6W*(g1%Pj{Vg~!V9XWG{D0f;OiUUu?_I}26$2fJTnaYt<`UY6+Y2;B8&kDoO}_>X-$AZ^jN9O?MJC48{Hrf;k0ffzQb zw>kROJ4k=0?*cw-*QkE)aJ1QGKnc?Q1DfbJ4rnEEckvPW6_;l7vHHAAtN1v5-=$so zW?dbaudg2%rMDQ^4ybxy5g)m0!@zZ%kI}0JKh3Yye;fQRs=qa4v447a+znHXObIW! zSUJ)lHd`o1CWrInC`YajFL5YGd|@R8Nd?0*&sJXr+uTa+hctVrwI`Tm`+1v z$Hb|gY2c05EZ+F5kZFA}XUyWgn#wE=i{58kO5838NC|60pE)kAZ|5{N*^(sIINwPP zZ9E*(hQs2}Jkv=^gd?K(GU#0%)EgG~9z^AHW%}Es|BXy%d?eJJK|-VAqC6P1)_7}S zyEv<77G>PXn+ox}Zzk8qEKI0Po=L9rL1M`*-3?B1)OiVTEJ z9;^*khrIyIE-<%l63*Pf8;qyZj* zvdNvQc&H`sWk@!879mxQ6Y!Ob`RZVRXzAqs# z*j0MQ#2kODB8G8sv`tIspA>I-Ux73fis2j)7hw!%L3VW6%tQ8_i^* znYr0)ve{;eTr4&q2vn;l23t4b2)H{9?lxG4h46UApf`EGrO?IZIYI1Nie{2#Xrh-b zB2`+_$rh2mgr+=WBKZi-A>M4JP3{z5MGk>flm1P0ivGZY zvY~l>3({skML&$j6lnJS*$7*`B2%G=6LY8{XAUdkTg)7hmDn<|xJWs9L@ACaIaZ=o zM>O0-{cW?@BIM*jz0oGv8)Y_sgl}{%rYeV7^oAp*SWPI5cEpGP6&ica=^!}F_gqLN z&*iW|>gg~!V!ZRhvqPO=aWCh zRBLm_#};BY0CBP28&HgknCfC7G}aMoFqgk?(;PPH{boUv+z7BNw+JwY&5I43wec`( zU593E7Qb)7d6=a^g_olV)45IjY1EQhH{L{1794eXp46owqZFUyM})11Ch>M7as~$@ z=Mq~)Mra%#g`{8{^L!DskaST3fFq>3wJxep7Le{vl6EJQCAyHL;rDh03|ct|}is$i(Q2%HI?E|@K>=Av|P ze8Esd5fX}`%@NhDVRfww+mGXF-f?Cwj;qH0%j{O`305;K;dW@m+OcL)lbA}IjwXud zM!lmq8GEK--nPjXp@o7}kGOy_t<=Q6z{=T2-|j8Hn1ae#YSF~lFEBg3H>03HYq-ra z&$$cApgep}SQIq}w++_CUVYT$q>hUzOIt+nh3^Z&w}gmP3S#&c5yYiL^y#&etNd5O zO`3NC3VgN5dPbt%>PwN#a*FX(pm~>pSRA2wm*ZNL?%PT=QK5ZnBxg24}Ti>1&p|2F=iAf$gOfC@P z%c0d^E!SwVDACiV<>}u{Gl#MbZI-L7WQGtQC=#aGw0=h8 zDZbbBw`UZ^4xo*TNp4&e@9ld0%zS=UZ!2qt_;0}CWZMRS(dg=YMQ*rzlj5 z4P|=&ISG;=W3RnGAyl5M_zYEa=}VSb;x4wC1H)U~`+YAZ2o91^lo4WNa=CDZNu0h? zhg9ja>+af2b~nZ_;0^%jUE6lDWl+#)b?*Vl%hi4=35o zKBDx%SuE)KmbngPa)sV(Zn^&X+$6WDIfY(7hefQwA-g0Tsn9KJI;s!S^X84jvBH9R zSuOT}-w`RsuJ0J~Au|=-YM{mpo7rLVh4Ja{&daf#38|^J(4*(4+Gr9A2t!fx`Ta7A zTR^6`I~KE_Arsp_$Q+_?oZls83z~_Kj&wv{5#xx~SIkRnu?%$_Q5V)Jj8EwFct}k{ zCz>1r!cesGhJH<6`x~8bc&aS8O3%F|MW4GMKE6{+Y`{YT9JBeY6B2N@Yc_j+*AFac z8%J&kTwz7zRy z5fF#F1Fcwf7Sd?~&eU)gg1ZHQO`eDK;ftDt4xls5^{g8cQ{!7vpr>L)q&dvkaQOZ} zL7~%^SE={A(Gzh=r9gA^x*JpVvx}UeBkYXsy3v`s4IKYWK)-XF$272iY1lL%grc~` zO`g!U=IRq}x;!ytfNpOs2B^h*Oz*I)yPk7%-*csxexsxvMWOw9qsg1 z%lhd*E$e%Z)QW!{LDD2nA=Eh{u*27XTiz-AQFK5-TB((-!L&bE+^A^cFepk`j%_?W^@S@+`4jr1E4n5QX{5$>-EnJr3?4Xy)Tp|(jPKPa-8za7 z)eqk~J82)NwKz}E+}H-w{?`^^i}XCDkJ58f<#_V-Z%6)5GReI<_5i+_`dxZeho@0h zMB;vA&YBM(t@ct`9Z@`;Fy+j7hA>5|Q-K84Eah~nu?G7g3F zJYUa46v0yvUutF^8B)Vga$6g+h}1(r2!U@;}+0?xLmZEn=-T?w<>A?2@4Kc|)lq68Pf1GhyiPfxk(JS{HN-l!>&IbFhr8bbv-dxsyNeU^%w9a^ zv3lRs_pfQ1{1#P8_59bUn}*Igg1RGgzBb7}Up!_EIX92O?e+_Z=h~kUhUOYd#dFT$ zA)X#2vnLWfj=TZ&s^3PQj@}ESBN9Bv0E)7W8hCDIjXs``Ct>xw#Q2^lV_|pwKgTCgMRPY{(c-9Ib!8g0($zusuu{|P=rmZHK^AwLN7`i(`(^*^%6De^&%Li zS4L1V4T{AaZ+^TqUPC|8$uKlrxvsbUIP`}5<1sH(z=I?8@79&2eL#gJN9fEz90=0w zteMq;eg!=2)GxiQy|RWZEYT0#mW1au$8PI^i7w~%o_KnB-R(}xDA=ufPUsJ9O2%{d z6)z?BISG-oP=rfnWQ8S3oDvrp1=b3SGqj*dMXc)i5UkF#YG`>z$6KI->N%x%x+mE` zx+CuIjYk^KA}@~wL_=ou_-WKHvRGvOz|PzE1sE~cV84;3W^&nTYOo7b|&c zgGb8+@Vv(M!75BH;Vvr-aaofJ&_01I&lPyUv6vnt8otY@=xq3dk0wX^rTw0fK|bFL zVQQa8YZB@Gl(dCLCWd=bHX0f!8J^Ej7{@%H<5HxU3j+I@0{l;qBNWT2VY-pmK$(yk6j|>WuUJ#W>96M`r4dZ3&S=U-FdRVqzq-y#uVpue^y3&Sa3-q?R+p65GMmj`Du>@qHF-uH%i;<)X6 z=G}2mu8fm^VIF2=GLPhvd1u1RBQwLIdHUc;42)-e)a~i| zotqpyWP;0UXa$W{5$6Fb&NEOa(?F&1P;lYSAa#h<`!lH{!wEY%hNl+}#dR-ofzkR4 z`QvX#tA>eh75O4$1io{36AK>mg!hk}y0zdGrdee$HmMk@dT10znLWSa5^Y(Ge&P|H z-vDCFk)Cd(Lj*FvBU9k3!^j7VUb8tfA;ei39X~dD_rcAna>yFJ>(G+oOrEZ&T->wa z{EbIbn843MBqngPe8`AtJE4m{c}serzDcT}I!p!ss{SAu9qqe>PWO$>n!$5B?RH5bUEF{ysMQ7yaOu}KynU8vP zptRwM3)vv1uQW7^6^h0;iO2Xp>!eIWoAf)M$h4!zzgn^8P=GHZt&K_SJOiON$oJ0% z0*P#}VqsGivSR&39{bxdfFZCfuwuQsHO;fXRkD%;U$FgueGs;AjSpM6meInMCLG*o zSJ&S9Qj^fRq4AX*kNo8G$P4&oevt2<`K9(;ztAqfekI3a`wRP}HpDM_iwEMI#u7du z%oDW@mhkv{v-H@PVlQm`&14i!7Z))8rXb%xGoD1sRadOFw2he*W<38V#xG@hkNapF z^Xk^(fZ-Fv3|~ryCx?XqO~)F;!$WqhzH>*i?tLmjpRg^SU#lP3oxv6TgWZX(@g&TQ z`xZR&GUJX?!OO$orh$hMm)H@vVP=mC1!)G+=t}oly|HML#CRq7BK_>PZg~E*V|xn3 zXX|y_%?8@AJyzsA1~%`sDPGB#_8^f8AIvZ`F_9+Cbg2m*c1cY<(BwMOL~hZeDrgc& zOoltn=pwVe^}!~2Vy25#RFB2bF@mnRH_#ZZJ=pn(mE)TSU2@YOA^Zc0dhtU^Jw>^e_oV6-=OpuN|W^s z+im*(y%xRM!`7%{4LH6L9P1yp>#y#NjryhmHB#TXivllKpS(8_cDth@yN0*h0vxTP zS~Mu_Axe8RD7{RSUe=&=iqS?19)Ato?TMNaMo-f3+MR89I-Wf3-zGi1l`h$M<8Kk} zX61fRWi#=l1{=!ypi>fz`5>tz&9?%QWjNC97Wd3UXvo{F>d|;iWv%p}x@1VMz&tOr zX&uogbdQMHKT7rR1bynB8uY@qdlLM@-(NvY)jjB^%03ub7N0O%6#htstwo{myC_^j zg}I_oxIz?uN`M!4q`2Wu z4Rnd>!JF;Ys>6T&^_TCli%EpOra8QdP19Ud z+<5Lw?G8sZL1DZl&LWPXqT{%4DQ{RBQ=Lgn8i*}X1ry{#+P{oLM!ZBBiPwz8t56Yz zm3SvjyhMrDqkQe6Ns=D*c+;5Ks8IYgUO|e$D@b~$$J4uA0!b0#ftl>@NO`{GaF`=8 z@8UxRt2Y}-Ay04{FE=LnW}r@_?>^tL5wXq3gg zbVB)4su!O^7^Tz)Zbe^sC{y42gzdt6jwdh$Ena@%J;&ogzJIpoAdw9=qL{BjHe7j7 z`GOnK<$(>?*G~rb99M_!IhHrR=diI;nDA(+Ypbu@+Yax}1y&jT$8gwxUuD!&=!=kr zNa>NUKeVqa&)4sIDg`c#eJUr4UQh~U84|*xMD)<@Pi05ZOH1`UH0xV*?dk4v=J)55 z3OAuAJ%_0~Uqwxu};vIsuRZ6 z)PJ)|R0Ng@tSwk3zQq`AWSO8>1cJ*%f}Xy=c)YM&%;mi;;=L@70K*G?#td0YZ_O0W z3eK12JYUNmY;r9Q@3g`zRr72YGM=Wevv`niC@6B+nUxx{lHwk-sduoxbAO5-H|~YC z*zRb-IdumrgdAcTQ>qEC75cjL49J4k@p#+p0It`;DIvF@p@UOA?ly)WriIDIXIx@Z zl}hS84#Xsi8C7CBkj9=5n>+oQ18MQ(3S;sU-}ab1L-chA(wb7o2g*r#l-Ey+LbnJ8 zTSYw5R-`1OkzkM!fu|V-ddFuH90#e-MO_nXCh8v8o$gn>g^h#}Z6VfBXwdc<3lzJ) z(a1=_{$VG4cAoviTndcB3`tEDn+KBz%P-#fDzca@UM$VUR_XuZcYVJHolo!i^+2Q3 z8hAcOFMc)|k4d^c+l1%pgw<~EblIJXq-U6dn8ae;7VkFOy{Z;>Hpz)Jk ze4%H&7AQL@qvK*&5O&nvTHIck14n?($QB%sP}6^ZKyp1DkjP`L3;U9Wn>^sf5jmZZ zkbDdj4@yH1c&lE>*4J+`>DvxjFT4~Tqt0AGiW=6P=S&1oqYSbYt zc#4k?JTKJYDfqIj{uE#T=(8h&x6d1 z^$Xj5I>`6W>?V=k=3#cv4zs((Ke1a6>F_KJGxlwl(8o5{1zmvF@J%CzzCFwjCDotm z_rKf>UsN4?xh9!Du2pJAVQ~>pp_|whx1tPv zI8~|7I_$z@LHUi=~reeIYne02N#~#h_|4dw| zzOqZi13A}%i>U*9_#Oq41IPg4>0weIUzAv_qY+t;f z7?k8UM5)0O`M!cLP4&Kw{Y9XGbl){p{I?CPYqWv$%g1)k>hv2AaKjq#`TmT+Lr2-T zP}TPz2p2monl4n<1(x|SY^v8IbggEk*36lv;+236z}48RT0Pj)6q;+OJb^y>Hb?og z`eJ+G#ZE$gd|{!-!212KJNzBNUyC~c4Le>?mFn&o)E>N*YF4V9_1R7&R+i!*BEWzb z5@fcZn_A2Zlo}GGdOM5YquuZr=vqg*c0mzz6%2DO0uZmkbXD+K*jaNo;OdL1u#QNi zTVZW?KBwt77uQy3#Fs5zA#wqsNO0ZrPkWukf5M# zaHV-XEgCdhVyJDp5B6oC>~f_I0xH?P&r$O zr;p$v3Pn+2K}#G^#fql@`_84>b!4RPnU{ z6?RkQ2LpJN(_i24MndEOVgMuF9DXBp0M#wRO)A~odixV8iE0l-j%tH%kikFLxQ1gH z1Y#Ij*jP*)^Yo@~y3EmJ#1Osjn@Nc{957c8MP+xqR(mtL8?R4}(`(+G#>ea5zd0M< zzvAH3SKJ@BZLEbq?;c#FzQFF~#dHlqxk# zkb3)q7aiVIFZgeMI^*P|ssj<1qxtU%_vRcM~>*J(h&XVv9;-P=Q&VuCZ*;nS#kQk6Ot zH%VzpUi>sS=**i*b(WI=gRq*Wsz4fuOI1E_) z2n9&)y4xY#;u)jQK2~PC5+&ZTNa#D-ar%?TGP~mVtMGl?<1wXXGdN?E*>pLZ2sfrF ziEeA{U4Y=e8nvR8n!AA_O5!nDQ%jZG%@CJ`b^`Y;$_|^K%cij9M zef>Mh&PgC0kEgYTq$#u4BlTRXAAaW||8*dVHAQ2$L49786 zz<7yb_7q&__>hP(bph9cl9w!iQEsO!!qO4p zolZk_lLw1wp%+K7QvX@VVD)sucv1xC^R+c}ojn_A9Og8vjD_!iD||)-9O2KV>)AxC z$I@w351yEVON>%OjYkTnQBJ8LGpffUnRVRnG&`F)l-Z>3Sto9+jIVi(l0+(z`g%!K z@z|gand0_JQcyor?OYTY`k55ZMCf;Xpi)Dn@g`+9UC$=o6sDZXGY_RvMaNU(&D1f% zQemOP?3oYJ@r3YVi{}RYiTBzSkP}o7-r~Zke>G-ObKODoW-zkl%|Zs8vq;Z+zq?~D z2CY(a2rObj^ljH?z2B1GsPBB=XlF;H^tb2}o4CW=oyi_tT3uh)s%rlOA(CEhFA&ufpMl|~T@lL%k_n7OBhT2h* z=~9!2S1(6>*oi5|T=$<)I%d5UM4P92-^GY7a;o0Lc{WBqfY4>4OyiBRc7a+ySR+4JGRSBsm z*Ef9R^wSHMO3g21TO&Ndax8;a1y+XIU&(giirU|i#haZNR8%O|J%zQV?iSvoLQHOE zb5cpRvp`9dRgd5hNfG|3_5Vh)l0BmtfeKLo8s}NeBzFthEaA(gZ4%{XaHn0n?xUB+W1Qk6xj z&zPts(KA%o&@5G$CD=pE(x`e;Efov1B)#4&K~D~+3$ut2VwQkGvz7|8NR~8wMs+u`EkL4VH0gL+&9+J0dCfWc?^{)d5T z@h<_B*ESsDKcg|`28KCoKFVT%;VYJge{JyZV*Ja;KQxT4$8i|`;ZBy7;~!=d@vKW3QJ?mw56CjJvL-9@%xqW&rJsp(hDe^&>-jhb+%3JNoHM3q2YGw)Plzt)7kxi()ZA(huO52fZWeX~6@snV8Hm~)V znJt1!3KRVU%B<`SL3mLRwx)^-1z%R#SVUn9D3{ih8cT`r*v%|Dil_rc$+1X}eGMzZ zX|b7&j+jw_m-9uFqtZ#OVz&i#2S$ln;50Lr$bXPe5-L+D{U#~L57zyOIc3kYmoeDP zY;y!vw&jqxGuf1WU?JAx3R2=v52dTnyp>s64FKmw5udlli|3+&Rl>>fLdDBPV|5u1 z=2+Q*v>xE^mQLw9q-Hi9)|=VKmKha(R2EIXSx5~(T|lWb&yr2Q zl`LO$#L_B~Gg)}(qj>7?z4_GN!{K%-`y+xB{j!isZb+hb{fkBTK7zUZMeKY|230aeVbYugU6wUv|7O+wrV5okTo9cES-L+Nax zie2QpiRRscS}N3HB29!DR<8i@Jcg;n3b()N_Cm=%3VT2TOnbfNVkZRd-AALDwD_)OiXVR=+RC&c6@%~_gfRjs zhzmlf4w{AuXAnk}oy`Th%hRh>SJZL{R8vXFZ(&XW9Ze-MRiJlW5~T@rf0ab(0%Mo}Dc&lqg@IKFtWlvbF-!x=Eme6`|;~fo>5^Y_v(*Vu8k&5NgfV31lxM z)QR*)GxGW1@-9Y(K*`fxVl&y5d^ijXfi1e+pV( z10`2cy}s;OAtk9uq8A0{+k*2__NHIJR|Oozj{VPYKWvfj$I{&}9O>Y@pEsU1o5O6X*wnHbJ1f4RozQX$Gf{ z;|G-NY=uFbCWvzkG)tht2C5TiqJb6)v>al}q0C}|rUSLar%Sr9{)dPWo+_Z*1g#5b z?SbwRwBw+42HFgo-_DvtXg54C-6~k`1#3^BT>_mH4US-s2=pXqeL&kQ(1$<+*yZe? zK#vN}%h}5U6@yj++EIaO4ceOm4KmQXg#2uR0Z(!@%g(HJ8tIp_Pq>N^i`!D5E7<2D zXcYT0gmX0eTF~OKUFnLk^qoL;CWa4M#8vP3<|@`) zaE`>N>5A_kTM6{NAN!}SY%OaoV1hvFSeuZFx3P8t?Z$N1mED1#fhE;i2@CIHT?E<( z+CFv<>#C52Pi7P6gY05~e#j!U$3X95i`kX!1(N1s{q4#Q5=6!KMDvGOPl0gb3EK1Q z5`nH2&A-U{2(&<;msvl7whQMUW&;!z*1wrV^L1u;GCg{9t{DjS&!E z)B(NA#tJkM6M9$n9=j@};wL!DsPCq)*z^$eJ*ySDLvMKGKHL9WF|*a8R0AN$G|XO1~|mbXM*GKQ7y| z2(Us*Clyed=%(~=E~RgZ^m9AqQ-$c?gwT(hP|5A0`Q4e6hd88)D7gvEXR%$)pMxg` zz>Gk8y5PkO2fH`!Fw)pIcv@A)5>pmu^sl?8WgXJ-xs=XGtjjQ)d)Xec46f_%ehaoP z&!u#`hf=@DFB0j%HdOMdDECyog~q0ozk_rrx+aT_LCV?IY?4FDm9ajFry*Y|ItnTd z^w+gOGg)k2>8){D?4`0><65%Y(U)cHZ21?l5QGS5$xODdGMsqLAHcooBUVon&aSc)BRv^aj&lFG z64QTjHxfu_UbUCKbpUXa-G;jQ#oouD__9#l{#!N-JGiE+g z4g;L5kBQR1A}L+zkJdqWH~Ma*AH`5Ui`|XpYlW4=V7-|wfE&%MAYl>Gv{rwT_0~P$ zQHq{M`dbcJTvhQb@*jzOOfHq|!ALQ)pIo=b;m#6r0%3jtVNPbefVSqU{==vyWn@X7M@X(vKszB;ysQs0WZO4f`mlM z{~|`v15GI(TK9P|7W#|Pv|LErE+qR!ei2gt;JSfgq<$)!MDt|v8CdU8et`wj?PMix zL`s!RvUnq`X;u0_l$l*_e+j9~FUhllN<3LCCO#HvhxmsqBrPe^$=34pOv<~FkK_eN zdnMSHf|@%fT7RsoHT9rE59WZHl_MRl_7B({t6u=m&Lh-H;SY?`c6P&7R=& zW4wl2Mj;bgBBPg@9}Ei)n=eDU)H(_2<59K3vJFmh<|-#{uS@nLrN|##*GfdvE)g*u zMg9qq?S?$qo+5@$06I>eS$Oi@+# zoPkmjPvIkjKMb@yYbek}Y&S@XHDM%f!+ZuJnI<+%A|cbnmJ{+bDb&K=7sNx1ghsFg z>|q#aBs7A}6KEkSau&&M4x&3UBiW`P+6c5Wh={gdpw+Bz<-I`f1<}sTXx1@@TJW!C zYdm{1V_07SSF_(*?#*fmb2h8f72)ZNvAjW*E3&OWY=Y@&hwWAE3EqW$%?rVo3{2Ry9kkAhep5rNv}TWi1W#T|#QsbT&|+g{(N& zlU2jkHYZL*e#^F53)lvUM93~+uSmp}x;le)FCCb&g>0>BkUwiFdnE|RX6ek{N)jiP zR%fka#Rl3@wlM2X*3&>ACalZa$i@cIdY~r_^m*pGth?D8L9`yIs8Y(DpR*-v3megz z(CUaYrCYQ7_p-+XT*015eI)B%=Bgqhp}kr6vo{1<$PyD?$=b?(#R)T6xUTt$tewo< zUZSxj9|746bf@ieAg4sEH1-=Hw}B422vr1WKW6Rpvo1m6k69110S3y5{Ud7^yV5|% zY=2}u!loLiGA$zeadxDGY~E%cfWwHl4U}g8(z>6WG*B1N4zSM*)DN@+?0W-!oida^ z!~DM)@Xr)G&MnlAQs|?$VG+-=C9c?@(D zXfLxe1GR6_G5ZzP+CT$Z^vXWWE;7)Ajsfgd_N_!hwO5(FvuGY@NcOAjU}t{-j?8|I zwe3PkPP#|fH5UbF9KUR`SfCqFaa{Hh_O_%kThgTL*V$G6}Y|HIO|ih68o6JPe5 zY_@@Zb<}3R%{CZlal1*`$JpCAe*^DAR_XF(zsoAS$%=#9)n*@O!ws}9^M>pX*mwhN z&0L&)f-N>s$IMCDAF^M9Xl3>(X1SQ8tY%9+w`ZSb&LG;7{Rwj$C@u5B?9W*XAhf@l zrDZ;z{WZH(5alBCJ-aN3Oze9$Hi!;ppJ7vj=%wr*Se-L@ESk<7GgN5Ih;>2 z&_S0iN9A=vnlne^i+fAXgRblx3s3J$XfE4XUYHZfNA#1ljM%E2XntBC|3Y@mR+VGt zNARSGnwVzmo|DK+>ESq3GnJ3YN#XR-5sBGQnwFi;H_&_BKo6%lb29jb%OqN0`!UPK z?;A--`Y?;{kVwoIS^Tje&6|_OZyO~z{j7V@^*P!6NeS7@EjY{JhYhsUH9IGVpD@s! zmGg7*_!$GOb={KV;pWi{e>9b@%qif_f@pJ2F~2N`cIT9HZxHRvY02js$iLS0Voqzm zT0%s(oweZy1v<&wCU<6S`SSvO%zHWPT08!Vfquw(Gp9WtGKQM}n7h(A>%xZ#bdonI z>;*JhAnA*X_=F&`vy1rj0P;8cHm57!V^pLDFXoR4Bpd9`_Zo;A?7^SCQri1lQ;y@x zqXNlb>%)%=v^wI2gr9Tz@Q)1iX2KspUrEl$7|aQM`5zMT(;mN)+m~C%lF&ndk-7bN zau7Ll`}4dY%E=wTn;U41y&(5ezBq^~atHF$LDV&O5Wi@glsU#eBzG|1ERgiY5WY>I z70DMj8|BX(!Ve0#nq8SV7HGqGswjxLL-}rjR%0J{4N%(&L|e@)t|__0_*4VMy8O8# zdF4b&yDM=;?pS^#i0;mv$j8%@Nz}U|{)yble7}L#wtG3(KaD%F0gy5KUhZt}4x&@J zwY;}P?5V7;a{YX$fnEoi%da%hCqVQ0bq0DKXaS#Tpl^T{@f!^EF3?SUxq%KO{gQi& zpFegjH7`SGDSu9&xoo=2>|V;hHBfR@qI)@Se;skIX31GOZkR z5cPN8!*l)BlC`O4sQX^N&_Jr^3io~dIRjOCu5#bcwW&rEo@?C?@S-4^>fX-NuP53H z_G#*CmIwJjfmY*7jceTx@!1A?J8_2cF#p3qI}>W$yZJ@a#QL|IUE}KFeuN*!H#KC% z4dwIQkMh$7TAtX&{W$MFUDEDI*`z+fC(V%Pg|@5Qd--Mq&278h{WQ;=DQWB4KHz?a z-)NwRoqOER@!Nu^kLwVBXQo*HRiyZ#cl(erz#?M!;o{R(#lQFqrXoG2LDG2`?00v;51kXjxp>aiu|1XO8wKK5=49seSN1NJs9V9x>|x5NC4}V6He4Am(4ojp znFDafeT{+0`NNg#479^dXo^Js$fE*Im5`H#!-Kvcn??i?*)&>t)ZiqSUZp%`AadzM z>HGh_e*qryXWlp1=S{Z?+7CD_Tx&zLz1(N zzw|f9GfJ8Prd?)9KD6bWxDCj^bQsGNPM zd@4CblmP9ddNNABR4#g0q8(-aNsjN74FbwA`AK=jAd=pHD&2QUR?^$7J|mFq?O64t zAlecatDcaY5!Blb^^{SMy3nEiW+3W9r<$-^%0y@TRx}Lwg01 z4PK(gJt8Yocnwz51(IWAn40h?(WDPYsq8UAQs&iav_MkI)oL$+4l$B4#jg%Eh!lEL z)gKH*de^8V1icn>jeRJe7eJ;`97NQkgRW4tvb-{Ho_fRMq8=Mo)yp}?E7-& zt8YFbX;afW=PgqGrv#Lm-mHFM5UJwLYQ&S0l`7t%9yJhET&A87Xa$?ss)GcQ^){=Q8#Jo7S)FYlYH*9XY=3>d`_()5`w7a5+te)vkt%Le4;qLn zZdY$SK%9pdS-(?VE6@t|UE<)p-Rf@wNhy0&QDnwy+dl&q566+sigv~ zU`a8TVy z`42L8;vBI4A)pj`TGd{rCgjfVw0ij~gk%$^)v*SmCQhq&2_&01t!^<8HE~+qW}qMJ z3&DB9KvUE7yie5M1zPAAv-Ri3*gj}-CoWC+T-6R!gXEdJ^1e`CFwoSrt$E+59}6UB z%kR~11(K?LuQq>GDoUz-uU;pRRPB59dIOPa->b6)I>`L)#7DroRzPX}8Fhm|t0R2& z(b^gHURg1cCZRKiqKCvJRF6pL8TCQQ$!QWgqwWz%DtboUCy?~$8TA!`ptc{qc1C^g z^?JR}sLuaL>!r{P15qEIQF9F>n=?FM`_O+h^&v& zUNsO|AEmu*pdakN0eLuRUoX zY9d}cXrLeLaryCD8m|*FfZ_G;P6$0nW~Onc7N$R?3vH@x5lGf6(zY8kQl?0I#XwZA zSX=c`eZ4Ymvp_4@@6pZkn`?gUV`J!YVG)-L%SG~1`dDP?xloB~Oi9ku3u1Cr2A+MfoZIk2l1{TWqU z!R*o9^Sf)443r+-JHLnaj6hOoPwgd1qXUK9p4wYswBrKJ$167j@_TBZhtPUyKYS+E zzxk}UWk7x}?b^@lHN8Z;UZDBx4Q*KdCE5~!5lwS+7$xH zt{9_TBWYqC8KYGTG@rd}dMtm8R%4*<>QnhwY6}eXmw7<`SZ#|y{`vTy_Vu!j)3zJL zwWeOStF*@qG|Sq{cD44Bfwn~TvW?f?G|(+EFXvCt-Z#*bF>mEh)IKs$G=l9K?Tmqj znFi!f(qhk$5%cjy)qwnKwN#1xY<={H`PXSURTjjLqrcAgY6S-RHs-_p$y&LAirClr zQ?x39q)UC;MF#CQZ7BC?yMK^UUa|mExJM%`(v0sC>^1ZHa+~#-w;=YFiDoJ|^EYOM6Nn z*?f(5$e>*ZU({&F0yIDF)n;oa4dM%0g=e;A`-zlU7(vfOYBi^U=$S~ZmT91iK%1lG z8>lyEbF@;4L?rmN${O*etc*oX?23R2v&YT&7J5p)J#PhtQU5dqZf;HSJF!G~({0t34~U zXajA`KE+pP2@=7hU*)dQ3Ivh{>YBf0kjR;?wHIhU+sNBS=~~Ya)|J}e5YCm_NQ1Kn zpX^zwU1gy8N-x_gZIOY#23oD%XP}8{FWVaJMFZUev{w7vZ@?@|FWWjT>TJDhZqwog zn$NaswVvCwvJl$sT5E%Lnbym8yEZyVGqF3g*#e#9F?c9(r}m41PE`#6nu?ch>EX?H z__X~_Z8^RFOt2=3;5I=#8R=`iUb$1-EfK%E715qFXb-o(5{TaUrGR>@-nbq3mMo)NcMn4)-{=Xtx?D*1Xtpk9M1Zegy4a zZ4(gMpU*~uc%SxwAWEV4YmW#dh2F0{WmGKWSH<10y=b6MOjpG{pdB^PZayP!tM;CO z%Ax2s?X*Br%69EbgEm83>e;TP;4K?k|HMdtP|G3+WhQ|5pw>qqqM6u3+IWFvkM7W> z8i;yyhxWKcqSJP02Mii@+79gngGQaULpx&7sMB_87LD4&Z?IFZJ*>qFC>z|RH5EuU zxLa#(AZl>8wpk+4;BIZ3L8AtDYr6~@HMm>bYtX2{J=z%qQG1VQzY8Sp|4i&rEfFt6 zQ3z4YKBl=0L=8TsEtg0%_?WiVpizU5Y3mIdHTalzpFyJrAJ;xG5H%F|jWgLANQzw>GBk^qg?zx~=kgV@n>zjMDfCO`!3fHu*fMVJnG4rmN7 zYHdx$C!H)Qm-0hg&{X{4xoJY<<*J>^hc&a?RrY>!s?|I(Pz&RqM1+z9;{Lw;w@mu> zkA!N5&=r+ZPCztgvSbg`B~EHMz(cC|lTg>s_GH`bCiYQ*9p8K{z#BaEOjK=8Dm6f? zCc%k!Ji%!(IAhA}b_H*^Qmx3M`aG53*ZrIdB-~R%R9R904~6Ibp$;TX$rJS!)R)j} zhE$mpQrMn$G9R8QcwLc7NP;3%R9Sx0h9xFAUkjA`Ls&0G1CkKv0x5_@{F@T8HNfA& zO{#>Juoh$hl_SVo_!W@rT@Z10qYN-t|~qal@1S-Qx}JL z#UFH3m|KKb6!fUdZb+aA_$^072~snQ$)yNLYh|}b;5}ixJ(B%ux8qYZ5*<+Y0HR7^ z%Tj`Y#x8e}go`Uk0>zv~M3O(k2qhIF6n4}}6^N_}vMG!z#SxEoPS^$-mhnN&s9_47 zryTK*UazlAy_yYk;`TVBD{}X(m zvf1VJCH~N&1KHFVc|v8=;ks}u$)>=-lL|FV17^tbK=6{ZpM(L8@YDJ)ry$Z%>P4w^ zmp_T}l*&Ju4}=nx%V6Yed;EoYwuwPYY6T<=aiM+A-tkaMbCYzMf)dhNHZSLZ&=P4G zl~eQNr%)9omC6GWMz$r%Tg8O^a9)_A^bvo&=wmq|L-P|vcgPn-sS;-uC?Q2@(B=ts zX#!S^9u2jO{1@oydUXSKOX_CC0p@>6F2FFOej;lqK;&P5O5qbq4U}6&oS$2Z78sg< zNnOeZ_@&Qe0E+PR8w&&dFRTMi%AqJ%s?a=jfnqE+Qt1_yq!w`o>ITZGYsgBuI0dNG z(F%(ezLE0%^9mXk&BFo{Xy#H8fLEuH1&7g1GF8||7@R7ja*chKkN@8*ixmp__{c(IhoPhMijro#?+nwQ$Q8iTw4nb5$98Dkr+Jn z)PR+NCaF7U@eS|3bGu*}zF1n6whw~ZB=M} zQ^1*rHP9sPkitVxTD%{e=kKOPDVYEN&eh@6fUEy5pw2I?(RiZyUqo`#O&|F_v9Bs~3L_p_m*Db6+ac$0>4 zK#||rL?ropoD=!ygr{qTF13#>3I43upEah1L`;KUwr=b${-5Qvb5dV!Y z3`R%sPoU%gHqWj;5l-=C{#Xl1#b`X}lVXJB;v4Zq)rLp%leXBC3{ehB}t00Ut=^`rC-}+%!|TU7ilL5f7STg%6$< zr$gsSZA|v@0MV~w)|9p5Nta1pI?|R0+nhZrI!&g32-D6@&&N-X6XZh$bh~JPhg}L~ z$AmhdVLsr1eQWM+$A`J6;^+0IvMC(DlZ9(*T-)(!D4T||X(*e9vT18#+VdoTB%`k} z97s(pMdY)PT3H!VJ8Lb{P9p6g(*7bHCeqO&ogmUFBAtmek`>pnwKbNq}qH5rK%3Wj^Z(MI!kHhWha%CW;5{%dGtF@+t_LRbkj{jmEG)>RwaBl zekr4p?`3`hdj;5yU$(&4oFctR{I130>Y5j^W_yN{3`62!ucy{xnP&ARfXz(v}J;A?YDbANcy$PQlMYASVr}&qwlfT(_ z{G|GMtDpG~f;Aetw^05NHfiw6cPOuN?B;(kvh`PkXI<_n<#ClhEF=}LQGR9Hl6=Z{ z?CQ2Nl5 znwO;=LV9V{5u|TtA6F7M=@_Y!pmeCRn^R+X>f`FuEsND$PEIc3QE$R0i0g%-)6^{f7Oc_GeaqydyE<|7+}gz@sYey+3o#*<|xSyV=bjlCT@HQ6eEGArS%* z60;}~P@`a>q9!C!srUy$5d?SFgen%4T)|Z@)TCLoT(#B;H%9Tk*(O-it5sB5@PZ~K zSgBym6867Y8WZ_hOoDAC58#z0fsi+rmn>lRL>sY&V4k zIdRWZ?gXCM}m>H1O89v~~84kUpZ0}KAomZx}Z;m7t8II^*!9d*jb=Y@O3zUhqWxIW5Z*ozt?|(mBoLbHh#4n|@)cP9J?xX5A86SZEhDB#&tb7=M5KSqvAJm`Edc-!Q z4oxkyAHhN!Gr%ADX4{)B8SUcCwTIBG`S2^Umm*$<_A0&8h4G)vn@qBCX4=d4WMih| zW&4o+0Y+?8N3x$bT*w{6VBC}~{c+y;PvbIMc#$I^5B{Wt6=0Fwhg%J<1>AgYz*2<6 z2^l)i_+C;L_;u1YM7AeyN90V$6QJz4l6yRe@o^}SP{U$=LoOJnYNLt2SE;D5Ax^!=5lF&m= zN$N8r-s#}pf|{gOW0`wl(wNnob#2mFRM(tzR{gp7mLzSP=(r6Gq#xJa`dQqPR~l!h z-=Nm;h!bqmmwm>YGapJ?gOTx(u_3^W4TcPgj+mC!sx}z%x?u-{w$R^V$XZt0v18mV zv^C?ulC~HUxrLEY_-M2qne=&5ANuJ`(wIs-Urw3Vsi&q~trE;P(@K+f z@{~o4fSj`-!^90?$Ov3CKlyo{p}mG&atCozuwo^rQ153W_8aZ^)Y{LO{l;B0A59)a z%nfkfhTd=dhPn66{1LSD;(jCG{Yi3xb*7*%`7P8irav&M9KTNPGi4sQ!**Vv=nu&b zTd(hI@=@c98q7wQ!log@Iu-Jw_HHvyZM4kJ6^HcHR2g;JQ8a zOzb%B&hA99pOx)PJr{GbD&9%8@Ci1ieM;uVM)h-^q_iQjz*>oWepEe(JAAqMTi2|# zs9HbEpH_`)L3ptGMflHoE{7h!xp`ezr3J>!W$y2!6omS2dkEq&w@r$V1nzaOtkPB%9H{;ik`=qBkZJjyY^P98~crrhE1$dGG+LvI*dyz8U3+W*C?3_TwrAvHd-}GW;fSxQnh?Uy@Ikb|F=fT zyx$s5#CDmoikiy|$dYOcI;24@LRK_yvZ z$|8+5rYzD}$)l{~0n~6>dJSXzri>du^#U-k+gWVN04z4+vVP`l<~Uzq%6#Qw^Ut%Q z&P6=pVq7qvAhOKy1!CkwMf9KGtcJe`743E|W2sK#y`0jFr6`4UgN>UL@qV)*A6G9~ zlXELGR+&YfQ;EyC`%dE<_pAV{g&vz)VS95zea4VEIkzFB*b0|ko6%`pn!G$?9V)sB zoSb_rc>A>FjK3)(WDb-)2u{v@1Uy={9Pz%wf6Lem=ckPMKja%3d$8koGL9H{*fK-5 zce8&7?VSEOa+5uHU1yc~hlyw@ik-_?htM?FN$5P+D(I=MR#T?ft-wd;LRTwWy;6xY zqJA;)W>-{wHTh20%5mS6nSn>(>~O82vxa34;kJazFR+*FKsot~|{Rw()fg8FhD;*zBk9fa<^VvmMB9#?IagCI? zH=wsLT>+QwUcJG5V%AKz-2ZFctIX$TNj)pU?TioM@RquFs(>8L&R7+L6IZ$;)Db0< z3FXv0=I%!5PWK+3h&|H1&~hR+!+!%+FZ8_c?u0%{ebW6T^nbg%_4CucnVa>6O6RC4O9YOx*ePx?O%=pVp|W*sHh7&$Xuwx5I6>Kq#P zn=H!`CmBx$WBB_;Vrg1-x6ZD~%XV2KQ!2CbEa{Ir1N{-m%(K#F+@Ln&qFRigktxfv zs}0GnHl(BKj9;RM>Wn;UnQG;;WC2Et&oDlVkCC`@?8-i8%EStvB`bIOEP0;rS+d@z zWyynv?0#Q%C3j!L-TSPY;(wPN@LBRy>bKu#)l{_uUyN2qHj;rhB&k??`@Ni0)uE1o52;UboN6=uE$Yi0JHBB?Vz#No z2~N!R6TMw|pm$I|p)SCe0qj&&V3(=|BdQU(yP5lAb;ATZmaUgk&#M*iU!eY}`WI*{ zFDDs$)xVR-~Ae<3Q7JgV}+ zVO0W-Vh5?}Q?&{DlzKSVP&WN&F8-22rt3~N4{R*mEprz_X&a$1x1lyC z{%u5ZH&Tb7@zSyJXf9r_-Df<7V|vAS2ibkp`;GUYUq=v5f$iHZ)QBAf{0b2RG^OC2_Bc{m-0GY>E7F?ZPT$=GE(QLQG(o0jq1GA zfI4Mdij33D_`DDV9TGZZ3|m+LGg| zu@t@>;$k?dsuns``N?_Y0?<$k8F>}7Q#G>e^>Cc3nYHa8cagivmjX-Tk-A28NyqnG@>doX^f zGXC$TzmD8UZUybClX?gBlZbSxUDW&N_ffx2KT3Us{xRwi`X{Krf(|&>!6apSqZODzsBACRc!HsGSW(E*eUI9l4S0BzJ?jrl?;hqjZi?9}^u=Biz9WW_-g8 zogl?*3DTehXs2?MIrQ_X3&w3DOR29Xo5|bgZ)D9O>Odzqc5tJcdN=h; z)cc^F>Iiv^{s{F6@+A2UeVr(gHZlRUt9JzbULYbP(R7YUDPkp-$&g?|8?p^^p8*f`;l$ zmgC-m0(SKz{as`?xsMzI?dlly2zi3eH`F>sa&0Lb!qf$zQx#Dc)1N9%K-D2J)Z7%= zWdkyBi(qIYxfsL+M!k;QNT-u}2l*uV0*KEv*0YcG^wEEvjDmJGLVbcfNhk0PH*~7B z&IT4>hTrq0Md~i~xTjPlsi$&FRff8vG|*pC%iMbA&ZU11{U-V?sCiq! z)544v2DMSICW8!Wr@x8LW1zpJM~ZoR==3l*0=><%o6a6A#QRk49{s+WSFuMAsB0@; zX6^tFV}LOS$RS1sh7fvx#SlWbRE%gjE62Hm;|w}Mo@D4rMDDFPiO9hU(_pU{>=k1` zohh?XCm6E(1mk@yOPv7!{An5R{Uu&Qdae+qws{H}S%}=_<-Pzmwt0$Z6&vf-zS3e= zQH&}!PAf*x6Vs-#idt4tXGjz4Sy4Sp%_SQc)4;L~D4Sl{fU=&-CYEYusTP)M!CoFM zYh$SuMnKxT0tGrNSD?Vtl|dE=u|PWuv@^7w{wC_p+|6c`I#anBrL3wh=617G4@>nh zG6Le%Q19X4?P1wHDBE1M#~28xd#d_bILhoOs~BJv11xrc95Vh34SgTlUoy*jGRk0F|vt~_;(ZRvW4>*y3((l>?leIN!hbuDx2k-N=PPiHRKM8Acl zTIjR|m;{;8&I0XpHj!QQd+6_`)6c^F^atnhh98Miw%%n32VZTyjw{B9~t@ zjghs8e6XaRk@bx9!{6pUA9Xhbh)6R_!7VBbmPmI!(e(Hf3#TjJeFrDKuim&2% z05OVV#k0fd6vm6=qf<+#UKsGyGoy)CON=tm+Ns-_+reU8@p3iy(C?uir9VJtfX*;9 zKGNtMr!&eJWfx!Dc|U;0_^?ZZ0+yW}%j`mC6f&cZem$LfIxW;K)Ir9#(`l#EL)}B& z&)fkz19Xm4AEzE=%_@PzGk^qbNszV_CP?e*Nq>SgtA)Cq>>&rpLFOK()`=2nf{%L& zs0*om)b)u{XEQX;J#~n>ow_5D^QYbVdF?Eg`I)inRz6?GwP&bnyvIF#&bY(mckSb_J7&@FG zl`5Cea!F7D__)VM-9*2cPAi=toe-T7Qn{sUfm=$|LT~f<-O`9=Izi@!sJp?y;~teM z1)4JD_=034Q+7NEqHvZ_WlKf1F-q5kvSkMy*-~{kb%Z)fJwoaniSg#JLDaR>b=1wm zfG5O_jvQ7*9ifg=kK{>JI>`7D z`YK;a>3pf#3LwGR;)aj9j=Gr)ksb8AsUy_=`CqEmsZlzE;5N?)^(eKTC{=qWvfHU^ zsq3hlsGF%n6FL67xe)bzQ-0xc`^caif`REs${#ERY?9=ya1&a+v-oG_H|J5@Ssg?WL|I zgJcK&!AY;-8!`qbtyfbktU`(O70L%vtxw7}fw=#-`lJKZfj^ zjzHtiPd!Yk^CZSg`p%PFKN*Cx&C^9j=!}BfJl15PcQSGV9{=R^YF0&)YzEYu|93`y_#ZeVfwsx8@&@oMR z79ndZS!pG!sN``}35Tn`RL@TxrM9NiC%rSiROhA+&Jagcf2po6?Wh(#NREOy)|tXV za+I`c*euctT+9u!l~l7Iz%cR67QdFdl{z^42e=1xQ1?^U&S7)sNOK40$c{&;TWdx4 z*GfI3)U9>m1nb1hA99;H_G ztcUc~^I)i($bM4!asS6nz%NJO^-FqPwsQbxb(tD|lHs7VV|D%(+(L}bw+UDsX`{Cfz;W9Rh^j{{qLFx|b2su0+ zGfyI0=P_m;PZo7QwYNcXo5+p^9+K#QXPDL~E$ed0@Ln#VK4=^bbt`r7ayh0h(mP*{ zpmn~)2cdCZ=SwekQTLO>q*@@k)&kZ{?OPy`P1J!dCJ|=$FOXIblfH#gplP8vL9&aC z(C?=nq#maBT_N$cSIEwq$RODP9~Urngu0*Fd!~XoS~5sR zu9BVgQx9JyO;n9St5JL(Sxd)H-9#NEyU2cWxG^BbMj528W))Yn3J@cMP7~QmCrI5z z_LIY;x<<SxLAL|D8 z(+#2{)Po@IcQ* zNZ;*@Cw+H__LHq38br3<$(mOQBX^Oj$-o*WcMAvaA=i@k3a#}*|9!&Njdbp(Car*I zUyx3Se1L2hMmoq%!qM*weGgG@md6n5VWIC~R^i76&Jc)QZXvgkoeUydJLOR#@|ZY- z#A^l`WSe}R?;nz};406Nw1g!AZJM?3L8`9RLJ(Jd(_C{J} zdS3d~=}XcdO#d+bIma=F!&&MKINP1iIe+0)8EF~$8O0fMGOo*5pApR1mC=*&pBYCp zzRIw7&W zL$J{G4}DeI$8dsV{p3;TqKiHS|5WxlxU2FkcuVPD!R}NO(`7vAc5o4?CrJF!RH-38 zEgAl1mpJ<}!GPK-iMp*S6B$2qNU`zilH3K*4myV_&Vycgp5#8`5svG!kadKI$&HwD;UwLVL#{%uw@O1(Jg5-_d$QgD9t{Qs3UFdfKL z2L9Oa8z0_ump&NC($(0=*0aDIeK9ye&jDxYIL()dKmgfwFUZi^($)*_^wq4{@SVs4_kA=53S3ku@K) zoziV{K&Pz^blK{`Oxs*A+jbc^!PWrgEwatWi1s4%9yJM}_b4Ai?@{L=^d2=Cq4%f@ z5PFX)LF8IhhRC(50+DM~B_h|Vix9b1%|Ils3Pi3|HR1TX^+4c`^km0{j>jAk$K}q4 zoj-KG;{3JKo#A(V-}OURg?mZnt(j{wH)U?k{AuQYW}eElXBA~#m9-@6zO0U{SF_&9 zI+111o{~K~dq?&Q*>7imnEh3DMovRcW6si?P|h}t3Rh4)7um#di~?4KW8Jxs5$r|=2{&5 zB{-~lb+5Wgty7I^J$~=Q?*{yCRL!j0Qit)HyUaiT1VdY?IQ%bB*^Y~CPgM<=ji)f3 zY&^XpmR=Q0ua;Eim@bML_r}rrDL)5Pq8$`YUus!UY&q<+Vo+aVMeV! zBQgE7s&*`e);emPR=XUlp&!x@>UK4ZZx~zV`a=J^YfQawOVF9AU+6|7$@r}+$vEYj zj>%Tjty8Xf`abt`>ppjqaad2c4(rv{jLg;6yv)<8C9_?znMqSiYP+>XC5IdcCCAai3md^*$QV zS?=B!x9dZ8`(h8L$d0p?uT^g^EHq_s-vUf|j-f5;@)qL0Lwz(_YZfm=J&7wPc z3wG-#(!&S#>FhX^xw?66@8Ny=WqkYAEBgC6x;K36kgg1;eXX;@P48*1yAH$qE)3dr zxI(<@TBw!k?(m#5M>;Q@_K|jm|MHQZqA!$PTo@OB zK0cVe;oFbuOL9|gQY%y&4z3l~vP!xW2YrXSQ*=)F-FI|$@5dkO)`D=OHYUeSy#kS| z!~gMDo!k42HhyGxsNX;P^1jyd->dDMHTl8L^s%3Zvy+VddSCcZk}=t_PiLrjw=?*) zcGlY6&d|PaYO+z8djxiZQZPitdkrH4uS_5?bR>Lzvf^ZR%vHwE?V`#-Qf!|jV2um{~*)I)tkc4W*WQos_?=rBQIl>c7#?i z=x<_G?}jWRUhA#lj%?$HdPi?zjuEG;i{ss?&ULAdX1jBpquII68G6$02v)kiM z=RAZoOV_2QJDS5f*T}kLcfK>&nV*NCI7jGpK~!)^Fl5E$W0zhWGs4y-CElSNq0X^R$?4Z=T^fR97(l9hWdNg z74@18Gdkx0(?y_80{h58*sY z=ZA(^+Yo}{z7>@IPaAgRCVne7{Gi8}ozRyWAD4kK@OJoD9wSfpg+KEcw_MaWQ90Wt z#iuw|Ihs>b(RVxa_>lV6pjhV&ZnAkb0_5_NfNd@8pIq^Ek_MaC|h9_@Yk0^@WXdaS$LxGJX)*Hv4x*UV5jss>zpxWY};*cVQ$ zFgz3boVCuj6t9Us`&)ieEWf_On3U7!oX6tM1%Jzq74N;j!uX!<{lhe)*sQKBEi1)p zNz=3{ttiFDl#1!C)2hm<%3G^vRF}0@lvUldd|LU8imKMC>1|cj)2CN0U%tGorM#`T zak}wpV#Rxbxo4DKc$I;V_*M9=xxr91wX!w-qmjsxw^(~ry@0$IYQwWHF(#!q-}%N* z{`kTDm+w9B$)y{L-k1}<^Ae-ABIf@7Ow5(q2SaN%UVXz0iHXZUi#vxJ{`Ql3Beb`( zAk(nl7@k;feDPQrz8*5wZ_Lt7;miHT>@*9HgYpwgd&7QwTY#++pL}J#)eXjV`a9HW zimFi4@j)>St3#LJw;Z|}f6uGNRx9$vyGboaemUF};}0mTZpAo$MyrWXKI65bpcFPeG9z1wqQ8V*t&xbZIGswlW^2X~Douf`F-w@d8Z=h8hI^ zBVCL$G&N4c_TjGs{F8`@gqtb(y+0*%Wu2B%gWWx(HWHn@d%3co(r=AkYMuCRC{asV zfc~!XL7<*yA|yLYQm?7s8BeNR==a8VYCQdqVKNuQ7f3{Pw*%Jf2%8dIIt18DlOS>D z7~Rbkbei#=xf7kXJ27all6w)vh6M+?fyq)f9GKMLYw9IzYbcax;H~UP#ir!8QxXqCDswnz4W{PoBwkWPsj2Q7RPHH9V^!=gW+l2PB@k|r9H z?MN)MZyUxMDmui?PFFH}=Gi1X_Gt zBBTxCx@=LoNi@P}o}88viNwvUdKp%a!Rlr$mUE^W6~Gx~tW8cc z)+C1*?X)>g)Tsb2u3ruckgk`Ry^^NVB@w5Tt zlXriexrN#qi5ib#4Pa|*tf}1?!r;@|@epXg)J~LwdqYfFY>K<@?)V(H1TD2FaO~ci z7e#3wyPs|lfx+i)mTr9B z)TU2^aM7Bg=ek0YJ6%1oAh2YtdSYQ<$=&LS$$=#u)Dw#WOX{m976+E3t0$HOmPDv0 zmVzEdE+eoUIF|*M0H>R2{6jMvU2a@&RuuW*4IJZ)_RTvP?V2~*{dV&LN>>?IS|sX& z!C!7liA8l2NK5`z15dAkPu0MW)WDzDz>n6zUj<^l$M|=fkyO+0m6Qguc+w-)u(W+f8 z?P82?caHWndbMvrdl`?mp97H@(V-r-U`XY}+N_%=8$CL1^n<&vcKl6&@cyM+b4ZK2 z*M)Rc_dMFySlPWnXm1!Zvb!Git-Fu$Y4=&Q|L!3@KBdrR<6e1?9_*ED%2PC4??!ZlaiDiwy3vsPWYU2~cAxf;FX@wGWc3Nzy|vFGN{1Sa`n^hr8GrWs zH&p-Vu1X!}R=>eAKswBRVelB}4lyHl=}j0D?U zQrQ2|TsGOC&<1p(adBvx@zc-{BYBtxjkO>4ETlgVdkWIWhtGnU#yMg&RgC>3oO-DP zw3fBjFdc^l=XSN4a|*2HIClw7pBYxOXLcMJAGg2;YKg@vES`+t;EMG`;#GSHl(ecj zkycgpG=&mv9!^nRAb}aw?7k1k4V{z_vl|A4CDLRwMX?Di=N@ROuGMUmjSPu63Kav} zFjkLDkOm|e&ifMT+f2dk<|uU%N>FWTuBa=t)IiJE6K*B}0@rb0vTn|<7i=@TJA#bJ zAlViKI#^9H{jI9i>~0C`Wn{1|I48uW+K}sEXvAiY=^tsfnR&Z)U$d!Mk~zZSw!naC z7z5VGW_Mex7-9>_E#`*!DYHK@K$*-KkMNl}hON)sYv3MF`y?{n4KslyGSn7oSnrS1 zyF*EUD?Wn)aJR4OqhLi;RE)nkSws<0j}?1 z{BVD=g@)tC?rW(fw6MAF@01VEj3n+Y8UZS3K^f^C)g8aTpV>@ z#2yD3FKO6>`yNO<1x|xjM%mLasOmdw(2S*{+tF8zQ=@bBX&^s~BowHmAM6i16Jjfw zB$nnMAgWcXTJ|Hmn#+D-*PybCcrC?J?L+9n5S4?N{tT8~Vq|1;Gwwi)iI9IY2{eG}FJHqU_0wk-+ zlb8lWXcv@eVK5-IIgnFJQTLq2{903tZuxsTZZ;HElLqRmWtTxxOi->Il^qcT`<92n zStVuQL=2(qu-wsBC8}qD4e{LkR93>8`JiDXA>>xgx8=RRw<)}9#7tw4gc(2w_e2I|$Jv71<(LiT^Mng$2tHW^ zxnT;mU$N2d${b#5KuVerV6(d3Www*cp8B;^j(O1rl0S0 zK9+>itAXdHO6N4?-n5~lS8gha3lci}1aX~XAcSFvlE~1S@v5t0y!7CzCiw>}4jq`t zdA!!mL=bNab&rGC+2Bg>yc~uliBNJ9OSIvT@<{L-T#dNmaBs@iV>S?QoC&#loTr9h zCdL=_XNb*|MM%qf{MLL4jDZX=|OHB2D zk#OhT0O&Bq*yIy%?`O<%*Nbfry_gk`D@-=D1+%k>d=F9C)p*ZcSI0=V7nl{z1LkGH z>^d8TDt>#Mu0Y69r>{Y}Qe%^r6Eb}ccK3sz5D>w2e!Dy%!a%W9Vkr;bK0FQeiBvINTWcM1cxM2rx(cnY`?wkd!soCAlxO+koU1BVr zkVMms?Gv(KXLxEtx1yVSE(({wU{VCjUw7yQNme3A1T|SrY;&{J80g8ThVkk|dnk{X zAe=h)8rwZ{rJpt9frnD)y+;3%B~^CXeT{Py=YpkU9&&&d^B+o*XtvPe=tFhk_VMaN z0b1zUT(~(vi+q1YAHOLJwyzr7CZ!el*>t0{*IDIwvz7`|8VAS1giyrO{wYCjHPDJS7e8q-RbkDzxR2@v1V-0X^l?nn4WahGs;_ z*P=(lSPagN&>n-R3yqMpH!!SM1E?Hnd_ToSXBoDs14NiGT{UCU)DdZZD&3dIl;=^P z!6FB2Z0Wr^wPVeSg~qJXIu5_eFY(G&MeHJ>=H)zN-c*P2YiXero^K49l45k4W=VP+ zRO$(P%Q{xGSTHkTm)R#Dtn#cdCQQroSLq2ThRN?{DkT}8PfPGWWxJ4Xn9J^>PZ)#C za_MMeL0JqPV{9&K40j3V%G#y+>6%)=q?%Zu`3xOrbex{z&o=26Y>!TF=da`YH)Z>> zz<5k|#Ny-*_fPGs2NLuB!^V4hv;;ig=ni9;9_8Qc&E-92wA2%fpF9@hfF9)E8*iZ~ zie#q|RGyLKXXj6Kgq?U?ihXG_)|F>~5Vf|@t7{FtQtIBMM%{!bq0bt^1WRB7GMm^f zC7XhR+-r;<%2VMOA-tln7Tz~l)rJRQRj{faEXy_YibVPbQjOykAyWH7!&Z?4g{onl z5##!-5JR~K+?C-fsojqW+i$LAl5!G3OpeiM#vr=RcxHyPsIndzZwqF<=6N3Seycxk zJ7=<0wZZn)pU=2HBh7l$ujJEuM#0Q@E6!zJVNle6W{)JL2xPJwT&9JP8O3Dg8L!W5 zP1hU2k2s6&13p{G?ez%YV|pz1t10xr*~Tjjie7r8NAi}x(c_aMBV*Qmw70QfR(51; zeb{vQ4Gzz}weil%_6heX=QxG(VF$98G~m#VPUA4oty~=Q5L+# ziOTLaeIv`qWK7=#nQQ81NK3Y0S@}jIe@;@*ry!3A`L(t!8VG z`x&et1a4({pTK7UmTbeaa-?Wo47wn;?sJ&EL8=MP?PYtm*`{dQfdNhmOdOlE?Z)&u z$^L`l=+b%i`0v0<*o47=OR%Z1EAf2LfQ-+!d#*MxKAEEhr4=*O*J!aY+DMukFqa&y zZ_s(s@iT$tP9^M{+qk9%bes`0FU5b3;&P#Uz&y>LyT7qu-o5?>j~KtsTjyVZ7SUG> zIzQ4_yI74ABcz2@^==l~ydvZyUwfl+R-7?(eq`j24Qs>zi;}pQ{*7)Z$1!DmH>*Je z2I`G*^M1`5JZ_d7-6{j)W(Rg@uCJS)2PooZ$NwyD;Plg9;4T!%;|3}JtGMy+pT$Mk z?{U9_o!)*oW90b7O6}n4F#2dgzJDLRZ`c;vqb3z{M^t26lW|72%i^>yB7`j=lccglWF7;anO?Hy9O562ss;8Xt(@0gu+z*t(>EzF;g~YBw$|$*290 zFAL{^(4br?3Z7;y0=BU15y9@okbw!q{bE4%K}+k9m-AKrLgBZeVJ` zy{|RM8m#@xc*IDL=L?<(&V!-PVoZFW4x0uzxq8iL)cj?r%0h8JWUi@jN^U7}V_7*; zw3aYoO@9S3hAZcJwVLM@qw%t&(FX(Rhmbx2%gT|WwL%%M1@auO=0OVM$%=%dELe+4 zf866KCK;crsTSDNvmJeu*3CGyERqf|dM;0>wG?XQx?rP}Vl}ItiN^J1x%8xwyF884 z!vpC#&6X~A8c!|{k(M?y@|MTm12=SV{%^_ZCuy(4Rg%l9@S>Exfu$CE*41Z~7)dKq;iS363N7R)QtesL*xN?Qik7)IH_U3* zx`B%nwP#Eg$_XYAC4`%K3>$;JU`{qSGh}5`VPGI7r^cLpeXG_06B1QhFsc5 z07B11WpG&XF*1I_$_TKYqQ+Vfd$F1E_NtzF@Cd+GOW|90IO!)Pv)P#@6g>cAAtZuo zZ^E_g8f*O;w%%;cEn7z}zt$1n(7Lhqu^4RprN_F4e+o*mKCqBNu=idgdUc(Ie`8_Z z3492^o~rOO*K(i_0)O;@`R3@VeB-OtF?5h|b#+I;?H})yyxhyq4*VguO{6w#5eI{L zvi6y=G-6q^FH* zZ=@T8)&$$XhFXQP^nRAd)(Q^pE0;ktU~9$Js060yO?~m8v29JPdFTy+k;kfVPcx6e zTOpQ`Zz17FK%A1)VnKDsbEh<76;)=69lHBFv<14G9URp(NOU(h=t0A=HEA%betSQH z=OhvhjSoZBmCYy~UH3gS=l%~QH}Cp!M##E2GaT^M7zZ~sSNj3YWrtdKZ0?W3TvqFE z%^ip4cKxU3j^O5^cr^E?0CSDiYf_EU^}%MCVQUyzJD@*9W$hx`<_0lhuqU-xShFh| z^R7&B^SGqiC1D(^_P55l(YU5}XdDh%6zRSU@_qegtUH_-0V+inDC8x$!s1-Rri_|4 z;izR{@3rX`0~YoeD(Aa>prLX&lH5O|a;T1p^aFL^k(o_$JOw%LsQoW`=Wj=?apH(G zm?@28(YUrT*$g+L3L?~UOLpfk*4z=Y3$jP*w&`N$RBZBt?*Oj=tY=XR<&J=@@@ zBG&UENQN0mWgAM^i+jFrWi;KK=&f(!n#geA{g3Lm=HU|E`ZhECASKrQ!sl^=b2wb$d2kJ30E>eGjK%BMku=<8+IAGQq zkCEP+tG?uRsZwK6j~Judm#R8>V^J>th>aBjGbAK;md;JY(Fph&*9vRM%KEwC;2^TX9U@xy{@KO`7o+akQa z7!%-&1?Y=dHmtlc!Iy%1(CS!Y=(gr`tg(Jul5~w42e(BvoDa6ZO+zJKaBC2bY&ghZ zbVav-^LJId4%*=)0WV8ffG3!Ag)K3Pij;n8ZKOWe4xSlYeJ;*ey(=}u{VsuNr3c(k zxQs*l(=Gzf>-=4&kq11SFTXa6${-G|0R~HR*f&B^2^PoaXNFfgS z=veTk3Oytl$G0bUVAEMGN!Dc44zyJ83Qw36f@{n;|LXPl;t-|H1toL5!V=Uh58u=W zF}Fc$M$a8F9VHMH7VL(nh42m#%x5W z^i(QA`2^YIR)w-=rYu{ki_q~##eU6rE4P+W@O-FnO%S?fRp3E5cm<{ec?KL zVvQemr-X#I4Wu}H6h{q8T_2@x4N3=~w1c<$ChyTgIt0*SjMw(03Mbb>C+p90C&Q_& znw1Lg$jyaYRM@$IgJd!V9yr5Zt2!3p$cM*eOkRR#6iDWX-Cb!jAXfNoRSDc8sFq?4 zs&j_eLYVh?HlYQ)(k!+VA+kopja7R~b>{jXKtQ9d(01`~7;I(_^E@luf`#2!A@c$& z`~?devO?AotZ)Gqrn5rk6;}8S7S>{gtXHrw_u*o=pMxqAJ&XHk2UZ|rdCuSm=diW`m` z7#+KW(P7JmWhM=p$(D82D~cQL$hbt=gVCV4;SR=IfSsY1X0f=aE2Cd9Dgagb3BkD%@JmH z$`e+SfKZ8`p0EyNJj|SlfOAGdCJ70_5y4tAW`eSdBZ9OP%vvHWo~EHpUjDw?x)aKB zUxiowg09)&txn2k*&yw6P!`{&6z^l&JZaEC5PQ7EyFHk_L!WE2slhN$r-B$ayuJ@F zphmf(w3(oJPK>8J)Cu>=4rRP*Xkf1yF{?m!aN&;h=0BEbKmA6d7+ z+nph1b>Zl`ugGrr+Q3rXSs9X1{8Wmu|L~n`c;U=htz2~b9nMQWzB}9DpvW3KJrDS= zdZ(8OI^TA~yQq4H6K~|~kM!AGGg?{WaT<05_tt`CB1{BDH(M>u=> z4TG@>&gs)|%=3@3*o@6}Z3fonq{tq)O%~HOFNgiS1vZqh4K)@WN{B?yh#8iBonzAO zHVzz0sEtnYmZKF=UOn?NC4&f<3t>!OYuD-~vB&}vAArCILl!wV#8Vv^5EO?o8kiUp z$!B8LZ%foAcg~GL%{DlzFlcbs4+|ohc+;4PgWb&r@Z&9ZHx(E&*-4xl&$c!u4W5L+ z`(Zg|)$E4NL9T@_p}GI|{KwZJ3D6rIq0xtG^hUbT^KdMDGBM_GGIbgA4yVIASlbUL z-oZ^QJTg%$joVLc@Q_9D-PzQJBHi%a8mx%kBP@72SK|mP59qg<-i)?yJK$da@{zny zzhh?OHn*-5jeA~?Ge69`#_##VP0w9R@<9#$x*+e6q!yLn*INbO#^JG+A*^KgGd}wu`J-=gS?zDhHepjcV;w-bQgkQ)lX8U1{huMPK;jSU9Ydbk?K4PGwx3< zi$Wzy6X#S9`s|0+PAtJl_jAIaHw*5d*Wqc8TDkQ04uBJE!FOj4K#}gE00)!?IH3NW zIAAF;mcJz)CB+kRv9-VX<9y~D7vG8tXCudZB$HS2weil|`S2ao)wfGy@vB&=bO@{m z{OvtlMYHlsel!*z8$rvAUyhBWQ;d7wDTr(UO0j)5eE$hA3q`0}Lu1W5_$unF?`XEh zP$-qchF)RsDS48O-`{DG-h`FH1h3$y8Swh@&4dhm7lo9)tJ%2_Sk9D6IFX?23m1^| z^1EHtrXcA|f^qTPXgNr0W@H@C4u{nT?-X!0$NSVWW9acQpzE>YH`le+2?soGEjZ!T z753OZT}Pj2lFVC>Qm&Q35tJfDl_7&P4VG*LTReOsOJ^;{dvZFH@mY6!t_IF7kM98!hB4;@7Q}tH?A1lW(Z|` zl4Rdwf)y? z$vOy5iz=0cQuyW$KEPQIxTFR2yEYh>d9@VFqwrg7HN+Fq9X1MX*c0$iL(J^)GRl+K zVvn{T(thnuPBL&}%TSl5%W(8}pi676sn%33?7)*9EF$-(^p? z0*&bu1{wXDx(Uf$f$SujAN1nih8tkitC4J>ft=DxD2<9>|_i#{ZVj@EV<@;ljYjWL^oZ$ZGj=&{1?4y0)Z_vFVe$Xj9|LC-LnE zfg%~uvxDJUJ_NPmJ&R}d60t*M!U2d)p7;pN*Ww&KL3P8ATtF?H!WFU{y!wc7@2NTw z&7iT8VL)<~KMqZrwc*CPQ+2E(pu{~AlCTI_yU+OiR0_P5e*IKTF03pGQnbrbwg$L? zHVSw`rP7Ung7P9^BFCImx(?Ek(MIb}^DSe51I~!x1tDYFrw(|l{mD;V`D3B*OkAW1 zR{)eGOA&rNJ9VBp#LneQ7Rn|g>anHOpR$=4+={r0EV5( zG!I;-mq5bC9XjcnIuTM~D^h~A;gFU+1PPp$-3!yeB*YIx61!j?*y*zKB3My^+>>=E z-+*s9WxJU+%&w^ZV=Oq4`p3D?(M4lL|R zMg?SYn@KS~{Y+&uph)Ovk~COwv(NOAO0g7rV;WwsMK1_Y&ZNzR(vTeJFBLn6S;{Q5 zsoEnzIujLGY}RHOXFh9cMJLD_ydZ{qoc2cT(+PC8(e`u)n+L`ryub%N&OfPLW2`w{ zKNCU=I`j=Jg;^1de-pAX?m5^(Edf>cT;s3Pweh(Q@XiC0`%y@MIv*~oyOJ@7aA<|oc*LW_4$3kTBz4u<@-K!{p=f2Jk9%gFsaw(b(B4F=wg z!}bH9`bHznbW4#oLw5fg=E5Ai?EVa{CpH?Ne(r$Z3HbE$I>vXOJK3^R4|cBvaw8b35S_eWm2vLeEIP`#|BI2T5cB>Q z4@5u!sxY)KKqb2cmAKF7e*XUEk3sdR$x`WQDCcW`Ymf!AFLVtUs-#>ijFn2!91RUC z43X?*ST0$z&GwR!#xLicK``D`aNc+$`Aeq`GZ3x*WdvA@;PJ^gh|nm$Y^(+$HHsd` zBJb>ndoMFVd*GBCboc?H|636@%NV`TA;C=1)GhV?7Eew^j6fsX89PYoVlIjrdS zc~`Bn>%c9s9xJ1;tAO`ZVbEhCTV5K1d%H0AVi7w@kI755XGzh#;t6Lf@fq9zUo}oJV;@;B`i>l{I3&q#(x6#wL)z(){iN}Vx~2xM+DMi1L@2vtmtPIgKSHT zX2JOkn1!;qS*RH^s}~y=tPy4xOh&>k7=iwF1w##CR|vMm`UMMXnib5C7zF2r~;$AbEGKL9@JLwvoi{ zD`a~K?5%N(Yy@gK?vs5iS#l{slQ7d3e)eDTg~1|8ewCEUaHfhi}5u zHUQxoWxGzXtL!DijDUOfC^tmETC&k7{ib%)Ca|4Pn@-5na6Q=+^1UZ|MvuE6?l=gU zb{ziog0jB7$?s0e-kV5k-?w8MparE;a?v>VO{TsR5C|agFvC745pq}|#0414Av2*Y z5-|9RB*4GM@UK1m%Y=W+;2)5Q4FBrGzq;@brabYRXXEgp9Q>)ILsD@amHfieMah^i zOl(*XN&448s-h>YbgE=h^3e>HtVzN&kL3@r{5>q4%+hF9J~0t_UQNLC`ver%zQKib zl`KwZ2rrWl0MRNbNWyeuGM4wOk7+{Ug>(y9UHlaM7TKW z;S3{OjK>P!XN5JklBDdZpj_M9*jV475#eA>2vVoBl4#3_@Rg(`oMNlw$)E*=DjCL_ z+?{cx{E_ttzb+CvV5?H5+=rBnjKrL1M8LO;yQ1TvoP3HayAGnw@E{BwON_~ znyRE#Au5r{8h#a03rTEnKX8@@U2mw8P*6oBpE4(RXDU9%8hbtI%QV;{ChEXHpQUFY zRmpB9GZmE9p>i-b`CT@qC$li^lF`L!A#3Vk13xg`cZH(6HZ{hQLwWP-spMibmds5- z|83932IgixrCG_JX;Yn65@$ot#V7ml$p#_p)?Eo}LJS^BZ&fex~VUR&6zFa-SEfOrFx?aItU%j8oLZ*6sL@S zR!reX3?T_8i6Mlv^`W{9$;8ZPrxL~%_JyB43}dK0LIm0K^DKn$OG^mJn~h#eQ;CyQuqGO%JzLlo z&>V*L)aPg(L#GP;(H{cMXHA^8a#|%rWqF7ilE)ZwWg}`zHZ!De&qCOYJj38DMr=m5 zG1T3Lv=(GHYw#V`L@RQTz|Mxm#64U1AZ+(sN7W>Ru;IC_h5{=lU4 zCLc0%UT3f`IYZWi6Q9eU1B34_a+Ov54G=obLMwn)5?`keeMfL$pzUoU4ioHN}`dNE>h z?rbd&P(Ox#0~(^c89FP_5QfGJ&Jhd+3p9$Mmjvfnh6)620z+GJX6sDoLkt>%bqYfd z2&6Nl3p9(N%^;>OC^Mg-NcHF65$V8smdeg>(k>iF`n}`=m0`r5fjOWpeAV==9~uHbc~Z?el>ESG z8f;YB!eF|{(0&sk2gnF=nW1|uh{lrt(itFgZys`%kY6djMEXDzM0yF4rD~xQNw8Gi z{46*!LIlTlz&f8KGxQBK(UvSGnGC^35zrD+TSB47U=eRi43h09b2%wwoD&)6N&@Gc zATtQ|Rc*;CofI>e1^cMBFhU#oRa{M)Fmw^7ySC&B(wuRYFbmhf@6e){KY(_CY$0tV zwD3|Ya&9N>7;4DSUV&obkhULCwYf)#4iG9{X3bZTjtu?G?0tiDX6PB#{9B|eLx&i8 zn{;RBFmwJpq^Bf<^)}eQwk5|&Z@(tqBmMl)2V{U>3!jpqI)ndVuK70^&d?T^(A$#F z$Vk76UlTW@wPdROKqmRoekPOs(C=hA zZ%ZV)m=Qx@1!+rEfhc1QrmGl@_hBeq=ht8u-M~;p5Z1HNrx@oc=BG%ym7(U$zfp7> z>zFMp5TogK$*B`tL?m?*M??&y#Fx(?^$PbVWsvsHgGuS+!TiIJ_Ne^^q%ESqi%lmh zd z#D`F>KNxWVE^kI)Q9dI?vUF+WPf7jC<(NYzmAuT-Kkb-*ui%d)mHb%`)855bf#+iU zAydEd_6;z75ESZHo?jPJxgMs+8e{s41JlQCnAR)CbZ;J}CY`Z9pN`TQtY=Dgl7(P~H~k|kIN3(d zN9bX2aW%Fj*C9RQjwSUhf4TlaNY|uex+nTzlB&+J?lAW&pX0J8_babT$Mj zl9dH-ML5alb>51oPdhfmQl%+Y%o8VtW%N{>bYr!=%R;j zQh^ewemQIMQE2j-X%VD-4p&vjR6Lua_rxo)~avqvr%B&m**24}T+^CZKqTYbCpg~ihUbXxJX-v*_ zNDI=@;_iiV3f%9pd~25e2_r=%jh$~rs08JB!~80Qe0`#~v<9Wv$x1m4s_{6lPSLUP zK%Gxc!*71Hw8q5RNeVq3TkYNysD&<7NyjvFX{zO`SW32qdItpu*yxLd9=6?tZyb2g9Y5}XeHdo z6tsepj%n+`nl1%fLsaru_)$oC{%}Sopdycxbd2l{X=&sRGfKM)q;01IQ!qag@+owT z4wvB)2FNLvPLeUdlBK=PTcNRGjY~lsPCk^m5u&%@G3dqb!gqiJPuR+V`ZlDL@Sgq! z`WrhU6`m|^3qhXwp;+>I=v>zD5@>Ng3)11zya?S+UR38rVDp?CVFG6WFkYcy zm?*rVN5MlX`4Q#`w4k32VwiS@w59qIq*TKEV#p(P8AwZ^C^??V-%e5fNG87mCL)z! z3I~lC8vZu*D}Mn9{mP9vOkXa-6sOpmSzog*k8o^$1y~tuejL)HmTi#!5^{i9f7Xuv z{?M+H$MZ-G-2bA}`jvNK;r0^?tG+D%E6WdId2`euQx6BMykvIJ)3JkK{Pq_pG<~d0 z$qkRec# z)*tQ^pWq0*{e+_(aloDne6It1+R>35xepZ-bv4gGmc`1Pf6@_ZCUcVdMmC)UgHKRaDD4 zf-K76aBqs;Ig;!bXk$u>a}?vgJ}Pm)doB}boft|udW=yO0X z3zYBBPdPV|lRo%4pqyeZbYI$6&dp>XJ~tsNf`aON?|h29#LyCQJ^oifjz&mBbjA4$ zImys$(laVF^;z-Z>xpo=NTQqPh~fo7#_OZ|fU?n5V2&l7Wd9mT95FKPcy{gT+*d(l^^ zUlW%=LsEWB{f-p*&>yKklHLsQMdmWO$A?VhG8y4RO4`q4q7PZp{!7X^B53_DWLtZ- z{&6S%MjRb@^EnyOX}^)NKIBaMgTU+hOwqSpTTFkE&jdl^WJ<*fXc3J19Mbm;lh*psJx*O8sD1HWqQuY?ylbT4^_2cNbxPEC#blp81 z9kLdsI_T4b0O@S`rNl{}M<^x?Cw++{2%#NmPP%#sqmg@a_NJxMmjyaoL`fQbN1zWJ z`_t0sS%G#HA5P1lKMQo)@phU<)u9Cba3(#OmPPX!(h+`{mP_yP5&xT3m%4rEYFd3d z(}#l78`2dVfogDeOph>hj!uqkMVinz7`i~`*us^j^q4>mQ=`+H(YuCW^B3sI1WH=d z0SukfX<>F}fI}JN0o8_%_MvdnhCb{=1?g?+US5$k*q*+`5O1&p-7gR}*pVI{&h1Ul zqNE#rk0Bmw-RK#HRs`uvbc6J6^eX{v(M+#06+*q-zU1J zQI`*;xyoo)j)(Xg|fnoqv(BT5*0Gdh12^5-|tGi~=DFW65;vD*jKn_6j=wgAc z#uU5e)0ZB^=6MJ$q^~nnLH0Y^xfark0_8U9>sm~kjYG~A=hle@mCvh9(f6=j%IXctiL)U(~NuZTYK6kxJ)AcEwxTDE;uGi>Xfqu0A z;(DE~_MwH2D*CBFwQPU6-lYHWp}CGX>G$wy1ormz@V{Jd(JMYQ*YOtp_cTuH1>5zb z)LJG`%wMi!)KMnZKfDJxMv(+#J8VrxFY=BTnci%2ETS5nc90BsIfrwaK5$&O!LP6h zRF+`NIO{`aBQr9-@u3%*Hp_q?84{co1$j33Ar`j&kSf#bbvD|r`-npe9=0ixmvw!e zhiySVlpOtFc&HEMJ01+LC0%4T9ih)O>X#8AwV94Js|czcC9PxV2zeV6wM*lfxmB=V z&a*kBV}RhlDuT_sB&{4P9w8S~PK9Sl#~7+2Xhj`qVg=Hw2wKrdN}RzF`l7XTa2BE? zq%3VntxnRWISkRq(gtVrl$tC+#HX?WQX7VLiF4T!^{~ zWM;G;98t6hUKVD5RVK6o-VYWCjTj;sf)jl>Qd%t#`f#+gNg(v$gVMG|SW&N{=*00- z;$nnV6m2SzmMuqggvO-&YMm-wS-}xnI74~@-gZNpr+9n#JZTR@RTTRk-T-2MM@;C$ zRnm}+oU>WPmGHGv;xio0Z+a#CDX-eP2_3w{3r*AwP9sxv;~J)bhz_a969pT~N*DTSk0=?Fvc`~!&qcQT1jbjwXaC&yE!MSZIah8#CzK+Z}FkeBCPU$&KZQA9WK8r z>Q(5_hvD)^0%C*F@zp(-XOQSQePi}ghLeuj9gC(Gr6hEbx) zGX=sHa^<;#^J0pAD!i_|M-b7bhH~QGYMWZgGa2GGb(ZG`pj8a<20P0?ih3AcedXT; z4P&OieDMX8$$dCj{_I6WOr}omlfPk*i@8tEdoLR!SBZQ=APldmGV%du z*i+&CX@wV=#=_A|^!FxJI3=dWAeqzyjOVv>Ecwe{tfzgc%vL zT0-7PuFO~>C+*P8N@p>EO zhJuFmHp+T`0kPsH`GJGg6`z);FvKf9D^~~_R(w`oClJ=#CXYB&UGI5$B15pm+E--k zmOo;MOW7lz5u7Mxk9<)V5XJ13cOR}6vrm4TAzpF6{DGih#r^U{fw0~I`I{rv^$y9k zUPr|19g$-g;{H7%*Aocqy)IvUU9YbAmK<7zXbI^Z_ISoId8|N#!q#QHBfrKF7y7Pz zlp!wkUHKbtJdR{)u5Z~*amm40#26+?b zqA3BKs1)&-hV(jUhf;UXqV7#8tZ_hrf%QD}qq9OL8ZMbgtVa zxhI3%`b%zUs$X$9_ewrb!_htE|_faU<`?7qQA+G3U`454x3!%XeSo4H8 z5h_rD?HV+=>_bl5mk^Tqt6cUGqS>)JHvfk_mqF|bd*&bVe?IoIre|J}SAD`+k@LE| zjv*eT*X8{JVUQAK)+ukj85xSQjG-mux#WUOv+`eoUP>;?3{pCMDrD*)G+61wAh$PI zX>%Hh+}>biIz#CE9cjVJ6AW?dEy_lL(0YrqRUow9qU;f2~=R~l^LN#og?svfjBtRu1pgM zr>i(+l|bmHcxC7pUd|bMMzS)V!6oE=`bo;j73XWT$QIYUcGgRqI2h00lhT8B-^ ztf$n!P_0an(u^UlOp!8BAQW0(aeZAavyoDdp(W%&%k<2q$})kbSY~E6)0OWS%p~8!iTgwopz9giCW9&)xUkuJ9&yl5>9hE9SVkhN#@hpJ(T|lG(UZ)(o^|ap!-rk0hILAWSGX%O=^a-HF3{{Z4 z`sV@VUgq_33NB_2RvI(JyJD!)p3~SYJ5=e;&`e@hf6pAM+yw~ipGgME*D{AGLj|!h z^!9M2LZG3fvvq_rPoR^g&eoC2GJ#&Obhh57Y!T>ONN4N)%1(ja2n*3hDfNFNp0dOEbqRzY6s4&{XX~<(fdLVR_m(Mg1A=ok?=6 zshV4{dJ)Of;QyD!5D(G`ibK#oQu@;g%Hsd>dP+zWZG!SNLpo>mD9;Px`q0i+kMgEK z7eiZVCCUYX9t`WIO;oN3)Yf_}^C6}97cTVmkOA5xrJq2JLhsieRz?f7ENp-_S(ziy zzr*g=rYNf!V$c65nW}6M#I4|qsmfkXWW83Z91t|cRHBtC-*H+HJ_VVkTowqQf=pBX z5~v(#Ws3YOsx~`l9?;5^P>z_)=}MG9u>M7p=}MN5SgMsPJs9Fgr4`C3hIl|#C=-30 zlvF6w7@A2A$}_YIN)N6n&6k`_UFDtNdt- zl(U=`^m)uv+G6D!fj&+>PxZyhMPJ2S*J4GzT5aJ{C4wQ|;8G=?p_$}G+9YJDlHtc` zD0Tff4W*Ib9890r45gJok4c@a%aq{)DRO7)a$R{uz;b{ql+6OI2eeYD66juYXX`5E zYlgTok10P1+Skef?J*_nTD50ZE71(iBoma**40W=AI(G_SNbz_j`o7b5Kk!jNdbRv z)C*t_crzED-sD=4i0>>TdJ#U3dP14Y(7E6_4cAIfD2q9w(;6UcrJ%jha5$iKg7!|{ z5$#FkCxL3^Pa$iR-x%T%v{sRI37gW+wwN-u`E%uUKbL3>_#SKFin!RspC0N$cR$q@hE=C>$`j5w1l zql0Z*lyreQtA}k*DLDd#NP}%pEA<3g4YX&JCJb?#wkjPM;>~YWdI-*z^vQ^=$~^+5 zff3It!)18?2WRr5bVtNCWt%Xp z?pD?^g!Y=q9%Y}PVR-FTjtGPe?p5l+OHAJO_9{&f>7WfZxL0W{h}hs>r3*(C8+<{T zDG)aJqB5T$-r!5hWquw2q!QTB4A zlWQT@v{#h_4016CmA4q;4IWh96BWlw-`Wo6`A<6%PnonN^Q zf_T+x){ht~mUD-DMPMJ_nO9<3L~H(9U~wCQuyo)QZu|e93CD7Zx)A#2NS)> z77pLCp=ka`$!`!W&`)tFf(P0L2Z|V2h$1kQ%q*0GL?|^6NB%GfG?`HqMl}UcxisXd z5w`F@O!YO)0|c9q0~ES-xqn^sRx37K+dxXm$TaLe4AQ_n`bJ{Dk-;}QYJ|=)G3Ve5 z)K~$d*PHgNjTIU@QLQC)vHZr8-%_zL^n^@ygXaRRM3cM&k1OO$G5!(oKc4sYH#Q?f z!`P&+!cF=4NZ~2>*2^(i3sbLTUc$o@^E?<~{r@)dc1)DK))d8IWv_BvA%zW5oUFLF zB%&~-d1HJI@Gs%bV>vbte$oSUh=aBo-)x1qLTz)ED#K3fyU2T^^oX>1ddwy-o6=$QLUAJa&O37BH^XmSR-; zay_>GVRdga*q7eL0|oIv`0gHh8sr#W#+fc~F5}c8j%p9xxCH-09$l={w-UsAy$D4D%k{8!qPe{JqBqt55&Jjwli_V8l%=7 zATq1TNaaHjn~}&FCZayXibS?TiFb|QlP>#z*|D|5UxGKP-j1u@Q%z-K2TF9bn8iPD zU}zR+!annEm9V$GdH*I+3BGH>x~K|r^2p$$oG(bY_$q9HzHuig#=m#;M45^;1~gz-F+ncdx+J;{2#I%>URcCbk#^ZZ5Fe&6;7= zAlh$N%i9IK$-`_rgEEzXy@h^rvezFqtKF(RurXBe#uDrx4D%!Yo#ns(33Lu>%6IR7 z*9C#i9EQM@S!V@W=`}KtipH?`_v;VV5rO^ZA5fh0ZD>aM#~sIO)#y0gx0$@Lzi%JC z-tOqZMh&j(Fk!i#ys>}oA+(!&2xkTO{t3r);C|jK8YiN^b7`QWc&F^$H{7_GahW%nCv<4u zduNI+z2!u4M}dj)uaIlWH6!3wk`Kp0{3~Q3yT3uLZ>P%tVdQhK>tJzpu(SO{$3ri+Jf;;OC{sI*tPTl+)z-_HbA#;Mn< zM040!{rzm{<@7drYkvf^qWdED);j+GR8haxAeqvL>xn+#cY0XjJp~QV!KlV9xwqt> z^0!Sv$xrAYf|7q)`Tvfe#sB}6HU9&(pndlD|JBKV<73_1e>eI-^w8f_zO%X?vg4Lp z#E>2kM99kj?#v(ml>bI0^c(H|r@iR!C!DvEe^}vvqukBvLj2vj7jIVK@A&;a zaffyB{+Ayo{SPYNv~ELoJkK9de93Ot%h`=R&v%1E33#`NrI~QsFUB7wA3%`=n{gyR z05iY$zJGF4%;Q}P93uMV{a1%nSluc9pa7))8+Oja8|Hbg7SFR3&wsqt;GCMBcJh?7 z`ltR>|Ga+#TpFkHJeS}v(?74{!-l})Wjs=VCmZZcgj0FS-RPgbUmxr8)Zekl;~fQD zxdm3_yfM9}6>~d37x0gsg*;U(3txC$f*sfvuq)r|Fzhp@02x%t#Ivu|MQo&xn z`xZ*-!S8!uW5JMuLM$K0@{~Ra-+u0thQNoMP8sV?k?wJbj3?Q^t=CQN;(OKgc&R9_bP(Ycg5-RxT=9AUP?%qUEHM z;@3)-K$URvt*qk*_^22Bs+pXWv`M1ye>mz-d(x4yyQNS%DWM9|QH@SOYE3&MMNw2S zSVlpKpvoSKjb+F?<&*Vu<#dWp&Y|f1dQ8zK^h)DF1gZDQP3VQ>vGOG{J#K{~9|V@TjUg z@1Jw-on$8eGnxDuk}wl85h5YxFY+fyLWU{|Y7kp(Q8Om7#e#~05-U!Igl_=P2PmJrLUGy!3Pj*QbH?T*^)}T(n5E?zi-A}-+i8ao)z;s-}^nk zbM86ko_ps8?zvNjg<3)Vj_)EWdc?aD-`O@j^Dma>+FzHhM5W8Q<5Sk{H~Q>;Yk$#V zyWDzc(XF=EUNWP>F0db8_!+y{cI1SZ8*V`|glCdm)^px(n9Do_r>ubmTkKWrtMqTL z?b4fTdhJuzpBC)5S91re@%D@RmDfJ%X|N|%Ck9!1c@n$%L1}~CiP^K>YUX&X<#-I~ zcYXPe4RoHNb3~l5qa)Vdxivw@DJwpAt)t)StNbGP#*8U`1^S}@1<+OX6-R-7q;RFW ztfr24XU#Vy7fWyZ(aRy|PS1#AJL}v@?jiePd-+^}H|`bcSIO`pCL`n<#laVY+jRZ%k>-$19q z_AWdcKcq4|$H711MKm&3PRAcZQ;M9&v|aNR#}W01Ic3gMYV^i=;K{&E&U6ZJ$X$uIEEQEAPAIqJ+IK|#?Yff*2kiy9zezZv&Mgd|M{+X#{e&YJ z7Ojrig*lUODoXzb9;-HqtGUb7M%J^-@Ma|nedO%Ke(Qq&M(|2OO=7coF>^)Y1^a{S zPbN;Hwou{~6}bL$iC64Tr>_HFOFO4~^dVf5*P4`h_p1%kVf<6HLk9DpIlJIXi5oCA z-Zjr~B%U!cCpuzU-g5Pfk%taD7_*)Jb|de=+KC z6AxmXE+Of3tobjQ9NWu z{NGLT+Vf`|OnM`XDkk-Dvp)Upq<%{lhr1lBi~gA8a$F2tNjhWV7NK#D3x0p{8S{Hj z1$k4l%kl3SSCUSdb-A}EpE6zP_kf!#9!NgU2BkZ`R`gJEy5rgMwaF8#Uz+{v2yJ}7UAn#2n@f+<6&=QQg4 zQ{c9gN%cw?SN=)$qlLl)^0}nG%GoVT4!vqSolFERB9>9zX{8gTuep1B~k+`8nePnBc8EcJr*V& zQL>u2M#=i>8YQcTYr;yF4X?7~t!=B&qMbrKwYkcYH@K~`-UOXf$AdO3K;U2KJAq7_?9vM;#r z<$PXi$>QWvG#N3hY-( zcVu*#ElImG)}W%j;JVzG!1l`BQoy%2W4(21#tx^O(B70&M&^B3}3 zMh|uz=Q(BMo~qT6 z;WH|?!_&-GuT|oVt0dpoJwwV@{4LK~`r9p8BX|nV8=ehxcCzd-e8#`)d4|pgmfgTN zYj)6?#5d>tDeT$Kj4p%qTo|-lFilS3_SuygvKHZ^B4jNrDuq5&kO{pf8v~0Q=PEb~ zx!YOM5YCaN%sm(_t%lS$7Ol@5QqM1J&D<@Yc;N5me^2kR%h#XTN8P7n(V-lOw=#Dl@=RtA4^t0D?h>>d zrU?AFEQ!g;>V(dtF3#$LuE^@qLpL^Nb?U#&{%lrPSigpTC+WWQby;0HFi)#}x)QfX zN&524NLG?ApZBUx;_pL!*v+JVg}r>0e1&~FL_K233kFAU291I8(CQnEKV;!=2(k}Z z@~Enmy(9iO{WIi*g{2si`h9Lps_gPFW=|?#mRzTvMzbzk@>amhmb`xOf|Z4j&Z~5; zTJr8d8=vu$I*B}G)z!S2?Xl%^{W5CPIakRIrgYv9oq4>s$g|~To&j5&5|(PvzHsRs zIX$|xW_3=L{g>Gra%yZDk2)jcQDX;CT?D7xF2ww0_MV*iMsnvH8K*iUBUER~sb#rV z(3U&L0CyI&48Ln+_kYe=#oe#w?t}JA z@iV+y zo&*>fkW%5SXKcBL>9FM<<{4)9W9WLk#n!)9q`TyIfp2+>?W1Ym_iks;I`PZsIlbMM zThr~f+?qDXP^%#=x9vxCF-EA0&MI=JEw`OJZMg;A!y5MBK#XPe*)lW2JH^Uzt2ycf zzW%L_Y#>{}6uigVJuO9bsC@8C>bhxeh2MkWZ&#JmocNLw(a)<};dfE*R1ZM!qJB|5 zj8c2lMzCA8fqkkAxi2&K+v@AnoME+(8?UH+$kBLKV`?opf%;R_d+IQB_O#drbr9S}Y=?_ueaxGP_ zBJa`H;IGyRd2Y28_2939P`^`MVQtq)9X7u~KO9B|-v#2CR&xwGolZVkK)(pS6K_2K zLB3PXpKoM+TA)Pnax$_QC961%}LW+OKakcUAO zuv;+_E?abxy#K0M2pl)EPRkp(4tKCDc293H8xhbj< z+O0y2X_nlu`W&rYu-s}d`7+s09wtwa?x3@mx`%w3 z&H?K1K_;)$dXxGXd4kS)>I>vW^3UWI(5ZBs&_$+`Ub28JCaXalTo7OU9e)(e2iYcB@R%OFy5wfGnajpL!8l zN8U~UUh4Zm%>NKK)-a=$Rd1%=LES~Ym%4{~KXpGe&S~-#{j=2L7AmKS+Iy ze)t48PBG&w^`Dt>h58zOohWTjCo@5#yoqv*^AqJ57tk*vi$SNFPrZn&qtifrH+e6) z4m4_gm=$egMXk)(Oh(9E4BE|@y^QIh|1!Cce4RW7I@NjV3*<#QjwETi3v{aFBpJVS z>SFqHk}&_Bs+<|sv>M13aviym>;#S4ktF@@LcCM$qQ9HmOZJn8L8m%JeU==jbA|dE zwN93%IFd2{F^tL5#sbi(ikMMMXAx*rU9#+QDSW3|Mt=p_0OEQ;y^(CCvxB;e+(qsK zjoQz8`dQBb`UgSG|JRW?)md8Ohrz|YNSFwCv z5`00*ijq3+pk93eRX5^w>8Eo;beib2(TUJ$r_-(;Mob49H0s;RX6>ZzW@Im8dKuG; zZ%4UU_A+AznLEtfVHK7thLPkQQJz+(K*Myo~Ql*+*YEs?9$Q_V?nJoIGL(I&s*$C_Ar@<5u_o;1?XngYTf+itP~c?M z8Wi|lRT~RL7~jtLc1E_--%8!dQk^KZu(}haZmaHQZVyZKvQ#f4`#>C;FgNzGfBRT? z9}0iFdLIhEUOmXdL(Cmw6~nAzn57PrN6b6u_gm20O2(Kw#!};S##w3tdenEG@ye3% z)|QNxWrbzfElZA;gUkT8mG~J{z@S2lhlaUDWF=#2;bZU_S!c-ss$+aTV zBO{yPjQU#Ww^>`lsG^M-5m?(wB9=^(h&8O@Z)j&l?F{W;?pEq<#`iF?2a({W`REOYnfn&~F_gmmA7kh^t#O8qBlJf%oM-3<^p(v)v!!v`eoN^x zZ4axYGK;>$mhsB~F>-VYZ5f+F`T-CVnYxy_^~fFd)r%AMEoDX{Gn!beiB1dIM!%f} z+Uaa1yXp7Rf0@o8%MQ{XrXIFsI-g_AImS$YINT1Nq7KOo+YYfZK(rSbI0>0iNvD=h zeT-7zF8Ymhn!s%(O^knnejEMv7$v@&x;I8~`Vcwl8>BNFX7Uybk5QkCQ9?hUZ^dzv z#);#KlSAN&lhe`_H>_sP&4}Z$$MveNl0rrnGP0PF#fbdI++sxTn_J1qT15V;q@I!W zabc-6hzwkAX*IEMJ9RsC2a9#n>5WlJ4N?!sD9#8Y$LO4kQG6B84#X&q9nbNO$Na~D z$BPx9Rm-6I7{za-Zi-PRT03<+OLeeRcf6d5PCcC_>L%(o#<$aHr_)Q_OFhWkVLHR&gv-ux z;~cXmShaF-eu8L7_GBJxo1Do}+UPk#GCXB@C-eNnpDZM6$$B!F z__V67Xbf|siEK{%8d}uMj5a#$WCxvY>Rxh?941FtYJ@Rkbk30zbW{?Xk|a&Elb%+! z6$R9VWNi|!|8?A`CxZ-XOp<;#vTz8_&&ryp+vvAZx0Bsu56kva_d#zf8KfSfKTJJF zj-$5!S9XqCC4WKgf2?F_qn*s@MqL2?wy%(^rBhEfk7i{3sNxuu^VpGlfe`z&_o?UHcmF`9_n7|KIm;FLv)78aq=8f-5wgR} zim3ajhp5NB(kwkqSTKzv0b(9Zlj~-XIz+#jIzrt&O`6*Sjrl)BYn;@%Qbhq70JoKd zsM~U-z!3pf$2642g1JrfYAu>XC(C?w{qaMuvKz(P<2=u6Loc;v0o-TFzr%U|>)Ztog)N!Mc zIz$~Id&oWzXFT;dseSA>S?iPP>Zn6xgzO>vK$N8(W2Ev+yq~NMGYR@7sF8Yvy0$7gE^9x8ZEjh7A48K)kno&Yh5GbC5f;8=p_H+3D^ID_*)#El5q z0b*{W*z$^z8BePX6?UOGfkOGb@&}~twPbTZhPRt~keq;n)3Qi>e-R_eAf0aNK4^S8 zP>&Vy{$E`up@HipBS<#F8TGZ1eRL*3oMyAcw`Ym2r4G(|T5YLlq#l8ffi0HWnu{f- zjk>Q`Vg{-G*Gq12w&dC+{QY+X$*8Y+jtp|g9MOSNVUV04?Hj}yE0@ClN?}K(?5vOU zSFzG6)=*pST2#Z)NN$Pa;d0$xio!{v{#5zyF#2svW@JZ-%Z^|Jt#Vy zYu_#f{I|20WFy%OA7?Oi2X!~~7+HIV#0T%-z*4u7-K4rxa{Xj2*+_QW$>vgzk>Lqi zszKUoH?S$x0qRETHnN)>BuAJ#Mm<5TRh}0;A55D%NjT9e417Tt+)RfIK1)r8?KWmd$Sq{Mu%m?23W2{I664B1TfMdhw$gq$FQFT#nw@Q8jP{HfH(Q!k``n7ShEuC&gy=hA+fmY)9G zbdUQMcZ>T+?w`1S>;Aj@#*9y7tjcK2crxRw8T}ap8OJkDXBbb0r^Hj^S>kE*toQ8p zeAn|=Pi^LNng5jeoy@_^pJ$%TypVY{(`4mjm1Zr@TAtOF^;(uAyC>U~B&ABsdL(w}mscwM^G`EKe{U{Znb zf70O}q{(+@{%;Dfz3NBVQuXwlHt_t6PB1Wk2iQ3OtN$x)ux!Xu27es*iI47BuaAYZ z^n7e&>xE#BUIcpeVsL@30~hOhaHYn#v8!$RW^lV+27XoF2EL$IfP3{F;Ma8n*sJdX z`}E!5KkH9{-_iGiuj$W#hxC2muzmoXZ9-^HNf?R2Ln}?#C805XMB{ zeH+$ExgCo^kE0IEa@2!4j-}u<$IW1#V;MNzaT})i3`9Pq0*HJ{U5Ci0)GS0krLK3} zT!o|rp--t&gl3MX6kQK zznNB_{!sca)89#txQE;)+<$Z@XDrY7g6DP53D26$7qh;T^=j7PtT(eRWhG{pWZ#nA zlKp)4i`lPbk7WNfJ2j^$=a!t$=RBKpBIjaGvUi%d+IyF`)7$0U?H%&|&U?xGq1SGc zhg4+ZeDvR=H^P~VBle&FOZ;=h^8T@R<&$BAT9oWj?VR>26o`&}pDHW&w zR4m(ZvF$l(E7tcZEJ~Y7Z;z#S#?qDHm{=7{H^tItnA%v8HdU}QmhOn9J+ZVmmi{D` z{xp`p5lep+OMe?nCt~S^SQ`FgOneYa{}M|-jHOynH6T5fdShvBEL|K+>tg9L{hR61 z;}yD-?~7`PrT4|sP%M3PDtR0; zX_pR{sOr>fW-!%Z{e#A$E1hT!*#}Ks+C}wPdIPl9L-s{=JpJ>~UqXJn%FcLGzu>u~ zFXGkmZ#i5#E9H`IFp0*SnP>_#Z?L`iDa0DP>eL(T>eNJYQr}>o)En(hSsU#yWnEOg zS?ze=RI&bRmezkQgOQ(ItZTBD>3gy-s;9CKn!jcpz><@r_CMqtwO`8lz`p3YW-szy zvzxv9)UTp1wCa1@O%FT}T=T?ZYeTK|YeP>wQC{}I!0%i2YCBr+tj>r|->Z`bYM#~8 zGxeH*l`rVC&Vifu=rnua)7^TfHDLAWuC(~aLe0%<9{FtaxmR`mz`(2eyJnziKz|8` zEc)+9wLkjPFB;L_3yg)uIS`D*u&K~byDJTwFc+G2Bq4zMZ4e9 zv!O1;ty=36IU+bU5YhSedZSDET(>lTE#nH6WI*>gR7paYS zK65QjGCxkM5Y$H15-opMSNT6R6Q8$!Ja6!*TBll|T9Dg9{RkTIMD+eQbbfThd7Y)3 zqmMdFW_0kh&WZYer#<#erB_7PADQZq9W8lJr$yg8t&5`3)B4Hi`BOSK+I>b3q5R}I zojn-s#i|2T456*^WPy>v#GMH|oRkiYPMDzSEIjH0if)tj90Q(e#l zKYv$mE=Wp#2$|B0X7$9bCFc5s#K%zkqv|pB#DRy+VW)12UVld46PScEE8__@ ze;>a6XWvd3NeUjCh}OEyA-z9(&1Gh#@7EbBKGWUyp?23gGu@H>(YXnxD)$6zT-#uX ziuW6nf#*9A7&#H$nqd6;Z1kH6X05(3kdbJDnEnIzB$O+bE%(LwwMK#h+PD;CC?7vlZYaTzeQ#N(-I1ke)t2?q zl^)ZWzgb$kS5H@tc;xJiLvu2mL|xzd_`otKxN2eC_|mV+*|{BFyAi!(1b z%@_tx*Hej%`Q6rZ<(#e^2?qN?FVPt$s3kc~>-^~mf;U}B9%{7WwKD#ayW6_*^YF~MU*|dPxb(;Y!9l?>!H^x7kH-5kM2Ky5DT4;@#s6`Bm5;=MK1TLN z@AR6x-97He%kIcNv_3vFeGLY%CpzFYyY)!4VVcR%BLgkdOtH@V1?EeFv}=cPw~ZU8 z9Hd`FdvndItoEz_Nco>lb2uqM@lz@j^O$xHpw;W7^k68jv1s#l2*f{v# zLJl(I;8e)#4!=r|yq=_8fm{IO<4r{$Z1MMIbbXe~yQwGAOC?qfk| z|KElaxd|W3jc&>}H@W&#;^Q)~^TW}f<(oX+A3c+AJ{RntuG}p%Fs0U~hf-2_sNJpU zA^Ayb851@AN`%r1oY*BM8CljY;W}YGfd(2htUvs3TNz6Fj$y+k!9tDw z85rYuoB(YX(nH91*Z#MBcdff68GhT=k1`Uc3Pxv_nHi}5_A-;L`v)E>GY@Nr$-_xK z@T+n&+nQflT2YF3PgUYRzoHZyv#ZNnLRF!%c@;O#ZEh*Am{(C&J$K%`2Opdl3eB%B zudWQut!!$kd~n`CMYZ`!LZ$lQ{4abRA3t{*+~=&v&+0~_RxgpQssA(*d3Eqi{Je?0 zH*il!_v3w2xc)+Oeq)Tuk;kM5AoKy-=OYdb3O$yPBi z|7P(element.GetRawText()); - if (member != null && element.TryGetProperty(nameof(SerializedMember.valueJsonElement), out var valueProp)) - { - member.valueJsonElement = valueProp; - } - } - catch { } - } - - if (member == null) - { - member = new SerializedMember - { - valueJsonElement = element - }; - } - - var value = reflector.Deserialize(member, elementType, null, depth + 1, stringBuilder, logger); - array.SetValue(value, index); - index++; - } - - return array; - } - public override IEnumerable? GetSerializableFields(Reflector reflector, Type objType, BindingFlags flags, ILogger? logger = null) => objType.GetFields(flags) .Where(field => field.GetCustomAttribute() == null) From f866843a74d128bf1cc94096c660591b8c7c094d Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 28 Nov 2025 03:31:26 -0800 Subject: [PATCH 06/13] Update package references for ReflectorNet and McpPlugin.Server to latest versions --- Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj index ce3f3100..a54f1b7e 100644 --- a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj +++ b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj @@ -38,8 +38,8 @@ - - + + From 695c9cfdafe4ab928aa87d85f305c7a079ab476a Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 28 Nov 2025 03:57:38 -0800 Subject: [PATCH 07/13] Refactor logging in UnityEngine_GameObject_ReflectionConvertor for improved clarity; update material array assertions in DataPopulationTests; remove obsolete MaterialReflectionConverterTests; optimize texture creation in CreateTextureExecutor; enhance resource management in DynamicCallToolExecutor. --- ...tyEngine_GameObject_ReflectionConvertor.cs | 2 +- .../DataPopulationTests.cs | 2 +- .../MaterialReflectionConverterTests.cs | 70 ------------------- .../MaterialReflectionConverterTests.cs.meta | 11 --- .../Utils/Executor/CreateTextureExecutor.cs | 9 ++- .../Utils/Executor/DynamicCallToolExecutor.cs | 13 ++-- 6 files changed, 13 insertions(+), 94 deletions(-) delete mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs delete mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs index 11fee955..550313f8 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GameObject_ReflectionConvertor.cs @@ -186,7 +186,7 @@ protected override bool TryPopulateField( ILogger? logger = null) { if (logger?.IsEnabled(LogLevel.Information) == true) - logger.LogInformation($"[UnityEngine_GameObject_ReflectionConvertor] TryPopulateField called for obj type: {obj?.GetType().FullName}, field: {fieldValue.name}"); + logger.LogInformation($"[{GetType().GetTypeShortName()}] TryPopulateField called for obj type: {obj?.GetType().GetTypeName(pretty: false)}, field: {fieldValue.name}"); var padding = StringUtils.GetPadding(depth); var go = obj as UnityEngine.GameObject; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs index d95cb272..0291ab7b 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs @@ -57,7 +57,7 @@ public IEnumerator Populate_All_Types_Test() Assert.AreEqual(prefabEx.Asset!.name, comp.prefabField.name); Assert.IsNotNull(comp.materialArray, "Material array should be populated"); - Assert.AreEqual(2, comp!.materialArray!.Length); + Assert.AreEqual(2, comp.materialArray.Length); Assert.AreEqual(materialEx.Asset.name, comp.materialArray[0].name); Assert.IsNotNull(comp.gameObjectArray, "GameObject array should be populated"); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs deleted file mode 100644 index 03cb69da..00000000 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* -┌──────────────────────────────────────────────────────────────────┐ -│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ -│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ -│ Copyright (c) 2025 Ivan Murzak │ -│ Licensed under the Apache License, Version 2.0. │ -│ See the LICENSE file in the project root for more information. │ -└──────────────────────────────────────────────────────────────────┘ -*/ - -#nullable enable -using System.Collections; -using System.Linq.Expressions; -using System.Text.Json; -using com.IvanMurzak.ReflectorNet.Model; -using com.IvanMurzak.Unity.MCP.Editor.API; -using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; -using NUnit.Framework; -using UnityEngine; -using UnityEngine.TestTools; - -namespace com.IvanMurzak.Unity.MCP.Editor.Tests -{ - public partial class MaterialReflectionConverterTests - { - [UnitySetUp] - public IEnumerator SetUp() - { - Debug.Log($"[{nameof(DemoTest)}] SetUp"); - yield return null; - } - [UnityTearDown] - public IEnumerator TearDown() - { - Debug.Log($"[{nameof(DemoTest)}] TearDown"); - yield return null; - } - - // [UnityTest] - // public IEnumerator Always_Valid_Test() - // { - // var goName = "DemoGO"; - // var goRef = new Runtime.Data.GameObjectRef() { Name = goName }; - // var materialEx = new CreateMaterialExecutor( - // materialName: "TestMaterial__.mat", - // shaderName: "Standard", - // "Assets", "Unity-MCP-Test", "Materials" - // ); - - // materialEx - // .AddChild(new CreateGameObjectExecutor(goName)) - // .AddChild(new AddComponentExecutor(goRef)) - // .AddChild(new CallToolExecutor( - // toolMethod: typeof(Tool_GameObject).GetMethod(nameof(Tool_GameObject.Modify)), - // json: JsonTestUtils.Fill(@"{ - // ""gameObjectRefs"": ""{gameObjectRefs}"", - // ""gameObjectDiffs"": ""{gameObjectDiffs}"" - // }", - // new System.Collections.Generic.Dictionary - // { - // { "{gameObjectRefs}", new Runtime.Data.GameObjectRef[] { goRef } }, - // { "{gameObjectDiffs}", new SerializedMemberList() } - // })) - // ) - // .AddChild(new ValidateToolResultExecutor()) - // .Execute(); - // yield return null; - // } - } -} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta deleted file mode 100644 index 17aa90e1..00000000 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/MaterialReflectionConverterTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 69646e6142b6a204c8378c53c0ff5afb -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs index f3f910d7..001226ea 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs @@ -14,13 +14,12 @@ public CreateTextureExecutor(string assetName, params string[] folders) : base(a var texture = new Texture2D(64, 64); // Fill with some color - for (int x = 0; x < 64; x++) + var colors = new Color[64 * 64]; + for (int i = 0; i < colors.Length; i++) { - for (int y = 0; y < 64; y++) - { - texture.SetPixel(x, y, Color.red); - } + colors[i] = Color.red; } + texture.SetPixels(colors); texture.Apply(); var bytes = texture.EncodeToPNG(); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs index 6399d19b..3b2030eb 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs @@ -33,14 +33,15 @@ public DynamicCallToolExecutor(string toolName, Func jsonProvider, Refle Debug.Log($"{toolName} Started with JSON:\n{JsonTestUtils.Prettify(json)}"); var parameters = JsonSerializer.Deserialize>(json, reflector.JsonSerializerOptions); - var request = new RequestCallTool(toolName, parameters!); + using (var request = new RequestCallTool(toolName, parameters!)) + { + var task = McpPlugin.McpPlugin.Instance!.McpManager.ToolManager!.RunCallTool(request); + var result = task.Result; - var task = McpPlugin.McpPlugin.Instance!.McpManager.ToolManager!.RunCallTool(request); - var result = task.Result; + Debug.Log($"{toolName} Completed"); - Debug.Log($"{toolName} Completed"); - - return result; + return result; + } }); } } From bfd475575ce65811d918b5ed30a2e0b211a2188a Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 28 Nov 2025 04:18:57 -0800 Subject: [PATCH 08/13] Add CreateSpriteExecutor and update DataPopulationTests for sprite handling; include licensing information in executor files --- .../DataPopulationTests.cs | 11 +++- .../Utils/Executor/CreatePrefabExecutor.cs | 10 ++++ .../CreateScriptableObjectExecutor.cs | 11 ++++ .../Utils/Executor/CreateSpriteExecutor.cs | 51 +++++++++++++++++++ .../Executor/CreateSpriteExecutor.cs.meta | 11 ++++ .../Utils/Executor/CreateTextureExecutor.cs | 11 ++++ .../Utils/Executor/DynamicCallToolExecutor.cs | 11 ++++ 7 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs.meta diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs index 0291ab7b..d922a9d5 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs @@ -20,6 +20,7 @@ public IEnumerator Populate_All_Types_Test() // Executors for creating assets var materialEx = new CreateMaterialExecutor("TestMaterial.mat", "Standard", "Assets", "Unity-MCP-Test", "DataPopulation"); var textureEx = new CreateTextureExecutor("TestTexture.png", "Assets", "Unity-MCP-Test", "DataPopulation"); + var spriteEx = new CreateSpriteExecutor("TestSprite.png", "Assets", "Unity-MCP-Test", "DataPopulation"); var soEx = new CreateScriptableObjectExecutor("TestSO.asset", "Assets", "Unity-MCP-Test", "DataPopulation"); var prefabSourceGoEx = new CreateGameObjectExecutor("PrefabSource"); @@ -50,6 +51,9 @@ public IEnumerator Populate_All_Types_Test() Assert.IsNotNull(comp.textureField, "Texture should be populated"); Assert.AreEqual(textureEx.Asset!.name, comp.textureField.name); + Assert.IsNotNull(comp.spriteField, "Sprite should be populated"); + Assert.AreEqual(spriteEx.Sprite!.name, comp.spriteField.name); + Assert.IsNotNull(comp.scriptableObjectField, "SO should be populated"); Assert.AreEqual(soEx.Asset!.name, comp.scriptableObjectField.name); @@ -76,6 +80,7 @@ public IEnumerator Populate_All_Types_Test() var soRef = new AssetObjectRef() { AssetPath = soEx.AssetPath }; var prefabRef = new AssetObjectRef() { AssetPath = prefabEx.AssetPath }; var goRef = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var spriteRef = new AssetObjectRef() { AssetPath = spriteEx.AssetPath }; var goModification = SerializedMember.FromValue( reflector: reflector, @@ -94,14 +99,15 @@ public IEnumerator Populate_All_Types_Test() componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialField", type: typeof(Material), value: matRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectField", type: typeof(GameObject), value: goRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "textureField", type: typeof(Texture2D), value: texRef)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "spriteField", type: typeof(Sprite), value: spriteRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectField", type: typeof(DataPopulationTestScriptableObject), value: soRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "prefabField", type: typeof(GameObject), value: prefabRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "intField", type: typeof(int), value: 42)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "stringField", type: typeof(string), value: "Hello World")); - var matRefArrayItem = new ObjectRef(materialEx.Asset!.GetInstanceID()); + var matRefArrayItem = new AssetObjectRef(materialEx.AssetPath!); var goRefArrayItem = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); - var prefabRefArrayItem = new ObjectRef(prefabEx.Asset!.GetInstanceID()); + var prefabRefArrayItem = new AssetObjectRef(prefabEx.AssetPath!); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialArray", type: typeof(Material[]), value: new object[] { matRefArrayItem, matRefArrayItem })); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectArray", type: typeof(GameObject[]), value: new object[] { goRefArrayItem, prefabRefArrayItem })); @@ -135,6 +141,7 @@ public IEnumerator Populate_All_Types_Test() materialEx .Nest(textureEx) + .Nest(spriteEx) .Nest(soEx) .Nest(prefabSourceGoEx) .Nest(prefabEx) diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs index 37a413cd..fb4c4757 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreatePrefabExecutor.cs @@ -1,3 +1,13 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + #nullable enable using com.IvanMurzak.Unity.MCP.Runtime.Data; using com.IvanMurzak.Unity.MCP.Runtime.Extensions; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs index 5609225b..f12eead6 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateScriptableObjectExecutor.cs @@ -1,3 +1,14 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable using UnityEditor; using UnityEngine; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs new file mode 100644 index 00000000..8800eadf --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs @@ -0,0 +1,51 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable +using UnityEditor; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils +{ + public class CreateSpriteExecutor : CreateTextureExecutor + { + public Sprite Sprite { get; private set; } = null!; + + public CreateSpriteExecutor(string assetName, params string[] folders) : base(assetName, folders) + { + SetAction((texture) => + { + if (texture == null) throw new System.ArgumentNullException(nameof(texture)); + + Debug.Log($"Converting Texture to Sprite: {AssetPath}"); + + var importer = AssetImporter.GetAtPath(AssetPath) as TextureImporter; + if (importer != null) + { + importer.textureType = TextureImporterType.Sprite; + importer.SaveAndReimport(); + } + + Sprite = AssetDatabase.LoadAssetAtPath(AssetPath); + + if (Sprite == null) + { + Debug.LogError($"Failed to load created sprite at {AssetPath}"); + } + else + { + Debug.Log($"Created Sprite: {AssetPath}"); + } + + return Sprite; + }); + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs.meta new file mode 100644 index 00000000..2c9ad237 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 917fa846d1e55114baccdb53939ef047 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs index 001226ea..9be22da1 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs @@ -1,3 +1,14 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable using System.IO; using UnityEditor; using UnityEngine; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs index 3b2030eb..a6bd4eab 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/DynamicCallToolExecutor.cs @@ -1,3 +1,14 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable using System; using System.Collections.Generic; using System.Reflection; From feb7647a70911f82cc3814e8d1fd7fe0ebdbbc7b Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 29 Nov 2025 02:38:32 -0800 Subject: [PATCH 09/13] Fixed Unity objects reflection converters --- .../Base/UnityGenericReflectionConvertor.cs | 136 +++++++++++++++++ .../UnityEngine_Asset_ReflectionConvertor.cs | 139 ++++++++++++++++++ ...tyEngine_Asset_ReflectionConvertor.cs.meta | 11 ++ ...ne_GenericComponent_ReflectionConvertor.cs | 19 +++ ...nericComponent_ReflectionConvertor.cs.meta | 11 ++ .../UnityEngine_Object_ReflectionConvertor.cs | 31 +++- ...nityEngine_Renderer_ReflectionConvertor.cs | 2 +- ...ngine_Sprite_ReflectionConvertor.Editor.cs | 108 +++----------- ...gine_Sprite_ReflectionConvertor.Runtime.cs | 2 +- .../UnityEngine_Sprite_ReflectionConvertor.cs | 2 +- ...ityEngine_Transform_ReflectionConvertor.cs | 2 +- .../Scripts/DataPopulationTestScript.cs | 2 +- .../Assets/root/Tests/Editor/BaseTest.cs | 1 - .../DataPopulationTests.cs | 27 +++- .../root/Tests/Editor/SpriteConverterTest.cs | 135 +++++++++++++++++ .../Tests/Editor/SpriteConverterTest.cs.meta | 11 ++ .../Utils/Executor/CreateSpriteExecutor.cs | 1 + 17 files changed, 535 insertions(+), 105 deletions(-) create mode 100644 Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs.meta diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityGenericReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityGenericReflectionConvertor.cs index 11d06053..dc6b4462 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityGenericReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/Base/UnityGenericReflectionConvertor.cs @@ -13,8 +13,13 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Convertor; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.ReflectorNet.Utils; +using com.IvanMurzak.Unity.MCP.Runtime.Extensions; +using Microsoft.Extensions.Logging; using UnityEngine; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -26,5 +31,136 @@ public partial class UnityGenericReflectionConvertor : GenericReflectionConve => objType.GetFields(flags) .Where(field => field.GetCustomAttribute() == null) .Where(field => field.IsPublic || field.IsPrivate && field.GetCustomAttribute() != null); + + public override object? Deserialize( + Reflector reflector, + SerializedMember data, + Type? fallbackType = null, + string? fallbackName = null, + int depth = 0, + StringBuilder? stringBuilder = null, + ILogger? logger = null) + { + var type = fallbackType ?? typeof(T); + if (typeof(UnityEngine.Object).IsAssignableFrom(type)) + { + return data.valueJsonElement + .ToAssetObjectRef(reflector, suppressException: true, depth: depth, stringBuilder: stringBuilder, logger: logger) + .FindAssetObject(type); + } + return base.Deserialize(reflector, data, fallbackType, fallbackName, depth, stringBuilder, logger); + } + + protected override bool SetValue( + Reflector reflector, + ref object? obj, + Type type, + System.Text.Json.JsonElement? value, + int depth = 0, + StringBuilder? stringBuilder = null, + ILogger? logger = null) + { + var originalObj = obj; + var result = base.SetValue(reflector, ref obj, type, value, depth, stringBuilder, logger); + + // If obj became null but we had an object, and the value didn't explicitly say null, restore it. + // This handles cases where TryPopulate is called with an existing object but no valueJsonElement. + if (obj == null && originalObj != null) + { + var isExplicitNull = value.HasValue && value.Value.ValueKind == System.Text.Json.JsonValueKind.Null; + if (!isExplicitNull) + { + obj = originalObj; + } + } + + return result; + } + + protected override bool TryPopulateField( + Reflector reflector, + ref object? obj, + Type objType, + SerializedMember fieldValue, + int depth = 0, + StringBuilder? stringBuilder = null, + BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + ILogger? logger = null) + { + var padding = StringUtils.GetPadding(depth); + if (obj == null) + { + logger?.LogError("{padding}obj is null in TryPopulateField for {field}", padding, fieldValue.name); + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Error] obj is null in TryPopulateField for {fieldValue.name}"); + return false; + } + + var field = objType.GetField(fieldValue.name, flags); + if (field == null) + { + logger?.LogError("{padding}Field {field} not found on {type}", padding, fieldValue.name, objType.Name); + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Error] Field {fieldValue.name} not found on {objType.Name}"); + return false; + } + + try + { + var value = reflector.Deserialize(fieldValue, field.FieldType, depth: depth + 1, stringBuilder: stringBuilder, logger: logger); + field.SetValue(obj, value); + return true; + } + catch (Exception e) + { + logger?.LogError(e, "{padding}Failed to set field {field}", padding, fieldValue.name); + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Error] Failed to set field {fieldValue.name}: {e.Message}"); + return false; + } + } + + protected override bool TryPopulateProperty( + Reflector reflector, + ref object? obj, + Type objType, + SerializedMember member, + int depth = 0, + StringBuilder? stringBuilder = null, + BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + ILogger? logger = null) + { + var padding = StringUtils.GetPadding(depth); + if (obj == null) + { + logger?.LogError("{padding}obj is null in TryPopulateProperty for {property}", padding, member.name); + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Error] obj is null in TryPopulateProperty for {member.name}"); + return false; + } + + var property = objType.GetProperty(member.name, flags); + if (property == null || !property.CanWrite) + { + logger?.LogError("{padding}Property {property} not found or not writable on {type}", padding, member.name, objType.Name); + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Error] Property {member.name} not found or not writable on {objType.Name}"); + return false; + } + + try + { + var value = reflector.Deserialize(member, property.PropertyType, depth: depth + 1, stringBuilder: stringBuilder, logger: logger); + property.SetValue(obj, value); + return true; + } + catch (Exception e) + { + logger?.LogError(e, "{padding}Failed to set property {property}", padding, member.name); + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Error] Failed to set property {member.name}: {e.Message}"); + return false; + } + } } } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs new file mode 100644 index 00000000..e5f9c7d3 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs @@ -0,0 +1,139 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable +using System; +using System.Reflection; +using System.Text; +using com.IvanMurzak.ReflectorNet; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.ReflectorNet.Utils; +using com.IvanMurzak.Unity.MCP.Runtime.Extensions; +using Microsoft.Extensions.Logging; +using UnityEngine; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace com.IvanMurzak.Unity.MCP.Reflection.Convertor +{ + public class UnityEngine_Asset_ReflectionConvertor : UnityEngine_Object_ReflectionConvertor where T : UnityEngine.Object + { + public override bool TryPopulate( + Reflector reflector, + ref object? obj, + SerializedMember data, + Type? dataType = null, + int depth = 0, + StringBuilder? stringBuilder = null, + BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + ILogger? logger = null) + { + var padding = StringUtils.GetPadding(depth); + + if (logger?.IsEnabled(LogLevel.Trace) == true) + logger.LogTrace($"{padding}Populate asset from data. Convertor='{GetType().GetTypeShortName()}'."); + + var objectRef = data.valueJsonElement.ToAssetObjectRef( + reflector: reflector, + depth: depth, + stringBuilder: stringBuilder, + logger: logger); + + if (objectRef == null) + { + // If no object ref, maybe we should fall back to base behavior? + // But for assets, usually we expect an object ref. + // Let's return false to indicate we couldn't populate it as an asset. + return false; + } + +#if UNITY_EDITOR + var instanceID = objectRef.InstanceID; + if (instanceID != 0) + { + var loadedObj = LoadFromInstanceID(instanceID); + if (loadedObj != null) + { + obj = loadedObj; + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Success] Assigned asset from InstanceID: {instanceID}. Convertor: {GetType().GetTypeShortName()}"); + return true; + } + + if (logger?.IsEnabled(LogLevel.Warning) == true) + logger.LogWarning($"{padding}InstanceID {instanceID} found but failed to load asset. Convertor: {GetType().GetTypeShortName()}"); + } + + if (!string.IsNullOrEmpty(objectRef.AssetPath)) + { + var loadedObj = LoadFromAssetPath(objectRef.AssetPath); + if (loadedObj != null) + { + obj = loadedObj; + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Success] Assigned asset from AssetPath: {objectRef.AssetPath}. Convertor: {GetType().GetTypeShortName()}"); + return true; + } + + if (logger?.IsEnabled(LogLevel.Warning) == true) + logger.LogWarning($"{padding}AssetPath {objectRef.AssetPath} found but failed to load asset. Convertor: {GetType().GetTypeShortName()}"); + } + + if (!string.IsNullOrEmpty(objectRef.AssetGuid)) + { + var loadedObj = LoadFromAssetGuid(objectRef.AssetGuid); + if (loadedObj != null) + { + obj = loadedObj; + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Success] Assigned asset from AssetGuid: {objectRef.AssetGuid}. Convertor: {GetType().GetTypeShortName()}"); + return true; + } + + if (logger?.IsEnabled(LogLevel.Warning) == true) + logger.LogWarning($"{padding}AssetGuid {objectRef.AssetGuid} found but failed to load asset. Convertor: {GetType().GetTypeShortName()}"); + } +#endif + + // If we reached here, we failed to find the asset. + // Should we set obj to null? The Sprite convertor does. + obj = null; + + if (logger?.IsEnabled(LogLevel.Trace) == true) + logger.LogTrace($"{padding}[Success] Failed to find asset. Cleared the reference. Convertor: {GetType().GetTypeShortName()}"); + + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Success] Failed to find asset. Cleared the reference. Convertor: {GetType().GetTypeShortName()}"); + + return true; + } + +#if UNITY_EDITOR + protected virtual T? LoadFromInstanceID(int instanceID) + { + var obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceID); + return obj as T; + } + + protected virtual T? LoadFromAssetPath(string path) + { + return UnityEditor.AssetDatabase.LoadAssetAtPath(path); + } + + protected virtual T? LoadFromAssetGuid(string guid) + { + var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrEmpty(path)) + return null; + return LoadFromAssetPath(path); + } +#endif + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs.meta b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs.meta new file mode 100644 index 00000000..5cb68ae5 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Asset_ReflectionConvertor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27ed2cf4e88a15c4ea3a3746e890f24d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs new file mode 100644 index 00000000..052f7914 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs @@ -0,0 +1,19 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable + +namespace com.IvanMurzak.Unity.MCP.Reflection.Convertor +{ + public abstract class UnityEngine_GenericComponent_ReflectionConvertor : UnityEngine_Object_ReflectionConvertor where T : UnityEngine.Component + { + public override bool AllowSetValue => false; + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs.meta b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs.meta new file mode 100644 index 00000000..ce593f65 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_GenericComponent_ReflectionConvertor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e39e582c3df85ef44a85258004d3ee3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Object_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Object_ReflectionConvertor.cs index 7f84db2d..ed859d70 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Object_ReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Object_ReflectionConvertor.cs @@ -22,7 +22,6 @@ using com.IvanMurzak.ReflectorNet.Utils; using com.IvanMurzak.Unity.MCP.Runtime.Data; using com.IvanMurzak.Unity.MCP.Runtime.Extensions; -using com.IvanMurzak.Unity.MCP.Utils; using Microsoft.Extensions.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; using LogLevel = Microsoft.Extensions.Logging.LogLevel; @@ -33,7 +32,7 @@ public class UnityEngine_Object_ReflectionConvertor : UnityEngine_Object_Reflect public partial class UnityEngine_Object_ReflectionConvertor : UnityGenericReflectionConvertor where T : UnityEngine.Object { public override bool AllowCascadePropertiesConversion => false; - public override bool AllowSetValue => false; + public override bool AllowSetValue => true; protected virtual IEnumerable RestrictedInValuePropertyNames(Reflector reflector, JsonElement valueJsonElement) => new[] { @@ -105,6 +104,9 @@ public override bool TryPopulate( BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, ILogger? logger = null) { + if (stringBuilder != null) + stringBuilder.AppendLine($"{StringUtils.GetPadding(depth)}[Info] TryPopulate called for type '{obj?.GetType().Name}'."); + // Trying to fix JSON value body, if critical property is missed or detected return false if (!FixJsonValueBody( reflector: reflector, @@ -250,19 +252,28 @@ protected override bool SetValue( { var padding = StringUtils.GetPadding(depth); + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Info] SetValue called for type '{type.Name}'. Value kind: {value?.ValueKind}"); + if (logger?.IsEnabled(LogLevel.Trace) == true) logger.LogTrace($"{padding}Set value type='{type.GetTypeName(pretty: true)}'. Convertor='{GetType().GetTypeShortName()}'."); try { - obj = value + var assetObj = value .ToAssetObjectRef( reflector: reflector, suppressException: false, depth: depth, stringBuilder: stringBuilder, logger: logger) - .FindAssetObject(); + .FindAssetObject(type); + + obj = assetObj; + + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Info] SetValue success. Obj is null? {obj == null}"); + return true; } catch (Exception ex) @@ -286,13 +297,21 @@ protected override bool SetValue( StringBuilder? stringBuilder = null, ILogger? logger = null) { + var targetType = fallbackType ?? typeof(T); + var padding = StringUtils.GetPadding(depth); + if (logger?.IsEnabled(LogLevel.Information) == true) + logger.LogInformation($"{padding}[UnityEngine_Object_ReflectionConvertor] Deserialize called for {targetType.Name}. Convertor: {GetType().Name}"); + + if (stringBuilder != null) + stringBuilder.AppendLine($"{padding}[Info] Deserialize called for {targetType.Name}. Convertor: {GetType().Name}"); + return data.valueJsonElement .ToAssetObjectRef( reflector: reflector, depth: depth, stringBuilder: stringBuilder, logger: logger) - .FindAssetObject(); + .FindAssetObject(targetType); } protected override object? DeserializeValueAsJsonElement( @@ -309,7 +328,7 @@ protected override bool SetValue( depth: depth, stringBuilder: stringBuilder, logger: logger) - .FindAssetObject(); + .FindAssetObject(type); } } } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Renderer_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Renderer_ReflectionConvertor.cs index 40b8d423..e0713b6b 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Renderer_ReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Renderer_ReflectionConvertor.cs @@ -13,7 +13,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Convertor { - public partial class UnityEngine_Renderer_ReflectionConvertor : UnityEngine_Object_ReflectionConvertor + public partial class UnityEngine_Renderer_ReflectionConvertor : UnityEngine_GenericComponent_ReflectionConvertor { protected override IEnumerable GetIgnoredProperties() { diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Editor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Editor.cs index 3a5c70c8..ec0e6881 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Editor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Editor.cs @@ -10,113 +10,41 @@ #nullable enable #if UNITY_EDITOR -using System; using System.Linq; -using System.Reflection; -using System.Text; -using com.IvanMurzak.ReflectorNet; -using com.IvanMurzak.ReflectorNet.Model; -using com.IvanMurzak.ReflectorNet.Utils; -using com.IvanMurzak.Unity.MCP.Runtime.Extensions; -using Microsoft.Extensions.Logging; using UnityEditor; -using ILogger = Microsoft.Extensions.Logging.ILogger; -using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using UnityEngine; namespace com.IvanMurzak.Unity.MCP.Reflection.Convertor { - public partial class UnityEngine_Sprite_ReflectionConvertor : UnityEngine_Object_ReflectionConvertor + public partial class UnityEngine_Sprite_ReflectionConvertor : UnityEngine_Asset_ReflectionConvertor { - public override bool TryPopulate( - Reflector reflector, - ref object? obj, - SerializedMember data, - Type? dataType = null, - int depth = 0, - StringBuilder? stringBuilder = null, - BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - ILogger? logger = null) + protected override UnityEngine.Sprite? LoadFromInstanceID(int instanceID) { - var padding = StringUtils.GetPadding(depth); - - if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{StringUtils.GetPadding(depth)}Populate sprite from data. Convertor='{GetType().GetTypeShortName()}'."); - - if (!data.TryGetInstanceID(out var instanceID)) - { - if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError($"{padding}InstanceID not found. Set 'instanceID` as 0 if you want to set it to null. Convertor: {GetType().GetTypeShortName()}"); - - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Error] InstanceID not found. Set 'instanceID` as 0 if you want to set it to null. Convertor: {GetType().GetTypeShortName()}"); - - return false; - } - if (instanceID == 0) - { - obj = null; - - if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{padding}[Success] InstanceID is 0. Cleared the reference. Convertor: {GetType().GetTypeShortName()}"); - - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Success] InstanceID is 0. Cleared the reference. Convertor: {GetType().GetTypeShortName()}"); - - return true; - } var textureOrSprite = EditorUtility.InstanceIDToObject(instanceID); - if (textureOrSprite == null) - { - if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError($"{padding}InstanceID {instanceID} not found. Convertor: {GetType().GetTypeShortName()}"); - - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Error] InstanceID {instanceID} not found. Convertor: {GetType().GetTypeShortName()}"); + if (textureOrSprite == null) return null; - return false; - } - - if (textureOrSprite is UnityEngine.Texture2D texture) + if (textureOrSprite is Texture2D texture) { var path = AssetDatabase.GetAssetPath(texture); var sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(path) - .OfType() + .OfType() .ToArray(); - if (sprites.Length == 0) - { - if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError($"{padding}No sprites found for texture at path: {path}. Convertor: {GetType().GetTypeShortName()}"); - - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Error] No sprites found for texture at path: {path}. Convertor: {GetType().GetTypeShortName()}"); - - return false; - } - - obj = sprites[0]; // Assign the first sprite found - - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Success] Assigned sprite from texture: {path}. Convertor: {GetType().GetTypeShortName()}"); - - return true; + return sprites.FirstOrDefault(); } - if (textureOrSprite is UnityEngine.Sprite sprite) + if (textureOrSprite is Sprite sprite) { - obj = sprite; - - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Success] Assigned sprite: {sprite.name}. Convertor: {GetType().GetTypeShortName()}"); - - return true; + return sprite; } + return null; + } - if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError($"{padding}InstanceID {instanceID} is not a Texture2D or Sprite. Convertor: {GetType().GetTypeShortName()}"); - - if (stringBuilder != null) - stringBuilder.AppendLine($"{padding}[Error] InstanceID {instanceID} is not a Texture2D or Sprite. Convertor: {GetType().GetTypeShortName()}"); - - return false; + protected override UnityEngine.Sprite? LoadFromAssetPath(string path) + { + var allAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(path); + var sprites = allAssets + .OfType() + .ToArray(); + return sprites.FirstOrDefault(); } } } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Runtime.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Runtime.cs index 886bb65e..fe74309a 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Runtime.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.Runtime.cs @@ -22,7 +22,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Convertor { - public partial class UnityEngine_Sprite_ReflectionConvertor : UnityEngine_Object_ReflectionConvertor + public partial class UnityEngine_Sprite_ReflectionConvertor : UnityEngine_Asset_ReflectionConvertor { public override bool TryPopulate( Reflector reflector, diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.cs index 3c010587..0ddc929f 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Sprite_ReflectionConvertor.cs @@ -19,7 +19,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Convertor { - public partial class UnityEngine_Sprite_ReflectionConvertor : UnityEngine_Object_ReflectionConvertor + public partial class UnityEngine_Sprite_ReflectionConvertor : UnityEngine_Asset_ReflectionConvertor { public override bool AllowCascadeSerialization => false; diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Transform_ReflectionConvertor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Transform_ReflectionConvertor.cs index 86ec3317..a279bf5c 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Transform_ReflectionConvertor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/ReflectionConverters/UnityEngine_Transform_ReflectionConvertor.cs @@ -12,7 +12,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Convertor { - public partial class UnityEngine_Transform_ReflectionConvertor : UnityEngine_Object_ReflectionConvertor + public partial class UnityEngine_Transform_ReflectionConvertor : UnityEngine_GenericComponent_ReflectionConvertor { // public override bool AllowCascadeSerialize => false; // public override bool AllowCascadePopulate => false; diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs index 6ddf7faa..d5624c0b 100644 --- a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs @@ -4,10 +4,10 @@ namespace com.IvanMurzak.Unity.MCP.TestFiles { public class DataPopulationTestScript : MonoBehaviour { + public Sprite spriteField; public Material materialField; public GameObject gameObjectField; public Texture2D textureField; - public Sprite spriteField; public ScriptableObject scriptableObjectField; public GameObject prefabField; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/BaseTest.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/BaseTest.cs index 2cdfcefc..8c78872d 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/BaseTest.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/BaseTest.cs @@ -12,7 +12,6 @@ using System.Collections; using System.Collections.Generic; using System.Text.Json; -using com.IvanMurzak.McpPlugin.Common; using com.IvanMurzak.McpPlugin.Common.Model; using com.IvanMurzak.ReflectorNet; using NUnit.Framework; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs index d922a9d5..da942ba5 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs @@ -4,6 +4,7 @@ using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.Unity.MCP.Editor.API; using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; +using com.IvanMurzak.Unity.MCP.Reflection.Convertor; using com.IvanMurzak.Unity.MCP.Runtime.Data; using com.IvanMurzak.Unity.MCP.TestFiles; using NUnit.Framework; @@ -12,11 +13,18 @@ namespace com.IvanMurzak.Unity.MCP.Editor.Tests { - public class DataPopulationTests + public class DataPopulationTests : BaseTest { + [UnitySetUp] + public override IEnumerator SetUp() => base.SetUp(); + + [UnityTearDown] + public override IEnumerator TearDown() => base.TearDown(); + [UnityTest] public IEnumerator Populate_All_Types_Test() { + Debug.Log("[DataPopulationTests] Running updated test version."); // Executors for creating assets var materialEx = new CreateMaterialExecutor("TestMaterial.mat", "Standard", "Assets", "Unity-MCP-Test", "DataPopulation"); var textureEx = new CreateTextureExecutor("TestTexture.png", "Assets", "Unity-MCP-Test", "DataPopulation"); @@ -73,7 +81,20 @@ public IEnumerator Populate_All_Types_Test() typeof(Tool_GameObject).GetMethod(nameof(Tool_GameObject.Modify)), () => { - var reflector = McpPlugin.McpPlugin.Instance!.McpManager.Reflector; + var plugin = UnityMcpPlugin.Instance; + Debug.Log($"[DataPopulationTests] Plugin: {plugin}"); + var mcpInstance = plugin?.McpPluginInstance; + Debug.Log($"[DataPopulationTests] McpInstance: {mcpInstance}"); + var manager = mcpInstance?.McpManager; + Debug.Log($"[DataPopulationTests] Manager: {manager}"); + var reflector = manager?.Reflector; + Debug.Log($"[DataPopulationTests] Reflector: {reflector}"); + + if (reflector == null) + { + Debug.LogError("[DataPopulationTests] Reflector is null! Cannot proceed with serialization."); + return "{}"; + } var matRef = new AssetObjectRef() { AssetPath = materialEx.AssetPath }; var texRef = new AssetObjectRef() { AssetPath = textureEx.AssetPath }; @@ -96,10 +117,10 @@ public IEnumerator Populate_All_Types_Test() value: new ComponentRef(addCompEx.Component!.GetInstanceID()) ); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "spriteField", type: typeof(Sprite), value: spriteRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialField", type: typeof(Material), value: matRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectField", type: typeof(GameObject), value: goRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "textureField", type: typeof(Texture2D), value: texRef)); - componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "spriteField", type: typeof(Sprite), value: spriteRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectField", type: typeof(DataPopulationTestScriptableObject), value: soRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "prefabField", type: typeof(GameObject), value: prefabRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "intField", type: typeof(int), value: 42)); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs new file mode 100644 index 00000000..b550cf5c --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs @@ -0,0 +1,135 @@ +/* +┌──────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the Apache License, Version 2.0. │ +│ See the LICENSE file in the project root for more information. │ +└──────────────────────────────────────────────────────────────────┘ +*/ + +#nullable enable +using System.IO; +using com.IvanMurzak.McpPlugin.Common.Reflection.Convertor; +using com.IvanMurzak.ReflectorNet; +using com.IvanMurzak.ReflectorNet.Convertor; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.Unity.MCP.Reflection.Convertor; +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests +{ + [TestFixture] + public class SpriteConverterTest + { + private string _testFolder = "Assets/SpriteConverterTest"; + private string? _texturePath; + private string? _spritePath; + + [SetUp] + public void SetUp() + { + if (Directory.Exists(_testFolder)) + Directory.Delete(_testFolder, true); + + AssetDatabase.CreateFolder("Assets", "SpriteConverterTest"); + _texturePath = $"{_testFolder}/TestTexture.png"; + + // Create a simple texture + var texture = new Texture2D(64, 64); + var bytes = texture.EncodeToPNG(); + File.WriteAllBytes(_texturePath, bytes); + AssetDatabase.Refresh(); + + // Import as Sprite + var importer = AssetImporter.GetAtPath(_texturePath) as TextureImporter; + if (importer != null) + { + importer.textureType = TextureImporterType.Sprite; + importer.SaveAndReimport(); + } + + _spritePath = _texturePath; // Sprite is inside the texture asset + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_testFolder)) + { + AssetDatabase.DeleteAsset(_testFolder); + } + } + + [Test] + public void TestSpritePopulation() + { + var reflector = new Reflector(); + + // Match UnityMcpPlugin.CreateDefaultReflector + reflector.Convertors.Remove>(); + reflector.Convertors.Add(new UnityGenericReflectionConvertor()); + + // Register converters in the order they are in UnityMcpPlugin.Converters.cs + // Assets + reflector.Convertors.Add(new UnityEngine_Material_ReflectionConvertor()); + reflector.Convertors.Add(new UnityEngine_Sprite_ReflectionConvertor()); + + // Fallback + reflector.Convertors.Add(new UnityEngine_Object_ReflectionConvertor()); + + // Create a dummy object to populate + var container = new SpriteContainer(); + + // Create SerializedMember for the sprite field + // We use AssetObjectRef pointing to the texture path + var assetRef = new AssetObjectRef() { AssetPath = _spritePath }; + + // Manually serialize AssetObjectRef to JsonElement to ensure valueJsonElement is populated + // This mimics how data comes from the wire (JSON) + var json = System.Text.Json.JsonSerializer.Serialize(assetRef, reflector.JsonSerializerOptions); + var jsonElement = System.Text.Json.JsonSerializer.Deserialize(json, reflector.JsonSerializerOptions); + + var spriteMember = new SerializedMember + { + name = "spriteField", + typeName = typeof(Sprite).AssemblyQualifiedName, + valueJsonElement = jsonElement + }; + + var spritePropertyMember = new SerializedMember + { + name = "spriteProperty", + typeName = typeof(Sprite).AssemblyQualifiedName, + valueJsonElement = jsonElement + }; + + // Try to populate + object? obj = container; + bool result = false; + + result = reflector.TryPopulate(ref obj, new SerializedMember + { + typeName = typeof(SpriteContainer).AssemblyQualifiedName, + fields = new SerializedMemberList { spriteMember }, + props = new SerializedMemberList { spritePropertyMember } + }); + + // Assert.IsTrue(result, "Population should succeed"); + Assert.IsNotNull((obj as SpriteContainer)?.spriteField, "Sprite field should be populated"); + Assert.AreEqual("TestTexture", (obj as SpriteContainer)?.spriteField?.name); + + Assert.IsNotNull((obj as SpriteContainer)?.spriteProperty, "Sprite property should be populated"); + Assert.AreEqual("TestTexture", (obj as SpriteContainer)?.spriteProperty?.name); + } + + public class SpriteContainer + { + public Sprite? spriteField; + public Sprite? spriteProperty { get; set; } + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs.meta new file mode 100644 index 00000000..404a4472 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11d526570415b134fbce241c55cd1515 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs index 8800eadf..a2fc8d03 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs @@ -38,6 +38,7 @@ public CreateSpriteExecutor(string assetName, params string[] folders) : base(as if (Sprite == null) { Debug.LogError($"Failed to load created sprite at {AssetPath}"); + throw new System.Exception($"Failed to load created sprite at {AssetPath}"); } else { From e3617232a240efa5b896024aedf405ff43cc2f4c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 29 Nov 2025 02:54:19 -0800 Subject: [PATCH 10/13] Enhance asset object retrieval methods and update test executors for improved functionality and clarity --- .../ExtensionsRuntimeAssetObjectRef.cs | 80 ++++++++- .../ExtensionsRuntimeGameObjectRef.cs | 1 + .../DataPopulationTests.cs | 4 +- .../root/Tests/Editor/SpriteConverterTest.cs | 155 +++++++----------- .../Utils/Executor/CreateSpriteExecutor.cs | 6 +- .../Utils/Executor/CreateTextureExecutor.cs | 10 +- 6 files changed, 148 insertions(+), 108 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeAssetObjectRef.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeAssetObjectRef.cs index 20861f3e..db9bff3a 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeAssetObjectRef.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeAssetObjectRef.cs @@ -16,22 +16,96 @@ namespace com.IvanMurzak.Unity.MCP.Runtime.Extensions public static class ExtensionsRuntimeAssetObjectRef { public static UnityEngine.Object? FindAssetObject(this AssetObjectRef? assetObjectRef) + { + return FindAssetObject(assetObjectRef); + } + + public static UnityEngine.Object? FindAssetObject(this AssetObjectRef? assetObjectRef, System.Type type) + { + if (assetObjectRef == null) + return null; + + if (type == null) + throw new System.ArgumentNullException(nameof(type)); + +#if UNITY_EDITOR + if (assetObjectRef.InstanceID != 0) + { + var obj = UnityEditor.EditorUtility.InstanceIDToObject(assetObjectRef.InstanceID); + if (obj != null && type.IsAssignableFrom(obj.GetType())) + return obj; + return null; + } + + if (!string.IsNullOrEmpty(assetObjectRef.AssetPath)) + { + var result = UnityEditor.AssetDatabase.LoadAssetAtPath(assetObjectRef.AssetPath, type); + if (result == null) + { + // Fallback: Try loading all assets and finding the one of the correct type + var allAssets = UnityEditor.AssetDatabase.LoadAllAssetsAtPath(assetObjectRef.AssetPath); + foreach (var asset in allAssets) + { + if (asset != null) + { + if (type.IsAssignableFrom(asset.GetType())) + { + result = asset; + break; + } + } + } + } + return result; + } + + if (!string.IsNullOrEmpty(assetObjectRef.AssetGuid)) + { + var path = UnityEditor.AssetDatabase.GUIDToAssetPath(assetObjectRef.AssetGuid); + if (!string.IsNullOrEmpty(path)) + return UnityEditor.AssetDatabase.LoadAssetAtPath(path, type); + } +#endif + + return null; + } + + public static T? FindAssetObject(this AssetObjectRef? assetObjectRef) where T : UnityEngine.Object { if (assetObjectRef == null) return null; #if UNITY_EDITOR if (assetObjectRef.InstanceID != 0) - return UnityEditor.EditorUtility.InstanceIDToObject(assetObjectRef.InstanceID); + { + var obj = UnityEditor.EditorUtility.InstanceIDToObject(assetObjectRef.InstanceID); + return obj as T; + } if (!string.IsNullOrEmpty(assetObjectRef.AssetPath)) - return UnityEditor.AssetDatabase.LoadAssetAtPath(assetObjectRef.AssetPath); + { + var result = UnityEditor.AssetDatabase.LoadAssetAtPath(assetObjectRef.AssetPath); + if (result == null) + { + // Fallback: Try loading all assets and finding the one of the correct type + var allAssets = UnityEditor.AssetDatabase.LoadAllAssetsAtPath(assetObjectRef.AssetPath); + foreach (var asset in allAssets) + { + if (asset is T typedAsset) + { + result = typedAsset; + break; + } + } + } + return result; + } if (!string.IsNullOrEmpty(assetObjectRef.AssetGuid)) { var path = UnityEditor.AssetDatabase.GUIDToAssetPath(assetObjectRef.AssetGuid); if (!string.IsNullOrEmpty(path)) - return UnityEditor.AssetDatabase.LoadAssetAtPath(path); + return UnityEditor.AssetDatabase.LoadAssetAtPath(path); } #endif diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeGameObjectRef.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeGameObjectRef.cs index 32bc6aac..3ed589e7 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeGameObjectRef.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Extensions/ExtensionsRuntimeGameObjectRef.cs @@ -19,6 +19,7 @@ public static class ExtensionsRuntimeGameObjectRef { public static GameObject? FindGameObject(this GameObjectRef? objectRef) => FindGameObject(objectRef, out _); + public static GameObject? FindGameObject(this GameObjectRef? objectRef, out string? error) { if (objectRef == null) diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs index da942ba5..096a97b2 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs @@ -27,8 +27,8 @@ public IEnumerator Populate_All_Types_Test() Debug.Log("[DataPopulationTests] Running updated test version."); // Executors for creating assets var materialEx = new CreateMaterialExecutor("TestMaterial.mat", "Standard", "Assets", "Unity-MCP-Test", "DataPopulation"); - var textureEx = new CreateTextureExecutor("TestTexture.png", "Assets", "Unity-MCP-Test", "DataPopulation"); - var spriteEx = new CreateSpriteExecutor("TestSprite.png", "Assets", "Unity-MCP-Test", "DataPopulation"); + var textureEx = new CreateTextureExecutor("TestTexture.png", Color.magenta, 64, 64, "Assets", "Unity-MCP-Test", "DataPopulation"); + var spriteEx = new CreateSpriteExecutor("TestSprite.png", Color.green, 64, 64, "Assets", "Unity-MCP-Test", "DataPopulation"); var soEx = new CreateScriptableObjectExecutor("TestSO.asset", "Assets", "Unity-MCP-Test", "DataPopulation"); var prefabSourceGoEx = new CreateGameObjectExecutor("PrefabSource"); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs index b550cf5c..16233a3b 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs @@ -9,15 +9,14 @@ */ #nullable enable -using System.IO; using com.IvanMurzak.McpPlugin.Common.Reflection.Convertor; using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Convertor; using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; using com.IvanMurzak.Unity.MCP.Reflection.Convertor; using com.IvanMurzak.Unity.MCP.Runtime.Data; using NUnit.Framework; -using UnityEditor; using UnityEngine; namespace com.IvanMurzak.Unity.MCP.Editor.Tests @@ -25,105 +24,73 @@ namespace com.IvanMurzak.Unity.MCP.Editor.Tests [TestFixture] public class SpriteConverterTest { - private string _testFolder = "Assets/SpriteConverterTest"; - private string? _texturePath; - private string? _spritePath; - - [SetUp] - public void SetUp() - { - if (Directory.Exists(_testFolder)) - Directory.Delete(_testFolder, true); - - AssetDatabase.CreateFolder("Assets", "SpriteConverterTest"); - _texturePath = $"{_testFolder}/TestTexture.png"; - - // Create a simple texture - var texture = new Texture2D(64, 64); - var bytes = texture.EncodeToPNG(); - File.WriteAllBytes(_texturePath, bytes); - AssetDatabase.Refresh(); - - // Import as Sprite - var importer = AssetImporter.GetAtPath(_texturePath) as TextureImporter; - if (importer != null) - { - importer.textureType = TextureImporterType.Sprite; - importer.SaveAndReimport(); - } - - _spritePath = _texturePath; // Sprite is inside the texture asset - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_testFolder)) - { - AssetDatabase.DeleteAsset(_testFolder); - } - } - [Test] public void TestSpritePopulation() { - var reflector = new Reflector(); + var spriteEx = new CreateSpriteExecutor("TestTexture.png", Color.red, 64, 64, "Assets", "SpriteConverterTest"); - // Match UnityMcpPlugin.CreateDefaultReflector - reflector.Convertors.Remove>(); - reflector.Convertors.Add(new UnityGenericReflectionConvertor()); - - // Register converters in the order they are in UnityMcpPlugin.Converters.cs - // Assets - reflector.Convertors.Add(new UnityEngine_Material_ReflectionConvertor()); - reflector.Convertors.Add(new UnityEngine_Sprite_ReflectionConvertor()); - - // Fallback - reflector.Convertors.Add(new UnityEngine_Object_ReflectionConvertor()); - - // Create a dummy object to populate - var container = new SpriteContainer(); - - // Create SerializedMember for the sprite field - // We use AssetObjectRef pointing to the texture path - var assetRef = new AssetObjectRef() { AssetPath = _spritePath }; - - // Manually serialize AssetObjectRef to JsonElement to ensure valueJsonElement is populated - // This mimics how data comes from the wire (JSON) - var json = System.Text.Json.JsonSerializer.Serialize(assetRef, reflector.JsonSerializerOptions); - var jsonElement = System.Text.Json.JsonSerializer.Deserialize(json, reflector.JsonSerializerOptions); - - var spriteMember = new SerializedMember + spriteEx.AddChild(() => { - name = "spriteField", - typeName = typeof(Sprite).AssemblyQualifiedName, - valueJsonElement = jsonElement - }; - - var spritePropertyMember = new SerializedMember - { - name = "spriteProperty", - typeName = typeof(Sprite).AssemblyQualifiedName, - valueJsonElement = jsonElement - }; - - // Try to populate - object? obj = container; - bool result = false; - - result = reflector.TryPopulate(ref obj, new SerializedMember - { - typeName = typeof(SpriteContainer).AssemblyQualifiedName, - fields = new SerializedMemberList { spriteMember }, - props = new SerializedMemberList { spritePropertyMember } + var reflector = new Reflector(); + + // Match UnityMcpPlugin.CreateDefaultReflector + reflector.Convertors.Remove>(); + reflector.Convertors.Add(new UnityGenericReflectionConvertor()); + + // Register converters in the order they are in UnityMcpPlugin.Converters.cs + // Assets + reflector.Convertors.Add(new UnityEngine_Material_ReflectionConvertor()); + reflector.Convertors.Add(new UnityEngine_Sprite_ReflectionConvertor()); + + // Fallback + reflector.Convertors.Add(new UnityEngine_Object_ReflectionConvertor()); + + // Create a dummy object to populate + var container = new SpriteContainer(); + + // Create SerializedMember for the sprite field + // We use AssetObjectRef pointing to the texture path + var assetRef = new AssetObjectRef() { AssetPath = spriteEx.AssetPath }; + + // Manually serialize AssetObjectRef to JsonElement to ensure valueJsonElement is populated + // This mimics how data comes from the wire (JSON) + var json = System.Text.Json.JsonSerializer.Serialize(assetRef, reflector.JsonSerializerOptions); + var jsonElement = System.Text.Json.JsonSerializer.Deserialize(json, reflector.JsonSerializerOptions); + + var spriteMember = new SerializedMember + { + name = "spriteField", + typeName = typeof(Sprite).AssemblyQualifiedName, + valueJsonElement = jsonElement + }; + + var spritePropertyMember = new SerializedMember + { + name = "spriteProperty", + typeName = typeof(Sprite).AssemblyQualifiedName, + valueJsonElement = jsonElement + }; + + // Try to populate + object? obj = container; + bool result = false; + + result = reflector.TryPopulate(ref obj, new SerializedMember + { + typeName = typeof(SpriteContainer).AssemblyQualifiedName, + fields = new SerializedMemberList { spriteMember }, + props = new SerializedMemberList { spritePropertyMember } + }); + + // Assert.IsTrue(result, "Population should succeed"); + Assert.IsNotNull((obj as SpriteContainer)?.spriteField, "Sprite field should be populated"); + Assert.AreEqual("TestTexture", (obj as SpriteContainer)?.spriteField?.name); + + Assert.IsNotNull((obj as SpriteContainer)?.spriteProperty, "Sprite property should be populated"); + Assert.AreEqual("TestTexture", (obj as SpriteContainer)?.spriteProperty?.name); }); - // Assert.IsTrue(result, "Population should succeed"); - Assert.IsNotNull((obj as SpriteContainer)?.spriteField, "Sprite field should be populated"); - Assert.AreEqual("TestTexture", (obj as SpriteContainer)?.spriteField?.name); - - Assert.IsNotNull((obj as SpriteContainer)?.spriteProperty, "Sprite property should be populated"); - Assert.AreEqual("TestTexture", (obj as SpriteContainer)?.spriteProperty?.name); + spriteEx.Execute(); } public class SpriteContainer diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs index a2fc8d03..7cf08149 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs @@ -18,12 +18,10 @@ public class CreateSpriteExecutor : CreateTextureExecutor { public Sprite Sprite { get; private set; } = null!; - public CreateSpriteExecutor(string assetName, params string[] folders) : base(assetName, folders) + public CreateSpriteExecutor(string assetName, Color color, int width = 64, int height = 64, params string[] folders) : base(assetName, color, width, height, folders) { - SetAction((texture) => + SetAction(() => { - if (texture == null) throw new System.ArgumentNullException(nameof(texture)); - Debug.Log($"Converting Texture to Sprite: {AssetPath}"); var importer = AssetImporter.GetAtPath(AssetPath) as TextureImporter; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs index 9be22da1..75a6132f 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs @@ -17,18 +17,18 @@ namespace com.IvanMurzak.Unity.MCP.Editor.Tests.Utils { public class CreateTextureExecutor : BaseCreateAssetExecutor { - public CreateTextureExecutor(string assetName, params string[] folders) : base(assetName, folders) + public CreateTextureExecutor(string assetName, Color color, int width = 64, int height = 64, params string[] folders) : base(assetName, folders) { SetAction(() => { - Debug.Log($"Creating Texture: {AssetPath}"); + Debug.Log($"Creating Texture: {AssetPath} ({width}x{height}) with color {color}"); - var texture = new Texture2D(64, 64); + var texture = new Texture2D(width, height); // Fill with some color - var colors = new Color[64 * 64]; + var colors = new Color[width * height]; for (int i = 0; i < colors.Length; i++) { - colors[i] = Color.red; + colors[i] = color; } texture.SetPixels(colors); texture.Apply(); From 515580e7a41ebf8285a28bda26531fcc88f68f81 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 29 Nov 2025 03:07:32 -0800 Subject: [PATCH 11/13] Add data field and property population test scripts - Created DataFieldPopulationTestScript and DataFieldPopulationTestScriptableObject classes to define various data fields for testing. - Implemented DataPropertyPopulationTestScript and DataPropertyPopulationTestScriptableObject classes to utilize properties instead of fields. - Added corresponding test scripts: DataFieldScriptableObjectPopulationTests and DataPropertyScriptableObjectPopulationTests to validate the population of fields and properties. - Updated existing tests to reference the new classes and ensure proper population of data. - Removed obsolete DataPopulationTestScriptableObject class to streamline the codebase. --- ...pt.cs => DataFieldPopulationTestScript.cs} | 6 +- ... => DataFieldPopulationTestScript.cs.meta} | 2 +- ...DataFieldPopulationTestScriptableObject.cs | 24 +++ ...eldPopulationTestScriptableObject.cs.meta} | 2 +- .../DataPopulationTestScriptableObject.cs | 10 - .../DataPropertyPopulationTestScript.cs | 24 +++ .../DataPropertyPopulationTestScript.cs.meta | 11 ++ ...aPropertyPopulationTestScriptableObject.cs | 24 +++ ...ertyPopulationTestScriptableObject.cs.meta | 11 ++ ...ataFieldScriptableObjectPopulationTests.cs | 169 ++++++++++++++++ ...eldScriptableObjectPopulationTests.cs.meta | 11 ++ .../DataPopulationTests.cs | 20 +- .../DataPropertyPopulationTests.cs | 180 ++++++++++++++++++ .../DataPropertyPopulationTests.cs.meta | 11 ++ ...PropertyScriptableObjectPopulationTests.cs | 169 ++++++++++++++++ ...rtyScriptableObjectPopulationTests.cs.meta | 11 ++ 16 files changed, 667 insertions(+), 18 deletions(-) rename Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/{DataPopulationTestScript.cs => DataFieldPopulationTestScript.cs} (71%) rename Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/{DataPopulationTestScript.cs.meta => DataFieldPopulationTestScript.cs.meta} (83%) create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScriptableObject.cs rename Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/{DataPopulationTestScriptableObject.cs.meta => DataFieldPopulationTestScriptableObject.cs.meta} (83%) delete mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs create mode 100644 Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs.meta create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs create mode 100644 Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs.meta diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScript.cs similarity index 71% rename from Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs rename to Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScript.cs index d5624c0b..c00c0c39 100644 --- a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScript.cs @@ -1,8 +1,9 @@ +using System.Collections.Generic; using UnityEngine; namespace com.IvanMurzak.Unity.MCP.TestFiles { - public class DataPopulationTestScript : MonoBehaviour + public class DataFieldPopulationTestScript : MonoBehaviour { public Sprite spriteField; public Material materialField; @@ -14,6 +15,9 @@ public class DataPopulationTestScript : MonoBehaviour public Material[] materialArray; public GameObject[] gameObjectArray; + public List materialList; + public List gameObjectList; + public int intField; public string stringField; } diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs.meta b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScript.cs.meta similarity index 83% rename from Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs.meta rename to Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScript.cs.meta index 07f3a9ac..9fa9d946 100644 --- a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScript.cs.meta +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScript.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: fc89d6a3123d605488671f413ddfb193 +guid: 3ae0e74ae7c494a4794177e3b166ffaf MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScriptableObject.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScriptableObject.cs new file mode 100644 index 00000000..1d8414c0 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScriptableObject.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.TestFiles +{ + public class DataFieldPopulationTestScriptableObject : ScriptableObject + { + public Sprite spriteField; + public Material materialField; + public GameObject gameObjectField; + public Texture2D textureField; + public ScriptableObject scriptableObjectField; + public GameObject prefabField; + + public Material[] materialArray; + public GameObject[] gameObjectArray; + + public List materialList; + public List gameObjectList; + + public int intField; + public string stringField; + } +} diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs.meta b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScriptableObject.cs.meta similarity index 83% rename from Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs.meta rename to Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScriptableObject.cs.meta index b2262720..2c1346cc 100644 --- a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs.meta +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataFieldPopulationTestScriptableObject.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 5637e27532d5fcc48b7dead4b8afac1b +guid: d0b3a87ea594a204394626bce54ab521 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs deleted file mode 100644 index 167e0515..00000000 --- a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPopulationTestScriptableObject.cs +++ /dev/null @@ -1,10 +0,0 @@ -using UnityEngine; - -namespace com.IvanMurzak.Unity.MCP.TestFiles -{ - [CreateAssetMenu(fileName = "DataPopulationTestScriptableObject", menuName = "Tests/DataPopulationTestScriptableObject")] - public class DataPopulationTestScriptableObject : ScriptableObject - { - public int value; - } -} diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs new file mode 100644 index 00000000..56ee4ef8 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.TestFiles +{ + public class DataPropertyPopulationTestScript : MonoBehaviour + { + public Sprite spriteProperty { get; set; } + public Material materialProperty { get; set; } + public GameObject gameObjectProperty { get; set; } + public Texture2D textureProperty { get; set; } + public ScriptableObject scriptableObjectProperty { get; set; } + public GameObject prefabProperty { get; set; } + + public Material[] materialArrayProperty { get; set; } + public GameObject[] gameObjectArrayProperty { get; set; } + + public List materialListProperty { get; set; } + public List gameObjectListProperty { get; set; } + + public int intProperty { get; set; } + public string stringProperty { get; set; } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs.meta b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs.meta new file mode 100644 index 00000000..d4085b92 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3e4a54b437a969478a19ade1eb5e9e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs new file mode 100644 index 00000000..8f743076 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace com.IvanMurzak.Unity.MCP.TestFiles +{ + public class DataPropertyPopulationTestScriptableObject : ScriptableObject + { + public Sprite spriteProperty { get; set; } + public Material materialProperty { get; set; } + public GameObject gameObjectProperty { get; set; } + public Texture2D textureProperty { get; set; } + public ScriptableObject scriptableObjectProperty { get; set; } + public GameObject prefabProperty { get; set; } + + public Material[] materialArrayProperty { get; set; } + public GameObject[] gameObjectArrayProperty { get; set; } + + public List materialListProperty { get; set; } + public List gameObjectListProperty { get; set; } + + public int intProperty { get; set; } + public string stringProperty { get; set; } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs.meta b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs.meta new file mode 100644 index 00000000..47e81313 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/TestFiles/Scripts/DataPropertyPopulationTestScriptableObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c82805dba7ba4e744ade1f54c56e55eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs new file mode 100644 index 00000000..aa17c6ac --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs @@ -0,0 +1,169 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.Unity.MCP.Editor.API; +using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using com.IvanMurzak.Unity.MCP.TestFiles; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests +{ + public class DataFieldScriptableObjectPopulationTests : BaseTest + { + [UnitySetUp] + public override IEnumerator SetUp() => base.SetUp(); + + [UnityTearDown] + public override IEnumerator TearDown() => base.TearDown(); + + [UnityTest] + public IEnumerator Populate_All_Fields_SO_Test() + { + Debug.Log("[DataFieldScriptableObjectPopulationTests] Running field population test for SO."); + // Executors for creating assets + var materialEx = new CreateMaterialExecutor("TestMaterialSO.mat", "Standard", "Assets", "Unity-MCP-Test", "DataFieldSOPopulation"); + var textureEx = new CreateTextureExecutor("TestTextureSO.png", Color.magenta, 64, 64, "Assets", "Unity-MCP-Test", "DataFieldSOPopulation"); + var spriteEx = new CreateSpriteExecutor("TestSpriteSO.png", Color.green, 64, 64, "Assets", "Unity-MCP-Test", "DataFieldSOPopulation"); + + // The SO we are testing + var soEx = new CreateScriptableObjectExecutor("TestSOField.asset", "Assets", "Unity-MCP-Test", "DataFieldSOPopulation"); + + var prefabSourceGoEx = new CreateGameObjectExecutor("PrefabSourceSO"); + var prefabEx = new CreatePrefabExecutor("TestPrefabSO.prefab", null, "Assets", "Unity-MCP-Test", "DataFieldSOPopulation"); + + // Target GameObject for reference + var targetGoName = "TargetGOSO"; + var targetGoRef = new GameObjectRef() { Name = targetGoName }; + var targetGoEx = new CreateGameObjectExecutor(targetGoName); + + // Validation Executor + var validateEx = new LazyNodeExecutor(); + validateEx.SetAction((input) => + { + var so = soEx.Asset; + Assert.IsNotNull(so, "ScriptableObject should exist"); + + Assert.AreEqual(42, so!.intField, "intField not populated"); + Assert.AreEqual("Hello World", so.stringField, "stringField not populated"); + + Assert.IsNotNull(so.materialField, "Material should be populated"); + Assert.AreEqual(materialEx.Asset!.name, so.materialField.name); + + Assert.IsNotNull(so.gameObjectField, "GameObject should be populated"); + Assert.AreEqual(targetGoEx.GameObject!.name, so.gameObjectField.name); + + Assert.IsNotNull(so.textureField, "Texture should be populated"); + Assert.AreEqual(textureEx.Asset!.name, so.textureField.name); + + Assert.IsNotNull(so.spriteField, "Sprite should be populated"); + Assert.AreEqual(spriteEx.Sprite!.name, so.spriteField.name); + + Assert.IsNotNull(so.scriptableObjectField, "SO should be populated"); + Assert.AreEqual(soEx.Asset!.name, so.scriptableObjectField.name); + + Assert.IsNotNull(so.prefabField, "Prefab should be populated"); + Assert.AreEqual(prefabEx.Asset!.name, so.prefabField.name); + + Assert.IsNotNull(so.materialArray, "Material array should be populated"); + Assert.AreEqual(2, so.materialArray.Length); + Assert.AreEqual(materialEx.Asset.name, so.materialArray[0].name); + + Assert.IsNotNull(so.gameObjectArray, "GameObject array should be populated"); + Assert.AreEqual(2, so.gameObjectArray!.Length); + + Assert.IsNotNull(so.materialList, "Material list should be populated"); + Assert.AreEqual(2, so.materialList.Count); + Assert.AreEqual(materialEx.Asset.name, so.materialList[0].name); + + Assert.IsNotNull(so.gameObjectList, "GameObject list should be populated"); + Assert.AreEqual(2, so.gameObjectList.Count); + }); + + // Chain creation + var modifyEx = new DynamicCallToolExecutor( + typeof(Tool_Assets).GetMethod(nameof(Tool_Assets.Modify)), + () => + { + var plugin = UnityMcpPlugin.Instance; + var mcpInstance = plugin?.McpPluginInstance; + var manager = mcpInstance?.McpManager; + var reflector = manager?.Reflector; + + if (reflector == null) + { + Debug.LogError("[DataFieldScriptableObjectPopulationTests] Reflector is null! Cannot proceed with serialization."); + return "{}"; + } + + var matRef = new AssetObjectRef() { AssetPath = materialEx.AssetPath }; + var texRef = new AssetObjectRef() { AssetPath = textureEx.AssetPath }; + var soRef = new AssetObjectRef() { AssetPath = soEx.AssetPath }; + var prefabRef = new AssetObjectRef() { AssetPath = prefabEx.AssetPath }; + var goRef = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var spriteRef = new AssetObjectRef() { AssetPath = spriteEx.AssetPath }; + + var soModification = SerializedMember.FromValue( + reflector: reflector, + name: "DataFieldPopulationTestScriptableObject", + type: typeof(DataFieldPopulationTestScriptableObject), + value: new AssetObjectRef() { AssetPath = soEx.AssetPath } + ); + + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "spriteField", type: typeof(Sprite), value: spriteRef)); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialField", type: typeof(Material), value: matRef)); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectField", type: typeof(GameObject), value: goRef)); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "textureField", type: typeof(Texture2D), value: texRef)); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectField", type: typeof(DataFieldPopulationTestScriptableObject), value: soRef)); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "prefabField", type: typeof(GameObject), value: prefabRef)); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "intField", type: typeof(int), value: 42)); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "stringField", type: typeof(string), value: "Hello World")); + + var matRefArrayItem = new AssetObjectRef(materialEx.AssetPath!); + var goRefArrayItem = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var prefabRefArrayItem = new AssetObjectRef(prefabEx.AssetPath!); + + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialArray", type: typeof(Material[]), value: new object[] { matRefArrayItem, matRefArrayItem })); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectArray", type: typeof(GameObject[]), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialList", type: typeof(List), value: new object[] { matRefArrayItem, matRefArrayItem })); + soModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectList", type: typeof(List), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + + var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true }; + var assetRefJson = System.Text.Json.JsonSerializer.Serialize(new AssetObjectRef() { AssetPath = soEx.AssetPath }, options); + var contentJson = System.Text.Json.JsonSerializer.Serialize(soModification, options); + + var json = JsonTestUtils.Fill(@"{ + ""assetRef"": {assetRef}, + ""content"": {content} + }", + new Dictionary + { + { "{assetRef}", assetRefJson }, + { "{content}", contentJson } + }); + + Debug.Log($"[DataFieldScriptableObjectPopulationTests] JSON Input: {json}"); + return json; + } + ); + + modifyEx.AddChild(validateEx); + soEx.AddChild(modifyEx); + targetGoEx.AddChild(soEx); + + materialEx + .Nest(textureEx) + .Nest(spriteEx) + .Nest(prefabSourceGoEx) + .Nest(prefabEx) + .Nest(targetGoEx); + + materialEx.Execute(); + yield return null; + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs.meta new file mode 100644 index 00000000..8091a928 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2840f565d398a2c4ca0e7d26ddfed356 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs index 096a97b2..e448e2c5 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs @@ -29,7 +29,7 @@ public IEnumerator Populate_All_Types_Test() var materialEx = new CreateMaterialExecutor("TestMaterial.mat", "Standard", "Assets", "Unity-MCP-Test", "DataPopulation"); var textureEx = new CreateTextureExecutor("TestTexture.png", Color.magenta, 64, 64, "Assets", "Unity-MCP-Test", "DataPopulation"); var spriteEx = new CreateSpriteExecutor("TestSprite.png", Color.green, 64, 64, "Assets", "Unity-MCP-Test", "DataPopulation"); - var soEx = new CreateScriptableObjectExecutor("TestSO.asset", "Assets", "Unity-MCP-Test", "DataPopulation"); + var soEx = new CreateScriptableObjectExecutor("TestSO.asset", "Assets", "Unity-MCP-Test", "DataPopulation"); var prefabSourceGoEx = new CreateGameObjectExecutor("PrefabSource"); var prefabEx = new CreatePrefabExecutor("TestPrefab.prefab", null, "Assets", "Unity-MCP-Test", "DataPopulation"); @@ -38,7 +38,7 @@ public IEnumerator Populate_All_Types_Test() var targetGoName = "TargetGO"; var targetGoRef = new GameObjectRef() { Name = targetGoName }; var targetGoEx = new CreateGameObjectExecutor(targetGoName); - var addCompEx = new AddComponentExecutor(targetGoRef); + var addCompEx = new AddComponentExecutor(targetGoRef); // Validation Executor var validateEx = new LazyNodeExecutor(); @@ -74,6 +74,13 @@ public IEnumerator Populate_All_Types_Test() Assert.IsNotNull(comp.gameObjectArray, "GameObject array should be populated"); Assert.AreEqual(2, comp.gameObjectArray!.Length); + + Assert.IsNotNull(comp.materialList, "Material list should be populated"); + Assert.AreEqual(2, comp.materialList.Count); + Assert.AreEqual(materialEx.Asset.name, comp.materialList[0].name); + + Assert.IsNotNull(comp.gameObjectList, "GameObject list should be populated"); + Assert.AreEqual(2, comp.gameObjectList.Count); }); // Chain creation @@ -112,8 +119,8 @@ public IEnumerator Populate_All_Types_Test() var componentModification = SerializedMember.FromValue( reflector: reflector, - name: "DataPopulationTestScript", - type: typeof(DataPopulationTestScript), + name: "DataFieldPopulationTestScript", + type: typeof(DataFieldPopulationTestScript), value: new ComponentRef(addCompEx.Component!.GetInstanceID()) ); @@ -121,7 +128,7 @@ public IEnumerator Populate_All_Types_Test() componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialField", type: typeof(Material), value: matRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectField", type: typeof(GameObject), value: goRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "textureField", type: typeof(Texture2D), value: texRef)); - componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectField", type: typeof(DataPopulationTestScriptableObject), value: soRef)); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectField", type: typeof(DataFieldPopulationTestScriptableObject), value: soRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "prefabField", type: typeof(GameObject), value: prefabRef)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "intField", type: typeof(int), value: 42)); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "stringField", type: typeof(string), value: "Hello World")); @@ -133,6 +140,9 @@ public IEnumerator Populate_All_Types_Test() componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialArray", type: typeof(Material[]), value: new object[] { matRefArrayItem, matRefArrayItem })); componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectArray", type: typeof(GameObject[]), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "materialList", type: typeof(List), value: new object[] { matRefArrayItem, matRefArrayItem })); + componentModification.AddField(SerializedMember.FromValue(reflector: reflector, name: "gameObjectList", type: typeof(List), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + goModification.AddField(componentModification); var gameObjectDiffs = new SerializedMemberList { goModification }; diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs new file mode 100644 index 00000000..dc0eddcb --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs @@ -0,0 +1,180 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.Unity.MCP.Editor.API; +using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using com.IvanMurzak.Unity.MCP.TestFiles; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests +{ + public class DataPropertyPopulationTests : BaseTest + { + [UnitySetUp] + public override IEnumerator SetUp() => base.SetUp(); + + [UnityTearDown] + public override IEnumerator TearDown() => base.TearDown(); + + [UnityTest] + public IEnumerator Populate_All_Properties_Test() + { + Debug.Log("[DataPropertyPopulationTests] Running property population test."); + // Executors for creating assets + var materialEx = new CreateMaterialExecutor("TestMaterialProp.mat", "Standard", "Assets", "Unity-MCP-Test", "DataPropertyPopulation"); + var textureEx = new CreateTextureExecutor("TestTextureProp.png", Color.magenta, 64, 64, "Assets", "Unity-MCP-Test", "DataPropertyPopulation"); + var spriteEx = new CreateSpriteExecutor("TestSpriteProp.png", Color.green, 64, 64, "Assets", "Unity-MCP-Test", "DataPropertyPopulation"); + var soEx = new CreateScriptableObjectExecutor("TestSOProp.asset", "Assets", "Unity-MCP-Test", "DataPropertyPopulation"); + + var prefabSourceGoEx = new CreateGameObjectExecutor("PrefabSourceProp"); + var prefabEx = new CreatePrefabExecutor("TestPrefabProp.prefab", null, "Assets", "Unity-MCP-Test", "DataPropertyPopulation"); + + // Target GameObject + var targetGoName = "TargetGOProp"; + var targetGoRef = new GameObjectRef() { Name = targetGoName }; + var targetGoEx = new CreateGameObjectExecutor(targetGoName); + var addCompEx = new AddComponentExecutor(targetGoRef); + + // Validation Executor + var validateEx = new LazyNodeExecutor(); + validateEx.SetAction((input) => + { + var comp = addCompEx.Component; + Assert.IsNotNull(comp, "Component should exist"); + + Assert.AreEqual(42, comp!.intProperty, "intProperty not populated"); + Assert.AreEqual("Hello World", comp.stringProperty, "stringProperty not populated"); + + Assert.IsNotNull(comp.materialProperty, "Material should be populated"); + Assert.AreEqual(materialEx.Asset!.name, comp.materialProperty.name); + + Assert.IsNotNull(comp.gameObjectProperty, "GameObject should be populated"); + Assert.AreEqual(targetGoEx.GameObject!.name, comp.gameObjectProperty.name); + + Assert.IsNotNull(comp.textureProperty, "Texture should be populated"); + Assert.AreEqual(textureEx.Asset!.name, comp.textureProperty.name); + + Assert.IsNotNull(comp.spriteProperty, "Sprite should be populated"); + Assert.AreEqual(spriteEx.Sprite!.name, comp.spriteProperty.name); + + Assert.IsNotNull(comp.scriptableObjectProperty, "SO should be populated"); + Assert.AreEqual(soEx.Asset!.name, comp.scriptableObjectProperty.name); + + Assert.IsNotNull(comp.prefabProperty, "Prefab should be populated"); + Assert.AreEqual(prefabEx.Asset!.name, comp.prefabProperty.name); + + Assert.IsNotNull(comp.materialArrayProperty, "Material array should be populated"); + Assert.AreEqual(2, comp.materialArrayProperty.Length); + Assert.AreEqual(materialEx.Asset.name, comp.materialArrayProperty[0].name); + + Assert.IsNotNull(comp.gameObjectArrayProperty, "GameObject array should be populated"); + Assert.AreEqual(2, comp.gameObjectArrayProperty!.Length); + + Assert.IsNotNull(comp.materialListProperty, "Material list should be populated"); + Assert.AreEqual(2, comp.materialListProperty.Count); + Assert.AreEqual(materialEx.Asset.name, comp.materialListProperty[0].name); + + Assert.IsNotNull(comp.gameObjectListProperty, "GameObject list should be populated"); + Assert.AreEqual(2, comp.gameObjectListProperty.Count); + }); + + // Chain creation + var modifyEx = new DynamicCallToolExecutor( + typeof(Tool_GameObject).GetMethod(nameof(Tool_GameObject.Modify)), + () => + { + var plugin = UnityMcpPlugin.Instance; + var mcpInstance = plugin?.McpPluginInstance; + var manager = mcpInstance?.McpManager; + var reflector = manager?.Reflector; + + if (reflector == null) + { + Debug.LogError("[DataPropertyPopulationTests] Reflector is null! Cannot proceed with serialization."); + return "{}"; + } + + var matRef = new AssetObjectRef() { AssetPath = materialEx.AssetPath }; + var texRef = new AssetObjectRef() { AssetPath = textureEx.AssetPath }; + var soRef = new AssetObjectRef() { AssetPath = soEx.AssetPath }; + var prefabRef = new AssetObjectRef() { AssetPath = prefabEx.AssetPath }; + var goRef = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var spriteRef = new AssetObjectRef() { AssetPath = spriteEx.AssetPath }; + + var goModification = SerializedMember.FromValue( + reflector: reflector, + name: "TargetGOProp", + type: typeof(GameObject), + value: new GameObjectRef(targetGoEx.GameObject!.GetInstanceID()) + ); + + var componentModification = SerializedMember.FromValue( + reflector: reflector, + name: "DataPropertyPopulationTestScript", + type: typeof(DataPropertyPopulationTestScript), + value: new ComponentRef(addCompEx.Component!.GetInstanceID()) + ); + + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "spriteProperty", type: typeof(Sprite), value: spriteRef)); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "materialProperty", type: typeof(Material), value: matRef)); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "gameObjectProperty", type: typeof(GameObject), value: goRef)); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "textureProperty", type: typeof(Texture2D), value: texRef)); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectProperty", type: typeof(DataFieldPopulationTestScriptableObject), value: soRef)); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "prefabProperty", type: typeof(GameObject), value: prefabRef)); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "intProperty", type: typeof(int), value: 42)); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "stringProperty", type: typeof(string), value: "Hello World")); + + var matRefArrayItem = new AssetObjectRef(materialEx.AssetPath!); + var goRefArrayItem = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var prefabRefArrayItem = new AssetObjectRef(prefabEx.AssetPath!); + + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "materialArrayProperty", type: typeof(Material[]), value: new object[] { matRefArrayItem, matRefArrayItem })); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "gameObjectArrayProperty", type: typeof(GameObject[]), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "materialListProperty", type: typeof(List), value: new object[] { matRefArrayItem, matRefArrayItem })); + componentModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "gameObjectListProperty", type: typeof(List), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + + goModification.AddField(componentModification); + + var gameObjectDiffs = new SerializedMemberList { goModification }; + + var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true }; + var gameObjectRefsJson = System.Text.Json.JsonSerializer.Serialize(new GameObjectRef[] { targetGoRef }, options); + var gameObjectDiffsJson = System.Text.Json.JsonSerializer.Serialize(gameObjectDiffs, options); + + var json = JsonTestUtils.Fill(@"{ + ""gameObjectRefs"": {gameObjectRefs}, + ""gameObjectDiffs"": {gameObjectDiffs} + }", + new Dictionary + { + { "{gameObjectRefs}", gameObjectRefsJson }, + { "{gameObjectDiffs}", gameObjectDiffsJson } + }); + + Debug.Log($"[DataPropertyPopulationTests] JSON Input: {json}"); + return json; + } + ); + + modifyEx.AddChild(validateEx); + addCompEx.AddChild(modifyEx); + targetGoEx.AddChild(addCompEx); + + materialEx + .Nest(textureEx) + .Nest(spriteEx) + .Nest(soEx) + .Nest(prefabSourceGoEx) + .Nest(prefabEx) + .Nest(targetGoEx); + + materialEx.Execute(); + yield return null; + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs.meta new file mode 100644 index 00000000..52c096f6 --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9299104c2283e6d4aabf4dab9b73872f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs new file mode 100644 index 00000000..1fee487f --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs @@ -0,0 +1,169 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.Unity.MCP.Editor.API; +using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; +using com.IvanMurzak.Unity.MCP.Runtime.Data; +using com.IvanMurzak.Unity.MCP.TestFiles; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace com.IvanMurzak.Unity.MCP.Editor.Tests +{ + public class DataPropertyScriptableObjectPopulationTests : BaseTest + { + [UnitySetUp] + public override IEnumerator SetUp() => base.SetUp(); + + [UnityTearDown] + public override IEnumerator TearDown() => base.TearDown(); + + [UnityTest] + public IEnumerator Populate_All_Properties_SO_Test() + { + Debug.Log("[DataPropertyScriptableObjectPopulationTests] Running property population test for SO."); + // Executors for creating assets + var materialEx = new CreateMaterialExecutor("TestMaterialSOProp.mat", "Standard", "Assets", "Unity-MCP-Test", "DataPropertySOPopulation"); + var textureEx = new CreateTextureExecutor("TestTextureSOProp.png", Color.magenta, 64, 64, "Assets", "Unity-MCP-Test", "DataPropertySOPopulation"); + var spriteEx = new CreateSpriteExecutor("TestSpriteSOProp.png", Color.green, 64, 64, "Assets", "Unity-MCP-Test", "DataPropertySOPopulation"); + + // The SO we are testing + var soEx = new CreateScriptableObjectExecutor("TestSOProp.asset", "Assets", "Unity-MCP-Test", "DataPropertySOPopulation"); + + var prefabSourceGoEx = new CreateGameObjectExecutor("PrefabSourceSOProp"); + var prefabEx = new CreatePrefabExecutor("TestPrefabSOProp.prefab", null, "Assets", "Unity-MCP-Test", "DataPropertySOPopulation"); + + // Target GameObject for reference + var targetGoName = "TargetGOSOProp"; + var targetGoRef = new GameObjectRef() { Name = targetGoName }; + var targetGoEx = new CreateGameObjectExecutor(targetGoName); + + // Validation Executor + var validateEx = new LazyNodeExecutor(); + validateEx.SetAction((input) => + { + var so = soEx.Asset; + Assert.IsNotNull(so, "ScriptableObject should exist"); + + Assert.AreEqual(42, so!.intProperty, "intProperty not populated"); + Assert.AreEqual("Hello World", so.stringProperty, "stringProperty not populated"); + + Assert.IsNotNull(so.materialProperty, "Material should be populated"); + Assert.AreEqual(materialEx.Asset!.name, so.materialProperty.name); + + Assert.IsNotNull(so.gameObjectProperty, "GameObject should be populated"); + Assert.AreEqual(targetGoEx.GameObject!.name, so.gameObjectProperty.name); + + Assert.IsNotNull(so.textureProperty, "Texture should be populated"); + Assert.AreEqual(textureEx.Asset!.name, so.textureProperty.name); + + Assert.IsNotNull(so.spriteProperty, "Sprite should be populated"); + Assert.AreEqual(spriteEx.Sprite!.name, so.spriteProperty.name); + + Assert.IsNotNull(so.scriptableObjectProperty, "SO should be populated"); + Assert.AreEqual(soEx.Asset!.name, so.scriptableObjectProperty.name); + + Assert.IsNotNull(so.prefabProperty, "Prefab should be populated"); + Assert.AreEqual(prefabEx.Asset!.name, so.prefabProperty.name); + + Assert.IsNotNull(so.materialArrayProperty, "Material array should be populated"); + Assert.AreEqual(2, so.materialArrayProperty.Length); + Assert.AreEqual(materialEx.Asset.name, so.materialArrayProperty[0].name); + + Assert.IsNotNull(so.gameObjectArrayProperty, "GameObject array should be populated"); + Assert.AreEqual(2, so.gameObjectArrayProperty!.Length); + + Assert.IsNotNull(so.materialListProperty, "Material list should be populated"); + Assert.AreEqual(2, so.materialListProperty.Count); + Assert.AreEqual(materialEx.Asset.name, so.materialListProperty[0].name); + + Assert.IsNotNull(so.gameObjectListProperty, "GameObject list should be populated"); + Assert.AreEqual(2, so.gameObjectListProperty.Count); + }); + + // Chain creation + var modifyEx = new DynamicCallToolExecutor( + typeof(Tool_Assets).GetMethod(nameof(Tool_Assets.Modify)), + () => + { + var plugin = UnityMcpPlugin.Instance; + var mcpInstance = plugin?.McpPluginInstance; + var manager = mcpInstance?.McpManager; + var reflector = manager?.Reflector; + + if (reflector == null) + { + Debug.LogError("[DataPropertyScriptableObjectPopulationTests] Reflector is null! Cannot proceed with serialization."); + return "{}"; + } + + var matRef = new AssetObjectRef() { AssetPath = materialEx.AssetPath }; + var texRef = new AssetObjectRef() { AssetPath = textureEx.AssetPath }; + var soRef = new AssetObjectRef() { AssetPath = soEx.AssetPath }; + var prefabRef = new AssetObjectRef() { AssetPath = prefabEx.AssetPath }; + var goRef = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var spriteRef = new AssetObjectRef() { AssetPath = spriteEx.AssetPath }; + + var soModification = SerializedMember.FromValue( + reflector: reflector, + name: "DataPropertyPopulationTestScriptableObject", + type: typeof(DataPropertyPopulationTestScriptableObject), + value: new AssetObjectRef() { AssetPath = soEx.AssetPath } + ); + + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "spriteProperty", type: typeof(Sprite), value: spriteRef)); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "materialProperty", type: typeof(Material), value: matRef)); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "gameObjectProperty", type: typeof(GameObject), value: goRef)); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "textureProperty", type: typeof(Texture2D), value: texRef)); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "scriptableObjectProperty", type: typeof(DataPropertyPopulationTestScriptableObject), value: soRef)); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "prefabProperty", type: typeof(GameObject), value: prefabRef)); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "intProperty", type: typeof(int), value: 42)); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "stringProperty", type: typeof(string), value: "Hello World")); + + var matRefArrayItem = new AssetObjectRef(materialEx.AssetPath!); + var goRefArrayItem = new ObjectRef(targetGoEx.GameObject!.GetInstanceID()); + var prefabRefArrayItem = new AssetObjectRef(prefabEx.AssetPath!); + + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "materialArrayProperty", type: typeof(Material[]), value: new object[] { matRefArrayItem, matRefArrayItem })); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "gameObjectArrayProperty", type: typeof(GameObject[]), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "materialListProperty", type: typeof(List), value: new object[] { matRefArrayItem, matRefArrayItem })); + soModification.AddProperty(SerializedMember.FromValue(reflector: reflector, name: "gameObjectListProperty", type: typeof(List), value: new object[] { goRefArrayItem, prefabRefArrayItem })); + + var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true }; + var assetRefJson = System.Text.Json.JsonSerializer.Serialize(new AssetObjectRef() { AssetPath = soEx.AssetPath }, options); + var contentJson = System.Text.Json.JsonSerializer.Serialize(soModification, options); + + var json = JsonTestUtils.Fill(@"{ + ""assetRef"": {assetRef}, + ""content"": {content} + }", + new Dictionary + { + { "{assetRef}", assetRefJson }, + { "{content}", contentJson } + }); + + Debug.Log($"[DataPropertyScriptableObjectPopulationTests] JSON Input: {json}"); + return json; + } + ); + + modifyEx.AddChild(validateEx); + soEx.AddChild(modifyEx); + targetGoEx.AddChild(soEx); + + materialEx + .Nest(textureEx) + .Nest(spriteEx) + .Nest(prefabSourceGoEx) + .Nest(prefabEx) + .Nest(targetGoEx); + + materialEx.Execute(); + yield return null; + } + } +} diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs.meta b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs.meta new file mode 100644 index 00000000..f5e25a0f --- /dev/null +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d178ad952a7e63a4aa547f48ed441f26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 4e792b3889f66d848f51a0a691c41ce1a975cf6d Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 29 Nov 2025 03:22:24 -0800 Subject: [PATCH 12/13] Implement PostExecute method in CreateSpriteExecutor and CreateTextureExecutor to ensure proper cleanup of created assets. --- .../Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs | 6 ++++++ .../Tests/Editor/Utils/Executor/CreateTextureExecutor.cs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs index 7cf08149..a8e988d0 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs @@ -46,5 +46,11 @@ public CreateSpriteExecutor(string assetName, Color color, int width = 64, int h return Sprite; }); } + + protected override void PostExecute(object? input) + { + base.PostExecute(input); + Object.DestroyImmediate(Sprite); + } } } diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs index 75a6132f..b3266167 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateTextureExecutor.cs @@ -52,5 +52,10 @@ public CreateTextureExecutor(string assetName, Color color, int width = 64, int return Asset; }); } + protected override void PostExecute(object? input) + { + base.PostExecute(input); + Object.DestroyImmediate(Asset); + } } } From 2066604f51ecfe70436240a8ba139285366d9911 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 29 Nov 2025 18:49:02 -0800 Subject: [PATCH 13/13] Refactor test scripts to remove unused GameObjectRef and improve logging for null checks --- .../DataFieldScriptableObjectPopulationTests.cs | 1 - .../ReflectionConverter/DataPopulationTests.cs | 9 +++++---- ...ataPropertyScriptableObjectPopulationTests.cs | 1 - .../root/Tests/Editor/SpriteConverterTest.cs | 5 +++-- .../Utils/Executor/CreateSpriteExecutor.cs | 16 +++++++++++++++- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs index aa17c6ac..c935fa03 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs @@ -37,7 +37,6 @@ public IEnumerator Populate_All_Fields_SO_Test() // Target GameObject for reference var targetGoName = "TargetGOSO"; - var targetGoRef = new GameObjectRef() { Name = targetGoName }; var targetGoEx = new CreateGameObjectExecutor(targetGoName); // Validation Executor diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs index e448e2c5..1a0a50a7 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPopulationTests.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections; using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.Unity.MCP.Editor.API; using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils; @@ -89,13 +90,13 @@ public IEnumerator Populate_All_Types_Test() () => { var plugin = UnityMcpPlugin.Instance; - Debug.Log($"[DataPopulationTests] Plugin: {plugin}"); + Debug.Log($"[DataPopulationTests] Plugin: {plugin?.GetType().GetTypeShortName() ?? "null"}"); var mcpInstance = plugin?.McpPluginInstance; - Debug.Log($"[DataPopulationTests] McpInstance: {mcpInstance}"); + Debug.Log($"[DataPopulationTests] McpInstance: {mcpInstance?.GetType().GetTypeShortName() ?? "null"}"); var manager = mcpInstance?.McpManager; - Debug.Log($"[DataPopulationTests] Manager: {manager}"); + Debug.Log($"[DataPopulationTests] Manager: {manager?.GetType().GetTypeShortName() ?? "null"}"); var reflector = manager?.Reflector; - Debug.Log($"[DataPopulationTests] Reflector: {reflector}"); + Debug.Log($"[DataPopulationTests] Reflector: {reflector?.GetType().GetTypeShortName() ?? "null"}"); if (reflector == null) { diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs index 1fee487f..ef1c7923 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs @@ -37,7 +37,6 @@ public IEnumerator Populate_All_Properties_SO_Test() // Target GameObject for reference var targetGoName = "TargetGOSOProp"; - var targetGoRef = new GameObjectRef() { Name = targetGoName }; var targetGoEx = new CreateGameObjectExecutor(targetGoName); // Validation Executor diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs index 16233a3b..2158ea0e 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs @@ -73,15 +73,16 @@ public void TestSpritePopulation() // Try to populate object? obj = container; - bool result = false; - result = reflector.TryPopulate(ref obj, new SerializedMember + var result = reflector.TryPopulate(ref obj, new SerializedMember { typeName = typeof(SpriteContainer).AssemblyQualifiedName, fields = new SerializedMemberList { spriteMember }, props = new SerializedMemberList { spritePropertyMember } }); + Assert.IsTrue(result, "Population should succeed"); + // Assert.IsTrue(result, "Population should succeed"); Assert.IsNotNull((obj as SpriteContainer)?.spriteField, "Sprite field should be populated"); Assert.AreEqual("TestTexture", (obj as SpriteContainer)?.spriteField?.name); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs index a8e988d0..705f5b77 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Utils/Executor/CreateSpriteExecutor.cs @@ -28,10 +28,24 @@ public CreateSpriteExecutor(string assetName, Color color, int width = 64, int h if (importer != null) { importer.textureType = TextureImporterType.Sprite; - importer.SaveAndReimport(); + importer.spriteImportMode = SpriteImportMode.Single; + AssetDatabase.WriteImportSettingsIfDirty(AssetPath); + AssetDatabase.ImportAsset(AssetPath, ImportAssetOptions.ForceSynchronousImport); } Sprite = AssetDatabase.LoadAssetAtPath(AssetPath); + if (Sprite == null) + { + var allAssets = AssetDatabase.LoadAllAssetsAtPath(AssetPath); + foreach (var asset in allAssets) + { + if (asset is Sprite s) + { + Sprite = s; + break; + } + } + } if (Sprite == null) {