muffin v6.12

This commit is contained in:
alydev 2025-10-09 07:53:51 +10:00
parent cfb4dea47e
commit c8197297b2
58 changed files with 40038 additions and 58059 deletions

View file

@ -55,7 +55,7 @@ internal sealed class ActiveQuestComponent
[GeneratedCode("System.Text.RegularExpressions.Generator", "9.0.12.41916")]
private static Regex MultipleWhitespaceRegex()
{
return _003CRegexGenerator_g_003EFF909AF37F4C319C8940E7DA0E71D9E470824ECE485FE299B23B08984F5D534F6__MultipleWhitespaceRegex_0.Instance;
return _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0.Instance;
}
public ActiveQuestComponent(QuestController questController, MovementController movementController, CombatController combatController, GatheringController gatheringController, QuestFunctions questFunctions, ICommandManager commandManager, Configuration configuration, QuestRegistry questRegistry, PriorityWindow priorityWindow, UiUtils uiUtils, IClientState clientState, IChatGui chatGui, ILogger<ActiveQuestComponent> logger)
@ -236,7 +236,8 @@ internal sealed class ActiveQuestComponent
}
bool flag = _configuration.Stop.Enabled && _configuration.Stop.LevelToStopAfter;
bool flag2 = _configuration.Stop.Enabled && _configuration.Stop.QuestsToStopAfter.Any((ElementId x) => !_questFunctions.IsQuestComplete(x) && !_questFunctions.IsQuestUnobtainable(x));
if (flag || flag2)
bool flag3 = _configuration.Stop.Enabled && _configuration.Stop.SequenceToStopAfter;
if (flag || flag2 || flag3)
{
ImGui.SameLine();
Vector4 col = ImGuiColors.ParsedPurple;
@ -252,6 +253,10 @@ internal sealed class ActiveQuestComponent
col = ImGuiColors.ParsedBlue;
}
}
if (flag3)
{
col = ((startedQuest.Sequence < _configuration.Stop.TargetSequence) ? ImGuiColors.ParsedBlue : ImGuiColors.ParsedGreen);
}
ImGui.TextColored(in col, SeIconChar.Clock.ToIconString());
if (ImGui.IsItemHovered())
{
@ -290,12 +295,43 @@ internal sealed class ActiveQuestComponent
}
}
}
if (flag2)
if (flag3)
{
if (flag)
{
ImGui.Spacing();
}
int sequence = startedQuest.Sequence;
text = new ImU8String(23, 1);
text.AppendLiteral("Stop at quest sequence ");
text.AppendFormatted(_configuration.Stop.TargetSequence);
ImGui.BulletText(text);
ImGui.SameLine();
if (sequence >= _configuration.Stop.TargetSequence)
{
Vector4 col2 = ImGuiColors.ParsedGreen;
text = new ImU8String(22, 1);
text.AppendLiteral("(Current: ");
text.AppendFormatted(sequence);
text.AppendLiteral(" - Reached!)");
ImGui.TextColored(in col2, text);
}
else
{
Vector4 col2 = ImGuiColors.ParsedBlue;
text = new ImU8String(11, 1);
text.AppendLiteral("(Current: ");
text.AppendFormatted(sequence);
text.AppendLiteral(")");
ImGui.TextColored(in col2, text);
}
}
if (flag2)
{
if (flag || flag3)
{
ImGui.Spacing();
}
ImGui.BulletText("Stop after completing any of these quests:");
ImGui.Indent();
foreach (ElementId item in _configuration.Stop.QuestsToStopAfter)
@ -415,7 +451,6 @@ internal sealed class ActiveQuestComponent
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(questProgressInfo.Tooltip);
ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
ImGui.Text(FontAwesomeIcon.Copy.ToIconString());

View file

@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Humanizer;
using Humanizer.Localisation;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Functions;
@ -20,9 +19,7 @@ namespace Questionable.Windows.QuestComponents;
internal sealed class EventInfoComponent
{
private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc);
private readonly List<EventQuest> _eventQuests;
private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc, string? Patch);
private readonly QuestData _questData;
@ -38,42 +35,33 @@ internal sealed class EventInfoComponent
private readonly Configuration _configuration;
private readonly IDataManager _dataManager;
private List<IQuestInfo> _cachedActiveSeasonalQuests = new List<IQuestInfo>();
private DateTime _cachedAtUtc = DateTime.MinValue;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5L);
private readonly ILogger<EventInfoComponent> _logger;
private readonly HashSet<ushort> _alreadyLoggedActiveSeasonalSkip = new HashSet<ushort>();
public bool ShouldDraw
{
get
{
if (_configuration.General.ShowIncompleteSeasonalEvents)
if (!_configuration.General.ShowIncompleteSeasonalEvents)
{
return _eventQuests.Any(IsIncomplete);
return false;
}
return false;
UpdateCacheIfNeeded();
return _cachedActiveSeasonalQuests.Count > 0;
}
}
public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, QuestTooltipComponent questTooltipComponent, Configuration configuration)
public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, QuestTooltipComponent questTooltipComponent, Configuration configuration, IDataManager dataManager, ILogger<EventInfoComponent> logger)
{
int num = 2;
List<EventQuest> list = new List<EventQuest>(num);
CollectionsMarshal.SetCount(list, num);
Span<EventQuest> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
ref EventQuest reference = ref span[num2];
int num3 = 1;
List<ElementId> list2 = new List<ElementId>(num3);
CollectionsMarshal.SetCount(list2, num3);
CollectionsMarshal.AsSpan(list2)[0] = new UnlockLinkId(568);
reference = new EventQuest("Limited Time Items", list2, DateTime.MaxValue);
ref EventQuest reference2 = ref span[num2 + 1];
int num4 = 2;
List<ElementId> list3 = new List<ElementId>(num4);
CollectionsMarshal.SetCount(list3, num4);
Span<ElementId> span2 = CollectionsMarshal.AsSpan(list3);
num3 = 0;
span2[num3] = new QuestId(5297);
span2[num3 + 1] = new QuestId(5298);
reference2 = new EventQuest("The Rising 2025", list3, AtDailyReset(new DateOnly(2025, 9, 11)));
_eventQuests = list;
base._002Ector();
_questData = questData;
_questRegistry = questRegistry;
_questFunctions = questFunctions;
@ -81,39 +69,97 @@ internal sealed class EventInfoComponent
_questController = questController;
_questTooltipComponent = questTooltipComponent;
_configuration = configuration;
}
private static DateTime AtDailyReset(DateOnly date)
{
return new DateTime(date, new TimeOnly(14, 59), DateTimeKind.Utc);
_dataManager = dataManager;
_logger = logger ?? throw new ArgumentNullException("logger");
}
public void Draw()
{
foreach (EventQuest eventQuest in _eventQuests)
UpdateCacheIfNeeded();
foreach (IGrouping<string, IQuestInfo> item in _cachedActiveSeasonalQuests.GroupBy(delegate(IQuestInfo q)
{
if (IsIncomplete(eventQuest))
if (q.QuestId is UnlockLinkId)
{
DrawEventQuest(eventQuest);
return "Limited Unlocks";
}
if (_questRegistry.TryGetQuestFolderName(q.QuestId, out string folderName) && !string.IsNullOrEmpty(folderName))
{
return folderName;
}
return q.JournalGenre.HasValue ? GetJournalGenreName(q.JournalGenre.Value) : q.Name;
}))
{
if (item.All((IQuestInfo q) => _questFunctions.IsQuestComplete(q.QuestId)))
{
continue;
}
DateTime endsAtUtc = item.Select(delegate(IQuestInfo q)
{
DateTime? dateTime = (q as QuestInfo)?.SeasonalQuestExpiry ?? ((q is UnlockLinkQuestInfo unlockLinkQuestInfo) ? unlockLinkQuestInfo.QuestExpiry : ((DateTime?)null));
if (dateTime.HasValue)
{
DateTime valueOrDefault = dateTime.GetValueOrDefault();
return NormalizeExpiry(valueOrDefault);
}
return DateTime.MaxValue;
}).DefaultIfEmpty(DateTime.MaxValue).Min();
List<string> list = (from q in item
select (q as UnlockLinkQuestInfo)?.Patch into p
where !string.IsNullOrEmpty(p)
select p).Distinct().ToList();
string patch = ((list.Count == 1) ? list[0] : null);
EventQuest eventQuest = new EventQuest(item.Key, item.Select((IQuestInfo q) => q.QuestId).ToList(), endsAtUtc, patch);
DrawEventQuest(eventQuest);
}
}
private string GetJournalGenreName(uint journalGenreId)
{
try
{
JournalGenre row = _dataManager.GetExcelSheet<JournalGenre>().GetRow(journalGenreId);
if (!row.Equals(default(JournalGenre)))
{
return row.Name.ExtractText();
}
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to get journal genre name for id {JournalGenreId}", journalGenreId);
}
return $"Event {journalGenreId}";
}
private void DrawEventQuest(EventQuest eventQuest)
{
string text = eventQuest.Name;
if (!string.IsNullOrEmpty(eventQuest.Patch))
{
text = text + " [" + eventQuest.Patch + "]";
}
if (eventQuest.EndsAtUtc != DateTime.MaxValue)
{
string value = (eventQuest.EndsAtUtc - DateTime.UtcNow).Humanize(1, CultureInfo.InvariantCulture, TimeUnit.Day, TimeUnit.Minute);
ImU8String text = new ImU8String(3, 2);
text.AppendFormatted(eventQuest.Name);
text.AppendLiteral(" (");
text.AppendFormatted(value);
text.AppendLiteral(")");
ImGui.Text(text);
TimeSpan timeSpan = eventQuest.EndsAtUtc - DateTime.UtcNow;
if (timeSpan < TimeSpan.Zero)
{
timeSpan = TimeSpan.Zero;
}
string value = FormatRemainingDays(timeSpan);
string text2 = FormatRemainingFull(timeSpan);
ImU8String text3 = new ImU8String(3, 2);
text3.AppendFormatted(text);
text3.AppendLiteral(" (");
text3.AppendFormatted(value);
text3.AppendLiteral(")");
ImGui.Text(text3);
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(text2);
}
}
else
{
ImGui.Text(eventQuest.Name);
ImGui.Text(text);
}
List<ElementId> list = eventQuest.QuestIds.Where((ElementId x) => _questRegistry.IsKnownQuest(x) && _questFunctions.IsReadyToAcceptQuest(x) && x != _questController.StartedQuest?.Quest.Id && x != _questController.NextQuest?.Quest.Id).ToList();
foreach (ElementId questId in eventQuest.QuestIds)
@ -122,13 +168,13 @@ internal sealed class EventInfoComponent
{
continue;
}
ImU8String text = new ImU8String(21, 1);
text.AppendLiteral("##EventQuestSelection");
text.AppendFormatted(questId);
using (ImRaii.PushId(text))
ImU8String text3 = new ImU8String(21, 1);
text3.AppendLiteral("##EventQuestSelection");
text3.AppendFormatted(questId);
using (ImRaii.PushId(text3))
{
string name = _questData.GetQuestInfo(questId).Name;
if (list.Contains(questId) && _questRegistry.TryGetQuest(questId, out Quest quest))
if (list.Contains(questId) && _questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest))
{
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{
@ -157,18 +203,23 @@ internal sealed class EventInfoComponent
}
}
private bool IsIncomplete(EventQuest eventQuest)
{
if (eventQuest.EndsAtUtc <= DateTime.UtcNow)
{
return false;
}
return eventQuest.QuestIds.Any(ShouldShowQuest);
}
public IEnumerable<ElementId> GetCurrentlyActiveEventQuests()
{
return _eventQuests.Where((EventQuest x) => x.EndsAtUtc >= DateTime.UtcNow).SelectMany((EventQuest x) => x.QuestIds).Where(ShouldShowQuest);
UpdateCacheIfNeeded();
return (from q in _cachedActiveSeasonalQuests.Where(delegate(IQuestInfo q)
{
DateTime? dateTime = (q as QuestInfo)?.SeasonalQuestExpiry;
if (dateTime.HasValue)
{
DateTime valueOrDefault = dateTime.GetValueOrDefault();
if (NormalizeExpiry(valueOrDefault) >= DateTime.UtcNow)
{
return true;
}
}
return (q is UnlockLinkQuestInfo { QuestExpiry: { } questExpiry } && NormalizeExpiry(questExpiry) >= DateTime.UtcNow) ? true : false;
})
select q.QuestId).Where(ShouldShowQuest);
}
private bool ShouldShowQuest(ElementId elementId)
@ -179,4 +230,208 @@ internal sealed class EventInfoComponent
}
return false;
}
private IEnumerable<IQuestInfo> GetActiveSeasonalQuestsNoCache()
{
IEnumerable<ElementId> allQuestIds = _questRegistry.GetAllQuestIds();
foreach (ElementId item in allQuestIds)
{
if (!_questData.TryGetQuestInfo(item, out IQuestInfo questInfo))
{
if (!_questRegistry.TryGetQuest(item, out Questionable.Model.Quest quest))
{
if (_alreadyLoggedActiveSeasonalSkip.Add(item.Value))
{
_logger.LogDebug("Skipping quest {QuestId}: no QuestInfo", item);
}
continue;
}
questInfo = quest.Info;
}
try
{
bool flag = false;
DateTime? dateTime = null;
if (questInfo is QuestInfo questInfo2)
{
flag = questInfo2.IsSeasonalQuest || questInfo2.IsSeasonalEvent || questInfo2.SeasonalQuestExpiry is DateTime || (questInfo2.JournalGenre >= 234 && questInfo2.JournalGenre <= 247);
dateTime = questInfo2.SeasonalQuestExpiry;
}
if (flag)
{
if (dateTime.HasValue)
{
DateTime valueOrDefault = dateTime.GetValueOrDefault();
DateTime dateTime2 = NormalizeExpiry(valueOrDefault);
_logger.LogInformation("Seasonal details: Quest {QuestId} '{Name}' rawExpiry={Raw:o} Kind={Kind} TimeOfDay={TimeOfDay} normalizedUtc={Normalized:o}", questInfo.QuestId, questInfo.Name, valueOrDefault, valueOrDefault.Kind, valueOrDefault.TimeOfDay, dateTime2);
}
else
{
_logger.LogInformation("Seasonal details: Quest {QuestId} '{Name}' has no expiry (seasonal flag present). IsSeasonalEvent={IsSeasonalEvent} IsSeasonalQuest={IsSeasonalQuest} JournalGenre={JournalGenre} SeasonalQuestExpiry={SeasonalQuestExpiry}", questInfo.QuestId, questInfo.Name, questInfo is QuestInfo questInfo3 && questInfo3.IsSeasonalEvent, questInfo is QuestInfo questInfo4 && questInfo4.IsSeasonalQuest, (questInfo is QuestInfo questInfo5) ? questInfo5.JournalGenre : ((uint?)null), (questInfo is QuestInfo questInfo6) ? questInfo6.SeasonalQuestExpiry : ((DateTime?)null));
}
}
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Failed to log seasonal details for {QuestId}", questInfo.QuestId);
}
if (_questFunctions.IsQuestUnobtainable(questInfo.QuestId))
{
if (_alreadyLoggedActiveSeasonalSkip.Add(questInfo.QuestId.Value))
{
_logger.LogDebug("Skipping quest {QuestId} '{Name}': marked unobtainable", questInfo.QuestId, questInfo.Name);
}
}
else if (questInfo is UnlockLinkQuestInfo { QuestExpiry: var questExpiry })
{
if (questExpiry.HasValue)
{
DateTime valueOrDefault2 = questExpiry.GetValueOrDefault();
DateTime dateTime3 = NormalizeExpiry(valueOrDefault2);
if (dateTime3 > DateTime.UtcNow)
{
yield return questInfo;
}
else if (_alreadyLoggedActiveSeasonalSkip.Add(questInfo.QuestId.Value))
{
_logger.LogDebug("Skipping UnlockLink quest {QuestId} '{Name}': expiry {Expiry:o} UTC is not in the future", questInfo.QuestId, questInfo.Name, dateTime3);
}
}
else
{
yield return questInfo;
}
}
else
{
if (!(questInfo is QuestInfo { SeasonalQuestExpiry: var seasonalQuestExpiry } questInfo7))
{
continue;
}
if (seasonalQuestExpiry.HasValue)
{
DateTime valueOrDefault3 = seasonalQuestExpiry.GetValueOrDefault();
DateTime dateTime4 = NormalizeExpiry(valueOrDefault3);
if (dateTime4 > DateTime.UtcNow)
{
yield return questInfo;
}
else if (_alreadyLoggedActiveSeasonalSkip.Add(questInfo.QuestId.Value))
{
_logger.LogDebug("Skipping quest {QuestId} '{Name}': seasonal expiry {Expiry:o} UTC is not in the future", questInfo.QuestId, questInfo.Name, dateTime4);
}
}
else if (questInfo7.IsSeasonalQuest && !questInfo7.SeasonalQuestExpiry.HasValue)
{
yield return questInfo;
}
}
}
}
private void UpdateCacheIfNeeded()
{
if (DateTime.UtcNow - _cachedAtUtc < _cacheDuration)
{
return;
}
_cachedActiveSeasonalQuests = GetActiveSeasonalQuestsNoCache().ToList();
_cachedAtUtc = DateTime.UtcNow;
_logger.LogDebug("Refreshed seasonal quest cache: {Count} active seasonal quests (UTC now {UtcNow:o})", _cachedActiveSeasonalQuests.Count, _cachedAtUtc);
foreach (IGrouping<string, IQuestInfo> item in _cachedActiveSeasonalQuests.GroupBy(delegate(IQuestInfo q)
{
if (q.QuestId is UnlockLinkId)
{
return "Limited Unlocks";
}
if (_questRegistry.TryGetQuestFolderName(q.QuestId, out string folderName) && !string.IsNullOrEmpty(folderName))
{
return folderName;
}
return q.JournalGenre.HasValue ? GetJournalGenreName(q.JournalGenre.Value) : q.Name;
}))
{
DateTime dateTime = item.Select(delegate(IQuestInfo q)
{
DateTime? dateTime2 = (q as QuestInfo)?.SeasonalQuestExpiry ?? ((q is UnlockLinkQuestInfo unlockLinkQuestInfo) ? unlockLinkQuestInfo.QuestExpiry : ((DateTime?)null));
if (dateTime2.HasValue)
{
DateTime valueOrDefault = dateTime2.GetValueOrDefault();
return NormalizeExpiry(valueOrDefault);
}
return DateTime.MaxValue;
}).DefaultIfEmpty(DateTime.MaxValue).Min();
List<string> list = (from q in item
select (q as UnlockLinkQuestInfo)?.Patch into p
where !string.IsNullOrEmpty(p)
select p).Distinct().ToList();
string text = ((list.Count == 1) ? list[0] : null);
if (dateTime != DateTime.MaxValue)
{
_logger.LogInformation("Seasonal event '{Name}' ends at {Expiry:o} UTC (patch={Patch})", item.Key, dateTime, text ?? "n/a");
}
else
{
_logger.LogInformation("Seasonal event '{Name}' has no expiry (patch={Patch})", item.Key, text ?? "n/a");
}
}
}
public void RefreshAndLogSeasonalExpiries()
{
_cachedAtUtc = DateTime.MinValue;
UpdateCacheIfNeeded();
}
public static DateTime AtDailyReset(DateOnly date)
{
return new DateTime(date, new TimeOnly(14, 59, 59), DateTimeKind.Utc);
}
private static DateTime NormalizeExpiry(DateTime d)
{
TimeSpan timeOfDay = d.TimeOfDay;
TimeSpan timeSpan = new TimeSpan(23, 59, 59);
if (timeOfDay == TimeSpan.Zero || timeOfDay == timeSpan)
{
return AtDailyReset(DateOnly.FromDateTime(d));
}
if (d.Kind != DateTimeKind.Utc)
{
return d.ToUniversalTime();
}
return d;
}
private static string FormatRemainingDays(TimeSpan remaining)
{
int num = (int)Math.Ceiling(Math.Max(0.0, remaining.TotalSeconds));
int num2 = num / 86400;
if (num2 >= 1)
{
if (num2 != 1)
{
return $"{num2} days";
}
return "1 day";
}
int value = num % 86400 / 3600;
int value2 = num % 3600 / 60;
int value3 = num % 60;
return $"{value:D2}:{value2:D2}:{value3:D2}";
}
private static string FormatRemainingFull(TimeSpan remaining)
{
int num = (int)Math.Ceiling(Math.Max(0.0, remaining.TotalSeconds));
int num2 = num / 86400;
int value = num % 86400 / 3600;
int value2 = num % 3600 / 60;
int value3 = num % 60;
if (num2 < 1)
{
return $"Ends in {value:D2}d {value2:D2}m {value3:D2}s";
}
return $"Ends in {num2}d {value:D2}h {value2:D2}m {value3:D2}s";
}
}

View file

@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Questionable.Controller;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.Windows.Utils;
namespace Questionable.Windows.QuestComponents;
internal sealed class ManualPriorityComponent
{
private readonly QuestController _questController;
private readonly QuestFunctions _questFunctions;
private readonly QuestSelector _questSelector;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly UiUtils _uiUtils;
private readonly IChatGui _chatGui;
private readonly IDalamudPluginInterface _pluginInterface;
private ElementId? _draggedItem;
public ManualPriorityComponent(QuestController questController, QuestFunctions questFunctions, QuestSelector questSelector, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IChatGui chatGui, IDalamudPluginInterface pluginInterface)
{
ManualPriorityComponent manualPriorityComponent = this;
_questController = questController;
_questFunctions = questFunctions;
_questSelector = questSelector;
_questTooltipComponent = questTooltipComponent;
_uiUtils = uiUtils;
_chatGui = chatGui;
_pluginInterface = pluginInterface;
_questSelector.SuggestionPredicate = (Quest quest) => !quest.Info.IsMainScenarioQuest && !questFunctions.IsQuestUnobtainable(quest.Id) && questController.ManualPriorityQuests.All((Quest x) => x.Id != quest.Id);
_questSelector.DefaultPredicate = (Quest quest) => questFunctions.IsQuestAccepted(quest.Id);
_questSelector.QuestSelected = delegate(Quest quest)
{
manualPriorityComponent._questController.ManualPriorityQuests.Add(quest);
};
}
public void Draw()
{
ImGui.TextWrapped("When you have an active Main Scenario Quest, Questionable will prioritise quests in this order:");
ImGui.BulletText("Priority quests: class quests, A Realm Reborn primals and raids");
ImGui.BulletText("Supported quests from your Journal's To-Do list (always visible on-screen quests)");
ImGui.BulletText("Main Scenario Quest (if available and not marked as ignored in your Journal)");
ImGui.TextWrapped("Without an active Main Scenario Quest, it will always try to pick up the next MSQ first.");
ImGui.Spacing();
ImGui.Text("Custom priority quests:");
_questSelector.DrawSelection();
ImGui.Spacing();
if (_questController.ManualPriorityQuests.Count > 0)
{
ImGui.Text("Priority queue (drag arrow buttons to reorder):");
}
using (ImRaii.IEndObject endObject = ImRaii.Child("ManualPriorityList", new Vector2(-1f, -27f), border: true))
{
if (endObject)
{
DrawQuestList();
}
}
List<ElementId> list = ParseClipboardItems();
using (ImRaii.Disabled(list.Count == 0))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Download, "Import from Clipboard"))
{
ImportFromClipboard(list);
}
}
ImGui.SameLine();
using (ImRaii.Disabled(_questController.ManualPriorityQuests.Count == 0))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Upload, "Export to Clipboard"))
{
ExportToClipboard();
}
ImGui.SameLine();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove finished Quests"))
{
_questController.ManualPriorityQuests.RemoveAll((Quest q) => _questFunctions.IsQuestComplete(q.Id));
}
ImGui.SameLine();
using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, "Clear All"))
{
_questController.ClearQuestPriority();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
ImGui.SetTooltip("Hold CTRL to enable this button.");
}
}
}
private void DrawQuestList()
{
List<Quest> manualPriorityQuests = _questController.ManualPriorityQuests;
if (manualPriorityQuests.Count == 0)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.TextWrapped("No quests in priority queue. Add quests using the selector above to prioritise them over other available quests.");
ImGui.PopStyleColor();
return;
}
Quest quest = null;
Quest quest2 = null;
int index = 0;
float x = ImGui.GetContentRegionAvail().X;
List<(Vector2, Vector2)> list = new List<(Vector2, Vector2)>();
for (int i = 0; i < manualPriorityQuests.Count; i++)
{
Vector2 item = ImGui.GetCursorScreenPos() + new Vector2(0f, (0f - ImGui.GetStyle().ItemSpacing.Y) / 2f);
Quest quest3 = manualPriorityQuests[i];
ImU8String id = new ImU8String(5, 1);
id.AppendLiteral("Quest");
id.AppendFormatted(quest3.Id);
using (ImRaii.PushId(id))
{
ImGui.AlignTextToFramePadding();
id = new ImU8String(1, 1);
id.AppendFormatted(i + 1);
id.AppendLiteral(".");
ImGui.Text(id);
ImGui.SameLine();
(Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(quest3.Id);
bool flag;
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString());
flag = ImGui.IsItemHovered();
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text(quest3.Info.Name);
flag |= ImGui.IsItemHovered();
if (flag)
{
_questTooltipComponent.Draw(quest3.Info);
}
if (manualPriorityQuests.Count > 1)
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 4f - ImGui.GetStyle().ItemSpacing.X);
}
if (_draggedItem == quest3.Id)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown, ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive)));
}
else
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown);
}
if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedItem = quest3.Id;
}
ImGui.SameLine();
}
else
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f);
}
}
if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times))
{
quest = quest3;
}
}
Vector2 item2 = new Vector2(item.X + x, ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2f);
list.Add((item, item2));
}
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedItem = null;
}
else if (_draggedItem != null)
{
Quest item3 = manualPriorityQuests.Single((Quest quest4) => quest4.Id == _draggedItem);
int num = manualPriorityQuests.IndexOf(item3);
var (pMin, pMax) = list[num];
ImGui.GetWindowDrawList().AddRect(pMin, pMax, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f, ImDrawFlags.RoundCornersAll);
int num2 = list.FindIndex(((Vector2 TopLeft, Vector2 BottomRight) tuple2) => ImGui.IsMouseHoveringRect(tuple2.TopLeft, tuple2.BottomRight, clip: true));
if (num2 >= 0 && num != num2)
{
quest2 = manualPriorityQuests.Single((Quest quest4) => quest4.Id == _draggedItem);
index = num2;
}
}
if (quest != null)
{
manualPriorityQuests.Remove(quest);
}
if (quest2 != null)
{
manualPriorityQuests.Remove(quest2);
manualPriorityQuests.Insert(index, quest2);
}
}
public string EncodeQuestPriority()
{
return "qst:priority:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Join(';', _questController.ManualPriorityQuests.Select((Quest x) => x.Id.ToString()))));
}
private static List<ElementId> ParseClipboardItems()
{
return PriorityWindow.DecodeQuestPriority(ImGui.GetClipboardText().Trim());
}
private void ExportToClipboard()
{
ImGui.SetClipboardText(EncodeQuestPriority());
_chatGui.Print("Copied quests to clipboard.", "Questionable", 576);
}
private void ImportFromClipboard(List<ElementId> questElements)
{
_questController.ImportQuestPriority(questElements);
}
}

View file

@ -0,0 +1,788 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents;
internal sealed class PresetBuilderComponent
{
private sealed class QuestPreset
{
public required string Name { get; init; }
public required string Description { get; init; }
public required int DisplayOrder { get; init; }
public required List<ElementId> QuestIds { get; init; }
public List<ElementId> GetQuestIds()
{
return QuestIds;
}
}
private readonly QuestController _questController;
private readonly QuestFunctions _questFunctions;
private readonly QuestData _questData;
private readonly AetheryteFunctions _aetheryteFunctions;
private readonly IClientState _clientState;
private readonly IChatGui _chatGui;
private readonly UiUtils _uiUtils;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly QuestRegistry _questRegistry;
private readonly ILogger<PresetBuilderComponent> _logger;
private readonly Dictionary<string, QuestPreset> _availablePresets;
public PresetBuilderComponent(QuestController questController, QuestFunctions questFunctions, QuestData questData, AetheryteFunctions aetheryteFunctions, IClientState clientState, IChatGui chatGui, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, QuestRegistry questRegistry, ILogger<PresetBuilderComponent> logger)
{
_questController = questController;
_questFunctions = questFunctions;
_questData = questData;
_aetheryteFunctions = aetheryteFunctions;
_clientState = clientState;
_chatGui = chatGui;
_uiUtils = uiUtils;
_questTooltipComponent = questTooltipComponent;
_questRegistry = questRegistry;
_logger = logger;
_availablePresets = BuildPresetList();
}
public void Draw()
{
ImGui.TextWrapped("Quest presets allow you to quickly add related quests to your priority list. These are useful for unlocking content that may be required later.");
ImGui.Spacing();
using (ImRaii.IEndObject endObject = ImRaii.Child("PresetList", new Vector2(-1f, -27f), border: true))
{
if (endObject)
{
DrawPresetGroups();
}
}
List<ElementId> list = (from questId in GetAllPresetQuests()
where _questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId))
select questId).ToList();
using (ImRaii.Disabled(list.Count == 0))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, $"Clear All Preset Quests ({list.Count})"))
{
ClearAllPresetQuests(list);
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
if (list.Count == 0)
{
ImGui.SetTooltip("No preset quests are currently in the priority list.");
return;
}
ImU8String tooltip = new ImU8String(59, 1);
tooltip.AppendLiteral("Remove all ");
tooltip.AppendFormatted(list.Count);
tooltip.AppendLiteral(" preset-related quest(s) from the priority list.");
ImGui.SetTooltip(tooltip);
}
}
private void DrawPresetGroups()
{
IOrderedEnumerable<KeyValuePair<string, QuestPreset>> orderedEnumerable = from x in _availablePresets
where x.Key.StartsWith("aether_currents_", StringComparison.Ordinal)
orderby x.Value.DisplayOrder
select x;
IOrderedEnumerable<KeyValuePair<string, QuestPreset>> orderedEnumerable2 = from x in _availablePresets
where x.Key.StartsWith("aethernet_", StringComparison.Ordinal)
orderby x.Value.DisplayOrder
select x;
IOrderedEnumerable<KeyValuePair<string, QuestPreset>> orderedEnumerable3 = from x in _availablePresets
where !x.Key.StartsWith("aether_currents_", StringComparison.Ordinal) && !x.Key.StartsWith("aethernet_", StringComparison.Ordinal)
orderby x.Value.DisplayOrder
select x;
string key;
QuestPreset value;
if (DrawGroupHeader("Aether Currents", "Unlock aether currents in various expansion zones to enable flying.", orderedEnumerable))
{
using (ImRaii.PushIndent())
{
foreach (KeyValuePair<string, QuestPreset> item in orderedEnumerable)
{
item.Deconstruct(out key, out value);
string key2 = key;
QuestPreset preset = value;
DrawPreset(key2, preset);
}
}
}
if (DrawGroupHeader("City Aethernet", "Unlock aethernet shards in major cities to enable city teleports.", orderedEnumerable2))
{
using (ImRaii.PushIndent())
{
foreach (KeyValuePair<string, QuestPreset> item2 in orderedEnumerable2)
{
item2.Deconstruct(out key, out value);
string key3 = key;
QuestPreset preset2 = value;
DrawPreset(key3, preset2);
}
}
}
if (!orderedEnumerable3.Any() || !DrawGroupHeader("Content Unlocks", "Essential quest series and unlocks that may be required for progression.", orderedEnumerable3))
{
return;
}
using (ImRaii.PushIndent())
{
foreach (KeyValuePair<string, QuestPreset> item3 in orderedEnumerable3)
{
item3.Deconstruct(out key, out value);
string key4 = key;
QuestPreset preset3 = value;
DrawPreset(key4, preset3);
}
}
}
private bool DrawGroupHeader(string groupName, string groupDescription, IEnumerable<KeyValuePair<string, QuestPreset>> presets)
{
int num = 0;
int num2 = 0;
int num3 = 0;
foreach (KeyValuePair<string, QuestPreset> preset2 in presets)
{
preset2.Deconstruct(out var _, out var value);
QuestPreset preset = value;
num += GetAvailableQuestsForPreset(preset).Count;
num2 += GetCompletedQuestsForPreset(preset).Count;
num3 += GetAlreadyPriorityQuestsForPreset(preset).Count;
}
string text = groupName;
if (num > 0 || num2 > 0 || num3 > 0)
{
List<string> list = new List<string>();
if (num > 0)
{
list.Add($"{num} available");
}
if (num3 > 0)
{
list.Add($"{num3} priority");
}
if (num2 > 0)
{
list.Add($"{num2} completed");
}
if (list.Count > 0)
{
text = text + " (" + string.Join(", ", list) + ")";
}
}
ImU8String label = new ImU8String(9, 2);
label.AppendFormatted(text);
label.AppendLiteral("###Group_");
label.AppendFormatted(groupName);
bool result = ImGui.CollapsingHeader(label);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(groupDescription);
ImGui.EndTooltip();
}
return result;
}
private void DrawPreset(string key, QuestPreset preset)
{
using (ImRaii.PushId(key))
{
List<ElementId> availableQuestsForPreset = GetAvailableQuestsForPreset(preset);
List<ElementId> completedQuestsForPreset = GetCompletedQuestsForPreset(preset);
List<ElementId> alreadyPriorityQuestsForPreset = GetAlreadyPriorityQuestsForPreset(preset);
string text = preset.Name;
if (availableQuestsForPreset.Count > 0 || completedQuestsForPreset.Count > 0 || alreadyPriorityQuestsForPreset.Count > 0)
{
List<string> list = new List<string>();
if (availableQuestsForPreset.Count > 0)
{
list.Add($"{availableQuestsForPreset.Count} available");
}
if (alreadyPriorityQuestsForPreset.Count > 0)
{
list.Add($"{alreadyPriorityQuestsForPreset.Count} priority");
}
if (completedQuestsForPreset.Count > 0)
{
list.Add($"{completedQuestsForPreset.Count} completed");
}
if (list.Count > 0)
{
text = text + " (" + string.Join(", ", list) + ")";
}
}
ImU8String label = new ImU8String(3, 2);
label.AppendFormatted(text);
label.AppendLiteral("###");
label.AppendFormatted(key);
bool num = ImGui.CollapsingHeader(label);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(preset.Description);
ImGui.EndTooltip();
}
if (!num)
{
return;
}
using (ImRaii.PushIndent())
{
ImGui.TextWrapped(preset.Description);
ImGui.Spacing();
bool flag = key.StartsWith("aethernet_", StringComparison.Ordinal);
if (flag && availableQuestsForPreset.Count == 0 && completedQuestsForPreset.Count == 0 && alreadyPriorityQuestsForPreset.Count == 0)
{
EAetheryteLocation? mainAetheryteForAethernetPreset = GetMainAetheryteForAethernetPreset(key);
if (mainAetheryteForAethernetPreset.HasValue && !_aetheryteFunctions.IsAetheryteUnlocked(mainAetheryteForAethernetPreset.Value))
{
_uiUtils.ChecklistItem("Main aetheryte must be attuned first", ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle);
ImGui.Spacing();
}
}
if (availableQuestsForPreset.Count > 0)
{
_uiUtils.ChecklistItem($"{availableQuestsForPreset.Count} {((availableQuestsForPreset.Count == 1) ? "quest" : "quests")} available", ImGuiColors.DalamudYellow, FontAwesomeIcon.Running);
}
if (alreadyPriorityQuestsForPreset.Count > 0)
{
_uiUtils.ChecklistItem($"{alreadyPriorityQuestsForPreset.Count} {((alreadyPriorityQuestsForPreset.Count == 1) ? "quest" : "quests")} already in priority list", ImGuiColors.DalamudOrange, FontAwesomeIcon.PersonWalkingArrowRight);
}
if (completedQuestsForPreset.Count > 0)
{
_uiUtils.ChecklistItem($"{completedQuestsForPreset.Count} {((completedQuestsForPreset.Count == 1) ? "quest" : "quests")} already completed", ImGuiColors.ParsedGreen, FontAwesomeIcon.Check);
}
if (availableQuestsForPreset.Count == 0 && completedQuestsForPreset.Count == 0 && alreadyPriorityQuestsForPreset.Count == 0)
{
if (!flag)
{
goto IL_03b8;
}
if (flag)
{
EAetheryteLocation? mainAetheryteForAethernetPreset2 = GetMainAetheryteForAethernetPreset(key);
if (mainAetheryteForAethernetPreset2.HasValue && _aetheryteFunctions.IsAetheryteUnlocked(mainAetheryteForAethernetPreset2.Value))
{
goto IL_03b8;
}
}
}
goto IL_03d8;
IL_03d8:
if (availableQuestsForPreset.Count > 0)
{
ImGui.Spacing();
string text2 = ((availableQuestsForPreset.Count == 1) ? "Add Quest to Priority" : $"Add All {availableQuestsForPreset.Count} Quests to Priority");
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, text2))
{
AddPresetToPriority(preset, availableQuestsForPreset);
}
}
if (availableQuestsForPreset.Count > 0 || completedQuestsForPreset.Count > 0 || alreadyPriorityQuestsForPreset.Count > 0)
{
ImGui.Spacing();
ImGui.Separator();
foreach (ElementId item in availableQuestsForPreset)
{
if (_questData.TryGetQuestInfo(item, out IQuestInfo questInfo))
{
if (_uiUtils.ChecklistItem($"{questInfo.Name} ({item})", ImGuiColors.DalamudYellow, FontAwesomeIcon.Running))
{
_questTooltipComponent.Draw(questInfo);
}
ImGui.SameLine(0f, 5f);
if (ImGuiComponents.IconButton($"##AddQuest{item}", FontAwesomeIcon.Plus))
{
AddIndividualQuestToPriority(questInfo, item);
}
if (ImGui.IsItemHovered())
{
label = new ImU8String(23, 1);
label.AppendLiteral("Add '");
label.AppendFormatted(questInfo.Name);
label.AppendLiteral("' to priority list");
ImGui.SetTooltip(label);
}
}
}
foreach (ElementId item2 in alreadyPriorityQuestsForPreset)
{
if (_questData.TryGetQuestInfo(item2, out IQuestInfo questInfo2) && _uiUtils.ChecklistItem($"{questInfo2.Name} ({item2})", ImGuiColors.DalamudOrange, FontAwesomeIcon.PersonWalkingArrowRight))
{
_questTooltipComponent.Draw(questInfo2);
}
}
foreach (ElementId item3 in completedQuestsForPreset)
{
if (_questData.TryGetQuestInfo(item3, out IQuestInfo questInfo3) && _uiUtils.ChecklistItem($"{questInfo3.Name} ({item3})", ImGuiColors.ParsedGreen, FontAwesomeIcon.Check))
{
_questTooltipComponent.Draw(questInfo3);
}
}
}
ImGui.Spacing();
return;
IL_03b8:
_uiUtils.ChecklistItem("No applicable quests found", ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus);
goto IL_03d8;
}
}
}
private List<ElementId> GetAvailableQuestsForPreset(QuestPreset preset)
{
return (from questId in preset.GetQuestIds()
where _questFunctions.IsReadyToAcceptQuest(questId) || _questFunctions.IsQuestAccepted(questId)
where !_questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId))
where _questRegistry.IsKnownQuest(questId)
select questId).ToList();
}
private List<ElementId> GetCompletedQuestsForPreset(QuestPreset preset)
{
return (from questId in preset.GetQuestIds()
where _questFunctions.IsQuestComplete(questId)
select questId).ToList();
}
private List<ElementId> GetAlreadyPriorityQuestsForPreset(QuestPreset preset)
{
return (from questId in preset.GetQuestIds()
where _questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId))
where !_questFunctions.IsQuestComplete(questId)
select questId).ToList();
}
private List<ElementId> GetAllPresetQuests()
{
return _availablePresets.Values.SelectMany((QuestPreset preset) => preset.GetQuestIds()).Distinct().ToList();
}
private void ClearAllPresetQuests(List<ElementId> questsToRemove)
{
int num = 0;
foreach (ElementId questId in questsToRemove)
{
Quest quest = _questController.ManualPriorityQuests.FirstOrDefault((Quest q) => q.Id.Equals(questId));
if (quest != null)
{
_questController.ManualPriorityQuests.Remove(quest);
num++;
}
}
_logger.LogInformation("Removed {Count} preset quests from priority list", num);
}
private void AddPresetToPriority(QuestPreset preset, List<ElementId> questIds)
{
int num = 0;
foreach (ElementId questId in questIds)
{
if (_questController.AddQuestPriority(questId))
{
num++;
}
}
_logger.LogInformation("Added {Count} quests from preset '{PresetName}' to priority list", num, preset.Name);
}
private void AddIndividualQuestToPriority(IQuestInfo questInfo, ElementId questId)
{
if (_questController.AddQuestPriority(questId))
{
_logger.LogInformation("Added individual quest '{QuestName}' ({QuestId}) to priority list", questInfo.Name, questId);
}
}
private static Dictionary<string, QuestPreset> BuildPresetList()
{
return new Dictionary<string, QuestPreset>
{
["aether_currents_hw"] = new QuestPreset
{
Name = "Heavensward",
Description = "Unlock all missed aether currents in Heavensward zones to enable flying.",
DisplayOrder = 10,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Heavensward)
},
["aether_currents_sb"] = new QuestPreset
{
Name = "Stormblood",
Description = "Unlock all missed aether currents in Stormblood zones to enable flying.",
DisplayOrder = 11,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Stormblood)
},
["aether_currents_shb"] = new QuestPreset
{
Name = "Shadowbringers",
Description = "Unlock all missed aether currents in Shadowbringers zones to enable flying.",
DisplayOrder = 12,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Shadowbringers)
},
["aether_currents_ew"] = new QuestPreset
{
Name = "Endwalker",
Description = "Unlock all missed aether currents in Endwalker zones to enable flying.",
DisplayOrder = 13,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Endwalker)
},
["aether_currents_dt"] = new QuestPreset
{
Name = "Dawntrail",
Description = "Unlock all missed aether currents in Dawntrail zones to enable flying.",
DisplayOrder = 14,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Dawntrail)
},
["aethernet_limsa"] = new QuestPreset
{
Name = "Limsa Lominsa",
Description = "Unlock all aethernet shards in Limsa Lominsa for convenient city travel.",
DisplayOrder = 20,
QuestIds = GetAethernetQuestsByCity("Limsa")
},
["aethernet_gridania"] = new QuestPreset
{
Name = "Gridania",
Description = "Unlock all aethernet shards in Gridania for convenient city travel.",
DisplayOrder = 21,
QuestIds = GetAethernetQuestsByCity("Gridania")
},
["aethernet_uldah"] = new QuestPreset
{
Name = "Ul'dah",
Description = "Unlock all aethernet shards in Ul'dah for convenient city travel.",
DisplayOrder = 22,
QuestIds = GetAethernetQuestsByCity("Uldah")
},
["aethernet_goldsaucer"] = new QuestPreset
{
Name = "The Gold Saucer",
Description = "Unlock all aethernet shards in The Gold Saucer for convenient city travel.",
DisplayOrder = 23,
QuestIds = GetAethernetQuestsByCity("GoldSaucer")
},
["aethernet_ishgard"] = new QuestPreset
{
Name = "Ishgard",
Description = "Unlock all aethernet shards in Ishgard for convenient city travel.",
DisplayOrder = 24,
QuestIds = GetAethernetQuestsByCity("Ishgard")
},
["aethernet_idyllshire"] = new QuestPreset
{
Name = "Idyllshire",
Description = "Unlock all aethernet shards in Idyllshire for convenient city travel.",
DisplayOrder = 25,
QuestIds = GetAethernetQuestsByCity("Idyllshire")
},
["aethernet_rhalgrs_reach"] = new QuestPreset
{
Name = "Rhalgr's Reach",
Description = "Unlock all aethernet shards in Rhalgr's Reach for convenient city travel.",
DisplayOrder = 26,
QuestIds = GetAethernetQuestsByCity("Rhalgr's Reach")
},
["aethernet_kugane"] = new QuestPreset
{
Name = "Kugane",
Description = "Unlock all aethernet shards in Kugane for convenient city travel.",
DisplayOrder = 27,
QuestIds = GetAethernetQuestsByCity("Kugane")
},
["aethernet_doman_enclave"] = new QuestPreset
{
Name = "Doman Enclave",
Description = "Unlock all aethernet shards in Doman Enclave for convenient city travel.",
DisplayOrder = 28,
QuestIds = GetAethernetQuestsByCity("Doman Enclave")
},
["aethernet_the_crystarium"] = new QuestPreset
{
Name = "The Crystarium",
Description = "Unlock all aethernet shards in The Crystarium for convenient city travel.",
DisplayOrder = 29,
QuestIds = GetAethernetQuestsByCity("The Crystarium")
},
["aethernet_eulmore"] = new QuestPreset
{
Name = "Eulmore",
Description = "Unlock all aethernet shards in Eulmore for convenient city travel.",
DisplayOrder = 30,
QuestIds = GetAethernetQuestsByCity("Eulmore")
},
["aethernet_old_sharlayan"] = new QuestPreset
{
Name = "Old Sharlayan",
Description = "Unlock all aethernet shards in Old Sharlayan for convenient city travel.",
DisplayOrder = 31,
QuestIds = GetAethernetQuestsByCity("Old Sharlayan")
},
["aethernet_radz_at_han"] = new QuestPreset
{
Name = "Radz-at-Han",
Description = "Unlock all aethernet shards in Radz-at-Han for convenient city travel.",
DisplayOrder = 32,
QuestIds = GetAethernetQuestsByCity("Radz-at-Han")
},
["aethernet_tuliyollal"] = new QuestPreset
{
Name = "Tuliyollal",
Description = "Unlock all aethernet shards in Tuliyollal for convenient city travel.",
DisplayOrder = 33,
QuestIds = GetAethernetQuestsByCity("Tuliyollal")
},
["aethernet_solution_nine"] = new QuestPreset
{
Name = "Solution Nine",
Description = "Unlock all aethernet shards in Solution Nine for convenient city travel.",
DisplayOrder = 34,
QuestIds = GetAethernetQuestsByCity("Solution Nine")
},
["crystal_tower"] = new QuestPreset
{
Name = "Crystal Tower Raids",
Description = "Complete the Crystal Tower raid series (required for A Realm Reborn MSQ).",
DisplayOrder = 40,
QuestIds = QuestData.CrystalTowerQuests.Cast<ElementId>().ToList()
},
["hard_primals"] = new QuestPreset
{
Name = "A Realm Reborn Hard Mode Primals",
Description = "Unlock hard mode primal fights from A Realm Reborn.",
DisplayOrder = 41,
QuestIds = QuestData.HardModePrimals.Cast<ElementId>().ToList()
}
};
}
private static List<ElementId> GetAetherCurrentQuestsByExpansion(EExpansionVersion expansion)
{
uint[] territoryRanges = expansion switch
{
EExpansionVersion.Heavensward => new uint[5] { 397u, 398u, 399u, 400u, 401u },
EExpansionVersion.Stormblood => new uint[6] { 612u, 613u, 614u, 620u, 621u, 622u },
EExpansionVersion.Shadowbringers => new uint[6] { 813u, 814u, 815u, 816u, 817u, 818u },
EExpansionVersion.Endwalker => new uint[6] { 956u, 957u, 958u, 959u, 960u, 961u },
EExpansionVersion.Dawntrail => new uint[6] { 1187u, 1188u, 1189u, 1190u, 1191u, 1192u },
_ => Array.Empty<uint>(),
};
return QuestData.AetherCurrentQuestsByTerritory.Where<KeyValuePair<uint, ImmutableList<QuestId>>>((KeyValuePair<uint, ImmutableList<QuestId>> kvp) => territoryRanges.Contains(kvp.Key)).SelectMany((KeyValuePair<uint, ImmutableList<QuestId>> kvp) => kvp.Value).Cast<ElementId>()
.ToList();
}
private static List<ElementId> GetAethernetQuestsByCity(string cityName)
{
switch (cityName)
{
case "Limsa":
{
int num = 1;
List<ElementId> list15 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list15, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list15);
int index = 0;
span[index] = new AethernetId(1);
return list15;
}
case "Gridania":
{
int index = 1;
List<ElementId> list14 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list14, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list14);
int num = 0;
span[num] = new AethernetId(2);
return list14;
}
case "Uldah":
{
int num = 1;
List<ElementId> list13 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list13, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list13);
int index = 0;
span[index] = new AethernetId(3);
return list13;
}
case "GoldSaucer":
{
int index = 1;
List<ElementId> list12 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list12, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list12);
int num = 0;
span[num] = new AethernetId(4);
return list12;
}
case "Ishgard":
{
int num = 1;
List<ElementId> list11 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list11, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list11);
int index = 0;
span[index] = new AethernetId(5);
return list11;
}
case "Idyllshire":
{
int index = 1;
List<ElementId> list10 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list10, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list10);
int num = 0;
span[num] = new AethernetId(6);
return list10;
}
case "Rhalgr's Reach":
{
int num = 1;
List<ElementId> list9 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list9, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list9);
int index = 0;
span[index] = new AethernetId(7);
return list9;
}
case "Kugane":
{
int index = 1;
List<ElementId> list8 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list8, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list8);
int num = 0;
span[num] = new AethernetId(8);
return list8;
}
case "Doman Enclave":
{
int num = 1;
List<ElementId> list7 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list7, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list7);
int index = 0;
span[index] = new AethernetId(9);
return list7;
}
case "The Crystarium":
{
int index = 1;
List<ElementId> list6 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list6, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list6);
int num = 0;
span[num] = new AethernetId(10);
return list6;
}
case "Eulmore":
{
int num = 1;
List<ElementId> list5 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list5, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list5);
int index = 0;
span[index] = new AethernetId(11);
return list5;
}
case "Old Sharlayan":
{
int index = 1;
List<ElementId> list4 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list4, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list4);
int num = 0;
span[num] = new AethernetId(12);
return list4;
}
case "Radz-at-Han":
{
int num = 1;
List<ElementId> list3 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list3, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list3);
int index = 0;
span[index] = new AethernetId(13);
return list3;
}
case "Tuliyollal":
{
int index = 1;
List<ElementId> list2 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list2, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list2);
int num = 0;
span[num] = new AethernetId(14);
return list2;
}
case "Solution Nine":
{
int num = 1;
List<ElementId> list = new List<ElementId>(num);
CollectionsMarshal.SetCount(list, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list);
int index = 0;
span[index] = new AethernetId(15);
return list;
}
default:
return new List<ElementId>();
}
}
private static EAetheryteLocation? GetMainAetheryteForAethernetPreset(string presetKey)
{
return presetKey switch
{
"aethernet_limsa" => EAetheryteLocation.Limsa,
"aethernet_gridania" => EAetheryteLocation.Gridania,
"aethernet_uldah" => EAetheryteLocation.Uldah,
"aethernet_goldsaucer" => EAetheryteLocation.GoldSaucer,
"aethernet_ishgard" => EAetheryteLocation.Ishgard,
"aethernet_idyllshire" => EAetheryteLocation.Idyllshire,
"aethernet_rhalgrs_reach" => EAetheryteLocation.RhalgrsReach,
"aethernet_kugane" => EAetheryteLocation.Kugane,
"aethernet_doman_enclave" => EAetheryteLocation.DomanEnclave,
"aethernet_the_crystarium" => EAetheryteLocation.Crystarium,
"aethernet_eulmore" => EAetheryteLocation.Eulmore,
"aethernet_old_sharlayan" => EAetheryteLocation.OldSharlayan,
"aethernet_radz_at_han" => EAetheryteLocation.RadzAtHan,
"aethernet_tuliyollal" => EAetheryteLocation.Tuliyollal,
"aethernet_solution_nine" => EAetheryteLocation.SolutionNine,
_ => null,
};
}
}

View file

@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Questionable.Data;
using Questionable.Validation;
namespace Questionable.Windows.QuestComponents;
internal sealed class QuestValidationComponent
{
private readonly QuestValidator _questValidator;
private readonly QuestData _questData;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly ValidationDetailsRenderer _detailsRenderer;
public bool ShouldDraw => _questValidator.Issues.Count > 0;
public QuestValidationComponent(QuestValidator questValidator, QuestData questData, IDalamudPluginInterface pluginInterface)
{
_questValidator = questValidator;
_questData = questData;
_pluginInterface = pluginInterface;
_detailsRenderer = new ValidationDetailsRenderer(questData, pluginInterface);
}
public void Draw()
{
DrawSummaryHeader();
ImGui.Separator();
DrawValidationTable();
_detailsRenderer.DrawDetailWindows();
}
private void DrawSummaryHeader()
{
int issueCount = _questValidator.IssueCount;
int errorCount = _questValidator.ErrorCount;
int num = issueCount - errorCount;
ImU8String text = new ImU8String(33, 1);
text.AppendLiteral("Validation Results: ");
text.AppendFormatted(issueCount);
text.AppendLiteral(" total issues");
ImGui.Text(text);
if (errorCount > 0)
{
ImGui.SameLine();
ImGui.Text("(");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
text = new ImU8String(7, 1);
text.AppendFormatted(errorCount);
text.AppendLiteral(" errors");
ImGui.Text(text);
}
}
if (num > 0)
{
if (errorCount > 0)
{
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange))
{
text = new ImU8String(12, 1);
text.AppendLiteral(", ");
text.AppendFormatted(num);
text.AppendLiteral(" warnings)");
ImGui.Text(text);
return;
}
}
ImGui.SameLine();
ImGui.Text("(");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange))
{
text = new ImU8String(10, 1);
text.AppendFormatted(num);
text.AppendLiteral(" warnings)");
ImGui.Text(text);
return;
}
}
if (errorCount > 0)
{
ImGui.SameLine();
ImGui.Text(")");
}
}
private void DrawValidationTable()
{
using ImRaii.IEndObject endObject = ImRaii.Table("ValidationIssues", 6, ImGuiTableFlags.Borders | ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY);
if (!(!endObject))
{
ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, 60f);
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 250f);
ImGui.TableSetupColumn("Seq", ImGuiTableColumnFlags.WidthFixed, 40f);
ImGui.TableSetupColumn("Step", ImGuiTableColumnFlags.WidthFixed, 40f);
ImGui.TableSetupColumn("Issue", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 60f);
ImGui.TableHeadersRow();
IReadOnlyList<ValidationIssue> issues = _questValidator.Issues;
for (int i = 0; i < issues.Count; i++)
{
DrawValidationRow(issues[i], i);
}
}
}
private void DrawValidationRow(ValidationIssue issue, int index)
{
ImGui.TableNextRow();
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted(issue.ElementId?.ToString() ?? string.Empty);
}
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted((issue.ElementId != null) ? _questData.GetQuestInfo(issue.ElementId).Name : issue.AlliedSociety.ToString());
}
if (ImGui.TableNextColumn())
{
if (issue.Sequence.HasValue)
{
ImGui.TextUnformatted(issue.Sequence.Value.ToString(CultureInfo.InvariantCulture));
}
else
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey2))
{
ImGui.TextUnformatted("\ufffd");
}
}
}
if (ImGui.TableNextColumn())
{
if (issue.Step.HasValue)
{
ImGui.TextUnformatted(issue.Step.Value.ToString(CultureInfo.InvariantCulture));
}
else
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey2))
{
ImGui.TextUnformatted("\ufffd");
}
}
}
if (ImGui.TableNextColumn())
{
DrawIssueCell(issue, index);
}
if (ImGui.TableNextColumn())
{
DrawActionsCell(issue, index);
}
}
private void DrawIssueCell(ValidationIssue issue, int index)
{
Vector4 color = ((issue.Severity == EIssueSeverity.Error) ? ImGuiColors.DalamudRed : ImGuiColors.DalamudOrange);
FontAwesomeIcon icon = ((issue.Severity == EIssueSeverity.Error) ? FontAwesomeIcon.ExclamationTriangle : FontAwesomeIcon.InfoCircle);
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TextUnformatted(icon.ToIconString());
}
}
ImGui.SameLine();
string issueSummary = GetIssueSummary(issue);
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TextWrapped(issueSummary);
}
}
private void DrawActionsCell(ValidationIssue issue, int index)
{
if (HasDetailedDescription(issue) && ImGui.SmallButton($"Details##{index}"))
{
_detailsRenderer.OpenDetails(issue, index);
}
}
private static string GetIssueSummary(ValidationIssue issue)
{
string text = ValidationDetailsRenderer.CleanJsonText(issue.Description ?? string.Empty);
if (issue.Type == EIssueType.QuestDisabled && issue.ElementId == null)
{
string[] array = text.Split(':', 2);
if (array.Length == 0)
{
return text;
}
return array[0];
}
if (issue.Type == EIssueType.InvalidJsonSchema)
{
string[] array2 = text.Split('\n', 2, StringSplitOptions.RemoveEmptyEntries);
if (array2.Length == 0)
{
return text;
}
return array2[0];
}
if (issue.Type == EIssueType.InvalidJsonSyntax)
{
string[] array3 = text.Split('\n', 2, StringSplitOptions.RemoveEmptyEntries);
if (array3.Length == 0)
{
return text;
}
return array3[0];
}
string[] array4 = text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (array4.Length <= 2)
{
return text;
}
return array4[0] + ((array4.Length > 1) ? "..." : "");
}
private static bool HasDetailedDescription(ValidationIssue issue)
{
string text = issue.Description ?? string.Empty;
if (issue.Type == EIssueType.QuestDisabled && issue.ElementId == null)
{
return true;
}
if (issue.Type == EIssueType.InvalidJsonSchema)
{
return true;
}
if (issue.Type == EIssueType.InvalidJsonSyntax)
{
return true;
}
if (!text.Contains('\n', StringComparison.Ordinal))
{
return text.Length > 100;
}
return true;
}
}

View file

@ -0,0 +1,567 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.Validation;
namespace Questionable.Windows.QuestComponents;
internal sealed class ValidationDetailsRenderer
{
private sealed record JsonValidationError
{
public string Path { get; init; } = string.Empty;
public List<string> Messages { get; init; } = new List<string>();
}
private readonly QuestData _questData;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Dictionary<int, ValidationIssue> _storedIssues = new Dictionary<int, ValidationIssue>();
private readonly Dictionary<int, bool> _openDetailWindows = new Dictionary<int, bool>();
private static readonly Regex JsonPropertyPathRegex = new Regex("#/([^:]*)", RegexOptions.Compiled);
private static readonly Regex UnicodeEscapeRegex = new Regex("\\\\u([0-9A-Fa-f]{4})", RegexOptions.Compiled);
private static readonly Regex JsonEscapeRegex = new Regex("\\\\(.)", RegexOptions.Compiled);
private static readonly Regex ConsecutiveQuotesRegex = new Regex("\"{2,}", RegexOptions.Compiled);
public ValidationDetailsRenderer(QuestData questData, IDalamudPluginInterface pluginInterface)
{
_questData = questData;
_pluginInterface = pluginInterface;
}
public static string CleanJsonText(string text)
{
if (string.IsNullOrEmpty(text))
{
return text;
}
text = UnicodeEscapeRegex.Replace(text, (Match match) => int.TryParse(match.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result) ? ((char)result).ToString() : match.Value);
text = JsonEscapeRegex.Replace(text, (Match match) => match.Groups[1].Value switch
{
"\"" => "\"",
"\\" => "\\",
"/" => "/",
"b" => "\b",
"f" => "\f",
"n" => "\n",
"r" => "\r",
"t" => "\t",
_ => match.Value,
});
if (text.Contains("\"\"", StringComparison.Ordinal))
{
text = ConsecutiveQuotesRegex.Replace(text, "\"");
text = text.Replace("Expected \"\"", "Expected \"\"", StringComparison.Ordinal);
}
return text;
}
public void OpenDetails(ValidationIssue issue, int index)
{
_storedIssues[index] = issue;
_openDetailWindows[index] = true;
}
public void DrawDetailWindows()
{
List<int> list = new List<int>();
foreach (KeyValuePair<int, bool> item in _openDetailWindows.ToList())
{
if (item.Value && _storedIssues.TryGetValue(item.Key, out ValidationIssue value))
{
string obj = $"Validation Details##{item.Key}";
bool open = true;
ImGui.SetNextWindowSize(new Vector2(800f, 600f), ImGuiCond.FirstUseEver);
ImGui.SetNextWindowSizeConstraints(new Vector2(500f, 300f), new Vector2(1200f, 800f));
if (ImGui.Begin(obj, ref open))
{
DrawIssueDetails(value);
ImGui.End();
}
if (!open)
{
list.Add(item.Key);
}
}
}
foreach (int item2 in list)
{
_openDetailWindows.Remove(item2);
_storedIssues.Remove(item2);
}
}
private void DrawIssueDetails(ValidationIssue issue)
{
Vector4 color = ((issue.Severity == EIssueSeverity.Error) ? ImGuiColors.DalamudRed : ImGuiColors.DalamudOrange);
FontAwesomeIcon icon = ((issue.Severity == EIssueSeverity.Error) ? FontAwesomeIcon.ExclamationTriangle : FontAwesomeIcon.InfoCircle);
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TextUnformatted(icon.ToIconString());
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImU8String text = new ImU8String(2, 2);
text.AppendFormatted(issue.Severity);
text.AppendLiteral(": ");
text.AppendFormatted(issue.Type);
ImGui.Text(text);
}
ImGui.Separator();
if (issue.ElementId != null)
{
IQuestInfo questInfo = _questData.GetQuestInfo(issue.ElementId);
ImU8String text = new ImU8String(10, 2);
text.AppendLiteral("Quest: ");
text.AppendFormatted(issue.ElementId);
text.AppendLiteral(" - ");
text.AppendFormatted(questInfo.Name);
ImGui.Text(text);
}
else if (issue.AlliedSociety != EAlliedSociety.None)
{
ImU8String text = new ImU8String(16, 1);
text.AppendLiteral("Allied Society: ");
text.AppendFormatted(issue.AlliedSociety);
ImGui.Text(text);
}
if (issue.Sequence.HasValue)
{
ImU8String text = new ImU8String(10, 1);
text.AppendLiteral("Sequence: ");
text.AppendFormatted(issue.Sequence);
ImGui.Text(text);
}
if (issue.Step.HasValue)
{
ImU8String text = new ImU8String(6, 1);
text.AppendLiteral("Step: ");
text.AppendFormatted(issue.Step);
ImGui.Text(text);
}
ImGui.Separator();
ImGui.Text("Description:");
string description = CleanJsonText(issue.Description ?? "(no description)");
if (issue.Type == EIssueType.QuestDisabled && issue.ElementId == null)
{
DrawDisabledTribesDetails(description);
}
else if (issue.Type == EIssueType.InvalidJsonSchema)
{
DrawEnhancedJsonSchemaDetails(description);
}
else if (issue.Type == EIssueType.InvalidJsonSyntax)
{
DrawJsonSyntaxErrorDetails(description);
}
else
{
DrawGenericDetails(description);
}
}
private static void DrawJsonSyntaxErrorDetails(string description)
{
string[] array = description.Split('\n');
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (string.IsNullOrEmpty(text))
{
ImGui.Spacing();
}
else if (text.StartsWith("JSON parsing error", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text);
}
}
else if (text.StartsWith("This usually indicates", StringComparison.Ordinal) || text.StartsWith("Please check", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
{
ImGui.TextWrapped(text);
}
}
else if (text.StartsWith("\ufffd ", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
{
ImGui.TextWrapped(text);
}
}
else
{
ImGui.TextWrapped(text);
}
}
}
private void DrawDisabledTribesDetails(string description)
{
string[] array = description.Split(':', 2);
if (array.Length < 2)
{
ImGui.TextWrapped(description);
return;
}
ImGui.TextWrapped(array[0]);
ImGui.Spacing();
List<string> list = (from x in array[1].Split(',', StringSplitOptions.RemoveEmptyEntries)
select x.Trim() into x
where !string.IsNullOrEmpty(x)
select x).Distinct().ToList();
if (list.Count == 0)
{
ImGui.TextWrapped("(no disabled quests listed)");
return;
}
ImGui.Text("Disabled Quests:");
ImGui.Indent();
Vector4[] array2 = new Vector4[6]
{
ImGuiColors.TankBlue,
ImGuiColors.HealerGreen,
ImGuiColors.DPSRed,
ImGuiColors.ParsedGreen,
ImGuiColors.ParsedBlue,
ImGuiColors.DalamudViolet
};
for (int num = 0; num < list.Count; num++)
{
string value = list[num];
Vector4 color = array2[num % array2.Length];
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
if (ElementId.TryFromString(value, out ElementId elementId) && elementId != null)
{
try
{
IQuestInfo questInfo = _questData.GetQuestInfo(elementId);
ImU8String text = new ImU8String(5, 2);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(value);
text.AppendLiteral(" - ");
text.AppendFormatted(questInfo.Name);
ImGui.TextWrapped(text);
}
catch
{
ImU8String text = new ImU8String(18, 1);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(value);
text.AppendLiteral(" (unknown quest)");
ImGui.TextWrapped(text);
}
}
else
{
ImU8String text = new ImU8String(2, 1);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(value);
ImGui.TextWrapped(text);
}
}
}
ImGui.Unindent();
}
private void DrawEnhancedJsonSchemaDetails(string description)
{
if (description.Split('\n').Length == 0)
{
ImGui.TextWrapped("No validation details available.");
return;
}
List<JsonValidationError> list = ParseJsonValidationErrors(description);
if (list.Count > 0)
{
ImGui.Text("JSON Schema Validation Errors:");
ImGui.Spacing();
for (int i = 0; i < list.Count; i++)
{
DrawJsonValidationError(list[i], i);
if (i < list.Count - 1)
{
ImGui.Separator();
}
}
}
else
{
DrawSimpleJsonSchemaDetails(description);
}
}
private void DrawJsonValidationError(JsonValidationError error, int index)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
{
ImU8String text = new ImU8String(8, 1);
text.AppendLiteral("Error #");
text.AppendFormatted(index + 1);
text.AppendLiteral(":");
ImGui.Text(text);
}
ImGui.Indent(12f);
if (!string.IsNullOrEmpty(error.Path))
{
ImGui.AlignTextToFramePadding();
ImGui.Text("Location:");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
{
ImGui.TextWrapped(FormatJsonPath(error.Path));
}
}
if (error.Messages.Count > 0)
{
ImGui.AlignTextToFramePadding();
ImGui.Text((error.Messages.Count == 1) ? "Issue:" : "Issues:");
foreach (string message in error.Messages)
{
ImGui.Indent(12f);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
string text2 = CleanJsonText(message);
if (string.Equals(text2, "validation failed", StringComparison.OrdinalIgnoreCase))
{
text2 = "JSON schema validation failed - check that all properties match the expected format and values";
}
ImU8String text = new ImU8String(2, 1);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(text2);
ImGui.TextWrapped(text);
ImGui.Unindent(12f);
}
}
}
List<string> validationSuggestions = GetValidationSuggestions(error);
if (validationSuggestions.Count > 0)
{
ImGui.Spacing();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
{
ImGui.Text("Suggestions:");
foreach (string item in validationSuggestions)
{
ImGui.Indent(12f);
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
{
ImGui.Text(FontAwesomeIcon.Lightbulb.ToIconString());
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
{
ImGui.TextWrapped(item);
ImGui.Unindent(12f);
}
}
}
}
ImGui.Unindent(12f);
}
private static void DrawSimpleJsonSchemaDetails(string description)
{
string[] array = description.Split('\n');
foreach (string text in array)
{
if (text.StartsWith("JSON Validation failed:", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text);
}
}
else if (text.StartsWith(" - ", StringComparison.Ordinal))
{
int num = text.IndexOf(':', 3);
if (num > 0)
{
string path = text.Substring(3, num - 3).Trim();
string text2 = CleanJsonText(text.Substring(num + 1).Trim());
if (string.Equals(text2, "validation failed", StringComparison.OrdinalIgnoreCase))
{
text2 = "Schema validation failed - check property format and values";
}
ImGui.Text("\ufffd");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
{
ImGui.Text(FormatJsonPath(path));
}
ImGui.SameLine();
ImGui.Text(":");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text2);
}
}
else
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(CleanJsonText(text));
}
}
}
else
{
ImGui.TextWrapped(CleanJsonText(text));
}
}
}
private static List<JsonValidationError> ParseJsonValidationErrors(string description)
{
List<JsonValidationError> list = new List<JsonValidationError>();
string[] array = description.Split('\n');
foreach (string text in array)
{
if (!text.StartsWith(" - ", StringComparison.Ordinal))
{
continue;
}
int num = text.IndexOf(':', 3);
if (num > 0)
{
string path = text.Substring(3, num - 3).Trim();
List<string> messages = (from m in text.Substring(num + 1).Trim().Split(';', StringSplitOptions.RemoveEmptyEntries)
select CleanJsonText(m.Trim()) into m
where !string.IsNullOrEmpty(m)
select m).ToList();
list.Add(new JsonValidationError
{
Path = path,
Messages = messages
});
}
}
return list;
}
private static string FormatJsonPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return "<root>";
}
Match match = JsonPropertyPathRegex.Match(path);
if (match.Success)
{
string value = match.Groups[1].Value;
if (string.IsNullOrEmpty(value))
{
return "<root>";
}
return value.Replace('/', '.');
}
if (!(path == "<root>"))
{
return path;
}
return "<root>";
}
private static List<string> GetValidationSuggestions(JsonValidationError error)
{
List<string> list = new List<string>();
foreach (string message in error.Messages)
{
string text = message.ToUpperInvariant();
if (text.Contains("REQUIRED", StringComparison.Ordinal))
{
list.Add("Add the missing required property to your JSON.");
}
else if (text.Contains("TYPE", StringComparison.Ordinal))
{
list.Add("Check that the property value has the correct data type (string, number, boolean, etc.).");
}
else if (text.Contains("ENUM", StringComparison.Ordinal) || text.Contains("ALLOWED VALUES", StringComparison.Ordinal))
{
list.Add("Use one of the allowed enumeration values for this property.");
}
else if (text.Contains("FORMAT", StringComparison.Ordinal))
{
list.Add("Ensure the property value follows the expected format.");
}
else if (text.Contains("MINIMUM", StringComparison.Ordinal) || text.Contains("MAXIMUM", StringComparison.Ordinal))
{
list.Add("Check that numeric values are within the allowed range.");
}
else if (text.Contains("ADDITIONAL", StringComparison.Ordinal) && text.Contains("NOT ALLOWED", StringComparison.Ordinal))
{
list.Add("Remove any extra properties that are not defined in the schema.");
}
else if (text.Contains("VALIDATION FAILED", StringComparison.Ordinal))
{
list.Add("Review the JSON structure and ensure all properties match the expected schema format.");
}
}
return list.Distinct().ToList();
}
private static void DrawGenericDetails(string description)
{
string[] array = description.Split('\n');
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (string.IsNullOrEmpty(text))
{
ImGui.Spacing();
}
else if (text.StartsWith("Error:", StringComparison.Ordinal) || text.StartsWith("Invalid", StringComparison.Ordinal) || text.StartsWith("Missing", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text);
}
}
else if (text.Contains(':', StringComparison.Ordinal))
{
int num = text.IndexOf(':', StringComparison.Ordinal);
string text2 = text.Substring(0, num);
string text3 = text.Substring(num + 1).TrimStart();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
{
ImGui.Text(text2 + ":");
}
ImGui.SameLine();
ImGui.TextWrapped(text3);
}
else
{
ImGui.TextWrapped(text);
}
}
}
}