1009 lines
46 KiB
C#
1009 lines
46 KiB
C#
using Ananta.SDK.Logging;
|
||
using Ananta.SDK.Network;
|
||
using Ananta.SDK.Rpc;
|
||
using Ananta.SDK.Serialization;
|
||
using Ananta.Server.ClientData.Client4229938;
|
||
using Ananta.Server.Configuration;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 车库 / 停车(House Garage)—— 2026-09-30。
|
||
///
|
||
/// <para><b>用户报的症状(3 条)</b>:① 靠近车位按 F「调取载具」能开选车面板(纯客户端 UI),
|
||
/// 选完车后按 F「驶离车库」<b>毫无后续</b>;② 车的模型<b>不出现在车位</b>;③ 车库<b>大门不开</b>。</para>
|
||
///
|
||
/// <para><b>根因</b>(客户端 <c>GarageManager.lua</c> 逐行读出来的):</para>
|
||
/// <list type="number">
|
||
/// <item>⛔⛔ <b>2026-09-30 追加:真正的第一道墙在客户端配置里。</b>
|
||
/// <c>HouseConfig[11200003].GarageIndoorId</c> 官方写成了 <c>301</c>(= 绿屿公馆<b>主体</b>),
|
||
/// 真实车库 indoor 是 <c>302</c>(<c>IndoorConfig</c>:301 绿屿公馆 / 302 绿屿公馆车库)。
|
||
/// <c>ChangeIndoor</c> 建的是 <c>garageIndoorIdList[GarageIndoorId] = houseId</c>,
|
||
/// 而 <c>LoadingFinish</c> 用<b>当前</b> <c>gMapManager.IndoorId</c> 查它 ⇒ 站在车库里
|
||
/// (实测客户端只报 332 大厅 → 302 车库,<b>从不经过 301</b>)永远查不到 ⇒
|
||
/// <c>CurrentHouseId</c> 恒为 0 ⇒ <c>SwitchCarprotVehicle</c> 第一行
|
||
/// <c>parkinginfolist[0]</c> 为空直接 <c>FatalLog("换车失败,该房屋未购买或数据异常, houseId=0")</c>
|
||
/// + <c>return</c> ⇒ 连 <c>AskHouseMoveParkingSpace</c> 都发不出去(车不出、也没有「驶离车库」)。
|
||
/// 5 套房里另外 4 套的 <c>GarageIndoorId</c> 都等于各自车库 indoor(201/300/299/379),只有绿屿错。
|
||
/// <b>服务端改不了</b>(<c>CurrentHouseId</c> / <c>garageIndoorIdList</c> / <c>parkinginfolist</c>
|
||
/// 全是纯客户端状态,没有任何 RPC 能写;<c>HouseConfig</c> 是客户端自带表,服务端那份不下发)
|
||
/// ⇒ 走客户端 fastpatch <c>tools/garage_indoor_fix.lua</c>(<c>rawset(getmetatable(row),...)</c>,
|
||
/// 非 BepInEx)。本文件的所有 handler 只有在那一行修好之后才可能被调用。</item>
|
||
/// <item>这一族 9 条 RPC 在服务端一直是 <c>DefaultReturnCatalog4229938</c> 的
|
||
/// <c>shape="void"</c> 空 body 兜底 —— 客户端发出去的请求永远等不到有意义的回应。</item>
|
||
/// <item>两个关键 S2C <b>服务端从来不发</b>:
|
||
/// <c>SyncHouseParking</c>(64277819)(车位→车的清单,触发进库演出 + 车辆生成)
|
||
/// 和 <c>SyncHouseParkingEntityBinding</c>(64060781)(每辆车的 <c>EntityId</c>)。</item>
|
||
/// <item><c>LeaveGarage</c>(行 156) 第一步就读 <c>currentParkingInfo.ParkingVehicleUid[parkingId]</c>,
|
||
/// 而这个表<b>只能</b>由 <c>SyncHouseParkingEntityBinding</c> 填 ⇒ 为空 ⇒
|
||
/// 行 164 <c>FatalLog("离开车库","ParkingVehicleUid为空")</c> + 行 165 <c>return</c> ⇒
|
||
/// 连 <c>AskTeleportFromHouseGarage</c> 都没发出去 —— 这就是「按 F 没后续」。</item>
|
||
/// </list>
|
||
///
|
||
/// <para><b>本文件做的事</b>:把 9 条 RPC 全部接起来,按客户端 <c>GarageManager</c> 的期望回包:</para>
|
||
/// <list type="bullet">
|
||
/// <item><c>AskHouseParking</c> / <c>AskHouseMoveParkingSpace</c> / <c>GmHouseParking</c>:
|
||
/// 记账(<c>Progress.GarageParking</c>)+ 落盘 + 回推 <c>SyncHouseParking(addInfoList)</c>。
|
||
/// ⛔ <c>AskHouseMoveParkingSpace</c>(换车 / 调取载具)还要**自己 spawn 车 + 推
|
||
/// <c>SyncHouseParkingEntityBinding</c>** —— 客户端在这条链路上 <c>IsSwitchingCar=true</c>,
|
||
/// <c>SyncHouseParking</c> 的处理会跳过进库演出,因而永远不会自己发
|
||
/// <c>AskCreateHouseParkingVehicles</c>(详见 <c>EnsureParkingVehiclesAsync</c>)。
|
||
/// 而 <c>AskHouseParking</c>(开自己的车进库)不推 <c>SyncHouseParking</c>:客户端回调里自己会发
|
||
/// <c>AskCreateHouseParkingVehicles</c>(<c>GarageManager.lua:124</c>),再推一次会把进库演出播两遍。</item>
|
||
/// <item><c>AskCreateHouseParkingVehicles</c>:<b>命门</b> —— 为该房每个已指派车位
|
||
/// <c>SpawnDirectAsync</c> 出一辆车(<c>CreateSourceType = 10</c> HouseParking、
|
||
/// <c>destroyPrevious:false</c> 以便多车位共存),再回推
|
||
/// <c>SyncHouseParkingEntityBinding</c>(填 <c>ParkingVehicleUid</c>)。</item>
|
||
/// <item><c>AskDestroyHouseParkingVehicles</c>:出库销毁本房车辆。</item>
|
||
/// <item><c>AskTeleportToHouseGarage</c>:只回 OK(进库演出由客户端自己播)。</item>
|
||
/// <item><c>AskTeleportFromHouseGarage</c>:<b>开门</b>(复用 <c>PushDoorOpen</c> + 缓存的门 id)+ 回 OK。</item>
|
||
/// <item><c>AskHouseCancelParking</c> / <c>GmHouseCancelParking</c>:取消指派 + 回推 <c>SyncHouseParking(cancelInfoList)</c>。</item>
|
||
/// </list>
|
||
///
|
||
/// <para><b>车辆落点</b>:<c>HouseConfig.ParkingSpaceWaypoints</c> 是 <b>Spoon 航点 id</b>
|
||
/// (如 <c>1893051</c>),服务端没有 Spoon 位置表(<c>E:\ConfigDump</c> 里查不到),
|
||
/// 所以无法把车精确摆到车位格。退而求其次:<b>落在玩家当前位置</b>(进库演出刚把玩家放到车位,
|
||
/// 或玩家就站在车库里),前向偏移由 <c>gameplay.houseGarage.spawnForwardOffset</c> 热调。
|
||
/// 车一旦生成,客户端就能按 F 上车 / 驶离。</para>
|
||
///
|
||
/// <para><b>回滚</b>:<c>gameplay.houseGarage.enabled=false</c>(全部回空 body 兜底),
|
||
/// 或把 9 条 mid 从 <c>Enabled4229938MethodIds</c> 删掉。两者都与改动前逐字节一致。</para>
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
private const string GarageRuntimeKey = "client4229938-garage";
|
||
|
||
/// <summary>本会话运行时的车库实体表:<c>houseId → (车位号(0 基) → 车辆 entityId)</c>。</summary>
|
||
private sealed class GarageRuntimeState
|
||
{
|
||
public object SyncRoot { get; } = new();
|
||
public Dictionary<uint, Dictionary<int, ulong>> EntitiesByHouse { get; } = new();
|
||
|
||
/// <summary>
|
||
/// 最近一次车库 RPC 里出现的 <c>houseId</c>。给「出库传送」用:<c>AskTeleport(config=7)</c>
|
||
/// 的 <c>PreTeleportOption</c> 里<b>没有</b> houseId(客户端只填了 timeline 资源名),
|
||
/// 而它前面必定先来过一条带 houseId 的车库 RPC(<c>AskDestroyHouseParkingVehicles</c> 4B /
|
||
/// <c>AskCreateHouseParkingVehicles</c> 4B / <c>AskHouseMoveParkingSpace</c> 的元素里),
|
||
/// 所以在这里记一份。
|
||
/// </summary>
|
||
public uint LastHouseId;
|
||
}
|
||
|
||
/// <summary>记下最近一次车库事务的 houseId(出库传送要用来查落点)。</summary>
|
||
private static void RememberGarageHouse(TcpSession session, uint houseId)
|
||
{
|
||
if (houseId == 0)
|
||
return;
|
||
var runtime = GetGarageRuntime(session);
|
||
lock (runtime.SyncRoot)
|
||
runtime.LastHouseId = houseId;
|
||
}
|
||
|
||
/// <summary>本会话最近一次车库事务的 houseId;没有则 0。</summary>
|
||
private static uint LastGarageHouseId(TcpSession session)
|
||
{
|
||
var runtime = GetGarageRuntime(session);
|
||
lock (runtime.SyncRoot)
|
||
return runtime.LastHouseId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 玩家<b>此刻</b>正在开的车(<c>entityId</c>)。出库销毁必须跳过它 ——
|
||
/// 客户端一坐进车位上的车就会走「驶离车库」流程、发 <c>AskDestroyHouseParkingVehicles</c>,
|
||
/// 无差别销毁会把玩家正坐着的车一起拆掉(2026-09-30 实测:23:38:20 上车,
|
||
/// 同秒 <c>SyncDestroyVehicle</c>,模型当场消失、角色保持开车姿势悬空,4 秒后
|
||
/// <c>LeaveGarage</c> 再也找不到车辆实例 ⇒ 出库演出卡死 ⇒ 黑屏)。
|
||
///
|
||
/// <para>三个信号取并集,都在「当前」语义上:
|
||
/// <c>CurrentVehicleId</c>(<c>AskPlayerStartEnterOrExitVehicle</c> 写)、
|
||
/// <c>StoryEnterVehicleId</c>(剧情通道 <c>S011PlayerNetEnterVehicleRequest</c> 写,实测走的是这条)、
|
||
/// <c>Vehicles[id].ControllerPid == 本玩家 pid</c>。
|
||
/// <b>不用</b> <c>LastDrivenVehicleId</c>:它下车不清,会把早先停好的车误判成「正开着」。</para>
|
||
/// </summary>
|
||
private static HashSet<ulong> ResolveDrivenVehicleIds(TcpSession session)
|
||
{
|
||
var driven = new HashSet<ulong>();
|
||
var runtime = GetVehicleRuntimeState(session);
|
||
var pid = Profile.PlayerPid;
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
if (runtime.CurrentVehicleId != 0)
|
||
driven.Add(runtime.CurrentVehicleId);
|
||
if (runtime.StoryEnterVehicleId != 0)
|
||
driven.Add(runtime.StoryEnterVehicleId);
|
||
if (pid != 0)
|
||
{
|
||
foreach (var (id, rec) in runtime.Vehicles)
|
||
{
|
||
if (rec.ControllerPid == pid)
|
||
driven.Add(id);
|
||
}
|
||
}
|
||
}
|
||
return driven;
|
||
}
|
||
|
||
private static GarageRuntimeState GetGarageRuntime(TcpSession session)
|
||
{
|
||
if (session.Items.TryGetValue(GarageRuntimeKey, out var raw) && raw is GarageRuntimeState existing)
|
||
return existing;
|
||
|
||
var created = new GarageRuntimeState();
|
||
session.Items[GarageRuntimeKey] = created;
|
||
return created;
|
||
}
|
||
|
||
private static HouseGarageSettings GarageCfg => PrivateServerConfigStore.Current.Gameplay.HouseGarage;
|
||
|
||
// =========================================================================================
|
||
// 1) AskHouseParking(63154177) —— 「开着自己的车进车库」
|
||
// =========================================================================================
|
||
|
||
[Handler(MethodId.AskHouseParking, HandlerPacketKind.Invoke)]
|
||
private async Task OnAskHouseParking(Connection conn, UxRpcMessage msg)
|
||
{
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
if (!TryReadGarageArgs(conn, msg, out SceneMethods.AskHouseParkingArgs4229938 args))
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var world = GetStateIfExists(conn.Session);
|
||
if (world is null)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var added = new List<SceneMethods.HouseVehicleParkingInfo>();
|
||
lock (world.SyncRoot)
|
||
{
|
||
foreach (var item in args.parkingInfoList)
|
||
{
|
||
if (item.HouseId == 0 || item.VehicleId == 0)
|
||
continue;
|
||
|
||
var index = AssignFreeSpace(world, item.HouseId, item.VehicleId);
|
||
if (index < 0)
|
||
{
|
||
conn.Log.Warn($"[GARAGE] 入库失败:house={item.HouseId} 没有空车位(车={item.VehicleId})");
|
||
continue;
|
||
}
|
||
|
||
added.Add(new SceneMethods.HouseVehicleParkingInfo
|
||
{
|
||
HouseId = item.HouseId,
|
||
VehicleId = item.VehicleId,
|
||
ParkingSpaceIndex = index,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (added.Count > 0)
|
||
PlayerProgressStore.Save(world, conn.Log, quiet: true);
|
||
|
||
if (added.Count > 0)
|
||
RememberGarageHouse(conn.Session, added[0].HouseId);
|
||
|
||
// ⛔ 这里**不推** SyncHouseParking:客户端 AskHouseParking 的回调里自己会发
|
||
// AskCreateHouseParkingVehicles(见 GarageManager.lua:124),再推一次会把进库演出
|
||
// 触发两遍。落盘 + 回 OK 即可。
|
||
conn.Log.Info($"[GARAGE] AskHouseParking 入库 {added.Count} 辆 "
|
||
+ $"({string.Join(",", added.Select(a => $"house={a.HouseId}/space={a.ParkingSpaceIndex}/veh={a.VehicleId}"))})");
|
||
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
}
|
||
|
||
// =========================================================================================
|
||
// 2) AskHouseMoveParkingSpace(63780060) —— 「调取载具 / 换车」(用户场景的主链路)
|
||
// =========================================================================================
|
||
|
||
[Handler(MethodId.AskHouseMoveParkingSpace, HandlerPacketKind.Invoke)]
|
||
private async Task OnAskHouseMoveParkingSpace(Connection conn, UxRpcMessage msg)
|
||
{
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
if (!TryReadGarageArgs(conn, msg, out SceneMethods.AskHouseMoveParkingSpaceArgs4229938 args))
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var world = GetStateIfExists(conn.Session);
|
||
if (world is null)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var cancel = new List<SceneMethods.HouseVehicleParkingInfo>();
|
||
var add = new List<SceneMethods.HouseVehicleParkingInfo>();
|
||
|
||
// 落位后要确保「车位上有活着的车」的清单:(houseId, 车位号, 车辆, 这辆车在本房的旧车位)。
|
||
// 旧车位用来把运行时的实体搬过来复用 —— 换车位不该重新生成一辆车。
|
||
var wanted = new List<(uint HouseId, int Space, uint VehicleId, int PreviousSpace)>();
|
||
|
||
lock (world.SyncRoot)
|
||
{
|
||
foreach (var item in args.moveParkingSpaceInfoList)
|
||
{
|
||
if (item.HouseId == 0 || item.ParkingSpaceIndex < 0)
|
||
continue;
|
||
|
||
if (!world.Progress.GarageParking.TryGetValue(item.HouseId, out var spaces))
|
||
{
|
||
spaces = new Dictionary<int, uint>();
|
||
world.Progress.GarageParking[item.HouseId] = spaces;
|
||
}
|
||
|
||
if (item.VehicleId == 0)
|
||
{
|
||
// 清空某个车位。
|
||
if (spaces.TryGetValue(item.ParkingSpaceIndex, out var old) && old != 0)
|
||
{
|
||
spaces.Remove(item.ParkingSpaceIndex);
|
||
cancel.Add(new SceneMethods.HouseVehicleParkingInfo
|
||
{
|
||
HouseId = item.HouseId,
|
||
VehicleId = old,
|
||
ParkingSpaceIndex = item.ParkingSpaceIndex,
|
||
});
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
// 同一辆车先从这个房的其它车位挪走(避免一辆车占两格)。
|
||
var previousSpace = -1;
|
||
foreach (var key in spaces.Where(kv => kv.Value == item.VehicleId).Select(kv => kv.Key).ToList())
|
||
{
|
||
spaces.Remove(key);
|
||
previousSpace = key;
|
||
cancel.Add(new SceneMethods.HouseVehicleParkingInfo
|
||
{
|
||
HouseId = item.HouseId,
|
||
VehicleId = item.VehicleId,
|
||
ParkingSpaceIndex = key,
|
||
});
|
||
}
|
||
|
||
// 目标格若已有别的车,先请出去。
|
||
if (spaces.TryGetValue(item.ParkingSpaceIndex, out var occupant)
|
||
&& occupant != 0 && occupant != item.VehicleId)
|
||
{
|
||
spaces.Remove(item.ParkingSpaceIndex);
|
||
cancel.Add(new SceneMethods.HouseVehicleParkingInfo
|
||
{
|
||
HouseId = item.HouseId,
|
||
VehicleId = occupant,
|
||
ParkingSpaceIndex = item.ParkingSpaceIndex,
|
||
});
|
||
}
|
||
|
||
spaces[item.ParkingSpaceIndex] = item.VehicleId;
|
||
add.Add(new SceneMethods.HouseVehicleParkingInfo
|
||
{
|
||
HouseId = item.HouseId,
|
||
VehicleId = item.VehicleId,
|
||
ParkingSpaceIndex = item.ParkingSpaceIndex,
|
||
});
|
||
wanted.Add((item.HouseId, item.ParkingSpaceIndex, item.VehicleId, previousSpace));
|
||
}
|
||
}
|
||
|
||
PlayerProgressStore.Save(world, conn.Log, quiet: true);
|
||
if (wanted.Count > 0)
|
||
RememberGarageHouse(conn.Session, wanted[0].HouseId);
|
||
await PushSyncHouseParkingAsync(conn.Session, cancel, add, conn.Log);
|
||
|
||
// ⛔⛔ 这里**必须自己生成车**。客户端 SwitchCarprotVehicle 在发这条请求前就把
|
||
// IsSwitchingCar 置了 true,于是 SyncHouseParking 的处理里
|
||
// (GarageManager.lua:641 `if self.IsSwitchingCar then ... return`)会跳过进库演出,
|
||
// 也就永远不会发 AskCreateHouseParkingVehicles ⇒ 车位上有数据、但场上没有模型。
|
||
// 所以换车/调取载具这条链路,spawn + SyncHouseParkingEntityBinding 都得服务端自己来。
|
||
foreach (var group in wanted.GroupBy(w => w.HouseId))
|
||
{
|
||
var bindings = await EnsureParkingVehiclesAsync(
|
||
conn, group.Key, group.Select(w => (w.Space, w.VehicleId, w.PreviousSpace)).ToList());
|
||
|
||
if (bindings.Count > 0)
|
||
await PushSyncHouseParkingEntityBindingAsync(conn.Session, bindings, conn.Log);
|
||
}
|
||
|
||
conn.Log.Info($"[GARAGE] AskHouseMoveParkingSpace 取消={cancel.Count} 新增={add.Count}");
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
}
|
||
|
||
// =========================================================================================
|
||
// 3) AskHouseCancelParking(63495236) / GmHouseCancelParking(65601170) —— 按车辆 id 取消
|
||
// =========================================================================================
|
||
|
||
[Handler(MethodId.AskHouseCancelParking, HandlerPacketKind.Invoke)]
|
||
private async Task OnAskHouseCancelParking(Connection conn, UxRpcMessage msg)
|
||
{
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
if (!TryReadGarageArgs(conn, msg, out SceneMethods.AskHouseCancelParkingArgs4229938 args))
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var cancel = CancelVehicles(conn, args.vehicleIdList);
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
conn.Log.Info($"[GARAGE] AskHouseCancelParking 取消={cancel.Count}");
|
||
}
|
||
|
||
[Handler(MethodId.GmHouseCancelParking, HandlerPacketKind.Invoke)]
|
||
private async Task OnGmHouseCancelParking(Connection conn, UxRpcMessage msg)
|
||
{
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
if (!TryReadGarageArgs(conn, msg, out SceneMethods.GmHouseCancelParkingArgs4229938 args))
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var cancel = CancelVehicles(conn, args.vehicleIdList);
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
conn.Log.Info($"[GARAGE] GmHouseCancelParking 取消={cancel.Count}");
|
||
}
|
||
|
||
// =========================================================================================
|
||
// 4) GmHouseParking(65027125) —— GM:把 houseIdList[i] 的车 vehicleIdList[i] 停进去
|
||
// =========================================================================================
|
||
|
||
[Handler(MethodId.GmHouseParking, HandlerPacketKind.Invoke)]
|
||
private async Task OnGmHouseParking(Connection conn, UxRpcMessage msg)
|
||
{
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
if (!TryReadGarageArgs(conn, msg, out SceneMethods.GmHouseParkingArgs4229938 args))
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var world = GetStateIfExists(conn.Session);
|
||
if (world is null)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var add = new List<SceneMethods.HouseVehicleParkingInfo>();
|
||
lock (world.SyncRoot)
|
||
{
|
||
var count = Math.Min(args.houseIdList.Count, args.vehicleIdList.Count);
|
||
for (var i = 0; i < count; i++)
|
||
{
|
||
var houseId = args.houseIdList[i];
|
||
var vehicleId = args.vehicleIdList[i];
|
||
if (houseId == 0 || vehicleId == 0)
|
||
continue;
|
||
|
||
var index = AssignFreeSpace(world, houseId, vehicleId);
|
||
if (index < 0)
|
||
continue;
|
||
|
||
add.Add(new SceneMethods.HouseVehicleParkingInfo
|
||
{
|
||
HouseId = houseId,
|
||
VehicleId = vehicleId,
|
||
ParkingSpaceIndex = index,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (add.Count > 0)
|
||
{
|
||
PlayerProgressStore.Save(world, conn.Log, quiet: true);
|
||
await PushSyncHouseParkingAsync(conn.Session, [], add, conn.Log);
|
||
}
|
||
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
conn.Log.Info($"[GARAGE] GmHouseParking 新增={add.Count}");
|
||
}
|
||
|
||
// =========================================================================================
|
||
// 5) AskCreateHouseParkingVehicles(63479109) —— 命门:spawn 车 + 回填 EntityId
|
||
// =========================================================================================
|
||
|
||
[Handler(MethodId.AskCreateHouseParkingVehicles, HandlerPacketKind.Invoke)]
|
||
private async Task OnAskCreateHouseParkingVehicles(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var houseId = msg.Body.Length >= 4 ? BitConverter.ToUInt32(msg.Body, 0) : 0u;
|
||
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var world = GetStateIfExists(conn.Session);
|
||
if (world is null || houseId == 0)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
RememberGarageHouse(conn.Session, houseId);
|
||
|
||
Dictionary<int, uint>? spaces = null;
|
||
lock (world.SyncRoot)
|
||
{
|
||
if (world.Progress.GarageParking.TryGetValue(houseId, out var stored) && stored.Count > 0)
|
||
spaces = new Dictionary<int, uint>(stored);
|
||
}
|
||
|
||
if (spaces is null)
|
||
{
|
||
conn.Log.Info($"[GARAGE] AskCreateHouseParkingVehicles house={houseId} 无车位指派 ⇒ 不生成车");
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var wanted = spaces.OrderBy(kv => kv.Key)
|
||
.Select(kv => (kv.Key, kv.Value, -1))
|
||
.ToList();
|
||
|
||
var binding = await EnsureParkingVehiclesAsync(conn, houseId, wanted);
|
||
|
||
if (binding.Count > 0)
|
||
await PushSyncHouseParkingEntityBindingAsync(conn.Session, binding, conn.Log);
|
||
|
||
conn.Log.Info($"[GARAGE] AskCreateHouseParkingVehicles house={houseId} 生成/回填 {binding.Count} 辆");
|
||
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
}
|
||
|
||
// =========================================================================================
|
||
// 6) AskDestroyHouseParkingVehicles(63266373) —— 出库销毁本房车辆
|
||
// =========================================================================================
|
||
|
||
[Handler(MethodId.AskDestroyHouseParkingVehicles, HandlerPacketKind.Invoke)]
|
||
private async Task OnAskDestroyHouseParkingVehicles(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var houseId = msg.Body.Length >= 4 ? BitConverter.ToUInt32(msg.Body, 0) : 0u;
|
||
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var runtime = GetGarageRuntime(conn.Session);
|
||
List<ulong>? entities = null;
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
runtime.LastHouseId = houseId != 0 ? houseId : runtime.LastHouseId;
|
||
if (runtime.EntitiesByHouse.TryGetValue(houseId, out var map))
|
||
{
|
||
entities = map.Values.Where(id => id != 0).Distinct().ToList();
|
||
// ⛔ 只清「车位 → 实体」映射,**不销毁**玩家正开着的那辆车。
|
||
// 被跳过的那辆保持映射,下次进库 EnsureParkingVehiclesAsync 会复用它,
|
||
// 不会重复生成。详见 ResolveDrivenVehicleIds 的注释。
|
||
var driven = ResolveDrivenVehicleIds(conn.Session);
|
||
foreach (var (space, id) in map.Where(kv => driven.Contains(kv.Value)).ToList())
|
||
{
|
||
map.Remove(space);
|
||
conn.Log.Info($"[GARAGE] 出库保留玩家正开的车 entity={id} house={houseId} space={space}(不销毁)");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ⛑ await 不能写在 lock 体内(CS1996)—— 先出锁再销毁。
|
||
if (entities is null)
|
||
{
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
// 玩家正开着的车不销毁:客户端一坐进车位上的车就会走「驶离车库」流程并发这条 RPC,
|
||
// 无差别销毁会把玩家正坐着的车一起拆掉 ⇒ 模型当场消失、角色保持开车姿势悬空
|
||
// (2026-09-30 23:38:20 实测),随后 LeaveGarage 找不到车辆实例 ⇒ 出库演出卡死黑屏。
|
||
var keep = ResolveDrivenVehicleIds(conn.Session);
|
||
var destroyed = 0;
|
||
var vehicleRuntime = GetVehicleRuntimeState(conn.Session);
|
||
foreach (var entityId in entities)
|
||
{
|
||
if (keep.Contains(entityId))
|
||
{
|
||
conn.Log.Info($"[GARAGE] 出库跳过销毁 entity={entityId}(玩家正开着)");
|
||
continue;
|
||
}
|
||
|
||
await DestroyVehicleAsync(conn.Session, vehicleRuntime, entityId, $"garage-exit house={houseId}");
|
||
destroyed++;
|
||
}
|
||
|
||
conn.Log.Info($"[GARAGE] AskDestroyHouseParkingVehicles house={houseId} 销毁 {destroyed}/{entities.Count} 辆"
|
||
+ $"(保留玩家正开的 {entities.Count - destroyed} 辆)");
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
}
|
||
|
||
// =========================================================================================
|
||
// 7) AskTeleportToHouseGarage(63099218) —— 进库(演出客户端自己播,回 OK 即可)
|
||
// =========================================================================================
|
||
|
||
[Handler(MethodId.AskTeleportToHouseGarage, HandlerPacketKind.Invoke)]
|
||
private async Task OnAskTeleportToHouseGarage(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var houseId = msg.Body.Length >= 4 ? BitConverter.ToUInt32(msg.Body, 0) : 0u;
|
||
var spaceIndex = msg.Body.Length >= 8 ? BitConverter.ToInt32(msg.Body, 4) : -1;
|
||
|
||
RememberGarageHouse(conn.Session, houseId);
|
||
conn.Log.Info($"[GARAGE] AskTeleportToHouseGarage house={houseId} space={spaceIndex} ⇒ ok");
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
}
|
||
|
||
// =========================================================================================
|
||
// 8) AskTeleportFromHouseGarage(63203581) —— 出库:开门 + **推 SyncTeleport 落点** + 回 OK
|
||
// =========================================================================================
|
||
|
||
/// <summary>
|
||
/// 一趟出库事务待落的点(写于 <see cref="OnAskTeleportFromHouseGarage"/>,
|
||
/// 消费于 <c>ReportPostTeleportFinish</c> 的硬设兜底 —— 与景点电梯四修同一个套路)。
|
||
/// </summary>
|
||
internal sealed record PendingGarageLanding(
|
||
uint HouseId, float X, float Y, float Z, float Facing, ulong VehicleId);
|
||
|
||
internal const string PendingGarageLandingKey = "client4229938-garage-landing";
|
||
|
||
[Handler(MethodId.AskTeleportFromHouseGarage, HandlerPacketKind.Invoke)]
|
||
private async Task OnAskTeleportFromHouseGarage(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var houseId = msg.Body.Length >= 4 ? BitConverter.ToUInt32(msg.Body, 0) : 0u;
|
||
var spaceIndex = msg.Body.Length >= 8 ? BitConverter.ToInt32(msg.Body, 4) : -1;
|
||
var vehicleConfigId = msg.Body.Length >= 12 ? BitConverter.ToUInt32(msg.Body, 8) : 0u;
|
||
|
||
RememberGarageHouse(conn.Session, houseId);
|
||
|
||
if (GarageCfg.Enabled && GarageCfg.OpenDoorOnExit)
|
||
OpenCachedGarageDoor(conn);
|
||
|
||
// ⛔⛔ 这里**必须推一帧 SyncTeleport**,否则就是黑屏。
|
||
// 客户端 LeaveGarage 走的是原生 loading 事务:
|
||
// AskTeleport(config=7, beforeResName=loading_garage_out*) ← 播离库过场
|
||
// → ReportPreTeleportFinish
|
||
// → AskTeleportFromHouseGarage(本方法)
|
||
// 而它的回调(GarageManager.lua PlayLoadingCutscene 内)**只在失败时**才
|
||
// FatalLog + StopLoading,成功分支什么都不做 —— 也就是说「结束加载画面」
|
||
// 这件事完全交给服务端。原实现只 ReturnEmptyOkAsync ⇒ 客户端永远停在
|
||
// loading ⇒ 过场播完就黑屏(2026-09-30 23:38 实测:23:38:25 收到本 RPC 并 ack,
|
||
// 之后再没有 ReportPostTeleportFinish,indoor 于 23:38:27 归零后一直黑)。
|
||
// 与景点电梯「一修」是同一个根因,同一个修法。
|
||
if (!GarageCfg.Enabled)
|
||
{
|
||
conn.Log.Info($"[GARAGE] AskTeleportFromHouseGarage house={houseId} space={spaceIndex} "
|
||
+ "⇒ ok(enabled=false,不推 SyncTeleport,逐字节等于改动前)");
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
return;
|
||
}
|
||
|
||
var landing = ResolveGarageExitLanding(houseId, conn.Log);
|
||
var state = GetStateIfExists(conn.Session);
|
||
ulong teleportId = 0;
|
||
if (state is not null)
|
||
{
|
||
lock (state.SyncRoot)
|
||
{
|
||
teleportId = state.PendingTeleportId;
|
||
state.PendingTeleportSyncSent = true;
|
||
state.LastReportedPlayerPosition = new Vec3(landing.X, landing.Y, landing.Z);
|
||
state.LastReportedPlayerRotation = new Vec3(0f, landing.Facing, 0f);
|
||
state.HasLastReportedPlayerTransform = true;
|
||
}
|
||
}
|
||
|
||
var driven = ResolveDrivenVehicleIds(conn.Session);
|
||
var vehicleId = driven.Count > 0 ? driven.First() : 0UL;
|
||
|
||
// ⛑ 先 ack、再推 —— 与 donor 在改装店验证过的顺序一致
|
||
// (GameRouter.Teleport.cs 的 AskTeleportToLeavingWaypoint:ReturnEmptyOkAsync 之后
|
||
// 才 PushPlayerAndVehicleAsync)。ack 让客户端加载流程走到「等落点」那一步,
|
||
// 随后推的 SyncTeleport 才会被采纳;反序会把 SyncTeleport 吃在流程还没就位的时候。
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
|
||
await conn.NotifyAsync(MethodId.SyncTeleport, new GameMethods.TeleportOption
|
||
{
|
||
teleportId = teleportId,
|
||
Position = new GameMethods.TeleportVec3(landing.X, landing.Y, landing.Z),
|
||
Facing = landing.Facing,
|
||
IsSwitchScene = false,
|
||
WaitTaskResource = false,
|
||
MapEntranceId = landing.EntranceId,
|
||
});
|
||
|
||
if (vehicleId != 0)
|
||
{
|
||
await conn.NotifyAsync(MethodId.SyncTeleportVehicle, new SceneMethods.SyncTeleportVehicle4229938
|
||
{
|
||
EntityId = vehicleId,
|
||
Position = new SceneMethods.UxVector3(landing.X, landing.Y, landing.Z),
|
||
Rotation = new SceneMethods.UxVector3(0f, landing.Facing, 0f),
|
||
Velocity = 0f,
|
||
Reset = true,
|
||
MoveToken = 0,
|
||
});
|
||
}
|
||
|
||
// 交给 ReportPostTeleportFinish 的硬设兜底(客户端可能不采纳 SyncTeleport 的落点,
|
||
// 见景点电梯四修)。新事务开始前会被覆盖。
|
||
conn.Session.Items[PendingGarageLandingKey] =
|
||
new PendingGarageLanding(houseId, landing.X, landing.Y, landing.Z, landing.Facing, vehicleId);
|
||
|
||
conn.Log.Info($"[GARAGE] AskTeleportFromHouseGarage house={houseId} space={spaceIndex} "
|
||
+ $"veh={vehicleConfigId} ⇒ SyncTeleport 落点=({landing.X:F1},{landing.Y:F1},{landing.Z:F1}) "
|
||
+ $"facing={landing.Facing:F0} 门={landing.EntranceId}({landing.Source}) 车={vehicleId} teleportId={teleportId}");
|
||
}
|
||
|
||
/// <summary>出库落点 + 该填进 <c>SyncTeleport.MapEntranceId</c> 的入口 id。</summary>
|
||
private readonly record struct GarageExitLanding(
|
||
float X, float Y, float Z, float Facing, uint EntranceId, string Source);
|
||
|
||
/// <summary>
|
||
/// 出库落点推导,优先级:配置 <c>exitLandingOverrides</c> >
|
||
/// <c>HouseConfig.VisitorExit</c>(绝对坐标)> 该房 <c>MapentranceConfig</c> 那一行 >
|
||
/// 玩家当前上报位置(原地,宁可不动也不能黑屏)。
|
||
///
|
||
/// <para>为什么不能用客户端自己那份:出库落点客户端取的是
|
||
/// <c>HouseConfig.GarageOutWaypoint</c>(<b>Spoon 航点 id</b>,服务端没有 Spoon 位置表),
|
||
/// 而且 <c>ExitTimeline</c> 只有 1 个元素的房(素光小筑)连 Spoon 反查都不做 ⇒
|
||
/// <c>afterPosition</c> 全零。</para>
|
||
/// </summary>
|
||
private static GarageExitLanding ResolveGarageExitLanding(uint houseId, ServerLogger log)
|
||
{
|
||
uint entranceId = 0;
|
||
|
||
if (HouseCatalog4229938.TryGet(houseId, out var house) && house is not null)
|
||
{
|
||
entranceId = house.MapEntrance;
|
||
|
||
foreach (var o in GarageCfg.ExitLandingOverrides)
|
||
{
|
||
if (o.HouseId == houseId)
|
||
{
|
||
log.Info($"[GARAGE] 出库落点=配置覆盖 house={houseId} ({o.X:F1},{o.Y:F1},{o.Z:F1}) facing={o.Facing:F0}");
|
||
return new GarageExitLanding(o.X, o.Y, o.Z, o.Facing, entranceId, "override");
|
||
}
|
||
}
|
||
|
||
if (house.VisitorExit.IsUsable)
|
||
{
|
||
log.Info($"[GARAGE] 出库落点=HouseConfig.VisitorExit house={houseId} "
|
||
+ $"({house.VisitorExit.X:F1},{house.VisitorExit.Y:F1},{house.VisitorExit.Z:F1}) "
|
||
+ $"facing={house.VisitorExit.Facing:F0} 门={entranceId}");
|
||
return new GarageExitLanding(house.VisitorExit.X, house.VisitorExit.Y, house.VisitorExit.Z,
|
||
house.VisitorExit.Facing, entranceId, "VisitorExit");
|
||
}
|
||
}
|
||
|
||
if (entranceId != 0)
|
||
{
|
||
foreach (var e in SceneCatalog4229938.MapEntrances)
|
||
{
|
||
if (e.Id != entranceId)
|
||
continue;
|
||
log.Info($"[GARAGE] 出库落点=MapentranceConfig house={houseId} 门={entranceId} "
|
||
+ $"({e.X:F1},{e.Y:F1},{e.Z:F1}) facing={e.Facing:F0}");
|
||
return new GarageExitLanding(e.X, e.Y, e.Z, e.Facing, entranceId, "MapentranceConfig");
|
||
}
|
||
}
|
||
|
||
log.Warn($"[GARAGE] 出库落点解析失败 house={houseId}(VisitorExit 全零且门 {entranceId} 不在表里)⇒ 退化成原地");
|
||
return new GarageExitLanding(0f, 0f, 0f, 0f, entranceId, "none");
|
||
}
|
||
|
||
// =========================================================================================
|
||
// helpers
|
||
// =========================================================================================
|
||
|
||
/// <summary>
|
||
/// 确保「本房这些车位」上各有一辆活着的车,并返回可直接推给客户端的
|
||
/// <c>SyncHouseParkingEntityBinding</c> 列表。
|
||
///
|
||
/// <para>复用顺序:① 同一辆车换车位(<c>PreviousSpace >= 0</c>)时把运行时里旧车位的实体搬过来,
|
||
/// 不重新生成;② 目标车位已有活着的实体直接复用;③ 都没有才 <c>SpawnDirectAsync</c>。</para>
|
||
///
|
||
/// <para>两条链路都用它:<c>AskCreateHouseParkingVehicles</c>(进库演出结束后客户端主动要)
|
||
/// 和 <c>AskHouseMoveParkingSpace</c>(换车 / 调取载具 —— 那条链路上客户端因为
|
||
/// <c>IsSwitchingCar</c> 不会自己发前者,所以服务端必须自己生成)。</para>
|
||
/// </summary>
|
||
private async Task<List<SceneMethods.HouseParkingEntityBinding>> EnsureParkingVehiclesAsync(
|
||
Connection conn,
|
||
uint houseId,
|
||
IReadOnlyList<(int Space, uint VehicleId, int PreviousSpace)> wanted)
|
||
{
|
||
var bindings = new List<SceneMethods.HouseParkingEntityBinding>();
|
||
if (wanted.Count == 0)
|
||
return bindings;
|
||
|
||
var state = GetStateStatic(conn.Session);
|
||
var anchor = GarageSpawnPoint(state, GarageCfg.SpawnForwardOffset);
|
||
var facing = state.LastReportedPlayerRotation.Y;
|
||
var runtime = GetGarageRuntime(conn.Session);
|
||
|
||
foreach (var (space, vehicleId, previousSpace) in wanted)
|
||
{
|
||
if (vehicleId == 0)
|
||
continue;
|
||
|
||
ulong entity = 0;
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
if (!runtime.EntitiesByHouse.TryGetValue(houseId, out var map))
|
||
{
|
||
map = new Dictionary<int, ulong>();
|
||
runtime.EntitiesByHouse[houseId] = map;
|
||
}
|
||
|
||
// ① 同一辆车换车位:旧车位的实体搬过来。
|
||
if (previousSpace >= 0 && previousSpace != space
|
||
&& map.TryGetValue(previousSpace, out var moved) && moved != 0)
|
||
{
|
||
map.Remove(previousSpace);
|
||
entity = moved;
|
||
}
|
||
// ② 目标车位已有活着的车(客户端会多次调这条,必须幂等)。
|
||
else if (map.TryGetValue(space, out var existing) && existing != 0
|
||
&& IsVehicleAlive(conn.Session, existing))
|
||
{
|
||
entity = existing;
|
||
}
|
||
else
|
||
{
|
||
// ②b 这辆车玩家**正开着**(开出车库后又开回来入库)。
|
||
// 出库时刻意没销毁它(见 OnAskDestroyHouseParkingVehicles),实体还在
|
||
// 世界里活着 ⇒ 直接复用,否则同一个 vehicleConfigId 会有两份实体。
|
||
var drivenEntity = FindDrivenVehicleEntity(conn.Session, vehicleId);
|
||
if (drivenEntity != 0)
|
||
{
|
||
entity = drivenEntity;
|
||
conn.Log.Info($"[GARAGE] 复用玩家正开的车 entity={drivenEntity} veh={vehicleId} "
|
||
+ $"house={houseId} space={space}(不重复生成)");
|
||
}
|
||
}
|
||
|
||
if (entity != 0)
|
||
map[space] = entity;
|
||
}
|
||
|
||
if (entity == 0)
|
||
{
|
||
// ⛑ await 不能在 lock 体内(CS1996)—— 上面已经出锁了。
|
||
var (ok, message, spawned, _) = await SpawnDirectAsync(
|
||
conn.Session, vehicleId, anchor, facing,
|
||
$"garage house={houseId} space={space}", anchor,
|
||
createSourceType: 10, destroyPrevious: false);
|
||
|
||
if (!ok)
|
||
{
|
||
conn.Log.Warn($"[GARAGE] 生成车失败 house={houseId} space={space} veh={vehicleId}: {message}");
|
||
continue;
|
||
}
|
||
|
||
entity = spawned;
|
||
lock (runtime.SyncRoot)
|
||
runtime.EntitiesByHouse[houseId][space] = entity;
|
||
}
|
||
|
||
bindings.Add(new SceneMethods.HouseParkingEntityBinding
|
||
{
|
||
VehicleId = vehicleId,
|
||
EntityId = entity,
|
||
ParkingSpaceIndex = space,
|
||
});
|
||
}
|
||
|
||
return bindings;
|
||
}
|
||
|
||
private static bool TryReadGarageArgs<T>(Connection conn, UxRpcMessage msg, out T args)
|
||
{
|
||
try
|
||
{
|
||
args = UxSerializer.Deserialize<T>(msg.Body);
|
||
return args is not null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
conn.Log.Warn($"[GARAGE] {typeof(T).Name} 解析失败 ({ex.GetType().Name}: {ex.Message}) "
|
||
+ $"bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}");
|
||
args = default!;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>把 <paramref name="vehicleId"/> 放进该房第一个空车位,返回车位号(0 基);无空位返回 -1。
|
||
/// 该车已在本房某个车位时返回原位置(幂等)。调用方必须持有 <c>world.SyncRoot</c>。</summary>
|
||
private static int AssignFreeSpace(WorldEntryState world, uint houseId, uint vehicleId)
|
||
{
|
||
var count = 2u;
|
||
if (HouseCatalog4229938.TryGet(houseId, out var house) && house.ParkingSpaceCount > 0)
|
||
count = house.ParkingSpaceCount;
|
||
|
||
if (!world.Progress.GarageParking.TryGetValue(houseId, out var spaces))
|
||
{
|
||
spaces = new Dictionary<int, uint>();
|
||
world.Progress.GarageParking[houseId] = spaces;
|
||
}
|
||
|
||
foreach (var (index, existing) in spaces)
|
||
{
|
||
if (existing == vehicleId)
|
||
return index;
|
||
}
|
||
|
||
for (var i = 0; i < count; i++)
|
||
{
|
||
if (!spaces.ContainsKey(i))
|
||
{
|
||
spaces[i] = vehicleId;
|
||
return i;
|
||
}
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
/// <summary>按车辆 id 取消车位指派(跨所有房),落盘并回推 cancelInfoList。</summary>
|
||
private static List<SceneMethods.HouseVehicleParkingInfo> CancelVehicles(Connection conn, List<uint> vehicleIds)
|
||
{
|
||
var cancel = new List<SceneMethods.HouseVehicleParkingInfo>();
|
||
var world = GetStateIfExists(conn.Session);
|
||
if (world is null || vehicleIds.Count == 0)
|
||
return cancel;
|
||
|
||
var wanted = vehicleIds.Where(id => id != 0).ToHashSet();
|
||
lock (world.SyncRoot)
|
||
{
|
||
foreach (var (houseId, spaces) in world.Progress.GarageParking)
|
||
{
|
||
foreach (var key in spaces.Where(kv => wanted.Contains(kv.Value)).Select(kv => kv.Key).ToList())
|
||
{
|
||
var vid = spaces[key];
|
||
spaces.Remove(key);
|
||
cancel.Add(new SceneMethods.HouseVehicleParkingInfo
|
||
{
|
||
HouseId = houseId,
|
||
VehicleId = vid,
|
||
ParkingSpaceIndex = key,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (cancel.Count > 0)
|
||
{
|
||
PlayerProgressStore.Save(world, conn.Log, quiet: true);
|
||
_ = PushSyncHouseParkingAsync(conn.Session, cancel, [], conn.Log);
|
||
}
|
||
|
||
return cancel;
|
||
}
|
||
|
||
private static async Task PushSyncHouseParkingAsync(
|
||
TcpSession session,
|
||
List<SceneMethods.HouseVehicleParkingInfo> cancel,
|
||
List<SceneMethods.HouseVehicleParkingInfo> add,
|
||
ServerLogger log)
|
||
{
|
||
var args = new SceneMethods.SyncHouseParkingArgs4229938
|
||
{
|
||
cancelInfoList = cancel,
|
||
addInfoList = add,
|
||
};
|
||
var body = UxSerializer.Serialize(args);
|
||
await session.NotifyAsync(MethodId.SyncHouseParking, body, CancellationToken.None);
|
||
log.Info($"[GARAGE] -> SyncHouseParking(64277819) cancel={cancel.Count} add={add.Count} body={body.Length}B");
|
||
}
|
||
|
||
private static async Task PushSyncHouseParkingEntityBindingAsync(
|
||
TcpSession session,
|
||
List<SceneMethods.HouseParkingEntityBinding> binding,
|
||
ServerLogger log)
|
||
{
|
||
var args = new SceneMethods.SyncHouseParkingEntityBindingArgs4229938 { bindingList = binding };
|
||
var body = UxSerializer.Serialize(args);
|
||
await session.NotifyAsync(MethodId.IGameToClient_SyncHouseParkingEntityBinding, body, CancellationToken.None);
|
||
log.Info($"[GARAGE] -> SyncHouseParkingEntityBinding(64060781) n={binding.Count} body={body.Length}B");
|
||
}
|
||
|
||
/// <summary>车辆落点:玩家当前位置(+ 配置的前向偏移)。</summary>
|
||
private static Vec3 GarageSpawnPoint(WorldEntryState state, float forwardOffset)
|
||
{
|
||
var p = state.LastReportedPlayerPosition;
|
||
if (forwardOffset == 0f)
|
||
return p;
|
||
|
||
var rad = state.LastReportedPlayerRotation.Y * (MathF.PI / 180f);
|
||
return new Vec3(p.X + MathF.Sin(rad) * forwardOffset, p.Y, p.Z + MathF.Cos(rad) * forwardOffset);
|
||
}
|
||
|
||
private static bool IsVehicleAlive(TcpSession session, ulong entityId)
|
||
{
|
||
var runtime = GetVehicleRuntimeState(session);
|
||
lock (runtime.SyncRoot)
|
||
return runtime.Vehicles.ContainsKey(entityId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 本玩家<b>正开着</b>的、<c>VehicleRecord.ConfigId == vehicleConfigId</c> 的那个实体;没有则 0。
|
||
///
|
||
/// <para>用途:玩家把车库里的车开出去、又开回来入库时复用同一个实体
|
||
/// (<c>EnsureParkingVehiclesAsync</c> 的步骤 ②b)。出库时刻意不销毁玩家正开的车,
|
||
/// 所以它仍在世界里活着;不复用就会给同一辆车生成第二份实体。</para>
|
||
///
|
||
/// <para>锁序:调用方持有 <c>GarageRuntimeState.SyncRoot</c> 时本方法会再取
|
||
/// <c>VehicleRuntimeState.SyncRoot</c> ⇒ 顺序恒为 Garage → Vehicle(与
|
||
/// <c>OnAskDestroyHouseParkingVehicles</c> 一致),不会反向嵌套。</para>
|
||
/// </summary>
|
||
private static ulong FindDrivenVehicleEntity(TcpSession session, uint vehicleConfigId)
|
||
{
|
||
if (vehicleConfigId == 0)
|
||
return 0;
|
||
|
||
var driven = ResolveDrivenVehicleIds(session);
|
||
if (driven.Count == 0)
|
||
return 0;
|
||
|
||
var runtime = GetVehicleRuntimeState(session);
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
foreach (var id in driven)
|
||
{
|
||
if (runtime.Vehicles.TryGetValue(id, out var rec) && rec.ConfigId == vehicleConfigId)
|
||
return id;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
}
|