muffin v6.12

This commit is contained in:
alydev 2025-10-09 07:53:51 +10:00
parent e786325cda
commit 0950798597
64 changed files with 40100 additions and 58121 deletions

View file

@ -14,8 +14,12 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.Fate;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
@ -540,4 +544,73 @@ internal sealed class GameFunctions
_logger.LogInformation("Unlocked unlock links: {UnlockedUnlockLinks}", string.Join(", ", list));
return list;
}
public unsafe bool SyncToFate(uint fateId)
{
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
if (localPlayer == null)
{
_logger.LogWarning("Cannot sync to FATE: LocalPlayer is null");
return false;
}
FateManager* ptr = FateManager.Instance();
if (ptr == null || ptr->CurrentFate == null)
{
return false;
}
FateContext* currentFate = ptr->CurrentFate;
byte maxLevel = currentFate->MaxLevel;
if (localPlayer.Level <= maxLevel)
{
return true;
}
try
{
_logger.LogInformation("Syncing to FATE {FateId} (max level {MaxLevel})", currentFate->FateId, maxLevel);
ExecuteCommand("/lsync");
return true;
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to sync to FATE {FateId}", fateId);
return false;
}
}
public unsafe ushort GetCurrentFateId()
{
FateManager* ptr = FateManager.Instance();
if (ptr == null || ptr->CurrentFate == null)
{
return 0;
}
return ptr->CurrentFate->FateId;
}
private unsafe void ExecuteCommand(string command)
{
try
{
UIModule* uIModule = Framework.Instance()->GetUIModule();
if (uIModule == null)
{
_logger.LogError("UIModule is null, cannot execute command: {Command}", command);
return;
}
Utf8String utf8String = new Utf8String(command);
try
{
uIModule->ProcessChatBoxEntry(&utf8String, (nint)utf8String.StringLength);
_logger.LogDebug("Executed chat command: {Command}", command);
}
finally
{
((IDisposable)utf8String/*cast due to .constrained prefix*/).Dispose();
}
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to execute command: {Command}", command);
}
}
}

View file

@ -16,11 +16,13 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameData;
using LLib.GameUI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
using Questionable.Windows.QuestComponents;
namespace Questionable.Functions;
@ -36,6 +38,8 @@ internal sealed class QuestFunctions
private readonly AlliedSocietyData _alliedSocietyData;
private readonly AetheryteData _aetheryteData;
private readonly Configuration _configuration;
private readonly IDataManager _dataManager;
@ -46,18 +50,24 @@ internal sealed class QuestFunctions
private readonly IAetheryteList _aetheryteList;
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, AetheryteFunctions aetheryteFunctions, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, AlliedSocietyData alliedSocietyData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui, IAetheryteList aetheryteList)
private readonly ILogger<QuestFunctions> _logger;
private readonly HashSet<ushort> _alreadyLoggedUnobtainableQuestsDetailed = new HashSet<ushort>();
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, AetheryteFunctions aetheryteFunctions, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, AlliedSocietyData alliedSocietyData, AetheryteData aetheryteData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui, IAetheryteList aetheryteList, ILogger<QuestFunctions> logger)
{
_questRegistry = questRegistry;
_questData = questData;
_aetheryteFunctions = aetheryteFunctions;
_alliedSocietyQuestFunctions = alliedSocietyQuestFunctions;
_alliedSocietyData = alliedSocietyData;
_aetheryteData = aetheryteData;
_configuration = configuration;
_dataManager = dataManager;
_clientState = clientState;
_gameGui = gameGui;
_aetheryteList = aetheryteList;
_logger = logger;
}
public unsafe QuestReference GetCurrentQuest(bool allowNewMsq = true)
@ -558,10 +568,20 @@ internal sealed class QuestFunctions
if (!ignoreLevel)
{
byte b = _clientState.LocalPlayer?.Level ?? 0;
if (b != 0 && quest != null && quest.Info.Level > b)
if (b == 0)
{
return false;
}
if (quest != null && quest.Info.Level > b)
{
_logger.LogDebug("Quest {QuestId} level requirement not met: required {RequiredLevel}, current {CurrentLevel}", questId, quest.Info.Level, b);
return false;
}
if (quest == null && questId is QuestId questId3 && _questData.TryGetQuestInfo(questId3, out IQuestInfo questInfo) && questInfo is QuestInfo questInfo2 && questInfo2.Level > b)
{
_logger.LogDebug("Quest {QuestId} (from data) level requirement not met: required {RequiredLevel}, current {CurrentLevel}", questId3, questInfo2.Level, b);
return false;
}
}
return true;
}
@ -593,6 +613,14 @@ internal sealed class QuestFunctions
{
return false;
}
if (elementId is AethernetId)
{
return false;
}
if (elementId is AetherCurrentId)
{
return false;
}
throw new ArgumentOutOfRangeException("elementId");
}
@ -619,6 +647,14 @@ internal sealed class QuestFunctions
{
return IsQuestComplete(unlockLinkId);
}
if (elementId is AethernetId aethernetId)
{
return IsQuestComplete(aethernetId);
}
if (elementId is AetherCurrentId aetherCurrentId)
{
return IsQuestComplete(aetherCurrentId);
}
throw new ArgumentOutOfRangeException("elementId");
}
@ -632,6 +668,29 @@ internal sealed class QuestFunctions
return UIState.Instance()->IsUnlockLinkUnlocked(unlockLinkId.Value);
}
public bool IsQuestComplete(AethernetId aethernetId)
{
if (!_questRegistry.TryGetQuest(aethernetId, out Questionable.Model.Quest quest))
{
_logger.LogWarning("Aethernet quest {AethernetId} not found in registry", aethernetId);
return false;
}
List<(QuestSequence, int, QuestStep)> list = (from x in quest.AllSteps()
where x.Step.InteractionType == EInteractionType.AttuneAethernetShard && x.Step.AethernetShard.HasValue
select x).ToList();
if (list.Count == 0)
{
_logger.LogWarning("Aethernet quest {AethernetId} has no aethernet shard attunement steps", aethernetId);
return false;
}
return list.All<(QuestSequence, int, QuestStep)>(((QuestSequence Sequence, int StepId, QuestStep Step) step) => _aetheryteFunctions.IsAetheryteUnlocked(step.Step.AethernetShard.Value));
}
public bool IsQuestComplete(AetherCurrentId aetherCurrentId)
{
return false;
}
public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
{
if (elementId is QuestId questId)
@ -650,6 +709,14 @@ internal sealed class QuestFunctions
{
return IsQuestLocked(unlockLinkId);
}
if (elementId is AethernetId aethernetId)
{
return IsQuestLocked(aethernetId);
}
if (elementId is AetherCurrentId aetherCurrentId)
{
return IsQuestLocked(aetherCurrentId);
}
throw new ArgumentOutOfRangeException("elementId");
}
@ -708,6 +775,39 @@ internal sealed class QuestFunctions
return IsQuestUnobtainable(unlockLinkId);
}
private bool IsQuestLocked(AethernetId aethernetId)
{
EAetheryteLocation aetheryteLocation = aethernetId.Value switch
{
1 => EAetheryteLocation.Limsa,
2 => EAetheryteLocation.Gridania,
3 => EAetheryteLocation.Uldah,
4 => EAetheryteLocation.GoldSaucer,
5 => EAetheryteLocation.Ishgard,
6 => EAetheryteLocation.Idyllshire,
7 => EAetheryteLocation.RhalgrsReach,
8 => EAetheryteLocation.Kugane,
9 => EAetheryteLocation.DomanEnclave,
10 => EAetheryteLocation.Crystarium,
11 => EAetheryteLocation.Eulmore,
12 => EAetheryteLocation.OldSharlayan,
13 => EAetheryteLocation.RadzAtHan,
14 => EAetheryteLocation.Tuliyollal,
15 => EAetheryteLocation.SolutionNine,
_ => throw new ArgumentOutOfRangeException("aethernetId", $"Unknown AethernetId: {aethernetId.Value}"),
};
if (!_aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{
return true;
}
return false;
}
private static bool IsQuestLocked(AetherCurrentId aetherCurrentId)
{
return false;
}
public bool IsDailyAlliedSocietyQuest(QuestId questId)
{
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
@ -743,19 +843,110 @@ internal sealed class QuestFunctions
public unsafe bool IsQuestUnobtainable(QuestId questId, ElementId? extraCompletedQuest = null)
{
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
if ((int)questInfo.Expansion > (int)PlayerState.Instance()->MaxExpansion)
IQuestInfo questInfo = _questData.GetQuestInfo(questId);
if (questInfo is UnlockLinkQuestInfo { QuestExpiry: { TimeOfDay: var timeOfDay } questExpiry })
{
TimeSpan timeSpan = new TimeSpan(23, 59, 59);
bool flag = false;
DateTime dateTime;
if (timeOfDay == TimeSpan.Zero || timeOfDay == timeSpan)
{
dateTime = EventInfoComponent.AtDailyReset(DateOnly.FromDateTime(questExpiry));
flag = true;
}
else
{
dateTime = ((questExpiry.Kind == DateTimeKind.Utc) ? questExpiry : questExpiry.ToUniversalTime());
}
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("UnlockLink quest {QuestId} expiry raw={ExpiryRaw} Kind={Kind} TimeOfDay={TimeOfDay}", questId, questExpiry.ToString("o"), questExpiry.Kind, questExpiry.TimeOfDay);
_logger.LogDebug("UnlockLink quest {QuestId} normalized expiryUtc={ExpiryUtc:o} treatedAsDailyReset={TreatedAsDailyReset}", questId, dateTime, flag);
}
if (DateTime.UtcNow > dateTime)
{
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("UnlockLink quest {QuestId} unobtainable: expiry {ExpiryUtc} (UTC) is before now {NowUtc}", questId, dateTime.ToString("o"), DateTime.UtcNow.ToString("o"));
}
return true;
}
}
QuestInfo questInfo2 = (QuestInfo)questInfo;
if ((int)questInfo2.Expansion > (int)PlayerState.Instance()->MaxExpansion)
{
return true;
}
if (questInfo.QuestLocks.Count > 0)
if (questInfo2.JournalGenre >= 234 && questInfo2.JournalGenre <= 247)
{
int num = questInfo.QuestLocks.Count((QuestId x) => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo.QuestLockJoin == EQuestJoin.All && questInfo.QuestLocks.Count == num)
if (_questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest))
{
List<QuestSequence> list = quest?.Root?.QuestSequence;
if (list != null && list.Count > 0)
{
goto IL_0288;
}
}
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_questData.ApplySeasonalOverride(questId, isSeasonal: true, null);
_logger.LogDebug("Quest {QuestId} unobtainable: journal genre is 'event (seasonal)' and no quest path", questId);
}
return true;
}
goto IL_0288;
IL_0288:
if (questInfo2.QuestLocks.Count > 0)
{
int num = questInfo2.QuestLocks.Count((QuestId x) => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo2.QuestLockJoin == EQuestJoin.All && questInfo2.QuestLocks.Count == num)
{
return true;
}
if (questInfo.QuestLockJoin == EQuestJoin.AtLeastOne && num > 0)
if (questInfo2.QuestLockJoin == EQuestJoin.AtLeastOne && num > 0)
{
return true;
}
}
DateTime? seasonalQuestExpiry = questInfo2.SeasonalQuestExpiry;
if (seasonalQuestExpiry.HasValue)
{
DateTime valueOrDefault = seasonalQuestExpiry.GetValueOrDefault();
TimeSpan timeOfDay2 = valueOrDefault.TimeOfDay;
TimeSpan timeSpan2 = new TimeSpan(23, 59, 59);
bool flag2 = false;
DateTime dateTime2;
if (timeOfDay2 == TimeSpan.Zero || timeOfDay2 == timeSpan2)
{
dateTime2 = EventInfoComponent.AtDailyReset(DateOnly.FromDateTime(valueOrDefault));
flag2 = true;
}
else
{
dateTime2 = ((valueOrDefault.Kind == DateTimeKind.Utc) ? valueOrDefault : valueOrDefault.ToUniversalTime());
}
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("Quest {QuestId} seasonal expiry raw={ExpiryRaw} Kind={Kind} TimeOfDay={TimeOfDay}", questId, valueOrDefault.ToString("o"), valueOrDefault.Kind, valueOrDefault.TimeOfDay);
_logger.LogDebug("Quest {QuestId} normalized expiryUtc={ExpiryUtc:o} treatedAsDailyReset={TreatedAsDailyReset}", questId, dateTime2, flag2);
_logger.LogTrace("Quest {QuestId} expiry check: nowUtc={Now:o}, expiryUtc={Expiry:o}, expired={Expired}", questId, DateTime.UtcNow, dateTime2, DateTime.UtcNow > dateTime2);
}
if (DateTime.UtcNow > dateTime2)
{
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("Quest {QuestId} unobtainable: seasonal expiry {ExpiryUtc} (UTC) is before now {NowUtc}", questId, dateTime2.ToString("o"), DateTime.UtcNow.ToString("o"));
}
return true;
}
}
if ((questInfo2.IsSeasonalEvent || questInfo2.IsSeasonalQuest) && !(questInfo2.SeasonalQuestExpiry is DateTime))
{
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("Quest {QuestId} is seasonal/event with no expiry; ShowIncompleteSeasonalEvents={ShowIncomplete}", questId, _configuration.General.ShowIncompleteSeasonalEvents);
}
if (!_configuration.General.ShowIncompleteSeasonalEvents)
{
return true;
}
@ -765,7 +956,7 @@ internal sealed class QuestFunctions
return true;
}
byte startTown = PlayerState.Instance()->StartTown;
if (questInfo.StartingCity > 0 && questInfo.StartingCity != startTown)
if (questInfo2.StartingCity > 0 && questInfo2.StartingCity != startTown)
{
return true;
}