Files

1310 lines
72 KiB
C#
Raw Permalink 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.Text;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Handlers;
using Ananta.Server.Protocol.Client4229938;
using Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 三条「客户端在发、服务端从没接」的轻量回执(2026-09-25,见桌面清单 T1-1 / T1-2 / T1-18)。
///
/// 共同背景:`logs/unknown-methods.log` 里它们都有记录,之前全部落到 typed-default 兜底。
/// 只有 **INVOKE** 会真的卡住客户端(await 一个永远不来的回包),NOTIFY 只是静默无害 ——
/// 所以下面三条按各自的真实方向注册,回包类型一律以 dump.cs 的声明为准:
///
/// | 方法 | 日志方向 | dump 返回 | 这里怎么回 |
/// |---|---|---|---|
/// | `AskAgentStartNpcSound`(67003057) | INVOKE ×268 | `UXRPCTask&lt;uint&gt;`(dump.cs:1728967) | `ReturnAsync(msg, 0u)` |
/// | `AskReadCityPedia`(63817604) | INVOKE ×120 | `UXRPCTask`(void,dump.cs:1726930) | `ReturnEmptyOkAsync` |
/// | `AskSetClientLockTarget`(67197587) | **NOTIFY** ×79 | `void`(dump.cs:1729846) | 不回,只打日志 |
///
/// ⛑ `AskSetClientLockTarget` 在日志里是 **NOTIFY 不是 INVOKE**(`0x04015A93`,8 字节 = ulong targetId)
/// ⇒ 它<b>不会</b>让客户端 await 卡死,接它唯一的收益是拿到玩家当前锁定的目标(打日志观察)。
/// 这里仍然注册 `HandlerPacketKind.Both`:万一将来客户端改用 INVOKE,也不会重新落兜底
/// (`ReturnEmptyOkAsync` 对非 Invoke 直接返回 CompletedTask,不会凭空回包)。
///
/// ⛑⛑ 硬规则 94:这些 id 必须同时在 `GameRouter.cs` 的 `Enabled4229938MethodIds` 里,
/// 否则编译通过但 handler 永不注册 —— Invoke 类还会让客户端 await 到超时。
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>
/// NPC 语音请求:`AskAgentStartNpcSound(ulong agentEntityId, uint libraryId)` — 67003057。
/// 请求体实测 12 字节(`8300BE5306862F73 50FD1300` = u64 + u32)。
/// 返回 `UXRPCTask&lt;uint&gt;` ⇒ 必须回一个 uint,取 0(=成功)。
/// 与 `AskStartDialog` 同型(`GameRouter.Dialog.cs` 也是 `ReturnAsync(msg, 0u)`)。
/// </summary>
[Handler(MethodId.AskAgentStartNpcSound, HandlerPacketKind.Invoke)]
private Task AskAgentStartNpcSound(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] npc-sound bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnAsync(msg, 0u);
}
/// <summary>
/// 城市图鉴点开详情:`AskReadCityPedia(uint cityPediaId)` — 63817604。
/// 请求体实测 4 字节(`1561B900` = u32 cityPediaId)。返回 `UXRPCTask`(void)⇒ 回空 OK。
/// 列表侧(295 条)早就通了,这条补的是「点开条目」那一半。
/// </summary>
[Handler(MethodId.AskReadCityPedia, HandlerPacketKind.Invoke)]
private Task AskReadCityPedia(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] city-pedia bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// 战斗锁定目标:`AskSetClientLockTarget(ulong targetId)` — 67197587。
/// 请求体实测 8 字节;`0000000000000000` = 清除锁定。
/// 纯观察:把 targetId 打出来,为以后「锁定/切目标」手感那条线留证据。
/// </summary>
[Handler(MethodId.AskSetClientLockTarget, HandlerPacketKind.Both)]
private Task AskSetClientLockTarget(Connection conn, UxRpcMessage msg)
{
var body = msg.Body.ToArray();
var targetId = body.Length >= 8 ? BitConverter.ToUInt64(body, 0) : 0UL;
conn.Log.Info($"[MISC-ACK] lock-target invoke={msg.IsInvoke} targetId={targetId} bytes={body.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
// ------------------------------------------------------------------
// 第二批(2026-09-25 19:5x)。
//
// ⛑⛑ 这一批的来源不是桌面清单,而是 **`logs/unknown-methods.log` 的真实统计**:
// 清单 T0–T5 表的状态列已经滞后(T1-3 / T2-16 / T1-12 实际都已实现),
// 所以本次改成「日志说了算」—— 取 09-23 以来仍在兜底的 INVOKE,再按**最后出现时间**
// 过滤掉老记录,剩下的就是现在真的还在 await 的。
//
// 这七条全部是 **INVOKE** 且 dump 声明返回 `UXRPCTask`(void)⇒ 空 OK 就是正解:
// AskDoNpcAction (67257034, dump.cs:1730257) 最后出现 09-25 19:20 ← 最活跃
// SyncWorldBattlePlayers (63999507, dump.cs:1727899) 09-25 16:28
// AskReleaseLuaSlotEntityEvent(67314956, dump.cs:1729447) 09-25 16:26
// AskAddPaokuLimit (67763540, dump.cs:1730638) 09-25 16:25
// AskRemovePaokuLimit (67281330, dump.cs:1730641)
// AskEnterMoveGround (67200298, dump.cs:1730539) 09-25 14:05
// AskLeaveMoveGround (67083087, dump.cs:1730542) 09-25 14:05
//
// ⛑ 清单 T1-11 把 AskEnterMoveGround / AskLeaveMoveGround 记成 NOTIFY —— **日志里是 INVOKE**
//(两边都注册了 Both 之外的可能),这里按 INVOKE 注册即可覆盖。
//
// ⛑⛑ 没做的三条(都要真返回值,不能空 OK,留下一轮):
// `AskTimePanelInfo` → `UXRPCTask<TimePanelInfo>`、`GetUploadObjectUrl` → `UXRPCTask<UploadObjectUrl>`、
// `AskItemDestructibleCreate` → `UXRPCTask<ulong>`。
// ------------------------------------------------------------------
[Handler(MethodId.AskDoNpcAction, HandlerPacketKind.Invoke)]
private Task AskDoNpcAction(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] npc-action bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.SyncWorldBattlePlayers, HandlerPacketKind.Invoke)]
private Task SyncWorldBattlePlayers(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] world-battle-players bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AskReleaseLuaSlotEntityEvent, HandlerPacketKind.Invoke)]
private Task AskReleaseLuaSlotEntityEvent(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] lua-slot-release bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AskAddPaokuLimit, HandlerPacketKind.Invoke)]
private Task AskAddPaokuLimit(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] paoku-limit add bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AskRemovePaokuLimit, HandlerPacketKind.Invoke)]
private Task AskRemovePaokuLimit(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] paoku-limit remove bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AskEnterMoveGround, HandlerPacketKind.Both)]
private Task AskEnterMoveGround(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] move-ground enter invoke={msg.IsInvoke} bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AskLeaveMoveGround, HandlerPacketKind.Both)]
private Task AskLeaveMoveGround(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] move-ground leave invoke={msg.IsInvoke} bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
// ------------------------------------------------------------------
// 第三批(2026-09-27)。
//
// 前两批把「返回 void ⇒ 空 OK」的都收完了,剩下这五条是**要真返回值**的,
// 空 OK 会让客户端拿到 0 字节后在反序列化处出问题(或拿到 null 后走错分支)。
//
// ⛑⛑ 这一批的 wire 布局一律以 **`RPCDeserializeAuto.lua` 的返回值 reader** 为准,
// dump.cs 只给字段声明、给不出 inline / 7Bit / presence 这些编码细节 —— 第二批之前的
// 几次踩坑都是「照 dump 写契约」。逐条依据:
//
// AskTimePanelInfo(63323227) rpc lua:10868 `Base.ReadStruct(Reader[612])`
// dump.cs:1728862 `UXRPCTask<TimePanelInfo>`
// ⇒ struct **无顶层标记**;回空列表 `FF 01`
// GetUploadObjectUrl(63757076) rpc lua:12156 `Base.ReadComplex(Reader[588])`
// dump.cs:1726xxx `UXRPCTask<UploadObjectUrl>`
// ⇒ complex 顶层 `FF` + string/u32/string
// AskItemDestructibleCreate(67362514) rpc lua:15250 `reader.ReadUInt64()`
// dump.cs:1730xxx `UXRPCTask<ulong>`
// ⇒ 裸 u64,8 字节 LE,无标记
// AskStartClimb(67646893) / AskEndClimb(67647726) ClientToGameSceneDelegate.lua:3706/3722 是
// `self.Notify(...)` ⇒ **NOTIFY 不是 INVOKE**,void 无参
//
// ⛑ 最后两条爬墙是日志里**最新**的兜底记录(09-27 07:32,各 240 次上下),
// 但方向是 NOTIFY ⇒ 不会让客户端 await 卡死,接它们纯粹是为了留证据 + 防将来改 INVOKE。
// ------------------------------------------------------------------
[Handler(MethodId.AskTimePanelInfo, HandlerPacketKind.Invoke)]
private Task AskTimePanelInfo(Connection conn, UxRpcMessage msg)
{
// 手机「时间/日程」面板。TimeAppUtils.lua:53 的回调只在 err ~= Ok 时才读
// `data.PersonalTimeSettings`,所以回 err=0 + 空列表是安全的(走不到读字段那条路)。
// ⛑ 元素类型 PersonalTimeSetting 在本轮**不会**被写到线上(列表为空),
// 所以它的字段是什么都无所谓 —— 但别信 dump.cs 的「空类」说法(lua reader 有 3 个字段),
// 复用 `TimeWeatherMethods4229938.cs` 里那个已有的定义。
// ⛑ 个人日程本身服务端没有存储,这次只解 await,不做持久化。
conn.Log.Info($"[MISC-ACK] time-panel bytes={msg.Body.Length} -> empty PersonalTimeSettings");
return conn.ReturnAsync(msg, new TimePanelInfo4229938());
}
[Handler(MethodId.GetUploadObjectUrl, HandlerPacketKind.Invoke)]
private Task GetUploadObjectUrl(Connection conn, UxRpcMessage msg)
{
// 客户端拿这个 URL 去 PUT 上传截图/涂鸦(OSS 预签名)。本服**严格本地、永不外联**,
// 也没有任何 HTTP 上传端点 ⇒ 只解 await,回一组空值。
// ⛑ 不要为了「让它看起来能用」编一个假 URL:客户端会真的去发那个 HTTP 请求,
// 结果是一个本地环境里永远连不上的域名,比空串更难排查。
// ⛑ ExpireTimeStamp 给 0(不是"已过期"的语义判断依据,客户端只在真上传时才用它)。
conn.Log.Info($"[MISC-ACK] upload-url bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body.ToArray())} -> empty (无上传端点)");
return conn.ReturnAsync(msg, new UploadObjectUrl4229938());
}
[Handler(MethodId.AskItemDestructibleCreate, HandlerPacketKind.Invoke)]
private Task AskItemDestructibleCreate(Connection conn, UxRpcMessage msg)
{
// 客户端请求服务端登记一个「可破坏物件」实例,等回一个 ulong 实例 id。
// 本服不跟踪可破坏物(只回号,不建实体),所以回的号只用于让客户端的 await 落地。
// ⛑ 硬规则 104:自分配号段的高水位必须落盘,否则重启后从下界重新发号 ⇒ 实例 id 撞号。
var id = unchecked((ulong)IdCounterStore.Next(DestructibleInstanceIdCounter, DestructibleInstanceIdFloor));
conn.Log.Info($"[MISC-ACK] destructible-create bytes={msg.Body.Length} -> id={id}");
return conn.ReturnAsync(msg, id);
}
[Handler(MethodId.AskStartClimb, HandlerPacketKind.Both)]
private Task AskStartClimb(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] climb start invoke={msg.IsInvoke} bytes={msg.Body.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AskEndClimb, HandlerPacketKind.Both)]
private Task AskEndClimb(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] climb end invoke={msg.IsInvoke} bytes={msg.Body.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
// 可破坏物实例号段(硬规则 104:走 IdCounterStore 落盘,不用进程内 static)。
// 8.4e18 —— 已占用号段:8.2e18 车实体 / 8.3e18 站桩 NPC / 8.31e18 归属 id / 7.1e18 车 token。
private const long DestructibleInstanceIdFloor = 8_400_000_000_000_000_000L;
private const string DestructibleInstanceIdCounter = "MiscAck.DestructibleInstanceId";
// ------------------------------------------------------------------
// 第四批(2026-09-27 23:3x):桌面清单 **T1-44 A 组 16 条 void 回执**。
//
// 选条方式跟第三批一样:**`unknown-methods.log` 的最后出现时间 + 逐条 grep 有无 handler**,
// 不照清单状态列(硬规则 214)。
//
// ⛑ 这 16 条的方向**逐条核过**:`INVOKE` 计数 1~156,`NOTIFY` **全为 0** ⇒ 全部按 `Invoke` 注册,
// 不像第三批的爬墙两条需要 `Both`。
// ⛑ 返回**全部 void**(`RPCDeserializeAuto.lua` 的 `midToReturnMessageReader[id]` 函数体都为空)
// ⇒ 空 OK 就是正解,不需要构造返回体。
//
// 只有四条做了字段解析(受击/击飞那四条),因为它们是**唯一**能让服务端读到
// 「客户端实际用了哪个 stiffId / 多久 / 落地速度」的通道;其余打 hex 足够。
// ------------------------------------------------------------------
/// <summary>统一的「打日志 + 回空 OK」。void 回执默认走这里。</summary>
private static Task AckVoid(Connection conn, UxRpcMessage msg, string tag)
{
conn.Log.Info($"[MISC-ACK] {tag} bytes={msg.Body.Length} " +
$"hex={Convert.ToHexString(msg.Body.ToArray())}");
return conn.ReturnEmptyOkAsync(msg);
}
// ---- 受击 / 击飞 / 落地四条(有解析,是唯一真值来源)----------------------------
/// <summary>
/// `ReportUnitHitFly`(67430580, 24B) = u64 defenderId | u64 attackerId | u32 hitFlyStiffId | f32 stiffTime。
/// dump 里还有个同名 S2C 签名 `void ReportUnitHitFly(ulong attackPid, uint stiffId)`(@535988),别抄错参数个数。
/// </summary>
[Handler(MethodId.ReportUnitHitFly, HandlerPacketKind.Invoke)]
private Task ReportUnitHitFly(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 24)
{
ulong defender = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0));
ulong attacker = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(8));
uint stiffId = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(16));
float stiffTime = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(20));
conn.Log.Info($"[MISC-ACK] hit-fly defender={defender} attacker={attacker} " +
$"stiffId={stiffId} stiffTime={stiffTime:F3}");
}
else
{
conn.Log.Warn($"[MISC-ACK] hit-fly short body bytes={b.Length} hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`ReportUnitHitFlyEnd`(67015647, 12B) = u64 defenderId | f32 speed。</summary>
[Handler(MethodId.ReportUnitHitFlyEnd, HandlerPacketKind.Invoke)]
private Task ReportUnitHitFlyEnd(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 12)
{
ulong defender = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0));
float speed = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(8));
conn.Log.Info($"[MISC-ACK] hit-fly-end defender={defender} speed={speed:F3}");
}
else
{
conn.Log.Warn($"[MISC-ACK] hit-fly-end short body bytes={b.Length} hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `ReportUnitFallGround`(67719880, 24B) = u64 unitId | i32 moveId | u32 stiffId | f32 stiffTime | f32 speed。
/// 日志实测:`A986010000200000 | 0B000000 | C5030000 | 00000000 | 609D6330`(stiffTime 恒 0)。
/// </summary>
[Handler(MethodId.ReportUnitFallGround, HandlerPacketKind.Invoke)]
private Task ReportUnitFallGround(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 24)
{
ulong unit = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0));
int moveId = BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(8));
uint stiffId = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12));
float stiffTime = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(16));
float speed = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(20));
conn.Log.Info($"[MISC-ACK] fall-ground unit={unit} moveId={moveId} stiffId={stiffId} " +
$"stiffTime={stiffTime:F3} speed={speed:F3}");
}
else
{
conn.Log.Warn($"[MISC-ACK] fall-ground short body bytes={b.Length} hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`ReportUnitFallGroundEnd`(67516089, 8B) = u64 unitId。</summary>
[Handler(MethodId.ReportUnitFallGroundEnd, HandlerPacketKind.Invoke)]
private Task ReportUnitFallGroundEnd(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var unit = b.Length >= 8 ? BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0)) : 0UL;
conn.Log.Info($"[MISC-ACK] fall-ground-end unit={unit} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
// ---- 投技 / 钳制两条(2026-09-29 20:4x,纯抓包,不改任何状态)------------------
/// <summary>
/// `AskControlPowerHoldEnemyUp`(67802701, 8B) = u64 enemyid —— 玩家把敌人<b>钳制 / 举起来</b>。
/// 契约出处 `_lua_all/lua/LuaGen/AutoGen/ClientToGameSceneDelegate.lua:4771-4777`
/// (Serializer 只写一个 u64)。客户端侧是 C# native 调用,lua 里没有调用点。
///
/// ⛑ 本轮只打日志 + 空回执,**不落地任何行为**(演出保护窗口等拿到时序再定)。
/// 判据:日志里出现 `[MISC-ACK] control-power-hold-up` 才说明客户端真走了这条 RPC。
/// </summary>
[Handler(MethodId.AskControlPowerHoldEnemyUp, HandlerPacketKind.Invoke)]
private Task AskControlPowerHoldEnemyUp(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 8)
{
var enemy = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0));
conn.Log.Info($"[MISC-ACK] control-power-hold-up enemy={enemy}");
}
else
{
conn.Log.Warn($"[MISC-ACK] control-power-hold-up short body bytes={b.Length} hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `AskControlPowerHoldEnemyFallGround`(67043294, 8B) = u64 enemyid —— 把钳制中的敌人<b>摔倒在地</b>。
/// 契约出处同上 `ClientToGameSceneDelegate.lua:322-328`。
/// </summary>
[Handler(MethodId.AskControlPowerHoldEnemyFallGrou, HandlerPacketKind.Invoke)]
private Task AskControlPowerHoldEnemyFallGround(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 8)
{
var enemy = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0));
conn.Log.Info($"[MISC-ACK] control-power-fall-ground enemy={enemy}");
}
else
{
conn.Log.Warn($"[MISC-ACK] control-power-fall-ground short body bytes={b.Length} hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
// ---- 其余 12 条(打 hex 即可)------------------------------------------------
/// <summary>`AskCancelInteractionActionRedPoi`(63584158) — u32 交互项 id(§46 乐器线那条:99323001 = 木吉他)。</summary>
[Handler(MethodId.AskCancelInteractionActionRedPoi, HandlerPacketKind.Invoke)]
private Task AskCancelInteractionActionRedPoi(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var actionId = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
conn.Log.Info($"[MISC-ACK] cancel-interact-red-poi actionId={actionId} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.ReportPlayerStartCommonInteract, HandlerPacketKind.Invoke)]
private Task ReportPlayerStartCommonInteract(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "common-interact start");
[Handler(MethodId.AskPlayerOnMetro, HandlerPacketKind.Invoke)]
private Task AskPlayerOnMetro(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "metro on-metro");
[Handler(MethodId.AskRemoveMapPin, HandlerPacketKind.Invoke)]
private Task AskRemoveMapPin(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "map-pin remove");
[Handler(MethodId.GmActiveBadgeEffect, HandlerPacketKind.Invoke)]
private Task GmActiveBadgeEffect(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm badge-effect");
[Handler(MethodId.GMActivateNpcProfile, HandlerPacketKind.Invoke)]
private Task GMActivateNpcProfile(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm npc-profile");
[Handler(MethodId.EndWorldLifeGame, HandlerPacketKind.Invoke)]
private Task EndWorldLifeGame(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "world-life-game end");
[Handler(MethodId.AskRestaurantBuyFoods, HandlerPacketKind.Invoke)]
private Task AskRestaurantBuyFoods(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "restaurant buy-foods");
[Handler(MethodId.RecordAgentBehavior, HandlerPacketKind.Invoke)]
private Task RecordAgentBehavior(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "record agent-behavior");
[Handler(MethodId.ChangeVehicleDoorState, HandlerPacketKind.Invoke)]
private Task ChangeVehicleDoorState(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "vehicle door-state");
[Handler(MethodId.AskStartGuideByCondition, HandlerPacketKind.Invoke)]
private Task AskStartGuideByCondition(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "guide start-by-condition");
[Handler(MethodId.AskSetSpiritFashionVariantPreference, HandlerPacketKind.Invoke)]
private Task AskSetSpiritFashionVariantPreference(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "fashion variant-preference");
// ------------------------------------------------------------------
// 第五批(2026-09-27 23:4x):T1-44 **B 组 5 条要真返回值的**。
//
// 前四批收的都是 void(空 OK);这五条空 OK 会让客户端拿到 0 字节后在反序列化处
// 拿到 null / 空壳而走错分支 ⇒ 必须按 lua 的返回值 reader 造真的返回体(硬规则 209)。
//
// ⛑ 方向逐条核过:INVOKE 1~86 次、**NOTIFY 全 0** ⇒ 全部按 `Invoke` 注册。
//
// ⛑⛑ 本批只保证**结构与 await 正确**,不保证语义正确(比如返回的 dialogId 是 0 = 无随机对话)。
// 真要做玩法(地图图钉持久化 / 世界生活局 / 篮球 / 武术赛季)得另开条目 —— 这里先把
// 客户端从"等不到回包"里放出来,并把请求参数打进日志给后面留数据。
// ------------------------------------------------------------------
/// <summary>
/// `AskPutMapPinFar`(63522219):请求 `(uint raidId, float x, float z, byte type)`(实测 13B),
/// 返回 `PinData`(struct 无标记)= `u64 Id` + `UXVector3 PinPos`(3×f32)。
/// 回一个落盘的自增 Id,并把请求里的 x/z 原样塞回 PinPos(y 补 0)—— 客户端拿到的是一个
/// 坐标自洽的图钉,不会瞬移。⛑ 本服**不持久化**图钉(重启即丢),要存得另开条目。
/// </summary>
[Handler(MethodId.AskPutMapPinFar, HandlerPacketKind.Invoke)]
private Task AskPutMapPinFar(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
uint raidId = 0;
float x = 0f, z = 0f;
byte pinType = 0;
if (b.Length >= 13)
{
raidId = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0));
x = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(4));
z = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(8));
pinType = b[12];
}
var id = unchecked((ulong)IdCounterStore.Next(MapPinIdCounter, MapPinIdFloor));
conn.Log.Info($"[MISC-ACK] map-pin put raid={raidId} x={x:F2} z={z:F2} type={pinType} -> id={id}");
return conn.ReturnAsync(msg, new PinData4229938
{
Id = id,
PinPos = new UxVector3 { X = x, Y = 0f, Z = z },
});
}
/// <summary>
/// `GetCharacterRandomDialog`(63175540):请求 `(uint agentId, uint MainTag, uint[] SubTag)`(实测 14B),
/// 返回裸 **u32** = 随机对话 id。
/// ⛑ 本服没有随机对话表 ⇒ 回 **0**(= 无随机对话,客户端应走"不播"分支,最安全)。
/// 先回 0 而不是编一个假 id:编 id 会让客户端去查表里不存在的行,比"没有"更难排查。
/// 接它的收益 = 解 await + 日志留下 `agentId / MainTag / SubTag`(以后要接真对话得查 DialogConfig)。
/// </summary>
[Handler(MethodId.GetCharacterRandomDialog, HandlerPacketKind.Invoke)]
private Task GetCharacterRandomDialog(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var agentId = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
var mainTag = b.Length >= 8 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4)) : 0u;
conn.Log.Info($"[MISC-ACK] random-dialog agent={agentId} mainTag={mainTag} bytes={b.Length} -> 0 (无对话表)");
return conn.ReturnAsync(msg, 0u);
}
/// <summary>
/// `StartWorldLifeGame`(67864009):请求 `uint typeId`(实测 4B),返回裸 **u64** = 世界生活局实例 id。
/// 号走 `IdCounterStore`(硬规则 104:重启不重号)。本服不跟踪对局,只发号解 await。
/// </summary>
[Handler(MethodId.StartWorldLifeGame, HandlerPacketKind.Invoke)]
private Task StartWorldLifeGame(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var typeId = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
var id = unchecked((ulong)IdCounterStore.Next(WorldLifeGameIdCounter, WorldLifeGameIdFloor));
conn.Log.Info($"[MISC-ACK] world-life-game start typeId={typeId} -> id={id}");
return conn.ReturnAsync(msg, id);
}
/// <summary>
/// `AskBasketballOperatorAction`(67028937):请求 `BasketballAskOperatorParam`(complex,实测 49B),
/// 返回裸 **u64**。**86 次,是本批最多的** —— 篮球机(§19 donor 段提过 T3-35 篮球 POI)。
/// ⛑ 返回的 u64 语义不明(可能是操作 token / 球局 id)⇒ 回 **0** 并打请求 hex,
/// 等日志里看清 `BasketballAskOperatorParam` 的实际取值再决定要不要发号。
/// </summary>
[Handler(MethodId.AskBasketballOperatorAction, HandlerPacketKind.Invoke)]
private Task AskBasketballOperatorAction(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] basketball operator bytes={msg.Body.Length} " +
$"hex={Convert.ToHexString(msg.Body.ToArray())} -> 0");
return conn.ReturnAsync(msg, 0UL);
}
/// <summary>
/// `AskWushuTournamentSeasonData`(63308286):**无参数**,返回 `ReadComplex(Reader[611])`
/// = `u32 SeasonId / u32 CurrentRound / bool IsSeasonCleared / List7 Rounds / u32 LastChallengeRound / u32 LastChallengeChoice`。
/// 本服没有赛季数据 ⇒ 回全 0 + **空 Rounds 列表**(`FF 01`)。
/// ⛑ 空列表也要占 2 字节,所以契约里 `Rounds` / `OpponentBestStars` 字段不能省。
/// </summary>
[Handler(MethodId.AskWushuTournamentSeasonData, HandlerPacketKind.Invoke)]
private Task AskWushuTournamentSeasonData(Connection conn, UxRpcMessage msg)
{
conn.Log.Info("[MISC-ACK] wushu season-data -> empty (无赛季数据)");
return conn.ReturnAsync(msg, new WushuTournamentSeasonClientInfo4229938());
}
// 第五批的号段(硬规则 104:走 IdCounterStore 落盘)。
// 8.5e18 地图图钉 / 8.6e18 世界生活局 —— 已占用:8.2e18 车实体 / 8.3e18 站桩 NPC /
// 8.31e18 归属 id / 8.4e18 可破坏物 / 7.1e18 车 token。
private const long MapPinIdFloor = 8_500_000_000_000_000_000L;
private const string MapPinIdCounter = "MiscAck.MapPinId";
private const long WorldLifeGameIdFloor = 8_600_000_000_000_000_000L;
private const string WorldLifeGameIdCounter = "MiscAck.WorldLifeGameId";
// ------------------------------------------------------------------
// 第六批(2026-09-29):`unknown-methods.log` 近三天(09-27 / 09-28)**仍在兜底**的全部 INVOKE。
//
// ⛑⛑ 26 万行的 unknown-methods.log 是 **09-15 起的累计**,top30 里绝大多数早就注册过了
// (清单状态列滞后 ⇒ 别照清单抄,硬规则 214)。本次取数方式:
// ① 只取 ts >= 2026-09-27 的行;② 与源码 `[Handler(...)]` 做差集;③ 逐条核对方向。
// 结果 56 个真待办里,**INVOKE 且未注册的只有这 13 个**(其余都是 NOTIFY,只登记不打紧)。
//
// ⛑ 方向:13 条 **全为 INVOKE、NOTIFY 计数 0** ⇒ 全部按 `Invoke` 注册,不需要 `Both`。
//
// ⛑ 返回类型以 `RPCDeserializeAuto.lua` 的 `midToReturnMessageReader[id]` 为准:
// **11 条在表里查不到** ⇒ 无返回值(void)⇒ 空 OK 就是正解;
// `AskCloseConnectionToGame`(63990036) → `reader.ReadUInt32()` ⇒ 回 `0u`;
// `AskMultiverseStatus`(63799383) → `Base.ReadComplex(Reader[288])` ⇒ 要造真的返回体
// (见 MiscMethods4229938.cs 的 `MultiverseStatusInfo4229938`)。
//
// ⛑ 参数布局全部来自 **unknown-methods.log 的实测 hex**(这些 id 在 `midToReader` 里没有
// lua 侧参数 reader ⇒ dump.cs 是唯一线索,但 dump 只给签名不给编码 ⇒ 以字节长度反推为准)。
// `GMChangeNpcFavor`(28 次) / `GmDropReward`(9) / `FastReenter`(9) 是这批里最活跃的三条,
// 都是客户端 GM 控制台(ALT+F1)发出来的 —— 接住它们 = 控制台按钮不再 await 卡死。
// ------------------------------------------------------------------
/// <summary>
/// `GMChangeNpcFavor`(62371863, 9B) = `uint npcAgentId | uint favor | byte flag`(实测
/// `1D506D05 10270000 00` ⇒ agent=91103261, favor=10000, flag=0)。**这批里最活跃(28 次)**。
/// ⛑ 本服**没有好感度存储**(NpcLife 只有好感 NPC 的坐标与日程)⇒ 这次只解 await + 留数据,
/// 不真的改好感。要落真值得先建 favor store,属另一条线。
/// </summary>
[Handler(MethodId.GMChangeNpcFavor, HandlerPacketKind.Invoke)]
private Task GMChangeNpcFavor(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 9)
{
uint npc = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0));
uint favor = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4));
byte flag = b[8];
conn.Log.Info($"[MISC-ACK] gm-npc-favor npc={npc} favor={favor} flag={flag}");
}
else
{
conn.Log.Warn($"[MISC-ACK] gm-npc-favor short body bytes={b.Length} " +
$"hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `GmDropReward`(62894352, 8B) = `uint rewardId | uint count`(实测 `C588DE00 0A000000`
/// ⇒ reward=14584005, count=10)。GM 控制台「发奖励」。本服无掉落表 ⇒ 只解 await + 留数据。
/// </summary>
[Handler(MethodId.GmDropReward, HandlerPacketKind.Invoke)]
private Task GmDropReward(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 8)
{
uint rewardId = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0));
uint count = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4));
conn.Log.Info($"[MISC-ACK] gm-drop-reward rewardId={rewardId} count={count}");
}
else
{
conn.Log.Warn($"[MISC-ACK] gm-drop-reward short body bytes={b.Length} " +
$"hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`FastReenter`(65694957, 0B) — GM 控制台「快速重进」,无参。</summary>
[Handler(MethodId.FastReenter, HandlerPacketKind.Invoke)]
private Task FastReenter(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "fast-reenter");
/// <summary>
/// `AskVehicleSendGamePlaySignal`(63816400, 1B) = `byte signalId`(实测 `0B` = 11)。
/// 客户端把车载玩法信号丢给服务端,返回 void ⇒ 空 OK。
/// </summary>
[Handler(MethodId.AskVehicleSendGamePlaySignal, HandlerPacketKind.Invoke)]
private Task AskVehicleSendGamePlaySignal(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
conn.Log.Info($"[MISC-ACK] vehicle-gameplay-signal signal={(b.Length > 0 ? b[0] : 0)} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`AskChangeSceneItemQuality`(67983820, 4B) = `uint quality`(实测 3)。场景物件画质档。</summary>
[Handler(MethodId.AskChangeSceneItemQuality, HandlerPacketKind.Invoke)]
private Task AskChangeSceneItemQuality(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var quality = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
conn.Log.Info($"[MISC-ACK] scene-item-quality quality={quality} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `AskPuppetGetup`(67548192, 8B) = `ulong puppetEntityId`(实测 `00E8764817000000` = 100000000000)。
/// 倒地单位起身回执。
/// </summary>
[Handler(MethodId.AskPuppetGetup, HandlerPacketKind.Invoke)]
private Task AskPuppetGetup(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var unit = b.Length >= 8 ? BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0)) : 0UL;
conn.Log.Info($"[MISC-ACK] puppet-getup unit={unit} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`AskChangeRoleUseSystemName`(63927753, 1B) = `bool/byte`(实测 0)。</summary>
[Handler(MethodId.AskChangeRoleUseSystemName, HandlerPacketKind.Invoke)]
private Task AskChangeRoleUseSystemName(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "role-use-system-name");
/// <summary>
/// `AskMetroHit`(67907276, 25B) = complex:`u32 a | u64 unit | f32 v | u32 b | u32 c`
/// (实测 `FF 2C010000 00E8764817000000 00004041 9D474703 64000000` ⇒ a=300, unit=1e11,
/// v=12.0, b=55000093, c=100)。⛑ 字段语义未定 ⇒ 只按长度解析并打出来,不猜名字。
/// </summary>
[Handler(MethodId.AskMetroHit, HandlerPacketKind.Invoke)]
private Task AskMetroHit(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 25 && b[0] == 0xFF)
{
uint a = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(1));
ulong unit = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(5));
float v = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(13));
uint c1 = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(17));
uint c2 = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(21));
conn.Log.Info($"[MISC-ACK] metro-hit a={a} unit={unit} v={v:F3} b={c1} c={c2}");
}
else
{
conn.Log.Warn($"[MISC-ACK] metro-hit unexpected bytes={b.Length} " +
$"hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`AskMetroHitEnd`(67737840, 8B) = `ulong unitId`。</summary>
[Handler(MethodId.AskMetroHitEnd, HandlerPacketKind.Invoke)]
private Task AskMetroHitEnd(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var unit = b.Length >= 8 ? BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(0)) : 0UL;
conn.Log.Info($"[MISC-ACK] metro-hit-end unit={unit} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `AskSaveSettings`(63411797, 28B):尾部是一个 **长度前缀(1 字节)+ JSON**,
/// 实测 `… 0D 7B22333034303031223A317D` ⇒ len=13 + `{"304001":1}`(设置项 KV)。
/// ⛑ 本服不持久化设置(重启即回默认)⇒ 只解 await + 把 JSON 打进日志。
/// </summary>
[Handler(MethodId.AskSaveSettings, HandlerPacketKind.Invoke)]
private Task AskSaveSettings(Connection conn, UxRpcMessage msg)
{
var b = msg.Body.ToArray();
var json = string.Empty;
if (b.Length >= 2)
{
var len = b[^1];
if (len > 0 && b.Length >= len + 1)
json = Encoding.UTF8.GetString(b, b.Length - 1 - len, len);
}
conn.Log.Info($"[MISC-ACK] save-settings json={json} bytes={b.Length} " +
$"hex={Convert.ToHexString(b)}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `AskCloseConnectionToGame`(63990036):参数 = 一个 string(**Int7 长度带 +1 偏置**),
/// 实测 `0C 4B69636B546F4C6F67696E` ⇒ 0x0C=12 ⇒ 11 字符 = `KickToLogin`。
/// 返回 **u32** ⇒ 回 0(= 同意)。客户端自己会断连回登录,服务端不需要额外动作。
/// </summary>
[Handler(MethodId.AskCloseConnectionToGame, HandlerPacketKind.Invoke)]
private Task AskCloseConnectionToGame(Connection conn, UxRpcMessage msg)
{
var b = msg.Body.ToArray();
var reason = string.Empty;
if (b.Length >= 2)
{
var len = b[0] > 0 ? b[0] - 1 : 0; // Int7 计数恒 +1 偏置
if (len > 0 && b.Length >= len + 1)
reason = Encoding.UTF8.GetString(b, 1, len);
else if (len == 0 && b.Length == 1)
reason = string.Empty;
}
conn.Log.Info($"[MISC-ACK] close-connection reason='{reason}' bytes={b.Length} -> 0");
return conn.ReturnAsync(msg, 0u);
}
/// <summary>`AskClearSwitchInFashionCache`(67098371, 4B) = `uint id`(实测 15021095)。</summary>
[Handler(MethodId.AskClearSwitchInFashionCache, HandlerPacketKind.Invoke)]
private Task AskClearSwitchInFashionCache(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var id = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
conn.Log.Info($"[MISC-ACK] clear-fashion-switch-cache id={id} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `AskMultiverseStatus`(63799383, 0B):返回 `MultiverseStatusInfo`(complex,顶层 0xFF)
/// = 三个 `Dict&lt;uint, byte&gt;` + `ShowLastMutiId` + `CurMutiPanelId`。
/// 本服没有多世界数据 ⇒ 回 **全 0 + 三个空字典**(空字典也占 `FF 00000000`)。
/// ⛑ 空 OK 会让客户端读到 0 字节 ⇒ 反序列化直接 nil 走错分支,所以必须回真返回体。
/// </summary>
[Handler(MethodId.AskMultiverseStatus, HandlerPacketKind.Invoke)]
private Task AskMultiverseStatus(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[MISC-ACK] multiverse-status bytes={msg.Body.Length} -> empty (无多世界数据)");
return conn.ReturnAsync(msg, new MultiverseStatusInfo4229938());
}
// ------------------------------------------------------------------
// 第七批(2026-09-29):客户端 **GM 控制台(ALT+F1)** 剩下的 9 条 INVOKE。
//
// ⛑ 这 9 条**全部只出现过 1 次**(09-28 06:00–06:07 那一次控制台会话),
// 单看频次毫无价值 —— 接它们的理由是另一条:它们是 **INVOKE**,
// 漏登记 ⇒ 控制台按钮 await 到超时(硬规则 94),用户点了就是"没反应"。
// 加上第六批的 13 条,`unknown-methods.log` 近三天里的 **INVOKE 已全部有回执**
// ⇒ 客户端理论上再无任何 await 挂死。剩下的 35 条全是 NOTIFY(静默无害)。
//
// ⛑ 返回**全部 void**(`midToReturnMessageReader` 里 9 个 id 一个都没有)⇒ 空 OK 即正解。
// ⛑ 本批**不做真实现**:`GmUnlockSystem` 的 139 / `GmChangeSystem` 的 (0,1) 都指向
// 服务端没有的那张「系统解锁表」,编一个假结果比不回更难排查(硬规则 230)。
// 只解 await + 把参数打进日志,等以后真要建系统解锁表时这批日志就是原始数据。
// ------------------------------------------------------------------
/// <summary>`GmWorldRefreshEnemyGroup`(69676618, 0B) — 刷新世界敌人组。</summary>
[Handler(MethodId.GmWorldRefreshEnemyGroup, HandlerPacketKind.Invoke)]
private Task GmWorldRefreshEnemyGroup(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm refresh-enemy-group");
/// <summary>`GmRecoverAllSkillCooldown`(69187025, 0B) — 清空全部技能 CD。</summary>
[Handler(MethodId.GmRecoverAllSkillCooldown, HandlerPacketKind.Invoke)]
private Task GmRecoverAllSkillCooldown(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm recover-skill-cd");
/// <summary>`GmFinishAllGuides`(65956919, 0B) — 完成全部新手引导。</summary>
[Handler(MethodId.GmFinishAllGuides, HandlerPacketKind.Invoke)]
private Task GmFinishAllGuides(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm finish-all-guides");
/// <summary>
/// `GmUnlockSystem`(62248824, 4B) = `uint systemId`。
///
/// ★ 2026-09-29 16:1x 实机后**升级为真实现**(原来是空回执):
/// 日志 `console-20260929-160541.log` 里这条一次会话被点了 **23 次**(12 个不同 systemId,
/// 且连点了两轮 16:12:48 / 16:12:49)⇒ 用户确实在用这个按钮,空回执没有意义。
///
/// 落点是现成的:`SyncUnlockSystems`(64916476) 这个 S2C 我们**从来没推过**,
/// 而登录包 `RuntimePayloadFactory.cs:309` 一直在用 `ClientConfigRepository.UnlockSystemIds()`
/// 填 `PlayerInfoGuide.UnlockSystems` ⇒ 这里只要把新 id 并进同一份集合再推一次即可。
///
/// ⛑ wire 以客户端 reader 为准(`RPCDeserializeAuto.lua:4801`):
/// `Base.ReadList7Bit(ReadUInt32)` ⇒ `FF`(集合 presence)+ **Int7 计数(+1 偏置)** + u32×n。
/// `UxWriter.Int7(n)` 内部就写 n+1,所以直接传 `ids.Count`。
/// ⛑ 只在会话内存(不持久化):重启后回到配置表的默认解锁集合。
/// </summary>
[Handler(MethodId.GmUnlockSystem, HandlerPacketKind.Invoke)]
private async Task GmUnlockSystem(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var systemId = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
var session = msg.Context.Session;
var unlocked = GetUnlockedSystems(session);
bool added;
lock (unlocked)
added = systemId != 0 && unlocked.Add(systemId);
await PushUnlockSystemsAsync(session, unlocked);
conn.Log.Info($"[MISC-ACK] gm-unlock-system systemId={systemId} added={added} " +
$"total={unlocked.Count} bytes={b.Length}");
await conn.ReturnEmptyOkAsync(msg);
}
/// <summary>会话内的「已解锁系统」集合,初值 = 配置表默认解锁的那些(与登录包同源)。</summary>
private static HashSet<uint> GetUnlockedSystems(Ananta.SDK.Network.TcpSession session)
{
lock (session.Items)
{
if (session.Items.TryGetValue(UnlockedSystemsKey, out var raw) && raw is HashSet<uint> existing)
return existing;
var created = new HashSet<uint>(ClientConfigRepository.UnlockSystemIds());
session.Items[UnlockedSystemsKey] = created;
return created;
}
}
/// <summary>
/// 推 `SyncUnlockSystems`(64916476):`FF` + Int7 计数(+1 偏置)+ u32 列表(已排序,便于核对)。
/// </summary>
private static async Task PushUnlockSystemsAsync(Ananta.SDK.Network.TcpSession session, HashSet<uint> unlocked)
{
List<uint> ids;
lock (unlocked)
ids = [.. unlocked];
ids.Sort();
var w = new UxWriter();
w.U8(0xFF); // 集合 presence:0xFF = 有值,0x00 才是 null
w.Int7(ids.Count); // Int7 内部写 count+1
foreach (var id in ids)
w.U32(id);
var body = w.ToArray();
await session.NotifyAsync(MethodId.SyncUnlockSystems, body, CancellationToken.None);
session.Log.Info($"[MISC-ACK] SyncUnlockSystems -> {ids.Count} id(s) bytes={body.Length} " +
$"hex={Convert.ToHexString(body)}");
}
private const string UnlockedSystemsKey = "client4229938-unlocked-systems";
/// <summary>`GmUnlockPlanningBoardMultiPlayer`(65321165, 4B) = `uint id`(实测 0)。</summary>
[Handler(MethodId.GmUnlockPlanningBoardMultiPlayer, HandlerPacketKind.Invoke)]
private Task GmUnlockPlanningBoardMultiPlayer(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var id = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
conn.Log.Info($"[MISC-ACK] gm-unlock-planning-board id={id} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`GmRefreshAllDestructibles`(69405892, 4B) = `uint id`(实测 0)。</summary>
[Handler(MethodId.GmRefreshAllDestructibles, HandlerPacketKind.Invoke)]
private Task GmRefreshAllDestructibles(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var id = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
conn.Log.Info($"[MISC-ACK] gm-refresh-destructibles id={id} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`GmCompleteAgentProfileTarget`(62006155, 4B) = `uint id`(实测 0)。</summary>
[Handler(MethodId.GmCompleteAgentProfileTarget, HandlerPacketKind.Invoke)]
private Task GmCompleteAgentProfileTarget(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var id = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
conn.Log.Info($"[MISC-ACK] gm-complete-agent-profile id={id} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`GmChangeSystem`(65660287, 5B) = `uint systemId | byte enable`(实测 0 / 1)。</summary>
[Handler(MethodId.GmChangeSystem, HandlerPacketKind.Invoke)]
private Task GmChangeSystem(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var systemId = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
var enable = b.Length >= 5 ? b[4] : (byte)0;
conn.Log.Info($"[MISC-ACK] gm-change-system systemId={systemId} enable={enable} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>`GmAddFan`(62042161, 8B) = `uint targetId | uint count`(实测 1224270 / 1)。</summary>
[Handler(MethodId.GmAddFan, HandlerPacketKind.Invoke)]
private Task GmAddFan(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 8)
{
uint target = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0));
uint count = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4));
conn.Log.Info($"[MISC-ACK] gm-add-fan target={target} count={count}");
}
else
{
conn.Log.Warn($"[MISC-ACK] gm-add-fan short body bytes={b.Length} " +
$"hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
// ------------------------------------------------------------------
// 第八批(2026-09-29):**全时段**仍未注册的 INVOKE 里,返回 void 的那些,一次清完。
//
// ⛑⛑ 上一轮(第六/七批)只按「近三天」取数,结论写成「INVOKE 全部有回执」是**不严谨的** ——
// 全时段(09-15 起)统计下来,仍未注册的 INVOKE 有 **56 个**。本批把其中返回 **void** 的
// 47 个一次接住;剩下 9 个要真返回值的见本批末尾「本批未做」清单,逐个评估性价比。
//
// ⛑ 为什么这些「很久没出现」的也接:unknown-methods.log 只记录**真的发过**的请求,
// 最后一次出现早 = 用户很久没碰那个入口,不等于它消失了。只要哪天点开,
// INVOKE 就会让客户端 await 到超时(硬规则 94)。GM 控制台(ALT+F1)尤其如此 ——
// 本批里 Gm*/Force* 占了 22 个,全是控制台上按了会卡住的按钮。
//
// ⛑ 返回类型一律以 `RPCDeserializeAuto.lua` 的 `midToReturnMessageReader[id]` 为准:
// 这些 id 在表里**查不到** ⇒ void ⇒ 空 OK 就是正解。
//
// ⛑ 本批**不做真实现**:`GmAddVehicle` / `GmAddAllSpirits` / `GmUnlockAllQuest` 这些
// 看着像「该给结果」的,实际都需要各自的表与实体系统,编结果比不回更难排查(硬规则 230)。
// 只解 await + 把请求 hex 打进日志,等以后真要建系统时这批日志就是原始数据。
// ------------------------------------------------------------------
/// <summary>`AskTeleportOnCrossBoundary`(67386879) — 跨界传送上报(**140 次,本批最多**)。</summary>
[Handler(MethodId.AskTeleportOnCrossBoundary, HandlerPacketKind.Invoke)]
private Task AskTeleportOnCrossBoundary(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "teleport cross-boundary");
/// <summary>`AskUseItems`(63959573) — 背包使用道具(`u64 itemUid | u32 count`,实测 12B)。</summary>
[Handler(MethodId.AskUseItems, HandlerPacketKind.Invoke)]
private Task AskUseItems(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "use-items");
/// <summary>`AskPoliceTakeCaseReward`(63234061) — 警察领取案件奖励。</summary>
[Handler(MethodId.AskPoliceTakeCaseReward, HandlerPacketKind.Invoke)]
private Task AskPoliceTakeCaseReward(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "police case-reward");
/// <summary>`AskActivateNpcProfile`(63692493) — 激活 NPC 档案。</summary>
[Handler(MethodId.AskActivateNpcProfile, HandlerPacketKind.Invoke)]
private Task AskActivateNpcProfile(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "activate-npc-profile");
/// <summary>`AskSwitchSpiritByTaskRole`(67290457) — 按任务角色切人。</summary>
[Handler(MethodId.AskSwitchSpiritByTaskRole, HandlerPacketKind.Invoke)]
private Task AskSwitchSpiritByTaskRole(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "switch-spirit-by-task-role");
/// <summary>`ForceAcceptTask`(62802451) — GM 强制接任务。</summary>
[Handler(MethodId.ForceAcceptTask, HandlerPacketKind.Invoke)]
private Task ForceAcceptTask(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "force-accept-task");
/// <summary>`ForceSubmitTask`(62675545) — GM 强制交任务。</summary>
[Handler(MethodId.ForceSubmitTask, HandlerPacketKind.Invoke)]
private Task ForceSubmitTask(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "force-submit-task");
/// <summary>`RecordSceneDeviceBehavior`(67705881) — 场景设备行为记录。</summary>
[Handler(MethodId.RecordSceneDeviceBehavior, HandlerPacketKind.Invoke)]
private Task RecordSceneDeviceBehavior(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "scene-device-behavior");
/// <summary>`AskPlayerEnterWater`(67675748) — 入水。</summary>
[Handler(MethodId.AskPlayerEnterWater, HandlerPacketKind.Invoke)]
private Task AskPlayerEnterWater(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "player-enter-water");
/// <summary>`AskPlayerLeaveWater`(67252339) — 出水。</summary>
[Handler(MethodId.AskPlayerLeaveWater, HandlerPacketKind.Invoke)]
private Task AskPlayerLeaveWater(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "player-leave-water");
// ⛑⛑ 2026-09-30:`AskStartTaxiNavigate`(67363555) 与 `AskEndTaxiNavigate`(67960304)
// 的**真实现搬到了 `GameRouter.Taxi.cs`**(认单 → 派 Cruise → 判到达 → 收车费)。
// 这两个占位 AckVoid 必须删掉:同一个 mid 注册两次 handler 会直接冲突。
// ⇒ 别在这里补回来。
/// <summary>`AskEndTaxiTeleport`(67643399) — 出租车传送结束(**跳过行程**用)。
/// ⛑ 阶段一没做「长按跳过 → 传送」,所以这里仍是占位;真做了跳过要搬到
/// <c>GameRouter.Taxi.cs</c> 去(同 Start/End 的处理方式)。</summary>
[Handler(MethodId.AskEndTaxiTeleport, HandlerPacketKind.Invoke)]
private Task AskEndTaxiTeleport(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "taxi teleport-end");
/// <summary>`AskCreateCustomLink`(63155988) — 创建自定义联机链接(单机服无意义,只解 await)。</summary>
[Handler(MethodId.AskCreateCustomLink, HandlerPacketKind.Invoke)]
private Task AskCreateCustomLink(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "create-custom-link");
/// <summary>`AskCancelInviterPlayerInteractio`(63438568) — 取消邀请者交互。</summary>
[Handler(MethodId.AskCancelInviterPlayerInteractio, HandlerPacketKind.Invoke)]
private Task AskCancelInviterPlayerInteractio(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "cancel-inviter-interaction");
/// <summary>`AskUpdatePlayerScenarioInfo`(63680600) — 更新剧情信息。</summary>
[Handler(MethodId.AskUpdatePlayerScenarioInfo, HandlerPacketKind.Invoke)]
private Task AskUpdatePlayerScenarioInfo(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "update-scenario-info");
/// <summary>`AskChangePlayerScenarioSlot`(63088205) — 切换剧情槽位。</summary>
[Handler(MethodId.AskChangePlayerScenarioSlot, HandlerPacketKind.Invoke)]
private Task AskChangePlayerScenarioSlot(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "change-scenario-slot");
/// <summary>`AskChangeNameByItem`(63087020) — 改名卡改名。</summary>
[Handler(MethodId.AskChangeNameByItem, HandlerPacketKind.Invoke)]
private Task AskChangeNameByItem(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "change-name-by-item");
/// <summary>`AskSetSpiritWearFashionHiddenPartsWithSource`(63452716) — 时装隐藏部位(带来源)。</summary>
[Handler(MethodId.AskSetSpiritWearFashionHiddenPartsWithSource, HandlerPacketKind.Invoke)]
private Task AskSetSpiritWearFashionHiddenPartsWithSource(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "fashion-hidden-parts");
/// <summary>`AskTakeNpcProfileProgressRewardWithWeb`(63898756) — NPC 档案进度奖励(Web 型)。</summary>
[Handler(MethodId.AskTakeNpcProfileProgressRewardWithWeb, HandlerPacketKind.Invoke)]
private Task AskTakeNpcProfileProgressRewardWithWeb(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "npc-profile-progress-reward");
/// <summary>`AskReportArcadeGameResult`(63878179) — 街机小游戏结果上报(**09-29 仍在发**)。</summary>
[Handler(MethodId.AskReportArcadeGameResult, HandlerPacketKind.Invoke)]
private Task AskReportArcadeGameResult(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "arcade-game-result");
/// <summary>`AskPoliceTrailTeleport`(63457607) — 警察追踪传送。</summary>
[Handler(MethodId.AskPoliceTrailTeleport, HandlerPacketKind.Invoke)]
private Task AskPoliceTrailTeleport(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "police trail-teleport");
/// <summary>`AskPoliceFakeFileTakeReward`(63972564) — 警察假档案领奖。</summary>
[Handler(MethodId.AskPoliceFakeFileTakeReward, HandlerPacketKind.Invoke)]
private Task AskPoliceFakeFileTakeReward(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "police fakefile-reward");
/// <summary>`AskTradeListItem`(63291047) — 交易行挂单。</summary>
[Handler(MethodId.AskTradeListItem, HandlerPacketKind.Invoke)]
private Task AskTradeListItem(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "trade list-item");
/// <summary>`RequestRevive`(67856952) — 请求复活。</summary>
[Handler(MethodId.RequestRevive, HandlerPacketKind.Invoke)]
private Task RequestRevive(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "request-revive");
/// <summary>`GmDaVinciCode`(69864720) — GM 达芬奇码。</summary>
[Handler(MethodId.GmDaVinciCode, HandlerPacketKind.Invoke)]
private Task GmDaVinciCode(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm davinci-code");
/// <summary>`GmAddVehicle`(62135059) — GM 加车(**白名单里早就有、但一直没有 handler**)。</summary>
[Handler(MethodId.GmAddVehicle, HandlerPacketKind.Invoke)]
private Task GmAddVehicle(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm add-vehicle");
/// <summary>`GmAddHealItems`(65533890) — GM 加治疗道具。</summary>
[Handler(MethodId.GmAddHealItems, HandlerPacketKind.Invoke)]
private Task GmAddHealItems(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm add-heal-items");
/// <summary>`GmAddAllSpirits`(65674296) — GM 加全部角色。</summary>
[Handler(MethodId.GmAddAllSpirits, HandlerPacketKind.Invoke)]
private Task GmAddAllSpirits(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm add-all-spirits");
/// <summary>`GmUnlockBadge`(62349595) — GM 解锁徽章。</summary>
[Handler(MethodId.GmUnlockBadge, HandlerPacketKind.Invoke)]
private Task GmUnlockBadge(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm unlock-badge");
/// <summary>`GmUnlockAllQuest`(65436618) — GM 解锁全部任务。</summary>
[Handler(MethodId.GmUnlockAllQuest, HandlerPacketKind.Invoke)]
private Task GmUnlockAllQuest(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm unlock-all-quest");
/// <summary>`GmSetMapEntrance`(65495623) — GM 设置地图入口。</summary>
[Handler(MethodId.GmSetMapEntrance, HandlerPacketKind.Invoke)]
private Task GmSetMapEntrance(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm set-map-entrance");
/// <summary>`GmSubmitEvent`(65753296) — GM 提交事件。</summary>
[Handler(MethodId.GmSubmitEvent, HandlerPacketKind.Invoke)]
private Task GmSubmitEvent(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm submit-event");
/// <summary>`GmEnableThreatDebug`(69702004) — GM 威胁调试。</summary>
[Handler(MethodId.GmEnableThreatDebug, HandlerPacketKind.Invoke)]
private Task GmEnableThreatDebug(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm threat-debug");
/// <summary>`GmStartEnemyDetectDebug`(69199157) — GM 敌人侦测调试(开)。</summary>
[Handler(MethodId.GmStartEnemyDetectDebug, HandlerPacketKind.Invoke)]
private Task GmStartEnemyDetectDebug(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm enemy-detect-debug");
/// <summary>`GmStartEnemyDetectStateDebug`(69310359) — GM 敌人侦测状态调试(开)。</summary>
[Handler(MethodId.GmStartEnemyDetectStateDebug, HandlerPacketKind.Invoke)]
private Task GmStartEnemyDetectStateDebug(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm enemy-detect-state-debug");
/// <summary>`GmEndEnemyStrategyDebug`(69236550) — GM 敌人策略调试(关)。</summary>
[Handler(MethodId.GmEndEnemyStrategyDebug, HandlerPacketKind.Invoke)]
private Task GmEndEnemyStrategyDebug(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm enemy-strategy-debug-end");
/// <summary>`GmStartSyncAIAction`(69345139) — GM AI 动作同步(开)。</summary>
[Handler(MethodId.GmStartSyncAIAction, HandlerPacketKind.Invoke)]
private Task GmStartSyncAIAction(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm sync-ai-action-start");
/// <summary>`GmEndSyncAIAction`(69026332) — GM AI 动作同步(关)。</summary>
[Handler(MethodId.GmEndSyncAIAction, HandlerPacketKind.Invoke)]
private Task GmEndSyncAIAction(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm sync-ai-action-end");
/// <summary>`GmRegenerateWildEnemy`(69561826) — GM 重生成野怪。</summary>
[Handler(MethodId.GmRegenerateWildEnemy, HandlerPacketKind.Invoke)]
private Task GmRegenerateWildEnemy(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm regenerate-wild-enemy");
/// <summary>`GmKillEnemy`(69834850) — GM 击杀敌人。</summary>
[Handler(MethodId.GmKillEnemy, HandlerPacketKind.Invoke)]
private Task GmKillEnemy(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm kill-enemy");
/// <summary>`GmEnemyAiStopFlagByCamp`(69827777) — GM 按阵营停 AI。</summary>
[Handler(MethodId.GmEnemyAiStopFlagByCamp, HandlerPacketKind.Invoke)]
private Task GmEnemyAiStopFlagByCamp(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm enemy-ai-stop-by-camp");
/// <summary>`GmAllSwitchToBattle`(69018541) — GM 全体切战斗。</summary>
[Handler(MethodId.GmAllSwitchToBattle, HandlerPacketKind.Invoke)]
private Task GmAllSwitchToBattle(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm all-switch-to-battle");
/// <summary>`GmTestDamage`(69061923) — GM 测试伤害。</summary>
[Handler(MethodId.GmTestDamage, HandlerPacketKind.Invoke)]
private Task GmTestDamage(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm test-damage");
/// <summary>`GmDeathPartyForcePlayerWin`(69208480) — GM 死亡派对强制胜利(联机玩法,单机无意义)。</summary>
[Handler(MethodId.GmDeathPartyForcePlayerWin, HandlerPacketKind.Invoke)]
private Task GmDeathPartyForcePlayerWin(Connection conn, UxRpcMessage msg)
=> AckVoid(conn, msg, "gm deathparty-force-win");
// ---- 本批未做(8 个要真返回值的,逐个评估性价比,不盲动)----------------------------
//
// ⛑⛑ **`AskVehicleNavigationPathPointsFr`(67650626) 不是缺口,是**别名误判** ——
// `MethodId.cs:2737 AskVehicleNavigationPathPointsFr` 与
// `:4548 AskVehicleNavigationPathPointsFromPos` **是同一个 id 67650626**,
// 而我们早就注册了后者(`GameRouter.VehicleNav.cs:63` + 白名单 `GameRouter.cs:540`)。
// 日志里的名字来自另一张名字表(`AskVehicleNavigationPathPointsFr`),
// ⇒ **做差集时必须按 MethodId 比,不能按方法名比**(否则这类会永远显示为"未注册")。
// 同理 `AskVehicleNavigationPathLengthLi`(67251681) = `AskVehicleNavigationPathLengthList`。
//
// 剩下真缺口的 8 个(都是 1~12 次,建契约的代价超过收益 ⇒ 全部跳过):
// GmAskQueryAgentDetailList(65326141, 12) → Dict7Bit<u64, ...>
// AskTradeGetOrderList(63882492, 3) → List7Bit complex Reader[651]
// AskBuyFerrisWheelTicket(63891470, 3) → 裸 byte(0/1 语义不明,不敢编)
// AskGetBeLikeList(63113609, 2) → List7Bit complex Reader[591]
// ShowAllServerDumpInfo(65624560, 1) → string
// AskStartRPSInterrogation(63361257, 1) → complex Reader[614]
// AskCreateAISession(63746945, 1) → complex Reader[587]
// AskQueryFunplayHudData(63254781, 1) → Dict7Bit<u32, ...>
// ------------------------------------------------------------------
// 第九批(2026-09-29 16:2x):**实机之后**才冒出来的 3 条 GM INVOKE。
//
// ⛑⛑ 教训:第八批那 56 个是拿**历史日志**做的差集 —— 用户这次点了以前没点过的 GM 按钮,
// 立刻冒出 3 个全新的未注册方法(历史日志里一次都没有)。
// ⇒ **「收口」永远是相对当前日志的**,每次实机完都要重跑一次 `_wm_pending_byid.py`,
// 不能因为「上次统计说没了」就不再查。
//
// ⛑ `GmAddWeapon` 7 次里有 6 次是 **0.5~1 秒的连发重试**(await 没落地的典型形态),
// 是最该接的一条。三条返回全部 void ⇒ 空 OK。
// ⛑ 不做真实现:加武器 / 加 buff / 删 buff 都要各自的实体与状态系统(硬规则 230)。
// ------------------------------------------------------------------
/// <summary>
/// `GmAddWeapon`(69802168, 4B) = `uint weaponId`(实测 `BBC1C404` = 80201147 等)。
/// **7 次,其中 6 次是连发重试** —— GM 控制台「加武器」,本批优先级最高的一条。
/// </summary>
[Handler(MethodId.GmAddWeapon, HandlerPacketKind.Invoke)]
private Task GmAddWeapon(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var weaponId = b.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)) : 0u;
conn.Log.Info($"[MISC-ACK] gm-add-weapon weaponId={weaponId} bytes={b.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `GmAddOtherBuff`(69805439, 24B) = `uint buffId | ulong target | ulong caster | f32 v`
/// (实测 `3DEB2503 00E8764817000000 00E8764817000000 000080BF` ⇒ buff=52999997,
/// target=caster=1e11, v=-1.0)。
/// </summary>
[Handler(MethodId.GmAddOtherBuff, HandlerPacketKind.Invoke)]
private Task GmAddOtherBuff(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 24)
{
uint buffId = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0));
ulong target = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(4));
ulong caster = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(12));
float v = BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(20));
conn.Log.Info($"[MISC-ACK] gm-add-other-buff buffId={buffId} target={target} " +
$"caster={caster} v={v:F3}");
}
else
{
conn.Log.Warn($"[MISC-ACK] gm-add-other-buff short body bytes={b.Length} " +
$"hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// `GmRemoveBuff`(69704292, 20B) = `uint buffId | ulong target | ulong extra`
/// (实测 `3DEB2503 00E8764817000000 0000000000000000`)。
/// </summary>
[Handler(MethodId.GmRemoveBuff, HandlerPacketKind.Invoke)]
private Task GmRemoveBuff(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
if (b.Length >= 20)
{
uint buffId = BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0));
ulong target = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(4));
ulong extra = BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(12));
conn.Log.Info($"[MISC-ACK] gm-remove-buff buffId={buffId} target={target} extra={extra}");
}
else
{
conn.Log.Warn($"[MISC-ACK] gm-remove-buff short body bytes={b.Length} " +
$"hex={Convert.ToHexString(b.ToArray())}");
}
return conn.ReturnEmptyOkAsync(msg);
}
// -----------------------------------------------------------------------------------------
// 第十批 1 条(2026-09-29 20:4x)—— _wm_pending_byid.py 相对上一轮的**唯一新增 INVOKE**。
// 回滚 = 删本条 + GameRouter.cs 白名单里 `// 第十批` 那 1 行。
// -----------------------------------------------------------------------------------------
/// <summary>
/// `AskSetSpiritFunctionSuitSchemeInfo`(63040765, 52B) — INVOKE,客户端 await 回执。
/// ⛑ `MethodId.cs` 里该 id 有两个别名:`AskSetSpiritFunctionSuitSchemeIn`(:255) 与
/// `...Info`(:4539),是同一个方法。登记用前者。
///
/// RET(`RPCDeserializeAuto.lua` 的 `midToReturnMessageReader[63040765]`):
/// <c>Base.ReadList7Bit(reader, ReadUInt32)</c> —— 单字段 <c>List&lt;uint&gt;</c>,
/// 与 <c>SyncUnlockSystems</c> 完全同形 ⇒ **不能**用 <c>ReturnEmptyOkAsync</c>
/// (那会让 reader 读不到 presence 字节而走位)。
/// 按硬规则:空列表 = <c>FF 01</c>(0xFF=presence,0x01=Int7(0) 的 +1 偏置),
/// 故直接手拼两字节,仿照 <c>GameRouter.MiniGameEntry.cs</c> 的 AskFarmerFryAccept。
///
/// 为什么只回空、不做真持久化:
/// ① 频次 —— `unknown-methods.log` **全时段仅 1 次**(2026-09-29T10:28:09),玩家只点过一回;
/// ② 无模型 —— 服务端没有 per-spirit 的"功能套装方案"存储
/// (市民侧只有 <c>FunctionSuitId</c>,且 <c>player-progress.json</c> 里全为 0,
/// 见 GameRouter.Civilians.cs:29);
/// ③ 参数布局未解干净 —— 实测 52B 试过 "u32 + u32 + n*(FF+u32)" 等多种切法都对不齐
/// (尾部余 2 字节),贸然回显解出的 id 有把 UI 显示带错的风险。
/// ⇒ 按"性价比过低跳过"原则,本次只**解除卡死**(不回包才会让客户端一直转圈),
/// 数据回显留待以后真需要时再做。真要做的话:先多抓几条不同 spirit/不同件数的
/// 样本把布局钉死,再落一张 per-spirit 方案表。
/// </summary>
[Handler(MethodId.AskSetSpiritFunctionSuitSchemeIn, HandlerPacketKind.Invoke)]
private Task AskSetSpiritFunctionSuitSchemeInfo(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
// 空 List7Bit<uint>:0xFF = presence(SerializeObjectMarkCommon),0x01 = Int7(0) 的 +1 偏置。
var payload = new byte[] { 0xFF, 0x01 };
conn.Log.Info($"[MISC-ACK] set-suit-scheme bytes={b.Length} " +
$"hex={Convert.ToHexString(b.ToArray())} -> empty list(ff 01)");
return conn.ReturnRawAsync(msg, payload);
}
}