136 lines
4.5 KiB
C#
136 lines
4.5 KiB
C#
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using MessagePack;
|
|
|
|
namespace OECS;
|
|
|
|
/// <summary>
|
|
/// Registry of component types used by <see cref="WorldSerializer"/>.
|
|
/// Types are discovered via the OECS.SourceGen incremental generator
|
|
/// at compile time, with a runtime fallback that scans loaded assemblies
|
|
/// for <c>[MessagePackObject]</c> structs.
|
|
/// </summary>
|
|
public static class ComponentRegistry
|
|
{
|
|
private static ComponentDescriptor[]? _descriptors;
|
|
private static Dictionary<string, ComponentDescriptor>? _byTypeName;
|
|
private static bool _scanned;
|
|
|
|
/// <summary>
|
|
/// All discovered component descriptors.
|
|
/// </summary>
|
|
public static ComponentDescriptor[] Descriptors
|
|
{
|
|
get
|
|
{
|
|
EnsureScanned();
|
|
return _descriptors ?? Array.Empty<ComponentDescriptor>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lookup by assembly-qualified type name.
|
|
/// </summary>
|
|
public static IReadOnlyDictionary<string, ComponentDescriptor> ByTypeName
|
|
{
|
|
get
|
|
{
|
|
EnsureScanned();
|
|
if (_byTypeName == null)
|
|
{
|
|
var dict = new Dictionary<string, ComponentDescriptor>();
|
|
foreach (var desc in Descriptors)
|
|
dict[desc.TypeName] = desc;
|
|
_byTypeName = dict;
|
|
}
|
|
return _byTypeName;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by generated code to register discovered component types.
|
|
/// Multiple assemblies may call this. Descriptors are accumulated.
|
|
/// </summary>
|
|
public static void Register(ComponentDescriptor[] descriptors)
|
|
{
|
|
if (_descriptors == null)
|
|
{
|
|
_descriptors = descriptors;
|
|
}
|
|
else
|
|
{
|
|
var existing = new HashSet<string>(_descriptors.Select(d => d.TypeName));
|
|
var merged = new List<ComponentDescriptor>(_descriptors);
|
|
foreach (var d in descriptors)
|
|
{
|
|
if (!existing.Contains(d.TypeName))
|
|
{
|
|
existing.Add(d.TypeName);
|
|
merged.Add(d);
|
|
}
|
|
}
|
|
_descriptors = merged.ToArray();
|
|
}
|
|
_byTypeName = null;
|
|
}
|
|
|
|
private static void EnsureScanned()
|
|
{
|
|
if (_scanned) return;
|
|
_scanned = true;
|
|
|
|
// Runtime fallback: scan loaded assemblies for [MessagePackObject] structs.
|
|
// This ensures serialization works even when the source generator
|
|
// doesn't run (e.g., in test projects referencing game DLLs).
|
|
var scanned = new List<ComponentDescriptor>(_descriptors ?? Array.Empty<ComponentDescriptor>());
|
|
var seen = new HashSet<string>(scanned.Select(d => d.TypeName));
|
|
|
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
try
|
|
{
|
|
foreach (var type in asm.GetTypes())
|
|
{
|
|
if (!type.IsValueType || type.IsAbstract || !type.IsPublic)
|
|
continue;
|
|
if (type.GetCustomAttribute<MessagePackObjectAttribute>() == null)
|
|
continue;
|
|
|
|
var aqn = $"{type.FullName}, {type.Assembly.GetName().Name}";
|
|
if (!seen.Add(aqn))
|
|
continue;
|
|
|
|
var desc = CreateDescriptor(type, aqn);
|
|
scanned.Add(desc);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Some assemblies may throw during reflection (e.g., mixed-mode).
|
|
}
|
|
}
|
|
|
|
_descriptors = scanned.ToArray();
|
|
}
|
|
|
|
private static ComponentDescriptor CreateDescriptor(Type type, string aqn)
|
|
{
|
|
// Build serialize/deserialize delegates via reflection.
|
|
var method = typeof(ComponentRegistry).GetMethod(
|
|
nameof(CreateTypedDescriptor),
|
|
BindingFlags.NonPublic | BindingFlags.Static)!;
|
|
var generic = method.MakeGenericMethod(type);
|
|
return (ComponentDescriptor)generic.Invoke(null, [aqn])!;
|
|
}
|
|
|
|
private static ComponentDescriptor CreateTypedDescriptor<T>(string aqn) where T : struct
|
|
{
|
|
return new ComponentDescriptor(
|
|
typeName: aqn,
|
|
type: typeof(T),
|
|
serialize: obj => MessagePackSerializer.Serialize((T)obj),
|
|
deserializeAndAdd: (world, entity, data) =>
|
|
world.AddComponent(entity, MessagePackSerializer.Deserialize<T>(data)),
|
|
deserialize: data => MessagePackSerializer.Deserialize<T>(data));
|
|
}
|
|
} |