Assignment(nameof(QuestStep.ContentFinderConditionId),
step.ContentFinderConditionId, emptyStep.ContentFinderConditionId)
.AsSyntaxNodeOrToken(),
+ Assignment(nameof(QuestStep.AutoDutyEnabled),
+ step.AutoDutyEnabled, emptyStep.AutoDutyEnabled)
+ .AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
emptyStep.SkipConditions)
.AsSyntaxNodeOrToken(),
{
"TerritoryId": 138,
"InteractionType": "Duty",
- "ContentFinderConditionId": 4
+ "ContentFinderConditionId": 4,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 146,
"InteractionType": "Duty",
- "ContentFinderConditionId": 56
+ "ContentFinderConditionId": 56,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 140,
"InteractionType": "Duty",
- "ContentFinderConditionId": 3
+ "ContentFinderConditionId": 3,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 148,
"InteractionType": "Duty",
- "ContentFinderConditionId": 2
+ "ContentFinderConditionId": 2,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 153,
"InteractionType": "Duty",
- "ContentFinderConditionId": 1
+ "ContentFinderConditionId": 1,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 148,
"InteractionType": "Duty",
- "ContentFinderConditionId": 6
+ "ContentFinderConditionId": 6,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 137,
"InteractionType": "Duty",
- "ContentFinderConditionId": 8
+ "ContentFinderConditionId": 8,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 139,
"InteractionType": "Duty",
- "ContentFinderConditionId": 57
+ "ContentFinderConditionId": 57,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 155,
"InteractionType": "Duty",
- "ContentFinderConditionId": 11
+ "ContentFinderConditionId": 11,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 331,
"InteractionType": "Duty",
- "ContentFinderConditionId": 58
+ "ContentFinderConditionId": 58,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 147,
"InteractionType": "Duty",
- "ContentFinderConditionId": 15
+ "ContentFinderConditionId": 15,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 147,
"InteractionType": "Duty",
- "ContentFinderConditionId": 16
+ "ContentFinderConditionId": 16,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1053,
"InteractionType": "Duty",
- "ContentFinderConditionId": 830
+ "ContentFinderConditionId": 830,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 155,
"InteractionType": "Duty",
- "ContentFinderConditionId": 27
+ "ContentFinderConditionId": 27,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 156,
"InteractionType": "Duty",
- "ContentFinderConditionId": 32
+ "ContentFinderConditionId": 32,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 155,
"InteractionType": "Duty",
- "ContentFinderConditionId": 5
+ "ContentFinderConditionId": 5,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 155,
"InteractionType": "Duty",
- "ContentFinderConditionId": 5
+ "ContentFinderConditionId": 5,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 155,
"InteractionType": "Duty",
- "ContentFinderConditionId": 5
+ "ContentFinderConditionId": 5,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 397,
"InteractionType": "Duty",
- "ContentFinderConditionId": 36
+ "ContentFinderConditionId": 36,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 398,
"InteractionType": "Duty",
- "ContentFinderConditionId": 37
+ "ContentFinderConditionId": 37,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 418,
"InteractionType": "Duty",
- "ContentFinderConditionId": 39
+ "ContentFinderConditionId": 39,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 419,
"InteractionType": "Duty",
- "ContentFinderConditionId": 34
+ "ContentFinderConditionId": 34,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 399,
"InteractionType": "Duty",
- "ContentFinderConditionId": 31
+ "ContentFinderConditionId": 31,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 402,
"InteractionType": "Duty",
- "ContentFinderConditionId": 38
+ "ContentFinderConditionId": 38,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 463,
"InteractionType": "Duty",
- "ContentFinderConditionId": 141
+ "ContentFinderConditionId": 141,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 155,
"InteractionType": "Duty",
- "ContentFinderConditionId": 182
+ "ContentFinderConditionId": 182,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 152,
"InteractionType": "Duty",
- "ContentFinderConditionId": 219
+ "ContentFinderConditionId": 219,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 680,
"InteractionType": "Duty",
- "ContentFinderConditionId": 238
+ "ContentFinderConditionId": 238,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 614,
"InteractionType": "Duty",
- "ContentFinderConditionId": 241
+ "ContentFinderConditionId": 241,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 620,
"InteractionType": "Duty",
- "ContentFinderConditionId": 242
+ "ContentFinderConditionId": 242,
+ "AutoDutyEnabled": true
}
]
},
{\r
"TerritoryId": 621,\r
"InteractionType": "Duty",\r
- "ContentFinderConditionId": 279\r
+ "ContentFinderConditionId": 279,\r
+ "AutoDutyEnabled": true\r
}\r
]\r
},\r
{
"TerritoryId": 614,
"InteractionType": "Duty",
- "ContentFinderConditionId": 585
+ "ContentFinderConditionId": 585,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 829,
"InteractionType": "Duty",
- "ContentFinderConditionId": 611
+ "ContentFinderConditionId": 611,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 813,
"InteractionType": "Duty",
- "ContentFinderConditionId": 676
+ "ContentFinderConditionId": 676,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 816,
"InteractionType": "Duty",
- "ContentFinderConditionId": 649
+ "ContentFinderConditionId": 649,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 817,
"InteractionType": "Duty",
- "ContentFinderConditionId": 651
+ "ContentFinderConditionId": 651,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 814,
"InteractionType": "Duty",
- "ContentFinderConditionId": 659
+ "ContentFinderConditionId": 659,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 814,
"InteractionType": "Duty",
- "ContentFinderConditionId": 714
+ "ContentFinderConditionId": 714,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 957,
"InteractionType": "Duty",
- "ContentFinderConditionId": 783
+ "ContentFinderConditionId": 783,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 957,
"InteractionType": "Duty",
- "ContentFinderConditionId": 789
+ "ContentFinderConditionId": 789,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 961,
"InteractionType": "Duty",
- "ContentFinderConditionId": 787
+ "ContentFinderConditionId": 787,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 956,
"InteractionType": "Duty",
- "ContentFinderConditionId": 786
+ "ContentFinderConditionId": 786,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1030,
"InteractionType": "Duty",
- "ContentFinderConditionId": 790
+ "ContentFinderConditionId": 790,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 957,
"InteractionType": "Duty",
- "ContentFinderConditionId": 844
+ "ContentFinderConditionId": 844,
+ "AutoDutyEnabled": false
}
]
},
{
"TerritoryId": 1056,
"InteractionType": "Duty",
- "ContentFinderConditionId": 869
+ "ContentFinderConditionId": 869,
+ "AutoDutyEnabled": false
}
]
},
"TerritoryId": 958,
"InteractionType": "Duty",
"Comment": "Lapis Manalis",
- "ContentFinderConditionId": 896
+ "ContentFinderConditionId": 896,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 962,
"InteractionType": "Duty",
- "ContentFinderConditionId": 822
+ "ContentFinderConditionId": 822,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1162,
"InteractionType": "Duty",
- "ContentFinderConditionId": 823
+ "ContentFinderConditionId": 823,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1185,
"InteractionType": "Duty",
- "ContentFinderConditionId": 826
+ "ContentFinderConditionId": 826,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1187,
"InteractionType": "Duty",
- "ContentFinderConditionId": 824
+ "ContentFinderConditionId": 824,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1189,
"InteractionType": "Duty",
- "ContentFinderConditionId": 829
+ "ContentFinderConditionId": 829,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1219,
"InteractionType": "Duty",
- "ContentFinderConditionId": 831
+ "ContentFinderConditionId": 831,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1191,
"InteractionType": "Duty",
- "ContentFinderConditionId": 825
+ "ContentFinderConditionId": 825,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1192,
"InteractionType": "Duty",
- "ContentFinderConditionId": 827
+ "ContentFinderConditionId": 827,
+ "AutoDutyEnabled": true
}
]
},
{
"TerritoryId": 1191,
"InteractionType": "Duty",
- "ContentFinderConditionId": 1008
+ "ContentFinderConditionId": 1008,
+ "AutoDutyEnabled": true
}
]
},
"exclusiveMinimum": 0,
"exclusiveMaximum": 3000
},
+ "AutoDutyEnabled": {
+ "type": "boolean"
+ },
"DataId": {
"type": "null"
},
public JumpDestination? JumpDestination { get; set; }
public uint? ContentFinderConditionId { get; set; }
+ public bool AutoDutyEnabled { get; set; }
public SkipConditions? SkipConditions { get; set; }
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
-using Dalamud.Configuration;
+using System.Collections.Generic;
+using Dalamud.Configuration;
using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.ImGui;
public int Version { get; set; } = 1;
public int PluginSetupCompleteVersion { get; set; }
public GeneralConfiguration General { get; } = new();
+ public DutyConfiguration Duties { get; } = new();
public NotificationConfiguration Notifications { get; } = new();
public AdvancedConfiguration Advanced { get; } = new();
public WindowConfig DebugWindowConfig { get; } = new();
public bool ConfigureTextAdvance { get; set; } = true;
}
+ internal sealed class DutyConfiguration
+ {
+ public bool RunInstancedContentWithAutoDuty { get; set; }
+ public HashSet<uint> WhitelistedDutyCfcIds { get; set; } = [];
+ public HashSet<uint> BlacklistedDutyCfcIds { get; set; } = [];
+ }
+
internal sealed class NotificationConfiguration
{
public bool Enabled { get; set; } = true;
private readonly LeveData _leveData;
private readonly ICallGateProvider<object> _reloadDataIpc;
- private readonly Dictionary<ElementId, Quest> _quests = new();
+ private readonly Dictionary<ElementId, Quest> _quests = [];
+ private readonly Dictionary<uint, (ElementId QuestId, QuestStep Step)> _contentFinderConditionIds = [];
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData,
QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator,
{
_questValidator.Reset();
_quests.Clear();
+ _contentFinderConditionIds.Clear();
LoadQuestsFromAssembly();
LoadQuestsFromProjectDirectory();
"Failed to load all quests from user directory (some may have been successfully loaded)");
}
+ LoadCfcIds();
ValidateQuests();
Reloaded?.Invoke(this, EventArgs.Empty);
try
}
}
+ private void LoadCfcIds()
+ {
+ foreach (var quest in _quests.Values)
+ {
+ foreach (var dutyStep in quest.AllSteps().Where(x =>
+ x.Step.InteractionType == EInteractionType.Duty && x.Step.ContentFinderConditionId != null))
+ {
+ _contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = (quest.Id, dutyStep.Step);
+ }
+ }
+ }
+
private void ValidateQuests()
{
_questValidator.Validate(_quests.Values.Where(x => x.Source != Quest.ESource.Assembly).ToList());
.Where(x => IsKnownQuest(x.QuestId))
.ToList();
}
+
+ public bool TryGetDutyByContentFinderConditionId(uint cfcId, out bool autoDutyEnabledByDefault)
+ {
+ if (_contentFinderConditionIds.TryGetValue(cfcId, out var value))
+ {
+ autoDutyEnabledByDefault = value.Step.AutoDutyEnabled;
+ return true;
+ }
+
+ autoDutyEnabledByDefault = false;
+ return false;
+ }
}
{
internal sealed class Factory(
AutomatonIpc automatonIpc,
+ AutoDutyIpc autoDutyIpc,
TerritoryData territoryData) : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType.Snipe when !automatonIpc.IsAutoSnipeEnabled =>
new Task(step.InteractionType, step.Comment),
- EInteractionType.Duty =>
+ EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled) =>
new Task(step.InteractionType, step.ContentFinderConditionId.HasValue
? territoryData.GetContentFinderConditionName(step.ContentFinderConditionId.Value)
: step.Comment),
using System;
+using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
+using Questionable.Controller.Steps.Shared;
+using Questionable.Data;
+using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
internal static class Duty
{
- internal sealed class Factory : SimpleTaskFactory
+ internal sealed class Factory(AutoDutyIpc autoDutyIpc) : ITaskFactory
{
- public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Duty)
- return null;
+ yield break;
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
- return new Task(step.ContentFinderConditionId.Value);
+
+ if (autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled))
+ {
+ yield return new StartAutoDutyTask(step.ContentFinderConditionId.Value);
+ yield return new WaitAutoDutyTask(step.ContentFinderConditionId.Value);
+ yield return new WaitAtEnd.WaitNextStepOrSequence();
+ }
+ else
+ {
+ yield return new OpenDutyFinderTask(step.ContentFinderConditionId.Value);
+ }
+ }
+ }
+
+ internal sealed record StartAutoDutyTask(uint ContentFinderConditionId) : ITask
+ {
+ public override string ToString() => $"StartAutoDuty({ContentFinderConditionId})";
+ }
+
+ internal sealed class StartAutoDutyExecutor(
+ AutoDutyIpc autoDutyIpc,
+ TerritoryData territoryData,
+ IClientState clientState) : TaskExecutor<StartAutoDutyTask>
+ {
+ protected override bool Start()
+ {
+ autoDutyIpc.StartInstance(Task.ContentFinderConditionId);
+ return true;
+ }
+
+ public override ETaskResult Update()
+ {
+ if (!territoryData.TryGetTerritoryIdForContentFinderCondition(Task.ContentFinderConditionId,
+ out uint territoryId))
+ throw new TaskException("Failed to get territory ID for content finder condition");
+
+ return clientState.TerritoryType == territoryId ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+ }
+ }
+
+ internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask
+ {
+ public override string ToString() => $"Wait(AutoDuty, left instance {ContentFinderConditionId})";
+ }
+
+ internal sealed class WaitAutoDutyExecutor(
+ AutoDutyIpc autoDutyIpc,
+ TerritoryData territoryData,
+ IClientState clientState) : TaskExecutor<WaitAutoDutyTask>
+ {
+ protected override bool Start() => true;
+
+ public override ETaskResult Update()
+ {
+ if (!territoryData.TryGetTerritoryIdForContentFinderCondition(Task.ContentFinderConditionId,
+ out uint territoryId))
+ throw new TaskException("Failed to get territory ID for content finder condition");
+
+ return clientState.TerritoryType != territoryId && autoDutyIpc.IsStopped()
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
}
}
- internal sealed record Task(uint ContentFinderConditionId) : ITask
+ internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask
{
public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
}
- internal sealed class OpenDutyWindowExecutor(
+ internal sealed class OpenDutyFinderExecutor(
GameFunctions gameFunctions,
- ICondition condition) : TaskExecutor<Task>
+ ICondition condition) : TaskExecutor<OpenDutyFinderTask>
{
protected override bool Start()
{
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils;
using Questionable.Data;
+using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
internal sealed class Factory(
IClientState clientState,
ICondition condition,
- TerritoryData territoryData)
+ TerritoryData territoryData,
+ AutoDutyIpc autoDutyIpc)
: ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
case EInteractionType.Snipe:
return [new WaitNextStepOrSequence()];
- case EInteractionType.Duty:
+ case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled):
case EInteractionType.SinglePlayerDuty:
return [new EndAutomation()];
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
+using Dalamud.Game;
using Dalamud.Plugin.Services;
+using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Lumina.Excel.Sheets;
private readonly ImmutableDictionary<ushort, uint> _dutyTerritories;
private readonly ImmutableDictionary<uint, string> _instanceNames;
private readonly ImmutableDictionary<uint, string> _contentFinderConditionNames;
+ private readonly ImmutableDictionary<uint, uint> _contentFinderConditionIds;
public TerritoryData(IDataManager dataManager)
{
_instanceNames = dataManager.GetExcelSheet<ContentFinderCondition>()
.Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
- .ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToString());
+ .ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToDalamudString().ToString());
_contentFinderConditionNames = dataManager.GetExcelSheet<ContentFinderCondition>()
.Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
- .ToImmutableDictionary(x => x.RowId, x => x.Name.ToString());
+ .ToImmutableDictionary(x => x.RowId, x => FixName(x.Name.ToDalamudString().ToString(), dataManager.Language));
+ _contentFinderConditionIds = dataManager.GetExcelSheet<ContentFinderCondition>()
+ .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
+ .ToImmutableDictionary(x => x.RowId, x => x.TerritoryType.RowId);
}
public string? GetName(ushort territoryId) => _territoryNames.GetValueOrDefault(territoryId);
public string? GetInstanceName(ushort instanceId) => _instanceNames.GetValueOrDefault(instanceId);
public string? GetContentFinderConditionName(uint cfcId) => _contentFinderConditionNames.GetValueOrDefault(cfcId);
+
+ public bool TryGetTerritoryIdForContentFinderCondition(uint cfcId, out uint territoryId) =>
+ _contentFinderConditionIds.TryGetValue(cfcId, out territoryId);
+
+ private static string FixName(string name, ClientLanguage language)
+ {
+ if (string.IsNullOrEmpty(name) || language != ClientLanguage.English)
+ return name;
+
+ return string.Concat(name[0].ToString().ToUpper(CultureInfo.InvariantCulture), name.AsSpan(1));
+ }
}
--- /dev/null
+using Dalamud.Plugin;
+using Dalamud.Plugin.Ipc;
+using Dalamud.Plugin.Ipc.Exceptions;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps;
+using Questionable.Data;
+
+namespace Questionable.External;
+
+internal sealed class AutoDutyIpc
+{
+ private readonly Configuration _configuration;
+ private readonly TerritoryData _territoryData;
+ private readonly ILogger<AutoDutyIpc> _logger;
+ private readonly ICallGateSubscriber<uint,bool> _contentHasPath;
+ private readonly ICallGateSubscriber<uint,int,bool,object> _run;
+ private readonly ICallGateSubscriber<bool> _isStopped;
+
+ public AutoDutyIpc(IDalamudPluginInterface pluginInterface, Configuration configuration, TerritoryData territoryData, ILogger<AutoDutyIpc> logger)
+ {
+ _configuration = configuration;
+ _territoryData = territoryData;
+ _logger = logger;
+ _contentHasPath = pluginInterface.GetIpcSubscriber<uint, bool>("AutoDuty.ContentHasPath");
+ _run = pluginInterface.GetIpcSubscriber<uint, int, bool, object>("AutoDuty.Run");
+ _isStopped = pluginInterface.GetIpcSubscriber<bool>("AutoDuty.IsStopped");
+ }
+
+ public bool IsConfiguredToRunContent(uint? cfcId, bool autoDutyEnabled)
+ {
+ if (cfcId == null)
+ return false;
+
+ if (!_configuration.Duties.RunInstancedContentWithAutoDuty)
+ return false;
+
+ if (_configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId.Value))
+ return false;
+
+ if (_configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId.Value) &&
+ _territoryData.TryGetTerritoryIdForContentFinderCondition(cfcId.Value, out _))
+ return true;
+
+ return autoDutyEnabled && HasPath(cfcId.Value);
+ }
+
+ public bool HasPath(uint cfcId)
+ {
+ if (!_territoryData.TryGetTerritoryIdForContentFinderCondition(cfcId, out uint territoryType))
+ return false;
+
+ try
+ {
+ return _contentHasPath.InvokeFunc(territoryType);
+ }
+ catch (IpcError e)
+ {
+ _logger.LogWarning("Unable to query AutoDuty for path in territory {TerritoryType}: {Message}", territoryType, e.Message);
+ return false;
+ }
+ }
+
+ public void StartInstance(uint cfcId)
+ {
+ if (!_territoryData.TryGetTerritoryIdForContentFinderCondition(cfcId, out uint territoryType))
+ throw new TaskException($"Unknown ContentFinderConditionId {cfcId}");
+
+ try
+ {
+ _run.InvokeAction(territoryType, 0, true);
+ }
+ catch (IpcError e)
+ {
+ throw new TaskException($"Unable to run content with AutoDuty: {e.Message}", e);
+ }
+ }
+
+ public bool IsStopped()
+ {
+ try
+ {
+ return _isStopped.InvokeFunc();
+ }
+ catch (IpcError)
+ {
+ return true;
+ }
+ }
+}
serviceCollection.AddSingleton<TextAdvanceIpc>();
serviceCollection.AddSingleton<NotificationMasterIpc>();
serviceCollection.AddSingleton<AutomatonIpc>();
+ serviceCollection.AddSingleton<AutoDutyIpc>();
}
private static void AddTaskFactories(ServiceCollection serviceCollection)
.AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>();
serviceCollection.AddTaskFactoryAndExecutor<Aetheryte.Attune, Aetheryte.Factory, Aetheryte.DoAttune>();
serviceCollection.AddTaskFactoryAndExecutor<Combat.Task, Combat.Factory, Combat.HandleCombat>();
- serviceCollection.AddTaskFactoryAndExecutor<Duty.Task, Duty.Factory, Duty.OpenDutyWindowExecutor>();
+ serviceCollection.AddTaskFactoryAndExecutor<Duty.OpenDutyFinderTask, Duty.Factory, Duty.OpenDutyFinderExecutor>();
+ serviceCollection.AddTaskExecutor<Duty.StartAutoDutyTask, Duty.StartAutoDutyExecutor>();
+ serviceCollection.AddTaskExecutor<Duty.WaitAutoDutyTask, Duty.WaitAutoDutyExecutor>();
serviceCollection.AddTaskFactory<Emote.Factory>();
serviceCollection.AddTaskExecutor<Emote.UseOnObject, Emote.UseOnObjectExecutor>();
serviceCollection.AddTaskExecutor<Emote.UseOnSelf, Emote.UseOnSelfExecutor>();
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
+using System.Numerics;
+using System.Text;
using Dalamud.Game.Text;
+using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using LLib.ImGui;
using Lumina.Excel.Sheets;
using Questionable.Controller;
+using Questionable.Data;
using Questionable.External;
+using Questionable.Model;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Questionable.Windows;
internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{
+ private const string DutyClipboardPrefix = "qst:duty:";
+ private const string DutyClipboardSeparator = ";";
+ private const string DutyWhitelistPrefix = "+";
+ private const string DutyBlacklistPrefix = "-";
+
private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")];
private readonly IDalamudPluginInterface _pluginInterface;
private readonly NotificationMasterIpc _notificationMasterIpc;
private readonly Configuration _configuration;
private readonly CombatController _combatController;
+ private readonly QuestRegistry _questRegistry;
+ private readonly AutoDutyIpc _autoDutyIpc;
private readonly uint[] _mountIds;
private readonly string[] _mountNames;
private readonly string[] _grandCompanyNames =
["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];
+ private readonly string[] _supportedCfcOptions =
+ [
+ $"{SeIconChar.Circle.ToIconChar()} Enabled (Default)",
+ $"{SeIconChar.Circle.ToIconChar()} Enabled",
+ $"{SeIconChar.Cross.ToIconChar()} Disabled"
+ ];
+
+ private readonly string[] _unsupportedCfcOptions =
+ [
+ $"{SeIconChar.Cross.ToIconChar()} Disabled (Default)",
+ $"{SeIconChar.Circle.ToIconChar()} Enabled",
+ $"{SeIconChar.Cross.ToIconChar()} Disabled"
+ ];
+
+ private readonly Dictionary<EExpansionVersion, List<DutyInfo>> _contentFinderConditionNames;
+
public ConfigWindow(IDalamudPluginInterface pluginInterface,
NotificationMasterIpc notificationMasterIpc,
Configuration configuration,
IDataManager dataManager,
- CombatController combatController)
+ CombatController combatController,
+ TerritoryData territoryData,
+ QuestRegistry questRegistry,
+ AutoDutyIpc autoDutyIpc)
: base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize)
{
_pluginInterface = pluginInterface;
_notificationMasterIpc = notificationMasterIpc;
_configuration = configuration;
_combatController = combatController;
+ _questRegistry = questRegistry;
+ _autoDutyIpc = autoDutyIpc;
var mounts = dataManager.GetExcelSheet<Mount>()
.Where(x => x is { RowId: > 0, Icon: > 0 })
.ToList();
_mountIds = DefaultMounts.Select(x => x.Id).Concat(mounts.Select(x => x.MountId)).ToArray();
_mountNames = DefaultMounts.Select(x => x.Name).Concat(mounts.Select(x => x.Name)).ToArray();
+
+ _contentFinderConditionNames = dataManager.GetExcelSheet<DawnContent>()
+ .Where(x => x.RowId > 0)
+ .Select(x => x.Content.ValueNullable)
+ .Where(x => x != null)
+ .Select(x => x!.Value)
+ .Select(x => new
+ {
+ Expansion = (EExpansionVersion)x.TerritoryType.Value.ExVersion.RowId,
+ CfcId = x.RowId,
+ Name = territoryData.GetContentFinderConditionName(x.RowId) ?? "?",
+ TerritoryId = x.TerritoryType.RowId,
+ ContentType = x.ContentType.RowId,
+ Level = x.ClassJobLevelRequired,
+ x.SortKey
+ })
+ .GroupBy(x => x.Expansion)
+ .ToDictionary(x => x.Key,
+ x => x.OrderBy(y => y.Level)
+ .ThenBy(y => y.ContentType)
+ .ThenBy(y => y.SortKey)
+ .Select(y => new DutyInfo(y.CfcId, y.TerritoryId, $"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}"))
+ .ToList());
}
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
+ private static string FormatLevel(int level)
+ {
+ if (level == 0)
+ return string.Empty;
+
+ return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}";
+ }
+
public override void Draw()
{
using var tabBar = ImRaii.TabBar("QuestionableConfigTabs");
return;
DrawGeneralTab();
+ DrawDutiesTab();
DrawNotificationsTab();
DrawAdvancedTab();
}
}
}
+ private void DrawDutiesTab()
+ {
+ using var tab = ImRaii.TabItem("Duties");
+ if (!tab)
+ return;
+
+ bool runInstancedContentWithAutoDuty = _configuration.Duties.RunInstancedContentWithAutoDuty;
+ if (ImGui.Checkbox("Run instanced content with AutoDuty and BossMod", ref runInstancedContentWithAutoDuty))
+ {
+ _configuration.Duties.RunInstancedContentWithAutoDuty = runInstancedContentWithAutoDuty;
+ Save();
+ }
+
+ ImGui.SameLine();
+ ImGuiComponents.HelpMarker(
+ "The combat module used for this is configured by AutoDuty, ignoring whichever selection you've made in Questionable's \"General\" configuration.");
+
+ ImGui.Separator();
+
+ using (ImRaii.Disabled(!runInstancedContentWithAutoDuty))
+ {
+ ImGui.Text(
+ "Questionable includes a default list of duties that work if AutoDuty and BossMod are installed.");
+
+ ImGui.Text("The included list of duties can change with each update, and is based on the following spreadsheet:");
+ if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.GlobeEurope, "Open AutoDuty spreadsheet"))
+ Util.OpenLink(
+ "https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0");
+
+ ImGui.Separator();
+ ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:");
+
+ using (var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true))
+ {
+ if (child)
+ {
+ foreach (EExpansionVersion expansion in Enum.GetValues<EExpansionVersion>())
+ {
+ if (ImGui.CollapsingHeader(expansion.ToString()))
+ {
+ using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit);
+ if (table)
+ {
+ ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f);
+
+ if (_contentFinderConditionNames.TryGetValue(expansion, out var cfcNames))
+ {
+ foreach (var (cfcId, territoryId, name) in cfcNames)
+ {
+ if (_questRegistry.TryGetDutyByContentFinderConditionId(cfcId,
+ out bool autoDutyEnabledByDefault))
+ {
+ ImGui.TableNextRow();
+
+ string[] labels = autoDutyEnabledByDefault
+ ? _supportedCfcOptions
+ : _unsupportedCfcOptions;
+ int value = 0;
+ if (_configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId))
+ value = 1;
+ if (_configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId))
+ value = 2;
+
+ if (ImGui.TableNextColumn())
+ {
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted(name);
+ if (ImGui.IsItemHovered() && _configuration.Advanced.AdditionalStatusInformation)
+ {
+ using var tooltip = ImRaii.Tooltip();
+ if (tooltip)
+ {
+ ImGui.TextUnformatted(name);
+ ImGui.Separator();
+ ImGui.BulletText($"TerritoryId: {territoryId}");
+ ImGui.BulletText($"ContentFinderConditionId: {cfcId}");
+ }
+ }
+
+ if (runInstancedContentWithAutoDuty && !_autoDutyIpc.HasPath(cfcId))
+ ImGuiComponents.HelpMarker("This duty is not supported by AutoDuty", FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
+ }
+
+ if (ImGui.TableNextColumn())
+ {
+ using var _ = ImRaii.PushId($"##Dungeon{cfcId}");
+ ImGui.SetNextItemWidth(200);
+ if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
+ {
+ _configuration.Duties.WhitelistedDutyCfcIds.Remove(cfcId);
+ _configuration.Duties.BlacklistedDutyCfcIds.Remove(cfcId);
+
+ if (value == 1)
+ _configuration.Duties.WhitelistedDutyCfcIds.Add(cfcId);
+ else if (value == 2)
+ _configuration.Duties.BlacklistedDutyCfcIds.Add(cfcId);
+
+ Save();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ using (ImRaii.Disabled(_configuration.Duties.WhitelistedDutyCfcIds.Count +
+ _configuration.Duties.BlacklistedDutyCfcIds.Count == 0))
+ {
+ if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, "Export to clipboard"))
+ {
+ var whitelisted =
+ _configuration.Duties.WhitelistedDutyCfcIds.Select(x => $"{DutyWhitelistPrefix}{x}");
+ var blacklisted =
+ _configuration.Duties.BlacklistedDutyCfcIds.Select(x => $"{DutyBlacklistPrefix}{x}");
+ string text = DutyClipboardPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(
+ string.Join(DutyClipboardSeparator, whitelisted.Concat(blacklisted))));
+ ImGui.SetClipboardText(text);
+ }
+ }
+
+ ImGui.SameLine();
+
+ string? clipboardText = GetClipboardText();
+ using (ImRaii.Disabled(clipboardText == null || !clipboardText.StartsWith(DutyClipboardPrefix, StringComparison.InvariantCulture)))
+ {
+ if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Paste, "Import from Clipboard"))
+ {
+ clipboardText = clipboardText!.Substring(DutyClipboardPrefix.Length);
+ string text = Encoding.UTF8.GetString(Convert.FromBase64String(clipboardText));
+
+ _configuration.Duties.WhitelistedDutyCfcIds.Clear();
+ _configuration.Duties.BlacklistedDutyCfcIds.Clear();
+ foreach (string part in text.Split(DutyClipboardSeparator))
+ {
+ if (part.StartsWith(DutyWhitelistPrefix, StringComparison.InvariantCulture) &&
+ uint.TryParse(part.AsSpan(DutyWhitelistPrefix.Length), CultureInfo.InvariantCulture,
+ out uint whitelistedCfcId))
+ _configuration.Duties.WhitelistedDutyCfcIds.Add(whitelistedCfcId);
+
+ if (part.StartsWith(DutyBlacklistPrefix, StringComparison.InvariantCulture) &&
+ uint.TryParse(part.AsSpan(DutyBlacklistPrefix.Length), CultureInfo.InvariantCulture,
+ out uint blacklistedCfcId))
+ _configuration.Duties.WhitelistedDutyCfcIds.Add(blacklistedCfcId);
+ }
+ }
+ }
+
+ ImGui.SameLine();
+
+ using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
+ {
+ if (ImGui.Button("Reset to default"))
+ {
+ _configuration.Duties.WhitelistedDutyCfcIds.Clear();
+ _configuration.Duties.BlacklistedDutyCfcIds.Clear();
+ Save();
+ }
+ }
+
+ if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
+ ImGui.SetTooltip("Hold CTRL to enable this button.");
+ }
+ }
+
private void DrawNotificationsTab()
{
using var tab = ImRaii.TabItem("Notifications");
private void Save() => _pluginInterface.SavePluginConfig(_configuration);
public void SaveWindowConfig() => Save();
+
+ /// <summary>
+ /// The default implementation for <see cref="ImGui.GetClipboardText"/> throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text.
+ /// </summary>
+ private unsafe string? GetClipboardText()
+ {
+ byte* ptr = ImGuiNative.igGetClipboardText();
+ if (ptr == null)
+ return null;
+
+ int byteCount = 0;
+ while (ptr[byteCount] != 0)
+ ++byteCount;
+ return Encoding.UTF8.GetString(ptr, byteCount);
+ }
+
+ private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name);
}