Add extra options to SwitchClassJob
authorLiza Carvelli <liza@carvel.li>
Mon, 10 Mar 2025 18:28:51 +0000 (19:28 +0100)
committerLiza Carvelli <liza@carvel.li>
Mon, 10 Mar 2025 18:28:51 +0000 (19:28 +0100)
14 files changed:
Questionable.Model/Gathering/GatheringRoot.cs
Questionable.Model/Questing/Converter/ExtendedClassJobConverter.cs
Questionable.Model/Questing/EExtendedClassJob.cs
Questionable.Model/common-classjob.json
Questionable/Configuration.cs
Questionable/Controller/ContextMenuController.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Controller/Steps/Shared/SwitchClassJob.cs
Questionable/Data/ClassJobUtils.cs
Questionable/Data/QuestData.cs
Questionable/Model/AlliedSocietyDailyInfo.cs
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs
Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs

index 77383d6..e2cdf80 100644 (file)
@@ -1,9 +1,7 @@
 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;
 
index 2eaffe5..9c6e6ee 100644 (file)
@@ -54,5 +54,7 @@ internal sealed class ExtendedClassJobConverter() : EnumConverter<EExtendedClass
         { EExtendedClassJob.DoM, "DoM" },
         { EExtendedClassJob.DoH, "DoH" },
         { EExtendedClassJob.DoL, "DoL" },
+        { EExtendedClassJob.ConfiguredCombatJob, "ConfiguredCombatJob" },
+        { EExtendedClassJob.QuestStartJob, "QuestStartJob" },
     };
 }
index e0f0d3f..e8d8349 100644 (file)
@@ -53,5 +53,7 @@ public enum EExtendedClassJob
     DoM,
     DoH,
     DoL,
+    ConfiguredCombatJob,
+    QuestStartJob,
 }
 
index e5e0d39..7b5c280 100644 (file)
@@ -48,6 +48,8 @@
     "DoW",
     "DoM",
     "DoH",
-    "DoL"
+    "DoL",
+    "ConfiguredCombatJob",
+    "QuestStartJob"
   ]
 }
index 2979cdb..4099fd3 100644 (file)
@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
 using Dalamud.Configuration;
 using Dalamud.Game.Text;
 using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using LLib.GameData;
 using LLib.ImGui;
 
 namespace Questionable;
@@ -30,6 +31,7 @@ internal sealed class Configuration : IPluginConfiguration
         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;
index 68b982b..1e9ed47 100644 (file)
@@ -3,7 +3,6 @@ using System.Linq;
 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;
@@ -44,6 +43,7 @@ internal sealed class ContextMenuController : IDisposable
         IGameGui gameGui,
         IChatGui chatGui,
         IClientState clientState,
+        ClassJobUtils classJobUtils,
         ILogger<ContextMenuController> logger)
     {
         _contextMenu = contextMenu;
@@ -83,8 +83,8 @@ internal sealed class ContextMenuController : IDisposable
 
         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);
@@ -107,11 +107,10 @@ internal sealed class ContextMenuController : IDisposable
         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;
 
@@ -156,13 +155,13 @@ internal sealed class ContextMenuController : IDisposable
             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);
@@ -171,7 +170,12 @@ internal sealed class ContextMenuController : IDisposable
             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 =
index f0f2f9c..68c2e4d 100644 (file)
@@ -56,7 +56,8 @@ internal static class SkipCondition
         QuestFunctions questFunctions,
         IClientState clientState,
         ICondition condition,
-        ExtraConditionUtils extraConditionUtils) : TaskExecutor<SkipTask>
+        ExtraConditionUtils extraConditionUtils,
+        ClassJobUtils classJobUtils) : TaskExecutor<SkipTask>
     {
         protected override bool Start()
         {
@@ -99,7 +100,7 @@ internal static class SkipCondition
             if (CheckQuestWorkConditions(elementId, step))
                 return true;
 
-            if (CheckJobCondition(step))
+            if (CheckJobCondition(elementId, step))
                 return true;
 
             if (CheckPositionCondition(skipConditions))
@@ -341,7 +342,7 @@ internal static class SkipCondition
                 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));
@@ -356,12 +357,12 @@ internal static class SkipCondition
             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));
index 18bfef7..518c436 100644 (file)
@@ -11,14 +11,14 @@ namespace Questionable.Controller.Steps.Shared;
 
 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);
         }
     }
index 8346d9e..e0f5f73 100644 (file)
@@ -1,14 +1,45 @@
 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
         {
@@ -59,8 +90,97 @@ public static class ClassJobUtils
             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;
+    }
 }
index e3cc60f..1993c41 100644 (file)
@@ -38,7 +38,7 @@ internal sealed class QuestData
 
     private readonly Dictionary<ElementId, IQuestInfo> _quests;
 
-    public QuestData(IDataManager dataManager)
+    public QuestData(IDataManager dataManager, ClassJobUtils classJobUtils)
     {
         JournalGenreOverrides journalGenreOverrides = new()
         {
@@ -89,11 +89,11 @@ internal sealed class QuestData
                                     .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)];
                     }
                 }));
 
index 0f20360..38b0792 100644 (file)
@@ -11,7 +11,7 @@ namespace Questionable.Model;
 
 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();
@@ -24,19 +24,19 @@ internal sealed class AlliedSocietyDailyInfo : IQuestInfo
                 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))
index e53bafa..a2516ac 100644 (file)
@@ -248,6 +248,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
 
         serviceCollection.AddSingleton<TaskCreator>();
         serviceCollection.AddSingleton<ExtraConditionUtils>();
+        serviceCollection.AddSingleton<ClassJobUtils>();
     }
 
     private static void AddControllers(ServiceCollection serviceCollection)
index d396ad9..dedce92 100644 (file)
@@ -6,8 +6,10 @@ using Dalamud.Interface.Utility.Raii;
 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;
@@ -15,6 +17,7 @@ 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;
 
@@ -25,11 +28,15 @@ internal sealed class GeneralConfigComponent : ConfigComponent
     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;
@@ -42,6 +49,16 @@ internal sealed class GeneralConfigComponent : ConfigComponent
             .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()
@@ -83,6 +100,21 @@ internal sealed class GeneralConfigComponent : ConfigComponent
             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))
         {
index 637ca7a..27acb83 100644 (file)
@@ -42,8 +42,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
     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;
@@ -69,6 +69,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         QuestRegistry questRegistry,
         QuestData questData,
         IDataManager dataManager,
+        ClassJobUtils classJobUtils,
         ILogger<SinglePlayerDutyConfigComponent> logger)
         : base(pluginInterface, configuration)
     {
@@ -76,14 +77,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         _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()
@@ -354,8 +349,11 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
             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)