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; /// /// 三条「客户端在发、服务端从没接」的轻量回执(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<uint>`(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) /// ⇒ 它不会让客户端 await 卡死,接它唯一的收益是拿到玩家当前锁定的目标(打日志观察)。 /// 这里仍然注册 `HandlerPacketKind.Both`:万一将来客户端改用 INVOKE,也不会重新落兜底 /// (`ReturnEmptyOkAsync` 对非 Invoke 直接返回 CompletedTask,不会凭空回包)。 /// /// ⛑⛑ 硬规则 94:这些 id 必须同时在 `GameRouter.cs` 的 `Enabled4229938MethodIds` 里, /// 否则编译通过但 handler 永不注册 —— Invoke 类还会让客户端 await 到超时。 /// internal sealed partial class GameRouter { /// /// NPC 语音请求:`AskAgentStartNpcSound(ulong agentEntityId, uint libraryId)` — 67003057。 /// 请求体实测 12 字节(`8300BE5306862F73 50FD1300` = u64 + u32)。 /// 返回 `UXRPCTask<uint>` ⇒ 必须回一个 uint,取 0(=成功)。 /// 与 `AskStartDialog` 同型(`GameRouter.Dialog.cs` 也是 `ReturnAsync(msg, 0u)`)。 /// [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); } /// /// 城市图鉴点开详情:`AskReadCityPedia(uint cityPediaId)` — 63817604。 /// 请求体实测 4 字节(`1561B900` = u32 cityPediaId)。返回 `UXRPCTask`(void)⇒ 回空 OK。 /// 列表侧(295 条)早就通了,这条补的是「点开条目」那一半。 /// [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); } /// /// 战斗锁定目标:`AskSetClientLockTarget(ulong targetId)` — 67197587。 /// 请求体实测 8 字节;`0000000000000000` = 清除锁定。 /// 纯观察:把 targetId 打出来,为以后「锁定/切目标」手感那条线留证据。 /// [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`、`GetUploadObjectUrl` → `UXRPCTask`、 // `AskItemDestructibleCreate` → `UXRPCTask`。 // ------------------------------------------------------------------ [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` // ⇒ struct **无顶层标记**;回空列表 `FF 01` // GetUploadObjectUrl(63757076) rpc lua:12156 `Base.ReadComplex(Reader[588])` // dump.cs:1726xxx `UXRPCTask` // ⇒ complex 顶层 `FF` + string/u32/string // AskItemDestructibleCreate(67362514) rpc lua:15250 `reader.ReadUInt64()` // dump.cs:1730xxx `UXRPCTask` // ⇒ 裸 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 足够。 // ------------------------------------------------------------------ /// 统一的「打日志 + 回空 OK」。void 回执默认走这里。 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); } // ---- 受击 / 击飞 / 落地四条(有解析,是唯一真值来源)---------------------------- /// /// `ReportUnitHitFly`(67430580, 24B) = u64 defenderId | u64 attackerId | u32 hitFlyStiffId | f32 stiffTime。 /// dump 里还有个同名 S2C 签名 `void ReportUnitHitFly(ulong attackPid, uint stiffId)`(@535988),别抄错参数个数。 /// [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); } /// `ReportUnitHitFlyEnd`(67015647, 12B) = u64 defenderId | f32 speed。 [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); } /// /// `ReportUnitFallGround`(67719880, 24B) = u64 unitId | i32 moveId | u32 stiffId | f32 stiffTime | f32 speed。 /// 日志实测:`A986010000200000 | 0B000000 | C5030000 | 00000000 | 609D6330`(stiffTime 恒 0)。 /// [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); } /// `ReportUnitFallGroundEnd`(67516089, 8B) = u64 unitId。 [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,纯抓包,不改任何状态)------------------ /// /// `AskControlPowerHoldEnemyUp`(67802701, 8B) = u64 enemyid —— 玩家把敌人钳制 / 举起来。 /// 契约出处 `_lua_all/lua/LuaGen/AutoGen/ClientToGameSceneDelegate.lua:4771-4777` /// (Serializer 只写一个 u64)。客户端侧是 C# native 调用,lua 里没有调用点。 /// /// ⛑ 本轮只打日志 + 空回执,**不落地任何行为**(演出保护窗口等拿到时序再定)。 /// 判据:日志里出现 `[MISC-ACK] control-power-hold-up` 才说明客户端真走了这条 RPC。 /// [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); } /// /// `AskControlPowerHoldEnemyFallGround`(67043294, 8B) = u64 enemyid —— 把钳制中的敌人摔倒在地。 /// 契约出处同上 `ClientToGameSceneDelegate.lua:322-328`。 /// [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 即可)------------------------------------------------ /// `AskCancelInteractionActionRedPoi`(63584158) — u32 交互项 id(§46 乐器线那条:99323001 = 木吉他)。 [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 = 无随机对话)。 // 真要做玩法(地图图钉持久化 / 世界生活局 / 篮球 / 武术赛季)得另开条目 —— 这里先把 // 客户端从"等不到回包"里放出来,并把请求参数打进日志给后面留数据。 // ------------------------------------------------------------------ /// /// `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)—— 客户端拿到的是一个 /// 坐标自洽的图钉,不会瞬移。⛑ 本服**不持久化**图钉(重启即丢),要存得另开条目。 /// [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 }, }); } /// /// `GetCharacterRandomDialog`(63175540):请求 `(uint agentId, uint MainTag, uint[] SubTag)`(实测 14B), /// 返回裸 **u32** = 随机对话 id。 /// ⛑ 本服没有随机对话表 ⇒ 回 **0**(= 无随机对话,客户端应走"不播"分支,最安全)。 /// 先回 0 而不是编一个假 id:编 id 会让客户端去查表里不存在的行,比"没有"更难排查。 /// 接它的收益 = 解 await + 日志留下 `agentId / MainTag / SubTag`(以后要接真对话得查 DialogConfig)。 /// [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); } /// /// `StartWorldLifeGame`(67864009):请求 `uint typeId`(实测 4B),返回裸 **u64** = 世界生活局实例 id。 /// 号走 `IdCounterStore`(硬规则 104:重启不重号)。本服不跟踪对局,只发号解 await。 /// [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); } /// /// `AskBasketballOperatorAction`(67028937):请求 `BasketballAskOperatorParam`(complex,实测 49B), /// 返回裸 **u64**。**86 次,是本批最多的** —— 篮球机(§19 donor 段提过 T3-35 篮球 POI)。 /// ⛑ 返回的 u64 语义不明(可能是操作 token / 球局 id)⇒ 回 **0** 并打请求 hex, /// 等日志里看清 `BasketballAskOperatorParam` 的实际取值再决定要不要发号。 /// [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); } /// /// `AskWushuTournamentSeasonData`(63308286):**无参数**,返回 `ReadComplex(Reader[611])` /// = `u32 SeasonId / u32 CurrentRound / bool IsSeasonCleared / List7 Rounds / u32 LastChallengeRound / u32 LastChallengeChoice`。 /// 本服没有赛季数据 ⇒ 回全 0 + **空 Rounds 列表**(`FF 01`)。 /// ⛑ 空列表也要占 2 字节,所以契约里 `Rounds` / `OpponentBestStars` 字段不能省。 /// [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 卡死。 // ------------------------------------------------------------------ /// /// `GMChangeNpcFavor`(62371863, 9B) = `uint npcAgentId | uint favor | byte flag`(实测 /// `1D506D05 10270000 00` ⇒ agent=91103261, favor=10000, flag=0)。**这批里最活跃(28 次)**。 /// ⛑ 本服**没有好感度存储**(NpcLife 只有好感 NPC 的坐标与日程)⇒ 这次只解 await + 留数据, /// 不真的改好感。要落真值得先建 favor store,属另一条线。 /// [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); } /// /// `GmDropReward`(62894352, 8B) = `uint rewardId | uint count`(实测 `C588DE00 0A000000` /// ⇒ reward=14584005, count=10)。GM 控制台「发奖励」。本服无掉落表 ⇒ 只解 await + 留数据。 /// [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); } /// `FastReenter`(65694957, 0B) — GM 控制台「快速重进」,无参。 [Handler(MethodId.FastReenter, HandlerPacketKind.Invoke)] private Task FastReenter(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "fast-reenter"); /// /// `AskVehicleSendGamePlaySignal`(63816400, 1B) = `byte signalId`(实测 `0B` = 11)。 /// 客户端把车载玩法信号丢给服务端,返回 void ⇒ 空 OK。 /// [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); } /// `AskChangeSceneItemQuality`(67983820, 4B) = `uint quality`(实测 3)。场景物件画质档。 [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); } /// /// `AskPuppetGetup`(67548192, 8B) = `ulong puppetEntityId`(实测 `00E8764817000000` = 100000000000)。 /// 倒地单位起身回执。 /// [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); } /// `AskChangeRoleUseSystemName`(63927753, 1B) = `bool/byte`(实测 0)。 [Handler(MethodId.AskChangeRoleUseSystemName, HandlerPacketKind.Invoke)] private Task AskChangeRoleUseSystemName(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "role-use-system-name"); /// /// `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)。⛑ 字段语义未定 ⇒ 只按长度解析并打出来,不猜名字。 /// [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); } /// `AskMetroHitEnd`(67737840, 8B) = `ulong unitId`。 [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); } /// /// `AskSaveSettings`(63411797, 28B):尾部是一个 **长度前缀(1 字节)+ JSON**, /// 实测 `… 0D 7B22333034303031223A317D` ⇒ len=13 + `{"304001":1}`(设置项 KV)。 /// ⛑ 本服不持久化设置(重启即回默认)⇒ 只解 await + 把 JSON 打进日志。 /// [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); } /// /// `AskCloseConnectionToGame`(63990036):参数 = 一个 string(**Int7 长度带 +1 偏置**), /// 实测 `0C 4B69636B546F4C6F67696E` ⇒ 0x0C=12 ⇒ 11 字符 = `KickToLogin`。 /// 返回 **u32** ⇒ 回 0(= 同意)。客户端自己会断连回登录,服务端不需要额外动作。 /// [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); } /// `AskClearSwitchInFashionCache`(67098371, 4B) = `uint id`(实测 15021095)。 [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); } /// /// `AskMultiverseStatus`(63799383, 0B):返回 `MultiverseStatusInfo`(complex,顶层 0xFF) /// = 三个 `Dict<uint, byte>` + `ShowLastMutiId` + `CurMutiPanelId`。 /// 本服没有多世界数据 ⇒ 回 **全 0 + 三个空字典**(空字典也占 `FF 00000000`)。 /// ⛑ 空 OK 会让客户端读到 0 字节 ⇒ 反序列化直接 nil 走错分支,所以必须回真返回体。 /// [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 + 把参数打进日志,等以后真要建系统解锁表时这批日志就是原始数据。 // ------------------------------------------------------------------ /// `GmWorldRefreshEnemyGroup`(69676618, 0B) — 刷新世界敌人组。 [Handler(MethodId.GmWorldRefreshEnemyGroup, HandlerPacketKind.Invoke)] private Task GmWorldRefreshEnemyGroup(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm refresh-enemy-group"); /// `GmRecoverAllSkillCooldown`(69187025, 0B) — 清空全部技能 CD。 [Handler(MethodId.GmRecoverAllSkillCooldown, HandlerPacketKind.Invoke)] private Task GmRecoverAllSkillCooldown(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm recover-skill-cd"); /// `GmFinishAllGuides`(65956919, 0B) — 完成全部新手引导。 [Handler(MethodId.GmFinishAllGuides, HandlerPacketKind.Invoke)] private Task GmFinishAllGuides(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm finish-all-guides"); /// /// `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`。 /// ⛑ 只在会话内存(不持久化):重启后回到配置表的默认解锁集合。 /// [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); } /// 会话内的「已解锁系统」集合,初值 = 配置表默认解锁的那些(与登录包同源)。 private static HashSet GetUnlockedSystems(Ananta.SDK.Network.TcpSession session) { lock (session.Items) { if (session.Items.TryGetValue(UnlockedSystemsKey, out var raw) && raw is HashSet existing) return existing; var created = new HashSet(ClientConfigRepository.UnlockSystemIds()); session.Items[UnlockedSystemsKey] = created; return created; } } /// /// 推 `SyncUnlockSystems`(64916476):`FF` + Int7 计数(+1 偏置)+ u32 列表(已排序,便于核对)。 /// private static async Task PushUnlockSystemsAsync(Ananta.SDK.Network.TcpSession session, HashSet unlocked) { List 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"; /// `GmUnlockPlanningBoardMultiPlayer`(65321165, 4B) = `uint id`(实测 0)。 [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); } /// `GmRefreshAllDestructibles`(69405892, 4B) = `uint id`(实测 0)。 [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); } /// `GmCompleteAgentProfileTarget`(62006155, 4B) = `uint id`(实测 0)。 [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); } /// `GmChangeSystem`(65660287, 5B) = `uint systemId | byte enable`(实测 0 / 1)。 [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); } /// `GmAddFan`(62042161, 8B) = `uint targetId | uint count`(实测 1224270 / 1)。 [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 打进日志,等以后真要建系统时这批日志就是原始数据。 // ------------------------------------------------------------------ /// `AskTeleportOnCrossBoundary`(67386879) — 跨界传送上报(**140 次,本批最多**)。 [Handler(MethodId.AskTeleportOnCrossBoundary, HandlerPacketKind.Invoke)] private Task AskTeleportOnCrossBoundary(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "teleport cross-boundary"); /// `AskUseItems`(63959573) — 背包使用道具(`u64 itemUid | u32 count`,实测 12B)。 [Handler(MethodId.AskUseItems, HandlerPacketKind.Invoke)] private Task AskUseItems(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "use-items"); /// `AskPoliceTakeCaseReward`(63234061) — 警察领取案件奖励。 [Handler(MethodId.AskPoliceTakeCaseReward, HandlerPacketKind.Invoke)] private Task AskPoliceTakeCaseReward(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "police case-reward"); /// `AskActivateNpcProfile`(63692493) — 激活 NPC 档案。 [Handler(MethodId.AskActivateNpcProfile, HandlerPacketKind.Invoke)] private Task AskActivateNpcProfile(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "activate-npc-profile"); /// `AskSwitchSpiritByTaskRole`(67290457) — 按任务角色切人。 [Handler(MethodId.AskSwitchSpiritByTaskRole, HandlerPacketKind.Invoke)] private Task AskSwitchSpiritByTaskRole(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "switch-spirit-by-task-role"); /// `ForceAcceptTask`(62802451) — GM 强制接任务。 [Handler(MethodId.ForceAcceptTask, HandlerPacketKind.Invoke)] private Task ForceAcceptTask(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "force-accept-task"); /// `ForceSubmitTask`(62675545) — GM 强制交任务。 [Handler(MethodId.ForceSubmitTask, HandlerPacketKind.Invoke)] private Task ForceSubmitTask(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "force-submit-task"); /// `RecordSceneDeviceBehavior`(67705881) — 场景设备行为记录。 [Handler(MethodId.RecordSceneDeviceBehavior, HandlerPacketKind.Invoke)] private Task RecordSceneDeviceBehavior(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "scene-device-behavior"); /// `AskPlayerEnterWater`(67675748) — 入水。 [Handler(MethodId.AskPlayerEnterWater, HandlerPacketKind.Invoke)] private Task AskPlayerEnterWater(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "player-enter-water"); /// `AskPlayerLeaveWater`(67252339) — 出水。 [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 会直接冲突。 // ⇒ 别在这里补回来。 /// `AskEndTaxiTeleport`(67643399) — 出租车传送结束(**跳过行程**用)。 /// ⛑ 阶段一没做「长按跳过 → 传送」,所以这里仍是占位;真做了跳过要搬到 /// GameRouter.Taxi.cs 去(同 Start/End 的处理方式)。 [Handler(MethodId.AskEndTaxiTeleport, HandlerPacketKind.Invoke)] private Task AskEndTaxiTeleport(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "taxi teleport-end"); /// `AskCreateCustomLink`(63155988) — 创建自定义联机链接(单机服无意义,只解 await)。 [Handler(MethodId.AskCreateCustomLink, HandlerPacketKind.Invoke)] private Task AskCreateCustomLink(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "create-custom-link"); /// `AskCancelInviterPlayerInteractio`(63438568) — 取消邀请者交互。 [Handler(MethodId.AskCancelInviterPlayerInteractio, HandlerPacketKind.Invoke)] private Task AskCancelInviterPlayerInteractio(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "cancel-inviter-interaction"); /// `AskUpdatePlayerScenarioInfo`(63680600) — 更新剧情信息。 [Handler(MethodId.AskUpdatePlayerScenarioInfo, HandlerPacketKind.Invoke)] private Task AskUpdatePlayerScenarioInfo(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "update-scenario-info"); /// `AskChangePlayerScenarioSlot`(63088205) — 切换剧情槽位。 [Handler(MethodId.AskChangePlayerScenarioSlot, HandlerPacketKind.Invoke)] private Task AskChangePlayerScenarioSlot(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "change-scenario-slot"); /// `AskChangeNameByItem`(63087020) — 改名卡改名。 [Handler(MethodId.AskChangeNameByItem, HandlerPacketKind.Invoke)] private Task AskChangeNameByItem(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "change-name-by-item"); /// `AskSetSpiritWearFashionHiddenPartsWithSource`(63452716) — 时装隐藏部位(带来源)。 [Handler(MethodId.AskSetSpiritWearFashionHiddenPartsWithSource, HandlerPacketKind.Invoke)] private Task AskSetSpiritWearFashionHiddenPartsWithSource(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "fashion-hidden-parts"); /// `AskTakeNpcProfileProgressRewardWithWeb`(63898756) — NPC 档案进度奖励(Web 型)。 [Handler(MethodId.AskTakeNpcProfileProgressRewardWithWeb, HandlerPacketKind.Invoke)] private Task AskTakeNpcProfileProgressRewardWithWeb(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "npc-profile-progress-reward"); /// `AskReportArcadeGameResult`(63878179) — 街机小游戏结果上报(**09-29 仍在发**)。 [Handler(MethodId.AskReportArcadeGameResult, HandlerPacketKind.Invoke)] private Task AskReportArcadeGameResult(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "arcade-game-result"); /// `AskPoliceTrailTeleport`(63457607) — 警察追踪传送。 [Handler(MethodId.AskPoliceTrailTeleport, HandlerPacketKind.Invoke)] private Task AskPoliceTrailTeleport(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "police trail-teleport"); /// `AskPoliceFakeFileTakeReward`(63972564) — 警察假档案领奖。 [Handler(MethodId.AskPoliceFakeFileTakeReward, HandlerPacketKind.Invoke)] private Task AskPoliceFakeFileTakeReward(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "police fakefile-reward"); /// `AskTradeListItem`(63291047) — 交易行挂单。 [Handler(MethodId.AskTradeListItem, HandlerPacketKind.Invoke)] private Task AskTradeListItem(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "trade list-item"); /// `RequestRevive`(67856952) — 请求复活。 [Handler(MethodId.RequestRevive, HandlerPacketKind.Invoke)] private Task RequestRevive(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "request-revive"); /// `GmDaVinciCode`(69864720) — GM 达芬奇码。 [Handler(MethodId.GmDaVinciCode, HandlerPacketKind.Invoke)] private Task GmDaVinciCode(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm davinci-code"); /// `GmAddVehicle`(62135059) — GM 加车(**白名单里早就有、但一直没有 handler**)。 [Handler(MethodId.GmAddVehicle, HandlerPacketKind.Invoke)] private Task GmAddVehicle(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm add-vehicle"); /// `GmAddHealItems`(65533890) — GM 加治疗道具。 [Handler(MethodId.GmAddHealItems, HandlerPacketKind.Invoke)] private Task GmAddHealItems(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm add-heal-items"); /// `GmAddAllSpirits`(65674296) — GM 加全部角色。 [Handler(MethodId.GmAddAllSpirits, HandlerPacketKind.Invoke)] private Task GmAddAllSpirits(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm add-all-spirits"); /// `GmUnlockBadge`(62349595) — GM 解锁徽章。 [Handler(MethodId.GmUnlockBadge, HandlerPacketKind.Invoke)] private Task GmUnlockBadge(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm unlock-badge"); /// `GmUnlockAllQuest`(65436618) — GM 解锁全部任务。 [Handler(MethodId.GmUnlockAllQuest, HandlerPacketKind.Invoke)] private Task GmUnlockAllQuest(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm unlock-all-quest"); /// `GmSetMapEntrance`(65495623) — GM 设置地图入口。 [Handler(MethodId.GmSetMapEntrance, HandlerPacketKind.Invoke)] private Task GmSetMapEntrance(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm set-map-entrance"); /// `GmSubmitEvent`(65753296) — GM 提交事件。 [Handler(MethodId.GmSubmitEvent, HandlerPacketKind.Invoke)] private Task GmSubmitEvent(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm submit-event"); /// `GmEnableThreatDebug`(69702004) — GM 威胁调试。 [Handler(MethodId.GmEnableThreatDebug, HandlerPacketKind.Invoke)] private Task GmEnableThreatDebug(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm threat-debug"); /// `GmStartEnemyDetectDebug`(69199157) — GM 敌人侦测调试(开)。 [Handler(MethodId.GmStartEnemyDetectDebug, HandlerPacketKind.Invoke)] private Task GmStartEnemyDetectDebug(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm enemy-detect-debug"); /// `GmStartEnemyDetectStateDebug`(69310359) — GM 敌人侦测状态调试(开)。 [Handler(MethodId.GmStartEnemyDetectStateDebug, HandlerPacketKind.Invoke)] private Task GmStartEnemyDetectStateDebug(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm enemy-detect-state-debug"); /// `GmEndEnemyStrategyDebug`(69236550) — GM 敌人策略调试(关)。 [Handler(MethodId.GmEndEnemyStrategyDebug, HandlerPacketKind.Invoke)] private Task GmEndEnemyStrategyDebug(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm enemy-strategy-debug-end"); /// `GmStartSyncAIAction`(69345139) — GM AI 动作同步(开)。 [Handler(MethodId.GmStartSyncAIAction, HandlerPacketKind.Invoke)] private Task GmStartSyncAIAction(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm sync-ai-action-start"); /// `GmEndSyncAIAction`(69026332) — GM AI 动作同步(关)。 [Handler(MethodId.GmEndSyncAIAction, HandlerPacketKind.Invoke)] private Task GmEndSyncAIAction(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm sync-ai-action-end"); /// `GmRegenerateWildEnemy`(69561826) — GM 重生成野怪。 [Handler(MethodId.GmRegenerateWildEnemy, HandlerPacketKind.Invoke)] private Task GmRegenerateWildEnemy(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm regenerate-wild-enemy"); /// `GmKillEnemy`(69834850) — GM 击杀敌人。 [Handler(MethodId.GmKillEnemy, HandlerPacketKind.Invoke)] private Task GmKillEnemy(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm kill-enemy"); /// `GmEnemyAiStopFlagByCamp`(69827777) — GM 按阵营停 AI。 [Handler(MethodId.GmEnemyAiStopFlagByCamp, HandlerPacketKind.Invoke)] private Task GmEnemyAiStopFlagByCamp(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm enemy-ai-stop-by-camp"); /// `GmAllSwitchToBattle`(69018541) — GM 全体切战斗。 [Handler(MethodId.GmAllSwitchToBattle, HandlerPacketKind.Invoke)] private Task GmAllSwitchToBattle(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm all-switch-to-battle"); /// `GmTestDamage`(69061923) — GM 测试伤害。 [Handler(MethodId.GmTestDamage, HandlerPacketKind.Invoke)] private Task GmTestDamage(Connection conn, UxRpcMessage msg) => AckVoid(conn, msg, "gm test-damage"); /// `GmDeathPartyForcePlayerWin`(69208480) — GM 死亡派对强制胜利(联机玩法,单机无意义)。 [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 // 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 // ------------------------------------------------------------------ // 第九批(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)。 // ------------------------------------------------------------------ /// /// `GmAddWeapon`(69802168, 4B) = `uint weaponId`(实测 `BBC1C404` = 80201147 等)。 /// **7 次,其中 6 次是连发重试** —— GM 控制台「加武器」,本批优先级最高的一条。 /// [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); } /// /// `GmAddOtherBuff`(69805439, 24B) = `uint buffId | ulong target | ulong caster | f32 v` /// (实测 `3DEB2503 00E8764817000000 00E8764817000000 000080BF` ⇒ buff=52999997, /// target=caster=1e11, v=-1.0)。 /// [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); } /// /// `GmRemoveBuff`(69704292, 20B) = `uint buffId | ulong target | ulong extra` /// (实测 `3DEB2503 00E8764817000000 0000000000000000`)。 /// [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 行。 // ----------------------------------------------------------------------------------------- /// /// `AskSetSpiritFunctionSuitSchemeInfo`(63040765, 52B) — INVOKE,客户端 await 回执。 /// ⛑ `MethodId.cs` 里该 id 有两个别名:`AskSetSpiritFunctionSuitSchemeIn`(:255) 与 /// `...Info`(:4539),是同一个方法。登记用前者。 /// /// RET(`RPCDeserializeAuto.lua` 的 `midToReturnMessageReader[63040765]`): /// Base.ReadList7Bit(reader, ReadUInt32) —— 单字段 List<uint>, /// 与 SyncUnlockSystems 完全同形 ⇒ **不能**用 ReturnEmptyOkAsync /// (那会让 reader 读不到 presence 字节而走位)。 /// 按硬规则:空列表 = FF 01(0xFF=presence,0x01=Int7(0) 的 +1 偏置), /// 故直接手拼两字节,仿照 GameRouter.MiniGameEntry.cs 的 AskFarmerFryAccept。 /// /// 为什么只回空、不做真持久化: /// ① 频次 —— `unknown-methods.log` **全时段仅 1 次**(2026-09-29T10:28:09),玩家只点过一回; /// ② 无模型 —— 服务端没有 per-spirit 的"功能套装方案"存储 /// (市民侧只有 FunctionSuitId,且 player-progress.json 里全为 0, /// 见 GameRouter.Civilians.cs:29); /// ③ 参数布局未解干净 —— 实测 52B 试过 "u32 + u32 + n*(FF+u32)" 等多种切法都对不齐 /// (尾部余 2 字节),贸然回显解出的 id 有把 UI 显示带错的风险。 /// ⇒ 按"性价比过低跳过"原则,本次只**解除卡死**(不回包才会让客户端一直转圈), /// 数据回显留待以后真需要时再做。真要做的话:先多抓几条不同 spirit/不同件数的 /// 样本把布局钉死,再落一张 per-spirit 方案表。 /// [Handler(MethodId.AskSetSpiritFunctionSuitSchemeIn, HandlerPacketKind.Invoke)] private Task AskSetSpiritFunctionSuitSchemeInfo(Connection conn, UxRpcMessage msg) { var b = msg.Body; // 空 List7Bit: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); } }