using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
using LLib.GameData;
using Lumina.Excel.GeneratedSheets;
using Questionable.Model;
public static readonly IReadOnlyList<ushort> MeleeRoleQuests = [138, 156, 180];
public static readonly IReadOnlyList<ushort> PhysicalRangedRoleQuests = [138, 157, 181];
public static readonly IReadOnlyList<ushort> CasterRoleQuests = [139, 158, 182];
+
public static readonly IReadOnlyList<IReadOnlyList<ushort>> AllRoleQuestChapters =
[
TankRoleQuests,
_quests = quests.ToDictionary(x => x.QuestId, x => x);
// workaround because the game doesn't require completion of the CT questline through normal means
- QuestInfo aTimeToEveryPurpose = (QuestInfo) _quests[new QuestId(425)];
+ QuestInfo aTimeToEveryPurpose = (QuestInfo)_quests[new QuestId(425)];
aTimeToEveryPurpose.AddPreviousQuest(new QuestId(495));
}
.Where(x => chapterIds.Contains(x.NewGamePlusChapter))
.ToList();
}
+
+ public List<QuestId> GetLockedClassQuests()
+ {
+ EClassJob startingClass;
+ unsafe
+ {
+ var playerState = PlayerState.Instance();
+ if (playerState != null)
+ startingClass = (EClassJob)playerState->FirstClass;
+ else
+ startingClass = EClassJob.Adventurer;
+ }
+
+ if (startingClass == EClassJob.Adventurer)
+ return [];
+
+ return
+ [
+ startingClass == EClassJob.Gladiator ? new(177) : new(253),
+ startingClass == EClassJob.Pugilist ? new(178) : new(533),
+ startingClass == EClassJob.Marauder ? new(179) : new(311),
+ startingClass == EClassJob.Lancer ? new(180) : new(23),
+ startingClass == EClassJob.Archer ? new(181) : new(21),
+ startingClass == EClassJob.Conjurer ? new(182) : new(22),
+ startingClass == EClassJob.Thaumaturge ? new(183) : new(345),
+ startingClass == EClassJob.Arcanist ? new(451) : new(453),
+ ];
+ }
}
using System;
+using System.Linq;
+using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Model.Common;
internal sealed unsafe class AetheryteFunctions
{
+ private const uint TeleportAction = 5;
+ private const uint ReturnAction = 8;
+
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<AetheryteFunctions> _logger;
+ private readonly IDataManager _dataManager;
- public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> logger)
+ public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> logger,
+ IDataManager dataManager)
{
_serviceProvider = serviceProvider;
_logger = logger;
+ _dataManager = dataManager;
}
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
public bool CanTeleport(EAetheryteLocation aetheryteLocation)
{
if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId &&
- ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
+ ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, ReturnAction) == 0)
return true;
- return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
+ return ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, TeleportAction) == 0;
+ }
+
+ public bool IsTeleportUnlocked()
+ {
+ ushort unlockLink = _dataManager.GetExcelSheet<GeneralAction>()!
+ .Single(x => x.Action.Row == 5)
+ .UnlockLink;
+ return UIState.Instance()->IsUnlockLinkUnlocked(unlockLink);
}
public bool TeleportAetheryte(uint aetheryteId)
if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
{
if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
- ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
+ ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, ReturnAction) == 0)
{
ReturnRequestedAt = DateTime.Now;
- if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
+ if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, ReturnAction))
{
_logger.LogInformation("Using 'return' for home aetheryte");
return true;
}
}
- if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
+ if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, TeleportAction) == 0)
{
// fallback if return isn't available or (more likely) on a different aetheryte
_logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
public ElementId? GetNextPriorityQuestThatCanBeAccepted()
{
+ // all priority quests assume we're able to teleport to the beginning (and for e.g. class quests, the end)
+ // ideally without having to wait 15m for Return.
+ if (!_aetheryteFunctions.IsTeleportUnlocked())
+ return null;
+
+ // ideally, we'd also be able to afford *some* teleports
+ // this implicitly makes sure we're not starting one of the lv1 class quests if we can't afford to teleport back
+ //
+ // Of course, they can still be accepted manually.
+ InventoryManager* inventoryManager = InventoryManager.Instance();
+ if (inventoryManager->GetItemCountInContainer(1, InventoryType.Currency) < 2000)
+ return null;
+
return GetPriorityQuestsThatCanBeAccepted()
- .FirstOrDefault(x =>
+ .Where(x =>
{
if (!_questRegistry.TryGetQuest(x, out Quest? quest))
return false;
if (firstStep == null)
return false;
- if (firstStep.AetheryteShortcut is { } aetheryteShortcut &&
- _aetheryteFunctions.IsAetheryteUnlocked(aetheryteShortcut))
+ if (firstStep.AetheryteShortcut != null)
return true;
if (firstStep is
return true;
return false;
+ })
+ .FirstOrDefault(x =>
+ {
+ if (!_questRegistry.TryGetQuest(x, out Quest? quest))
+ return false;
+
+ return quest.AllSteps().All(x =>
+ {
+ if (x.Step.AetheryteShortcut is { } aetheryteShortcut &&
+ _aetheryteFunctions.IsAetheryteUnlocked(aetheryteShortcut))
+ return false;
+
+ if (x.Step.AethernetShortcut is { } aethernetShortcut &&
+ (!_aetheryteFunctions.IsAetheryteUnlocked(aethernetShortcut.From) ||
+ !_aetheryteFunctions.IsAetheryteUnlocked(aethernetShortcut.To)))
+ return false;
+
+ return true;
+ });
});
}
if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
return true;
+ if (_questData.GetLockedClassQuests().Contains(questId))
+ return true;
+
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
}
-using System;
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
-using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using ImGuiNET;
using Questionable.Controller;
using Questionable.Data;
-using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Windows;