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

444 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using LLib.GameData;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Questionable.Data;
using Questionable.Model;
namespace Questionable.Windows.ConfigComponents;
internal sealed class GeneralConfigComponent : ConfigComponent
{
private static readonly List<(EClassJob ClassJob, string Name)> DefaultClassJobs;
private readonly IDataManager _dataManager;
private static readonly (Configuration.ECombatModule Module, string Name, string[] InternalNames)[] CombatModuleOptions;
private static readonly (EItemRewardType Type, string Label)[] RewardTypeOptions;
private readonly string[] _grandCompanyNames = new string[4] { "Auto (starting city)", "Maelstrom", "Twin Adder", "Immortal Flames" };
private readonly EClassJob[] _classJobIds;
private readonly string[] _classJobNames;
private string _classJobSearchText = string.Empty;
private string _cofferGearsetSearchText = string.Empty;
public System.Action? OpenDependenciesTab { get; set; }
public GeneralConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager, ClassJobUtils classJobUtils)
: base(pluginInterface, configuration)
{
_dataManager = dataManager;
List<EClassJob> sortedClassJobs = classJobUtils.SortedClassJobs.Select(((EClassJob ClassJob, int Category) x) => x.ClassJob).ToList();
List<EClassJob> list = (from x in Enum.GetValues<EClassJob>()
where x != EClassJob.Adventurer
where !x.IsCrafter() && !x.IsGatherer()
where !x.IsClass()
orderby sortedClassJobs.IndexOf(x)
select x).ToList();
_classJobIds = DefaultClassJobs.Select<(EClassJob, string), EClassJob>(((EClassJob ClassJob, string Name) x) => x.ClassJob).Concat(list).ToArray();
_classJobNames = DefaultClassJobs.Select<(EClassJob, string), string>(((EClassJob ClassJob, string Name) x) => x.Name).Concat(list.Select((EClassJob x) => x.ToFriendlyString())).ToArray();
}
public override void DrawTab()
{
DrawCombatAndClass();
UiThemeUtils.SectionSpacing();
DrawBehaviour();
UiThemeUtils.SectionSpacing();
DrawQuesting();
UiThemeUtils.SectionSpacing();
DrawEquipment();
UiThemeUtils.SectionSpacing();
DrawRewardRedemption();
}
public void DrawGrandCompanySelector(float comboWidth)
{
int currentItem = (int)base.Configuration.General.GrandCompany;
ImGui.SetNextItemWidth(comboWidth);
if (UiThemeUtils.ThemedCombo("Preferred Grand Company", ref currentItem, _grandCompanyNames, _grandCompanyNames.Length))
{
base.Configuration.General.GrandCompany = (FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany)currentItem;
Save();
}
}
private void DrawCombatAndClass()
{
UiThemeUtils.SectionHeader("Combat & Class");
(Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple = UiThemeUtils.BeginCard();
Vector2 item = tuple.ContentStartPos;
float item2 = tuple.AvailableWidth;
ImDrawListPtr item3 = tuple.DrawList;
float num = MathF.Min(ImGui.GetContentRegionAvail().X * 0.6f, 260f);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Combat module:");
ImGui.SameLine();
string text = CombatModuleOptions.FirstOrDefault(((Configuration.ECombatModule Module, string Name, string[] InternalNames) x) => x.Module == base.Configuration.General.CombatModule).Name ?? "None";
ImGui.TextColored(in UiThemeUtils.AccentColor, text);
ImGui.SameLine();
if (UiThemeUtils.PillButton("Configure in Dependencies", selected: false))
{
OpenDependenciesTab?.Invoke();
}
DrawGrandCompanySelector(num);
int currentItem = Array.IndexOf(_classJobIds, base.Configuration.General.CombatJob);
if (currentItem == -1)
{
base.Configuration.General.CombatJob = EClassJob.Adventurer;
Save();
currentItem = 0;
}
if (UiThemeUtils.SearchableCombo("Preferred Combat Job", ref currentItem, _classJobNames, ref _classJobSearchText, num))
{
base.Configuration.General.CombatJob = _classJobIds[currentItem];
Save();
}
UiThemeUtils.EndCard(item, item2, item3);
}
private void DrawBehaviour()
{
UiThemeUtils.SectionHeader("Behaviour");
var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard();
UiThemeUtils.SubSectionHeader("Quest window");
bool value = base.Configuration.General.HideInAllInstances;
if (UiThemeUtils.WrappedCheckbox("Hide quest window in all instanced duties", ref value))
{
base.Configuration.General.HideInAllInstances = value;
Save();
}
using (ImRaii.Disabled(!value))
{
using (ImRaii.PushIndent())
{
bool value2 = base.Configuration.Advanced.ShowWindowInInstances;
if (UiThemeUtils.WrappedCheckbox("Show quest window in instances anyway", ref value2, "When enabled, the quest window will always be shown in instanced duties, ignoring the 'Hide quest window in all instanced duties' setting above."))
{
base.Configuration.Advanced.ShowWindowInInstances = value2;
Save();
}
}
}
bool value3 = base.Configuration.General.ShowChangelogOnUpdate;
if (UiThemeUtils.WrappedCheckbox("Show changelog window when plugin updates", ref value3))
{
base.Configuration.General.ShowChangelogOnUpdate = value3;
Save();
}
UiThemeUtils.SubSectionHeader("Manual control");
bool value4 = base.Configuration.General.UseEscToCancelQuesting;
if (UiThemeUtils.WrappedCheckbox("Double tap ESC to cancel questing/movement", ref value4))
{
base.Configuration.General.UseEscToCancelQuesting = value4;
Save();
}
bool value5 = base.Configuration.General.StopOnPlayerInput;
if (UiThemeUtils.WrappedCheckbox("Stop automation when manually moving character", ref value5))
{
base.Configuration.General.StopOnPlayerInput = value5;
Save();
}
UiThemeUtils.SubSectionHeader("Seasonal events");
InlineArray2<UiThemeUtils.GridCheckbox> buffer = default(InlineArray2<UiThemeUtils.GridCheckbox>);
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<InlineArray2<UiThemeUtils.GridCheckbox>, UiThemeUtils.GridCheckbox>(ref buffer, 0) = new UiThemeUtils.GridCheckbox("Show details for incomplete seasonal events", base.Configuration.General.ShowIncompleteSeasonalEvents);
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<InlineArray2<UiThemeUtils.GridCheckbox>, UiThemeUtils.GridCheckbox>(ref buffer, 1) = new UiThemeUtils.GridCheckbox("Hide Seasonal Events from Journal Progress", base.Configuration.General.HideSeasonalEventsFromJournalProgress);
ReadOnlySpan<UiThemeUtils.GridCheckbox> items = global::_003CPrivateImplementationDetails_003E.InlineArrayAsReadOnlySpan<InlineArray2<UiThemeUtils.GridCheckbox>, UiThemeUtils.GridCheckbox>(in buffer, 2);
switch (UiThemeUtils.CheckboxGrid("seasonal", items))
{
case 0:
base.Configuration.General.ShowIncompleteSeasonalEvents = !base.Configuration.General.ShowIncompleteSeasonalEvents;
Save();
break;
case 1:
base.Configuration.General.HideSeasonalEventsFromJournalProgress = !base.Configuration.General.HideSeasonalEventsFromJournalProgress;
Save();
break;
}
UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList);
}
private void DrawQuesting()
{
UiThemeUtils.SectionHeader("Questing");
(Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple = UiThemeUtils.BeginCard();
Vector2 item = tuple.ContentStartPos;
float item2 = tuple.AvailableWidth;
ImDrawListPtr item3 = tuple.DrawList;
bool value = base.Configuration.General.AutoSolveQte;
if (UiThemeUtils.WrappedCheckbox("Automatically solve Quick Time Events (QTEs)", ref value, "Automatically mashes the button during Active Time Maneuver (ATM) prompts that appear in certain duties and quest battles."))
{
base.Configuration.General.AutoSolveQte = value;
Save();
}
bool value2 = base.Configuration.General.AutoSnipe;
if (UiThemeUtils.WrappedCheckbox("Automatically complete snipe quests", ref value2, "Automatically completes sniping minigames introduced in Stormblood. When enabled, snipe targets are instantly hit without manual aiming."))
{
base.Configuration.General.AutoSnipe = value2;
Save();
}
bool value3 = base.Configuration.General.CinemaMode;
if (UiThemeUtils.WrappedCheckbox("Cinema Mode (watch cutscenes)", ref value3, "When enabled, cutscenes will NOT be automatically skipped. This allows you to experience the story while Questionable handles navigation, combat, and other gameplay automation.\n\nRecommended for first-time story playthroughs."))
{
base.Configuration.General.CinemaMode = value3;
Save();
}
bool value4 = base.Configuration.General.ConfigureTextAdvance;
if (UiThemeUtils.WrappedCheckbox("Automatically configure TextAdvance with the recommended settings", ref value4))
{
base.Configuration.General.ConfigureTextAdvance = value4;
Save();
}
bool value5 = base.Configuration.General.DeathRecoveryEnabled;
if (UiThemeUtils.WrappedCheckbox("Automatically recover and resume after dying", ref value5, "When enabled, dying outside of a duty will not stop automation. After respawning, Questionable will automatically route back to continue the current quest step."))
{
base.Configuration.General.DeathRecoveryEnabled = value5;
Save();
}
using (ImRaii.Disabled(!value5))
{
using (ImRaii.PushIndent())
{
int value6 = base.Configuration.General.MaxDeathsBeforeStop;
if (UiThemeUtils.AccentSliderInt("Max deaths before stopping", ref value6, 1, 10))
{
base.Configuration.General.MaxDeathsBeforeStop = value6;
Save();
}
ImGui.PushTextWrapPos(0f);
ImU8String text = new ImU8String(66, 1);
text.AppendLiteral("Automation will stop after ");
text.AppendFormatted(value6);
text.AppendLiteral(" consecutive death(s) on the same step.");
ImGui.TextColored(in UiThemeUtils.MutedTextColor, text);
ImGui.PopTextWrapPos();
}
}
UiThemeUtils.EndCard(item, item2, item3);
}
private void DrawEquipment()
{
UiThemeUtils.SectionHeader("Equipment");
(Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple = UiThemeUtils.BeginCard();
Vector2 item = tuple.ContentStartPos;
float item2 = tuple.AvailableWidth;
ImDrawListPtr item3 = tuple.DrawList;
bool value = base.Configuration.General.RepairGear;
if (UiThemeUtils.WrappedCheckbox("Repair gear automatically", ref value, "Repairs equipped gear when its condition gets low. Uses dark matter self-repair when your crafter levels and inventory allow it, otherwise visits a nearby mender (teleporting to a city if needed). If gear is critically damaged and no repair is possible, automation stops before entering a duty."))
{
base.Configuration.General.RepairGear = value;
Save();
}
using (ImRaii.Disabled(!value))
{
using (ImRaii.PushIndent())
{
int value2 = base.Configuration.General.RepairThresholdPercent;
if (UiThemeUtils.AccentSliderInt("Repair below condition (%)", ref value2, 10, 90))
{
base.Configuration.General.RepairThresholdPercent = value2;
Save();
}
}
}
ImGui.Spacing();
string[] array = new string[3] { "Smart", "Game recommended", "Disabled" };
int currentItem = (int)base.Configuration.General.EquipMode;
object tooltip = base.Configuration.General.EquipMode switch
{
Questionable.Configuration.EEquipMode.Smart => "Picks gear with job-appropriate stats (and weapon damage for weapons)\ninstead of the game's item-level-only recommendation, which can pick\nwrong-role gear (default)",
Questionable.Configuration.EEquipMode.GameRecommended => "The game's built-in Equip Recommended - item level only",
Questionable.Configuration.EEquipMode.Disabled => "Skips equip-recommended quest steps entirely; gear is never changed",
_ => string.Empty,
};
ImGui.SetNextItemWidth(MathF.Min(260f, ImGui.GetContentRegionAvail().X));
if (UiThemeUtils.ThemedCombo("Equip recommended gear", ref currentItem, array, array.Length))
{
base.Configuration.General.EquipMode = (Configuration.EEquipMode)currentItem;
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon((string)tooltip);
using (ImRaii.Disabled(base.Configuration.General.EquipMode != Questionable.Configuration.EEquipMode.Smart))
{
using (ImRaii.PushIndent())
{
bool value3 = base.Configuration.General.PreserveGearset;
if (UiThemeUtils.WrappedCheckbox("Preserve gearsets", ref value3, "Applies gear upgrades by moving items individually instead of rewriting your current gearset. Slower, but your saved gearset entries stay untouched."))
{
base.Configuration.General.PreserveGearset = value3;
Save();
}
}
}
UiThemeUtils.EndCard(item, item2, item3);
}
private void DrawRewardRedemption()
{
UiThemeUtils.SectionHeader("Reward redemption");
var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard();
ImGui.TextWrapped("Quest-reward items in your inventory are used automatically after quest completion and when you accept a quest. Choose which types to redeem.");
ImGui.Spacing();
UiThemeUtils.GridCheckbox[] array = new UiThemeUtils.GridCheckbox[RewardTypeOptions.Length];
for (int i = 0; i < RewardTypeOptions.Length; i++)
{
array[i] = new UiThemeUtils.GridCheckbox(RewardTypeOptions[i].Label, !base.Configuration.General.DisabledRewardTypes.Contains(RewardTypeOptions[i].Type));
}
int num = UiThemeUtils.CheckboxGrid("rewards", array);
if (num >= 0)
{
EItemRewardType item = RewardTypeOptions[num].Type;
if (!base.Configuration.General.DisabledRewardTypes.Remove(item))
{
base.Configuration.General.DisabledRewardTypes.Add(item);
}
Save();
}
if (!base.Configuration.General.DisabledRewardTypes.Contains(EItemRewardType.Coffer))
{
using (ImRaii.PushIndent())
{
DrawCofferGearsetPicker();
}
}
ImGui.Spacing();
bool value = base.Configuration.General.SkipTradableRewards;
if (UiThemeUtils.WrappedCheckbox("Skip tradable rewards", ref value, "Leaves tradable reward items in your inventory (so you can sell them) instead of using them. Untradable rewards are still redeemed."))
{
base.Configuration.General.SkipTradableRewards = value;
Save();
}
UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList);
}
private unsafe void DrawCofferGearsetPicker()
{
RaptureGearsetModule* ptr = RaptureGearsetModule.Instance();
if (ptr == null)
{
return;
}
int? cofferGearsetIndex = base.Configuration.General.CofferGearsetIndex;
string text = "Current Job";
if (cofferGearsetIndex.HasValue)
{
int valueOrDefault = cofferGearsetIndex.GetValueOrDefault();
if (ptr->IsValidGearset(valueOrDefault))
{
RaptureGearsetModule.GearsetEntry* gearset = ptr->GetGearset(valueOrDefault);
if (_dataManager.GetExcelSheet<ClassJob>().TryGetRow(gearset->ClassJob, out var row))
{
text = $"Gearset {valueOrDefault + 1}: {row.Abbreviation.ExtractText()}";
}
}
else
{
text = $"Gearset {valueOrDefault + 1} (deleted)";
}
}
ImGui.Text("Open coffers as:");
ImGui.SameLine(0f, 4f);
float x = ImGui.GetStyle().ItemSpacing.X;
float x2;
float x3;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
x2 = ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X;
x3 = ImGui.CalcTextSize(FontAwesomeIcon.Trash.ToIconString()).X;
}
float num = x + x2;
if (cofferGearsetIndex.HasValue)
{
num += x + x3 + 6f + ImGui.CalcTextSize("Clear").X + ImGui.GetStyle().FramePadding.X * 2f;
}
UiThemeUtils.RowLabel(text, num, UiThemeUtils.AccentColor);
ImGui.SameLine();
UiThemeUtils.InfoIcon("Switch to this gearset before opening coffers so the gear drops match\nthe desired job. The original gearset is restored afterwards.\nLeave as Current Job to skip switching.");
if (cofferGearsetIndex.HasValue)
{
ImGui.SameLine();
if (UiThemeUtils.DestructiveButton(FontAwesomeIcon.Trash, "Clear"))
{
base.Configuration.General.CofferGearsetIndex = null;
Save();
}
}
int num2 = 1;
List<string> list = new List<string>(num2);
CollectionsMarshal.SetCount(list, num2);
CollectionsMarshal.AsSpan(list)[0] = "Current Job";
List<string> list2 = list;
num2 = 1;
List<int?> list3 = new List<int?>(num2);
CollectionsMarshal.SetCount(list3, num2);
CollectionsMarshal.AsSpan(list3)[0] = null;
List<int?> list4 = list3;
ExcelSheet<ClassJob> excelSheet = _dataManager.GetExcelSheet<ClassJob>();
for (int i = 0; i < 100; i++)
{
if (ptr->IsValidGearset(i))
{
RaptureGearsetModule.GearsetEntry* gearset2 = ptr->GetGearset(i);
string item = $"Gearset {i + 1}";
if (excelSheet.TryGetRow(gearset2->ClassJob, out var row2))
{
item = $"{i + 1}: {row2.Abbreviation.ExtractText()}";
}
list2.Add(item);
list4.Add(i);
}
}
int currentItem = list4.IndexOf(cofferGearsetIndex);
float width = MathF.Min(ImGui.GetContentRegionAvail().X * 0.6f, 260f);
if (UiThemeUtils.SearchableCombo("##cofferGearsetCombo", ref currentItem, list2.ToArray(), ref _cofferGearsetSearchText, width))
{
base.Configuration.General.CofferGearsetIndex = list4[currentItem];
Save();
}
}
static GeneralConfigComponent()
{
int num = 1;
List<(EClassJob, string)> list = new List<(EClassJob, string)>(num);
CollectionsMarshal.SetCount(list, num);
CollectionsMarshal.AsSpan(list)[0] = (EClassJob.Adventurer, "Auto (highest level/item level)");
DefaultClassJobs = list;
CombatModuleOptions = new(Configuration.ECombatModule, string, string[])[4]
{
(Questionable.Configuration.ECombatModule.None, "None", Array.Empty<string>()),
(Questionable.Configuration.ECombatModule.BossMod, "Boss Mod", new string[2] { "BossMod", "BossModReborn" }),
(Questionable.Configuration.ECombatModule.WrathCombo, "Wrath Combo", new string[1] { "WrathCombo" }),
(Questionable.Configuration.ECombatModule.RotationSolverReborn, "Rotation Solver Reborn", new string[1] { "RotationSolver" })
};
RewardTypeOptions = new(EItemRewardType, string)[7]
{
(EItemRewardType.Mount, "Mounts"),
(EItemRewardType.Minion, "Minions"),
(EItemRewardType.OrchestrionRoll, "Orchestrion rolls"),
(EItemRewardType.TripleTriadCard, "Triple Triad cards"),
(EItemRewardType.FashionAccessory, "Fashion accessories"),
(EItemRewardType.Coffer, "Gear coffers"),
(EItemRewardType.UnlockLink, "Other unlocks")
};
}
}