Draft for auto-completing quest battles (HW MSQ)
authorLiza Carvelli <liza@carvel.li>
Thu, 20 Feb 2025 00:34:59 +0000 (01:34 +0100)
committerLiza Carvelli <liza@carvel.li>
Thu, 20 Feb 2025 00:34:59 +0000 (01:34 +0100)
24 files changed:
QuestPathGenerator/RoslynElements/QuestStepExtensions.cs
QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json
QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json
QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json
QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json
QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json
QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json
QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json
QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json
QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json
QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json
QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json
QuestPaths/quest-v1.json
Questionable.Model/Questing/QuestStep.cs
Questionable/Controller/CombatModules/BossModModule.cs
Questionable/Controller/GameUi/InteractionUiController.cs
Questionable/Controller/QuestRegistry.cs
Questionable/Controller/Steps/Common/SendNotification.cs
Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs [new file with mode: 0644]
Questionable/Controller/Steps/Shared/WaitAtEnd.cs
Questionable/Controller/Steps/TaskCreator.cs
Questionable/Data/TerritoryData.cs
Questionable/External/BossModIpc.cs [new file with mode: 0644]
Questionable/QuestionablePlugin.cs

index 2d3ec8034483668bf7ffe106a26d71da00bc5051..ae0517d28f4f7646ae4baf64702950abd35acc48 100644 (file)
@@ -123,6 +123,9 @@ internal static class QuestStepExtensions
                             Assignment(nameof(QuestStep.AutoDutyEnabled),
                                     step.AutoDutyEnabled, emptyStep.AutoDutyEnabled)
                                 .AsSyntaxNodeOrToken(),
+                            Assignment(nameof(QuestStep.BossModEnabled),
+                                    step.BossModEnabled, emptyStep.BossModEnabled)
+                                .AsSyntaxNodeOrToken(),
                             Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
                                     emptyStep.SkipConditions)
                                 .AsSyntaxNodeOrToken(),
index fca23e0007bf754109b8f4e7218af6672637a21a..dae6eb11616f52222b4163b0135df52ae1e500d0 100644 (file)
@@ -95,7 +95,9 @@
           },
           "TerritoryId": 138,
           "InteractionType": "SinglePlayerDuty",
-          "Fly": true
+          "Fly": true,
+          "ContentFinderConditionId": 393,
+          "BossModEnabled": true
         }
       ]
     },
index 06d28cf649385f56191521fc41c742ea7912e824..b4107b9986a72f4c13e852311de53f94e33ebeb4 100644 (file)
@@ -58,7 +58,9 @@
             "Z": 349.96558
           },
           "TerritoryId": 401,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "ContentFinderConditionId": 395,
+          "BossModEnabled": true
         }
       ]
     },
index d8baf9d31e5bd7f37b1a1642661ffabb4b292347..238d8e8760859a46afec2e8ada1c2776ded8e956 100644 (file)
@@ -78,7 +78,9 @@
           "AethernetShortcut": [
             "[Ishgard] The Forgotten Knight",
             "[Ishgard] The Tribunal"
-          ]
+          ],
+          "ContentFinderConditionId": 396,
+          "BossModEnabled": true
         }
       ]
     },
index fe2d06748b9d594cb881b0572b934d4ed6c05d9f..a42a2a769f4f2b38b96d01b5fc0aefb9513e44ac 100644 (file)
@@ -28,7 +28,9 @@
             "Z": 388.63196
           },
           "TerritoryId": 145,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "ContentFinderConditionId": 400,
+          "BossModEnabled": true
         }
       ]
     },
index ff6cd6992b781b005803c5692eea30edeca89972..b00c7cc02597803e747125081d6713736008474f 100644 (file)
     {
       "Sequence": 1,
       "Steps": [
+        {
+          "Position": {
+            "X": 474.62885,
+            "Y": 200.2377,
+            "Z": 657.9519
+          },
+          "TerritoryId": 397,
+          "InteractionType": "WalkTo",
+          "AetheryteShortcut": "Coerthas Western Highlands - Falcon's Nest"
+        },
         {
           "Position": {
             "X": 486.38373,
@@ -28,8 +38,7 @@
             "Z": 239.54294
           },
           "TerritoryId": 397,
-          "InteractionType": "WalkTo",
-          "AetheryteShortcut": "Coerthas Western Highlands - Falcon's Nest"
+          "InteractionType": "WalkTo"
         },
         {
           "Position": {
@@ -69,7 +78,9 @@
           },
           "TerritoryId": 397,
           "InteractionType": "SinglePlayerDuty",
-          "DisableNavmesh": true
+          "DisableNavmesh": true,
+          "ContentFinderConditionId": 397,
+          "BossModEnabled": true
         }
       ]
     },
index d38df9943d7df54c57e7edc4904007bba74c06a4..ea46e2441feeeaf4803eee5d95394805bd4e0c47 100644 (file)
           "KillEnemyDataIds": [
             4015
           ],
-          "$": "0 0 0 0 0 0 -> "
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ]
         },
         {
           "Position": {
           "EnemySpawnType": "AutoOnEnterArea",
           "KillEnemyDataIds": [
             4015
+          ],
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            64
           ]
         }
       ]
index 61828e2cf65eb1b4cf44df63b8cc8521ec127831..cdca15510232f9183d56e838b1480c16f31be783 100644 (file)
           "InteractionType": "WalkTo",
           "Mount": true
         },
+        {
+          "Position": {
+            "X": -335.0186,
+            "Y": 13.983504,
+            "Z": -100.87753
+          },
+          "TerritoryId": 140,
+          "InteractionType": "WalkTo",
+          "Fly": true
+        },
         {
           "DataId": 1004019,
           "Position": {
           },
           "TerritoryId": 140,
           "InteractionType": "Interact",
-          "Fly": true,
           "TargetTerritoryId": 140
         },
         {
index ce1f3688a7ada15615b693dd9b52cb7705aaa67b..d3573b34fe53ab8801e36b1ff9bea6e9eb877c8a 100644 (file)
@@ -74,7 +74,9 @@
             "Z": 37.247192
           },
           "TerritoryId": 418,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "ContentFinderConditionId": 398,
+          "BossModEnabled": true
         }
       ]
     },
index c65d68153098d0dee666ac8cd21d81b1e4f5aa5f..0122bc84d5ad77c4665276012650754ccca6f512 100644 (file)
@@ -56,7 +56,9 @@
           "TerritoryId": 401,
           "InteractionType": "SinglePlayerDuty",
           "Emote": "lookout",
-          "StopDistance": 0.25
+          "StopDistance": 0.25,
+          "ContentFinderConditionId": 401,
+          "BossModEnabled": true
         }
       ]
     },
index 9f25ebdab0678a607877b402e33a4e42ccef4214..f3e3acdb50511eb59a71391a2a8986b3e809d9a2 100644 (file)
@@ -47,7 +47,9 @@
           "AethernetShortcut": [
             "[Idyllshire] Aetheryte Plaza",
             "[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
-          ]
+          ],
+          "ContentFinderConditionId": 422,
+          "BossModEnabled": true
         }
       ]
     },
index 907bdd4535fbe1bd736b1893c624fbb813f141c8..792ea96132e9af888b496b2080ba82d927770cf9 100644 (file)
@@ -68,7 +68,9 @@
             "Z": 553.97876
           },
           "TerritoryId": 402,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "ContentFinderConditionId": 399,
+          "BossModEnabled": true
         }
       ]
     },
index 62ad5764433d0c190d28dfb9bbef11a93d89f4d5..d078b4c83605cd6275b0e523bd571feb4c28dec5 100644 (file)
             ]
           }
         },
+        {
+          "if": {
+            "properties": {
+              "InteractionType": {
+                "const": "SinglePlayerDuty"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "ContentFinderConditionId": {
+                "type": "integer",
+                "exclusiveMinimum": 0,
+                "exclusiveMaximum": 3000
+              },
+              "BossModEnabled": {
+                "type": "boolean"
+              }
+            }
+          }
+        },
         {
           "if": {
             "properties": {
index bd1ce3040d4e02d1091ee8c4e2df3fe6aee86203..fe7170d5b293fcf0d7fcc49b05a1cba4464a7681 100644 (file)
@@ -75,6 +75,7 @@ public sealed class QuestStep
     public JumpDestination? JumpDestination { get; set; }
     public uint? ContentFinderConditionId { get; set; }
     public bool AutoDutyEnabled { get; set; }
+    public bool BossModEnabled { get; set; }
     public SkipConditions? SkipConditions { get; set; }
 
     public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
index ee25967f441041b7d342cbe7a3cef105f679192b..a69237f3e701194c36cdc7b52a3c462233a90a25 100644 (file)
@@ -9,33 +9,26 @@ using Questionable.Model;
 using System;
 using System.IO;
 using System.Numerics;
+using Questionable.External;
 
 namespace Questionable.Controller.CombatModules;
 
 internal sealed class BossModModule : ICombatModule, IDisposable
 {
-    private const string Name = "BossMod";
     private readonly ILogger<BossModModule> _logger;
+    private readonly BossModIpc _bossModIpc;
     private readonly Configuration _configuration;
-    private readonly ICallGateSubscriber<string, string?> _getPreset;
-    private readonly ICallGateSubscriber<string, bool, bool> _createPreset;
-    private readonly ICallGateSubscriber<string, bool> _setPreset;
-    private readonly ICallGateSubscriber<bool> _clearPreset;
 
     private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!;
 
     public BossModModule(
         ILogger<BossModModule> logger,
-        IDalamudPluginInterface pluginInterface,
+        BossModIpc bossModIpc,
         Configuration configuration)
     {
         _logger = logger;
+        _bossModIpc = bossModIpc;
         _configuration = configuration;
-
-        _getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{Name}.Presets.Get");
-        _createPreset = pluginInterface.GetIpcSubscriber<string, bool, bool>($"{Name}.Presets.Create");
-        _setPreset = pluginInterface.GetIpcSubscriber<string, bool>($"{Name}.Presets.SetActive");
-        _clearPreset = pluginInterface.GetIpcSubscriber<bool>($"{Name}.Presets.ClearActive");
     }
 
     public bool CanHandleFight(CombatController.CombatData combatData)
@@ -43,26 +36,19 @@ internal sealed class BossModModule : ICombatModule, IDisposable
         if (_configuration.General.CombatModule != Configuration.ECombatModule.BossMod)
             return false;
 
-        try
-        {
-            return _getPreset.HasFunction;
-        }
-        catch (IpcError)
-        {
-            return false;
-        }
+        return _bossModIpc.IsSupported();
     }
 
     public bool Start(CombatController.CombatData combatData)
     {
         try
         {
-            if (_getPreset.InvokeFunc("Questionable") == null)
+            if (_bossModIpc.GetPreset("Questionable") == null)
             {
                 using var reader = new StreamReader(Preset);
-                _logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _createPreset.InvokeFunc(reader.ReadToEnd(), true));
+                _logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _bossModIpc.CreatePreset(reader.ReadToEnd(), true));
             }
-            _setPreset.InvokeFunc("Questionable");
+            _bossModIpc.SetPreset("Questionable");
             return true;
         }
         catch (IpcError e)
@@ -76,7 +62,7 @@ internal sealed class BossModModule : ICombatModule, IDisposable
     {
         try
         {
-            _clearPreset.InvokeFunc();
+            _bossModIpc.ClearPreset();
             return true;
         }
         catch (IpcError e)
index 45333c2b754d9c6b8efa33f89abc75ee530ea8f4..3164a3bb266f74973b5283c1cc026f4ad520ef7e 100644 (file)
@@ -598,14 +598,14 @@ internal sealed class InteractionUiController : IDisposable
         if (checkAllSteps)
         {
             var sequence = quest.FindSequence(currentQuest.Sequence);
-            if (sequence != null && HandleDefaultYesNo(addonSelectYesno, quest,
-                    sequence.Steps.SelectMany(x => x.DialogueChoices).ToList(), actualPrompt))
+            if (sequence != null &&
+                sequence.Steps.Any(step => HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt)))
                 return true;
         }
         else
         {
             var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
-            if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step.DialogueChoices, actualPrompt))
+            if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt))
                 return true;
         }
 
@@ -619,7 +619,7 @@ internal sealed class InteractionUiController : IDisposable
                 Yes = true
             };
 
-            if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
+            if (HandleDefaultYesNo(addonSelectYesno, quest, null, [dialogueChoice], actualPrompt))
                 return true;
         }
 
@@ -630,7 +630,7 @@ internal sealed class InteractionUiController : IDisposable
     }
 
     private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
-        List<DialogueChoice> dialogueChoices, string actualPrompt)
+        QuestStep? step, List<DialogueChoice> dialogueChoices, string actualPrompt)
     {
         _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
         foreach (var dialogueChoice in dialogueChoices)
@@ -659,6 +659,13 @@ internal sealed class InteractionUiController : IDisposable
             return true;
         }
 
+        if (step is { InteractionType: EInteractionType.SinglePlayerDuty, BossModEnabled: true })
+        {
+            _logger.LogTrace("DefaultYesNo: probably Single Player Duty");
+            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+            return true;
+        }
+
         return false;
     }
 
index 7808b09509d40727e2802252997fcbcb0c7eda70..2c38b2410c37552a11b60c2d64562c126f32a985 100644 (file)
@@ -150,7 +150,8 @@ internal sealed class QuestRegistry
         foreach (var quest in _quests.Values)
         {
             foreach (var dutyStep in quest.AllSteps().Where(x =>
-                         x.Step.InteractionType == EInteractionType.Duty && x.Step.ContentFinderConditionId != null))
+                         x.Step.InteractionType is EInteractionType.Duty or EInteractionType.SinglePlayerDuty
+                         && x.Step.ContentFinderConditionId != null))
             {
                 _contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = (quest.Id, dutyStep.Step);
             }
index 6d8bbcec661eb54a1a291cdbf12d42b05dba6b6a..8bb4fa80392f86bdc7a78459dd2ba8453f89e1e6 100644 (file)
@@ -26,7 +26,8 @@ internal static class SendNotification
                     new Task(step.InteractionType, step.ContentFinderConditionId.HasValue
                         ? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name
                         : step.Comment),
-                EInteractionType.SinglePlayerDuty => new Task(step.InteractionType, quest.Info.Name),
+                EInteractionType.SinglePlayerDuty when !step.BossModEnabled =>
+                    new Task(step.InteractionType, quest.Info.Name),
                 _ => null,
             };
         }
diff --git a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs
new file mode 100644 (file)
index 0000000..4cd79ce
--- /dev/null
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using Dalamud.Plugin.Services;
+using Questionable.Controller.Steps.Shared;
+using Questionable.Data;
+using Questionable.External;
+using Questionable.Model;
+using Questionable.Model.Questing;
+
+namespace Questionable.Controller.Steps.Interactions;
+
+internal static class SinglePlayerDuty
+{
+    internal sealed class Factory : ITaskFactory
+    {
+        public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+        {
+            if (step.InteractionType != EInteractionType.SinglePlayerDuty)
+                yield break;
+
+            if (step.BossModEnabled)
+            {
+                ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
+
+                yield return new StartSinglePlayerDuty(step.ContentFinderConditionId.Value);
+                yield return new EnableAi();
+                yield return new WaitSinglePlayerDuty(step.ContentFinderConditionId.Value);
+                yield return new DisableAi();
+                yield return new WaitAtEnd.WaitNextStepOrSequence();
+            }
+        }
+    }
+
+    internal sealed record StartSinglePlayerDuty(uint ContentFinderConditionId) : ITask
+    {
+        public override string ToString() => $"Wait(BossMod, entered instance {ContentFinderConditionId})";
+    }
+
+    internal sealed class StartSinglePlayerDutyExecutor(
+        TerritoryData territoryData,
+        IClientState clientState) : TaskExecutor<StartSinglePlayerDuty>
+    {
+        protected override bool Start() => true;
+
+        public override ETaskResult Update()
+        {
+            if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId,
+                    out var cfcData))
+                throw new TaskException("Failed to get territory ID for content finder condition");
+
+            return clientState.TerritoryType == cfcData.TerritoryId
+                ? ETaskResult.TaskComplete
+                : ETaskResult.StillRunning;
+        }
+
+        public override bool ShouldInterruptOnDamage() => false;
+    }
+
+    internal sealed record EnableAi : ITask
+    {
+        public override string ToString() => "BossMod.EnableAi";
+    }
+
+    internal sealed class EnableAiExecutor(
+        BossModIpc bossModIpc) : TaskExecutor<EnableAi>
+    {
+        protected override bool Start()
+        {
+            bossModIpc.EnableAi();
+            return true;
+        }
+
+        public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+        public override bool ShouldInterruptOnDamage() => false;
+    }
+
+    internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : ITask
+    {
+        public override string ToString() => $"Wait(BossMod, left instance {ContentFinderConditionId})";
+    }
+
+    internal sealed class WaitSinglePlayerDutyExecutor(
+        TerritoryData territoryData,
+        IClientState clientState,
+        BossModIpc bossModIpc) : TaskExecutor<WaitSinglePlayerDuty>, IStoppableTaskExecutor
+    {
+        protected override bool Start() => true;
+
+        public override ETaskResult Update()
+        {
+            if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId,
+                 out var cfcData))
+            throw new TaskException("Failed to get territory ID for content finder condition");
+
+            return clientState.TerritoryType != cfcData.TerritoryId
+                ? ETaskResult.TaskComplete
+                : ETaskResult.StillRunning;
+        }
+
+        public void StopNow() => bossModIpc.DisableAi();
+
+        public override bool ShouldInterruptOnDamage() => false;
+    }
+
+    internal sealed record DisableAi : ITask
+    {
+        public override string ToString() => "BossMod.DisableAi";
+    }
+
+    internal sealed class DisableAiExecutor(
+        BossModIpc bossModIpc) : TaskExecutor<DisableAi>
+    {
+        protected override bool Start()
+        {
+            bossModIpc.DisableAi();
+            return true;
+        }
+
+        public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+        public override bool ShouldInterruptOnDamage() => false;
+    }
+}
index d39c7c2a36f06b8d4094bcc2b4e5b025b9a91744..59d108cdd195cda887f905bc1438f9b46858d8b2 100644 (file)
@@ -53,7 +53,7 @@ internal static class WaitAtEnd
                     return [new WaitNextStepOrSequence()];
 
                 case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled):
-                case EInteractionType.SinglePlayerDuty:
+                case EInteractionType.SinglePlayerDuty when !step.BossModEnabled:
                     return [new EndAutomation()];
 
                 case EInteractionType.WalkTo:
index 915f7d57095448028bf3e6f85b4d55fdcc8f37cf..997d40d1d5df1164cf8f954dc71be9fbed986fd7 100644 (file)
@@ -1,8 +1,11 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Dalamud.Plugin.Services;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.Interactions;
+using Questionable.Data;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
@@ -11,11 +14,19 @@ namespace Questionable.Controller.Steps;
 internal sealed class TaskCreator
 {
     private readonly IServiceProvider _serviceProvider;
+    private readonly TerritoryData _territoryData;
+    private readonly IClientState _clientState;
     private readonly ILogger<TaskCreator> _logger;
 
-    public TaskCreator(IServiceProvider serviceProvider, ILogger<TaskCreator> logger)
+    public TaskCreator(
+        IServiceProvider serviceProvider,
+        TerritoryData territoryData,
+        IClientState clientState,
+        ILogger<TaskCreator> logger)
     {
         _serviceProvider = serviceProvider;
+        _territoryData = territoryData;
+        _clientState = clientState;
         _logger = logger;
     }
 
@@ -40,6 +51,31 @@ internal sealed class TaskCreator
                 return tasks;
             })
             .ToList();
+
+        var singlePlayerDutyTask = newTasks
+            .Where(y => y is SinglePlayerDuty.StartSinglePlayerDuty)
+            .Cast<SinglePlayerDuty.StartSinglePlayerDuty>()
+            .FirstOrDefault();
+        if (singlePlayerDutyTask != null &&
+            _territoryData.TryGetContentFinderCondition(singlePlayerDutyTask.ContentFinderConditionId,
+                out var cfcData))
+        {
+            // if we have a single player duty in queue, we check if we're in the matching territory
+            // if yes, skip all steps before (e.g. teleporting, waiting for navmesh, moving, interacting)
+            if (_clientState.TerritoryType == cfcData.TerritoryId)
+            {
+                int index = newTasks.IndexOf(singlePlayerDutyTask);
+                _logger.LogWarning(
+                    "Skipping {SkippedTaskCount} out of {TotalCount} tasks, questionable was started while in single player duty",
+                    index + 1, newTasks.Count);
+
+                newTasks.RemoveRange(0, index + 1);
+                _logger.LogInformation("Next actual task: {NextTask}, total tasks left: {RemainingTaskCount}",
+                    newTasks.FirstOrDefault(),
+                    newTasks.Count);
+            }
+        }
+
         if (newTasks.Count == 0)
             _logger.LogInformation("Nothing to execute for step?");
         else
index f269b138abd8d0584efecd3276cfcec034cfd261..820ae65655667cc0488c1749665a9b5c20ca12f2 100644 (file)
@@ -45,7 +45,7 @@ internal sealed class TerritoryData
             .ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToDalamudString().ToString());
 
         _contentFinderConditions = dataManager.GetExcelSheet<ContentFinderCondition>()
-            .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
+            .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType is 1 or 5 && x.ContentType.RowId != 6)
             .Select(x => new ContentFinderConditionData(x, dataManager.Language))
             .ToImmutableDictionary(x => x.ContentFinderConditionId, x => x);
     }
diff --git a/Questionable/External/BossModIpc.cs b/Questionable/External/BossModIpc.cs
new file mode 100644 (file)
index 0000000..d1d02f7
--- /dev/null
@@ -0,0 +1,73 @@
+using Dalamud.Plugin;
+using Dalamud.Plugin.Ipc;
+using Dalamud.Plugin.Ipc.Exceptions;
+using Dalamud.Plugin.Services;
+
+namespace Questionable.External;
+
+internal sealed class BossModIpc
+{
+    private readonly ICommandManager _commandManager;
+    private const string Name = "BossMod";
+
+    private readonly ICallGateSubscriber<string, string?> _getPreset;
+    private readonly ICallGateSubscriber<string, bool, bool> _createPreset;
+    private readonly ICallGateSubscriber<string, bool> _setPreset;
+    private readonly ICallGateSubscriber<bool> _clearPreset;
+
+    public BossModIpc(IDalamudPluginInterface pluginInterface, ICommandManager commandManager)
+    {
+        _commandManager = commandManager;
+        _getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{Name}.Presets.Get");
+        _createPreset = pluginInterface.GetIpcSubscriber<string, bool, bool>($"{Name}.Presets.Create");
+        _setPreset = pluginInterface.GetIpcSubscriber<string, bool>($"{Name}.Presets.SetActive");
+        _clearPreset = pluginInterface.GetIpcSubscriber<bool>($"{Name}.Presets.ClearActive");
+    }
+
+    public bool IsSupported()
+    {
+        try
+        {
+            return _getPreset.HasFunction;
+        }
+        catch (IpcError)
+        {
+            return false;
+        }
+    }
+
+    public string? GetPreset(string name)
+    {
+        return _getPreset.InvokeFunc(name);
+    }
+
+    public bool CreatePreset(string name, bool overwrite)
+    {
+        return _createPreset.InvokeFunc(name, overwrite);
+    }
+
+    public void SetPreset(string name)
+    {
+        _setPreset.InvokeFunc(name);
+    }
+
+    public void ClearPreset()
+    {
+        _clearPreset.InvokeFunc();
+    }
+
+    // TODO this should use your actual rotation plugin, not always vbm
+    public void EnableAi(string presetName = "VBM Default")
+    {
+        _commandManager.ProcessCommand("/vbmai on");
+        _commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles true");
+        SetPreset(presetName);
+    }
+
+    public void DisableAi()
+    {
+        _commandManager.ProcessCommand("/vbmai off");
+        _commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles false");
+        ClearPreset();
+    }
+}
index e0794c2d75aec01d4eaac04cf0911d02e39ec74a..28d1f66b6f571b6fc54f1d1c40bdf35fc2a387ec 100644 (file)
@@ -131,6 +131,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<NotificationMasterIpc>();
         serviceCollection.AddSingleton<AutomatonIpc>();
         serviceCollection.AddSingleton<AutoDutyIpc>();
+        serviceCollection.AddSingleton<BossModIpc>();
 
         serviceCollection.AddSingleton<GearStatsCalculator>();
     }
@@ -222,6 +223,14 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddTaskExecutor<InitiateLeve.Initiate, InitiateLeve.InitiateExecutor>();
         serviceCollection.AddTaskExecutor<InitiateLeve.SelectDifficulty, InitiateLeve.SelectDifficultyExecutor>();
 
+        serviceCollection.AddTaskFactory<SinglePlayerDuty.Factory>();
+        serviceCollection
+            .AddTaskExecutor<SinglePlayerDuty.StartSinglePlayerDuty, SinglePlayerDuty.StartSinglePlayerDutyExecutor>();
+        serviceCollection.AddTaskExecutor<SinglePlayerDuty.EnableAi, SinglePlayerDuty.EnableAiExecutor>();
+        serviceCollection
+            .AddTaskExecutor<SinglePlayerDuty.WaitSinglePlayerDuty, SinglePlayerDuty.WaitSinglePlayerDutyExecutor>();
+        serviceCollection.AddTaskExecutor<SinglePlayerDuty.DisableAi, SinglePlayerDuty.DisableAiExecutor>();
+
         serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.WaitConditionExecutor>();
         serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
         serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();