muffin v6.12

This commit is contained in:
alydev 2025-10-09 07:53:51 +10:00
parent 060278c1b7
commit 155fbee291
59 changed files with 40083 additions and 58104 deletions

View file

@ -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";
}
}