Initial commit: Ananta CBT 4229938 private server with all fixes
This commit is contained in:
commit
c84135c357
286 files changed
+2214315
No files matched your search
@@ -0,0 +1,96 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
/// <summary>Scans [Handler] methods and registers them in RpcRouter.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static int RegisterSelected(RpcRouter router, object target, IReadOnlySet<uint> allowedMethodIds)
|
||||
=> RegisterCore(router, target, allowedMethodIds);
|
||||
|
||||
private static int RegisterCore(RpcRouter router, object target, IReadOnlySet<uint>? allowedMethodIds)
|
||||
{
|
||||
var type = target as Type ?? target.GetType();
|
||||
var instance = target is Type ? null : target;
|
||||
var registeredMethodIds = new HashSet<uint>();
|
||||
|
||||
foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
var attrs = method.GetCustomAttributes<HandlerAttribute>(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<RpcContext, Task> 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<RpcContext, Task> _handler;
|
||||
public uint MethodId { get; }
|
||||
public AttributedInvokeHandler(uint methodId, Func<RpcContext, Task> handler) { MethodId = methodId; _handler = handler; }
|
||||
public Task HandleAsync(RpcContext ctx) => _handler(ctx);
|
||||
}
|
||||
|
||||
private sealed class AttributedNotifyHandler : INotifyHandler
|
||||
{
|
||||
private readonly Func<RpcContext, Task> _handler;
|
||||
public uint MethodId { get; }
|
||||
public AttributedNotifyHandler(uint methodId, Func<RpcContext, Task> handler) { MethodId = methodId; _handler = handler; }
|
||||
public Task HandleAsync(RpcContext ctx) => _handler(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Ananta.SDK.Logging;
|
||||
using Ananta.SDK.Network;
|
||||
using Ananta.SDK.Serialization;
|
||||
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// Small handler-facing wrapper around TcpSession. Prefer typed Return/Notify overloads;
|
||||
/// Raw methods exist only for packets whose contract has not been described yet.
|
||||
/// </summary>
|
||||
public sealed class Connection
|
||||
{
|
||||
private readonly TcpSession _session;
|
||||
private readonly CancellationToken _token;
|
||||
|
||||
internal Connection(TcpSession session, CancellationToken token)
|
||||
{
|
||||
_session = session;
|
||||
_token = token;
|
||||
}
|
||||
|
||||
public ServerLogger Log => _session.Log;
|
||||
public TcpSession Session => _session;
|
||||
|
||||
public Task ReturnEmptyOkAsync(UxRpcMessage request)
|
||||
=> ReturnEmptyAsync(request);
|
||||
|
||||
public Task ReturnEmptyAsync(UxRpcMessage request, int error = 0)
|
||||
=> request.IsInvoke
|
||||
? _session.ReturnAsync(request.MethodId, request.InvokeId, error, Array.Empty<byte>(), _token)
|
||||
: Task.CompletedTask;
|
||||
|
||||
public Task ReturnAsync<T>(UxRpcMessage request, T body, int error = 0)
|
||||
{
|
||||
if (!request.IsInvoke) return Task.CompletedTask;
|
||||
var bytes = UxSerializer.Serialize(body);
|
||||
var send = _session.ReturnAsync(request.MethodId, request.InvokeId, error, bytes, _token);
|
||||
RuntimeLogs.Decoded(_session.Log.Scope, "S2C", request.MethodId, typeof(T), body);
|
||||
return send;
|
||||
}
|
||||
|
||||
public Task ReturnRawAsync(UxRpcMessage request, byte[] body, int error = 0)
|
||||
=> request.IsInvoke
|
||||
? _session.ReturnAsync(request.MethodId, request.InvokeId, error, body, _token)
|
||||
: Task.CompletedTask;
|
||||
|
||||
public Task NotifyAsync<T>(uint methodId, T body)
|
||||
{
|
||||
var bytes = UxSerializer.Serialize(body);
|
||||
var send = _session.NotifyAsync(methodId, bytes, _token);
|
||||
RuntimeLogs.Decoded(_session.Log.Scope, "S2C", methodId, typeof(T), body);
|
||||
return send;
|
||||
}
|
||||
|
||||
public Task NotifyRawAsync(uint methodId, byte[] body)
|
||||
=> _session.NotifyAsync(methodId, body, _token);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public enum HandlerPacketKind
|
||||
{
|
||||
Both,
|
||||
Invoke,
|
||||
Notify
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Declares an incoming RPC endpoint. Method ids stay build-specific constants; the dispatcher is generic.
|
||||
/// By default a handler accepts both invoke and notify forms. Return helpers are safe no-ops for notifications.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class HandlerAttribute : Attribute
|
||||
{
|
||||
public uint MethodId { get; }
|
||||
public HandlerPacketKind PacketKind { get; }
|
||||
|
||||
public HandlerAttribute(uint methodId, HandlerPacketKind packetKind = HandlerPacketKind.Both)
|
||||
{
|
||||
MethodId = methodId;
|
||||
PacketKind = packetKind;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public interface INotifyHandler
|
||||
{
|
||||
uint MethodId { get; }
|
||||
Task HandleAsync(RpcContext ctx);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public interface IRpcHandler
|
||||
{
|
||||
uint MethodId { get; }
|
||||
Task HandleAsync(RpcContext ctx);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Ananta.SDK.Logging;
|
||||
using Ananta.SDK.Network;
|
||||
using Ananta.SDK.Serialization;
|
||||
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public sealed class RpcContext
|
||||
{
|
||||
public TcpSession Session { get; }
|
||||
public RpcPacket Packet { get; }
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public uint MethodId => Packet.MethodId;
|
||||
public int InvokeId => Packet.InvokeId;
|
||||
public byte[] Body => Packet.Body;
|
||||
|
||||
public RpcContext(TcpSession session, RpcPacket packet, CancellationToken cancellationToken)
|
||||
{
|
||||
Session = session;
|
||||
Packet = packet;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public Task ReturnAsync(byte[] body, int err = 0)
|
||||
=> Session.ReturnAsync(MethodId, InvokeId, err, body, CancellationToken);
|
||||
|
||||
public Task ReturnEmptyOkAsync()
|
||||
=> ReturnAsync(Array.Empty<byte>());
|
||||
|
||||
public Task ReturnAsync<T>(T body, int err = 0)
|
||||
{
|
||||
var bytes = UxSerializer.Serialize(body);
|
||||
var send = ReturnAsync(bytes, err);
|
||||
RuntimeLogs.Decoded(Session.Log.Scope, "S2C", MethodId, typeof(T), body);
|
||||
return send;
|
||||
}
|
||||
|
||||
public Task NotifyAsync(uint methodId, byte[] body)
|
||||
=> Session.NotifyAsync(methodId, body, CancellationToken);
|
||||
|
||||
public Task NotifyAsync<T>(uint methodId, T body)
|
||||
{
|
||||
var bytes = UxSerializer.Serialize(body);
|
||||
var send = NotifyAsync(methodId, bytes);
|
||||
RuntimeLogs.Decoded(Session.Log.Scope, "S2C", methodId, typeof(T), body);
|
||||
return send;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public enum RpcPacketKind
|
||||
{
|
||||
Unknown,
|
||||
Invoke,
|
||||
Notify,
|
||||
Return
|
||||
}
|
||||
|
||||
public sealed record RpcPacket(RpcPacketKind Kind, uint MethodId, int InvokeId, byte[] Body);
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public static class RpcPacketReader
|
||||
{
|
||||
public static RpcPacket Parse(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 5)
|
||||
return new RpcPacket(RpcPacketKind.Unknown, 0, 0, payload.ToArray());
|
||||
|
||||
var tag = payload[0];
|
||||
var methodId = BinaryPrimitives.ReadUInt32LittleEndian(payload[1..5]);
|
||||
|
||||
if (tag == 0x03)
|
||||
{
|
||||
return new RpcPacket(RpcPacketKind.Notify, methodId, 0, payload[5..].ToArray());
|
||||
}
|
||||
|
||||
if (tag == 0x01 && payload.Length >= 9)
|
||||
{
|
||||
var invokeId = BinaryPrimitives.ReadInt32LittleEndian(payload[5..9]);
|
||||
return new RpcPacket(RpcPacketKind.Invoke, methodId, invokeId, payload[9..].ToArray());
|
||||
}
|
||||
|
||||
if (tag == 0x02 && payload.Length >= 13)
|
||||
{
|
||||
var invokeId = BinaryPrimitives.ReadInt32LittleEndian(payload[5..9]);
|
||||
return new RpcPacket(RpcPacketKind.Return, methodId, invokeId, payload[13..].ToArray());
|
||||
}
|
||||
|
||||
return new RpcPacket(RpcPacketKind.Unknown, methodId, 0, payload.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public static class RpcPacketWriter
|
||||
{
|
||||
public static byte[] WriteNotify(uint methodId, ReadOnlySpan<byte> body)
|
||||
{
|
||||
var buffer = new byte[5 + body.Length];
|
||||
buffer[0] = 0x03;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(1, 4), methodId);
|
||||
body.CopyTo(buffer.AsSpan(5));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static byte[] WriteReturn(uint methodId, int invokeId, int err, ReadOnlySpan<byte> body)
|
||||
{
|
||||
var buffer = new byte[13 + body.Length];
|
||||
buffer[0] = 0x02;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(1, 4), methodId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(5, 4), invokeId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(9, 4), err);
|
||||
body.CopyTo(buffer.AsSpan(13));
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using Ananta.SDK.Logging;
|
||||
using Ananta.SDK.Network;
|
||||
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
public sealed class RpcRouter
|
||||
{
|
||||
private readonly Dictionary<uint, IRpcHandler> _invokeHandlers = new();
|
||||
private readonly Dictionary<uint, INotifyHandler> _notifyHandlers = new();
|
||||
private readonly ServerLogger _log;
|
||||
private readonly Dictionary<uint, string> _methodNames = new();
|
||||
private Func<RpcContext, Task>? _unknownInvokeHandler;
|
||||
private Func<RpcContext, Task>? _unknownNotifyHandler;
|
||||
|
||||
public RpcRouter(string scope) => _log = new ServerLogger($"router:{scope}");
|
||||
|
||||
public RpcRouter Name(uint methodId, string name)
|
||||
{
|
||||
// MethodId dump contains a few aliases with the same numeric id. Keep the first
|
||||
// canonical name instead of letting a later *_0 alias make logs harder to read.
|
||||
_methodNames.TryAdd(methodId, name);
|
||||
RpcMethodNames.Register(methodId, name);
|
||||
return this;
|
||||
}
|
||||
|
||||
private string Display(uint methodId)
|
||||
=> _methodNames.TryGetValue(methodId, out var name) ? $"{name} [0x{methodId:X8}]" : $"0x{methodId:X8}";
|
||||
|
||||
/// <summary>
|
||||
/// Build-specific compatibility fallback for invokes that are known to the client but do not
|
||||
/// yet have a semantic private-server implementation. The callback must return a body that
|
||||
/// matches the exact return reader for that client build; this avoids the old zero-byte/nil
|
||||
/// fallback which crashes strict generated Lua/C# callbacks.
|
||||
/// </summary>
|
||||
public RpcRouter OnUnknownInvoke(Func<RpcContext, Task> handler)
|
||||
{
|
||||
_unknownInvokeHandler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public RpcRouter OnUnknownNotify(Func<RpcContext, Task> handler)
|
||||
{
|
||||
_unknownNotifyHandler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public RpcRouter Add(IRpcHandler handler)
|
||||
{
|
||||
if (!_invokeHandlers.TryAdd(handler.MethodId, handler))
|
||||
throw new InvalidOperationException($"Duplicate invoke handler for {Display(handler.MethodId)}");
|
||||
return this;
|
||||
}
|
||||
|
||||
public RpcRouter Add(INotifyHandler handler)
|
||||
{
|
||||
if (!_notifyHandlers.TryAdd(handler.MethodId, handler))
|
||||
throw new InvalidOperationException($"Duplicate notify handler for {Display(handler.MethodId)}");
|
||||
return this;
|
||||
}
|
||||
|
||||
public async Task DispatchAsync(TcpSession session, Frame frame, CancellationToken token)
|
||||
{
|
||||
RpcPacket packet;
|
||||
try
|
||||
{
|
||||
packet = RpcPacketReader.Parse(frame.Payload);
|
||||
}
|
||||
catch
|
||||
{
|
||||
RuntimeLogs.RawFrame(session.Log.Scope, "C2S", frame.Mode, frame.Payload, "RPC_PARSE_FAILED");
|
||||
throw;
|
||||
}
|
||||
|
||||
var ctx = new RpcContext(session, packet, token);
|
||||
RuntimeLogs.Incoming(
|
||||
session.Log.Scope,
|
||||
packet.Kind == RpcPacketKind.Invoke ? RpcPacketKindView.Invoke :
|
||||
packet.Kind == RpcPacketKind.Notify ? RpcPacketKindView.Notify : RpcPacketKindView.Return,
|
||||
packet.MethodId, packet.InvokeId, packet.Body);
|
||||
|
||||
try
|
||||
{
|
||||
if (packet.Kind == RpcPacketKind.Invoke)
|
||||
{
|
||||
session.Log.Info($"-> rpc {Display(packet.MethodId)} #{packet.InvokeId} {packet.Body.Length}b");
|
||||
if (_invokeHandlers.TryGetValue(packet.MethodId, out var handler))
|
||||
{
|
||||
await handler.HandleAsync(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_unknownInvokeHandler is not null)
|
||||
{
|
||||
await _unknownInvokeHandler(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
session.Log.Warn($"unknown rpc {Display(packet.MethodId)} -> empty OK");
|
||||
await ctx.ReturnEmptyOkAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.Kind == RpcPacketKind.Notify)
|
||||
{
|
||||
session.Log.Info($"-> ntf {Display(packet.MethodId)} {packet.Body.Length}b");
|
||||
if (_notifyHandlers.TryGetValue(packet.MethodId, out var handler))
|
||||
{
|
||||
await handler.HandleAsync(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_unknownNotifyHandler is not null)
|
||||
{
|
||||
await _unknownNotifyHandler(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
session.Log.Warn($"unknown ntf {Display(packet.MethodId)} ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
session.Log.Warn($"Unknown rpc packet raw={Convert.ToHexString(frame.Payload)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
session.Log.Error($"handler failed method={Display(packet.MethodId)}", ex);
|
||||
if (packet.Kind == RpcPacketKind.Invoke)
|
||||
{
|
||||
if (_unknownInvokeHandler is not null)
|
||||
await _unknownInvokeHandler(ctx);
|
||||
else
|
||||
await ctx.ReturnEmptyOkAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Ananta.SDK.Serialization;
|
||||
|
||||
using Ananta.SDK.Logging;
|
||||
|
||||
namespace Ananta.SDK.Rpc;
|
||||
|
||||
/// <summary>A parsed UX RPC message exposed to attribute handlers.</summary>
|
||||
public sealed class UxRpcMessage
|
||||
{
|
||||
public RpcContext Context { get; }
|
||||
public RpcPacketKind Kind => Context.Packet.Kind;
|
||||
public uint MethodId => Context.MethodId;
|
||||
public int InvokeId => Context.InvokeId;
|
||||
public byte[] Body => Context.Body;
|
||||
public bool IsInvoke => Kind == RpcPacketKind.Invoke;
|
||||
public bool IsNotify => Kind == RpcPacketKind.Notify;
|
||||
|
||||
internal UxRpcMessage(RpcContext context) => Context = context;
|
||||
|
||||
public T GetArgs<T>()
|
||||
{
|
||||
var value = UxSerializer.Deserialize<T>(Body);
|
||||
RuntimeLogs.Decoded(Context.Session.Log.Scope, "C2S", MethodId, typeof(T), value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public bool TryGetArgs<T>(out T value)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = GetArgs<T>();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user