qstbak/Questionable/Questionable.Windows.ConfigComponents/NavigationConfigComponent.cs
2026-08-17 20:29:32 +10:00

200 lines
8 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using Questionable.Model;
namespace Questionable.Windows.ConfigComponents;
internal sealed class NavigationConfigComponent : ConfigComponent
{
private static readonly List<(uint Id, string Name)> DefaultMounts;
private readonly uint[] _mountIds;
private readonly string[] _mountNames;
private string _mountSearchText = string.Empty;
public NavigationConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager)
: base(pluginInterface, configuration)
{
List<(uint, string)> source = (from x in dataManager.GetExcelSheet<Mount>()
where x.RowId != 0 && x.Icon > 0
select (MountId: x.RowId, Name: CapitalizeName(x.Singular.ExtractText())) into x
where !string.IsNullOrEmpty(x.Name)
select x).OrderBy<(uint, string), string>(((uint MountId, string Name) x) => x.Name, StringComparer.OrdinalIgnoreCase).ToList();
_mountIds = DefaultMounts.Select<(uint, string), uint>(((uint Id, string Name) x) => x.Id).Concat(source.Select<(uint, string), uint>(((uint MountId, string Name) x) => x.MountId)).ToArray();
_mountNames = DefaultMounts.Select<(uint, string), string>(((uint Id, string Name) x) => x.Name).Concat(source.Select<(uint, string), string>(((uint MountId, string Name) x) => x.Name)).ToArray();
}
private static string CapitalizeName(string text)
{
if (string.IsNullOrEmpty(text))
{
return text;
}
if (text.Equals(text.ToLowerInvariant(), StringComparison.Ordinal))
{
return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(text);
}
string[] array = text.Split(' ');
for (int i = 0; i < array.Length; i++)
{
if (array[i].Length > 0 && char.IsLower(array[i][0]))
{
array[i] = char.ToUpperInvariant(array[i][0]) + array[i].Substring(1);
}
}
return string.Join(" ", array);
}
public override void DrawTab()
{
Configuration.NavigationConfiguration navigation = base.Configuration.Navigation;
UiThemeUtils.SectionHeader("Mounts & Flying");
var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard();
DrawMountSelector(navigation);
bool value = navigation.NeverFly;
if (UiThemeUtils.WrappedCheckbox("Disable flying (even where unlocked)", ref value, "Always travel on the ground, even in zones where flying is unlocked."))
{
navigation.NeverFly = value;
Save();
}
float value2 = navigation.MountGroundDistance;
if (UiThemeUtils.AccentSliderFloat("Ground Mount (yalms)", ref value2, 0f, 100f, "%.0f", 260f))
{
navigation.MountGroundDistance = value2;
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon($"Minimum distance to use a ground mount. Default: {20f:0}");
using (ImRaii.Disabled(navigation.NeverFly))
{
float value3 = navigation.MountFlyDistance;
if (UiThemeUtils.AccentSliderFloat("Flying Mount (yalms)", ref value3, 0f, 100f, "%.0f", 260f))
{
navigation.MountFlyDistance = value3;
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon($"Minimum distance to use a flying mount. Default: {15f:0}");
}
UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList);
UiThemeUtils.SectionSpacing();
UiThemeUtils.SectionHeader("Sprint");
(Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple2 = UiThemeUtils.BeginCard();
Vector2 item = tuple2.ContentStartPos;
float item2 = tuple2.AvailableWidth;
ImDrawListPtr item3 = tuple2.DrawList;
float value4 = navigation.SprintDistanceTown;
if (UiThemeUtils.AccentSliderFloat("Town (yalms)", ref value4, 0f, 100f, "%.0f", 260f))
{
navigation.SprintDistanceTown = value4;
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon($"Minimum distance to sprint in towns. Default: {20f:0}");
float value5 = navigation.SprintDistanceOpenWorld;
if (UiThemeUtils.AccentSliderFloat("Open World (yalms)", ref value5, 0f, 200f, "%.0f", 260f))
{
navigation.SprintDistanceOpenWorld = value5;
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon($"Minimum distance to sprint in open world. Default: {50f:0}");
UiThemeUtils.EndCard(item, item2, item3);
UiThemeUtils.SectionSpacing();
UiThemeUtils.SectionHeader("Teleport");
(Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple3 = UiThemeUtils.BeginCard();
Vector2 item4 = tuple3.ContentStartPos;
float item5 = tuple3.AvailableWidth;
ImDrawListPtr item6 = tuple3.DrawList;
int value6 = navigation.GilReserve;
if (UiThemeUtils.AccentSliderInt("Gil Reserve", ref value6, 0, 20000, 260f))
{
navigation.GilReserve = Math.Clamp(value6, 0, 999999999);
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon("Minimum gil to keep when deciding whether to teleport.");
bool value7 = navigation.PreferFreeTravel;
if (UiThemeUtils.WrappedCheckbox("Prefer free travel in cities", ref value7, "When in a city, prefer aethernet and walking over teleporting to save gil.\nTeleporting from field zones is unaffected."))
{
navigation.PreferFreeTravel = value7;
Save();
}
bool value8 = navigation.UseTeleportTickets;
if (UiThemeUtils.WrappedCheckbox("Use teleport tickets", ref value8, "Use destination teleport tickets from your inventory (Grand Company, Firmament,\nVesper Bay, Gold Saucer) as free teleports when planning routes.\nEach use consumes one ticket. Disable to never consume tickets."))
{
navigation.UseTeleportTickets = value8;
Save();
}
UiThemeUtils.EndCard(item4, item5, item6);
UiThemeUtils.SectionSpacing();
UiThemeUtils.SectionHeader("Stuck Recovery");
(Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple4 = UiThemeUtils.BeginCard();
Vector2 item7 = tuple4.ContentStartPos;
float item8 = tuple4.AvailableWidth;
ImDrawListPtr item9 = tuple4.DrawList;
string[] names = Enum.GetNames<EStuckRecoveryMode>();
int currentItem = (int)navigation.StuckRecoveryMode;
object tooltip = navigation.StuckRecoveryMode switch
{
EStuckRecoveryMode.Aggressive => "Jump, recalculate, reload, and rebuild navmesh (default)",
EStuckRecoveryMode.Conservative => "Jump and recalculate only - no navmesh reload/rebuild",
EStuckRecoveryMode.Passive => "Log only - no automatic recovery",
_ => string.Empty,
};
ImGui.SetNextItemWidth(MathF.Min(260f, ImGui.GetContentRegionAvail().X));
if (UiThemeUtils.ThemedCombo("Recovery Mode", ref currentItem, names, names.Length))
{
navigation.StuckRecoveryMode = (EStuckRecoveryMode)currentItem;
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon((string)tooltip);
bool value9 = navigation.ReRouteOnFailure;
if (UiThemeUtils.WrappedCheckbox("Re-route on pathfinding failure", ref value9, "When pathfinding fails, SmartNav re-plans the route from your current position.\nDisable to stop automation instead (previous behavior)."))
{
navigation.ReRouteOnFailure = value9;
Save();
}
UiThemeUtils.EndCard(item7, item8, item9);
}
private void DrawMountSelector(Configuration.NavigationConfiguration nav)
{
int currentItem = Array.FindIndex(_mountIds, (uint x) => x == nav.MountId);
if (currentItem == -1)
{
currentItem = 0;
nav.MountId = _mountIds[currentItem];
Save();
}
string previewOverride = ((currentItem >= 0 && currentItem < _mountNames.Length) ? null : "Unknown");
if (UiThemeUtils.SearchableCombo("Preferred Mount", ref currentItem, _mountNames, ref _mountSearchText, MathF.Min(260f, ImGui.GetContentRegionAvail().X), previewOverride))
{
nav.MountId = _mountIds[currentItem];
Save();
}
ImGui.SameLine();
UiThemeUtils.InfoIcon("Mount used for ground travel. \"Mount Roulette\" picks a random mount.");
}
static NavigationConfigComponent()
{
int num = 1;
List<(uint, string)> list = new List<(uint, string)>(num);
CollectionsMarshal.SetCount(list, num);
CollectionsMarshal.AsSpan(list)[0] = (0u, "Mount Roulette");
DefaultMounts = list;
}
}