230 lines
11 KiB
C#
230 lines
11 KiB
C#
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 => 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);
|
||
}
|
||
}
|