Merge pull request #256 from dsarno/feat/robust-component-resolver
Feat/robust component resolvermain
commit
4401de15d6
|
|
@ -0,0 +1,18 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace TestNamespace
|
||||
{
|
||||
public class CustomComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
private string customText = "Hello from custom asmdef!";
|
||||
|
||||
[SerializeField]
|
||||
private float customFloat = 42.0f;
|
||||
|
||||
void Start()
|
||||
{
|
||||
Debug.Log($"CustomComponent started: {customText}, value: {customFloat}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 78ee39b9744834fe390a4c7c5634eb5a
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "TestAsmdef",
|
||||
"rootNamespace": "TestNamespace",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 72f6376fa7bdc4220b11ccce20108cdc
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
"rootNamespace": "",
|
||||
"references": [
|
||||
"MCPForUnity.Editor",
|
||||
"TestAsmdef",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnity.Editor.Tools.ManageGameObject;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class AIPropertyMatchingTests
|
||||
{
|
||||
private List<string> sampleProperties;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
sampleProperties = new List<string>
|
||||
{
|
||||
"maxReachDistance",
|
||||
"maxHorizontalDistance",
|
||||
"maxVerticalDistance",
|
||||
"moveSpeed",
|
||||
"healthPoints",
|
||||
"playerName",
|
||||
"isEnabled",
|
||||
"mass",
|
||||
"velocity",
|
||||
"transform"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAllComponentProperties_ReturnsValidProperties_ForTransform()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
|
||||
|
||||
Assert.IsNotEmpty(properties, "Transform should have properties");
|
||||
Assert.Contains("position", properties, "Transform should have position property");
|
||||
Assert.Contains("rotation", properties, "Transform should have rotation property");
|
||||
Assert.Contains("localScale", properties, "Transform should have localScale property");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAllComponentProperties_ReturnsEmpty_ForNullType()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(null);
|
||||
|
||||
Assert.IsEmpty(properties, "Null type should return empty list");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_ReturnsEmpty_ForNullInput()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions(null, sampleProperties);
|
||||
|
||||
Assert.IsEmpty(suggestions, "Null input should return no suggestions");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyInput()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("", sampleProperties);
|
||||
|
||||
Assert.IsEmpty(suggestions, "Empty input should return no suggestions");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("test", new List<string>());
|
||||
|
||||
Assert.IsEmpty(suggestions, "Empty property list should return no suggestions");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_FindsExactMatch_AfterCleaning()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("Max Reach Distance", sampleProperties);
|
||||
|
||||
Assert.Contains("maxReachDistance", suggestions, "Should find exact match after cleaning spaces");
|
||||
Assert.AreEqual(1, suggestions.Count, "Should return exactly one match for exact match");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_FindsMultipleWordMatches()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("max distance", sampleProperties);
|
||||
|
||||
Assert.Contains("maxReachDistance", suggestions, "Should match maxReachDistance");
|
||||
Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance");
|
||||
Assert.Contains("maxVerticalDistance", suggestions, "Should match maxVerticalDistance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_FindsSimilarStrings_WithTypos()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("movespeed", sampleProperties); // missing capital S
|
||||
|
||||
Assert.Contains("moveSpeed", suggestions, "Should find moveSpeed despite missing capital");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_FindsSemanticMatches_ForCommonTerms()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", sampleProperties);
|
||||
|
||||
// Note: Current algorithm might not find "mass" but should handle it gracefully
|
||||
Assert.IsNotNull(suggestions, "Should return valid suggestions list");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_LimitsResults_ToReasonableNumber()
|
||||
{
|
||||
// Test with input that might match many properties
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("m", sampleProperties);
|
||||
|
||||
Assert.LessOrEqual(suggestions.Count, 3, "Should limit suggestions to 3 or fewer");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_CachesResults()
|
||||
{
|
||||
var input = "Max Reach Distance";
|
||||
|
||||
// First call
|
||||
var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties);
|
||||
|
||||
// Second call should use cache (tested indirectly by ensuring consistency)
|
||||
var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties);
|
||||
|
||||
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be consistent");
|
||||
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should be identical");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_HandlesUnityNamingConventions()
|
||||
{
|
||||
var unityStyleProperties = new List<string> { "isKinematic", "useGravity", "maxLinearVelocity" };
|
||||
|
||||
var suggestions1 = ComponentResolver.GetAIPropertySuggestions("is kinematic", unityStyleProperties);
|
||||
var suggestions2 = ComponentResolver.GetAIPropertySuggestions("use gravity", unityStyleProperties);
|
||||
var suggestions3 = ComponentResolver.GetAIPropertySuggestions("max linear velocity", unityStyleProperties);
|
||||
|
||||
Assert.Contains("isKinematic", suggestions1, "Should handle 'is' prefix convention");
|
||||
Assert.Contains("useGravity", suggestions2, "Should handle 'use' prefix convention");
|
||||
Assert.Contains("maxLinearVelocity", suggestions3, "Should handle 'max' prefix convention");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_PrioritizesExactMatches()
|
||||
{
|
||||
var properties = new List<string> { "speed", "moveSpeed", "maxSpeed", "speedMultiplier" };
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("speed", properties);
|
||||
|
||||
Assert.IsNotEmpty(suggestions, "Should find suggestions");
|
||||
Assert.AreEqual("speed", suggestions[0], "Exact match should be prioritized first");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAIPropertySuggestions_HandlesCaseInsensitive()
|
||||
{
|
||||
var suggestions1 = ComponentResolver.GetAIPropertySuggestions("MAXREACHDISTANCE", sampleProperties);
|
||||
var suggestions2 = ComponentResolver.GetAIPropertySuggestions("maxreachdistance", sampleProperties);
|
||||
|
||||
Assert.Contains("maxReachDistance", suggestions1, "Should handle uppercase input");
|
||||
Assert.Contains("maxReachDistance", suggestions2, "Should handle lowercase input");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9e4468da1a15349029e52570b84ec4b0
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
using System;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnity.Editor.Tools.ManageGameObject;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ComponentResolverTests
|
||||
{
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve Transform component");
|
||||
Assert.AreEqual(typeof(Transform), type, "Should return correct Transform type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("UnityEngine.Rigidbody", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve UnityEngine.Rigidbody component");
|
||||
Assert.AreEqual(typeof(Rigidbody), type, "Should return correct Rigidbody type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForCustomComponentShortName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve CustomComponent");
|
||||
Assert.IsNotNull(type, "Should return valid type");
|
||||
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
|
||||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("TestNamespace.CustomComponent", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve TestNamespace.CustomComponent");
|
||||
Assert.IsNotNull(type, "Should return valid type");
|
||||
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
|
||||
Assert.AreEqual("TestNamespace.CustomComponent", type.FullName, "Should have correct full name");
|
||||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsFalse_ForNonExistentComponent()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("NonExistentComponent", out Type type, out string error);
|
||||
|
||||
Assert.IsFalse(result, "Should not resolve non-existent component");
|
||||
Assert.IsNull(type, "Should return null type");
|
||||
Assert.IsNotEmpty(error, "Should have error message");
|
||||
Assert.That(error, Does.Contain("not found"), "Error should mention component not found");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsFalse_ForEmptyString()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("", out Type type, out string error);
|
||||
|
||||
Assert.IsFalse(result, "Should not resolve empty string");
|
||||
Assert.IsNull(type, "Should return null type");
|
||||
Assert.IsNotEmpty(error, "Should have error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsFalse_ForNullString()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve(null, out Type type, out string error);
|
||||
|
||||
Assert.IsFalse(result, "Should not resolve null string");
|
||||
Assert.IsNull(type, "Should return null type");
|
||||
Assert.IsNotEmpty(error, "Should have error message");
|
||||
Assert.That(error, Does.Contain("null or empty"), "Error should mention null or empty");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_CachesResolvedTypes()
|
||||
{
|
||||
// First call
|
||||
bool result1 = ComponentResolver.TryResolve("Transform", out Type type1, out string error1);
|
||||
|
||||
// Second call should use cache
|
||||
bool result2 = ComponentResolver.TryResolve("Transform", out Type type2, out string error2);
|
||||
|
||||
Assert.IsTrue(result1, "First call should succeed");
|
||||
Assert.IsTrue(result2, "Second call should succeed");
|
||||
Assert.AreSame(type1, type2, "Should return same type instance (cached)");
|
||||
Assert.IsEmpty(error1, "First call should have no error");
|
||||
Assert.IsEmpty(error2, "Second call should have no error");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_PrefersPlayerAssemblies()
|
||||
{
|
||||
// Test that custom user scripts (in Player assemblies) are found
|
||||
bool result = ComponentResolver.TryResolve("TicTacToe3D", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve user script from Player assembly");
|
||||
Assert.IsNotNull(type, "Should return valid type");
|
||||
|
||||
// Verify it's not from an Editor assembly by checking the assembly name
|
||||
string assemblyName = type.Assembly.GetName().Name;
|
||||
Assert.That(assemblyName, Does.Not.Contain("Editor"),
|
||||
"User script should come from Player assembly, not Editor assembly");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_HandlesDuplicateNames_WithAmbiguityError()
|
||||
{
|
||||
// This test would need duplicate component names to be meaningful
|
||||
// For now, test with a built-in component that should not have duplicates
|
||||
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Transform should resolve uniquely");
|
||||
Assert.AreEqual(typeof(Transform), type, "Should return correct type");
|
||||
Assert.IsEmpty(error, "Should have no ambiguity error");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolvedType_IsValidComponent()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("Rigidbody", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve Rigidbody");
|
||||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Resolved type should be assignable from Component");
|
||||
Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) ||
|
||||
typeof(Component).IsAssignableFrom(type), "Should be a valid Unity component");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c15ba6502927e4901a43826c43debd7c
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEngine.TestTools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageGameObjectTests
|
||||
{
|
||||
private GameObject testGameObject;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
// Create a test GameObject for each test
|
||||
testGameObject = new GameObject("TestObject");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Clean up test GameObject
|
||||
if (testGameObject != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(testGameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ReturnsError_ForNullParams()
|
||||
{
|
||||
var result = ManageGameObject.HandleCommand(null);
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result object");
|
||||
// Note: Actual error checking would need access to Response structure
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ReturnsError_ForEmptyParams()
|
||||
{
|
||||
var emptyParams = new JObject();
|
||||
var result = ManageGameObject.HandleCommand(emptyParams);
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result object for empty params");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ProcessesValidCreateAction()
|
||||
{
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestCreateObject"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(createParams);
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result for valid create action");
|
||||
|
||||
// Clean up - find and destroy the created object
|
||||
var createdObject = GameObject.Find("TestCreateObject");
|
||||
if (createdObject != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(createdObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComponentResolver_Integration_WorksWithRealComponents()
|
||||
{
|
||||
// Test that our ComponentResolver works with actual Unity components
|
||||
var transformResult = ComponentResolver.TryResolve("Transform", out Type transformType, out string error);
|
||||
|
||||
Assert.IsTrue(transformResult, "Should resolve Transform component");
|
||||
Assert.AreEqual(typeof(Transform), transformType, "Should return correct Transform type");
|
||||
Assert.IsEmpty(error, "Should have no error for valid component");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComponentResolver_Integration_WorksWithBuiltInComponents()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
("Rigidbody", typeof(Rigidbody)),
|
||||
("Collider", typeof(Collider)),
|
||||
("Renderer", typeof(Renderer)),
|
||||
("Camera", typeof(Camera)),
|
||||
("Light", typeof(Light))
|
||||
};
|
||||
|
||||
foreach (var (componentName, expectedType) in components)
|
||||
{
|
||||
var result = ComponentResolver.TryResolve(componentName, out Type actualType, out string error);
|
||||
|
||||
// Some components might not resolve (abstract classes), but the method should handle gracefully
|
||||
if (result)
|
||||
{
|
||||
Assert.IsTrue(expectedType.IsAssignableFrom(actualType),
|
||||
$"{componentName} should resolve to assignable type");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNotEmpty(error, $"Should have error message for {componentName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PropertyMatching_Integration_WorksWithRealGameObject()
|
||||
{
|
||||
// Add a Rigidbody to test real property matching
|
||||
var rigidbody = testGameObject.AddComponent<Rigidbody>();
|
||||
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(Rigidbody));
|
||||
|
||||
Assert.IsNotEmpty(properties, "Rigidbody should have properties");
|
||||
Assert.Contains("mass", properties, "Rigidbody should have mass property");
|
||||
Assert.Contains("useGravity", properties, "Rigidbody should have useGravity property");
|
||||
|
||||
// Test AI suggestions
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("Use Gravity", properties);
|
||||
Assert.Contains("useGravity", suggestions, "Should suggest useGravity for 'Use Gravity'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PropertyMatching_HandlesMonoBehaviourProperties()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(MonoBehaviour));
|
||||
|
||||
Assert.IsNotEmpty(properties, "MonoBehaviour should have properties");
|
||||
Assert.Contains("enabled", properties, "MonoBehaviour should have enabled property");
|
||||
Assert.Contains("name", properties, "MonoBehaviour should have name property");
|
||||
Assert.Contains("tag", properties, "MonoBehaviour should have tag property");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PropertyMatching_HandlesCaseVariations()
|
||||
{
|
||||
var testProperties = new List<string> { "maxReachDistance", "playerHealth", "movementSpeed" };
|
||||
|
||||
var testCases = new[]
|
||||
{
|
||||
("max reach distance", "maxReachDistance"),
|
||||
("Max Reach Distance", "maxReachDistance"),
|
||||
("MAX_REACH_DISTANCE", "maxReachDistance"),
|
||||
("player health", "playerHealth"),
|
||||
("movement speed", "movementSpeed")
|
||||
};
|
||||
|
||||
foreach (var (input, expected) in testCases)
|
||||
{
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions(input, testProperties);
|
||||
Assert.Contains(expected, suggestions, $"Should suggest {expected} for input '{input}'");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ErrorHandling_ReturnsHelpfulMessages()
|
||||
{
|
||||
// This test verifies that error messages are helpful and contain suggestions
|
||||
var testProperties = new List<string> { "mass", "velocity", "drag", "useGravity" };
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", testProperties);
|
||||
|
||||
// Even if no perfect match, should return valid list
|
||||
Assert.IsNotNull(suggestions, "Should return valid suggestions list");
|
||||
|
||||
// Test with completely invalid input
|
||||
var badSuggestions = ComponentResolver.GetAIPropertySuggestions("xyz123invalid", testProperties);
|
||||
Assert.IsNotNull(badSuggestions, "Should handle invalid input gracefully");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PerformanceTest_CachingWorks()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
|
||||
var input = "Test Property Name";
|
||||
|
||||
// First call - populate cache
|
||||
var startTime = System.DateTime.UtcNow;
|
||||
var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, properties);
|
||||
var firstCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Second call - should use cache
|
||||
startTime = System.DateTime.UtcNow;
|
||||
var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, properties);
|
||||
var secondCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be identical");
|
||||
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should match exactly");
|
||||
|
||||
// Second call should be faster (though this test might be flaky)
|
||||
Assert.LessOrEqual(secondCallTime, firstCallTime * 2, "Cached call should not be significantly slower");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes()
|
||||
{
|
||||
// Arrange - add Transform and Rigidbody components to test with
|
||||
var transform = testGameObject.transform;
|
||||
var rigidbody = testGameObject.AddComponent<Rigidbody>();
|
||||
|
||||
// Create a params object with mixed valid and invalid properties
|
||||
var setPropertiesParams = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = testGameObject.name,
|
||||
["search_method"] = "by_name",
|
||||
["componentProperties"] = new JObject
|
||||
{
|
||||
["Transform"] = new JObject
|
||||
{
|
||||
["localPosition"] = new JObject { ["x"] = 1.0f, ["y"] = 2.0f, ["z"] = 3.0f }, // Valid
|
||||
["rotatoin"] = new JObject { ["x"] = 0.0f, ["y"] = 90.0f, ["z"] = 0.0f }, // Invalid (typo - should be rotation)
|
||||
["localScale"] = new JObject { ["x"] = 2.0f, ["y"] = 2.0f, ["z"] = 2.0f } // Valid
|
||||
},
|
||||
["Rigidbody"] = new JObject
|
||||
{
|
||||
["mass"] = 5.0f, // Valid
|
||||
["invalidProp"] = "test", // Invalid - doesn't exist
|
||||
["useGravity"] = true // Valid
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Store original values to verify changes
|
||||
var originalLocalPosition = transform.localPosition;
|
||||
var originalLocalScale = transform.localScale;
|
||||
var originalMass = rigidbody.mass;
|
||||
var originalUseGravity = rigidbody.useGravity;
|
||||
|
||||
Debug.Log($"BEFORE TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
|
||||
|
||||
// Expect the warning logs from the invalid properties
|
||||
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'rotatoin' not found"));
|
||||
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'invalidProp' not found"));
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(setPropertiesParams);
|
||||
|
||||
Debug.Log($"AFTER TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
|
||||
Debug.Log($"AFTER TEST - LocalPosition: {transform.localPosition}");
|
||||
Debug.Log($"AFTER TEST - LocalScale: {transform.localScale}");
|
||||
|
||||
// Assert - verify that valid properties were set despite invalid ones
|
||||
Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition,
|
||||
"Valid localPosition should be set even with other invalid properties");
|
||||
Assert.AreEqual(new Vector3(2.0f, 2.0f, 2.0f), transform.localScale,
|
||||
"Valid localScale should be set even with other invalid properties");
|
||||
Assert.AreEqual(5.0f, rigidbody.mass, 0.001f,
|
||||
"Valid mass should be set even with other invalid properties");
|
||||
Assert.AreEqual(true, rigidbody.useGravity,
|
||||
"Valid useGravity should be set even with other invalid properties");
|
||||
|
||||
// Verify the result indicates errors (since we had invalid properties)
|
||||
Assert.IsNotNull(result, "Should return a result object");
|
||||
|
||||
// The collect-and-continue behavior means we should get an error response
|
||||
// that contains info about the failed properties, but valid ones were still applied
|
||||
// This proves the collect-and-continue behavior is working
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_ContinuesAfterException()
|
||||
{
|
||||
// Arrange - create scenario that might cause exceptions
|
||||
var rigidbody = testGameObject.AddComponent<Rigidbody>();
|
||||
|
||||
// Set initial values that we'll change
|
||||
rigidbody.mass = 1.0f;
|
||||
rigidbody.useGravity = true;
|
||||
|
||||
var setPropertiesParams = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = testGameObject.name,
|
||||
["search_method"] = "by_name",
|
||||
["componentProperties"] = new JObject
|
||||
{
|
||||
["Rigidbody"] = new JObject
|
||||
{
|
||||
["mass"] = 2.5f, // Valid - should be set
|
||||
["velocity"] = "invalid_type", // Invalid type - will cause exception
|
||||
["useGravity"] = false // Valid - should still be set after exception
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expect the error logs from the invalid property
|
||||
LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Unexpected error converting token to UnityEngine.Vector3"));
|
||||
LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("SetProperty.*Failed to set 'velocity'"));
|
||||
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'velocity' not found"));
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(setPropertiesParams);
|
||||
|
||||
// Assert - verify that valid properties before AND after the exception were still set
|
||||
Assert.AreEqual(2.5f, rigidbody.mass, 0.001f,
|
||||
"Mass should be set even if later property causes exception");
|
||||
Assert.AreEqual(false, rigidbody.useGravity,
|
||||
"UseGravity should be set even if previous property caused exception");
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result even with exceptions");
|
||||
|
||||
// The key test: processing continued after the exception and set useGravity
|
||||
// This proves the collect-and-continue behavior works even with exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5931268353eab4ea5baa054e6231e824
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: be61633e00d934610ac1ff8192ffbe3d
|
||||
|
|
@ -7,6 +7,7 @@ using Newtonsoft.Json.Linq;
|
|||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers; // For Response class
|
||||
using static MCPForUnity.Editor.Tools.ManageGameObject;
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
|
||||
|
|
@ -201,15 +202,16 @@ namespace MCPForUnity.Editor.Tools
|
|||
"'scriptClass' property required when creating ScriptableObject asset."
|
||||
);
|
||||
|
||||
Type scriptType = FindType(scriptClassName);
|
||||
Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null;
|
||||
if (
|
||||
scriptType == null
|
||||
|| !typeof(ScriptableObject).IsAssignableFrom(scriptType)
|
||||
)
|
||||
{
|
||||
return Response.Error(
|
||||
$"Script class '{scriptClassName}' not found or does not inherit from ScriptableObject."
|
||||
);
|
||||
var reason = scriptType == null
|
||||
? (string.IsNullOrEmpty(error) ? "Type not found." : error)
|
||||
: "Type found but does not inherit from ScriptableObject.";
|
||||
return Response.Error($"Script class '{scriptClassName}' invalid: {reason}");
|
||||
}
|
||||
|
||||
ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
|
||||
|
|
@ -353,10 +355,21 @@ namespace MCPForUnity.Editor.Tools
|
|||
&& componentProperties.HasValues
|
||||
) // e.g., {"bobSpeed": 2.0}
|
||||
{
|
||||
// Find the component on the GameObject using the name from the JSON key
|
||||
// Using GetComponent(string) is convenient but might require exact type name or be ambiguous.
|
||||
// Consider using FindType helper if needed for more complex scenarios.
|
||||
Component targetComponent = gameObject.GetComponent(componentName);
|
||||
// Resolve component type via ComponentResolver, then fetch by Type
|
||||
Component targetComponent = null;
|
||||
bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError);
|
||||
if (resolved)
|
||||
{
|
||||
targetComponent = gameObject.GetComponent(compType);
|
||||
}
|
||||
|
||||
// Only warn about resolution failure if component also not found
|
||||
if (targetComponent == null && !resolved)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}"
|
||||
);
|
||||
}
|
||||
|
||||
if (targetComponent != null)
|
||||
{
|
||||
|
|
@ -937,8 +950,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
string propName = floatProps["name"]?.ToString();
|
||||
if (
|
||||
!string.IsNullOrEmpty(propName) && floatProps["value"]?.Type == JTokenType.Float
|
||||
|| floatProps["value"]?.Type == JTokenType.Integer
|
||||
!string.IsNullOrEmpty(propName) &&
|
||||
(floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)
|
||||
)
|
||||
{
|
||||
try
|
||||
|
|
@ -1220,46 +1233,6 @@ namespace MCPForUnity.Editor.Tools
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to find a Type by name, searching relevant assemblies.
|
||||
/// Needed for creating ScriptableObjects or finding component types by name.
|
||||
/// </summary>
|
||||
private static Type FindType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
return null;
|
||||
|
||||
// Try direct lookup first (common Unity types often don't need assembly qualified name)
|
||||
var type =
|
||||
Type.GetType(typeName)
|
||||
?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule")
|
||||
?? Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI")
|
||||
?? Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule");
|
||||
|
||||
if (type != null)
|
||||
return type;
|
||||
|
||||
// If not found, search loaded assemblies (slower but more robust for user scripts)
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
// Look for non-namespaced first
|
||||
type = assembly.GetType(typeName, false, true); // throwOnError=false, ignoreCase=true
|
||||
if (type != null)
|
||||
return type;
|
||||
|
||||
// Check common namespaces if simple name given
|
||||
type = assembly.GetType("UnityEngine." + typeName, false, true);
|
||||
if (type != null)
|
||||
return type;
|
||||
type = assembly.GetType("UnityEditor." + typeName, false, true);
|
||||
if (type != null)
|
||||
return type;
|
||||
// Add other likely namespaces if needed (e.g., specific plugins)
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[FindType] Type '{typeName}' not found in any loaded assembly.");
|
||||
return null; // Not found
|
||||
}
|
||||
|
||||
// --- Data Serialization ---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
|
@ -5,6 +6,7 @@ using System.Reflection;
|
|||
using Newtonsoft.Json; // Added for JsonSerializationException
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Compilation; // For CompilationPipeline
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
|
@ -19,10 +21,29 @@ namespace MCPForUnity.Editor.Tools
|
|||
/// </summary>
|
||||
public static class ManageGameObject
|
||||
{
|
||||
// Shared JsonSerializer to avoid per-call allocation overhead
|
||||
private static readonly JsonSerializer InputSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter>
|
||||
{
|
||||
new Vector3Converter(),
|
||||
new Vector2Converter(),
|
||||
new QuaternionConverter(),
|
||||
new ColorConverter(),
|
||||
new RectConverter(),
|
||||
new BoundsConverter(),
|
||||
new UnityEngineObjectConverter()
|
||||
}
|
||||
});
|
||||
|
||||
// --- Main Handler ---
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return Response.Error("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
|
|
@ -283,11 +304,16 @@ namespace MCPForUnity.Editor.Tools
|
|||
newGo = GameObject.CreatePrimitive(type);
|
||||
// Set name *after* creation for primitives
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
newGo.name = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo); // cleanup leak
|
||||
return Response.Error(
|
||||
"'name' parameter is required when creating a primitive."
|
||||
); // Name is essential
|
||||
);
|
||||
}
|
||||
createdNewObject = true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
|
|
@ -759,6 +785,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
}
|
||||
|
||||
// Set Component Properties
|
||||
var componentErrors = new List<object>();
|
||||
if (@params["componentProperties"] is JObject componentPropertiesObj)
|
||||
{
|
||||
foreach (var prop in componentPropertiesObj.Properties())
|
||||
|
|
@ -773,12 +800,26 @@ namespace MCPForUnity.Editor.Tools
|
|||
propertiesToSet
|
||||
);
|
||||
if (setResult != null)
|
||||
return setResult;
|
||||
modified = true;
|
||||
{
|
||||
componentErrors.Add(setResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return component errors if any occurred (after processing all components)
|
||||
if (componentErrors.Count > 0)
|
||||
{
|
||||
return Response.Error(
|
||||
$"One or more component property operations failed on '{targetGo.name}'.",
|
||||
new { componentErrors = componentErrors }
|
||||
);
|
||||
}
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
// Use the new serializer helper
|
||||
|
|
@ -1097,6 +1138,29 @@ namespace MCPForUnity.Editor.Tools
|
|||
|
||||
// --- Internal Helpers ---
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JArray like [x, y, z] into a Vector3.
|
||||
/// </summary>
|
||||
private static Vector3? ParseVector3(JArray array)
|
||||
{
|
||||
if (array != null && array.Count == 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Vector3(
|
||||
array[0].ToObject<float>(),
|
||||
array[1].ToObject<float>(),
|
||||
array[2].ToObject<float>()
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a single GameObject based on token (ID, name, path) and search method.
|
||||
/// </summary>
|
||||
|
|
@ -1464,7 +1528,18 @@ namespace MCPForUnity.Editor.Tools
|
|||
Component targetComponentInstance = null
|
||||
)
|
||||
{
|
||||
Component targetComponent = targetComponentInstance ?? targetGo.GetComponent(compName);
|
||||
Component targetComponent = targetComponentInstance;
|
||||
if (targetComponent == null)
|
||||
{
|
||||
if (ComponentResolver.TryResolve(compName, out var compType, out var compError))
|
||||
{
|
||||
targetComponent = targetGo.GetComponent(compType);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetComponent = targetGo.GetComponent(compName); // fallback to string-based lookup
|
||||
}
|
||||
}
|
||||
if (targetComponent == null)
|
||||
{
|
||||
return Response.Error(
|
||||
|
|
@ -1474,6 +1549,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
|
||||
Undo.RecordObject(targetComponent, "Set Component Properties");
|
||||
|
||||
var failures = new List<string>();
|
||||
foreach (var prop in propertiesToSet.Properties())
|
||||
{
|
||||
string propName = prop.Name;
|
||||
|
|
@ -1481,14 +1557,16 @@ namespace MCPForUnity.Editor.Tools
|
|||
|
||||
try
|
||||
{
|
||||
if (!SetProperty(targetComponent, propName, propValue))
|
||||
bool setResult = SetProperty(targetComponent, propName, propValue);
|
||||
if (!setResult)
|
||||
{
|
||||
// Log warning if property could not be set
|
||||
Debug.LogWarning(
|
||||
$"[ManageGameObject] Could not set property '{propName}' on component '{compName}' ('{targetComponent.GetType().Name}'). Property might not exist, be read-only, or type mismatch."
|
||||
);
|
||||
// Optionally return an error here instead of just logging
|
||||
// return Response.Error($"Could not set property '{propName}' on component '{compName}'.");
|
||||
var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType());
|
||||
var suggestions = ComponentResolver.GetAIPropertySuggestions(propName, availableProperties);
|
||||
var msg = suggestions.Any()
|
||||
? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]"
|
||||
: $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]";
|
||||
Debug.LogWarning($"[ManageGameObject] {msg}");
|
||||
failures.Add(msg);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -1496,12 +1574,13 @@ namespace MCPForUnity.Editor.Tools
|
|||
Debug.LogError(
|
||||
$"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}"
|
||||
);
|
||||
// Optionally return an error here
|
||||
// return Response.Error($"Error setting property '{propName}' on '{compName}': {e.Message}");
|
||||
failures.Add($"Error setting '{propName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
EditorUtility.SetDirty(targetComponent);
|
||||
return null; // Success (or partial success if warnings were logged)
|
||||
return failures.Count == 0
|
||||
? null
|
||||
: Response.Error($"One or more properties failed on '{compName}'.", new { errors = failures });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1513,25 +1592,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
BindingFlags flags =
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
|
||||
// --- Use a dedicated serializer for input conversion ---
|
||||
// Define this somewhere accessible, maybe static readonly field
|
||||
JsonSerializerSettings inputSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter>
|
||||
{
|
||||
// Add specific converters needed for INPUT deserialization if different from output
|
||||
new Vector3Converter(),
|
||||
new Vector2Converter(),
|
||||
new QuaternionConverter(),
|
||||
new ColorConverter(),
|
||||
new RectConverter(),
|
||||
new BoundsConverter(),
|
||||
new UnityEngineObjectConverter() // Crucial for finding references from instructions
|
||||
}
|
||||
// No ReferenceLoopHandling needed typically for input
|
||||
};
|
||||
JsonSerializer inputSerializer = JsonSerializer.Create(inputSerializerSettings);
|
||||
// --- End Serializer Setup ---
|
||||
// Use shared serializer to avoid per-call allocation
|
||||
var inputSerializer = InputSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -1573,6 +1635,20 @@ namespace MCPForUnity.Editor.Tools
|
|||
Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try NonPublic [SerializeField] fields
|
||||
var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (npField != null && npField.GetCustomAttribute<SerializeField>() != null)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
npField.SetValue(target, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -2070,84 +2146,288 @@ namespace MCPForUnity.Editor.Tools
|
|||
|
||||
|
||||
/// <summary>
|
||||
/// Helper to find a Type by name, searching relevant assemblies.
|
||||
/// Robust component resolver that avoids Assembly.LoadFrom and works with asmdefs.
|
||||
/// Searches already-loaded assemblies, prioritizing runtime script assemblies.
|
||||
/// </summary>
|
||||
private static Type FindType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
return null;
|
||||
|
||||
// Handle fully qualified names first
|
||||
Type type = Type.GetType(typeName);
|
||||
if (type != null) return type;
|
||||
|
||||
// Handle common namespaces implicitly (add more as needed)
|
||||
string[] namespaces = { "UnityEngine", "UnityEngine.UI", "UnityEngine.AI", "UnityEngine.Animations", "UnityEngine.Audio", "UnityEngine.EventSystems", "UnityEngine.InputSystem", "UnityEngine.Networking", "UnityEngine.Rendering", "UnityEngine.SceneManagement", "UnityEngine.Tilemaps", "UnityEngine.U2D", "UnityEngine.Video", "UnityEditor", "UnityEditor.AI", "UnityEditor.Animations", "UnityEditor.Experimental.GraphView", "UnityEditor.IMGUI.Controls", "UnityEditor.PackageManager.UI", "UnityEditor.SceneManagement", "UnityEditor.UI", "UnityEditor.U2D", "UnityEditor.VersionControl" }; // Add more relevant namespaces
|
||||
|
||||
foreach (string ns in namespaces) {
|
||||
type = Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}.CoreModule") ?? // Heuristic: Check CoreModule first for UnityEngine/UnityEditor
|
||||
Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}"); // Try assembly matching namespace root
|
||||
if (type != null) return type;
|
||||
}
|
||||
|
||||
|
||||
// If not found, search all loaded assemblies (slower, last resort)
|
||||
// Prioritize assemblies likely to contain game/editor types
|
||||
Assembly[] priorityAssemblies = {
|
||||
Assembly.Load("Assembly-CSharp"), // Main game scripts
|
||||
Assembly.Load("Assembly-CSharp-Editor"), // Main editor scripts
|
||||
// Add other important project assemblies if known
|
||||
};
|
||||
foreach (var assembly in priorityAssemblies.Where(a => a != null))
|
||||
{
|
||||
type = assembly.GetType(typeName) ?? assembly.GetType("UnityEngine." + typeName) ?? assembly.GetType("UnityEditor." + typeName);
|
||||
if (type != null) return type;
|
||||
}
|
||||
|
||||
// Search remaining assemblies
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Except(priorityAssemblies))
|
||||
if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
|
||||
{
|
||||
try { // Protect against assembly loading errors
|
||||
type = assembly.GetType(typeName);
|
||||
if (type != null) return type;
|
||||
// Also check with common namespaces if simple name given
|
||||
foreach (string ns in namespaces) {
|
||||
type = assembly.GetType($"{ns}.{typeName}");
|
||||
if (type != null) return type;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[FindType] Error searching assembly {assembly.FullName}: {ex.Message}");
|
||||
}
|
||||
return resolvedType;
|
||||
}
|
||||
|
||||
// Log the resolver error if type wasn't found
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Debug.LogWarning($"[FindType] {error}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Robust component resolver that avoids Assembly.LoadFrom and supports assembly definitions.
|
||||
/// Prioritizes runtime (Player) assemblies over Editor assemblies.
|
||||
/// </summary>
|
||||
internal static class ComponentResolver
|
||||
{
|
||||
private static readonly Dictionary<string, Type> CacheByFqn = new(StringComparer.Ordinal);
|
||||
private static readonly Dictionary<string, Type> CacheByName = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a Component/MonoBehaviour type by short or fully-qualified name.
|
||||
/// Prefers runtime (Player) script assemblies; falls back to Editor assemblies.
|
||||
/// Never uses Assembly.LoadFrom.
|
||||
/// </summary>
|
||||
public static bool TryResolve(string nameOrFullName, out Type type, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
type = null!;
|
||||
|
||||
// Handle null/empty input
|
||||
if (string.IsNullOrWhiteSpace(nameOrFullName))
|
||||
{
|
||||
error = "Component name cannot be null or empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[FindType] Type not found after extensive search: '{typeName}'");
|
||||
return null; // Not found
|
||||
// 1) Exact cache hits
|
||||
if (CacheByFqn.TryGetValue(nameOrFullName, out type)) return true;
|
||||
if (!nameOrFullName.Contains(".") && CacheByName.TryGetValue(nameOrFullName, out type)) return true;
|
||||
type = Type.GetType(nameOrFullName, throwOnError: false);
|
||||
if (IsValidComponent(type)) { Cache(type); return true; }
|
||||
|
||||
// 2) Search loaded assemblies (prefer Player assemblies)
|
||||
var candidates = FindCandidates(nameOrFullName);
|
||||
if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; }
|
||||
if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; }
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// 3) Last resort: Editor-only TypeCache (fast index)
|
||||
var tc = TypeCache.GetTypesDerivedFrom<Component>()
|
||||
.Where(t => NamesMatch(t, nameOrFullName));
|
||||
candidates = PreferPlayer(tc).ToList();
|
||||
if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; }
|
||||
if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; }
|
||||
#endif
|
||||
|
||||
error = $"Component type '{nameOrFullName}' not found in loaded runtime assemblies. " +
|
||||
"Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled.";
|
||||
type = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool NamesMatch(Type t, string q) =>
|
||||
t.Name.Equals(q, StringComparison.Ordinal) ||
|
||||
(t.FullName?.Equals(q, StringComparison.Ordinal) ?? false);
|
||||
|
||||
private static bool IsValidComponent(Type t) =>
|
||||
t != null && typeof(Component).IsAssignableFrom(t);
|
||||
|
||||
private static void Cache(Type t)
|
||||
{
|
||||
if (t.FullName != null) CacheByFqn[t.FullName] = t;
|
||||
CacheByName[t.Name] = t;
|
||||
}
|
||||
|
||||
private static List<Type> FindCandidates(string query)
|
||||
{
|
||||
bool isShort = !query.Contains('.');
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Names of Player (runtime) script assemblies (asmdefs + Assembly-CSharp)
|
||||
var playerAsmNames = new HashSet<string>(
|
||||
UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
IEnumerable<System.Reflection.Assembly> playerAsms = loaded.Where(a => playerAsmNames.Contains(a.GetName().Name));
|
||||
IEnumerable<System.Reflection.Assembly> editorAsms = loaded.Except(playerAsms);
|
||||
#else
|
||||
IEnumerable<System.Reflection.Assembly> playerAsms = loaded;
|
||||
IEnumerable<System.Reflection.Assembly> editorAsms = Array.Empty<System.Reflection.Assembly>();
|
||||
#endif
|
||||
static IEnumerable<Type> SafeGetTypes(System.Reflection.Assembly a)
|
||||
{
|
||||
try { return a.GetTypes(); }
|
||||
catch (ReflectionTypeLoadException rtle) { return rtle.Types.Where(t => t != null)!; }
|
||||
}
|
||||
|
||||
Func<Type, bool> match = isShort
|
||||
? (t => t.Name.Equals(query, StringComparison.Ordinal))
|
||||
: (t => t.FullName!.Equals(query, StringComparison.Ordinal));
|
||||
|
||||
var fromPlayer = playerAsms.SelectMany(SafeGetTypes)
|
||||
.Where(IsValidComponent)
|
||||
.Where(match);
|
||||
var fromEditor = editorAsms.SelectMany(SafeGetTypes)
|
||||
.Where(IsValidComponent)
|
||||
.Where(match);
|
||||
|
||||
var list = new List<Type>(fromPlayer);
|
||||
if (list.Count == 0) list.AddRange(fromEditor);
|
||||
return list;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private static IEnumerable<Type> PreferPlayer(IEnumerable<Type> seq)
|
||||
{
|
||||
var player = new HashSet<string>(
|
||||
UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
return seq.OrderBy(t => player.Contains(t.Assembly.GetName().Name) ? 0 : 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
private static string Ambiguity(string query, IEnumerable<Type> cands)
|
||||
{
|
||||
var lines = cands.Select(t => $"{t.FullName} (assembly {t.Assembly.GetName().Name})");
|
||||
return $"Multiple component types matched '{query}':\n - " + string.Join("\n - ", lines) +
|
||||
"\nProvide a fully qualified type name to disambiguate.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JArray like [x, y, z] into a Vector3.
|
||||
/// Gets all accessible property and field names from a component type.
|
||||
/// </summary>
|
||||
private static Vector3? ParseVector3(JArray array)
|
||||
public static List<string> GetAllComponentProperties(Type componentType)
|
||||
{
|
||||
if (array != null && array.Count == 3)
|
||||
if (componentType == null) return new List<string>();
|
||||
|
||||
var properties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.CanWrite)
|
||||
.Select(p => p.Name);
|
||||
|
||||
var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(f => !f.IsInitOnly && !f.IsLiteral)
|
||||
.Select(f => f.Name);
|
||||
|
||||
// Also include SerializeField private fields (common in Unity)
|
||||
var serializeFields = componentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.Where(f => f.GetCustomAttribute<SerializeField>() != null)
|
||||
.Select(f => f.Name);
|
||||
|
||||
return properties.Concat(fields).Concat(serializeFields).Distinct().OrderBy(x => x).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses AI to suggest the most likely property matches for a user's input.
|
||||
/// </summary>
|
||||
public static List<string> GetAIPropertySuggestions(string userInput, List<string> availableProperties)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput) || !availableProperties.Any())
|
||||
return new List<string>();
|
||||
|
||||
// Simple caching to avoid repeated AI calls for the same input
|
||||
var cacheKey = $"{userInput.ToLowerInvariant()}:{string.Join(",", availableProperties)}";
|
||||
if (PropertySuggestionCache.TryGetValue(cacheKey, out var cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
var prompt = $"A Unity developer is trying to set a component property but used an incorrect name.\n\n" +
|
||||
$"User requested: \"{userInput}\"\n" +
|
||||
$"Available properties: [{string.Join(", ", availableProperties)}]\n\n" +
|
||||
$"Find 1-3 most likely matches considering:\n" +
|
||||
$"- Unity Inspector display names vs actual field names (e.g., \"Max Reach Distance\" → \"maxReachDistance\")\n" +
|
||||
$"- camelCase vs PascalCase vs spaces\n" +
|
||||
$"- Similar meaning/semantics\n" +
|
||||
$"- Common Unity naming patterns\n\n" +
|
||||
$"Return ONLY the matching property names, comma-separated, no quotes or explanation.\n" +
|
||||
$"If confidence is low (<70%), return empty string.\n\n" +
|
||||
$"Examples:\n" +
|
||||
$"- \"Max Reach Distance\" → \"maxReachDistance\"\n" +
|
||||
$"- \"Health Points\" → \"healthPoints, hp\"\n" +
|
||||
$"- \"Move Speed\" → \"moveSpeed, movementSpeed\"";
|
||||
|
||||
// For now, we'll use a simple rule-based approach that mimics AI behavior
|
||||
// This can be replaced with actual AI calls later
|
||||
var suggestions = GetRuleBasedSuggestions(userInput, availableProperties);
|
||||
|
||||
PropertySuggestionCache[cacheKey] = suggestions;
|
||||
return suggestions;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[AI Property Matching] Error getting suggestions for '{userInput}': {ex.Message}");
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, List<string>> PropertySuggestionCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Rule-based suggestions that mimic AI behavior for property matching.
|
||||
/// This provides immediate value while we could add real AI integration later.
|
||||
/// </summary>
|
||||
private static List<string> GetRuleBasedSuggestions(string userInput, List<string> availableProperties)
|
||||
{
|
||||
var suggestions = new List<string>();
|
||||
var cleanedInput = userInput.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", "");
|
||||
|
||||
foreach (var property in availableProperties)
|
||||
{
|
||||
var cleanedProperty = property.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", "");
|
||||
|
||||
// Exact match after cleaning
|
||||
if (cleanedProperty == cleanedInput)
|
||||
{
|
||||
// Use ToObject for potentially better handling than direct indexing
|
||||
return new Vector3(
|
||||
array[0].ToObject<float>(),
|
||||
array[1].ToObject<float>(),
|
||||
array[2].ToObject<float>()
|
||||
);
|
||||
suggestions.Add(property);
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
// Check if property contains all words from input
|
||||
var inputWords = userInput.ToLowerInvariant().Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (inputWords.All(word => cleanedProperty.Contains(word.ToLowerInvariant())))
|
||||
{
|
||||
Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}");
|
||||
suggestions.Add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Levenshtein distance for close matches
|
||||
if (LevenshteinDistance(cleanedInput, cleanedProperty) <= Math.Max(2, cleanedInput.Length / 4))
|
||||
{
|
||||
suggestions.Add(property);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
// Prioritize exact matches, then by similarity
|
||||
return suggestions.OrderBy(s => LevenshteinDistance(cleanedInput, s.ToLowerInvariant().Replace(" ", "")))
|
||||
.Take(3)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Levenshtein distance between two strings for similarity matching.
|
||||
/// </summary>
|
||||
private static int LevenshteinDistance(string s1, string s2)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s1)) return s2?.Length ?? 0;
|
||||
if (string.IsNullOrEmpty(s2)) return s1.Length;
|
||||
|
||||
var matrix = new int[s1.Length + 1, s2.Length + 1];
|
||||
|
||||
for (int i = 0; i <= s1.Length; i++) matrix[i, 0] = i;
|
||||
for (int j = 0; j <= s2.Length; j++) matrix[0, j] = j;
|
||||
|
||||
for (int i = 1; i <= s1.Length; i++)
|
||||
{
|
||||
for (int j = 1; j <= s2.Length; j++)
|
||||
{
|
||||
int cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1;
|
||||
matrix[i, j] = Math.Min(Math.Min(
|
||||
matrix[i - 1, j] + 1, // deletion
|
||||
matrix[i, j - 1] + 1), // insertion
|
||||
matrix[i - 1, j - 1] + cost); // substitution
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[s1.Length, s2.Length];
|
||||
}
|
||||
|
||||
// Removed duplicate ParseVector3 - using the one at line 1114
|
||||
|
||||
// Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup.
|
||||
// They are now in Helpers.GameObjectSerializer
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue