Add quest battle notes
authorLiza Carvelli <liza@carvel.li>
Fri, 21 Feb 2025 02:22:47 +0000 (03:22 +0100)
committerLiza Carvelli <liza@carvel.li>
Fri, 21 Feb 2025 02:22:47 +0000 (03:22 +0100)
QuestPathGenerator/RoslynElements/QuestStepExtensions.cs
QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json
QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json
QuestPaths/quest-v1.json
Questionable.Model/Questing/QuestStep.cs
Questionable/Controller/GameUi/InteractionUiController.cs
Questionable/External/BossModIpc.cs
Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs

index ca5591bd64bdf3c396d237af131fc4f22eb7010c..7e57e1ae1366fee391e377e121f346120350486a 100644 (file)
@@ -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(),
index 1a788c09f37e775c5d527dbe6ddb8976ccec89db..cd396c519cbab433d7ade17ffdefbc0a36a7ea19 100644 (file)
             "Z": 479.9724
           },
           "TerritoryId": 1053,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "BossModEnabled": false,
+          "BossModNotes": [
+            "Doesn't handle death properly"
+          ]
         }
       ]
     },
index c8a671c3315ba74bf6d0a03b86a5e8d4647ae322..51863a11f26344406887e477d51081edeb60bff9 100644 (file)
           "TerritoryId": 156,
           "InteractionType": "Interact",
           "AetheryteShortcut": "Mor Dhona",
-          "TargetTerritoryId": 351
+          "TargetTerritoryId": 351,
+          "SkipConditions": {
+            "AetheryteShortcutIf": {
+              "InTerritory": [
+                351
+              ]
+            },
+            "StepIf": {
+              "InTerritory": [
+                351
+              ]
+            }
+          }
         },
         {
           "DataId": 1032081,
           "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"
         }
       ]
     },
index 350b0bf132670362aa61d1ef8303331573d5711c..f5df18c8a15ba6ff5bd72471798c8a52711d1719 100644 (file)
               "BossModEnabled": {
                 "type": "boolean"
               },
+              "BossModNotes": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
               "SinglePlayerDutyIndex": {
                 "type": "integer",
                 "minimum": 0,
index 98127cde775eb2eaebf50a9d554550eea4bc93fc..8d9d31ecb5e82df4172053806b0802f63049efae 100644 (file)
@@ -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<string> BossModNotes { get; set; } = [];
     public byte SinglePlayerDutyIndex { get; set; }
     public SkipConditions? SkipConditions { get; set; }
 
index 3164a3bb266f74973b5283c1cc026f4ad520ef7e..0c7e4d0d2ee5972e5ffc592b95a8ee833b04683a 100644 (file)
@@ -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<InteractionUiController> _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<InteractionUiController> 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<Addon>().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;
         }
index 939a35d7ae3135ed188b40c9d02013a7affb0ddf..e73e1863dc21540b238700d75018aa8591020fe4 100644 (file)
@@ -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;
 
index 443ccd87ad9b4be701e6e72a39f5bed5bed5113d..263a3b54d1a981c10e1c65d71830987f5a3e5193 100644 (file)
@@ -40,12 +40,22 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         (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 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;
+
+    private ImmutableList<(string Label, List<SinglePlayerDutyInfo>)> _otherQuestBattles =
+        ImmutableList<(string Label, List<SinglePlayerDutyInfo>)>.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<string> Notes);
 }