SetComponentValueCommand.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. using System;
  2. using System.Linq;
  3. using System.Reflection;
  4. using UnityEngine;
  5. using UnityEditor;
  6. using LLM.Editor.Helper;
  7. using Newtonsoft.Json.Linq;
  8. using JetBrains.Annotations;
  9. using Object = UnityEngine.Object;
  10. namespace LLM.Editor.Commands
  11. {
  12. [Serializable]
  13. public class SetComponentValueParams
  14. {
  15. public string subjectIdentifier;
  16. public string componentName;
  17. public string memberName;
  18. public JToken value;
  19. }
  20. [UsedImplicitly]
  21. public class SetComponentValueCommand : ICommand
  22. {
  23. private readonly SetComponentValueParams _params;
  24. public SetComponentValueCommand(string jsonParams)
  25. {
  26. _params = jsonParams?.FromJson<SetComponentValueParams>();
  27. }
  28. public CommandOutcome Execute(Data.CommandContext context)
  29. {
  30. if (_params == null || string.IsNullOrEmpty(_params.subjectIdentifier) || string.IsNullOrEmpty(_params.componentName) || string.IsNullOrEmpty(_params.memberName))
  31. {
  32. context.ErrorMessage = "Invalid parameters. Subject, component, and member names are required.";
  33. return CommandOutcome.Error;
  34. }
  35. var targetObject = CommandUtility.ResolveIdentifier(context, _params.subjectIdentifier);
  36. if (!targetObject)
  37. {
  38. context.ErrorMessage = $"Could not find subject with logical name '{_params.subjectIdentifier}'.";
  39. return CommandOutcome.Error;
  40. }
  41. if (targetObject is not GameObject go)
  42. {
  43. context.ErrorMessage = $"The identified subject '{targetObject.name}' is not a GameObject.";
  44. return CommandOutcome.Error;
  45. }
  46. var targetComponent = go.GetComponent(_params.componentName);
  47. if (!targetComponent)
  48. {
  49. context.ErrorMessage = $"Could not find component '{_params.componentName}' on subject '{_params.subjectIdentifier}'.";
  50. return CommandOutcome.Error;
  51. }
  52. // Attempt 1: Use SerializedObject (the preferred Unity way)
  53. var serializedObject = new SerializedObject(targetComponent);
  54. var serializedProperty = serializedObject.FindProperty(_params.memberName);
  55. if (serializedProperty != null)
  56. {
  57. return SetValueWithSerializedProperty(context, serializedObject, serializedProperty);
  58. }
  59. // Attempt 2: Fallback to C# Reflection for public properties
  60. var propertyInfo = targetComponent.GetType().GetProperty(_params.memberName, BindingFlags.Public | BindingFlags.Instance);
  61. if (propertyInfo != null && propertyInfo.CanWrite)
  62. {
  63. return SetValueWithReflection(context, targetComponent, propertyInfo);
  64. }
  65. context.ErrorMessage = $"Could not find a writable serialized field or public property named '{_params.memberName}' on component '{_params.componentName}'.";
  66. return CommandOutcome.Error;
  67. }
  68. private CommandOutcome SetValueWithSerializedProperty(Data.CommandContext context, SerializedObject serializedObject, SerializedProperty property)
  69. {
  70. serializedObject.Update();
  71. try
  72. {
  73. switch (property.propertyType)
  74. {
  75. case SerializedPropertyType.ObjectReference:
  76. var objToAssign = GetObjectReferenceValue(context, property);
  77. if (objToAssign == null && _params.value.Type != JTokenType.Null) return CommandOutcome.Error;
  78. property.objectReferenceValue = objToAssign;
  79. break;
  80. case SerializedPropertyType.Float:
  81. property.floatValue = _params.value.Value<float>();
  82. break;
  83. case SerializedPropertyType.Integer:
  84. property.intValue = _params.value.Value<int>();
  85. break;
  86. case SerializedPropertyType.Boolean:
  87. property.boolValue = _params.value.Value<bool>();
  88. break;
  89. case SerializedPropertyType.String:
  90. property.stringValue = _params.value.Value<string>();
  91. break;
  92. case SerializedPropertyType.Vector3:
  93. property.vector3Value = _params.value.ToVector3();
  94. break;
  95. case SerializedPropertyType.Quaternion:
  96. property.quaternionValue = _params.value.ToQuaternion();
  97. break;
  98. default:
  99. context.ErrorMessage = $"The property type '{property.propertyType}' is not currently supported for setting values via SerializedObject.";
  100. return CommandOutcome.Error;
  101. }
  102. }
  103. catch (Exception e)
  104. {
  105. context.ErrorMessage = $"Failed to set value for '{_params.memberName}'. Error: {e.Message}";
  106. return CommandOutcome.Error;
  107. }
  108. serializedObject.ApplyModifiedProperties();
  109. Debug.Log($"[SetComponentValueCommand] Successfully set '{_params.memberName}' on '{_params.componentName}' using SerializedObject.");
  110. return CommandOutcome.Success;
  111. }
  112. private CommandOutcome SetValueWithReflection(Data.CommandContext context, Component target, PropertyInfo propertyInfo)
  113. {
  114. Undo.RecordObject(target, $"Set {propertyInfo.Name}");
  115. try
  116. {
  117. object valueToSet;
  118. var targetType = propertyInfo.PropertyType;
  119. if (targetType == typeof(Vector3)) valueToSet = _params.value.ToVector3();
  120. else if (targetType == typeof(Quaternion)) valueToSet = _params.value.ToQuaternion();
  121. else if (targetType.IsSubclassOf(typeof(Object)) || targetType == typeof(Object))
  122. {
  123. valueToSet = GetObjectReferenceValue(context, null);
  124. if (valueToSet == null && _params.value.Type != JTokenType.Null) return CommandOutcome.Error;
  125. }
  126. else
  127. {
  128. valueToSet = Convert.ChangeType(_params.value.ToObject(targetType), targetType);
  129. }
  130. propertyInfo.SetValue(target, valueToSet);
  131. }
  132. catch (Exception e)
  133. {
  134. context.ErrorMessage = $"Failed to set value for '{_params.memberName}' using Reflection. Error: {e.Message}";
  135. return CommandOutcome.Error;
  136. }
  137. EditorUtility.SetDirty(target);
  138. Debug.Log($"[SetComponentValueCommand] Successfully set '{_params.memberName}' on '{_params.componentName}' using Reflection.");
  139. return CommandOutcome.Success;
  140. }
  141. private Object GetObjectReferenceValue(Data.CommandContext context, SerializedProperty property)
  142. {
  143. var referenceData = _params.value.ToObject<ObjectReferenceValue>();
  144. if (referenceData == null)
  145. {
  146. if (_params.value.Type == JTokenType.Null) return null;
  147. context.ErrorMessage = "The 'value' for an ObjectReference must be an object with an 'identifier' or be null.";
  148. return null;
  149. }
  150. var baseObject = CommandUtility.ResolveIdentifier(context, referenceData.identifier);
  151. if (!baseObject)
  152. {
  153. context.ErrorMessage = $"Could not resolve the base identifier '{referenceData.identifier}' for the object reference.";
  154. return null;
  155. }
  156. Object objectToAssign = baseObject;
  157. if (baseObject is GameObject go && !string.IsNullOrEmpty(referenceData.childPath))
  158. {
  159. var childTransform = go.transform.Find(referenceData.childPath);
  160. if (!childTransform)
  161. {
  162. context.ErrorMessage = $"Could not find child '{referenceData.childPath}' on object '{baseObject.name}'.";
  163. return null;
  164. }
  165. objectToAssign = childTransform.gameObject;
  166. var componentType = GetTypeFromProperty(property);
  167. if (componentType != null && childTransform.TryGetComponent(componentType, out var component))
  168. {
  169. objectToAssign = component;
  170. }
  171. }
  172. return objectToAssign;
  173. }
  174. private static Type GetTypeFromProperty(SerializedProperty property)
  175. {
  176. if (property == null) return null;
  177. var typeString = property.type;
  178. var startIndex = typeString.IndexOf('<');
  179. var endIndex = typeString.IndexOf('>');
  180. if (startIndex == -1 || endIndex == -1) return null;
  181. var managedTypeName = typeString.Substring(startIndex + 1, endIndex - startIndex - 1).Trim();
  182. return GetTypeByName(managedTypeName);
  183. }
  184. private static Type GetTypeByName(string typeName)
  185. {
  186. return AppDomain.CurrentDomain.GetAssemblies()
  187. .Select(assembly => assembly.GetType(typeName, false))
  188. .FirstOrDefault(type => type != null);
  189. }
  190. [Serializable]
  191. private class ObjectReferenceValue
  192. {
  193. public string identifier;
  194. public string childPath;
  195. }
  196. }
  197. }