/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/
#nullable enable
#if !UNITY_6000_5_OR_NEWER
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using com.IvanMurzak.ReflectorNet;
using com.IvanMurzak.ReflectorNet.Model;
using com.IvanMurzak.ReflectorNet.Utils;
using AIGD;
using com.IvanMurzak.Unity.MCP.Runtime.Extensions;
using Microsoft.Extensions.Logging;
using UnityEngine;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace com.IvanMurzak.Unity.MCP.Reflection.Converter
{
///
/// Reflection converter for UnityEngine.Material
/// IMPORTANT: it implements custom depth handling to avoid heavy serialization of Unity objects.
/// As a result it only serializes a Material at depth == 0
///
public partial class UnityEngine_Material_ReflectionConverter : UnityEngine_Object_ReflectionConverter
{
const string FieldShader = "shader";
const string FieldName = "name";
static readonly string[] AllFieldNames = new[]
{
FieldShader,
FieldName
};
// public override bool AllowCascadeSerialization => false;
public override bool AllowSetValue => false;
protected override IEnumerable RestrictedInValuePropertyNames(Reflector reflector, JsonElement valueJsonElement)
{
var result = base.RestrictedInValuePropertyNames(reflector, valueJsonElement).Concat(new[]
{
FieldShader,
FieldName
});
var assetObjectRef = valueJsonElement.ToAssetObjectRef(
reflector: null, // parse it without custom json serializer, that is why reflector is not needed
suppressException: true
);
if (assetObjectRef == null)
return result;
var assetObject = assetObjectRef.FindAssetObject();
if (assetObject is not Material material)
return result;
var shader = material.shader;
int propertyCount = shader.GetPropertyCount();
var restrictedProperties = Enumerable.Range(0, propertyCount)
.Select(i => shader.GetPropertyName(i))
.Where(propName => propName != FieldShader && propName != FieldName);
var test = restrictedProperties.ToList();
return result.Concat(restrictedProperties);
}
protected override IEnumerable GetKnownSerializableFields(Reflector reflector, object? obj)
{
var result = base.GetKnownSerializableFields(reflector, obj);
return result.Concat(new[]
{
FieldShader,
FieldName
});
}
protected override IEnumerable GetKnownSerializableProperties(Reflector reflector, object? obj)
{
var result = base.GetKnownSerializableProperties(reflector, obj);
if (obj is not Material material)
return result;
return result.Concat(GetMaterialProperties(material));
}
IEnumerable GetMaterialProperties(Material material)
{
var shader = material.shader;
int propertyCount = shader.GetPropertyCount();
return Enumerable.Range(0, propertyCount)
.Select(i => shader.GetPropertyName(i));
}
public override IEnumerable GetAdditionalSerializableFields(Reflector reflector, Type objType, BindingFlags flags, ILogger? logger = null)
{
return base.GetAdditionalSerializableFields(reflector, objType, flags, logger)
.Concat(new[]
{
FieldShader,
FieldName,
});
}
protected override SerializedMember InternalSerialize(
Reflector reflector,
object? obj,
Type type,
string? name = null,
bool recursive = true,
BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
int depth = 0,
Logs? logs = null,
ILogger? logger = null,
SerializationContext? context = null)
{
if (obj == null)
return SerializedMember.Null(type, name);
var padding = StringUtils.GetPadding(depth);
var material = obj as Material;
if (material == null)
{
// UnityEngine.Material is destroyed but reference is not null
return SerializedMember.Null(type, name);
}
if (depth >= 1 || !recursive)
{
return SerializedMember.FromValue(
reflector: reflector,
type: type,
value: material.IsAsset()
? new AssetObjectRef(material)
: new ObjectRef(material),
name: name ?? material.name);
}
var shader = material.shader;
int propertyCount = shader.GetPropertyCount();
var properties = new SerializedMemberList(propertyCount);
for (int i = 0; i < propertyCount; i++)
{
var propName = shader.GetPropertyName(i);
var propType = shader.GetPropertyType(i) switch
{
UnityEngine.Rendering.ShaderPropertyType.Int => typeof(int),
UnityEngine.Rendering.ShaderPropertyType.Float => typeof(float),
UnityEngine.Rendering.ShaderPropertyType.Range => typeof(float),
UnityEngine.Rendering.ShaderPropertyType.Color => typeof(Color),
UnityEngine.Rendering.ShaderPropertyType.Vector => typeof(Vector4),
UnityEngine.Rendering.ShaderPropertyType.Texture => typeof(Texture),
_ => null
};
if (propType == null)
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}Material property '{propName}' has unsupported type '{shader.GetPropertyType(i)}'.");
logs?.Warning($"Material property '{propName}' has unsupported type '{shader.GetPropertyType(i)}'. Supported types: Int, Float, Range, Color, Vector, Texture", depth);
continue;
}
if (reflector.Converters.IsTypeBlacklisted(propType))
{
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}Skipping blacklisted property '{propName}' of type '{propType}'.");
continue;
}
var propValue = shader.GetPropertyType(i) switch
{
UnityEngine.Rendering.ShaderPropertyType.Int => material.GetInt(propName) as object,
UnityEngine.Rendering.ShaderPropertyType.Float => material.GetFloat(propName),
UnityEngine.Rendering.ShaderPropertyType.Range => material.GetFloat(propName),
UnityEngine.Rendering.ShaderPropertyType.Color => material.GetColor(propName),
UnityEngine.Rendering.ShaderPropertyType.Vector => material.GetVector(propName),
UnityEngine.Rendering.ShaderPropertyType.Texture => material.GetTexture(propName) != null
? new ObjectRef(material.GetTexture(propName))
: null,
_ => throw new NotSupportedException($"Unsupported shader property type: '{shader.GetPropertyType(i)}'."
+ " Supported types are: Int, Float, Range, Color, Vector, Texture.")
};
properties.Add(SerializedMember.FromValue(reflector, propType, propValue, name: propName));
}
return new SerializedMember()
{
name = name ?? material.name,
typeName = type.GetTypeId(),
fields = new SerializedMemberList()
{
SerializedMember.FromValue(reflector, name: FieldName, value: material.name),
SerializedMember.FromValue(reflector, name: FieldShader, value: shader.name)
},
props = properties,
}.SetValue(reflector, new ObjectRef(material));
}
public override bool TryModify(
Reflector reflector,
ref object? obj,
SerializedMember data,
Type type,
int depth = 0,
Logs? logs = null,
BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
ILogger? logger = null)
{
// Trying to fix JSON value body, if critical property is missed or detected return false
if (!FixJsonValueBody(
reflector: reflector,
obj: ref obj,
data: data,
type: type,
depth: depth,
logs: logs,
flags: flags,
logger: logger))
{
return false;
}
if (obj is Material material)
{
var unityObject = data.valueJsonElement
.ToAssetObjectRef(
reflector: reflector,
suppressException: true,
logs: logs,
logger: logger)
?.FindAssetObject();
if (unityObject == null)
{
// Recognized as a command to remove material
obj = null;
return true;
}
if (material.GetInstanceID() == unityObject.GetInstanceID())
{
// Recognized as a command to update material
return base.TryModify(
reflector: reflector,
obj: ref obj,
data: data,
type: type,
depth: depth,
logs: logs,
flags: flags,
logger: logger);
}
// Need to set new material after and maybe to modify the new material.
var newMaterial = reflector.Deserialize(
data,
fallbackType: obj?.GetType() ?? typeof(Material),
fallbackName: null,
depth: depth,
logs: logs,
logger: logger);
var success = base.TryModify(
reflector: reflector,
obj: ref newMaterial,
data: data,
type: type,
depth: depth,
logs: logs,
flags: flags,
logger: logger);
if (success)
obj = newMaterial;
return success;
}
return base.TryModify(
reflector: reflector,
obj: ref obj,
data: data,
type: type,
depth: depth,
logs: logs,
flags: flags,
logger: logger);
}
protected override bool TryModifyField(
Reflector reflector,
ref object obj,
Type objType,
SerializedMember fieldValue,
int depth = 0,
Logs? logs = null,
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
ILogger? logger = null)
{
var padding = StringUtils.GetPadding(depth);
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{StringUtils.GetPadding(depth)}Modify field for type='{objType.GetTypeId()}'. Converter='{GetType().GetTypeShortName()}'.");
var material = obj as Material;
if (material == null)
{
// UnityEngine.Material is destroyed but reference is not null
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError($"{padding}UnityEngine.Material is destroyed but reference is not null. Converter: {GetType().GetTypeShortName()}");
logs?.Error($"UnityEngine.Material is destroyed but reference is not null. Converter: {GetType().GetTypeShortName()}", depth);
return false;
}
if (fieldValue.name == FieldName)
{
material.name = fieldValue.GetValue(reflector);
if (logger?.IsEnabled(LogLevel.Information) == true)
logger.LogInformation($"{padding}[Success] Material name set to '{material.name}'. Converter: {GetType().GetTypeShortName()}");
logs?.Success($"Material name set to '{material.name}'.", depth);
return true;
}
if (fieldValue.name == FieldShader)
{
var shaderName = fieldValue.GetValue(reflector);
// Check if the shader is already set
if (string.IsNullOrEmpty(shaderName) || material.shader.name == shaderName)
{
if (logger?.IsEnabled(LogLevel.Information) == true)
logger.LogInformation($"{padding}Material '{material.name}' shader is already set to '{shaderName}'. Converter: {GetType().GetTypeShortName()}");
logs?.Info($"Material '{material.name}' shader is already set to '{shaderName}'.", depth);
return true;
}
var shader = Shader.Find(shaderName);
if (shader == null)
{
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError($"{padding}[Error] Shader '{shaderName}' not found. Converter: {GetType().GetTypeShortName()}");
logs?.Error($"Shader '{shaderName}' not found. Converter: {GetType().GetTypeShortName()}", depth);
return false;
}
material.shader = shader;
if (logger?.IsEnabled(LogLevel.Information) == true)
logger.LogInformation($"{padding}[Success] Material '{material.name}' shader set to '{shaderName}'. Converter: {GetType().GetTypeShortName()}");
logs?.Success($"Material '{material.name}' shader set to '{shaderName}'.", depth);
return true;
}
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError($"{padding}[Error] Field '{fieldValue.name}' doesn't exist. Available fields: {string.Join(", ", AllFieldNames)}. If you need something else, please check Properties instead of Fields. Converter: {GetType().GetTypeShortName()}");
logs?.Error($"Field '{fieldValue.name}' doesn't exist. Available fields: {string.Join(", ", AllFieldNames)}. If you need something else, please check Properties instead of Fields. Converter: {GetType().GetTypeShortName()}", depth);
return false;
}
public override object CreateInstance(Reflector reflector, Type type)
{
return new Material(Shader.Find("Standard"));
}
}
}
#endif