diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 25340e84..b6e50c80 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -11,8 +11,10 @@ public partial class ModManager public event ModPathChangeDelegate ModPathChanged; - // Rename/Move a mod directory. - // Updates all collection settings and sort order settings. + /// + /// Rename/Move a mod directory. + /// Updates all collection settings and sort order settings. + /// public void MoveModDirectory(int idx, string newName) { var mod = this[idx]; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 35f8d900..575954d5 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -1,11 +1,10 @@ -using Penumbra.UI.Classes; using System; using System.Collections.Concurrent; using System.IO; using System.Threading.Tasks; namespace Penumbra.Mods; - + public sealed partial class ModManager { public DirectoryInfo BasePath { get; private set; } = null!; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 974dd837..8e96b3e2 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -12,19 +12,15 @@ using Penumbra.Util; namespace Penumbra.Mods; - - public class ModOptionEditor { private readonly CommunicatorService _communicator; - private readonly FilenameService _filenames; private readonly SaveService _saveService; - public ModOptionEditor(CommunicatorService communicator, SaveService saveService, FilenameService filenames) + public ModOptionEditor(CommunicatorService communicator, SaveService saveService) { _communicator = communicator; _saveService = saveService; - _filenames = filenames; } /// Change the type of a group given by mod and index to type, if possible. @@ -209,7 +205,7 @@ public class ModOptionEditor /// Add a new empty option of the given name for the given group. public void AddOption(Mod mod, int groupIdx, string newName) { - var group = mod._groups[groupIdx]; + var group = mod._groups[groupIdx]; var subMod = new SubMod(mod) { Name = newName }; subMod.SetPosition(groupIdx, group.Count); switch (group) @@ -319,7 +315,7 @@ public class ModOptionEditor /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); + var subMod = GetSubMod(mod, groupIdx, optionIdx); var oldCount = subMod.FileData.Count; subMod.FileData.AddFrom(additions); if (oldCount != subMod.FileData.Count) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index fae1f0a0..2d4415d9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -12,34 +12,31 @@ 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 readonly ResourceTreeViewer _quickImportViewer; + private readonly Dictionary _quickImportWritables = new(); + private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); + private void DrawQuickImportTab() { using var tab = ImRaii.TabItem("Import from Screen"); if (!tab) { - _quickImportActions = null; + _quickImportActions.Clear(); 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(); + _quickImportWritables.Clear(); + _quickImportActions.Clear(); } private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize) @@ -56,34 +53,43 @@ public partial class ModEditWindow 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)) + + 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; + 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); + 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)) + + 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)); @@ -92,7 +98,8 @@ public partial class ModEditWindow private record class RawFileWritable(string Path) : IWritable { - public bool Valid => true; + public bool Valid + => true; public byte[] Write() => File.ReadAllBytes(Path); @@ -100,7 +107,8 @@ public partial class ModEditWindow private record class RawGameFileWritable(FileResource FileResource) : IWritable { - public bool Valid => true; + public bool Valid + => true; public byte[] Write() => FileResource.Data; @@ -110,16 +118,21 @@ public partial class ModEditWindow { public const string FallbackOptionName = "the current option"; - private readonly string _optionName; + private readonly string _optionName; private readonly Utf8GamePath _gamePath; - private readonly ModEditor _editor; - private readonly IWritable? _file; - private readonly string? _targetPath; - private readonly int _subDirs; + 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; + 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. @@ -127,11 +140,11 @@ public partial class ModEditWindow private QuickImportAction(ModEditor editor, string optionName, Utf8GamePath gamePath) { _optionName = optionName; - _gamePath = gamePath; - _editor = editor; - _file = null; + _gamePath = gamePath; + _editor = editor; + _file = null; _targetPath = null; - _subDirs = 0; + _subDirs = 0; } /// @@ -140,41 +153,35 @@ public partial class ModEditWindow private QuickImportAction(string optionName, Utf8GamePath gamePath, ModEditor editor, IWritable file, string targetPath, int subDirs) { _optionName = optionName; - _gamePath = gamePath; - _editor = editor; - _file = file; + _gamePath = gamePath; + _editor = editor; + _file = file; _targetPath = targetPath; - _subDirs = subDirs; + _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 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); } @@ -182,26 +189,26 @@ public partial class ModEditWindow 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!, (SubMod) _editor.Option!); + _editor.FileEditor.AddPathsToSelected(_editor.Option!, new[] + { + fileRegistry, + }, _subDirs); + _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); return fileRegistry; } private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod) { - var path = mod.ModPath; + var path = mod.ModPath; var subDirs = 0; if (subMod == mod.Default) return (path, subDirs); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d7e7efa1..bcc8022e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -17,7 +17,6 @@ using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; -using static Penumbra.Mods.Mod; namespace Penumbra.UI.AdvancedWindow; @@ -25,11 +24,10 @@ 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 ResourceTreeFactory _resourceTreeFactory; - private readonly DataManager _gameData; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly DataManager _gameData; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -56,7 +54,7 @@ public partial class ModEditWindow : Window, IDisposable } public void ChangeOption(SubMod? subMod) - => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.GroupIdx ?? 0); + => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.OptionIdx ?? 0); public void UpdateModels() { @@ -495,12 +493,11 @@ public partial class ModEditWindow : Window, IDisposable Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory) : base(WindowBaseLabel) { - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _gameData = gameData; - _resourceTreeFactory = resourceTreeFactory; - _fileDialog = fileDialog; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _gameData = gameData; + _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))); @@ -508,7 +505,8 @@ public partial class ModEditWindow : Window, IDisposable () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); _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); + _center = new CombinedTexture(_left, _right); + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); } public void Dispose() diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 1a452bde..4d8c77a7 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,98 +1,106 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Threading.Tasks; using Dalamud.Interface; using ImGuiNET; using OtterGui.Raii; using OtterGui; using Penumbra.Interop.ResourceTree; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { - private readonly Configuration _config; + 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, + private Task? _task; + + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, 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(); - } + if (ImGui.Button("Refresh Character List") || _task == null) + _task = RefreshCharacterList(); - _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(); - } + using var child = ImRaii.Child("##Data"); + if (!child) + return; var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text); var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green - - foreach (var (tree, index) in _trees.WithIndex()) + if (!_task.IsCompleted) { - using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer)) + ImGui.NewLine(); + ImGui.TextUnformatted("Calculating character list..."); + } + else if (_task.Exception != null) + { + ImGui.NewLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGui.TextUnformatted($"Error during calculation of character list:\n\n{_task.Exception}"); + } + else if (_task.IsCompletedSuccessfully) + { + foreach (var (tree, index) in _task.Result.WithIndex()) { - if (!ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0)) + 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); } - - 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 Task RefreshCharacterList() + => Task.Run(() => + { + try + { + return _treeFactory.FromObjectTable(); + } + finally + { + _unfolded.Clear(); + _onRefresh(); + } + }); + private void DrawNodes(IEnumerable resourceNodes, int level) { var debugMode = _config.DebugMode; @@ -105,7 +113,7 @@ public class ResourceTreeViewer using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); - var unfolded = _unfolded!.Contains(resourceNode); + var unfolded = _unfolded.Contains(resourceNode); using (var indent = ImRaii.PushIndent(level)) { ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 5fe9b8cc..0ebc7dbd 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -7,22 +7,18 @@ namespace Penumbra.UI.Tabs; public class OnScreenTab : ITab { - private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; - private ResourceTreeViewer? _viewer; + private readonly Configuration _config; + private ResourceTreeViewer _viewer; public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) { - _config = config; - _treeFactory = treeFactory; + _config = config; + _viewer = new ResourceTreeViewer(_config, treeFactory, 0, delegate { }, delegate { }); } public ReadOnlySpan Label => "On-Screen"u8; public void DrawContent() - { - _viewer ??= new ResourceTreeViewer(_config, _treeFactory, "On-Screen tab", 0, delegate { }, delegate { }); - _viewer.Draw(); - } + => _viewer.Draw(); }