Penumbra/Penumbra/UI/Tabs/Debug/DebugTab.cs
2025-05-23 15:17:19 +02:00

1248 lines
52 KiB
C#

using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET;
using Microsoft.Extensions.DependencyInjection;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.Api;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Files;
using Penumbra.GameData.Interop;
using Penumbra.Import.Structs;
using Penumbra.Import.Textures;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.UI.Classes;
using Penumbra.Util;
using static OtterGui.Raii.ImRaii;
using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
using ImGuiClip = OtterGui.ImGuiClip;
using Penumbra.Api.IpcTester;
using Penumbra.GameData.Data;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.UI.Tabs.Debug;
public class Diagnostics(ServiceManager provider) : IUiService
{
public void DrawDiagnostics()
{
if (!ImGui.CollapsingHeader("Diagnostics"))
return;
using var table = ImRaii.Table("##data", 4, ImGuiTableFlags.RowBg);
if (!table)
return;
foreach (var type in typeof(ActorManager).Assembly.GetTypes()
.Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer))))
{
var container = (IAsyncDataContainer)provider.Provider!.GetRequiredService(type);
ImGuiUtil.DrawTableColumn(container.Name);
ImGuiUtil.DrawTableColumn(container.Time.ToString());
ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory));
ImGuiUtil.DrawTableColumn(container.TotalCount.ToString());
}
}
}
public class DebugTab : Window, ITab, IUiService
{
private readonly PerformanceTracker _performance;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi;
private readonly ActorManager _actors;
private readonly StainService _stains;
private readonly GlobalVariablesDrawer _globalVariablesDrawer;
private readonly ResourceManagerService _resourceManager;
private readonly ResourceLoader _resourceLoader;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly PathState _pathState;
private readonly SubfileHelper _subfileHelper;
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
private readonly CutsceneService _cutsceneService;
private readonly ModImportManager _modImporter;
private readonly ImportPopup _importPopup;
private readonly FrameworkManager _framework;
private readonly TextureManager _textureManager;
private readonly ShaderReplacementFixer _shaderReplacementFixer;
private readonly RedrawService _redraws;
private readonly DictEmote _emotes;
private readonly Diagnostics _diagnostics;
private readonly ObjectManager _objects;
private readonly IClientState _clientState;
private readonly IDataManager _dataManager;
private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel;
private readonly TexHeaderDrawer _texHeaderDrawer;
private readonly HookOverrideDrawer _hookOverrides;
private readonly RsfService _rsfService;
private readonly SchedulerResourceManagementService _schedulerService;
private readonly ObjectIdentification _objectIdentification;
private readonly RenderTargetDrawer _renderTargetDrawer;
private readonly ModMigratorDebug _modMigratorDebug;
private readonly ShapeInspector _shapeInspector;
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects,
IClientState clientState, IDataManager dataManager,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes,
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer,
SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer,
ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector)
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
{
IsOpen = true;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(200, 200),
MaximumSize = new Vector2(2000, 2000),
};
_performance = performance;
_config = config;
_collectionManager = collectionManager;
_validityChecker = validityChecker;
_modManager = modManager;
_httpApi = httpApi;
_actors = actors;
_stains = stains;
_resourceManager = resourceManager;
_resourceLoader = resourceLoader;
_collectionResolver = collectionResolver;
_drawObjectState = drawObjectState;
_pathState = pathState;
_subfileHelper = subfileHelper;
_identifiedCollectionCache = identifiedCollectionCache;
_cutsceneService = cutsceneService;
_modImporter = modImporter;
_importPopup = importPopup;
_framework = framework;
_textureManager = textureManager;
_shaderReplacementFixer = shaderReplacementFixer;
_redraws = redraws;
_emotes = emotes;
_diagnostics = diagnostics;
_ipcTester = ipcTester;
_crashHandlerPanel = crashHandlerPanel;
_texHeaderDrawer = texHeaderDrawer;
_hookOverrides = hookOverrides;
_rsfService = rsfService;
_globalVariablesDrawer = globalVariablesDrawer;
_schedulerService = schedulerService;
_objectIdentification = objectIdentification;
_renderTargetDrawer = renderTargetDrawer;
_modMigratorDebug = modMigratorDebug;
_shapeInspector = shapeInspector;
_objects = objects;
_clientState = clientState;
_dataManager = dataManager;
}
public ReadOnlySpan<byte> Label
=> "Debug"u8;
public bool IsVisible
=> _config is { DebugMode: true, Ephemeral.DebugSeparateWindow: false };
#if DEBUG
private const string DebugVersionString = "(Debug)";
#else
private const string DebugVersionString = "(Release)";
#endif
public void DrawContent()
{
using var child = Child("##DebugTab", -Vector2.One);
if (!child)
return;
DrawDebugTabGeneral();
_crashHandlerPanel.Draw();
DebugConfigurationDrawer.Draw();
_diagnostics.DrawDiagnostics();
DrawPerformanceTab();
DrawPathResolverDebug();
DrawActorsDebug();
DrawCollectionCaches();
_texHeaderDrawer.Draw();
_modMigratorDebug.Draw();
DrawShaderReplacementFixer();
DrawData();
DrawCrcCache();
DrawResourceLoader();
DrawResourceProblems();
_renderTargetDrawer.Draw();
_hookOverrides.Draw();
DrawPlayerModelInfo();
_globalVariablesDrawer.Draw();
DrawDebugTabIpc();
}
private unsafe void DrawCollectionCaches()
{
if (!ImGui.CollapsingHeader(
$"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections"))
return;
foreach (var collection in _collectionManager.Storage)
{
if (collection.HasCache)
{
using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value());
using var node =
TreeNode($"{collection.Identity.Name} (Change Counter {collection.Counters.Change})###{collection.Identity.Name}");
if (!node)
continue;
color.Pop();
using (var inheritanceNode = ImUtf8.TreeNode("Inheritance"u8))
{
if (inheritanceNode)
{
using var table = ImUtf8.Table("table"u8, 3,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV);
if (table)
{
var max = Math.Max(
Math.Max(collection.Inheritance.DirectlyInheritedBy.Count, collection.Inheritance.DirectlyInheritsFrom.Count),
collection.Inheritance.FlatHierarchy.Count);
for (var i = 0; i < max; ++i)
{
ImGui.TableNextColumn();
if (i < collection.Inheritance.DirectlyInheritsFrom.Count)
ImUtf8.Text(collection.Inheritance.DirectlyInheritsFrom[i].Identity.Name);
else
ImGui.Dummy(new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetTextLineHeight()));
ImGui.TableNextColumn();
if (i < collection.Inheritance.DirectlyInheritedBy.Count)
ImUtf8.Text(collection.Inheritance.DirectlyInheritedBy[i].Identity.Name);
else
ImGui.Dummy(new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetTextLineHeight()));
ImGui.TableNextColumn();
if (i < collection.Inheritance.FlatHierarchy.Count)
ImUtf8.Text(collection.Inheritance.FlatHierarchy[i].Identity.Name);
else
ImGui.Dummy(new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetTextLineHeight()));
}
}
}
}
using (var resourceNode = ImUtf8.TreeNode("Custom Resources"u8))
{
if (resourceNode)
foreach (var (path, resource) in collection._cache!.CustomResources)
{
ImUtf8.TreeNode($"{path} -> 0x{(ulong)resource.ResourceHandle:X}",
ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
}
}
using var modNode = ImUtf8.TreeNode("Enabled Mods"u8);
if (modNode)
foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name))
{
using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name);
using var node2 = TreeNode(mod.Name.Text);
if (!node2)
continue;
foreach (var path in paths)
TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
foreach (var manip in manips)
TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
}
}
else
{
using var color = PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value());
TreeNode($"{collection.Identity.Name} (Change Counter {collection.Counters.Change})",
ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
}
}
}
/// <summary> Draw general information about mod and collection state. </summary>
private void DrawDebugTabGeneral()
{
if (!ImGui.CollapsingHeader("General"))
return;
var separateWindow = _config.Ephemeral.DebugSeparateWindow;
if (ImGui.Checkbox("Draw as Separate Window", ref separateWindow))
{
IsOpen = true;
_config.Ephemeral.DebugSeparateWindow = separateWindow;
_config.Ephemeral.Save();
}
using (var table = Table("##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit))
{
if (table)
{
PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}");
PrintValue("Git Commit Hash", _validityChecker.CommitHash);
PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Identity.Name);
PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString());
PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Identity.Name);
PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString());
PrintValue("Mod Manager BasePath", _modManager.BasePath.Name);
PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName);
PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString());
PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString());
PrintValue("Mod Manager Valid", _modManager.Valid.ToString());
PrintValue("Web Server Enabled", _httpApi.Enabled.ToString());
}
}
var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index);
using (var tree = TreeNode($"Mods ({issues} Issues)###Mods"))
{
if (tree)
{
using var table = Table("##DebugModsTable", 3, ImGuiTableFlags.SizingFixedFit);
if (table)
{
var lastIndex = -1;
foreach (var mod in _modManager)
{
PrintValue(mod.Name, mod.Index.ToString("D5"));
ImGui.TableNextColumn();
var index = mod.Index;
if (index != lastIndex + 1)
ImGui.TextUnformatted("!!!");
lastIndex = index;
}
}
}
}
using (var tree = TreeNode("Mod Import"))
{
if (tree)
{
using var table = Table("##DebugModImport", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
{
var importing = _modImporter.IsImporting(out var importer);
PrintValue("Is Importing", importing.ToString());
PrintValue("Importer State", (importer?.State ?? ImporterState.None).ToString());
PrintValue("Import Window Was Drawn", _importPopup.WasDrawn.ToString());
PrintValue("Import Popup Was Drawn", _importPopup.PopupWasDrawn.ToString());
ImGui.TableNextColumn();
ImGui.TextUnformatted("Import Batches");
ImGui.TableNextColumn();
foreach (var (batch, index) in _modImporter.ModBatches.WithIndex())
{
foreach (var mod in batch)
PrintValue(index.ToString(), mod);
}
ImGui.TableNextColumn();
ImGui.TextUnformatted("Addable Mods");
ImGui.TableNextColumn();
foreach (var mod in _modImporter.AddableMods)
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.Name);
}
}
}
}
using (var tree = TreeNode("Framework"))
{
if (tree)
{
using var table = Table("##DebugFramework", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
{
foreach (var important in _framework.Important)
PrintValue(important, "Immediate");
foreach (var (onTick, idx) in _framework.OnTick.WithIndex())
PrintValue(onTick, $"{idx + 1} Tick(s) From Now");
foreach (var (time, name) in _framework.Delayed)
{
var span = time - DateTime.UtcNow;
PrintValue(name, $"After {span.Minutes:D2}:{span.Seconds:D2}.{span.Milliseconds / 10:D2} (+ Ticks)");
}
}
}
}
using (var tree = TreeNode($"Texture Manager {_textureManager.Tasks.Count}###Texture Manager"))
{
if (tree)
{
using var table = Table("##Tasks", 2, ImGuiTableFlags.RowBg);
if (table)
foreach (var task in _textureManager.Tasks)
{
ImGuiUtil.DrawTableColumn(task.Key.ToString()!);
ImGuiUtil.DrawTableColumn(task.Value.Item1.Status.ToString());
}
}
}
using (var tree = TreeNode("Redraw Service"))
{
if (tree)
{
using var table = Table("##redraws", 3, ImGuiTableFlags.RowBg);
if (table)
{
ImGuiUtil.DrawTableColumn("In GPose");
ImGuiUtil.DrawTableColumn(_redraws.InGPose.ToString());
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Target");
ImGuiUtil.DrawTableColumn(_redraws.Target.ToString());
ImGui.TableNextColumn();
foreach (var (objectIdx, idx) in _redraws.Queue.WithIndex())
{
var (actualIdx, state) = objectIdx < 0 ? (~objectIdx, "Queued") : (objectIdx, "Invisible");
ImGuiUtil.DrawTableColumn($"Redraw Queue #{idx}");
ImGuiUtil.DrawTableColumn(actualIdx.ToString());
ImGuiUtil.DrawTableColumn(state);
}
foreach (var (objectIdx, idx) in _redraws.AfterGPoseQueue.WithIndex())
{
var (actualIdx, state) = objectIdx < 0 ? (~objectIdx, "Queued") : (objectIdx, "Invisible");
ImGuiUtil.DrawTableColumn($"GPose Queue #{idx}");
ImGuiUtil.DrawTableColumn(actualIdx.ToString());
ImGuiUtil.DrawTableColumn(state);
}
foreach (var (name, idx) in _redraws.GPoseNames.OfType<string>().WithIndex())
{
ImGuiUtil.DrawTableColumn($"GPose Name #{idx}");
ImGuiUtil.DrawTableColumn(name);
ImGui.TableNextColumn();
}
}
}
}
using (var tree = ImUtf8.TreeNode("String Memory"u8))
{
if (tree)
{
using (ImUtf8.Group())
{
ImUtf8.Text("Currently Allocated Strings"u8);
ImUtf8.Text("Total Allocated Strings"u8);
ImUtf8.Text("Free'd Allocated Strings"u8);
ImUtf8.Text("Currently Allocated Bytes"u8);
ImUtf8.Text("Total Allocated Bytes"u8);
ImUtf8.Text("Free'd Allocated Bytes"u8);
}
ImGui.SameLine();
using (ImUtf8.Group())
{
ImUtf8.Text($"{PenumbraStringMemory.CurrentStrings}");
ImUtf8.Text($"{PenumbraStringMemory.AllocatedStrings}");
ImUtf8.Text($"{PenumbraStringMemory.FreedStrings}");
ImUtf8.Text($"{PenumbraStringMemory.CurrentBytes}");
ImUtf8.Text($"{PenumbraStringMemory.AllocatedBytes}");
ImUtf8.Text($"{PenumbraStringMemory.FreedBytes}");
}
}
}
}
private void DrawPerformanceTab()
{
if (!ImGui.CollapsingHeader("Performance"))
return;
using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen))
{
if (start)
ImGui.NewLine();
}
_performance.Draw("##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName);
}
private unsafe void DrawActorsDebug()
{
if (!ImGui.CollapsingHeader("Actors"))
return;
using (var objectTree = ImUtf8.TreeNode("Object Manager"u8))
{
if (objectTree)
{
_objects.DrawDebug();
using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
-Vector2.UnitX);
if (!table)
return;
DrawSpecial("Current Player", _actors.GetCurrentPlayer());
DrawSpecial("Current Inspect", _actors.GetInspectPlayer());
DrawSpecial("Current Card", _actors.GetCardPlayer());
DrawSpecial("Current Glamour", _actors.GetGlamourPlayer());
foreach (var obj in _objects)
{
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL");
ImGui.TableNextColumn();
Penumbra.Dynamis.DrawPointer(obj.Address);
ImGui.TableNextColumn();
if (obj.Address != nint.Zero)
Penumbra.Dynamis.DrawPointer((nint)((Character*)obj.Address)->GameObject.GetDrawObject());
var identifier = _actors.FromObject(obj, out _, false, true, false);
ImGuiUtil.DrawTableColumn(_actors.ToString(identifier));
var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc
? $"{identifier.DataId} | {obj.AsObject->BaseId}"
: identifier.DataId.ToString();
ImGuiUtil.DrawTableColumn(id);
ImGui.TableNextColumn();
Penumbra.Dynamis.DrawPointer(obj.Address != nint.Zero ? *(nint*)obj.Address : nint.Zero);
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL");
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero
? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character"
: "NULL");
}
}
}
using (var shapeTree = ImUtf8.TreeNode("Shape Inspector"u8))
{
if (shapeTree)
_shapeInspector.Draw();
}
return;
void DrawSpecial(string name, ActorIdentifier id)
{
if (!id.IsValid)
return;
ImGuiUtil.DrawTableColumn(name);
ImGuiUtil.DrawTableColumn(string.Empty);
ImGuiUtil.DrawTableColumn(string.Empty);
ImGuiUtil.DrawTableColumn(_actors.ToString(id));
ImGuiUtil.DrawTableColumn(string.Empty);
ImGuiUtil.DrawTableColumn(string.Empty);
ImGuiUtil.DrawTableColumn(string.Empty);
ImGuiUtil.DrawTableColumn(string.Empty);
}
}
/// <summary>
/// Draw information about which draw objects correspond to which game objects
/// and which paths are due to be loaded by which collection.
/// </summary>
private unsafe void DrawPathResolverDebug()
{
if (!ImGui.CollapsingHeader("Path Resolver"))
return;
ImGui.TextUnformatted(
$"Last Game Object: 0x{_collectionResolver.IdentifyLastGameObjectCollection(true).AssociatedGameObject:X} ({_collectionResolver.IdentifyLastGameObjectCollection(true).ModCollection.Identity.Name})");
using (var drawTree = TreeNode("Draw Object to Object"))
{
if (drawTree)
{
using var table = Table("###DrawObjectResolverTable", 8, ImGuiTableFlags.SizingFixedFit);
if (table)
foreach (var (drawObject, (gameObjectPtr, idx, child)) in _drawObjectState
.OrderBy(kvp => kvp.Value.Item2.Index)
.ThenBy(kvp => kvp.Value.Item3)
.ThenBy(kvp => kvp.Key.Address))
{
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable($"{drawObject}");
ImUtf8.DrawTableColumn($"{gameObjectPtr.Index}");
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, gameObjectPtr.Index != idx))
{
ImUtf8.DrawTableColumn($"{idx}");
}
ImUtf8.DrawTableColumn(child ? "Child"u8 : "Main"u8);
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable($"{gameObjectPtr}");
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, _objects[idx] != gameObjectPtr))
{
ImUtf8.DrawTableColumn($"{_objects[idx]}");
}
ImUtf8.DrawTableColumn(gameObjectPtr.Utf8Name.Span);
var collection = _collectionResolver.IdentifyCollection(gameObjectPtr.AsObject, true);
ImUtf8.DrawTableColumn(collection.ModCollection.Identity.Name);
}
}
}
using (var pathTree = TreeNode("Path Collections"))
{
if (pathTree)
{
using var table = Table("###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
foreach (var data in _pathState.CurrentData)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{data.AssociatedGameObject:X}");
ImGui.TableNextColumn();
ImGui.TextUnformatted(data.ModCollection.Identity.Name);
}
}
}
using (var resourceTree = TreeNode("Subfile Collections"))
{
if (resourceTree)
{
using var table = Table("###ResourceCollectionResolverTable", 4, ImGuiTableFlags.SizingFixedFit);
if (table)
{
ImGuiUtil.DrawTableColumn("Current Mtrl Data");
ImGuiUtil.DrawTableColumn(_subfileHelper.MtrlData.ModCollection.Identity.Name);
ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.MtrlData.AssociatedGameObject:X}");
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Current Avfx Data");
ImGuiUtil.DrawTableColumn(_subfileHelper.AvfxData.ModCollection.Identity.Name);
ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.AvfxData.AssociatedGameObject:X}");
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Current Resources");
ImGuiUtil.DrawTableColumn(_subfileHelper.Count.ToString());
ImGui.TableNextColumn();
ImGui.TableNextColumn();
foreach (var (resource, resolve) in _subfileHelper)
{
ImGuiUtil.DrawTableColumn($"0x{resource:X}");
ImGuiUtil.DrawTableColumn(resolve.ModCollection.Identity.Name);
ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}");
ImGuiUtil.DrawTableColumn($"{((ResourceHandle*)resource)->FileName()}");
}
}
}
}
using (var identifiedTree = TreeNode("Identified Collections"))
{
if (identifiedTree)
{
using var table = Table("##PathCollectionsIdentifiedTable", 4, ImGuiTableFlags.SizingFixedFit);
if (table)
foreach (var (address, identifier, collection) in _identifiedCollectionCache
.OrderBy(kvp => ((GameObject*)kvp.Address)->ObjectIndex))
{
ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}");
ImGuiUtil.DrawTableColumn($"0x{address:X}");
ImGuiUtil.DrawTableColumn(identifier.ToString());
ImGuiUtil.DrawTableColumn(collection.Identity.Name);
}
}
}
using (var cutsceneTree = TreeNode("Cutscene Actors"))
{
if (cutsceneTree)
{
using var table = Table("###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
foreach (var (idx, actor) in _cutsceneService.Actors)
{
ImGuiUtil.DrawTableColumn($"Cutscene Actor {idx}");
ImGuiUtil.DrawTableColumn(actor.Name.ToString());
}
}
}
using (var groupTree = TreeNode("Group"))
{
if (groupTree)
{
using var table = Table("###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
{
ImGuiUtil.DrawTableColumn("Group Members");
ImGuiUtil.DrawTableColumn(GroupManager.Instance()->MainGroup.MemberCount.ToString());
for (var i = 0; i < 8; ++i)
{
ImGuiUtil.DrawTableColumn($"Member #{i}");
var member = GroupManager.Instance()->MainGroup.GetPartyMemberByIndex(i);
ImGuiUtil.DrawTableColumn(member == null ? "NULL" : new ByteString(member->Name).ToString());
}
}
}
}
using (var bannerTree = TreeNode("Party Banner"))
{
if (bannerTree)
{
var agent = &AgentBannerParty.Instance()->AgentBannerInterface;
if (agent->Data == null)
agent = &AgentBannerMIP.Instance()->AgentBannerInterface;
if (agent->Data != null)
{
using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
for (var i = 0; i < 8; ++i)
{
ref var c = ref agent->Data->Characters[i];
ImGuiUtil.DrawTableColumn($"Character {i}");
var name = c.Name1.ToString();
ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c.WorldId})");
}
}
else
{
ImGui.TextUnformatted("INACTIVE");
}
}
}
using (var tmbCache = TreeNode("TMB Cache"))
{
if (tmbCache)
{
using var table = Table("###TmbTable", 2, ImGuiTableFlags.SizingFixedFit);
if (table)
foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key))
{
ImUtf8.DrawTableColumn($"{id:D6}");
ImUtf8.DrawTableColumn(name.Span);
}
}
}
}
private void DrawData()
{
if (!ImGui.CollapsingHeader("Game Data"))
return;
DrawEmotes();
DrawActionTmbs();
DrawStainTemplates();
DrawAtch();
DrawChangedItemTest();
}
private string _changedItemPath = string.Empty;
private readonly Dictionary<string, IIdentifiedObjectData> _changedItems = [];
private void DrawChangedItemTest()
{
using var node = TreeNode("Changed Item Test");
if (!node)
return;
if (ImUtf8.InputText("##ChangedItemTest"u8, ref _changedItemPath, "Changed Item File Path..."u8))
{
_changedItems.Clear();
_objectIdentification.Identify(_changedItems, _changedItemPath);
}
if (_changedItems.Count == 0)
return;
using var list = ImUtf8.ListBox("##ChangedItemList"u8,
new Vector2(ImGui.GetContentRegionAvail().X, 8 * ImGui.GetTextLineHeightWithSpacing()));
if (!list)
return;
foreach (var item in _changedItems)
ImUtf8.Selectable(item.Key);
}
private string _emoteSearchFile = string.Empty;
private string _emoteSearchName = string.Empty;
private AtchFile? _atchFile;
private void DrawAtch()
{
try
{
_atchFile ??= new AtchFile(_dataManager.GetFile("chara/xls/attachOffset/c0101.atch")!.Data);
}
catch
{
// ignored
}
if (_atchFile == null)
return;
using var mainTree = ImUtf8.TreeNode("Atch File C0101"u8);
if (!mainTree)
return;
AtchDrawer.Draw(_atchFile);
}
private void DrawEmotes()
{
using var mainTree = TreeNode("Emotes");
if (!mainTree)
return;
ImGui.InputText("File Name", ref _emoteSearchFile, 256);
ImGui.InputText("Emote Name", ref _emoteSearchName, 256);
using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing()));
if (!table)
return;
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing());
var dummy = ImGuiClip.FilteredClippedDraw(_emotes, skips,
p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase)
&& (_emoteSearchName.Length == 0
|| p.Value.Any(s => s.Name.ToDalamudString().TextValue.Contains(_emoteSearchName, StringComparison.OrdinalIgnoreCase))),
p =>
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(p.Key);
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.Join(", ", p.Value.Select(v => v.Name.ToDalamudString().TextValue)));
});
ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing());
}
private string _tmbKeyFilter = string.Empty;
private CiByteString _tmbKeyFilterU8 = CiByteString.Empty;
private void DrawActionTmbs()
{
using var mainTree = TreeNode("Action TMBs");
if (!mainTree)
return;
if (ImGui.InputText("Key", ref _tmbKeyFilter, 256))
_tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty;
using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing()));
if (!table)
return;
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing());
var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips,
kvp => kvp.Key.Contains(_tmbKeyFilterU8),
p =>
{
ImUtf8.DrawTableColumn($"{p.Value}");
ImUtf8.DrawTableColumn(p.Key.Span);
});
ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing());
}
private void DrawStainTemplates()
{
using var mainTree = TreeNode("Staining Templates");
if (!mainTree)
return;
using (var legacyTree = TreeNode("stainingtemplate.stm"))
{
if (legacyTree)
DrawStainTemplatesFile(_stains.LegacyStmFile);
}
using (var gudTree = TreeNode("stainingtemplate_gud.stm"))
{
if (gudTree)
DrawStainTemplatesFile(_stains.GudStmFile);
}
}
private static void DrawStainTemplatesFile<TDyePack>(StmFile<TDyePack> stmFile) where TDyePack : unmanaged, IDyePack
{
foreach (var (key, data) in stmFile.Entries)
{
using var tree = TreeNode($"Template {key}");
if (!tree)
continue;
using var table = Table("##table", data.Colors.Length + data.Scalars.Length,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
continue;
for (var i = 0; i < StmFile<TDyePack>.StainingTemplateEntry.NumElements; ++i)
{
foreach (var list in data.Colors)
{
var color = list[i];
ImGui.TableNextColumn();
var frame = new Vector2(ImGui.GetTextLineHeight());
ImGui.ColorButton("###color", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)color), 1), 0, frame);
ImGui.SameLine();
ImGui.TextUnformatted($"{color.Red:F6} | {color.Green:F6} | {color.Blue:F6}");
}
foreach (var list in data.Scalars)
{
var scalar = list[i];
ImGuiUtil.DrawTableColumn($"{scalar:F6}");
}
}
}
}
private void DrawShaderReplacementFixer()
{
if (!ImGui.CollapsingHeader("Shader Replacement Fixer"))
return;
var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled;
if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer))
_shaderReplacementFixer.Enabled = enableShaderReplacementFixer;
if (!enableShaderReplacementFixer)
return;
using var table = Table("##ShaderReplacementFixer", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
-Vector2.UnitX);
if (!table)
return;
var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas();
ImGui.TableSetupColumn("Shader Package Name", ImGuiTableColumnFlags.WidthStretch, 0.6f);
ImGui.TableSetupColumn("Materials with Modded ShPk", ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableSetupColumn("\u0394 Slow-Path Calls", ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableHeadersRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted("characterglass.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterGlassShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterGlass}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("characterlegacy.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterLegacyShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterLegacy}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("characterocclusion.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterOcclusionShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterOcclusion}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("characterstockings.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterStockingsShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterStockings}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("charactertattoo.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("charactertransparency.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("hairmask.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedHairMaskShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.HairMask}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("iris.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}");
ImGui.TableNextColumn();
ImGui.TextUnformatted("skin.shpk");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}");
}
/// <summary> Draw information about the models, materials and resources currently loaded by the local player. </summary>
private unsafe void DrawPlayerModelInfo()
{
var player = _clientState.LocalPlayer;
var name = player?.Name.ToString() ?? "NULL";
if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null)
return;
DrawCopyableAddress("PlayerCharacter"u8, player.Address);
var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject();
if (model == null)
return;
DrawCopyableAddress("CharacterBase"u8, model);
using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit))
{
if (t1)
{
ImGuiUtil.DrawTableColumn("Flags");
ImGuiUtil.DrawTableColumn($"{model->UnkFlags_01:X2}");
ImGuiUtil.DrawTableColumn("Has Model In Slot Loaded");
ImGuiUtil.DrawTableColumn($"{model->HasModelInSlotLoaded:X8}");
ImGuiUtil.DrawTableColumn("Has Model Files In Slot Loaded");
ImGuiUtil.DrawTableColumn($"{model->HasModelFilesInSlotLoaded:X8}");
}
}
using var table = Table($"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
ImGui.TableNextColumn();
ImGui.TableHeader("Slot");
ImGui.TableNextColumn();
ImGui.TableHeader("Imc Ptr");
ImGui.TableNextColumn();
ImGui.TableHeader("Imc File");
ImGui.TableNextColumn();
ImGui.TableHeader("Model Ptr");
ImGui.TableNextColumn();
ImGui.TableHeader("Model File");
for (var i = 0; i < model->SlotCount; ++i)
{
var imc = (ResourceHandle*)model->IMCArray[i];
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"Slot {i}");
ImGui.TableNextColumn();
Penumbra.Dynamis.DrawPointer((nint)imc);
ImGui.TableNextColumn();
if (imc != null)
UiHelpers.Text(imc);
var mdl = (RenderModel*)model->Models[i];
ImGui.TableNextColumn();
Penumbra.Dynamis.DrawPointer((nint)mdl);
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
continue;
ImGui.TableNextColumn();
{
UiHelpers.Text(mdl->ResourceHandle);
}
}
}
private string _crcInput = string.Empty;
private FullPath _crcPath = FullPath.Empty;
private unsafe void DrawCrcCache()
{
var header = ImUtf8.CollapsingHeader("CRC Cache"u8);
if (!header)
return;
if (ImUtf8.InputText("##crcInput"u8, ref _crcInput, "Input path for CRC..."u8))
_crcPath = new FullPath(_crcInput);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImUtf8.Text($" CRC32: {_crcPath.InternalName.CiCrc32:X8}");
ImUtf8.Text($"CI CRC32: {_crcPath.InternalName.Crc32:X8}");
ImUtf8.Text($" CRC64: {_crcPath.Crc64:X16}");
using var table = ImUtf8.Table("table"u8, 2);
if (!table)
return;
ImUtf8.TableSetupColumn("Hash"u8, ImGuiTableColumnFlags.WidthFixed, 18 * UiBuilder.MonoFont.GetCharAdvance('0'));
ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0'));
ImGui.TableHeadersRow();
foreach (var (hash, type) in _rsfService.CustomCache)
{
ImGui.TableNextColumn();
ImUtf8.Text($"{hash:X16}");
ImGui.TableNextColumn();
ImUtf8.Text($"{type}");
}
}
private unsafe void DrawResourceLoader()
{
if (!ImUtf8.CollapsingHeader("Resource Loader"u8))
return;
var ongoingLoads = _resourceLoader.OngoingLoads;
var ongoingLoadCount = ongoingLoads.Count;
ImUtf8.Text($"Ongoing Loads: {ongoingLoadCount}");
if (ongoingLoadCount == 0)
return;
using var table = ImUtf8.Table("ongoingLoadTable"u8, 3);
if (!table)
return;
ImUtf8.TableSetupColumn("Resource Handle"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImUtf8.TableSetupColumn("Actual Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f);
ImUtf8.TableSetupColumn("Original Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f);
ImGui.TableHeadersRow();
foreach (var (handle, original) in ongoingLoads)
{
ImUtf8.DrawTableColumn($"0x{handle:X}");
ImUtf8.DrawTableColumn(((ResourceHandle*)handle)->CsHandle.FileName);
ImUtf8.DrawTableColumn(original.Path.Span);
}
}
/// <summary> Draw resources with unusual reference count. </summary>
private unsafe void DrawResourceProblems()
{
var header = ImGui.CollapsingHeader("Resource Problems");
ImGuiUtil.HoverTooltip("Draw resources with unusually high reference count to detect overflows.");
if (!header)
return;
using var table = Table("##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
_resourceManager.IterateResources((_, r) =>
{
if (r->RefCount < 10000)
return;
ImGui.TableNextColumn();
ImGui.TextUnformatted(((ResourceCategory)r->Type.Value).ToString());
ImGui.TableNextColumn();
ImGui.TextUnformatted(r->FileType.ToString("X"));
ImGui.TableNextColumn();
ImGui.TextUnformatted(r->Id.ToString("X"));
ImGui.TableNextColumn();
ImGui.TextUnformatted(((ulong)r).ToString("X"));
ImGui.TableNextColumn();
ImGui.TextUnformatted(r->RefCount.ToString());
ImGui.TableNextColumn();
ref var name = ref r->FileName;
if (name.Capacity > 15)
UiHelpers.Text(name.BufferPtr, (int)name.Length);
else
fixed (byte* ptr = name.Buffer)
{
UiHelpers.Text(ptr, (int)name.Length);
}
});
}
/// <summary> Draw information about IPC options and availability. </summary>
private void DrawDebugTabIpc()
{
if (!ImUtf8.CollapsingHeader("IPC"u8))
return;
using (var tree = ImUtf8.TreeNode("Dynamis"u8))
{
if (tree)
Penumbra.Dynamis.DrawDebugInfo();
}
_ipcTester.Draw();
}
/// <summary> Helper to print a property and its value in a 2-column table. </summary>
private static void PrintValue(string name, string value)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(name);
ImGui.TableNextColumn();
ImGui.TextUnformatted(value);
}
public override void Draw()
=> DrawContent();
public override bool DrawConditions()
=> _config.DebugMode && _config.Ephemeral.DebugSeparateWindow;
public override void OnClose()
{
_config.Ephemeral.DebugSeparateWindow = false;
_config.Ephemeral.Save();
}
public static unsafe void DrawCopyableAddress(ReadOnlySpan<byte> label, void* address)
{
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
if (ImUtf8.Selectable($"0x{(nint)address:X16} {label}"))
ImUtf8.SetClipboardText($"0x{(nint)address:X16}");
}
ImUtf8.HoverTooltip("Click to copy address to clipboard."u8);
}
public static unsafe void DrawCopyableAddress(ReadOnlySpan<byte> label, nint address)
=> DrawCopyableAddress(label, (void*)address);
}