mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
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:
commit
720b1676e5
49 changed files with 4573 additions and 93 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs
Normal file
20
Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
242
Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs
Normal file
242
Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
Normal file
67
Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
384
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
Normal file
384
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
316
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
Normal file
316
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
420
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
Normal file
420
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
Normal 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})"));
|
||||
}
|
||||
}
|
||||
120
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
Normal file
120
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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("...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
385
Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
Normal file
385
Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
490
Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
Normal file
490
Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
Normal file
52
Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
71
Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
Normal file
71
Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
213
Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
Normal file
213
Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
Normal file
109
Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
179
Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
Normal file
179
Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
170
Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
Normal file
170
Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ internal class DataWindow : Window, IDisposable
|
|||
private readonly IDataWindowWidget[] modules =
|
||||
{
|
||||
new AddonInspectorWidget(),
|
||||
new AddonInspectorWidget2(),
|
||||
new AddonLifecycleWidget(),
|
||||
new AddonWidget(),
|
||||
new AddressesWidget(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// Widget for displaying addon inspector.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ Dale
|
|||
Arcane Disgea
|
||||
Risu
|
||||
Tom
|
||||
Blyoom
|
||||
Blooym
|
||||
Valk
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ namespace Dalamud.Plugin.Ipc;
|
|||
/// </summary>
|
||||
public interface ICallGateSubscriber
|
||||
{
|
||||
|
||||
/// <inheritdoc cref="CallGatePubSubBase.HasAction"/>
|
||||
public bool HasAction { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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<nint, void>)(*(nint**)ptr)[3](ptr)</c>.</para>
|
||||
/// </remarks>
|
||||
nint ConvertToKernelTexture(IDalamudTextureWrap wrap, bool leaveWrapOpen = false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue