Files
Tomicz Engineering LLC deea7d2a56 Replace reflection with version-gated conditional compilation.
Addresses review feedback on the Unity 6.5 GetInstanceID migration:

- UnityObjectIdCompatExtensions: drop the reflective method lookup in
  favor of a simple #if UNITY_6000_5_OR_NEWER / #else split calling
  GetEntityId() or GetInstanceID() directly. Also wrap the class in
  the MCPForUnity.Runtime.Helpers namespace.
- UnityTypeConverters: remove the reflective EntityIdToObject probe and
  call EditorUtility.EntityIdToObject(EntityId) directly under the same
  version gate. Serialize entityID as EntityId.ToULong() rather than
  ToString(), since Unity's docs explicitly warn that the textual form
  is not a stable serialization contract.
- Drop #pragma warning disable 0619 from 22 files that no longer make
  any direct calls to obsolete APIs. The remaining 7 files still need
  it (FindObjectsOfType, InstanceIDToObject fallback) and are left as-is
  — those deprecations are out of scope for this PR.
- Add the MCPForUnity.Runtime.Helpers using to every file that calls
  GetInstanceIDCompat() now that the extension method lives in a
  namespace.
2026-04-11 22:55:45 +02:00

719 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Runtime.Helpers;
namespace MCPForUnity.Editor.Tools
{
[McpForUnityTool("manage_material", AutoRegister = false)]
public static class ManageMaterial
{
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString()?.ToLowerInvariant();
if (string.IsNullOrEmpty(action))
{
return new ErrorResponse("Action is required");
}
try
{
switch (action)
{
case "ping":
return new SuccessResponse("pong", new { 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 ErrorResponse($"Unknown action: {action}");
}
}
catch (Exception ex)
{
return new ErrorResponse(ex.Message, new { 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 ErrorResponse("materialPath, property, and value are required");
}
// Find material
var findInstruction = new JObject { ["find"] = materialPath };
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
if (mat == null)
{
return new ErrorResponse($"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 = ObjectResolver.Resolve(obj, typeof(Texture)) as Texture;
if (tex != null && mat.HasProperty(property))
{
mat.SetTexture(property, tex);
EditorUtility.SetDirty(mat);
return new SuccessResponse($"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, UnityJsonSerializer.Instance);
if (success)
{
EditorUtility.SetDirty(mat);
return new SuccessResponse($"Set property {property} on {mat.name}");
}
else
{
return new ErrorResponse($"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 ErrorResponse("materialPath and color are required");
}
var findInstruction = new JObject { ["find"] = materialPath };
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
if (mat == null)
{
return new ErrorResponse($"Could not find material at path: {materialPath}");
}
Color color;
try
{
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
}
catch (Exception e)
{
return new ErrorResponse($"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 SuccessResponse($"Set color on {property}");
}
else
{
return new ErrorResponse("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<int>() ?? 0;
if (string.IsNullOrEmpty(target) || string.IsNullOrEmpty(materialPath))
{
return new ErrorResponse("target and materialPath are required");
}
var goInstruction = new JObject { ["find"] = target };
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
if (go == null)
{
return new ErrorResponse($"Could not find target GameObject: {target}");
}
Renderer renderer = go.GetComponent<Renderer>();
if (renderer == null)
{
return new ErrorResponse($"GameObject {go.name} has no Renderer component");
}
var matInstruction = new JObject { ["find"] = materialPath };
Material mat = ObjectResolver.Resolve(matInstruction, typeof(Material)) as Material;
if (mat == null)
{
return new ErrorResponse($"Could not find material: {materialPath}");
}
Undo.RecordObject(renderer, "Assign Material");
Material[] sharedMats = renderer.sharedMaterials;
if (slot < 0 || slot >= sharedMats.Length)
{
return new ErrorResponse($"Slot {slot} out of bounds (count: {sharedMats.Length})");
}
sharedMats[slot] = mat;
renderer.sharedMaterials = sharedMats;
EditorUtility.SetDirty(renderer);
return new SuccessResponse($"Assigned material {mat.name} to {go.name} slot {slot}");
}
private static object SetRendererColor(JObject @params)
{
var p = new ToolParams(@params);
var targetResult = p.GetRequired("target");
var targetError = targetResult.GetOrError(out string target);
if (targetError != null) return targetError;
string searchMethod = p.Get("searchMethod");
JToken colorToken = p.GetRaw("color");
if (colorToken == null)
{
return new ErrorResponse("'color' parameter is required.");
}
int slot = p.GetInt("slot") ?? 0;
string mode = p.Get("mode", "property_block");
Color color;
try
{
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
}
catch (Exception e)
{
return new ErrorResponse($"Invalid color format: {e.Message}");
}
var goInstruction = new JObject { ["find"] = target };
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
if (go == null)
{
return new ErrorResponse($"Could not find target GameObject: {target}");
}
Renderer renderer = go.GetComponent<Renderer>();
if (renderer == null)
{
return new ErrorResponse($"GameObject {go.name} has no Renderer component");
}
RendererHelpers.EnsureMaterial(renderer);
if (mode == "property_block")
{
if (slot < 0 || slot >= renderer.sharedMaterials.Length)
{
return new ErrorResponse($"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];
bool wroteAnyProperty = false;
if (mat.HasProperty("_BaseColor"))
{
block.SetColor("_BaseColor", color);
wroteAnyProperty = true;
}
if (mat.HasProperty("_Color"))
{
block.SetColor("_Color", color);
wroteAnyProperty = true;
}
if (!wroteAnyProperty)
{
block.SetColor("_BaseColor", color);
block.SetColor("_Color", color);
}
}
else
{
block.SetColor("_BaseColor", color);
block.SetColor("_Color", color);
}
renderer.SetPropertyBlock(block, slot);
EditorUtility.SetDirty(renderer);
return new SuccessResponse($"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 ErrorResponse($"No material in slot {slot}");
}
Undo.RecordObject(mat, "Set Material Color");
SetColorProperties(mat, color);
EditorUtility.SetDirty(mat);
return new SuccessResponse("Set shared material color");
}
return new ErrorResponse("Invalid slot");
}
else if (mode == "instance")
{
if (slot >= 0 && slot < renderer.materials.Length)
{
Material mat = renderer.materials[slot];
if (mat == null)
{
return new ErrorResponse($"No material in slot {slot}");
}
// Note: Undo cannot fully revert material instantiation
Undo.RecordObject(mat, "Set Instance Material Color");
SetColorProperties(mat, color);
return new SuccessResponse("Set instance material color", new { warning = "Material instance created; Undo cannot fully revert instantiation." });
}
return new ErrorResponse("Invalid slot");
}
else if (mode == "create_unique")
{
return CreateUniqueAndAssign(renderer, go, color, slot);
}
return new ErrorResponse($"Unknown mode: {mode}");
}
private static void EnsureAssetFolderExists(string assetFolderPath)
{
if (AssetDatabase.IsValidFolder(assetFolderPath))
return;
string[] parts = assetFolderPath.Replace('\\', '/').Split('/');
string current = parts[0]; // "Assets"
for (int i = 1; i < parts.Length; i++)
{
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, parts[i]);
current = next;
}
}
private static void SetColorProperties(Material mat, Color color)
{
bool wrote = false;
if (mat.HasProperty("_BaseColor"))
{
mat.SetColor("_BaseColor", color);
wrote = true;
}
if (mat.HasProperty("_Color"))
{
mat.SetColor("_Color", color);
wrote = true;
}
if (!wrote)
{
mat.SetColor("_BaseColor", color);
mat.SetColor("_Color", color);
}
}
private static object CreateUniqueAndAssign(Renderer renderer, GameObject go, Color color, int slot)
{
string safeName = go.name.Replace(" ", "_");
// Derive material folder from the scene context so generated materials
// live next to the scene/generation folder instead of a global dump.
string materialFolder = "Assets/Materials";
var scene = go.scene;
if (scene.IsValid() && !string.IsNullOrEmpty(scene.path) && scene.path.StartsWith("Assets/"))
{
string sceneDir = System.IO.Path.GetDirectoryName(scene.path).Replace("\\", "/");
materialFolder = $"{sceneDir}/Materials";
}
string matPath = $"{materialFolder}/{safeName}_{go.GetInstanceIDCompat()}_mat.mat";
matPath = AssetPathUtility.SanitizeAssetPath(matPath);
if (matPath == null)
{
return new ErrorResponse($"Invalid GameObject name '{go.name}' — cannot build a safe material path.");
}
// Ensure the Materials directory exists (recursive)
EnsureAssetFolderExists(materialFolder);
Material existing = AssetDatabase.LoadAssetAtPath<Material>(matPath);
if (existing != null)
{
// Material already exists (e.g. retry) — update its color and re-assign
Undo.RecordObject(existing, "Update unique material color");
SetColorProperties(existing, color);
EditorUtility.SetDirty(existing);
}
else
{
Shader shader = RenderPipelineUtility.ResolveShader("Standard");
if (shader == null)
{
return new ErrorResponse("Could not resolve a suitable shader for the active render pipeline.");
}
existing = new Material(shader);
SetColorProperties(existing, color);
AssetDatabase.CreateAsset(existing, matPath);
}
AssetDatabase.SaveAssets();
// Assign to renderer
Undo.RecordObject(renderer, "Assign unique material");
Material[] sharedMats = renderer.sharedMaterials;
if (slot < 0 || slot >= sharedMats.Length)
{
return new ErrorResponse($"Slot {slot} out of bounds (count: {sharedMats.Length})");
}
sharedMats[slot] = existing;
renderer.sharedMaterials = sharedMats;
EditorUtility.SetDirty(renderer);
return new SuccessResponse($"Created unique material at {matPath} and assigned to {go.name}",
new { materialPath = matPath });
}
private static object GetMaterialInfo(JObject @params)
{
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
if (string.IsNullOrEmpty(materialPath))
{
return new ErrorResponse("materialPath is required");
}
var findInstruction = new JObject { ["find"] = materialPath };
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
if (mat == null)
{
return new ErrorResponse($"Could not find material at path: {materialPath}");
}
Shader shader = mat.shader;
var properties = new List<object>();
#if UNITY_6000_0_OR_NEWER
int propertyCount = shader.GetPropertyCount();
for (int i = 0; i < propertyCount; i++)
{
string name = shader.GetPropertyName(i);
var type = shader.GetPropertyType(i);
string description = shader.GetPropertyDescription(i);
object currentValue = null;
try
{
if (mat.HasProperty(name))
{
switch (type)
{
case UnityEngine.Rendering.ShaderPropertyType.Color:
var c = mat.GetColor(name);
currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a };
break;
case UnityEngine.Rendering.ShaderPropertyType.Vector:
var v = mat.GetVector(name);
currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w };
break;
case UnityEngine.Rendering.ShaderPropertyType.Float:
case UnityEngine.Rendering.ShaderPropertyType.Range:
currentValue = mat.GetFloat(name);
break;
case UnityEngine.Rendering.ShaderPropertyType.Texture:
currentValue = mat.GetTexture(name)?.name ?? "null";
break;
}
}
}
catch (Exception ex)
{
currentValue = $"<error: {ex.Message}>";
}
properties.Add(new
{
name = name,
type = type.ToString(),
description = description,
value = currentValue
});
}
#else
int propertyCount = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < propertyCount; i++)
{
string name = ShaderUtil.GetPropertyName(shader, i);
ShaderUtil.ShaderPropertyType type = ShaderUtil.GetPropertyType(shader, i);
string description = ShaderUtil.GetPropertyDescription(shader, i);
object currentValue = null;
try
{
if (mat.HasProperty(name))
{
switch (type)
{
case ShaderUtil.ShaderPropertyType.Color:
var c = mat.GetColor(name);
currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a };
break;
case ShaderUtil.ShaderPropertyType.Vector:
var v = mat.GetVector(name);
currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w };
break;
case ShaderUtil.ShaderPropertyType.Float: currentValue = mat.GetFloat(name); break;
case ShaderUtil.ShaderPropertyType.Range: currentValue = mat.GetFloat(name); break;
case ShaderUtil.ShaderPropertyType.TexEnv: currentValue = mat.GetTexture(name)?.name ?? "null"; break;
}
}
}
catch (Exception ex)
{
currentValue = $"<error: {ex.Message}>";
}
properties.Add(new
{
name = name,
type = type.ToString(),
description = description,
value = currentValue
});
}
#endif
return new SuccessResponse($"Retrieved material info for {mat.name}", new
{
material = mat.name,
shader = shader.name,
properties = properties
});
}
private static object CreateMaterial(JObject @params)
{
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
string shaderName = @params["shader"]?.ToString() ?? "Standard";
JToken colorToken = @params["color"];
string colorProperty = @params["property"]?.ToString();
JObject properties = null;
JToken propsToken = @params["properties"];
if (propsToken != null)
{
if (propsToken.Type == JTokenType.String)
{
try { properties = JObject.Parse(propsToken.ToString()); }
catch (Exception ex) { return new ErrorResponse($"Invalid JSON in properties: {ex.Message}"); }
}
else if (propsToken is JObject obj)
{
properties = obj;
}
}
if (string.IsNullOrEmpty(materialPath))
{
return new ErrorResponse("materialPath is required");
}
// Safety check: SanitizeAssetPath should guarantee Assets/ prefix
// This check catches edge cases where normalization might fail
if (!materialPath.StartsWith("Assets/"))
{
return new ErrorResponse($"Invalid path '{materialPath}'. Path must be within Assets/ folder.");
}
Shader shader = RenderPipelineUtility.ResolveShader(shaderName);
if (shader == null)
{
return new ErrorResponse($"Could not find shader: {shaderName}");
}
// Check for existing asset to avoid silent overwrite
if (AssetDatabase.LoadAssetAtPath<Material>(materialPath) != null)
{
return new ErrorResponse($"Material already exists at {materialPath}");
}
Material material = null;
var shouldDestroyMaterial = true;
try
{
material = new Material(shader);
// Apply color param during creation (keeps Python tool signature and C# implementation consistent).
// If "properties" already contains a color property, let properties win.
bool shouldApplyColor = false;
if (colorToken != null)
{
if (properties == null)
{
shouldApplyColor = true;
}
else if (!string.IsNullOrEmpty(colorProperty))
{
// If colorProperty is specified, only check that specific property.
shouldApplyColor = !properties.ContainsKey(colorProperty);
}
else
{
// If colorProperty is not specified, check fallback properties.
shouldApplyColor = !properties.ContainsKey("_BaseColor") && !properties.ContainsKey("_Color");
}
}
if (shouldApplyColor)
{
Color color;
try
{
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
}
catch (Exception e)
{
return new ErrorResponse($"Invalid color format: {e.Message}");
}
if (!string.IsNullOrEmpty(colorProperty))
{
if (material.HasProperty(colorProperty))
{
material.SetColor(colorProperty, color);
}
else
{
return new ErrorResponse($"Specified color property '{colorProperty}' does not exist on this material.");
}
}
else if (material.HasProperty("_BaseColor"))
{
material.SetColor("_BaseColor", color);
}
else if (material.HasProperty("_Color"))
{
material.SetColor("_Color", color);
}
else
{
return new ErrorResponse("Could not find suitable color property (_BaseColor or _Color) on this material's shader.");
}
}
AssetDatabase.CreateAsset(material, materialPath);
shouldDestroyMaterial = false; // material is now owned by the AssetDatabase
if (properties != null)
{
MaterialOps.ApplyProperties(material, properties, UnityJsonSerializer.Instance);
}
EditorUtility.SetDirty(material);
AssetDatabase.SaveAssets();
return new SuccessResponse($"Created material at {materialPath} with shader {shaderName}");
}
finally
{
if (shouldDestroyMaterial && material != null)
{
UnityEngine.Object.DestroyImmediate(material);
}
}
}
}
}