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(),
},
"TerritoryId": 138,
"InteractionType": "SinglePlayerDuty",
- "Fly": true
+ "Fly": true,
+ "ContentFinderConditionId": 393,
+ "BossModEnabled": true
}
]
},
"Z": 349.96558
},
"TerritoryId": 401,
- "InteractionType": "SinglePlayerDuty"
+ "InteractionType": "SinglePlayerDuty",
+ "ContentFinderConditionId": 395,
+ "BossModEnabled": true
}
]
},
"AethernetShortcut": [
"[Ishgard] The Forgotten Knight",
"[Ishgard] The Tribunal"
- ]
+ ],
+ "ContentFinderConditionId": 396,
+ "BossModEnabled": true
}
]
},
"Z": 388.63196
},
"TerritoryId": 145,
- "InteractionType": "SinglePlayerDuty"
+ "InteractionType": "SinglePlayerDuty",
+ "ContentFinderConditionId": 400,
+ "BossModEnabled": true
}
]
},
{
"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,
"Z": 239.54294
},
"TerritoryId": 397,
- "InteractionType": "WalkTo",
- "AetheryteShortcut": "Coerthas Western Highlands - Falcon's Nest"
+ "InteractionType": "WalkTo"
},
{
"Position": {
},
"TerritoryId": 397,
"InteractionType": "SinglePlayerDuty",
- "DisableNavmesh": true
+ "DisableNavmesh": true,
+ "ContentFinderConditionId": 397,
+ "BossModEnabled": true
}
]
},
"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
]
}
]
"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
},
{
"Z": 37.247192
},
"TerritoryId": 418,
- "InteractionType": "SinglePlayerDuty"
+ "InteractionType": "SinglePlayerDuty",
+ "ContentFinderConditionId": 398,
+ "BossModEnabled": true
}
]
},
"TerritoryId": 401,
"InteractionType": "SinglePlayerDuty",
"Emote": "lookout",
- "StopDistance": 0.25
+ "StopDistance": 0.25,
+ "ContentFinderConditionId": 401,
+ "BossModEnabled": true
}
]
},
"AethernetShortcut": [
"[Idyllshire] Aetheryte Plaza",
"[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
- ]
+ ],
+ "ContentFinderConditionId": 422,
+ "BossModEnabled": true
}
]
},
"Z": 553.97876
},
"TerritoryId": 402,
- "InteractionType": "SinglePlayerDuty"
+ "InteractionType": "SinglePlayerDuty",
+ "ContentFinderConditionId": 399,
+ "BossModEnabled": true
}
]
},
]
}
},
+ {
+ "if": {
+ "properties": {
+ "InteractionType": {
+ "const": "SinglePlayerDuty"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "ContentFinderConditionId": {
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "exclusiveMaximum": 3000
+ },
+ "BossModEnabled": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
{
"if": {
"properties": {
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();
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)
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)
{
try
{
- _clearPreset.InvokeFunc();
+ _bossModIpc.ClearPreset();
return true;
}
catch (IpcError e)
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;
}
Yes = true
};
- if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
+ if (HandleDefaultYesNo(addonSelectYesno, quest, null, [dialogueChoice], actualPrompt))
return true;
}
}
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)
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;
}
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);
}
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,
};
}
--- /dev/null
+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;
+ }
+}
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:
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;
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;
}
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
.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);
}
--- /dev/null
+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();
+ }
+}
serviceCollection.AddSingleton<NotificationMasterIpc>();
serviceCollection.AddSingleton<AutomatonIpc>();
serviceCollection.AddSingleton<AutoDutyIpc>();
+ serviceCollection.AddSingleton<BossModIpc>();
serviceCollection.AddSingleton<GearStatsCalculator>();
}
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>();