From 31eb121cf043d4bb5079fbeab93a99b69d0c383e Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 21 Feb 2025 03:22:47 +0100 Subject: [PATCH] Add quest battle notes --- .../RoslynElements/QuestStepExtensions.cs | 3 + .../4522_The Ultimate Weapon.json | 6 +- ...3682_Vows of Virtue, Deeds of Cruelty.json | 29 +++++--- QuestPaths/quest-v1.json | 6 ++ Questionable.Model/Questing/QuestStep.cs | 1 + .../GameUi/InteractionUiController.cs | 21 +++++- Questionable/External/BossModIpc.cs | 3 + .../SinglePlayerDutyConfigComponent.cs | 71 ++++++++++++++----- 8 files changed, 110 insertions(+), 30 deletions(-) diff --git a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs index ca5591bd6..7e57e1ae1 100644 --- a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs +++ b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs @@ -126,6 +126,9 @@ internal static class QuestStepExtensions Assignment(nameof(QuestStep.BossModEnabled), step.BossModEnabled, emptyStep.BossModEnabled) .AsSyntaxNodeOrToken(), + Assignment(nameof(QuestStep.BossModNotes), + step.BossModNotes, emptyStep.BossModNotes) + .AsSyntaxNodeOrToken(), Assignment(nameof(QuestStep.SinglePlayerDutyIndex), step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex) .AsSyntaxNodeOrToken(), diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json index 1a788c09f..cd396c519 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json @@ -103,7 +103,11 @@ "Z": 479.9724 }, "TerritoryId": 1053, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "BossModEnabled": false, + "BossModNotes": [ + "Doesn't handle death properly" + ] } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json index c8a671c33..51863a11f 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json @@ -61,7 +61,19 @@ "TerritoryId": 156, "InteractionType": "Interact", "AetheryteShortcut": "Mor Dhona", - "TargetTerritoryId": 351 + "TargetTerritoryId": 351, + "SkipConditions": { + "AetheryteShortcutIf": { + "InTerritory": [ + 351 + ] + }, + "StepIf": { + "InTerritory": [ + 351 + ] + } + } }, { "DataId": 1032081, @@ -73,13 +85,14 @@ "TerritoryId": 351, "InteractionType": "SinglePlayerDuty", "Comment": "Estinien vs. Arch Ultima", - "DialogueChoices": [ - { - "Type": "YesNo", - "Prompt": "TEXT_LUCKMG110_03682_Q1_100_125", - "Yes": true - } - ] + "BossModEnabled": false, + "BossModNotes": [ + "AI doesn't move automatically for the first boss", + "AI doesn't move automatically for the dialogue with gaius on the bridge", + "After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)", + "After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking" + ], + "$.1": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one" } ] }, diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 350b0bf13..f5df18c8a 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1270,6 +1270,12 @@ "BossModEnabled": { "type": "boolean" }, + "BossModNotes": { + "type": "array", + "items": { + "type": "string" + } + }, "SinglePlayerDutyIndex": { "type": "integer", "minimum": 0, diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index 98127cde7..8d9d31ecb 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -76,6 +76,7 @@ public sealed class QuestStep public uint? ContentFinderConditionId { get; set; } public bool AutoDutyEnabled { get; set; } public bool BossModEnabled { get; set; } + public List BossModNotes { get; set; } = []; public byte SinglePlayerDutyIndex { get; set; } public SkipConditions? SkipConditions { get; set; } diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index 3164a3bb2..0c7e4d0d2 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -19,6 +19,7 @@ using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Interactions; using Questionable.Data; +using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Gathering; @@ -45,6 +46,7 @@ internal sealed class InteractionUiController : IDisposable private readonly ITargetManager _targetManager; private readonly IClientState _clientState; private readonly ShopController _shopController; + private readonly BossModIpc _bossModIpc; private readonly ILogger _logger; private readonly Regex _returnRegex; private readonly Regex _purchaseItemRegex; @@ -68,6 +70,7 @@ internal sealed class InteractionUiController : IDisposable IPluginLog pluginLog, IClientState clientState, ShopController shopController, + BossModIpc bossModIpc, ILogger logger) { _addonLifecycle = addonLifecycle; @@ -85,6 +88,7 @@ internal sealed class InteractionUiController : IDisposable _targetManager = targetManager; _clientState = clientState; _shopController = shopController; + _bossModIpc = bossModIpc; _logger = logger; _returnRegex = _dataManager.GetExcelSheet().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!; @@ -176,7 +180,10 @@ internal sealed class InteractionUiController : IDisposable int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt); if (answer != null) + { + _logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt); addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value); + } } private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args) @@ -224,6 +231,7 @@ internal sealed class InteractionUiController : IDisposable int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps); if (answer != null) { + _logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt); addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value); return; } @@ -266,6 +274,7 @@ internal sealed class InteractionUiController : IDisposable int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x)); if (questSelection >= 0) { + _logger.LogInformation("Selecting quest {QuestName}", questName); addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection); return true; } @@ -655,13 +664,21 @@ internal sealed class InteractionUiController : IDisposable continue; } + _logger.LogInformation("Returning {YesNo} for '{Prompt}'", dialogueChoice.Yes ? "Yes" : "No", actualPrompt); addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1); return true; } - if (step is { InteractionType: EInteractionType.SinglePlayerDuty, BossModEnabled: true }) + if (step is { InteractionType: EInteractionType.SinglePlayerDuty } && + _bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled)) { - _logger.LogTrace("DefaultYesNo: probably Single Player Duty"); + // Most of these are yes/no dialogs "Duty calls, ...". + // + // For 'Vows of Virtue, Deeds of Cruelty', there's no such dialog, and it just puts you into the instance + // after you confirm 'Wait for Krile?'. However, if you fail that duty, you'll get a DifficultySelectYesNo. + + // DifficultySelectYesNo → [0, 2] for very easy + _logger.LogInformation("DefaultYesNo: probably Single Player Duty"); addonSelectYesno->AtkUnitBase.FireCallbackInt(0); return true; } diff --git a/Questionable/External/BossModIpc.cs b/Questionable/External/BossModIpc.cs index 939a35d7a..e73e1863d 100644 --- a/Questionable/External/BossModIpc.cs +++ b/Questionable/External/BossModIpc.cs @@ -84,6 +84,9 @@ internal sealed class BossModIpc public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault) { + if (!IsSupported()) + return false; + if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod) return false; diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index 443ccd87a..263a3b54d 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -40,12 +40,22 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent (EClassJob.BlackMage, "Magical Ranged Role Quests"), ]; - private ImmutableDictionary> _startingCityBattles = ImmutableDictionary>.Empty; - private ImmutableDictionary> _mainScenarioBattles = ImmutableDictionary>.Empty; - private ImmutableDictionary> _jobQuestBattles = ImmutableDictionary>.Empty; - private ImmutableDictionary> _roleQuestBattles = ImmutableDictionary>.Empty; + private ImmutableDictionary> _startingCityBattles = + ImmutableDictionary>.Empty; + + private ImmutableDictionary> _mainScenarioBattles = + ImmutableDictionary>.Empty; + + private ImmutableDictionary> _jobQuestBattles = + ImmutableDictionary>.Empty; + + private ImmutableDictionary> _roleQuestBattles = + ImmutableDictionary>.Empty; + private ImmutableList _otherRoleQuestBattles = ImmutableList.Empty; - private ImmutableList<(string Label, List)> _otherQuestBattles = ImmutableList<(string Label, List)>.Empty; + + private ImmutableList<(string Label, List)> _otherQuestBattles = + ImmutableList<(string Label, List)>.Empty; public SinglePlayerDutyConfigComponent( IDalamudPluginInterface pluginInterface, @@ -103,10 +113,10 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent { IQuestInfo questInfo = _questData.GetQuestInfo(questId); QuestStep questStep = new QuestStep - { - SinglePlayerDutyIndex = 0, - BossModEnabled = false, - }; + { + SinglePlayerDutyIndex = 0, + BossModEnabled = false, + }; bool enabled; if (_questRegistry.TryGetQuest(questId, out var quest)) { @@ -122,7 +132,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent x.Step.SinglePlayerDutyIndex == index); if (foundStep == default) { - _logger.LogWarning("Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, index); + _logger.LogWarning( + "Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, + index); enabled = false; } else @@ -156,7 +168,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent questInfo.SortKey, questStep.SinglePlayerDutyIndex, enabled, - questStep.BossModEnabled); + questStep.BossModEnabled, + questStep.BossModNotes); if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334) startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo); @@ -343,7 +356,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent } } - if(ImGui.CollapsingHeader("General Role Quests")) + if (ImGui.CollapsingHeader("General Role Quests")) DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles); } @@ -380,9 +393,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ? SupportedCfcOptions : UnsupportedCfcOptions; int value = 0; - if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(dutyInfo.CfcId)) + if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId)) value = 1; - if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(dutyInfo.CfcId)) + if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId)) value = 2; if (ImGui.TableNextColumn()) @@ -407,6 +420,25 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.", FontAwesomeIcon.Times, ImGuiColors.DalamudRed); } + else if (dutyInfo.Notes.Count > 0) + { + using var color = new ImRaii.Color(); + color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + { + using var _ = ImRaii.Tooltip(); + + ImGui.TextColored(ImGuiColors.DalamudYellow, "While testing, the following issues have been found:"); + foreach (string note in dutyInfo.Notes) + ImGui.BulletText(note); + } + } } if (ImGui.TableNextColumn()) @@ -417,13 +449,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent 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); + Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId); if (value == 1) - Configuration.Duties.WhitelistedDutyCfcIds.Add(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId); else if (value == 2) - Configuration.Duties.BlacklistedDutyCfcIds.Add(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId); Save(); } @@ -460,5 +492,6 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ushort SortKey, byte Index, bool Enabled, - bool BossModEnabledByDefault); + bool BossModEnabledByDefault, + List Notes); }