ResourceTree improvements + IPC

- Moves ResourceType enum out of GameData as discussed on Discord ;
- Adds new color coding for local player and non-networked objects on On-Screen ;
- Adds ResourceTree-related IPC ;
- Fixes #342.
This commit is contained in:
Exter-N 2023-09-17 22:25:31 +02:00 committed by Ottermandias
parent 2b4a01df06
commit d7205344eb
32 changed files with 826 additions and 80 deletions

View file

@ -15,6 +15,8 @@ using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Dalamud.Plugin.Services;
using Penumbra.GameData.Enums;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -35,6 +37,7 @@ public class IpcTester : IDisposable
private readonly ModSettings _modSettings; private readonly ModSettings _modSettings;
private readonly Editing _editing; private readonly Editing _editing;
private readonly Temporary _temporary; private readonly Temporary _temporary;
private readonly ResourceTree _resourceTree;
public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager, public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager,
CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService)
@ -52,6 +55,7 @@ public class IpcTester : IDisposable
_modSettings = new ModSettings(dalamud.PluginInterface); _modSettings = new ModSettings(dalamud.PluginInterface);
_editing = new Editing(dalamud.PluginInterface); _editing = new Editing(dalamud.PluginInterface);
_temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config); _temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config);
_resourceTree = new ResourceTree(dalamud.PluginInterface, dalamud.Objects);
UnsubscribeEvents(); UnsubscribeEvents();
} }
@ -75,6 +79,7 @@ public class IpcTester : IDisposable
_temporary.Draw(); _temporary.Draw();
_temporary.DrawCollections(); _temporary.DrawCollections();
_temporary.DrawMods(); _temporary.DrawMods();
_resourceTree.Draw();
} }
catch (Exception e) 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<string, string[]>?)[]? _lastGameObjectResourcePaths;
private (string, IReadOnlyDictionary<string, string[]>?)[]? _lastPlayerResourcePaths;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastGameObjectResourcesOfType;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _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<ResourceType>());
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<string, string[]>?)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<nint, (string, string, ChangedItemIcon)>?)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<T>(string popupId, ref T? result, Action<T> 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<T>((string, T?)[] result, Action<T> drawItem) where T : class
{
var firstSeen = new Dictionary<T, string>();
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<string, string[]>?)[] 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<nint, (string, string, ChangedItemIcon)>?)[] 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";
}
}
} }

View file

@ -23,13 +23,15 @@ using Penumbra.Import.Textures;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.UI; using Penumbra.UI;
using TextureType = Penumbra.Api.Enums.TextureType; using TextureType = Penumbra.Api.Enums.TextureType;
using Penumbra.Interop.ResourceTree;
using System.Collections.Immutable;
namespace Penumbra.Api; namespace Penumbra.Api;
public class PenumbraApi : IDisposable, IPenumbraApi public class PenumbraApi : IDisposable, IPenumbraApi
{ {
public (int, int) ApiVersion public (int, int) ApiVersion
=> (4, 21); => (4, 22);
public event Action<string>? PreSettingsPanelDraw public event Action<string>? PreSettingsPanelDraw
{ {
@ -104,31 +106,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private ModFileSystem _modFileSystem; private ModFileSystem _modFileSystem;
private ConfigWindow _configWindow; private ConfigWindow _configWindow;
private TextureManager _textureManager; private TextureManager _textureManager;
private ResourceTreeFactory _resourceTreeFactory;
public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader,
Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService,
ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem,
ConfigWindow configWindow, TextureManager textureManager) ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory)
{ {
_communicator = communicator; _communicator = communicator;
_modManager = modManager; _modManager = modManager;
_resourceLoader = resourceLoader; _resourceLoader = resourceLoader;
_config = config; _config = config;
_collectionManager = collectionManager; _collectionManager = collectionManager;
_dalamud = dalamud; _dalamud = dalamud;
_tempCollections = tempCollections; _tempCollections = tempCollections;
_tempMods = tempMods; _tempMods = tempMods;
_actors = actors; _actors = actors;
_collectionResolver = collectionResolver; _collectionResolver = collectionResolver;
_cutsceneService = cutsceneService; _cutsceneService = cutsceneService;
_modImportManager = modImportManager; _modImportManager = modImportManager;
_collectionEditor = collectionEditor; _collectionEditor = collectionEditor;
_redrawService = redrawService; _redrawService = redrawService;
_modFileSystem = modFileSystem; _modFileSystem = modFileSystem;
_configWindow = configWindow; _configWindow = configWindow;
_textureManager = textureManager; _textureManager = textureManager;
_lumina = _dalamud.GameData.GameData; _resourceTreeFactory = resourceTreeFactory;
_lumina = _dalamud.GameData.GameData;
_resourceLoader.ResourceLoaded += OnResourceLoaded; _resourceLoader.ResourceLoaded += OnResourceLoaded;
_communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api);
@ -145,24 +149,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
_lumina = null; _lumina = null;
_communicator = null!; _communicator = null!;
_modManager = null!; _modManager = null!;
_resourceLoader = null!; _resourceLoader = null!;
_config = null!; _config = null!;
_collectionManager = null!; _collectionManager = null!;
_dalamud = null!; _dalamud = null!;
_tempCollections = null!; _tempCollections = null!;
_tempMods = null!; _tempMods = null!;
_actors = null!; _actors = null!;
_collectionResolver = null!; _collectionResolver = null!;
_cutsceneService = null!; _cutsceneService = null!;
_modImportManager = null!; _modImportManager = null!;
_collectionEditor = null!; _collectionEditor = null!;
_redrawService = null!; _redrawService = null!;
_modFileSystem = null!; _modFileSystem = null!;
_configWindow = null!; _configWindow = null!;
_textureManager = null!; _textureManager = null!;
_resourceTreeFactory = null!;
} }
public event ChangedItemClick? ChangedItemClicked public event ChangedItemClick? ChangedItemClicked
@ -1011,6 +1016,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi
}; };
// @formatter:on // @formatter:on
public IReadOnlyDictionary<string, string[]>?[] GetGameObjectResourcePaths(ushort[] gameObjects, bool mergeSameCollection)
{
var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType<Character>();
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<ushort, IReadOnlyDictionary<string, string[]>> GetPlayerResourcePaths(bool mergeSameCollection)
{
var resourceTrees = _resourceTreeFactory.FromObjectTable(true, false, false);
var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees, mergeSameCollection);
return pathDictionaries.AsReadOnly();
}
public IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?[] GetGameObjectResourcesOfType(ushort[] gameObjects, ResourceType type, bool withUIData)
{
var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType<Character>();
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<ushort, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>> 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 // TODO: cleanup when incrementing API
public string GetMetaManipulations(string characterName) public string GetMetaManipulations(string characterName)

View file

@ -118,6 +118,12 @@ public class PenumbraIpcProviders : IDisposable
internal readonly FuncProvider<string, int, PenumbraApiEc> RemoveTemporaryModAll; internal readonly FuncProvider<string, int, PenumbraApiEc> RemoveTemporaryModAll;
internal readonly FuncProvider<string, string, int, PenumbraApiEc> RemoveTemporaryMod; internal readonly FuncProvider<string, string, int, PenumbraApiEc> RemoveTemporaryMod;
// Resource Tree
internal readonly FuncProvider<ushort[], bool, IReadOnlyDictionary<string, string[]>?[]> GetGameObjectResourcePaths;
internal readonly FuncProvider<bool, IReadOnlyDictionary<ushort, IReadOnlyDictionary<string, string[]>>> GetPlayerResourcePaths;
internal readonly FuncProvider<ushort[], ResourceType, bool, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?[]> GetGameObjectResourcesOfType;
internal readonly FuncProvider<ResourceType, bool, IReadOnlyDictionary<ushort, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>>> GetPlayerResourcesOfType;
public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections,
TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config)
{ {
@ -236,6 +242,12 @@ public class PenumbraIpcProviders : IDisposable
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll);
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); 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); Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService);
Initialized.Invoke(); Initialized.Invoke();
@ -345,6 +357,12 @@ public class PenumbraIpcProviders : IDisposable
ConvertTextureFile.Dispose(); ConvertTextureFile.Dispose();
ConvertTextureData.Dispose(); ConvertTextureData.Dispose();
// Resource Tree
GetGameObjectResourcePaths.Dispose();
GetPlayerResourcePaths.Dispose();
GetGameObjectResourcesOfType.Dispose();
GetPlayerResourcesOfType.Dispose();
Disposed.Invoke(); Disposed.Invoke();
Disposed.Dispose(); Disposed.Dispose();
} }

View file

@ -6,6 +6,7 @@ using OtterGui.Classes;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Mods; using Penumbra.Mods;

View file

@ -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<ResourceTypeFlag>().Aggregate((v, f) => v | f);
public static readonly ResourceCategoryFlag AllResourceCategories = Enum.GetValues<ResourceCategoryFlag>().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,
};
}
}

View file

@ -4,8 +4,8 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Api.Enums;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;

View file

@ -3,6 +3,7 @@ using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Api.Enums;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;

View file

@ -3,7 +3,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.GameData.Enums;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;

View file

@ -1,8 +1,8 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;

View file

@ -1,6 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;

View file

@ -3,8 +3,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop; using FFXIVClientStructs.Interop;
using FFXIVClientStructs.STD; using FFXIVClientStructs.STD;
using Penumbra.Api.Enums;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums;
namespace Penumbra.Interop.ResourceLoading; namespace Penumbra.Interop.ResourceLoading;

View file

@ -1,9 +1,9 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;

View file

@ -1,8 +1,8 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.Api.Enums;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Interop.ResourceLoading; namespace Penumbra.Interop.ResourceLoading;

View file

@ -1,6 +1,7 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui; using OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;

View file

@ -1,4 +1,4 @@
using Penumbra.GameData.Enums; using Penumbra.Api.Enums;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon;

View file

@ -12,9 +12,12 @@ namespace Penumbra.Interop.ResourceTree;
public class ResourceTree public class ResourceTree
{ {
public readonly string Name; public readonly string Name;
public readonly int GameObjectIndex;
public readonly nint GameObjectAddress; public readonly nint GameObjectAddress;
public readonly nint DrawObjectAddress; public readonly nint DrawObjectAddress;
public readonly bool LocalPlayerRelated;
public readonly bool PlayerRelated; public readonly bool PlayerRelated;
public readonly bool Networked;
public readonly string CollectionName; public readonly string CollectionName;
public readonly List<ResourceNode> Nodes; public readonly List<ResourceNode> Nodes;
public readonly HashSet<ResourceNode> FlatNodes; public readonly HashSet<ResourceNode> FlatNodes;
@ -23,15 +26,18 @@ public class ResourceTree
public CustomizeData CustomizeData; public CustomizeData CustomizeData;
public GenderRace RaceCode; 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; Name = name;
GameObjectAddress = gameObjectAddress; GameObjectIndex = gameObjectIndex;
DrawObjectAddress = drawObjectAddress; GameObjectAddress = gameObjectAddress;
PlayerRelated = playerRelated; DrawObjectAddress = drawObjectAddress;
CollectionName = collectionName; LocalPlayerRelated = localPlayerRelated;
Nodes = new List<ResourceNode>(); Networked = networked;
FlatNodes = new HashSet<ResourceNode>(); PlayerRelated = playerRelated;
CollectionName = collectionName;
Nodes = new List<ResourceNode>();
FlatNodes = new HashSet<ResourceNode>();
} }
internal unsafe void LoadResources(GlobalResolveContext globalContext) internal unsafe void LoadResources(GlobalResolveContext globalContext)
@ -64,7 +70,7 @@ public class ResourceTree
AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); 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); AddHumanResources(globalContext, (HumanExt*)model);
} }

View file

@ -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<ushort, IReadOnlyDictionary<string, string[]>> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees,
bool mergeSameCollection)
=> mergeSameCollection ? GetResourcePathDictionariesMerged(resourceTrees) : GetResourcePathDictionariesUnmerged(resourceTrees);
private static Dictionary<ushort, IReadOnlyDictionary<string, string[]>> GetResourcePathDictionariesMerged(IEnumerable<(Character, ResourceTree)> resourceTrees)
{
var collections = new Dictionary<ushort, string>(4);
var pathDictionaries = new Dictionary<string, Dictionary<string, HashSet<string>>>(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<string, string[]>)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly());
return collections.ToDictionary(pair => pair.Key, pair => pathRODictionaries[pair.Value]);
}
private static Dictionary<ushort, IReadOnlyDictionary<string, string[]>> GetResourcePathDictionariesUnmerged(IEnumerable<(Character, ResourceTree)> resourceTrees)
{
var pathDictionaries = new Dictionary<ushort, Dictionary<string, HashSet<string>>>(4);
foreach (var (gameObject, resourceTree) in resourceTrees)
{
if (pathDictionaries.ContainsKey(gameObject.ObjectIndex))
continue;
var pathDictionary = new Dictionary<string, HashSet<string>>();
pathDictionaries.Add(gameObject.ObjectIndex, pathDictionary);
CollectResourcePaths(pathDictionary, resourceTree);
}
return pathDictionaries.ToDictionary(pair => pair.Key,
pair => (IReadOnlyDictionary<string, string[]>)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly());
}
private static void CollectResourcePaths(Dictionary<string, HashSet<string>> 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<ushort, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>> GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees,
ResourceType type)
{
var resDictionaries = new Dictionary<ushort, Dictionary<nint, (string, string, ChangedItemIcon)>>(4);
foreach (var (gameObject, resourceTree) in resourceTrees)
{
if (resDictionaries.ContainsKey(gameObject.ObjectIndex))
continue;
var resDictionary = new Dictionary<nint, (string, string, ChangedItemIcon)>();
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<nint, (string, string, ChangedItemIcon)>)pair.Value.AsReadOnly());
}
}

View file

@ -27,21 +27,35 @@ public class ResourceTreeFactory
_actors = actors; _actors = actors;
} }
public ResourceTree[] FromObjectTable(bool withNames = true, bool redactExternalPaths = true) private TreeBuildCache CreateTreeBuildCache()
{ => new(_objects, _gameData, _actors);
var cache = new TreeBuildCache(_objects, _gameData);
return cache.Characters public IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> GetLocalPlayerRelatedCharacters()
.Select(c => FromCharacter(c, cache, withNames, redactExternalPaths)) {
.OfType<ResourceTree>() var cache = CreateTreeBuildCache();
.ToArray();
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( public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters(
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters, IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters,
bool withUIData = true, bool redactExternalPaths = true) bool withUIData = true, bool redactExternalPaths = true)
{ {
var cache = new TreeBuildCache(_objects, _gameData); var cache = CreateTreeBuildCache();
foreach (var character in characters) foreach (var character in characters)
{ {
var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); 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, public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true,
bool redactExternalPaths = 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, private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache,
bool withUIData = true, bool redactExternalPaths = true) bool withUIData = true, bool redactExternalPaths = true)
@ -69,8 +83,10 @@ public class ResourceTreeFactory
if (!collectionResolveData.Valid) if (!collectionResolveData.Valid)
return null; return null;
var (name, related) = GetCharacterName(character, cache); var localPlayerRelated = cache.IsLocalPlayerRelated(character);
var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); 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, var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection,
((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths);
tree.LoadResources(globalContext); tree.LoadResources(globalContext);

View file

@ -1,6 +1,8 @@
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Interop.Services;
using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
@ -8,18 +10,38 @@ namespace Penumbra.Interop.ResourceTree;
internal class TreeBuildCache internal class TreeBuildCache
{ {
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly ActorService _actors;
private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = new(); private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = new();
private readonly uint _localPlayerId;
public readonly List<Character> Characters; public readonly List<Character> Characters;
public readonly Dictionary<uint, Character> CharactersById; public readonly Dictionary<uint, Character> CharactersById;
public TreeBuildCache(IObjectTable objects, IDataManager dataManager) public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors)
{ {
_dataManager = dataManager; _dataManager = dataManager;
Characters = objects.Where(c => c is Character ch && ch.IsValid()).Cast<Character>().ToList(); _actors = actors;
Characters = objects.OfType<Character>().Where(ch => ch.IsValid()).ToList();
CharactersById = Characters CharactersById = Characters
.Where(c => c.ObjectId != GameObject.InvalidGameObjectId) .Where(c => c.ObjectId != GameObject.InvalidGameObjectId)
.GroupBy(c => c.ObjectId) .GroupBy(c => c.ObjectId)
.ToDictionary(c => c.Key, c => c.First()); .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;
} }
/// <summary> Try to read a shpk file from the given path and cache it on success. </summary> /// <summary> Try to read a shpk file from the given path and cache it on success. </summary>

View file

@ -1,6 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;
using Penumbra.String.Classes; using Penumbra.String.Classes;

View file

@ -1,8 +1,8 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;

View file

@ -1,3 +1,4 @@
using Penumbra.Api.Enums;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;

View file

@ -1,3 +1,4 @@
using Penumbra.Api.Enums;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;

View file

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Penumbra.Api.Enums;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;

View file

@ -1,7 +1,7 @@
using Penumbra.Api.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.GameData.Enums;
using Penumbra.Meta; using Penumbra.Meta;
using static Penumbra.Mods.ItemSwap.ItemSwap; using static Penumbra.Mods.ItemSwap.ItemSwap;
using Penumbra.Services; using Penumbra.Services;

View file

@ -90,6 +90,10 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Enums\" />
</ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion"> <Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low"> <Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" /> <Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" />

View file

@ -40,8 +40,6 @@ public class ResourceTreeViewer
if (!child) if (!child)
return; return;
var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text);
var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green
if (!_task.IsCompleted) if (!_task.IsCompleted)
{ {
ImGui.NewLine(); ImGui.NewLine();
@ -55,17 +53,30 @@ public class ResourceTreeViewer
} }
else if (_task.IsCompletedSuccessfully) else if (_task.IsCompletedSuccessfully)
{ {
var debugMode = _config.DebugMode;
foreach (var (tree, index) in _task.Result.WithIndex()) 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; continue;
} }
using var id = ImRaii.PushId(index); 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, using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
@ -90,7 +101,9 @@ public class ResourceTreeViewer
{ {
try try
{ {
return _treeFactory.FromObjectTable(); return _treeFactory.FromObjectTable()
.Select(entry => entry.ResourceTree)
.ToArray();
} }
finally finally
{ {

View file

@ -15,6 +15,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -311,6 +312,29 @@ public class ChangedItemDrawer : IDisposable
_ => "Other", _ => "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,
};
/// <summary> Apply Changed Item Counters to the Name if necessary. </summary> /// <summary> Apply Changed Item Counters to the Name if necessary. </summary>
private static string ChangedItemName(string name, object? data) private static string ChangedItemName(string name, object? data)
=> data is int counter ? $"{counter} Files Manipulating {name}s" : name; => data is int counter ? $"{counter} Files Manipulating {name}s" : name;

View file

@ -24,10 +24,16 @@ public enum ColorId
NoAssignment, NoAssignment,
SelectorPriority, SelectorPriority,
InGameHighlight, InGameHighlight,
ResTreeLocalPlayer,
ResTreePlayer,
ResTreeNetworked,
ResTreeNonNetworked,
} }
public static class Colors public static class Colors
{ {
// These are written as 0xAABBGGRR.
public const uint PressEnterWarningBg = 0xFF202080; public const uint PressEnterWarningBg = 0xFF202080;
public const uint RegexWarningBorder = 0xFF0000B0; public const uint RegexWarningBorder = 0xFF0000B0;
public const uint MetaInfoText = 0xAAFFFFFF; 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.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.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.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 ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ),
// @formatter:on // @formatter:on
}; };

View file

@ -1,5 +1,6 @@
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Enums;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;

View file

@ -4,9 +4,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
using ImGuiNET; using ImGuiNET;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Services; using Penumbra.Services;

View file

@ -4,6 +4,7 @@ using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Table; using OtterGui.Table;
using Penumbra.Enums;
using Penumbra.String; using Penumbra.String;
namespace Penumbra.UI.ResourceWatcher; namespace Penumbra.UI.ResourceWatcher;