using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using SmartNav.Data; using SmartNav.Model; using SmartNav.Model.Converter; namespace Questionable.Controller.Steps.Shared; internal static class AethernetRide { internal sealed record Task(EAetheryteLocation From, EAetheryteLocation To) : ISkippableTask, ITask { public override string ToString() { return $"UseAethernet({From} -> {To})"; } } internal sealed class AethernetRideExecutor(ILogger logger, AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, IClientState clientState, IObjectTable objectTable, AetheryteData aetheryteData, TerritoryData territoryData, AethernetTeleportService aethernetTeleportService, MovementController movementController, ICondition condition) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private enum EAethernetPhase { None, Mounting, Moving, Unmounting, WaitingForTargetable, Interacting, WaitingForArrival } private EAethernetPhase _phase; private long _continueAt; private long _phaseStartedAt; private int _interactionAttempts; private const long InteractionRetryMs = 3000L; private const int MaxInteractionAttempts = 3; private void SetPhase(EAethernetPhase phase) { _phase = phase; _phaseStartedAt = Environment.TickCount64; } public void StopNow() { aethernetTeleportService.Reset(); } public override bool HasTimedOut(float timeoutSeconds) { if (_phase == EAethernetPhase.None) { return false; } return Environment.TickCount64 - _phaseStartedAt > (long)(timeoutSeconds * 1000f); } protected override bool Start() { SetPhase(EAethernetPhase.None); _interactionAttempts = 0; aethernetTeleportService.Reset(); if (aetheryteFunctions.IsAetheryteUnlocked(base.Task.From) && aetheryteFunctions.IsAetheryteUnlocked(base.Task.To)) { uint territoryType = clientState.TerritoryType; IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer == null) { return false; } Vector3 playerPosition = localPlayer.Position; if (aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) < aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.To)) { float num = (base.Task.From.IsFirmamentAetheryte() ? 11f : (AetheryteConverter.IsLargeAetheryte(base.Task.From) ? 11f : 4f)); if (aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) < num) { BeginTeleport(); return true; } if (base.Task.From == EAetheryteLocation.SolutionNine) { logger.LogDebug("Moving to S9 aetheryte"); int num2 = 4; List list = new List(num2); CollectionsMarshal.SetCount(list, num2); Span span = CollectionsMarshal.AsSpan(list); span[0] = new Vector3(0f, 8.442986f, 9f); span[1] = new Vector3(9f, 8.442986f, 0f); span[2] = new Vector3(-9f, 8.442986f, 0f); span[3] = new Vector3(0f, 8.442986f, -9f); Vector3 to = list.MinBy((Vector3 x) => Vector3.Distance(playerPosition, x)); SetPhase(EAethernetPhase.Moving); movementController.NavigateTo(EMovementType.Quest, (uint)base.Task.From, to, fly: false, sprint: true, 0.25f); return true; } if (territoryData.CanUseMount(territoryType) && aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) > 30f && !gameFunctions.HasStatusPreventingMount() && gameFunctions.Mount()) { SetPhase(EAethernetPhase.Mounting); _continueAt = Environment.TickCount64 + 500; return true; } StartMoving(); return true; } } else { if (clientState.TerritoryType != aetheryteData.TerritoryIds[base.Task.To]) { throw new TaskException($"Aethernet ride not unlocked (from: {base.Task.From}, to: {base.Task.To})"); } logger.LogWarning("Aethernet ride not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), skipping as we are already in the destination territory", base.Task.From, base.Task.To); } return false; } private void StartMoving() { logger.LogDebug("Moving to aethernet shard"); SetPhase(EAethernetPhase.Moving); EAetheryteLocation eAetheryteLocation = base.Task.From; float num = (base.Task.From.IsFirmamentAetheryte() ? 4.4f : ((eAetheryteLocation != EAetheryteLocation.UldahChamberOfRule) ? ((!AetheryteConverter.IsLargeAetheryte(base.Task.From)) ? 6.9f : 10.9f) : 5f)); float value = num; bool flag = aetheryteData.IsGoldSaucerAetheryte(base.Task.From) && !AetheryteConverter.IsLargeAetheryte(base.Task.From); movementController.NavigateTo(EMovementType.Quest, (uint)base.Task.From, aetheryteData.Locations[base.Task.From], fly: false, sprint: true, value, flag ? new float?(5f) : ((float?)null)); } private void BeginTeleport() { logger.LogInformation("Initiating aethernet teleport to {Destination} via {From}", base.Task.To, base.Task.From); if (condition[ConditionFlag.Mounted]) { gameFunctions.Unmount(); SetPhase(EAethernetPhase.Unmounting); } else { SetPhase(EAethernetPhase.WaitingForTargetable); } } private bool TryInteractWithShard() { IGameObject gameObject = gameFunctions.FindObjectByDataId((uint)base.Task.From, null, warnIfMissing: false); if (gameObject == null || !gameObject.IsTargetable) { return false; } aethernetTeleportService.RequestTeleport(base.Task.To); gameFunctions.InteractWith(gameObject); _interactionAttempts++; SetPhase(EAethernetPhase.Interacting); return true; } public override ETaskResult Update() { if (Environment.TickCount64 < _continueAt) { return ETaskResult.StillRunning; } switch (_phase) { case EAethernetPhase.Mounting: if (condition[ConditionFlag.Mounted]) { StartMoving(); } return ETaskResult.StillRunning; case EAethernetPhase.Moving: if (!movementController.HasBeenMovingForAtLeast(2000)) { return ETaskResult.StillRunning; } if (!movementController.IsPathfinding && !movementController.IsPathRunning) { BeginTeleport(); } return ETaskResult.StillRunning; case EAethernetPhase.Unmounting: if (!condition[ConditionFlag.Mounted]) { SetPhase(EAethernetPhase.WaitingForTargetable); } return ETaskResult.StillRunning; case EAethernetPhase.WaitingForTargetable: TryInteractWithShard(); return ETaskResult.StillRunning; case EAethernetPhase.Interacting: if (aethernetTeleportService.State == AethernetTeleportService.ETeleportState.WaitingForAddon) { if (_interactionAttempts < 3 && Environment.TickCount64 - _phaseStartedAt >= 3000) { logger.LogWarning("Aethernet menu did not open; retrying interaction ({Attempt}/{Limit})", _interactionAttempts + 1, 3); aethernetTeleportService.Reset(); SetPhase(EAethernetPhase.WaitingForTargetable); } return ETaskResult.StillRunning; } if (aethernetTeleportService.State == AethernetTeleportService.ETeleportState.Failed) { aethernetTeleportService.Reset(); throw new TaskException("Aethernet teleport failed"); } SetPhase(EAethernetPhase.WaitingForArrival); return ETaskResult.StillRunning; case EAethernetPhase.WaitingForArrival: { Vector3? vector = objectTable.LocalPlayer?.Position; if (!vector.HasValue) { return ETaskResult.StillRunning; } if (aetheryteData.IsAirshipLanding(base.Task.To)) { if (aetheryteData.CalculateAirshipLandingDistance(vector.Value, clientState.TerritoryType, base.Task.To) > 5f) { return ETaskResult.StillRunning; } } else if (aetheryteData.IsCityAetheryte(base.Task.To) || aetheryteData.IsGoldSaucerAetheryte(base.Task.To)) { if (aetheryteData.CalculateDistance(vector.Value, clientState.TerritoryType, base.Task.To) > 20f) { return ETaskResult.StillRunning; } } else if (clientState.TerritoryType != aetheryteData.TerritoryIds[base.Task.To]) { return ETaskResult.StillRunning; } aethernetTeleportService.Reset(); return ETaskResult.TaskComplete; } default: return ETaskResult.StillRunning; } } public override bool ShouldInterruptOnDamage() { return true; } } }