From 49f1e2dcde010db85c4adef007e9f5b83363097d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 18:54:16 +0100 Subject: [PATCH] Hopefully merge the rest of the changes correctly. --- Penumbra.Api | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 10 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 64 +++-- .../ResourceTree/ResourceTreeFactory.cs | 51 +++- .../{FileCache.cs => TreeBuildCache.cs} | 18 +- Penumbra/Interop/Structs/WeaponExt.cs | 14 -- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/PenumbraNew.cs | 1 + Penumbra/UI/AdvancedWindow/FileEditor.cs | 40 ++- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 1 - .../ModEditWindow.QuickImport.cs | 226 +++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 34 +-- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 169 +++++++++++++ .../UI/Classes/ModEditWindow.QuickImport.cs | 234 ------------------ Penumbra/UI/Classes/ResourceTreeViewer.cs | 174 ------------- Penumbra/UI/ConfigWindow.OnScreenTab.cs | 23 -- Penumbra/UI/Tabs/ConfigTabBar.cs | 48 ++-- Penumbra/UI/Tabs/OnScreenTab.cs | 28 +++ Penumbra/UI/WindowSystem.cs | 1 - 20 files changed, 606 insertions(+), 536 deletions(-) rename Penumbra/Interop/ResourceTree/{FileCache.cs => TreeBuildCache.cs} (70%) delete mode 100644 Penumbra/Interop/Structs/WeaponExt.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs create mode 100644 Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs delete mode 100644 Penumbra/UI/Classes/ModEditWindow.QuickImport.cs delete mode 100644 Penumbra/UI/Classes/ResourceTreeViewer.cs delete mode 100644 Penumbra/UI/ConfigWindow.OnScreenTab.cs create mode 100644 Penumbra/UI/Tabs/OnScreenTab.cs diff --git a/Penumbra.Api b/Penumbra.Api index f66e49bd..abdc732b 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f66e49bde2878542de17edf428de61f6c8a42efc +Subproject commit abdc732be8b36061dc35bb72e25f1dc4876d5286 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 82d8af29..5b93a726 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -13,13 +13,13 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames) +internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames) { public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, FileCache, Collection, Skeleton, WithNames, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -166,12 +166,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (node == null) return null; - var mtrlFile = WithNames ? FileCache.ReadMaterial(node.FullPath) : null; + var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null; var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false); if (shpkNode != null) node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); - var shpkFile = WithNames && shpkNode != null ? FileCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; for (var i = 0; i < resource->NumTex; i++) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index b30b5f0e..e0cb1a95 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -13,13 +13,15 @@ public class ResourceTree { public readonly string Name; public readonly nint SourceAddress; + public readonly bool PlayerRelated; public readonly string CollectionName; public readonly List Nodes; - public ResourceTree(string name, nint sourceAddress, string collectionName) + public ResourceTree(string name, nint sourceAddress, bool playerRelated, string collectionName) { Name = name; SourceAddress = sourceAddress; + PlayerRelated = playerRelated; CollectionName = collectionName; Nodes = new List(); } @@ -27,7 +29,7 @@ public class ResourceTree internal unsafe void LoadResources(GlobalResolveContext globalContext) { var character = (Character*)SourceAddress; - var model = (CharacterBase*) character->GameObject.GetDrawObject(); + var model = (CharacterBase*)character->GameObject.GetDrawObject(); var equipment = new ReadOnlySpan(character->EquipSlotData, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); @@ -49,42 +51,54 @@ public class ResourceTree Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); } - if (character->GameObject.GetObjectKind() == (byte) ObjectKind.Pc) + if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) AddHumanResources(globalContext, (HumanExt*)model); - } + } private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human) { - var prependIndex = 0; - - var firstWeapon = (WeaponExt*)human->Human.CharacterBase.DrawObject.Object.ChildObject; - if (firstWeapon != null) + var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject; + if (firstSubObject != null) { - var weapon = firstWeapon; - var weaponIndex = 0; + var subObjectNodes = new List(); + var subObject = firstSubObject; + var subObjectIndex = 0; do { - var weaponContext = globalContext.CreateContext( - slot: EquipSlot.MainHand, - equipment: new CharacterArmor(weapon->Weapon.ModelSetId, (byte)weapon->Weapon.Variant, (byte)weapon->Weapon.ModelUnknown) + var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; + var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; + var subObjectContext = globalContext.CreateContext( + weapon != null ? EquipSlot.MainHand : EquipSlot.Unknown, + weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default ); - var weaponMdlNode = weaponContext.CreateNodeFromRenderModel(*weapon->WeaponRenderModel); - if (weaponMdlNode != null) - Nodes.Insert(prependIndex++, - globalContext.WithNames ? weaponMdlNode.WithName(weaponMdlNode.Name ?? $"Weapon Model #{weaponIndex}") : weaponMdlNode); + for (var i = 0; i < subObject->SlotCount; ++i) + { + var imc = (ResourceHandle*)subObject->IMCArray[i]; + var imcNode = subObjectContext.CreateNodeFromImc(imc); + if (imcNode != null) + subObjectNodes.Add(globalContext.WithNames + ? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}") + : imcNode); - weapon = (WeaponExt*)weapon->Weapon.CharacterBase.DrawObject.Object.NextSiblingObject; - ++weaponIndex; - } while (weapon != null && weapon != firstWeapon); + var mdl = (RenderModel*)subObject->ModelArray[i]; + var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); + if (mdlNode != null) + subObjectNodes.Add(globalContext.WithNames + ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") + : mdlNode); + } + + subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; + ++subObjectIndex; + } while (subObject != null && subObject != firstSubObject); + + Nodes.InsertRange(0, subObjectNodes); } - var context = globalContext.CreateContext( - EquipSlot.Unknown, - default - ); + var context = globalContext.CreateContext(EquipSlot.Unknown, default); - var skeletonNode = context.CreateHumanSkeletonNode((GenderRace) human->Human.RaceSexId); + var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId); if (skeletonNode != null) Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index a90d688e..b177e404 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.GameData; +using Penumbra.GameData.Actors; using Penumbra.Interop.Resolver; using Penumbra.Services; @@ -17,23 +18,24 @@ public class ResourceTreeFactory private readonly CollectionResolver _collectionResolver; private readonly IdentifierService _identifier; private readonly Configuration _config; + private readonly ActorService _actors; public ResourceTreeFactory(DataManager gameData, ObjectTable objects, CollectionResolver resolver, IdentifierService identifier, - Configuration config) + Configuration config, ActorService actors) { _gameData = gameData; _objects = objects; _collectionResolver = resolver; _identifier = identifier; _config = config; + _actors = actors; } public ResourceTree[] FromObjectTable(bool withNames = true) { - var cache = new FileCache(_gameData); + var cache = new TreeBuildCache(_objects, _gameData); - return _objects - .OfType() + return cache.Characters .Select(c => FromCharacter(c, cache, withNames)) .OfType() .ToArray(); @@ -43,7 +45,7 @@ public class ResourceTreeFactory IEnumerable characters, bool withNames = true) { - var cache = new FileCache(_gameData); + var cache = new TreeBuildCache(_objects, _gameData); foreach (var character in characters) { var tree = FromCharacter(character, cache, withNames); @@ -53,11 +55,14 @@ public class ResourceTreeFactory } public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true) - => FromCharacter(character, new FileCache(_gameData), withNames); + => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withNames); - private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, FileCache cache, + private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, bool withNames = true) { + if (!character.IsValid()) + return null; + var gameObjStruct = (GameObject*)character.Address; if (gameObjStruct->GetDrawObject() == null) return null; @@ -66,11 +71,37 @@ public class ResourceTreeFactory if (!collectionResolveData.Valid) return null; - var tree = new ResourceTree(character.Name.ToString(), (nint)gameObjStruct, collectionResolveData.ModCollection.Name); + var (name, related) = GetCharacterName(character, cache); + var tree = new ResourceTree(name, (nint)gameObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->ModelCharaId, - withNames); + ((Character*)gameObjStruct)->ModelCharaId, withNames); tree.LoadResources(globalContext); return tree; } + + private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, + TreeBuildCache cache) + { + var identifier = _actors.AwaitedService.FromObject((GameObject*)character.Address, out var owner, true, false, false); + string name; + bool playerRelated; + switch (identifier.Type) + { + case IdentifierType.Player: + name = identifier.PlayerName.ToString(); + playerRelated = true; + break; + case IdentifierType.Owned when cache.CharactersById.TryGetValue(owner->ObjectID, out var ownerChara): + var ownerName = GetCharacterName(ownerChara, cache); + name = $"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})"; + playerRelated = ownerName.PlayerRelated; + break; + default: + name = $"{character.Name} ({identifier.Kind.ToName()})"; + playerRelated = false; + break; + } + + return (name, playerRelated); + } } diff --git a/Penumbra/Interop/ResourceTree/FileCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs similarity index 70% rename from Penumbra/Interop/ResourceTree/FileCache.cs rename to Penumbra/Interop/ResourceTree/TreeBuildCache.cs index c6c9ac9d..4e432dd4 100644 --- a/Penumbra/Interop/ResourceTree/FileCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,20 +1,32 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Dalamud.Data; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Files; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal class FileCache +internal class TreeBuildCache { private readonly DataManager _dataManager; private readonly Dictionary _materials = new(); private readonly Dictionary _shaderPackages = new(); + public readonly List Characters; + public readonly Dictionary CharactersById; - public FileCache(DataManager dataManager) - => _dataManager = dataManager; + public TreeBuildCache(ObjectTable objects, DataManager dataManager) + { + _dataManager = dataManager; + Characters = objects.Where(c => c is Character ch && ch.IsValid()).Cast().ToList(); + CharactersById = Characters + .Where(c => c.ObjectId != GameObject.InvalidGameObjectId) + .GroupBy(c => c.ObjectId) + .ToDictionary(c => c.Key, c => c.First()); + } /// Try to read a material file from the given path and cache it on success. public MtrlFile? ReadMaterial(FullPath path) diff --git a/Penumbra/Interop/Structs/WeaponExt.cs b/Penumbra/Interop/Structs/WeaponExt.cs deleted file mode 100644 index de7038d7..00000000 --- a/Penumbra/Interop/Structs/WeaponExt.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; - -namespace Penumbra.Interop.Structs; - -[StructLayout( LayoutKind.Explicit )] -public unsafe struct WeaponExt -{ - [FieldOffset( 0x0 )] - public Weapon Weapon; - - [FieldOffset( 0xA8 )] - public RenderModel** WeaponRenderModel; -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 031c7485..2e46314c 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -40,7 +40,7 @@ public class ModFileEditor return num; } - public void RevertFiles(Mod mod, ISubMod option) + public void Revert(Mod mod, ISubMod option) { _files.UpdatePaths(mod, option); Changes = false; diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 00e32edd..08f4a0dd 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -133,6 +133,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 47d53832..313d0f26 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -20,11 +20,13 @@ public class FileEditor where T : class, IWritable private readonly Configuration _config; private readonly FileDialogService _fileDialog; private readonly DataManager _gameData; + private readonly ModEditWindow _owner; - public FileEditor(DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, + public FileEditor(ModEditWindow owner, DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, Func? parseFile) { + _owner = owner; _gameData = gameData; _config = config; _fileDialog = fileDialog; @@ -41,7 +43,10 @@ public class FileEditor where T : class, IWritable _list = _getFiles(); using var tab = ImRaii.TabItem(_tabName); if (!tab) + { + _quickImport = null; return; + } ImGui.NewLine(); DrawFileSelectCombo(); @@ -67,21 +72,27 @@ public class FileEditor where T : class, IWritable private Exception? _currentException; private bool _changed; - private string _defaultPath = string.Empty; - private bool _inInput; - private T? _defaultFile; - private Exception? _defaultException; + private string _defaultPath = string.Empty; + private bool _inInput; + private Utf8GamePath _defaultPathUtf8; + private bool _isDefaultPathUtf8Valid; + private T? _defaultFile; + private Exception? _defaultException; private IReadOnlyList _list = null!; + private ModEditWindow.QuickImportAction? _quickImport; + private void DefaultInput() { - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale }); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight()); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = UiHelpers.ScaleX3 }); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (UiHelpers.ScaleX3 + ImGui.GetFrameHeight())); ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength); _inInput = ImGui.IsItemActive(); if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) { + _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8, true); + _quickImport = null; _fileDialog.Reset(); try { @@ -123,6 +134,21 @@ public class FileEditor where T : class, IWritable } }, _getInitialPath(), false); + _quickImport ??= ModEditWindow.QuickImportAction.Prepare(_owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), new Vector2(ImGui.GetFrameHeight()), $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true)) + { + try + { + UpdateCurrentFile(_quickImport.Execute()); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not add a copy of {_quickImport.GamePath} to {_quickImport.OptionName}:\n{e}"); + } + _quickImport = null; + } + _fileDialog.Draw(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 3d0df39d..55eecdae 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -307,7 +307,7 @@ public partial class ModEditWindow var label = changes ? "Revert Changes" : "Reload Files"; var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0); if (ImGui.Button(label, length)) - _editor.FileEditor.RevertFiles(_editor.Mod!, _editor.Option!); + _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 306293af..d7e23ac3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -6,7 +6,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; -using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs new file mode 100644 index 00000000..29671c7c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using Lumina.Data; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.Interop.ResourceTree; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private ResourceTreeViewer? _quickImportViewer; + private Dictionary? _quickImportWritables; + private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions; + private void DrawQuickImportTab() + { + using var tab = ImRaii.TabItem("Import from Screen"); + if (!tab) + { + _quickImportActions = null; + return; + } + + _quickImportViewer ??= new ResourceTreeViewer(_config, _resourceTreeFactory, "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportWritables ??= new Dictionary(); + _quickImportActions ??= new Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>(); + + _quickImportViewer.Draw(); + } + + private void OnQuickImportRefresh() + { + _quickImportWritables?.Clear(); + _quickImportActions?.Clear(); + } + + private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize) + { + if (!_quickImportWritables!.TryGetValue(resourceNode.FullPath, out var writable)) + { + var path = resourceNode.FullPath.ToPath(); + if (resourceNode.FullPath.IsRooted) + { + writable = new RawFileWritable(path); + } + else + { + var file = _gameData.GetFile(path); + writable = file == null ? null : new RawGameFileWritable(file); + } + _quickImportWritables.Add(resourceNode.FullPath, writable); + } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || writable == null, true)) + { + var fullPathStr = resourceNode.FullPath.FullName; + var ext = resourceNode.PossibleGamePaths.Length == 1 ? Path.GetExtension(resourceNode.GamePath.ToString()) : Path.GetExtension(fullPathStr); + _fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext, (success, name) => + { + if (!success) + return; + + try + { + File.WriteAllBytes(name, writable!.Write()); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}"); + } + }, null, false); + } + ImGui.SameLine(); + if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport)) + { + quickImport = QuickImportAction.Prepare(this, resourceNode.GamePath, writable); + _quickImportActions.Add((resourceNode.GamePath, writable), quickImport); + } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true)) + { + quickImport.Execute(); + _quickImportActions.Remove((resourceNode.GamePath, writable)); + } + } + + private record class RawFileWritable(string Path) : IWritable + { + public bool Valid => true; + + public byte[] Write() + => File.ReadAllBytes(Path); + } + + private record class RawGameFileWritable(FileResource FileResource) : IWritable + { + public bool Valid => true; + + public byte[] Write() + => FileResource.Data; + } + + public class QuickImportAction + { + public const string FallbackOptionName = "the current option"; + + private readonly string _optionName; + private readonly Utf8GamePath _gamePath; + private readonly ModEditor _editor; + private readonly IWritable? _file; + private readonly string? _targetPath; + private readonly int _subDirs; + + public string OptionName => _optionName; + public Utf8GamePath GamePath => _gamePath; + public bool CanExecute => !_gamePath.IsEmpty && _editor.Mod != null && _file != null && _targetPath != null; + + /// + /// Creates a non-executable QuickImportAction. + /// + private QuickImportAction(ModEditor editor, string optionName, Utf8GamePath gamePath) + { + _optionName = optionName; + _gamePath = gamePath; + _editor = editor; + _file = null; + _targetPath = null; + _subDirs = 0; + } + + /// + /// Creates an executable QuickImportAction. + /// + private QuickImportAction(string optionName, Utf8GamePath gamePath, ModEditor editor, IWritable file, string targetPath, int subDirs) + { + _optionName = optionName; + _gamePath = gamePath; + _editor = editor; + _file = file; + _targetPath = targetPath; + _subDirs = subDirs; + } + + public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file) + { + var editor = owner._editor; + if (editor == null) + { + return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); + } + var subMod = editor.Option; + var optionName = subMod!.FullName; + if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) + { + return new QuickImportAction(editor, optionName, gamePath); + } + if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath)) + { + return new QuickImportAction(editor, optionName, gamePath); + } + var mod = owner._mod; + if (mod == null) + { + return new QuickImportAction(editor, optionName, gamePath); + } + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod); + var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; + if (File.Exists(targetPath)) + { + return new QuickImportAction(editor, optionName, gamePath); + } + + return new QuickImportAction(optionName, gamePath, editor, file, targetPath, subDirs); + } + + public FileRegistry Execute() + { + if (!CanExecute) + { + throw new InvalidOperationException(); + } + var directory = Path.GetDirectoryName(_targetPath); + if (directory != null) + { + Directory.CreateDirectory(directory); + } + File.WriteAllBytes(_targetPath!, _file!.Write()); + _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); + var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath); + _editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs); + _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!); + + return fileRegistry; + } + + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod) + { + var path = mod.ModPath; + var subDirs = 0; + if (subMod == mod.Default) + return (path, subDirs); + + var name = subMod.Name; + var fullName = subMod.FullName; + if (fullName.EndsWith(": " + name)) + { + path = Mod.Creator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); + path = Mod.Creator.NewOptionDirectory(path, name); + subDirs = 2; + } + else + { + path = Mod.Creator.NewOptionDirectory(path, fullName); + subDirs = 1; + } + + return (path, subDirs); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 035c9286..d6d33175 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; +using Penumbra.Interop.ResourceTree; using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -24,12 +25,14 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly ResourceTreeFactory _resourceTreeFactory; + private readonly DataManager _gameData; private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; + private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; public void ChangeMod(Mod mod) @@ -136,13 +139,14 @@ public partial class ModEditWindow : Window, IDisposable DrawSwapTab(); DrawMissingFilesTab(); DrawDuplicatesTab(); + DrawQuickImportTab(); DrawMaterialReassignmentTab(); _modelTab.Draw(); _materialTab.Draw(); DrawTextureTab(); _shaderPackageTab.Draw(); - using var tab = ImRaii.TabItem("Item Swap (WIP)"); - if (tab) + using var tab = ImRaii.TabItem("Item Swap (WIP)"); + if (tab) _itemSwapTab.DrawContent(); } @@ -488,19 +492,21 @@ public partial class ModEditWindow : Window, IDisposable } public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, - Configuration config, ModEditor editor) + Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory) : base(WindowBaseLabel) { - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _fileDialog = fileDialog; - _materialTab = new FileEditor(gameData, config, _fileDialog, "Materials", ".mtrl", + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _gameData = gameData; + _resourceTreeFactory = resourceTreeFactory; + _fileDialog = fileDialog; + _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); - _modelTab = new FileEditor(gameData, config, _fileDialog, "Models", ".mdl", + _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); - _shaderPackageTab = new FileEditor(gameData, config, _fileDialog, "Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shader Packages", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null); _center = new CombinedTexture(_left, _right); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs new file mode 100644 index 00000000..1a452bde --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.Interop.ResourceTree; + +namespace Penumbra.UI.AdvancedWindow; + +public class ResourceTreeViewer +{ + private readonly Configuration _config; + private readonly ResourceTreeFactory _treeFactory; + private readonly string _name; + private readonly int _actionCapacity; + private readonly Action _onRefresh; + private readonly Action _drawActions; + private readonly HashSet _unfolded; + private ResourceTree[]? _trees; + + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, string name, int actionCapacity, Action onRefresh, + Action drawActions) + { + _config = config; + _treeFactory = treeFactory; + _name = name; + _actionCapacity = actionCapacity; + _onRefresh = onRefresh; + _drawActions = drawActions; + _unfolded = new HashSet(); + _trees = null; + } + + public void Draw() + { + if (ImGui.Button("Refresh Character List")) + { + try + { + _trees = _treeFactory.FromObjectTable(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}"); + _trees = Array.Empty(); + } + + _unfolded.Clear(); + _onRefresh(); + } + + try + { + _trees ??= _treeFactory.FromObjectTable(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}"); + _trees ??= Array.Empty(); + } + + var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text); + var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green + + foreach (var (tree, index) in _trees.WithIndex()) + { + using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer)) + { + if (!ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0)) + continue; + } + + using var id = ImRaii.PushId(index); + + ImGui.Text($"Collection: {tree.CollectionName}"); + + using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + continue; + + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + if (_actionCapacity > 0) + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, + (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); + ImGui.TableHeadersRow(); + + DrawNodes(tree.Nodes, 0); + } + } + + private void DrawNodes(IEnumerable resourceNodes, int level) + { + var debugMode = _config.DebugMode; + var frameHeight = ImGui.GetFrameHeight(); + var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; + foreach (var (resourceNode, index) in resourceNodes.WithIndex()) + { + if (resourceNode.Internal && !debugMode) + continue; + + using var id = ImRaii.PushId(index); + ImGui.TableNextColumn(); + var unfolded = _unfolded!.Contains(resourceNode); + using (var indent = ImRaii.PushIndent(level)) + { + ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); + if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) + { + if (unfolded) + _unfolded.Remove(resourceNode); + else + _unfolded.Add(resourceNode); + unfolded = !unfolded; + } + + if (debugMode) + ImGuiUtil.HoverTooltip( + $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress:X16}"); + } + + ImGui.TableNextColumn(); + var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; + ImGui.Selectable(resourceNode.PossibleGamePaths.Length switch + { + 0 => "(none)", + 1 => resourceNode.GamePath.ToString(), + _ => "(multiple)", + }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + if (hasGamePaths) + { + var allPaths = string.Join('\n', resourceNode.PossibleGamePaths); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(allPaths); + ImGuiUtil.HoverTooltip($"{allPaths}\n\nClick to copy to clipboard."); + } + + ImGui.TableNextColumn(); + if (resourceNode.FullPath.FullName.Length > 0) + { + ImGui.Selectable(resourceNode.FullPath.ToString(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(resourceNode.FullPath.ToString()); + ImGuiUtil.HoverTooltip($"{resourceNode.FullPath}\n\nClick to copy to clipboard."); + } + else + { + ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, + new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); + } + + if (_actionCapacity > 0) + { + ImGui.TableNextColumn(); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); + _drawActions(resourceNode, new Vector2(frameHeight)); + } + + if (unfolded) + DrawNodes(resourceNode.Children, level + 1); + } + } +} diff --git a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs deleted file mode 100644 index c3f394c8..00000000 --- a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using ImGuiNET; -using Lumina.Data; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.Interop; -using Penumbra.Mods; -using Penumbra.String.Classes; - -namespace Penumbra.UI.Classes; - -public partial class ModEditWindow -{ - private ResourceTreeViewer? _quickImportViewer; - private Dictionary? _quickImportWritables; - private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions; - - private readonly FileDialogManager _quickImportFileDialog = ConfigWindow.SetupFileManager(); - - private void DrawQuickImportTab() - { - using var tab = ImRaii.TabItem( "Import from Screen" ); - if( !tab ) - { - _quickImportActions = null; - return; - } - - _quickImportViewer ??= new( "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions ); - _quickImportWritables ??= new(); - _quickImportActions ??= new(); - - _quickImportViewer.Draw(); - - _quickImportFileDialog.Draw(); - } - - private void OnQuickImportRefresh() - { - _quickImportWritables?.Clear(); - _quickImportActions?.Clear(); - } - - private void DrawQuickImportActions( ResourceTree.Node resourceNode, Vector2 buttonSize ) - { - if( !_quickImportWritables!.TryGetValue( resourceNode.FullPath, out var writable ) ) - { - var path = resourceNode.FullPath.ToPath(); - if( resourceNode.FullPath.IsRooted ) - { - writable = new RawFileWritable( path ); - } - else - { - var file = Dalamud.GameData.GetFile( path ); - writable = ( file == null ) ? null : new RawGameFileWritable( file ); - } - _quickImportWritables.Add( resourceNode.FullPath, writable ); - } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || writable == null, true ) ) - { - var fullPathStr = resourceNode.FullPath.FullName; - var ext = ( resourceNode.PossibleGamePaths.Length == 1 ) ? Path.GetExtension( resourceNode.GamePath.ToString() ) : Path.GetExtension( fullPathStr ); - _quickImportFileDialog.SaveFileDialog( $"Export {Path.GetFileName( fullPathStr )} to...", ext, Path.GetFileNameWithoutExtension( fullPathStr ), ext, ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - File.WriteAllBytes( name, writable!.Write() ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {fullPathStr}:\n{e}" ); - } - } ); - } - ImGui.SameLine(); - if( !_quickImportActions!.TryGetValue( (resourceNode.GamePath, writable), out var quickImport ) ) - { - quickImport = QuickImportAction.Prepare( this, resourceNode.GamePath, writable ); - _quickImportActions.Add( (resourceNode.GamePath, writable), quickImport ); - } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true ) ) - { - quickImport.Execute(); - _quickImportActions.Remove( (resourceNode.GamePath, writable) ); - } - } - - private record class RawFileWritable( string Path ) : IWritable - { - public bool Valid => true; - - public byte[] Write() - => File.ReadAllBytes( Path ); - } - - private record class RawGameFileWritable( FileResource FileResource ) : IWritable - { - public bool Valid => true; - - public byte[] Write() - => FileResource.Data; - } - - private class QuickImportAction - { - public const string FallbackOptionName = "the current option"; - - private readonly string _optionName; - private readonly Utf8GamePath _gamePath; - private readonly Mod.Editor? _editor; - private readonly IWritable? _file; - private readonly string? _targetPath; - private readonly int _subDirs; - - public string OptionName => _optionName; - public Utf8GamePath GamePath => _gamePath; - public bool CanExecute => !_gamePath.IsEmpty && _editor != null && _file != null && _targetPath != null; - - /// - /// Creates a non-executable QuickImportAction. - /// - private QuickImportAction( string optionName, Utf8GamePath gamePath ) - { - _optionName = optionName; - _gamePath = gamePath; - _editor = null; - _file = null; - _targetPath = null; - _subDirs = 0; - } - - /// - /// Creates an executable QuickImportAction. - /// - private QuickImportAction( string optionName, Utf8GamePath gamePath, Mod.Editor editor, IWritable file, string targetPath, int subDirs ) - { - _optionName = optionName; - _gamePath = gamePath; - _editor = editor; - _file = file; - _targetPath = targetPath; - _subDirs = subDirs; - } - - public static QuickImportAction Prepare( ModEditWindow owner, Utf8GamePath gamePath, IWritable? file ) - { - var editor = owner._editor; - if( editor == null ) - { - return new QuickImportAction( FallbackOptionName, gamePath ); - } - var subMod = editor.CurrentOption; - var optionName = subMod.FullName; - if( gamePath.IsEmpty || file == null || editor.FileChanges ) - { - return new QuickImportAction( optionName, gamePath ); - } - if( subMod.Files.ContainsKey( gamePath ) || subMod.FileSwaps.ContainsKey( gamePath ) ) - { - return new QuickImportAction( optionName, gamePath ); - } - var mod = owner._mod; - if( mod == null ) - { - return new QuickImportAction( optionName, gamePath ); - } - var ( preferredPath, subDirs ) = GetPreferredPath( mod, subMod ); - var targetPath = new FullPath( Path.Combine( preferredPath.FullName, gamePath.ToString() ) ).FullName; - if( File.Exists( targetPath ) ) - { - return new QuickImportAction( optionName, gamePath ); - } - - return new QuickImportAction( optionName, gamePath, editor, file, targetPath, subDirs ); - } - - public Mod.Editor.FileRegistry Execute() - { - if( !CanExecute ) - { - throw new InvalidOperationException(); - } - var directory = Path.GetDirectoryName( _targetPath ); - if( directory != null ) - { - Directory.CreateDirectory( directory ); - } - File.WriteAllBytes( _targetPath!, _file!.Write() ); - _editor!.RevertFiles(); - var fileRegistry = _editor.AvailableFiles.First( file => file.File.FullName == _targetPath ); - _editor.AddPathsToSelected( new Mod.Editor.FileRegistry[] { fileRegistry }, _subDirs ); - _editor.ApplyFiles(); - - return fileRegistry; - } - - private static (DirectoryInfo, int) GetPreferredPath( Mod mod, ISubMod subMod ) - { - var path = mod.ModPath; - var subDirs = 0; - if( subMod != mod.Default ) - { - var name = subMod.Name; - var fullName = subMod.FullName; - if( fullName.EndsWith( ": " + name ) ) - { - path = Mod.Creator.NewOptionDirectory( path, fullName[..^( name.Length + 2 )] ); - path = Mod.Creator.NewOptionDirectory( path, name ); - subDirs = 2; - } - else - { - path = Mod.Creator.NewOptionDirectory( path, fullName ); - subDirs = 1; - } - } - - return (path, subDirs); - } - } -} diff --git a/Penumbra/UI/Classes/ResourceTreeViewer.cs b/Penumbra/UI/Classes/ResourceTreeViewer.cs deleted file mode 100644 index 2615db42..00000000 --- a/Penumbra/UI/Classes/ResourceTreeViewer.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui.Raii; -using OtterGui; -using Penumbra.Interop; - -namespace Penumbra.UI.Classes; - -public class ResourceTreeViewer -{ - private readonly string _name; - private readonly int _actionCapacity; - private readonly Action _onRefresh; - private readonly Action _drawActions; - private readonly HashSet _unfolded; - private ResourceTree[]? _trees; - - public ResourceTreeViewer( string name, int actionCapacity, Action onRefresh, Action drawActions ) - { - _name = name; - _actionCapacity = actionCapacity; - _onRefresh = onRefresh; - _drawActions = drawActions; - _unfolded = new(); - _trees = null; - } - - public void Draw() - { - if( ImGui.Button( "Refresh Character List" ) ) - { - try - { - _trees = ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" ); - _trees = Array.Empty(); - } - _unfolded.Clear(); - _onRefresh(); - } - - try - { - _trees ??= ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" ); - _trees ??= Array.Empty(); - } - - var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green - - foreach( var (tree, index) in _trees.WithIndex() ) - { - using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) ) - { - if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) - { - continue; - } - } - using var id = ImRaii.PushId( index ); - - ImGui.Text( $"Collection: {tree.CollectionName}" ); - - using var table = ImRaii.Table( "##ResourceTree", ( _actionCapacity > 0 ) ? 4 : 3, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - continue; - } - - ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthStretch, 0.2f ); - ImGui.TableSetupColumn( "Game Path" , ImGuiTableColumnFlags.WidthStretch, 0.3f ); - ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f ); - if( _actionCapacity > 0 ) - { - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight() ); - } - ImGui.TableHeadersRow(); - - DrawNodes( tree.Nodes, 0 ); - } - } - - private void DrawNodes( IEnumerable resourceNodes, int level ) - { - var debugMode = Penumbra.Config.DebugMode; - var frameHeight = ImGui.GetFrameHeight(); - var cellHeight = ( _actionCapacity > 0 ) ? frameHeight : 0.0f; - foreach( var (resourceNode, index) in resourceNodes.WithIndex() ) - { - if( resourceNode.Internal && !debugMode ) - { - continue; - } - using var id = ImRaii.PushId( index ); - ImGui.TableNextColumn(); - var unfolded = _unfolded!.Contains( resourceNode ); - using( var indent = ImRaii.PushIndent( level ) ) - { - ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name ); - if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 ) - { - if( unfolded ) - { - _unfolded.Remove( resourceNode ); - } - else - { - _unfolded.Add( resourceNode ); - } - unfolded = !unfolded; - } - if( debugMode ) - { - ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString( "X" + nint.Size * 2 )}" ); - } - } - ImGui.TableNextColumn(); - var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; - ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch - { - 0 => "(none)", - 1 => resourceNode.GamePath.ToString(), - _ => "(multiple)", - }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); - if( hasGamePaths ) - { - var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( allPaths ); - } - ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." ); - } - ImGui.TableNextColumn(); - if( resourceNode.FullPath.FullName.Length > 0 ) - { - ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( resourceNode.FullPath.ToString() ); - } - ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." ); - } - else - { - ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); - ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." ); - } - if( _actionCapacity > 0 ) - { - ImGui.TableNextColumn(); - using( var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ) ) - { - _drawActions( resourceNode, new Vector2( frameHeight ) ); - } - } - if( unfolded ) - { - DrawNodes( resourceNode.Children, level + 1 ); - } - } - } -} diff --git a/Penumbra/UI/ConfigWindow.OnScreenTab.cs b/Penumbra/UI/ConfigWindow.OnScreenTab.cs deleted file mode 100644 index 786fbca8..00000000 --- a/Penumbra/UI/ConfigWindow.OnScreenTab.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using OtterGui.Widgets; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class OnScreenTab : ITab - { - private ResourceTreeViewer? _viewer; - - public ReadOnlySpan Label - => "On-Screen"u8; - - public void DrawContent() - { - _viewer ??= new( "On-Screen tab", 0, delegate { }, delegate { } ); - - _viewer.Draw(); - } - } -} diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 0f91f3dc..a69b6cd7 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -7,14 +7,15 @@ namespace Penumbra.UI.Tabs; public class ConfigTabBar { - public readonly SettingsTab Settings; - public readonly ModsTab Mods; - public readonly CollectionsTab Collections; + public readonly SettingsTab Settings; + public readonly ModsTab Mods; + public readonly CollectionsTab Collections; public readonly ChangedItemsTab ChangedItems; - public readonly EffectiveTab Effective; - public readonly DebugTab Debug; - public readonly ResourceTab Resource; + public readonly EffectiveTab Effective; + public readonly DebugTab Debug; + public readonly ResourceTab Resource; public readonly ResourceWatcher Watcher; + public readonly OnScreenTab OnScreenTab; public readonly ITab[] Tabs; @@ -22,16 +23,17 @@ public class ConfigTabBar public TabType SelectTab = TabType.None; public ConfigTabBar(SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective, - DebugTab debug, ResourceTab resource, ResourceWatcher watcher) + DebugTab debug, ResourceTab resource, ResourceWatcher watcher, OnScreenTab onScreenTab) { - Settings = settings; - Mods = mods; - Collections = collections; + Settings = settings; + Mods = mods; + Collections = collections; ChangedItems = changedItems; - Effective = effective; - Debug = debug; - Resource = resource; - Watcher = watcher; + Effective = effective; + Debug = debug; + Resource = resource; + Watcher = watcher; + OnScreenTab = onScreenTab; Tabs = new ITab[] { Settings, @@ -39,6 +41,7 @@ public class ConfigTabBar Collections, ChangedItems, Effective, + OnScreenTab, Debug, Resource, Watcher, @@ -54,14 +57,15 @@ public class ConfigTabBar private ReadOnlySpan ToLabel(TabType type) => type switch { - TabType.Settings => Settings.Label, - TabType.Mods => Mods.Label, - TabType.Collections => Collections.Label, - TabType.ChangedItems => ChangedItems.Label, + TabType.Settings => Settings.Label, + TabType.Mods => Mods.Label, + TabType.Collections => Collections.Label, + TabType.ChangedItems => ChangedItems.Label, TabType.EffectiveChanges => Effective.Label, - TabType.ResourceWatcher => Watcher.Label, - TabType.Debug => Debug.Label, - TabType.ResourceManager => Resource.Label, - _ => ReadOnlySpan.Empty, + TabType.OnScreen => OnScreenTab.Label, + TabType.ResourceWatcher => Watcher.Label, + TabType.Debug => Debug.Label, + TabType.ResourceManager => Resource.Label, + _ => ReadOnlySpan.Empty, }; } diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs new file mode 100644 index 00000000..5fe9b8cc --- /dev/null +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -0,0 +1,28 @@ +using System; +using OtterGui.Widgets; +using Penumbra.Interop.ResourceTree; +using Penumbra.UI.AdvancedWindow; + +namespace Penumbra.UI.Tabs; + +public class OnScreenTab : ITab +{ + private readonly Configuration _config; + private readonly ResourceTreeFactory _treeFactory; + private ResourceTreeViewer? _viewer; + + public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) + { + _config = config; + _treeFactory = treeFactory; + } + + public ReadOnlySpan Label + => "On-Screen"u8; + + public void DrawContent() + { + _viewer ??= new ResourceTreeViewer(_config, _treeFactory, "On-Screen tab", 0, delegate { }, delegate { }); + _viewer.Draw(); + } +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 378aebf0..f2f0a8b6 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -3,7 +3,6 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Penumbra.UI; -using Penumbra.UI.Classes; using Penumbra.UI.AdvancedWindow; namespace Penumbra;