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": "",
|
"rootNamespace": "",
|
||||||
"references": [
|
"references": [
|
||||||
"MCPForUnity.Editor",
|
"MCPForUnity.Editor",
|
||||||
|
"TestAsmdef",
|
||||||
"UnityEngine.TestRunner",
|
"UnityEngine.TestRunner",
|
||||||
"UnityEditor.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 UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Helpers; // For Response class
|
using MCPForUnity.Editor.Helpers; // For Response class
|
||||||
|
using static MCPForUnity.Editor.Tools.ManageGameObject;
|
||||||
|
|
||||||
#if UNITY_6000_0_OR_NEWER
|
#if UNITY_6000_0_OR_NEWER
|
||||||
using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
|
using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
|
||||||
|
|
@ -201,15 +202,16 @@ namespace MCPForUnity.Editor.Tools
|
||||||
"'scriptClass' property required when creating ScriptableObject asset."
|
"'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 (
|
if (
|
||||||
scriptType == null
|
scriptType == null
|
||||||
|| !typeof(ScriptableObject).IsAssignableFrom(scriptType)
|
|| !typeof(ScriptableObject).IsAssignableFrom(scriptType)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return Response.Error(
|
var reason = scriptType == null
|
||||||
$"Script class '{scriptClassName}' not found or does not inherit from ScriptableObject."
|
? (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);
|
ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
|
||||||
|
|
@ -353,10 +355,21 @@ namespace MCPForUnity.Editor.Tools
|
||||||
&& componentProperties.HasValues
|
&& componentProperties.HasValues
|
||||||
) // e.g., {"bobSpeed": 2.0}
|
) // e.g., {"bobSpeed": 2.0}
|
||||||
{
|
{
|
||||||
// Find the component on the GameObject using the name from the JSON key
|
// Resolve component type via ComponentResolver, then fetch by Type
|
||||||
// Using GetComponent(string) is convenient but might require exact type name or be ambiguous.
|
Component targetComponent = null;
|
||||||
// Consider using FindType helper if needed for more complex scenarios.
|
bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError);
|
||||||
Component targetComponent = gameObject.GetComponent(componentName);
|
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)
|
if (targetComponent != null)
|
||||||
{
|
{
|
||||||
|
|
@ -937,8 +950,8 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
string propName = floatProps["name"]?.ToString();
|
string propName = floatProps["name"]?.ToString();
|
||||||
if (
|
if (
|
||||||
!string.IsNullOrEmpty(propName) && floatProps["value"]?.Type == JTokenType.Float
|
!string.IsNullOrEmpty(propName) &&
|
||||||
|| floatProps["value"]?.Type == JTokenType.Integer
|
(floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
try
|
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 ---
|
// --- Data Serialization ---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -5,6 +6,7 @@ using System.Reflection;
|
||||||
using Newtonsoft.Json; // Added for JsonSerializationException
|
using Newtonsoft.Json; // Added for JsonSerializationException
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
|
using UnityEditor.Compilation; // For CompilationPipeline
|
||||||
using UnityEditor.SceneManagement;
|
using UnityEditor.SceneManagement;
|
||||||
using UnityEditorInternal;
|
using UnityEditorInternal;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
@ -19,10 +21,29 @@ namespace MCPForUnity.Editor.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ManageGameObject
|
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 ---
|
// --- Main Handler ---
|
||||||
|
|
||||||
public static object HandleCommand(JObject @params)
|
public static object HandleCommand(JObject @params)
|
||||||
{
|
{
|
||||||
|
if (@params == null)
|
||||||
|
{
|
||||||
|
return Response.Error("Parameters cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
string action = @params["action"]?.ToString().ToLower();
|
string action = @params["action"]?.ToString().ToLower();
|
||||||
if (string.IsNullOrEmpty(action))
|
if (string.IsNullOrEmpty(action))
|
||||||
|
|
@ -283,11 +304,16 @@ namespace MCPForUnity.Editor.Tools
|
||||||
newGo = GameObject.CreatePrimitive(type);
|
newGo = GameObject.CreatePrimitive(type);
|
||||||
// Set name *after* creation for primitives
|
// Set name *after* creation for primitives
|
||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
newGo.name = name;
|
newGo.name = name;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(newGo); // cleanup leak
|
||||||
return Response.Error(
|
return Response.Error(
|
||||||
"'name' parameter is required when creating a primitive."
|
"'name' parameter is required when creating a primitive."
|
||||||
); // Name is essential
|
);
|
||||||
|
}
|
||||||
createdNewObject = true;
|
createdNewObject = true;
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
|
|
@ -759,6 +785,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Component Properties
|
// Set Component Properties
|
||||||
|
var componentErrors = new List<object>();
|
||||||
if (@params["componentProperties"] is JObject componentPropertiesObj)
|
if (@params["componentProperties"] is JObject componentPropertiesObj)
|
||||||
{
|
{
|
||||||
foreach (var prop in componentPropertiesObj.Properties())
|
foreach (var prop in componentPropertiesObj.Properties())
|
||||||
|
|
@ -773,12 +800,26 @@ namespace MCPForUnity.Editor.Tools
|
||||||
propertiesToSet
|
propertiesToSet
|
||||||
);
|
);
|
||||||
if (setResult != null)
|
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)
|
if (!modified)
|
||||||
{
|
{
|
||||||
// Use the new serializer helper
|
// Use the new serializer helper
|
||||||
|
|
@ -1097,6 +1138,29 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
// --- Internal Helpers ---
|
// --- 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>
|
/// <summary>
|
||||||
/// Finds a single GameObject based on token (ID, name, path) and search method.
|
/// Finds a single GameObject based on token (ID, name, path) and search method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1464,7 +1528,18 @@ namespace MCPForUnity.Editor.Tools
|
||||||
Component targetComponentInstance = null
|
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)
|
if (targetComponent == null)
|
||||||
{
|
{
|
||||||
return Response.Error(
|
return Response.Error(
|
||||||
|
|
@ -1474,6 +1549,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
Undo.RecordObject(targetComponent, "Set Component Properties");
|
Undo.RecordObject(targetComponent, "Set Component Properties");
|
||||||
|
|
||||||
|
var failures = new List<string>();
|
||||||
foreach (var prop in propertiesToSet.Properties())
|
foreach (var prop in propertiesToSet.Properties())
|
||||||
{
|
{
|
||||||
string propName = prop.Name;
|
string propName = prop.Name;
|
||||||
|
|
@ -1481,14 +1557,16 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!SetProperty(targetComponent, propName, propValue))
|
bool setResult = SetProperty(targetComponent, propName, propValue);
|
||||||
|
if (!setResult)
|
||||||
{
|
{
|
||||||
// Log warning if property could not be set
|
var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType());
|
||||||
Debug.LogWarning(
|
var suggestions = ComponentResolver.GetAIPropertySuggestions(propName, availableProperties);
|
||||||
$"[ManageGameObject] Could not set property '{propName}' on component '{compName}' ('{targetComponent.GetType().Name}'). Property might not exist, be read-only, or type mismatch."
|
var msg = suggestions.Any()
|
||||||
);
|
? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]"
|
||||||
// Optionally return an error here instead of just logging
|
: $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]";
|
||||||
// return Response.Error($"Could not set property '{propName}' on component '{compName}'.");
|
Debug.LogWarning($"[ManageGameObject] {msg}");
|
||||||
|
failures.Add(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -1496,12 +1574,13 @@ namespace MCPForUnity.Editor.Tools
|
||||||
Debug.LogError(
|
Debug.LogError(
|
||||||
$"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}"
|
$"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}"
|
||||||
);
|
);
|
||||||
// Optionally return an error here
|
failures.Add($"Error setting '{propName}': {e.Message}");
|
||||||
// return Response.Error($"Error setting property '{propName}' on '{compName}': {e.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditorUtility.SetDirty(targetComponent);
|
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>
|
/// <summary>
|
||||||
|
|
@ -1513,25 +1592,8 @@ namespace MCPForUnity.Editor.Tools
|
||||||
BindingFlags flags =
|
BindingFlags flags =
|
||||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||||
|
|
||||||
// --- Use a dedicated serializer for input conversion ---
|
// Use shared serializer to avoid per-call allocation
|
||||||
// Define this somewhere accessible, maybe static readonly field
|
var inputSerializer = InputSerializer;
|
||||||
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 ---
|
|
||||||
|
|
||||||
try
|
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)}");
|
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)
|
catch (Exception ex)
|
||||||
|
|
@ -2070,84 +2146,288 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private static Type FindType(string typeName)
|
private static Type FindType(string typeName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(typeName))
|
if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
try { // Protect against assembly loading errors
|
return resolvedType;
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogWarning($"[FindType] Type not found after extensive search: '{typeName}'");
|
// Log the resolver error if type wasn't found
|
||||||
return null; // Not 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>
|
/// <summary>
|
||||||
/// Parses a JArray like [x, y, z] into a Vector3.
|
/// Gets all accessible property and field names from a component type.
|
||||||
/// </summary>
|
/// </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
|
suggestions.Add(property);
|
||||||
return new Vector3(
|
continue;
|
||||||
array[0].ToObject<float>(),
|
|
||||||
array[1].ToObject<float>(),
|
|
||||||
array[2].ToObject<float>()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
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.
|
// Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup.
|
||||||
// They are now in Helpers.GameObjectSerializer
|
// They are now in Helpers.GameObjectSerializer
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue