using System; using System.Collections.Generic; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameUI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Controller.GameUi; using SmartNav.Model; namespace Questionable.Functions; internal sealed class AethernetTeleportService : IDisposable { public enum ETeleportState { Idle, WaitingForAddon, Complete, Failed } private static readonly TimeSpan AddonTimeout = TimeSpan.FromSeconds(15L); private const long FireRetryIntervalMs = 100L; private static readonly Dictionary FirmamentPlaceNameIds = new Dictionary { { EAetheryteLocation.FirmamentMendicantsCourt, 3436u }, { EAetheryteLocation.FirmamentMattock, 3473u }, { EAetheryteLocation.FirmamentNewNest, 3475u }, { EAetheryteLocation.FirmanentSaintRoellesDais, 3474u }, { EAetheryteLocation.FirmamentFeatherfall, 3525u }, { EAetheryteLocation.FirmamentHoarfrostHall, 3528u }, { EAetheryteLocation.FirmamentWesternRisensongQuarter, 3646u }, { EAetheryteLocation.FirmamentEasternRisensongQuarter, 3645u } }; private readonly IFramework _framework; private readonly IGameGui _gameGui; private readonly IDataManager _dataManager; private readonly ICondition _condition; private readonly InteractionUiController _interactionUiController; private readonly ILogger _logger; private long _requestedAtMs; private bool _selectStringDismissed; private bool _fired; private bool _entriesLogged; private long _nextFireAtMs; public ETeleportState State { get; private set; } public EAetheryteLocation? PendingDestination { get; private set; } public AethernetTeleportService(IFramework framework, IGameGui gameGui, IDataManager dataManager, ICondition condition, InteractionUiController interactionUiController, ILogger logger) { _framework = framework; _gameGui = gameGui; _dataManager = dataManager; _condition = condition; _interactionUiController = interactionUiController; _logger = logger; _framework.Update += OnFrameworkUpdate; } public void RequestTeleport(EAetheryteLocation destination) { if (State != ETeleportState.Idle) { Reset(); } if (FirmamentPlaceNameIds.TryGetValue(destination, out var value)) { string text = ResolvePlaceName(value); if (text != null) { RequestCustomZoneTeleport(destination, text); return; } _logger.LogWarning("Failed to resolve PlaceName {Id} for Firmament destination {Dest}", value, destination); } _logger.LogInformation("Requesting aethernet teleport to {Destination}", destination); PendingDestination = destination; _requestedAtMs = Environment.TickCount64; State = ETeleportState.WaitingForAddon; } public void RequestCustomZoneTeleport(EAetheryteLocation destination, string destinationName) { if (State != ETeleportState.Idle) { _logger.LogWarning("RequestCustomZoneTeleport called while in state {State}, resetting first", State); Reset(); } _logger.LogInformation("Requesting custom zone aethernet teleport to {Destination} ({Name})", destination, destinationName); PendingDestination = destination; _interactionUiController.AethernetDestinationName = destinationName; State = ETeleportState.Complete; } public void Reset() { if (State != ETeleportState.Idle) { _logger.LogDebug("Resetting AethernetTeleportService from state {State}", State); } State = ETeleportState.Idle; PendingDestination = null; _requestedAtMs = 0L; _selectStringDismissed = false; _fired = false; _entriesLogged = false; _nextFireAtMs = 0L; ClearCustomZoneState(); } private void ClearCustomZoneState() { _interactionUiController.AethernetDestinationName = null; } private string? ResolvePlaceName(uint placeNameId) { return _dataManager.GetExcelSheet().GetRowOrDefault(placeNameId)?.Name.ToString(); } private string? ResolveAethernetDisplayName(EAetheryteLocation aetheryte) { Aetheryte? rowOrDefault = _dataManager.GetExcelSheet().GetRowOrDefault((uint)aetheryte); if (!rowOrDefault.HasValue) { return null; } return rowOrDefault.Value.AethernetName.ValueNullable?.Name.ToString(); } private unsafe void OnFrameworkUpdate(IFramework _) { if (State != ETeleportState.WaitingForAddon) { return; } AddonSelectString* addonPtr; AtkUnitBase* addonPtr2; if (_fired && (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51])) { _logger.LogDebug("Aethernet teleport to {Destination} accepted (between areas)", PendingDestination); State = ETeleportState.Complete; } else if (Environment.TickCount64 - _requestedAtMs > (long)AddonTimeout.TotalMilliseconds) { _logger.LogWarning("Aethernet teleport request to {Destination} timed out after {Timeout}s", PendingDestination, AddonTimeout.TotalSeconds); State = ETeleportState.Failed; } else if (_gameGui.TryGetAddonByName("SelectString", out addonPtr) && LAddon.IsAddonReady(&addonPtr->AtkUnitBase)) { _logger.LogDebug("Dismissing aetheryte SelectString (selecting Aethernet)"); addonPtr->AtkUnitBase.FireCallbackInt(0); _selectStringDismissed = true; } else if (_selectStringDismissed) { _selectStringDismissed = false; } else if (!_gameGui.TryGetAddonByName("TelepotTown", out addonPtr2)) { if (_fired) { _logger.LogDebug("Aethernet teleport to {Destination} accepted (menu closed)", PendingDestination); State = ETeleportState.Complete; } } else { if (!LAddon.IsAddonReady(addonPtr2)) { return; } string text = ResolveAethernetDisplayName(PendingDestination.Value); if (text == null) { _logger.LogWarning("Failed to resolve display name for {Destination}", PendingDestination); State = ETeleportState.Failed; return; } int num = -1; for (int i = 0; i < 20; i++) { int num2 = 262 + i; if (num2 >= addonPtr2->AtkValuesCount) { break; } AtkValue atkValue = addonPtr2->AtkValues[num2]; if (atkValue.Type == AtkValueType.Undefined) { break; } string text2 = atkValue.ReadAtkString(); int num3 = 6 + i * 4 + 3; int num4 = ((num3 < addonPtr2->AtkValuesCount) ? addonPtr2->AtkValues[num3].Int : (-1)); if (!_entriesLogged) { _logger.LogDebug("TelepotTown entry {Index}: name='{Name}' callback={Callback}", i, text2, num4); } if (num < 0 && text2 != null && GameFunctions.GameStringEquals(text2, text)) { num = num4; } } _entriesLogged = true; if (num < 0) { return; } long tickCount = Environment.TickCount64; if (tickCount >= _nextFireAtMs) { if (!_fired) { _logger.LogDebug("Selecting aethernet destination {Destination} ({Name}) via callback {Callback}", PendingDestination, text, num); } AtkValue* values = stackalloc AtkValue[2] { new AtkValue { Type = AtkValueType.Int, Int = 11 }, new AtkValue { Type = AtkValueType.Int, Int = num } }; addonPtr2->FireCallback(2u, values, close: true); _fired = true; _nextFireAtMs = tickCount + 100; } } } public void Dispose() { _framework.Update -= OnFrameworkUpdate; } }