445 lines
23 KiB
C#
445 lines
23 KiB
C#
using Ananta.SDK.Rpc;
|
||
using Ananta.Server.Configuration;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
using PoliceMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 巡卫官(里希 / 15021021)执勤终端里的三条派遣,C2S 都是 <c>AskPoliceDispatch</c>(63500791),
|
||
/// 只靠 <c>dispatchid</c> 区分:
|
||
/// <code>
|
||
/// PoliceDispatchCatalog.json
|
||
/// Id=2 NCCA Dog 巡卫犬 SkillId 51938184 / SwitchSkillId 51938186
|
||
/// Id=3 Helicopter Search 直升机搜查 SkillId 0 → 走 S2C SyncPoliceDispatchHelicopter(68359668)
|
||
/// Id=4 Armed Support 战术支援 SkillId 0 Des「冲锋枪2人」,DialogId 100025472
|
||
/// </code>
|
||
///
|
||
/// <b>2026-09-24 抓包:按钮一直在发,服务端一直在回空 OK。</b>
|
||
/// <c>logs/packets-20260924-233242.log:209939</c> 23:46:42 C2S AskPoliceDispatch id=3,
|
||
/// 服务端 <c>DefaultUnknownInvoke4229938</c> 回 <c>error=0 body=0b</c> ⇒ 客户端不卡死,
|
||
/// 但世界上什么也没发生。<c>unknown-methods.log</c> 里 09-21 还有 id=2/3/4 各一次。
|
||
///
|
||
/// 本文件只接管 dispatchid=4(战术支援)。2 走技能链(GameRouter.HackerSummon.cs),
|
||
/// 3(直升机)契约只有两个参数但目前没有实体模板可刷,先留日志位。
|
||
///
|
||
/// ⛑ 硬规则 94:光写 [Handler] 不算注册,<c>GameRouter.cs</c> 的
|
||
/// <c>Enabled4229938MethodIds</c> 必须同步加 <c>MethodId.AskPoliceDispatch</c>,
|
||
/// 否则编译通过、功能零生效,而这是个 INVOKE —— 客户端会 await 到超时。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
// ---------------------------------------------------------------------------------
|
||
// 常量
|
||
// ---------------------------------------------------------------------------------
|
||
|
||
private const uint PoliceDispatchDog = 2;
|
||
private const uint PoliceDispatchHelicopter = 3;
|
||
private const uint PoliceDispatchArmedSupport = 4;
|
||
|
||
/// <summary>
|
||
/// 战术支援刷的两个友方模板。都来自 AgentSummonCatalog:
|
||
/// <code>
|
||
/// 40120875 巡卫官(冲锋枪) EnemyClassType=1 GroupEnemy=40211004 AI=40213022 InitWeapon=98005562
|
||
/// 40120983 冲锋枪巡卫官 同上(另一个 variant,字段完全一致)
|
||
/// </code>
|
||
/// 与 PoliceDispatchCatalog Id=4 的 <c>Des="冲锋枪2人"</c>、<c>Number=2</c> 对得上。
|
||
/// ⛑ 两者都<b>不在</b> EnemyAiCatalog.json(3160 行)里 ⇒ SpawnAgentAsync 里 aiProfile=null,
|
||
/// tree 会 fallback 到 Main_Common、武器取模板自带的 98005562、技能槽不绑。
|
||
/// 机械狗(40120911)也是同一个处境,说明这条 fallback 路径是可用的。
|
||
/// </summary>
|
||
private static readonly uint[] ArmedSupportAgentIds = [40120875, 40120983];
|
||
|
||
/// <summary>per-session 当前在场支援单位的 uid 列表的 key。</summary>
|
||
private const string ArmedSupportStateKey = "client4229938-police-armed-support";
|
||
|
||
/// <summary>per-session 直升机搜查记录的 key。</summary>
|
||
private const string HelicopterScanStateKey = "client4229938-police-helicopter";
|
||
|
||
/// <summary>
|
||
/// 「直升机搜查」落地用的载具模板 —— <c>VehicleConfig.json</c> 的 <b>81007007</b>
|
||
/// (<c>Assets/Res/Prefab/Drive/Helicopter/Helicopter01a_Rope.prefab</c>,带索降绳,比 81006001
|
||
/// "Reed·Ranger" 的素机体更贴巡卫设定;VehicleType=21、6 座、VehicleControlId=7007)。
|
||
///
|
||
/// ⛑ 它是 <b>载具</b>不是 agent:不在 AgentSummonCatalog 里,只能用
|
||
/// <c>SpawnVehicleAsync(force: true)</c> 刷(fleetIds 全是地面车,飞行载具必须 force)。
|
||
///
|
||
/// ⛑ <b>这是替代方案,不是官方实现的复刻</b>:官方的直升机搜查在 build 4229938 里是半成品
|
||
/// (硬规则 134 —— 面板无 Open 入口 / SyncPoliceDispatchHelicopter 空实现 / dump.cs 无 C# 类),
|
||
/// 服务端无论怎么接都只是在后台记账。刷这台载具是唯一能"让场景里真的出现一架直升机"的手段。
|
||
/// </summary>
|
||
private const uint HelicopterVehicleId = 81007007;
|
||
|
||
/// <summary>per-session 当前这架搜查直升机载具 uid 的 key。</summary>
|
||
private const string HelicopterVehicleStateKey = "client4229938-police-helicopter-vehicle";
|
||
|
||
/// <summary>支援单位站位:玩家正前方 4 m,左右各 1.6 m(避免两只叠在一个坐标)。</summary>
|
||
private const float ArmedSupportForwardMetres = 4.0f;
|
||
private const float ArmedSupportLateralMetres = 1.6f;
|
||
|
||
// =================================================================================
|
||
// ① C2S AskPoliceDispatch(63500791)
|
||
// =================================================================================
|
||
|
||
/// <summary>
|
||
/// 执勤终端 / 轮盘的派遣按钮。INVOKE —— 客户端 <c>gReliableRpcManager</c> 等回执,
|
||
/// <b>无论 dispatchid 认不认都得先回 empty OK</b>,否则按钮 await 永不落地。
|
||
/// </summary>
|
||
[Handler(MethodId.AskPoliceDispatch, HandlerPacketKind.Invoke)]
|
||
private async Task AskPoliceDispatch(Connection conn, UxRpcMessage msg)
|
||
{
|
||
uint dispatchId = 0;
|
||
try
|
||
{
|
||
dispatchId = msg.GetArgs<PoliceMethods.AskPoliceDispatch4229938>().dispatchid;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 回执照发。parse 失败只是拿不到语义,行为按"不认识"处理。
|
||
msg.Context.Session.Log.Warn($"[POLICE] AskPoliceDispatch 解析失败(按 dispatchid=0 处理): {ex.Message}");
|
||
}
|
||
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
await OnPoliceDispatch(msg.Context, dispatchId);
|
||
}
|
||
|
||
private async Task OnPoliceDispatch(RpcContext ctx, uint dispatchId)
|
||
{
|
||
switch (dispatchId)
|
||
{
|
||
case PoliceDispatchArmedSupport:
|
||
await DispatchArmedSupportAsync(ctx);
|
||
return;
|
||
|
||
case PoliceDispatchDog:
|
||
// 狗走技能链:PoliceDispatchCatalog Id=2 的 SkillId=51938184 / SwitchSkillId=51938186,
|
||
// 由客户端自己 UseSkillByPid 发 AskUseSkill ⇒ GameRouter.HackerSummon.cs 接。
|
||
// 这条派遣包只是 UI 入口,服务端不需要额外动作。
|
||
ctx.Session.Log.Info($"[POLICE] dispatch={dispatchId} (NCCA Dog) — 走技能链 51938184/51938186,本通道无需动作");
|
||
return;
|
||
|
||
case PoliceDispatchHelicopter:
|
||
// 开一轮新的搜查:清掉上一轮的标记。
|
||
// ⛔ 别指望 SyncPoliceDispatchHelicopter(68359668) 能把直升机送进场 —— 客户端那个
|
||
// handler 是空函数体(GameSceneToClientImpl.lua:770),发了等于没发。视角与面板由
|
||
// 客户端 C# 侧自己开(lua 里 S_POLICE_HELICOPTER 没有任何 Open 调用点),
|
||
// 服务端在这条玩法里真正要做的只是把下面那三个 C2S 接住。
|
||
ctx.Session.Items.Remove(HelicopterScanStateKey);
|
||
ctx.Session.Log.Info($"[POLICE] dispatch={dispatchId} (Helicopter Search) — 新一轮搜查,已清空上一轮标记;等待 AskHelicopterNpc / AskHelicopterScanChange / AskPoliceStopHelicopterDispatch");
|
||
await SpawnHelicopterVehicleAsync(ctx);
|
||
return;
|
||
|
||
default:
|
||
ctx.Session.Log.Warn($"[POLICE] dispatch={dispatchId} 未知派遣 id — 只回执,不做动作");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// =================================================================================
|
||
// ② 战术支援:刷两个友方冲锋枪巡卫官
|
||
// =================================================================================
|
||
|
||
private async Task DispatchArmedSupportAsync(RpcContext ctx)
|
||
{
|
||
var cfg = PrivateServerConfigStore.Current.Gameplay;
|
||
if (!cfg.PoliceArmedSupportEnabled)
|
||
{
|
||
ctx.Session.Log.Info("[POLICE] dispatch=4 (Armed Support) skipped — Gameplay.PoliceArmedSupportEnabled=false (回滚开关)");
|
||
return;
|
||
}
|
||
|
||
var session = ctx.Session;
|
||
var state = GetStateIfExists(session);
|
||
if (state is null)
|
||
{
|
||
ctx.Session.Log.Warn("[POLICE] dispatch=4 (Armed Support) — 没有世界状态,跳过");
|
||
return;
|
||
}
|
||
|
||
// 先把上一批收掉:按钮可以反复点,不收会一直在城里堆人,且旧的再也找不回来。
|
||
await DismissArmedSupportAsync(ctx);
|
||
|
||
float px, py, pz, facing;
|
||
lock (state.SyncRoot)
|
||
{
|
||
if (state.HasLastReportedPlayerTransform)
|
||
{
|
||
var p = state.LastReportedPlayerPosition;
|
||
px = p.X; py = p.Y; pz = p.Z;
|
||
facing = state.LastReportedPlayerRotation.Y;
|
||
}
|
||
else
|
||
{
|
||
px = py = pz = facing = 0f;
|
||
}
|
||
}
|
||
|
||
// forward = (sin f, cos f);right = (cos f, -sin f)。与 GameAdminBridge 的扇形摆放同源。
|
||
// ⛑ facing 是度数,必须先转弧度(2026-09-26 修,同 SpawnAgentAsync 那处)。
|
||
var yaw = facing * (float)(Math.PI / 180.0);
|
||
var fx = (float)Math.Sin(yaw);
|
||
var fz = (float)Math.Cos(yaw);
|
||
var rx = (float)Math.Cos(yaw);
|
||
var rz = -(float)Math.Sin(yaw);
|
||
|
||
var spawned = new List<ulong>();
|
||
for (var i = 0; i < ArmedSupportAgentIds.Length; i++)
|
||
{
|
||
var lateral = (i == 0 ? -1f : 1f) * ArmedSupportLateralMetres;
|
||
var x = px + fx * ArmedSupportForwardMetres + rx * lateral;
|
||
var z = pz + fz * ArmedSupportForwardMetres + rz * lateral;
|
||
|
||
var agentId = ArmedSupportAgentIds[i];
|
||
var result = await GameAdminBridge.SpawnAgentAsync(agentId, x, py, z, facing,
|
||
hostile: true, // EnemyClassType=1 ⇒ 必须 true,否则 entry.Hostile 校验直接拒
|
||
engage: false, // 不注册进敌人账本:友军不该被打死,也不需要服务端拉去战斗
|
||
forceEnemyRelation: false, // ⛑ 别把玩家和友军标成敌对(否则能锁定、挥刀不掉血)
|
||
// ⛑ 2026-10-01:原来是硬编码 0(MonsterDefault)⇒ 玩家能锁定自己人、头顶还挂血条。
|
||
// 与嘉姆随从同一处理,改读 gameplay.aether.companionCamp(默认 1 = PlayerDefault)。
|
||
campOverride: PrivateServerConfigStore.Current.Gameplay.Aether.CompanionCamp);
|
||
|
||
if (!result.Ok || result.Uid == 0)
|
||
{
|
||
ctx.Session.Log.Warn($"[POLICE] armed-support spawn failed agent={agentId} reason={result.Message}");
|
||
continue;
|
||
}
|
||
|
||
spawned.Add(result.Uid);
|
||
ctx.Session.Log.Info($"[POLICE] armed-support spawned agent={agentId} uid={result.Uid} at=({x:F2},{py:F2},{z:F2}) facing={facing:F2}");
|
||
|
||
// ⛑⛑ 2026-10-01(用户点名):把这两名支援接入<b>和嘉姆随从同一套 AI</b>
|
||
// —— 跟随 + 协战 + 收血条 + 清瞄准态,见 GameRouter.CompanionBind.cs。
|
||
// 不接的话他们只会杵在原地(spawn 后服务端一个动作指令都不发)。
|
||
// ⛑ 必须等 spawn 包落地再推:客户端消费方当场拿 uid 去 SceneDataMgr 查单位。
|
||
if (PrivateServerConfigStore.Current.Gameplay.Aether.CompanionBindArmedSupport)
|
||
{
|
||
await Task.Delay(250);
|
||
var (bindOk, bindMsg) = await BindCompanionAsync(result.Uid);
|
||
ctx.Session.Log.Info($"[POLICE] armed-support companion bind: {(bindOk ? "OK" : "FAILED")} {bindMsg}");
|
||
}
|
||
}
|
||
|
||
if (spawned.Count == 0)
|
||
{
|
||
ctx.Session.Log.Warn("[POLICE] dispatch=4 (Armed Support) — 两只都没刷出来,不记账");
|
||
return;
|
||
}
|
||
|
||
session.Items[ArmedSupportStateKey] = spawned;
|
||
ctx.Session.Log.Info($"[POLICE] dispatch=4 (Armed Support) — {spawned.Count}/{ArmedSupportAgentIds.Length} 名支援巡卫官就位 uid=[{string.Join(",", spawned)}]");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收掉当前在场的支援单位。SpawnAgentAsync 出来的单位配对的移除通道是
|
||
/// <c>SyncLogicAgentLeave</c>(68281244)(第一句话是 SyncLogicAgentEnter)—— 与
|
||
/// GameRouter.HackerSummon.cs 里回收召唤物用的是同一条,别用 SyncAetherAINpcRemove(那是
|
||
/// ambient 行人 / functional NPC 的通道,对这类单位发了等于没发)。
|
||
/// </summary>
|
||
private async Task DismissArmedSupportAsync(RpcContext ctx)
|
||
{
|
||
if (!ctx.Session.Items.TryGetValue(ArmedSupportStateKey, out var raw) || raw is not List<ulong> uids)
|
||
return;
|
||
|
||
ctx.Session.Items.Remove(ArmedSupportStateKey);
|
||
if (uids.Count == 0) return;
|
||
|
||
foreach (var uid in uids)
|
||
{
|
||
// 先停掉跟随/协战 worker,再撤场 —— 否则 worker 会对着已经消失的单位继续发包。
|
||
StopCompanionAssist(uid);
|
||
await ctx.NotifyAsync(MethodId.SyncLogicAgentLeave, WorldCodec.LogicAgentLeave(uid));
|
||
}
|
||
|
||
ctx.Session.Log.Info($"[POLICE] armed-support dismissed {uids.Count} uid=[{string.Join(",", uids)}] — SyncLogicAgentLeave sent");
|
||
}
|
||
|
||
// =================================================================================
|
||
// ②b 直升机载具:让场景里真的出现一架直升机
|
||
//
|
||
// ⛑ 这是替代方案,不是官方实现的复刻 —— 官方直升机搜查面板在 build 4229938 里是半成品
|
||
// (硬规则 134:无 Open 入口 / SyncPoliceDispatchHelicopter 空实现 / dump.cs 无 C# 类),
|
||
// 服务端再怎么接也只是在后台记账。刷这台载具是唯一能"看见直升机"的手段。
|
||
// =================================================================================
|
||
|
||
/// <summary>
|
||
/// 刷一台搜查直升机载具(81007007 Helicopter01a_Rope)。
|
||
///
|
||
/// ⛑ <c>SpawnDirectAsync</c> 内部自带 replacement:刷之前会把 <c>runtime.SummonedEntity</c>
|
||
/// 上那台(<b>可能是玩家自己召唤的车</b>)<c>DestroyVehicleAsync</c> 掉。这里只提前打一行
|
||
/// warn,不拦 —— 玩家点「直升机搜查」时通常没在开车,而且顶掉比堆车好。
|
||
///
|
||
/// 落点是玩家右侧 5.5 m、+0.15 m(SpawnDirectAsync 里的 proven offset)。对直升机来说
|
||
/// 就是停在旁边地面上,<b>不需要抬高度</b>(抬了反而会卡在半空下不来)。
|
||
/// </summary>
|
||
private static async Task<bool> SpawnHelicopterVehicleAsync(RpcContext ctx)
|
||
{
|
||
if (!PrivateServerConfigStore.Current.Gameplay.PoliceHelicopterVehicleEnabled)
|
||
{
|
||
ctx.Session.Log.Info($"[POLICE-HELI] vehicle spawn skipped — Gameplay.PoliceHelicopterVehicleEnabled=false (回滚开关)");
|
||
return false;
|
||
}
|
||
|
||
ulong prev;
|
||
var runtime = GetVehicleRuntimeState(ctx.Session);
|
||
lock (runtime.SyncRoot) prev = runtime.SummonedEntity;
|
||
if (prev != 0)
|
||
ctx.Session.Log.Warn($"[POLICE-HELI] 上一台召唤载具 uid={prev} 将被直升机顶掉(SpawnDirectAsync 的 replacement 逻辑)");
|
||
|
||
var result = await GameAdminBridge.SpawnVehicleAsync(HelicopterVehicleId, force: true);
|
||
if (!result.Ok || result.Uid == 0)
|
||
{
|
||
ctx.Session.Log.Warn($"[POLICE-HELI] helicopter vehicle spawn failed id={HelicopterVehicleId} reason={result.Message}");
|
||
return false;
|
||
}
|
||
|
||
ctx.Session.Items[HelicopterVehicleStateKey] = result.Uid;
|
||
ctx.Session.Log.Info($"[POLICE-HELI] helicopter vehicle spawned id={HelicopterVehicleId} (Helicopter01a_Rope) uid={result.Uid} — 停在玩家右侧 5.5 m");
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收掉这架搜查直升机(一轮搜查结束时调)。
|
||
/// 走 <c>DestroyVehicleAsync</c> ⇒ S2C <c>SyncDestroyVehicle</c>,与玩家召唤车 / 交通车
|
||
/// 用的是同一条通道,不是 SyncLogicAgentLeave(那是 agent 的)。
|
||
/// </summary>
|
||
private static async Task DismissHelicopterVehicleAsync(RpcContext ctx)
|
||
{
|
||
if (!ctx.Session.Items.TryGetValue(HelicopterVehicleStateKey, out var raw) || raw is not ulong uid || uid == 0)
|
||
return;
|
||
|
||
ctx.Session.Items.Remove(HelicopterVehicleStateKey);
|
||
var runtime = GetVehicleRuntimeState(ctx.Session);
|
||
await DestroyVehicleAsync(ctx.Session, runtime, uid, "police-helicopter-stop");
|
||
ctx.Session.Log.Info($"[POLICE-HELI] helicopter vehicle uid={uid} destroyed — SyncDestroyVehicle sent");
|
||
}
|
||
|
||
// =================================================================================
|
||
// ③ 直升机搜查:AskHelicopterNpc / AskHelicopterScanChange / AskPoliceStopHelicopterDispatch
|
||
// =================================================================================
|
||
|
||
/// <summary>
|
||
/// 一轮直升机搜查的服务端记账。官方玩法里这些标记会喂给"盘查 / 通缉 / 案件"那条链,
|
||
/// 我们那条链是空的,所以先只把玩家框过的单位记下来 —— 至少让 RPC 有回执、有日志可查。
|
||
/// </summary>
|
||
private sealed class HelicopterScanState
|
||
{
|
||
/// <summary>扫描开关(AskHelicopterScanChange)。</summary>
|
||
public bool ScanOpen;
|
||
|
||
/// <summary>instanceid → 最后一次 optype。</summary>
|
||
public Dictionary<ulong, byte> Marks = new();
|
||
}
|
||
|
||
/// <summary>
|
||
/// C2S AskHelicopterNpc(67113443),INVOKE —— 玩家在直升机视角里框住一个 NPC 并下判定。
|
||
///
|
||
/// ⛑⛑ 这条以前不在 <c>Enabled4229938MethodIds</c> 里,等于走中性兜底;而它是 INVOKE,
|
||
/// 客户端四个调用点(PoliceHelicopterPanelStore.lua:162 / :378 / :470 / :558)全在用
|
||
/// <c>gReliableRpcManager:RegisterRPC</c> 等回调 ⇒ 面板会卡在"点了没反应"。
|
||
/// 硬规则 94 的老坑,别再踩。
|
||
/// </summary>
|
||
[Handler(MethodId.AskHelicopterNpc, HandlerPacketKind.Invoke)]
|
||
private async Task AskHelicopterNpc(Connection conn, UxRpcMessage msg)
|
||
{
|
||
ulong instanceId = 0;
|
||
byte optype = 0;
|
||
try
|
||
{
|
||
var args = msg.GetArgs<PoliceMethods.AskHelicopterNpc4229938>();
|
||
instanceId = args.instanceid;
|
||
optype = args.optype;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
msg.Context.Session.Log.Warn($"[POLICE-HELI] AskHelicopterNpc 解析失败: {ex.Message}");
|
||
}
|
||
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
|
||
if (!PrivateServerConfigStore.Current.Gameplay.PoliceHelicopterEnabled)
|
||
{
|
||
msg.Context.Session.Log.Info($"[POLICE-HELI] npc uid={instanceId} optype={optype} skipped — Gameplay.PoliceHelicopterEnabled=false(只回执,不记账)");
|
||
return;
|
||
}
|
||
|
||
var state = GetOrCreateHelicopterScan(msg.Context);
|
||
lock (state.Marks)
|
||
state.Marks[instanceId] = optype;
|
||
|
||
msg.Context.Session.Log.Info($"[POLICE-HELI] npc uid={instanceId} optype={optype} scan={(state.ScanOpen ? "on" : "off")} — 本轮已标记 {state.Marks.Count} 个单位");
|
||
}
|
||
|
||
/// <summary>
|
||
/// C2S AskHelicopterScanChange(67898923),<b>NOTIFY</b> —— 直升机扫描开关。
|
||
/// 客户端用的是 <c>self.Notify</c>(ClientToGameSceneDelegate.lua:5198),没有回执;
|
||
/// 这里登记 <c>Both</c> 兜住,将来客户端改成 Invoke 也不用改代码(AskControlAgent 那次就是这么栽的)。
|
||
/// </summary>
|
||
[Handler(MethodId.AskHelicopterScanChange, HandlerPacketKind.Both)]
|
||
private async Task AskHelicopterScanChange(Connection conn, UxRpcMessage msg)
|
||
{
|
||
bool open = false;
|
||
try
|
||
{
|
||
open = msg.GetArgs<PoliceMethods.AskHelicopterScanChange4229938>().open;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
msg.Context.Session.Log.Warn($"[POLICE-HELI] AskHelicopterScanChange 解析失败(按 open=false 处理): {ex.Message}");
|
||
}
|
||
|
||
// 与 AskControlAgent(HackerSummon.cs)同一套做法:Both 注册的 handler 无条件回执。
|
||
// notify 方向多一个回执包客户端不会消费,反过来漏了 invoke 的回执会挂死,宁可多发。
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
|
||
if (!PrivateServerConfigStore.Current.Gameplay.PoliceHelicopterEnabled)
|
||
{
|
||
msg.Context.Session.Log.Info($"[POLICE-HELI] scan open={open} skipped — Gameplay.PoliceHelicopterEnabled=false");
|
||
return;
|
||
}
|
||
|
||
GetOrCreateHelicopterScan(msg.Context).ScanOpen = open;
|
||
msg.Context.Session.Log.Info($"[POLICE-HELI] scan open={open}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// C2S AskPoliceStopHelicopterDispatch(63057829),INVOKE,无参 —— 一轮搜查结束
|
||
/// (客户端在"扫描成功"后延迟 1 s 发它,见 PoliceHelicopterPanelStore.lua:483)。
|
||
///
|
||
/// 收到后清账,并推一次 S2C <c>SynExitHelicopterView</c>(68309986) 收视角。客户端那个
|
||
/// handler 就是 <c>Close(gPanelId.S_POLICE_HELICOPTER)</c>,幂等 —— 面板已经关了再 Close 一次无害。
|
||
/// </summary>
|
||
[Handler(MethodId.AskPoliceStopHelicopterDispatch, HandlerPacketKind.Invoke)]
|
||
private async Task AskPoliceStopHelicopterDispatch(Connection conn, UxRpcMessage msg)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
|
||
var marked = 0;
|
||
if (msg.Context.Session.Items.TryGetValue(HelicopterScanStateKey, out var raw) && raw is HelicopterScanState st)
|
||
{
|
||
lock (st.Marks) marked = st.Marks.Count;
|
||
msg.Context.Session.Items.Remove(HelicopterScanStateKey);
|
||
}
|
||
|
||
if (!PrivateServerConfigStore.Current.Gameplay.PoliceHelicopterEnabled)
|
||
{
|
||
msg.Context.Session.Log.Info($"[POLICE-HELI] stop — skipped(只回执,不推 SynExitHelicopterView)");
|
||
return;
|
||
}
|
||
|
||
// 一轮搜查结束 ⇒ 把那架载具也收掉,否则城里会一直停着直升机。
|
||
await DismissHelicopterVehicleAsync(msg.Context);
|
||
|
||
await msg.Context.NotifyAsync(MethodId.SynExitHelicopterView, new PoliceMethods.SynExitHelicopterView4229938());
|
||
msg.Context.Session.Log.Info($"[POLICE-HELI] stop — 本轮标记 {marked} 个单位已清空,SynExitHelicopterView(68309986) 已下发");
|
||
}
|
||
|
||
private static HelicopterScanState GetOrCreateHelicopterScan(RpcContext ctx)
|
||
{
|
||
if (ctx.Session.Items.TryGetValue(HelicopterScanStateKey, out var raw) && raw is HelicopterScanState st)
|
||
return st;
|
||
st = new HelicopterScanState();
|
||
ctx.Session.Items[HelicopterScanStateKey] = st;
|
||
return st;
|
||
}
|
||
}
|