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); } }