forked from liza/Questionable
393 lines
15 KiB
C#
393 lines
15 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Dalamud.Memory;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
using LLib.GameData;
|
|
using LLib.GameUI;
|
|
using Lumina.Excel.GeneratedSheets;
|
|
using Questionable.Controller;
|
|
using Questionable.Data;
|
|
using Questionable.Model;
|
|
using Questionable.Model.Questing;
|
|
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
|
|
using Quest = Questionable.Model.Quest;
|
|
|
|
namespace Questionable.Functions;
|
|
|
|
internal sealed unsafe class QuestFunctions
|
|
{
|
|
private readonly QuestRegistry _questRegistry;
|
|
private readonly QuestData _questData;
|
|
private readonly Configuration _configuration;
|
|
private readonly IDataManager _dataManager;
|
|
private readonly IClientState _clientState;
|
|
private readonly IGameGui _gameGui;
|
|
|
|
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration,
|
|
IDataManager dataManager, IClientState clientState, IGameGui gameGui)
|
|
{
|
|
_questRegistry = questRegistry;
|
|
_questData = questData;
|
|
_configuration = configuration;
|
|
_dataManager = dataManager;
|
|
_clientState = clientState;
|
|
_gameGui = gameGui;
|
|
}
|
|
|
|
public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuest()
|
|
{
|
|
var (currentQuest, sequence) = GetCurrentQuestInternal();
|
|
PlayerState* playerState = PlayerState.Instance();
|
|
|
|
if (currentQuest == null || currentQuest.Value == 0)
|
|
{
|
|
if (_clientState.TerritoryType == 181) // Starting in Limsa
|
|
return (new QuestId(107), 0);
|
|
if (_clientState.TerritoryType == 182) // Starting in Ul'dah
|
|
return (new QuestId(594), 0);
|
|
if (_clientState.TerritoryType == 183) // Starting in Gridania
|
|
return (new QuestId(39), 0);
|
|
return default;
|
|
}
|
|
else if (currentQuest.Value == 681)
|
|
{
|
|
// if we have already picked up the GC quest, just return the progress for it
|
|
if (IsQuestAccepted(currentQuest) || IsQuestComplete(currentQuest))
|
|
return (currentQuest, sequence);
|
|
|
|
// The company you keep...
|
|
return _configuration.General.GrandCompany switch
|
|
{
|
|
GrandCompany.TwinAdder => (new QuestId(680), 0),
|
|
GrandCompany.Maelstrom => (new QuestId(681), 0),
|
|
_ => default
|
|
};
|
|
}
|
|
else if (currentQuest.Value == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace
|
|
{
|
|
ushort chocoboQuest = (GrandCompany)playerState->GrandCompany switch
|
|
{
|
|
GrandCompany.TwinAdder => 700,
|
|
GrandCompany.Maelstrom => 701,
|
|
_ => 0
|
|
};
|
|
|
|
if (chocoboQuest != 0 && !QuestManager.IsQuestComplete(chocoboQuest))
|
|
return (new QuestId(chocoboQuest), QuestManager.GetQuestSequence(chocoboQuest));
|
|
}
|
|
else if (currentQuest.Value == 801)
|
|
{
|
|
// skeletons in her closet, finish 'broadening horizons' to unlock the white wolf gate
|
|
QuestId broadeningHorizons = new QuestId(802);
|
|
if (IsQuestAccepted(broadeningHorizons))
|
|
return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons.Value));
|
|
}
|
|
|
|
return (currentQuest, sequence);
|
|
}
|
|
|
|
public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuestInternal()
|
|
{
|
|
var questManager = QuestManager.Instance();
|
|
if (questManager != null)
|
|
{
|
|
// always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do
|
|
// side quests until the end of time.
|
|
var msqQuest = GetMainScenarioQuest(questManager);
|
|
if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
|
|
return msqQuest;
|
|
|
|
// Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item,
|
|
// do the MSQ; if a side quest is the first item do that side quest.
|
|
//
|
|
// If no quests are marked as 'priority', accepting a new quest adds it to the top of the list.
|
|
for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i)
|
|
{
|
|
ElementId currentQuest;
|
|
var trackedQuest = questManager->TrackedQuests[i];
|
|
switch (trackedQuest.QuestType)
|
|
{
|
|
default:
|
|
continue;
|
|
|
|
case 1: // normal quest
|
|
currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId);
|
|
if (_questRegistry.IsKnownQuest(currentQuest))
|
|
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
|
|
break;
|
|
|
|
case 2: // leve
|
|
currentQuest = new LeveId(questManager->LeveQuests[trackedQuest.Index].LeveId);
|
|
if (_questRegistry.IsKnownQuest(currentQuest))
|
|
return (currentQuest, questManager->GetLeveQuestById(currentQuest.Value)->Sequence);
|
|
break;
|
|
}
|
|
|
|
if (_questRegistry.IsKnownQuest(currentQuest))
|
|
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
|
|
}
|
|
|
|
// if we know no quest of those currently in the to-do list, just do MSQ
|
|
return msqQuest;
|
|
}
|
|
|
|
return default;
|
|
}
|
|
|
|
private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
|
|
{
|
|
if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
|
|
{
|
|
AgentInterface* questRedoHud = AgentModule.Instance()->GetAgentByInternalId(AgentId.QuestRedoHud);
|
|
if (questRedoHud != null && questRedoHud->IsAgentActive())
|
|
{
|
|
// there's surely better ways to check this, but the one in the OOB Plugin was even less reliable
|
|
if (_gameGui.TryGetAddonByName<AtkUnitBase>("QuestRedoHud", out var addon) &&
|
|
addon->AtkValuesCount == 4 &&
|
|
// 0 seems to be active,
|
|
// 1 seems to be paused,
|
|
// 2 is unknown, but it happens e.g. before the quest 'Alzadaal's Legacy'
|
|
// 3 seems to be having /ng+ open while active,
|
|
// 4 seems to be when (a) suspending the chapter, or (b) having turned in a quest
|
|
addon->AtkValues[0].UInt is 0 or 2 or 3 or 4)
|
|
{
|
|
// redoHud+44 is chapter
|
|
// redoHud+46 is quest
|
|
ushort questId = MemoryHelper.Read<ushort>((nint)questRedoHud + 46);
|
|
return (new QuestId(questId), QuestManager.GetQuestSequence(questId));
|
|
}
|
|
}
|
|
}
|
|
|
|
var scenarioTree = AgentScenarioTree.Instance();
|
|
if (scenarioTree == null)
|
|
return default;
|
|
|
|
if (scenarioTree->Data == null)
|
|
return default;
|
|
|
|
QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest);
|
|
if (currentQuest.Value == 0)
|
|
return default;
|
|
|
|
// if the MSQ is hidden, we generally ignore it
|
|
if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
|
|
return default;
|
|
|
|
// it can sometimes happen (although this isn't reliably reproducible) that the quest returned here
|
|
// is one you've just completed. We return 255 as sequence here, since that is the end of said quest;
|
|
// but this is just really hoping that this breaks nothing.
|
|
if (IsQuestComplete(currentQuest))
|
|
return (currentQuest, 255);
|
|
else if (!IsReadyToAcceptQuest(currentQuest))
|
|
return default;
|
|
|
|
// if we're not at a high enough level to continue, we also ignore it
|
|
var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
|
|
if (currentLevel != 0 &&
|
|
_questRegistry.TryGetQuest(currentQuest, out Quest? quest)
|
|
&& quest.Info.Level > currentLevel)
|
|
return default;
|
|
|
|
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
|
|
}
|
|
|
|
public QuestProgressInfo? GetQuestProgressInfo(ElementId elementId)
|
|
{
|
|
if (elementId is QuestId questId)
|
|
{
|
|
QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value);
|
|
return questWork != null ? new QuestProgressInfo(*questWork) : null;
|
|
}
|
|
else if (elementId is LeveId leveId)
|
|
{
|
|
LeveWork* leveWork = QuestManager.Instance()->GetLeveQuestById(leveId.Value);
|
|
return leveWork != null ? new QuestProgressInfo(*leveWork) : null;
|
|
}
|
|
else
|
|
return null;
|
|
}
|
|
|
|
public bool IsReadyToAcceptQuest(ElementId questId)
|
|
{
|
|
_questRegistry.TryGetQuest(questId, out var quest);
|
|
if (quest is { Info.IsRepeatable: true })
|
|
{
|
|
if (IsQuestAccepted(questId))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (IsQuestAcceptedOrComplete(questId))
|
|
return false;
|
|
}
|
|
|
|
if (IsQuestLocked(questId))
|
|
return false;
|
|
|
|
// if we're not at a high enough level to continue, we also ignore it
|
|
var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
|
|
if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool IsQuestAcceptedOrComplete(ElementId elementId)
|
|
{
|
|
return IsQuestComplete(elementId) || IsQuestAccepted(elementId);
|
|
}
|
|
|
|
public bool IsQuestAccepted(ElementId elementId)
|
|
{
|
|
if (elementId is QuestId questId)
|
|
return IsQuestAccepted(questId);
|
|
else if (elementId is LeveId leveId)
|
|
return IsQuestAccepted(leveId);
|
|
else if (elementId is SatisfactionSupplyNpcId)
|
|
return false;
|
|
else
|
|
throw new ArgumentOutOfRangeException(nameof(elementId));
|
|
}
|
|
|
|
public bool IsQuestAccepted(QuestId questId)
|
|
{
|
|
QuestManager* questManager = QuestManager.Instance();
|
|
return questManager->IsQuestAccepted(questId.Value);
|
|
}
|
|
|
|
public bool IsQuestAccepted(LeveId leveId)
|
|
{
|
|
QuestManager* questManager = QuestManager.Instance();
|
|
foreach (var leveQuest in questManager->LeveQuests)
|
|
{
|
|
if (leveQuest.LeveId == leveId.Value)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool IsQuestComplete(ElementId elementId)
|
|
{
|
|
if (elementId is QuestId questId)
|
|
return IsQuestComplete(questId);
|
|
else if (elementId is LeveId leveId)
|
|
return IsQuestComplete(leveId);
|
|
else if (elementId is SatisfactionSupplyNpcId)
|
|
return false;
|
|
else
|
|
throw new ArgumentOutOfRangeException(nameof(elementId));
|
|
}
|
|
|
|
[SuppressMessage("Performance", "CA1822")]
|
|
public bool IsQuestComplete(QuestId questId)
|
|
{
|
|
return QuestManager.IsQuestComplete(questId.Value);
|
|
}
|
|
|
|
public bool IsQuestComplete(LeveId leveId)
|
|
{
|
|
return QuestManager.Instance()->IsLevequestComplete(leveId.Value);
|
|
}
|
|
|
|
public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
|
|
{
|
|
if (elementId is QuestId questId)
|
|
return IsQuestLocked(questId, extraCompletedQuest);
|
|
else if (elementId is LeveId leveId)
|
|
return IsQuestLocked(leveId);
|
|
else if (elementId is SatisfactionSupplyNpcId)
|
|
return false;
|
|
else
|
|
throw new ArgumentOutOfRangeException(nameof(elementId));
|
|
}
|
|
|
|
public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
|
|
{
|
|
var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
|
|
if (questInfo.QuestLocks.Count > 0)
|
|
{
|
|
var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
|
|
if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests)
|
|
return true;
|
|
else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
|
|
return true;
|
|
}
|
|
|
|
if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
|
|
return true;
|
|
|
|
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
|
|
}
|
|
|
|
public bool IsQuestLocked(LeveId leveId)
|
|
{
|
|
// this only checks for the current class
|
|
IQuestInfo questInfo = _questData.GetQuestInfo(leveId);
|
|
if (!questInfo.ClassJobs.Contains((EClassJob)_clientState.LocalPlayer!.ClassJob.Id) ||
|
|
questInfo.Level > _clientState.LocalPlayer.Level)
|
|
return true;
|
|
|
|
return !IsQuestAccepted(leveId) && QuestManager.Instance()->NumLeveAllowances == 0;
|
|
}
|
|
|
|
private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest)
|
|
{
|
|
if (questInfo.PreviousQuests.Count == 0)
|
|
return true;
|
|
|
|
var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
|
|
if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All &&
|
|
questInfo.PreviousQuests.Count == completedQuests)
|
|
return true;
|
|
else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
private static bool HasCompletedPreviousInstances(QuestInfo questInfo)
|
|
{
|
|
if (questInfo.PreviousInstanceContent.Count == 0)
|
|
return true;
|
|
|
|
var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x));
|
|
if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All &&
|
|
questInfo.PreviousInstanceContent.Count == completedInstances)
|
|
return true;
|
|
else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
public bool IsClassJobUnlocked(EClassJob classJob)
|
|
{
|
|
var classJobRow = _dataManager.GetExcelSheet<ClassJob>()!.GetRow((uint)classJob)!;
|
|
var questId = (ushort)classJobRow.UnlockQuest.Row;
|
|
if (questId != 0)
|
|
return IsQuestComplete(new QuestId(questId));
|
|
|
|
PlayerState* playerState = PlayerState.Instance();
|
|
return playerState != null && playerState->ClassJobLevels[classJobRow.ExpArrayIndex] > 0;
|
|
}
|
|
|
|
public bool IsJobUnlocked(EClassJob classJob)
|
|
{
|
|
var classJobRow = _dataManager.GetExcelSheet<ClassJob>()!.GetRow((uint)classJob)!;
|
|
return IsClassJobUnlocked((EClassJob)classJobRow.ClassJobParent.Row);
|
|
}
|
|
|
|
public GrandCompany GetGrandCompany()
|
|
{
|
|
return (GrandCompany)PlayerState.Instance()->GrandCompany;
|
|
}
|
|
}
|