diff --git a/OECS.sln b/OECS.sln index b6b35a8..dab7a30 100644 --- a/OECS.sln +++ b/OECS.sln @@ -1,25 +1,84 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.SourceGen", "src\OECS.SourceGen\OECS.SourceGen.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS", "src\OECS\OECS.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.Tests", "tests\OECS.Tests\OECS.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blackjack", "examples\Blackjack\Blackjack.csproj", "{85631EDE-F984-4DA6-8EE9-0715AF1204E6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x64.ActiveCfg = Debug|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x64.Build.0 = Debug|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x86.ActiveCfg = Debug|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x86.Build.0 = Debug|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|Any CPU.Build.0 = Release|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x64.ActiveCfg = Release|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x64.Build.0 = Release|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x86.ActiveCfg = Release|Any CPU + {85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C3D4E5F6-A7B8-9012-CDEF-123456789012} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {85631EDE-F984-4DA6-8EE9-0715AF1204E6} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} EndGlobalSection EndGlobal diff --git a/examples/Blackjack/Blackjack.csproj b/examples/Blackjack/Blackjack.csproj index a91f1df..c0cd25f 100644 --- a/examples/Blackjack/Blackjack.csproj +++ b/examples/Blackjack/Blackjack.csproj @@ -12,4 +12,9 @@ + + + + \ No newline at end of file diff --git a/examples/TicTacToe/Program.cs b/examples/TicTacToe/Program.cs index 9c8d882..92d9192 100644 --- a/examples/TicTacToe/Program.cs +++ b/examples/TicTacToe/Program.cs @@ -50,8 +50,8 @@ public static class Program // ── Wire up systems ─────────────────────────────────────────── var group = new SystemGroup(world); - group.Add(new WinCheckSystem(world)); - group.Add(new RenderSystem(world)); + group.Add(new WinCheckSystem()); + group.Add(new RenderSystem()); // ── Game loop ───────────────────────────────────────────────── diff --git a/examples/TicTacToe/Systems/WinCheckSystem.cs b/examples/TicTacToe/Systems/WinCheckSystem.cs index 58b7137..e43bb0e 100644 --- a/examples/TicTacToe/Systems/WinCheckSystem.cs +++ b/examples/TicTacToe/Systems/WinCheckSystem.cs @@ -10,11 +10,13 @@ public class WinCheckSystem : ISystem { public void Run(World world) { - ref var state = ref world.GetSingleton(); - - if (state.Status != GameStatus.Playing) + // Check game status before doing any work. Use ReadSingleton + // to avoid auto-marking GameState as modified when we bail early. + if (world.ReadSingleton().Status != GameStatus.Playing) return; + ref var state = ref world.GetSingleton(); + // Build a 3×3 grid of marks using the iterator API. var grid = new Player[3, 3]; diff --git a/examples/TicTacToe/TicTacToe.csproj b/examples/TicTacToe/TicTacToe.csproj index 8852db0..c4b6b7f 100644 --- a/examples/TicTacToe/TicTacToe.csproj +++ b/examples/TicTacToe/TicTacToe.csproj @@ -12,10 +12,15 @@ + + + + PreserveNewest - + \ No newline at end of file diff --git a/src/OECS.SourceGen/ComponentDiscoveryGenerator.cs b/src/OECS.SourceGen/ComponentDiscoveryGenerator.cs new file mode 100644 index 0000000..b845fdc --- /dev/null +++ b/src/OECS.SourceGen/ComponentDiscoveryGenerator.cs @@ -0,0 +1,226 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Text; + +namespace OECS.SourceGen; + +/// +/// Incremental source generator that discovers all component types used with +/// and generates a strongly-typed registry so that +/// can save/load without reflection. +/// +[Generator] +public class ComponentDiscoveryGenerator : IIncrementalGenerator +{ + private static readonly HashSet _componentApiMethods = new() + { + // Component management + "AddComponent", + "RemoveComponent", + "HasComponent", + "GetComponent", + "TryGetComponent", + "ReadComponent", + "MarkModified", + + // Singletons + "SetSingleton", + "GetSingleton", + "ReadSingleton", + "HasSingleton", + "RemoveSingleton", + + // Observability + "ObserveComponentChanges", + + // Query building + "With", + "Without", + }; + + private static readonly HashSet _forEachNames = new() + { + "ForEach", + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Collect all generic method invocations that reference component types. + var invocations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: IsCandidateInvocation, + transform: ExtractComponentType) + .Where(t => t.FullyQualifiedName != null) + .Select((t, _) => (t.FullyQualifiedName!, t.AssemblyQualifiedName!)); + + // Also collect ForEach type arguments from the World class. + var forEachCalls = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: IsForEachCandidate, + transform: ExtractForEachTypes) + .Where(list => list.Length > 0) + .SelectMany((list, _) => list); + + var allTypes = invocations.Collect().Combine(forEachCalls.Collect()); + + context.RegisterSourceOutput(allTypes, GenerateRegistry); + } + + private static bool IsCandidateInvocation(SyntaxNode node, CancellationToken _) + { + if (node is not InvocationExpressionSyntax invocation) + return false; + + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + if (memberAccess.Name is GenericNameSyntax genericName) + { + return _componentApiMethods.Contains(genericName.Identifier.Text); + } + } + + return false; + } + + private static bool IsForEachCandidate(SyntaxNode node, CancellationToken _) + { + if (node is not InvocationExpressionSyntax invocation) + return false; + + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + var name = memberAccess.Name is GenericNameSyntax gn + ? gn.Identifier.Text + : memberAccess.Name.Identifier.Text; + return _forEachNames.Contains(name); + } + + return false; + } + + private static (string? FullyQualifiedName, string? AssemblyQualifiedName) ExtractComponentType( + GeneratorSyntaxContext ctx, CancellationToken _) + { + if (ctx.Node is not InvocationExpressionSyntax invocation) + return default; + + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + return default; + + if (memberAccess.Name is not GenericNameSyntax genericName) + return default; + + var typeArg = genericName.TypeArgumentList.Arguments.FirstOrDefault(); + if (typeArg == null) + return default; + + var symbolInfo = ctx.SemanticModel.GetSymbolInfo(typeArg); + if (symbolInfo.Symbol is not INamedTypeSymbol typeSymbol) + return default; + + // Only public struct types are valid components. + if (!typeSymbol.IsValueType || typeSymbol.IsAbstract) + return default; + if (typeSymbol.DeclaredAccessibility != Accessibility.Public) + return default; + + var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + // Assembly-qualified name for stable serialization (without assembly version). + var aqn = typeSymbol.ContainingAssembly != null + ? $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}, {typeSymbol.ContainingAssembly.Name}" + : fqn; + + return (fqn, aqn); + } + + private static ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> ExtractForEachTypes( + GeneratorSyntaxContext ctx, CancellationToken _) + { + if (ctx.Node is not InvocationExpressionSyntax invocation) + return ImmutableArray<(string, string)>.Empty; + + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + return ImmutableArray<(string, string)>.Empty; + + if (memberAccess.Name is not GenericNameSyntax genericName) + return ImmutableArray<(string, string)>.Empty; + + var types = ImmutableArray.CreateBuilder<(string, string)>(); + foreach (var typeArg in genericName.TypeArgumentList.Arguments) + { + var symbolInfo = ctx.SemanticModel.GetSymbolInfo(typeArg); + if (symbolInfo.Symbol is INamedTypeSymbol typeSymbol && + typeSymbol.IsValueType && !typeSymbol.IsAbstract && + typeSymbol.DeclaredAccessibility == Accessibility.Public) + { + var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var aqn = typeSymbol.ContainingAssembly != null + ? $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}, {typeSymbol.ContainingAssembly.Name}" + : fqn; + types.Add((fqn, aqn)); + } + } + + return types.ToImmutable(); + } + + private static void GenerateRegistry(SourceProductionContext context, + (ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> Left, + ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> Right) data) + { + var (invocations, forEachTypes) = data; + + // Collect unique types by fully-qualified name, deduplicating. + var typeMap = new Dictionary(); + foreach (var t in invocations) + typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName); + foreach (var t in forEachTypes) + { + typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName); + } + + if (typeMap.Count == 0) + return; + + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using MessagePack;"); + sb.AppendLine(); + sb.AppendLine("namespace OECS"); + sb.AppendLine("{"); + sb.AppendLine(" file static class ComponentRegistryInitializer"); + sb.AppendLine(" {"); + sb.AppendLine(" [ModuleInitializer]"); + sb.AppendLine(" public static void Initialize()"); + sb.AppendLine(" {"); + sb.AppendLine(" ComponentRegistry.Initialize(new ComponentDescriptor[]"); + sb.AppendLine(" {"); + + bool first = true; + foreach (var (fqn, aqn) in typeMap.Values.OrderBy(t => t.Fqn)) + { + if (!first) + sb.AppendLine(","); + first = false; + + sb.AppendLine($" new ComponentDescriptor("); + sb.AppendLine($" typeName: \"{aqn}\","); + sb.AppendLine($" type: typeof({fqn}),"); + sb.AppendLine($" serialize: obj => MessagePackSerializer.Serialize(({fqn})obj),"); + sb.AppendLine($" deserialize: data => MessagePackSerializer.Deserialize<{fqn}>(data)"); + sb.Append(" )"); + } + + sb.AppendLine(); + sb.AppendLine(" });"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + + context.AddSource("ComponentRegistry.g.cs", sb.ToString()); + } +} \ No newline at end of file diff --git a/src/OECS.SourceGen/OECS.SourceGen.csproj b/src/OECS.SourceGen/OECS.SourceGen.csproj new file mode 100644 index 0000000..b7a71b9 --- /dev/null +++ b/src/OECS.SourceGen/OECS.SourceGen.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + enable + enable + 12 + OECS.SourceGen + OECS.SourceGen + true + true + false + true + + + + + + + + + + + + \ No newline at end of file diff --git a/src/OECS/ComponentDescriptor.cs b/src/OECS/ComponentDescriptor.cs new file mode 100644 index 0000000..d91efa8 --- /dev/null +++ b/src/OECS/ComponentDescriptor.cs @@ -0,0 +1,41 @@ +namespace OECS; + +/// +/// Describes a component type discovered at compile time by the source generator. +/// Used by to save/load components without reflection. +/// +public sealed class ComponentDescriptor +{ + /// + /// The assembly-qualified name of the component type, for stable serialization + /// across assemblies. + /// + public string TypeName { get; } + + /// + /// The of the component. + /// + public Type Type { get; } + + /// + /// Serializes a boxed component instance to a MessagePack byte array. + /// + public Func Serialize { get; } + + /// + /// Deserializes a MessagePack byte array into a boxed component instance. + /// + public Func Deserialize { get; } + + public ComponentDescriptor( + string typeName, + Type type, + Func serialize, + Func deserialize) + { + TypeName = typeName; + Type = type; + Serialize = serialize; + Deserialize = deserialize; + } +} \ No newline at end of file diff --git a/src/OECS/ComponentRegistry.cs b/src/OECS/ComponentRegistry.cs new file mode 100644 index 0000000..f17202d --- /dev/null +++ b/src/OECS/ComponentRegistry.cs @@ -0,0 +1,51 @@ +using System.Runtime.CompilerServices; + +namespace OECS; + +/// +/// Registry of component types discovered at compile time by the +/// OECS.SourceGen incremental generator. The consuming project's +/// generated code populates this via a module initializer, so no +/// manual registration is needed. +/// +public static class ComponentRegistry +{ + private static ComponentDescriptor[]? _descriptors; + private static Dictionary? _byTypeName; + + /// + /// All discovered component descriptors. Populated automatically + /// by the source generator at module initialization. + /// + public static ComponentDescriptor[] Descriptors => + _descriptors ?? Array.Empty(); + + /// + /// Lookup by assembly-qualified type name. Populated automatically + /// by the source generator at module initialization. + /// + public static IReadOnlyDictionary ByTypeName + { + get + { + if (_byTypeName == null) + { + var dict = new Dictionary(); + foreach (var desc in Descriptors) + dict[desc.TypeName] = desc; + _byTypeName = dict; + } + return _byTypeName; + } + } + + /// + /// Called by generated code to register discovered component types. + /// Must be called before any serialization occurs. + /// + public static void Initialize(ComponentDescriptor[] descriptors) + { + _descriptors = descriptors; + _byTypeName = null; // Rebuild on next access. + } +} \ No newline at end of file diff --git a/src/OECS/World.cs b/src/OECS/World.cs index cadf8bb..b5eeb65 100644 --- a/src/OECS/World.cs +++ b/src/OECS/World.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Reflection; using R3; namespace OECS; @@ -664,6 +665,27 @@ public class World : IDisposable /// internal ComponentStore Components => _components; + /// + /// Adds a boxed component to an entity by its runtime type. + /// Used by during deserialization to + /// invoke the typed without reflection. + /// + internal void AddComponentBoxed(Type componentType, Entity entity, object component) + { + // Use a cached delegate per type to avoid reflection on every call. + if (!_addComponentDelegates.TryGetValue(componentType, out var del)) + { + var method = typeof(World).GetMethod(nameof(AddComponent))! + .MakeGenericMethod(componentType); + del = (Action)Delegate.CreateDelegate( + typeof(Action), method); + _addComponentDelegates[componentType] = del; + } + del(this, entity, component); + } + + private readonly Dictionary> _addComponentDelegates = new(); + // ── Helpers ─────────────────────────────────────────────────────── private void ThrowIfNotAlive(Entity entity) diff --git a/src/OECS/WorldSerializer.cs b/src/OECS/WorldSerializer.cs index e2d3476..8c45b17 100644 --- a/src/OECS/WorldSerializer.cs +++ b/src/OECS/WorldSerializer.cs @@ -5,9 +5,10 @@ namespace OECS; /// /// Saves and loads a to/from a MessagePack stream. /// -/// Components are serialized by their runtime type. The caller must -/// ensure all component types used in the world are resolvable via -/// at load time. +/// Uses the compile-time generated to +/// serialize and deserialize components without reflection. All component +/// types used with the World API are discovered by the OECS.SourceGen +/// incremental generator at build time. /// public static class WorldSerializer { @@ -18,9 +19,9 @@ public static class WorldSerializer { var entityComponents = new Dictionary>(); - foreach (var ct in world.Components.ComponentTypes) + foreach (var desc in ComponentRegistry.Descriptors) { - var set = world.Components.GetSet(ct); + var set = world.Components.GetSet(desc.Type); if (set == null || set.Count == 0) continue; var entities = set.GetDenseEntities(); @@ -37,8 +38,8 @@ public static class WorldSerializer list.Add(new ComponentEntry { - TypeName = ct.AssemblyQualifiedName!, - Data = MessagePackSerializer.Serialize(ct, component) + TypeName = desc.TypeName, + Data = desc.Serialize(component) }); } } @@ -67,6 +68,11 @@ public static class WorldSerializer { var snapshot = MessagePackSerializer.Deserialize(stream); + // Build a lookup by type name for O(1) descriptor resolution. + var lookup = new Dictionary(); + foreach (var desc in ComponentRegistry.Descriptors) + lookup[desc.TypeName] = desc; + foreach (var es in snapshot.Entities) { var entity = world.CreateEntity(es.Entity); @@ -81,18 +87,18 @@ public static class WorldSerializer foreach (var ce in es.Components) { - var type = Type.GetType(ce.TypeName) - ?? throw new InvalidOperationException( + if (!lookup.TryGetValue(ce.TypeName, out var desc)) + throw new InvalidOperationException( $"Unknown component type '{ce.TypeName}'. " + - $"Ensure the assembly is loaded."); + $"Ensure the component type is used with the World " + + $"API so the source generator can discover it."); - var component = MessagePackSerializer.Deserialize(type, ce.Data) + var component = desc.Deserialize(ce.Data) ?? throw new InvalidOperationException( $"Failed to deserialize component of type '{ce.TypeName}'."); - var method = typeof(World).GetMethod(nameof(World.AddComponent))! - .MakeGenericMethod(type); - method.Invoke(world, [entity, component]); + // Use the generated typed delegate to add the component. + world.AddComponentBoxed(desc.Type, entity, component); } } } diff --git a/tests/OECS.Tests/OECS.Tests.csproj b/tests/OECS.Tests/OECS.Tests.csproj index 1c8ca26..be47b57 100644 --- a/tests/OECS.Tests/OECS.Tests.csproj +++ b/tests/OECS.Tests/OECS.Tests.csproj @@ -23,4 +23,9 @@ + + + +