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.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 QuestTooltipComponent _questTooltipComponent; private readonly Configuration _configuration; private readonly IDataManager _dataManager; 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.Count > 0; } } public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, QuestTooltipComponent questTooltipComponent, Configuration configuration, IDataManager dataManager, ILogger logger) { _questData = questData; _questRegistry = questRegistry; _questFunctions = questFunctions; _uiUtils = uiUtils; _questController = questController; _questTooltipComponent = questTooltipComponent; _configuration = configuration; _dataManager = dataManager; _logger = logger ?? throw new ArgumentNullException("logger"); } public void Draw() { UpdateCacheIfNeeded(); 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); } } 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 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(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 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 Questionable.Model.Quest quest)) { if (ImGuiComponents.IconButton(FontAwesomeIcon.Play)) { _questController.SetNextQuest(quest); _questController.Start("SeasonalEventSelection"); } bool num = ImGui.IsItemHovered(); ImGui.SameLine(); ImGui.AlignTextToFramePadding(); ImGui.Text(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; } 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 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 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"; } }