using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Controller; using Questionable.Controller.Utils; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows.JournalComponents; namespace Questionable.Windows.QuestComponents; internal sealed class EventInfoComponent { private sealed record EventQuest(string Name, List QuestIds, DateTime EndsAtUtc, string? Patch); private readonly QuestData _questData; private readonly QuestRegistry _questRegistry; private readonly QuestFunctions _questFunctions; private readonly UiUtils _uiUtils; private readonly QuestController _questController; private readonly FateController _fateController; private readonly SeasonalDutyController _seasonalDutyController; private readonly CustomDeliveryController _customDeliveryController; private readonly AttunementController _attunementController; private readonly QuestTooltipComponent _questTooltipComponent; private readonly QuestJournalUtils _questJournalUtils; private readonly Configuration _configuration; private readonly IDataManager _dataManager; private readonly JournalData _journalData; private List _cachedActiveSeasonalQuests = new List(); private DateTime _cachedAtUtc = DateTime.MinValue; private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5L); private readonly ILogger _logger; private readonly HashSet _alreadyLoggedActiveSeasonalSkip = new HashSet(); public bool ShouldDraw { get { if (!_configuration.General.ShowIncompleteSeasonalEvents) { return false; } UpdateCacheIfNeeded(); return _cachedActiveSeasonalQuests.Any((IQuestInfo q) => !_questFunctions.IsQuestComplete(q.QuestId)); } } public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, AttunementController attunementController, QuestTooltipComponent questTooltipComponent, QuestJournalUtils questJournalUtils, Configuration configuration, IDataManager dataManager, JournalData journalData, ILogger logger) { _questData = questData; _questRegistry = questRegistry; _questFunctions = questFunctions; _uiUtils = uiUtils; _questController = questController; _fateController = fateController; _seasonalDutyController = seasonalDutyController; _customDeliveryController = customDeliveryController; _attunementController = attunementController; _questTooltipComponent = questTooltipComponent; _questJournalUtils = questJournalUtils; _configuration = configuration; _dataManager = dataManager; _journalData = journalData; _logger = logger ?? throw new ArgumentNullException("logger"); } public void Draw() { UpdateCacheIfNeeded(); var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard(); UiThemeUtils.SectionHeader("Events"); foreach (IGrouping 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; })) { 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 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); } UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList); } private string GetJournalGenreName(uint journalGenreId) { try { JournalGenre row = _dataManager.GetExcelSheet().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) { TimeSpan timeSpan = eventQuest.EndsAtUtc - DateTime.UtcNow; if (timeSpan < TimeSpan.Zero) { timeSpan = TimeSpan.Zero; } string text2 = FormatRemainingDays(timeSpan); string text3 = FormatRemainingFull(timeSpan); UiThemeUtils.WrappedText(text + " (" + text2 + ")"); if (ImGui.IsItemHovered()) { ImGui.SetTooltip(text3); } } else { UiThemeUtils.WrappedText(text); } List 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) { if (_questFunctions.IsQuestComplete(questId)) { continue; } ImU8String id = new ImU8String(21, 1); id.AppendLiteral("##EventQuestSelection"); id.AppendFormatted(questId); using (ImRaii.PushId(id)) { string name = _questData.GetQuestInfo(questId).Name; if (list.Contains(questId) && _questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest)) { using (ImRaii.Disabled(_questController.IsRunning || _fateController.IsRunning || _seasonalDutyController.IsRunning || _customDeliveryController.IsRunning || _attunementController.IsRunning)) { if (ImGuiComponents.IconButton(FontAwesomeIcon.Play)) { _questJournalUtils.StartQuestAsNext(quest, "SeasonalEventSelection"); } } bool num = ImGui.IsItemHovered(); ImGui.SameLine(); ImGui.AlignTextToFramePadding(); UiThemeUtils.WrappedText(name); if (num | ImGui.IsItemHovered()) { _questTooltipComponent.Draw(quest.Info); } } else { ImGui.SetCursorPosX(ImGui.GetCursorPosX()); (Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(questId); if (_uiUtils.ChecklistItem(name, questStyle.Item1, questStyle.Item2, ImGui.GetStyle().FramePadding.X)) { _questTooltipComponent.Draw(_questData.GetQuestInfo(questId)); } } } } } public IEnumerable GetCurrentlyActiveEventQuests() { 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) { if (!_questFunctions.IsQuestComplete(elementId)) { return !_questFunctions.IsQuestUnobtainable(elementId); } return false; } private IEnumerable GetActiveSeasonalQuestsNoCache() { IEnumerable 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; } if (_questFunctions.IsQuestUnobtainable(questInfo.QuestId)) { continue; } if (questInfo is UnlockLinkQuestInfo { QuestExpiry: var questExpiry }) { if (questExpiry.HasValue) { DateTime valueOrDefault = questExpiry.GetValueOrDefault(); DateTime dateTime = NormalizeExpiry(valueOrDefault); if (dateTime > 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, dateTime); } } else { yield return questInfo; } } else { if (!(questInfo is QuestInfo { SeasonalQuestExpiry: var seasonalQuestExpiry } questInfo2)) { continue; } if (seasonalQuestExpiry.HasValue) { DateTime valueOrDefault2 = seasonalQuestExpiry.GetValueOrDefault(); if (NormalizeExpiry(valueOrDefault2) > DateTime.UtcNow) { yield return questInfo; } } else if (questInfo2.IsSeasonalQuest && !questInfo2.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 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; })) { 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 list = (from q in item select (q as UnlockLinkQuestInfo)?.Patch into p where !string.IsNullOrEmpty(p) select p).Distinct().ToList(); if (list.Count == 1) { _ = list[0]; } } } public void RefreshAndLogSeasonalExpiries() { _cachedAtUtc = DateTime.MinValue; UpdateCacheIfNeeded(); } public static DateTime AtDailyReset(DateOnly date) { return ExpiryUtils.AtDailyReset(date); } internal static DateTime NormalizeExpiry(DateTime d) { return ExpiryUtils.NormalizeExpiry(d); } internal 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}"; } internal 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}h {value2:D2}m {value3:D2}s"; } return $"Ends in {num2}d {value:D2}h {value2:D2}m {value3:D2}s"; } }