qstbak/Questionable/Questionable.Windows.QuestComponents/EventInfoComponent.cs
2025-10-09 07:53:51 +10:00

437 lines
14 KiB
C#

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<ElementId> 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<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)
{
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<EventInfoComponent> 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<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;
}))
{
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)
{
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<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)
{
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<ElementId> 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<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";
}
}