using Ananta.SDK.Network;
using Ananta.SDK.Serialization;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
///
/// 里希**押送**:把犯人押上警车、开车带到牢房、再押下来收押(2026-09-30 新增)。
///
/// ★ 捐赠端 v0.5-2 确实做得到,而且它用的两条帧主线全都具备:
///
/// - SyncUnitVehicleStatus(68195719) —— NewClientBoardingInfo,
/// Status:4=已上车 / 5=正在下车 / 0=不在车上。主线
/// GameRouter.VehicleStory.cs 本来就在用它(玩家上下车);
/// - SyncUnitBoardingInfo(68699595) —— NPC 的座位挂载句柄
/// (LogicAgentAttachVehicleHandle.Setup:原生座位偏移/旋转,attach 句柄每帧跟随车辆)。
/// ⛑ VehicleUId=0 会同时解除挂载与原生座位注册 ⇒ 下车就用它。
///
/// ⇒ 捐赠端 GameRouter.PoliceVehicle.cs:93-103 的 SendPolicePassengerBinding4229938
/// 就是这两条;主线 mid 一个在用、一个从未用过(全仓零调用点),
/// **不需要 GATE Lua 通道** ⇒ 这一条主线能做,跟牢房那步不一样。
///
/// 与捐赠端的差异(照旧:不做演出,做功能):
///
/// - 捐赠端触发的是原生押送过场
/// (gPoliceJobManager.cs:AskPoliceEscortInteract(agent, 20) →
/// SEscortToVehicleInteract,映射表 LixiPolice.cs:445 的 20 => 20001),
/// 有"押着人走到车边、开门、塞进去"的动画;
/// - 主线直接发绑定帧 ⇒ 犯人直接出现在座位上,没有过场动画。
/// 跟出租车/牢房同一个取舍:先要功能,动画等通道。
///
///
/// ★ SyncUnitBoardingInfo 的契约已按规则 239 核过(2026-09-30):
/// 客户端把 68699595 标成 CSharpOnly(RPCDeserializeAuto.lua:1153)
/// ⇒ Lua 侧没有明文 Reader,改走 C# 侧取证 ——
/// ux_game_all.dump.cs:22762 ClientBoardingInfo 字段布局 + 签名第二个参数
/// bool force。主线据此建了 ClientBoardingInfo4229938 /
/// SyncUnitBoardingInfo4229938 两个 DTO(不再手写字节),
/// 序列化结果与捐赠端 PoliceVehicle.cs:82-91 的手写字节逐字节相同,
/// 且已进 _ux_wire_check 做 53 B 回归断言。
///
internal sealed partial class GameRouter
{
private static PolicePrisonSettings EscortCfg => PrivateServerConfigStore.Current.Gameplay.PolicePrison;
private const string PoliceEscortStateKey = "client4229938-police-escort";
private sealed class PoliceEscortState
{
/// 坐在车上的犯人 uid;0 = 车上没人。
public ulong AgentUid;
public ulong VehicleUid;
public byte Seat;
}
private static PoliceEscortState EscortStateOf(TcpSession session)
{
if (session.Items.TryGetValue(PoliceEscortStateKey, out var raw) && raw is PoliceEscortState st)
return st;
st = new PoliceEscortState();
session.Items[PoliceEscortStateKey] = st;
return st;
}
///
/// SyncUnitBoardingInfo(68699595) 全包(info + force,53 B)。
/// 走 序列化,不再手写字节;
/// vehicle=0 ⇒ 解除挂载。
///
private static byte[] BoardingInfoWire(ulong agent, ulong vehicle, byte seat)
=> UxSerializer.Serialize(new GameMethods.SyncUnitBoardingInfo4229938
{
info = new GameMethods.ClientBoardingInfo4229938
{
EntityId = agent,
VehicleUId = vehicle,
SeatIndex = seat,
PositionOffset = default,
RotationOffset = default,
CanBeEjected = false,
UseSpecificAction = false,
ActionGroup = 0,
ActionId = 0,
},
force = true,
});
/// 发这两条绑定帧。 = false ⇒ 解除(VehicleUId=0)。
private static async Task SendPassengerBindingAsync(
TcpSession session, ulong agent, ulong vehicle, byte seat, bool board)
{
await session.NotifyAsync(MethodId.SyncUnitVehicleStatus, UxSerializer.Serialize(
new GameMethods.NewClientBoardingInfo4229938
{
EntityId = agent,
VehicleUId = vehicle,
SeatIndex = seat,
Status = board ? (byte)4 : (byte)0,
ExtInfo = null,
}), CancellationToken.None);
await session.NotifyAsync(MethodId.SyncUnitBoardingInfo,
BoardingInfoWire(agent, board ? vehicle : 0UL, seat), CancellationToken.None);
}
// ------------------------------------------------------------------ 押上车
///
/// 把 押上 的
/// 号座(0 = 主驾,犯人应该坐 1..SeatCount-1)。
///
internal static async Task<(bool Ok, string Message)> EscortBoardAsync(
TcpSession session, ulong agentUid, ulong vehicleUid, int seat)
{
if (!EscortCfg.Enabled)
return (false, "gameplay.policePrison.enabled=false(押送与牢房共用一个开关)");
if (agentUid == 0)
return (false, "请先填要押送的 NPC uid(用「生成一个 NPC 并盘查」刷一个)。");
var st = EscortStateOf(session);
if (st.AgentUid != 0)
return (false, $"车上已经有 uid={st.AgentUid}(座位 {st.Seat}),先「押下车」再换人。");
// 犯人必须在服务端账本里 —— 城里绝大多数路人是客户端本地实体,服务端点不了名。
if (!TryFindInLedger(session, agentUid, out var ledger, out _))
return (false, $"uid={agentUid} 不在服务端账本(多半是客户端本地路人),无法押送;"
+ "请用 GM 召唤的 NPC。");
VehicleRecord? vehicle;
var runtime = GetVehicleRuntimeState(session);
lock (runtime.SyncRoot)
runtime.Vehicles.TryGetValue(vehicleUid, out vehicle);
if (vehicle is null)
return (false, $"uid={vehicleUid} 不在服务端车辆账本里;请先用 GM 面板召唤一辆车再押送。");
if (!vehicle.Interactable)
return (false, $"车辆 {vehicleUid} 当前不可交互。");
if (seat <= 0 || seat >= vehicle.SeatCount)
return (false, $"座位号 {seat} 不合法(这车 {vehicle.SeatCount} 座;0 是主驾,犯人坐 1..{vehicle.SeatCount - 1})。");
lock (runtime.SyncRoot)
{
if (vehicle.SeatOccupants.ContainsKey((byte)seat) || vehicle.SeatReservations.ContainsKey((byte)seat))
return (false, $"座位 {seat} 已经被占了。");
vehicle.SeatOccupants[(byte)seat] = agentUid;
}
await SendPassengerBindingAsync(session, agentUid, vehicleUid, (byte)seat, board: true);
st.AgentUid = agentUid;
st.VehicleUid = vehicleUid;
st.Seat = (byte)seat;
session.Log.Info($"[POLICE-ESCORT] board agent={agentUid} ledger={ledger} vehicle={vehicleUid} seat={seat}");
return (true, $"已把 uid={agentUid}({ledger})押上 {vehicleUid} 的 {seat} 号座;"
+ "现在你自己开车去牢房,到了点「押下车」再「收押进牢房」。");
}
// ------------------------------------------------------------------ 押下车
/// 把车上的犯人押下来,放在车旁(捐赠端用 seat 奇偶决定左右 ±2 m)。
internal static async Task<(bool Ok, string Message)> EscortAlightAsync(TcpSession session)
{
if (!EscortCfg.Enabled)
return (false, "gameplay.policePrison.enabled=false(押送与牢房共用一个开关)");
var st = EscortStateOf(session);
if (st.AgentUid == 0)
return (false, "车上没有本次押送的犯人。");
var agent = st.AgentUid;
var vehicleUid = st.VehicleUid;
var seat = st.Seat;
VehicleRecord? vehicle;
var runtime = GetVehicleRuntimeState(session);
lock (runtime.SyncRoot)
runtime.Vehicles.TryGetValue(vehicleUid, out vehicle);
// ① 先解除挂载:捐赠端 PreparePoliceAlighting 的"handoff" ——
// VehicleUId=0 清掉 attach 句柄,再用 Status=5(Leaving)让原生动作接手。
await session.NotifyAsync(MethodId.SyncUnitBoardingInfo,
BoardingInfoWire(agent, 0, seat), CancellationToken.None);
await session.NotifyAsync(MethodId.SyncUnitVehicleStatus, UxSerializer.Serialize(
new GameMethods.NewClientBoardingInfo4229938
{
EntityId = agent, VehicleUId = vehicleUid, SeatIndex = seat,
Status = 5, ExtInfo = null,
}), CancellationToken.None);
// ② 落点:车旁 2 m(捐赠端 `p.Seat % 2 == 0 ? +2 : -2`),别把人留在车中心。
var pos = vehicle is not null
? new Vec3(vehicle.Position.X + (seat % 2 == 0 ? 2f : -2f), vehicle.Position.Y, vehicle.Position.Z)
: new Vec3(float.NaN, float.NaN, float.NaN);
if (float.IsFinite(pos.X))
{
await session.NotifyAsync(MethodId.SyncUnitPosition_P, UxSerializer.Serialize(
WorldCodec.PositionP(agent, pos, 0, WorldCodec.SetPositionType.Teleport)), CancellationToken.None);
RewriteLedgerPosition(session, agent, pos, stopWalking: true);
}
// ③ 清状态(先清再解绑,避免中断时残留"幽灵乘客")。
await SendPassengerBindingAsync(session, agent, vehicleUid, seat, board: false);
lock (runtime.SyncRoot)
{
if (runtime.Vehicles.TryGetValue(vehicleUid, out var v2))
v2.SeatOccupants.Remove(seat);
}
st.AgentUid = 0;
st.VehicleUid = 0;
st.Seat = 0;
session.Log.Info($"[POLICE-ESCORT] alight agent={agent} vehicle={vehicleUid} seat={seat} "
+ $"pos=({pos.X:F1},{pos.Y:F1},{pos.Z:F1})");
return (true, $"已把 uid={agent} 押下车,落在车旁 ({pos.X:F1},{pos.Z:F1});"
+ "现在可以点「收押进牢房」(玩家要在牢房门口 4 m 内)。");
}
/// 押送状态(给 GM 面板显示)。
internal static (bool Enabled, ulong Agent, ulong Vehicle, int Seat) EscortStatus(TcpSession session)
{
var st = EscortStateOf(session);
return (EscortCfg.Enabled, st.AgentUid, st.VehicleUid, st.Seat);
}
}