diff --git a/Penumbra.Api b/Penumbra.Api index 80f9793e..e2f578a9 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 80f9793ef2ddaa50246b7112fde4d9b2098d8823 +Subproject commit e2f578a903f4e2de6c5967eb92f1b5a0a413d287 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 675a61a3..f7b740b9 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -16,8 +16,8 @@ using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; -using ImGuiScene; using Penumbra.GameData.Structs; +using Penumbra.GameData.Enums; namespace Penumbra.Api; @@ -1437,6 +1437,8 @@ public class IpcTester : IDisposable private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + private (string, Ipc.ResourceTree?)[]? _lastGameObjectResourceTrees; + private (string, Ipc.ResourceTree)[]? _lastPlayerResourceTrees; private TimeSpan _lastCallDuration; public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) @@ -1523,11 +1525,46 @@ public class IpcTester : IDisposable ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); } + DrawIntro(Ipc.GetGameObjectResourceTrees.Label, "Get GameObject resource trees"); + if (ImGui.Button("Get##GameObjectResourceTrees")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(_pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUIData, gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourceTrees = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(trees) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourceTrees)); + } + + DrawIntro(Ipc.GetPlayerResourceTrees.Label, "Get local player resource trees"); + if (ImGui.Button("Get##PlayerResourceTrees")) + { + var subscriber = Ipc.GetPlayerResourceTrees.Subscriber(_pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUIData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourceTrees = trees + .Select(pair => (GameObjectToString(pair.Key), pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetPlayerResourceTrees)); + } + DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration); DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); + + DrawPopup(nameof(Ipc.GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration); } private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class @@ -1638,6 +1675,70 @@ public class IpcTester : IDisposable }); } + private void DrawResourceTrees((string, Ipc.ResourceTree?)[] result) + { + DrawWithHeaders(result, tree => + { + ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}"); + + using var table = ImRaii.Table(string.Empty, _withUIData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); + if (!table) + return; + + if (_withUIData) + { + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f); + ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f); + } + else + { + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f); + } + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableHeadersRow(); + + void DrawNode(Ipc.ResourceNode node) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + var hasChildren = node.Children.Any(); + using var treeNode = ImRaii.TreeNode( + $"{(_withUIData ? (node.Name ?? "Unknown") : node.Type)}##{node.ObjectAddress:X8}", + hasChildren ? + ImGuiTreeNodeFlags.SpanFullWidth : + (ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen)); + if (_withUIData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(node.Type.ToString()); + ImGui.TableNextColumn(); + TextUnformattedMono(node.Icon.ToString()); + } + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.GamePath ?? "Unknown"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.ActualPath); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ObjectAddress:X8}"); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ResourceHandle:X8}"); + + if (treeNode) + { + foreach (var child in node.Children) + DrawNode(child); + } + } + + foreach (var node in tree.Nodes) + DrawNode(node); + }); + } + private static void TextUnformattedMono(string text) { using var _ = ImRaii.PushFont(UiBuilder.MonoFont); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0ae4fcca..8974e823 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1075,6 +1075,23 @@ public class PenumbraApi : IDisposable, IPenumbraApi return resDictionaries.AsReadOnly(); } + public Ipc.ResourceTree?[] GetGameObjectResourceTrees(bool withUIData, params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return Array.ConvertAll(gameObjects, obj => resDictionary.TryGetValue(obj, out var nodes) ? nodes : null); + } + + public IReadOnlyDictionary GetPlayerResourceTrees(bool withUIData) + { + var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly + | (withUIData ? ResourceTreeFactory.Flags.WithUiData : 0)); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return resDictionary.AsReadOnly(); + } // TODO: cleanup when incrementing API public string GetMetaManipulations(string characterName) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index b72073fb..a564588b 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -130,6 +130,9 @@ public class PenumbraIpcProviders : IDisposable FuncProvider>> GetPlayerResourcesOfType; + internal readonly FuncProvider GetGameObjectResourceTrees; + internal readonly FuncProvider> GetPlayerResourceTrees; + public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) { @@ -254,6 +257,8 @@ public class PenumbraIpcProviders : IDisposable GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths); GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType); GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType); + GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees); + GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees); Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService); @@ -370,6 +375,8 @@ public class PenumbraIpcProviders : IDisposable GetPlayerResourcePaths.Dispose(); GetGameObjectResourcesOfType.Dispose(); GetPlayerResourcesOfType.Dispose(); + GetGameObjectResourceTrees.Dispose(); + GetPlayerResourceTrees.Dispose(); Disposed.Invoke(); Disposed.Dispose(); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 6c1e4d1e..386caf9d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -1,5 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.Api; using Penumbra.Api.Enums; +using Penumbra.String.Classes; using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; @@ -71,4 +73,39 @@ internal static class ResourceTreeApiHelper return resDictionaries.ToDictionary(pair => pair.Key, pair => (IReadOnlyDictionary)pair.Value.AsReadOnly()); } + + public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) + { + static Ipc.ResourceNode GetIpcNode(ResourceNode node) => + new() + { + Type = node.Type, + Icon = ChangedItemDrawer.ToApiIcon(node.Icon), + Name = node.Name, + GamePath = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), + ActualPath = node.FullPath.ToString(), + ObjectAddress = node.ObjectAddress, + ResourceHandle = node.ResourceHandle, + Children = node.Children.Select(GetIpcNode).ToList(), + }; + + static Ipc.ResourceTree GetIpcTree(ResourceTree tree) => + new() + { + Name = tree.Name, + RaceCode = (ushort)tree.RaceCode, + Nodes = tree.Nodes.Select(GetIpcNode).ToList(), + }; + + var resDictionary = new Dictionary(4); + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (resDictionary.ContainsKey(gameObject.ObjectIndex)) + continue; + + resDictionary.Add(gameObject.ObjectIndex, GetIpcTree(resourceTree)); + } + + return resDictionary; + } }