Merge branch 'master' into api11

# Conflicts:
#	Dalamud/Game/ClientState/Fates/Fate.cs
#	Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs
#	Dalamud/Interface/Internal/DalamudInterface.cs
#	Dalamud/Interface/Windowing/Window.cs
#	Dalamud/Utility/StringExtensions.cs
#	lib/FFXIVClientStructs
This commit is contained in:
Kaz Wolfe 2024-11-11 08:38:20 -08:00
commit 720b1676e5
No known key found for this signature in database
GPG key ID: 258813F53A16EBB4
49 changed files with 4573 additions and 93 deletions

View file

@ -52,7 +52,7 @@ public enum AddonEvent
PostDraw,
/// <summary>
/// An event that is fired immediately before an addon is finalized via <see cref="AtkUnitBase.Finalize"/> and
/// An event that is fired immediately before an addon is finalized via <see cref="AtkUnitBase.Finalizer"/> and
/// destroyed. After this event, the addon will destruct its UI node data as well as free any allocated memory.
/// This event can be used for cleanup and tracking tasks.
/// </summary>

View file

@ -71,7 +71,7 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a EXP bonus.
/// </summary>
[Obsolete("Use HasBonus instead")]
[Obsolete($"Use {nameof(HasBonus)} instead")]
bool HasExpBonus { get; }
/// <summary>
@ -222,7 +222,7 @@ internal unsafe partial class Fate : IFate
public byte Progress => this.Struct->Progress;
/// <inheritdoc/>
[Obsolete("Use HasBonus instead")]
[Obsolete($"Use {nameof(HasBonus)} instead")]
public bool HasExpBonus => this.Struct->IsExpBonus;
/// <inheritdoc/>

View file

@ -1,14 +1,16 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
namespace Dalamud.Game.Gui.NamePlate;
@ -38,38 +40,44 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// </summary>
internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
private readonly AddonLifecycleEventListener preRequestedUpdateListener;
private readonly NamePlateGuiAddressResolver address;
private readonly Hook<AtkUnitBase.Delegates.OnRequestedUpdate> onRequestedUpdateHook;
private NamePlateUpdateContext? context;
private NamePlateUpdateHandler[] updateHandlers = [];
[ServiceManager.ServiceConstructor]
private NamePlateGui()
private unsafe NamePlateGui(TargetSigScanner sigScanner)
{
this.preRequestedUpdateListener = new AddonLifecycleEventListener(
AddonEvent.PreRequestedUpdate,
"NamePlate",
this.OnPreRequestedUpdate);
this.address = new NamePlateGuiAddressResolver();
this.address.Setup(sigScanner);
this.addonLifecycle.RegisterListener(this.preRequestedUpdateListener);
this.onRequestedUpdateHook = Hook<AtkUnitBase.Delegates.OnRequestedUpdate>.FromAddress(
this.address.OnRequestedUpdate,
this.OnRequestedUpdateDetour);
this.onRequestedUpdateHook.Enable();
}
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate;
/// <inheritdoc/>
public unsafe void RequestRedraw()
{
@ -91,7 +99,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener);
this.onRequestedUpdateHook.Dispose();
}
/// <summary>
@ -144,65 +152,124 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
this.updateHandlers = handlers.ToArray();
}
private void OnPreRequestedUpdate(AddonEvent type, AddonArgs args)
private unsafe void OnRequestedUpdateDetour(
AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null)
{
return;
}
var calledOriginal = false;
var reqArgs = (AddonRequestedUpdateArgs)args;
if (this.context == null)
try
{
this.context = new NamePlateUpdateContext(this.objectTable, reqArgs);
this.CreateHandlers(this.context);
}
else
{
this.context.ResetState(reqArgs);
}
var activeNamePlateCount = this.context.ActiveNamePlateCount;
if (activeNamePlateCount == 0)
return;
var activeHandlers = this.updateHandlers[..activeNamePlateCount];
if (this.context.IsFullUpdate)
{
foreach (var handler in activeHandlers)
if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null && this.OnPostDataUpdate == null &&
this.OnPostNamePlateUpdate == null)
{
handler.ResetState();
return;
}
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
if (this.context.HasParts)
this.ApplyBuilders(activeHandlers);
}
else
{
var udpatedHandlers = new List<NamePlateUpdateHandler>(activeNamePlateCount);
foreach (var handler in activeHandlers)
if (this.context == null)
{
handler.ResetState();
if (handler.IsUpdating)
udpatedHandlers.Add(handler);
this.context = new NamePlateUpdateContext(this.objectTable);
this.CreateHandlers(this.context);
}
if (this.OnDataUpdate is not null)
this.context.ResetState(addon, numberArrayData, stringArrayData);
var activeNamePlateCount = this.context!.ActiveNamePlateCount;
if (activeNamePlateCount == 0)
return;
var activeHandlers = this.updateHandlers[..activeNamePlateCount];
if (this.context.IsFullUpdate)
{
foreach (var handler in activeHandlers)
{
handler.ResetState();
}
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
if (this.context.HasParts)
this.ApplyBuilders(activeHandlers);
try
{
calledOriginal = true;
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
}
this.OnPostNamePlateUpdate?.Invoke(this.context, activeHandlers);
this.OnPostDataUpdate?.Invoke(this.context, activeHandlers);
}
else if (udpatedHandlers.Count != 0)
else
{
var changedHandlersSpan = udpatedHandlers.ToArray().AsSpan();
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
if (this.context.HasParts)
this.ApplyBuilders(changedHandlersSpan);
var updatedHandlers = new List<NamePlateUpdateHandler>(activeNamePlateCount);
foreach (var handler in activeHandlers)
{
handler.ResetState();
if (handler.IsUpdating)
updatedHandlers.Add(handler);
}
if (this.OnDataUpdate is not null)
{
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
this.OnNamePlateUpdate?.Invoke(this.context, updatedHandlers);
if (this.context.HasParts)
this.ApplyBuilders(activeHandlers);
try
{
calledOriginal = true;
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
}
this.OnPostNamePlateUpdate?.Invoke(this.context, updatedHandlers);
this.OnPostDataUpdate?.Invoke(this.context, activeHandlers);
}
else if (updatedHandlers.Count != 0)
{
this.OnNamePlateUpdate?.Invoke(this.context, updatedHandlers);
if (this.context.HasParts)
this.ApplyBuilders(updatedHandlers);
try
{
calledOriginal = true;
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
}
this.OnPostNamePlateUpdate?.Invoke(this.context, updatedHandlers);
this.OnPostDataUpdate?.Invoke(this.context, activeHandlers);
}
}
}
finally
{
if (!calledOriginal)
{
try
{
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
}
}
}
}
@ -217,6 +284,17 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
}
}
}
private void ApplyBuilders(List<NamePlateUpdateHandler> handlers)
{
foreach (var handler in handlers)
{
if (handler.PartsContainer is { } container)
{
container.ApplyBuilders(handler);
}
}
}
}
/// <summary>
@ -239,6 +317,7 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
{
if (this.OnNamePlateUpdateScoped == null)
this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward;
this.OnNamePlateUpdateScoped += value;
}
@ -250,6 +329,25 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
}
}
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate
{
add
{
if (this.OnPostNamePlateUpdateScoped == null)
this.parentService.OnPostNamePlateUpdate += this.OnPostNamePlateUpdateForward;
this.OnPostNamePlateUpdateScoped += value;
}
remove
{
this.OnPostNamePlateUpdateScoped -= value;
if (this.OnPostNamePlateUpdateScoped == null)
this.parentService.OnPostNamePlateUpdate -= this.OnPostNamePlateUpdateForward;
}
}
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate
{
@ -257,6 +355,7 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
{
if (this.OnDataUpdateScoped == null)
this.parentService.OnDataUpdate += this.OnDataUpdateForward;
this.OnDataUpdateScoped += value;
}
@ -268,10 +367,33 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
}
}
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate
{
add
{
if (this.OnPostDataUpdateScoped == null)
this.parentService.OnPostDataUpdate += this.OnPostDataUpdateForward;
this.OnPostDataUpdateScoped += value;
}
remove
{
this.OnPostDataUpdateScoped -= value;
if (this.OnPostDataUpdateScoped == null)
this.parentService.OnPostDataUpdate -= this.OnPostDataUpdateForward;
}
}
private event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdateScoped;
/// <inheritdoc/>
public void RequestRedraw()
{
@ -284,8 +406,14 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
this.OnNamePlateUpdateScoped = null;
this.parentService.OnPostNamePlateUpdate -= this.OnPostNamePlateUpdateForward;
this.OnPostNamePlateUpdateScoped = null;
this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
this.OnDataUpdateScoped = null;
this.parentService.OnPostDataUpdate -= this.OnPostDataUpdateForward;
this.OnPostDataUpdateScoped = null;
}
private void OnNamePlateUpdateForward(
@ -294,9 +422,21 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
this.OnNamePlateUpdateScoped?.Invoke(context, handlers);
}
private void OnPostNamePlateUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
this.OnPostNamePlateUpdateScoped?.Invoke(context, handlers);
}
private void OnDataUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
this.OnDataUpdateScoped?.Invoke(context, handlers);
}
private void OnPostDataUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
this.OnPostDataUpdateScoped?.Invoke(context, handlers);
}
}

View file

@ -0,0 +1,20 @@
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// An address resolver for the <see cref="NamePlateGui"/> class.
/// </summary>
internal class NamePlateGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the AddonNamePlate OnRequestedUpdate method. We need to use a hook for this because
/// AddonNamePlate.Show calls OnRequestedUpdate directly, bypassing the AddonLifecycle callsite hook.
/// </summary>
public IntPtr OnRequestedUpdate { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.OnRequestedUpdate = sig.ScanText(
"4C 8B DC 41 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B 40 20");
}
}

View file

@ -1,4 +1,3 @@
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.UI;
@ -54,13 +53,11 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
/// Initializes a new instance of the <see cref="NamePlateUpdateContext"/> class.
/// </summary>
/// <param name="objectTable">An object table.</param>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args)
internal NamePlateUpdateContext(ObjectTable objectTable)
{
this.ObjectTable = objectTable;
this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance();
this.Ui3DModule = UIModule.Instance()->GetUI3DModule();
this.ResetState(args);
}
/// <summary>
@ -137,13 +134,15 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
/// <summary>
/// Resets the state of the context based on the provided addon lifecycle arguments.
/// </summary>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
internal void ResetState(AddonRequestedUpdateArgs args)
/// <param name="addon">A pointer to the addon.</param>
/// <param name="numberArrayData">A pointer to the global number array data struct.</param>
/// <param name="stringArrayData">A pointer to the global string array data struct.</param>
public void ResetState(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
this.Addon = (AddonNamePlate*)args.Addon;
this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex];
this.Addon = (AddonNamePlate*)addon;
this.NumberData = numberArrayData[NamePlateGui.NumberArrayIndex];
this.NumberStruct = (AddonNamePlate.AddonNamePlateNumberArray*)this.NumberData->IntArray;
this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex];
this.StringData = stringArrayData[NamePlateGui.StringArrayIndex];
this.HasParts = false;
this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;

View file

@ -403,6 +403,9 @@ internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInve
/// <inheritdoc/>
public event IGameInventory.InventoryChangedDelegate<InventoryItemMergedArgs>? ItemMergedExplicit;
/// <inheritdoc/>
public ReadOnlySpan<GameInventoryItem> GetInventoryItems(GameInventoryType type) => GameInventoryItem.GetReadOnlySpanOfInventory(type);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{

View file

@ -9,6 +9,7 @@ using CheapLoc;
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;
@ -106,7 +107,8 @@ internal class DalamudInterface : IInternalDisposableService
ClientState clientState,
TitleScreenMenu titleScreenMenu,
GameGui gameGui,
ConsoleManager consoleManager)
ConsoleManager consoleManager,
AddonLifecycle addonLifecycle)
{
this.dalamud = dalamud;
this.configuration = configuration;
@ -133,7 +135,8 @@ internal class DalamudInterface : IInternalDisposableService
framework,
gameGui,
titleScreenMenu,
consoleManager) { IsOpen = false };
consoleManager,
addonLifecycle) { IsOpen = false };
this.changelogWindow = new ChangelogWindow(
this.titleScreenMenuWindow,
fontAtlasFactory,

View file

@ -1,7 +1,9 @@
using System.Numerics;
using Dalamud.Game;
using Dalamud.Game.Gui;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@ -317,6 +319,32 @@ internal unsafe class UiDebug
ImGui.TreePop();
}
}
if (ImGui.Button($"Replace with a random image##{(ulong)textureInfo:X}"))
{
var texm = Service<TextureManager>.Get();
texm.Shared
.GetFromGame(
Random.Shared.Next(0, 1) == 0
? $"ui/loadingimage/-nowloading_base{Random.Shared.Next(1, 33)}.tex"
: $"ui/loadingimage/-nowloading_base{Random.Shared.Next(1, 33)}_hr1.tex")
.RentAsync()
.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(
() =>
{
if (!r.IsCompletedSuccessfully)
return;
using (r.Result)
{
textureInfo->AtkTexture.ReleaseTexture();
textureInfo->AtkTexture.KernelTexture =
texm.ConvertToKernelTexture(r.Result);
textureInfo->AtkTexture.TextureType = TextureType.KernelTexture;
}
}));
}
}
}
else

View file

@ -0,0 +1,195 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using FFXIVClientStructs.Attributes;
using FFXIVClientStructs.FFXIV.Component.GUI;
using static System.Reflection.BindingFlags;
using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <inheritdoc cref="AddonTree"/>
public unsafe partial class AddonTree
{
private static readonly Dictionary<string, Type?> AddonTypeDict = [];
private static readonly Assembly? ClientStructsAssembly = typeof(Addon).Assembly;
/// <summary>
/// Gets or sets a collection of names for field offsets that have been documented in FFXIVClientStructs.
/// </summary>
internal Dictionary<nint, List<string>> FieldNames { get; set; } = [];
private object? GetAddonObj(AtkUnitBase* addon)
{
if (addon == null)
{
return null;
}
if (AddonTypeDict.TryAdd(this.AddonName, null) && ClientStructsAssembly != null)
{
try
{
foreach (var t in from t in ClientStructsAssembly.GetTypes()
where t.IsPublic
let xivAddonAttr = (Addon?)t.GetCustomAttribute(typeof(Addon), false)
where xivAddonAttr != null
where xivAddonAttr.AddonIdentifiers.Contains(this.AddonName)
select t)
{
AddonTypeDict[this.AddonName] = t;
break;
}
}
catch
{
// ignored
}
}
return AddonTypeDict.TryGetValue(this.AddonName, out var result) && result != null ? Marshal.PtrToStructure(new(addon), result) : *addon;
}
private void PopulateFieldNames(nint ptr)
{
this.PopulateFieldNames(this.GetAddonObj((AtkUnitBase*)ptr), ptr);
}
private void PopulateFieldNames(object? obj, nint baseAddr, List<string>? path = null)
{
if (obj == null)
{
return;
}
path ??= [];
var baseType = obj.GetType();
foreach (var field in baseType.GetFields(Static | Public | NonPublic | Instance))
{
if (field.GetCustomAttribute(typeof(FieldOffsetAttribute)) is FieldOffsetAttribute offset)
{
try
{
var fieldAddr = baseAddr + offset.Value;
var name = field.Name[0] == '_' ? char.ToUpperInvariant(field.Name[1]) + field.Name[2..] : field.Name;
var fieldType = field.FieldType;
if (!field.IsStatic && fieldType.IsPointer)
{
var pointer = (nint)Pointer.Unbox((Pointer)field.GetValue(obj)!);
var itemType = fieldType.GetElementType();
ParsePointer(fieldAddr, pointer, itemType, name);
}
else if (fieldType.IsExplicitLayout)
{
ParseExplicitField(fieldAddr, field, fieldType, name);
}
else if (fieldType.Name.Contains("FixedSizeArray"))
{
ParseFixedSizeArray(fieldAddr, fieldType, name);
}
}
catch (Exception ex)
{
Log.Warning($"Failed to parse field at {offset.Value:X} in {baseType}!\n{ex}");
}
}
}
return;
void ParseExplicitField(nint fieldAddr, FieldInfo field, MemberInfo fieldType, string name)
{
try
{
if (this.FieldNames.TryAdd(fieldAddr, [..path, name]) && fieldType.DeclaringType == baseType)
{
this.PopulateFieldNames(field.GetValue(obj), fieldAddr, [..path, name]);
}
}
catch (Exception ex)
{
Log.Warning($"Failed to parse explicit field: {fieldType} {name} in {baseType}!\n{ex}");
}
}
void ParseFixedSizeArray(nint fieldAddr, Type fieldType, string name)
{
try
{
var spanLength = (int)(fieldType.CustomAttributes.ToArray()[0].ConstructorArguments[0].Value ?? 0);
if (spanLength <= 0)
{
return;
}
var itemType = fieldType.UnderlyingSystemType.GenericTypeArguments[0];
if (!itemType.IsGenericType)
{
var size = Marshal.SizeOf(itemType);
for (var i = 0; i < spanLength; i++)
{
var itemAddr = fieldAddr + (size * i);
var itemName = $"{name}[{i}]";
this.FieldNames.TryAdd(itemAddr, [..path, itemName]);
var item = Marshal.PtrToStructure(itemAddr, itemType);
if (itemType.DeclaringType == baseType)
{
this.PopulateFieldNames(item, itemAddr, [..path, itemName]);
}
}
}
else if (itemType.Name.Contains("Pointer"))
{
itemType = itemType.GenericTypeArguments[0];
for (var i = 0; i < spanLength; i++)
{
var itemAddr = fieldAddr + (0x08 * i);
var pointer = Marshal.ReadIntPtr(itemAddr);
ParsePointer(itemAddr, pointer, itemType, $"{name}[{i}]");
}
}
}
catch (Exception ex)
{
Log.Warning($"Failed to parse fixed size array: {fieldType} {name} in {baseType}!\n{ex}");
}
}
void ParsePointer(nint fieldAddr, nint pointer, Type? itemType, string name)
{
try
{
if (pointer == 0)
{
return;
}
this.FieldNames.TryAdd(fieldAddr, [..path, name]);
this.FieldNames.TryAdd(pointer, [..path, name]);
if (itemType?.DeclaringType != baseType || itemType.IsPointer)
{
return;
}
var item = Marshal.PtrToStructure(pointer, itemType);
this.PopulateFieldNames(item, pointer, [..path, name]);
}
catch (Exception ex)
{
Log.Warning($"Failed to parse pointer: {itemType}* {name} in {baseType}!\n{ex}");
}
}
}
}

View file

@ -0,0 +1,242 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Components;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.FontAwesomeIcon;
using static Dalamud.Interface.Internal.UiDebug2.ElementSelector;
using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A class representing an <see cref="AtkUnitBase"/>, allowing it to be browsed within an ImGui window.
/// </summary>
public unsafe partial class AddonTree : IDisposable
{
private readonly nint initialPtr;
private AddonPopoutWindow? window;
private AddonTree(string name, nint ptr)
{
this.AddonName = name;
this.initialPtr = ptr;
this.PopulateFieldNames(ptr);
}
/// <summary>
/// Gets the name of the addon this tree represents.
/// </summary>
internal string AddonName { get; init; }
/// <summary>
/// Gets or sets a collection of trees representing nodes within this addon.
/// </summary>
internal Dictionary<nint, ResNodeTree> NodeTrees { get; set; } = [];
/// <inheritdoc/>
public void Dispose()
{
foreach (var nodeTree in this.NodeTrees)
{
nodeTree.Value.Dispose();
}
this.NodeTrees.Clear();
this.FieldNames.Clear();
AddonTrees.Remove(this.AddonName);
if (this.window != null && PopoutWindows.Windows.Contains(this.window))
{
PopoutWindows.RemoveWindow(this.window);
this.window?.Dispose();
}
GC.SuppressFinalize(this);
}
/// <summary>
/// Gets an instance of <see cref="AddonTree"/> for the given addon name (or creates one if none are found).
/// The tree can then be drawn within the Addon Inspector and browsed.
/// </summary>
/// <param name="name">The name of the addon.</param>
/// <returns>The <see cref="AddonTree"/> for the named addon. Returns null if it does not exist, or if it is not at the expected address.</returns>
internal static AddonTree? GetOrCreate(string? name)
{
if (name == null)
{
return null;
}
try
{
var ptr = GameGui.GetAddonByName(name);
if ((AtkUnitBase*)ptr != null)
{
if (AddonTrees.TryGetValue(name, out var tree))
{
if (tree.initialPtr == ptr)
{
return tree;
}
tree.Dispose();
}
var newTree = new AddonTree(name, ptr);
AddonTrees.Add(name, newTree);
return newTree;
}
}
catch
{
Log.Warning("Couldn't get addon!");
}
return null;
}
/// <summary>
/// Draws this AddonTree within a window.
/// </summary>
internal void Draw()
{
if (!this.ValidateAddon(out var addon))
{
return;
}
var isVisible = addon->IsVisible;
ImGui.TextUnformatted($"{this.AddonName}");
ImGui.SameLine();
ImGui.SameLine();
ImGui.TextColored(isVisible ? new(0.1f, 1f, 0.1f, 1f) : new(0.6f, 0.6f, 0.6f, 1), isVisible ? "Visible" : "Not Visible");
ImGui.SameLine(ImGui.GetWindowWidth() - 100);
if (ImGuiComponents.IconButton($"##vis{(nint)addon:X}", isVisible ? Eye : EyeSlash, isVisible ? new(0.0f, 0.8f, 0.2f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1)))
{
addon->IsVisible = !isVisible;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Toggle Visibility");
}
ImGui.SameLine();
if (ImGuiComponents.IconButton("pop", this.window?.IsOpen == true ? Times : ArrowUpRightFromSquare, null))
{
this.TogglePopout();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Toggle Popout Window");
}
ImGui.Separator();
PrintFieldValuePair("Address", $"{(nint)addon:X}");
var uldManager = addon->UldManager;
PrintFieldValuePairs(
("X", $"{addon->X}"),
("Y", $"{addon->X}"),
("Scale", $"{addon->Scale}"),
("Widget Count", $"{uldManager.ObjectCount}"));
ImGui.Separator();
var addonObj = this.GetAddonObj(addon);
if (addonObj != null)
{
ShowStruct(addonObj, (ulong)addon);
}
ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale));
ImGui.Separator();
ResNodeTree.PrintNodeList(uldManager.NodeList, uldManager.NodeListCount, this);
ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale));
ImGui.Separator();
ResNodeTree.PrintNodeListAsTree(addon->CollisionNodeList, (int)addon->CollisionNodeListCount, "Collision List", this, new(0.5F, 0.7F, 1F, 1F));
if (SearchResults.Length > 0 && Countdown <= 0)
{
SearchResults = [];
}
}
/// <summary>
/// Checks whether a given <see cref="AtkResNode"/> exists somewhere within this <see cref="AddonTree"/>'s associated <see cref="AtkUnitBase"/> (or any of its <see cref="AtkComponentNode"/>s).
/// </summary>
/// <param name="node">The node to check.</param>
/// <returns>true if the node was found.</returns>
internal bool ContainsNode(AtkResNode* node) => this.ValidateAddon(out var addon) && FindNode(node, addon);
private static bool FindNode(AtkResNode* node, AtkUnitBase* addon) => addon != null && FindNode(node, addon->UldManager);
private static bool FindNode(AtkResNode* node, AtkComponentNode* compNode) => compNode != null && FindNode(node, compNode->Component->UldManager);
private static bool FindNode(AtkResNode* node, AtkUldManager uldManager) => FindNode(node, uldManager.NodeList, uldManager.NodeListCount);
private static bool FindNode(AtkResNode* node, AtkResNode** list, int count)
{
for (var i = 0; i < count; i++)
{
var listNode = list[i];
if (listNode == node)
{
return true;
}
if ((int)listNode->Type >= 1000 && FindNode(node, (AtkComponentNode*)listNode))
{
return true;
}
}
return false;
}
/// <summary>
/// Checks whether the addon exists at the expected address. If the addon is null or has a new address, disposes this instance of <see cref="AddonTree"/>.
/// </summary>
/// <param name="addon">The addon, if successfully found.</param>
/// <returns>true if the addon is found.</returns>
private bool ValidateAddon(out AtkUnitBase* addon)
{
addon = (AtkUnitBase*)GameGui.GetAddonByName(this.AddonName);
if (addon == null || (nint)addon != this.initialPtr)
{
this.Dispose();
return false;
}
return true;
}
private void TogglePopout()
{
if (this.window == null)
{
this.window = new AddonPopoutWindow(this, $"{this.AddonName}###addonPopout{this.initialPtr}");
PopoutWindows.AddWindow(this.window);
}
else
{
this.window.IsOpen = !this.window.IsOpen;
}
}
}

View file

@ -0,0 +1,67 @@
using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static ImGuiNET.ImGuiTableColumnFlags;
using static ImGuiNET.ImGuiTableFlags;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// Class that prints the events table for a node, where applicable.
/// </summary>
public static class Events
{
/// <summary>
/// Prints out each <see cref="AtkEventManager.Event"/> for a given node.
/// </summary>
/// <param name="node">The node to print events for.</param>
internal static unsafe void PrintEvents(AtkResNode* node)
{
var evt = node->AtkEventManager.Event;
if (evt == null)
{
return;
}
using var tree = ImRaii.TreeNode($"Events##{(nint)node:X}eventTree");
if (tree)
{
using (ImRaii.Table($"##{(nint)node:X}eventTable", 7, Resizable | SizingFixedFit | Borders | RowBg))
{
ImGui.TableSetupColumn("#", WidthFixed);
ImGui.TableSetupColumn("Type", WidthFixed);
ImGui.TableSetupColumn("Param", WidthFixed);
ImGui.TableSetupColumn("Flags", WidthFixed);
ImGui.TableSetupColumn("Unk29", WidthFixed);
ImGui.TableSetupColumn("Target", WidthFixed);
ImGui.TableSetupColumn("Listener", WidthFixed);
ImGui.TableHeadersRow();
var i = 0;
while (evt != null)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{i++}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Type}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Param}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Flags}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Unk29}");
ImGui.TableNextColumn();
Gui.ClickToCopyText($"{(nint)evt->Target:X}");
ImGui.TableNextColumn();
Gui.ClickToCopyText($"{(nint)evt->Listener:X}");
evt = evt->NextEvent;
}
}
}
}
}

View file

@ -0,0 +1,35 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using static Dalamud.Utility.Util;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkClippingMaskNode"/> that can be printed and browsed via ImGui.
/// </summary>
internal unsafe class ClippingMaskNodeTree : ImageNodeTree
{
/// <summary>
/// Initializes a new instance of the <see cref="ClippingMaskNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
internal ClippingMaskNodeTree(AtkResNode* node, AddonTree addonTree)
: base(node, addonTree)
{
}
/// <inheritdoc/>
private protected override uint PartId => this.CmNode->PartId;
/// <inheritdoc/>
private protected override AtkUldPartsList* PartsList => this.CmNode->PartsList;
private AtkClippingMaskNode* CmNode => (AtkClippingMaskNode*)this.Node;
/// <inheritdoc/>
private protected override void PrintNodeObject() => ShowStruct(this.CmNode);
/// <inheritdoc/>
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) => this.DrawTextureAndParts();
}

View file

@ -0,0 +1,24 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using static Dalamud.Utility.Util;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkCollisionNode"/> that can be printed and browsed via ImGui.
/// </summary>
internal unsafe class CollisionNodeTree : ResNodeTree
{
/// <summary>
/// Initializes a new instance of the <see cref="CollisionNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
internal CollisionNodeTree(AtkResNode* node, AddonTree addonTree)
: base(node, addonTree)
{
}
/// <inheritdoc/>
private protected override void PrintNodeObject() => ShowStruct((AtkCollisionNode*)this.Node);
}

View file

@ -0,0 +1,282 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
using static FFXIVClientStructs.FFXIV.Component.GUI.ComponentType;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkComponentNode"/> that can be printed and browsed via ImGui.
/// </summary>
internal unsafe class ComponentNodeTree : ResNodeTree
{
private readonly ComponentType componentType;
/// <summary>
/// Initializes a new instance of the <see cref="ComponentNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
internal ComponentNodeTree(AtkResNode* node, AddonTree addonTree)
: base(node, addonTree)
{
this.NodeType = 0;
this.componentType = ((AtkUldComponentInfo*)this.UldManager->Objects)->ComponentType;
}
private AtkComponentBase* Component => this.CompNode->Component;
private AtkComponentNode* CompNode => (AtkComponentNode*)this.Node;
private AtkUldManager* UldManager => &this.Component->UldManager;
/// <inheritdoc/>
private protected override string GetHeaderText()
{
var childCount = (int)this.UldManager->NodeListCount;
return $"{this.componentType} Component Node{(childCount > 0 ? $" [+{childCount}]" : string.Empty)} (Node: {(nint)this.Node:X} / Comp: {(nint)this.Component:X})";
}
/// <inheritdoc/>
private protected override void PrintNodeObject()
{
base.PrintNodeObject();
this.PrintComponentObject();
ImGui.SameLine();
ImGui.NewLine();
this.PrintComponentDataObject();
ImGui.SameLine();
ImGui.NewLine();
}
/// <inheritdoc/>
private protected override void PrintChildNodes()
{
base.PrintChildNodes();
var count = this.UldManager->NodeListCount;
PrintNodeListAsTree(this.UldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f));
}
/// <inheritdoc/>
private protected override void PrintFieldNames()
{
this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1));
this.PrintFieldName((nint)this.Component, new(0f, 0.5f, 0.8f, 1f));
}
/// <inheritdoc/>
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
{
if (this.Component == null)
{
return;
}
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (this.componentType)
{
case TextInput:
var textInputComponent = (AtkComponentTextInput*)this.Component;
ImGui.TextUnformatted(
$"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
ImGui.TextUnformatted(
$"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
ImGui.TextUnformatted(
$"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}");
ImGui.TextUnformatted(
$"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
ImGui.TextUnformatted(
$"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}");
ImGui.TextUnformatted(
$"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}");
ImGui.TextUnformatted(
$"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}");
break;
case List:
case TreeList:
var l = (AtkComponentList*)this.Component;
if (ImGui.SmallButton("Inc.Selected"))
{
l->SelectedItemIndex++;
}
break;
}
}
private void PrintComponentObject()
{
PrintFieldValuePair("Component", $"{(nint)this.Component:X}");
ImGui.SameLine();
switch (this.componentType)
{
case Button:
ShowStruct((AtkComponentButton*)this.Component);
break;
case Slider:
ShowStruct((AtkComponentSlider*)this.Component);
break;
case Window:
ShowStruct((AtkComponentWindow*)this.Component);
break;
case CheckBox:
ShowStruct((AtkComponentCheckBox*)this.Component);
break;
case GaugeBar:
ShowStruct((AtkComponentGaugeBar*)this.Component);
break;
case RadioButton:
ShowStruct((AtkComponentRadioButton*)this.Component);
break;
case TextInput:
ShowStruct((AtkComponentTextInput*)this.Component);
break;
case Icon:
ShowStruct((AtkComponentIcon*)this.Component);
break;
case NumericInput:
ShowStruct((AtkComponentNumericInput*)this.Component);
break;
case List:
ShowStruct((AtkComponentList*)this.Component);
break;
case TreeList:
ShowStruct((AtkComponentTreeList*)this.Component);
break;
case DropDownList:
ShowStruct((AtkComponentDropDownList*)this.Component);
break;
case ScrollBar:
ShowStruct((AtkComponentScrollBar*)this.Component);
break;
case ListItemRenderer:
ShowStruct((AtkComponentListItemRenderer*)this.Component);
break;
case IconText:
ShowStruct((AtkComponentIconText*)this.Component);
break;
case ComponentType.DragDrop:
ShowStruct((AtkComponentDragDrop*)this.Component);
break;
case GuildLeveCard:
ShowStruct((AtkComponentGuildLeveCard*)this.Component);
break;
case TextNineGrid:
ShowStruct((AtkComponentTextNineGrid*)this.Component);
break;
case JournalCanvas:
ShowStruct((AtkComponentJournalCanvas*)this.Component);
break;
case HoldButton:
ShowStruct((AtkComponentHoldButton*)this.Component);
break;
case Portrait:
ShowStruct((AtkComponentPortrait*)this.Component);
break;
default:
ShowStruct(this.Component);
break;
}
}
private void PrintComponentDataObject()
{
var componentData = this.Component->UldManager.ComponentData;
PrintFieldValuePair("Data", $"{(nint)componentData:X}");
if (componentData != null)
{
ImGui.SameLine();
switch (this.componentType)
{
case Base:
ShowStruct(componentData);
break;
case Button:
ShowStruct((AtkUldComponentDataButton*)componentData);
break;
case Window:
ShowStruct((AtkUldComponentDataWindow*)componentData);
break;
case CheckBox:
ShowStruct((AtkUldComponentDataCheckBox*)componentData);
break;
case RadioButton:
ShowStruct((AtkUldComponentDataRadioButton*)componentData);
break;
case GaugeBar:
ShowStruct((AtkUldComponentDataGaugeBar*)componentData);
break;
case Slider:
ShowStruct((AtkUldComponentDataSlider*)componentData);
break;
case TextInput:
ShowStruct((AtkUldComponentDataTextInput*)componentData);
break;
case NumericInput:
ShowStruct((AtkUldComponentDataNumericInput*)componentData);
break;
case List:
ShowStruct((AtkUldComponentDataList*)componentData);
break;
case DropDownList:
ShowStruct((AtkUldComponentDataDropDownList*)componentData);
break;
case Tab:
ShowStruct((AtkUldComponentDataTab*)componentData);
break;
case TreeList:
ShowStruct((AtkUldComponentDataTreeList*)componentData);
break;
case ScrollBar:
ShowStruct((AtkUldComponentDataScrollBar*)componentData);
break;
case ListItemRenderer:
ShowStruct((AtkUldComponentDataListItemRenderer*)componentData);
break;
case Icon:
ShowStruct((AtkUldComponentDataIcon*)componentData);
break;
case IconText:
ShowStruct((AtkUldComponentDataIconText*)componentData);
break;
case ComponentType.DragDrop:
ShowStruct((AtkUldComponentDataDragDrop*)componentData);
break;
case GuildLeveCard:
ShowStruct((AtkUldComponentDataGuildLeveCard*)componentData);
break;
case TextNineGrid:
ShowStruct((AtkUldComponentDataTextNineGrid*)componentData);
break;
case JournalCanvas:
ShowStruct((AtkUldComponentDataJournalCanvas*)componentData);
break;
case Multipurpose:
ShowStruct((AtkUldComponentDataMultipurpose*)componentData);
break;
case Map:
ShowStruct((AtkUldComponentDataMap*)componentData);
break;
case Preview:
ShowStruct((AtkUldComponentDataPreview*)componentData);
break;
case HoldButton:
ShowStruct((AtkUldComponentDataHoldButton*)componentData);
break;
case Portrait:
ShowStruct((AtkUldComponentDataPortrait*)componentData);
break;
default:
ShowStruct(componentData);
break;
}
}
}
}

View file

@ -0,0 +1,36 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkCounterNode"/> that can be printed and browsed via ImGui.
/// </summary>
internal unsafe partial class CounterNodeTree : ResNodeTree
{
/// <summary>
/// Initializes a new instance of the <see cref="CounterNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
internal CounterNodeTree(AtkResNode* node, AddonTree addonTree)
: base(node, addonTree)
{
}
private AtkCounterNode* CntNode => (AtkCounterNode*)this.Node;
/// <inheritdoc/>
private protected override void PrintNodeObject() => ShowStruct(this.CntNode);
/// <inheritdoc/>
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
{
if (!isEditorOpen)
{
PrintFieldValuePairs(("Text", ((AtkCounterNode*)this.Node)->NodeText.ToString()));
}
}
}

View file

@ -0,0 +1,384 @@
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Interface.FontAwesomeIcon;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Interface.Utility.ImGuiHelpers;
using static ImGuiNET.ImGuiColorEditFlags;
using static ImGuiNET.ImGuiInputTextFlags;
using static ImGuiNET.ImGuiTableColumnFlags;
using static ImGuiNET.ImGuiTableFlags;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <inheritdoc cref="ResNodeTree"/>
internal unsafe partial class ResNodeTree
{
/// <summary>
/// Sets up the table for the node editor, if the "Edit" checkbox is ticked.
/// </summary>
private protected void DrawNodeEditorTable()
{
using (ImRaii.Table($"###Editor{(nint)this.Node}", 2, SizingStretchProp | NoHostExtendX))
{
this.DrawEditorRows();
}
}
/// <summary>
/// Draws each row in the node editor table.
/// </summary>
private protected virtual void DrawEditorRows()
{
var pos = new Vector2(this.Node->X, this.Node->Y);
var size = new Vector2(this.Node->Width, this.Node->Height);
var scale = new Vector2(this.Node->ScaleX, this.Node->ScaleY);
var origin = new Vector2(this.Node->OriginX, this.Node->OriginY);
var angle = (float)((this.Node->Rotation * (180 / Math.PI)) + 360);
var rgba = RgbaUintToVector4(this.Node->Color.RGBA);
var mult = new Vector3(this.Node->MultiplyRed, this.Node->MultiplyGreen, this.Node->MultiplyBlue) / 255f;
var add = new Vector3(this.Node->AddRed, this.Node->AddGreen, this.Node->AddBlue);
var hov = false;
ImGui.TableSetupColumn("Labels", WidthFixed);
ImGui.TableSetupColumn("Editors", WidthFixed);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Position:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.DragFloat2($"##{(nint)this.Node:X}position", ref pos, 1, default, default, "%.0f"))
{
this.Node->X = pos.X;
this.Node->Y = pos.Y;
this.Node->DrawFlags |= 0xD;
}
hov |= SplitTooltip("X", "Y") || ImGui.IsItemActive();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Size:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.DragFloat2($"##{(nint)this.Node:X}size", ref size, 1, 0, default, "%.0f"))
{
this.Node->Width = (ushort)Math.Max(size.X, 0);
this.Node->Height = (ushort)Math.Max(size.Y, 0);
this.Node->DrawFlags |= 0xD;
}
hov |= SplitTooltip("Width", "Height") || ImGui.IsItemActive();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Scale:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.DragFloat2($"##{(nint)this.Node:X}scale", ref scale, 0.05f))
{
this.Node->ScaleX = scale.X;
this.Node->ScaleY = scale.Y;
this.Node->DrawFlags |= 0xD;
}
hov |= SplitTooltip("ScaleX", "ScaleY") || ImGui.IsItemActive();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Origin:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.DragFloat2($"##{(nint)this.Node:X}origin", ref origin, 1, default, default, "%.0f"))
{
this.Node->OriginX = origin.X;
this.Node->OriginY = origin.Y;
this.Node->DrawFlags |= 0xD;
}
hov |= SplitTooltip("OriginX", "OriginY") || ImGui.IsItemActive();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Rotation:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
while (angle > 180)
{
angle -= 360;
}
if (ImGui.DragFloat($"##{(nint)this.Node:X}rotation", ref angle, 0.05f, default, default, "%.2f°"))
{
this.Node->Rotation = (float)(angle / (180 / Math.PI));
this.Node->DrawFlags |= 0xD;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Rotation (deg)");
hov = true;
}
hov |= ImGui.IsItemActive();
if (hov)
{
Vector4 brightYellow = new(1, 1, 0.5f, 0.8f);
new NodeBounds(this.Node).Draw(brightYellow);
new NodeBounds(origin, this.Node).Draw(brightYellow);
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("RGBA:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.ColorEdit4($"##{(nint)this.Node:X}RGBA", ref rgba, DisplayHex))
{
this.Node->Color = new() { RGBA = RgbaVector4ToUint(rgba) };
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Multiply:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.ColorEdit3($"##{(nint)this.Node:X}multiplyRGB", ref mult, DisplayHex))
{
this.Node->MultiplyRed = (byte)(mult.X * 255);
this.Node->MultiplyGreen = (byte)(mult.Y * 255);
this.Node->MultiplyBlue = (byte)(mult.Z * 255);
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Add:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(124);
if (ImGui.DragFloat3($"##{(nint)this.Node:X}addRGB", ref add, 1, -255, 255, "%.0f"))
{
this.Node->AddRed = (short)add.X;
this.Node->AddGreen = (short)add.Y;
this.Node->AddBlue = (short)add.Z;
}
SplitTooltip("+/- Red", "+/- Green", "+/- Blue");
var addTransformed = (add / 510f) + new Vector3(0.5f);
ImGui.SameLine();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() - (4 * GlobalScale));
if (ImGui.ColorEdit3($"##{(nint)this.Node:X}addRGBPicker", ref addTransformed, NoAlpha | NoInputs))
{
this.Node->AddRed = (short)Math.Floor((addTransformed.X * 510f) - 255f);
this.Node->AddGreen = (short)Math.Floor((addTransformed.Y * 510f) - 255f);
this.Node->AddBlue = (short)Math.Floor((addTransformed.Z * 510f) - 255f);
}
}
}
/// <inheritdoc cref="CounterNodeTree"/>
internal unsafe partial class CounterNodeTree
{
/// <inheritdoc/>
private protected override void DrawEditorRows()
{
base.DrawEditorRows();
var str = this.CntNode->NodeText.ToString();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Counter:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.InputText($"##{(nint)this.Node:X}counterEdit", ref str, 512, EnterReturnsTrue))
{
this.CntNode->SetText(str);
}
}
}
/// <inheritdoc cref="ImageNodeTree"/>
internal unsafe partial class ImageNodeTree
{
private static int TexDisplayStyle { get; set; }
/// <inheritdoc/>
private protected override void DrawEditorRows()
{
base.DrawEditorRows();
var partId = (int)this.PartId;
var partcount = this.ImgNode->PartsList->PartCount;
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Part Id:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.InputInt($"##partId{(nint)this.Node:X}", ref partId, 1, 1))
{
if (partId < 0)
{
partId = 0;
}
if (partId >= partcount)
{
partId = (int)(partcount - 1);
}
this.ImgNode->PartId = (ushort)partId;
}
}
}
/// <inheritdoc cref="NineGridNodeTree"/>
internal unsafe partial class NineGridNodeTree
{
/// <inheritdoc/>
private protected override void DrawEditorRows()
{
base.DrawEditorRows();
var lr = new Vector2(this.Offsets.Left, this.Offsets.Right);
var tb = new Vector2(this.Offsets.Top, this.Offsets.Bottom);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Ninegrid Offsets:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.DragFloat2($"##{(nint)this.Node:X}ngOffsetLR", ref lr, 1, 0))
{
this.NgNode->LeftOffset = (short)Math.Max(0, lr.X);
this.NgNode->RightOffset = (short)Math.Max(0, lr.Y);
}
SplitTooltip("Left", "Right");
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.DragFloat2($"##{(nint)this.Node:X}ngOffsetTB", ref tb, 1, 0))
{
this.NgNode->TopOffset = (short)Math.Max(0, tb.X);
this.NgNode->BottomOffset = (short)Math.Max(0, tb.Y);
}
SplitTooltip("Top", "Bottom");
}
}
/// <inheritdoc cref="TextNodeTree"/>
internal unsafe partial class TextNodeTree
{
private static readonly List<FontType> FontList = [.. Enum.GetValues<FontType>()];
private static readonly string[] FontNames = Enum.GetNames<FontType>();
/// <inheritdoc/>
private protected override void DrawEditorRows()
{
base.DrawEditorRows();
var text = this.TxtNode->NodeText.ToString();
var fontIndex = FontList.IndexOf(this.TxtNode->FontType);
int fontSize = this.TxtNode->FontSize;
var alignment = this.TxtNode->AlignmentType;
var textColor = RgbaUintToVector4(this.TxtNode->TextColor.RGBA);
var edgeColor = RgbaUintToVector4(this.TxtNode->EdgeColor.RGBA);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Text:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(Math.Max(ImGui.GetWindowContentRegionMax().X - ImGui.GetCursorPosX() - 50f, 150));
if (ImGui.InputText($"##{(nint)this.Node:X}textEdit", ref text, 512, EnterReturnsTrue))
{
this.TxtNode->SetText(text);
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Font:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.Combo($"##{(nint)this.Node:X}fontType", ref fontIndex, FontNames, FontList.Count))
{
this.TxtNode->FontType = FontList[fontIndex];
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Font Size:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.InputInt($"##{(nint)this.Node:X}fontSize", ref fontSize, 1, 10))
{
this.TxtNode->FontSize = (byte)fontSize;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Alignment:");
ImGui.TableNextColumn();
if (InputAlignment($"##{(nint)this.Node:X}alignment", ref alignment))
{
this.TxtNode->AlignmentType = alignment;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Text Color:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.ColorEdit4($"##{(nint)this.Node:X}TextRGB", ref textColor, DisplayHex))
{
this.TxtNode->TextColor = new() { RGBA = RgbaVector4ToUint(textColor) };
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text("Edge Color:");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
if (ImGui.ColorEdit4($"##{(nint)this.Node:X}EdgeRGB", ref edgeColor, DisplayHex))
{
this.TxtNode->EdgeColor = new() { RGBA = RgbaVector4ToUint(edgeColor) };
}
}
private static bool InputAlignment(string label, ref AlignmentType alignment)
{
var hAlign = (int)alignment % 3;
var vAlign = ((int)alignment - hAlign) / 3;
var hAlignInput = IconButtonSelect($"{label}H", ref hAlign, [0, 1, 2], [AlignLeft, AlignCenter, AlignRight]);
var vAlignInput = IconButtonSelect($"{label}V", ref vAlign, [0, 1, 2], [ArrowsUpToLine, GripLines, ArrowsDownToLine]);
if (hAlignInput || vAlignInput)
{
alignment = (AlignmentType)((vAlign * 3) + hAlign);
return true;
}
return false;
}
}

View file

@ -0,0 +1,316 @@
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
using static FFXIVClientStructs.FFXIV.Component.GUI.TextureType;
using static ImGuiNET.ImGuiTableColumnFlags;
using static ImGuiNET.ImGuiTableFlags;
using static ImGuiNET.ImGuiTreeNodeFlags;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkImageNode"/> that can be printed and browsed via ImGui.
/// </summary>
internal unsafe partial class ImageNodeTree : ResNodeTree
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
internal ImageNodeTree(AtkResNode* node, AddonTree addonTree)
: base(node, addonTree)
{
}
/// <summary>
/// Gets the part ID that this node uses.
/// </summary>
private protected virtual uint PartId => this.ImgNode->PartId;
/// <summary>
/// Gets the parts list that this node uses.
/// </summary>
private protected virtual AtkUldPartsList* PartsList => this.ImgNode->PartsList;
/// <summary>
/// Gets or sets a summary of pertinent data about this <see cref="AtkImageNode"/>'s texture. Updated each time <see cref="DrawTextureAndParts"/> is called.
/// </summary>
private protected TextureData TexData { get; set; }
private AtkImageNode* ImgNode => (AtkImageNode*)this.Node;
/// <summary>
/// Draws the texture inside the window, in either of two styles.<br/><br/>
/// <term>Full Image (0)</term>presents the texture in full as a spritesheet.<br/>
/// <term>Parts List (1)</term>presents the individual parts as rows in a table.
/// </summary>
private protected void DrawTextureAndParts()
{
this.TexData = new TextureData(this.PartsList, this.PartId);
if (this.TexData.Texture == null)
{
return;
}
using var tree = ImRaii.TreeNode($"Texture##texture{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", SpanFullWidth);
if (tree)
{
PrintFieldValuePairs(
("Texture Type", $"{this.TexData.TexType}"),
("Part ID", $"{this.TexData.PartId}"),
("Part Count", $"{this.TexData.PartCount}"));
if (this.TexData.Path != null)
{
PrintFieldValuePairs(("Texture Path", this.TexData.Path));
}
if (ImGui.RadioButton("Full Image##textureDisplayStyle0", TexDisplayStyle == 0))
{
TexDisplayStyle = 0;
}
ImGui.SameLine();
if (ImGui.RadioButton("Parts List##textureDisplayStyle1", TexDisplayStyle == 1))
{
TexDisplayStyle = 1;
}
ImGui.NewLine();
if (TexDisplayStyle == 1)
{
this.PrintPartsTable();
}
else
{
this.DrawFullTexture();
}
}
}
/// <summary>
/// Draws an outline of a given part within the texture.
/// </summary>
/// <param name="partId">The part ID.</param>
/// <param name="cursorScreenPos">The absolute position of the cursor onscreen.</param>
/// <param name="cursorLocalPos">The relative position of the cursor within the window.</param>
/// <param name="col">The color of the outline.</param>
/// <param name="reqHover">Whether this outline requires the user to mouse over it.</param>
private protected virtual void DrawPartOutline(uint partId, Vector2 cursorScreenPos, Vector2 cursorLocalPos, Vector4 col, bool reqHover = false)
{
var part = this.TexData.PartsList->Parts[partId];
var hrFactor = this.TexData.HiRes ? 2f : 1f;
var uv = new Vector2(part.U, part.V) * hrFactor;
var wh = new Vector2(part.Width, part.Height) * hrFactor;
var partBegin = cursorScreenPos + uv;
var partEnd = partBegin + wh;
if (reqHover && !ImGui.IsMouseHoveringRect(partBegin, partEnd))
{
return;
}
var savePos = ImGui.GetCursorPos();
ImGui.GetWindowDrawList().AddRect(partBegin, partEnd, RgbaVector4ToUint(col));
ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20));
ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}");
ImGui.SetCursorPos(savePos);
}
/// <inheritdoc/>
private protected override void PrintNodeObject() => ShowStruct(this.ImgNode);
/// <inheritdoc/>
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
{
PrintFieldValuePairs(
("Wrap", $"{this.ImgNode->WrapMode}"),
("Image Flags", $"0x{this.ImgNode->Flags:X}"));
this.DrawTextureAndParts();
}
private static void PrintPartCoords(float u, float v, float w, float h, bool asFloat = false, bool lineBreak = false)
{
ImGui.TextDisabled($"{u}, {v},{(lineBreak ? "\n" : " ")}{w}, {h}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Click to copy as Vector2\nShift-click to copy as Vector4");
}
var suffix = asFloat ? "f" : string.Empty;
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText(
ImGui.IsKeyDown(ImGuiKey.ModShift)
? $"new Vector4({u}{suffix}, {v}{suffix}, {w}{suffix}, {h}{suffix})"
: $"new Vector2({u}{suffix}, {v}{suffix});\nnew Vector2({w}{suffix}, {h}{suffix})");
}
}
private void DrawFullTexture()
{
var cursorScreenPos = ImGui.GetCursorScreenPos();
var cursorLocalPos = ImGui.GetCursorPos();
ImGui.Image(new(this.TexData.Texture->D3D11ShaderResourceView), new(this.TexData.Texture->ActualWidth, this.TexData.Texture->ActualHeight));
for (uint p = 0; p < this.TexData.PartsList->PartCount; p++)
{
if (p == this.TexData.PartId)
{
continue;
}
this.DrawPartOutline(p, cursorScreenPos, cursorLocalPos, new(0.6f, 0.6f, 0.6f, 1), true);
}
this.DrawPartOutline(this.TexData.PartId, cursorScreenPos, cursorLocalPos, new(0, 0.85F, 1, 1));
}
private void PrintPartsTable()
{
using (ImRaii.Table($"partsTable##{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", 3, Borders | RowBg | Reorderable))
{
ImGui.TableSetupColumn("Part ID", WidthFixed);
ImGui.TableSetupColumn("Part Texture", WidthFixed);
ImGui.TableSetupColumn("Coordinates", WidthFixed);
ImGui.TableHeadersRow();
var tWidth = this.TexData.Texture->ActualWidth;
var tHeight = this.TexData.Texture->ActualHeight;
var textureSize = new Vector2(tWidth, tHeight);
for (ushort i = 0; i < this.TexData.PartCount; i++)
{
ImGui.TableNextColumn();
var col = i == this.TexData.PartId ? new Vector4(0, 0.85F, 1, 1) : new(1);
ImGui.TextColored(col, $"#{i.ToString().PadLeft(this.TexData.PartCount.ToString().Length, '0')}");
ImGui.TableNextColumn();
var part = this.TexData.PartsList->Parts[i];
var hiRes = this.TexData.HiRes;
var u = hiRes ? part.U * 2f : part.U;
var v = hiRes ? part.V * 2f : part.V;
var width = hiRes ? part.Width * 2f : part.Width;
var height = hiRes ? part.Height * 2f : part.Height;
ImGui.Image(
new(this.TexData.Texture->D3D11ShaderResourceView),
new(width, height),
new Vector2(u, v) / textureSize,
new Vector2(u + width, v + height) / textureSize);
ImGui.TableNextColumn();
ImGui.TextColored(!hiRes ? new(1) : new(0.6f, 0.6f, 0.6f, 1), "Standard:\t");
ImGui.SameLine();
var cursX = ImGui.GetCursorPosX();
PrintPartCoords(u / 2f, v / 2f, width / 2f, height / 2f);
ImGui.TextColored(hiRes ? new(1) : new(0.6f, 0.6f, 0.6f, 1), "Hi-Res:\t");
ImGui.SameLine();
ImGui.SetCursorPosX(cursX);
PrintPartCoords(u, v, width, height);
ImGui.Text("UV:\t");
ImGui.SameLine();
ImGui.SetCursorPosX(cursX);
PrintPartCoords(u / tWidth, v / tWidth, (u + width) / tWidth, (v + height) / tHeight, true, true);
}
}
}
/// <summary>
/// A summary of pertinent data about a node's texture.
/// </summary>
protected struct TextureData
{
/// <summary>The texture's partslist.</summary>
public AtkUldPartsList* PartsList;
/// <summary>The number of parts in the texture.</summary>
public uint PartCount;
/// <summary>The part ID the node is using.</summary>
public uint PartId;
/// <summary>The texture itself.</summary>
public Texture* Texture = null;
/// <summary>The type of texture.</summary>
public TextureType TexType = 0;
/// <summary>The texture's file path (if <see cref="TextureType.Resource"/>, otherwise this value is null).</summary>
public string? Path = null;
/// <summary>Whether this is a high-resolution texture.</summary>
public bool HiRes = false;
/// <summary>
/// Initializes a new instance of the <see cref="TextureData"/> struct.
/// </summary>
/// <param name="partsList">The texture's parts list.</param>
/// <param name="partId">The part ID being used by the node.</param>
public TextureData(AtkUldPartsList* partsList, uint partId)
{
this.PartsList = partsList;
this.PartCount = this.PartsList->PartCount;
this.PartId = partId >= this.PartCount ? 0 : partId;
if (this.PartsList == null)
{
return;
}
var asset = this.PartsList->Parts[this.PartId].UldAsset;
if (asset == null)
{
return;
}
this.TexType = asset->AtkTexture.TextureType;
if (this.TexType == Resource)
{
var resource = asset->AtkTexture.Resource;
this.Texture = resource->KernelTextureObject;
this.Path = Marshal.PtrToStringAnsi(new(resource->TexFileResourceHandle->ResourceHandle.FileName.BufferPtr));
}
else
{
this.Texture = this.TexType == KernelTexture ? asset->AtkTexture.KernelTexture : null;
this.Path = null;
}
this.HiRes = this.Path?.Contains("_hr1") ?? false;
}
}
}

View file

@ -0,0 +1,69 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <inheritdoc cref="NineGridNodeTree"/>
internal unsafe partial class NineGridNodeTree
{
/// <summary>
/// A struct representing the four offsets of an <see cref="AtkNineGridNode"/>.
/// </summary>
internal struct NineGridOffsets
{
/// <summary>Top offset.</summary>
internal int Top;
/// <summary>Left offset.</summary>
internal int Left;
/// <summary>Right offset.</summary>
internal int Right;
/// <summary>Bottom offset.</summary>
internal int Bottom;
/// <summary>
/// Initializes a new instance of the <see cref="NineGridOffsets"/> struct.
/// </summary>
/// <param name="top">The top offset.</param>
/// <param name="right">The right offset.</param>
/// <param name="bottom">The bottom offset.</param>
/// <param name="left">The left offset.</param>
internal NineGridOffsets(int top, int right, int bottom, int left)
{
this.Top = top;
this.Right = right;
this.Left = left;
this.Bottom = bottom;
}
/// <summary>
/// Initializes a new instance of the <see cref="NineGridOffsets"/> struct.
/// </summary>
/// <param name="ngNode">The node using these offsets.</param>
internal NineGridOffsets(AtkNineGridNode* ngNode)
: this(ngNode->TopOffset, ngNode->RightOffset, ngNode->BottomOffset, ngNode->LeftOffset)
{
}
private NineGridOffsets(Vector4 v)
: this((int)v.X, (int)v.Y, (int)v.Z, (int)v.W)
{
}
public static implicit operator NineGridOffsets(Vector4 v) => new(v);
public static implicit operator Vector4(NineGridOffsets v) => new(v.Top, v.Right, v.Bottom, v.Left);
public static NineGridOffsets operator *(float n, NineGridOffsets a) => n * (Vector4)a;
public static NineGridOffsets operator *(NineGridOffsets a, float n) => n * a;
/// <summary>Prints the offsets in ImGui.</summary>
internal readonly void Print() => PrintFieldValuePairs(("Top", $"{this.Top}"), ("Bottom", $"{this.Bottom}"), ("Left", $"{this.Left}"), ("Right", $"{this.Right}"));
}
}

View file

@ -0,0 +1,88 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Utility.Util;
using Vector2 = System.Numerics.Vector2;
using Vector4 = System.Numerics.Vector4;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkNineGridNode"/> that can be printed and browsed via ImGui.
/// </summary>
internal unsafe partial class NineGridNodeTree : ImageNodeTree
{
/// <summary>
/// Initializes a new instance of the <see cref="NineGridNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
internal NineGridNodeTree(AtkResNode* node, AddonTree addonTree)
: base(node, addonTree)
{
}
/// <inheritdoc/>
private protected override uint PartId => this.NgNode->PartId;
/// <inheritdoc/>
private protected override AtkUldPartsList* PartsList => this.NgNode->PartsList;
private AtkNineGridNode* NgNode => (AtkNineGridNode*)this.Node;
private NineGridOffsets Offsets => new(this.NgNode);
/// <inheritdoc/>
private protected override void DrawPartOutline(uint partId, Vector2 cursorScreenPos, Vector2 cursorLocalPos, Vector4 col, bool reqHover = false)
{
var part = this.TexData.PartsList->Parts[partId];
var hrFactor = this.TexData.HiRes ? 2f : 1f;
var uv = new Vector2(part.U, part.V) * hrFactor;
var wh = new Vector2(part.Width, part.Height) * hrFactor;
var partBegin = cursorScreenPos + uv;
var partEnd = cursorScreenPos + uv + wh;
var savePos = ImGui.GetCursorPos();
if (!reqHover || ImGui.IsMouseHoveringRect(partBegin, partEnd))
{
var adjustedOffsets = this.Offsets * hrFactor;
var ngBegin1 = partBegin with { X = partBegin.X + adjustedOffsets.Left };
var ngEnd1 = partEnd with { X = partEnd.X - adjustedOffsets.Right };
var ngBegin2 = partBegin with { Y = partBegin.Y + adjustedOffsets.Top };
var ngEnd2 = partEnd with { Y = partEnd.Y - adjustedOffsets.Bottom };
var ngCol = RgbaVector4ToUint(col with { W = 0.75f * col.W });
ImGui.GetWindowDrawList()
.AddRect(partBegin, partEnd, RgbaVector4ToUint(col));
ImGui.GetWindowDrawList().AddRect(ngBegin1, ngEnd1, ngCol);
ImGui.GetWindowDrawList().AddRect(ngBegin2, ngEnd2, ngCol);
ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20));
ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}");
}
ImGui.SetCursorPos(savePos);
}
/// <inheritdoc/>
private protected override void PrintNodeObject() => ShowStruct(this.NgNode);
/// <inheritdoc/>
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
{
if (!isEditorOpen)
{
ImGui.Text("NineGrid Offsets:\t");
ImGui.SameLine();
this.Offsets.Print();
}
this.DrawTextureAndParts();
}
}

View file

@ -0,0 +1,420 @@
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Interface.FontAwesomeIcon;
using static Dalamud.Interface.Internal.UiDebug2.Browsing.Events;
using static Dalamud.Interface.Internal.UiDebug2.ElementSelector;
using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags;
using static ImGuiNET.ImGuiCol;
using static ImGuiNET.ImGuiTreeNodeFlags;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkResNode"/> that can be printed and browsed via ImGui.
/// </summary>
/// <remarks>As with the structs they represent, this class serves as the base class for other types of NodeTree.</remarks>
internal unsafe partial class ResNodeTree : IDisposable
{
private NodePopoutWindow? window;
private bool editorOpen;
/// <summary>
/// Initializes a new instance of the <see cref="ResNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
private protected ResNodeTree(AtkResNode* node, AddonTree addonTree)
{
this.Node = node;
this.AddonTree = addonTree;
this.NodeType = node->Type;
this.AddonTree.NodeTrees.Add((nint)this.Node, this);
}
/// <summary>
/// Gets or sets the <see cref="AtkResNode"/> this tree represents.
/// </summary>
protected internal AtkResNode* Node { get; set; }
/// <summary>
/// Gets the <see cref="Browsing.AddonTree"/> containing this tree.
/// </summary>
protected internal AddonTree AddonTree { get; private set; }
/// <summary>
/// Gets this node's type.
/// </summary>
private protected NodeType NodeType { get; init; }
/// <summary>
/// Clears this NodeTree's popout window, if it has one.
/// </summary>
public void Dispose()
{
if (this.window != null && PopoutWindows.Windows.Contains(this.window))
{
PopoutWindows.RemoveWindow(this.window);
this.window.Dispose();
}
}
/// <summary>
/// Gets an instance of <see cref="ResNodeTree"/> (or one of its inheriting types) for the given node. If no instance exists, one is created.
/// </summary>
/// <param name="node">The node to get a tree for.</param>
/// <param name="addonTree">The tree for the node's containing addon.</param>
/// <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
? new ComponentNodeTree(node, addonTree)
: node->Type switch
{
NodeType.Text => new TextNodeTree(node, addonTree),
NodeType.Image => new ImageNodeTree(node, addonTree),
NodeType.NineGrid => new NineGridNodeTree(node, addonTree),
NodeType.ClippingMask => new ClippingMaskNodeTree(node, addonTree),
NodeType.Counter => new CounterNodeTree(node, addonTree),
NodeType.Collision => new CollisionNodeTree(node, addonTree),
_ => new ResNodeTree(node, addonTree),
};
/// <summary>
/// Prints a list of NodeTrees for a given list of nodes.
/// </summary>
/// <param name="nodeList">The address of the start of the list.</param>
/// <param name="count">The number of nodes in the list.</param>
/// <param name="addonTree">The tree for the containing addon.</param>
internal static void PrintNodeList(AtkResNode** nodeList, int count, AddonTree addonTree)
{
for (uint j = 0; j < count; j++)
{
GetOrCreate(nodeList[j], addonTree).Print(j);
}
}
/// <summary>
/// Calls <see cref="PrintNodeList"/>, but outputs the results as a collapsible tree.
/// </summary>
/// <param name="nodeList">The address of the start of the list.</param>
/// <param name="count">The number of nodes in the list.</param>
/// <param name="label">The heading text of the tree.</param>
/// <param name="addonTree">The tree for the containing addon.</param>
/// <param name="color">The text color of the heading.</param>
internal static void PrintNodeListAsTree(AtkResNode** nodeList, int count, string label, AddonTree addonTree, Vector4 color)
{
if (count <= 0)
{
return;
}
using var c = ImRaii.PushColor(Text, color);
using var tree = ImRaii.TreeNode($"{label}##{(nint)nodeList:X}", SpanFullWidth);
c.Pop();
if (tree)
{
var lineStart = ImGui.GetCursorScreenPos() + new Vector2(-10, 2);
PrintNodeList(nodeList, count, addonTree);
var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 };
if (lineStart.Y < lineEnd.Y)
{
ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(color), 1);
}
}
}
/// <summary>
/// Prints this tree in the window.
/// </summary>
/// <param name="index">The index of the tree within its containing node or addon, if applicable.</param>
/// <param name="forceOpen">Whether the tree should default to being open.</param>
internal void Print(uint? index, bool forceOpen = false)
{
if (SearchResults.Length > 0 && SearchResults[0] == (nint)this.Node)
{
this.PrintWithHighlights(index);
}
else
{
this.PrintTree(index, forceOpen);
}
}
/// <summary>
/// Prints out the tree's header text.
/// </summary>
internal void WriteTreeHeading()
{
ImGui.TextUnformatted(this.GetHeaderText());
this.PrintFieldNames();
}
/// <summary>
/// If the given pointer has been identified as a field within the addon struct, this method prints that field's name.
/// </summary>
/// <param name="ptr">The pointer to check.</param>
/// <param name="color">The text color to use.</param>
private protected void PrintFieldName(nint ptr, Vector4 color)
{
if (this.AddonTree.FieldNames.TryGetValue(ptr, out var result))
{
ImGui.SameLine();
ImGui.TextColored(color, string.Join(".", result));
}
}
/// <summary>
/// Builds a string that will serve as the header text for the tree. Indicates the node type, the number of direct children it contains, and its pointer.
/// </summary>
/// <returns>The resulting header text string.</returns>
private protected virtual string GetHeaderText()
{
var count = this.GetDirectChildCount();
return $"{this.NodeType} Node{(count > 0 ? $" [+{count}]" : string.Empty)} ({(nint)this.Node:X})";
}
/// <summary>
/// Prints the node struct.
/// </summary>
private protected virtual void PrintNodeObject()
{
ShowStruct(this.Node);
ImGui.SameLine();
ImGui.NewLine();
}
/// <summary>
/// Prints any field names for the node.
/// </summary>
private protected virtual void PrintFieldNames() => this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1));
/// <summary>
/// Prints all direct children of this node.
/// </summary>
private protected virtual void PrintChildNodes()
{
var prevNode = this.Node->ChildNode;
while (prevNode != null)
{
GetOrCreate(prevNode, this.AddonTree).Print(null);
prevNode = prevNode->PrevSiblingNode;
}
}
/// <summary>
/// Prints any specific fields pertaining to the specific type of node.
/// </summary>
/// <param name="isEditorOpen">Whether the "Edit" box is currently checked.</param>
private protected virtual void PrintFieldsForNodeType(bool isEditorOpen = false)
{
}
private int GetDirectChildCount()
{
var count = 0;
if (this.Node->ChildNode != null)
{
count++;
var prev = this.Node->ChildNode;
while (prev->PrevSiblingNode != null)
{
prev = prev->PrevSiblingNode;
count++;
}
}
return count;
}
private void PrintWithHighlights(uint? index)
{
if (!Scrolled)
{
ImGui.SetScrollHereY();
Scrolled = true;
}
var start = ImGui.GetCursorScreenPos() - new Vector2(5);
this.PrintTree(index, true);
var end = new Vector2(ImGui.GetMainViewport().WorkSize.X, ImGui.GetCursorScreenPos().Y + 5);
ImGui.GetWindowDrawList().AddRectFilled(start, end, RgbaVector4ToUint(new Vector4(1, 1, 0.2f, 1) { W = Countdown / 200f }));
}
private void PrintTree(uint? index, bool forceOpen = false)
{
var visible = this.Node->NodeFlags.HasFlag(Visible);
var label = $"{(index == null ? string.Empty : $"[{index}] ")}[#{this.Node->NodeId}]###{(nint)this.Node:X}nodeTree";
var displayColor = !visible ? new Vector4(0.8f, 0.8f, 0.8f, 1) :
this.Node->Color.A == 0 ? new(0.015f, 0.575f, 0.355f, 1) :
new(0.1f, 1f, 0.1f, 1f);
if (forceOpen || SearchResults.Contains((nint)this.Node))
{
ImGui.SetNextItemOpen(true, ImGuiCond.Always);
}
using var col = ImRaii.PushColor(Text, displayColor);
using var tree = ImRaii.TreeNode(label, SpanFullWidth);
if (ImGui.IsItemHovered())
{
new NodeBounds(this.Node).Draw(visible ? new(0.1f, 1f, 0.1f, 1f) : new(1f, 0f, 0.2f, 1f));
}
ImGui.SameLine();
this.WriteTreeHeading();
col.Pop();
if (tree)
{
var lineStart = ImGui.GetCursorScreenPos() + new Vector2(-10, 2);
try
{
PrintFieldValuePair("Node", $"{(nint)this.Node:X}");
ImGui.SameLine();
this.PrintNodeObject();
PrintFieldValuePairs(
("NodeID", $"{this.Node->NodeId}"),
("Type", $"{this.Node->Type}"));
this.DrawBasicControls();
if (this.editorOpen)
{
this.DrawNodeEditorTable();
}
else
{
this.PrintResNodeFields();
}
this.PrintFieldsForNodeType(this.editorOpen);
PrintEvents(this.Node);
new TimelineTree(this.Node).Print();
this.PrintChildNodes();
}
catch (Exception ex)
{
ImGui.TextDisabled($"Couldn't display node!\n\n{ex}");
}
var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 };
if (lineStart.Y < lineEnd.Y)
{
ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(displayColor), 1);
}
}
}
private void DrawBasicControls()
{
ImGui.SameLine();
var y = ImGui.GetCursorPosY();
ImGui.SetCursorPosY(y - 2);
var isVisible = this.Node->NodeFlags.HasFlag(Visible);
if (ImGuiComponents.IconButton("vis", isVisible ? Eye : EyeSlash, isVisible ? new Vector4(0.0f, 0.8f, 0.2f, 1f) : new(0.6f, 0.6f, 0.6f, 1)))
{
if (isVisible)
{
this.Node->NodeFlags &= ~Visible;
}
else
{
this.Node->NodeFlags |= Visible;
}
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Toggle Visibility");
}
ImGui.SameLine();
ImGui.SetCursorPosY(y - 2);
ImGui.Checkbox($"Edit###editCheckBox{(nint)this.Node}", ref this.editorOpen);
ImGui.SameLine();
ImGui.SetCursorPosY(y - 2);
if (ImGuiComponents.IconButton($"###{(nint)this.Node}popoutButton", this.window?.IsOpen == true ? Times : ArrowUpRightFromSquare, null))
{
this.TogglePopout();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Toggle Popout Window");
}
}
private void TogglePopout()
{
if (this.window != null)
{
this.window.IsOpen = !this.window.IsOpen;
}
else
{
this.window = new NodePopoutWindow(this, $"{this.AddonTree.AddonName}: {this.GetHeaderText()}###nodePopout{(nint)this.Node}");
PopoutWindows.AddWindow(this.window);
}
}
private void PrintResNodeFields()
{
PrintFieldValuePairs(
("X", $"{this.Node->X}"),
("Y", $"{this.Node->Y}"),
("Width", $"{this.Node->Width}"),
("Height", $"{this.Node->Height}"),
("Priority", $"{this.Node->Priority}"),
("Depth", $"{this.Node->Depth}"),
("DrawFlags", $"0x{this.Node->DrawFlags:X}"));
PrintFieldValuePairs(
("ScaleX", $"{this.Node->ScaleX:F2}"),
("ScaleY", $"{this.Node->ScaleY:F2}"),
("OriginX", $"{this.Node->OriginX}"),
("OriginY", $"{this.Node->OriginY}"),
("Rotation", $"{this.Node->Rotation * (180d / Math.PI):F1}° / {this.Node->Rotation:F7}rad "));
var color = this.Node->Color;
var add = new Vector3(this.Node->AddRed, this.Node->AddGreen, this.Node->AddBlue);
var multiply = new Vector3(this.Node->MultiplyRed, this.Node->MultiplyGreen, this.Node->MultiplyBlue);
PrintColor(RgbaUintToVector4(color.RGBA) with { W = 1 }, $"RGB: {SwapEndianness(color.RGBA) >> 8:X6}");
ImGui.SameLine();
PrintColor(color, $"Alpha: {color.A}");
ImGui.SameLine();
PrintColor((add / new Vector3(510f)) + new Vector3(0.5f), $"Add: {add.X} {add.Y} {add.Z}");
ImGui.SameLine();
PrintColor(multiply / 255f, $"Multiply: {multiply.X} {multiply.Y} {multiply.Z}");
PrintFieldValuePairs(("Flags", $"0x{(uint)this.Node->NodeFlags:X} ({this.Node->NodeFlags})"));
}
}

View file

@ -0,0 +1,120 @@
using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A tree for an <see cref="AtkTextNode"/> that can be printed and browsed via ImGui.
/// </summary>
internal unsafe partial class TextNodeTree : ResNodeTree
{
/// <summary>
/// Initializes a new instance of the <see cref="TextNodeTree"/> class.
/// </summary>
/// <param name="node">The node to create a tree for.</param>
/// <param name="addonTree">The tree representing the containing addon.</param>
internal TextNodeTree(AtkResNode* node, AddonTree addonTree)
: base(node, addonTree)
{
}
private AtkTextNode* TxtNode => (AtkTextNode*)this.Node;
private Utf8String NodeText => this.TxtNode->NodeText;
/// <inheritdoc/>
private protected override void PrintNodeObject() => ShowStruct(this.TxtNode);
/// <inheritdoc/>
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
{
if (isEditorOpen)
{
return;
}
ImGui.TextColored(new(1), "Text:");
ImGui.SameLine();
try
{
var style = new SeStringDrawParams
{
Color = this.TxtNode->TextColor.RGBA,
EdgeColor = this.TxtNode->EdgeColor.RGBA,
ForceEdgeColor = true,
EdgeStrength = 1f,
};
#pragma warning disable SeStringRenderer
ImGuiHelpers.SeStringWrapped(this.NodeText.AsSpan(), style);
#pragma warning restore SeStringRenderer
}
catch
{
ImGui.TextUnformatted(Marshal.PtrToStringAnsi(new(this.NodeText.StringPtr)) ?? string.Empty);
}
PrintFieldValuePairs(
("Font", $"{this.TxtNode->FontType}"),
("Font Size", $"{this.TxtNode->FontSize}"),
("Alignment", $"{this.TxtNode->AlignmentType}"));
PrintColor(this.TxtNode->TextColor, $"Text Color: {SwapEndianness(this.TxtNode->TextColor.RGBA):X8}");
ImGui.SameLine();
PrintColor(this.TxtNode->EdgeColor, $"Edge Color: {SwapEndianness(this.TxtNode->EdgeColor.RGBA):X8}");
this.PrintPayloads();
}
private void PrintPayloads()
{
using var tree = ImRaii.TreeNode($"Text Payloads##{(nint)this.Node:X}");
if (tree)
{
var utf8String = this.NodeText;
var seStringBytes = new byte[utf8String.BufUsed];
for (var i = 0L; i < utf8String.BufUsed; i++)
{
seStringBytes[i] = utf8String.StringPtr[i];
}
var seString = SeString.Parse(seStringBytes);
for (var i = 0; i < seString.Payloads.Count; i++)
{
var payload = seString.Payloads[i];
ImGui.TextUnformatted($"[{i}]");
ImGui.SameLine();
switch (payload.Type)
{
case PayloadType.RawText when payload is TextPayload tp:
{
Gui.PrintFieldValuePair("Raw Text", tp.Text ?? string.Empty);
break;
}
default:
{
ImGui.TextUnformatted(payload.ToString());
break;
}
}
}
}
}
}

View file

@ -0,0 +1,90 @@
using System.Collections.Generic;
using ImGuiNET;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <inheritdoc cref="TimelineTree"/>
public readonly partial struct TimelineTree
{
/// <summary>
/// An interface for retrieving and printing the contents of a given column in an animation timeline table.
/// </summary>
public interface IKeyGroupColumn
{
/// <summary>Gets the column's name/heading.</summary>
public string Name { get; }
/// <summary>Gets the number of cells in the column.</summary>
public int Count { get; }
/// <summary>Gets the column's width.</summary>
public float Width { get; }
/// <summary>
/// Calls this column's print function for a given row.
/// </summary>
/// <param name="i">The row number.</param>
public void PrintValueAt(int i);
}
/// <summary>
/// A column within an animation timeline table, representing a particular KeyGroup.
/// </summary>
/// <typeparam name="T">The value type of the KeyGroup.</typeparam>
public struct KeyGroupColumn<T> : IKeyGroupColumn
{
/// <summary>The values of each cell in the column.</summary>
public List<T> Values;
/// <summary>The method that should be used to format and print values in this KeyGroup.</summary>
public Action<T> PrintFunc;
/// <summary>
/// Initializes a new instance of the <see cref="KeyGroupColumn{T}"/> struct.
/// </summary>
/// <param name="name">The column's name/heading.</param>
/// <param name="printFunc">The method that should be used to format and print values in this KeyGroup.</param>
internal KeyGroupColumn(string name, Action<T>? printFunc = null)
{
this.Name = name;
this.PrintFunc = printFunc ?? PlainTextCell;
this.Values = [];
this.Width = 50;
}
/// <inheritdoc/>
public string Name { get; set; }
/// <inheritdoc/>
public float Width { get; init; }
/// <inheritdoc/>
public readonly int Count => this.Values.Count;
/// <summary>
/// The default print function, if none is specified.
/// </summary>
/// <param name="value">The value to print.</param>
public static void PlainTextCell(T value) => ImGui.TextUnformatted($"{value}");
/// <summary>
/// Adds a value to this column.
/// </summary>
/// <param name="val">The value to add.</param>
public readonly void Add(T val) => this.Values.Add(val);
/// <inheritdoc/>
public readonly void PrintValueAt(int i)
{
if (this.Values.Count > i)
{
this.PrintFunc.Invoke(this.Values[i]);
}
else
{
ImGui.TextDisabled("...");
}
}
}
}

View file

@ -0,0 +1,385 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Graphics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
using static FFXIVClientStructs.FFXIV.Component.GUI.NodeType;
using static ImGuiNET.ImGuiTableColumnFlags;
using static ImGuiNET.ImGuiTableFlags;
using static ImGuiNET.ImGuiTreeNodeFlags;
// ReSharper disable SuggestBaseTypeForParameter
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// <summary>
/// A struct allowing a node's animation timeline to be printed and browsed.
/// </summary>
public readonly unsafe partial struct TimelineTree
{
private readonly AtkResNode* node;
/// <summary>
/// Initializes a new instance of the <see cref="TimelineTree"/> struct.
/// </summary>
/// <param name="node">The node whose timelines are to be displayed.</param>
internal TimelineTree(AtkResNode* node)
{
this.node = node;
}
private AtkTimeline* NodeTimeline => this.node->Timeline;
private AtkTimelineResource* Resource => this.NodeTimeline->Resource;
private AtkTimelineAnimation* ActiveAnimation => this.NodeTimeline->ActiveAnimation;
/// <summary>
/// Prints out this timeline tree within a window.
/// </summary>
internal void Print()
{
if (this.NodeTimeline == null)
{
return;
}
var count = this.Resource->AnimationCount;
if (count > 0)
{
using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth);
if (tree)
{
PrintFieldValuePair("Timeline", $"{(nint)this.NodeTimeline:X}");
ImGui.SameLine();
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++)
{
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))));
}
}
}
}
private static void GetFrameColumn(Span<AtkTimelineKeyGroup> keyGroups, List<IKeyGroupColumn> columns, ushort endFrame)
{
for (var i = 0; i < keyGroups.Length; i++)
{
if (keyGroups[i].Type != AtkTimelineKeyGroupType.None)
{
var keyGroup = keyGroups[i];
var idColumn = new KeyGroupColumn<ushort>("Frame");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
idColumn.Add(keyGroup.KeyFrames[f].FrameIdx);
}
if (idColumn.Values.Last() != endFrame)
{
idColumn.Add(endFrame);
}
columns.Add(idColumn);
break;
}
}
}
private static void GetPosColumns(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var xColumn = new KeyGroupColumn<float>("X");
var yColumn = new KeyGroupColumn<float>("Y");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
var (x, y) = keyGroup.KeyFrames[f].Value.Float2;
xColumn.Add(x);
yColumn.Add(y);
}
columns.Add(xColumn);
columns.Add(yColumn);
}
private static void GetRotationColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var rotColumn = new KeyGroupColumn<float>("Rotation", static r => ImGui.TextUnformatted($"{r * (180d / Math.PI):F1}°"));
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
rotColumn.Add(keyGroup.KeyFrames[f].Value.Float);
}
columns.Add(rotColumn);
}
private static void GetScaleColumns(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var scaleXColumn = new KeyGroupColumn<float>("ScaleX");
var scaleYColumn = new KeyGroupColumn<float>("ScaleY");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
var (scaleX, scaleY) = keyGroup.KeyFrames[f].Value.Float2;
scaleXColumn.Add(scaleX);
scaleYColumn.Add(scaleY);
}
columns.Add(scaleXColumn);
columns.Add(scaleYColumn);
}
private static void GetAlphaColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var alphaColumn = new KeyGroupColumn<byte>("Alpha", PrintAlpha);
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
alphaColumn.Add(keyGroup.KeyFrames[f].Value.Byte);
}
columns.Add(alphaColumn);
}
private static void GetTintColumns(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var addRGBColumn = new KeyGroupColumn<Vector3>("Add", PrintAddCell) { Width = 110 };
var multiplyRGBColumn = new KeyGroupColumn<ByteColor>("Multiply", PrintMultiplyCell) { Width = 110 };
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
var nodeTint = keyGroup.KeyFrames[f].Value.NodeTint;
addRGBColumn.Add(new Vector3(nodeTint.AddR, nodeTint.AddG, nodeTint.AddB));
multiplyRGBColumn.Add(nodeTint.MultiplyRGB);
}
columns.Add(addRGBColumn);
columns.Add(multiplyRGBColumn);
}
private static void GetTextColorColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var textColorColumn = new KeyGroupColumn<ByteColor>("Text Color", PrintRGB);
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
textColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB);
}
columns.Add(textColorColumn);
}
private static void GetPartIdColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var partColumn = new KeyGroupColumn<ushort>("Part ID");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
partColumn.Add(keyGroup.KeyFrames[f].Value.UShort);
}
columns.Add(partColumn);
}
private static void GetEdgeColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var edgeColorColumn = new KeyGroupColumn<ByteColor>("Edge Color", PrintRGB);
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
edgeColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB);
}
columns.Add(edgeColorColumn);
}
private static void GetLabelColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var labelColumn = new KeyGroupColumn<ushort>("Label");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
labelColumn.Add(keyGroup.KeyFrames[f].Value.Label.LabelId);
}
columns.Add(labelColumn);
}
private static void PrintRGB(ByteColor c) => PrintColor(c, $"0x{SwapEndianness(c.RGBA):X8}");
private static void PrintAlpha(byte b) => PrintColor(new Vector4(b / 255f), PadEvenly($"{b}", 25));
private static void PrintAddCell(Vector3 add)
{
var fmt = PadEvenly($"{PadEvenly($"{add.X}", 30)}{PadEvenly($"{add.Y}", 30)}{PadEvenly($"{add.Z}", 30)}", 100);
PrintColor(new Vector4((add / new Vector3(510f)) + new Vector3(0.5f), 1), fmt);
}
private static void PrintMultiplyCell(ByteColor byteColor)
{
var multiply = new Vector3(byteColor.R, byteColor.G, byteColor.B);
var fmt = PadEvenly($"{PadEvenly($"{multiply.X}", 25)}{PadEvenly($"{multiply.Y}", 25)}{PadEvenly($"{multiply.Z}", 25)}", 100);
PrintColor(multiply / 255f, fmt);
}
private static string PadEvenly(string str, float size)
{
while (ImGui.CalcTextSize(str).X < size * ImGuiHelpers.GlobalScale)
{
str = $" {str} ";
}
return str;
}
private void PrintAnimation(AtkTimelineAnimation animation, int a, bool isActive, nint address)
{
var columns = this.BuildColumns(animation);
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 0.65F, 0.4F, 1), isActive))
{
using var tree = ImRaii.TreeNode($"[#{a}] [Frames {animation.StartFrameIdx}-{animation.EndFrameIdx}] {(isActive ? " (Active)" : string.Empty)}###{(nint)this.node}animTree{a}");
if (tree)
{
PrintFieldValuePair("Animation", $"{address:X}");
ShowStruct((AtkTimelineAnimation*)address);
if (columns.Count > 0)
{
using (ImRaii.Table(
$"##{(nint)this.node}animTable{a}",
columns.Count,
Borders | SizingFixedFit | RowBg | NoHostExtendX))
{
foreach (var c in columns)
{
ImGui.TableSetupColumn(c.Name, WidthFixed, c.Width);
}
ImGui.TableHeadersRow();
var rows = columns.Select(static c => c.Count).Max();
for (var i = 0; i < rows; i++)
{
ImGui.TableNextRow();
foreach (var c in columns)
{
ImGui.TableNextColumn();
c.PrintValueAt(i);
}
}
}
}
}
}
}
private List<IKeyGroupColumn> BuildColumns(AtkTimelineAnimation animation)
{
var keyGroups = animation.KeyGroups;
var columns = new List<IKeyGroupColumn>();
GetFrameColumn(keyGroups, columns, animation.EndFrameIdx);
GetPosColumns(keyGroups[0], columns);
GetRotationColumn(keyGroups[1], columns);
GetScaleColumns(keyGroups[2], columns);
GetAlphaColumn(keyGroups[3], columns);
GetTintColumns(keyGroups[4], columns);
if (this.node->Type is Image or NineGrid or ClippingMask)
{
GetPartIdColumn(keyGroups[5], columns);
}
else if (this.node->Type == Text)
{
GetTextColorColumn(keyGroups[5], columns);
}
GetEdgeColumn(keyGroups[6], columns);
GetLabelColumn(keyGroups[7], columns);
return columns;
}
}

View file

@ -0,0 +1,490 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.UiDebug2.Browsing;
using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static System.Globalization.NumberFormatInfo;
using static Dalamud.Interface.FontAwesomeIcon;
using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
using static Dalamud.Interface.UiBuilder;
using static Dalamud.Interface.Utility.ImGuiHelpers;
using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags;
using static ImGuiNET.ImGuiCol;
using static ImGuiNET.ImGuiWindowFlags;
// ReSharper disable StructLacksIEquatable.Global
#pragma warning disable CS0659
namespace Dalamud.Interface.Internal.UiDebug2;
/// <summary>
/// A tool that enables the user to select UI elements within the inspector by mousing over them onscreen.
/// </summary>
internal unsafe class ElementSelector : IDisposable
{
private const int UnitListCount = 18;
private readonly UiDebug2 uiDebug2;
private string addressSearchInput = string.Empty;
private int index;
/// <summary>
/// Initializes a new instance of the <see cref="ElementSelector"/> class.
/// </summary>
/// <param name="uiDebug2">The instance of <see cref="UiDebug2"/> this Element Selector belongs to.</param>
internal ElementSelector(UiDebug2 uiDebug2)
{
this.uiDebug2 = uiDebug2;
}
/// <summary>
/// Gets or sets the results retrieved by the Element Selector.
/// </summary>
internal static nint[] SearchResults { get; set; } = [];
/// <summary>
/// Gets or sets a value governing the highlighting of nodes when found via search.
/// </summary>
internal static float Countdown { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the window has scrolled down to the position of the search result.
/// </summary>
internal static bool Scrolled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the mouseover UI is currently active.
/// </summary>
internal bool Active { get; set; }
/// <inheritdoc/>
public void Dispose()
{
this.Active = false;
}
/// <summary>
/// Draws the Element Selector and Address Search interface at the bottom of the sidebar.
/// </summary>
internal void DrawInterface()
{
using (ImRaii.Child("###sidebar_elementSelector", new(250, 0), true))
{
using (ImRaii.PushFont(IconFont))
{
using (ImRaii.PushColor(Text, new Vector4(1, 1, 0.2f, 1), this.Active))
{
if (ImGui.Button($"{(char)ObjectUngroup}"))
{
this.Active = !this.Active;
}
if (Countdown > 0)
{
Countdown -= 1;
if (Countdown < 0)
{
Countdown = 0;
}
}
}
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Element Selector");
}
ImGui.SameLine();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 32);
ImGui.InputTextWithHint(
"###addressSearchInput",
"Address Search",
ref this.addressSearchInput,
18,
ImGuiInputTextFlags.AutoSelectAll);
ImGui.SameLine();
if (ImGuiComponents.IconButton("###elemSelectorAddrSearch", Search) && nint.TryParse(
this.addressSearchInput,
NumberStyles.HexNumber | NumberStyles.AllowHexSpecifier,
InvariantInfo,
out var address))
{
this.PerformSearch(address);
}
}
}
/// <summary>
/// Draws the Element Selector's search output within the main window.
/// </summary>
internal void DrawSelectorOutput()
{
ImGui.GetIO().WantCaptureKeyboard = true;
ImGui.GetIO().WantCaptureMouse = true;
ImGui.GetIO().WantTextInput = true;
if (ImGui.IsKeyPressed(ImGuiKey.Escape))
{
this.Active = false;
return;
}
ImGui.Text("ELEMENT SELECTOR");
ImGui.TextDisabled("Use the mouse to hover and identify UI elements, then click to jump to them in the inspector");
ImGui.TextDisabled("Use the scrollwheel to choose between overlapping elements");
ImGui.TextDisabled("Press ESCAPE to cancel");
ImGui.Spacing();
var mousePos = ImGui.GetMousePos() - MainViewport.Pos;
var addonResults = GetAtkUnitBaseAtPosition(mousePos);
using (ImRaii.PushColor(WindowBg, new Vector4(0.5f)))
{
using (ImRaii.Child("noClick", new(800, 2000), false, NoInputs | NoBackground | NoScrollWithMouse))
{
using (ImRaii.Group())
{
Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}");
ImGui.Spacing();
ImGui.Text("RESULTS:\n");
var i = 0;
foreach (var a in addonResults)
{
var name = a.Addon->NameString;
ImGui.TextUnformatted($"[Addon] {name}");
ImGui.Indent(15);
foreach (var n in a.Nodes)
{
var nSelected = i++ == this.index;
PrintNodeHeaderOnly(n.Node, nSelected, a.Addon);
if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
this.Active = false;
this.uiDebug2.SelectedAddonName = a.Addon->NameString;
var ptrList = new List<nint> { (nint)n.Node };
var nextNode = n.Node->ParentNode;
while (nextNode != null)
{
ptrList.Add((nint)nextNode);
nextNode = nextNode->ParentNode;
}
SearchResults = [.. ptrList];
Countdown = 100;
Scrolled = false;
}
if (nSelected)
{
n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1));
}
}
ImGui.Indent(-15);
}
if (i != 0)
{
this.index -= (int)ImGui.GetIO().MouseWheel;
while (this.index < 0)
{
this.index += i;
}
while (this.index >= i)
{
this.index -= i;
}
}
}
}
}
}
private static List<AddonResult> GetAtkUnitBaseAtPosition(Vector2 position)
{
var addonResults = new List<AddonResult>();
var unitListBaseAddr = GetUnitListBaseAddr();
if (unitListBaseAddr == null)
{
return addonResults;
}
foreach (var unit in UnitListOptions)
{
var unitManager = &unitListBaseAddr[unit.Index];
var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length);
for (var i = 0; i < safeCount; i++)
{
var addon = unitManager->Entries[i].Value;
if (addon == null || addon->RootNode == null)
{
continue;
}
if (!addon->IsVisible || !addon->RootNode->NodeFlags.HasFlag(Visible))
{
continue;
}
var addonResult = new AddonResult(addon, []);
if (addonResults.Contains(addonResult))
{
continue;
}
if (addon->X > position.X || addon->Y > position.Y)
{
continue;
}
if (addon->X + addon->RootNode->Width < position.X)
{
continue;
}
if (addon->Y + addon->RootNode->Height < position.Y)
{
continue;
}
addonResult.Nodes.AddRange(GetNodeAtPosition(&addon->UldManager, position, true));
addonResults.Add(addonResult);
}
}
return [.. addonResults.OrderBy(static w => w.Area)];
}
private static List<NodeResult> GetNodeAtPosition(AtkUldManager* uldManager, Vector2 position, bool reverse)
{
var nodeResults = new List<NodeResult>();
for (var i = 0; i < uldManager->NodeListCount; i++)
{
var node = uldManager->NodeList[i];
var bounds = new NodeBounds(node);
if (!bounds.ContainsPoint(position))
{
continue;
}
if ((int)node->Type >= 1000)
{
var compNode = (AtkComponentNode*)node;
nodeResults.AddRange(GetNodeAtPosition(&compNode->Component->UldManager, position, false));
}
nodeResults.Add(new() { NodeBounds = bounds, Node = node });
}
if (reverse)
{
nodeResults.Reverse();
}
return nodeResults;
}
private static bool FindByAddress(AtkUnitBase* atkUnitBase, nint address)
{
if (atkUnitBase->RootNode == null)
{
return false;
}
if (!FindByAddress(atkUnitBase->RootNode, address, out var path))
{
return false;
}
Scrolled = false;
SearchResults = path?.ToArray() ?? [];
Countdown = 100;
return true;
}
private static bool FindByAddress(AtkResNode* node, nint address, out List<nint>? path)
{
if (node == null)
{
path = null;
return false;
}
if ((nint)node == address)
{
path = [(nint)node];
return true;
}
if ((int)node->Type >= 1000)
{
var cNode = (AtkComponentNode*)node;
if (cNode->Component != null)
{
if ((nint)cNode->Component == address)
{
path = [(nint)node];
return true;
}
if (FindByAddress(cNode->Component->UldManager.RootNode, address, out path) && path != null)
{
path.Add((nint)node);
return true;
}
}
}
if (FindByAddress(node->ChildNode, address, out path) && path != null)
{
path.Add((nint)node);
return true;
}
if (FindByAddress(node->PrevSiblingNode, address, out path) && path != null)
{
return true;
}
path = null;
return false;
}
private static void PrintNodeHeaderOnly(AtkResNode* node, bool selected, AtkUnitBase* addon)
{
if (addon == null)
{
return;
}
if (node == null)
{
return;
}
var tree = AddonTree.GetOrCreate(addon->NameString);
if (tree == null)
{
return;
}
using (ImRaii.PushColor(Text, selected ? new Vector4(1, 1, 0.2f, 1) : new(0.6f, 0.6f, 0.6f, 1)))
{
ResNodeTree.GetOrCreate(node, tree).WriteTreeHeading();
}
}
private void PerformSearch(nint address)
{
var unitListBaseAddr = GetUnitListBaseAddr();
if (unitListBaseAddr == null)
{
return;
}
for (var i = 0; i < UnitListCount; i++)
{
var unitManager = &unitListBaseAddr[i];
var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length);
for (var j = 0; j < safeCount; j++)
{
var addon = unitManager->Entries[j].Value;
if ((nint)addon == address || FindByAddress(addon, address))
{
this.uiDebug2.SelectedAddonName = addon->NameString;
return;
}
}
}
}
/// <summary>
/// An <see cref="AtkUnitBase"/> found by the Element Selector.
/// </summary>
internal struct AddonResult
{
/// <summary>The addon itself.</summary>
internal AtkUnitBase* Addon;
/// <summary>A list of nodes discovered within this addon by the Element Selector.</summary>
internal List<NodeResult> Nodes;
/// <summary>The calculated area of the addon's root node.</summary>
internal float Area;
/// <summary>
/// Initializes a new instance of the <see cref="AddonResult"/> struct.
/// </summary>
/// <param name="addon">The addon found.</param>
/// <param name="nodes">A list for documenting nodes found within the addon.</param>
public AddonResult(AtkUnitBase* addon, List<NodeResult> nodes)
{
this.Addon = addon;
this.Nodes = nodes;
var rootNode = addon->RootNode;
this.Area = rootNode != null ? rootNode->Width * rootNode->Height * rootNode->ScaleY * rootNode->ScaleX : 0;
}
/// <inheritdoc/>
public override readonly bool Equals(object? obj)
{
if (obj is not AddonResult ar)
{
return false;
}
return (nint)this.Addon == (nint)ar.Addon;
}
}
/// <summary>
/// An <see cref="AtkResNode"/> found by the Element Selector.
/// </summary>
internal struct NodeResult
{
/// <summary>The node itself.</summary>
internal AtkResNode* Node;
/// <summary>A struct representing the perimeter of the node.</summary>
internal NodeBounds NodeBounds;
/// <inheritdoc/>
public override readonly bool Equals(object? obj)
{
if (obj is not NodeResult nr)
{
return false;
}
return nr.Node == this.Node;
}
}
}

View file

@ -0,0 +1,52 @@
using System.Numerics;
using Dalamud.Interface.Internal.UiDebug2.Browsing;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using ImGuiNET;
namespace Dalamud.Interface.Internal.UiDebug2;
/// <summary>
/// A popout window for an <see cref="AddonTree"/>.
/// </summary>
internal class AddonPopoutWindow : Window, IDisposable
{
private readonly AddonTree addonTree;
/// <summary>
/// Initializes a new instance of the <see cref="AddonPopoutWindow"/> class.
/// </summary>
/// <param name="tree">The AddonTree this popout will show.</param>
/// <param name="name">the window's name.</param>
public AddonPopoutWindow(AddonTree tree, string name)
: base(name)
{
this.addonTree = tree;
this.PositionCondition = ImGuiCond.Once;
var pos = ImGui.GetMousePos() + new Vector2(50, -50);
var workSize = ImGui.GetMainViewport().WorkSize;
var pos2 = new Vector2(Math.Min(workSize.X - 750, pos.X), Math.Min(workSize.Y - 250, pos.Y));
this.Position = pos2;
this.SizeCondition = ImGuiCond.Once;
this.Size = new(700, 200);
this.IsOpen = true;
this.SizeConstraints = new() { MinimumSize = new(100, 100) };
}
/// <inheritdoc/>
public override void Draw()
{
using (ImRaii.Child($"{this.WindowName}child", new(-1, -1), true))
{
this.addonTree.Draw();
}
}
/// <inheritdoc/>
public void Dispose()
{
}
}

View file

@ -0,0 +1,71 @@
using System.Numerics;
using Dalamud.Interface.Internal.UiDebug2.Browsing;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
namespace Dalamud.Interface.Internal.UiDebug2;
/// <summary>
/// A popout window for a <see cref="ResNodeTree"/>.
/// </summary>
internal unsafe class NodePopoutWindow : Window, IDisposable
{
private readonly ResNodeTree resNodeTree;
private bool firstDraw = true;
/// <summary>
/// Initializes a new instance of the <see cref="NodePopoutWindow"/> class.
/// </summary>
/// <param name="nodeTree">The node tree this window will show.</param>
/// <param name="windowName">The name of the window.</param>
public NodePopoutWindow(ResNodeTree nodeTree, string windowName)
: base(windowName)
{
this.resNodeTree = nodeTree;
var pos = ImGui.GetMousePos() + new Vector2(50, -50);
var workSize = ImGui.GetMainViewport().WorkSize;
var pos2 = new Vector2(Math.Min(workSize.X - 750, pos.X), Math.Min(workSize.Y - 250, pos.Y));
this.Position = pos2;
this.IsOpen = true;
this.PositionCondition = ImGuiCond.Once;
this.SizeCondition = ImGuiCond.Once;
this.Size = new(700, 200);
this.SizeConstraints = new() { MinimumSize = new(100, 100) };
}
private AddonTree AddonTree => this.resNodeTree.AddonTree;
private AtkResNode* Node => this.resNodeTree.Node;
/// <inheritdoc/>
public override void Draw()
{
if (this.Node != null && this.AddonTree.ContainsNode(this.Node))
{
using (ImRaii.Child($"{(nint)this.Node:X}popoutChild", new(-1, -1), true))
{
ResNodeTree.GetOrCreate(this.Node, this.AddonTree).Print(null, this.firstDraw);
this.firstDraw = false;
}
}
else
{
Log.Warning($"Popout closed ({this.WindowName}); Node or Addon no longer exists.");
this.IsOpen = false;
this.Dispose();
}
}
/// <inheritdoc/>
public void Dispose()
{
}
}

View file

@ -0,0 +1,213 @@
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static System.StringComparison;
using static Dalamud.Interface.FontAwesomeIcon;
namespace Dalamud.Interface.Internal.UiDebug2;
/// <inheritdoc cref="UiDebug2"/>
internal unsafe partial class UiDebug2
{
/// <summary>
/// All unit lists to check for addons.
/// </summary>
internal static readonly List<UnitListOption> UnitListOptions =
[
new(13, "Loaded"),
new(14, "Focused"),
new(0, "Depth Layer 1"),
new(1, "Depth Layer 2"),
new(2, "Depth Layer 3"),
new(3, "Depth Layer 4"),
new(4, "Depth Layer 5"),
new(5, "Depth Layer 6"),
new(6, "Depth Layer 7"),
new(7, "Depth Layer 8"),
new(8, "Depth Layer 9"),
new(9, "Depth Layer 10"),
new(10, "Depth Layer 11"),
new(11, "Depth Layer 12"),
new(12, "Depth Layer 13"),
new(15, "Units 16"),
new(16, "Units 17"),
new(17, "Units 18")
];
private string addonNameSearch = string.Empty;
private bool visFilter;
/// <summary>
/// Gets the base address for all unit lists.
/// </summary>
/// <returns>The address, if found.</returns>
internal static AtkUnitList* GetUnitListBaseAddr() => &((UIModule*)GameGui.GetUIModule())->GetRaptureAtkModule()->RaptureAtkUnitManager.AtkUnitManager.DepthLayerOneList;
private void DrawSidebar()
{
using (ImRaii.Group())
{
this.DrawNameSearch();
this.DrawAddonSelectionList();
this.elementSelector.DrawInterface();
}
}
private void DrawNameSearch()
{
using (ImRaii.Child("###sidebar_nameSearch", new(250, 40), true))
{
var atkUnitBaseSearch = this.addonNameSearch;
Vector4? defaultColor = this.visFilter ? new(0.0f, 0.8f, 0.2f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1);
if (ImGuiComponents.IconButton("filter", LowVision, defaultColor))
{
this.visFilter = !this.visFilter;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Filter by visibility");
}
ImGui.SameLine();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputTextWithHint("###atkUnitBaseSearch", "Filter by name", ref atkUnitBaseSearch, 0x20))
{
this.addonNameSearch = atkUnitBaseSearch;
}
}
}
private void DrawAddonSelectionList()
{
using (ImRaii.Child("###sideBar_addonList", new(250, -44), true, ImGuiWindowFlags.AlwaysVerticalScrollbar))
{
var unitListBaseAddr = GetUnitListBaseAddr();
foreach (var unit in UnitListOptions)
{
this.DrawUnitListOption(unitListBaseAddr, unit);
}
}
}
private void DrawUnitListOption(AtkUnitList* unitListBaseAddr, UnitListOption unit)
{
var atkUnitList = &unitListBaseAddr[unit.Index];
var safeLength = Math.Min(atkUnitList->Count, atkUnitList->Entries.Length);
var options = new List<AddonOption>();
var totalCount = 0;
var matchCount = 0;
var anyVisible = false;
var usingFilter = this.visFilter || !string.IsNullOrEmpty(this.addonNameSearch);
for (var i = 0; i < safeLength; i++)
{
var addon = atkUnitList->Entries[i].Value;
if (addon == null)
{
continue;
}
totalCount++;
if (this.visFilter && !addon->IsVisible)
{
continue;
}
if (!string.IsNullOrEmpty(this.addonNameSearch) && !addon->NameString.Contains(this.addonNameSearch, InvariantCultureIgnoreCase))
{
continue;
}
matchCount++;
anyVisible |= addon->IsVisible;
options.Add(new AddonOption(addon->NameString, addon->IsVisible));
}
if (matchCount == 0)
{
return;
}
var countStr = $"{(usingFilter ? $"{matchCount}/" : string.Empty)}{totalCount}";
using var col1 = ImRaii.PushColor(ImGuiCol.Text, anyVisible ? new Vector4(1) : new Vector4(0.6f, 0.6f, 0.6f, 1));
using var tree = ImRaii.TreeNode($"{unit.Name} [{countStr}]###unitListTree{unit.Index}");
col1.Pop();
if (tree)
{
foreach (var option in options)
{
using (ImRaii.PushColor(ImGuiCol.Text, option.Visible ? new Vector4(0.1f, 1f, 0.1f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1)))
{
if (ImGui.Selectable($"{option.Name}##select{option.Name}", this.SelectedAddonName == option.Name))
{
this.SelectedAddonName = option.Name;
}
}
}
}
}
/// <summary>
/// A struct representing a unit list that can be browed in the sidebar.
/// </summary>
internal struct UnitListOption
{
/// <summary>The index of the unit list.</summary>
internal uint Index;
/// <summary>The name of the unit list.</summary>
internal string Name;
/// <summary>
/// Initializes a new instance of the <see cref="UnitListOption"/> struct.
/// </summary>
/// <param name="i">The index of the unit list.</param>
/// <param name="name">The name of the unit list.</param>
internal UnitListOption(uint i, string name)
{
this.Index = i;
this.Name = name;
}
}
/// <summary>
/// A struct representing an addon that can be selected in the sidebar.
/// </summary>
internal struct AddonOption
{
/// <summary>The name of the addon.</summary>
internal string Name;
/// <summary>Whether the addon is visible.</summary>
internal bool Visible;
/// <summary>
/// Initializes a new instance of the <see cref="AddonOption"/> struct.
/// </summary>
/// <param name="name">The name of the addon.</param>
/// <param name="visible">Whether the addon is visible.</param>
internal AddonOption(string name, bool visible)
{
this.Name = name;
this.Visible = visible;
}
}
}

View file

@ -0,0 +1,109 @@
using System.Collections.Generic;
using Dalamud.Game.Gui;
using Dalamud.Interface.Internal.UiDebug2.Browsing;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static ImGuiNET.ImGuiWindowFlags;
namespace Dalamud.Interface.Internal.UiDebug2;
// Original version by aers https://github.com/aers/FFXIVUIDebug
// Also incorporates features from Caraxi's fork https://github.com/Caraxi/SimpleTweaksPlugin/blob/main/Debugging/UIDebug.cs
/// <summary>
/// A tool for browsing the contents and structure of UI elements.
/// </summary>
internal partial class UiDebug2 : IDisposable
{
private readonly ElementSelector elementSelector;
/// <summary>
/// Initializes a new instance of the <see cref="UiDebug2"/> class.
/// </summary>
internal UiDebug2()
{
this.elementSelector = new(this);
}
/// <inheritdoc cref="ModuleLog"/>
internal static ModuleLog Log { get; set; } = new("UiDebug2");
/// <inheritdoc cref="IGameGui"/>
internal static IGameGui GameGui { get; set; } = Service<GameGui>.Get();
/// <summary>
/// Gets a collection of <see cref="AddonTree"/> instances, each representing an <see cref="FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase"/>.
/// </summary>
internal static Dictionary<string, AddonTree> AddonTrees { get; } = [];
/// <summary>
/// Gets or sets a window system to handle any popout windows for addons or nodes.
/// </summary>
internal static WindowSystem PopoutWindows { get; set; } = new("UiDebugPopouts");
/// <summary>
/// Gets or sets the name of the currently-selected <see cref="AtkUnitBase"/>.
/// </summary>
internal string? SelectedAddonName { get; set; }
/// <summary>
/// Clears all windows and <see cref="AddonTree"/>s.
/// </summary>
public void Dispose()
{
foreach (var a in AddonTrees)
{
a.Value.Dispose();
}
AddonTrees.Clear();
PopoutWindows.RemoveAllWindows();
this.elementSelector.Dispose();
}
/// <summary>
/// Draws the UiDebug tool's interface and contents.
/// </summary>
internal void Draw()
{
PopoutWindows.Draw();
this.DrawSidebar();
this.DrawMainPanel();
}
private void DrawMainPanel()
{
ImGui.SameLine();
using (ImRaii.Child("###uiDebugMainPanel", new(-1, -1), true, HorizontalScrollbar))
{
if (this.elementSelector.Active)
{
this.elementSelector.DrawSelectorOutput();
}
else
{
if (this.SelectedAddonName != null)
{
var addonTree = AddonTree.GetOrCreate(this.SelectedAddonName);
if (addonTree == null)
{
this.SelectedAddonName = null;
return;
}
addonTree.Draw();
}
}
}
}
}

View file

@ -0,0 +1,179 @@
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Graphics;
using ImGuiNET;
using static Dalamud.Interface.ColorHelpers;
using static ImGuiNET.ImGuiCol;
namespace Dalamud.Interface.Internal.UiDebug2.Utility;
/// <summary>
/// Miscellaneous ImGui tools used by <see cref="UiDebug2"/>.
/// </summary>
internal static class Gui
{
/// <summary>
/// A radio-button-esque input that uses Fontawesome icon buttons.
/// </summary>
/// <typeparam name="T">The type of value being set.</typeparam>
/// <param name="label">The label for the inputs.</param>
/// <param name="val">The value being set.</param>
/// <param name="options">A list of all options.</param>
/// <param name="icons">A list of icons corresponding to the options.</param>
/// <returns>true if a button is clicked.</returns>
internal static unsafe bool IconButtonSelect<T>(string label, ref T val, List<T> options, List<FontAwesomeIcon> icons)
{
var ret = false;
for (var i = 0; i < options.Count; i++)
{
if (i > 0)
{
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
}
var option = options[i];
var icon = icons.Count > i ? icons[i] : FontAwesomeIcon.Question;
var color = *ImGui.GetStyleColorVec4(val is not null && val.Equals(option) ? ButtonActive : Button);
if (ImGuiComponents.IconButton($"{label}{option}{i}", icon, color))
{
val = option;
ret = true;
}
}
return ret;
}
/// <summary>
/// Prints field name and its value.
/// </summary>
/// <param name="fieldName">The name of the field.</param>
/// <param name="value">The value of the field.</param>
/// <param name="copy">Whether to enable click-to-copy.</param>
internal static void PrintFieldValuePair(string fieldName, string value, bool copy = true)
{
ImGui.TextUnformatted($"{fieldName}:");
ImGui.SameLine();
if (copy)
{
ClickToCopyText(value);
}
else
{
ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), value);
}
}
/// <summary>
/// Prints a set of fields and their values.
/// </summary>
/// <param name="pairs">Tuples of fieldnames and values to display.</param>
internal static void PrintFieldValuePairs(params (string FieldName, string Value)[] pairs)
{
for (var i = 0; i < pairs.Length; i++)
{
if (i != 0)
{
ImGui.SameLine();
}
PrintFieldValuePair(pairs[i].FieldName, pairs[i].Value, false);
}
}
/// <inheritdoc cref="PrintColor(Vector4,string)"/>
internal static void PrintColor(ByteColor color, string fmt) => PrintColor(RgbaUintToVector4(color.RGBA), fmt);
/// <inheritdoc cref="PrintColor(Vector4,string)"/>
internal static void PrintColor(Vector3 color, string fmt) => PrintColor(new Vector4(color, 1), fmt);
/// <summary>
/// Prints a text string representing a color, with a backdrop in that color.
/// </summary>
/// <param name="color">The color value.</param>
/// <param name="fmt">The text string to print.</param>
/// <remarks>Colors the text itself either white or black, depending on the luminosity of the background color.</remarks>
internal static void PrintColor(Vector4 color, string fmt)
{
using (new ImRaii.Color().Push(Text, Luminosity(color) < 0.5f ? new Vector4(1) : new(0, 0, 0, 1)).Push(Button, color).Push(ButtonActive, color).Push(ButtonHovered, color))
{
ImGui.SmallButton(fmt);
}
return;
static double Luminosity(Vector4 vector4) =>
Math.Pow(
(Math.Pow(vector4.X, 2) * 0.299f) +
(Math.Pow(vector4.Y, 2) * 0.587f) +
(Math.Pow(vector4.Z, 2) * 0.114f),
0.5f) * vector4.W;
}
/// <summary>
/// Print out text that can be copied when clicked.
/// </summary>
/// <param name="text">The text to show.</param>
/// <param name="textCopy">The text to copy when clicked.</param>
internal static void ClickToCopyText(string text, string? textCopy = null)
{
using (ImRaii.PushColor(Text, new Vector4(0.6f, 0.6f, 0.6f, 1)))
{
textCopy ??= text;
ImGui.TextUnformatted($"{text}");
}
if (ImGui.IsItemHovered())
{
using (ImRaii.Tooltip())
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.Copy.ToIconString());
}
ImGui.SameLine();
ImGui.TextUnformatted($"{textCopy}");
}
}
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText($"{textCopy}");
}
}
/// <summary>
/// Draws a tooltip that changes based on the cursor's x-position within the hovered item.
/// </summary>
/// <param name="tooltips">The text for each section.</param>
/// <returns>true if the item is hovered.</returns>
internal static bool SplitTooltip(params string[] tooltips)
{
if (!ImGui.IsItemHovered())
{
return false;
}
var mouseX = ImGui.GetMousePos().X;
var minX = ImGui.GetItemRectMin().X;
var maxX = ImGui.GetItemRectMax().X;
var prog = (mouseX - minX) / (maxX - minX);
var index = (int)Math.Floor(prog * tooltips.Length);
using (ImRaii.Tooltip())
{
ImGui.TextUnformatted(tooltips[index]);
}
return true;
}
}

View file

@ -0,0 +1,170 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static System.Math;
using static Dalamud.Interface.ColorHelpers;
namespace Dalamud.Interface.Internal.UiDebug2.Utility;
/// <summary>
/// A struct representing the perimeter of an <see cref="AtkResNode"/>, accounting for all transformations.
/// </summary>
public unsafe struct NodeBounds
{
/// <summary>
/// Initializes a new instance of the <see cref="NodeBounds"/> struct.
/// </summary>
/// <param name="node">The node to calculate the bounds of.</param>
internal NodeBounds(AtkResNode* node)
{
if (node == null)
{
return;
}
var w = node->Width;
var h = node->Height;
this.Points = w == 0 && h == 0 ? [new(0)] : [new(0), new(w, 0), new(w, h), new(0, h)];
this.TransformPoints(node);
}
/// <summary>
/// Initializes a new instance of the <see cref="NodeBounds"/> struct, containing only a single given point.
/// </summary>
/// <param name="point">The point onscreen.</param>
/// <param name="node">The node used to calculate transformations.</param>
internal NodeBounds(Vector2 point, AtkResNode* node)
{
this.Points = [point];
this.TransformPoints(node);
}
private List<Vector2> Points { get; set; } = [];
/// <summary>
/// Draws the bounds onscreen.
/// </summary>
/// <param name="col">The color of line to use.</param>
/// <param name="thickness">The thickness of line to use.</param>
/// <remarks>If there is only a single point to draw, it will be indicated with a circle and dot.</remarks>
internal readonly void Draw(Vector4 col, int thickness = 1)
{
if (this.Points == null || this.Points.Count == 0)
{
return;
}
if (this.Points.Count == 1)
{
ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness);
ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], thickness, RgbaVector4ToUint(col), 12, thickness + 1);
}
else
{
var path = new ImVectorWrapper<Vector2>(this.Points.Count);
foreach (var p in this.Points)
{
path.Add(p);
}
ImGui.GetBackgroundDrawList()
.AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
path.Dispose();
}
}
/// <summary>
/// Draws the bounds onscreen, filled in.
/// </summary>
/// <param name="col">The fill and border color.</param>
/// <param name="thickness">The border thickness.</param>
internal readonly void DrawFilled(Vector4 col, int thickness = 1)
{
if (this.Points == null || this.Points.Count == 0)
{
return;
}
if (this.Points.Count == 1)
{
ImGui.GetBackgroundDrawList()
.AddCircleFilled(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12);
ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, RgbaVector4ToUint(col), 12, thickness);
}
else
{
var path = new ImVectorWrapper<Vector2>(this.Points.Count);
foreach (var p in this.Points)
{
path.Add(p);
}
ImGui.GetBackgroundDrawList()
.AddConvexPolyFilled(ref path[0], path.Length, RgbaVector4ToUint(col with { W = col.W / 2 }));
ImGui.GetBackgroundDrawList()
.AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
path.Dispose();
}
}
/// <summary>
/// Checks whether the bounds contain a given point.
/// </summary>
/// <param name="p">The point to check.</param>
/// <returns>True if the point exists within the bounds.</returns>
internal readonly bool ContainsPoint(Vector2 p)
{
var count = this.Points.Count;
var inside = false;
for (var i = 0; i < count; i++)
{
var p1 = this.Points[i];
var p2 = this.Points[(i + 1) % count];
if (p.Y > Min(p1.Y, p2.Y) &&
p.Y <= Max(p1.Y, p2.Y) &&
p.X <= Max(p1.X, p2.X) &&
(p1.X.Equals(p2.X) || p.X <= ((p.Y - p1.Y) * (p2.X - p1.X) / (p2.Y - p1.Y)) + p1.X))
{
inside = !inside;
}
}
return inside;
}
private static Vector2 TransformPoint(Vector2 p, Vector2 o, float r, Vector2 s)
{
var cosR = (float)Cos(r);
var sinR = (float)Sin(r);
var d = (p - o) * s;
return new(
o.X + (d.X * cosR) - (d.Y * sinR),
o.Y + (d.X * sinR) + (d.Y * cosR));
}
private void TransformPoints(AtkResNode* transformNode)
{
while (transformNode != null)
{
var offset = new Vector2(transformNode->X, transformNode->Y);
var origin = offset + new Vector2(transformNode->OriginX, transformNode->OriginY);
var rotation = transformNode->Rotation;
var scale = new Vector2(transformNode->ScaleX, transformNode->ScaleY);
this.Points = this.Points.Select(b => TransformPoint(b + offset, origin, rotation, scale)).ToList();
transformNode = transformNode->ParentNode;
}
}
}

View file

@ -21,6 +21,7 @@ internal class DataWindow : Window, IDisposable
private readonly IDataWindowWidget[] modules =
{
new AddonInspectorWidget(),
new AddonInspectorWidget2(),
new AddonLifecycleWidget(),
new AddonWidget(),
new AddressesWidget(),

View file

@ -1,4 +1,4 @@
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for displaying addon inspector.

View file

@ -0,0 +1,35 @@
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for displaying addon inspector.
/// </summary>
internal class AddonInspectorWidget2 : IDataWindowWidget
{
private UiDebug2.UiDebug2? addonInspector2;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["ai2", "addoninspector2"];
/// <inheritdoc/>
public string DisplayName { get; init; } = "Addon Inspector v2 (Testing)";
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
public void Load()
{
this.addonInspector2 = new UiDebug2.UiDebug2();
if (this.addonInspector2 is not null)
{
this.Ready = true;
}
}
/// <inheritdoc/>
public void Draw()
{
this.addonInspector2?.Draw();
}
}

View file

@ -98,6 +98,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private bool deletePluginConfigWarningModalDrawing = true;
private bool deletePluginConfigWarningModalOnNextFrame = false;
private bool deletePluginConfigWarningModalExplainTesting = false;
private string deletePluginConfigWarningModalPluginName = string.Empty;
private TaskCompletionSource<bool>? deletePluginConfigWarningModalTaskCompletionSource;
@ -732,7 +733,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
}
private void DrawFooter()
{
var configuration = Service<DalamudConfiguration>.Get();
@ -972,10 +973,11 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
private Task<bool> ShowDeletePluginConfigWarningModal(string pluginName)
private Task<bool> ShowDeletePluginConfigWarningModal(string pluginName, bool explainTesting = false)
{
this.deletePluginConfigWarningModalOnNextFrame = true;
this.deletePluginConfigWarningModalPluginName = pluginName;
this.deletePluginConfigWarningModalExplainTesting = explainTesting;
this.deletePluginConfigWarningModalTaskCompletionSource = new TaskCompletionSource<bool>();
return this.deletePluginConfigWarningModalTaskCompletionSource.Task;
}
@ -986,6 +988,13 @@ internal class PluginInstallerWindow : Window, IDisposable
if (ImGui.BeginPopupModal(modalTitle, ref this.deletePluginConfigWarningModalDrawing, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar))
{
if (this.deletePluginConfigWarningModalExplainTesting)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudOrange);
ImGui.Text(Locs.DeletePluginConfigWarningModal_ExplainTesting());
ImGui.PopStyleColor();
}
ImGui.Text(Locs.DeletePluginConfigWarningModal_Body(this.deletePluginConfigWarningModalPluginName));
ImGui.Spacing();
@ -2898,7 +2907,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (ImGui.MenuItem(Locs.PluginContext_DeletePluginConfigReload))
{
this.ShowDeletePluginConfigWarningModal(plugin.Manifest.Name).ContinueWith(t =>
this.ShowDeletePluginConfigWarningModal(plugin.Manifest.Name, optIn != null).ContinueWith(t =>
{
var shouldDelete = t.Result;
@ -4263,7 +4272,9 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string DeletePluginConfigWarningModal_Title => Loc.Localize("InstallerDeletePluginConfigWarning", "Warning###InstallerDeletePluginConfigWarning");
public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for {0}?").Format(pluginName);
public static string DeletePluginConfigWarningModal_ExplainTesting() => Loc.Localize("InstallerDeletePluginConfigWarningExplainTesting", "Do not select this option if you are only trying to disable testing!");
public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for {0}?\nYou will lose all of your settings for this plugin.").Format(pluginName);
public static string DeletePluginConfirmWarningModal_Yes => Loc.Localize("InstallerDeletePluginConfigWarningYes", "Yes");

View file

@ -140,7 +140,7 @@ Dale
Arcane Disgea
Risu
Tom
Blyoom
Blooym
Valk

View file

@ -122,7 +122,7 @@ public class SettingsTabLook : SettingsTab
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingToggleTsm", "Show title screen menu"),
Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen."),
Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen.\nDisabling this will also hide the Dalamud version text on the title screen."),
c => c.ShowTsm,
(v, c) => c.ShowTsm = v),

View file

@ -5,8 +5,12 @@ using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Console;
using Dalamud.Game;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
@ -14,10 +18,15 @@ using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Support;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows;
@ -39,7 +48,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private readonly IFontAtlas privateAtlas;
private readonly Lazy<IFontHandle> myFontHandle;
private readonly Lazy<IDalamudTextureWrap> shadeTexture;
private readonly AddonLifecycleEventListener versionStringListener;
private readonly Dictionary<Guid, InOutCubic> shadeEasings = new();
private readonly Dictionary<Guid, InOutQuint> moveEasings = new();
private readonly Dictionary<Guid, InOutCubic> logoEasings = new();
@ -49,7 +59,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private InOutCubic? fadeOutEasing;
private State state = State.Hide;
/// <summary>
/// Initializes a new instance of the <see cref="TitleScreenMenuWindow"/> class.
/// </summary>
@ -61,6 +71,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
/// <param name="titleScreenMenu">An instance of <see cref="TitleScreenMenu"/>.</param>
/// <param name="gameGui">An instance of <see cref="GameGui"/>.</param>
/// <param name="consoleManager">An instance of <see cref="ConsoleManager"/>.</param>
/// <param name="addonLifecycle">An instance of <see cref="AddonLifecycle"/>.</param>
public TitleScreenMenuWindow(
ClientState clientState,
DalamudConfiguration configuration,
@ -69,7 +80,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
Framework framework,
GameGui gameGui,
TitleScreenMenu titleScreenMenu,
ConsoleManager consoleManager)
ConsoleManager consoleManager,
AddonLifecycle addonLifecycle)
: base(
"TitleScreenMenuOverlay",
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar |
@ -109,6 +121,10 @@ internal class TitleScreenMenuWindow : Window, IDisposable
framework.Update += this.FrameworkOnUpdate;
this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate);
this.versionStringListener = new AddonLifecycleEventListener(AddonEvent.PreDraw, "_TitleRevision", this.OnVersionStringDraw);
addonLifecycle.RegisterListener(this.versionStringListener);
this.scopedFinalizer.Add(() => addonLifecycle.UnregisterListener(this.versionStringListener));
}
private enum State
@ -414,5 +430,41 @@ internal class TitleScreenMenuWindow : Window, IDisposable
this.IsOpen = false;
}
private unsafe void OnVersionStringDraw(AddonEvent ev, AddonArgs args)
{
if (args is not AddonDrawArgs setupArgs) return;
var addon = (AtkUnitBase*)setupArgs.Addon;
var textNode = addon->GetTextNodeById(3);
// look and feel init. should be harmless to set.
textNode->TextFlags |= (byte)TextFlags.MultiLine;
textNode->AlignmentType = AlignmentType.TopLeft;
if (!this.configuration.ShowTsm || !this.showTsm.Value)
{
textNode->NodeText.SetString(addon->AtkValues[1].String);
return;
}
var pm = Service<PluginManager>.GetNullable();
var pluginCount = pm?.InstalledPlugins.Count(c => c.State == PluginState.Loaded) ?? 0;
var titleVersionText = new SeStringBuilder()
.AddText(addon->AtkValues[1].GetValueAsString())
.AddText("\n\n")
.AddUiGlow(701)
.AddUiForeground(SeIconChar.BoxedLetterD.ToIconString(), 539)
.AddUiGlowOff()
.AddText($" Dalamud: {Util.GetScmVersion()}")
.AddText($" - {pluginCount} {(pluginCount != 1 ? "plugins" : "plugin")} loaded");
if (pm?.SafeMode ?? false)
titleVersionText.AddUiForeground(" [SAFE MODE]", 17);
textNode->NodeText.SetString(titleVersionText.Build().EncodeWithNullTerminator());
}
private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync();
}

View file

@ -25,7 +25,8 @@ public interface ISharedImmediateTexture
/// <see cref="ISharedImmediateTexture"/>s may be cached, but the performance benefit will be minimal.</para>
/// <para>Calling outside the main thread will fail.</para>
/// <para>This function does not throw.</para>
/// <para><see cref="IDisposable.Dispose"/> will be ignored.</para>
/// <para><see cref="IDisposable.Dispose"/> will be ignored, including the cases when the returned texture wrap
/// is passed to a function with <c>leaveWrapOpen</c> parameter.</para>
/// <para>If the texture is unavailable for any reason, then the returned instance of
/// <see cref="IDalamudTextureWrap"/> will point to an empty texture instead.</para>
/// </remarks>
@ -42,7 +43,8 @@ public interface ISharedImmediateTexture
/// <see cref="ISharedImmediateTexture"/>s may be cached, but the performance benefit will be minimal.</para>
/// <para>Calling outside the main thread will fail.</para>
/// <para>This function does not throw.</para>
/// <para><see cref="IDisposable.Dispose"/> will be ignored.</para>
/// <para><see cref="IDisposable.Dispose"/> will be ignored, including the cases when the returned texture wrap
/// is passed to a function with <c>leaveWrapOpen</c> parameter.</para>
/// <para>If the texture is unavailable for any reason, then <paramref name="defaultWrap"/> will be returned.</para>
/// </remarks>
[return: NotNullIfNotNull(nameof(defaultWrap))]
@ -59,7 +61,8 @@ public interface ISharedImmediateTexture
/// <see cref="ISharedImmediateTexture"/>s may be cached, but the performance benefit will be minimal.</para>
/// <para>Calling outside the main thread will fail.</para>
/// <para>This function does not throw.</para>
/// <para><see cref="IDisposable.Dispose"/> on the returned <paramref name="texture"/> will be ignored.</para>
/// <para><see cref="IDisposable.Dispose"/> on the returned <paramref name="texture"/> will be ignored, including
/// the cases when the returned texture wrap is passed to a function with <c>leaveWrapOpen</c> parameter.</para>
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception);

View file

@ -2,7 +2,6 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Textures.TextureWraps.Internal;
using Dalamud.Plugin.Internal.Types;
@ -10,6 +9,8 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.TerraFxCom;
using Lumina.Data.Files;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
@ -18,6 +19,72 @@ namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
/// <inheritdoc/>
unsafe nint ITextureProvider.ConvertToKernelTexture(IDalamudTextureWrap wrap, bool leaveWrapOpen) =>
(nint)this.ConvertToKernelTexture(wrap, leaveWrapOpen);
/// <inheritdoc cref="ITextureProvider.ConvertToKernelTexture"/>
public unsafe FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture* ConvertToKernelTexture(
IDalamudTextureWrap wrap,
bool leaveWrapOpen = false)
{
using var wrapAux = new WrapAux(wrap, leaveWrapOpen);
var flags = TexFile.Attribute.TextureType2D;
if (wrapAux.Desc.Usage == D3D11_USAGE.D3D11_USAGE_IMMUTABLE)
flags |= TexFile.Attribute.Immutable;
if (wrapAux.Desc.Usage == D3D11_USAGE.D3D11_USAGE_DYNAMIC)
flags |= TexFile.Attribute.ReadWrite;
if ((wrapAux.Desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) != 0)
flags |= TexFile.Attribute.CpuRead;
if ((wrapAux.Desc.BindFlags & (uint)D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET) != 0)
flags |= TexFile.Attribute.TextureRenderTarget;
if ((wrapAux.Desc.BindFlags & (uint)D3D11_BIND_FLAG.D3D11_BIND_DEPTH_STENCIL) != 0)
flags |= TexFile.Attribute.TextureDepthStencil;
if (wrapAux.Desc.ArraySize != 1)
throw new NotSupportedException("TextureArray2D is currently not supported.");
var gtex = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture.CreateTexture2D(
(int)wrapAux.Desc.Width,
(int)wrapAux.Desc.Height,
(byte)wrapAux.Desc.MipLevels,
(uint)TexFile.TextureFormat.Null, // instructs the game to skip preprocessing it seems
(uint)flags,
0);
// Kernel::Texture owns these resources. We're passing the ownership to them.
wrapAux.TexPtr->AddRef();
wrapAux.SrvPtr->AddRef();
// Not sure this is needed
var ltf = wrapAux.Desc.Format switch
{
DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => TexFile.TextureFormat.R32G32B32A32F,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => TexFile.TextureFormat.R16G16B16A16F,
DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT => TexFile.TextureFormat.R32G32F,
DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT => TexFile.TextureFormat.R16G16F,
DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => TexFile.TextureFormat.R32F,
DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS => TexFile.TextureFormat.D24S8,
DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS => TexFile.TextureFormat.D16,
DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => TexFile.TextureFormat.A8,
DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM => TexFile.TextureFormat.BC1,
DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM => TexFile.TextureFormat.BC2,
DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM => TexFile.TextureFormat.BC3,
DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM => TexFile.TextureFormat.BC5,
DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM => TexFile.TextureFormat.B4G4R4A4,
DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => TexFile.TextureFormat.B5G5R5A1,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => TexFile.TextureFormat.B8G8R8A8,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => TexFile.TextureFormat.B8G8R8X8,
DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM => TexFile.TextureFormat.BC7,
_ => TexFile.TextureFormat.Null,
};
gtex->TextureFormat = (FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.TextureFormat)ltf;
gtex->D3D11Texture2D = wrapAux.TexPtr;
gtex->D3D11ShaderResourceView = wrapAux.SrvPtr;
return gtex;
}
/// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) =>
this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat);

View file

@ -134,6 +134,10 @@ internal sealed class TextureManagerPluginScoped
: $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name})";
}
/// <inheritdoc/>
public unsafe nint ConvertToKernelTexture(IDalamudTextureWrap wrap, bool leaveWrapOpen = false) =>
(nint)this.ManagerOrThrow.ConvertToKernelTexture(wrap, leaveWrapOpen);
/// <inheritdoc/>
public IDalamudTextureWrap CreateEmpty(
RawImageSpecification specs,

View file

@ -347,7 +347,7 @@ public abstract class Window
}
catch (Exception ex)
{
Log.Error(ex, $"Error during Draw(): {this.WindowName}");
Log.Error(ex, "Error during Draw(): {WindowName}", this.WindowName);
}
}

View file

@ -526,13 +526,40 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
/// <param name="affectedThisPlugin">If this plugin was affected by the change.</param>
internal void NotifyActivePluginsChanged(PluginListInvalidationKind kind, bool affectedThisPlugin)
{
this.ActivePluginsChanged?.Invoke(kind, affectedThisPlugin);
if (this.ActivePluginsChanged is { } callback)
{
foreach (var action in callback.GetInvocationList().Cast<IDalamudPluginInterface.ActivePluginsChangedDelegate>())
{
try
{
action(kind, affectedThisPlugin);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
}
}
private void OnLocalizationChanged(string langCode)
{
this.UiLanguage = langCode;
this.LanguageChanged?.Invoke(langCode);
if (this.LanguageChanged is { } callback)
{
foreach (var action in callback.GetInvocationList().Cast<IDalamudPluginInterface.LanguageChangedDelegate>())
{
try
{
action(langCode);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
}
}
private void OnDalamudConfigurationSaved(DalamudConfiguration dalamudConfiguration)

View file

@ -205,6 +205,13 @@ internal class PluginRepository
return false;
}
if (manifest.TestingAssemblyVersion != null &&
manifest.TestingAssemblyVersion > manifest.AssemblyVersion &&
manifest.TestingDalamudApiLevel == null)
{
Log.Warning("The plugin {PluginName} in {RepoLink} has a testing version available, but it lacks an associated testing API. The 'TestingDalamudApiLevel' property is required.", manifest.InternalName, this.PluginMasterUrl);
}
return true;
}

View file

@ -9,7 +9,6 @@ namespace Dalamud.Plugin.Ipc;
/// </summary>
public interface ICallGateSubscriber
{
/// <inheritdoc cref="CallGatePubSubBase.HasAction"/>
public bool HasAction { get; }

View file

@ -103,4 +103,11 @@ public interface IGameInventory
/// <inheritdoc cref="ItemMerged"/>
public event InventoryChangedDelegate<InventoryItemMergedArgs> ItemMergedExplicit;
/// <summary>
/// Gets all item slots of the specified inventory type.
/// </summary>
/// <param name="type">The type of inventory to get the items for.</param>
/// <returns>A read-only span of all items in the specified inventory type.</returns>
public ReadOnlySpan<GameInventoryItem> GetInventoryItems(GameInventoryType type);
}

View file

@ -26,6 +26,15 @@ public interface INamePlateGui
/// </remarks>
event OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <summary>
/// An event which fires after nameplate data is updated and at least one nameplate had important updates. The
/// subscriber is provided with a list of handlers for nameplates with important updates.
/// </summary>
/// <remarks>
/// Fires before <see cref="OnPostDataUpdate"/>.
/// </remarks>
event OnPlateUpdateDelegate? OnPostNamePlateUpdate;
/// <summary>
/// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all
/// nameplates.
@ -36,6 +45,16 @@ public interface INamePlateGui
/// </remarks>
event OnPlateUpdateDelegate? OnDataUpdate;
/// <summary>
/// An event which fires after nameplate data is updated. The subscriber is provided with a list of handlers for all
/// nameplates.
/// </summary>
/// <remarks>
/// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases
/// <see cref="OnNamePlateUpdate"/> is preferred. Fires after <see cref="OnPostNamePlateUpdate"/>.
/// </remarks>
event OnPlateUpdateDelegate? OnPostDataUpdate;
/// <summary>
/// Requests that all nameplates should be redrawn on the following frame.
/// </summary>

View file

@ -5,7 +5,6 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
@ -281,4 +280,20 @@ public interface ITextureProvider
/// <returns><c>true</c> if supported.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat);
/// <summary>Converts an existing <see cref="IDalamudTextureWrap"/> instance to a new instance of
/// <see cref="FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture"/> which can be used to supply a custom
/// texture onto an in-game addon (UI element.)</summary>
/// <param name="wrap">Instance of <see cref="IDalamudTextureWrap"/> to convert.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <returns>Address of the new <see cref="FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture"/>.</returns>
/// <example>See <c>PrintTextureInfo</c> in <see cref="Interface.Internal.UiDebug.PrintSimpleNode"/> for an example
/// of replacing the texture of an image node.</example>
/// <remarks>
/// <para>If the returned kernel texture is to be destroyed, call the fourth function in its vtable, by calling
/// <see cref="FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture.DecRef"/> or
/// <c>((delegate* unmanaged&lt;nint, void&gt;)(*(nint**)ptr)[3](ptr)</c>.</para>
/// </remarks>
nint ConvertToKernelTexture(IDalamudTextureWrap wrap, bool leaveWrapOpen = false);
}

View file

@ -22,6 +22,9 @@ public interface ITextureReadbackProvider
/// <remarks>
/// <para>The length of the returned <c>RawData</c> may not match
/// <see cref="RawImageSpecification.Height"/> * <see cref="RawImageSpecification.Pitch"/>.</para>
/// <para><see cref="RawImageSpecification.Pitch"/> may not be the minimal value required to represent the texture
/// bitmap data. For example, if a texture is 4x4 B8G8R8A8, the minimal pitch would be 32, but the function may
/// return 64 instead.</para>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync(

View file

@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Utility;
@ -40,7 +40,7 @@ public static class StringExtensions
public static bool IsValidCharacterName(this string value, bool includeLegacy = true)
{
if (string.IsNullOrEmpty(value)) return false;
if (!FFXIVClientStructs.FFXIV.Client.UI.UIGlobals.IsValidPlayerCharacterName(value)) return false;
if (!UIGlobals.IsValidPlayerCharacterName(value)) return false;
return includeLegacy || value.Length <= 21;
}
}