Files

223 lines
9.2 KiB
C#
Raw Permalink Normal View History

2026-03-16 21:38:49 +01:00
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<string> childTypes = new();
public List<string> defaultPlatforms = new(); // from [GuildPlatforms]
public List<string> defaultTags = new(); // from [GuildTags]
public List<FieldSchema> fields = new();
}
[Serializable]
public class PlatformConfig
{
public List<string> platforms = new();
public List<string> tags = new();
}
[Serializable]
public class SchemaManifest
{
public string version = "2";
public long exportedAt;
public string unityProjectId;
public List<string> shards = new(); // distinct shard names
public PlatformConfig platformConfig = new(); // all valid platforms + tags
public List<TypeSchema> 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<Type>(); } })
.Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t))
.ToList();
var shardSet = new HashSet<string>();
var typeMap = new Dictionary<string, TypeSchema>();
foreach (var t in entryTypes)
{
var shardAttr = t.GetCustomAttribute<ShardTargetAttribute>(false);
var shard = shardAttr?.ShardName ?? "default";
shardSet.Add(shard);
// Display name
var labelAttr = t.GetCustomAttribute<GuildLabelAttribute>(false);
var displayName = labelAttr?.Label ?? t.Name;
// Description
var descAttr = t.GetCustomAttribute<GuildDescriptionAttribute>(false);
var desc = descAttr?.Description ?? "";
// Default platforms
var platformAttr = t.GetCustomAttribute<GuildPlatformsAttribute>(false);
var defaultPlats = platformAttr != null
? platformAttr.Platforms.ToList()
: new List<string> { GuildPlatformRegistry.AllPlatforms };
// Default tags
var tagsAttr = t.GetCustomAttribute<GuildTagsAttribute>(false);
var defaultTags = tagsAttr?.Tags.ToList() ?? new List<string>();
// 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<FieldSchema> BuildFields(Type t, List<Type> allEntryTypes)
{
var result = new List<FieldSchema>();
var baseType = typeof(DataEntry);
var seen = new HashSet<string>();
// Walk up chain so parent fields appear first
var chain = new List<Type>();
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<GuildExcludeAttribute>() != null) continue;
if (f.GetCustomAttribute<HideInInspector>() != null) continue;
if (seen.Contains(f.Name)) continue;
seen.Add(f.Name);
var refAttr = f.GetCustomAttribute<GuildRefAttribute>();
var enumAttr = f.GetCustomAttribute<GuildEnumAttribute>();
result.Add(new FieldSchema
{
name = f.Name,
type = MapType(f.FieldType),
isArray = f.FieldType.IsArray,
isInherited = inherited,
isDisplayName = f.GetCustomAttribute<GuildDisplayNameAttribute>() != null,
isMultiline = f.GetCustomAttribute<GuildMultilineAttribute>() != 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 "";
}
}
}