qstbak/Questionable/Questionable.Functions/AethernetTeleportService.cs
2026-08-17 20:29:32 +10:00

283 lines
7.4 KiB
C#

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<EAetheryteLocation, uint> FirmamentPlaceNameIds = new Dictionary<EAetheryteLocation, uint>
{
{
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<AethernetTeleportService> _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<AethernetTeleportService> 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<PlaceName>().GetRowOrDefault(placeNameId)?.Name.ToString();
}
private string? ResolveAethernetDisplayName(EAetheryteLocation aetheryte)
{
Aetheryte? rowOrDefault = _dataManager.GetExcelSheet<Aetheryte>().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<AddonSelectString>("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<AtkUnitBase>("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;
}
}