/* ┌──────────────────────────────────────────────────────────────────┐ │ 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 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Converter; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; using com.IvanMurzak.Unity.MCP.Runtime.Extensions; using Microsoft.Extensions.Logging; using UnityEngine; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityGenericReflectionConverter : GenericReflectionConverter { protected override IEnumerable? GetSerializableFieldsInternal(Reflector reflector, Type objType, BindingFlags flags, ILogger? logger = null) { return objType.GetFields(flags) .Where(field => field.GetCustomAttribute() == null) .Where(field => field.GetCustomAttribute() == null) .Where(field => field.IsPublic || field.IsPrivate && field.GetCustomAttribute() != null); } public override object? Deserialize( Reflector reflector, SerializedMember data, Type? fallbackType = null, string? fallbackName = null, int depth = 0, Logs? logs = null, ILogger? logger = null, DeserializationContext? context = null) { var type = fallbackType ?? typeof(T); if (typeof(UnityEngine.Object).IsAssignableFrom(type)) { var result = data.valueJsonElement .ToAssetObjectRef( reflector: reflector, suppressException: true, depth: depth, logs: logs, logger: logger) .FindAssetObject(type); // Register the object early (before deserializing children) so child references can resolve if (result != null && context != null) context.Register(result); return result; } return base.Deserialize( reflector: reflector, data: data, fallbackType: fallbackType, fallbackName: fallbackName, depth: depth, logs: logs, logger: logger); } protected override bool SetValue( Reflector reflector, ref object? obj, Type type, System.Text.Json.JsonElement? value, int depth = 0, Logs? logs = null, ILogger? logger = null) { var originalObj = obj; // For value types (structs) without an explicit value, preserve the original to allow partial updates. // This prevents resetting structs to default values when only nested fields are specified. if (type.IsValueType && !type.IsPrimitive && !type.IsEnum && originalObj != null) { var hasExplicitValue = value.HasValue && value.Value.ValueKind != System.Text.Json.JsonValueKind.Undefined; if (!hasExplicitValue) { // No explicit value provided - keep the original struct value // so that nested field updates can modify individual members return true; } } var result = base.SetValue( reflector: reflector, obj: ref obj, type: type, value: value, depth: depth, logs: logs, logger: logger); // If obj became null but we had an object, and the value didn't explicitly say null, restore it. // This handles cases where TryModify is called with an existing object but no valueJsonElement. if (obj == null && originalObj != null) { var isExplicitNull = value.HasValue && value.Value.ValueKind == System.Text.Json.JsonValueKind.Null; if (!isExplicitNull) { obj = originalObj; } } return result; } protected override bool TryModifyField( Reflector reflector, ref object obj, Type objType, SerializedMember fieldValue, int depth = 0, Logs? logs = null, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, ILogger? logger = null) { var padding = StringUtils.GetPadding(depth); if (obj == null) { if (logger?.IsEnabled(LogLevel.Error) == true) logger.LogError("{padding}obj is null in TryModifyField for '{field}'", padding, fieldValue.name); logs?.Error($"obj is null in TryModifyField for '{fieldValue.name}'", depth); return false; } var field = objType.GetField(fieldValue.name, flags); if (field == null) { if (logger?.IsEnabled(LogLevel.Error) == true) logger.LogError("{padding}Field '{field}' not found on '{type}'", padding, fieldValue.name, objType.GetTypeId()); logs?.Error($"Field '{fieldValue.name}' not found on '{objType.GetTypeId()}'", depth); return false; } try { // For value types (structs) with nested fields/props, we need to: // 1. Get the existing value to preserve unspecified members // 2. Call TryModify on it to modify only the specified members // 3. Write the modified value back to the parent object // This prevents losing existing values when doing partial updates on structs. var fieldType = field.FieldType; var hasNestedMembers = (fieldValue.fields != null && fieldValue.fields.Count > 0) || (fieldValue.props != null && fieldValue.props.Count > 0); if (fieldType.IsValueType && !fieldType.IsPrimitive && !fieldType.IsEnum && hasNestedMembers) { // Get existing struct value (boxed) var existingValue = field.GetValue(obj); if (existingValue != null) { // Modify the existing struct with only the specified members var success = reflector.TryModify( ref existingValue, data: fieldValue, depth: depth + 1, logs: logs, flags: flags, logger: logger); if (success) { // Write the modified struct back to the parent object field.SetValue(obj, existingValue); return true; } return false; } } // For reference types, primitives, enums, or full replacements, use standard deserialization var value = reflector.Deserialize(fieldValue, fieldType, depth: depth + 1, logs: logs, logger: logger); field.SetValue(obj, value); return true; } catch (Exception e) { if (logger?.IsEnabled(LogLevel.Error) == true) logger.LogError(e, "{padding}Failed to set field {field}", padding, fieldValue.name); logs?.Error($"Failed to set field {fieldValue.name}: {e.Message}", depth); return false; } } protected override bool TryModifyProperty( Reflector reflector, ref object obj, Type objType, SerializedMember member, int depth = 0, Logs? logs = null, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, ILogger? logger = null) { var padding = StringUtils.GetPadding(depth); if (obj == null) { if (logger?.IsEnabled(LogLevel.Error) == true) logger.LogError("{padding}obj is null in TryModifyProperty for '{property}'", padding, member.name); logs?.Error($"obj is null in TryModifyProperty for '{member.name}'", depth); return false; } var property = objType.GetProperty(member.name, flags); if (property == null || !property.CanWrite) { if (logger?.IsEnabled(LogLevel.Error) == true) logger.LogError("{padding}Property '{property}' not found or not writable on '{type}'", padding, member.name, objType.GetTypeId()); logs?.Error($"Property '{member.name}' not found or not writable on '{objType.GetTypeId()}'", depth); return false; } try { // For value types (structs) with nested fields/props, we need to: // 1. Get the existing value to preserve unspecified members // 2. Call TryModify on it to modify only the specified members // 3. Write the modified value back to the parent object // This prevents losing existing values when doing partial updates on structs. var propertyType = property.PropertyType; var hasNestedMembers = (member.fields != null && member.fields.Count > 0) || (member.props != null && member.props.Count > 0); if (propertyType.IsValueType && !propertyType.IsPrimitive && !propertyType.IsEnum && hasNestedMembers && property.CanRead) { // Get existing struct value (boxed) var existingValue = property.GetValue(obj); if (existingValue != null) { // Modify the existing struct with only the specified members var success = reflector.TryModify( ref existingValue, data: member, depth: depth + 1, logs: logs, flags: flags, logger: logger); if (success) { // Write the modified struct back to the parent object property.SetValue(obj, existingValue); return true; } return false; } } // For reference types, primitives, enums, or full replacements, use standard deserialization var value = reflector.Deserialize(member, propertyType, depth: depth + 1, logs: logs, logger: logger); property.SetValue(obj, value); return true; } catch (Exception e) { if (logger?.IsEnabled(LogLevel.Error) == true) logger.LogError(e, "{padding}Failed to set property '{property}'", padding, member.name); logs?.Error($"Failed to set property '{member.name}': {e.Message}", depth); return false; } } } }