Add AutoDuty integration

This commit is contained in:
Liza 2024-12-30 02:50:18 +01:00
parent f20b5e08a7
commit 8d85a0f896
Signed by: liza
GPG Key ID: 2C41B84815CF6445
65 changed files with 599 additions and 72 deletions

View File

@ -117,6 +117,9 @@ internal static class QuestStepExtensions
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(),

View File

@ -112,7 +112,8 @@
{
"TerritoryId": 138,
"InteractionType": "Duty",
"ContentFinderConditionId": 4
"ContentFinderConditionId": 4,
"AutoDutyEnabled": true
}
]
},

View File

@ -71,7 +71,8 @@
{
"TerritoryId": 146,
"InteractionType": "Duty",
"ContentFinderConditionId": 56
"ContentFinderConditionId": 56,
"AutoDutyEnabled": true
}
]
},

View File

@ -62,7 +62,8 @@
{
"TerritoryId": 140,
"InteractionType": "Duty",
"ContentFinderConditionId": 3
"ContentFinderConditionId": 3,
"AutoDutyEnabled": true
}
]
},

View File

@ -57,7 +57,8 @@
{
"TerritoryId": 148,
"InteractionType": "Duty",
"ContentFinderConditionId": 2
"ContentFinderConditionId": 2,
"AutoDutyEnabled": true
}
]
},

View File

@ -44,7 +44,8 @@
{
"TerritoryId": 153,
"InteractionType": "Duty",
"ContentFinderConditionId": 1
"ContentFinderConditionId": 1,
"AutoDutyEnabled": true
}
]
},

View File

@ -66,7 +66,8 @@
{
"TerritoryId": 148,
"InteractionType": "Duty",
"ContentFinderConditionId": 6
"ContentFinderConditionId": 6,
"AutoDutyEnabled": true
}
]
},

View File

@ -85,7 +85,8 @@
{
"TerritoryId": 137,
"InteractionType": "Duty",
"ContentFinderConditionId": 8
"ContentFinderConditionId": 8,
"AutoDutyEnabled": true
}
]
},

View File

@ -45,7 +45,8 @@
{
"TerritoryId": 139,
"InteractionType": "Duty",
"ContentFinderConditionId": 57
"ContentFinderConditionId": 57,
"AutoDutyEnabled": true
}
]
},

View File

@ -59,7 +59,8 @@
{
"TerritoryId": 155,
"InteractionType": "Duty",
"ContentFinderConditionId": 11
"ContentFinderConditionId": 11,
"AutoDutyEnabled": true
}
]
},

View File

@ -38,7 +38,8 @@
{
"TerritoryId": 331,
"InteractionType": "Duty",
"ContentFinderConditionId": 58
"ContentFinderConditionId": 58,
"AutoDutyEnabled": true
}
]
},

View File

@ -45,7 +45,8 @@
{
"TerritoryId": 147,
"InteractionType": "Duty",
"ContentFinderConditionId": 15
"ContentFinderConditionId": 15,
"AutoDutyEnabled": true
}
]
},

View File

@ -46,7 +46,8 @@
{
"TerritoryId": 147,
"InteractionType": "Duty",
"ContentFinderConditionId": 16
"ContentFinderConditionId": 16,
"AutoDutyEnabled": true
}
]
},
@ -71,7 +72,8 @@
{
"TerritoryId": 1053,
"InteractionType": "Duty",
"ContentFinderConditionId": 830
"ContentFinderConditionId": 830,
"AutoDutyEnabled": true
}
]
},

View File

@ -88,7 +88,8 @@
{
"TerritoryId": 155,
"InteractionType": "Duty",
"ContentFinderConditionId": 27
"ContentFinderConditionId": 27,
"AutoDutyEnabled": true
}
]
},

View File

@ -107,7 +107,8 @@
{
"TerritoryId": 156,
"InteractionType": "Duty",
"ContentFinderConditionId": 32
"ContentFinderConditionId": 32,
"AutoDutyEnabled": true
}
]
},

View File

@ -71,7 +71,8 @@
{
"TerritoryId": 155,
"InteractionType": "Duty",
"ContentFinderConditionId": 5
"ContentFinderConditionId": 5,
"AutoDutyEnabled": true
}
]
},

View File

@ -71,7 +71,8 @@
{
"TerritoryId": 155,
"InteractionType": "Duty",
"ContentFinderConditionId": 5
"ContentFinderConditionId": 5,
"AutoDutyEnabled": true
}
]
},

View File

@ -71,7 +71,8 @@
{
"TerritoryId": 155,
"InteractionType": "Duty",
"ContentFinderConditionId": 5
"ContentFinderConditionId": 5,
"AutoDutyEnabled": true
}
]
},

View File

@ -38,7 +38,8 @@
{
"TerritoryId": 397,
"InteractionType": "Duty",
"ContentFinderConditionId": 36
"ContentFinderConditionId": 36,
"AutoDutyEnabled": true
}
]
},

View File

@ -78,7 +78,8 @@
{
"TerritoryId": 398,
"InteractionType": "Duty",
"ContentFinderConditionId": 37
"ContentFinderConditionId": 37,
"AutoDutyEnabled": true
}
]
},

View File

@ -42,7 +42,8 @@
{
"TerritoryId": 418,
"InteractionType": "Duty",
"ContentFinderConditionId": 39
"ContentFinderConditionId": 39,
"AutoDutyEnabled": true
}
]
},

View File

@ -59,7 +59,8 @@
{
"TerritoryId": 419,
"InteractionType": "Duty",
"ContentFinderConditionId": 34
"ContentFinderConditionId": 34,
"AutoDutyEnabled": true
}
]
},

View File

@ -110,7 +110,8 @@
{
"TerritoryId": 399,
"InteractionType": "Duty",
"ContentFinderConditionId": 31
"ContentFinderConditionId": 31,
"AutoDutyEnabled": true
}
]
},

View File

@ -62,7 +62,8 @@
{
"TerritoryId": 402,
"InteractionType": "Duty",
"ContentFinderConditionId": 38
"ContentFinderConditionId": 38,
"AutoDutyEnabled": true
}
]
},

View File

@ -77,7 +77,8 @@
{
"TerritoryId": 463,
"InteractionType": "Duty",
"ContentFinderConditionId": 141
"ContentFinderConditionId": 141,
"AutoDutyEnabled": true
}
]
},

View File

@ -57,7 +57,8 @@
{
"TerritoryId": 155,
"InteractionType": "Duty",
"ContentFinderConditionId": 182
"ContentFinderConditionId": 182,
"AutoDutyEnabled": true
}
]
},

View File

@ -109,7 +109,8 @@
{
"TerritoryId": 152,
"InteractionType": "Duty",
"ContentFinderConditionId": 219
"ContentFinderConditionId": 219,
"AutoDutyEnabled": true
}
]
},

View File

@ -87,7 +87,8 @@
{
"TerritoryId": 680,
"InteractionType": "Duty",
"ContentFinderConditionId": 238
"ContentFinderConditionId": 238,
"AutoDutyEnabled": true
}
]
},

View File

@ -114,7 +114,8 @@
{
"TerritoryId": 614,
"InteractionType": "Duty",
"ContentFinderConditionId": 241
"ContentFinderConditionId": 241,
"AutoDutyEnabled": true
}
]
},

View File

@ -114,7 +114,8 @@
{
"TerritoryId": 620,
"InteractionType": "Duty",
"ContentFinderConditionId": 242
"ContentFinderConditionId": 242,
"AutoDutyEnabled": true
}
]
},

View File

@ -98,7 +98,8 @@
{
"TerritoryId": 621,
"InteractionType": "Duty",
"ContentFinderConditionId": 279
"ContentFinderConditionId": 279,
"AutoDutyEnabled": true
}
]
},

View File

@ -40,7 +40,8 @@
{
"TerritoryId": 614,
"InteractionType": "Duty",
"ContentFinderConditionId": 585
"ContentFinderConditionId": 585,
"AutoDutyEnabled": true
}
]
},

View File

@ -27,7 +27,8 @@
{
"TerritoryId": 829,
"InteractionType": "Duty",
"ContentFinderConditionId": 611
"ContentFinderConditionId": 611,
"AutoDutyEnabled": true
}
]
},

View File

@ -120,7 +120,8 @@
{
"TerritoryId": 813,
"InteractionType": "Duty",
"ContentFinderConditionId": 676
"ContentFinderConditionId": 676,
"AutoDutyEnabled": true
}
]
},

View File

@ -49,7 +49,8 @@
{
"TerritoryId": 816,
"InteractionType": "Duty",
"ContentFinderConditionId": 649
"ContentFinderConditionId": 649,
"AutoDutyEnabled": true
}
]
},

View File

@ -61,7 +61,8 @@
{
"TerritoryId": 817,
"InteractionType": "Duty",
"ContentFinderConditionId": 651
"ContentFinderConditionId": 651,
"AutoDutyEnabled": true
}
]
},

View File

@ -62,7 +62,8 @@
{
"TerritoryId": 814,
"InteractionType": "Duty",
"ContentFinderConditionId": 659
"ContentFinderConditionId": 659,
"AutoDutyEnabled": true
}
]
},

View File

@ -40,7 +40,8 @@
{
"TerritoryId": 814,
"InteractionType": "Duty",
"ContentFinderConditionId": 714
"ContentFinderConditionId": 714,
"AutoDutyEnabled": true
}
]
},

View File

@ -55,7 +55,8 @@
{
"TerritoryId": 957,
"InteractionType": "Duty",
"ContentFinderConditionId": 783
"ContentFinderConditionId": 783,
"AutoDutyEnabled": true
}
]
},

View File

@ -71,7 +71,8 @@
{
"TerritoryId": 957,
"InteractionType": "Duty",
"ContentFinderConditionId": 789
"ContentFinderConditionId": 789,
"AutoDutyEnabled": true
}
]
},

View File

@ -39,7 +39,8 @@
{
"TerritoryId": 961,
"InteractionType": "Duty",
"ContentFinderConditionId": 787
"ContentFinderConditionId": 787,
"AutoDutyEnabled": true
}
]
},

View File

@ -38,7 +38,8 @@
{
"TerritoryId": 956,
"InteractionType": "Duty",
"ContentFinderConditionId": 786
"ContentFinderConditionId": 786,
"AutoDutyEnabled": true
}
]
},
@ -63,7 +64,8 @@
{
"TerritoryId": 1030,
"InteractionType": "Duty",
"ContentFinderConditionId": 790
"ContentFinderConditionId": 790,
"AutoDutyEnabled": true
}
]
},

View File

@ -23,7 +23,8 @@
{
"TerritoryId": 957,
"InteractionType": "Duty",
"ContentFinderConditionId": 844
"ContentFinderConditionId": 844,
"AutoDutyEnabled": false
}
]
},

View File

@ -71,7 +71,8 @@
{
"TerritoryId": 1056,
"InteractionType": "Duty",
"ContentFinderConditionId": 869
"ContentFinderConditionId": 869,
"AutoDutyEnabled": false
}
]
},

View File

@ -57,7 +57,8 @@
"TerritoryId": 958,
"InteractionType": "Duty",
"Comment": "Lapis Manalis",
"ContentFinderConditionId": 896
"ContentFinderConditionId": 896,
"AutoDutyEnabled": true
}
]
},

View File

@ -160,7 +160,8 @@
{
"TerritoryId": 962,
"InteractionType": "Duty",
"ContentFinderConditionId": 822
"ContentFinderConditionId": 822,
"AutoDutyEnabled": true
}
]
},

View File

@ -24,7 +24,8 @@
{
"TerritoryId": 1162,
"InteractionType": "Duty",
"ContentFinderConditionId": 823
"ContentFinderConditionId": 823,
"AutoDutyEnabled": true
}
]
},

View File

@ -58,7 +58,8 @@
{
"TerritoryId": 1185,
"InteractionType": "Duty",
"ContentFinderConditionId": 826
"ContentFinderConditionId": 826,
"AutoDutyEnabled": true
}
]
},

View File

@ -23,7 +23,8 @@
{
"TerritoryId": 1187,
"InteractionType": "Duty",
"ContentFinderConditionId": 824
"ContentFinderConditionId": 824,
"AutoDutyEnabled": true
}
]
},

View File

@ -115,7 +115,8 @@
{
"TerritoryId": 1189,
"InteractionType": "Duty",
"ContentFinderConditionId": 829
"ContentFinderConditionId": 829,
"AutoDutyEnabled": true
}
]
},

View File

@ -60,7 +60,8 @@
{
"TerritoryId": 1219,
"InteractionType": "Duty",
"ContentFinderConditionId": 831
"ContentFinderConditionId": 831,
"AutoDutyEnabled": true
}
]
},

View File

@ -39,7 +39,8 @@
{
"TerritoryId": 1191,
"InteractionType": "Duty",
"ContentFinderConditionId": 825
"ContentFinderConditionId": 825,
"AutoDutyEnabled": true
}
]
},

View File

@ -56,7 +56,8 @@
{
"TerritoryId": 1192,
"InteractionType": "Duty",
"ContentFinderConditionId": 827
"ContentFinderConditionId": 827,
"AutoDutyEnabled": true
}
]
},

View File

@ -122,7 +122,8 @@
{
"TerritoryId": 1191,
"InteractionType": "Duty",
"ContentFinderConditionId": 1008
"ContentFinderConditionId": 1008,
"AutoDutyEnabled": true
}
]
},

View File

@ -1217,6 +1217,9 @@
"exclusiveMinimum": 0,
"exclusiveMaximum": 3000
},
"AutoDutyEnabled": {
"type": "boolean"
},
"DataId": {
"type": "null"
},

View File

@ -74,6 +74,7 @@ public sealed class QuestStep
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();

View File

@ -1,4 +1,5 @@
using Dalamud.Configuration;
using System.Collections.Generic;
using Dalamud.Configuration;
using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.ImGui;
@ -12,6 +13,7 @@ internal sealed class Configuration : IPluginConfiguration
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();
@ -32,6 +34,13 @@ internal sealed class Configuration : IPluginConfiguration
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;

View File

@ -29,7 +29,8 @@ internal sealed class QuestRegistry
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,
@ -55,6 +56,7 @@ internal sealed class QuestRegistry
{
_questValidator.Reset();
_quests.Clear();
_contentFinderConditionIds.Clear();
LoadQuestsFromAssembly();
LoadQuestsFromProjectDirectory();
@ -70,6 +72,7 @@ internal sealed class QuestRegistry
"Failed to load all quests from user directory (some may have been successfully loaded)");
}
LoadCfcIds();
ValidateQuests();
Reloaded?.Invoke(this, EventArgs.Empty);
try
@ -142,6 +145,18 @@ internal sealed class QuestRegistry
}
}
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());
@ -223,4 +238,16 @@ internal sealed class QuestRegistry
.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;
}
}

View File

@ -13,6 +13,7 @@ internal static class SendNotification
{
internal sealed class Factory(
AutomatonIpc automatonIpc,
AutoDutyIpc autoDutyIpc,
TerritoryData territoryData) : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
@ -21,7 +22,7 @@ internal static class SendNotification
{
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),

View File

@ -1,6 +1,10 @@
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;
@ -9,26 +13,86 @@ namespace Questionable.Controller.Steps.Interactions;
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 Task(uint ContentFinderConditionId) : ITask
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 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()
{

View File

@ -8,6 +8,7 @@ using Dalamud.Plugin.Services;
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;
@ -19,7 +20,8 @@ internal static class WaitAtEnd
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)
@ -50,7 +52,7 @@ internal static class WaitAtEnd
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()];

View File

@ -1,8 +1,11 @@
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;
@ -15,6 +18,7 @@ internal sealed class TerritoryData
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)
{
@ -40,11 +44,14 @@ internal sealed class TerritoryData
_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);
@ -68,4 +75,15 @@ internal sealed class TerritoryData
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));
}
}

89
Questionable/External/AutoDutyIpc.cs vendored Normal file
View File

@ -0,0 +1,89 @@
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;
}
}
}

View File

@ -129,6 +129,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<TextAdvanceIpc>();
serviceCollection.AddSingleton<NotificationMasterIpc>();
serviceCollection.AddSingleton<AutomatonIpc>();
serviceCollection.AddSingleton<AutoDutyIpc>();
}
private static void AddTaskFactories(ServiceCollection serviceCollection)
@ -178,7 +179,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin
.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>();

View File

@ -1,7 +1,11 @@
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;
@ -12,19 +16,28 @@ using ImGuiNET;
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;
@ -34,17 +47,38 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
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 })
@ -54,10 +88,41 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
.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");
@ -65,6 +130,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
return;
DrawGeneralTab();
DrawDutiesTab();
DrawNotificationsTab();
DrawAdvancedTab();
}
@ -138,6 +204,175 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
}
}
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");
@ -231,4 +466,21 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
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);
}