Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.UnhandledBatch.cs
T

326 lines
16 KiB
C#
Raw Normal View History

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;
/// <summary>
/// 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.
/// </summary>
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<GameMethods.ReportDrivingVehicleArgs4229938>(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<GameMethods.AskVehicleNitroValueArgs4229938>(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<GameMethods.AskVehicleNitroArgs4229938>(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<GameMethods.AskModifyVehicleTopSpeedArgs4229938>(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<GameMethods.AskVehicleHornArgs4229938>(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<GameMethods.AskVehicleContactDamageArgs4229938>(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<GameMethods.SyncChangeSafeAreaArgs4229938>(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<GameMethods.SyncChangeBuildingArgs4229938>(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<GameMethods.SyncChangeIndoorArgs4229938>(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<byte>())`)⇒ 客户端 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<GameMethods.AskNotifyDestructibleHitsArgs4229938>(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<GameMethods.AskOperateDestructibleObjectArgs4229938>(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<GameMethods.AskMoveDestructibleObjectsArgs4229938>(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<GameMethods.AskBreakDestructibleObjectsArgs4229938>(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<GameMethods.AskDiscardWeaponByInstanceIdArgs4229938>(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<GameMethods.AskDiscardWeaponArgs4229938>(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<GameMethods.AskBreakSkillTimeCurveArgs4229938>(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;
}
}