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