using System; using System.Collections.Generic; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Group; using LLib.Gear; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Interactions; internal static class Duty { internal sealed class Factory(AutoDutyIpc autoDutyIpc, Configuration configuration) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.Duty) { yield break; } ArgumentNullException.ThrowIfNull(step.DutyOptions, "step.DutyOptions"); if (autoDutyIpc.IsConfiguredToRunContent(step.DutyOptions)) { AutoDutyIpc.DutyMode dutyMode = GetDutyMode(step.DutyOptions.ContentFinderConditionId, step.DutyOptions.DutyMode); if (dutyMode == AutoDutyIpc.DutyMode.UnsyncRegular && (step.DutyOptions.DutyMode == EDutyMode.UnsyncParty || (!step.DutyOptions.DutyMode.HasValue && configuration.Duties.DutyModeOverrides.TryGetValue(step.DutyOptions.ContentFinderConditionId, out var value) && value == EDutyMode.UnsyncParty) || (!step.DutyOptions.DutyMode.HasValue && !configuration.Duties.DutyModeOverrides.ContainsKey(step.DutyOptions.ContentFinderConditionId) && configuration.Duties.DefaultDutyMode == EDutyMode.UnsyncParty))) { yield return new WaitForPartyTask(); } yield return new StartAutoDutyTask(step.DutyOptions.ContentFinderConditionId, dutyMode); yield return new WaitAutoDutyTask(step.DutyOptions.ContentFinderConditionId); if (!QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags)) { yield return new WaitAtEnd.WaitNextStepOrSequence(); } } else if (!step.DutyOptions.LowPriority) { yield return new OpenDutyFinderTask(step.DutyOptions.ContentFinderConditionId); } } private AutoDutyIpc.DutyMode GetDutyMode(uint cfcId, EDutyMode? stepDutyMode) { if (stepDutyMode.HasValue) { return ConvertToAutoDutyMode(stepDutyMode.Value); } if (configuration.Duties.DutyModeOverrides.TryGetValue(cfcId, out var value)) { return ConvertToAutoDutyMode(value); } return ConvertToAutoDutyMode(configuration.Duties.DefaultDutyMode); } private static AutoDutyIpc.DutyMode ConvertToAutoDutyMode(EDutyMode mode) { return mode switch { EDutyMode.Support => AutoDutyIpc.DutyMode.Support, EDutyMode.UnsyncSolo => AutoDutyIpc.DutyMode.UnsyncRegular, EDutyMode.UnsyncParty => AutoDutyIpc.DutyMode.UnsyncRegular, _ => AutoDutyIpc.DutyMode.Support, }; } } internal sealed record WaitForPartyTask : ITask { public override string ToString() { return "WaitForParty"; } } internal sealed class WaitForPartyExecutor(IChatGui chatGui, ILogger logger) : TaskExecutor() { private DateTime _lastWarningTime = DateTime.MinValue; protected override bool Start() { logger.LogInformation("Waiting for party members before starting duty..."); return true; } public unsafe override ETaskResult Update() { GroupManager* ptr = GroupManager.Instance(); if (ptr == null) { return ETaskResult.StillRunning; } byte memberCount = ptr->MainGroup.MemberCount; bool isAlliance = ptr->MainGroup.IsAlliance; if (memberCount > 1 || isAlliance) { logger.LogInformation("Party detected with {MemberCount} members, proceeding with duty", memberCount); return ETaskResult.TaskComplete; } if (DateTime.Now - _lastWarningTime > TimeSpan.FromSeconds(10L)) { chatGui.Print("[Questionable] Waiting for party members before starting duty (Unsync Party mode)...", "Questionable", 576); _lastWarningTime = DateTime.Now; } return ETaskResult.StillRunning; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record StartAutoDutyTask(uint ContentFinderConditionId, AutoDutyIpc.DutyMode DutyMode) : ITask { public override string ToString() { return $"StartAutoDuty({ContentFinderConditionId}, {DutyMode})"; } } internal sealed class StartAutoDutyExecutor(GearStatsCalculator gearStatsCalculator, AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState, IChatGui chatGui, SendNotification.Executor sendNotificationExecutor) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { protected unsafe override bool Start() { if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { throw new TaskException("Failed to get territory ID for content finder condition"); } InventoryManager* intPtr = InventoryManager.Instance(); if (intPtr == null) { throw new TaskException("Inventory unavailable"); } InventoryContainer* inventoryContainer = intPtr->GetInventoryContainer(InventoryType.EquippedItems); if (inventoryContainer == null) { throw new TaskException("Equipped items unavailable"); } short num = gearStatsCalculator.CalculateAverageItemLevel(inventoryContainer); if (contentFinderConditionData.RequiredItemLevel > num) { string text = $"Could not use AutoDuty to queue for {contentFinderConditionData.Name}, required item level: {contentFinderConditionData.RequiredItemLevel}, current item level: {num}."; if (!sendNotificationExecutor.Start(new SendNotification.Task(EInteractionType.Duty, text))) { chatGui.PrintError(text, "Questionable", 576); } return false; } autoDutyIpc.StartInstance(base.Task.ContentFinderConditionId, base.Task.DutyMode); return true; } public override ETaskResult Update() { if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { throw new TaskException("Failed to get territory ID for content finder condition"); } if (clientState.TerritoryType != contentFinderConditionData.TerritoryId) { return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask { public override string ToString() { return $"Wait(AutoDuty, left instance {ContentFinderConditionId})"; } } internal sealed class WaitAutoDutyExecutor(AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { protected override bool Start() { return true; } public override ETaskResult Update() { if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { throw new TaskException("Failed to get territory ID for content finder condition"); } if (clientState.TerritoryType == contentFinderConditionData.TerritoryId || !autoDutyIpc.IsStopped()) { return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask { public override string ToString() { return $"OpenDutyFinder({ContentFinderConditionId})"; } } internal sealed class OpenDutyFinderExecutor(GameFunctions gameFunctions, ICondition condition) : TaskExecutor() { protected override bool Start() { if (condition[ConditionFlag.InDutyQueue]) { return false; } gameFunctions.OpenDutyFinder(base.Task.ContentFinderConditionId); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record StartLevelingModeTask(int RequiredLevel, string? QuestName) : ITask { public override string ToString() { return $"StartLevelingMode(target: Lv{RequiredLevel} for '{QuestName}')"; } } internal sealed class StartLevelingModeExecutor(AutoDutyIpc autoDutyIpc, ICondition condition, ILogger logger) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private bool _started; private DateTime _startTime; private DateTime _lastRetryTime = DateTime.MinValue; protected override bool Start() { logger.LogInformation("Starting AutoDuty Leveling mode to reach level {RequiredLevel} for quest '{QuestName}'", base.Task.RequiredLevel, base.Task.QuestName); _started = autoDutyIpc.StartLevelingMode(); _startTime = DateTime.Now; return _started; } public override ETaskResult Update() { bool flag = condition[ConditionFlag.BoundByDuty]; bool flag2 = condition[ConditionFlag.InDutyQueue]; if (flag || flag2) { logger.LogInformation("AutoDuty started successfully (inDuty={InDuty}, inQueue={InQueue})", flag, flag2); return ETaskResult.TaskComplete; } if (!autoDutyIpc.IsStopped()) { return ETaskResult.StillRunning; } if (DateTime.Now - _lastRetryTime > TimeSpan.FromSeconds(5L)) { logger.LogWarning("AutoDuty stopped before entering duty, retrying..."); _started = autoDutyIpc.StartLevelingMode(); _lastRetryTime = DateTime.Now; } if (DateTime.Now - _startTime > TimeSpan.FromSeconds(60L)) { logger.LogError("AutoDuty failed to start after 60 seconds"); return ETaskResult.TaskComplete; } return ETaskResult.StillRunning; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record WaitLevelingModeTask(int RequiredLevel) : ITask { public override string ToString() { return $"WaitLevelingMode(until Lv{RequiredLevel})"; } } internal sealed class WaitLevelingModeExecutor(AutoDutyIpc autoDutyIpc, IObjectTable objectTable, ICondition condition, IChatGui chatGui, ILogger logger) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private bool _wasInDuty; private DateTime _lastStatusMessage = DateTime.MinValue; private DateTime _idleStartTime = DateTime.MinValue; protected override bool Start() { _wasInDuty = false; _idleStartTime = DateTime.MinValue; return true; } public override ETaskResult Update() { bool flag = condition[ConditionFlag.BoundByDuty]; bool flag2 = condition[ConditionFlag.InDutyQueue]; if (flag && !_wasInDuty) { logger.LogInformation("Entered duty for leveling"); _wasInDuty = true; _idleStartTime = DateTime.MinValue; } if (flag || flag2) { _idleStartTime = DateTime.MinValue; } byte b = objectTable.LocalPlayer?.Level ?? 0; if (b >= base.Task.RequiredLevel) { logger.LogInformation("Reached required level {RequiredLevel} (current: {CurrentLevel})", base.Task.RequiredLevel, b); chatGui.Print($"Reached level {b}, can now continue MSQ.", "Questionable", 576); autoDutyIpc.DisableLevelingMode(); return ETaskResult.TaskComplete; } if (autoDutyIpc.IsStopped() && !flag && !flag2) { if (_idleStartTime == DateTime.MinValue) { _idleStartTime = DateTime.Now; } if (_wasInDuty && DateTime.Now - _idleStartTime > TimeSpan.FromSeconds(3L)) { _ = base.Task.RequiredLevel; _lastStatusMessage = DateTime.Now; logger.LogInformation("Starting another leveling run (current: {CurrentLevel}, need: {RequiredLevel})", b, base.Task.RequiredLevel); autoDutyIpc.StartLevelingMode(); _wasInDuty = false; _idleStartTime = DateTime.MinValue; } } return ETaskResult.StillRunning; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } }