This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

229 lines
11 KiB
C#
Raw Permalink Normal View History

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);
}
}