241 lines
11 KiB
C#
241 lines
11 KiB
C#
using Ananta.Server.RpcTypes.Client4229938;
|
|
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
|
namespace Ananta.Server.Protocol.Client4229938;
|
|
|
|
internal sealed class WorldEntryState
|
|
{
|
|
internal object SyncRoot { get; } = new();
|
|
|
|
internal Dictionary<uint, GameMethods.SpiritWearFashionsInfo> CustomWornFashionsBySpirit { get; } = [];
|
|
|
|
internal bool InitialStateSent { get; set; }
|
|
internal bool EnterSceneSent { get; set; }
|
|
internal bool SceneLoadAckSeen { get; set; }
|
|
internal bool GameResourcesReadySeen { get; set; }
|
|
internal bool ControlPublished { get; set; }
|
|
internal bool SceneCompletionSent { get; set; }
|
|
internal bool CurrentSpiritStateSent { get; set; }
|
|
internal bool Ready { get; set; }
|
|
internal bool LivePlayerProfilePublished { get; set; }
|
|
internal bool SameSceneAckSeen { get; set; }
|
|
internal bool MovementCapabilityPublished { get; set; }
|
|
internal bool FreeRoamReleased { get; set; }
|
|
internal bool FirstMovementSeen { get; set; }
|
|
internal bool GaragePublished { get; set; }
|
|
internal bool AetherVehicleInitSent { get; set; }
|
|
internal bool InitialCapabilityBuffsPublished { get; set; }
|
|
internal bool AllBuildBuffsPublished { get; set; }
|
|
internal bool InitialActorPresentationPublished { get; set; }
|
|
internal bool CombatProfilePublished { get; set; }
|
|
internal bool AccountArmoryPublished { get; set; }
|
|
|
|
// Current public-world destination. Initial values come from private-server.json; airport
|
|
// travel updates these per scene generation without mutating global configuration.
|
|
internal uint ActiveRaidId { get; set; } = Profile.RaidId;
|
|
internal ulong ActiveInstanceId { get; set; } = Profile.SceneInstanceId;
|
|
internal uint ActiveUniverseId { get; set; } = Profile.UniverseId;
|
|
internal string ActiveContentScene { get; set; } = "WorldMap_Release";
|
|
internal bool WorldEntryIsAirportTravel { get; set; }
|
|
|
|
// Native LoadingManager common-teleport transaction. Airport travel reuses the retail
|
|
// AskTeleport -> SyncPreTeleportOption -> ReportPreTeleportFinish -> domain RPC ->
|
|
// SyncTeleport -> ReportPostTeleportFinish sequence so SwitchTeleportManager cannot retain
|
|
// a stale AcrossRaid flow after landing.
|
|
internal ulong PendingTeleportId { get; set; }
|
|
internal string PendingAirportRouteKey { get; set; } = string.Empty;
|
|
internal bool PendingTeleportPreFinished { get; set; }
|
|
internal bool PendingTeleportSyncSent { get; set; }
|
|
|
|
// Build 4229938: one generation-bound initial-player handoff.
|
|
// 0 = edge not claimed; -generation = claimed/in-flight; +generation = published.
|
|
internal bool WorldEntryControlPending { get; set; }
|
|
internal bool WorldEntryControlFinalized { get; set; }
|
|
internal ulong WorldEntryControlUnit { get; set; }
|
|
internal uint WorldEntryControlTemplate { get; set; }
|
|
internal int WorldEntryControlGeneration { get; set; }
|
|
internal int WorldEntryLoadingCompletedGeneration { get; set; }
|
|
internal int WorldEntryOpeningEndedGeneration { get; set; }
|
|
internal int WorldEntryControlFinalizingGeneration { get; set; }
|
|
internal int WorldEntryLogicProjectionPublishedGeneration { get; set; }
|
|
internal int WorldEntryCurrentMetadataPublishedGeneration { get; set; }
|
|
internal ulong WorldEntrySceneId4229938 { get; set; }
|
|
internal ulong WorldEntrySessionId4229938 { get; set; }
|
|
internal Vec3 WorldEntryCreateHeroPosition { get; set; } = Profile.WorldSpawn;
|
|
internal float WorldEntryCreateHeroFacing { get; set; } = Profile.WorldFacing;
|
|
internal Vec3 LastReportedPlayerPosition { get; set; } = Profile.WorldSpawn;
|
|
internal Vec3 LastReportedPlayerRotation { get; set; } = new(0f, Profile.WorldFacing, 0f);
|
|
internal bool HasLastReportedPlayerTransform { get; set; }
|
|
internal Vec3 PendingTeleportPosition { get; set; } = new(0f, 0f, 0f);
|
|
internal float PendingTeleportFacing { get; set; } = 0f;
|
|
|
|
|
|
internal uint ActiveSkillId { get; set; }
|
|
internal bool RestoreResourcesAfterActiveSkill { get; set; }
|
|
internal int ActiveClientSkillInstanceId { get; set; }
|
|
internal long ActiveSkillStartedTicks { get; set; }
|
|
internal Dictionary<ulong, uint> StaticNpcPreparedPlotEvents { get; } = new();
|
|
internal ulong ActiveWeaponInstanceId { get; set; }
|
|
internal uint ActiveFightStyleId { get; set; }
|
|
internal Dictionary<uint, ulong> LastWeaponBySpirit { get; } = [];
|
|
internal Dictionary<uint, List<ulong>> WeaponSlotsBySpirit { get; } = [];
|
|
internal Dictionary<ulong, uint> WeaponStyleOverrides { get; } = [];
|
|
// Persistent weapon-instance ammunition. These dictionaries intentionally survive public-scene
|
|
// generations and character switches; a weapon switch must never refill its magazine.
|
|
internal Dictionary<ulong, int> WeaponMagazineAmmo { get; } = [];
|
|
// For firearms without separate backpack bullets, Durability is the remaining total ammo
|
|
// (loaded magazine + reserve). Keep it instance-persistent for the same reason as the magazine.
|
|
internal Dictionary<ulong, int> WeaponDurabilityAmmo { get; } = [];
|
|
internal Dictionary<ulong, uint> WeaponBulletByInstance { get; } = [];
|
|
internal Dictionary<uint, uint> BackpackItemCounts { get; } = [];
|
|
internal Dictionary<(uint SpiritId, uint FightStyleTypeId), uint> SpiritStyleOverrides { get; } = [];
|
|
internal int CombatUseCount { get; set; }
|
|
internal int CombatEndCount { get; set; }
|
|
internal int CombatHitCount { get; set; }
|
|
internal uint NextBuffInstanceId { get; set; } = 1200000u;
|
|
internal uint ActiveFeiSuoBuffInstanceId { get; set; }
|
|
internal ulong ActiveFeiSuoBuffUnitId { get; set; }
|
|
// Exact instance ids for client-owned 4229938 traversal states (SwingBuff/WallRushBuff).
|
|
// They are created/removed on demand and are intentionally not resident login buffs.
|
|
internal Dictionary<(ulong UnitId, uint BuffId), uint> ActiveClientWebBuffInstances { get; } = [];
|
|
internal ulong SceneId { get; set; }
|
|
internal ulong ActiveSpiritUnitId { get; set; } = Profile.InitialUnitId;
|
|
internal uint ActiveSpiritTemplateId { get; set; } = Profile.InitialSpiritTemplateId;
|
|
internal uint WorldEntrySwitchShowId { get; set; }
|
|
internal uint LastSwitchShowId { get; set; }
|
|
internal int SwitchCount { get; set; }
|
|
|
|
// Native 4229938 SwitchTeleport is a three-phase transaction. Only the current character
|
|
// exists as the controlled actor. Every target is materialized by the same generic AOI path
|
|
// after the client reports that the source close-up has finished.
|
|
internal uint PendingSwitchTemplateId { get; set; }
|
|
internal ulong PendingSwitchUnitId { get; set; }
|
|
internal ulong PendingSwitchOldUnitId { get; set; }
|
|
internal uint PendingSwitchShowId { get; set; }
|
|
internal Vec3 PendingSwitchPosition { get; set; } = Profile.WorldSpawn;
|
|
internal float PendingSwitchFacing { get; set; } = Profile.WorldFacing;
|
|
internal bool PendingSwitchControlTransferred { get; set; }
|
|
internal bool PendingSwitchLandingStarted { get; set; }
|
|
|
|
// Time-of-day state (persists across scene switches; pushed on load when set).
|
|
internal uint TimeHour { get; set; }
|
|
internal uint TimeMinute { get; set; }
|
|
internal bool TimeFixed { get; set; }
|
|
internal uint TimeTransitionSeconds { get; set; }
|
|
internal bool HasExplicitTime { get; set; }
|
|
internal uint WeatherId { get; set; }
|
|
internal uint WeatherTransitionSeconds { get; set; }
|
|
internal bool HasExplicitWeather { get; set; }
|
|
|
|
// Personal time settings and mobile customization
|
|
internal List<GameMethods.PersonalTimeSetting4229938> PersonalTimeSettings { get; } = [];
|
|
internal uint MobileWallpaperId { get; set; }
|
|
internal uint MobileDecorationId { get; set; }
|
|
internal uint MobilePendantId { get; set; }
|
|
|
|
// 10-Second Position History Rollback Buffer (holds last 30s locally, 60s globally across reconnects)
|
|
internal sealed record PositionCheckpoint(DateTime Timestamp, Vec3 Position, float Facing);
|
|
private readonly LinkedList<PositionCheckpoint> _positionHistory = new();
|
|
private DateTime _lastCheckpointTime = DateTime.MinValue;
|
|
|
|
private static readonly LinkedList<PositionCheckpoint> s_globalPositionHistory = new();
|
|
private static readonly object s_globalSyncRoot = new();
|
|
|
|
internal static void RecordGlobalCheckpoint(Vec3 pos, float facing)
|
|
{
|
|
lock (s_globalSyncRoot)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
s_globalPositionHistory.AddLast(new PositionCheckpoint(now, pos, facing));
|
|
var cutoff = now.AddSeconds(-60);
|
|
while (s_globalPositionHistory.Count > 0 && s_globalPositionHistory.First!.Value.Timestamp < cutoff)
|
|
{
|
|
s_globalPositionHistory.RemoveFirst();
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static PositionCheckpoint? TryGetGlobalCheckpointSecondsAgo(double seconds = 10.0)
|
|
{
|
|
lock (s_globalSyncRoot)
|
|
{
|
|
if (s_globalPositionHistory.Count == 0)
|
|
return null;
|
|
|
|
var targetTime = DateTime.UtcNow.AddSeconds(-seconds);
|
|
PositionCheckpoint? best = null;
|
|
double bestDiff = double.MaxValue;
|
|
|
|
foreach (var cp in s_globalPositionHistory)
|
|
{
|
|
var diff = Math.Abs((cp.Timestamp - targetTime).TotalSeconds);
|
|
if (diff < bestDiff)
|
|
{
|
|
bestDiff = diff;
|
|
best = cp;
|
|
}
|
|
}
|
|
|
|
return best ?? s_globalPositionHistory.First?.Value;
|
|
}
|
|
}
|
|
|
|
internal void SeedHistoryFromGlobal()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
lock (s_globalSyncRoot)
|
|
{
|
|
foreach (var cp in s_globalPositionHistory)
|
|
{
|
|
_positionHistory.AddLast(cp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void RecordPositionCheckpoint(Vec3 pos, float facing)
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if ((now - _lastCheckpointTime).TotalMilliseconds < 500)
|
|
return;
|
|
|
|
_lastCheckpointTime = now;
|
|
_positionHistory.AddLast(new PositionCheckpoint(now, pos, facing));
|
|
|
|
var cutoff = now.AddSeconds(-30);
|
|
while (_positionHistory.Count > 0 && _positionHistory.First!.Value.Timestamp < cutoff)
|
|
{
|
|
_positionHistory.RemoveFirst();
|
|
}
|
|
}
|
|
RecordGlobalCheckpoint(pos, facing);
|
|
}
|
|
|
|
internal PositionCheckpoint? TryGetCheckpointSecondsAgo(double seconds = 10.0)
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
if (_positionHistory.Count == 0)
|
|
return TryGetGlobalCheckpointSecondsAgo(seconds);
|
|
|
|
var targetTime = DateTime.UtcNow.AddSeconds(-seconds);
|
|
PositionCheckpoint? best = null;
|
|
double bestDiff = double.MaxValue;
|
|
|
|
foreach (var cp in _positionHistory)
|
|
{
|
|
var diff = Math.Abs((cp.Timestamp - targetTime).TotalSeconds);
|
|
if (diff < bestDiff)
|
|
{
|
|
bestDiff = diff;
|
|
best = cp;
|
|
}
|
|
}
|
|
|
|
return best ?? _positionHistory.First?.Value;
|
|
}
|
|
}
|
|
}
|