Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Hypercube.Ecs/Entities/EntityRefAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ public delegate void EntityRefAction<T1, T2>(Entity entity, ref T1 component1, r

public delegate void EntityRefAction<T1, T2, T3>(Entity entity, ref T1 component1, ref T2 component2, ref T3 component3)
where T1 : IComponent where T2 : IComponent where T3 : IComponent;

public delegate void EntityRefAction<T1, T2, T3, T4>(Entity entity, ref T1 component1, ref T2 component2, ref T3 component3, ref T4 component4)
where T1 : IComponent where T2 : IComponent where T3 : IComponent where T4 : IComponent;
7 changes: 4 additions & 3 deletions src/Hypercube.Ecs/Events/Handling/EventHandlerList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ private void AddInternal(object handler, int priority)
try
{
var entry = new HandlerEntry(priority, handler);
if (priority == NoPriority)
var index = _handlers.FindIndex(h => priority > h.Priority);

if (index == -1)
{
_handlers.Add(entry);
return;
}

var index = _handlers.FindIndex(h => priority > h.Priority);

_handlers.Insert(index, entry);
}
finally
Expand Down
143 changes: 143 additions & 0 deletions src/Hypercube.Ecs/World.Dynamic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System.Reflection;
using JetBrains.Annotations;

namespace Hypercube.Ecs;

/// <summary>
/// Provides a dynamic bridge for component manipulation using runtime <see cref="Type"/> information.
/// </summary>
public partial class World
{
/// <summary>
/// Caches fully constructed generic methods to avoid repeated <see cref="MethodInfo.MakeGenericMethod"/> calls.
/// Key: (Method Name, Component Type).
/// </summary>
private readonly Dictionary<(string Name, Type GenericType), MethodInfo> _methodCache = new();

/// <summary>
/// Caches open generic method definitions to avoid expensive <see cref="Type.GetMethods"/> lookups and LINQ filtering.
/// Key: (Method Name, Parameter Count).
/// </summary>
private readonly Dictionary<(string Name, int ParamCount), MethodInfo> _genericDefinitions = new();

/// <summary>
/// Dynamically adds a component of the specified <paramref name="type"/> to an entity.
/// Uses the default constructor for the component type.
/// </summary>
/// <param name="entity">The target entity.</param>
/// <param name="type">The runtime type of the component to add.</param>
/// <returns>The newly created component instance.</returns>
public object Add(Entity entity, Type type)
{
var method = GetGenericMethod(nameof(Add), type, Type.EmptyTypes);
return method.Invoke(this, [entity])!;
}

/// <summary>
/// Dynamically adds a specific component instance to an entity.
/// </summary>
/// <param name="entity">The target entity.</param>
/// <param name="value">The component instance to add. If null, the operation is ignored.</param>
public void Add(Entity entity, object? value)
{
if (value is null)
return;

var type = value.GetType();
// Finds the overload: Add<T>(Entity entity, ref T component)
var method = GetGenericMethod(nameof(Add), type, [typeof(Entity), type.MakeByRefType()]);
method.Invoke(this, [entity, value]);
}

/// <summary>
/// Dynamically retrieves a component instance of the specified <paramref name="type"/> from an entity.
/// </summary>
/// <param name="entity">The target entity.</param>
/// <param name="type">The runtime type of the component to retrieve.</param>
/// <returns>The component instance.</returns>
public object Get(Entity entity, Type type)
{
var method = GetGenericMethod(nameof(Get), type, [typeof(Entity)]);
return method.Invoke(this, [entity])!;
}

/// <summary>
/// Dynamically checks if an entity has a component of the specified <paramref name="type"/>.
/// </summary>
/// <param name="entity">The target entity.</param>
/// <param name="type">The runtime type of the component to check.</param>
/// <returns>True if the entity possesses the component; otherwise, false.</returns>
public bool Has(Entity entity, Type type)
{
var method = GetGenericMethod(nameof(Has), type, [typeof(Entity)]);
return (bool)method.Invoke(this, [entity])!;
}

/// <summary>
/// Dynamically removes a component of the specified <paramref name="type"/> from an entity.
/// </summary>
/// <param name="entity">The target entity.</param>
/// <param name="type">The runtime type of the component to remove.</param>
public void Remove(Entity entity, Type type)
{
var method = GetGenericMethod(nameof(Remove), type, [typeof(Entity)]);
method.Invoke(this, [entity]);
}

/// <summary>
/// Resolves and caches a generic method using a two-tier caching strategy.
/// </summary>
/// <param name="name">The name of the method to resolve.</param>
/// <param name="genericType">The type argument for the generic method.</param>
/// <param name="parameterTypes">The types of the method parameters used to resolve overloads.</param>
/// <returns>A constructed <see cref="MethodInfo"/> ready for invocation.</returns>
private MethodInfo GetGenericMethod(string name, Type genericType, Type[] parameterTypes)
{
var cacheKey = (name, genericType);
if (_methodCache.TryGetValue(cacheKey, out var cached))
return cached;

var definitionKey = (name, parameterTypes.Length);
if (!_genericDefinitions.TryGetValue(definitionKey, out var methodDefinition))
{
methodDefinition = GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
.First(m => m.Name == name &&
m.IsGenericMethod &&
m.GetGenericArguments().Length == 1 &&
MatchParameters(m.GetParameters(), parameterTypes));

_genericDefinitions[definitionKey] = methodDefinition;
}

var genericMethod = methodDefinition.MakeGenericMethod(genericType);
_methodCache[cacheKey] = genericMethod;

return genericMethod;
}

/// <summary>
/// Validates if a method's parameters match the expected types,
/// accounting for generic placeholders and reference types.
/// </summary>
/// <param name="parameters">The parameters from the <see cref="MethodInfo"/>.</param>
/// <param name="parameterTypes">The expected runtime types.</param>
/// <returns>True if the signatures match; otherwise, false.</returns>
private bool MatchParameters(ParameterInfo[] parameters, Type[] parameterTypes)
{
if (parameters.Length != parameterTypes.Length)
return false;

for (var i = 0; i < parameters.Length; i++)
{
var paramType = parameters[i].ParameterType;

if (paramType.IsGenericParameter || (paramType.IsByRef && paramType.GetElementType()!.IsGenericParameter))
continue;

if (paramType != parameterTypes[i])
return false;
}

return true;
}
}
Loading