using System; using System.Collections.Generic; using System.Reflection; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using UnityEngine.Events; using MCPForUnity.Runtime.Helpers; namespace MCPForUnity.Editor.Helpers { /// /// Low-level component operations extracted from ManageGameObject and ManageComponents. /// Provides pure C# operations without JSON parsing or response formatting. /// public static class ComponentOps { /// /// Adds a component to a GameObject with Undo support. /// /// The target GameObject /// The type of component to add /// Error message if operation fails /// The added component, or null if failed public static Component AddComponent(GameObject target, Type componentType, out string error) { error = null; if (target == null) { error = "Target GameObject is null."; return null; } if (componentType == null || !typeof(Component).IsAssignableFrom(componentType)) { error = $"Type '{componentType?.Name ?? "null"}' is not a valid Component type."; return null; } // Prevent adding duplicate Transform if (componentType == typeof(Transform)) { error = "Cannot add another Transform component."; return null; } // Check for 2D/3D physics conflicts string conflictError = CheckPhysicsConflict(target, componentType); if (conflictError != null) { error = conflictError; return null; } // Produce a clearer error when this component already exists and cannot be duplicated. Component existingComponent = target.GetComponent(componentType); if (existingComponent != null && !AllowsMultiple(target, componentType)) { error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances."; return null; } try { Component newComponent = Undo.AddComponent(target, componentType); if (newComponent == null) { if (target.GetComponent(componentType) != null && !AllowsMultiple(target, componentType)) { error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances."; } else { error = $"Failed to add component '{componentType.Name}' to '{target.name}'. Unity may restrict this component on the current target."; } return null; } // Apply default values for specific component types ApplyDefaultValues(newComponent); return newComponent; } catch (Exception ex) { error = $"Error adding component '{componentType.Name}': {ex.Message}"; return null; } } /// /// Removes a component from a GameObject with Undo support. /// /// The target GameObject /// The type of component to remove /// Error message if operation fails /// True if component was removed successfully public static bool RemoveComponent(GameObject target, Type componentType, out string error) { error = null; if (target == null) { error = "Target GameObject is null."; return false; } if (componentType == null) { error = "Component type is null."; return false; } // Prevent removing Transform if (componentType == typeof(Transform)) { error = "Cannot remove Transform component."; return false; } Component component = target.GetComponent(componentType); if (component == null) { error = $"Component '{componentType.Name}' not found on '{target.name}'."; return false; } try { Undo.DestroyObjectImmediate(component); return true; } catch (Exception ex) { error = $"Error removing component '{componentType.Name}': {ex.Message}"; return false; } } /// /// Sets a property value on a component using reflection. /// /// The target component /// The property or field name /// The value to set (JToken) /// Error message if operation fails /// True if property was set successfully public static bool SetProperty(Component component, string propertyName, JToken value, out string error) { error = null; if (component == null) { error = "Component is null."; return false; } if (string.IsNullOrEmpty(propertyName)) { error = "Property name is null or empty."; return false; } Type type = component.GetType(); BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; string normalizedName = ParamCoercion.NormalizePropertyName(propertyName); // UnityEventBase-derived types must be set via SerializedProperty, not reflection. // Reflection creates a disconnected object that Unity's serialization layer doesn't track, // causing m_PersistentCalls to be empty when the scene is saved. Type memberType = ResolveMemberType(type, propertyName, normalizedName); if (memberType != null && typeof(UnityEventBase).IsAssignableFrom(memberType)) { return SetViaSerializedProperty(component, propertyName, normalizedName, value, out error); } // Try reflection first (property, field, then non-public serialized field) if (TrySetViaReflection(component, type, propertyName, normalizedName, flags, value, out error)) return true; // Reflection failed — fall back to SerializedProperty which handles arrays, // custom serialization (e.g. UdonSharp), and types reflection can't convert. string reflectionError = error; if (SetViaSerializedProperty(component, propertyName, normalizedName, value, out error)) return true; // Both paths failed. If reflection found the member but couldn't convert, // report that (more useful than the SerializedProperty error). // If reflection didn't find it at all, report the SerializedProperty error. if (reflectionError != null && !reflectionError.Contains("not found")) error = reflectionError; return false; } private static bool TrySetViaReflection(object component, Type type, string propertyName, string normalizedName, BindingFlags flags, JToken value, out string error) { error = null; // Skip reflection for UnityEngine.Object types with JObject values // so SerializedProperty can resolve guid/spriteName/fileID forms. bool isJObjectValue = value != null && value.Type == JTokenType.Object; // Try property first PropertyInfo propInfo = type.GetProperty(propertyName, flags) ?? type.GetProperty(normalizedName, flags); if (propInfo != null && propInfo.CanWrite) { if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType)) { // Let SerializedProperty path handle complex object references. return false; } try { object convertedValue = PropertyConversion.ConvertToType(value, propInfo.PropertyType); if (convertedValue == null && value.Type != JTokenType.Null) { error = $"Failed to convert value for property '{propertyName}' to type '{propInfo.PropertyType.Name}'."; return false; } propInfo.SetValue(component, convertedValue); return true; } catch (Exception ex) { error = $"Failed to set property '{propertyName}': {ex.Message}"; return false; } } // Try field FieldInfo fieldInfo = type.GetField(propertyName, flags) ?? type.GetField(normalizedName, flags); if (fieldInfo != null && !fieldInfo.IsInitOnly) { if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType)) { // Let SerializedProperty path handle complex object references. return false; } try { object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType); if (convertedValue == null && value.Type != JTokenType.Null) { error = $"Failed to convert value for field '{propertyName}' to type '{fieldInfo.FieldType.Name}'."; return false; } fieldInfo.SetValue(component, convertedValue); return true; } catch (Exception ex) { error = $"Failed to set field '{propertyName}': {ex.Message}"; return false; } } // Try non-public serialized fields — traverse inheritance hierarchy fieldInfo = FindSerializedFieldInHierarchy(type, propertyName) ?? FindSerializedFieldInHierarchy(type, normalizedName); if (fieldInfo != null) { if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType)) { // Let SerializedProperty path handle complex object references. return false; } try { object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType); if (convertedValue == null && value.Type != JTokenType.Null) { error = $"Failed to convert value for serialized field '{propertyName}' to type '{fieldInfo.FieldType.Name}'."; return false; } fieldInfo.SetValue(component, convertedValue); return true; } catch (Exception ex) { error = $"Failed to set serialized field '{propertyName}': {ex.Message}"; return false; } } error = $"Property or field '{propertyName}' not found on component '{type.Name}'."; return false; } /// /// Gets all public properties and fields from a component type. /// public static List GetAccessibleMembers(Type componentType) { var members = new List(); if (componentType == null) return members; BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; foreach (var prop in componentType.GetProperties(flags)) { if (prop.CanWrite && prop.GetSetMethod() != null) { members.Add(prop.Name); } } foreach (var field in componentType.GetFields(flags)) { if (!field.IsInitOnly) { members.Add(field.Name); } } // Include private [SerializeField] fields - traverse inheritance hierarchy // Type.GetFields with NonPublic only returns fields declared directly on that type, // so we need to walk up the chain to find inherited private serialized fields var seenFieldNames = new HashSet(members); // Avoid duplicates with public fields Type currentType = componentType; while (currentType != null && currentType != typeof(object)) { foreach (var field in currentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { if (field.GetCustomAttribute() != null && !seenFieldNames.Contains(field.Name)) { members.Add(field.Name); seenFieldNames.Add(field.Name); } } currentType = currentType.BaseType; } members.Sort(); return members; } // --- Private Helpers --- /// /// Searches for a non-public [SerializeField] field through the entire inheritance hierarchy. /// Type.GetField() with NonPublic only returns fields declared directly on that type, /// so this method walks up the chain to find inherited private serialized fields. /// internal static FieldInfo FindSerializedFieldInHierarchy(Type type, string fieldName) { if (type == null || string.IsNullOrEmpty(fieldName)) return null; BindingFlags privateFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; Type currentType = type; // Walk up the inheritance chain while (currentType != null && currentType != typeof(object)) { // Search for the field on this specific type (case-insensitive) foreach (var field in currentType.GetFields(privateFlags)) { if (string.Equals(field.Name, fieldName, StringComparison.OrdinalIgnoreCase) && field.GetCustomAttribute() != null) { return field; } } currentType = currentType.BaseType; } return null; } private static string CheckPhysicsConflict(GameObject target, Type componentType) { bool isAdding2DPhysics = typeof(Rigidbody2D).IsAssignableFrom(componentType) || typeof(Collider2D).IsAssignableFrom(componentType); bool isAdding3DPhysics = typeof(Rigidbody).IsAssignableFrom(componentType) || typeof(Collider).IsAssignableFrom(componentType); if (isAdding2DPhysics) { if (target.GetComponent() != null || target.GetComponent() != null) { return $"Cannot add 2D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 3D Rigidbody or Collider."; } } else if (isAdding3DPhysics) { if (target.GetComponent() != null || target.GetComponent() != null) { return $"Cannot add 3D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 2D Rigidbody or Collider."; } } return null; } private static void ApplyDefaultValues(Component component) { // Default newly added Lights to Directional if (component is Light light) { light.type = LightType.Directional; } } private static bool AllowsMultiple(GameObject target, Type componentType) { if (target == null || componentType == null) { return false; } if (Attribute.IsDefined(componentType, typeof(DisallowMultipleComponent), inherit: true)) { return false; } return true; } // --- UnityEvent SerializedProperty support --- private static Type ResolveMemberType(Type componentType, string propertyName, string normalizedName) { BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; PropertyInfo propInfo = componentType.GetProperty(propertyName, flags) ?? componentType.GetProperty(normalizedName, flags); if (propInfo != null) return propInfo.PropertyType; FieldInfo fieldInfo = componentType.GetField(propertyName, flags) ?? componentType.GetField(normalizedName, flags); if (fieldInfo != null) return fieldInfo.FieldType; fieldInfo = FindSerializedFieldInHierarchy(componentType, propertyName) ?? FindSerializedFieldInHierarchy(componentType, normalizedName); if (fieldInfo != null) return fieldInfo.FieldType; return null; } private static bool SetViaSerializedProperty(Component component, string propertyName, string normalizedName, JToken value, out string error) { error = null; using var so = new SerializedObject(component); SerializedProperty prop = so.FindProperty(propertyName) ?? so.FindProperty(normalizedName); if (prop == null) { error = $"SerializedProperty '{propertyName}' not found on component '{component.GetType().Name}'."; return false; } if (!SetSerializedPropertyRecursive(prop, value, out error, 0)) return false; so.ApplyModifiedProperties(); // Readback verification for ObjectReference — these can silently fail if (prop.propertyType == SerializedPropertyType.ObjectReference && value != null && !(value is JValue jv && jv.Type == JTokenType.Null)) { so.Update(); var verifyProp = so.FindProperty(propertyName) ?? so.FindProperty(normalizedName); if (verifyProp != null && verifyProp.propertyType == SerializedPropertyType.ObjectReference && verifyProp.objectReferenceValue == null) { error = $"Property '{propertyName}' was set but the object reference did not persist. " + "Check that the referenced object exists and is the correct type."; return false; } } return true; } private static bool SetSerializedPropertyRecursive(SerializedProperty prop, JToken value, out string error, int depth) { error = null; const int MaxDepth = 20; if (depth > MaxDepth) { error = $"Maximum recursion depth ({MaxDepth}) exceeded."; return false; } try { // Array + JArray if (prop.isArray && prop.propertyType != SerializedPropertyType.String && value is JArray jArray) { prop.arraySize = jArray.Count; prop.serializedObject.ApplyModifiedProperties(); prop.serializedObject.Update(); for (int i = 0; i < jArray.Count; i++) { var element = prop.GetArrayElementAtIndex(i); if (!SetSerializedPropertyRecursive(element, jArray[i], out error, depth + 1)) return false; } return true; } // Generic (struct/class) + JObject if (prop.propertyType == SerializedPropertyType.Generic && !prop.isArray && value is JObject jObj) { foreach (var kvp in jObj) { var child = FindPropertyRelativeFuzzy(prop, kvp.Key); if (child == null) { error = $"Sub-property '{kvp.Key}' not found under '{prop.propertyPath}'."; return false; } if (!SetSerializedPropertyRecursive(child, kvp.Value, out error, depth + 1)) return false; } return true; } // ObjectReference if (prop.propertyType == SerializedPropertyType.ObjectReference) return SetObjectReference(prop, value, out error); // Leaf types switch (prop.propertyType) { case SerializedPropertyType.Integer: if (value == null || value.Type == JTokenType.Null || (value.Type != JTokenType.Integer && value.Type != JTokenType.Float && !long.TryParse(value.ToString(), out _))) { error = "Expected integer value."; return false; } if (prop.type == "long") prop.longValue = ParamCoercion.CoerceLong(value, 0); else prop.intValue = ParamCoercion.CoerceInt(value, 0); return true; case SerializedPropertyType.Boolean: if (value == null || value.Type == JTokenType.Null) { error = "Expected boolean value."; return false; } prop.boolValue = ParamCoercion.CoerceBool(value, false); return true; case SerializedPropertyType.Float: float floatVal = ParamCoercion.CoerceFloat(value, float.NaN); if (float.IsNaN(floatVal)) { error = "Expected float value."; return false; } prop.floatValue = floatVal; return true; case SerializedPropertyType.String: prop.stringValue = value == null || value.Type == JTokenType.Null ? string.Empty : value.ToString(); return true; case SerializedPropertyType.Enum: return SetEnum(prop, value, out error); default: error = $"Unsupported SerializedPropertyType: {prop.propertyType} at '{prop.propertyPath}'."; return false; } } catch (Exception ex) { error = $"Error setting '{prop.propertyPath}': {ex.Message}"; return false; } } internal static bool SetObjectReference(SerializedProperty prop, JToken value, out string error) { error = null; if (value == null || value.Type == JTokenType.Null) { prop.objectReferenceValue = null; return true; } if (value.Type == JTokenType.Integer) { int id = value.Value(); var resolved = GameObjectLookup.ResolveInstanceID(id); if (resolved == null) { error = $"No object found with instanceID {id}."; return false; } return AssignObjectReference(prop, resolved, null, out error); } if (value is JObject jObj) { // Optional component type filter — e.g. {"instanceID": 123, "component": "Button"} string componentFilter = jObj["component"]?.ToString(); var idToken = jObj["instanceID"]; if (idToken != null) { int id = ParamCoercion.CoerceInt(idToken, 0); var resolved = GameObjectLookup.ResolveInstanceID(id); if (resolved == null) { error = $"No object found with instanceID {id}."; return false; } return AssignObjectReference(prop, resolved, componentFilter, out error); } var guidToken = jObj["guid"]; if (guidToken != null) { string path = AssetDatabase.GUIDToAssetPath(guidToken.ToString()); if (string.IsNullOrEmpty(path)) { error = $"No asset found for GUID '{guidToken}'."; return false; } var spriteNameToken = jObj["spriteName"]; if (spriteNameToken != null) { string spriteName = spriteNameToken.ToString(); var allAssets = AssetDatabase.LoadAllAssetsAtPath(path); var originalRef = prop.objectReferenceValue; foreach (var asset in allAssets) { if (asset is Sprite sprite && sprite.name == spriteName) { prop.objectReferenceValue = sprite; if (prop.objectReferenceValue != null) return true; // Unity rejected the type — restore and report prop.objectReferenceValue = originalRef; error = $"Sprite '{spriteName}' found but is not compatible with the property type."; return false; } } error = $"Sprite '{spriteName}' not found in atlas '{path}'."; return false; } var fileIdToken = jObj["fileID"]; if (fileIdToken != null) { long targetFileId = fileIdToken.Value(); if (targetFileId != 0) { var allAssets = AssetDatabase.LoadAllAssetsAtPath(path); var originalRef = prop.objectReferenceValue; foreach (var asset in allAssets) { if (asset is Sprite sprite) { long spriteFileId = GetSpriteFileId(sprite); if (spriteFileId == targetFileId) { prop.objectReferenceValue = sprite; if (prop.objectReferenceValue != null) return true; prop.objectReferenceValue = originalRef; error = $"Sprite with fileID '{targetFileId}' found but is not compatible with the property type."; return false; } } } } error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'."; return false; } var loaded = AssetDatabase.LoadAssetAtPath(path); return AssignObjectReference(prop, loaded, componentFilter, out error); } var pathToken = jObj["path"]; if (pathToken != null) { string sanitized = AssetPathUtility.SanitizeAssetPath(pathToken.ToString()); var resolved = AssetDatabase.LoadAssetAtPath(sanitized); if (resolved == null) { error = $"No asset found at path '{pathToken}'."; return false; } return AssignObjectReference(prop, resolved, componentFilter, out error); } var nameToken = jObj["name"]; if (nameToken != null) { return ResolveSceneObjectByName(prop, nameToken.ToString(), componentFilter, out error); } error = "Object reference must contain 'instanceID', 'guid', 'path', or 'name'."; return false; } if (value.Type == JTokenType.String) { string strVal = value.ToString(); // Try as instanceID if the string is purely numeric if (int.TryParse(strVal, out int parsedId)) { var resolved = GameObjectLookup.ResolveInstanceID(parsedId); if (resolved != null) return AssignObjectReference(prop, resolved, null, out error); // Not a valid instanceID — fall through to path/name resolution } if (strVal.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || strVal.Contains("/")) { string sanitized = AssetPathUtility.SanitizeAssetPath(strVal); var resolved = AssetDatabase.LoadAssetAtPath(sanitized); if (resolved == null) { error = $"No asset found at path '{strVal}'."; return false; } return AssignObjectReference(prop, resolved, null, out error); } // Try as asset GUID (32-char hex string) if (strVal.Length == 32 && IsHexString(strVal)) { string assetPath = AssetDatabase.GUIDToAssetPath(strVal); if (!string.IsNullOrEmpty(assetPath)) { var resolved = AssetDatabase.LoadAssetAtPath(assetPath); if (resolved != null) return AssignObjectReference(prop, resolved, null, out error); } } // Fall back to scene hierarchy lookup by name. return ResolveSceneObjectByName(prop, strVal, null, out error); } error = $"Unsupported object reference format: {value.Type}."; return false; } /// /// Assigns a resolved object to a SerializedProperty, with automatic component fallback. /// If the resolved object is a GameObject but the property expects a Component type, /// searches the GameObject's components for a compatible one. /// Optionally filters by component type name (e.g. "Button", "Rigidbody"). /// private static bool AssignObjectReference(SerializedProperty prop, UnityEngine.Object resolved, string componentFilter, out string error) { error = null; if (resolved == null) { error = "Resolved object is null."; return false; } // If a component filter is specified and the resolved object is a GameObject, // find the specific component by type name. if (!string.IsNullOrEmpty(componentFilter) && resolved is GameObject filterGo) { var components = filterGo.GetComponents(); foreach (var comp in components) { if (comp == null) continue; if (string.Equals(comp.GetType().Name, componentFilter, StringComparison.OrdinalIgnoreCase) || string.Equals(comp.GetType().FullName, componentFilter, StringComparison.OrdinalIgnoreCase)) { prop.objectReferenceValue = comp; if (prop.objectReferenceValue != null) return true; } } error = $"Component '{componentFilter}' not found on GameObject '{filterGo.name}'."; return false; } // Try direct assignment first prop.objectReferenceValue = resolved; if (prop.objectReferenceValue != null) return true; // Sub-asset fallback: e.g., Texture2D → Sprite string subAssetPath = AssetDatabase.GetAssetPath(resolved); if (!string.IsNullOrEmpty(subAssetPath)) { var subAssets = AssetDatabase.LoadAllAssetsAtPath(subAssetPath); UnityEngine.Object match = null; int matchCount = 0; foreach (var sub in subAssets) { if (sub == null || sub == resolved) continue; prop.objectReferenceValue = sub; if (prop.objectReferenceValue != null) { match = sub; matchCount++; if (matchCount > 1) break; } } if (matchCount == 1) { prop.objectReferenceValue = match; return true; } // Clean up: probing may have left the property dirty prop.objectReferenceValue = null; if (matchCount > 1) { error = $"Multiple compatible sub-assets found in '{subAssetPath}'. " + "Use {\"guid\": \"...\", \"spriteName\": \"\"} or " + "{\"guid\": \"...\", \"fileID\": } for precise selection."; return false; } } // If the resolved object is a GameObject but the property expects a Component, // try each component on the GameObject until one is accepted. if (resolved is GameObject go) { var components = go.GetComponents(); foreach (var comp in components) { if (comp == null) continue; prop.objectReferenceValue = comp; if (prop.objectReferenceValue != null) return true; } error = $"GameObject '{go.name}' found but no compatible component for the property type."; return false; } error = $"Object '{resolved.name}' (type: {resolved.GetType().Name}) is not compatible with the property type."; return false; } /// /// Resolves a scene GameObject by name and assigns it (or a component on it) /// to a SerializedProperty. Uses GameObjectLookup for robust search /// including inactive objects and prefab stage support. /// private static bool ResolveSceneObjectByName(SerializedProperty prop, string name, string componentFilter, out string error) { error = null; if (string.IsNullOrWhiteSpace(name)) { error = "Cannot resolve object reference from empty name."; return false; } var ids = GameObjectLookup.SearchGameObjects( GameObjectLookup.SearchMethod.ByName, name, includeInactive: true, maxResults: 1); if (ids.Count == 0) { error = $"No GameObject named '{name}' found in scene."; return false; } var go = GameObjectLookup.FindById(ids[0]); if (go == null) { error = $"GameObject '{name}' found but could not be resolved."; return false; } return AssignObjectReference(prop, go, componentFilter, out error); } /// /// Finds a child SerializedProperty by name, falling back to underscore-insensitive matching. /// The batch_execute transport can strip underscores from JSON keys /// (e.g. m_PersistentCalls → mPersistentCalls), so we iterate immediate children /// and compare with underscores removed. /// private static SerializedProperty FindPropertyRelativeFuzzy(SerializedProperty parent, string key) { var child = parent.FindPropertyRelative(key); if (child != null) return child; string normalizedKey = key.Replace("_", "").ToLowerInvariant(); var end = parent.GetEndProperty(); var iter = parent.Copy(); if (!iter.Next(true)) return null; while (!SerializedProperty.EqualContents(iter, end)) { if (iter.depth == parent.depth + 1) { string normalizedName = iter.name.Replace("_", "").ToLowerInvariant(); if (normalizedName == normalizedKey) return parent.FindPropertyRelative(iter.name); } if (!iter.Next(false)) break; } return null; } private static bool SetEnum(SerializedProperty prop, JToken value, out string error) { error = null; var names = prop.enumNames; if (names == null || names.Length == 0) { error = "Enum has no names."; return false; } if (value.Type == JTokenType.Integer) { int idx = value.Value(); if (idx < 0 || idx >= names.Length) { error = $"Enum index out of range: {idx}."; return false; } prop.enumValueIndex = idx; return true; } string s = value.ToString(); for (int i = 0; i < names.Length; i++) { if (string.Equals(names[i], s, StringComparison.OrdinalIgnoreCase)) { prop.enumValueIndex = i; return true; } } error = $"Unknown enum name '{s}'."; return false; } private static long GetSpriteFileId(Sprite sprite) { if (sprite == null) return 0; try { var globalId = GlobalObjectId.GetGlobalObjectIdSlow(sprite); return unchecked((long)globalId.targetObjectId); } catch (Exception ex) { McpLog.Warn($"Failed to get fileID for sprite '{sprite.name}' (instanceID={sprite.GetInstanceIDCompat()}): {ex.Message}"); return 0; } } private static bool IsHexString(string str) { foreach (char c in str) { if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) return false; } return true; } } }