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

230 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 里希**押送**:把犯人押上警车、开车带到牢房、再押下来收押(2026-09-30 新增)。
///
/// <para>★ 捐赠端 v0.5-2 确实做得到,而且它用的两条帧<b>主线全都具备</b>:</para>
/// <list type="number">
/// <item><c>SyncUnitVehicleStatus</c>(68195719) —— <c>NewClientBoardingInfo</c>,
/// <c>Status</c>:4=已上车 / 5=正在下车 / 0=不在车上。主线
/// <c>GameRouter.VehicleStory.cs</c> 本来就在用它(玩家上下车);</item>
/// <item><c>SyncUnitBoardingInfo</c>(68699595) —— NPC 的<b>座位挂载句柄</b>
/// (<c>LogicAgentAttachVehicleHandle.Setup</c>:原生座位偏移/旋转,attach 句柄每帧跟随车辆)。
/// ⛑ <c>VehicleUId=0</c> 会<b>同时</b>解除挂载与原生座位注册 ⇒ 下车就用它。</item>
/// </list>
/// ⇒ 捐赠端 <c>GameRouter.PoliceVehicle.cs:93-103</c> 的 <c>SendPolicePassengerBinding4229938</c>
/// 就是这两条;主线 mid 一个在用、一个从未用过(全仓零调用点),
/// **不需要 GATE Lua 通道** ⇒ 这一条主线能做,跟牢房那步不一样。
///
/// <para><b>与捐赠端的差异(照旧:不做演出,做功能)</b>:</para>
/// <list type="bullet">
/// <item>捐赠端触发的是<b>原生押送过场</b>
/// (<c>gPoliceJobManager.cs:AskPoliceEscortInteract(agent, 20)</c> →
/// <c>SEscortToVehicleInteract</c>,映射表 <c>LixiPolice.cs:445</c> 的 <c>20 =&gt; 20001</c>),
/// 有"押着人走到车边、开门、塞进去"的动画;</item>
/// <item>主线直接发绑定帧 ⇒ <b>犯人直接出现在座位上</b>,没有过场动画。
/// 跟出租车/牢房同一个取舍:先要功能,动画等通道。</item>
/// </list>
///
/// <para>★ <c>SyncUnitBoardingInfo</c> 的契约<b>已按规则 239 核过</b>(2026-09-30):
/// 客户端把 68699595 标成 <b>CSharpOnly</b>(<c>RPCDeserializeAuto.lua:1153</c>)
/// ⇒ Lua 侧没有明文 Reader,改走 C# 侧取证 ——
/// <c>ux_game_all.dump.cs:22762 ClientBoardingInfo</c> 字段布局 + 签名第二个参数
/// <c>bool force</c>。主线据此建了 <c>ClientBoardingInfo4229938</c> /
/// <c>SyncUnitBoardingInfo4229938</c> 两个 DTO(不再手写字节),
/// 序列化结果与捐赠端 <c>PoliceVehicle.cs:82-91</c> 的手写字节<b>逐字节相同</b>,
/// 且已进 <c>_ux_wire_check</c> 做 53 B 回归断言。</para>
/// </summary>
internal sealed partial class GameRouter
{
private static PolicePrisonSettings EscortCfg => PrivateServerConfigStore.Current.Gameplay.PolicePrison;
private const string PoliceEscortStateKey = "client4229938-police-escort";
private sealed class PoliceEscortState
{
/// <summary>坐在车上的犯人 uid;0 = 车上没人。</summary>
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;
}
/// <summary>
/// <c>SyncUnitBoardingInfo</c>(68699595) 全包(<c>info</c> + <c>force</c>,53 B)。
/// 走 <see cref="GameMethods.SyncUnitBoardingInfo4229938"/> 序列化,不再手写字节;
/// <c>vehicle=0</c> ⇒ 解除挂载。
/// </summary>
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,
});
/// <summary>发这两条绑定帧。<paramref name="board"/> = false ⇒ 解除(<c>VehicleUId=0</c>)。</summary>
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);
}
// ------------------------------------------------------------------ 押上车
/// <summary>
/// 把 <paramref name="agentUid"/> 押上 <paramref name="vehicleUid"/> 的
/// <paramref name="seat"/> 号座(0 = 主驾,犯人应该坐 1..SeatCount-1)。
/// </summary>
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} 号座;"
+ "现在你自己开车去牢房,到了点「押下车」再「收押进牢房」。");
}
// ------------------------------------------------------------------ 押下车
/// <summary>把车上的犯人押下来,放在车旁(捐赠端用 seat 奇偶决定左右 ±2 m)。</summary>
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 内)。");
}
/// <summary>押送状态(给 GM 面板显示)。</summary>
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);
}
}