Add UI to enable/disable quest battles

This commit is contained in:
Liza 2025-02-21 02:17:47 +01:00
parent 3820647827
commit a75286e927
Signed by: liza
GPG Key ID: 2C41B84815CF6445
20 changed files with 588 additions and 47 deletions

2
LLib

@ -1 +1 @@
Subproject commit 746d14681baa91132784ab17f8f49671e86ea211
Subproject commit edab3c7ecc6bd66ac07e3c3938eb9c8a835a1c42

View File

@ -34,7 +34,7 @@
"Z": -509.51404
},
"TerritoryId": 622,
"InteractionType": "Interact",
"InteractionType": "SinglePlayerDuty",
"Fly": true
}
]

View File

@ -35,7 +35,7 @@
"Z": 686.427
},
"TerritoryId": 135,
"InteractionType": "Interact",
"InteractionType": "SinglePlayerDuty",
"AetheryteShortcut": "Lower La Noscea - Moraby Drydocks"
}
]

View File

@ -14,7 +14,7 @@ internal sealed class Configuration : IPluginConfiguration
public int PluginSetupCompleteVersion { get; set; }
public GeneralConfiguration General { get; } = new();
public DutyConfiguration Duties { get; } = new();
public SoloDutyConfiguration SoloDuties { get; } = new();
public SinglePlayerDutyConfiguration SinglePlayerDuties { get; } = new();
public NotificationConfiguration Notifications { get; } = new();
public AdvancedConfiguration Advanced { get; } = new();
public WindowConfig DebugWindowConfig { get; } = new();
@ -42,11 +42,11 @@ internal sealed class Configuration : IPluginConfiguration
public HashSet<uint> BlacklistedDutyCfcIds { get; set; } = [];
}
internal sealed class SoloDutyConfiguration
internal sealed class SinglePlayerDutyConfiguration
{
public bool RunSoloInstancesWithBossMod { get; set; }
public HashSet<uint> WhitelistedSoloDutyCfcIds { get; set; } = [];
public HashSet<uint> BlacklistedSoloDutyCfcIds { get; set; } = [];
public HashSet<uint> WhitelistedSinglePlayerDutyCfcIds { get; set; } = [];
public HashSet<uint> BlacklistedSinglePlayerDutyCfcIds { get; set; } = [];
}
internal sealed class NotificationConfiguration

View File

@ -15,6 +15,7 @@ using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.Windows.ConfigComponents;
using Quest = Questionable.Model.Quest;
namespace Questionable.Controller;
@ -35,6 +36,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly TaskCreator _taskCreator;
private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent;
private readonly ILogger<QuestController> _logger;
private readonly object _progressLock = new();
@ -76,7 +78,8 @@ internal sealed class QuestController : MiniTaskController<QuestController>
TaskCreator taskCreator,
IServiceProvider serviceProvider,
InterruptHandler interruptHandler,
IDataManager dataManager)
IDataManager dataManager,
SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent)
: base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger)
{
_clientState = clientState;
@ -93,6 +96,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_configuration = configuration;
_yesAlreadyIpc = yesAlreadyIpc;
_taskCreator = taskCreator;
_singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent;
_logger = logger;
_condition.ConditionChange += OnConditionChange;
@ -169,6 +173,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
DebugState = null;
_questRegistry.Reload();
_singlePlayerDutyConfigComponent.Reload();
}
}

View File

@ -240,11 +240,11 @@ internal sealed class QuestRegistry
public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest)
=> _quests.TryGetValue(questId, out quest);
public List<QuestInfo> GetKnownClassJobQuests(EClassJob classJob)
public List<QuestInfo> GetKnownClassJobQuests(EClassJob classJob, bool includeRoleQuests = true)
{
List<QuestInfo> allQuests = [.._questData.GetClassJobQuests(classJob)];
List<QuestInfo> allQuests = [.._questData.GetClassJobQuests(classJob, includeRoleQuests)];
if (classJob.AsJob() != classJob)
allQuests.AddRange(_questData.GetClassJobQuests(classJob.AsJob()));
allQuests.AddRange(_questData.GetClassJobQuests(classJob.AsJob(), includeRoleQuests));
return allQuests
.Where(x => IsKnownQuest(x.QuestId))

View File

@ -247,8 +247,8 @@ internal sealed class QuestData
private void AddPreviousQuest(QuestId questToUpdate, QuestId requiredQuestId)
{
QuestInfo quest = (QuestInfo)_quests[questToUpdate];
quest.AddPreviousQuest(new PreviousQuestInfo(requiredQuestId));
if (_quests.TryGetValue(questToUpdate, out IQuestInfo? quest) && quest is QuestInfo questInfo)
questInfo.AddPreviousQuest(new PreviousQuestInfo(requiredQuestId));
}
private void AddGcFollowUpQuests()
@ -300,7 +300,7 @@ internal sealed class QuestData
.ToList();
}
public List<QuestInfo> GetClassJobQuests(EClassJob classJob)
public List<QuestInfo> GetClassJobQuests(EClassJob classJob, bool includeRoleQuests = false)
{
List<uint> chapterIds = classJob switch
{
@ -367,7 +367,20 @@ internal sealed class QuestData
_ => throw new ArgumentOutOfRangeException(nameof(classJob)),
};
chapterIds.AddRange(classJob switch
if (includeRoleQuests)
{
chapterIds.AddRange(GetRoleQuestIds(classJob));
}
return GetQuestsInNewGamePlusChapters(chapterIds);
}
public List<QuestInfo> GetRoleQuests(EClassJob referenceClassJob) =>
GetQuestsInNewGamePlusChapters(GetRoleQuestIds(referenceClassJob).ToList());
private static IEnumerable<uint> GetRoleQuestIds(EClassJob classJob)
{
return classJob switch
{
_ when classJob.IsTank() => TankRoleQuests,
_ when classJob.IsHealer() => HealerRoleQuests,
@ -375,9 +388,7 @@ internal sealed class QuestData
_ when classJob.IsPhysicalRanged() => PhysicalRangedRoleQuests,
_ when classJob.IsCaster() && classJob != EClassJob.BlueMage => CasterRoleQuests,
_ => []
});
return GetQuestsInNewGamePlusChapters(chapterIds);
};
}
private List<QuestInfo> GetQuestsInNewGamePlusChapters(List<uint> chapterIds)

View File

@ -99,6 +99,11 @@ internal sealed class TerritoryData
}
}
public IEnumerable<(ElementId QuestId, byte Index, ContentFinderConditionData Data)> GetAllQuestsWithQuestBattles()
{
return _questsToCfc.Select(x => (x.Key.QuestId, x.Key.Index, _contentFinderConditions[x.Value]));
}
private static string FixName(string name, ClientLanguage language)
{
if (string.IsNullOrEmpty(name) || language != ClientLanguage.English)

View File

@ -84,16 +84,16 @@ internal sealed class BossModIpc
public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault)
{
if (!_configuration.SoloDuties.RunSoloInstancesWithBossMod)
if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod)
return false;
if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyIndex, out var cfcData))
return false;
if (_configuration.SoloDuties.BlacklistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
return false;
if (_configuration.SoloDuties.WhitelistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
return true;
return enabledByDefault;

View File

@ -302,6 +302,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<GeneralConfigComponent>();
serviceCollection.AddSingleton<DutyConfigComponent>();
serviceCollection.AddSingleton<SinglePlayerDutyConfigComponent>();
serviceCollection.AddSingleton<NotificationConfigComponent>();
serviceCollection.AddSingleton<DebugConfigComponent>();
}
@ -317,6 +318,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<IQuestValidator, AethernetShortcutValidator>();
serviceCollection.AddSingleton<IQuestValidator, DialogueChoiceValidator>();
serviceCollection.AddSingleton<IQuestValidator, ClassQuestShouldHaveShortcutValidator>();
serviceCollection.AddSingleton<IQuestValidator, SinglePlayerInstanceValidator>();
serviceCollection.AddSingleton<IQuestValidator, UniqueSinglePlayerInstanceValidator>();
serviceCollection.AddSingleton<JsonSchemaValidator>();
serviceCollection.AddSingleton<IQuestValidator>(sp => sp.GetRequiredService<JsonSchemaValidator>());
@ -326,6 +328,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
{
serviceProvider.GetRequiredService<QuestRegistry>().Reload();
serviceProvider.GetRequiredService<GatheringPointRegistry>().Reload();
serviceProvider.GetRequiredService<SinglePlayerDutyConfigComponent>().Reload();
serviceProvider.GetRequiredService<CommandHandler>();
serviceProvider.GetRequiredService<ContextMenuController>();
serviceProvider.GetRequiredService<CraftworksSupplyController>();

View File

@ -19,4 +19,5 @@ public enum EIssueType
InvalidExcelRef,
ClassQuestWithoutAetheryteShortcut,
DuplicateSinglePlayerInstance,
UnusedSinglePlayerInstance,
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class SinglePlayerInstanceValidator : IQuestValidator
{
private readonly Dictionary<ElementId, List<byte>> _questIdToDutyIndexes;
public SinglePlayerInstanceValidator(TerritoryData territoryData)
{
_questIdToDutyIndexes = territoryData.GetAllQuestsWithQuestBattles()
.GroupBy(x => x.QuestId)
.ToDictionary(x => x.Key, x => x.Select(y => y.Index).ToList());
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (_questIdToDutyIndexes.TryGetValue(quest.Id, out var indexes))
{
foreach (var index in indexes)
{
if (quest.AllSteps().Any(x =>
x.Step.InteractionType == EInteractionType.SinglePlayerDuty &&
x.Step.SinglePlayerDutyIndex == index))
continue;
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.UnusedSinglePlayerInstance,
Severity = EIssueSeverity.Error,
Description = $"Single player instance {index} not used",
};
}
}
}
}

View File

@ -39,12 +39,12 @@ internal abstract class ConfigComponent
protected void Save() => _pluginInterface.SavePluginConfig(Configuration);
protected static string FormatLevel(int level)
protected static string FormatLevel(int level, bool includePrefix = true)
{
if (level == 0)
return string.Empty;
return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}";
return $"{(includePrefix ? SeIconChar.LevelEn.ToIconString() : string.Empty)}{FormatLevel(level / 10, false)}{(SeIconChar.Number0 + level % 10).ToIconChar()}";
}
/// <summary>

View File

@ -14,7 +14,7 @@ internal sealed class DebugConfigComponent : ConfigComponent
public override void DrawTab()
{
using var tab = ImRaii.TabItem("Advanced");
using var tab = ImRaii.TabItem("Advanced###Debug");
if (!tab)
return;

View File

@ -60,14 +60,13 @@ internal sealed class DutyConfigComponent : ConfigComponent
.GroupBy(x => x.Expansion)
.ToDictionary(x => x.Key,
x => x
.Select(y => new DutyInfo(y.CfcId, y.TerritoryId,
$"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}"))
.Select(y => new DutyInfo(y.CfcId, y.TerritoryId, $"{FormatLevel(y.Level)} {y.Name}"))
.ToList());
}
public override void DrawTab()
{
using var tab = ImRaii.TabItem("Duties");
using var tab = ImRaii.TabItem("Duties###Duties");
if (!tab)
return;
@ -96,37 +95,25 @@ internal sealed class DutyConfigComponent : ConfigComponent
"https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0");
ImGui.Separator();
ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:");
ImGui.Text("You can override the settings for each individual dungeon/trial:");
DrawConfigTable(runInstancedContentWithAutoDuty);
DrawClipboardButtons();
ImGui.SameLine();
using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
{
if (ImGui.Button("Reset to default"))
{
Configuration.Duties.WhitelistedDutyCfcIds.Clear();
Configuration.Duties.BlacklistedDutyCfcIds.Clear();
Save();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("Hold CTRL to enable this button.");
DrawResetButton();
}
}
private void DrawConfigTable(bool runInstancedContentWithAutoDuty)
{
using var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true);
using var child = ImRaii.Child("DutyConfiguration", new Vector2(650, 400), true);
if (!child)
return;
foreach (EExpansionVersion expansion in Enum.GetValues<EExpansionVersion>())
{
if (ImGui.CollapsingHeader(expansion.ToString()))
if (ImGui.CollapsingHeader(expansion.ToFriendlyString()))
{
using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
@ -245,5 +232,21 @@ internal sealed class DutyConfigComponent : ConfigComponent
}
}
private void DrawResetButton()
{
using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
{
if (ImGui.Button("Reset to default"))
{
Configuration.Duties.WhitelistedDutyCfcIds.Clear();
Configuration.Duties.BlacklistedDutyCfcIds.Clear();
Save();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("Hold CTRL to enable this button.");
}
private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name);
}

View File

@ -45,7 +45,7 @@ internal sealed class GeneralConfigComponent : ConfigComponent
public override void DrawTab()
{
using var tab = ImRaii.TabItem("General");
using var tab = ImRaii.TabItem("General###General");
if (!tab)
return;

View File

@ -25,7 +25,7 @@ internal sealed class NotificationConfigComponent : ConfigComponent
public override void DrawTab()
{
using var tab = ImRaii.TabItem("Notifications");
using var tab = ImRaii.TabItem("Notifications###Notifications");
if (!tab)
return;

View File

@ -0,0 +1,464 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
using LLib.GameData;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Controller.Steps.Interactions;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Windows.ConfigComponents;
internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
{
private readonly TerritoryData _territoryData;
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly IDataManager _dataManager;
private readonly ILogger<SinglePlayerDutyConfigComponent> _logger;
private static readonly List<(EClassJob ClassJob, string Name)> RoleQuestCategories =
[
(EClassJob.Paladin, "Tank Role Quests"),
(EClassJob.WhiteMage, "Healer Role Quests"),
(EClassJob.Lancer, "Melee Role Quests"),
(EClassJob.Bard, "Physical Ranged Role Quests"),
(EClassJob.BlackMage, "Magical Ranged Role Quests"),
];
private ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> _startingCityBattles = ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>> _mainScenarioBattles = ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _jobQuestBattles = ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _roleQuestBattles = ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableList<SinglePlayerDutyInfo> _otherRoleQuestBattles = ImmutableList<SinglePlayerDutyInfo>.Empty;
private ImmutableList<(string Label, List<SinglePlayerDutyInfo>)> _otherQuestBattles = ImmutableList<(string Label, List<SinglePlayerDutyInfo>)>.Empty;
public SinglePlayerDutyConfigComponent(
IDalamudPluginInterface pluginInterface,
Configuration configuration,
TerritoryData territoryData,
QuestRegistry questRegistry,
QuestData questData,
IDataManager dataManager,
ILogger<SinglePlayerDutyConfigComponent> logger)
: base(pluginInterface, configuration)
{
_territoryData = territoryData;
_questRegistry = questRegistry;
_questData = questData;
_dataManager = dataManager;
_logger = logger;
}
public void Reload()
{
List<ElementId> questsWithMultipleBattles = _territoryData.GetAllQuestsWithQuestBattles()
.GroupBy(x => x.QuestId)
.Where(x => x.Count() > 1)
.Select(x => x.Key)
.ToList();
List<SinglePlayerDutyInfo> mainScenarioBattles = [];
Dictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> startingCityBattles =
new()
{
{ EAetheryteLocation.Limsa, [] },
{ EAetheryteLocation.Gridania, [] },
{ EAetheryteLocation.Uldah, [] },
};
List<SinglePlayerDutyInfo> otherBattles = [];
Dictionary<ElementId, EClassJob> questIdsToJob = Enum.GetValues<EClassJob>()
.Where(x => x != EClassJob.Adventurer && !x.IsCrafter() && !x.IsGatherer())
.Where(x => x.IsClass() || !x.HasBaseClass())
.SelectMany(x => _questRegistry.GetKnownClassJobQuests(x, false).Select(y => (y.QuestId, ClassJob: x)))
.ToDictionary(x => x.QuestId, x => x.ClassJob);
Dictionary<EClassJob, List<SinglePlayerDutyInfo>> jobQuestBattles = questIdsToJob.Values.Distinct()
.ToDictionary(x => x, _ => new List<SinglePlayerDutyInfo>());
Dictionary<ElementId, List<EClassJob>> questIdToRole = RoleQuestCategories
.SelectMany(x => _questData.GetRoleQuests(x.ClassJob).Select(y => (y.QuestId, x.ClassJob)))
.GroupBy(x => x.QuestId)
.ToDictionary(x => x.Key, x => x.Select(y => y.ClassJob).ToList());
Dictionary<EClassJob, List<SinglePlayerDutyInfo>> roleQuestBattles = RoleQuestCategories
.ToDictionary(x => x.ClassJob, _ => new List<SinglePlayerDutyInfo>());
List<SinglePlayerDutyInfo> otherRoleQuestBattles = [];
foreach (var (questId, index, cfcData) in _territoryData.GetAllQuestsWithQuestBattles())
{
IQuestInfo questInfo = _questData.GetQuestInfo(questId);
QuestStep questStep = new QuestStep
{
SinglePlayerDutyIndex = 0,
BossModEnabled = false,
};
bool enabled;
if (_questRegistry.TryGetQuest(questId, out var quest))
{
if (quest.Root.Disabled)
{
_logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId);
enabled = false;
}
else
{
var foundStep = quest.AllSteps().FirstOrDefault(x =>
x.Step.InteractionType == EInteractionType.SinglePlayerDuty &&
x.Step.SinglePlayerDutyIndex == index);
if (foundStep == default)
{
_logger.LogWarning("Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, index);
enabled = false;
}
else
{
questStep = foundStep.Step;
enabled = true;
}
}
}
else
{
_logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId);
enabled = false;
}
string name = $"{FormatLevel(questInfo.Level)} {questInfo.Name}";
if (!string.IsNullOrEmpty(cfcData.Name) && !questInfo.Name.EndsWith(cfcData.Name, StringComparison.Ordinal))
name += $" ({cfcData.Name})";
if (questsWithMultipleBattles.Contains(questId))
name += $" (Part {questStep.SinglePlayerDutyIndex + 1})";
else if (cfcData.ContentFinderConditionId is 674 or 691)
name += " (Melee/Phys. Ranged)";
var dutyInfo = new SinglePlayerDutyInfo(
cfcData.ContentFinderConditionId,
cfcData.TerritoryId,
name,
questInfo.Expansion,
questInfo.JournalGenre ?? uint.MaxValue,
questInfo.SortKey,
questStep.SinglePlayerDutyIndex,
enabled,
questStep.BossModEnabled);
if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334)
startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo);
else if (cfcData.ContentFinderConditionId is 296 or 297 or 299 or 298)
startingCityBattles[EAetheryteLocation.Gridania].Add(dutyInfo);
else if (cfcData.ContentFinderConditionId is 335 or 312 or 337 or 336)
startingCityBattles[EAetheryteLocation.Uldah].Add(dutyInfo);
else if (questInfo.IsMainScenarioQuest)
mainScenarioBattles.Add(dutyInfo);
else if (questIdsToJob.TryGetValue(questId, out EClassJob classJob))
jobQuestBattles[classJob].Add(dutyInfo);
else if (questIdToRole.TryGetValue(questId, out var classJobs))
{
foreach (var roleClassJob in classJobs)
roleQuestBattles[roleClassJob].Add(dutyInfo);
}
else if (dutyInfo.CfcId is 845 or 1016)
otherRoleQuestBattles.Add(dutyInfo);
else
otherBattles.Add(dutyInfo);
}
_startingCityBattles = startingCityBattles
.ToImmutableDictionary(x => x.Key,
x => x.Value.OrderBy(y => y.SortKey)
.ToList());
_mainScenarioBattles = mainScenarioBattles
.GroupBy(x => x.Expansion)
.ToImmutableDictionary(x => x.Key,
x =>
x.OrderBy(y => y.JournalGenreId)
.ThenBy(y => y.SortKey)
.ThenBy(y => y.Index)
.ToList());
_jobQuestBattles = jobQuestBattles
.Where(x => x.Value.Count > 0)
.ToImmutableDictionary(x => x.Key,
x =>
x.Value
// level 10 quests use the same quest battle for [you started as this class] and [you picked this class up later]
.DistinctBy(y => y.CfcId)
.OrderBy(y => y.JournalGenreId)
.ThenBy(y => y.SortKey)
.ThenBy(y => y.Index)
.ToList());
_roleQuestBattles = roleQuestBattles
.ToImmutableDictionary(x => x.Key,
x =>
x.Value.OrderBy(y => y.JournalGenreId)
.ThenBy(y => y.SortKey)
.ThenBy(y => y.Index)
.ToList());
_otherRoleQuestBattles = otherRoleQuestBattles.ToImmutableList();
_otherQuestBattles = otherBattles
.OrderBy(x => x.JournalGenreId)
.ThenBy(x => x.SortKey)
.ThenBy(x => x.Index)
.GroupBy(x => x.JournalGenreId)
.Select(x => (BuildJournalGenreLabel(x.Key), x.ToList()))
.ToImmutableList();
}
private string BuildJournalGenreLabel(uint journalGenreId)
{
var journalGenre = _dataManager.GetExcelSheet<JournalGenre>().GetRow(journalGenreId);
var journalCategory = journalGenre.JournalCategory.Value;
string genreName = journalGenre.Name.ExtractText();
string categoryName = journalCategory.Name.ExtractText();
return $"{categoryName} {SeIconChar.ArrowRight.ToIconString()} {genreName}";
}
public override void DrawTab()
{
using var tab = ImRaii.TabItem("Quest Battles###QuestBattles");
if (!tab)
return;
bool runSoloInstancesWithBossMod = Configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod;
if (ImGui.Checkbox("Run quest battles with BossMod", ref runSoloInstancesWithBossMod))
{
Configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod = runSoloInstancesWithBossMod;
Save();
}
ImGui.TextColored(ImGuiColors.DalamudRed,
"Work in Progress: For now, this will always use BossMod for combat.");
ImGui.Separator();
using (ImRaii.Disabled(!runSoloInstancesWithBossMod))
{
ImGui.Text(
"Questionable includes a default list of quest battles that work if BossMod is installed.");
ImGui.Text("The included list of quest battles can change with each update.");
ImGui.Separator();
ImGui.Text("You can override the settings for each individual quest battle:");
using var tabBar = ImRaii.TabBar("QuestionableConfigTabs");
if (tabBar)
{
DrawMainScenarioConfigTable();
DrawJobQuestConfigTable();
DrawRoleQuestConfigTable();
DrawOtherQuestConfigTable();
}
DrawResetButton();
}
}
private void DrawMainScenarioConfigTable()
{
using var tab = ImRaii.TabItem("MSQ###MSQ");
if (!tab)
return;
using var child = BeginChildArea();
if (!child)
return;
if (ImGui.CollapsingHeader($"Limsa Lominsa ({FormatLevel(5)} - {FormatLevel(14)})"))
DrawQuestTable("LimsaLominsa", _startingCityBattles[EAetheryteLocation.Limsa]);
if (ImGui.CollapsingHeader($"Gridania ({FormatLevel(5)} - {FormatLevel(14)})"))
DrawQuestTable("Gridania", _startingCityBattles[EAetheryteLocation.Gridania]);
if (ImGui.CollapsingHeader($"Ul'dah ({FormatLevel(4)} - {FormatLevel(14)})"))
DrawQuestTable("Uldah", _startingCityBattles[EAetheryteLocation.Uldah]);
foreach (EExpansionVersion expansion in Enum.GetValues<EExpansionVersion>())
{
if (_mainScenarioBattles.TryGetValue(expansion, out var dutyInfos))
{
if (ImGui.CollapsingHeader(expansion.ToFriendlyString()))
DrawQuestTable($"Duties{expansion}", dutyInfos);
}
}
}
private void DrawJobQuestConfigTable()
{
using var tab = ImRaii.TabItem("Class/Job Quests###JobQuests");
if (!tab)
return;
using var child = BeginChildArea();
if (!child)
return;
foreach (EClassJob classJob in Enum.GetValues<EClassJob>())
{
if (_jobQuestBattles.TryGetValue(classJob, out var dutyInfos))
{
string jobName = classJob.ToFriendlyString();
if (classJob.IsClass())
jobName += $" / {classJob.AsJob().ToFriendlyString()}";
if (ImGui.CollapsingHeader(jobName))
DrawQuestTable($"JobQuests{classJob}", dutyInfos);
}
}
}
private void DrawRoleQuestConfigTable()
{
using var tab = ImRaii.TabItem("Role Quests###RoleQuests");
if (!tab)
return;
using var child = BeginChildArea();
if (!child)
return;
foreach (var (classJob, label) in RoleQuestCategories)
{
if (_roleQuestBattles.TryGetValue(classJob, out var dutyInfos))
{
if (ImGui.CollapsingHeader(label))
DrawQuestTable($"RoleQuests{classJob}", dutyInfos);
}
}
if(ImGui.CollapsingHeader("General Role Quests"))
DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles);
}
private void DrawOtherQuestConfigTable()
{
using var tab = ImRaii.TabItem("Other Quests###MiscQuests");
if (!tab)
return;
using var child = BeginChildArea();
if (!child)
return;
foreach (var (label, dutyInfos) in _otherQuestBattles)
{
if (ImGui.CollapsingHeader(label))
DrawQuestTable($"Other{label}", dutyInfos);
}
}
private void DrawQuestTable(string label, IReadOnlyList<SinglePlayerDutyInfo> dutyInfos)
{
using var table = ImRaii.Table(label, 2, ImGuiTableFlags.SizingFixedFit);
if (table)
{
ImGui.TableSetupColumn("Quest", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f);
foreach (var dutyInfo in dutyInfos)
{
ImGui.TableNextRow();
string[] labels = dutyInfo.BossModEnabledByDefault
? SupportedCfcOptions
: UnsupportedCfcOptions;
int value = 0;
if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(dutyInfo.CfcId))
value = 1;
if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(dutyInfo.CfcId))
value = 2;
if (ImGui.TableNextColumn())
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(dutyInfo.Name);
if (ImGui.IsItemHovered() && Configuration.Advanced.AdditionalStatusInformation)
{
using var tooltip = ImRaii.Tooltip();
if (tooltip)
{
ImGui.TextUnformatted(dutyInfo.Name);
ImGui.Separator();
ImGui.BulletText($"TerritoryId: {dutyInfo.TerritoryId}");
ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.CfcId}");
}
}
if (!dutyInfo.Enabled)
{
ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.",
FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
}
}
if (ImGui.TableNextColumn())
{
using var _ = ImRaii.PushId($"##Duty{dutyInfo.CfcId}");
using (ImRaii.Disabled(!dutyInfo.Enabled))
{
ImGui.SetNextItemWidth(200);
if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
{
Configuration.Duties.WhitelistedDutyCfcIds.Remove(dutyInfo.CfcId);
Configuration.Duties.BlacklistedDutyCfcIds.Remove(dutyInfo.CfcId);
if (value == 1)
Configuration.Duties.WhitelistedDutyCfcIds.Add(dutyInfo.CfcId);
else if (value == 2)
Configuration.Duties.BlacklistedDutyCfcIds.Add(dutyInfo.CfcId);
Save();
}
}
}
}
}
}
private static ImRaii.IEndObject BeginChildArea() => ImRaii.Child("DutyConfiguration", new Vector2(650, 400), true);
private void DrawResetButton()
{
using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
{
if (ImGui.Button("Reset to default"))
{
Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Clear();
Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Clear();
Save();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("Hold CTRL to enable this button.");
}
private sealed record SinglePlayerDutyInfo(
uint CfcId,
uint TerritoryId,
string Name,
EExpansionVersion Expansion,
uint JournalGenreId,
ushort SortKey,
byte Index,
bool Enabled,
bool BossModEnabledByDefault);
}

View File

@ -2,6 +2,7 @@
using Dalamud.Plugin;
using ImGuiNET;
using LLib.ImGui;
using Questionable.Controller.Steps.Interactions;
using Questionable.Windows.ConfigComponents;
namespace Questionable.Windows;
@ -11,6 +12,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
private readonly IDalamudPluginInterface _pluginInterface;
private readonly GeneralConfigComponent _generalConfigComponent;
private readonly DutyConfigComponent _dutyConfigComponent;
private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent;
private readonly NotificationConfigComponent _notificationConfigComponent;
private readonly DebugConfigComponent _debugConfigComponent;
private readonly Configuration _configuration;
@ -19,6 +21,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
IDalamudPluginInterface pluginInterface,
GeneralConfigComponent generalConfigComponent,
DutyConfigComponent dutyConfigComponent,
SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent,
NotificationConfigComponent notificationConfigComponent,
DebugConfigComponent debugConfigComponent,
Configuration configuration)
@ -27,6 +30,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
_pluginInterface = pluginInterface;
_generalConfigComponent = generalConfigComponent;
_dutyConfigComponent = dutyConfigComponent;
_singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent;
_notificationConfigComponent = notificationConfigComponent;
_debugConfigComponent = debugConfigComponent;
_configuration = configuration;
@ -42,6 +46,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
_generalConfigComponent.DrawTab();
_dutyConfigComponent.DrawTab();
_singlePlayerDutyConfigComponent.DrawTab();
_notificationConfigComponent.DrawTab();
_debugConfigComponent.DrawTab();
}

2
vendor/pictomancy vendored

@ -1 +1 @@
Subproject commit d147acc0ea5eed00e25b12508bf5d3fb8eefed53
Subproject commit 70c0e31aabfbc7067c5b57fd02ee0c72ebc7a22e