using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using ImSharp; using Luna; using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra.UI.Tabs; public sealed class ResourceTab(Configuration config, ResourceManagerService resourceManager) : ITab { public TabType Identifier => TabType.ResourceManager; public ReadOnlySpan Label => "Resource Manager"u8; public bool IsVisible => config.DebugMode; public readonly ResourceFilter Filter = new(config); public sealed class ResourceFilter : Utf8FilterBase { public ResourceFilter(Configuration config) { if (config.RememberResourceManagerFilters) Set(new StringU8(config.Filters.ResourceManagerFilter)); FilterChanged += () => config.Filters.ResourceManagerFilter = Text.ToString(); } protected override ReadOnlySpan ToFilterString(in ResourceHandle item, int globalIndex) => item.FileName.AsSpan(); } /// Draw a tab to iterate over the main resource maps and see what resources are currently loaded. public unsafe void DrawContent() { // Filter for resources containing the input string. Filter.DrawFilter("Filter..."u8, Im.ContentRegion.Available); using var child = Im.Child.Begin("##ResourceManagerTab"u8, Im.ContentRegion.Available); if (!child) return; resourceManager.IterateGraphs(DrawCategoryContainer); Im.Line.New(); using var table = Im.Table.Begin("##t"u8, 2, TableFlags.SizingFixedFit); if (!table) return; table.DrawColumn("Static Address:"u8); table.NextColumn(); Penumbra.Dynamis.DrawPointer(resourceManager.ResourceManagerAddress); table.DrawColumn("Actual Address:"u8); table.NextColumn(); Penumbra.Dynamis.DrawPointer(resourceManager.ResourceManager); } private float _hashColumnWidth; private float _pathColumnWidth; private float _refsColumnWidth; /// Draw a single resource map. private unsafe void DrawResourceMap(ResourceCategory category, uint ext, StdMap>* map) { if (map == null) return; var label = GetNodeLabel((uint)category, ext, map->Count); using var tree = Im.Tree.Node(label); if (!tree || map->Count == 0) return; using var table = Im.Table.Begin("##table"u8, 4, TableFlags.SizingFixedFit | TableFlags.RowBackground); if (!table) return; table.SetupColumn("Hash"u8, TableColumnFlags.WidthFixed, _hashColumnWidth); table.SetupColumn("Ptr"u8, TableColumnFlags.WidthFixed, _hashColumnWidth); table.SetupColumn("Path"u8, TableColumnFlags.WidthFixed, _pathColumnWidth); table.SetupColumn("Refs"u8, TableColumnFlags.WidthFixed, _refsColumnWidth); Im.Table.HeaderRow(); resourceManager.IterateResourceMap(map, (hash, r) => { // Filter unwanted names. if (!Filter.WouldBeVisible(in *r, -1)) return; Im.Table.DrawColumn($"0x{hash:X8}"); if (Im.Item.Clicked()) Im.Clipboard.Set($"0x{hash:X8}"); Im.Table.NextColumn(); Penumbra.Dynamis.DrawPointer(r); var resource = (Interop.Structs.ResourceHandle*)r; Im.Table.DrawColumn(resource->FileName().Span); if (Im.Item.Clicked()) { Im.Clipboard.Set(resource->FileName().Span); } else if (Im.Item.RightClicked()) { var data = resource->CsHandle.GetData(); if (data != null) { var length = (int)resource->CsHandle.GetLength(); Im.Clipboard.Set(StringU8.Join((byte)' ', new ReadOnlySpan(data, length).ToArray().Select(b => b.ToString("X2")))); } } Im.Tooltip.OnHover("Click to copy the file name to clipboard.\nRight-Click to copy byte-wise file data to clipboard, if any."u8); Im.Table.DrawColumn($"{r->RefCount}"); }); } /// Draw a full category for the resource manager. private unsafe void DrawCategoryContainer(ResourceCategory category, StdMap>>>* map, int idx) { if (map is null) return; using var tree = Im.Tree.Node($"({(uint)category:D2}) {category} (Ex {idx}) - {map->Count}###{(uint)category}_{idx}"); if (!tree) return; SetTableWidths(); resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m)); } /// Obtain a label for an extension node. private static Utf8StringHandler GetNodeLabel(uint label, uint type, int count) { var (lowest, mid1, mid2, highest) = BitFunctions.SplitBytes(type); return highest is 0 ? $"({type:X8}) {(char)mid2}{(char)mid1}{(char)lowest} - {count}###{label}{type}" : $"({type:X8}) {(char)highest}{(char)mid2}{(char)mid1}{(char)lowest} - {count}###{label}{type}"; } /// Set the widths for a resource table. private void SetTableWidths() { _hashColumnWidth = 100 * Im.Style.GlobalScale; _pathColumnWidth = Im.Window.MaximumContentRegion.X - Im.Window.MinimumContentRegion.X - 300 * Im.Style.GlobalScale; _refsColumnWidth = 30 * Im.Style.GlobalScale; } }