using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; using Dalamud.Utility; using LLib; using Lumina.Excel; using Lumina.Excel.Sheets; using Questionable.Controller; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows.QuestComponents; namespace Questionable.Windows.JournalComponents; internal sealed class QuestChainComponent { private readonly record struct QuestStatusEntry(Vector4 Color, bool IsCompleted); private sealed class CachedGraphLayout { public required List> Layers { get; init; } public required Dictionary NodeWidths { get; init; } public required float[] LayerWidths { get; init; } public required Dictionary TruncatedLabels { get; init; } } private readonly record struct NodeStatusEntry(Vector4 BgColor, bool IsDone); private static readonly string[] ExpansionNames = new string[7] { "All", "A Realm Reborn", "Heavensward", "Stormblood", "Shadowbringers", "Endwalker", "Dawntrail" }; private readonly QuestChainBuilder _questChainBuilder; private readonly QuestData _questData; private readonly QuestFunctions _questFunctions; private readonly UiUtils _uiUtils; private readonly ITextureProvider _textureProvider; private readonly IDataManager _dataManager; private readonly QuestMapComponent _questMapComponent; private readonly QuestRegistry _questRegistry; private readonly IGameGui _gameGui; private readonly QuestTooltipComponent _questTooltipComponent; private string _searchText = string.Empty; private int _selectedExpansion = -1; private bool _showUnobtainable; private ElementId? _selectedQuestId; private List _filteredQuests = new List(); private bool _filterDirty = true; private string _previousSearchText = string.Empty; private int _previousExpansion = -1; private ElementId? _previousChainQuestId; private bool _scrollToSelected; private bool _showDetail; private bool _scrollToSelectedInList; private readonly Dictionary _questStatusCache = new Dictionary(); private long _statusCacheRefreshedAt; private const long StatusCacheLifetimeMs = 3000L; private CachedGraphLayout? _cachedLayout; private QuestId? _cachedLayoutQuestId; private QuestId? _cachedDescriptionQuestId; private string? _cachedDescription; private readonly Dictionary _nodeStatusCache = new Dictionary(); private long _nodeStatusRefreshedAt; private readonly List _addableQuestIds = new List(); private ElementId? _addableCachedForQuestId; private long _addableRefreshedAt; private Vector2 _graphOffset = Vector2.Zero; private bool _isDragging; private Vector2 _dragStart; private Vector2 _dragOrigin; private bool _dragMoved; private QuestId? _contextMenuNodeId; private float _graphZoom = 1f; private const float GraphMinZoom = 0.25f; private const float GraphMaxZoom = 2f; private const float GraphZoomSpeed = 0.1f; private static readonly Vector4 GraphBackgroundColor = new Vector4(0.06f, 0.05f, 0.1f, 1f); private static readonly Vector4 NodeFillActive = Vector4.Lerp(GraphBackgroundColor, UiThemeUtils.StatusActive, 0.48f); private static readonly Vector4 NodeFillComplete = Vector4.Lerp(GraphBackgroundColor, UiThemeUtils.StatusComplete, 0.42f); private static readonly Vector4 NodeFillUnobtainable = Vector4.Lerp(GraphBackgroundColor, UiThemeUtils.StatusUnobtainable, 0.36f); private static readonly Vector4 NodeFillAvailable = Vector4.Lerp(GraphBackgroundColor, UiThemeUtils.StatusAvailable, 0.22f); private static readonly Vector4 NodeFillLocked = Vector4.Lerp(GraphBackgroundColor, UiThemeUtils.StatusLocked, 0.45f); public Action? SelectTabAction { get; set; } public QuestJournalUtils? QuestJournalUtils { get; set; } public QuestChainComponent(QuestChainBuilder questChainBuilder, QuestData questData, QuestFunctions questFunctions, UiUtils uiUtils, ITextureProvider textureProvider, IDataManager dataManager, QuestMapComponent questMapComponent, QuestRegistry questRegistry, IGameGui gameGui, QuestTooltipComponent questTooltipComponent) { _questChainBuilder = questChainBuilder; _questData = questData; _questFunctions = questFunctions; _uiUtils = uiUtils; _textureProvider = textureProvider; _dataManager = dataManager; _questMapComponent = questMapComponent; _questRegistry = questRegistry; _gameGui = gameGui; _questTooltipComponent = questTooltipComponent; } public void DrawQuestChain() { if (_searchText != _previousSearchText || _selectedExpansion != _previousExpansion) { _filterDirty = true; _previousSearchText = _searchText; _previousExpansion = _selectedExpansion; } if (_filterDirty) { RebuildFilteredQuests(); _filterDirty = false; } DrawSearchBar(); ImGui.Separator(); Vector2 contentRegionAvail = ImGui.GetContentRegionAvail(); float num = MathF.Max(contentRegionAvail.X * 0.25f, 180f); float width = contentRegionAvail.X - num - ImGui.GetStyle().ItemSpacing.X; if (_showDetail && _selectedQuestId != null) { DrawDetailPanel(num, contentRegionAvail.Y); } else { DrawQuestList(num, contentRegionAvail.Y); } ImGui.SameLine(); DrawChainPanel(width, contentRegionAvail.Y); } public void SelectQuest(ElementId questId) { _selectedQuestId = questId; _filterDirty = true; _scrollToSelected = true; } public void MarkDirty() { _filterDirty = true; _cachedLayout = null; _questStatusCache.Clear(); } private void DrawSearchBar() { UiThemeUtils.SearchInput("##ChainSearch", ref _searchText, -280f * ImGui.GetIO().FontGlobalScale, "Search quests..."); ImGui.SameLine(); int currentItem = _selectedExpansion + 1; ImGui.SetNextItemWidth(140f * ImGui.GetIO().FontGlobalScale); if (UiThemeUtils.ThemedCombo("##ChainExpansion", ref currentItem, ExpansionNames, ExpansionNames.Length)) { _selectedExpansion = currentItem - 1; } ImGui.SameLine(); if (UiThemeUtils.WrappedCheckbox("Unobtainable", ref _showUnobtainable)) { _filterDirty = true; } } private void RebuildFilteredQuests() { _filteredQuests = (from kvp in _questData.AllQuests where kvp.Key is QuestId select kvp.Value into q where !string.IsNullOrEmpty(q.Name) where q.Expansion != (EExpansionVersion)255 where _showUnobtainable || !_questFunctions.IsQuestUnobtainable(q.QuestId) select q).Where(delegate(IQuestInfo q) { if (_selectedExpansion >= 0 && (uint)q.Expansion != (byte)_selectedExpansion) { return false; } return (string.IsNullOrEmpty(_searchText) || q.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) ? true : false; }).OrderBy((IQuestInfo q) => q.Name, StringComparer.OrdinalIgnoreCase).ToList(); } private QuestStatusEntry GetCachedQuestStatus(ElementId questId) { long tickCount = Environment.TickCount64; if (tickCount - _statusCacheRefreshedAt > 3000) { _questStatusCache.Clear(); _statusCacheRefreshedAt = tickCount; } if (!_questStatusCache.TryGetValue(questId, out var value)) { Vector4 item = _uiUtils.GetQuestStyle(questId).Color; bool isCompleted = _questFunctions.IsQuestAcceptedOrComplete(questId); value = new QuestStatusEntry(item, isCompleted); _questStatusCache[questId] = value; } return value; } private unsafe void DrawQuestList(float width, float height) { using ImRaii.ChildDisposable childDisposable = ImRaii.Child("QuestChainList", new Vector2(width, height), border: true); if (!childDisposable) { return; } if (_filteredQuests.Count == 0) { ImGui.TextColored(in UiThemeUtils.DimTextColor, "No quests match your search."); return; } float textLineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); if (_scrollToSelectedInList && _selectedQuestId != null) { int num = _filteredQuests.FindIndex((IQuestInfo q) => q.QuestId == _selectedQuestId); if (num >= 0) { float y = (float)num * textLineHeightWithSpacing - ImGui.GetWindowHeight() * 0.5f; ImGui.SetScrollY(MathF.Max(0f, y)); } _scrollToSelectedInList = false; } ImGuiListClipper* intPtr = ImGuiNative.ImGuiListClipper(); ImGuiNative.Begin(intPtr, _filteredQuests.Count, textLineHeightWithSpacing); ImGuiListClipperPtr imGuiListClipperPtr = intPtr; try { while (imGuiListClipperPtr.Step()) { for (int num2 = imGuiListClipperPtr.DisplayStart; num2 < imGuiListClipperPtr.DisplayEnd; num2++) { IQuestInfo questInfo = _filteredQuests[num2]; UiThemeUtils.DrawAltRowBackground(num2); bool selected = _selectedQuestId != null && _selectedQuestId == questInfo.QuestId; QuestStatusEntry cachedQuestStatus = GetCachedQuestStatus(questInfo.QuestId); using (cachedQuestStatus.IsCompleted ? ImRaii.PushColor(ImGuiCol.Text, UiThemeUtils.DimTextColor) : null) { ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 14f); if (ImGui.Selectable(UiThemeUtils.CleanSeString(questInfo.Name), selected, ImGuiSelectableFlags.AllowDoubleClick)) { _selectedQuestId = questInfo.QuestId; if (ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left)) { _showDetail = true; } } ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList(); Vector2 itemRectMin = ImGui.GetItemRectMin(); Vector2 itemRectMax = ImGui.GetItemRectMax(); float y2 = (itemRectMin.Y + itemRectMax.Y) * 0.5f; float x = itemRectMin.X - 14f + 5f; windowDrawList.AddCircleFilled(new Vector2(x, y2), 3f, ImGui.ColorConvertFloat4ToU32(cachedQuestStatus.Color), 12); Questionable.Model.Quest quest = null; _questRegistry.TryGetQuest(questInfo.QuestId, out quest); QuestJournalUtils?.ShowContextMenu(questInfo, quest, "QuestChainComponent"); } } } } finally { imGuiListClipperPtr.Destroy(); } } private void DrawChainPanel(float width, float height) { using ImRaii.ChildDisposable childDisposable = ImRaii.Child("QuestChainView", new Vector2(width, height), border: true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); if (!childDisposable) { return; } if (_selectedQuestId == null) { ImGui.TextColored(in UiThemeUtils.DimTextColor, "Select a quest to view its chain"); return; } if (!(_selectedQuestId is QuestId questId)) { ImGui.TextColored(in UiThemeUtils.DimTextColor, "Chain view is only available for quests."); return; } if (!_questData.TryGetQuestInfo(_selectedQuestId, out IQuestInfo _)) { ImGui.TextColored(in UiThemeUtils.StatusLocked, "Quest info not found."); return; } if (_previousChainQuestId != _selectedQuestId) { _previousChainQuestId = _selectedQuestId; _scrollToSelected = true; } QuestChainGraph chainGraph = _questChainBuilder.GetChainGraph(questId); if (chainGraph.LayerOrder.Count == 0) { ImGui.TextColored(in UiThemeUtils.DimTextColor, "No chain data available."); return; } if (UiThemeUtils.PillButton("Reset View", selected: false)) { _scrollToSelected = true; _graphZoom = 1f; } ImGui.SameLine(); ImU8String text = new ImU8String(1, 1); text.AppendFormatted((int)(_graphZoom * 100f)); text.AppendLiteral("%"); ImGui.TextColored(in UiThemeUtils.MutedTextColor, text); DrawChainGraph(chainGraph, width); } private void DrawDetailPanel(float width, float height) { using ImRaii.ChildDisposable childDisposable = ImRaii.Child("QuestDetailPanel", new Vector2(width, height), border: true); if (!childDisposable) { return; } if (!(_selectedQuestId is QuestId questId) || !_questData.TryGetQuestInfo(_selectedQuestId, out IQuestInfo questInfo)) { _showDetail = false; return; } if (UiThemeUtils.PillButton("← Back to list", selected: false)) { _showDetail = false; _scrollToSelectedInList = true; return; } ImGui.Separator(); UiThemeUtils.WrappedTextColored(UiThemeUtils.AccentColor, UiThemeUtils.CleanSeString(questInfo.Name)); ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); try { Lumina.Excel.Sheets.Quest? rowOrDefault = _dataManager.GetExcelSheet().GetRowOrDefault((uint)(questId.Value + 65536)); ImU8String text = new ImU8String(7, 1); text.AppendLiteral("Level: "); text.AppendFormatted(questInfo.Level); ImGui.TextColored(in UiThemeUtils.DimTextColor, text); if (rowOrDefault.HasValue && rowOrDefault.Value.Icon != 0) { DrawQuestBanner(rowOrDefault.Value.Icon); } if (rowOrDefault.HasValue) { ushort expFactor = rowOrDefault.Value.ExpFactor; uint gilReward = rowOrDefault.Value.GilReward; if (expFactor > 0) { ImU8String text2 = new ImU8String(5, 1); text2.AppendLiteral("Exp: "); text2.AppendFormatted(expFactor, "N0"); ImGui.TextUnformatted(text2); } if (gilReward != 0) { if (expFactor > 0) { ImGui.SameLine(0f, 20f); } ImU8String text3 = new ImU8String(5, 1); text3.AppendLiteral("Gil: "); text3.AppendFormatted(gilReward, "N0"); ImGui.TextUnformatted(text3); } DrawRewards(rowOrDefault.Value); } DrawQuestDescription(questId, rowOrDefault); DrawNpcLine(rowOrDefault, questInfo); ImU8String text4 = new ImU8String(4, 1); text4.AppendLiteral("ID: "); text4.AppendFormatted(questId.Value); ImGui.TextColored(in UiThemeUtils.MutedTextColor, text4); ImGui.Spacing(); ushort num = 0; Vector3 vector = Vector3.Zero; if (_questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest)) { QuestSequence questSequence = quest.FindSequence(0); if (questSequence != null) { List steps = questSequence.Steps; if (steps != null && steps.Count > 0) { num = questSequence.Steps[0].TerritoryId; Vector3? position = questSequence.Steps[0].Position; if (position.HasValue) { Vector3 valueOrDefault = position.GetValueOrDefault(); vector = valueOrDefault; } } } } if (num == 0 && rowOrDefault.HasValue) { try { Level? valueNullable = rowOrDefault.Value.IssuerLocation.ValueNullable; if (valueNullable.HasValue) { num = (ushort)valueNullable.Value.Territory.RowId; vector = new Vector3(valueNullable.Value.X, valueNullable.Value.Y, valueNullable.Value.Z); } } catch { } } if (num != 0 && vector != Vector3.Zero && UiThemeUtils.PillButton("Open on Map", selected: false)) { TerritoryType? rowOrDefault2 = _dataManager.GetExcelSheet().GetRowOrDefault(num); if (rowOrDefault2.HasValue) { Map? valueNullable2 = rowOrDefault2.Value.Map.ValueNullable; if (valueNullable2.HasValue) { Vector2 vector2 = MapUtil.WorldToMap(new Vector2(vector.X, vector.Z), valueNullable2.Value); MapLinkPayload mapLink = new MapLinkPayload(num, valueNullable2.Value.RowId, vector2.X, vector2.Y); _gameGui.OpenMapWithMapLink(mapLink); } } } if (num != 0 && UiThemeUtils.PillButton("Show in Quest Map", selected: false)) { _questMapComponent.SelectQuest(questId, num); SelectTabAction?.Invoke("Quest Map"); } QuestJournalUtils questJournalUtils = QuestJournalUtils; if (questJournalUtils != null) { long tickCount = Environment.TickCount64; if (!questId.Equals(_addableCachedForQuestId) || tickCount - _addableRefreshedAt > 3000) { _addableQuestIds.Clear(); _addableQuestIds.AddRange(questJournalUtils.GetIncompletePrerequisiteQuests(questInfo)); _addableCachedForQuestId = questId; _addableRefreshedAt = tickCount; } int count = _addableQuestIds.Count; using (ImRaii.Disabled(count == 0)) { if (UiThemeUtils.PillButton("Add to Priority", selected: false)) { questJournalUtils.AddRequiredQuestsToPriority(questInfo, _addableQuestIds); _addableCachedForQuestId = null; } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { object obj2; switch (count) { case 0: ImGui.SetTooltip("No quests to add (quest may be complete, disabled, locked with no unlock path, or already in priority)"); goto end_IL_00a4; default: obj2 = $"Add this quest and {count - 1} required {((count - 1 == 1) ? "quest" : "quests")} to the priority list in completion order"; break; case 1: obj2 = "Add this quest to the priority list"; break; } ImGui.SetTooltip((string?)obj2); } } end_IL_00a4:; } catch { } ImGui.PopTextWrapPos(); } private void DrawQuestDescription(QuestId questId, Lumina.Excel.Sheets.Quest? questEx) { if (questId.Equals(_cachedDescriptionQuestId)) { if (!string.IsNullOrWhiteSpace(_cachedDescription)) { ImGui.Spacing(); ImGui.TextColored(in UiThemeUtils.DimTextColor, _cachedDescription); } return; } _cachedDescriptionQuestId = questId; _cachedDescription = null; try { if (!questEx.HasValue) { return; } string value = questEx.Value.Id.ExtractText(); if (string.IsNullOrEmpty(value)) { return; } string text = $"quest/{questId.Value / 100:000}/{value}"; IDataManager dataManager = _dataManager; string name = text; ExcelSheet excelSheet = dataManager.GetExcelSheet(null, name); string text2 = null; foreach (QuestDialogueText item in excelSheet) { string text3 = item.Key.ExtractText(); if (text3.Contains("SEQ_00", StringComparison.Ordinal) && text3.Contains("BODY", StringComparison.Ordinal)) { string text4 = item.Value.ExtractText(); if (!string.IsNullOrWhiteSpace(text4)) { text2 = text4; break; } } } if (text2 == null) { foreach (QuestDialogueText item2 in excelSheet) { string text5 = item2.Key.ExtractText(); if (text5.Contains("TODO_01", StringComparison.Ordinal) || text5.Contains("OBJECTIVE", StringComparison.Ordinal)) { string text6 = item2.Value.ExtractText(); if (!string.IsNullOrWhiteSpace(text6)) { text2 = text6; break; } } } } if (!string.IsNullOrWhiteSpace(text2)) { _cachedDescription = text2; ImGui.Spacing(); ImGui.TextColored(in UiThemeUtils.DimTextColor, text2); } } catch { } } private void DrawNpcLine(Lumina.Excel.Sheets.Quest? questEx, IQuestInfo questInfo) { string text = null; string text2 = null; try { if (questEx.HasValue) { uint rowId = questEx.Value.IssuerStart.RowId; if (rowId != 0) { text = ResolveNpcName(rowId); } } } catch { } if (text == null && questInfo.IssuerDataId != 0) { try { text = ResolveNpcName(questInfo.IssuerDataId); } catch { } } try { if (questEx.HasValue) { uint rowId2 = questEx.Value.TargetEnd.RowId; if (rowId2 != 0) { text2 = ResolveNpcName(rowId2); } } } catch { } if (text != null || text2 != null) { if (text != null && text2 != null) { ImU8String text3 = new ImU8String(3, 2); text3.AppendFormatted(text); text3.AppendLiteral(" → "); text3.AppendFormatted(text2); ImGui.TextColored(in UiThemeUtils.DimTextColor, text3); } else if (text != null) { ImGui.TextColored(in UiThemeUtils.DimTextColor, text); } else { ImU8String text4 = new ImU8String(2, 1); text4.AppendLiteral("→ "); text4.AppendFormatted(text2); ImGui.TextColored(in UiThemeUtils.DimTextColor, text4); } } } private string? ResolveNpcName(uint npcId) { ENpcResident? rowOrDefault = _dataManager.GetExcelSheet().GetRowOrDefault(npcId); if (rowOrDefault.HasValue) { string text = rowOrDefault.Value.Singular.ExtractText(); if (!string.IsNullOrEmpty(text)) { return text; } } return null; } private void DrawRewards(Lumina.Excel.Sheets.Quest questEx) { bool flag; switch (questEx.ItemRewardType) { case 1: case 3: case 5: flag = true; break; default: flag = false; break; } if (flag) { bool flag2 = false; for (int i = 0; i < questEx.Reward.Count; i++) { try { RowRef rowRef = questEx.Reward[i]; if (rowRef.RowId == 0 || !rowRef.Is()) { continue; } Item? valueOrDefault = rowRef.GetValueOrDefault(); if (valueOrDefault.HasValue) { if (!flag2) { UiThemeUtils.SubSectionHeader("Rewards"); flag2 = true; } else { ImGui.SameLine(); } byte count = (byte)((i >= questEx.ItemCountReward.Count) ? 1 : questEx.ItemCountReward[i]); DrawItemIcon(valueOrDefault.Value, count); } } catch { } } } bool flag3 = false; for (int j = 0; j < questEx.OptionalItemReward.Count; j++) { try { RowRef rowRef2 = questEx.OptionalItemReward[j]; if (rowRef2.RowId == 0) { continue; } Item? valueNullable = rowRef2.ValueNullable; if (valueNullable.HasValue) { if (!flag3) { UiThemeUtils.SubSectionHeader("Optional rewards"); flag3 = true; } else { ImGui.SameLine(); } byte count2 = (byte)((j >= questEx.OptionalItemCountReward.Count) ? 1 : questEx.OptionalItemCountReward[j]); DrawItemIcon(valueNullable.Value, count2); } } catch { } } } private void DrawItemIcon(Item item, byte count) { try { ushort icon = item.Icon; if (icon > 0) { string path = $"ui/icon/{icon / 1000 * 1000:000000}/{icon:000000}_hr1.tex"; IDalamudTextureWrap wrapOrDefault = _textureProvider.GetFromGame(path).GetWrapOrDefault(); if (wrapOrDefault != null) { ImGui.Image(wrapOrDefault.Handle, new Vector2(32f, 32f)); bool flag = ImGui.IsItemHovered(); if (count > 1) { ImGui.SameLine(0f, 2f); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 16f); ImU8String text = new ImU8String(1, 1); text.AppendLiteral("x"); text.AppendFormatted(count); ImGui.TextColored(in UiThemeUtils.DimTextColor, text); flag |= ImGui.IsItemHovered(); } if (flag) { ImGui.BeginTooltip(); string text2 = item.Name.ExtractText(); ImGui.TextUnformatted((count > 1) ? $"{text2} x{count}" : text2); ImGui.EndTooltip(); } return; } } } catch { } string text3 = item.Name.ExtractText(); ImGui.TextUnformatted((count > 1) ? $"[{text3} x{count}]" : ("[" + text3 + "]")); if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); ImGui.TextUnformatted(text3); ImGui.EndTooltip(); } } private void DrawQuestBanner(uint iconId) { if (iconId == 0) { return; } try { string path = $"ui/icon/{iconId / 1000 * 1000:000000}/{iconId:000000}_hr1.tex"; IDalamudTextureWrap wrapOrDefault = _textureProvider.GetFromGame(path).GetWrapOrDefault(); if (wrapOrDefault != null) { float num = MathF.Min(ImGui.GetContentRegionAvail().X, (float)wrapOrDefault.Width * 0.5f); float num2 = (float)wrapOrDefault.Height / (float)wrapOrDefault.Width; float y = num * num2; ImGui.Image(wrapOrDefault.Handle, new Vector2(num, y)); } } catch { } } private CachedGraphLayout ComputeLayout(QuestChainGraph graph) { List> list = ReorderLayers(graph); for (int i = 0; i < list.Count; i++) { list[i] = list[i].Where((QuestId qid) => _questData.TryGetQuestInfo(qid, out IQuestInfo _)).ToList(); } list.RemoveAll((List l) => l.Count == 0); Dictionary dictionary = new Dictionary(); foreach (List item in list) { foreach (QuestId item2 in item) { IQuestInfo questInfo; float y = (_questData.TryGetQuestInfo(item2, out questInfo) ? (ImGui.CalcTextSize(UiThemeUtils.CleanSeString(questInfo.Name)).X + 16f) : 150f); dictionary[item2] = MathF.Max(150f, MathF.Min(240f, y)); } } float[] array = new float[list.Count]; for (int num = 0; num < list.Count; num++) { float num2 = 0f; for (int num3 = 0; num3 < list[num].Count; num3++) { if (num3 > 0) { num2 += 20f; } num2 += dictionary[list[num][num3]]; } array[num] = num2; } Dictionary dictionary2 = new Dictionary(); foreach (List item3 in list) { foreach (QuestId item4 in item3) { if (!_questData.TryGetQuestInfo(item4, out IQuestInfo questInfo2)) { continue; } string text = UiThemeUtils.CleanSeString(questInfo2.Name); float num4 = dictionary[item4]; float num5 = 8f; if (ImGui.CalcTextSize(text).X + num5 > num4) { for (int num6 = text.Length - 1; num6 > 3; num6--) { string text2 = text.Substring(0, num6) + "..."; if (ImGui.CalcTextSize(text2).X + num5 <= num4) { text = text2; break; } } } dictionary2[item4] = text; } } return new CachedGraphLayout { Layers = list, NodeWidths = dictionary, LayerWidths = array, TruncatedLabels = dictionary2 }; } private void RefreshNodeStatusCache() { _nodeStatusCache.Clear(); if (_cachedLayout == null) { return; } foreach (List layer in _cachedLayout.Layers) { foreach (QuestId item in layer) { Vector4 questNodeColor = GetQuestNodeColor(item); bool isDone = _questFunctions.IsQuestAcceptedOrComplete(item); _nodeStatusCache[item] = new NodeStatusEntry(questNodeColor, isDone); } } _nodeStatusRefreshedAt = Environment.TickCount64; } private void DrawChainGraph(QuestChainGraph graph, float panelWidth) { float graphZoom = _graphZoom; ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList(); Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); Vector2 contentRegionAvail = ImGui.GetContentRegionAvail(); if (contentRegionAvail.X < 1f) { contentRegionAvail.X = 1f; } if (contentRegionAvail.Y < 1f) { contentRegionAvail.Y = 1f; } ImGui.InvisibleButton("##chainCanvas", contentRegionAvail, ImGuiButtonFlags.MouseButtonMask); bool flag = ImGui.IsItemHovered(); bool flag2 = ImGui.IsItemActive(); Vector2 mousePos = ImGui.GetMousePos(); windowDrawList.PushClipRect(cursorScreenPos, cursorScreenPos + contentRegionAvail, intersectWithCurrentClipRect: true); windowDrawList.AddRectFilled(cursorScreenPos, cursorScreenPos + contentRegionAvail, ImGui.ColorConvertFloat4ToU32(GraphBackgroundColor)); float mouseWheel = ImGui.GetIO().MouseWheel; if (flag && MathF.Abs(mouseWheel) > 0.01f) { float graphZoom2 = _graphZoom; _graphZoom = Math.Clamp(_graphZoom + mouseWheel * 0.1f, 0.25f, 2f); Vector2 vector = mousePos - cursorScreenPos - contentRegionAvail * 0.5f - _graphOffset; _graphOffset -= vector * (_graphZoom / graphZoom2 - 1f); graphZoom = _graphZoom; } if (flag2) { if (!_isDragging) { _isDragging = true; _dragStart = mousePos; _dragOrigin = mousePos; } if (Vector2.Distance(mousePos, _dragOrigin) > 4f) { _dragMoved = true; } Vector2 vector2 = mousePos - _dragStart; _graphOffset += vector2; _dragStart = mousePos; } else { _isDragging = false; } if (_cachedLayout == null || _cachedLayoutQuestId != graph.SelectedQuest) { _cachedLayout = ComputeLayout(graph); _cachedLayoutQuestId = graph.SelectedQuest; RefreshNodeStatusCache(); } Vector2 vector3 = cursorScreenPos + contentRegionAvail * 0.5f; List> layers = _cachedLayout.Layers; Dictionary nodeWidths = _cachedLayout.NodeWidths; float[] layerWidths = _cachedLayout.LayerWidths; if (Environment.TickCount64 - _nodeStatusRefreshedAt > 3000) { RefreshNodeStatusCache(); } if (_scrollToSelected) { _scrollToSelected = false; for (int i = 0; i < layers.Count; i++) { int num = layers[i].IndexOf(graph.SelectedQuest); if (num >= 0) { float num2 = (0f - layerWidths[i]) * 0.5f; for (int j = 0; j < num; j++) { num2 += nodeWidths[layers[i][j]] + 20f; } float num3 = nodeWidths[graph.SelectedQuest]; float num4 = (float)i * 81f; _graphOffset = -new Vector2(num2 + num3 * 0.5f, num4 + 13f) * graphZoom; break; } } } Dictionary nodePos = new Dictionary(); for (int k = 0; k < layers.Count; k++) { List list = layers[k]; float num5 = (0f - layerWidths[k]) * 0.5f; float num6 = (float)k * 81f; foreach (QuestId item11 in list) { float num7 = nodeWidths[item11]; Vector2 item = vector3 + new Vector2(num5, num6) * graphZoom + _graphOffset; Vector2 item2 = vector3 + new Vector2(num5 + num7, num6 + 26f) * graphZoom + _graphOffset; nodePos[item11] = (item, item2); num5 += num7 + 20f; } } QuestId questId = null; bool dragMoved = _dragMoved; QuestId key; if (flag && !dragMoved) { foreach (KeyValuePair item12 in nodePos) { item12.Deconstruct(out key, out var value); QuestId questId2 = key; (Vector2, Vector2) tuple = value; if (mousePos.X >= tuple.Item1.X && mousePos.X <= tuple.Item2.X && mousePos.Y >= tuple.Item1.Y && mousePos.Y <= tuple.Item2.Y) { questId = questId2; break; } } } QuestId questId3 = questId ?? graph.SelectedQuest; Dictionary<(QuestId, QuestId), (float, float)> dictionary = new Dictionary<(QuestId, QuestId), (float, float)>(); Dictionary> dictionary2 = new Dictionary>(); Dictionary> dictionary3 = new Dictionary>(); List value2; foreach (KeyValuePair> prerequisite in graph.Prerequisites) { prerequisite.Deconstruct(out key, out value2); QuestId questId4 = key; foreach (QuestId item13 in value2) { if (nodePos.ContainsKey(item13) && nodePos.ContainsKey(questId4)) { if (!dictionary2.TryGetValue(item13, out var value3)) { value3 = (dictionary2[item13] = new List()); } value3.Add(questId4); if (!dictionary3.TryGetValue(questId4, out var value4)) { value4 = (dictionary3[questId4] = new List()); } value4.Add(item13); } } } foreach (KeyValuePair> item14 in dictionary2) { item14.Deconstruct(out key, out value2); QuestId item3 = key; List list4 = value2; list4.Sort(delegate(QuestId a, QuestId b) { float num26 = (nodePos[a].Min.X + nodePos[a].Max.X) * 0.5f; float value9 = (nodePos[b].Min.X + nodePos[b].Max.X) * 0.5f; return num26.CompareTo(value9); }); for (int num8 = 0; num8 < list4.Count; num8++) { float item4 = ((list4.Count > 1) ? ((float)num8 / (float)(list4.Count - 1)) : 0.5f); dictionary[(item3, list4[num8])] = (item4, 0.5f); } } foreach (KeyValuePair> item15 in dictionary3) { item15.Deconstruct(out key, out value2); QuestId item5 = key; List list5 = value2; list5.Sort(delegate(QuestId a, QuestId b) { float num26 = (nodePos[a].Min.X + nodePos[a].Max.X) * 0.5f; float value9 = (nodePos[b].Min.X + nodePos[b].Max.X) * 0.5f; return num26.CompareTo(value9); }); for (int num9 = 0; num9 < list5.Count; num9++) { float item6 = ((list5.Count > 1) ? ((float)num9 / (float)(list5.Count - 1)) : 0.5f); (QuestId, QuestId) key2 = (list5[num9], item5); dictionary[key2] = (dictionary.GetValueOrDefault(key2, (0.5f, 0.5f)).Item1, item6); } } Vector4 accentColor = UiThemeUtils.AccentColor; accentColor.W = 0.9f; uint num10 = ImGui.ColorConvertFloat4ToU32(accentColor); float size = 6f * graphZoom; for (int num11 = 0; num11 < 2; num11++) { foreach (KeyValuePair> prerequisite2 in graph.Prerequisites) { prerequisite2.Deconstruct(out key, out value2); QuestId questId5 = key; List list6 = value2; if (!nodePos.TryGetValue(questId5, out (Vector2, Vector2) value5)) { continue; } foreach (QuestId item16 in list6) { if (!nodePos.TryGetValue(item16, out (Vector2, Vector2) value6)) { continue; } bool flag3 = questId3 != null && (questId5 == questId3 || item16 == questId3); if (num11 == 0 == flag3) { continue; } uint num12; float thickness; if (flag3) { num12 = num10; thickness = 2.5f; } else if (questId3 != null) { num12 = ImGui.ColorConvertFloat4ToU32(HueToColor((float)(int)item16.Value * 0.618034f % 1f, 0.15f, 0.35f)); thickness = 1.5f; } else { num12 = ImGui.ColorConvertFloat4ToU32(HueToColor((float)(int)item16.Value * 0.618034f % 1f, 0.35f, 0.85f)); thickness = 2f; } (float, float) valueOrDefault = dictionary.GetValueOrDefault((item16, questId5), (0.5f, 0.5f)); float item7 = valueOrDefault.Item1; float item8 = valueOrDefault.Item2; float num13 = value6.Item2.X - value6.Item1.X; float num14 = value5.Item2.X - value5.Item1.X; float num15 = 10f * graphZoom; float x = value6.Item1.X + num15 + (num13 - num15 * 2f) * item7; float x2 = value5.Item1.X + num15 + (num14 - num15 * 2f) * item8; Vector2 vector4 = new Vector2(x, value6.Item2.Y); Vector2 vector5 = new Vector2(x2, value5.Item1.Y); int num16 = graph.Layers.GetValueOrDefault(questId5) - graph.Layers.GetValueOrDefault(item16); if (num16 > 1) { int valueOrDefault2 = graph.Layers.GetValueOrDefault(item16); float num17 = 0f; for (int num18 = valueOrDefault2 + 1; num18 < valueOrDefault2 + num16 && num18 < layerWidths.Length; num18++) { num17 = MathF.Max(num17, layerWidths[num18] * 0.5f); } float num19 = vector3.X + _graphOffset.X; float x3 = (vector4.X + vector5.X) * 0.5f - num19; float num20 = ((MathF.Abs(x3) > 1f) ? ((float)MathF.Sign(x3)) : ((item16.Value % 2 == 0) ? 1f : (-1f))); float x4 = num19 + num20 * (num17 + 40f + (float)(item16.Value % 3) * 16f) * graphZoom; float num21 = MathF.Min(30f * graphZoom, (vector5.Y - vector4.Y) * 0.2f); Vector2 p = new Vector2(x4, vector4.Y + num21 * 2.5f); Vector2 pos = new Vector2(x4, MathF.Max(vector5.Y - num21 * 2.5f, vector4.Y + num21 * 2.5f)); windowDrawList.PathLineTo(vector4); windowDrawList.PathBezierCubicCurveTo(new Vector2(vector4.X, vector4.Y + num21 * 1.5f), new Vector2(x4, vector4.Y + num21), p, 20); windowDrawList.PathLineTo(pos); windowDrawList.PathBezierCubicCurveTo(new Vector2(x4, vector5.Y - num21), new Vector2(vector5.X, vector5.Y - num21 * 1.5f), vector5, 20); windowDrawList.PathStroke(num12, ImDrawFlags.None, thickness); DrawArrowhead(windowDrawList, vector5, new Vector2(vector5.X, vector5.Y - num21), size, num12); } else { float num22 = (vector5.Y - vector4.Y) * 0.4f; Vector2 p2 = new Vector2(vector4.X, vector4.Y + num22); Vector2 vector6 = new Vector2(vector5.X, vector5.Y - num22); windowDrawList.AddBezierCubic(vector4, p2, vector6, vector5, num12, thickness, 28); DrawArrowhead(windowDrawList, vector5, vector6, size, num12); } } } } float fontSize = ImGui.GetFontSize() * graphZoom; ImFontPtr font = ImGui.GetFont(); foreach (List item17 in layers) { foreach (QuestId item18 in item17) { if (nodePos.TryGetValue(item18, out (Vector2, Vector2) value7)) { bool num23 = item18 == graph.SelectedQuest; bool flag4 = questId == item18; NodeStatusEntry valueOrDefault3 = _nodeStatusCache.GetValueOrDefault(item18, new NodeStatusEntry(NodeFillLocked, IsDone: false)); Vector4 vector7 = valueOrDefault3.BgColor; bool isDone = valueOrDefault3.IsDone; if (isDone) { accentColor = GraphBackgroundColor; accentColor.W = 1f; Vector4 value8 = accentColor; accentColor = vector7; accentColor.W = 1f; vector7 = Vector4.Lerp(value8, accentColor, 0.5f); } windowDrawList.AddRectFilled(value7.Item1, value7.Item2, ImGui.ColorConvertFloat4ToU32(vector7), 5f); if (num23) { windowDrawList.AddRect(value7.Item1 - Vector2.One, value7.Item2 + Vector2.One, ImGui.ColorConvertFloat4ToU32(UiThemeUtils.AccentColor), 5f, ImDrawFlags.None, 3f); } else if (flag4) { Vector2 item9 = value7.Item1; Vector2 item10 = value7.Item2; accentColor = UiThemeUtils.AccentColor; accentColor.W = 0.6f; windowDrawList.AddRect(item9, item10, ImGui.ColorConvertFloat4ToU32(accentColor), 5f, ImDrawFlags.None, 1.5f); } string valueOrDefault4 = _cachedLayout.TruncatedLabels.GetValueOrDefault(item18, item18.ToString()); float num24 = value7.Item2.X - value7.Item1.X; float num25 = value7.Item2.Y - value7.Item1.Y; Vector2 vector8 = ImGui.CalcTextSize(valueOrDefault4) * graphZoom; Vector2 pos2 = new Vector2(value7.Item1.X + (num24 - vector8.X) * 0.5f, value7.Item1.Y + (num25 - vector8.Y) * 0.5f); uint col = (isDone ? ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.5f)) : ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 1f))); windowDrawList.AddText(font, fontSize, pos2, col, valueOrDefault4); } } } windowDrawList.PopClipRect(); if (questId != null && ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !dragMoved) { _selectedQuestId = questId; _showDetail = true; } if (questId != null && ImGui.IsMouseReleased(ImGuiMouseButton.Right) && !dragMoved && _questData.TryGetQuestInfo(questId, out IQuestInfo questInfo)) { _contextMenuNodeId = questId; QuestJournalUtils?.OpenContextMenu(questInfo); } if (!flag2) { _dragMoved = false; } if (questId != null && _questData.TryGetQuestInfo(questId, out IQuestInfo questInfo2)) { _questTooltipComponent.Draw(questInfo2); } DrawNodeContextMenu(); static Vector4 HueToColor(float hue, float sat, float alpha) { float num26 = hue * 6f; float num27 = sat * (1f - MathF.Abs(num26 % 2f - 1f)); float num28; float num29; float num30; if (num26 < 1f) { num28 = sat; num29 = num27; num30 = 0f; } else if (num26 < 2f) { num28 = num27; num29 = sat; num30 = 0f; } else if (num26 < 3f) { num28 = 0f; num29 = sat; num30 = num27; } else if (num26 < 4f) { num28 = 0f; num29 = num27; num30 = sat; } else if (num26 < 5f) { num28 = num27; num29 = 0f; num30 = sat; } else { num28 = sat; num29 = 0f; num30 = num27; } float num31 = 1f - sat; return new Vector4(num28 + num31, num29 + num31, num30 + num31, alpha); } } private void DrawNodeContextMenu() { QuestId contextMenuNodeId = _contextMenuNodeId; if ((object)contextMenuNodeId == null) { return; } QuestJournalUtils questJournalUtils = QuestJournalUtils; if (questJournalUtils == null) { return; } if (!_questData.TryGetQuestInfo(contextMenuNodeId, out IQuestInfo questInfo)) { _contextMenuNodeId = null; return; } Questionable.Model.Quest quest = null; _questRegistry.TryGetQuest(contextMenuNodeId, out quest); if (!questJournalUtils.DrawContextMenu(questInfo, quest, "QuestChainComponent")) { _contextMenuNodeId = null; } } private static List> ReorderLayers(QuestChainGraph graph) { List> list = new List>(); for (int i = 0; i < graph.LayerOrder.Count; i++) { list.Add(new List(graph.LayerOrder[i])); } if (list.Count <= 1) { return list; } Dictionary> dependents = new Dictionary>(); foreach (var (item, list3) in graph.Prerequisites) { foreach (QuestId item2 in list3) { if (!dependents.TryGetValue(item2, out List value)) { value = new List(); dependents[item2] = value; } value.Add(item); } } for (int j = 0; j < 6; j++) { if (j % 2 == 0) { for (int k = 1; k < list.Count; k++) { List referenceLayer = list[k - 1]; Dictionary bary = ComputeBarycenters(list[k], referenceLayer, (QuestId qid) => graph.Prerequisites.GetValueOrDefault(qid)); list[k].Sort((QuestId a, QuestId b) => bary.GetValueOrDefault(a).CompareTo(bary.GetValueOrDefault(b))); } continue; } for (int num = list.Count - 2; num >= 0; num--) { List referenceLayer2 = list[num + 1]; Dictionary bary2 = ComputeBarycenters(list[num], referenceLayer2, (QuestId qid) => dependents.GetValueOrDefault(qid)); list[num].Sort((QuestId a, QuestId b) => bary2.GetValueOrDefault(a).CompareTo(bary2.GetValueOrDefault(b))); } } for (int num2 = 0; num2 < 3; num2++) { bool flag = false; for (int num3 = 0; num3 < list.Count; num3++) { List list4 = list[num3]; if (list4.Count < 2) { continue; } List adjAbove = ((num3 > 0) ? list[num3 - 1] : null); List adjBelow = ((num3 < list.Count - 1) ? list[num3 + 1] : null); for (int num4 = 0; num4 < list4.Count - 1; num4++) { int num5 = CountCrossings(list4, adjAbove, adjBelow, graph.Prerequisites, dependents); List list5 = list4; int index = num4; int index2 = num4 + 1; QuestId value2 = list4[num4 + 1]; QuestId value3 = list4[num4]; list5[index] = value2; list4[index2] = value3; if (CountCrossings(list4, adjAbove, adjBelow, graph.Prerequisites, dependents) < num5) { flag = true; continue; } list5 = list4; index2 = num4; index = num4 + 1; value3 = list4[num4 + 1]; value2 = list4[num4]; list5[index2] = value3; list4[index] = value2; } } if (!flag) { break; } } return list; } private static int CountCrossings(List layer, List? adjAbove, List? adjBelow, Dictionary> prerequisites, Dictionary> dependents) { int num = 0; if (adjAbove != null) { num += CountLayerCrossings(adjAbove, layer, (QuestId qid) => prerequisites.GetValueOrDefault(qid)); } if (adjBelow != null) { num += CountLayerCrossings(layer, adjBelow, (QuestId qid) => dependents.GetValueOrDefault(qid)); } return num; } private static int CountLayerCrossings(List upperLayer, List lowerLayer, Func?> getUpperConnections) { int num = 0; for (int i = 0; i < lowerLayer.Count; i++) { List list = getUpperConnections(lowerLayer[i]); if (list == null) { continue; } for (int j = i + 1; j < lowerLayer.Count; j++) { List list2 = getUpperConnections(lowerLayer[j]); if (list2 == null) { continue; } foreach (QuestId item in list) { int num2 = upperLayer.IndexOf(item); if (num2 < 0) { continue; } foreach (QuestId item2 in list2) { int num3 = upperLayer.IndexOf(item2); if (num3 >= 0 && num2 > num3) { num++; } } } } } return num; } private static Dictionary ComputeBarycenters(List layer, List referenceLayer, Func?> getConnected) { Dictionary dictionary = new Dictionary(); foreach (QuestId item in layer) { List list = getConnected(item); if (list != null) { List list2 = (from p in list select referenceLayer.IndexOf(p) into idx where idx >= 0 select idx).ToList(); dictionary[item] = ((list2.Count > 0) ? ((float)list2.Average()) : ((float)referenceLayer.Count * 0.5f)); } else { dictionary[item] = (float)referenceLayer.Count * 0.5f; } } return dictionary; } private static void DrawArrowhead(ImDrawListPtr drawList, Vector2 tip, Vector2 controlPt, float size, uint color) { if (!(size <= 1.5f)) { Vector2 vector = Vector2.Normalize(tip - controlPt); Vector2 vector2 = new Vector2(0f - vector.Y, vector.X); Vector2 vector3 = tip - vector * size; drawList.AddTriangleFilled(tip, vector3 - vector2 * size * 0.45f, vector3 + vector2 * size * 0.45f, color); } } private Vector4 GetQuestNodeColor(QuestId questId) { if (_questFunctions.IsQuestAccepted(questId)) { return NodeFillActive; } if (_questFunctions.IsQuestComplete(questId)) { return NodeFillComplete; } if (_questFunctions.IsQuestUnobtainable(questId)) { return NodeFillUnobtainable; } if (!_questFunctions.IsQuestLocked(questId) && _questFunctions.IsReadyToAcceptQuest(questId)) { return NodeFillAvailable; } return NodeFillLocked; } }