using System.Buffers.Binary;
using System.Threading;
using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.Server.Protocol.Client4229938;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
namespace Ananta.Server.Handlers.Game;
///
/// 移动命令的回执观测 —— AskReportLogicAgentCommandSuccess2(67057559)、
/// AskReportLogicAgentCommandFail(67193144)、AskReportLogicAgentCommandData2(67780469)、
/// AskAgentPoiActionDone(67016793)。
///
/// 移植自捐赠端 DremkaPS(2026-09-24)。他们这几条端点的价值写得很直白:
/// SyncLogicAgentCommand 对服务端是 fire-and-forget —— 客户端装了这条命令、还是拿到就丢了,
/// 在出站日志里长得一模一样。这几条是客户端的回答,有了它们,「行人还是不走」才能细分成
/// 「客户端收了 12 条一条没跑」还是「每一条进来就被拒了」—— 两种完全不同的故障,
/// 修法也完全相反。这正是服务端侧 stall 看门狗(规则 45)缺的那只眼睛。
///
/// ⛑⛑ 三条本服的取舍:
/// 1. 只观测,不改判定。回执计数先落进 AmbientPed,但不参与
/// StepFailCount 的回收判据 —— 那套判据是实测调过的,先把数据看清楚再动。
/// 日志里同时打出 stepFail=,两者的相关性肉眼就能对上。
/// 2. 只认我们自己的人。捐赠端提醒过:ambient crowd 和客户端自己的场景行人也走这几条
/// notify,所以这里按 agentId 在 AmbientState.Peds 里查,查不到就直接返回。
/// 3. POI 不重放。捐赠端的 AskAgentPoiActionDone 会把同一个 POI 再放回去,
/// 但本服根本不下发任何 POI action(Handlers / RpcTypes 里没有 POI 载荷),重放无从谈起;
/// 而他们自己也写了客户端「350 ms 内上报 31 条」,没有闸门的重放就是一场包风暴。
/// 这里只记观测日志。
///
internal sealed partial class GameRouter
{
// 规则 110:周期性/重复性事件不要「只打前 N 条」—— 那会让节奏类排查看到的全是假象。
// 一律「前 N 条 + 之后每 M 条」,并且日志带 count,让「连续性」本身肉眼可验。
private const int AckLogFirst = 10;
private const int AckLogEvery = 50;
///
/// ⛑⛑⛑ agentId 在 body 里的起点 —— 两条不一样,而且跟捐赠端给的契约都不一样。
///
/// 实测取自 logs/unknown-methods.log(2026-09-23 18:56,3.8 万条 Fail 全是 16 字节):
///
/// Success2 (67057559) bytes=14 FF 02 | 1A00BE5306862F73 | A8000000
/// ↑标记 ↑u64 agentId ↑int commandId
/// Fail (67193144) bytes=16 0600BE5306862F73 | C6000000 | 00000000
/// ↑u64 agentId ↑int commandId ↑尾部还有 4 字节
///
/// 校验:agentId 按 LE 读出来是 0x732F860653BE00xx ≈ 8.29e18,正好落在规则 23 的
/// 行人号段 8_300e15 上 —— 说明这两个偏移是对的,也能确认这些回执确实是我们自己的人发的。
///
/// 捐赠端把这两条当成同一个 12 字节布局(u64 + int,无标记),在 4229938 下是错的:
/// Success2 少算了 2 字节前缀,Fail 少算了 4 字节尾巴。这正是规则 22 说的
/// 「手写契约以实际 wire 为准,别信抄来的表」—— 所以这里干脆不碰契约,直接按偏移读。
///
private const int SuccessAckAgentOffset = 2;
private const int FailAckAgentOffset = 0;
private static int s_ackReports;
private static int s_ackUndecodable;
private static int s_poiDone;
[Handler(MethodId.AskReportLogicAgentCommandSuccess2, HandlerPacketKind.Notify)]
private Task AgentCommandSuccess(Connection conn, UxRpcMessage msg)
{
// Success2 的 agentId 从第 2 字节起(前面 FF 02 是 nullable 标记 + dispatch tag)。
if (!TryReadCommandAck(msg.Body, SuccessAckAgentOffset, out var agentId, out var commandId))
{
NoteUndecodable(conn, "Success2", msg.Body.Length);
return Task.CompletedTask;
}
var ped = FindAmbientPed(conn.Session, agentId);
if (ped is null)
return Task.CompletedTask;
int acks, fails, count;
lock (AmbientState(conn.Session).SyncRoot)
{
ped.CommandAcks++;
ped.LastAckCommandId = commandId;
acks = ped.CommandAcks;
fails = ped.CommandFails;
}
count = acks + fails;
LogAck(conn, "ack", agentId, commandId, acks, fails, count, ped);
return Task.CompletedTask;
}
[Handler(MethodId.AskReportLogicAgentCommandFail, HandlerPacketKind.Notify)]
private Task AgentCommandFail(Connection conn, UxRpcMessage msg)
{
// Fail 的 body 没有前置标记,agentId 从第 0 字节起。
if (!TryReadCommandAck(msg.Body, FailAckAgentOffset, out var agentId, out var commandId))
{
NoteUndecodable(conn, "Fail", msg.Body.Length);
return Task.CompletedTask;
}
var ped = FindAmbientPed(conn.Session, agentId);
if (ped is null)
return Task.CompletedTask;
int acks, fails, count;
lock (AmbientState(conn.Session).SyncRoot)
{
ped.CommandFails++;
ped.LastAckCommandId = commandId;
acks = ped.CommandAcks;
fails = ped.CommandFails;
}
count = acks + fails;
LogAck(conn, "fail", agentId, commandId, acks, fails, count, ped);
return Task.CompletedTask;
}
///
/// AskReportLogicAgentCommandData2 是 per-agent 的状态 / 层报告。只记条数与关键字段:
/// 它回答的是「客户端到底有没有在跑这台状态机」,不需要逐条解析 Data。
///
[Handler(MethodId.AskReportLogicAgentCommandData2, HandlerPacketKind.Notify)]
private Task AgentCommandReport(Connection conn, UxRpcMessage msg)
{
var n = Interlocked.Increment(ref s_ackReports);
if (n > AckLogFirst && n % AckLogEvery != 0)
return Task.CompletedTask;
var args = msg.TryGetArgs(out var decoded) ? decoded : null;
conn.Log.Info(args is null
? $"[CMD-ACK] report count={n} undecodable bytes={msg.Body.Length}"
: $"[CMD-ACK] report count={n} agent={args.AgentId} type={args.Type} "
+ $"layer={args.Layer}/{args.StateLayer} data={(args.Data?.Count ?? 0)}B");
return Task.CompletedTask;
}
///
/// AskAgentPoiActionDone:观测 only(理由见文件头第 3 条)。
///
[Handler(MethodId.AskAgentPoiActionDone, HandlerPacketKind.Notify)]
private Task AgentPoiActionDone(Connection conn, UxRpcMessage msg)
{
var n = Interlocked.Increment(ref s_poiDone);
if (n > AckLogFirst && n % AckLogEvery != 0)
return Task.CompletedTask;
var args = msg.TryGetArgs(out var decoded) ? decoded : null;
conn.Log.Info(args is null
? $"[CMD-ACK] poi-done count={n} undecodable bytes={msg.Body.Length}"
: $"[CMD-ACK] poi-done count={n} agent={args.agentId} poi={args.poiActionId} (no replay: server sends no POI)");
return Task.CompletedTask;
}
// ------------------------------------------------------------------ helpers
///
/// 按实测偏移读回执里的 (agentId, commandId)。agentId 固定 8 字节 LE,commandId 紧跟其后 4 字节 LE。
/// 长度不够就返回 false —— 宁可记一条 undecodable,也不要读半个字段当真值。
///
private static bool TryReadCommandAck(byte[] body, int agentOffset, out ulong agentId, out int commandId)
{
agentId = 0;
commandId = 0;
if (body is null || body.Length < agentOffset + 12)
return false;
agentId = BinaryPrimitives.ReadUInt64LittleEndian(body.AsSpan(agentOffset));
commandId = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(agentOffset + 8));
return true;
}
private static AmbientPed? FindAmbientPed(TcpSession session, ulong agentId)
{
if (agentId == 0)
return null;
var state = AmbientState(session);
lock (state.SyncRoot)
{
foreach (var ped in state.Peds)
{
if (ped.Id == agentId)
return ped;
}
}
return null;
}
private static void LogAck(Connection conn, string kind, ulong agentId, int commandId,
int acks, int fails, int count, AmbientPed ped)
{
if (count > AckLogFirst && count % AckLogEvery != 0)
return;
// stepFail 一并打出:服务端自己算的卡死计数与客户端回执的相关性,一眼就能对上。
conn.Log.Info($"[CMD-ACK] {kind} agent={agentId} cmd={commandId} acks={acks} fails={fails} "
+ $"count={count} stepFail={ped.StepFailCount} walking={ped.Walking}");
}
private static void NoteUndecodable(Connection conn, string kind, int bytes)
{
var n = Interlocked.Increment(ref s_ackUndecodable);
if (n > AckLogFirst && n % AckLogEvery != 0)
return;
conn.Log.Warn($"[CMD-ACK] {kind} body did not decode ({bytes}B) count={n}");
}
}