diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 69c2fd3d..fb8719f4 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -15,6 +15,8 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; +using Dalamud.Plugin.Services; +using Penumbra.GameData.Enums; namespace Penumbra.Api; @@ -35,6 +37,7 @@ public class IpcTester : IDisposable private readonly ModSettings _modSettings; private readonly Editing _editing; private readonly Temporary _temporary; + private readonly ResourceTree _resourceTree; public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) @@ -52,6 +55,7 @@ public class IpcTester : IDisposable _modSettings = new ModSettings(dalamud.PluginInterface); _editing = new Editing(dalamud.PluginInterface); _temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config); + _resourceTree = new ResourceTree(dalamud.PluginInterface, dalamud.Objects); UnsubscribeEvents(); } @@ -75,6 +79,7 @@ public class IpcTester : IDisposable _temporary.Draw(); _temporary.DrawCollections(); _temporary.DrawMods(); + _resourceTree.Draw(); } catch (Exception e) { @@ -1397,4 +1402,222 @@ public class IpcTester : IDisposable } } } + + private class ResourceTree + { + private readonly DalamudPluginInterface _pi; + private readonly IObjectTable _objects; + + private string _gameObjectIndices = "0"; + private bool _mergeSameCollection = false; + private ResourceType _type = ResourceType.Mtrl; + private bool _withUIData = false; + + private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcePaths; + private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; + private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; + private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + + public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) + { + _pi = pi; + _objects = objects; + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Resource Tree"); + if (!_) + return; + + ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); + ImGui.Checkbox("Merge entries that use the same collection", ref _mergeSameCollection); + ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); + ImGui.Checkbox("Also get names and icons", ref _withUIData); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + DrawIntro(Ipc.GetGameObjectResourcePaths.Label, "Get GameObject resource paths"); + if (ImGui.Button("Get##GameObjectResourcePaths")) + { + var gameObjects = GetSelectedGameObjects(); + var resourcePaths = Ipc.GetGameObjectResourcePaths.Subscriber(_pi).Invoke(gameObjects, _mergeSameCollection); + + _lastGameObjectResourcePaths = gameObjects + .Select(GameObjectToString) + .Zip(resourcePaths) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcePaths)); + } + + DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); + if (ImGui.Button("Get##PlayerResourcePaths")) + { + _lastPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Subscriber(_pi).Invoke(_mergeSameCollection) + .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcePaths)); + } + + DrawIntro(Ipc.GetGameObjectResourcesOfType.Label, "Get GameObject resources of type"); + if (ImGui.Button("Get##GameObjectResourcesOfType")) + { + var gameObjects = GetSelectedGameObjects(); + var resourcesOfType = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi).Invoke(gameObjects, _type, _withUIData); + + _lastGameObjectResourcesOfType = gameObjects + .Select(GameObjectToString) + .Zip(resourcesOfType) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcesOfType)); + } + + DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); + if (ImGui.Button("Get##PlayerResourcesOfType")) + { + _lastPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Subscriber(_pi).Invoke(_type, _withUIData) + .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); + } + + DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths); + DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths); + + DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType); + DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType); + } + + private static void DrawPopup(string popupId, ref T? result, Action drawResult) where T : class + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); + using var popup = ImRaii.Popup(popupId); + if (!popup) + { + result = null; + return; + } + + if (result == null) + { + ImGui.CloseCurrentPopup(); + return; + } + + drawResult(result); + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + { + result = null; + ImGui.CloseCurrentPopup(); + } + } + + private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class + { + var firstSeen = new Dictionary(); + foreach (var (label, item) in result) + { + if (item == null) + { + ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + if (firstSeen.TryGetValue(item, out var firstLabel)) + { + ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + firstSeen.Add(item, label); + + using var header = ImRaii.TreeNode(label); + if (!header) + continue; + + drawItem(item); + } + } + + private static void DrawResourcePaths((string, IReadOnlyDictionary?)[] result) + { + DrawWithHeaders(result, paths => + { + using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableHeadersRow(); + + foreach (var (actualPath, gamePaths) in paths) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + ImGui.TableNextColumn(); + foreach (var gamePath in gamePaths) + ImGui.TextUnformatted(gamePath); + } + }); + } + + private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result) + { + DrawWithHeaders(result, resources => + { + using var table = ImRaii.Table(string.Empty, _withUIData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUIData ? 0.55f : 0.85f); + if (_withUIData) + ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var (resourceHandle, (actualPath, name, icon)) in resources) + { + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{resourceHandle:X}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + if (_withUIData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(icon.ToString()); + ImGui.SameLine(); + ImGui.TextUnformatted(name); + } + } + }); + } + + private static void TextUnformattedMono(string text) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(text); + } + + private ushort[] GetSelectedGameObjects() + => _gameObjectIndices.Split(',') + .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) + .ToArray(); + + private unsafe string GameObjectToString(ushort gameObjectIndex) + { + var gameObject = _objects[gameObjectIndex]; + + return gameObject != null + ? $"[{gameObjectIndex}] {gameObject.Name} ({gameObject.ObjectKind})" + : $"[{gameObjectIndex}] null"; + } + } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 73a87fab..1c56b9df 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -23,13 +23,15 @@ using Penumbra.Import.Textures; using Penumbra.Interop.Services; using Penumbra.UI; using TextureType = Penumbra.Api.Enums.TextureType; +using Penumbra.Interop.ResourceTree; +using System.Collections.Immutable; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => (4, 21); + => (4, 22); public event Action? PreSettingsPanelDraw { @@ -104,31 +106,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi private ModFileSystem _modFileSystem; private ConfigWindow _configWindow; private TextureManager _textureManager; + private ResourceTreeFactory _resourceTreeFactory; public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, - ConfigWindow configWindow, TextureManager textureManager) + ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory) { - _communicator = communicator; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _dalamud = dalamud; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; - _collectionResolver = collectionResolver; - _cutsceneService = cutsceneService; - _modImportManager = modImportManager; - _collectionEditor = collectionEditor; - _redrawService = redrawService; - _modFileSystem = modFileSystem; - _configWindow = configWindow; - _textureManager = textureManager; - _lumina = _dalamud.GameData.GameData; + _communicator = communicator; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; + _modImportManager = modImportManager; + _collectionEditor = collectionEditor; + _redrawService = redrawService; + _modFileSystem = modFileSystem; + _configWindow = configWindow; + _textureManager = textureManager; + _resourceTreeFactory = resourceTreeFactory; + _lumina = _dalamud.GameData.GameData; _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); @@ -145,24 +149,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); - _lumina = null; - _communicator = null!; - _modManager = null!; - _resourceLoader = null!; - _config = null!; - _collectionManager = null!; - _dalamud = null!; - _tempCollections = null!; - _tempMods = null!; - _actors = null!; - _collectionResolver = null!; - _cutsceneService = null!; - _modImportManager = null!; - _collectionEditor = null!; - _redrawService = null!; - _modFileSystem = null!; - _configWindow = null!; - _textureManager = null!; + _lumina = null; + _communicator = null!; + _modManager = null!; + _resourceLoader = null!; + _config = null!; + _collectionManager = null!; + _dalamud = null!; + _tempCollections = null!; + _tempMods = null!; + _actors = null!; + _collectionResolver = null!; + _cutsceneService = null!; + _modImportManager = null!; + _collectionEditor = null!; + _redrawService = null!; + _modFileSystem = null!; + _configWindow = null!; + _textureManager = null!; + _resourceTreeFactory = null!; } public event ChangedItemClick? ChangedItemClicked @@ -1011,6 +1016,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; // @formatter:on + public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects, bool mergeSameCollection) + { + var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, false, false); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees, mergeSameCollection); + + return Array.ConvertAll(gameObjects, obj => pathDictionaries.TryGetValue(obj, out var pathDict) ? pathDict : null); + } + + public IReadOnlyDictionary> GetPlayerResourcePaths(bool mergeSameCollection) + { + var resourceTrees = _resourceTreeFactory.FromObjectTable(true, false, false); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees, mergeSameCollection); + + return pathDictionaries.AsReadOnly(); + } + + public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ushort[] gameObjects, ResourceType type, bool withUIData) + { + var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData, false); + var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + + return Array.ConvertAll(gameObjects, obj => resDictionaries.TryGetValue(obj, out var resDict) ? resDict : null); + } + + public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, bool withUIData) + { + var resourceTrees = _resourceTreeFactory.FromObjectTable(true, withUIData, false); + var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + + return resDictionaries.AsReadOnly(); + } + // TODO: cleanup when incrementing API public string GetMetaManipulations(string characterName) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 2d3fcf97..c05a7c47 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -118,6 +118,12 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider RemoveTemporaryModAll; internal readonly FuncProvider RemoveTemporaryMod; + // Resource Tree + internal readonly FuncProvider?[]> GetGameObjectResourcePaths; + internal readonly FuncProvider>> GetPlayerResourcePaths; + internal readonly FuncProvider?[]> GetGameObjectResourcesOfType; + internal readonly FuncProvider>> GetPlayerResourcesOfType; + public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) { @@ -236,6 +242,12 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); + // ResourceTree + GetGameObjectResourcePaths = Ipc.GetGameObjectResourcePaths.Provider(pi, Api.GetGameObjectResourcePaths); + GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths); + GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType); + GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType); + Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService); Initialized.Invoke(); @@ -345,6 +357,12 @@ public class PenumbraIpcProviders : IDisposable ConvertTextureFile.Dispose(); ConvertTextureData.Dispose(); + // Resource Tree + GetGameObjectResourcePaths.Dispose(); + GetPlayerResourcePaths.Dispose(); + GetGameObjectResourcesOfType.Dispose(); + GetPlayerResourcesOfType.Dispose(); + Disposed.Invoke(); Disposed.Dispose(); } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 63d58a16..0206d0ae 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -6,6 +6,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Interop.Services; using Penumbra.Mods; diff --git a/Penumbra/Enums/ResourceType.cs b/Penumbra/Enums/ResourceType.cs new file mode 100644 index 00000000..0cfc5469 --- /dev/null +++ b/Penumbra/Enums/ResourceType.cs @@ -0,0 +1,260 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.String; +using Penumbra.String.Functions; + +namespace Penumbra.Enums; + +[Flags] +public enum ResourceTypeFlag : ulong +{ + Aet = 0x0000_0000_0000_0001, + Amb = 0x0000_0000_0000_0002, + Atch = 0x0000_0000_0000_0004, + Atex = 0x0000_0000_0000_0008, + Avfx = 0x0000_0000_0000_0010, + Awt = 0x0000_0000_0000_0020, + Cmp = 0x0000_0000_0000_0040, + Dic = 0x0000_0000_0000_0080, + Eid = 0x0000_0000_0000_0100, + Envb = 0x0000_0000_0000_0200, + Eqdp = 0x0000_0000_0000_0400, + Eqp = 0x0000_0000_0000_0800, + Essb = 0x0000_0000_0000_1000, + Est = 0x0000_0000_0000_2000, + Evp = 0x0000_0000_0000_4000, + Exd = 0x0000_0000_0000_8000, + Exh = 0x0000_0000_0001_0000, + Exl = 0x0000_0000_0002_0000, + Fdt = 0x0000_0000_0004_0000, + Gfd = 0x0000_0000_0008_0000, + Ggd = 0x0000_0000_0010_0000, + Gmp = 0x0000_0000_0020_0000, + Gzd = 0x0000_0000_0040_0000, + Imc = 0x0000_0000_0080_0000, + Lcb = 0x0000_0000_0100_0000, + Lgb = 0x0000_0000_0200_0000, + Luab = 0x0000_0000_0400_0000, + Lvb = 0x0000_0000_0800_0000, + Mdl = 0x0000_0000_1000_0000, + Mlt = 0x0000_0000_2000_0000, + Mtrl = 0x0000_0000_4000_0000, + Obsb = 0x0000_0000_8000_0000, + Pap = 0x0000_0001_0000_0000, + Pbd = 0x0000_0002_0000_0000, + Pcb = 0x0000_0004_0000_0000, + Phyb = 0x0000_0008_0000_0000, + Plt = 0x0000_0010_0000_0000, + Scd = 0x0000_0020_0000_0000, + Sgb = 0x0000_0040_0000_0000, + Shcd = 0x0000_0080_0000_0000, + Shpk = 0x0000_0100_0000_0000, + Sklb = 0x0000_0200_0000_0000, + Skp = 0x0000_0400_0000_0000, + Stm = 0x0000_0800_0000_0000, + Svb = 0x0000_1000_0000_0000, + Tera = 0x0000_2000_0000_0000, + Tex = 0x0000_4000_0000_0000, + Tmb = 0x0000_8000_0000_0000, + Ugd = 0x0001_0000_0000_0000, + Uld = 0x0002_0000_0000_0000, + Waoe = 0x0004_0000_0000_0000, + Wtd = 0x0008_0000_0000_0000, +} + +[Flags] +public enum ResourceCategoryFlag : ushort +{ + Common = 0x0001, + BgCommon = 0x0002, + Bg = 0x0004, + Cut = 0x0008, + Chara = 0x0010, + Shader = 0x0020, + Ui = 0x0040, + Sound = 0x0080, + Vfx = 0x0100, + UiScript = 0x0200, + Exd = 0x0400, + GameScript = 0x0800, + Music = 0x1000, + SqpackTest = 0x2000, +} + +public static class ResourceExtensions +{ + public static readonly ResourceTypeFlag AllResourceTypes = Enum.GetValues().Aggregate((v, f) => v | f); + public static readonly ResourceCategoryFlag AllResourceCategories = Enum.GetValues().Aggregate((v, f) => v | f); + + public static ResourceTypeFlag ToFlag(this ResourceType type) + => type switch + { + ResourceType.Aet => ResourceTypeFlag.Aet, + ResourceType.Amb => ResourceTypeFlag.Amb, + ResourceType.Atch => ResourceTypeFlag.Atch, + ResourceType.Atex => ResourceTypeFlag.Atex, + ResourceType.Avfx => ResourceTypeFlag.Avfx, + ResourceType.Awt => ResourceTypeFlag.Awt, + ResourceType.Cmp => ResourceTypeFlag.Cmp, + ResourceType.Dic => ResourceTypeFlag.Dic, + ResourceType.Eid => ResourceTypeFlag.Eid, + ResourceType.Envb => ResourceTypeFlag.Envb, + ResourceType.Eqdp => ResourceTypeFlag.Eqdp, + ResourceType.Eqp => ResourceTypeFlag.Eqp, + ResourceType.Essb => ResourceTypeFlag.Essb, + ResourceType.Est => ResourceTypeFlag.Est, + ResourceType.Evp => ResourceTypeFlag.Evp, + ResourceType.Exd => ResourceTypeFlag.Exd, + ResourceType.Exh => ResourceTypeFlag.Exh, + ResourceType.Exl => ResourceTypeFlag.Exl, + ResourceType.Fdt => ResourceTypeFlag.Fdt, + ResourceType.Gfd => ResourceTypeFlag.Gfd, + ResourceType.Ggd => ResourceTypeFlag.Ggd, + ResourceType.Gmp => ResourceTypeFlag.Gmp, + ResourceType.Gzd => ResourceTypeFlag.Gzd, + ResourceType.Imc => ResourceTypeFlag.Imc, + ResourceType.Lcb => ResourceTypeFlag.Lcb, + ResourceType.Lgb => ResourceTypeFlag.Lgb, + ResourceType.Luab => ResourceTypeFlag.Luab, + ResourceType.Lvb => ResourceTypeFlag.Lvb, + ResourceType.Mdl => ResourceTypeFlag.Mdl, + ResourceType.Mlt => ResourceTypeFlag.Mlt, + ResourceType.Mtrl => ResourceTypeFlag.Mtrl, + ResourceType.Obsb => ResourceTypeFlag.Obsb, + ResourceType.Pap => ResourceTypeFlag.Pap, + ResourceType.Pbd => ResourceTypeFlag.Pbd, + ResourceType.Pcb => ResourceTypeFlag.Pcb, + ResourceType.Phyb => ResourceTypeFlag.Phyb, + ResourceType.Plt => ResourceTypeFlag.Plt, + ResourceType.Scd => ResourceTypeFlag.Scd, + ResourceType.Sgb => ResourceTypeFlag.Sgb, + ResourceType.Shcd => ResourceTypeFlag.Shcd, + ResourceType.Shpk => ResourceTypeFlag.Shpk, + ResourceType.Sklb => ResourceTypeFlag.Sklb, + ResourceType.Skp => ResourceTypeFlag.Skp, + ResourceType.Stm => ResourceTypeFlag.Stm, + ResourceType.Svb => ResourceTypeFlag.Svb, + ResourceType.Tera => ResourceTypeFlag.Tera, + ResourceType.Tex => ResourceTypeFlag.Tex, + ResourceType.Tmb => ResourceTypeFlag.Tmb, + ResourceType.Ugd => ResourceTypeFlag.Ugd, + ResourceType.Uld => ResourceTypeFlag.Uld, + ResourceType.Waoe => ResourceTypeFlag.Waoe, + ResourceType.Wtd => ResourceTypeFlag.Wtd, + _ => 0, + }; + + public static bool FitsFlag(this ResourceType type, ResourceTypeFlag flags) + => (type.ToFlag() & flags) != 0; + + public static ResourceCategoryFlag ToFlag(this ResourceCategory type) + => type switch + { + ResourceCategory.Common => ResourceCategoryFlag.Common, + ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon, + ResourceCategory.Bg => ResourceCategoryFlag.Bg, + ResourceCategory.Cut => ResourceCategoryFlag.Cut, + ResourceCategory.Chara => ResourceCategoryFlag.Chara, + ResourceCategory.Shader => ResourceCategoryFlag.Shader, + ResourceCategory.Ui => ResourceCategoryFlag.Ui, + ResourceCategory.Sound => ResourceCategoryFlag.Sound, + ResourceCategory.Vfx => ResourceCategoryFlag.Vfx, + ResourceCategory.UiScript => ResourceCategoryFlag.UiScript, + ResourceCategory.Exd => ResourceCategoryFlag.Exd, + ResourceCategory.GameScript => ResourceCategoryFlag.GameScript, + ResourceCategory.Music => ResourceCategoryFlag.Music, + ResourceCategory.SqpackTest => ResourceCategoryFlag.SqpackTest, + _ => 0, + }; + + public static bool FitsFlag(this ResourceCategory type, ResourceCategoryFlag flags) + => (type.ToFlag() & flags) != 0; + + public static ResourceType FromBytes(byte a1, byte a2, byte a3) + => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 16) + | ((uint)ByteStringFunctions.AsciiToLower(a2) << 8) + | ByteStringFunctions.AsciiToLower(a3)); + + public static ResourceType FromBytes(byte a1, byte a2, byte a3, byte a4) + => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 24) + | ((uint)ByteStringFunctions.AsciiToLower(a2) << 16) + | ((uint)ByteStringFunctions.AsciiToLower(a3) << 8) + | ByteStringFunctions.AsciiToLower(a4)); + + public static ResourceType FromBytes(char a1, char a2, char a3) + => FromBytes((byte)a1, (byte)a2, (byte)a3); + + public static ResourceType FromBytes(char a1, char a2, char a3, char a4) + => FromBytes((byte)a1, (byte)a2, (byte)a3, (byte)a4); + + public static ResourceType Type(string path) + { + var ext = Path.GetExtension(path.AsSpan()); + ext = ext.Length == 0 ? path.AsSpan() : ext[1..]; + + return ext.Length switch + { + 0 => 0, + 1 => (ResourceType)ext[^1], + 2 => FromBytes('\0', ext[^2], ext[^1]), + 3 => FromBytes(ext[^3], ext[^2], ext[^1]), + _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), + }; + } + + public static ResourceType Type(ByteString path) + { + var extIdx = path.LastIndexOf((byte)'.'); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1); + + return ext.Length switch + { + 0 => 0, + 1 => (ResourceType)ext[^1], + 2 => FromBytes(0, ext[^2], ext[^1]), + 3 => FromBytes(ext[^3], ext[^2], ext[^1]), + _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), + }; + } + + public static ResourceCategory Category(ByteString path) + { + if (path.Length < 3) + return ResourceCategory.Debug; + + return ByteStringFunctions.AsciiToUpper(path[0]) switch + { + (byte)'C' => ByteStringFunctions.AsciiToUpper(path[1]) switch + { + (byte)'O' => ResourceCategory.Common, + (byte)'U' => ResourceCategory.Cut, + (byte)'H' => ResourceCategory.Chara, + _ => ResourceCategory.Debug, + }, + (byte)'B' => ByteStringFunctions.AsciiToUpper(path[2]) switch + { + (byte)'C' => ResourceCategory.BgCommon, + (byte)'/' => ResourceCategory.Bg, + _ => ResourceCategory.Debug, + }, + (byte)'S' => ByteStringFunctions.AsciiToUpper(path[1]) switch + { + (byte)'H' => ResourceCategory.Shader, + (byte)'O' => ResourceCategory.Sound, + (byte)'Q' => ResourceCategory.SqpackTest, + _ => ResourceCategory.Debug, + }, + (byte)'U' => ByteStringFunctions.AsciiToUpper(path[2]) switch + { + (byte)'/' => ResourceCategory.Ui, + (byte)'S' => ResourceCategory.UiScript, + _ => ResourceCategory.Debug, + }, + (byte)'V' => ResourceCategory.Vfx, + (byte)'E' => ResourceCategory.Exd, + (byte)'G' => ResourceCategory.GameScript, + (byte)'M' => ResourceCategory.Music, + _ => ResourceCategory.Debug, + }; + } +} diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 51612819..616cb2f6 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -4,8 +4,8 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 6afaf5d1..40984c6a 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -3,6 +3,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; +using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 4494dc77..20713fe7 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -3,7 +3,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 00b06963..a8fd816a 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,8 +1,8 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 5d5e4590..94fdcce4 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index 0b29ee10..ce7d3d4c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -3,8 +3,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; namespace Penumbra.Interop.ResourceLoading; diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 8b4a5d15..5d060d85 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -1,9 +1,9 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index 6dac0e6b..574da240 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -1,8 +1,8 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceLoading; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 0bbe8e66..972e3c55 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index c3327a9e..2fffaedd 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,4 +1,4 @@ -using Penumbra.GameData.Enums; +using Penumbra.Api.Enums; using Penumbra.String.Classes; using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index a8ad9d4f..161e0368 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -12,9 +12,12 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTree { public readonly string Name; + public readonly int GameObjectIndex; public readonly nint GameObjectAddress; public readonly nint DrawObjectAddress; + public readonly bool LocalPlayerRelated; public readonly bool PlayerRelated; + public readonly bool Networked; public readonly string CollectionName; public readonly List Nodes; public readonly HashSet FlatNodes; @@ -23,15 +26,18 @@ public class ResourceTree public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, nint gameObjectAddress, nint drawObjectAddress, bool playerRelated, string collectionName) + public ResourceTree(string name, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName) { - Name = name; - GameObjectAddress = gameObjectAddress; - DrawObjectAddress = drawObjectAddress; - PlayerRelated = playerRelated; - CollectionName = collectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Name = name; + GameObjectIndex = gameObjectIndex; + GameObjectAddress = gameObjectAddress; + DrawObjectAddress = drawObjectAddress; + LocalPlayerRelated = localPlayerRelated; + Networked = networked; + PlayerRelated = playerRelated; + CollectionName = collectionName; + Nodes = new List(); + FlatNodes = new HashSet(); } internal unsafe void LoadResources(GlobalResolveContext globalContext) @@ -64,7 +70,7 @@ public class ResourceTree AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); - if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) + if (model->GetModelType() == CharacterBase.ModelType.Human) AddHumanResources(globalContext, (HumanExt*)model); } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs new file mode 100644 index 00000000..e7d9abc2 --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -0,0 +1,104 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.Api.Enums; +using Penumbra.UI; + +namespace Penumbra.Interop.ResourceTree; + +internal static class ResourceTreeApiHelper +{ + public static Dictionary> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees, + bool mergeSameCollection) + => mergeSameCollection ? GetResourcePathDictionariesMerged(resourceTrees) : GetResourcePathDictionariesUnmerged(resourceTrees); + + private static Dictionary> GetResourcePathDictionariesMerged(IEnumerable<(Character, ResourceTree)> resourceTrees) + { + var collections = new Dictionary(4); + var pathDictionaries = new Dictionary>>(4); + + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (collections.ContainsKey(gameObject.ObjectIndex)) + continue; + + collections.Add(gameObject.ObjectIndex, resourceTree.CollectionName); + if (!pathDictionaries.TryGetValue(resourceTree.CollectionName, out var pathDictionary)) + { + pathDictionary = new(); + pathDictionaries.Add(resourceTree.CollectionName, pathDictionary); + } + + CollectResourcePaths(pathDictionary, resourceTree); + } + + var pathRODictionaries = pathDictionaries.ToDictionary(pair => pair.Key, + pair => (IReadOnlyDictionary)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly()); + + return collections.ToDictionary(pair => pair.Key, pair => pathRODictionaries[pair.Value]); + } + + private static Dictionary> GetResourcePathDictionariesUnmerged(IEnumerable<(Character, ResourceTree)> resourceTrees) + { + var pathDictionaries = new Dictionary>>(4); + + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (pathDictionaries.ContainsKey(gameObject.ObjectIndex)) + continue; + + var pathDictionary = new Dictionary>(); + pathDictionaries.Add(gameObject.ObjectIndex, pathDictionary); + + CollectResourcePaths(pathDictionary, resourceTree); + } + + return pathDictionaries.ToDictionary(pair => pair.Key, + pair => (IReadOnlyDictionary)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly()); + } + + private static void CollectResourcePaths(Dictionary> pathDictionary, ResourceTree resourceTree) + { + foreach (var node in resourceTree.FlatNodes) + { + if (node.PossibleGamePaths.Length == 0) + continue; + + var fullPath = node.FullPath.ToPath(); + if (!pathDictionary.TryGetValue(fullPath, out var gamePaths)) + { + gamePaths = new(); + pathDictionary.Add(fullPath, gamePaths); + } + + foreach (var gamePath in node.PossibleGamePaths) + gamePaths.Add(gamePath.ToString()); + } + } + + public static Dictionary> GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, + ResourceType type) + { + var resDictionaries = new Dictionary>(4); + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (resDictionaries.ContainsKey(gameObject.ObjectIndex)) + continue; + + var resDictionary = new Dictionary(); + resDictionaries.Add(gameObject.ObjectIndex, resDictionary); + + foreach (var node in resourceTree.FlatNodes) + { + if (node.Type != type) + continue; + if (resDictionary.ContainsKey(node.ResourceHandle)) + continue; + + var fullPath = node.FullPath.ToPath(); + resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, ChangedItemDrawer.ToApiIcon(node.Icon))); + } + } + + return resDictionaries.ToDictionary(pair => pair.Key, + pair => (IReadOnlyDictionary)pair.Value.AsReadOnly()); + } +} diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index a2e29e48..e3418e5b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -27,21 +27,35 @@ public class ResourceTreeFactory _actors = actors; } - public ResourceTree[] FromObjectTable(bool withNames = true, bool redactExternalPaths = true) - { - var cache = new TreeBuildCache(_objects, _gameData); + private TreeBuildCache CreateTreeBuildCache() + => new(_objects, _gameData, _actors); - return cache.Characters - .Select(c => FromCharacter(c, cache, withNames, redactExternalPaths)) - .OfType() - .ToArray(); + public IEnumerable GetLocalPlayerRelatedCharacters() + { + var cache = CreateTreeBuildCache(); + + return cache.Characters.Where(cache.IsLocalPlayerRelated); + } + + public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromObjectTable( + bool localPlayerRelatedOnly = false, bool withUIData = true, bool redactExternalPaths = true) + { + var cache = CreateTreeBuildCache(); + var characters = localPlayerRelatedOnly ? cache.Characters.Where(cache.IsLocalPlayerRelated) : cache.Characters; + + foreach (var character in characters) + { + var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); + if (tree != null) + yield return (character, tree); + } } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, bool withUIData = true, bool redactExternalPaths = true) { - var cache = new TreeBuildCache(_objects, _gameData); + var cache = CreateTreeBuildCache(); foreach (var character in characters) { var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); @@ -52,7 +66,7 @@ public class ResourceTreeFactory public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true, bool redactExternalPaths = true) - => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withUIData, redactExternalPaths); + => FromCharacter(character, CreateTreeBuildCache(), withUIData, redactExternalPaths); private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, bool withUIData = true, bool redactExternalPaths = true) @@ -69,8 +83,10 @@ public class ResourceTreeFactory if (!collectionResolveData.Valid) return null; - var (name, related) = GetCharacterName(character, cache); - var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); + var localPlayerRelated = cache.IsLocalPlayerRelated(character); + var (name, related) = GetCharacterName(character, cache); + var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; + var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); tree.LoadResources(globalContext); diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 43b25476..60714fbb 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,6 +1,8 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Files; +using Penumbra.Interop.Services; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -8,18 +10,38 @@ namespace Penumbra.Interop.ResourceTree; internal class TreeBuildCache { private readonly IDataManager _dataManager; + private readonly ActorService _actors; private readonly Dictionary _shaderPackages = new(); + private readonly uint _localPlayerId; public readonly List Characters; public readonly Dictionary CharactersById; - public TreeBuildCache(IObjectTable objects, IDataManager dataManager) + public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors) { - _dataManager = dataManager; - Characters = objects.Where(c => c is Character ch && ch.IsValid()).Cast().ToList(); + _dataManager = dataManager; + _actors = actors; + Characters = objects.OfType().Where(ch => ch.IsValid()).ToList(); CharactersById = Characters .Where(c => c.ObjectId != GameObject.InvalidGameObjectId) .GroupBy(c => c.ObjectId) .ToDictionary(c => c.Key, c => c.First()); + _localPlayerId = Characters.Count > 0 && Characters[0].ObjectIndex == 0 ? Characters[0].ObjectId : GameObject.InvalidGameObjectId; + } + + public unsafe bool IsLocalPlayerRelated(Character character) + { + if (_localPlayerId == GameObject.InvalidGameObjectId) + return false; + + // Index 0 is the local player, index 1 is the mount/minion/accessory. + if (character.ObjectIndex < 2 || character.ObjectIndex == RedrawService.GPosePlayerIdx) + return true; + + if (!_actors.AwaitedService.FromObject(character, out var owner, true, false, false).IsValid) + return false; + + // Check for SMN/SCH pet, chocobo and other owned NPCs. + return owner != null && owner->ObjectID == _localPlayerId; } /// Try to read a shpk file from the given path and cache it on success. diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index 18c88766..17d8d2e0 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index dba113f3..1b78e857 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,8 +1,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index b9200a24..acd6eae9 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -1,3 +1,4 @@ +using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index d95c8796..3d8ab1b6 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -1,3 +1,4 @@ +using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 0140d189..7c2f50c4 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; +using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 0fa81a52..27935ffb 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,7 +1,7 @@ +using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; -using Penumbra.GameData.Enums; using Penumbra.Meta; using static Penumbra.Mods.ItemSwap.ItemSwap; using Penumbra.Services; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index ec433113..0cda8f59 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -90,6 +90,10 @@ + + + + diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 5f09e584..2d474a83 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -40,8 +40,6 @@ public class ResourceTreeViewer if (!child) return; - var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text); - var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green if (!_task.IsCompleted) { ImGui.NewLine(); @@ -55,17 +53,30 @@ public class ResourceTreeViewer } else if (_task.IsCompletedSuccessfully) { + var debugMode = _config.DebugMode; foreach (var (tree, index) in _task.Result.WithIndex()) { - using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer)) + var headerColorId = + tree.LocalPlayerRelated ? ColorId.ResTreeLocalPlayer : + tree.PlayerRelated ? ColorId.ResTreePlayer : + tree.Networked ? ColorId.ResTreeNetworked : + ColorId.ResTreeNonNetworked; + using (var c = ImRaii.PushColor(ImGuiCol.Text, headerColorId.Value())) { - if (!ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0)) + var isOpen = ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); + if (debugMode) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.HoverTooltip( + $"Object Index: {tree.GameObjectIndex}\nObject Address: 0x{tree.GameObjectAddress:X16}\nDraw Object Address: 0x{tree.DrawObjectAddress:X16}"); + } + if (!isOpen) continue; } using var id = ImRaii.PushId(index); - ImGui.Text($"Collection: {tree.CollectionName}"); + ImGui.TextUnformatted($"Collection: {tree.CollectionName}"); using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); @@ -90,7 +101,9 @@ public class ResourceTreeViewer { try { - return _treeFactory.FromObjectTable(); + return _treeFactory.FromObjectTable() + .Select(entry => entry.ResourceTree) + .ToArray(); } finally { diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 91d81ea3..13a5787e 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -15,6 +15,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Services; using Penumbra.UI.Classes; +using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon; namespace Penumbra.UI; @@ -311,6 +312,29 @@ public class ChangedItemDrawer : IDisposable _ => "Other", }; + internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon) + => icon switch + { + ChangedItemIcon.Head => ApiChangedItemIcon.Head, + ChangedItemIcon.Body => ApiChangedItemIcon.Body, + ChangedItemIcon.Hands => ApiChangedItemIcon.Hands, + ChangedItemIcon.Legs => ApiChangedItemIcon.Legs, + ChangedItemIcon.Feet => ApiChangedItemIcon.Feet, + ChangedItemIcon.Ears => ApiChangedItemIcon.Ears, + ChangedItemIcon.Neck => ApiChangedItemIcon.Neck, + ChangedItemIcon.Wrists => ApiChangedItemIcon.Wrists, + ChangedItemIcon.Finger => ApiChangedItemIcon.Finger, + ChangedItemIcon.Monster => ApiChangedItemIcon.Monster, + ChangedItemIcon.Demihuman => ApiChangedItemIcon.Demihuman, + ChangedItemIcon.Customization => ApiChangedItemIcon.Customization, + ChangedItemIcon.Action => ApiChangedItemIcon.Action, + ChangedItemIcon.Emote => ApiChangedItemIcon.Emote, + ChangedItemIcon.Mainhand => ApiChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand => ApiChangedItemIcon.Offhand, + ChangedItemIcon.Unknown => ApiChangedItemIcon.Unknown, + _ => ApiChangedItemIcon.None, + }; + /// Apply Changed Item Counters to the Name if necessary. private static string ChangedItemName(string name, object? data) => data is int counter ? $"{counter} Files Manipulating {name}s" : name; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index ebcff821..0e3b9377 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -24,10 +24,16 @@ public enum ColorId NoAssignment, SelectorPriority, InGameHighlight, + ResTreeLocalPlayer, + ResTreePlayer, + ResTreeNetworked, + ResTreeNonNetworked, } public static class Colors { + // These are written as 0xAABBGGRR. + public const uint PressEnterWarningBg = 0xFF202080; public const uint RegexWarningBorder = 0xFF0000B0; public const uint MetaInfoText = 0xAAFFFFFF; @@ -64,6 +70,10 @@ public static class Colors ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight", "An in-game element that has been highlighted for ease of editing."), + ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), + ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), + ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), + ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index dea68955..1a25d722 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Collections; +using Penumbra.Enums; using Penumbra.Interop.Structs; using Penumbra.String; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 781c5fc1..de5a179d 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -4,9 +4,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.Services; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index eb034459..3789baf4 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; +using Penumbra.Enums; using Penumbra.String; namespace Penumbra.UI.ResourceWatcher;