using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using UnityEngine; namespace Uigenx.Binding { /// One bindable path and its studio data type. public readonly struct UgxSchemaEntry { public readonly string Path; /// One of the studio data types: string, int, float, bool, color, vector2, vector3, sprite, enum, object, array. public readonly string Type; public readonly bool Writable; public UgxSchemaEntry(string path, string type, bool writable) { Path = path; Type = type; Writable = writable; } } /// /// Reflects a data source type into the flat path list the studio binds against. Shared by /// and the editor schema reflector so both report the same /// shape. Bounded by depth, entry count and a cycle guard. /// public static class UgxSchemaWalker { public const int DefaultMaxDepth = 6; public const int DefaultMaxEntries = 2048; const BindingFlags Members = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; public static List Collect(Type type, int maxDepth = DefaultMaxDepth, int maxEntries = DefaultMaxEntries) { var result = new List(32); if (type == null) return result; var seen = new HashSet(); Walk(type, null, result, seen, maxDepth, maxEntries); return result; } /// Studio data type name for a CLR type. Unknown reference types become `object`. public static string TypeName(Type type) { if (type == null) return "object"; var underlying = Nullable.GetUnderlyingType(type) ?? type; if (underlying.IsEnum) return "enum"; if (underlying == typeof(string)) return "string"; if (underlying == typeof(bool)) return "bool"; if (underlying == typeof(float) || underlying == typeof(double) || underlying == typeof(decimal)) return "float"; if (underlying == typeof(byte) || underlying == typeof(sbyte) || underlying == typeof(short) || underlying == typeof(ushort) || underlying == typeof(int) || underlying == typeof(uint) || underlying == typeof(long) || underlying == typeof(ulong)) return "int"; if (underlying == typeof(Color) || underlying == typeof(Color32)) return "color"; if (underlying == typeof(Vector2) || underlying == typeof(Vector2Int)) return "vector2"; if (underlying == typeof(Vector3) || underlying == typeof(Vector3Int)) return "vector3"; if (typeof(Sprite).IsAssignableFrom(underlying) || typeof(Texture).IsAssignableFrom(underlying)) return "sprite"; if (IsCollection(underlying)) return "array"; return "object"; } public static bool IsCollection(Type type) => type != null && type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type); public static Type ElementType(Type type) { if (type == null) return null; if (type.IsArray) return type.GetElementType(); foreach (var i in type.GetInterfaces()) { if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) return i.GetGenericArguments()[0]; } return typeof(object); } static void Walk(Type type, string prefix, List result, HashSet stack, int depth, int maxEntries) { if (depth <= 0 || result.Count >= maxEntries) return; if (!stack.Add(type)) return; foreach (var member in EnumerateMembers(type)) { if (result.Count >= maxEntries) break; var attribute = member.Info.GetCustomAttribute(); if (attribute != null && attribute.Ignore) continue; var segment = attribute?.Path ?? Camel(member.Info.Name); var path = prefix == null ? segment : prefix + "." + segment; var typeName = TypeName(member.ValueType); result.Add(new UgxSchemaEntry(path, typeName, member.Writable)); if (typeName == "array") { var element = ElementType(member.ValueType); var elementTypeName = TypeName(element); var indexed = path + "[0]"; result.Add(new UgxSchemaEntry(indexed, elementTypeName, false)); if (elementTypeName == "object") Walk(element, indexed, result, stack, depth - 1, maxEntries); } else if (typeName == "object") { Walk(member.ValueType, path, result, stack, depth - 1, maxEntries); } } stack.Remove(type); } readonly struct Member { public readonly MemberInfo Info; public readonly Type ValueType; public readonly bool Writable; public Member(MemberInfo info, Type valueType, bool writable) { Info = info; ValueType = valueType; Writable = writable; } } static IEnumerable EnumerateMembers(Type type) { for (var current = type; current != null && current != typeof(object); current = current.BaseType) { // The source's own members are always walked, but climbing into MonoBehaviour or // ScriptableObject would report name, tag, transform and gameObject and then // recurse through the whole Unity object graph, burying the real fields. if (current != type && typeof(UnityEngine.Object).IsAssignableFrom(current)) yield break; foreach (var field in current.GetFields(Members)) { if (field.IsStatic || field.IsInitOnly) continue; yield return new Member(field, field.FieldType, true); } foreach (var property in current.GetProperties(Members)) { if (property.GetIndexParameters().Length > 0) continue; var getter = property.GetGetMethod(false); if (getter == null || getter.IsStatic) continue; yield return new Member(property, property.PropertyType, property.GetSetMethod(false) != null); } } } static string Camel(string name) { if (string.IsNullOrEmpty(name)) return name; if (name.Length > 2 && name[0] == 'm' && name[1] == '_') name = name.Substring(2); if (name.Length == 0 || char.IsLower(name[0])) return name; return char.ToLowerInvariant(name[0]) + name.Substring(1); } } }