Initial commit: Ananta CBT 4229938 private server with all fixes

This commit is contained in:
CAtlas0784 committed 2026-09-15 18:13:46 +07:00
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);
}
}
+57
View File
@@ -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);
}
+25
View File
@@ -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;
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Ananta.SDK.Rpc;
public interface INotifyHandler
{
uint MethodId { get; }
Task HandleAsync(RpcContext ctx);
}
+7
View File
@@ -0,0 +1,7 @@
namespace Ananta.SDK.Rpc;
public interface IRpcHandler
{
uint MethodId { get; }
Task HandleAsync(RpcContext ctx);
}
+48
View File
@@ -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;
}
}
+11
View File
@@ -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);
+34
View File
@@ -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());
}
}
+26
View File
@@ -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;
}
}
+136
View File
@@ -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();
}
}
}
}
+40
View File
@@ -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;
}
}
}