using Ananta.SDK.Network; using Ananta.SDK.Rpc; using Ananta.Server.Configuration; using Ananta.Server.Protocol.Client4229938; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; namespace Ananta.Server.Handlers.Game; /// /// 掉落物拾取的两个回执 —— 2026-09-25 接入。 /// /// 起因:用户回报「路人 NPC 手里的手机能打落拾取,但他们的 BBChat 点不开」。 /// 掉手机这件事客户端本地就能做(GM 命令 DebugEnableDropPhoneWhenDeath, /// 补丁和工具/ClientGmCommands.txt:320),掉出来的手机是客户端自己生成的 SceneItem; /// 但拾取时它发了两个 INVOKE 给服务端: /// /// AskPickUpSceneItemTransferToItem (67297154) u64 sceneitemuid /// AskEnemyItemPickUp (67453017) u64 enemyinstanceid + i32 binditemsindex /// /// 这两个 MethodId 在本服务端零 handler、也不在白名单(全项目 grep 只命中 MethodId.cs), /// ⇒ 走 DefaultUnknownInvoke 兜底或直接 await 挂起(硬规则 94)。 /// /// 本文件只回空 OK,不做内容。理由(见 2026-09-25 记忆 §路人手机调研): /// NPC 手机里的聊天一个字节都不走服务端 —— /// GUI/Chat/RandomChatManager.lua:13 GetChatId(npcId, itemId) 用 /// AgentConfig.GetConfig(npcId) 的 AppearanceTag / BodyTag / SexType / JobTag 去匹配 /// 35 行 NPCChatAtmosphereNewConfig(84028000-84028034),按 60/Weight 加权 /// 随机挑 1 条 NPCChatConfig 聊天链,然后打开面板。 /// ⇒ 想让"每个人手机不一样",要动的是 npcId(AgentConfig id),不是往服务端塞文本。 /// /// ⛑ 注册的 kind 用 Both:按 AskControlAgent 那次的教训,同族 RPC 会有 NOTIFY 变体, /// 只注册 Invoke 就是静默丢包;ReturnEmptyOkAsync 对 NOTIFY 是 no-op。 /// internal sealed partial class GameRouter { /// per-session:本局拾取过的掉落物 uid(去重后只用于日志计数)。 private const string SceneItemPickupStateKey = "client4229938-sceneitem-pickup"; /// /// AskPickUpSceneItemTransferToItem(67297154) —— 把场景里的掉落物收进背包。 /// /// 客户端本地掉落、本地回收,服务端没有 SceneItem 账本(我们从未推过 /// SyncEnemyBeginSceneItemDrop 68357238 / SyncDropBelonging 68316751), /// 所以这里能做的只有放行:不回执,拾取动作 await 到超时。 /// [Handler(MethodId.AskPickUpSceneItemTransferToItem, HandlerPacketKind.Both)] private async Task OnAskPickUpSceneItemTransferToItem(Connection conn, UxRpcMessage msg) { ulong uid = 0; try { var args = msg.GetArgs(); if (args is not null) uid = args.sceneitemuid; } catch (Exception ex) { // ⛑ 不 throw:这是 INVOKE,抛异常等于不回执 ⇒ 客户端卡死。解析不出 uid 也要放行。 conn.Log.Warn($"[PICKUP] AskPickUpSceneItemTransferToItem 解析失败(按 uid=0 处理): {ex.Message}"); } await conn.ReturnEmptyOkAsync(msg); if (!PrivateServerConfigStore.Current.Gameplay.SceneItemPickupEnabled) return; var items = msg.Context.Session.Items; if (items.TryGetValue(SceneItemPickupStateKey, out var raw) is false || raw is not HashSet set) { set = []; items[SceneItemPickupStateKey] = set; } bool fresh; lock (set) fresh = set.Add(uid); conn.Log.Info($"[PICKUP] sceneitem uid={uid} kind={(msg.IsInvoke ? "invoke" : "notify")} " + $"{(fresh ? "new" : "repeat")} — 本局已拾取 {set.Count} 件(服务端无 SceneItem 账本,只回执)"); } /// /// AskEnemyItemPickUp(67453017) —— 从敌人/NPC 身上拿走第 N 件携带物(belonging)。 /// /// 官方完整链是 SyncAgentBelongings → SyncDropBelonging(68316751) → /// SyncEnemyBeginSceneItemDrop(68357238) → 本包。我们只发过第一步 /// (GameRouter.HackNpc.cs 给路人递手机),后两步是 T3-9 的整块待办。 /// 客户端仍会自发这个包 ⇒ 同样只放行。 /// [Handler(MethodId.AskEnemyItemPickUp, HandlerPacketKind.Both)] private async Task OnAskEnemyItemPickUp(Connection conn, UxRpcMessage msg) { ulong enemy = 0; int index = -1; try { var args = msg.GetArgs(); if (args is not null) { enemy = args.enemyinstanceid; index = args.binditemsindex; } } catch (Exception ex) { conn.Log.Warn($"[PICKUP] AskEnemyItemPickUp 解析失败(按 0/-1 处理): {ex.Message}"); } await conn.ReturnEmptyOkAsync(msg); if (!PrivateServerConfigStore.Current.Gameplay.SceneItemPickupEnabled) return; conn.Log.Info($"[PICKUP] belonging from enemy={enemy} index={index} " + $"kind={(msg.IsInvoke ? "invoke" : "notify")}(belongings 是 HackNpc 递的手机,服务端无掉落账本)"); } }