From: Liza Carvelli Date: Mon, 10 Mar 2025 18:28:51 +0000 (+0100) Subject: Add extra options to SwitchClassJob X-Git-Tag: v4.25~2 X-Git-Url: https://git.jacobcasper.com/?a=commitdiff_plain;h=fce5a0e03698c9211178f1f028a46835e146e7d3;p=Questionable.git Add extra options to SwitchClassJob --- diff --git a/Questionable.Model/Gathering/GatheringRoot.cs b/Questionable.Model/Gathering/GatheringRoot.cs index 77383d6d..e2cdf803 100644 --- a/Questionable.Model/Gathering/GatheringRoot.cs +++ b/Questionable.Model/Gathering/GatheringRoot.cs @@ -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; diff --git a/Questionable.Model/Questing/Converter/ExtendedClassJobConverter.cs b/Questionable.Model/Questing/Converter/ExtendedClassJobConverter.cs index 2eaffe5e..9c6e6eef 100644 --- a/Questionable.Model/Questing/Converter/ExtendedClassJobConverter.cs +++ b/Questionable.Model/Questing/Converter/ExtendedClassJobConverter.cs @@ -54,5 +54,7 @@ internal sealed class ExtendedClassJobConverter() : EnumConverter 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 = diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index f0f2f9c3..68c2e4d5 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -56,7 +56,8 @@ internal static class SkipCondition QuestFunctions questFunctions, IClientState clientState, ICondition condition, - ExtraConditionUtils extraConditionUtils) : TaskExecutor + ExtraConditionUtils extraConditionUtils, + ClassJobUtils classJobUtils) : TaskExecutor { 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 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 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)); diff --git a/Questionable/Controller/Steps/Shared/SwitchClassJob.cs b/Questionable/Controller/Steps/Shared/SwitchClassJob.cs index 18bfef7e..518c4367 100644 --- a/Questionable/Controller/Steps/Shared/SwitchClassJob.cs +++ b/Questionable/Controller/Steps/Shared/SwitchClassJob.cs @@ -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); } } diff --git a/Questionable/Data/ClassJobUtils.cs b/Questionable/Data/ClassJobUtils.cs index 8346d9ec..e0f5f734 100644 --- a/Questionable/Data/ClassJobUtils.cs +++ b/Questionable/Data/ClassJobUtils.cs @@ -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 AsIndividualJobs(EExtendedClassJob classJob) + private readonly Configuration _configuration; + private readonly ReadOnlyDictionary _classJobToExpArrayIndex; + + public ClassJobUtils( + Configuration configuration, + IDataManager dataManager) + { + _configuration = configuration; + + _classJobToExpArrayIndex = dataManager.GetExcelSheet() + .Where(x => x is { RowId: > 0, ExpArrayIndex: >= 0 }) + .ToDictionary(x => (EClassJob)x.RowId, x => x.ExpArrayIndex) + .AsReadOnly(); + + SortedClassJobs = dataManager.GetExcelSheet() + .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 AsIndividualJobs(EExtendedClassJob classJob, ElementId? referenceQuest) { return classJob switch { @@ -59,8 +90,97 @@ public static class ClassJobUtils EExtendedClassJob.DoM => Enum.GetValues().Where(x => x.DealsMagicDamage()), EExtendedClassJob.DoH => Enum.GetValues().Where(x => x.IsCrafter()), EExtendedClassJob.DoL => Enum.GetValues().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 jobsWithGearSet = combatJobGearSets + .Select(x => x.ClassJob) + .Distinct() + .ToHashSet(); + + if (configuredJob != EClassJob.Adventurer) + { + if (jobsWithGearSet.Contains(configuredJob)) + return configuredJob; + + EClassJob baseClass = Enum.GetValues() + .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; + } } diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index e3cc60f1..1993c414 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -38,7 +38,7 @@ internal sealed class QuestData private readonly Dictionary _quests; - public QuestData(IDataManager dataManager) + public QuestData(IDataManager dataManager, ClassJobUtils classJobUtils) { JournalGenreOverrides journalGenreOverrides = new() { @@ -89,11 +89,11 @@ internal sealed class QuestData .Cast() .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)]; } })); diff --git a/Questionable/Model/AlliedSocietyDailyInfo.cs b/Questionable/Model/AlliedSocietyDailyInfo.cs index 0f203609..38b0792b 100644 --- a/Questionable/Model/AlliedSocietyDailyInfo.cs +++ b/Questionable/Model/AlliedSocietyDailyInfo.cs @@ -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)) diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index e53bafa1..a2516ac5 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -248,6 +248,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } private static void AddControllers(ServiceCollection serviceCollection) diff --git a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs index d396ad9a..dedce924 100644 --- a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs @@ -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() + .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)) { diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index 637ca7aa..27acb836 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -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 _logger; - private readonly List<(EClassJob ClassJob, int Category)> _sortedClassJobs; private ImmutableDictionary> _startingCityBattles = ImmutableDictionary>.Empty; @@ -69,6 +69,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent QuestRegistry questRegistry, QuestData questData, IDataManager dataManager, + ClassJobUtils classJobUtils, ILogger 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() - .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)