qstbak/Questionable/Questionable.Windows.ConfigComponents/StopConditionComponent.cs
2026-08-19 13:19:57 +10:00

663 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
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 const string ClipboardPrefix = "qst:stop:";
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<StopConditionComponent> _logger;
private int _addConditionType;
private int _addLevel = 50;
private int _addSequence = 1;
private int _addGilThreshold = 1000;
private string _questAcceptSearchText = string.Empty;
private List<Quest> _acceptedQuests = new List<Quest>();
private long _acceptedQuestsRefreshAtMs;
private StopCondition? _draggedCondition;
public StopConditionComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestData questData, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IObjectTable objectTable, QuestController questController, Configuration configuration, ILogger<StopConditionComponent> 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<StopCondition> conditions = base.Configuration.Stop.Conditions;
DrawClipboardButtons(conditions);
if (conditions.Count == 0)
{
ImGui.TextDisabled("No conditions configured.");
return;
}
ImGui.SameLine();
if (UiThemeUtils.DestructiveButton(FontAwesomeIcon.Trash, "Clear all"))
{
conditions.Clear();
Save();
}
int? indexToRemove = null;
StopCondition stopCondition = null;
int index = 0;
float x = ImGui.GetContentRegionAvail().X;
List<(Vector2, Vector2)> list = new List<(Vector2, Vector2)>();
for (int i = 0; i < conditions.Count; i++)
{
Vector2 item = ImGui.GetCursorScreenPos() + new Vector2(0f, (0f - ImGui.GetStyle().ItemSpacing.Y) / 2f);
using (ImRaii.PushId(i))
{
StopCondition stopCondition2 = conditions[i];
if (conditions.Count > 1)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.Bars);
if (_draggedCondition == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedCondition = stopCondition2;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Drag to reorder");
}
ImGui.SameLine();
}
DrawConditionRow(stopCondition2, i, ref indexToRemove);
Vector2 item2 = new Vector2(item.X + x, ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2f);
list.Add((item, item2));
}
}
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedCondition = null;
}
else if (_draggedCondition != null)
{
int num = conditions.IndexOf(_draggedCondition);
if (num >= 0)
{
var (pMin, pMax) = list[num];
ImGui.GetWindowDrawList().AddRect(pMin, pMax, ImGui.ColorConvertFloat4ToU32(UiThemeUtils.CardBorderHighColor), 3f, ImDrawFlags.RoundCornersAll);
int num2 = list.FindIndex(((Vector2 TopLeft, Vector2 BottomRight) tuple2) => ImGui.IsMouseHoveringRect(tuple2.TopLeft, tuple2.BottomRight, clip: true));
if (num2 >= 0 && num != num2)
{
stopCondition = _draggedCondition;
index = num2;
}
}
}
if (indexToRemove.HasValue)
{
int valueOrDefault = indexToRemove.GetValueOrDefault();
conditions.RemoveAt(valueOrDefault);
_draggedCondition = null;
Save();
}
else if (stopCondition != null)
{
conditions.Remove(stopCondition);
conditions.Insert(index, stopCondition);
Save();
}
}
private void DrawClipboardButtons(List<StopCondition> conditions)
{
using (ImRaii.Disabled(conditions.Count == 0))
{
if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Copy, "Copy"))
{
string s = JsonConvert.SerializeObject(conditions);
ImGui.SetClipboardText("qst:stop:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(s)));
}
}
ImGui.SameLine();
string text = ThrottledClipboard.GetText();
using (ImRaii.Disabled(!text.StartsWith("qst:stop:", StringComparison.InvariantCulture)))
{
if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Paste, "Paste"))
{
ImportConditions(text);
}
}
}
private void ImportConditions(string clipboardText)
{
try
{
string s = clipboardText.Substring("qst:stop:".Length);
List<StopCondition> list = JsonConvert.DeserializeObject<List<StopCondition>>(Encoding.UTF8.GetString(Convert.FromBase64String(s))) ?? new List<StopCondition>();
list.RemoveAll((StopCondition x) => x?.IsSession ?? true);
if (list.Count != 0)
{
base.Configuration.Stop.Conditions.Clear();
base.Configuration.Stop.Conditions.AddRange(list);
Save();
}
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Failed to import stop conditions from clipboard");
}
}
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<InventoryFullCondition>();
break;
case 5:
DrawAddSimpleCondition<BeforeDutyCondition>();
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<Quest> 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<LevelCondition>().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<GlobalSequenceCondition>().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<GilThresholdCondition>().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<T>() where T : StopCondition, new()
{
if (base.Configuration.Stop.Conditions.OfType<T>().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<StopCondition> 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<Quest> GetCurrentlyAcceptedQuests()
{
List<Quest> list = new List<Quest>();
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;
}
}