Material tools: support direct shader property keys + add EditMode coverage (#344)
* Add TDD tests for MCP material management issues - MCPMaterialTests.cs: Tests for material creation, assignment, and data reading - MCPParameterHandlingTests.cs: Tests for JSON parameter parsing issues - SphereMaterialWorkflowTests.cs: Tests for complete sphere material workflow These tests document the current issues with: - JSON parameter parsing in manage_asset and manage_gameobject tools - Material creation with properties - Material assignment to GameObjects - Material component data reading All tests currently fail (Red phase of TDD) and serve as specifications for what needs to be fixed in the MCP system. * Refine TDD tests to focus on actual MCP tool parameter parsing issues - Removed redundant tests that verify working functionality (GameObjectSerializer, Unity APIs) - Kept focused tests that document the real issue: MCP tool parameter validation - Tests now clearly identify the root cause: JSON string parsing in MCP tools - Tests specify exactly what needs to be fixed: parameter type flexibility The issue is NOT in Unity APIs or serialization (which work fine), but in MCP tool parameter validation being too strict. * Fix port discovery protocol mismatch - Update _try_probe_unity_mcp to recognize Unity bridge welcome message - Unity bridge sends 'WELCOME UNITY-MCP' instead of JSON pong response - Maintains backward compatibility with JSON pong format - Fixes MCP server connection to Unity Editor * Resolve merge: unify manage_gameobject param coercion and schema widening * Tests: add MaterialParameterToolTests; merge port probe fix; widen tool schemas for JSON-string params * feat(material): support direct shader property keys and texture paths; add EditMode tests * chore(tests): track required .meta files; remove ephemeral Assets/Editor test helper * fix(manage_gameobject): validate parsed component_properties is a dict; return clear errormain
parent
a0287afbbc
commit
075d68dba3
|
|
@ -64,7 +64,19 @@ namespace MCPForUnity.Editor.Tools
|
||||||
string path = @params["path"]?.ToString();
|
string path = @params["path"]?.ToString();
|
||||||
|
|
||||||
// Coerce string JSON to JObject for 'properties' if provided as a JSON string
|
// Coerce string JSON to JObject for 'properties' if provided as a JSON string
|
||||||
JsonUtil.CoerceJsonStringParameter(@params, "properties");
|
var propertiesToken = @params["properties"];
|
||||||
|
if (propertiesToken != null && propertiesToken.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JObject.Parse(propertiesToken.ToString());
|
||||||
|
@params["properties"] = parsed;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ManageAsset] Could not parse 'properties' JSON string: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -1002,6 +1014,107 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Flexible direct property assignment ---
|
||||||
|
// Allow payloads like: { "_Color": [r,g,b,a] }, { "_Glossiness": 0.5 }, { "_MainTex": "Assets/.." }
|
||||||
|
// while retaining backward compatibility with the structured keys above.
|
||||||
|
// This iterates all top-level keys except the reserved structured ones and applies them
|
||||||
|
// if they match known shader properties.
|
||||||
|
var reservedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "shader", "color", "float", "texture" };
|
||||||
|
|
||||||
|
// Helper resolves common URP/Standard aliasing (e.g., _Color <-> _BaseColor, _MainTex <-> _BaseMap, _Glossiness <-> _Smoothness)
|
||||||
|
string ResolvePropertyName(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name)) return name;
|
||||||
|
string[] candidates;
|
||||||
|
switch (name)
|
||||||
|
{
|
||||||
|
case "_Color": candidates = new[] { "_Color", "_BaseColor" }; break;
|
||||||
|
case "_BaseColor": candidates = new[] { "_BaseColor", "_Color" }; break;
|
||||||
|
case "_MainTex": candidates = new[] { "_MainTex", "_BaseMap" }; break;
|
||||||
|
case "_BaseMap": candidates = new[] { "_BaseMap", "_MainTex" }; break;
|
||||||
|
case "_Glossiness": candidates = new[] { "_Glossiness", "_Smoothness" }; break;
|
||||||
|
case "_Smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break;
|
||||||
|
default: candidates = new[] { name }; break;
|
||||||
|
}
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (mat.HasProperty(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
return name; // fall back to original
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var prop in properties.Properties())
|
||||||
|
{
|
||||||
|
if (reservedKeys.Contains(prop.Name)) continue;
|
||||||
|
string shaderProp = ResolvePropertyName(prop.Name);
|
||||||
|
JToken v = prop.Value;
|
||||||
|
|
||||||
|
// Color: numeric array [r,g,b,(a)]
|
||||||
|
if (v is JArray arr && arr.Count >= 3 && arr.All(t => t.Type == JTokenType.Float || t.Type == JTokenType.Integer))
|
||||||
|
{
|
||||||
|
if (mat.HasProperty(shaderProp))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var c = new Color(
|
||||||
|
arr[0].ToObject<float>(),
|
||||||
|
arr[1].ToObject<float>(),
|
||||||
|
arr[2].ToObject<float>(),
|
||||||
|
arr.Count > 3 ? arr[3].ToObject<float>() : 1f
|
||||||
|
);
|
||||||
|
if (mat.GetColor(shaderProp) != c)
|
||||||
|
{
|
||||||
|
mat.SetColor(shaderProp, c);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Error setting color '{shaderProp}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float: single number
|
||||||
|
if (v.Type == JTokenType.Float || v.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
if (mat.HasProperty(shaderProp))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
float f = v.ToObject<float>();
|
||||||
|
if (!Mathf.Approximately(mat.GetFloat(shaderProp), f))
|
||||||
|
{
|
||||||
|
mat.SetFloat(shaderProp, f);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Error setting float '{shaderProp}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texture: string path
|
||||||
|
if (v.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
string texPath = v.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(texPath) && mat.HasProperty(shaderProp))
|
||||||
|
{
|
||||||
|
var tex = AssetDatabase.LoadAssetAtPath<Texture>(AssetPathUtility.SanitizeAssetPath(texPath));
|
||||||
|
if (tex != null && mat.GetTexture(shaderProp) != tex)
|
||||||
|
{
|
||||||
|
mat.SetTexture(shaderProp, tex);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
|
// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
|
||||||
return modified;
|
return modified;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,19 @@ namespace MCPForUnity.Editor.Tools
|
||||||
// --- End add parameter ---
|
// --- End add parameter ---
|
||||||
|
|
||||||
// Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string
|
// Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string
|
||||||
JsonUtil.CoerceJsonStringParameter(@params, "componentProperties");
|
var componentPropsToken = @params["componentProperties"];
|
||||||
|
if (componentPropsToken != null && componentPropsToken.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JObject.Parse(componentPropsToken.ToString());
|
||||||
|
@params["componentProperties"] = parsed;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ManageGameObject] Could not parse 'componentProperties' JSON string: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Prefab Redirection Check ---
|
// --- Prefab Redirection Check ---
|
||||||
string targetPath =
|
string targetPath =
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ async def manage_asset(
|
||||||
try:
|
try:
|
||||||
properties = json.loads(properties)
|
properties = json.loads(properties)
|
||||||
ctx.info("manage_asset: coerced properties from JSON string to dict")
|
ctx.info("manage_asset: coerced properties from JSON string to dict")
|
||||||
except json.JSONDecodeError as e:
|
except Exception as e:
|
||||||
ctx.warn(f"manage_asset: failed to parse properties JSON string: {e}")
|
ctx.warn(f"manage_asset: failed to parse properties JSON string: {e}")
|
||||||
# Leave properties as-is; Unity side may handle defaults
|
# Leave properties as-is; Unity side may handle defaults
|
||||||
# Ensure properties is a dict if None
|
# Ensure properties is a dict if None
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ def manage_gameobject(
|
||||||
"List of component names to remove"] | None = None,
|
"List of component names to remove"] | None = None,
|
||||||
component_properties: Annotated[dict[str, dict[str, Any]] | str,
|
component_properties: Annotated[dict[str, dict[str, Any]] | str,
|
||||||
"""Dictionary of component names to their properties to set. For example:
|
"""Dictionary of component names to their properties to set. For example:
|
||||||
Can also be provided as a JSON string representation of the dict.
|
|
||||||
`{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
|
`{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
|
||||||
`{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
|
`{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
|
||||||
Example set nested property:
|
Example set nested property:
|
||||||
|
|
@ -122,6 +121,9 @@ def manage_gameobject(
|
||||||
ctx.info("manage_gameobject: coerced component_properties from JSON string to dict")
|
ctx.info("manage_gameobject: coerced component_properties from JSON string to dict")
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
return {"success": False, "message": f"Invalid JSON in component_properties: {e}"}
|
return {"success": False, "message": f"Invalid JSON in component_properties: {e}"}
|
||||||
|
# Ensure final type is a dict (object) if provided
|
||||||
|
if component_properties is not None and not isinstance(component_properties, dict):
|
||||||
|
return {"success": False, "message": "component_properties must be a JSON object (dict)."}
|
||||||
try:
|
try:
|
||||||
# Map tag to search_term when search_method is by_tag for backward compatibility
|
# Map tag to search_term when search_method is by_tag for backward compatibility
|
||||||
if action == "find" and search_method == "by_tag" and tag is not None and search_term is None:
|
if action == "find" and search_method == "by_tag" and tag is not None and search_term is None:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0c392d9059b864f608a4d32e4347c3d6
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d5876265244e44b0dbea3a1351bf24be
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e6bcc993a43284ece98cc3f36f2dc8ab
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 91e200a37b70f45b28121bae6a351cf6
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -18,24 +18,21 @@ namespace Tests.EditMode
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MCPToolParameterTests
|
public class MCPToolParameterTests
|
||||||
{
|
{
|
||||||
private const string TempDir = "Assets/Temp/MCPToolParameterTests";
|
[Test]
|
||||||
|
public void Test_ManageAsset_ShouldAcceptJSONProperties()
|
||||||
[SetUp]
|
|
||||||
public void SetUp()
|
|
||||||
{
|
{
|
||||||
|
// Arrange: create temp folder
|
||||||
|
const string tempDir = "Assets/Temp/MCPToolParameterTests";
|
||||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||||
{
|
{
|
||||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||||
}
|
}
|
||||||
if (!AssetDatabase.IsValidFolder(TempDir))
|
if (!AssetDatabase.IsValidFolder(tempDir))
|
||||||
{
|
{
|
||||||
AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
|
AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
[Test]
|
var matPath = $"{tempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
|
||||||
public void Test_ManageAsset_ShouldAcceptJSONProperties()
|
|
||||||
{
|
|
||||||
var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
|
|
||||||
|
|
||||||
// Build params with properties as a JSON string (to be coerced)
|
// Build params with properties as a JSON string (to be coerced)
|
||||||
var p = new JObject
|
var p = new JObject
|
||||||
|
|
@ -73,7 +70,10 @@ namespace Tests.EditMode
|
||||||
[Test]
|
[Test]
|
||||||
public void Test_ManageGameObject_ShouldAcceptJSONComponentProperties()
|
public void Test_ManageGameObject_ShouldAcceptJSONComponentProperties()
|
||||||
{
|
{
|
||||||
var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
|
const string tempDir = "Assets/Temp/MCPToolParameterTests";
|
||||||
|
if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
|
||||||
|
if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
|
||||||
|
var matPath = $"{tempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
|
||||||
|
|
||||||
// Create a material first (object-typed properties)
|
// Create a material first (object-typed properties)
|
||||||
var createMat = new JObject
|
var createMat = new JObject
|
||||||
|
|
@ -121,7 +121,10 @@ namespace Tests.EditMode
|
||||||
[Test]
|
[Test]
|
||||||
public void Test_JSONParsing_ShouldWorkInMCPTools()
|
public void Test_JSONParsing_ShouldWorkInMCPTools()
|
||||||
{
|
{
|
||||||
var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
|
const string tempDir = "Assets/Temp/MCPToolParameterTests";
|
||||||
|
if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
|
||||||
|
if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
|
||||||
|
var matPath = $"{tempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
|
||||||
|
|
||||||
// manage_asset with JSON string properties (coercion path)
|
// manage_asset with JSON string properties (coercion path)
|
||||||
var createMat = new JObject
|
var createMat = new JObject
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using MCPForUnity.Editor.Tools;
|
||||||
|
|
||||||
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
|
{
|
||||||
|
public class MaterialDirectPropertiesTests
|
||||||
|
{
|
||||||
|
private const string TempRoot = "Assets/Temp/MaterialDirectPropertiesTests";
|
||||||
|
private string _matPath;
|
||||||
|
private string _baseMapPath;
|
||||||
|
private string _normalMapPath;
|
||||||
|
private string _occlusionMapPath;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||||
|
{
|
||||||
|
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||||
|
}
|
||||||
|
if (!AssetDatabase.IsValidFolder(TempRoot))
|
||||||
|
{
|
||||||
|
AssetDatabase.CreateFolder("Assets/Temp", "MaterialDirectPropertiesTests");
|
||||||
|
}
|
||||||
|
|
||||||
|
string guid = Guid.NewGuid().ToString("N");
|
||||||
|
_matPath = $"{TempRoot}/DirectProps_{guid}.mat";
|
||||||
|
_baseMapPath = $"{TempRoot}/TexBase_{guid}.asset";
|
||||||
|
_normalMapPath = $"{TempRoot}/TexNormal_{guid}.asset";
|
||||||
|
_occlusionMapPath = $"{TempRoot}/TexOcc_{guid}.asset";
|
||||||
|
|
||||||
|
// Clean any leftovers just in case
|
||||||
|
TryDeleteAsset(_matPath);
|
||||||
|
TryDeleteAsset(_baseMapPath);
|
||||||
|
TryDeleteAsset(_normalMapPath);
|
||||||
|
TryDeleteAsset(_occlusionMapPath);
|
||||||
|
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
TryDeleteAsset(_matPath);
|
||||||
|
TryDeleteAsset(_baseMapPath);
|
||||||
|
TryDeleteAsset(_normalMapPath);
|
||||||
|
TryDeleteAsset(_occlusionMapPath);
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDeleteAsset(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path)) return;
|
||||||
|
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
|
||||||
|
{
|
||||||
|
AssetDatabase.DeleteAsset(path);
|
||||||
|
}
|
||||||
|
var abs = Path.Combine(Directory.GetCurrentDirectory(), path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(abs)) File.Delete(abs);
|
||||||
|
if (File.Exists(abs + ".meta")) File.Delete(abs + ".meta");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture2D CreateSolidTextureAsset(string path, Color color)
|
||||||
|
{
|
||||||
|
var tex = new Texture2D(4, 4, TextureFormat.RGBA32, false);
|
||||||
|
var pixels = new Color[16];
|
||||||
|
for (int i = 0; i < pixels.Length; i++) pixels[i] = color;
|
||||||
|
tex.SetPixels(pixels);
|
||||||
|
tex.Apply();
|
||||||
|
AssetDatabase.CreateAsset(tex, path);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JObject ToJObject(object result)
|
||||||
|
{
|
||||||
|
return result as JObject ?? JObject.FromObject(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateAndModifyMaterial_WithDirectPropertyKeys_Works()
|
||||||
|
{
|
||||||
|
// Arrange: create textures as assets
|
||||||
|
CreateSolidTextureAsset(_baseMapPath, Color.white);
|
||||||
|
CreateSolidTextureAsset(_normalMapPath, new Color(0.5f, 0.5f, 1f));
|
||||||
|
CreateSolidTextureAsset(_occlusionMapPath, Color.gray);
|
||||||
|
|
||||||
|
// Create material using direct keys via JSON string
|
||||||
|
var createParams = new JObject
|
||||||
|
{
|
||||||
|
["action"] = "create",
|
||||||
|
["path"] = _matPath,
|
||||||
|
["assetType"] = "Material",
|
||||||
|
["properties"] = new JObject
|
||||||
|
{
|
||||||
|
["shader"] = "Universal Render Pipeline/Lit",
|
||||||
|
["_Color"] = new JArray(0f, 1f, 0f, 1f),
|
||||||
|
["_Glossiness"] = 0.25f
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var createRes = ToJObject(ManageAsset.HandleCommand(createParams));
|
||||||
|
Assert.IsTrue(createRes.Value<bool>("success"), createRes.ToString());
|
||||||
|
|
||||||
|
// Modify with aliases and textures
|
||||||
|
var modifyParams = new JObject
|
||||||
|
{
|
||||||
|
["action"] = "modify",
|
||||||
|
["path"] = _matPath,
|
||||||
|
["properties"] = new JObject
|
||||||
|
{
|
||||||
|
["_BaseColor"] = new JArray(0f, 0f, 1f, 1f),
|
||||||
|
["_Smoothness"] = 0.5f,
|
||||||
|
["_BaseMap"] = _baseMapPath,
|
||||||
|
["_BumpMap"] = _normalMapPath,
|
||||||
|
["_OcclusionMap"] = _occlusionMapPath
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var modifyRes = ToJObject(ManageAsset.HandleCommand(modifyParams));
|
||||||
|
Assert.IsTrue(modifyRes.Value<bool>("success"), modifyRes.ToString());
|
||||||
|
|
||||||
|
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||||
|
Assert.IsNotNull(mat, "Material should exist at path.");
|
||||||
|
|
||||||
|
// Verify color alias applied
|
||||||
|
if (mat.HasProperty("_BaseColor"))
|
||||||
|
{
|
||||||
|
Assert.AreEqual(Color.blue, mat.GetColor("_BaseColor"));
|
||||||
|
}
|
||||||
|
else if (mat.HasProperty("_Color"))
|
||||||
|
{
|
||||||
|
Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify float
|
||||||
|
string smoothProp = mat.HasProperty("_Smoothness") ? "_Smoothness" : (mat.HasProperty("_Glossiness") ? "_Glossiness" : null);
|
||||||
|
Assert.IsNotNull(smoothProp, "Material should expose Smoothness/Glossiness.");
|
||||||
|
Assert.That(Mathf.Abs(mat.GetFloat(smoothProp) - 0.5f) < 1e-4f);
|
||||||
|
|
||||||
|
// Verify textures
|
||||||
|
string baseMapProp = mat.HasProperty("_BaseMap") ? "_BaseMap" : (mat.HasProperty("_MainTex") ? "_MainTex" : null);
|
||||||
|
Assert.IsNotNull(baseMapProp, "Material should expose BaseMap/MainTex.");
|
||||||
|
Assert.IsNotNull(mat.GetTexture(baseMapProp), "BaseMap/MainTex should be assigned.");
|
||||||
|
if (mat.HasProperty("_BumpMap")) Assert.IsNotNull(mat.GetTexture("_BumpMap"));
|
||||||
|
if (mat.HasProperty("_OcclusionMap")) Assert.IsNotNull(mat.GetTexture("_OcclusionMap"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3623619548eef40568ac5e3cef4c22a5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -96,25 +96,13 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateTestMaterial()
|
[Test]
|
||||||
|
public void AssignMaterial_ToSphere_UsingComponentPropertiesObject_Succeeds()
|
||||||
{
|
{
|
||||||
var createParams = new JObject
|
// Ensure material exists first
|
||||||
{
|
CreateMaterial_WithObjectProperties_SucceedsAndSetsColor();
|
||||||
["action"] = "create",
|
|
||||||
["path"] = _matPath,
|
|
||||||
["assetType"] = "Material",
|
|
||||||
["properties"] = new JObject
|
|
||||||
{
|
|
||||||
["shader"] = "Universal Render Pipeline/Lit",
|
|
||||||
["color"] = new JArray(0f, 0f, 1f, 1f)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var result = ToJObject(ManageAsset.HandleCommand(createParams));
|
|
||||||
Assert.IsTrue(result.Value<bool>("success"), result.Value<string>("error"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateSphere()
|
// Create a sphere via handler
|
||||||
{
|
|
||||||
var createGo = new JObject
|
var createGo = new JObject
|
||||||
{
|
{
|
||||||
["action"] = "create",
|
["action"] = "create",
|
||||||
|
|
@ -123,18 +111,9 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
};
|
};
|
||||||
var createGoResult = ToJObject(ManageGameObject.HandleCommand(createGo));
|
var createGoResult = ToJObject(ManageGameObject.HandleCommand(createGo));
|
||||||
Assert.IsTrue(createGoResult.Value<bool>("success"), createGoResult.Value<string>("error"));
|
Assert.IsTrue(createGoResult.Value<bool>("success"), createGoResult.Value<string>("error"));
|
||||||
|
|
||||||
_sphere = GameObject.Find("ToolTestSphere");
|
_sphere = GameObject.Find("ToolTestSphere");
|
||||||
Assert.IsNotNull(_sphere, "Sphere should be created.");
|
Assert.IsNotNull(_sphere, "Sphere should be created.");
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void AssignMaterial_ToSphere_UsingComponentPropertiesObject_Succeeds()
|
|
||||||
{
|
|
||||||
CreateTestMaterial();
|
|
||||||
CreateSphere();
|
|
||||||
|
|
||||||
// Create a sphere via handler
|
|
||||||
|
|
||||||
|
|
||||||
// Assign material via object-typed componentProperties
|
// Assign material via object-typed componentProperties
|
||||||
var modifyParams = new JObject
|
var modifyParams = new JObject
|
||||||
|
|
@ -163,20 +142,8 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
[Test]
|
[Test]
|
||||||
public void ReadRendererData_DoesNotInstantiateMaterial_AndIncludesSharedMaterial()
|
public void ReadRendererData_DoesNotInstantiateMaterial_AndIncludesSharedMaterial()
|
||||||
{
|
{
|
||||||
CreateTestMaterial();
|
// Prepare object and assignment
|
||||||
CreateSphere();
|
AssignMaterial_ToSphere_UsingComponentPropertiesObject_Succeeds();
|
||||||
var modifyParams = new JObject
|
|
||||||
{
|
|
||||||
["action"] = "modify",
|
|
||||||
["target"] = "ToolTestSphere",
|
|
||||||
["searchMethod"] = "by_name",
|
|
||||||
["componentProperties"] = new JObject
|
|
||||||
{
|
|
||||||
["MeshRenderer"] = new JObject { ["sharedMaterial"] = _matPath }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var modifyResult = ToJObject(ManageGameObject.HandleCommand(modifyParams));
|
|
||||||
Assert.IsTrue(modifyResult.Value<bool>("success"), modifyResult.Value<string>("error"));
|
|
||||||
|
|
||||||
var renderer = _sphere.GetComponent<MeshRenderer>();
|
var renderer = _sphere.GetComponent<MeshRenderer>();
|
||||||
int beforeId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
|
int beforeId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.coplaydev.unity-mcp": "file:../../../MCPForUnity",
|
"com.coplaydev.unity-mcp": "file:/Users/davidsarno/unity-mcp/MCPForUnity",
|
||||||
"com.unity.ai.navigation": "1.1.4",
|
"com.unity.ai.navigation": "1.1.4",
|
||||||
"com.unity.collab-proxy": "2.5.2",
|
"com.unity.collab-proxy": "2.5.2",
|
||||||
"com.unity.feature.development": "1.0.1",
|
"com.unity.feature.development": "1.0.1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue