using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using UnityEngine; using Newtonsoft.Json; namespace Guildlib.Runtime { // ========================================================================= // Schema wire types — serialised to JSON and sent to the server // ========================================================================= [Serializable] public class FieldSchema { public string name; public string type; // "string" | "int" | "float" | "bool" | "string[]" … public bool isArray; public bool isInherited; // true = defined on a parent class public bool isDisplayName; // true = [GuildDisplayName] present public bool isMultiline; // true = [GuildMultiline] present public string refShard; // non-null = [GuildRef("shard")] present public string[] enumOptions; // non-null = [GuildEnum(...)] present public string defaultValue; } [Serializable] public class TypeSchema { public string typeName; // fully-qualified C# type name public string displayName; // from [GuildLabel] or class name public string description; // from [GuildDescription] or "" public string shard; // from [ShardTarget] public string parentType; // null if top-level DataEntry subclass public List childTypes = new(); public List defaultPlatforms = new(); // from [GuildPlatforms] public List defaultTags = new(); // from [GuildTags] public List fields = new(); } [Serializable] public class PlatformConfig { public List platforms = new(); public List tags = new(); } [Serializable] public class SchemaManifest { public string version = "2"; public long exportedAt; public string unityProjectId; public List shards = new(); // distinct shard names public PlatformConfig platformConfig = new(); // all valid platforms + tags public List types = new(); } // ========================================================================= // Exporter // ========================================================================= public static class SchemaExporter { public static string Export(string projectId = "") { var manifest = new SchemaManifest { exportedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), unityProjectId = projectId, platformConfig = new PlatformConfig { platforms = GuildPlatformRegistry.All.ToList(), tags = GuildTagRegistry.All.ToList(), } }; // Discover all concrete DataEntry subclasses in all loaded assemblies var baseType = typeof(DataEntry); var entryTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty(); } }) .Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t)) .ToList(); var shardSet = new HashSet(); var typeMap = new Dictionary(); foreach (var t in entryTypes) { var shardAttr = t.GetCustomAttribute(false); var shard = shardAttr?.ShardName ?? "default"; shardSet.Add(shard); // Display name var labelAttr = t.GetCustomAttribute(false); var displayName = labelAttr?.Label ?? t.Name; // Description var descAttr = t.GetCustomAttribute(false); var desc = descAttr?.Description ?? ""; // Default platforms var platformAttr = t.GetCustomAttribute(false); var defaultPlats = platformAttr != null ? platformAttr.Platforms.ToList() : new List { GuildPlatformRegistry.AllPlatforms }; // Default tags var tagsAttr = t.GetCustomAttribute(false); var defaultTags = tagsAttr?.Tags.ToList() ?? new List(); // Parent type (walk up skipping non-DataEntry classes) string parentTypeName = null; var parent = t.BaseType; while (parent != null && parent != baseType && parent != typeof(ScriptableObject) && parent != typeof(UnityEngine.Object)) { if (entryTypes.Contains(parent)) { parentTypeName = parent.FullName; break; } parent = parent.BaseType; } var schema = new TypeSchema { typeName = t.FullName, displayName = displayName, description = desc, shard = shard, parentType = parentTypeName, defaultPlatforms = defaultPlats, defaultTags = defaultTags, fields = BuildFields(t, entryTypes), }; typeMap[t.FullName] = schema; manifest.types.Add(schema); } // Second pass — populate childTypes foreach (var ts in manifest.types) if (ts.parentType != null && typeMap.TryGetValue(ts.parentType, out var p)) p.childTypes.Add(ts.typeName); manifest.shards = shardSet.OrderBy(s => s).ToList(); return JsonConvert.SerializeObject(manifest, Formatting.Indented); } // ── Field reflection ───────────────────────────────────────────────── static List BuildFields(Type t, List allEntryTypes) { var result = new List(); var baseType = typeof(DataEntry); var seen = new HashSet(); // Walk up chain so parent fields appear first var chain = new List(); var cur = t; while (cur != null && cur != baseType && cur != typeof(ScriptableObject)) { chain.Add(cur); cur = cur.BaseType; } chain.Reverse(); foreach (var type in chain) { bool inherited = type != t; foreach (var f in type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { if (f.GetCustomAttribute() != null) continue; if (f.GetCustomAttribute() != null) continue; if (seen.Contains(f.Name)) continue; seen.Add(f.Name); var refAttr = f.GetCustomAttribute(); var enumAttr = f.GetCustomAttribute(); result.Add(new FieldSchema { name = f.Name, type = MapType(f.FieldType), isArray = f.FieldType.IsArray, isInherited = inherited, isDisplayName = f.GetCustomAttribute() != null, isMultiline = f.GetCustomAttribute() != null, refShard = refAttr?.TargetShard, enumOptions = enumAttr?.Options, defaultValue = GetDefault(f.FieldType), }); } } return result; } static string MapType(Type t) { if (t == typeof(string)) return "string"; if (t == typeof(int)) return "int"; if (t == typeof(long)) return "long"; if (t == typeof(float)) return "float"; if (t == typeof(double)) return "double"; if (t == typeof(bool)) return "bool"; if (t == typeof(string[])) return "string[]"; if (t == typeof(int[])) return "int[]"; if (t == typeof(float[])) return "float[]"; return "string"; } static string GetDefault(Type t) { if (t == typeof(bool)) return "false"; if (t == typeof(float) || t == typeof(double) || t == typeof(int) || t == typeof(long)) return "0"; return ""; } } }