227 lines
9.9 KiB
C#
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 -> 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}");
|
|
}
|
|
}
|