Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.Elevator.cs
T

227 lines
9.9 KiB
C#

using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Handlers;
using Ananta.Server.Protocol.Client4229938;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// Lifts (MobilePlatform).
///
/// Why lifts never worked here: the client's call button is only a *request*. The cab actually
/// moves when the server echoes <b>SyncMobilePlatformInfo(68705244)</b> back — without it the
/// press is acked "ok" into the void and nothing happens. Handoff round 16B identified this and
/// 16F confirmed it live (Bassline 808: full GetOn/Move/GetOff round trip, error=0) once the body
/// stopped being rejected — see the wire note on
/// <see cref="SceneMethods.MobilePlatformSyncInfo"/> for the one-byte count bug that doomed the
/// first attempt.
///
/// Two pushes per ride, matching the donor's proven sequence:
/// 1. departure — CurLevel=from, TgtLevel=target (the cab starts moving);
/// 2. arrival — CurLevel=TgtLevel=target after the travel delay.
///
/// The cab position is the player's last reported transform: we have no authored floor heights, and
/// the donor used the same anchor. The client keeps its own shaft geometry.
/// </summary>
internal sealed partial class GameRouter
{
private const string ElevatorKey = "client4229938-elevators";
internal sealed class ElevatorSessionState
{
internal object SyncRoot { get; } = new();
/// <summary>platformId -&gt; the level the cab is parked at.</summary>
internal Dictionary<ulong, int> Level { get; } = new();
internal int Rides;
}
private static ElevatorSessionState ElevatorStateOf(TcpSession session)
{
if (session.Items.TryGetValue(ElevatorKey, out var existing)
&& existing is ElevatorSessionState typed)
return typed;
var created = new ElevatorSessionState();
session.Items[ElevatorKey] = created;
return created;
}
/// <summary>
/// Client invoke AskMoveMobilePlatform(63185921): (ulong mobilePlatformId, int targetIndex).
/// </summary>
[Handler(MethodId.AskMoveMobilePlatform, HandlerPacketKind.Invoke)]
private async Task OnAskMoveMobilePlatform(Connection conn, UxRpcMessage msg)
{
var cfg = PrivateServerConfigStore.Current.World.Content.Elevators;
if (!cfg.Enabled)
{
await conn.ReturnEmptyOkAsync(msg);
return;
}
if (!msg.TryGetArgs<SceneMethods.AskMoveMobilePlatformArgs4229938>(out var args) || args is null)
{
conn.Log.Warn($"[ELEVATOR] AskMoveMobilePlatform undecodable bytes={msg.Body.Length} " +
$"hex={Convert.ToHexString(msg.Body)}");
await conn.ReturnEmptyOkAsync(msg);
return;
}
var owner = ElevatorStateOf(conn.Session);
int from;
lock (owner.SyncRoot)
{
// ⛑️ Never default the cab's floor to 0. Floor tables are per-lift and this one has no
// floor 0: Player.log 22:42:59 showed
// [error]LiftComponent GetFloorPos NotExistFloorData 0 11034023767650314226
// at LiftComponent:GetFloorPos(Int32) <- LiftInfo:RefreshMoveInfo <- LiftMgr:RefreshLiftState
// and the whole refresh aborted there, so the cab never moved and the doors never ran
// their state machine. With no history, treat the cab as already being on the floor the
// player is asking for (the F-press is a hall call): start == target is a legal "here
// it is" refresh, 0 is not.
from = owner.Level.TryGetValue(args.mobilePlatformId, out var lvl) ? lvl : args.targetIndex;
owner.Level[args.mobilePlatformId] = args.targetIndex;
owner.Rides++;
}
conn.Log.Info($"[ELEVATOR] move platform={args.mobilePlatformId} from={from} to={args.targetIndex}");
await PushPlatformAsync(conn, args.mobilePlatformId, from, args.targetIndex,
force: false, doorMode: 0); // Normal while the cab is travelling: doors shut.
// Arrival. Sent on its own clock so the ride is visible instead of instantaneous; the
// donor's second push is what finally parks the cab and lets GetOff succeed.
// ⛑️ Arrival carries cfg.ArrivalDoorMode (default StayOpen=1): the F-press is only a hall
// call, there is no door RPC, and LiftMgr keeps its own per-lift door mode — parking with
// Normal leaves the doors shut and the player can never step in.
var travel = Math.Clamp(cfg.TravelMs, 200, 15000);
var arrivalDoorMode = cfg.ArrivalDoorMode;
var openDoors = cfg.OpenDoorsOnArrival;
_ = Task.Run(async () =>
{
try
{
await Task.Delay(travel);
await PushPlatformAsync(conn, args.mobilePlatformId, args.targetIndex, args.targetIndex,
force: false, doorMode: arrivalDoorMode);
// ⛑️ The door is a SEPARATE rpc. Parking the cab is not enough: LiftMgr holds a
// per-lift LiftDoorMode of its own and defaults to shut. Ask for StayOpen right
// after arrival (SyncLiftDoorMode 68799106, 9 bytes) — see the note on
// SyncLiftDoorModeArgs4229938 for the dump + lua evidence.
if (openDoors)
{
await PushDoorModeAsync(conn, args.mobilePlatformId, arrivalDoorMode);
}
}
catch (ObjectDisposedException) { }
catch (Exception ex)
{
conn.Log.Info($"[ELEVATOR] arrival push failed: {ex.Message}");
}
});
await conn.ReturnEmptyOkAsync(msg);
}
/// <summary>Client invoke AskGetOnMobilePlatform(63687984): (ulong mobilePlatformId).</summary>
[Handler(MethodId.AskGetOnMobilePlatform, HandlerPacketKind.Invoke)]
private async Task OnAskGetOnMobilePlatform(Connection conn, UxRpcMessage msg)
{
if (msg.TryGetArgs<SceneMethods.AskGetOnMobilePlatformArgs4229938>(out var args) && args is not null)
{
conn.Log.Info($"[ELEVATOR] get-on platform={args.mobilePlatformId}");
// Restate where the cab currently is: the client asks right before it rides.
var owner = ElevatorStateOf(conn.Session);
int level;
lock (owner.SyncRoot)
{
// Same law as the hall call: 0 is not a floor this lift has (see the
// NotExistFloorData evidence above), so fall back to the last known level and, if
// there is none, leave the cab where the caller last asked it to be.
level = owner.Level.TryGetValue(args.mobilePlatformId, out var lvl) ? lvl : 1;
}
// Get-on is the "I am stepping in" moment: hold the doors open.
await PushPlatformAsync(conn, args.mobilePlatformId, level, level,
force: false, doorMode: PrivateServerConfigStore.Current.World.Content.Elevators.ArrivalDoorMode);
}
else
{
conn.Log.Warn($"[ELEVATOR] AskGetOnMobilePlatform undecodable bytes={msg.Body.Length} " +
$"hex={Convert.ToHexString(msg.Body)}");
}
await conn.ReturnEmptyOkAsync(msg);
}
/// <summary>Client invoke AskGetOffMobilePlatform(63974264): no arguments.</summary>
[Handler(MethodId.AskGetOffMobilePlatform, HandlerPacketKind.Invoke)]
private Task OnAskGetOffMobilePlatform(Connection conn, UxRpcMessage msg)
{
conn.Log.Info("[ELEVATOR] get-off");
return conn.ReturnEmptyOkAsync(msg);
}
/// <summary>SyncLiftDoorMode(68799106) — the only thing that opens a lift door.</summary>
private static async Task PushDoorModeAsync(Connection conn, ulong platformId, byte doorMode)
{
var body = UxSerializer.Serialize(new SceneMethods.SyncLiftDoorModeArgs4229938
{
mobilePlatformId = platformId,
doorMode = doorMode,
});
await conn.Session.NotifyAsync(MethodId.SyncLiftDoorMode, body, CancellationToken.None);
conn.Log.Info($"[ELEVATOR] door-mode platform={platformId} mode={doorMode} bytes={body.Length}");
}
private static async Task PushPlatformAsync(
Connection conn, ulong platformId, int curLevel, int tgtLevel, bool force, byte doorMode)
{
var state = GetStateStatic(conn.Session);
float px;
float py;
float pz;
lock (state.SyncRoot)
{
px = state.LastReportedPlayerPosition.X;
py = state.LastReportedPlayerPosition.Y;
pz = state.LastReportedPlayerPosition.Z;
}
if (!float.IsFinite(px) || !float.IsFinite(py) || !float.IsFinite(pz))
{
px = py = pz = 0f;
}
var now = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var info = new SceneMethods.MobilePlatformSyncInfo
{
CurLevel = curLevel,
TgtLevel = tgtLevel,
StartTime = now,
Players = [],
CurLevelPos = new() { X = px, Y = py, Z = pz },
TgtLevelPos = new() { X = px, Y = py, Z = pz },
InteractBanInfo = null,
DoorMode = doorMode,
};
var body = UxSerializer.Serialize(new SceneMethods.SyncMobilePlatformInfoArgs4229938
{
mobilePlatformId = platformId,
mobilePlatformInfo = info,
force = force,
});
await conn.Session.NotifyAsync(MethodId.SyncMobilePlatformInfo, body, CancellationToken.None);
conn.Log.Info($"[ELEVATOR] push platform={platformId} cur={curLevel} tgt={tgtLevel} " +
$"doorMode={doorMode} pos=({px:F1},{py:F1},{pz:F1}) bytes={body.Length}");
}
}