This commit is contained in:
goaaats 2025-06-22 21:39:38 +02:00
commit 95ec633cc5
163 changed files with 7036 additions and 1585 deletions

View file

@ -146,6 +146,11 @@ internal class DalamudCommands : IServiceType
"DalamudCopyLogHelp",
"Copy the dalamud.log file to your clipboard."),
});
// Add the new command handler for toggling multi-monitor option
commandManager.AddHandler("/xltogglemultimonitor", new CommandInfo(this.OnToggleMultiMonitorCommand)
{
HelpMessage = Loc.Localize("DalamudToggleMultiMonitorHelp", "Toggle multi-monitor windows."),
});
}
private void OnUnloadCommand(string command, string arguments)
@ -387,4 +392,19 @@ internal class DalamudCommands : IServiceType
: Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard.");
chatGui.Print(message);
}
private void OnToggleMultiMonitorCommand(string command, string arguments)
{
var configuration = Service<DalamudConfiguration>.Get();
var chatGui = Service<ChatGui>.Get();
configuration.IsDisableViewport = !configuration.IsDisableViewport;
configuration.QueueSave();
var message = configuration.IsDisableViewport
? Loc.Localize("DalamudMultiMonitorDisabled", "Multi-monitor windows disabled.")
: Loc.Localize("DalamudMultiMonitorEnabled", "Multi-monitor windows enabled.");
chatGui.Print(message);
}
}

View file

@ -11,13 +11,11 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImPlot;
using Dalamud.Configuration.Internal;
using Dalamud.Console;
using Dalamud.Data;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Gui;
using Dalamud.Game.Internal;
using Dalamud.Hooking;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
@ -305,8 +303,14 @@ internal class DalamudInterface : IInternalDisposableService
/// <summary>
/// Opens the <see cref="ConsoleWindow"/>.
/// </summary>
public void OpenLogWindow()
/// <param name="textFilter">The filter to set, if not null.</param>
public void OpenLogWindow(string? textFilter = "")
{
if (textFilter != null)
{
this.consoleWindow.TextFilter = textFilter;
}
this.consoleWindow.IsOpen = true;
this.consoleWindow.BringToFront();
}
@ -518,7 +522,7 @@ internal class DalamudInterface : IInternalDisposableService
/// <summary>
/// Toggle the screen darkening effect used for the credits.
/// </summary>
/// <param name="status">Whether or not to turn the effect on.</param>
/// <param name="status">Whether to turn the effect on.</param>
public void SetCreditsDarkeningAnimation(bool status)
{
this.isCreditsDarkening = status;
@ -713,19 +717,6 @@ internal class DalamudInterface : IInternalDisposableService
this.dalamud.StartInfo.LogName);
}
var antiDebug = Service<AntiDebug>.Get();
if (ImGui.MenuItem("Disable Debugging Protections", (byte*)null, antiDebug.IsEnabled))
{
var newEnabled = !antiDebug.IsEnabled;
if (newEnabled)
antiDebug.Enable();
else
antiDebug.Disable();
this.configuration.IsAntiAntiDebugEnabled = newEnabled;
this.configuration.QueueSave();
}
ImGui.Separator();
if (ImGui.MenuItem("Open Data window"))
@ -1012,7 +1003,7 @@ internal class DalamudInterface : IInternalDisposableService
if (ImGui.MenuItem("Scan dev plugins"))
{
pluginManager.ScanDevPlugins();
_ = pluginManager.ScanDevPluginsAsync();
}
ImGui.Separator();

View file

@ -34,13 +34,14 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using FFXIVClientStructs.FFXIV.Client.Graphics.Environment;
using JetBrains.Annotations;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
using DWMWINDOWATTRIBUTE = Windows.Win32.Graphics.Dwm.DWMWINDOWATTRIBUTE;
// general dev notes, here because it's easiest
@ -198,7 +199,7 @@ internal partial class InterfaceManager : IInternalDisposableService
public IImGuiBackend? Backend => this.backend;
/// <summary>
/// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor.
/// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor.
/// </summary>
public bool OverrideGameCursor
{
@ -217,7 +218,7 @@ internal partial class InterfaceManager : IInternalDisposableService
public bool IsReady => this.backend != null;
/// <summary>
/// Gets or sets a value indicating whether or not Draw events should be dispatched.
/// Gets or sets a value indicating whether Draw events should be dispatched.
/// </summary>
public bool IsDispatchingEvents { get; set; } = true;
@ -515,7 +516,9 @@ internal partial class InterfaceManager : IInternalDisposableService
// Some graphics drivers seem to consider the game's shader cache as invalid if we hook too early.
// The game loads shader packages on the file thread and then compiles them. It will show the logo once it is done.
// This is a workaround, but it fixes an issue where the game would take a very long time to get to the title screen.
if (EnvManager.Instance() == null)
// NetworkModuleProxy is set up after lua scripts are loaded (EventFramework.LoadState >= 5), which can only happen
// after the shaders are compiled (if necessary) and loaded. AgentLobby.Update doesn't do much until this condition is met.
if (CSFramework.Instance()->GetNetworkModuleProxy() == null)
return;
this.SetupHooks(Service<TargetSigScanner>.Get(), Service<FontAtlasFactory>.Get());

View file

@ -0,0 +1,282 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Utility;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>Dedicated thread for OLE operations, and possibly more native thread-serialized operations.</summary>
[ServiceManager.EarlyLoadedService]
internal partial class StaThreadService : IInternalDisposableService
{
private readonly CancellationTokenSource cancellationTokenSource = new();
private readonly Thread thread;
private readonly ThreadBoundTaskScheduler taskScheduler;
private readonly TaskFactory taskFactory;
private readonly TaskCompletionSource<HWND> messageReceiverHwndTask =
new(TaskCreationOptions.RunContinuationsAsynchronously);
[ServiceManager.ServiceConstructor]
private StaThreadService()
{
try
{
this.thread = new(this.OleThreadBody);
this.thread.SetApartmentState(ApartmentState.STA);
this.taskScheduler = new(this.thread);
this.taskScheduler.TaskQueued += this.TaskSchedulerOnTaskQueued;
this.taskFactory = new(
this.cancellationTokenSource.Token,
TaskCreationOptions.None,
TaskContinuationOptions.None,
this.taskScheduler);
this.thread.Start();
this.messageReceiverHwndTask.Task.Wait();
}
catch (Exception e)
{
this.cancellationTokenSource.Cancel();
this.messageReceiverHwndTask.SetException(e);
throw;
}
}
/// <summary>Gets all the available clipboard formats.</summary>
public IReadOnlySet<uint> AvailableClipboardFormats { get; private set; } = ImmutableSortedSet<uint>.Empty;
/// <summary>Places a pointer to a specific data object onto the clipboard. This makes the data object accessible
/// to the <see cref="OleGetClipboard(IDataObject**)"/> function.</summary>
/// <param name="pdo">Pointer to the <see cref="IDataObject"/> interface on the data object from which the data to
/// be placed on the clipboard can be obtained. This parameter can be NULL; in which case the clipboard is emptied.
/// </param>
/// <returns>This function returns <see cref="S.S_OK"/> on success.</returns>
[LibraryImport("ole32.dll")]
public static unsafe partial int OleSetClipboard(IDataObject* pdo);
/// <inheritdoc cref="OleSetClipboard(IDataObject*)"/>
public static unsafe void OleSetClipboard(ComPtr<IDataObject> pdo) =>
Marshal.ThrowExceptionForHR(OleSetClipboard(pdo.Get()));
/// <summary>Retrieves a data object that you can use to access the contents of the clipboard.</summary>
/// <param name="pdo">Address of <see cref="IDataObject"/> pointer variable that receives the interface pointer to
/// the clipboard data object.</param>
/// <returns>This function returns <see cref="S.S_OK"/> on success.</returns>
[LibraryImport("ole32.dll")]
public static unsafe partial int OleGetClipboard(IDataObject** pdo);
/// <inheritdoc cref="OleGetClipboard(IDataObject**)"/>
public static unsafe ComPtr<IDataObject> OleGetClipboard()
{
var pdo = default(ComPtr<IDataObject>);
Marshal.ThrowExceptionForHR(OleGetClipboard(pdo.GetAddressOf()));
return pdo;
}
/// <summary>Calls the appropriate method or function to release the specified storage medium.</summary>
/// <param name="stgm">Address of <see cref="STGMEDIUM"/> to release.</param>
[LibraryImport("ole32.dll")]
public static unsafe partial void ReleaseStgMedium(STGMEDIUM* stgm);
/// <inheritdoc cref="ReleaseStgMedium(STGMEDIUM*)"/>
public static unsafe void ReleaseStgMedium(ref STGMEDIUM stgm)
{
fixed (STGMEDIUM* pstgm = &stgm)
ReleaseStgMedium(pstgm);
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.cancellationTokenSource.Cancel();
if (this.messageReceiverHwndTask.Task.IsCompletedSuccessfully)
SendMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_CLOSE, 0, 0);
this.thread.Join();
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <param name="action">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task"/> representating the state of the operation.</returns>
public async Task Run(Action action, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
await this.taskFactory.StartNew(action, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <typeparam name="T">Type of the return value.</typeparam>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task<T> Run<T>(Func<T> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task Run(Func<Task> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <typeparam name="T">Type of the return value.</typeparam>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task<T> Run<T>(Func<Task<T>> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
return await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
[LibraryImport("ole32.dll")]
private static partial int OleInitialize(nint reserved);
[LibraryImport("ole32.dll")]
private static partial void OleUninitialize();
[LibraryImport("ole32.dll")]
private static partial int OleFlushClipboard();
private void TaskSchedulerOnTaskQueued() =>
PostMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_NULL, 0, 0);
private void UpdateAvailableClipboardFormats(HWND hWnd)
{
if (!OpenClipboard(hWnd))
{
this.AvailableClipboardFormats = ImmutableSortedSet<uint>.Empty;
return;
}
var formats = new SortedSet<uint>();
for (var cf = EnumClipboardFormats(0); cf != 0; cf = EnumClipboardFormats(cf))
formats.Add(cf);
this.AvailableClipboardFormats = formats;
CloseClipboard();
}
private LRESULT MessageReceiverWndProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
this.taskScheduler.Run();
switch (uMsg)
{
case WM.WM_CLIPBOARDUPDATE:
this.UpdateAvailableClipboardFormats(hWnd);
break;
case WM.WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}
private unsafe void OleThreadBody()
{
var hInstance = (HINSTANCE)Marshal.GetHINSTANCE(typeof(StaThreadService).Module);
ushort wndClassAtom = 0;
var gch = GCHandle.Alloc(this);
try
{
((HRESULT)OleInitialize(0)).ThrowOnError();
fixed (char* name = typeof(StaThreadService).FullName!)
{
var wndClass = new WNDCLASSEXW
{
cbSize = (uint)sizeof(WNDCLASSEXW),
lpfnWndProc = &MessageReceiverWndProcStatic,
hInstance = hInstance,
hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1),
lpszClassName = (ushort*)name,
};
wndClassAtom = RegisterClassExW(&wndClass);
if (wndClassAtom == 0)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
this.messageReceiverHwndTask.SetResult(
CreateWindowExW(
0,
(ushort*)wndClassAtom,
(ushort*)name,
0,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
default,
default,
hInstance,
(void*)GCHandle.ToIntPtr(gch)));
[UnmanagedCallersOnly]
static LRESULT MessageReceiverWndProcStatic(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
nint gchn;
if (uMsg == WM.WM_NCCREATE)
{
gchn = (nint)((CREATESTRUCTW*)lParam)->lpCreateParams;
SetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA, gchn);
}
else
{
gchn = GetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA);
}
if (gchn == 0)
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
return ((StaThreadService)GCHandle.FromIntPtr(gchn).Target!)
.MessageReceiverWndProc(hWnd, uMsg, wParam, lParam);
}
}
AddClipboardFormatListener(this.messageReceiverHwndTask.Task.Result);
this.UpdateAvailableClipboardFormats(this.messageReceiverHwndTask.Task.Result);
for (MSG msg; GetMessageW(&msg, default, 0, 0);)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
catch (Exception e)
{
gch.Free();
_ = OleFlushClipboard();
OleUninitialize();
if (wndClassAtom != 0)
UnregisterClassW((ushort*)wndClassAtom, hInstance);
this.messageReceiverHwndTask.TrySetException(e);
}
}
}

View file

@ -429,17 +429,17 @@ internal unsafe class UiDebug
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText02);
ImGui.Text("Text3: ");
ImGui.Text("AvailableLines: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText03);
Service<SeStringRenderer>.Get().Draw(textInputComponent->AvailableLines);
ImGui.Text("Text4: ");
ImGui.Text("HighlightedAutoTranslateOptionColorPrefix: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText04);
Service<SeStringRenderer>.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorPrefix);
ImGui.Text("Text5: ");
ImGui.Text("HighlightedAutoTranslateOptionColorSuffix: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText05);
Service<SeStringRenderer>.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorSuffix);
break;
}

View file

@ -58,7 +58,13 @@ internal unsafe class ComponentNodeTree : ResNodeTree
/// <inheritdoc/>
private protected override void PrintChildNodes()
{
base.PrintChildNodes();
var prevNode = this.CompNode->Component->UldManager.RootNode;
while (prevNode != null)
{
GetOrCreate(prevNode, this.AddonTree).Print(null);
prevNode = prevNode->PrevSiblingNode;
}
var count = this.UldManager->NodeListCount;
PrintNodeListAsTree(this.UldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f));
}
@ -92,11 +98,11 @@ internal unsafe class ComponentNodeTree : ResNodeTree
ImGui.TextUnformatted(
$"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
ImGui.TextUnformatted(
$"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}");
$"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}");
ImGui.TextUnformatted(
$"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}");
$"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}");
ImGui.TextUnformatted(
$"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}");
$"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}");
break;
case List:
case TreeList:

View file

@ -84,7 +84,7 @@ internal unsafe partial class ResNodeTree : IDisposable
/// <returns>An existing or newly-created instance of <see cref="ResNodeTree"/>.</returns>
internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) =>
addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree
: (int)node->Type > 1000
: (int)node->Type >= 1000
? new ComponentNodeTree(node, addonTree)
: node->Type switch
{

View file

@ -51,9 +51,10 @@ public readonly unsafe partial struct TimelineTree
return;
}
var count = this.Resource->AnimationCount;
var animationCount = this.Resource->AnimationCount;
var labelSetCount = this.Resource->LabelSetCount;
if (count > 0)
if (animationCount > 0)
{
using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth);
@ -65,22 +66,35 @@ public readonly unsafe partial struct TimelineTree
ShowStruct(this.NodeTimeline);
PrintFieldValuePairs(
("Id", $"{this.NodeTimeline->Resource->Id}"),
("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"),
("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})"));
PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}"));
ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List");
for (var a = 0; a < count; a++)
if (this.Resource->Animations is not null)
{
var animation = this.Resource->Animations[a];
var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation;
this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation))));
PrintFieldValuePairs(
("Id", $"{this.NodeTimeline->Resource->Id}"),
("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"),
("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})"));
PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}"));
ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List");
for (var a = 0; a < animationCount; a++)
{
var animation = this.Resource->Animations[a];
var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation;
this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + a));
}
}
}
}
if (labelSetCount > 0 && this.Resource->LabelSets is not null)
{
using var tree = ImRaii.TreeNode($"Timeline Label Sets##{(nint)this.node:X}LabelSets", SpanFullWidth);
if (tree.Success)
{
this.DrawLabelSets();
}
}
}
private static void GetFrameColumn(Span<AtkTimelineKeyGroup> keyGroups, List<IKeyGroupColumn> columns, ushort endFrame)
@ -380,4 +394,63 @@ public readonly unsafe partial struct TimelineTree
return columns;
}
private void DrawLabelSets()
{
PrintFieldValuePair("LabelSet", $"{(nint)this.NodeTimeline->Resource->LabelSets:X}");
ImGui.SameLine();
ShowStruct(this.NodeTimeline->Resource->LabelSets);
PrintFieldValuePairs(
("StartFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->StartFrameIdx}"),
("EndFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->EndFrameIdx}"));
using var labelSetTable = ImRaii.TreeNode("Entries");
if (labelSetTable.Success)
{
var keyFrameGroup = this.Resource->LabelSets->LabelKeyGroup;
using var table = ImRaii.Table($"##{(nint)this.node}labelSetKeyFrameTable", 7, Borders | SizingFixedFit | RowBg | NoHostExtendX);
if (table.Success)
{
ImGui.TableSetupColumn("Frame ID", WidthFixed);
ImGui.TableSetupColumn("Speed Start", WidthFixed);
ImGui.TableSetupColumn("Speed End", WidthFixed);
ImGui.TableSetupColumn("Interpolation", WidthFixed);
ImGui.TableSetupColumn("Label ID", WidthFixed);
ImGui.TableSetupColumn("Jump Behavior", WidthFixed);
ImGui.TableSetupColumn("Target Label ID", WidthFixed);
ImGui.TableHeadersRow();
for (var l = 0; l < keyFrameGroup.KeyFrameCount; l++)
{
var keyFrame = keyFrameGroup.KeyFrames[l];
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.FrameIdx}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient1:F2}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient2:F2}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Interpolation}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Value.Label.LabelId}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpBehavior}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpLabelId}");
}
}
}
}
}

View file

@ -121,6 +121,19 @@ internal class ConsoleWindow : Window, IDisposable
/// <summary>Gets the queue where log entries that are not processed yet are stored.</summary>
public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
/// <summary>
/// Gets or sets the current text filter.
/// </summary>
public string TextFilter
{
get => this.textFilter;
set
{
this.textFilter = value;
this.RecompileLogFilter();
}
}
/// <inheritdoc/>
public override void OnOpen()
{
@ -620,24 +633,29 @@ internal class ConsoleWindow : Window, IDisposable
ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
|| ImGui.IsItemDeactivatedAfterEdit())
{
this.compiledLogFilter = null;
this.exceptionLogFilter = null;
try
{
this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase);
this.QueueRefilter();
}
catch (Exception e)
{
this.exceptionLogFilter = e;
}
foreach (var log in this.logText)
log.HighlightMatches = null;
this.RecompileLogFilter();
}
}
private void RecompileLogFilter()
{
this.compiledLogFilter = null;
this.exceptionLogFilter = null;
try
{
this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase);
this.QueueRefilter();
}
catch (Exception e)
{
this.exceptionLogFilter = e;
}
foreach (var log in this.logText)
log.HighlightMatches = null;
}
private void DrawSettingsPopup()
{
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))

View file

@ -1,4 +1,5 @@
using System.Numerics;
using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Gui.FlyText;
@ -38,13 +39,15 @@ internal class FlyTextWidget : IDataWindowWidget
/// <inheritdoc/>
public void Draw()
{
if (ImGui.BeginCombo("Kind", this.flyKind.ToString()))
if (ImGui.BeginCombo("Kind", $"{this.flyKind} ({(int)this.flyKind})"))
{
var names = Enum.GetNames(typeof(FlyTextKind));
for (var i = 0; i < names.Length; i++)
var values = Enum.GetValues<FlyTextKind>().Distinct();
foreach (var value in values)
{
if (ImGui.Selectable($"{names[i]} ({i})"))
this.flyKind = (FlyTextKind)i;
if (ImGui.Selectable($"{value} ({(int)value})"))
{
this.flyKind = value;
}
}
ImGui.EndCombo();

View file

@ -1,7 +1,13 @@
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Game;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
@ -11,17 +17,40 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for displaying hook information.
/// </summary>
internal class HookWidget : IDataWindowWidget
internal unsafe class HookWidget : IDataWindowWidget
{
private readonly List<IDalamudHook> hookStressTestList = [];
private Hook<MessageBoxWDelegate>? messageBoxMinHook;
private bool hookUseMinHook;
private int hookStressTestCount = 0;
private int hookStressTestMax = 1000;
private int hookStressTestWait = 100;
private int hookStressTestMaxDegreeOfParallelism = 10;
private StressTestHookTarget hookStressTestHookTarget = StressTestHookTarget.Random;
private bool hookStressTestRunning = false;
private MessageBoxWDelegate? messageBoxWOriginal;
private AddonFinalizeDelegate? addonFinalizeOriginal;
private AddonLifecycleAddressResolver? address;
private delegate int MessageBoxWDelegate(
IntPtr hWnd,
[MarshalAs(UnmanagedType.LPWStr)] string text,
[MarshalAs(UnmanagedType.LPWStr)] string caption,
MESSAGEBOX_STYLE type);
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
private enum StressTestHookTarget
{
MessageBoxW,
AddonFinalize,
Random,
}
/// <inheritdoc/>
public string DisplayName { get; init; } = "Hook";
@ -35,6 +64,9 @@ internal class HookWidget : IDataWindowWidget
public void Load()
{
this.Ready = true;
this.address = new AddonLifecycleAddressResolver();
this.address.Setup(Service<TargetSigScanner>.Get());
}
/// <inheritdoc/>
@ -42,7 +74,9 @@ internal class HookWidget : IDataWindowWidget
{
try
{
ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook);
ImGui.Checkbox("Use MinHook (only for regular hooks, AsmHook is Reloaded-only)", ref this.hookUseMinHook);
ImGui.Separator();
if (ImGui.Button("Create"))
this.messageBoxMinHook = Hook<MessageBoxWDelegate>.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook);
@ -67,18 +101,94 @@ internal class HookWidget : IDataWindowWidget
if (this.messageBoxMinHook != null)
ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled);
ImGui.Separator();
ImGui.BeginDisabled(this.hookStressTestRunning);
ImGui.Text("Stress Test");
if (ImGui.InputInt("Max", ref this.hookStressTestMax))
this.hookStressTestCount = 0;
ImGui.InputInt("Wait (ms)", ref this.hookStressTestWait);
ImGui.InputInt("Max Degree of Parallelism", ref this.hookStressTestMaxDegreeOfParallelism);
if (ImGui.BeginCombo("Target", HookTargetToString(this.hookStressTestHookTarget)))
{
foreach (var target in Enum.GetValues<StressTestHookTarget>())
{
if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target))
this.hookStressTestHookTarget = target;
}
ImGui.EndCombo();
}
if (ImGui.Button("Stress Test"))
{
Task.Run(() =>
{
this.hookStressTestRunning = true;
this.hookStressTestCount = 0;
Parallel.For(
0,
this.hookStressTestMax,
new ParallelOptions
{
MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism,
},
_ =>
{
this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget));
this.hookStressTestCount++;
Thread.Sleep(this.hookStressTestWait);
});
}).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Stress test failed");
}
else
{
Log.Information("Stress test completed");
}
this.hookStressTestRunning = false;
this.hookStressTestList.ForEach(hook =>
{
hook.Dispose();
});
this.hookStressTestList.Clear();
});
}
ImGui.EndDisabled();
ImGui.TextUnformatted("Status: " + (this.hookStressTestRunning ? "Running" : "Idle"));
ImGui.ProgressBar(this.hookStressTestCount / (float)this.hookStressTestMax, new System.Numerics.Vector2(0, 0), $"{this.hookStressTestCount}/{this.hookStressTestMax}");
}
catch (Exception ex)
{
Log.Error(ex, "MinHook error caught");
Log.Error(ex, "Hook error caught");
}
}
private static string HookTargetToString(StressTestHookTarget target)
{
return target switch
{
StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)",
StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)",
_ => target.ToString(),
};
}
private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, MESSAGEBOX_STYLE type)
{
Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type);
var result = this.messageBoxMinHook!.Original(hwnd, "Cause Access Violation?", caption, MESSAGEBOX_STYLE.MB_YESNO);
var result = this.messageBoxWOriginal!(hwnd, "Cause Access Violation?", caption, MESSAGEBOX_STYLE.MB_YESNO);
if (result == (int)MESSAGEBOX_RESULT.IDYES)
{
@ -87,4 +197,52 @@ internal class HookWidget : IDataWindowWidget
return result;
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
{
Log.Information("OnAddonFinalize");
this.addonFinalizeOriginal!(unitManager, atkUnitBase);
}
private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta)
{
Log.Information("OnAddonUpdate");
}
private IDalamudHook HookMessageBoxW()
{
var hook = Hook<MessageBoxWDelegate>.FromSymbol(
"User32",
"MessageBoxW",
this.MessageBoxWDetour,
this.hookUseMinHook);
this.messageBoxWOriginal = hook.Original;
hook.Enable();
return hook;
}
private IDalamudHook HookAddonFinalize()
{
var hook = Hook<AddonFinalizeDelegate>.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize);
this.addonFinalizeOriginal = hook.Original;
hook.Enable();
return hook;
}
private IDalamudHook HookTarget(StressTestHookTarget target)
{
if (target == StressTestHookTarget.Random)
{
target = (StressTestHookTarget)Random.Shared.Next(0, 2);
}
return target switch
{
StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(),
StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(),
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
};
}
}

View file

@ -23,7 +23,7 @@ internal class InventoryWidget : IDataWindowWidget
{
private DataManager dataManager;
private TextureManager textureManager;
private InventoryType? selectedInventoryType = InventoryType.Inventory1;
private GameInventoryType? selectedInventoryType = GameInventoryType.Inventory1;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["inv", "inventory"];
@ -53,7 +53,7 @@ internal class InventoryWidget : IDataWindowWidget
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
this.DrawInventoryType((InventoryType)this.selectedInventoryType);
this.DrawInventoryType(this.selectedInventoryType.Value);
}
private static string StripSoftHypen(string input)
@ -71,9 +71,9 @@ internal class InventoryWidget : IDataWindowWidget
ImGui.TableSetupScrollFreeze(2, 1);
ImGui.TableHeadersRow();
foreach (var inventoryType in Enum.GetValues<InventoryType>())
foreach (var inventoryType in Enum.GetValues<GameInventoryType>())
{
var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType);
var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType);
using var itemDisabled = ImRaii.Disabled(items.IsEmpty);
@ -95,7 +95,7 @@ internal class InventoryWidget : IDataWindowWidget
if (ImGui.MenuItem("Copy Address"))
{
var container = InventoryManager.Instance()->GetInventoryContainer(inventoryType);
var container = InventoryManager.Instance()->GetInventoryContainer((InventoryType)inventoryType);
ImGui.SetClipboardText($"0x{(nint)container:X}");
}
}
@ -106,9 +106,9 @@ internal class InventoryWidget : IDataWindowWidget
}
}
private unsafe void DrawInventoryType(InventoryType inventoryType)
private unsafe void DrawInventoryType(GameInventoryType inventoryType)
{
var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType);
var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType);
if (items.IsEmpty)
{
ImGui.TextUnformatted($"{inventoryType} is empty.");

View file

@ -20,7 +20,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
internal class NounProcessorWidget : IDataWindowWidget
{
/// <summary>A list of German grammatical cases.</summary>
internal static readonly string[] GermanCases = ["Nominative", "Genitive", "Dative", "Accusative"];
internal static readonly string[] GermanCases = [string.Empty, "Nominative", "Genitive", "Dative", "Accusative"];
private static readonly Type[] NounSheets = [
typeof(Aetheryte),
@ -155,7 +155,7 @@ internal class NounProcessorWidget : IDataWindowWidget
GrammaticalCase = grammaticalCase,
};
var output = nounProcessor.ProcessNoun(nounParams).ExtractText().Replace("\"", "\\\"");
var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase]}" : "1";
var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase + 1]}" : "1";
sb.AppendLine($"new(nameof(LSheets.{sheetType.Name}), {this.rowId}, ClientLanguage.{language}, {this.amount}, (int){articleTypeEnumType.Name}.{Enum.GetName(articleTypeEnumType, articleType)}, {caseParam}, \"{output}\"),");
}
}

View file

@ -7,6 +7,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.Noun.Enums;
using Dalamud.Game.Text.SeStringHandling;
@ -90,6 +91,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
{ MacroCode.LowerHead, ["String"] },
{ MacroCode.ColorType, ["ColorType"] },
{ MacroCode.EdgeColorType, ["ColorType"] },
{ MacroCode.Ruby, ["StandardText", "RubyText"] },
{ MacroCode.Digit, ["Value", "TargetLength"] },
{ MacroCode.Ordinal, ["Value"] },
{ MacroCode.Sound, ["IsJingle", "SoundId"] },
@ -132,8 +134,8 @@ internal class SeStringCreatorWidget : IDataWindowWidget
new TextEntry(TextEntryType.Macro, "<colortype(17)>"),
new TextEntry(TextEntryType.Macro, "<edgecolortype(19)>"),
new TextEntry(TextEntryType.String, "Dalamud"),
new TextEntry(TextEntryType.Macro, "<edgecolor(0)>"),
new TextEntry(TextEntryType.Macro, "<colortype(0)>"),
new TextEntry(TextEntryType.Macro, "<edgecolor(stackcolor)>"),
new TextEntry(TextEntryType.Macro, "<color(stackcolor)>"),
new TextEntry(TextEntryType.Macro, " <string(lstr1)>"),
];
@ -165,7 +167,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
/// <inheritdoc/>
public void Load()
{
this.language = Service<DalamudConfiguration>.Get().EffectiveLanguage.ToClientLanguage();
this.language = Service<ClientState>.Get().ClientLanguage;
this.UpdateInputString(false);
this.Ready = true;
}
@ -473,7 +475,12 @@ internal class SeStringCreatorWidget : IDataWindowWidget
}
}
RaptureLogModule.Instance()->PrintString(Service<SeStringEvaluator>.Get().Evaluate(sb.ToReadOnlySeString()));
var evaluated = Service<SeStringEvaluator>.Get().Evaluate(
sb.ToReadOnlySeString(),
this.localParameters,
this.language);
RaptureLogModule.Instance()->PrintString(evaluated);
}
if (this.entries.Count != 0)
@ -1011,7 +1018,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
ImGui.TextUnformatted(Enum.GetName(articleTypeEnumType, u32));
}
if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 3)
if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 4)
{
ImGui.SameLine();
ImGui.TextUnformatted(NounProcessorWidget.GermanCases[u32]);

View file

@ -237,29 +237,42 @@ internal class ServicesWidget : IDataWindowWidget
}
}
if (ImGui.CollapsingHeader("Plugin-facing Services"))
if (ImGui.CollapsingHeader("Singleton Services"))
{
foreach (var instance in container.Instances)
{
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
var isPublic = instance.Key.IsPublic;
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
{
ImGui.Text(
hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
}
if (isPublic)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text("\t => PUBLIC!!!");
}
switch (instance.Value.Visibility)
{
case ObjectInstanceVisibility.Internal:
ImGui.Text("\t => Internally resolved");
break;
case ObjectInstanceVisibility.ExposedToPlugins:
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
{
ImGui.Text("\t => Exposed to plugins!");
ImGui.Text(
hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
}
break;
default:
throw new ArgumentOutOfRangeException();
}
ImGuiHelpers.ScaledDummy(2);
}
}

View file

@ -180,6 +180,15 @@ internal class TexWidget : IDataWindowWidget
ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
if (!this.textureManager.HasClipboardImage())
{
ImGuiComponents.DisabledButton("Paste from Clipboard");
}
else if (ImGui.Button("Paste from Clipboard"))
{
this.addedTextures.Add(new(Api10: this.textureManager.CreateFromClipboardAsync()));
}
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon)))
{
ImGui.PushID(nameof(this.DrawGetFromGameIcon));

View file

@ -226,6 +226,7 @@ internal class PluginInstallerWindow : Window, IDisposable
IsInstallableOutdated = 1 << 5,
IsOrphan = 1 << 6,
IsTesting = 1 << 7,
IsIncompatible = 1 << 8,
}
private enum InstalledPluginListFilter
@ -281,11 +282,15 @@ internal class PluginInstallerWindow : Window, IDisposable
var pluginManager = Service<PluginManager>.Get();
_ = pluginManager.ReloadPluginMastersAsync();
Service<PluginManager>.Get().ScanDevPlugins();
_ = pluginManager.ScanDevPluginsAsync();
if (!this.isSearchTextPrefilled)
{
this.searchText = string.Empty;
this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical;
}
if (!this.isSearchTextPrefilled) this.searchText = string.Empty;
this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical;
this.adaptiveSort = true;
if (this.updateStatus == OperationStatus.Complete || this.updateStatus == OperationStatus.Idle)
@ -361,11 +366,20 @@ internal class PluginInstallerWindow : Window, IDisposable
{
this.isSearchTextPrefilled = false;
this.searchText = string.Empty;
if (this.sortKind == PluginSortKind.SearchScore)
{
this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical;
this.ResortPlugins();
}
}
else
{
this.isSearchTextPrefilled = true;
this.searchText = text;
this.sortKind = PluginSortKind.SearchScore;
this.filterText = Locs.SortBy_SearchScore;
this.ResortPlugins();
}
}
@ -458,6 +472,36 @@ internal class PluginInstallerWindow : Window, IDisposable
configuration.QueueSave();
}
private static void DrawProgressBar<T>(IEnumerable<T> items, Func<T, bool> pendingFunc, Func<T, bool> totalFunc, Action<T> renderPending)
{
var windowSize = ImGui.GetWindowSize();
var numLoaded = 0;
var total = 0;
var itemsArray = items as T[] ?? items.ToArray();
var allPending = itemsArray.Where(pendingFunc)
.ToArray();
var allLoadedOrLoading = itemsArray.Count(totalFunc);
// Cap number of items we show to avoid clutter
const int maxShown = 3;
foreach (var repo in allPending.Take(maxShown))
{
renderPending(repo);
}
ImGuiHelpers.ScaledDummy(10);
numLoaded += allLoadedOrLoading - allPending.Length;
total += allLoadedOrLoading;
if (numLoaded != total)
{
ImGui.SetCursorPosX(windowSize.X / 3);
ImGui.ProgressBar(numLoaded / (float)total, new Vector2(windowSize.X / 3, 50), $"{numLoaded}/{total}");
}
}
private void SetOpenPage(PluginInstallerOpenKind kind)
{
switch (kind)
@ -486,6 +530,12 @@ internal class PluginInstallerWindow : Window, IDisposable
// Plugins category
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
break;
case PluginInstallerOpenKind.DalamudChangelogs:
// Changelog group
this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Changelog;
// Dalamud category
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.DalamudChangelogs;
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}
@ -523,7 +573,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.GetWindowDrawList().AddRectFilled(
ImGui.GetWindowPos() + new Vector2(0, titleHeight),
ImGui.GetWindowPos() + windowSize,
0xCC000000,
ImGui.ColorConvertFloat4ToU32(new(0f, 0f, 0f, 0.8f * ImGui.GetStyle().Alpha)),
ImGui.GetStyle().WindowRounding,
ImDrawFlags.RoundCornersBottom);
ImGui.PopClipRect();
@ -555,40 +605,29 @@ internal class PluginInstallerWindow : Window, IDisposable
if (pluginManager.PluginsReady && !pluginManager.ReposReady)
{
ImGuiHelpers.CenteredText("Loading repositories...");
ImGuiHelpers.ScaledDummy(10);
DrawProgressBar(pluginManager.Repos, x => x.State != PluginRepositoryState.Success &&
x.State != PluginRepositoryState.Fail &&
x.IsEnabled,
x => x.IsEnabled,
x => ImGuiHelpers.CenteredText($"Loading {x.PluginMasterUrl}"));
}
else if (!pluginManager.PluginsReady && pluginManager.ReposReady)
{
ImGuiHelpers.CenteredText("Loading installed plugins...");
ImGuiHelpers.ScaledDummy(10);
DrawProgressBar(pluginManager.InstalledPlugins, x => x.State == PluginState.Loading,
x => x.State is PluginState.Loaded or
PluginState.LoadError or
PluginState.Loading,
x => ImGuiHelpers.CenteredText($"Loading {x.Name}"));
}
else
{
ImGuiHelpers.CenteredText("Loading repositories and plugins...");
}
var currentProgress = 0;
var total = 0;
var pendingRepos = pluginManager.Repos.ToArray()
.Where(x => (x.State != PluginRepositoryState.Success &&
x.State != PluginRepositoryState.Fail) &&
x.IsEnabled)
.ToArray();
var allRepoCount =
pluginManager.Repos.Count(x => x.State != PluginRepositoryState.Fail && x.IsEnabled);
foreach (var repo in pendingRepos)
{
ImGuiHelpers.CenteredText($"{repo.PluginMasterUrl}: {repo.State}");
}
currentProgress += allRepoCount - pendingRepos.Length;
total += allRepoCount;
if (currentProgress != total)
{
ImGui.SetCursorPosX(windowSize.X / 3);
ImGui.ProgressBar(currentProgress / (float)total, new Vector2(windowSize.X / 3, 50));
}
}
break;
@ -599,11 +638,27 @@ internal class PluginInstallerWindow : Window, IDisposable
throw new ArgumentOutOfRangeException();
}
if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(90) && !pluginManager.PluginsReady)
if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(30) && !pluginManager.PluginsReady)
{
ImGuiHelpers.ScaledDummy(10);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer.");
ImGuiHelpers.CenteredText("You can try restarting in safe mode, and deleting the plugin.");
ImGui.PopStyleColor();
ImGuiHelpers.BeginHorizontalButtonGroup()
.Add(
"Restart in Safe Mode",
() =>
{
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.ForceSave();
Dalamud.RestartGame();
})
.SetCentered(true)
.WithHeight(30)
.Draw();
}
}
}
@ -761,7 +816,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SameLine();
if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
{
pluginManager.ScanDevPlugins();
_ = pluginManager.ScanDevPluginsAsync();
}
}
@ -2137,7 +2192,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha);
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
ImGui.Image(this.imageCache.UpdateIcon.Handle, iconSize);
else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan))
else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan) || flags.HasFlag(PluginHeaderFlags.IsIncompatible))
ImGui.Image(this.imageCache.TroubleIcon.Handle, iconSize);
else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
ImGui.Image(this.imageCache.OutdatedInstallableIcon.Handle, iconSize);
@ -2213,9 +2268,14 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPos(cursor);
// Outdated warning
if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
if (flags.HasFlag(PluginHeaderFlags.IsIncompatible))
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Locs.PluginBody_Incompatible);
}
else if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
var bodyText = Locs.PluginBody_Outdated + " ";
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
@ -2224,7 +2284,6 @@ internal class PluginInstallerWindow : Window, IDisposable
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
ImGui.TextWrapped(bodyText);
ImGui.PopStyleColor();
}
else if (plugin is { IsBanned: true })
{
@ -2393,6 +2452,14 @@ internal class PluginInstallerWindow : Window, IDisposable
var effectiveApiLevel = useTesting && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel;
var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel;
var isIncompatible = manifest.MinimumDalamudVersion != null &&
manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed;
var enableInstallButton = this.updateStatus != OperationStatus.InProgress &&
this.installStatus != OperationStatus.InProgress &&
!isOutdated &&
!isIncompatible;
// Check for valid versions
if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null)
{
@ -2417,6 +2484,11 @@ internal class PluginInstallerWindow : Window, IDisposable
label += Locs.PluginTitleMod_TestingAvailable;
}
if (isIncompatible)
{
label += Locs.PluginTitleMod_Incompatible;
}
var isThirdParty = manifest.SourceRepo.IsThirdParty;
ImGui.PushID($"available{index}{manifest.InternalName}");
@ -2430,6 +2502,8 @@ internal class PluginInstallerWindow : Window, IDisposable
flags |= PluginHeaderFlags.IsInstallableOutdated;
if (useTesting || manifest.IsTestingExclusive)
flags |= PluginHeaderFlags.IsTesting;
if (isIncompatible)
flags |= PluginHeaderFlags.IsIncompatible;
if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index))
{
@ -2457,9 +2531,6 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(5);
// Controls
var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress || isOutdated;
var versionString = useTesting
? $"{manifest.TestingAssemblyVersion}"
: $"{manifest.AssemblyVersion}";
@ -2468,7 +2539,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGuiComponents.DisabledButton(Locs.PluginButton_SafeMode);
}
else if (disabled)
else if (!enableInstallButton)
{
ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString));
}
@ -2713,7 +2784,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}");
var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog;
var applicableChangelog = plugin.IsTesting ? remoteManifest?.TestingChangelog : remoteManifest?.Changelog;
var hasChangelog = !applicableChangelog.IsNullOrWhitespace();
var didDrawApplicableChangelogInsideCollapsible = false;
@ -3096,11 +3167,13 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle);
}
else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled)
else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled || pluginManager.SafeMode)
{
ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable);
if (inMultipleProfiles && ImGui.IsItemHovered())
if (pluginManager.SafeMode && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_SafeMode);
else if (inMultipleProfiles && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile);
else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name));
@ -3494,6 +3567,24 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading);
}
// Error Notifications
ImGui.PushStyleColor(ImGuiCol.Button, plugin.NotifyForErrors ? greenColor : redColor);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.NotifyForErrors ? greenColor : redColor);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Bolt))
{
plugin.NotifyForErrors ^= true;
configuration.QueueSave();
}
ImGui.PopStyleColor(2);
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(Locs.PluginButtonToolTip_NotifyForErrors);
}
}
}
@ -4047,6 +4138,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginTitleMod_TestingAvailable => Loc.Localize("InstallerTestingAvailable", " (has testing version)");
public static string PluginTitleMod_Incompatible => Loc.Localize("InstallerTitleModIncompatible", " (incompatible)");
public static string PluginTitleMod_DevPlugin => Loc.Localize("InstallerDevPlugin", " (dev plugin)");
public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)");
@ -4103,6 +4196,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible.");
public static string PluginBody_Incompatible => Loc.Localize("InstallerIncompatiblePluginBody ", "This plugin is incompatible with your version of Dalamud. Please attempt to restart your game.");
public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author.");
public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation.");
@ -4160,6 +4255,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading");
public static string PluginButtonToolTip_NotifyForErrors => Loc.Localize("InstallerNotifyForErrors", "Show Dalamud notifications when this plugin is creating errors");
public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin");
public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game.");
@ -4180,6 +4277,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection.");
public static string PluginButtonToolTip_SafeMode => Loc.Localize("InstallerButtonSafeModeTooltip", "Cannot enable plugins in safe mode.");
public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name);
#endregion

View file

@ -224,6 +224,9 @@ internal class ProfileManagerWidget
ImGuiHelpers.ScaledDummy(3);
ImGui.SameLine();
// Center text in frame height
var textHeight = ImGui.CalcTextSize(profile.Name);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() / 2) - (textHeight.Y / 2));
ImGui.Text(profile.Name);
ImGui.SameLine();
@ -263,6 +266,17 @@ internal class ProfileManagerWidget
didAny = true;
ImGuiHelpers.ScaledDummy(2);
// Separator if not the last item
if (profile != profman.Profiles.Last())
{
// Very light grey
ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f));
ImGui.Separator();
ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(2);
}
}
if (toCloneGuid != null)
@ -386,10 +400,19 @@ internal class ProfileManagerWidget
ImGuiHelpers.ScaledDummy(5);
var enableAtBoot = profile.AlwaysEnableAtBoot;
if (ImGui.Checkbox(Locs.AlwaysEnableAtBoot, ref enableAtBoot))
ImGui.TextUnformatted(Locs.StartupBehavior);
if (ImGui.BeginCombo("##startupBehaviorPicker", Locs.PolicyToLocalisedName(profile.StartupPolicy)))
{
profile.AlwaysEnableAtBoot = enableAtBoot;
foreach (var policy in Enum.GetValues(typeof(ProfileModelV1.ProfileStartupPolicy)).Cast<ProfileModelV1.ProfileStartupPolicy>())
{
var name = Locs.PolicyToLocalisedName(policy);
if (ImGui.Selectable(name, profile.StartupPolicy == policy))
{
profile.StartupPolicy = policy;
}
}
ImGui.EndCombo();
}
ImGuiHelpers.ScaledDummy(5);
@ -514,6 +537,15 @@ internal class ProfileManagerWidget
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.RemovePlugin);
// Separator if not the last item
if (profileEntry != profile.Plugins.Last())
{
// Very light grey
ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f));
ImGui.Separator();
ImGui.PopStyleColor();
}
}
if (wantRemovePluginGuid != null)
@ -554,6 +586,9 @@ internal class ProfileManagerWidget
private static class Locs
{
public static string StartupBehavior =>
Loc.Localize("ProfileManagerStartupBehavior", "Startup behavior");
public static string TooltipEnableDisable =>
Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection");
@ -567,9 +602,6 @@ internal class ProfileManagerWidget
public static string NoPluginsInProfile =>
Loc.Localize("ProfileManagerNoPluginsInProfile", "Collection has no plugins!");
public static string AlwaysEnableAtBoot =>
Loc.Localize("ProfileManagerAlwaysEnableAtBoot", "Always enable when game starts");
public static string DeleteProfileHint => Loc.Localize("ProfileManagerDeleteProfile", "Delete this collection");
public static string CopyToClipboardHint =>
@ -647,5 +679,22 @@ internal class ProfileManagerWidget
public static string NotInstalled(string name) =>
Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name);
public static string PolicyToLocalisedName(ProfileModelV1.ProfileStartupPolicy policy)
{
return policy switch
{
ProfileModelV1.ProfileStartupPolicy.RememberState => Loc.Localize(
"ProfileManagerRememberState",
"Remember state"),
ProfileModelV1.ProfileStartupPolicy.AlwaysEnable => Loc.Localize(
"ProfileManagerAlwaysEnableAtBoot",
"Always enable at boot"),
ProfileModelV1.ProfileStartupPolicy.AlwaysDisable => Loc.Localize(
"ProfileManagerAlwaysDisableAtBoot",
"Always disable at boot"),
_ => throw new ArgumentOutOfRangeException(nameof(policy), policy, null),
};
}
}
}

View file

@ -9,6 +9,7 @@ using Dalamud.Hooking.Internal;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
@ -44,51 +45,54 @@ internal class PluginStatWindow : Window
{
var pluginManager = Service<PluginManager>.Get();
if (!ImGui.BeginTabBar("Stat Tabs"))
using var tabBar = ImRaii.TabBar("Stat Tabs");
if (!tabBar)
return;
if (ImGui.BeginTabItem("Draw times"))
using (var tabItem = ImRaii.TabItem("Draw times"))
{
var doStats = UiBuilder.DoStats;
if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats))
if (tabItem)
{
UiBuilder.DoStats = doStats;
}
var doStats = UiBuilder.DoStats;
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats))
{
foreach (var plugin in pluginManager.InstalledPlugins)
{
if (plugin.DalamudInterface != null)
{
plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear();
}
}
UiBuilder.DoStats = doStats;
}
var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded);
var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0);
var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0);
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
{
foreach (var plugin in pluginManager.InstalledPlugins)
{
if (plugin.DalamudInterface != null)
{
plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear();
}
}
}
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times");
var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded);
var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0);
var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0);
ImGui.InputTextWithHint(
"###PluginStatWindow_DrawSearch",
"Search",
ref this.drawSearchText,
500);
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times");
if (ImGui.BeginTable(
ImGui.InputTextWithHint(
"###PluginStatWindow_DrawSearch",
"Search",
ref this.drawSearchText,
500);
using var table = ImRaii.Table(
"##PluginStatsDrawTimes",
4,
ImGuiTableFlags.RowBg
@ -97,99 +101,100 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable))
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Plugin");
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort
ImGui.TableSetupColumn("Longest");
ImGui.TableSetupColumn("Average");
ImGui.TableHeadersRow();
| ImGuiTableFlags.Hideable);
var sortSpecs = ImGui.TableGetSortSpecs();
loadedPlugins = sortSpecs.Specs.ColumnIndex switch
if (table)
{
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.Name)
: loadedPlugins.OrderByDescending(plugin => plugin.Name),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0),
_ => loadedPlugins,
};
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Plugin");
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort
ImGui.TableSetupColumn("Longest");
ImGui.TableSetupColumn("Average");
ImGui.TableHeadersRow();
foreach (var plugin in loadedPlugins)
{
if (!this.drawSearchText.IsNullOrEmpty()
&& !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase))
var sortSpecs = ImGui.TableGetSortSpecs();
loadedPlugins = sortSpecs.Specs.ColumnIndex switch
{
continue;
}
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.Name)
: loadedPlugins.OrderByDescending(plugin => plugin.Name),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0),
_ => loadedPlugins,
};
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(plugin.Manifest.Name);
if (plugin.DalamudInterface != null)
foreach (var plugin in loadedPlugins)
{
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms");
if (!this.drawSearchText.IsNullOrEmpty()
&& !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase))
{
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms");
ImGui.Text(plugin.Manifest.Name);
ImGui.TableNextColumn();
ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0
? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms"
: "-");
if (plugin.DalamudInterface != null)
{
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms");
ImGui.TableNextColumn();
ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0
? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms"
: "-");
}
}
}
ImGui.EndTable();
}
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Framework times"))
using (var tabItem = ImRaii.TabItem("Framework times"))
{
var doStats = Framework.StatsEnabled;
if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats))
if (tabItem)
{
Framework.StatsEnabled = doStats;
}
var doStats = Framework.StatsEnabled;
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats))
{
Framework.StatsHistory.Clear();
Framework.StatsEnabled = doStats;
}
var statsHistory = Framework.StatsHistory.ToArray();
var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault());
var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average());
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
{
Framework.StatsHistory.Clear();
}
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times");
var statsHistory = Framework.StatsHistory.ToArray();
var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault());
var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average());
ImGui.InputTextWithHint(
"###PluginStatWindow_FrameworkSearch",
"Search",
ref this.frameworkSearchText,
500);
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times");
if (ImGui.BeginTable(
ImGui.InputTextWithHint(
"###PluginStatWindow_FrameworkSearch",
"Search",
ref this.frameworkSearchText,
500);
using var table = ImRaii.Table(
"##PluginStatsFrameworkTimes",
4,
ImGuiTableFlags.RowBg
@ -198,77 +203,77 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable))
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort
ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50);
ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50);
ImGui.TableHeadersRow();
var sortSpecs = ImGui.TableGetSortSpecs();
statsHistory = sortSpecs.Specs.ColumnIndex switch
| ImGuiTableFlags.Hideable);
if (table)
{
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Key).ToArray()
: statsHistory.OrderByDescending(handler => handler.Key).ToArray(),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(),
_ => statsHistory,
};
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort
ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50);
ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50);
ImGui.TableHeadersRow();
foreach (var handlerHistory in statsHistory)
{
if (!handlerHistory.Value.Any())
var sortSpecs = ImGui.TableGetSortSpecs();
statsHistory = sortSpecs.Specs.ColumnIndex switch
{
continue;
}
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Key).ToArray()
: statsHistory.OrderByDescending(handler => handler.Key).ToArray(),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(),
_ => statsHistory,
};
if (!this.frameworkSearchText.IsNullOrEmpty()
&& handlerHistory.Key != null
&& !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase))
foreach (var handlerHistory in statsHistory)
{
continue;
if (!handlerHistory.Value.Any())
{
continue;
}
if (!this.frameworkSearchText.IsNullOrEmpty()
&& handlerHistory.Key != null
&& !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase))
{
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Key}");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Last():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Max():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Average():F4}ms");
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Key}");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Last():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Max():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Average():F4}ms");
}
ImGui.EndTable();
}
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Hooks"))
using (var tabItem = ImRaii.TabItem("Hooks"))
{
ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks);
if (tabItem)
{
ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks);
ImGui.InputTextWithHint(
"###PluginStatWindow_HookSearch",
"Search",
ref this.hookSearchText,
500);
ImGui.InputTextWithHint(
"###PluginStatWindow_HookSearch",
"Search",
ref this.hookSearchText,
500);
if (ImGui.BeginTable(
using var table = ImRaii.Table(
"##PluginStatsHooks",
4,
ImGuiTableFlags.RowBg
@ -276,80 +281,78 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable))
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100);
ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40);
ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40);
ImGui.TableHeadersRow();
foreach (var (guid, trackedHook) in HookManager.TrackedHooks)
| ImGuiTableFlags.Hideable);
if (table)
{
try
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100);
ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40);
ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40);
ImGui.TableHeadersRow();
foreach (var (guid, trackedHook) in HookManager.TrackedHooks)
{
if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly())
continue;
if (!this.hookSearchText.IsNullOrEmpty())
try
{
if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
&& !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly())
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}");
ImGui.TextDisabled(trackedHook.Assembly.FullName);
ImGui.TableNextColumn();
if (!trackedHook.Hook.IsDisposed)
{
if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}"))
if (!this.hookSearchText.IsNullOrEmpty())
{
ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
Service<NotificationManager>.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success);
if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
&& !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
continue;
}
var processMemoryOffset = trackedHook.InProcessMemory;
if (processMemoryOffset.HasValue)
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}");
ImGui.TextDisabled(trackedHook.Assembly.FullName);
ImGui.TableNextColumn();
if (!trackedHook.Hook.IsDisposed)
{
if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}"))
if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}"))
{
ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
Service<NotificationManager>.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success);
ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
Service<NotificationManager>.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success);
}
var processMemoryOffset = trackedHook.InProcessMemory;
if (processMemoryOffset.HasValue)
{
if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}"))
{
ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
Service<NotificationManager>.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success);
}
}
}
ImGui.TableNextColumn();
if (trackedHook.Hook.IsDisposed)
{
ImGui.Text("Disposed");
}
else
{
ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled");
}
ImGui.TableNextColumn();
ImGui.Text(trackedHook.Hook.BackendName);
}
ImGui.TableNextColumn();
if (trackedHook.Hook.IsDisposed)
catch (Exception ex)
{
ImGui.Text("Disposed");
Log.Error(ex, "Error drawing hooks in plugin stats");
}
else
{
ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled");
}
ImGui.TableNextColumn();
ImGui.Text(trackedHook.Hook.BackendName);
}
catch (Exception ex)
{
Log.Error(ex, "Error drawing hooks in plugin stats");
}
}
ImGui.EndTable();
}
}
ImGui.EndTabBar();
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
@ -54,6 +55,7 @@ internal class SelfTestWindow : Window
new SheetRedirectResolverSelfTestStep(),
new NounProcessorSelfTestStep(),
new SeStringEvaluatorSelfTestStep(),
new CompletionSelfTestStep(),
new LogoutEventSelfTestStep()
];
@ -61,7 +63,7 @@ internal class SelfTestWindow : Window
private bool selfTestRunning = false;
private int currentStep = 0;
private int scrollToStep = -1;
private DateTimeOffset lastTestStart;
/// <summary>
@ -90,9 +92,10 @@ internal class SelfTestWindow : Window
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
{
this.testIndexToResult.Add(this.currentStep, (SelfTestStepResult.NotRan, null));
this.testIndexToResult[this.currentStep] = (SelfTestStepResult.NotRan, null);
this.steps[this.currentStep].CleanUp();
this.currentStep++;
this.scrollToStep = this.currentStep;
this.lastTestStart = DateTimeOffset.Now;
if (this.currentStep >= this.steps.Count)
@ -107,6 +110,7 @@ internal class SelfTestWindow : Window
{
this.selfTestRunning = true;
this.currentStep = 0;
this.scrollToStep = this.currentStep;
this.testIndexToResult.Clear();
this.lastTestStart = DateTimeOffset.Now;
}
@ -116,11 +120,11 @@ internal class SelfTestWindow : Window
ImGui.TextUnformatted($"Step: {this.currentStep} / {this.steps.Count}");
ImGuiHelpers.ScaledDummy(10);
ImGui.Spacing();
this.DrawResultTable();
ImGuiHelpers.ScaledDummy(10);
ImGui.Spacing();
if (this.currentStep >= this.steps.Count)
{
@ -131,11 +135,11 @@ internal class SelfTestWindow : Window
if (this.testIndexToResult.Any(x => x.Value.Result == SelfTestStepResult.Fail))
{
ImGui.TextColored(ImGuiColors.DalamudRed, "One or more checks failed!");
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "One or more checks failed!");
}
else
{
ImGui.TextColored(ImGuiColors.HealerGreen, "All checks passed!");
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "All checks passed!");
}
return;
@ -146,7 +150,8 @@ internal class SelfTestWindow : Window
return;
}
ImGui.Separator();
using var resultChild = ImRaii.Child("SelfTestResultChild", ImGui.GetContentRegionAvail());
if (!resultChild) return;
var step = this.steps[this.currentStep];
ImGui.TextUnformatted($"Current: {step.Name}");
@ -164,13 +169,12 @@ internal class SelfTestWindow : Window
result = SelfTestStepResult.Fail;
}
ImGui.Separator();
if (result != SelfTestStepResult.Waiting)
{
var duration = DateTimeOffset.Now - this.lastTestStart;
this.testIndexToResult.Add(this.currentStep, (result, duration));
this.testIndexToResult[this.currentStep] = (result, duration);
this.currentStep++;
this.scrollToStep = this.currentStep;
this.lastTestStart = DateTimeOffset.Now;
}
@ -178,90 +182,111 @@ internal class SelfTestWindow : Window
private void DrawResultTable()
{
if (ImGui.BeginTable("agingResultTable", 5, ImGuiTableFlags.Borders))
var tableSize = ImGui.GetContentRegionAvail();
if (this.selfTestRunning)
tableSize -= new Vector2(0, 200);
tableSize.Y = Math.Min(tableSize.Y, ImGui.GetWindowViewport().Size.Y * 0.5f);
using var table = ImRaii.Table("agingResultTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, tableSize);
if (!table)
return;
ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Name");
ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
for (var i = 0; i < this.steps.Count; i++)
{
ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Name");
ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale);
var step = this.steps[i];
ImGui.TableNextRow();
ImGui.TableHeadersRow();
for (var i = 0; i < this.steps.Count; i++)
if (this.selfTestRunning && this.currentStep == i)
{
var step = this.steps[i];
ImGui.TableNextRow();
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(ImGuiCol.TableRowBgAlt));
}
ImGui.TableSetColumnIndex(0);
ImGui.Text(i.ToString());
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(i.ToString());
ImGui.TableSetColumnIndex(1);
ImGui.Text(step.Name);
if (this.selfTestRunning && this.scrollToStep == i)
{
ImGui.SetScrollHereY();
this.scrollToStep = -1;
}
if (this.testIndexToResult.TryGetValue(i, out var result))
ImGui.TableSetColumnIndex(1);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(step.Name);
if (this.testIndexToResult.TryGetValue(i, out var result))
{
ImGui.TableSetColumnIndex(2);
ImGui.AlignTextToFramePadding();
switch (result.Result)
{
ImGui.TableSetColumnIndex(2);
ImGui.PushFont(InterfaceManager.MonoFont);
case SelfTestStepResult.Pass:
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "PASS");
break;
case SelfTestStepResult.Fail:
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "FAIL");
break;
default:
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR");
break;
}
switch (result.Result)
{
case SelfTestStepResult.Pass:
ImGui.TextColored(ImGuiColors.HealerGreen, "PASS");
break;
case SelfTestStepResult.Fail:
ImGui.TextColored(ImGuiColors.DalamudRed, "FAIL");
break;
default:
ImGui.TextColored(ImGuiColors.DalamudGrey, "NR");
break;
}
ImGui.PopFont();
ImGui.TableSetColumnIndex(3);
if (result.Duration.HasValue)
{
ImGui.TextUnformatted(result.Duration.Value.ToString("g"));
}
ImGui.TableSetColumnIndex(3);
if (result.Duration.HasValue)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(this.FormatTimeSpan(result.Duration.Value));
}
}
else
{
ImGui.TableSetColumnIndex(2);
ImGui.AlignTextToFramePadding();
if (this.selfTestRunning && this.currentStep == i)
{
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "WAIT");
}
else
{
ImGui.TableSetColumnIndex(2);
if (this.selfTestRunning && this.currentStep == i)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "WAIT");
}
else
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "NR");
}
ImGui.TableSetColumnIndex(3);
if (this.selfTestRunning && this.currentStep == i)
{
ImGui.TextUnformatted((DateTimeOffset.Now - this.lastTestStart).ToString("g"));
}
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR");
}
ImGui.TableSetColumnIndex(4);
using var id = ImRaii.PushId($"selfTest{i}");
if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward))
ImGui.TableSetColumnIndex(3);
ImGui.AlignTextToFramePadding();
if (this.selfTestRunning && this.currentStep == i)
{
this.StopTests();
this.testIndexToResult.Remove(i);
this.currentStep = i;
this.selfTestRunning = true;
this.lastTestStart = DateTimeOffset.Now;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Jump to this test");
ImGui.TextUnformatted(this.FormatTimeSpan(DateTimeOffset.Now - this.lastTestStart));
}
}
ImGui.EndTable();
ImGui.TableSetColumnIndex(4);
using var id = ImRaii.PushId($"selfTest{i}");
if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward))
{
this.StopTests();
this.testIndexToResult.Remove(i);
this.currentStep = i;
this.selfTestRunning = true;
this.lastTestStart = DateTimeOffset.Now;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Jump to this test");
}
}
}
@ -281,4 +306,11 @@ internal class SelfTestWindow : Window
}
}
}
private string FormatTimeSpan(TimeSpan ts)
{
var str = ts.ToString("g", CultureInfo.InvariantCulture);
var commaPos = str.LastIndexOf('.');
return commaPos > -1 && commaPos + 5 < str.Length ? str[..(commaPos + 5)] : str;
}
}

View file

@ -0,0 +1,94 @@
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
/// <summary>
/// Test setup for Chat.
/// </summary>
internal class CompletionSelfTestStep : ISelfTestStep
{
private int step = 0;
private bool registered;
private bool commandRun;
/// <inheritdoc/>
public string Name => "Test Completion";
/// <inheritdoc/>
public SelfTestStepResult RunStep()
{
var cmdManager = Service<CommandManager>.Get();
switch (this.step)
{
case 0:
this.step++;
break;
case 1:
ImGui.Text("[Chat Log]");
ImGui.TextWrapped("Use the category menus to navigate to [Dalamud], then complete a command from the list. Did it work?");
if (ImGui.Button("Yes"))
this.step++;
ImGui.SameLine();
if (ImGui.Button("No"))
return SelfTestStepResult.Fail;
break;
case 2:
ImGui.Text("[Chat Log]");
ImGui.Text("Type /xl into the chat log and tab-complete a dalamud command. Did it work?");
if (ImGui.Button("Yes"))
this.step++;
ImGui.SameLine();
if (ImGui.Button("No"))
return SelfTestStepResult.Fail;
break;
case 3:
ImGui.Text("[Chat Log]");
if (!this.registered)
{
cmdManager.AddHandler("/xlselftestcompletion", new CommandInfo((_, _) => this.commandRun = true));
this.registered = true;
}
ImGui.Text("Tab-complete /xlselftestcompletion in the chat log and send the command");
if (this.commandRun)
this.step++;
break;
case 4:
ImGui.Text("[Other text inputs]");
ImGui.Text("Open the party finder recruitment criteria dialog and try to tab-complete /xldev in the text box.");
ImGui.Text("Did the command appear in the text box? (It should not have)");
if (ImGui.Button("Yes"))
return SelfTestStepResult.Fail;
ImGui.SameLine();
if (ImGui.Button("No"))
this.step++;
break;
case 5:
return SelfTestStepResult.Pass;
}
return SelfTestStepResult.Waiting;
}
/// <inheritdoc/>
public void CleanUp()
{
Service<CommandManager>.Get().RemoveHandler("/completionselftest");
}
}

View file

@ -145,7 +145,7 @@ internal class ContextMenuSelfTestStep : ISelfTestStep
var targetItem = (a.Target as MenuTargetInventory)!.TargetItem;
if (targetItem is { } item)
{
name = (this.itemSheet.GetRowOrDefault(item.ItemId)?.Name.ExtractText() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
name = (this.itemSheet.GetRowOrDefault(item.BaseItemId)?.Name.ExtractText() ?? $"Unknown ({item.BaseItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
count = item.Quantity;
}
else

View file

@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
/// Test setup for Lumina.
/// </summary>
/// <typeparam name="T">ExcelRow to run test on.</typeparam>
/// <param name="isLargeSheet">Whether or not the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken.</param>
/// <param name="isLargeSheet">Whether the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken.</param>
internal class LuminaSelfTestStep<T>(bool isLargeSheet) : ISelfTestStep
where T : struct, IExcelRow<T>
{

View file

@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Linq;
using Dalamud.Bindings.ImGui;
@ -109,7 +109,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
}
else
{
ImGui.Text("Does this information match the purchase you made? This is testing the request to the server.");
ImGui.TextWrapped("Does this information match the purchase you made? This is testing the request to the server.");
ImGui.Separator();
ImGui.Text($"Quantity: {this.marketBoardPurchaseRequest.ItemQuantity.ToString()}");
ImGui.Text($"Item ID: {this.marketBoardPurchaseRequest.CatalogId}");
@ -135,7 +135,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
}
else
{
ImGui.Text("Does this information match the purchase you made? This is testing the response from the server.");
ImGui.TextWrapped("Does this information match the purchase you made? This is testing the response from the server.");
ImGui.Separator();
ImGui.Text($"Quantity: {this.marketBoardPurchase.ItemQuantity.ToString()}");
ImGui.Text($"Item ID: {this.marketBoardPurchase.CatalogId}");
@ -156,7 +156,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
case SubStep.Taxes:
if (this.marketTaxRate == null)
{
ImGui.Text("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item.");
ImGui.TextWrapped("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item.");
}
else
{

View file

@ -61,6 +61,10 @@ internal class SheetRedirectResolverSelfTestStep : ISelfTestStep
new("WeatherPlaceName", 40),
new("WeatherPlaceName", 52),
new("WeatherPlaceName", 2300),
new("InstanceContent", 1),
new("PartyContent", 2),
new("PublicContent", 1),
new("AkatsukiNote", 1),
];
private unsafe delegate SheetRedirectFlags ResolveSheetRedirect(RaptureTextModule* thisPtr, Utf8String* sheetName, uint* rowId, uint* flags);

View file

@ -11,12 +11,12 @@ public abstract class SettingsEntry
public string? Name { get; protected set; }
/// <summary>
/// Gets or sets a value indicating whether or not this entry is valid.
/// Gets or sets a value indicating whether this entry is valid.
/// </summary>
public virtual bool IsValid { get; protected set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not this entry is visible.
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
public virtual bool IsVisible { get; protected set; } = true;
@ -54,7 +54,7 @@ public abstract class SettingsEntry
{
// ignored
}
/// <summary>
/// Function to be called when the tab is closed.
/// </summary>

View file

@ -21,6 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
public class SettingsTabAutoUpdates : SettingsTab
{
private AutoUpdateBehavior behavior;
private bool updateDisabledPlugins;
private bool checkPeriodically;
private bool chatNotification;
private string pickerSearch = string.Empty;
@ -65,6 +66,7 @@ public class SettingsTabAutoUpdates : SettingsTab
ImGuiHelpers.ScaledDummy(8);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateDisabledPlugins", "Auto-Update plugins that are currently disabled"), ref this.updateDisabledPlugins);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint",
@ -236,6 +238,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get();
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
this.updateDisabledPlugins = configuration.UpdateDisabledPlugins;
this.chatNotification = configuration.SendUpdateNotificationToChat;
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
@ -248,6 +251,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.behavior;
configuration.UpdateDisabledPlugins = this.updateDisabledPlugins;
configuration.SendUpdateNotificationToChat = this.chatNotification;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;

View file

@ -60,7 +60,8 @@ public class SettingsTabExperimental : SettingsTab
"Enable ImGui asserts"),
Loc.Localize(
"DalamudSettingEnableImGuiAssertsHint",
"If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins."),
"If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins. " +
"This setting does not persist and will reset when the game restarts.\nUse the setting below to enable it at startup."),
c => Service<InterfaceManager>.Get().ShowAsserts,
(v, _) => Service<InterfaceManager>.Get().ShowAsserts = v),

View file

@ -12,6 +12,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.ManagedFontAtlas.Internals;
@ -38,7 +39,7 @@ public class SettingsTabLook : SettingsTab
private IFontSpec defaultFontSpec = null!;
public override SettingsEntry[] Entries { get; } =
{
[
new GapSettingsEntry(5, true),
new ButtonSettingsEntry(
@ -46,6 +47,11 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."),
() => Service<DalamudInterface>.Get().OpenStyleEditor()),
new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsOpenNotificationEditor", "Modify Notification Position"),
Loc.Localize("DalamudSettingsNotificationEditorHint", "Choose where Dalamud notifications appear on the screen."),
() => Service<NotificationManager>.Get().StartPositionChooser()),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingsUseDarkMode", "Use Windows immersive/dark mode"),
Loc.Localize("DalamudSettingsUseDarkModeHint", "This will cause the FFXIV window title bar to follow your preferred Windows color settings, and switch to dark mode if enabled."),
@ -167,8 +173,8 @@ public class SettingsTabLook : SettingsTab
ImGui.TextUnformatted("\uE020\uE021\uE022\uE023\uE024\uE025\uE026\uE027");
ImGui.PopStyleVar(1);
},
},
};
}
];
public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel");

View file

@ -51,7 +51,7 @@ public class DevPluginsSettingsEntry : SettingsEntry
if (this.devPluginLocationsChanged)
{
Service<PluginManager>.Get().ScanDevPlugins();
_ = Service<PluginManager>.Get().ScanDevPluginsAsync();
this.devPluginLocationsChanged = false;
}
}

View file

@ -148,6 +148,10 @@ internal class TitleScreenMenuWindow : Window, IDisposable
{
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0));
if (this.state == State.Show)
ImGui.SetNextWindowFocus();
base.PreDraw();
}