muffin v6.12
This commit is contained in:
parent
060278c1b7
commit
155fbee291
59 changed files with 40083 additions and 58104 deletions
|
@ -1,15 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Humanizer;
|
||||
using Humanizer.Localisation;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller;
|
||||
using Questionable.Data;
|
||||
using Questionable.Functions;
|
||||
|
@ -20,9 +19,7 @@ namespace Questionable.Windows.QuestComponents;
|
|||
|
||||
internal sealed class EventInfoComponent
|
||||
{
|
||||
private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc);
|
||||
|
||||
private readonly List<EventQuest> _eventQuests;
|
||||
private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc, string? Patch);
|
||||
|
||||
private readonly QuestData _questData;
|
||||
|
||||
|
@ -38,42 +35,33 @@ internal sealed class EventInfoComponent
|
|||
|
||||
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)
|
||||
if (!_configuration.General.ShowIncompleteSeasonalEvents)
|
||||
{
|
||||
return _eventQuests.Any(IsIncomplete);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
UpdateCacheIfNeeded();
|
||||
return _cachedActiveSeasonalQuests.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, QuestTooltipComponent questTooltipComponent, Configuration configuration)
|
||||
public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, QuestTooltipComponent questTooltipComponent, Configuration configuration, IDataManager dataManager, ILogger<EventInfoComponent> logger)
|
||||
{
|
||||
int num = 2;
|
||||
List<EventQuest> list = new List<EventQuest>(num);
|
||||
CollectionsMarshal.SetCount(list, num);
|
||||
Span<EventQuest> span = CollectionsMarshal.AsSpan(list);
|
||||
int num2 = 0;
|
||||
ref EventQuest reference = ref span[num2];
|
||||
int num3 = 1;
|
||||
List<ElementId> list2 = new List<ElementId>(num3);
|
||||
CollectionsMarshal.SetCount(list2, num3);
|
||||
CollectionsMarshal.AsSpan(list2)[0] = new UnlockLinkId(568);
|
||||
reference = new EventQuest("Limited Time Items", list2, DateTime.MaxValue);
|
||||
ref EventQuest reference2 = ref span[num2 + 1];
|
||||
int num4 = 2;
|
||||
List<ElementId> list3 = new List<ElementId>(num4);
|
||||
CollectionsMarshal.SetCount(list3, num4);
|
||||
Span<ElementId> span2 = CollectionsMarshal.AsSpan(list3);
|
||||
num3 = 0;
|
||||
span2[num3] = new QuestId(5297);
|
||||
span2[num3 + 1] = new QuestId(5298);
|
||||
reference2 = new EventQuest("The Rising 2025", list3, AtDailyReset(new DateOnly(2025, 9, 11)));
|
||||
_eventQuests = list;
|
||||
base._002Ector();
|
||||
_questData = questData;
|
||||
_questRegistry = questRegistry;
|
||||
_questFunctions = questFunctions;
|
||||
|
@ -81,39 +69,97 @@ internal sealed class EventInfoComponent
|
|||
_questController = questController;
|
||||
_questTooltipComponent = questTooltipComponent;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
private static DateTime AtDailyReset(DateOnly date)
|
||||
{
|
||||
return new DateTime(date, new TimeOnly(14, 59), DateTimeKind.Utc);
|
||||
_dataManager = dataManager;
|
||||
_logger = logger ?? throw new ArgumentNullException("logger");
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
foreach (EventQuest eventQuest in _eventQuests)
|
||||
UpdateCacheIfNeeded();
|
||||
foreach (IGrouping<string, IQuestInfo> item in _cachedActiveSeasonalQuests.GroupBy(delegate(IQuestInfo q)
|
||||
{
|
||||
if (IsIncomplete(eventQuest))
|
||||
if (q.QuestId is UnlockLinkId)
|
||||
{
|
||||
DrawEventQuest(eventQuest);
|
||||
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)
|
||||
{
|
||||
string value = (eventQuest.EndsAtUtc - DateTime.UtcNow).Humanize(1, CultureInfo.InvariantCulture, TimeUnit.Day, TimeUnit.Minute);
|
||||
ImU8String text = new ImU8String(3, 2);
|
||||
text.AppendFormatted(eventQuest.Name);
|
||||
text.AppendLiteral(" (");
|
||||
text.AppendFormatted(value);
|
||||
text.AppendLiteral(")");
|
||||
ImGui.Text(text);
|
||||
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(eventQuest.Name);
|
||||
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)
|
||||
|
@ -122,13 +168,13 @@ internal sealed class EventInfoComponent
|
|||
{
|
||||
continue;
|
||||
}
|
||||
ImU8String text = new ImU8String(21, 1);
|
||||
text.AppendLiteral("##EventQuestSelection");
|
||||
text.AppendFormatted(questId);
|
||||
using (ImRaii.PushId(text))
|
||||
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 Quest quest))
|
||||
if (list.Contains(questId) && _questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest))
|
||||
{
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
|
||||
{
|
||||
|
@ -157,18 +203,23 @@ internal sealed class EventInfoComponent
|
|||
}
|
||||
}
|
||||
|
||||
private bool IsIncomplete(EventQuest eventQuest)
|
||||
{
|
||||
if (eventQuest.EndsAtUtc <= DateTime.UtcNow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return eventQuest.QuestIds.Any(ShouldShowQuest);
|
||||
}
|
||||
|
||||
public IEnumerable<ElementId> GetCurrentlyActiveEventQuests()
|
||||
{
|
||||
return _eventQuests.Where((EventQuest x) => x.EndsAtUtc >= DateTime.UtcNow).SelectMany((EventQuest x) => x.QuestIds).Where(ShouldShowQuest);
|
||||
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)
|
||||
|
@ -179,4 +230,208 @@ internal sealed class EventInfoComponent
|
|||
}
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue