using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Lumina.Excel; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Controller; using Questionable.Controller.Steps; using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Movement; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Navigation; using Questionable.Windows.QuestComponents; using SmartNav; using SmartNav.Data; using SmartNav.Model; using SmartNav.Model.Converter; namespace Questionable.Windows.JournalComponents; internal sealed class AttunementJournalComponent { private sealed record AetheryteEntry(EAetheryteLocation Location, string Name, ushort TerritoryId); private sealed record CityGroup(string CityName, ImmutableList Shards); private sealed record ZoneCurrentGroup(ushort TerritoryId, string ZoneName, ImmutableList Currents); private sealed record CurrentEntry(uint AetherCurrentId, bool IsQuestGated, uint QuestId, string QuestName); private sealed record ExpansionGroup(string ExpansionName, ImmutableList Items, byte SortOrder = 0); private enum AttuneRowKind : byte { Header, AetheryteLeaf, CurrentLeaf } private enum BatchKind : byte { None, AethernetCity, CurrentZone } private sealed class FlatRow { public AttuneRowKind Kind { get; init; } public int Depth { get; init; } public string Key { get; init; } = string.Empty; public string Label { get; init; } = string.Empty; public string CountText { get; init; } = string.Empty; public bool CountComplete { get; init; } public bool CountEmpty { get; init; } public BatchKind Batch { get; init; } public CityGroup? City { get; init; } public ZoneCurrentGroup? Zone { get; init; } public AetheryteEntry? Aetheryte { get; init; } public bool IsAethernet { get; init; } public CurrentEntry? Current { get; init; } } private readonly AetheryteData _aetheryteData; private readonly AetherCurrentData _aetherCurrentData; private readonly AetheryteFunctions _aetheryteFunctions; private readonly GameFunctions _gameFunctions; private readonly TerritoryData _territoryData; private readonly NavRouter _navRouter; private readonly PlayerNavStateBuilder _playerNavStateBuilder; private readonly RouteInstructionBuilder _instructionBuilder; private readonly SmartNavTaskMapper _taskMapper; private readonly MovementController _movementController; private readonly AttunementController _attunementController; private readonly QuestController _questController; private readonly FateController _fateController; private readonly SeasonalDutyController _seasonalDutyController; private readonly CustomDeliveryController _customDeliveryController; private readonly IClientState _clientState; private readonly IObjectTable _objectTable; private readonly UiUtils _uiUtils; private readonly QuestTooltipComponent _questTooltipComponent; private readonly QuestData _questData; private readonly IDataManager _dataManager; private readonly IDalamudPluginInterface _pluginInterface; private readonly ILogger _logger; 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 ImmutableList> _aetherytesByExpansion = ImmutableList.Create(default(ReadOnlySpan>)); private ImmutableList _aethernetByCity = ImmutableList.Create(default(ReadOnlySpan)); private ImmutableList> _currentsByExpansion = ImmutableList.Create(default(ReadOnlySpan>)); private int _aetheryteUnlocked; private int _aetheryteTotal; private int _aethernetUnlocked; private int _aethernetTotal; private int _currentUnlocked; private int _currentTotal; private Dictionary _aetheryteUnlockCache = new Dictionary(); private Dictionary _currentUnlockCache = new Dictionary(); private Dictionary _aetheryteExpansionUnlockedCounts = new Dictionary(); private Dictionary _aethernetCityUnlockedCounts = new Dictionary(); private Dictionary _currentZoneUnlockedCounts = new Dictionary(); private long _lastStatusRefresh; private const long StatusRefreshIntervalMs = 3000L; private string _searchText = string.Empty; private bool _showUnattunedOnly; private List _flatRows = new List(); private readonly HashSet _expandedGroups = new HashSet(); private readonly List<(string Name, ushort TerritoryId, Vector3 Position, ITask AttuneTask)> _pendingBatchTargets = new List<(string, ushort, Vector3, ITask)>(); private string? _batchName; private const string SectionAetherytes = "Aetherytes"; private const string SectionAethernet = "Aethernet Shards"; private const string SectionCurrents = "Aether Currents"; private static readonly int CountPadWidth = 9999.ToString(CultureInfo.CurrentCulture).Length; public Action? SelectTabAction { get; set; } public QuestChainComponent? QuestChainComponent { get; set; } public AttunementJournalComponent(AetheryteData aetheryteData, AetherCurrentData aetherCurrentData, AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, TerritoryData territoryData, NavRouter navRouter, PlayerNavStateBuilder playerNavStateBuilder, RouteInstructionBuilder instructionBuilder, SmartNavTaskMapper taskMapper, MovementController movementController, AttunementController attunementController, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, QuestData questData, IClientState clientState, IObjectTable objectTable, IDataManager dataManager, IDalamudPluginInterface pluginInterface, ILogger logger) { _aetheryteData = aetheryteData; _aetherCurrentData = aetherCurrentData; _aetheryteFunctions = aetheryteFunctions; _gameFunctions = gameFunctions; _territoryData = territoryData; _navRouter = navRouter; _playerNavStateBuilder = playerNavStateBuilder; _instructionBuilder = instructionBuilder; _taskMapper = taskMapper; _movementController = movementController; _attunementController = attunementController; _questController = questController; _fateController = fateController; _seasonalDutyController = seasonalDutyController; _customDeliveryController = customDeliveryController; _uiUtils = uiUtils; _questTooltipComponent = questTooltipComponent; _questData = questData; _clientState = clientState; _objectTable = objectTable; _dataManager = dataManager; _pluginInterface = pluginInterface; _logger = logger; InitializeData(); } private void InitializeData() { ExcelSheet aetheryteSheet = _dataManager.GetExcelSheet(); ExcelSheet territorySheet = _dataManager.GetExcelSheet(); ExcelSheet questSheet = _dataManager.GetExcelSheet(); List list = new List(); List list2 = new List(); ExcelSheet excelSheet = _dataManager.GetExcelSheet(); foreach (EAetheryteLocation key2 in _aetheryteData.Locations.Keys) { ushort valueOrDefault = _aetheryteData.TerritoryIds.GetValueOrDefault(key2); Lumina.Excel.Sheets.Aetheryte? rowOrDefault = aetheryteSheet.GetRowOrDefault((uint)key2); if (!rowOrDefault.HasValue) { uint value; string name = ((!FirmamentPlaceNameIds.TryGetValue(key2, out value)) ? key2.ToString() : (excelSheet.GetRowOrDefault(value)?.Name.ToString() ?? key2.ToString())); bool num = _aetheryteData.AethernetGroups.ContainsKey(key2) && !AetheryteConverter.IsLargeAetheryte(key2); AetheryteEntry item = new AetheryteEntry(key2, name, valueOrDefault); if (num) { list2.Add(item); } else { list.Add(item); } } else if (AetheryteConverter.IsLargeAetheryte(key2)) { string name = rowOrDefault.Value.PlaceName.ValueNullable?.Name.ToString() ?? key2.ToString(); list.Add(new AetheryteEntry(key2, name, valueOrDefault)); } else { string name = rowOrDefault.Value.AethernetName.ValueNullable?.Name.ToString() ?? key2.ToString(); list2.Add(new AetheryteEntry(key2, name, valueOrDefault)); } } _aetherytesByExpansion = (from g in list.GroupBy(delegate(AetheryteEntry a) { ExVersion? exVersion = territorySheet.GetRowOrDefault(a.TerritoryId)?.ExVersion.ValueNullable; return (Name: exVersion?.Name.ToString() ?? "A Realm Reborn", SortOrder: (byte)(exVersion?.RowId ?? 0)); }) orderby g.Key.SortOrder select new ExpansionGroup(g.Key.Name, g.OrderBy((AetheryteEntry a) => a.Name).ToImmutableList(), g.Key.SortOrder)).ToImmutableList(); _aetheryteTotal = _aetherytesByExpansion.Sum((ExpansionGroup g) => g.Items.Count); _aethernetByCity = (from g in (from a in list2 group a by _aetheryteData.AethernetGroups.GetValueOrDefault(a.Location) into g where g.Key != 0 select g).Select(delegate(IGrouping g) { ushort groupId = g.Key; string text = (from kv in _aetheryteData.AethernetGroups where kv.Value == groupId && AetheryteConverter.IsLargeAetheryte(kv.Key) select aetheryteSheet.GetRowOrDefault((uint)kv.Key)?.PlaceName.ValueNullable?.Name.ToString()).FirstOrDefault(); if (string.IsNullOrEmpty(text)) { ushort territoryId = g.First().TerritoryId; text = territorySheet.GetRowOrDefault(territoryId)?.PlaceName.ValueNullable?.Name.ToString() ?? $"Group {groupId}"; } return new CityGroup(text, g.OrderBy((AetheryteEntry a) => a.Name).ToImmutableList()); }) orderby g.CityName select g).ToImmutableList(); _aethernetTotal = _aethernetByCity.Sum((CityGroup g) => g.Shards.Count); _currentsByExpansion = (from x in _aetherCurrentData.AllCurrentsByTerritory.Select>, (ushort, string, string, byte, ImmutableList)>(delegate(KeyValuePair> kv) { ushort key = kv.Key; TerritoryType? rowOrDefault2 = territorySheet.GetRowOrDefault(key); string item2 = rowOrDefault2?.PlaceName.ValueNullable?.Name.ToString() ?? key.ToString(CultureInfo.InvariantCulture); ExVersion? exVersion = rowOrDefault2?.ExVersion.ValueNullable; byte item3 = (byte)(exVersion?.RowId ?? 0); string item4 = exVersion?.Name.ToString() ?? "A Realm Reborn"; ImmutableList item5 = kv.Value.Select(delegate(AetherCurrentInfo info) { string questName = string.Empty; if (info.IsQuestGated && info.QuestId != 0) { questName = questSheet.GetRowOrDefault(info.QuestId)?.Name.ToString() ?? string.Empty; } return new CurrentEntry(info.AetherCurrentId, info.IsQuestGated, info.QuestId, questName); }).ToImmutableList(); return (territoryId: key, zoneName: item2, expansionName: item4, sortOrder: item3, currents: item5); }) where x.sortOrder > 0 group x by (expansionName: x.expansionName, sortOrder: x.sortOrder) into g orderby g.Key.sortOrder select new ExpansionGroup(g.Key.expansionName, (from x in g orderby x.zoneName select new ZoneCurrentGroup(x.territoryId, x.zoneName, x.currents)).ToImmutableList(), g.Key.sortOrder)).ToImmutableList(); _currentTotal = _currentsByExpansion.Sum((ExpansionGroup g) => g.Items.Sum((ZoneCurrentGroup z) => z.Currents.Count)); } public void RefreshCounts() { Dictionary dictionary = new Dictionary(); Dictionary dictionary2 = new Dictionary(); int num = 0; foreach (ExpansionGroup item in _aetherytesByExpansion) { int num2 = 0; foreach (AetheryteEntry item2 in item.Items) { bool flag = _aetheryteFunctions.IsAetheryteUnlocked(item2.Location); dictionary[item2.Location] = flag; if (flag) { num2++; } } dictionary2[item.SortOrder] = num2; num += num2; } Dictionary dictionary3 = new Dictionary(); int num3 = 0; foreach (CityGroup item3 in _aethernetByCity) { int num4 = 0; foreach (AetheryteEntry shard in item3.Shards) { bool flag2 = _aetheryteFunctions.IsAetheryteUnlocked(shard.Location); dictionary[shard.Location] = flag2; if (flag2) { num4++; } } dictionary3[item3.CityName] = num4; num3 += num4; } Dictionary dictionary4 = new Dictionary(); Dictionary dictionary5 = new Dictionary(); int num5 = 0; foreach (ExpansionGroup item4 in _currentsByExpansion) { foreach (ZoneCurrentGroup item5 in item4.Items) { int num6 = 0; foreach (CurrentEntry current in item5.Currents) { bool flag3 = GameFunctions.IsAetherCurrentUnlocked(current.AetherCurrentId); dictionary4[current.AetherCurrentId] = flag3; if (flag3) { num6++; } } dictionary5[item5.TerritoryId] = num6; num5 += num6; } } _aetheryteUnlockCache = dictionary; _currentUnlockCache = dictionary4; _aetheryteExpansionUnlockedCounts = dictionary2; _aethernetCityUnlockedCounts = dictionary3; _currentZoneUnlockedCounts = dictionary5; _aetheryteUnlocked = num; _aethernetUnlocked = num3; _currentUnlocked = num5; _lastStatusRefresh = Environment.TickCount64; RebuildFlatRows(); } public void ClearCounts(int type, int code) { _aetheryteUnlocked = (_aethernetUnlocked = (_currentUnlocked = 0)); _aetheryteUnlockCache.Clear(); _currentUnlockCache.Clear(); _aetheryteExpansionUnlockedCounts.Clear(); _aethernetCityUnlockedCounts.Clear(); _currentZoneUnlockedCounts.Clear(); RebuildFlatRows(); } public void UpdateFilter() { } public void DrawAttunement() { if (Environment.TickCount64 - _lastStatusRefresh > 3000) { RefreshCounts(); } if (!_attunementController.IsRunning && _pendingBatchTargets.Count > 0) { if (_attunementController.CompletedSuccessfully) { (string, ushort, Vector3, ITask) tuple = PopNearestTarget(); string text = _batchName; if (text == null) { (text, _, _, _) = tuple; } StartAttunement(text, tuple.Item2, tuple.Item3, tuple.Item4); } else { _pendingBatchTargets.Clear(); _batchName = null; } } if (_attunementController.IsRunning || _pendingBatchTargets.Count > 0) { string text2 = ((_pendingBatchTargets.Count > 0) ? $"Navigating to {_attunementController.CurrentTargetName}... ({_pendingBatchTargets.Count} remaining)" : ("Navigating to " + _attunementController.CurrentTargetName + "...")); float x; using (ImRaii.PushFont(UiBuilder.IconFont)) { x = ImGui.CalcTextSize(FontAwesomeIcon.Stop.ToIconString()).X; } float num = x + 6f + ImGui.CalcTextSize("Stop").X + ImGui.GetStyle().FramePadding.X * 2f + ImGui.GetStyle().ItemSpacing.X; ImGui.TextColored(in UiThemeUtils.StatusActive, UiThemeUtils.TruncateToWidth(text2, ImGui.GetContentRegionAvail().X - num)); ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Stop, "Stop")) { StopAll("User cancelled (attunement UI)"); } } float x2 = ImGui.GetStyle().ItemSpacing.X; float num2 = ImGui.GetFrameHeight() + ImGui.CalcTextSize("Unattuned only").X + 3f * x2; if (UiThemeUtils.SearchInput("##AttunementSearch", ref _searchText, 0f - num2)) { RebuildFlatRows(); } ImGui.SameLine(); if (UiThemeUtils.WrappedCheckbox("Unattuned only", ref _showUnattunedOnly)) { RebuildFlatRows(); } ImGui.Separator(); DrawTrackerTable(); } private unsafe void DrawTrackerTable() { if (_flatRows.Count == 0) { UiThemeUtils.EmptyState(FontAwesomeIcon.Wifi, "No attunements match your search."); return; } float x; using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { x = ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X; } float fontGlobalScale = ImGui.GetIO().FontGlobalScale; using ImRaii.TableDisposable tableDisposable = UiThemeUtils.BeginThemedTable("Attunement", 2, ImGuiTableFlags.NoSavedSettings | ImGuiTableFlags.ScrollY); if (!tableDisposable) { return; } ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide); ImGui.TableSetupColumn("Unlocked", ImGuiTableColumnFlags.WidthFixed, 120f * fontGlobalScale); ImGui.TableHeadersRow(); float textLineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); bool flag = false; ImGuiListClipper* intPtr = ImGuiNative.ImGuiListClipper(); ImGuiNative.Begin(intPtr, _flatRows.Count, textLineHeightWithSpacing); ImGuiListClipperPtr imGuiListClipperPtr = intPtr; try { while (imGuiListClipperPtr.Step()) { for (int i = imGuiListClipperPtr.DisplayStart; i < imGuiListClipperPtr.DisplayEnd; i++) { if (DrawFlatRow(_flatRows[i], x)) { flag = true; } } } } finally { imGuiListClipperPtr.Destroy(); } if (flag) { RebuildFlatRows(); } } private bool DrawFlatRow(FlatRow row, float statusIconSpacing) { ImGui.TableNextRow(); ImGui.TableNextColumn(); if (row.Depth > 0) { ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().IndentSpacing * (float)row.Depth); } if (row.Kind == AttuneRowKind.AetheryteLeaf) { DrawAetheryteLeaf(row, statusIconSpacing); return false; } if (row.Kind == AttuneRowKind.CurrentLeaf) { DrawCurrentLeaf(row, statusIconSpacing); return false; } bool flag = _expandedGroups.Contains(row.Key); UiThemeUtils.DrawTrackerGroupCaret(_pluginInterface.UiBuilder.IconFontFixedWidthHandle, flag); ImGui.SameLine(); bool result = false; ImU8String label = new ImU8String(2, 2); label.AppendFormatted(row.Label); label.AppendLiteral("##"); label.AppendFormatted(row.Key); if (ImGui.Selectable(label)) { if (flag) { _expandedGroups.Remove(row.Key); } else { _expandedGroups.Add(row.Key); } result = true; } DrawHeaderBatchPopup(row); ImGui.TableNextColumn(); UiThemeUtils.DrawTrackerCount(row.CountText, row.CountComplete, row.CountEmpty); return result; } private void DrawHeaderBatchPopup(FlatRow row) { if (row.Batch == BatchKind.AethernetCity) { CityGroup city = row.City; if ((object)city != null) { int count = city.Shards.Count; if (_aethernetCityUnlockedCounts.GetValueOrDefault(city.CityName) >= count || IsAnyControllerRunning()) { return; } string text = "##AttuneAllAethernet_" + city.CityName; if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { ImGui.OpenPopup(text); } ImRaii.PopupDisposable popupDisposable = ImRaii.Popup(text); try { if (!popupDisposable.Success) { return; } ImU8String label = new ImU8String(14, 1); label.AppendLiteral("Attune all in "); label.AppendFormatted(city.CityName); if (ImGui.MenuItem(label)) { IEnumerable<(string, ushort, Vector3, ITask)> targets = from s in city.Shards where !_aetheryteUnlockCache.GetValueOrDefault(s.Location) select ((string Name, ushort TerritoryId, Vector3, ITask))(Name: s.Name, TerritoryId: s.TerritoryId, _aetheryteData.Locations[s.Location], new AethernetShard.Attune(s.Location)); StartBatchAttunement("All shards in " + city.CityName, targets); } return; } finally { popupDisposable.Dispose(); } } } if (row.Batch != BatchKind.CurrentZone) { return; } ZoneCurrentGroup zone = row.Zone; if ((object)zone == null) { return; } List list = zone.Currents.Where((CurrentEntry c) => !c.IsQuestGated && !_currentUnlockCache.GetValueOrDefault(c.AetherCurrentId)).ToList(); if (list.Count <= 0 || IsAnyControllerRunning()) { return; } string text2 = $"##AttuneAllCurrents_{zone.TerritoryId}"; if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { ImGui.OpenPopup(text2); } ImRaii.PopupDisposable popupDisposable2 = ImRaii.Popup(text2); try { if (!popupDisposable2.Success) { return; } ImU8String label2 = new ImU8String(14, 1); label2.AppendLiteral("Attune all in "); label2.AppendFormatted(zone.ZoneName); if (ImGui.MenuItem(label2)) { IEnumerable<(string, ushort, Vector3, ITask)> targets2 = from c in list select (current: c, pos: _aetherCurrentData.GetOverworldPosition(c.AetherCurrentId)) into x where x.pos != null select ((string, ushort TerritoryId, Vector3 Position, ITask))("Aether Current", TerritoryId: x.pos.TerritoryId, Position: x.pos.Position, new Questionable.Controller.Steps.Interactions.AetherCurrent.Attune(x.pos.DataId, x.current.AetherCurrentId)); StartBatchAttunement("All currents in " + zone.ZoneName, targets2); } } finally { popupDisposable2.Dispose(); } } private void DrawAetheryteLeaf(FlatRow row, float statusIconSpacing) { AetheryteEntry aetheryte = row.Aetheryte; bool valueOrDefault = _aetheryteUnlockCache.GetValueOrDefault(aetheryte.Location); ImU8String label = new ImU8String(2, 2); label.AppendFormatted(aetheryte.Name); label.AppendLiteral("##"); label.AppendFormatted(row.Key); ImGui.Selectable(label); if (!valueOrDefault && !IsAnyControllerRunning()) { string text = (row.IsAethernet ? $"##AttuneAethernet_{aetheryte.Location}" : $"##AttuneAetheryte_{aetheryte.Location}"); if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { ImGui.OpenPopup(text); } ImRaii.PopupDisposable popupDisposable = ImRaii.Popup(text); try { if (popupDisposable.Success) { ImU8String label2 = new ImU8String(10, 1); label2.AppendLiteral("Attune to "); label2.AppendFormatted(aetheryte.Name); if (ImGui.MenuItem(label2)) { Vector3 position = _aetheryteData.Locations[aetheryte.Location]; ITask task2; if (!row.IsAethernet) { ITask task = new Questionable.Controller.Steps.Interactions.Aetheryte.Attune(aetheryte.Location); task2 = task; } else { ITask task = new AethernetShard.Attune(aetheryte.Location); task2 = task; } ITask attuneTask = task2; StartAttunement(aetheryte.Name, aetheryte.TerritoryId, position, attuneTask); } } } finally { popupDisposable.Dispose(); } } ImGui.TableNextColumn(); DrawStatusGlyph(valueOrDefault, statusIconSpacing); } private void DrawCurrentLeaf(FlatRow row, float statusIconSpacing) { CurrentEntry current = row.Current; bool valueOrDefault = _currentUnlockCache.GetValueOrDefault(current.AetherCurrentId); if (current.IsQuestGated) { string value = ((!string.IsNullOrEmpty(current.QuestName)) ? ("Quest: " + current.QuestName) : "Quest Current"); ImU8String label = new ImU8String(2, 2); label.AppendFormatted(value); label.AppendLiteral("##"); label.AppendFormatted(row.Key); ImGui.Selectable(label); if (current.QuestId != 0 && ImGui.IsItemHovered() && _questData.TryGetQuestInfo(QuestId.FromRowId(current.QuestId), out IQuestInfo questInfo)) { _questTooltipComponent.Draw(questInfo); } string text = $"##CurrentQuest_{current.AetherCurrentId}"; if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { ImGui.OpenPopup(text); } ImRaii.PopupDisposable popupDisposable = ImRaii.Popup(text); try { if (popupDisposable.Success && ImGui.MenuItem("View Quest") && current.QuestId != 0) { QuestChainComponent?.SelectQuest(QuestId.FromRowId(current.QuestId)); SelectTabAction?.Invoke("Quest Chain"); } } finally { popupDisposable.Dispose(); } } else { ImU8String label2 = new ImU8String(16, 1); label2.AppendLiteral("Aether Current##"); label2.AppendFormatted(row.Key); ImGui.Selectable(label2); if (!valueOrDefault) { AetherCurrentPosition overworldPosition = _aetherCurrentData.GetOverworldPosition(current.AetherCurrentId); string text2 = $"##AttuneCurrent_{current.AetherCurrentId}"; if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { ImGui.OpenPopup(text2); } using ImRaii.PopupDisposable popupDisposable2 = ImRaii.Popup(text2); if ((bool)popupDisposable2) { bool flag = overworldPosition != null && !IsAnyControllerRunning(); using (ImRaii.Disabled(!flag)) { if (ImGui.MenuItem("Attune") && overworldPosition != null) { StartAttunement("Aether Current", overworldPosition.TerritoryId, overworldPosition.Position, new Questionable.Controller.Steps.Interactions.AetherCurrent.Attune(overworldPosition.DataId, current.AetherCurrentId)); } } if (!flag && overworldPosition == null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Position unknown"); } } } } ImGui.TableNextColumn(); DrawStatusGlyph(valueOrDefault, statusIconSpacing); } private void DrawStatusGlyph(bool unlocked, float statusIconSpacing) { float num = ImGui.GetColumnWidth() / 2f - statusIconSpacing; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num); _uiUtils.ChecklistItem(string.Empty, unlocked ? UiThemeUtils.StatusComplete : UiThemeUtils.StatusLocked, unlocked ? FontAwesomeIcon.Check : FontAwesomeIcon.Times); } private void RebuildFlatRows() { List list = new List(); if (_aetherytesByExpansion.Any((ExpansionGroup exp) => exp.Items.Any(AetherytePasses))) { list.Add(HeaderRow(0, "Aetherytes", "Aetherytes", _aetheryteUnlocked, _aetheryteTotal)); if (_expandedGroups.Contains("Aetherytes")) { foreach (ExpansionGroup item in _aetherytesByExpansion) { List list2 = item.Items.Where(AetherytePasses).ToList(); if (list2.Count == 0) { continue; } string text = $"AetheryteExp_{item.SortOrder}"; int valueOrDefault = _aetheryteExpansionUnlockedCounts.GetValueOrDefault(item.SortOrder); list.Add(HeaderRow(1, text, item.ExpansionName, valueOrDefault, item.Items.Count)); if (!_expandedGroups.Contains(text)) { continue; } foreach (AetheryteEntry item2 in list2) { list.Add(AetheryteLeafRow(2, item2, isAethernet: false)); } } } } if (_aethernetByCity.Any((CityGroup city) => city.Shards.Any(AetherytePasses))) { list.Add(HeaderRow(0, "Aethernet Shards", "Aethernet Shards", _aethernetUnlocked, _aethernetTotal)); if (_expandedGroups.Contains("Aethernet Shards")) { foreach (CityGroup item3 in _aethernetByCity) { List list3 = item3.Shards.Where(AetherytePasses).ToList(); if (list3.Count == 0) { continue; } string text2 = "AethernetCity_" + item3.CityName; int valueOrDefault2 = _aethernetCityUnlockedCounts.GetValueOrDefault(item3.CityName); list.Add(HeaderRow(1, text2, item3.CityName, valueOrDefault2, item3.Shards.Count, BatchKind.AethernetCity, item3)); if (!_expandedGroups.Contains(text2)) { continue; } foreach (AetheryteEntry item4 in list3) { list.Add(AetheryteLeafRow(2, item4, isAethernet: true)); } } } } if (_currentsByExpansion.Any((ExpansionGroup exp) => exp.Items.Any(ZoneHasContent))) { list.Add(HeaderRow(0, "Aether Currents", "Aether Currents", _currentUnlocked, _currentTotal)); if (_expandedGroups.Contains("Aether Currents")) { foreach (ExpansionGroup item5 in _currentsByExpansion) { List list4 = item5.Items.Where(ZoneHasContent).ToList(); if (list4.Count == 0) { continue; } string text3 = $"CurrentsExp_{item5.SortOrder}"; int unlocked = item5.Items.Sum((ZoneCurrentGroup z) => _currentZoneUnlockedCounts.GetValueOrDefault(z.TerritoryId)); int total = item5.Items.Sum((ZoneCurrentGroup z) => z.Currents.Count); list.Add(HeaderRow(1, text3, item5.ExpansionName, unlocked, total)); if (!_expandedGroups.Contains(text3)) { continue; } foreach (ZoneCurrentGroup item6 in list4) { string text4 = $"CurrentZone_{item6.TerritoryId}"; int valueOrDefault3 = _currentZoneUnlockedCounts.GetValueOrDefault(item6.TerritoryId); list.Add(HeaderRow(2, text4, item6.ZoneName, valueOrDefault3, item6.Currents.Count, BatchKind.CurrentZone, null, item6)); if (!_expandedGroups.Contains(text4)) { continue; } foreach (CurrentEntry item7 in item6.Currents.Where(CurrentPasses)) { list.Add(CurrentLeafRow(3, item7)); } } } } } _flatRows = list; } private bool AetherytePasses(AetheryteEntry entry) { if (!string.IsNullOrEmpty(_searchText) && !entry.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) { return false; } if (_showUnattunedOnly && _aetheryteUnlockCache.GetValueOrDefault(entry.Location)) { return false; } return true; } private bool CurrentPasses(CurrentEntry entry) { if (_showUnattunedOnly) { return !_currentUnlockCache.GetValueOrDefault(entry.AetherCurrentId); } return true; } private bool ZoneHasContent(ZoneCurrentGroup zone) { if (!string.IsNullOrEmpty(_searchText) && !zone.ZoneName.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) { return false; } return zone.Currents.Any(CurrentPasses); } private static FlatRow HeaderRow(int depth, string key, string label, int unlocked, int total, BatchKind batch = BatchKind.None, CityGroup? city = null, ZoneCurrentGroup? zone = null) { return new FlatRow { Kind = AttuneRowKind.Header, Depth = depth, Key = key, Label = label, CountText = FormatCount(unlocked, total), CountComplete = (unlocked == total && total > 0), CountEmpty = (total == 0), Batch = batch, City = city, Zone = zone }; } private static FlatRow AetheryteLeafRow(int depth, AetheryteEntry entry, bool isAethernet) { return new FlatRow { Kind = AttuneRowKind.AetheryteLeaf, Depth = depth, Key = (isAethernet ? "Shard_" : "Aeth_") + entry.Location, Label = entry.Name, Aetheryte = entry, IsAethernet = isAethernet }; } private static FlatRow CurrentLeafRow(int depth, CurrentEntry entry) { return new FlatRow { Kind = AttuneRowKind.CurrentLeaf, Depth = depth, Key = "Cur_" + entry.AetherCurrentId, Current = entry }; } private static string FormatCount(int unlocked, int total) { if (total != 0) { return unlocked.ToString(CultureInfo.CurrentCulture).PadLeft(CountPadWidth) + " / " + total.ToString(CultureInfo.CurrentCulture).PadLeft(CountPadWidth); } return "-".PadLeft(CountPadWidth) + " / " + "-".PadLeft(CountPadWidth); } private void StopAll(string reason) { _pendingBatchTargets.Clear(); _batchName = null; _movementController.Stop(); _questController.Stop(reason); _fateController.Stop(reason); _seasonalDutyController.Stop(reason); _customDeliveryController.Stop(reason); _attunementController.Stop(reason); } public void ClearBatchQueue() { _pendingBatchTargets.Clear(); _batchName = null; } private bool IsAnyControllerRunning() { if (!_questController.IsRunning && !_fateController.IsRunning && !_seasonalDutyController.IsRunning && !_customDeliveryController.IsRunning) { return _attunementController.IsRunning; } return true; } private void StartBatchAttunement(string batchName, IEnumerable<(string Name, ushort TerritoryId, Vector3 Position, ITask AttuneTask)> targets) { _pendingBatchTargets.Clear(); _pendingBatchTargets.AddRange(targets); if (_pendingBatchTargets.Count != 0) { _batchName = batchName; (string, ushort, Vector3, ITask) tuple = PopNearestTarget(); StartAttunement(batchName, tuple.Item2, tuple.Item3, tuple.Item4); } } private (string Name, ushort TerritoryId, Vector3 Position, ITask AttuneTask) PopNearestTarget() { Vector3 value = _objectTable[0]?.Position ?? Vector3.Zero; int index = 0; float num = float.MaxValue; for (int i = 0; i < _pendingBatchTargets.Count; i++) { float num2 = Vector3.DistanceSquared(value, _pendingBatchTargets[i].Position); if (num2 < num) { num = num2; index = i; } } (string Name, ushort TerritoryId, Vector3 Position, ITask AttuneTask) result = _pendingBatchTargets[index]; _pendingBatchTargets.RemoveAt(index); return result; } private void StartAttunement(string targetName, ushort territoryId, Vector3 position, ITask attuneTask) { PlayerNavState playerNavState = _playerNavStateBuilder.Build(); if (playerNavState == null) { _logger.LogWarning("Cannot build PlayerNavState for attunement routing"); return; } RouteResult routeResult = _navRouter.FindRoute(playerNavState, territoryId, position); List list = new List(); if (routeResult.Segments.Count > 0) { list.AddRange(_taskMapper.MapInstructions(_instructionBuilder.Build(routeResult, territoryId))); } else if (_clientState.TerritoryType != territoryId) { _logger.LogWarning("No route found to territory {TerritoryId} for {Target}, aborting", territoryId, targetName); return; } list.Add(new MoveTask(territoryId, position, null, MountRequired: false, DismountRequired: false, 3f)); list.Add(new Questionable.Controller.Steps.Common.Mount.UnmountTask()); list.Add(attuneTask); _attunementController.Start(targetName, list); } }