437 lines
14 KiB
C#
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";
|
|
}
|
|
}
|