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; 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 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, 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; _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)) { _fateController.Stop("Seasonal event start"); _seasonalDutyController.Stop("Seasonal event start"); _customDeliveryController.Stop("Seasonal event start"); _attunementController.Stop("Seasonal event start"); _questController.SetNextQuest(quest); _questController.Start("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"; } }