Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.AgentCommandAck.cs
T

210 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 移动命令的回执观测 —— <c>AskReportLogicAgentCommandSuccess2</c>(67057559)、
/// <c>AskReportLogicAgentCommandFail</c>(67193144)、<c>AskReportLogicAgentCommandData2</c>(67780469)、
/// <c>AskAgentPoiActionDone</c>(67016793)。
///
/// 移植自捐赠端 DremkaPS(2026-09-24)。他们这几条端点的价值写得很直白:
/// <c>SyncLogicAgentCommand</c> 对服务端是 fire-and-forget —— 客户端装了这条命令、还是拿到就丢了,
/// 在出站日志里长得<b>一模一样</b>。这几条是客户端的回答,有了它们,「行人还是不走」才能细分成
/// 「客户端收了 12 条一条没跑」还是「每一条进来就被拒了」—— 两种完全不同的故障,
/// 修法也完全相反。这正是服务端侧 stall 看门狗(规则 45)缺的那只眼睛。
///
/// ⛑⛑ 三条本服的取舍:
/// 1. <b>只观测,不改判定</b>。回执计数先落进 <c>AmbientPed</c>,但不参与
/// <c>StepFailCount</c> 的回收判据 —— 那套判据是实测调过的,先把数据看清楚再动。
/// 日志里同时打出 <c>stepFail=</c>,两者的相关性肉眼就能对上。
/// 2. <b>只认我们自己的人</b>。捐赠端提醒过:ambient crowd 和客户端自己的场景行人也走这几条
/// notify,所以这里按 agentId 在 <c>AmbientState.Peds</c> 里查,查不到就直接返回。
/// 3. <b>POI 不重放</b>。捐赠端的 <c>AskAgentPoiActionDone</c> 会把同一个 POI 再放回去,
/// 但本服根本不下发任何 POI action(Handlers / RpcTypes 里没有 POI 载荷),重放无从谈起;
/// 而他们自己也写了客户端「350 ms 内上报 31 条」,没有闸门的重放就是一场包风暴。
/// 这里只记观测日志。
/// </summary>
internal sealed partial class GameRouter
{
// 规则 110:周期性/重复性事件不要「只打前 N 条」—— 那会让节奏类排查看到的全是假象。
// 一律「前 N 条 + 之后每 M 条」,并且日志带 count,让「连续性」本身肉眼可验。
private const int AckLogFirst = 10;
private const int AckLogEvery = 50;
/// <summary>
/// ⛑⛑⛑ agentId 在 body 里的起点 —— <b>两条不一样</b>,而且<b>跟捐赠端给的契约都不一样</b>。
///
/// 实测取自 <c>logs/unknown-methods.log</c>(2026-09-23 18:56,3.8 万条 Fail 全是 16 字节):
/// <code>
/// Success2 (67057559) bytes=14 FF 02 | 1A00BE5306862F73 | A8000000
/// ↑标记 ↑u64 agentId ↑int commandId
/// Fail (67193144) bytes=16 0600BE5306862F73 | C6000000 | 00000000
/// ↑u64 agentId ↑int commandId ↑尾部还有 4 字节
/// </code>
/// 校验:agentId 按 LE 读出来是 0x732F860653BE00xx ≈ 8.29e18,正好落在规则 23 的
/// 行人号段 8_300e15 上 —— 说明这两个偏移是对的,也能确认这些回执确实是我们自己的人发的。
///
/// 捐赠端把这两条当成同一个 12 字节布局(u64 + int,无标记),在 4229938 下<b>是错的</b>:
/// Success2 少算了 2 字节前缀,Fail 少算了 4 字节尾巴。这正是规则 22 说的
/// 「手写契约以实际 wire 为准,别信抄来的表」—— 所以这里干脆不碰契约,直接按偏移读。
/// </summary>
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;
}
/// <summary>
/// <c>AskReportLogicAgentCommandData2</c> 是 per-agent 的状态 / 层报告。只记条数与关键字段:
/// 它回答的是「客户端到底有没有在跑这台状态机」,不需要逐条解析 <c>Data</c>。
/// </summary>
[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<SceneMethods.LogicAgentCommandReportData>(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;
}
/// <summary>
/// <c>AskAgentPoiActionDone</c>:观测 only(理由见文件头第 3 条)。
/// </summary>
[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<SceneMethods.AskAgentPoiActionDone>(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
/// <summary>
/// 按实测偏移读回执里的 (agentId, commandId)。agentId 固定 8 字节 LE,commandId 紧跟其后 4 字节 LE。
/// 长度不够就返回 false —— 宁可记一条 undecodable,也不要读半个字段当真值。
/// </summary>
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}");
}
}