diff --git a/MCPForUnity/Editor/Helpers/MaterialOps.cs b/MCPForUnity/Editor/Helpers/MaterialOps.cs
new file mode 100644
index 000000000..0f80fcbb5
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/MaterialOps.cs
@@ -0,0 +1,397 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using UnityEngine;
+using UnityEditor;
+using MCPForUnity.Editor.Tools;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ public static class MaterialOps
+ {
+ ///
+ /// Applies a set of properties (JObject) to a material, handling aliases and structured formats.
+ ///
+ public static bool ApplyProperties(Material mat, JObject properties, JsonSerializer serializer)
+ {
+ if (mat == null || properties == null)
+ return false;
+ bool modified = false;
+
+ // Helper for case-insensitive lookup
+ JToken GetValue(string key)
+ {
+ return properties.Properties()
+ .FirstOrDefault(p => string.Equals(p.Name, key, StringComparison.OrdinalIgnoreCase))?.Value;
+ }
+
+ // --- Structured / Legacy Format Handling ---
+ // Example: Set shader
+ var shaderToken = GetValue("shader");
+ if (shaderToken?.Type == JTokenType.String)
+ {
+ string shaderRequest = shaderToken.ToString();
+ // Set shader
+ Shader newShader = RenderPipelineUtility.ResolveShader(shaderRequest);
+ if (newShader != null && mat.shader != newShader)
+ {
+ mat.shader = newShader;
+ modified = true;
+ }
+ }
+
+ // Example: Set color property (structured)
+ var colorToken = GetValue("color");
+ if (colorToken is JObject colorProps)
+ {
+ string propName = colorProps["name"]?.ToString() ?? GetMainColorPropertyName(mat);
+ if (colorProps["value"] is JArray colArr && colArr.Count >= 3)
+ {
+ try
+ {
+ Color newColor = ParseColor(colArr, serializer);
+ if (mat.HasProperty(propName))
+ {
+ if (mat.GetColor(propName) != newColor)
+ {
+ mat.SetColor(propName, newColor);
+ modified = true;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[MaterialOps] Failed to parse color for property '{propName}': {ex.Message}");
+ }
+ }
+ }
+ else if (colorToken is JArray colorArr) // Structured shorthand
+ {
+ string propName = GetMainColorPropertyName(mat);
+ try
+ {
+ Color newColor = ParseColor(colorArr, serializer);
+ if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
+ {
+ mat.SetColor(propName, newColor);
+ modified = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[MaterialOps] Failed to parse color array: {ex.Message}");
+ }
+ }
+
+ // Example: Set float property (structured)
+ var floatToken = GetValue("float");
+ if (floatToken is JObject floatProps)
+ {
+ string propName = floatProps["name"]?.ToString();
+ if (!string.IsNullOrEmpty(propName) &&
+ (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer))
+ {
+ try
+ {
+ float newVal = floatProps["value"].ToObject();
+ if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal)
+ {
+ mat.SetFloat(propName, newVal);
+ modified = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[MaterialOps] Failed to set float property '{propName}': {ex.Message}");
+ }
+ }
+ }
+
+ // Example: Set texture property (structured)
+ {
+ var texToken = GetValue("texture");
+ if (texToken is JObject texProps)
+ {
+ string rawName = (texProps["name"] ?? texProps["Name"])?.ToString();
+ string texPath = (texProps["path"] ?? texProps["Path"])?.ToString();
+ if (!string.IsNullOrEmpty(texPath))
+ {
+ var sanitizedPath = AssetPathUtility.SanitizeAssetPath(texPath);
+ var newTex = AssetDatabase.LoadAssetAtPath(sanitizedPath);
+ // Use ResolvePropertyName to handle aliases even for structured texture names
+ string candidateName = string.IsNullOrEmpty(rawName) ? "_BaseMap" : rawName;
+ string targetProp = ResolvePropertyName(mat, candidateName);
+
+ if (!string.IsNullOrEmpty(targetProp) && mat.HasProperty(targetProp))
+ {
+ if (mat.GetTexture(targetProp) != newTex)
+ {
+ mat.SetTexture(targetProp, newTex);
+ modified = true;
+ }
+ }
+ }
+ }
+ }
+
+ // --- Direct Property Assignment (Flexible) ---
+ var reservedKeys = new HashSet(StringComparer.OrdinalIgnoreCase) { "shader", "color", "float", "texture" };
+
+ foreach (var prop in properties.Properties())
+ {
+ if (reservedKeys.Contains(prop.Name)) continue;
+ string shaderProp = ResolvePropertyName(mat, prop.Name);
+ JToken v = prop.Value;
+
+ if (TrySetShaderProperty(mat, shaderProp, v, serializer))
+ {
+ modified = true;
+ }
+ }
+
+ return modified;
+ }
+
+ ///
+ /// Resolves common property aliases (e.g. "metallic" -> "_Metallic").
+ ///
+ public static string ResolvePropertyName(Material mat, string name)
+ {
+ if (mat == null || string.IsNullOrEmpty(name)) return name;
+ string[] candidates;
+ var lower = name.ToLowerInvariant();
+ switch (lower)
+ {
+ 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;
+ // Friendly names → shader property names
+ case "metallic": candidates = new[] { "_Metallic" }; break;
+ case "smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break;
+ case "albedo": candidates = new[] { "_BaseMap", "_MainTex" }; break;
+ default: candidates = new[] { name }; break; // keep original as-is
+ }
+ foreach (var candidate in candidates)
+ {
+ if (mat.HasProperty(candidate)) return candidate;
+ }
+ return name;
+ }
+
+ ///
+ /// Auto-detects the main color property name for a material's shader.
+ ///
+ public static string GetMainColorPropertyName(Material mat)
+ {
+ if (mat == null || mat.shader == null)
+ return "_Color";
+
+ string[] commonColorProps = { "_BaseColor", "_Color", "_MainColor", "_Tint", "_TintColor" };
+ foreach (var prop in commonColorProps)
+ {
+ if (mat.HasProperty(prop))
+ return prop;
+ }
+ return "_Color";
+ }
+
+ ///
+ /// Tries to set a shader property on a material based on a JToken value.
+ /// Handles Colors, Vectors, Floats, Ints, Booleans, and Textures.
+ ///
+ public static bool TrySetShaderProperty(Material material, string propertyName, JToken value, JsonSerializer serializer)
+ {
+ if (material == null || string.IsNullOrEmpty(propertyName) || value == null)
+ return false;
+
+ // Handle stringified JSON
+ if (value.Type == JTokenType.String)
+ {
+ string s = value.ToString();
+ if (s.TrimStart().StartsWith("[") || s.TrimStart().StartsWith("{"))
+ {
+ try
+ {
+ JToken parsed = JToken.Parse(s);
+ return TrySetShaderProperty(material, propertyName, parsed, serializer);
+ }
+ catch { }
+ }
+ }
+
+ // Use the serializer to convert the JToken value first
+ if (value is JArray jArray)
+ {
+ if (jArray.Count == 4)
+ {
+ if (material.HasProperty(propertyName))
+ {
+ try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; }
+ catch (Exception ex)
+ {
+ // Log at Debug level since we'll try other conversions
+ Debug.Log($"[MaterialOps] SetColor attempt for '{propertyName}' failed: {ex.Message}");
+ }
+
+ try { Vector4 vec = value.ToObject(serializer); material.SetVector(propertyName, vec); return true; }
+ catch (Exception ex)
+ {
+ Debug.Log($"[MaterialOps] SetVector (Vec4) attempt for '{propertyName}' failed: {ex.Message}");
+ }
+ }
+ }
+ else if (jArray.Count == 3)
+ {
+ if (material.HasProperty(propertyName))
+ {
+ try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; }
+ catch (Exception ex)
+ {
+ Debug.Log($"[MaterialOps] SetColor (Vec3) attempt for '{propertyName}' failed: {ex.Message}");
+ }
+ }
+ }
+ else if (jArray.Count == 2)
+ {
+ if (material.HasProperty(propertyName))
+ {
+ try { Vector2 vec = value.ToObject(serializer); material.SetVector(propertyName, vec); return true; }
+ catch (Exception ex)
+ {
+ Debug.Log($"[MaterialOps] SetVector (Vec2) attempt for '{propertyName}' failed: {ex.Message}");
+ }
+ }
+ }
+ }
+ else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
+ {
+ if (!material.HasProperty(propertyName))
+ return false;
+
+ try { material.SetFloat(propertyName, value.ToObject(serializer)); return true; }
+ catch (Exception ex)
+ {
+ Debug.Log($"[MaterialOps] SetFloat attempt for '{propertyName}' failed: {ex.Message}");
+ }
+ }
+ else if (value.Type == JTokenType.Boolean)
+ {
+ if (!material.HasProperty(propertyName))
+ return false;
+
+ try { material.SetFloat(propertyName, value.ToObject(serializer) ? 1f : 0f); return true; }
+ catch (Exception ex)
+ {
+ Debug.Log($"[MaterialOps] SetFloat (bool) attempt for '{propertyName}' failed: {ex.Message}");
+ }
+ }
+ else if (value.Type == JTokenType.String)
+ {
+ try
+ {
+ // Try loading as asset path first (most common case for strings in this context)
+ string path = value.ToString();
+ if (!string.IsNullOrEmpty(path) && path.Contains("/")) // Heuristic: paths usually have slashes
+ {
+ // We need to handle texture assignment here.
+ // Since we don't have easy access to AssetDatabase here directly without using UnityEditor namespace (which is imported),
+ // we can try to load it.
+ var sanitizedPath = AssetPathUtility.SanitizeAssetPath(path);
+ Texture tex = AssetDatabase.LoadAssetAtPath(sanitizedPath);
+ if (tex != null && material.HasProperty(propertyName))
+ {
+ material.SetTexture(propertyName, tex);
+ return true;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"SetTexture (string path) for '{propertyName}' failed: {ex.Message}");
+ }
+ }
+
+ if (value.Type == JTokenType.Object)
+ {
+ try
+ {
+ Texture texture = value.ToObject(serializer);
+ if (texture != null && material.HasProperty(propertyName))
+ {
+ material.SetTexture(propertyName, texture);
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"SetTexture (object) for '{propertyName}' failed: {ex.Message}");
+ }
+ }
+
+ Debug.LogWarning(
+ $"[MaterialOps] Unsupported or failed conversion for material property '{propertyName}' from value: {value.ToString(Formatting.None)}"
+ );
+ return false;
+ }
+
+ ///
+ /// Helper to parse color from JToken (array or object).
+ ///
+ public static Color ParseColor(JToken token, JsonSerializer serializer)
+ {
+ if (token.Type == JTokenType.String)
+ {
+ string s = token.ToString();
+ if (s.TrimStart().StartsWith("[") || s.TrimStart().StartsWith("{"))
+ {
+ try
+ {
+ return ParseColor(JToken.Parse(s), serializer);
+ }
+ catch { }
+ }
+ }
+
+ if (token is JArray jArray)
+ {
+ if (jArray.Count == 4)
+ {
+ return new Color(
+ (float)jArray[0],
+ (float)jArray[1],
+ (float)jArray[2],
+ (float)jArray[3]
+ );
+ }
+ else if (jArray.Count == 3)
+ {
+ return new Color(
+ (float)jArray[0],
+ (float)jArray[1],
+ (float)jArray[2],
+ 1f
+ );
+ }
+ else
+ {
+ throw new ArgumentException("Color array must have 3 or 4 elements.");
+ }
+ }
+
+ try
+ {
+ return token.ToObject(serializer);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[MaterialOps] Failed to parse color from token: {ex.Message}");
+ throw;
+ }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/MaterialOps.cs.meta b/MCPForUnity/Editor/Helpers/MaterialOps.cs.meta
new file mode 100644
index 000000000..9296369a4
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/MaterialOps.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a59e8545e32664dae9a696d449f82c3d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Tools/ManageAsset.cs b/MCPForUnity/Editor/Tools/ManageAsset.cs
index 7ca13ba1d..aee622ca9 100644
--- a/MCPForUnity/Editor/Tools/ManageAsset.cs
+++ b/MCPForUnity/Editor/Tools/ManageAsset.cs
@@ -213,7 +213,7 @@ private static object CreateAsset(JObject @params)
if (propertiesForApply.HasValues)
{
- ApplyMaterialProperties(mat, propertiesForApply);
+ MaterialOps.ApplyProperties(mat, propertiesForApply, ManageGameObject.InputSerializer);
}
}
AssetDatabase.CreateAsset(mat, fullPath);
@@ -441,7 +441,7 @@ prop.Value is JObject componentProperties
{
// Apply properties directly to the material. If this modifies, it sets modified=true.
// Use |= in case the asset was already marked modified by previous logic (though unlikely here)
- modified |= ApplyMaterialProperties(material, properties);
+ modified |= MaterialOps.ApplyProperties(material, properties, ManageGameObject.InputSerializer);
}
// Example: Modifying a ScriptableObject
else if (asset is ScriptableObject so)
@@ -895,299 +895,7 @@ private static void EnsureDirectoryExists(string directoryPath)
}
}
- ///
- /// Applies properties from JObject to a Material.
- ///
- private static bool ApplyMaterialProperties(Material mat, JObject properties)
- {
- if (mat == null || properties == null)
- return false;
- bool modified = false;
-
- // Example: Set shader
- if (properties["shader"]?.Type == JTokenType.String)
- {
- string shaderRequest = properties["shader"].ToString();
- Shader newShader = RenderPipelineUtility.ResolveShader(shaderRequest);
- if (newShader != null && mat.shader != newShader)
- {
- mat.shader = newShader;
- modified = true;
- }
- }
- // Example: Set color property
- if (properties["color"] is JObject colorProps)
- {
- string propName = colorProps["name"]?.ToString() ?? GetMainColorPropertyName(mat); // Auto-detect if not specified
- if (colorProps["value"] is JArray colArr && colArr.Count >= 3)
- {
- try
- {
- Color newColor = new Color(
- colArr[0].ToObject(),
- colArr[1].ToObject(),
- colArr[2].ToObject(),
- colArr.Count > 3 ? colArr[3].ToObject() : 1.0f
- );
- if (mat.HasProperty(propName))
- {
- if (mat.GetColor(propName) != newColor)
- {
- mat.SetColor(propName, newColor);
- modified = true;
- }
- }
- else
- {
- Debug.LogWarning(
- $"Material '{mat.name}' with shader '{mat.shader.name}' does not have color property '{propName}'. " +
- $"Color not applied. Common color properties: _BaseColor (URP), _Color (Standard)"
- );
- }
- }
- catch (Exception ex)
- {
- Debug.LogWarning(
- $"Error parsing color property '{propName}': {ex.Message}"
- );
- }
- }
- }
- else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
- {
- // Auto-detect the main color property for the shader
- string propName = GetMainColorPropertyName(mat);
- try
- {
- if (colorArr.Count >= 3)
- {
- Color newColor = new Color(
- colorArr[0].ToObject(),
- colorArr[1].ToObject(),
- colorArr[2].ToObject(),
- colorArr.Count > 3 ? colorArr[3].ToObject() : 1.0f
- );
- if (mat.HasProperty(propName))
- {
- if (mat.GetColor(propName) != newColor)
- {
- mat.SetColor(propName, newColor);
- modified = true;
- }
- }
- else
- {
- Debug.LogWarning(
- $"Material '{mat.name}' with shader '{mat.shader.name}' does not have color property '{propName}'. " +
- $"Color not applied. Common color properties: _BaseColor (URP), _Color (Standard)"
- );
- }
- }
- }
- catch (Exception ex)
- {
- Debug.LogWarning(
- $"Error parsing color property '{propName}': {ex.Message}"
- );
- }
- }
- // Example: Set float property
- if (properties["float"] is JObject floatProps)
- {
- string propName = floatProps["name"]?.ToString();
- if (
- !string.IsNullOrEmpty(propName) &&
- (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)
- )
- {
- try
- {
- float newVal = floatProps["value"].ToObject();
- if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal)
- {
- mat.SetFloat(propName, newVal);
- modified = true;
- }
- }
- catch (Exception ex)
- {
- Debug.LogWarning(
- $"Error parsing float property '{propName}': {ex.Message}"
- );
- }
- }
- }
- // Example: Set texture property (case-insensitive key and subkeys)
- {
- JObject texProps = null;
- var direct = properties.Property("texture");
- if (direct != null && direct.Value is JObject t0) texProps = t0;
- if (texProps == null)
- {
- var ci = properties.Properties().FirstOrDefault(
- p => string.Equals(p.Name, "texture", StringComparison.OrdinalIgnoreCase));
- if (ci != null && ci.Value is JObject t1) texProps = t1;
- }
- if (texProps != null)
- {
- string rawName = (texProps["name"] ?? texProps["Name"])?.ToString();
- string texPath = (texProps["path"] ?? texProps["Path"])?.ToString();
- if (!string.IsNullOrEmpty(texPath))
- {
- var newTex = AssetDatabase.LoadAssetAtPath(
- AssetPathUtility.SanitizeAssetPath(texPath));
- if (newTex == null)
- {
- Debug.LogWarning($"Texture not found at path: {texPath}");
- }
- else
- {
- // Reuse alias resolver so friendly names like 'albedo' work here too
- string candidateName = string.IsNullOrEmpty(rawName) ? "_BaseMap" : rawName;
- string targetProp = ResolvePropertyName(candidateName);
- if (!string.IsNullOrEmpty(targetProp) && mat.HasProperty(targetProp))
- {
- if (mat.GetTexture(targetProp) != newTex)
- {
- mat.SetTexture(targetProp, newTex);
- modified = true;
- }
- }
- }
- }
- }
- }
-
- // --- 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(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;
- var lower = name.ToLowerInvariant();
- switch (lower)
- {
- 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;
- // Friendly names → shader property names
- case "metallic": candidates = new[] { "_Metallic" }; break;
- case "smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break;
- case "albedo": candidates = new[] { "_BaseMap", "_MainTex" }; break;
- default: candidates = new[] { name }; break; // keep original as-is
- }
- 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(),
- arr[1].ToObject(),
- arr[2].ToObject(),
- arr.Count > 3 ? arr[3].ToObject() : 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();
- 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(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.)
- return modified;
- }
-
- ///
- /// Auto-detects the main color property name for a material's shader.
- /// Tries common color property names in order: _BaseColor (URP), _Color (Standard), etc.
- ///
- private static string GetMainColorPropertyName(Material mat)
- {
- if (mat == null || mat.shader == null)
- return "_Color";
-
- // Try common color property names in order of likelihood
- string[] commonColorProps = { "_BaseColor", "_Color", "_MainColor", "_Tint", "_TintColor" };
- foreach (var prop in commonColorProps)
- {
- if (mat.HasProperty(prop))
- return prop;
- }
-
- // Fallback to _Color if none found
- return "_Color";
- }
///
/// Applies properties from JObject to a PhysicsMaterial.
diff --git a/MCPForUnity/Editor/Tools/ManageGameObject.cs b/MCPForUnity/Editor/Tools/ManageGameObject.cs
index 8bfdfc08a..77f3fde52 100644
--- a/MCPForUnity/Editor/Tools/ManageGameObject.cs
+++ b/MCPForUnity/Editor/Tools/ManageGameObject.cs
@@ -23,7 +23,7 @@ namespace MCPForUnity.Editor.Tools
public static class ManageGameObject
{
// Shared JsonSerializer to avoid per-call allocation overhead
- private static readonly JsonSerializer InputSerializer = JsonSerializer.Create(new JsonSerializerSettings
+ internal static readonly JsonSerializer InputSerializer = JsonSerializer.Create(new JsonSerializerSettings
{
Converters = new List
{
@@ -54,10 +54,21 @@ public static object HandleCommand(JObject @params)
// Parameters used by various actions
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
- string searchMethod = @params["searchMethod"]?.ToString().ToLower();
-
- // Get common parameters (consolidated)
string name = @params["name"]?.ToString();
+
+ // --- Usability Improvement: Alias 'name' to 'target' for modification actions ---
+ // If 'target' is missing but 'name' is provided, and we aren't creating a new object,
+ // assume the user meant "find object by name".
+ if (targetToken == null && !string.IsNullOrEmpty(name) && action != "create")
+ {
+ targetToken = name;
+ // We don't update @params["target"] because we use targetToken locally mostly,
+ // but some downstream methods might parse @params directly. Let's update @params too for safety.
+ @params["target"] = name;
+ }
+ // -------------------------------------------------------------------------------
+
+ string searchMethod = @params["searchMethod"]?.ToString().ToLower();
string tag = @params["tag"]?.ToString();
string layer = @params["layer"]?.ToString();
JToken parentToken = @params["parent"];
@@ -2112,51 +2123,7 @@ private static bool SetNestedProperty(object target, string path, JToken value,
// Special handling for Material properties (shader properties)
if (currentObject is Material material && finalPart.StartsWith("_"))
{
- // Use the serializer to convert the JToken value first
- if (value is JArray jArray)
- {
- // Try converting to known types that SetColor/SetVector accept
- if (jArray.Count == 4)
- {
- try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { }
- try { Vector4 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { }
- }
- else if (jArray.Count == 3)
- {
- try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } // ToObject handles conversion to Color
- }
- else if (jArray.Count == 2)
- {
- try { Vector2 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { }
- }
- }
- else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
- {
- try { material.SetFloat(finalPart, value.ToObject(inputSerializer)); return true; } catch { }
- }
- else if (value.Type == JTokenType.Boolean)
- {
- try { material.SetFloat(finalPart, value.ToObject(inputSerializer) ? 1f : 0f); return true; } catch { }
- }
- else if (value.Type == JTokenType.String)
- {
- // Try converting to Texture using the serializer/converter
- try
- {
- Texture texture = value.ToObject(inputSerializer);
- if (texture != null)
- {
- material.SetTexture(finalPart, texture);
- return true;
- }
- }
- catch { }
- }
-
- Debug.LogWarning(
- $"[SetNestedProperty] Unsupported or failed conversion for material property '{finalPart}' from value: {value.ToString(Formatting.None)}"
- );
- return false;
+ return MaterialOps.TrySetShaderProperty(material, finalPart, value, inputSerializer);
}
// For standard properties (not shader specific)
diff --git a/MCPForUnity/Editor/Tools/ManageMaterial.cs b/MCPForUnity/Editor/Tools/ManageMaterial.cs
new file mode 100644
index 000000000..e8f701d10
--- /dev/null
+++ b/MCPForUnity/Editor/Tools/ManageMaterial.cs
@@ -0,0 +1,518 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Helpers;
+using UnityEngine;
+using UnityEditor;
+
+namespace MCPForUnity.Editor.Tools
+{
+ [McpForUnityTool("manage_material", AutoRegister = false)]
+ public static class ManageMaterial
+ {
+ public static object HandleCommand(JObject @params)
+ {
+ string action = @params["action"]?.ToString();
+ if (string.IsNullOrEmpty(action))
+ {
+ return new { status = "error", message = "Action is required" };
+ }
+
+ try
+ {
+ switch (action)
+ {
+ case "ping":
+ return new { status = "success", tool = "manage_material" };
+
+ case "create":
+ return CreateMaterial(@params);
+
+ case "set_material_shader_property":
+ return SetMaterialShaderProperty(@params);
+
+ case "set_material_color":
+ return SetMaterialColor(@params);
+
+ case "assign_material_to_renderer":
+ return AssignMaterialToRenderer(@params);
+
+ case "set_renderer_color":
+ return SetRendererColor(@params);
+
+ case "get_material_info":
+ return GetMaterialInfo(@params);
+
+ default:
+ return new { status = "error", message = $"Unknown action: {action}" };
+ }
+ }
+ catch (Exception ex)
+ {
+ return new { status = "error", message = ex.Message, stackTrace = ex.StackTrace };
+ }
+ }
+
+ private static string NormalizePath(string path)
+ {
+ if (string.IsNullOrEmpty(path)) return path;
+
+ // Normalize separators and ensure Assets/ root
+ path = AssetPathUtility.SanitizeAssetPath(path);
+
+ // Ensure .mat extension
+ if (!path.EndsWith(".mat", StringComparison.OrdinalIgnoreCase))
+ {
+ path += ".mat";
+ }
+
+ return path;
+ }
+
+ private static object SetMaterialShaderProperty(JObject @params)
+ {
+ string materialPath = NormalizePath(@params["materialPath"]?.ToString());
+ string property = @params["property"]?.ToString();
+ JToken value = @params["value"];
+
+ if (string.IsNullOrEmpty(materialPath) || string.IsNullOrEmpty(property) || value == null)
+ {
+ return new { status = "error", message = "materialPath, property, and value are required" };
+ }
+
+ // Find material
+ var findInstruction = new JObject { ["find"] = materialPath };
+ Material mat = ManageGameObject.FindObjectByInstruction(findInstruction, typeof(Material)) as Material;
+
+ if (mat == null)
+ {
+ return new { status = "error", message = $"Could not find material at path: {materialPath}" };
+ }
+
+ Undo.RecordObject(mat, "Set Material Property");
+
+ // Normalize alias/casing once for all code paths
+ property = MaterialOps.ResolvePropertyName(mat, property);
+
+ // 1. Try handling Texture instruction explicitly (ManageMaterial special feature)
+ if (value.Type == JTokenType.Object)
+ {
+ // Check if it looks like an instruction
+ if (value is JObject obj && (obj.ContainsKey("find") || obj.ContainsKey("method")))
+ {
+ Texture tex = ManageGameObject.FindObjectByInstruction(obj, typeof(Texture)) as Texture;
+ if (tex != null && mat.HasProperty(property))
+ {
+ mat.SetTexture(property, tex);
+ EditorUtility.SetDirty(mat);
+ return new { status = "success", message = $"Set texture property {property} on {mat.name}" };
+ }
+ }
+ }
+
+ // 2. Fallback to standard logic via MaterialOps (handles Colors, Floats, Strings->Path)
+ bool success = MaterialOps.TrySetShaderProperty(mat, property, value, ManageGameObject.InputSerializer);
+
+ if (success)
+ {
+ EditorUtility.SetDirty(mat);
+ return new { status = "success", message = $"Set property {property} on {mat.name}" };
+ }
+ else
+ {
+ return new { status = "error", message = $"Failed to set property {property}. Value format might be unsupported or texture not found." };
+ }
+ }
+
+ private static object SetMaterialColor(JObject @params)
+ {
+ string materialPath = NormalizePath(@params["materialPath"]?.ToString());
+ JToken colorToken = @params["color"];
+ string property = @params["property"]?.ToString();
+
+ if (string.IsNullOrEmpty(materialPath) || colorToken == null)
+ {
+ return new { status = "error", message = "materialPath and color are required" };
+ }
+
+ var findInstruction = new JObject { ["find"] = materialPath };
+ Material mat = ManageGameObject.FindObjectByInstruction(findInstruction, typeof(Material)) as Material;
+
+ if (mat == null)
+ {
+ return new { status = "error", message = $"Could not find material at path: {materialPath}" };
+ }
+
+ Color color;
+ try
+ {
+ color = MaterialOps.ParseColor(colorToken, ManageGameObject.InputSerializer);
+ }
+ catch (Exception e)
+ {
+ return new { status = "error", message = $"Invalid color format: {e.Message}" };
+ }
+
+ Undo.RecordObject(mat, "Set Material Color");
+
+ bool foundProp = false;
+ if (!string.IsNullOrEmpty(property))
+ {
+ if (mat.HasProperty(property))
+ {
+ mat.SetColor(property, color);
+ foundProp = true;
+ }
+ }
+ else
+ {
+ // Fallback logic: _BaseColor (URP/HDRP) then _Color (Built-in)
+ if (mat.HasProperty("_BaseColor"))
+ {
+ mat.SetColor("_BaseColor", color);
+ foundProp = true;
+ property = "_BaseColor";
+ }
+ else if (mat.HasProperty("_Color"))
+ {
+ mat.SetColor("_Color", color);
+ foundProp = true;
+ property = "_Color";
+ }
+ }
+
+ if (foundProp)
+ {
+ EditorUtility.SetDirty(mat);
+ return new { status = "success", message = $"Set color on {property}" };
+ }
+ else
+ {
+ return new { status = "error", message = "Could not find suitable color property (_BaseColor or _Color) or specified property does not exist." };
+ }
+ }
+
+ private static object AssignMaterialToRenderer(JObject @params)
+ {
+ string target = @params["target"]?.ToString();
+ string searchMethod = @params["searchMethod"]?.ToString();
+ string materialPath = NormalizePath(@params["materialPath"]?.ToString());
+ int slot = @params["slot"]?.ToObject() ?? 0;
+
+ if (string.IsNullOrEmpty(target) || string.IsNullOrEmpty(materialPath))
+ {
+ return new { status = "error", message = "target and materialPath are required" };
+ }
+
+ var goInstruction = new JObject { ["find"] = target };
+ if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
+
+ GameObject go = ManageGameObject.FindObjectByInstruction(goInstruction, typeof(GameObject)) as GameObject;
+ if (go == null)
+ {
+ return new { status = "error", message = $"Could not find target GameObject: {target}" };
+ }
+
+ Renderer renderer = go.GetComponent();
+ if (renderer == null)
+ {
+ return new { status = "error", message = $"GameObject {go.name} has no Renderer component" };
+ }
+
+ var matInstruction = new JObject { ["find"] = materialPath };
+ Material mat = ManageGameObject.FindObjectByInstruction(matInstruction, typeof(Material)) as Material;
+ if (mat == null)
+ {
+ return new { status = "error", message = $"Could not find material: {materialPath}" };
+ }
+
+ Undo.RecordObject(renderer, "Assign Material");
+
+ Material[] sharedMats = renderer.sharedMaterials;
+ if (slot < 0 || slot >= sharedMats.Length)
+ {
+ return new { status = "error", message = $"Slot {slot} out of bounds (count: {sharedMats.Length})" };
+ }
+
+ sharedMats[slot] = mat;
+ renderer.sharedMaterials = sharedMats;
+
+ EditorUtility.SetDirty(renderer);
+ return new { status = "success", message = $"Assigned material {mat.name} to {go.name} slot {slot}" };
+ }
+
+ private static object SetRendererColor(JObject @params)
+ {
+ string target = @params["target"]?.ToString();
+ string searchMethod = @params["searchMethod"]?.ToString();
+ JToken colorToken = @params["color"];
+ int slot = @params["slot"]?.ToObject() ?? 0;
+ string mode = @params["mode"]?.ToString() ?? "property_block";
+
+ if (string.IsNullOrEmpty(target) || colorToken == null)
+ {
+ return new { status = "error", message = "target and color are required" };
+ }
+
+ Color color;
+ try
+ {
+ color = MaterialOps.ParseColor(colorToken, ManageGameObject.InputSerializer);
+ }
+ catch (Exception e)
+ {
+ return new { status = "error", message = $"Invalid color format: {e.Message}" };
+ }
+
+ var goInstruction = new JObject { ["find"] = target };
+ if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
+
+ GameObject go = ManageGameObject.FindObjectByInstruction(goInstruction, typeof(GameObject)) as GameObject;
+ if (go == null)
+ {
+ return new { status = "error", message = $"Could not find target GameObject: {target}" };
+ }
+
+ Renderer renderer = go.GetComponent();
+ if (renderer == null)
+ {
+ return new { status = "error", message = $"GameObject {go.name} has no Renderer component" };
+ }
+
+ if (mode == "property_block")
+ {
+ if (slot < 0 || slot >= renderer.sharedMaterials.Length)
+ {
+ return new { status = "error", message = $"Slot {slot} out of bounds (count: {renderer.sharedMaterials.Length})" };
+ }
+
+ MaterialPropertyBlock block = new MaterialPropertyBlock();
+ renderer.GetPropertyBlock(block, slot);
+
+ if (renderer.sharedMaterials[slot] != null)
+ {
+ Material mat = renderer.sharedMaterials[slot];
+ if (mat.HasProperty("_BaseColor")) block.SetColor("_BaseColor", color);
+ else if (mat.HasProperty("_Color")) block.SetColor("_Color", color);
+ else block.SetColor("_Color", color);
+ }
+ else
+ {
+ block.SetColor("_Color", color);
+ }
+
+ renderer.SetPropertyBlock(block, slot);
+ EditorUtility.SetDirty(renderer);
+ return new { status = "success", message = $"Set renderer color (PropertyBlock) on slot {slot}" };
+ }
+ else if (mode == "shared")
+ {
+ if (slot >= 0 && slot < renderer.sharedMaterials.Length)
+ {
+ Material mat = renderer.sharedMaterials[slot];
+ if (mat == null)
+ {
+ return new { status = "error", message = $"No material in slot {slot}" };
+ }
+ Undo.RecordObject(mat, "Set Material Color");
+ if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
+ else mat.SetColor("_Color", color);
+ EditorUtility.SetDirty(mat);
+ return new { status = "success", message = "Set shared material color" };
+ }
+ return new { status = "error", message = "Invalid slot" };
+ }
+ else if (mode == "instance")
+ {
+ if (slot >= 0 && slot < renderer.materials.Length)
+ {
+ Material mat = renderer.materials[slot];
+ if (mat == null)
+ {
+ return new { status = "error", message = $"No material in slot {slot}" };
+ }
+ // Note: Undo cannot fully revert material instantiation
+ Undo.RecordObject(mat, "Set Instance Material Color");
+ if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
+ else mat.SetColor("_Color", color);
+ return new { status = "success", message = "Set instance material color", warning = "Material instance created; Undo cannot fully revert instantiation." };
+ }
+ return new { status = "error", message = "Invalid slot" };
+ }
+
+ return new { status = "error", message = $"Unknown mode: {mode}" };
+ }
+
+ private static object GetMaterialInfo(JObject @params)
+ {
+ string materialPath = NormalizePath(@params["materialPath"]?.ToString());
+ if (string.IsNullOrEmpty(materialPath))
+ {
+ return new { status = "error", message = "materialPath is required" };
+ }
+
+ var findInstruction = new JObject { ["find"] = materialPath };
+ Material mat = ManageGameObject.FindObjectByInstruction(findInstruction, typeof(Material)) as Material;
+
+ if (mat == null)
+ {
+ return new { status = "error", message = $"Could not find material at path: {materialPath}" };
+ }
+
+ Shader shader = mat.shader;
+ var properties = new List