using System.Collections.Generic;
using System.Text.Json.Serialization;
-using Questionable.Model.Common;
using Questionable.Model.Common.Converter;
using Questionable.Model.Questing;
-using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Gathering;
{ EExtendedClassJob.DoM, "DoM" },
{ EExtendedClassJob.DoH, "DoH" },
{ EExtendedClassJob.DoL, "DoL" },
+ { EExtendedClassJob.ConfiguredCombatJob, "ConfiguredCombatJob" },
+ { EExtendedClassJob.QuestStartJob, "QuestStartJob" },
};
}
DoM,
DoH,
DoL,
+ ConfiguredCombatJob,
+ QuestStartJob,
}
"DoW",
"DoM",
"DoH",
- "DoL"
+ "DoL",
+ "ConfiguredCombatJob",
+ "QuestStartJob"
]
}
using Dalamud.Configuration;
using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using LLib.GameData;
using LLib.ImGui;
namespace Questionable;
public ECombatModule CombatModule { get; set; } = ECombatModule.None;
public uint MountId { get; set; } = 71;
public GrandCompany GrandCompany { get; set; } = GrandCompany.None;
+ public EClassJob CombatJob { get; set; } = EClassJob.Adventurer;
public bool HideInAllInstances { get; set; } = true;
public bool UseEscToCancelQuesting { get; set; } = true;
public bool ShowIncompleteSeasonalEvents { get; set; } = true;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.GameData;
IGameGui gameGui,
IChatGui chatGui,
IClientState clientState,
+ ClassJobUtils classJobUtils,
ILogger<ContextMenuController> logger)
{
_contextMenu = contextMenu;
if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
{
- AddContextMenuEntry(args, itemId, npcId, EExtendedClassJob.Miner, "Mine");
- AddContextMenuEntry(args, itemId, npcId, EExtendedClassJob.Botanist, "Harvest");
+ AddContextMenuEntry(args, itemId, npcId, EClassJob.Miner, "Mine");
+ AddContextMenuEntry(args, itemId, npcId, EClassJob.Botanist, "Harvest");
}
else
_logger.LogDebug("No custom delivery NPC found for item {ItemId}.", itemId);
return 0;
}
- private void AddContextMenuEntry(IMenuOpenedArgs args, uint itemId, uint npcId, EExtendedClassJob extendedClassJob,
+ private void AddContextMenuEntry(IMenuOpenedArgs args, uint itemId, uint npcId, EClassJob classJob,
string verb)
{
EClassJob currentClassJob = (EClassJob)_clientState.LocalPlayer!.ClassJob.RowId;
- EClassJob classJob = ClassJobUtils.AsIndividualJobs(extendedClassJob).Single();
if (classJob != currentClassJob && currentClassJob is EClassJob.Miner or EClassJob.Botanist)
return;
Prefix = SeIconChar.Hyadelyn,
PrefixColor = 52,
Name = name,
- OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability, extendedClassJob),
+ OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability, classJob),
IsEnabled = string.IsNullOrEmpty(lockedReasonn),
});
}
private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability,
- EExtendedClassJob extendedClassJob)
+ EClassJob classJob)
{
var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId)
.Single(x => x is SatisfactionSupplyInfo);
var sequence = quest.FindSequence(0)!;
var switchClassStep = sequence.Steps.Single(x => x.InteractionType == EInteractionType.SwitchClass);
- switchClassStep.TargetClass = extendedClassJob;
+ switchClassStep.TargetClass = classJob switch
+ {
+ EClassJob.Miner => EExtendedClassJob.Miner,
+ EClassJob.Botanist => EExtendedClassJob.Botanist,
+ _ => throw new ArgumentOutOfRangeException(nameof(classJob), classJob, null),
+ };
var gatherStep = sequence.Steps.Single(x => x.InteractionType == EInteractionType.Gather);
gatherStep.ItemsToGather =
QuestFunctions questFunctions,
IClientState clientState,
ICondition condition,
- ExtraConditionUtils extraConditionUtils) : TaskExecutor<SkipTask>
+ ExtraConditionUtils extraConditionUtils,
+ ClassJobUtils classJobUtils) : TaskExecutor<SkipTask>
{
protected override bool Start()
{
if (CheckQuestWorkConditions(elementId, step))
return true;
- if (CheckJobCondition(step))
+ if (CheckJobCondition(elementId, step))
return true;
if (CheckPositionCondition(skipConditions))
if (step is { RequiredQuestAcceptedJob.Count: > 0 })
{
List<EClassJob> expectedJobs = step.RequiredQuestAcceptedJob
- .SelectMany(ClassJobUtils.AsIndividualJobs).ToList();
+ .SelectMany(x => classJobUtils.AsIndividualJobs(x, elementId)).ToList();
EClassJob questJob = questWork.ClassJob;
logger.LogInformation("Checking quest job {QuestJob} against {ExpectedJobs}", questJob,
string.Join(",", expectedJobs));
return false;
}
- private bool CheckJobCondition(QuestStep step)
+ private bool CheckJobCondition(ElementId elementId, QuestStep step)
{
if (step is { RequiredCurrentJob.Count: > 0 })
{
List<EClassJob> expectedJobs =
- step.RequiredCurrentJob.SelectMany(ClassJobUtils.AsIndividualJobs).ToList();
+ step.RequiredCurrentJob.SelectMany(x => classJobUtils.AsIndividualJobs(x, elementId)).ToList();
EClassJob currentJob = (EClassJob)clientState.LocalPlayer!.ClassJob.RowId;
logger.LogInformation("Checking current job {CurrentJob} against {ExpectedJobs}", currentJob,
string.Join(",", expectedJobs));
internal static class SwitchClassJob
{
- internal sealed class Factory : SimpleTaskFactory
+ internal sealed class Factory(ClassJobUtils classJobUtils) : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.SwitchClass)
return null;
- EClassJob classJob = ClassJobUtils.AsIndividualJobs(step.TargetClass).Single();
+ EClassJob classJob = classJobUtils.AsIndividualJobs(step.TargetClass, quest.Id).Single();
return new Task(classJob);
}
}
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
+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.Misc;
using LLib.GameData;
+using Lumina.Excel.Sheets;
using Questionable.Model.Questing;
namespace Questionable.Data;
-public static class ClassJobUtils
+internal sealed class ClassJobUtils
{
- public static IEnumerable<EClassJob> AsIndividualJobs(EExtendedClassJob classJob)
+ private readonly Configuration _configuration;
+ private readonly ReadOnlyDictionary<EClassJob, sbyte> _classJobToExpArrayIndex;
+
+ public ClassJobUtils(
+ Configuration configuration,
+ IDataManager dataManager)
+ {
+ _configuration = configuration;
+
+ _classJobToExpArrayIndex = dataManager.GetExcelSheet<ClassJob>()
+ .Where(x => x is { RowId: > 0, ExpArrayIndex: >= 0 })
+ .ToDictionary(x => (EClassJob)x.RowId, x => x.ExpArrayIndex)
+ .AsReadOnly();
+
+ SortedClassJobs = dataManager.GetExcelSheet<ClassJob>()
+ .Select(x => (ClassJob: (EClassJob)x.RowId, Priority: x.UIPriority))
+ .OrderBy(x => x.Priority)
+ .Select(x => (x.ClassJob, x.Priority / 10))
+ .ToList()
+ .AsReadOnly();
+ }
+
+ public readonly ReadOnlyCollection<(EClassJob ClassJob, int Category)> SortedClassJobs;
+
+ public IEnumerable<EClassJob> AsIndividualJobs(EExtendedClassJob classJob, ElementId? referenceQuest)
{
return classJob switch
{
EExtendedClassJob.DoM => Enum.GetValues<EClassJob>().Where(x => x.DealsMagicDamage()),
EExtendedClassJob.DoH => Enum.GetValues<EClassJob>().Where(x => x.IsCrafter()),
EExtendedClassJob.DoL => Enum.GetValues<EClassJob>().Where(x => x.IsGatherer()),
+ EExtendedClassJob.ConfiguredCombatJob => LookupConfiguredCombatJob() is var combatJob &&
+ combatJob != EClassJob.Adventurer
+ ? [combatJob]
+ : [],
+ EExtendedClassJob.QuestStartJob => LookupQuestStartJob(referenceQuest) is var startJob &&
+ startJob != EClassJob.Adventurer
+ ? [startJob]
+ : [],
_ => throw new ArgumentOutOfRangeException(nameof(classJob), classJob, null)
};
}
+
+ private EClassJob LookupConfiguredCombatJob()
+ {
+ var configuredJob = _configuration.General.CombatJob;
+ var combatJobGearSets = GetCombatJobGearSets();
+ HashSet<EClassJob> jobsWithGearSet = combatJobGearSets
+ .Select(x => x.ClassJob)
+ .Distinct()
+ .ToHashSet();
+
+ if (configuredJob != EClassJob.Adventurer)
+ {
+ if (jobsWithGearSet.Contains(configuredJob))
+ return configuredJob;
+
+ EClassJob baseClass = Enum.GetValues<EClassJob>()
+ .SingleOrDefault(x => x.IsClass() && x.AsJob() == configuredJob);
+ if (baseClass != EClassJob.Adventurer && jobsWithGearSet.Contains(baseClass))
+ return baseClass;
+ }
+
+ return combatJobGearSets
+ .OrderByDescending(x => x.Level)
+ .ThenByDescending(x => x.ItemLevel)
+ .ThenByDescending(x => x.ClassJob switch
+ {
+ _ when x.ClassJob.IsCaster() => 50,
+ _ when x.ClassJob.IsPhysicalRanged() => 40,
+ _ when x.ClassJob.IsMelee() => 30,
+ _ when x.ClassJob.IsTank() => 20,
+ _ when x.ClassJob.IsHealer() => 10,
+ _ => 0,
+ })
+ .Select(x => x.ClassJob)
+ .DefaultIfEmpty(EClassJob.Adventurer)
+ .FirstOrDefault();
+ }
+
+ private unsafe ReadOnlyCollection<(EClassJob ClassJob, short Level, short ItemLevel)> GetCombatJobGearSets()
+ {
+ List<(EClassJob, short, short)> jobs = [];
+
+ var playerState = PlayerState.Instance();
+ var gearsetModule = RaptureGearsetModule.Instance();
+ if (playerState == null || gearsetModule == null)
+ return jobs.AsReadOnly();
+
+ for (int i = 0; i < 100; ++i)
+ {
+ var gearset = gearsetModule->GetGearset(i);
+ if (gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists))
+ {
+ EClassJob classJob = (EClassJob)gearset->ClassJob;
+ if (classJob.IsCrafter() || classJob.IsGatherer())
+ continue;
+
+ short level = playerState->ClassJobLevels[_classJobToExpArrayIndex[classJob]];
+ if (level == 0)
+ continue;
+
+ short itemLevel = gearset->ItemLevel;
+ jobs.Add((classJob, level, itemLevel));
+ }
+ }
+
+ return jobs.AsReadOnly();
+ }
+
+ private unsafe EClassJob LookupQuestStartJob(ElementId? elementId)
+ {
+ ArgumentNullException.ThrowIfNull(elementId);
+
+ if (elementId is QuestId questId)
+ {
+ QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value);
+ if (questWork->AcceptClassJob != 0)
+ return (EClassJob)questWork->AcceptClassJob;
+ }
+
+ return EClassJob.Adventurer;
+ }
}
private readonly Dictionary<ElementId, IQuestInfo> _quests;
- public QuestData(IDataManager dataManager)
+ public QuestData(IDataManager dataManager, ClassJobUtils classJobUtils)
{
JournalGenreOverrides journalGenreOverrides = new()
{
.Cast<QuestInfo>()
.Select(y => (byte)y.AlliedSocietyRank).Distinct()
])
- .Select(rank => new AlliedSocietyDailyInfo(x, rank));
+ .Select(rank => new AlliedSocietyDailyInfo(x, rank, classJobUtils));
}
else
{
- return [new AlliedSocietyDailyInfo(x, 0)];
+ return [new AlliedSocietyDailyInfo(x, 0, classJobUtils)];
}
}));
internal sealed class AlliedSocietyDailyInfo : IQuestInfo
{
- public AlliedSocietyDailyInfo(BeastTribe beastTribe, byte rank)
+ public AlliedSocietyDailyInfo(BeastTribe beastTribe, byte rank, ClassJobUtils classJobUtils)
{
QuestId = new AlliedSocietyDailyId((byte)beastTribe.RowId, rank);
Name = beastTribe.Name.ToString();
EAlliedSociety.Arkasodara or
EAlliedSociety.Pelupelu =>
[
- ..ClassJobUtils.AsIndividualJobs(EExtendedClassJob.DoW),
- ..ClassJobUtils.AsIndividualJobs(EExtendedClassJob.DoM)
+ ..classJobUtils.AsIndividualJobs(EExtendedClassJob.DoW, null),
+ ..classJobUtils.AsIndividualJobs(EExtendedClassJob.DoM, null)
],
EAlliedSociety.Ixal or EAlliedSociety.Moogles or EAlliedSociety.Dwarves or EAlliedSociety.Loporrits =>
- ClassJobUtils.AsIndividualJobs(EExtendedClassJob.DoH).ToList(),
+ classJobUtils.AsIndividualJobs(EExtendedClassJob.DoH, null).ToList(),
EAlliedSociety.Qitari or EAlliedSociety.Omicrons =>
- ClassJobUtils.AsIndividualJobs(EExtendedClassJob.DoL).ToList(),
+ classJobUtils.AsIndividualJobs(EExtendedClassJob.DoL, null).ToList(),
EAlliedSociety.Namazu =>
[
- ..ClassJobUtils.AsIndividualJobs(EExtendedClassJob.DoH),
- ..ClassJobUtils.AsIndividualJobs(EExtendedClassJob.DoL)
+ ..classJobUtils.AsIndividualJobs(EExtendedClassJob.DoH, null),
+ ..classJobUtils.AsIndividualJobs(EExtendedClassJob.DoL, null)
],
_ => throw new ArgumentOutOfRangeException(nameof(beastTribe))
serviceCollection.AddSingleton<TaskCreator>();
serviceCollection.AddSingleton<ExtraConditionUtils>();
+ serviceCollection.AddSingleton<ClassJobUtils>();
}
private static void AddControllers(ServiceCollection serviceCollection)
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
+using LLib.GameData;
using Lumina.Excel.Sheets;
using Questionable.Controller;
+using Questionable.Data;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Questionable.Windows.ConfigComponents;
internal sealed class GeneralConfigComponent : ConfigComponent
{
private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")];
+ private static readonly List<(EClassJob ClassJob, string Name)> DefaultClassJobs = [(EClassJob.Adventurer, "Auto (highest level/item level)")];
private readonly CombatController _combatController;
private readonly string[] _grandCompanyNames =
["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];
+ private readonly EClassJob[] _classJobIds;
+ private readonly string[] _classJobNames;
+
public GeneralConfigComponent(
IDalamudPluginInterface pluginInterface,
Configuration configuration,
CombatController combatController,
- IDataManager dataManager)
+ IDataManager dataManager,
+ ClassJobUtils classJobUtils)
: base(pluginInterface, configuration)
{
_combatController = combatController;
.ToList();
_mountIds = DefaultMounts.Select(x => x.Id).Concat(mounts.Select(x => x.MountId)).ToArray();
_mountNames = DefaultMounts.Select(x => x.Name).Concat(mounts.Select(x => x.Name)).ToArray();
+
+ var sortedClassJobs = classJobUtils.SortedClassJobs.Select(x => x.ClassJob).ToList();
+ var classJobs = Enum.GetValues<EClassJob>()
+ .Where(x => x != EClassJob.Adventurer)
+ .Where(x => !x.IsCrafter() && !x.IsGatherer())
+ .Where(x => !x.IsClass())
+ .OrderBy(x => sortedClassJobs.IndexOf(x))
+ .ToList();
+ _classJobIds = DefaultClassJobs.Select(x => x.ClassJob).Concat(classJobs).ToArray();
+ _classJobNames = DefaultClassJobs.Select(x => x.Name).Concat(classJobs.Select(x => x.ToFriendlyString())).ToArray();
}
public override void DrawTab()
Save();
}
+ int combatJob = Array.IndexOf(_classJobIds, Configuration.General.CombatJob);
+ if (combatJob == -1)
+ {
+ Configuration.General.CombatJob = EClassJob.Adventurer;
+ Save();
+
+ combatJob = 0;
+ }
+
+ if (ImGui.Combo("Preferred Combat Job", ref combatJob, _classJobNames, _classJobNames.Length))
+ {
+ Configuration.General.CombatJob = _classJobIds[combatJob];
+ Save();
+ }
+
bool hideInAllInstances = Configuration.General.HideInAllInstances;
if (ImGui.Checkbox("Hide quest window in all instanced duties", ref hideInAllInstances))
{
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly IDataManager _dataManager;
+ private readonly ClassJobUtils _classJobUtils;
private readonly ILogger<SinglePlayerDutyConfigComponent> _logger;
- private readonly List<(EClassJob ClassJob, int Category)> _sortedClassJobs;
private ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> _startingCityBattles =
ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>>.Empty;
QuestRegistry questRegistry,
QuestData questData,
IDataManager dataManager,
+ ClassJobUtils classJobUtils,
ILogger<SinglePlayerDutyConfigComponent> logger)
: base(pluginInterface, configuration)
{
_questRegistry = questRegistry;
_questData = questData;
_dataManager = dataManager;
+ _classJobUtils = classJobUtils;
_logger = logger;
-
- _sortedClassJobs = dataManager.GetExcelSheet<ClassJob>()
- .Where(x => x is { RowId: > 0, UIPriority: < 100 })
- .Select(x => (ClassJob: (EClassJob)x.RowId, Priority: x.UIPriority))
- .OrderBy(x => x.Priority)
- .Select(x => (x.ClassJob, x.Priority / 10))
- .ToList();
}
public void Reload()
return;
int oldPriority = 0;
- foreach (var (classJob, priority) in _sortedClassJobs)
+ foreach (var (classJob, priority) in _classJobUtils.SortedClassJobs)
{
+ if (classJob.IsCrafter() || classJob.IsGatherer())
+ continue;
+
if (_jobQuestBattles.TryGetValue(classJob, out var dutyInfos))
{
if (priority != oldPriority)