using Ananta.SDK.Rpc; using Ananta.Server.Configuration; // IdCounterStore(硬规则 104 的落盘号段) using Ananta.Server.Protocol.Client4229938; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; namespace Ananta.Server.Handlers.Game; /// /// Batch implementation of top unhandled methods from unknown-methods.log: /// camera telemetry (silent no-ops), vehicle driving/radio/horn/top-speed/nitro set, /// indoor/building/safe-area transitions, and a second neutral UI/economy batch. /// internal sealed partial class GameRouter { // --- Camera telemetry: pure client-side state, zero server value. Silent accept. --- [Handler(MethodId.AskPlayerCameraMove, HandlerPacketKind.Notify)] private Task CamMove(Connection conn, UxRpcMessage msg) => Task.CompletedTask; [Handler(MethodId.AskUpdatePlayerCameraRotation, HandlerPacketKind.Notify)] private Task CamRot(Connection conn, UxRpcMessage msg) => Task.CompletedTask; [Handler(MethodId.AskUpdatePlayerCameraFOV, HandlerPacketKind.Notify)] private Task CamFov(Connection conn, UxRpcMessage msg) => Task.CompletedTask; [Handler(MethodId.AskUpdatePlayerCameraAspectRatio, HandlerPacketKind.Notify)] private Task CamAspect(Connection conn, UxRpcMessage msg) => Task.CompletedTask; // --- Vehicle driving telemetry --- [Handler(MethodId.ReportDrivingVehicle, HandlerPacketKind.Notify)] private Task ReportDrivingVehicle(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { var runtime = GetVehicleRuntime(msg.Context); lock (runtime.SyncRoot) { if (runtime.Vehicles.TryGetValue(args.vehicleId, out var record)) { record.DistanceDriven += args.deltaDistance; if (record.IsDriving != args.isDriving) { record.IsDriving = args.isDriving; conn.Log.Info($"[VEHICLE] driving vehicle={args.vehicleId} on={args.isDriving} odo={record.DistanceDriven:F0}m"); } } } } return Task.CompletedTask; } [Handler(MethodId.AskVehicleNitroValue, HandlerPacketKind.Notify)] private Task AskVehicleNitroValue(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { var runtime = GetVehicleRuntime(msg.Context); lock (runtime.SyncRoot) { if (runtime.Vehicles.TryGetValue(args.vehicleId, out var record)) record.LastNitroValue = args.value; } } return Task.CompletedTask; } [Handler(MethodId.AskVehicleNitro)] private async Task AskVehicleNitro(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[VEHICLE] nitro vehicle={args.VehicleId} value={args.NitrogenValue:F1} begin={args.BeginOrEnd}"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.AskModifyVehicleTopSpeed, HandlerPacketKind.Notify)] private Task AskModifyVehicleTopSpeed(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { var runtime = GetVehicleRuntime(msg.Context); lock (runtime.SyncRoot) { if (runtime.Vehicles.TryGetValue(args.vehicleId, out var record)) { record.TopSpeed = args.topSpeed; record.HasTopSpeed = args.beginOrEnd; } } conn.Log.Info($"[VEHICLE] topspeed vehicle={args.vehicleId} v={args.topSpeed:F1} active={args.beginOrEnd}"); } return Task.CompletedTask; } [Handler(MethodId.AskVehicleHorn, HandlerPacketKind.Notify)] private Task AskVehicleHorn(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[VEHICLE] horn entity={args.entityId} play={args.play}"); return Task.CompletedTask; } [Handler(MethodId.AskVehicleContactDamage, HandlerPacketKind.Notify)] private Task AskVehicleContactDamage(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args?.data is not null) conn.Log.Info($"[VEHICLE] contact vehicle={args.vehicleId} mass={args.data.VehicleMass:F0} layer={args.data.Layer} other={args.data.OtherVehicleEntityId}"); else conn.Log.Warn($"[VEHICLE] AskVehicleContactDamage undecodable bytes={msg.Body.Length}"); return Task.CompletedTask; } [Handler(MethodId.AskVehicleInteractConfig, HandlerPacketKind.Notify)] private Task AskVehicleInteractConfig(Connection conn, UxRpcMessage msg) { conn.Log.Info($"[VEHICLE] interact-config bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body, 0, Math.Min(msg.Body.Length, 48))}"); return Task.CompletedTask; } [Handler(MethodId.AskUpdateVehicleDestructibleParts, HandlerPacketKind.Notify)] private Task AskUpdateVehicleDestructibleParts(Connection conn, UxRpcMessage msg) { conn.Log.Info($"[VEHICLE] destructible-parts bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body, 0, Math.Min(msg.Body.Length, 48))}"); return Task.CompletedTask; } // AskGetVehicleRadioContent (67725622) 与 AskSwitchVehicleRadio (67959772) 以前在这里占位 // (只打日志 + 回全零 typed-default)。2026-09-27 搬到 GameRouter.VehicleRadio.cs: // 现在会真的按 RadioContentCatalog 下发 DJ 口播 / 广告 / 新闻的 id。 // ⛑ 不要在别处再挂同名 handler —— 同一 methodId 两个 invoke handler 会 // "Duplicate invoke handler at startup"。 // --- Indoor / building / safe-area transitions (logged accepts; sector control later) --- [Handler(MethodId.SyncChangeSafeArea, HandlerPacketKind.Notify)] private Task SyncChangeSafeArea(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[SCENE] safe-area region={args.regionId}"); return Task.CompletedTask; } [Handler(MethodId.SyncChangeBuilding)] private async Task SyncChangeBuilding(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[SCENE] building={args.buildingId} floor={args.floorId}"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.SyncChangeIndoor)] private async Task SyncChangeIndoor(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { conn.Log.Info($"[SCENE] indoor config={args.indoorConfigId} bound={args.boundId}"); // ★ 这是室内判定的权威来源:客户端进/出 GpsBound 时自己报 indoorConfigId。 // 服务端的几何最近邻在 MiniMapPos 矩形重叠的场馆(ACD 76 / ACD快乐房 129)会抖, // 所以 GameRouter.IndoorSector.cs 优先读这里。见 IndoorSectorSettings.PreferClientReport。 var state = GetWorldState(msg.Context); lock (state.SyncRoot) { state.ClientIndoorConfigId = args.indoorConfigId; state.ClientIndoorReportMs = Environment.TickCount64; } } await conn.ReturnEmptyOkAsync(msg); } // --- Neutral UI/economy batch 2: exact typed-default replies, info-level logging --- // AskGetAllMetroInfos used to be answered here with the generic empty list. It now has a real // scheduler in GameRouter.Metro.cs (round 54/55): keep the attribute off so the router does not see // two invoke handlers for the same method id (Duplicate invoke handler at startup). [Handler(MethodId.GetMailHeadList, HandlerPacketKind.Invoke)] // AskMomentsPostSimpleInfos used to be answered here with the generic empty list. It now has a // real catalogue in GameRouter.Bubble.cs (趴趴 / Bubble): keep the attribute off so the router // does not see two invoke handlers for the same method id. [Handler(MethodId.AskAkxSessionList, HandlerPacketKind.Invoke)] [Handler(MethodId.AskPanelOpenOrClose, HandlerPacketKind.Invoke)] [Handler(MethodId.AskQueryPlayerUnlockNameEffect, HandlerPacketKind.Invoke)] [Handler(MethodId.GetPersonalInfo, HandlerPacketKind.Invoke)] private Task KnownNeutralInvokeBatch2(Connection conn, UxRpcMessage msg) { if (DefaultReturnCatalog4229938.TryGet(msg.MethodId, out var body, out var shape)) { conn.Log.Info($"[RPC4229938] neutral {Ananta.SDK.Logging.RpcMethodNames.Display(msg.MethodId)} -> {shape} {body.Length}b"); return msg.Context.ReturnAsync(body); } return conn.ReturnEmptyOkAsync(msg); } // --- Skill/combat accepts: logged, protocol-correct empty replies (authoritative later) --- [Handler(MethodId.AskInterruptSkillExecute2)] [Handler(MethodId.AskSkillExecute3)] [Handler(MethodId.AskSkillExecuteEnd2)] [Handler(MethodId.AskSkillAddState)] [Handler(MethodId.AskSkillOpenShield)] [Handler(MethodId.AskSkillCloseShield)] [Handler(MethodId.AskSkillTimeCurve)] [Handler(MethodId.ReportSkillAnimationEnd)] [Handler(MethodId.AskSkillSpawnItem)] [Handler(MethodId.AskSpoonClientAttack)] [Handler(MethodId.AskVehicleSkillDamage)] private async Task SkillNeutralAccepts(Connection conn, UxRpcMessage msg) { conn.Log.Info($"[COMBAT] accept {Ananta.SDK.Logging.RpcMethodNames.Display(msg.MethodId)} bytes={msg.Body.Length}"); await conn.ReturnEmptyOkAsync(msg); } // ----------------------------------------------------------------------------------------- // ⛑⛑ 2026-09-29 修正:这两条**不是 void**。 // 客户端 `midToReturnMessageReader` 里两者都是 `reader.ReadUInt64(reader)`(裸 u64,无 presence), // 而原来挂在 SkillNeutralAccepts 上走 `ReturnEmptyOkAsync` —— 那个是 **0 字节载荷** // (`Connection.cs:30` `ReturnEmptyAsync(..., Array.Empty())`)⇒ 客户端 reader 读空 buffer。 // 实测触发频率:**[COMBAT] 三个会话共 29 次**,是本轮回归检查里最高频的一条。 // 修法照抄同类已有实现 `AskItemDestructibleCreate`(GameRouter.MiscAcks.cs:200)—— // 用硬规则 104 的落盘号段发型实例 id(只回号不建实体,目的是让客户端 await 落地 + 拿得到 uid)。 // ⛑ 回滚:把这两行属性挪回上面的 SkillNeutralAccepts 即可(handler 不注册走 typed-default)。 // ----------------------------------------------------------------------------------------- [Handler(MethodId.AskSkillDestructibleCreateGadget, HandlerPacketKind.Invoke)] [Handler(MethodId.AskSkillDestructibleCreateVehicle, HandlerPacketKind.Invoke)] private Task AskSkillDestructibleCreate(Connection conn, UxRpcMessage msg) { var id = unchecked((ulong)IdCounterStore.Next(DestructibleInstanceIdCounter, DestructibleInstanceIdFloor)); conn.Log.Info($"[COMBAT] skill-destructible-create {Ananta.SDK.Logging.RpcMethodNames.Display(msg.MethodId)} " + $"bytes={msg.Body.Length} -> id={id}"); return conn.ReturnAsync(msg, id); } // --- Destructibles / weapon red dots / discards / hangup (fresh log batch) --- [Handler(MethodId.AskNotifyDestructibleHits)] private async Task AskNotifyDestructibleHits(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[DESTRUCTIBLE] hits targets={args.hits.Count}"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.AskOperateDestructibleObject)] private async Task AskOperateDestructibleObject(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[DESTRUCTIBLE] operate id={args.id} op={args.operation}"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.AskMoveDestructibleObjects, HandlerPacketKind.Notify)] private Task AskMoveDestructibleObjects(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[DESTRUCTIBLE] move count={args.ids.Count}"); return Task.CompletedTask; } [Handler(MethodId.AskBreakDestructibleObjects, HandlerPacketKind.Notify)] private Task AskBreakDestructibleObjects(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[DESTRUCTIBLE] break breaker={args.breaker} count={args.brokenInfos.Count}"); else conn.Log.Warn($"[DESTRUCTIBLE] AskBreakDestructibleObjects undecodable bytes={msg.Body.Length}"); return Task.CompletedTask; } [Handler(MethodId.AskReadWeaponRedDots, HandlerPacketKind.Notify)] private Task AskReadWeaponRedDots(Connection conn, UxRpcMessage msg) => Task.CompletedTask; [Handler(MethodId.AskDiscardWeaponByInstanceId)] private async Task AskDiscardWeaponByInstanceId(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { var state = GetWorldState(msg.Context); var slots = WeaponSlotIds(state, args.spiritId); var index = slots.IndexOf(args.weaponInstanceId); if (index >= 0 && IsEditableWeaponSlot(index)) { slots[index] = 0; conn.Log.Info($"[ARMORY] discard instance={args.weaponInstanceId} spirit={args.spiritId} slot={index} dropout={args.isDropOut}"); await PublishWeaponSlotsAfterMutation(msg.Context, args.spiritId, $"discard:{args.weaponInstanceId}"); } else { conn.Log.Warn($"[ARMORY] discard rejected instance={args.weaponInstanceId} spirit={args.spiritId}"); } } await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.AskDiscardWeapon, HandlerPacketKind.Notify)] private async Task AskDiscardWeapon(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { var state = GetWorldState(msg.Context); await OnDepositSpiritWeapon(msg.Context, state.ActiveSpiritTemplateId, args.index); conn.Log.Info($"[ARMORY] discard-index slot={args.index}"); } } [Handler(MethodId.RequestPlayerStartHangup)] private async Task RequestPlayerStartHangup(Connection conn, UxRpcMessage msg) { GetWorldState(msg.Context).IsHangup = true; conn.Log.Info("[HANGUP] start"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.RequestPlayerStopHangup)] private async Task RequestPlayerStopHangup(Connection conn, UxRpcMessage msg) { GetWorldState(msg.Context).IsHangup = false; conn.Log.Info("[HANGUP] stop"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.AskBreakSkillTimeCurve, HandlerPacketKind.Notify)] private Task AskBreakSkillTimeCurve(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[COMBAT] break-curve releaser={args.releaser} id={args.id} index={args.index}"); return Task.CompletedTask; } }