using System.Reflection;
namespace Ananta.SDK.Rpc;
/// Scans [Handler] methods and registers them in RpcRouter.
public static class AttributedHandlerRegistry
{
public static RpcRouter Build(string scope, params object[] targets)
{
var router = new RpcRouter(scope);
foreach (var target in targets) Register(router, target);
return router;
}
public static void Register(RpcRouter router, object target)
=> RegisterCore(router, target, allowedMethodIds: null);
///
/// Registers only explicitly allowed method ids. This is used by build-4229938 minimal mode so
/// legacy feature handlers stay compiled for reference but cannot emit old-build world/story data.
///
public static int RegisterSelected(RpcRouter router, object target, IReadOnlySet allowedMethodIds)
=> RegisterCore(router, target, allowedMethodIds);
private static int RegisterCore(RpcRouter router, object target, IReadOnlySet? allowedMethodIds)
{
var type = target as Type ?? target.GetType();
var instance = target is Type ? null : target;
var registeredMethodIds = new HashSet();
foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
{
var attrs = method.GetCustomAttributes(inherit: false).ToArray();
if (attrs.Length == 0) continue;
ValidateSignature(method);
foreach (var attr in attrs)
{
if (allowedMethodIds is not null && !allowedMethodIds.Contains(attr.MethodId))
continue;
router.Name(attr.MethodId, $"{type.Name}.{method.Name}");
Func invoke = ctx => InvokeAsync(instance, method, ctx);
if (attr.PacketKind is HandlerPacketKind.Both or HandlerPacketKind.Invoke)
router.Add(new AttributedInvokeHandler(attr.MethodId, invoke));
if (attr.PacketKind is HandlerPacketKind.Both or HandlerPacketKind.Notify)
router.Add(new AttributedNotifyHandler(attr.MethodId, invoke));
registeredMethodIds.Add(attr.MethodId);
}
}
return registeredMethodIds.Count;
}
private static void ValidateSignature(MethodInfo method)
{
var p = method.GetParameters();
if (p.Length != 2 || p[0].ParameterType != typeof(Connection) || p[1].ParameterType != typeof(UxRpcMessage))
throw new InvalidOperationException($"[Handler] {method.DeclaringType?.FullName}.{method.Name} must be (Connection, UxRpcMessage).");
if (method.ReturnType != typeof(void) && method.ReturnType != typeof(Task) && method.ReturnType != typeof(ValueTask))
throw new InvalidOperationException($"[Handler] {method.DeclaringType?.FullName}.{method.Name} must return void, Task or ValueTask.");
}
private static async Task InvokeAsync(object? instance, MethodInfo method, RpcContext ctx)
{
var conn = new Connection(ctx.Session, ctx.CancellationToken);
var msg = new UxRpcMessage(ctx);
try
{
var result = method.Invoke(instance, new object[] { conn, msg });
if (result is Task task) await task;
else if (result is ValueTask valueTask) await valueTask;
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
throw ex.InnerException;
}
}
private sealed class AttributedInvokeHandler : IRpcHandler
{
private readonly Func _handler;
public uint MethodId { get; }
public AttributedInvokeHandler(uint methodId, Func handler) { MethodId = methodId; _handler = handler; }
public Task HandleAsync(RpcContext ctx) => _handler(ctx);
}
private sealed class AttributedNotifyHandler : INotifyHandler
{
private readonly Func _handler;
public uint MethodId { get; }
public AttributedNotifyHandler(uint methodId, Func handler) { MethodId = methodId; _handler = handler; }
public Task HandleAsync(RpcContext ctx) => _handler(ctx);
}
}