From 92873554cc838dca8e7e64db008063b2d676baf9 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 20 Feb 2025 01:34:59 +0100 Subject: [PATCH] Draft for auto-completing quest battles (HW MSQ) --- .../RoslynElements/QuestStepExtensions.cs | 3 + .../Class Quests/WAR/601_And My Axe.json | 4 +- .../1595_A Series of Unfortunate Events.json | 4 +- .../1597_Divine Intervention.json | 4 +- .../1601_Keeping the Flame Alive.json | 4 +- .../1606_Sounding Out the Amphitheatre.json | 17 ++- .../1626_Waiting for the Wind to Change.json | 17 ++- .../1630_A General Summons.json | 11 +- .../MSQ/A4-Ishgard/1639_Fire and Blood.json | 4 +- .../A5-Sea of Clouds/1644_Familiar Faces.json | 4 +- .../1657_An Illuminati Incident.json | 4 +- ...667_Close Encounters of the VIth Kind.json | 4 +- QuestPaths/quest-v1.json | 21 +++ Questionable.Model/Questing/QuestStep.cs | 1 + .../Controller/CombatModules/BossModModule.cs | 32 ++--- .../GameUi/InteractionUiController.cs | 17 ++- Questionable/Controller/QuestRegistry.cs | 3 +- .../Steps/Common/SendNotification.cs | 3 +- .../Steps/Interactions/SinglePlayerDuty.cs | 124 ++++++++++++++++++ .../Controller/Steps/Shared/WaitAtEnd.cs | 2 +- Questionable/Controller/Steps/TaskCreator.cs | 38 +++++- Questionable/Data/TerritoryData.cs | 2 +- Questionable/External/BossModIpc.cs | 73 +++++++++++ Questionable/QuestionablePlugin.cs | 9 ++ 24 files changed, 359 insertions(+), 46 deletions(-) create mode 100644 Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs create mode 100644 Questionable/External/BossModIpc.cs diff --git a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs index 2d3ec8034..ae0517d28 100644 --- a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs +++ b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs @@ -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(), diff --git a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json index fca23e000..dae6eb116 100644 --- a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json +++ b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json @@ -95,7 +95,9 @@ }, "TerritoryId": 138, "InteractionType": "SinglePlayerDuty", - "Fly": true + "Fly": true, + "ContentFinderConditionId": 393, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json index 06d28cf64..b4107b998 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json @@ -58,7 +58,9 @@ "Z": 349.96558 }, "TerritoryId": 401, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 395, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json index d8baf9d31..238d8e876 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json @@ -78,7 +78,9 @@ "AethernetShortcut": [ "[Ishgard] The Forgotten Knight", "[Ishgard] The Tribunal" - ] + ], + "ContentFinderConditionId": 396, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json index fe2d06748..a42a2a769 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json @@ -28,7 +28,9 @@ "Z": 388.63196 }, "TerritoryId": 145, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 400, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json index ff6cd6992..b00c7cc02 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json @@ -21,6 +21,16 @@ { "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 } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json index d38df9943..ea46e2441 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json @@ -59,7 +59,14 @@ "KillEnemyDataIds": [ 4015 ], - "$": "0 0 0 0 0 0 -> " + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "Position": { @@ -72,6 +79,14 @@ "EnemySpawnType": "AutoOnEnterArea", "KillEnemyDataIds": [ 4015 + ], + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 ] } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json index 61828e2cf..cdca15510 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json @@ -89,6 +89,16 @@ "InteractionType": "WalkTo", "Mount": true }, + { + "Position": { + "X": -335.0186, + "Y": 13.983504, + "Z": -100.87753 + }, + "TerritoryId": 140, + "InteractionType": "WalkTo", + "Fly": true + }, { "DataId": 1004019, "Position": { @@ -98,7 +108,6 @@ }, "TerritoryId": 140, "InteractionType": "Interact", - "Fly": true, "TargetTerritoryId": 140 }, { diff --git a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json index ce1f3688a..d3573b34f 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json @@ -74,7 +74,9 @@ "Z": 37.247192 }, "TerritoryId": 418, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 398, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json index c65d68153..0122bc84d 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json @@ -56,7 +56,9 @@ "TerritoryId": 401, "InteractionType": "SinglePlayerDuty", "Emote": "lookout", - "StopDistance": 0.25 + "StopDistance": 0.25, + "ContentFinderConditionId": 401, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json index 9f25ebdab..f3e3acdb5 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json @@ -47,7 +47,9 @@ "AethernetShortcut": [ "[Idyllshire] Aetheryte Plaza", "[Idyllshire] Epilogue Gate (Eastern Hinterlands)" - ] + ], + "ContentFinderConditionId": 422, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json index 907bdd453..792ea9613 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json @@ -68,7 +68,9 @@ "Z": 553.97876 }, "TerritoryId": 402, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 399, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 62ad57644..d078b4c83 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1257,6 +1257,27 @@ ] } }, + { + "if": { + "properties": { + "InteractionType": { + "const": "SinglePlayerDuty" + } + } + }, + "then": { + "properties": { + "ContentFinderConditionId": { + "type": "integer", + "exclusiveMinimum": 0, + "exclusiveMaximum": 3000 + }, + "BossModEnabled": { + "type": "boolean" + } + } + } + }, { "if": { "properties": { diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index bd1ce3040..fe7170d5b 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -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?> RequiredQuestVariables { get; set; } = new(); diff --git a/Questionable/Controller/CombatModules/BossModModule.cs b/Questionable/Controller/CombatModules/BossModModule.cs index ee25967f4..a69237f3e 100644 --- a/Questionable/Controller/CombatModules/BossModModule.cs +++ b/Questionable/Controller/CombatModules/BossModModule.cs @@ -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 _logger; + private readonly BossModIpc _bossModIpc; private readonly Configuration _configuration; - private readonly ICallGateSubscriber _getPreset; - private readonly ICallGateSubscriber _createPreset; - private readonly ICallGateSubscriber _setPreset; - private readonly ICallGateSubscriber _clearPreset; private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!; public BossModModule( ILogger logger, - IDalamudPluginInterface pluginInterface, + BossModIpc bossModIpc, Configuration configuration) { _logger = logger; + _bossModIpc = bossModIpc; _configuration = configuration; - - _getPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Get"); - _createPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Create"); - _setPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.SetActive"); - _clearPreset = pluginInterface.GetIpcSubscriber($"{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) diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index 45333c2b7..3164a3bb2 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -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 dialogueChoices, string actualPrompt) + QuestStep? step, List 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; } diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 7808b0950..2c38b2410 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -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); } diff --git a/Questionable/Controller/Steps/Common/SendNotification.cs b/Questionable/Controller/Steps/Common/SendNotification.cs index 6d8bbcec6..8bb4fa803 100644 --- a/Questionable/Controller/Steps/Common/SendNotification.cs +++ b/Questionable/Controller/Steps/Common/SendNotification.cs @@ -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 index 000000000..4cd79cec9 --- /dev/null +++ b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs @@ -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 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 + { + 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 + { + 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, 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 + { + protected override bool Start() + { + bossModIpc.DisableAi(); + return true; + } + + public override ETaskResult Update() => ETaskResult.TaskComplete; + + public override bool ShouldInterruptOnDamage() => false; + } +} diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index d39c7c2a3..59d108cdd 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -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: diff --git a/Questionable/Controller/Steps/TaskCreator.cs b/Questionable/Controller/Steps/TaskCreator.cs index 915f7d570..997d40d1d 100644 --- a/Questionable/Controller/Steps/TaskCreator.cs +++ b/Questionable/Controller/Steps/TaskCreator.cs @@ -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 _logger; - public TaskCreator(IServiceProvider serviceProvider, ILogger logger) + public TaskCreator( + IServiceProvider serviceProvider, + TerritoryData territoryData, + IClientState clientState, + ILogger 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() + .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 diff --git a/Questionable/Data/TerritoryData.cs b/Questionable/Data/TerritoryData.cs index f269b138a..820ae6565 100644 --- a/Questionable/Data/TerritoryData.cs +++ b/Questionable/Data/TerritoryData.cs @@ -45,7 +45,7 @@ internal sealed class TerritoryData .ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToDalamudString().ToString()); _contentFinderConditions = dataManager.GetExcelSheet() - .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 index 000000000..d1d02f794 --- /dev/null +++ b/Questionable/External/BossModIpc.cs @@ -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 _getPreset; + private readonly ICallGateSubscriber _createPreset; + private readonly ICallGateSubscriber _setPreset; + private readonly ICallGateSubscriber _clearPreset; + + public BossModIpc(IDalamudPluginInterface pluginInterface, ICommandManager commandManager) + { + _commandManager = commandManager; + _getPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Get"); + _createPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Create"); + _setPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.SetActive"); + _clearPreset = pluginInterface.GetIpcSubscriber($"{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(); + } +} diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index e0794c2d7..28d1f66b6 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -131,6 +131,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } @@ -222,6 +223,14 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskFactory(); + serviceCollection + .AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection + .AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskFactory(); serviceCollection.AddTaskExecutor();