forked from liza/Questionable
Start available priority quests if no msq quest is available
This commit is contained in:
parent
eb97c60065
commit
ada2cf2833
Questionable
Controller
Data
Functions
Model
QuestionablePlugin.csWindows/QuestComponents
@ -8,7 +8,6 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Event;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
@ -31,8 +30,8 @@ internal sealed class GameUiController : IDisposable
|
||||
{
|
||||
private readonly IAddonLifecycle _addonLifecycle;
|
||||
private readonly IDataManager _dataManager;
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
private readonly AetheryteFunctions _aetheryteFunctions;
|
||||
private readonly ExcelFunctions _excelFunctions;
|
||||
private readonly QuestController _questController;
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
@ -46,8 +45,8 @@ internal sealed class GameUiController : IDisposable
|
||||
public GameUiController(
|
||||
IAddonLifecycle addonLifecycle,
|
||||
IDataManager dataManager,
|
||||
GameFunctions gameFunctions,
|
||||
QuestFunctions questFunctions,
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
ExcelFunctions excelFunctions,
|
||||
QuestController questController,
|
||||
QuestRegistry questRegistry,
|
||||
@ -60,8 +59,8 @@ internal sealed class GameUiController : IDisposable
|
||||
{
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_dataManager = dataManager;
|
||||
_gameFunctions = gameFunctions;
|
||||
_questFunctions = questFunctions;
|
||||
_aetheryteFunctions = aetheryteFunctions;
|
||||
_excelFunctions = excelFunctions;
|
||||
_questController = questController;
|
||||
_questRegistry = questRegistry;
|
||||
@ -570,7 +569,7 @@ internal sealed class GameUiController : IDisposable
|
||||
private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno,
|
||||
QuestController.QuestProgress currentQuest, string actualPrompt)
|
||||
{
|
||||
if (_gameFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
|
||||
if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
|
||||
{
|
||||
_logger.LogInformation("Automatically confirming return...");
|
||||
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
|
||||
|
@ -752,94 +752,23 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
|
||||
return currentStep?.AetheryteShortcut != null;
|
||||
}
|
||||
|
||||
private List<ElementId> GetPriorityQuests()
|
||||
{
|
||||
List<ElementId> priorityQuests =
|
||||
[
|
||||
new QuestId(1157), // Garuda (Hard)
|
||||
new QuestId(1158), // Titan (Hard)
|
||||
..QuestData.CrystalTowerQuests
|
||||
];
|
||||
|
||||
EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer;
|
||||
ushort[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray();
|
||||
if (classJob != EClassJob.Adventurer)
|
||||
{
|
||||
priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob)
|
||||
.Where(x =>
|
||||
{
|
||||
if (!_questRegistry.TryGetQuest(x.QuestId, out Quest? quest) ||
|
||||
quest.Info is not QuestInfo questInfo)
|
||||
return false;
|
||||
|
||||
// if no shadowbringers role quest is complete, (at least one) is required
|
||||
if (shadowbringersRoleQuestChapters.Contains(questInfo.NewGamePlusChapter))
|
||||
return !QuestData.FinalShadowbringersRoleQuests.Any(_questFunctions.IsQuestComplete);
|
||||
|
||||
// ignore all other role quests
|
||||
if (QuestData.AllRoleQuestChapters.Any(y => y.Contains(questInfo.NewGamePlusChapter)))
|
||||
return false;
|
||||
|
||||
// even job quests for the later expacs (after role quests were introduced) might have skills locked
|
||||
// behind them, e.g. reaper and sage
|
||||
|
||||
return true;
|
||||
})
|
||||
.Select(x => x.QuestId));
|
||||
}
|
||||
|
||||
return priorityQuests;
|
||||
}
|
||||
|
||||
public bool TryPickPriorityQuest()
|
||||
{
|
||||
if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null)
|
||||
return false;
|
||||
|
||||
// don't start a second priority quest until the first one is resolved
|
||||
List<ElementId> priorityQuests = GetPriorityQuests();
|
||||
if (_startedQuest != null && priorityQuests.Contains(_startedQuest.Quest.Id))
|
||||
ElementId? priorityQuestId = _questFunctions.GetNextPriorityQuestThatCanBeAccepted();
|
||||
if (priorityQuestId == null)
|
||||
return false;
|
||||
|
||||
foreach (ElementId questId in priorityQuests)
|
||||
// don't start a second priority quest until the first one is resolved
|
||||
if (_startedQuest != null && priorityQuestId == _startedQuest.Quest.Id)
|
||||
return false;
|
||||
|
||||
if (_questRegistry.TryGetQuest(priorityQuestId, out var quest))
|
||||
{
|
||||
if (!_questFunctions.IsReadyToAcceptQuest(questId) || !_questRegistry.TryGetQuest(questId, out var quest))
|
||||
continue;
|
||||
|
||||
var firstStep = quest.FindSequence(0)?.FindStep(0);
|
||||
if (firstStep == null)
|
||||
continue;
|
||||
|
||||
if (firstStep.AetheryteShortcut is { } aetheryteShortcut)
|
||||
{
|
||||
if (_gameFunctions.IsAetheryteUnlocked(aetheryteShortcut))
|
||||
{
|
||||
_logger.LogInformation("Priority quest is accessible via aetheryte {Aetheryte}", aetheryteShortcut);
|
||||
SetNextQuest(quest);
|
||||
|
||||
_chatGui.Print(
|
||||
$"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests.");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Ignoring priority quest {QuestId} / {QuestName}, aetheryte locked", quest.Id,
|
||||
quest.Info.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (firstStep is { InteractionType: EInteractionType.UseItem, ItemId: UseItem.VesperBayAetheryteTicket })
|
||||
{
|
||||
_logger.LogInformation("Priority quest is accessible via vesper bay");
|
||||
SetNextQuest(quest);
|
||||
|
||||
_chatGui.Print(
|
||||
$"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests.");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
_logger.LogTrace("Ignoring priority quest {QuestId} / {QuestName}, as we don't know how to get there",
|
||||
questId, quest.Info.Name);
|
||||
SetNextQuest(quest);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -25,7 +25,10 @@ internal static class AethernetShard
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
|
||||
internal sealed class DoAttune(
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
GameFunctions gameFunctions,
|
||||
ILogger<DoAttune> logger) : ITask
|
||||
{
|
||||
public EAetheryteLocation AetheryteLocation { get; set; }
|
||||
|
||||
@ -37,7 +40,7 @@ internal static class AethernetShard
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
|
||||
if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation))
|
||||
{
|
||||
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation);
|
||||
gameFunctions.InteractWith((uint)AetheryteLocation, ObjectKind.Aetheryte);
|
||||
@ -49,7 +52,7 @@ internal static class AethernetShard
|
||||
}
|
||||
|
||||
public ETaskResult Update() =>
|
||||
gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
|
||||
aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
|
||||
|
@ -24,7 +24,10 @@ internal static class Aetheryte
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
|
||||
internal sealed class DoAttune(
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
GameFunctions gameFunctions,
|
||||
ILogger<DoAttune> logger) : ITask
|
||||
{
|
||||
public EAetheryteLocation AetheryteLocation { get; set; }
|
||||
|
||||
@ -36,7 +39,7 @@ internal static class Aetheryte
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
|
||||
if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation))
|
||||
{
|
||||
logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation);
|
||||
gameFunctions.InteractWith((uint)AetheryteLocation);
|
||||
@ -48,7 +51,7 @@ internal static class Aetheryte
|
||||
}
|
||||
|
||||
public ETaskResult Update() =>
|
||||
gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
|
||||
aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
|
||||
|
@ -32,7 +32,7 @@ internal static class AethernetShortcut
|
||||
|
||||
internal sealed class UseAethernetShortcut(
|
||||
ILogger<UseAethernetShortcut> logger,
|
||||
GameFunctions gameFunctions,
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
IClientState clientState,
|
||||
AetheryteData aetheryteData,
|
||||
LifestreamIpc lifestreamIpc,
|
||||
@ -72,22 +72,22 @@ internal static class AethernetShortcut
|
||||
}
|
||||
|
||||
if (SkipConditions.AetheryteLocked != null &&
|
||||
!gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
|
||||
!aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SkipConditions.AetheryteUnlocked != null &&
|
||||
gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
|
||||
aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameFunctions.IsAetheryteUnlocked(From) &&
|
||||
gameFunctions.IsAetheryteUnlocked(To))
|
||||
if (aetheryteFunctions.IsAetheryteUnlocked(From) &&
|
||||
aetheryteFunctions.IsAetheryteUnlocked(To))
|
||||
{
|
||||
ushort territoryType = clientState.TerritoryType;
|
||||
Vector3 playerPosition = clientState.LocalPlayer!.Position;
|
||||
|
@ -17,7 +17,7 @@ internal static class AetheryteShortcut
|
||||
{
|
||||
internal sealed class Factory(
|
||||
IServiceProvider serviceProvider,
|
||||
GameFunctions gameFunctions,
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
AetheryteData aetheryteData) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
@ -29,7 +29,7 @@ internal static class AetheryteShortcut
|
||||
.With(step, step.AetheryteShortcut.Value, aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]);
|
||||
return
|
||||
[
|
||||
new WaitConditionTask(() => gameFunctions.CanTeleport(step.AetheryteShortcut.Value), "CanTeleport"),
|
||||
new WaitConditionTask(() => aetheryteFunctions.CanTeleport(step.AetheryteShortcut.Value), "CanTeleport"),
|
||||
task
|
||||
];
|
||||
}
|
||||
@ -40,7 +40,7 @@ internal static class AetheryteShortcut
|
||||
|
||||
internal sealed class UseAetheryteShortcut(
|
||||
ILogger<UseAetheryteShortcut> logger,
|
||||
GameFunctions gameFunctions,
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
IClientState clientState,
|
||||
IChatGui chatGui,
|
||||
AetheryteData aetheryteData) : ISkippableTask
|
||||
@ -80,14 +80,14 @@ internal static class AetheryteShortcut
|
||||
}
|
||||
|
||||
if (skipConditions.AetheryteLocked != null &&
|
||||
!gameFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
|
||||
!aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteLocked)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipConditions.AetheryteUnlocked != null &&
|
||||
gameFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
|
||||
aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteUnlocked)");
|
||||
return false;
|
||||
@ -124,12 +124,12 @@ internal static class AetheryteShortcut
|
||||
}
|
||||
}
|
||||
|
||||
if (!gameFunctions.IsAetheryteUnlocked(TargetAetheryte))
|
||||
if (!aetheryteFunctions.IsAetheryteUnlocked(TargetAetheryte))
|
||||
{
|
||||
chatGui.PrintError($"[Questionable] Aetheryte {TargetAetheryte} is not unlocked.");
|
||||
throw new TaskException("Aetheryte is not unlocked");
|
||||
}
|
||||
else if (gameFunctions.TeleportAetheryte(TargetAetheryte))
|
||||
else if (aetheryteFunctions.TeleportAetheryte(TargetAetheryte))
|
||||
{
|
||||
logger.LogInformation("Travelling via aetheryte...");
|
||||
return true;
|
||||
|
@ -41,6 +41,7 @@ internal static class SkipCondition
|
||||
|
||||
internal sealed class CheckSkip(
|
||||
ILogger<CheckSkip> logger,
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
GameFunctions gameFunctions,
|
||||
QuestFunctions questFunctions,
|
||||
IClientState clientState) : ITask
|
||||
@ -145,21 +146,21 @@ internal static class SkipCondition
|
||||
DataId: not null,
|
||||
InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard
|
||||
} &&
|
||||
gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value))
|
||||
aetheryteFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SkipConditions.AetheryteLocked != null &&
|
||||
!gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
|
||||
!aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as aetheryte is locked");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SkipConditions.AetheryteUnlocked != null &&
|
||||
gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
|
||||
aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as aetheryte is unlocked");
|
||||
return true;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LLib.GameData;
|
||||
@ -63,6 +64,11 @@ internal sealed class QuestData
|
||||
return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId));
|
||||
}
|
||||
|
||||
public bool TryGetQuestInfo(ElementId elementId, [NotNullWhen(true)] out IQuestInfo? questInfo)
|
||||
{
|
||||
return _quests.TryGetValue(elementId, out questInfo);
|
||||
}
|
||||
|
||||
public List<IQuestInfo> GetAllByIssuerDataId(uint targetId)
|
||||
{
|
||||
return _quests.Values
|
||||
|
77
Questionable/Functions/AetheryteFunctions.cs
Normal file
77
Questionable/Functions/AetheryteFunctions.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model.Common;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed unsafe class AetheryteFunctions
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<AetheryteFunctions> _logger;
|
||||
|
||||
public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
|
||||
|
||||
public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
|
||||
{
|
||||
subIndex = 0;
|
||||
|
||||
var uiState = UIState.Instance();
|
||||
return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId);
|
||||
}
|
||||
|
||||
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
|
||||
return _serviceProvider.GetRequiredService<QuestFunctions>().IsQuestComplete(new QuestId(3672));
|
||||
return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
|
||||
}
|
||||
|
||||
public bool CanTeleport(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId &&
|
||||
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
|
||||
return true;
|
||||
|
||||
return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
|
||||
}
|
||||
|
||||
public bool TeleportAetheryte(uint aetheryteId)
|
||||
{
|
||||
_logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
|
||||
if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
|
||||
{
|
||||
if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
|
||||
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
|
||||
{
|
||||
ReturnRequestedAt = DateTime.Now;
|
||||
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
|
||||
{
|
||||
_logger.LogInformation("Using 'return' for home aetheryte");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
|
||||
{
|
||||
// fallback if return isn't available or (more likely) on a different aetheryte
|
||||
_logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
|
||||
return Telepo.Instance()->Teleport(aetheryteId, subIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
|
||||
=> TeleportAetheryte((uint)aetheryteLocation);
|
||||
}
|
@ -74,62 +74,6 @@ internal sealed unsafe class GameFunctions
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
|
||||
|
||||
public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
|
||||
{
|
||||
subIndex = 0;
|
||||
|
||||
var uiState = UIState.Instance();
|
||||
return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId);
|
||||
}
|
||||
|
||||
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
|
||||
return _questFunctions.IsQuestComplete(new QuestId(3672));
|
||||
return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
|
||||
}
|
||||
|
||||
public bool CanTeleport(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId &&
|
||||
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
|
||||
return true;
|
||||
|
||||
return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
|
||||
}
|
||||
|
||||
public bool TeleportAetheryte(uint aetheryteId)
|
||||
{
|
||||
_logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
|
||||
if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
|
||||
{
|
||||
if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
|
||||
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
|
||||
{
|
||||
ReturnRequestedAt = DateTime.Now;
|
||||
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
|
||||
{
|
||||
_logger.LogInformation("Using 'return' for home aetheryte");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
|
||||
{
|
||||
// fallback if return isn't available or (more likely) on a different aetheryte
|
||||
_logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
|
||||
return Telepo.Instance()->Teleport(aetheryteId, subIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
|
||||
=> TeleportAetheryte((uint)aetheryteLocation);
|
||||
|
||||
public bool IsFlyingUnlocked(ushort territoryId)
|
||||
{
|
||||
if (_configuration.Advanced.NeverFly)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Dalamud.Memory;
|
||||
@ -12,8 +13,10 @@ using LLib.GameData;
|
||||
using LLib.GameUI;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Questionable.Controller;
|
||||
using Questionable.Controller.Steps.Interactions;
|
||||
using Questionable.Data;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Common;
|
||||
using Questionable.Model.Questing;
|
||||
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
|
||||
using Quest = Questionable.Model.Quest;
|
||||
@ -24,16 +27,24 @@ internal sealed unsafe class QuestFunctions
|
||||
{
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
private readonly QuestData _questData;
|
||||
private readonly AetheryteFunctions _aetheryteFunctions;
|
||||
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)
|
||||
public QuestFunctions(
|
||||
QuestRegistry questRegistry,
|
||||
QuestData questData,
|
||||
AetheryteFunctions aetheryteFunctions,
|
||||
Configuration configuration,
|
||||
IDataManager dataManager,
|
||||
IClientState clientState,
|
||||
IGameGui gameGui)
|
||||
{
|
||||
_questRegistry = questRegistry;
|
||||
_questData = questData;
|
||||
_aetheryteFunctions = aetheryteFunctions;
|
||||
_configuration = configuration;
|
||||
_dataManager = dataManager;
|
||||
_clientState = clientState;
|
||||
@ -99,8 +110,11 @@ internal sealed unsafe class QuestFunctions
|
||||
{
|
||||
// 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))
|
||||
var msqQuest = GetMainScenarioQuest();
|
||||
if (msqQuest.CurrentQuest != null && !_questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
|
||||
msqQuest = default;
|
||||
|
||||
if (msqQuest.CurrentQuest != null && !IsQuestAccepted(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,
|
||||
@ -133,14 +147,24 @@ internal sealed unsafe class QuestFunctions
|
||||
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
|
||||
}
|
||||
|
||||
// if we know no quest of those currently in the to-do list, just do MSQ
|
||||
return msqQuest;
|
||||
ElementId? priorityQuest = GetNextPriorityQuestThatCanBeAccepted();
|
||||
if (priorityQuest != null)
|
||||
{
|
||||
// if we have an accepted msq quest, and know of no quest of those currently in the to-do list...
|
||||
// (1) try and find a priority quest to do
|
||||
return (priorityQuest, QuestManager.GetQuestSequence(priorityQuest.Value));
|
||||
}
|
||||
else if (msqQuest.CurrentQuest != null)
|
||||
{
|
||||
// (2) just do a normal msq quest
|
||||
return msqQuest;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
|
||||
private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest()
|
||||
{
|
||||
if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
|
||||
{
|
||||
@ -177,6 +201,7 @@ internal sealed unsafe class QuestFunctions
|
||||
return default;
|
||||
|
||||
// if the MSQ is hidden, we generally ignore it
|
||||
QuestManager* questManager = QuestManager.Instance();
|
||||
if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
|
||||
return default;
|
||||
|
||||
@ -214,6 +239,72 @@ internal sealed unsafe class QuestFunctions
|
||||
return null;
|
||||
}
|
||||
|
||||
public ElementId? GetNextPriorityQuestThatCanBeAccepted()
|
||||
{
|
||||
return GetPriorityQuestsThatCanBeAccepted()
|
||||
.FirstOrDefault(x =>
|
||||
{
|
||||
if (!_questRegistry.TryGetQuest(x, out Quest? quest))
|
||||
return false;
|
||||
|
||||
var firstStep = quest.FindSequence(0)?.FindStep(0);
|
||||
if (firstStep == null)
|
||||
return false;
|
||||
|
||||
if (firstStep.AetheryteShortcut is { } aetheryteShortcut &&
|
||||
_aetheryteFunctions.IsAetheryteUnlocked(aetheryteShortcut))
|
||||
return true;
|
||||
|
||||
if (firstStep is
|
||||
{ InteractionType: EInteractionType.UseItem, ItemId: UseItem.VesperBayAetheryteTicket })
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private List<ElementId> GetPriorityQuestsThatCanBeAccepted()
|
||||
{
|
||||
List<ElementId> priorityQuests =
|
||||
[
|
||||
new QuestId(1157), // Garuda (Hard)
|
||||
new QuestId(1158), // Titan (Hard)
|
||||
..QuestData.CrystalTowerQuests
|
||||
];
|
||||
|
||||
EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer;
|
||||
ushort[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray();
|
||||
if (classJob != EClassJob.Adventurer)
|
||||
{
|
||||
priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob)
|
||||
.Where(x =>
|
||||
{
|
||||
if (!_questRegistry.TryGetQuest(x.QuestId, out Quest? quest) ||
|
||||
quest.Info is not QuestInfo questInfo)
|
||||
return false;
|
||||
|
||||
// if no shadowbringers role quest is complete, (at least one) is required
|
||||
if (shadowbringersRoleQuestChapters.Contains(questInfo.NewGamePlusChapter))
|
||||
return !QuestData.FinalShadowbringersRoleQuests.Any(IsQuestComplete);
|
||||
|
||||
// ignore all other role quests
|
||||
if (QuestData.AllRoleQuestChapters.Any(y => y.Contains(questInfo.NewGamePlusChapter)))
|
||||
return false;
|
||||
|
||||
// even job quests for the later expacs (after role quests were introduced) might have skills locked
|
||||
// behind them, e.g. reaper and sage
|
||||
|
||||
return true;
|
||||
})
|
||||
.Select(x => x.QuestId));
|
||||
}
|
||||
|
||||
return priorityQuests
|
||||
.Where(_questRegistry.IsKnownQuest)
|
||||
.Where(IsReadyToAcceptQuest)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public bool IsReadyToAcceptQuest(ElementId questId)
|
||||
{
|
||||
_questRegistry.TryGetQuest(questId, out var quest);
|
||||
|
@ -64,7 +64,7 @@ internal sealed class QuestInfo : IQuestInfo
|
||||
public ushort Level { get; }
|
||||
public uint IssuerDataId { get; }
|
||||
public bool IsRepeatable { get; }
|
||||
public ImmutableList<QuestId> PreviousQuests { get; }
|
||||
public ImmutableList<QuestId> PreviousQuests { get; set; }
|
||||
public QuestJoin PreviousQuestJoin { get; }
|
||||
public ImmutableList<QuestId> QuestLocks { get; }
|
||||
public QuestJoin QuestLockJoin { get; }
|
||||
@ -88,4 +88,9 @@ internal sealed class QuestInfo : IQuestInfo
|
||||
All = 1,
|
||||
AtLeastOne = 2,
|
||||
}
|
||||
|
||||
public void AddPreviousQuest(QuestId questId)
|
||||
{
|
||||
PreviousQuests = [..PreviousQuests, questId];
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
|
||||
private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<AetheryteFunctions>();
|
||||
serviceCollection.AddSingleton<ExcelFunctions>();
|
||||
serviceCollection.AddSingleton<GameFunctions>();
|
||||
serviceCollection.AddSingleton<ChatFunctions>();
|
||||
|
@ -25,7 +25,6 @@ internal sealed class ActiveQuestComponent
|
||||
private readonly GatheringController _gatheringController;
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
private readonly ICommandManager _commandManager;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
private readonly IChatGui _chatGui;
|
||||
@ -37,7 +36,6 @@ internal sealed class ActiveQuestComponent
|
||||
GatheringController gatheringController,
|
||||
QuestFunctions questFunctions,
|
||||
ICommandManager commandManager,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
Configuration configuration,
|
||||
QuestRegistry questRegistry,
|
||||
IChatGui chatGui)
|
||||
@ -48,7 +46,6 @@ internal sealed class ActiveQuestComponent
|
||||
_gatheringController = gatheringController;
|
||||
_questFunctions = questFunctions;
|
||||
_commandManager = commandManager;
|
||||
_pluginInterface = pluginInterface;
|
||||
_configuration = configuration;
|
||||
_questRegistry = questRegistry;
|
||||
_chatGui = chatGui;
|
||||
|
@ -94,15 +94,22 @@ internal sealed class QuestTooltipComponent
|
||||
|
||||
foreach (var q in quest.PreviousQuests)
|
||||
{
|
||||
var qInfo = _questData.GetQuestInfo(q);
|
||||
var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
|
||||
if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
|
||||
iconColor = ImGuiColors.DalamudGrey;
|
||||
if (_questData.TryGetQuestInfo(q, out var qInfo))
|
||||
{
|
||||
var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
|
||||
if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
|
||||
iconColor = ImGuiColors.DalamudGrey;
|
||||
|
||||
_uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
|
||||
_uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
|
||||
|
||||
if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
|
||||
DrawQuestUnlocks(qstInfo, counter + 1);
|
||||
if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
|
||||
DrawQuestUnlocks(qstInfo, counter + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var _ = ImRaii.Disabled();
|
||||
_uiUtils.ChecklistItem($"Unknown Quest ({q})", ImGuiColors.DalamudGrey, FontAwesomeIcon.Question);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user