Files

1309 lines
72 KiB
C#
Raw Permalink Normal View History

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);
}
}