326 lines
16 KiB
C#
326 lines
16 KiB
C#
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;
|
|||
|
|
}
|
|||
|
|
}
|