411 lines
18 KiB
C#
411 lines
18 KiB
C#
using System.Buffers.Binary;
|
||||
|
|
using System.Collections.Concurrent;
|
|||
|
|
using Ananta.SDK.Network;
|
|||
|
|
using Ananta.SDK.Rpc;
|
|||
|
|
using Ananta.Server.Configuration;
|
|||
|
|
using Ananta.Server.Protocol.Client4229938;
|
|||
|
|
using Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
|||
|
|
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
|
|||
|
|
|
|||
|
|
namespace Ananta.Server.Handlers.Game;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Doors as gadgets — T2-11, ported from the donor's proven rounds 16C-16I.
|
|||
|
|
///
|
|||
|
|
/// Doors are PLAIN GADGETS carrying a <c>DoorModuleSyncInfo</c> module ("USER MODEL ADOPTED:
|
|||
|
|
/// DOORS ARE GADGETS", HANDOFF-2026-09-18 round 16I-7). The client reports crossings with
|
|||
|
|
/// <c>AskPlayerEnterOrLeaveDoor(63525373)</c> = (ulong gadgetId, bool isEnter), dump.cs:1893406.
|
|||
|
|
///
|
|||
|
|
/// ⛔ WIRE KIND: the client sends this as a NOTIFY, not an invoke — packets log shows
|
|||
|
|
/// "C2S NOTIFY AskPlayerEnterOrLeaveDoor [0x03C951FD] body=9b". Registered Invoke-only it never
|
|||
|
|
/// matched and every frame fell to DefaultUnknownNotify4229938, so nothing was ever logged.
|
|||
|
|
/// Registration is Both and only invokes get a reply (a notify has no callback).
|
|||
|
|
///
|
|||
|
|
/// There is NO dedicated door RPC: a census of all 2754 UXRPCMethodArgs classes in the client
|
|||
|
|
/// found zero door-proximity args classes (round 16C). The state travels inside
|
|||
|
|
/// GadgetEntityInfo.DoorModuleInfo and arrives through SyncGadgetRefresh(68765074), which the
|
|||
|
|
/// client applies in OnSyncDoorInfo. The chain is BaseDoorProxy.TriggerEnter ->
|
|||
|
|
/// DoorMgr.SyncDoorProximityState -> DoorModuleSyncInfo{LockState, ProximityState,
|
|||
|
|
/// ProximityStartTime, ClientInfos}.
|
|||
|
|
///
|
|||
|
|
/// ⚠ Two door classes, do not confuse them (donor round 16I-10):
|
|||
|
|
/// · venue / high-layer doors whose approach makes the client's own bundled
|
|||
|
|
/// <c>sector_high_indoor_<id>_0</c> binary fail to deserialize — no server fix exists;
|
|||
|
|
/// · ordinary gadget doors, which this file drives. The gym (Hypercore) and Sonic Boom doors
|
|||
|
|
/// were in the SECOND class and opened live in round 16H.
|
|||
|
|
/// </summary>
|
|||
|
|
internal sealed partial class GameRouter
|
|||
|
|
{
|
|||
|
|
private const string DoorTimersKey = "client4229938-door-timers";
|
|||
|
|
private const string DoorPlacementsKey = "client4229938-door-placements";
|
|||
|
|
|
|||
|
|
/// <summary>LockState values (DoorLockState enum, one wire byte).</summary>
|
|||
|
|
private const byte DoorLockStateUnlocked = 0;
|
|||
|
|
|
|||
|
|
/// <summary>ProximityState values (DoorProximityState enum, one wire byte).</summary>
|
|||
|
|
private const byte DoorProximityNoPlayer = 0;
|
|||
|
|
private const byte DoorProximityHasPlayer = 1;
|
|||
|
|
|
|||
|
|
/// <summary>Leaf slot we drive. The client's per-panel list is indexed; 0 is the first leaf.</summary>
|
|||
|
|
private const uint DoorLeafIndex = 0;
|
|||
|
|
|
|||
|
|
/// <summary>Leaf angle in degrees for a shut door (DoorClientInfo.OpenAngle, the only field).</summary>
|
|||
|
|
private const float DoorClosedAngle = 0f;
|
|||
|
|
|
|||
|
|
/// <summary>AskPlayerEnterOrLeaveDoor(63525373) — u64 gadgetId + u8 isEnter.</summary>
|
|||
|
|
///
|
|||
|
|
/// ⛔ NOTIFY on the wire, never invoke — see the class note for the evidence.
|
|||
|
|
[Handler(MethodId.AskPlayerEnterOrLeaveDoor, HandlerPacketKind.Both)]
|
|||
|
|
private async Task OnAskPlayerEnterOrLeaveDoor(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
var cfg = PrivateServerConfigStore.Current.World.Content.Doors;
|
|||
|
|
var body = msg.Body;
|
|||
|
|
|
|||
|
|
if (body.Length < 9)
|
|||
|
|
{
|
|||
|
|
conn.Log.Warn($"[DOOR] enter/leave short body kind={(msg.IsNotify ? "ntf" : "rpc")} " +
|
|||
|
|
$"bytes={body.Length} hex={Convert.ToHexString(body)}");
|
|||
|
|
if (msg.IsInvoke) await conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var gadgetId = BinaryPrimitives.ReadUInt64LittleEndian(body.AsSpan(0));
|
|||
|
|
var isEnter = body[8] != 0;
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[DOOR] {(isEnter ? "ENTER" : "LEAVE")} gadget={gadgetId} " +
|
|||
|
|
$"kind={(msg.IsNotify ? "ntf" : "rpc")} bytes={body.Length} " +
|
|||
|
|
$"hex={Convert.ToHexString(body)}");
|
|||
|
|
|
|||
|
|
// 车库门:玩家在车库室内里跨过某扇门 ⇒ 记住它是「本会话的车库门」,
|
|||
|
|
// 供 AskTeleportFromHouseGarage(出库)时把门推开(见 GameRouter.Garage.cs)。
|
|||
|
|
// 放在 cfg.Enabled 之前:即使门的自动开关被关掉,出库开门这条兜底也照样有门可用。
|
|||
|
|
if (isEnter)
|
|||
|
|
CacheGarageDoor(conn.Session, gadgetId);
|
|||
|
|
|
|||
|
|
if (msg.IsInvoke) await conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
|
|||
|
|
if (!cfg.Enabled)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
if (isEnter)
|
|||
|
|
{
|
|||
|
|
PushDoorOpen(conn.Session, gadgetId);
|
|||
|
|
ArmDoorAutoClose(conn.Session, gadgetId);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
CancelDoorAutoClose(conn.Session, gadgetId);
|
|||
|
|
if (cfg.PushOnLeave)
|
|||
|
|
PushDoorShut(conn.Session, gadgetId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- the official channel -------------------------------------------------------------
|
|||
|
|
// Three dedicated door S2Cs exist and the client really implements them (GameSceneToClient
|
|||
|
|
// overrides at RVA 0x127D4500 / 0x127D4690 / 0x127D4840 — a stub would be 0x9148360). The
|
|||
|
|
// donor's round 16C census missed them because bare-scalar methods own no args class.
|
|||
|
|
// Semantics are the client's own enums: DoorLockState{Unlocked=0, Locked=1},
|
|||
|
|
// DoorProximityState{NoPlayer=0, HasPlayer=1}.
|
|||
|
|
|
|||
|
|
/// <summary>Announce "somebody is at this door": unlock, HasPlayer, and swing the leaf.</summary>
|
|||
|
|
private static void PushDoorOpen(TcpSession session, ulong gadgetId)
|
|||
|
|
{
|
|||
|
|
var cfg = PrivateServerConfigStore.Current.World.Content.Doors;
|
|||
|
|
var now = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|||
|
|
|
|||
|
|
Push(session, MethodId.IGameSceneToClient_SyncDoorLockState,
|
|||
|
|
new SyncDoorLockStateArgs4229938 { gadgetId = gadgetId, state = DoorLockStateUnlocked },
|
|||
|
|
$"lock=unlocked");
|
|||
|
|
|
|||
|
|
Push(session, MethodId.IGameSceneToClient_SyncDoorProximityState,
|
|||
|
|
new SyncDoorProximityStateArgs4229938
|
|||
|
|
{
|
|||
|
|
gadgetId = gadgetId,
|
|||
|
|
state = DoorProximityHasPlayer,
|
|||
|
|
startTime = now,
|
|||
|
|
},
|
|||
|
|
$"proximity=1");
|
|||
|
|
|
|||
|
|
Push(session, MethodId.IGameSceneToClient_SyncDoorClientInfo,
|
|||
|
|
new SyncDoorClientInfoArgs4229938
|
|||
|
|
{
|
|||
|
|
gadgetId = gadgetId,
|
|||
|
|
index = DoorLeafIndex,
|
|||
|
|
clientInfo = new DoorClientInfo { OpenAngle = cfg.OpenAngle },
|
|||
|
|
},
|
|||
|
|
$"angle={cfg.OpenAngle}");
|
|||
|
|
|
|||
|
|
if (cfg.PushGadgetRefresh)
|
|||
|
|
PushDoorProximity(session, gadgetId, DoorProximityHasPlayer, cfg.OpenAngle);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Announce "nobody is here any more" — the leaf slides back to 0 degrees.</summary>
|
|||
|
|
private static void PushDoorShut(TcpSession session, ulong gadgetId)
|
|||
|
|
{
|
|||
|
|
var cfg = PrivateServerConfigStore.Current.World.Content.Doors;
|
|||
|
|
var now = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|||
|
|
|
|||
|
|
Push(session, MethodId.IGameSceneToClient_SyncDoorProximityState,
|
|||
|
|
new SyncDoorProximityStateArgs4229938
|
|||
|
|
{
|
|||
|
|
gadgetId = gadgetId,
|
|||
|
|
state = DoorProximityNoPlayer,
|
|||
|
|
startTime = now,
|
|||
|
|
},
|
|||
|
|
"proximity=0");
|
|||
|
|
|
|||
|
|
Push(session, MethodId.IGameSceneToClient_SyncDoorClientInfo,
|
|||
|
|
new SyncDoorClientInfoArgs4229938
|
|||
|
|
{
|
|||
|
|
gadgetId = gadgetId,
|
|||
|
|
index = DoorLeafIndex,
|
|||
|
|
clientInfo = new DoorClientInfo { OpenAngle = DoorClosedAngle },
|
|||
|
|
},
|
|||
|
|
"angle=0");
|
|||
|
|
|
|||
|
|
if (cfg.PushGadgetRefresh)
|
|||
|
|
PushDoorProximity(session, gadgetId, DoorProximityNoPlayer, DoorClosedAngle);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void Push<TArgs>(TcpSession session, uint methodId, TArgs args, string what)
|
|||
|
|
{
|
|||
|
|
var bytes = Ananta.SDK.Serialization.UxSerializer.Serialize(args);
|
|||
|
|
var id = args switch
|
|||
|
|
{
|
|||
|
|
SyncDoorLockStateArgs4229938 l => l.gadgetId,
|
|||
|
|
SyncDoorProximityStateArgs4229938 p => p.gadgetId,
|
|||
|
|
SyncDoorClientInfoArgs4229938 c => c.gadgetId,
|
|||
|
|
_ => 0UL,
|
|||
|
|
};
|
|||
|
|
session.Log.Info($"[DOOR] push {what} gadget={id} mid={methodId} bytes={bytes.Length}");
|
|||
|
|
_ = session.NotifyAsync(methodId, bytes, CancellationToken.None);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Legacy path: restate the door through SyncGadgetRefresh(68765074) with DoorModuleSyncInfo.
|
|||
|
|
///
|
|||
|
|
/// Body size is the acceptance proof: donor round 16H measured 8 presses -> 8x 130b refreshes
|
|||
|
|
/// -> ZERO `deserialize failed methodId=68765074`, and the gym / Sonic Boom doors opened.
|
|||
|
|
///
|
|||
|
|
/// ⛑️ Round 16I-4/5 then proved the 130b shape is what made the leaf VANISH on approach:
|
|||
|
|
/// an empty ClientInfos means "no leaf record", so the panel hides the leaf when the module
|
|||
|
|
/// is applied. Carrying one OpenAngle takes the body to 135b (donor's byte-exact size) and
|
|||
|
|
/// gives the panel something to swing. Every other collection stays EMPTY rather than null,
|
|||
|
|
/// which is what makes the arithmetic land on those numbers.
|
|||
|
|
///
|
|||
|
|
/// Placement is frozen per gadget on first push (see DoorPlacementsOf): Pack carries a real
|
|||
|
|
/// position the client applies to the entity, so re-sending a different/player-tracked
|
|||
|
|
/// position drags the door around (or to zero) instead of leaving it where it belongs.
|
|||
|
|
/// </summary>
|
|||
|
|
private static void PushDoorProximity(TcpSession session, ulong gadgetId, byte proximityState, float openAngle)
|
|||
|
|
{
|
|||
|
|
var now = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|||
|
|
|
|||
|
|
// Frozen on first sight: whatever we send in Pack is applied to the entity, so a moving
|
|||
|
|
// value would drag the door to the player (and to zero once no report has arrived yet).
|
|||
|
|
var placement = DoorPlacementsOf(session).GetOrAdd(gadgetId, static (_, s) =>
|
|||
|
|
{
|
|||
|
|
var snap = GetStateStatic(s);
|
|||
|
|
return new DoorPlacement(
|
|||
|
|
snap.LastReportedPlayerPosition.X,
|
|||
|
|
snap.LastReportedPlayerPosition.Y,
|
|||
|
|
snap.LastReportedPlayerPosition.Z,
|
|||
|
|
snap.LastReportedPlayerRotation.Y);
|
|||
|
|
}, session);
|
|||
|
|
|
|||
|
|
var entity = new GadgetEntityInfo
|
|||
|
|
{
|
|||
|
|
InstanceId = gadgetId,
|
|||
|
|
// The client keeps its own door geometry; position/facing only have to land on this
|
|||
|
|
// side of nowhere so the refresh addresses the entity it already registered.
|
|||
|
|
Position = new Auto.UXVector3 { X = placement.X, Y = placement.Y, Z = placement.Z },
|
|||
|
|
Facing = new Auto.UXVector3 { X = 0f, Y = placement.Facing, Z = 0f },
|
|||
|
|
NavId = 0,
|
|||
|
|
ChildNavIds = [],
|
|||
|
|
ForceLod0 = false,
|
|||
|
|
IsTask = false,
|
|||
|
|
|
|||
|
|
Pack = new PackedGadgetInfo
|
|||
|
|
{
|
|||
|
|
posX = placement.X, posY = placement.Y, posZ = placement.Z,
|
|||
|
|
eulerX = 0f, eulerY = placement.Facing, eulerZ = 0f,
|
|||
|
|
iScale = 100,
|
|||
|
|
uniqueId = gadgetId,
|
|||
|
|
pathId = 0,
|
|||
|
|
spoonSpecialList = [],
|
|||
|
|
delayDestroy = false,
|
|||
|
|
startTaskId = 0,
|
|||
|
|
endTaskId = 0,
|
|||
|
|
extractionItemContainerId = 0,
|
|||
|
|
isStatic = true,
|
|||
|
|
},
|
|||
|
|
ReplaceInfo = null,
|
|||
|
|
CommonStateInfoDic = [],
|
|||
|
|
CommonValueInfoDic = [],
|
|||
|
|
PersonalValueInfoDic = [],
|
|||
|
|
StateCheckIndexDic = [],
|
|||
|
|
ValueCheckIndexDic = [],
|
|||
|
|
OccupantInfos = [],
|
|||
|
|
LinkOccupiedId = 0,
|
|||
|
|
SymbiosisGadgets = [],
|
|||
|
|
MobilePlatformInfo = null,
|
|||
|
|
AdherePlatformInfo = null,
|
|||
|
|
DoorModuleInfo = new DoorModuleSyncInfo
|
|||
|
|
{
|
|||
|
|
LockState = DoorLockStateUnlocked,
|
|||
|
|
ProximityState = proximityState,
|
|||
|
|
ProximityStartTime = now,
|
|||
|
|
ClientInfos = [new DoorClientInfo { OpenAngle = openAngle }],
|
|||
|
|
},
|
|||
|
|
DrillShelfInfo = null,
|
|||
|
|
HangingInfo = null,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
var args = new SyncGadgetRefreshArgs4229938 { info = entity };
|
|||
|
|
var bytes = Ananta.SDK.Serialization.UxSerializer.Serialize(args);
|
|||
|
|
session.Log.Info($"[DOOR] refresh gadget={gadgetId} proximity={proximityState} " +
|
|||
|
|
$"angle={openAngle} bytes={bytes.Length} " +
|
|||
|
|
$"pos=({placement.X:F1},{placement.Y:F1},{placement.Z:F1})");
|
|||
|
|
|
|||
|
|
_ = session.NotifyAsync(MethodId.IGameSceneToClient_SyncGadgetRefresh, bytes, CancellationToken.None);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Where this session last said a door stands. GadgetEntityInfo.Pack is authoritative on the
|
|||
|
|
/// client side, so once we have declared a placement we must keep declaring the same one:
|
|||
|
|
/// a drifting value makes the leaf jump to the player instead of sitting in its frame.
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly record struct DoorPlacement(float X, float Y, float Z, float Facing);
|
|||
|
|
|
|||
|
|
private static ConcurrentDictionary<ulong, DoorPlacement> DoorPlacementsOf(TcpSession session)
|
|||
|
|
{
|
|||
|
|
if (session.Items.TryGetValue(DoorPlacementsKey, out var existing)
|
|||
|
|
&& existing is ConcurrentDictionary<ulong, DoorPlacement> typed)
|
|||
|
|
return typed;
|
|||
|
|
|
|||
|
|
var created = new ConcurrentDictionary<ulong, DoorPlacement>();
|
|||
|
|
session.Items[DoorPlacementsKey] = created;
|
|||
|
|
return created;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- auto-close ----------------------------------------------------------------------
|
|||
|
|
// Donor round 16I-4/5: the client emits ENTER far more often than LEAVE (one session logged
|
|||
|
|
// 3 ENTER / 0 LEAVE), and it never restates the module by itself, so a door opened by the
|
|||
|
|
// server stayed open forever / vanished on leave. Cancel the pending timer on any new event
|
|||
|
|
// for the same door so only the last one survives.
|
|||
|
|
|
|||
|
|
private static ConcurrentDictionary<ulong, CancellationTokenSource> DoorTimersOf(TcpSession session)
|
|||
|
|
{
|
|||
|
|
if (session.Items.TryGetValue(DoorTimersKey, out var existing)
|
|||
|
|
&& existing is ConcurrentDictionary<ulong, CancellationTokenSource> typed)
|
|||
|
|
return typed;
|
|||
|
|
|
|||
|
|
var created = new ConcurrentDictionary<ulong, CancellationTokenSource>();
|
|||
|
|
session.Items[DoorTimersKey] = created;
|
|||
|
|
return created;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void ArmDoorAutoClose(TcpSession session, ulong gadgetId)
|
|||
|
|
{
|
|||
|
|
var seconds = PrivateServerConfigStore.Current.World.Content.Doors.AutoCloseSeconds;
|
|||
|
|
if (seconds <= 0)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
var timers = DoorTimersOf(session);
|
|||
|
|
|
|||
|
|
if (timers.TryRemove(gadgetId, out var previous))
|
|||
|
|
{
|
|||
|
|
previous.Cancel();
|
|||
|
|
previous.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var cts = new CancellationTokenSource();
|
|||
|
|
timers[gadgetId] = cts;
|
|||
|
|
|
|||
|
|
_ = Task.Run(async () =>
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
await Task.Delay(seconds * 1000, cts.Token);
|
|||
|
|
if (cts.IsCancellationRequested)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
timers.TryRemove(gadgetId, out _);
|
|||
|
|
PushDoorShut(session, gadgetId);
|
|||
|
|
session.Log.Info($"[DOOR] auto-close push gadget={gadgetId}");
|
|||
|
|
}
|
|||
|
|
catch (OperationCanceledException)
|
|||
|
|
{
|
|||
|
|
// superseded by a newer event for the same door
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) when (ex is IOException or ObjectDisposedException)
|
|||
|
|
{
|
|||
|
|
// session went away
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
cts.Dispose();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void CancelDoorAutoClose(TcpSession session, ulong gadgetId)
|
|||
|
|
{
|
|||
|
|
if (!DoorTimersOf(session).TryRemove(gadgetId, out var cts))
|
|||
|
|
return;
|
|||
|
|
cts.Cancel();
|
|||
|
|
cts.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- 车库门(2026-09-30)--------------------------------------------------------------
|
|||
|
|
// 车库大门是个普通 gadget 门(Gadgets_888_GarageDoor),它的实例 id 是**动态**的 ——
|
|||
|
|
// IndoorConfig 里没有 gadget 清单,配置查不到,只能运行时从客户端跨门上报
|
|||
|
|
// (AskPlayerEnterOrLeaveDoor 的第一个 u64)里学。
|
|||
|
|
// 玩家在车库室内(ClientIndoorConfigId ∈ 5 个车库 indoor)里跨门 ⇒ 记下这扇门,
|
|||
|
|
// 出库(AskTeleportFromHouseGarage)时用它把门推开。
|
|||
|
|
|
|||
|
|
private const string GarageDoorKey = "client4229938-garage-door";
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 车库室内体积(<c>IndoorConfig.Id</c>)。⛔ 不要直接抄 <c>HouseConfig.GarageIndoorId</c>:
|
|||
|
|
/// 官方把绿屿公馆(11200003) 那一行写成了 <c>301</c>(= 绿屿公馆主体),真实车库是 <c>302</c>
|
|||
|
|
/// (<c>IndoorConfig</c>:301 绿屿公馆 / 302 绿屿公馆车库),客户端那边已用 fastpatch
|
|||
|
|
/// (<c>tools/garage_indoor_fix.lua</c>)改回 302。这里两个都收,免得因室内划分差异漏掉。
|
|||
|
|
/// 其余 4 条 = 各自车库:201 塔菲公寓车库 / 300 旧物仓公寓车库 / 299 观澜北岸车库 / 379 素光小筑车库。
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly uint[] GarageIndoorIds = [201u, 301u, 302u, 300u, 299u, 379u];
|
|||
|
|
|
|||
|
|
private static void CacheGarageDoor(TcpSession session, ulong gadgetId)
|
|||
|
|
{
|
|||
|
|
if (gadgetId == 0)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
uint indoorId;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
indoorId = GetStateStatic(session).ClientIndoorConfigId;
|
|||
|
|
}
|
|||
|
|
catch
|
|||
|
|
{
|
|||
|
|
return; // 还没有世界状态(理论到不了:门只可能在世界里跨)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (Array.IndexOf(GarageIndoorIds, indoorId) < 0)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
session.Items[GarageDoorKey] = gadgetId;
|
|||
|
|
session.Log.Info($"[DOOR] 记住车库门 gadget={gadgetId} indoor={indoorId}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>出库时把本会话记住的车库门推开。没有缓存(玩家没走过门)就只记一条日志。</summary>
|
|||
|
|
private static void OpenCachedGarageDoor(Connection conn)
|
|||
|
|
{
|
|||
|
|
if (!conn.Session.Items.TryGetValue(GarageDoorKey, out var raw) || raw is not ulong gadgetId || gadgetId == 0)
|
|||
|
|
{
|
|||
|
|
conn.Log.Info("[GARAGE] 出库开门:本会话还没缓存到车库门 gadgetId(玩家没从门里走过?)");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
PushDoorOpen(conn.Session, gadgetId);
|
|||
|
|
conn.Log.Info($"[GARAGE] 出库开门 gadget={gadgetId}");
|
|||
|
|
}
|
|||
|
|
}
|