using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Questionable.Controller; using Questionable.Controller.Conditions; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows.QuestComponents; using Questionable.Windows.Utils; namespace Questionable.Windows.ConfigComponents; internal sealed class StopConditionComponent : ConfigComponent { private static readonly string[] ConditionModeNames = new string[2] { "Pause", "Stop" }; private static readonly string[] ConditionTypeNames = new string[7] { "Quest Complete", "Quest Accept", "Level", "Global Sequence", "Inventory Full", "Before Duty", "Gil Threshold" }; private readonly IDalamudPluginInterface _pluginInterface; private readonly QuestSelector _questSelector; private readonly QuestRegistry _questRegistry; private readonly QuestData _questData; private readonly QuestFunctions _questFunctions; private readonly QuestTooltipComponent _questTooltipComponent; private readonly UiUtils _uiUtils; private readonly IObjectTable _objectTable; private readonly QuestController _questController; private readonly ILogger _logger; private int _addConditionType; private int _addLevel = 50; private int _addSequence = 1; private int _addGilThreshold = 1000; private string _questAcceptSearchText = string.Empty; private List _acceptedQuests = new List(); private long _acceptedQuestsRefreshAtMs; public StopConditionComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestData questData, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IObjectTable objectTable, QuestController questController, Configuration configuration, ILogger logger) : base(pluginInterface, configuration) { StopConditionComponent stopConditionComponent = this; _pluginInterface = pluginInterface; _questSelector = questSelector; _questRegistry = questRegistry; _questData = questData; _questFunctions = questFunctions; _questTooltipComponent = questTooltipComponent; _uiUtils = uiUtils; _logger = logger; _objectTable = objectTable; _questController = questController; _questSelector.SuggestionPredicate = (Quest quest) => !stopConditionComponent.HasQuestCondition(quest.Id, EStopConditionType.QuestComplete); _questSelector.DefaultPredicate = (Quest quest) => quest.Info.IsMainScenarioQuest && questFunctions.IsQuestAccepted(quest.Id); _questSelector.QuestSelected = delegate(Quest quest) { stopConditionComponent.Configuration.Stop.Conditions.Add(new QuestCompleteCondition { QuestId = quest.Id, Mode = EStopConditionMode.Stop }); stopConditionComponent.Save(); }; } public override void DrawTab() { bool value = base.Configuration.Stop.Enabled; UiThemeUtils.SectionHeader("Stop Conditions"); var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard(); if (UiThemeUtils.WrappedCheckbox("Enable stop conditions", ref value, "Pause: Stops automation when condition is met, but allows resuming past it.\nStop: True stop, blocks automation from starting/resuming if condition is already met.")) { base.Configuration.Stop.Enabled = value; Save(); } using (ImRaii.Disabled(!value)) { DrawConditionsList(); } UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList); UiThemeUtils.SectionSpacing(); UiThemeUtils.SectionHeader("Add Condition"); var (contentStartPos2, availableWidth2, drawList2) = UiThemeUtils.BeginCard(); using (ImRaii.Disabled(!value)) { DrawAddCondition(); } UiThemeUtils.EndCard(contentStartPos2, availableWidth2, drawList2); UiThemeUtils.SectionSpacing(); UiThemeUtils.SectionHeader("After Stopping"); var (contentStartPos3, availableWidth3, drawList3) = UiThemeUtils.BeginCard(); using (ImRaii.Disabled(!value)) { string buf = base.Configuration.Stop.CommandAfterStop; ImGui.SetNextItemWidth(MathF.Min(260f, ImGui.GetContentRegionAvail().X)); if (ImGui.InputText("Run command after stop", ref buf, 200)) { base.Configuration.Stop.CommandAfterStop = buf; Save(); } ImGui.SameLine(); UiThemeUtils.InfoIcon("Runs this chat command when a stop condition ends the run - not on a manual stop."); } UiThemeUtils.EndCard(contentStartPos3, availableWidth3, drawList3); DrawSessionConditions(); } private void DrawConditionsList() { List conditions = base.Configuration.Stop.Conditions; if (conditions.Count == 0) { ImGui.TextDisabled("No conditions configured."); return; } if (UiThemeUtils.DestructiveButton(FontAwesomeIcon.Trash, "Clear all")) { conditions.Clear(); Save(); } int? indexToRemove = null; for (int i = 0; i < conditions.Count; i++) { using (ImRaii.PushId(i)) { StopCondition condition = conditions[i]; DrawConditionRow(condition, i, ref indexToRemove); } } if (indexToRemove.HasValue) { int valueOrDefault = indexToRemove.GetValueOrDefault(); conditions.RemoveAt(valueOrDefault); Save(); } } private void DrawConditionRow(StopCondition condition, int index, ref int? indexToRemove) { ImGui.AlignTextToFramePadding(); if (!(condition is QuestCompleteCondition condition2)) { if (!(condition is QuestAcceptCondition condition3)) { if (!(condition is LevelCondition condition4)) { if (!(condition is GlobalSequenceCondition condition5)) { if (!(condition is InventoryFullCondition)) { if (!(condition is BeforeDutyCondition)) { if (condition is GilThresholdCondition gilThresholdCondition) { ImU8String text = new ImU8String(10, 1); text.AppendLiteral("Gil below "); text.AppendFormatted(gilThresholdCondition.MinGil, "N0"); ImGui.Text(text); } else { ImGui.Text(condition.GetDescription()); } } else { ImGui.Text("Before Duty"); } } else { ImGui.Text("Inventory Full"); } } else { DrawGlobalSequenceRow(condition5); } } else { DrawLevelRow(condition4); } } else { DrawQuestAcceptRow(condition3); } } else { DrawQuestCompleteRow(condition2); } ImGui.SameLine(); int currentItem = ((condition.Mode != EStopConditionMode.Pause) ? 1 : 0); ImGui.SetNextItemWidth(70f); if (UiThemeUtils.ThemedCombo("##Mode", ref currentItem, ConditionModeNames, ConditionModeNames.Length)) { condition.Mode = ((currentItem == 0) ? EStopConditionMode.Pause : EStopConditionMode.Stop); Save(); } ImGui.SameLine(); if (UiThemeUtils.IconButton(FontAwesomeIcon.Times, ImGui.GetFrameHeight())) { indexToRemove = index; } } private void DrawQuestCompleteRow(QuestCompleteCondition condition) { string text = ResolveQuestName(condition.QuestId); (Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(condition.QuestId); using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString()); } ImGui.SameLine(); float x = ImGui.GetStyle().ItemSpacing.X; float frameHeight = ImGui.GetFrameHeight(); float x2; using (ImRaii.PushFont(UiBuilder.IconFont)) { x2 = ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; } float reservedRightWidth = x * 6f + frameHeight * 2f + ImGui.CalcTextSize("Seq:").X + x2 + 85f + 70f; UiThemeUtils.RowLabel(text, reservedRightWidth); if (ImGui.IsItemHovered() && _questRegistry.TryGetQuest(condition.QuestId, out Quest quest)) { _questTooltipComponent.Draw(quest.Info); } ImGui.SameLine(); bool value = condition.Sequence.HasValue; if (UiThemeUtils.WrappedCheckbox("Seq:", ref value, "Stop at specific sequence (unchecked = stop on completion)")) { condition.Sequence = (value ? new int?(1) : ((int?)null)); Save(); } using (ImRaii.Disabled(!value)) { ImGui.SameLine(); ImGui.SetNextItemWidth(85f); int data = condition.Sequence ?? 1; if (ImGui.InputInt("##Seq", ref data, 1, 1) && value) { condition.Sequence = Math.Max(0, Math.Min(255, data)); Save(); } } } private void DrawQuestAcceptRow(QuestAcceptCondition condition) { string text = ResolveQuestName(condition.QuestId); (Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(condition.QuestId); using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString()); } ImGui.SameLine(); float num = ImGui.GetStyle().ItemSpacing.X * 2f + 70f + ImGui.GetFrameHeight() + ImGui.CalcTextSize(" (on accept)").X; float maxWidth = ImGui.GetContentRegionAvail().X - num; ImGui.TextUnformatted(UiThemeUtils.TruncateToWidth(text, maxWidth) + " (on accept)"); if (ImGui.IsItemHovered() && _questRegistry.TryGetQuest(condition.QuestId, out Quest quest)) { _questTooltipComponent.Draw(quest.Info); } } private void DrawLevelRow(LevelCondition condition) { ImGui.Text("Level"); ImGui.SameLine(); ImGui.SetNextItemWidth(100f); int data = condition.TargetLevel; if (ImGui.InputInt("##Level", ref data, 1, 5)) { condition.TargetLevel = Math.Max(1, Math.Min(100, data)); Save(); } int num = _objectTable.LocalPlayer?.Level ?? 0; if (num > 0) { ImGui.SameLine(); ImU8String text = new ImU8String(11, 1); text.AppendLiteral("(Current: "); text.AppendFormatted(num); text.AppendLiteral(")"); ImGui.TextDisabled(text); } } private void DrawGlobalSequenceRow(GlobalSequenceCondition condition) { ImGui.Text("Global Sequence"); ImGui.SameLine(); ImGui.SetNextItemWidth(100f); int data = condition.TargetSequence; if (ImGui.InputInt("##Seq", ref data, 1, 1)) { condition.TargetSequence = Math.Max(0, Math.Min(255, data)); Save(); } QuestController.QuestProgress currentQuest = _questController.CurrentQuest; if (currentQuest != null) { ImGui.SameLine(); ImU8String text = new ImU8String(11, 1); text.AppendLiteral("(Current: "); text.AppendFormatted(currentQuest.Sequence); text.AppendLiteral(")"); ImGui.TextDisabled(text); } } private void DrawAddCondition() { ImGui.SetNextItemWidth(MathF.Min(260f, ImGui.GetContentRegionAvail().X)); UiThemeUtils.ThemedCombo("##AddType", ref _addConditionType, ConditionTypeNames, ConditionTypeNames.Length); ImGui.SameLine(); switch (_addConditionType) { case 0: DrawAddQuestComplete(); break; case 1: DrawAddQuestAccept(); break; case 2: DrawAddLevel(); break; case 3: DrawAddGlobalSequence(); break; case 4: DrawAddSimpleCondition(); break; case 5: DrawAddSimpleCondition(); break; case 6: DrawAddGilThreshold(); break; } } private void DrawAddQuestComplete() { ImGui.NewLine(); _questSelector.DrawSelection(MathF.Min(260f, ImGui.GetContentRegionAvail().X)); } private void DrawAddQuestAccept() { ImGui.NewLine(); long tickCount = Environment.TickCount64; if (tickCount >= _acceptedQuestsRefreshAtMs) { _acceptedQuests = GetCurrentlyAcceptedQuests(); _acceptedQuestsRefreshAtMs = tickCount + 1000; } List list = _acceptedQuests.Where((Quest x) => !HasQuestCondition(x.Id, EStopConditionType.QuestAccept)).ToList(); if (list.Count == 0) { ImGui.TextDisabled((_acceptedQuests.Count == 0) ? "No quests currently accepted" : "All accepted quests already added"); return; } string[] items = list.Select((Quest x) => x.Info.Name).ToArray(); int currentItem = -1; if (UiThemeUtils.SearchableCombo("##QuestAcceptSelection", ref currentItem, items, ref _questAcceptSearchText, MathF.Min(260f, ImGui.GetContentRegionAvail().X), "Select quest to add...") && currentItem >= 0) { base.Configuration.Stop.Conditions.Add(new QuestAcceptCondition { QuestId = list[currentItem].Id, Mode = EStopConditionMode.Stop }); Save(); } } private void DrawAddLevel() { if (base.Configuration.Stop.Conditions.OfType().Any()) { ImGui.TextDisabled("Already configured"); return; } ImGui.SetNextItemWidth(100f); ImGui.InputInt("##AddLevel", ref _addLevel, 1, 5); _addLevel = Math.Max(1, Math.Min(100, _addLevel)); ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Plus, "Add")) { base.Configuration.Stop.Conditions.Add(new LevelCondition { TargetLevel = _addLevel, Mode = EStopConditionMode.Stop }); Save(); } } private void DrawAddGlobalSequence() { if (base.Configuration.Stop.Conditions.OfType().Any()) { ImGui.TextDisabled("Already configured"); return; } ImGui.SetNextItemWidth(100f); ImGui.InputInt("##AddSeq", ref _addSequence, 1, 1); _addSequence = Math.Max(0, Math.Min(255, _addSequence)); ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Plus, "Add")) { base.Configuration.Stop.Conditions.Add(new GlobalSequenceCondition { TargetSequence = _addSequence, Mode = EStopConditionMode.Stop }); Save(); } } private void DrawAddGilThreshold() { if (base.Configuration.Stop.Conditions.OfType().Any()) { ImGui.TextDisabled("Already configured"); return; } ImGui.SetNextItemWidth(100f); ImGui.InputInt("##AddGil", ref _addGilThreshold, 100, 1000); _addGilThreshold = Math.Max(1, Math.Min(999999999, _addGilThreshold)); ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Plus, "Add")) { base.Configuration.Stop.Conditions.Add(new GilThresholdCondition { MinGil = _addGilThreshold, Mode = EStopConditionMode.Stop }); Save(); } } private void DrawAddSimpleCondition() where T : StopCondition, new() { if (base.Configuration.Stop.Conditions.OfType().Any()) { ImGui.TextDisabled("Already configured"); } else if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Plus, "Add")) { base.Configuration.Stop.Conditions.Add(new T { Mode = EStopConditionMode.Stop }); Save(); } } private void DrawSessionConditions() { List sessionConditions = _questController.SessionConditions; if (sessionConditions.Count == 0) { return; } UiThemeUtils.SectionSpacing(); UiThemeUtils.SectionHeader("This Session"); (Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple = UiThemeUtils.BeginCard(); Vector2 item = tuple.ContentStartPos; float item2 = tuple.AvailableWidth; ImDrawListPtr item3 = tuple.DrawList; for (int i = 0; i < sessionConditions.Count; i++) { ImU8String id = new ImU8String(7, 1); id.AppendLiteral("Session"); id.AppendFormatted(i); using (ImRaii.PushId(id)) { StopCondition stopCondition = sessionConditions[i]; ImGui.BulletText(stopCondition.GetDescription()); ImGui.SameLine(); ImU8String text = new ImU8String(2, 1); text.AppendLiteral("["); text.AppendFormatted(stopCondition.Mode); text.AppendLiteral("]"); ImGui.TextDisabled(text); } } UiThemeUtils.EndCard(item, item2, item3); } private string ResolveQuestName(ElementId questId) { if (_questData.TryGetQuestInfo(questId, out IQuestInfo questInfo)) { return questInfo.Name; } return questId.ToString(); } private bool HasQuestCondition(ElementId questId, EStopConditionType type) { foreach (StopCondition condition in base.Configuration.Stop.Conditions) { if (condition.Type == type) { if (condition is QuestCompleteCondition questCompleteCondition && questCompleteCondition.QuestId.Equals(questId)) { return true; } if (condition is QuestAcceptCondition questAcceptCondition && questAcceptCondition.QuestId.Equals(questId)) { return true; } } } return false; } private List GetCurrentlyAcceptedQuests() { List list = new List(); try { foreach (Quest allQuest in _questRegistry.AllQuests) { if (_questFunctions.IsQuestAccepted(allQuest.Id)) { list.Add(allQuest); } } list.Sort((Quest a, Quest b) => string.Compare(a.Info.Name, b.Info.Name, StringComparison.OrdinalIgnoreCase)); } catch (Exception exception) { _logger.LogDebug(exception, "Failed to enumerate accepted quests"); list.Clear(); } return list; } }