diff --git a/Luna b/Luna index 06094555..2487453c 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit 06094555dc93eb302d7e823a84edab5926450db9 +Subproject commit 2487453cfa4e95b9dbc661ed6e7298655df3b055 diff --git a/Penumbra/ChangedItemMode.cs b/Penumbra/ChangedItemMode.cs index 471b3a77..2b666bdf 100644 --- a/Penumbra/ChangedItemMode.cs +++ b/Penumbra/ChangedItemMode.cs @@ -8,13 +8,11 @@ namespace Penumbra; public enum ChangedItemMode { [Name("Grouped (Collapsed)")] - [Tooltip( - "Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item.")] + [Tooltip("Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item.")] GroupedCollapsed, [Name("Grouped (Expanded)")] - [Tooltip( - "Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item.")] + [Tooltip("Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item.")] GroupedExpanded, [Name("Alphabetical")] diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 51ec8a25..7bdf86c7 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -1,4 +1,5 @@ using Penumbra.Mods.Manager; +using Penumbra.Util; namespace Penumbra.Mods.Editor; @@ -70,7 +71,7 @@ public class ModBackup } /// Create a backup zip without blocking the main thread. - public async void CreateAsync() + public async Task CreateAsync() { if (CreatingBackup) return; @@ -86,7 +87,7 @@ public class ModBackup try { Delete(); - ZipFile.CreateFromDirectory(_mod.ModPath.FullName, Name, CompressionLevel.Optimal, false); + ArchiveUtility.CreateFromDirectory(_mod.ModPath.FullName, Name); Penumbra.Log.Debug($"Created export file {Name} from {_mod.ModPath.FullName}."); } catch (Exception e) @@ -126,7 +127,8 @@ public class ModBackup Penumbra.Log.Debug($"Deleted mod folder {_mod.ModPath.FullName}."); } - ZipFile.ExtractToDirectory(Name, _mod.ModPath.FullName); + + ArchiveUtility.ExtractToDirectory(Name, _mod.ModPath.FullName); Penumbra.Log.Debug($"Extracted exported file {Name} to {_mod.ModPath.FullName}."); modManager.ReloadMod(_mod); } diff --git a/Penumbra/Mods/Manager/ModConfigUpdater.cs b/Penumbra/Mods/Manager/ModConfigUpdater.cs index 69aecc99..a86199dc 100644 --- a/Penumbra/Mods/Manager/ModConfigUpdater.cs +++ b/Penumbra/Mods/Manager/ModConfigUpdater.cs @@ -27,7 +27,7 @@ public class ModConfigUpdater : IDisposable, IRequiredService public IEnumerable<(Mod, (string Plugin, string Notes)[])> ListUnusedMods(TimeSpan age) { - var cutoff = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - (int)age.TotalMilliseconds; + var cutoff = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - (long)age.TotalMilliseconds; var noteDictionary = new Dictionary(); foreach (var mod in _mods) { @@ -44,8 +44,7 @@ public class ModConfigUpdater : IDisposable, IRequiredService continue; // Check whether other plugins mark this mod as in use. - noteDictionary.Clear(); - ModUsageQueried?.Invoke(mod.Name, mod.Identifier, noteDictionary); + QueryUsage(mod, noteDictionary); if (noteDictionary.Values.Any(n => n.InUse)) continue; @@ -57,6 +56,19 @@ public class ModConfigUpdater : IDisposable, IRequiredService } } + public Dictionary QueryUsage(Mod mod) + { + var noteDictionary = new Dictionary(); + ModUsageQueried?.Invoke(mod.Name, mod.Identifier, noteDictionary); + return noteDictionary; + } + + public void QueryUsage(Mod mod, Dictionary noteDictionary) + { + noteDictionary.Clear(); + ModUsageQueried?.Invoke(mod.Name, mod.Identifier, noteDictionary); + } + private void OnModSettingChanged(in ModSettingChanged.Arguments arguments) { if (arguments.Inherited) diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index 3fd3d97f..d61e4eb5 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -1,91 +1,97 @@ -using Penumbra.Communication; -using Penumbra.Mods.Editor; -using Penumbra.Services; - -namespace Penumbra.Mods.Manager; - -public class ModExportManager : IDisposable, Luna.IService -{ - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly ModManager _modManager; - - private DirectoryInfo? _exportDirectory; - - public DirectoryInfo ExportDirectory - => _exportDirectory ?? _modManager.BasePath; - - public ModExportManager(Configuration config, CommunicatorService communicator, ModManager modManager) - { - _config = config; - _communicator = communicator; - _modManager = modManager; - UpdateExportDirectory(_config.ExportDirectory, false); - _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModExportManager); - } - - /// - public void UpdateExportDirectory(string newDirectory) - => UpdateExportDirectory(newDirectory, true); - - /// - /// Update the export directory to a new directory. Can also reset it to null with empty input. - /// If the directory is changed, all existing backups will be moved to the new one. - /// - /// The new directory name. - /// Can be used to stop saving for the initial setting - private void UpdateExportDirectory(string newDirectory, bool change) - { - if (newDirectory.Length == 0) - { - if (_exportDirectory == null) - return; - - _exportDirectory = null; - _config.ExportDirectory = string.Empty; - _config.Save(); - return; - } - - var dir = new DirectoryInfo(newDirectory); - if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase)) - return; - - if (!dir.Exists) - try - { - Directory.CreateDirectory(dir.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not create Export Directory:\n{e}"); - return; - } - - if (change) - foreach (var mod in _modManager) - new ModBackup(this, mod).Move(dir.FullName); - - _exportDirectory = dir; - - if (!change) - return; - - _config.ExportDirectory = dir.FullName; - _config.Save(); - } - - public void Dispose() - => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); - - /// Automatically migrate the backup file to the new name if any exists. - private void OnModPathChange(in ModPathChanged.Arguments arguments) - { - if (arguments.Type is not ModPathChangeType.Moved || arguments.OldDirectory is null || arguments.NewDirectory is null) - return; - - arguments.Mod.ModPath = arguments.OldDirectory; - new ModBackup(this, arguments.Mod).Move(null, arguments.NewDirectory.Name); - arguments.Mod.ModPath = arguments.NewDirectory; - } -} +using Penumbra.Communication; +using Penumbra.Mods.Editor; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public class ModExportManager : IDisposable, Luna.IService +{ + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly ModManager _modManager; + + private DirectoryInfo? _exportDirectory; + + public DirectoryInfo ExportDirectory + => _exportDirectory ?? _modManager.BasePath; + + public ModExportManager(Configuration config, CommunicatorService communicator, ModManager modManager) + { + _config = config; + _communicator = communicator; + _modManager = modManager; + UpdateExportDirectory(_config.ExportDirectory, false); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModExportManager); + } + + /// + public void UpdateExportDirectory(string newDirectory) + => UpdateExportDirectory(newDirectory, true); + + public Task CreateAsync(Mod mod) + { + var backup = new ModBackup(this, mod); + return backup.CreateAsync(); + } + + /// + /// Update the export directory to a new directory. Can also reset it to null with empty input. + /// If the directory is changed, all existing backups will be moved to the new one. + /// + /// The new directory name. + /// Can be used to stop saving for the initial setting + private void UpdateExportDirectory(string newDirectory, bool change) + { + if (newDirectory.Length == 0) + { + if (_exportDirectory == null) + return; + + _exportDirectory = null; + _config.ExportDirectory = string.Empty; + _config.Save(); + return; + } + + var dir = new DirectoryInfo(newDirectory); + if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase)) + return; + + if (!dir.Exists) + try + { + Directory.CreateDirectory(dir.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create Export Directory:\n{e}"); + return; + } + + if (change) + foreach (var mod in _modManager) + new ModBackup(this, mod).Move(dir.FullName); + + _exportDirectory = dir; + + if (!change) + return; + + _config.ExportDirectory = dir.FullName; + _config.Save(); + } + + public void Dispose() + => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + + /// Automatically migrate the backup file to the new name if any exists. + private void OnModPathChange(in ModPathChanged.Arguments arguments) + { + if (arguments.Type is not ModPathChangeType.Moved || arguments.OldDirectory is null || arguments.NewDirectory is null) + return; + + arguments.Mod.ModPath = arguments.OldDirectory; + new ModBackup(this, arguments.Mod).Move(null, arguments.NewDirectory.Name); + arguments.Mod.ModPath = arguments.NewDirectory; + } +} diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f9e6a408..4de5b77a 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -52,7 +52,7 @@ - + diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 74401065..4e4f6a07 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -17,6 +17,7 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Services; @@ -135,7 +136,7 @@ public class PcpService : IApiService, IDisposable } // Move to folder. - if (arguments.Mod.Node is {} node) + if (arguments.Mod.Node is { } node) { try { @@ -204,7 +205,7 @@ public class PcpService : IApiService, IDisposable path = directory.FullName + extension; else if (Path.GetExtension(path.AsSpan()).IsEmpty) path += extension; - ZipFile.CreateFromDirectory(directory.FullName, path, CompressionLevel.Optimal, false); + ArchiveUtility.CreateFromDirectory(directory.FullName, path); directory.Delete(true); return path; } diff --git a/Penumbra/UI/ManagementTab/DuplicateModsTab.cs b/Penumbra/UI/ManagementTab/DuplicateModsTab.cs new file mode 100644 index 00000000..1e77f130 --- /dev/null +++ b/Penumbra/UI/ManagementTab/DuplicateModsTab.cs @@ -0,0 +1,158 @@ +using ImSharp; +using Luna; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI.ManagementTab; + +public sealed class DuplicateModsTab(ModConfigUpdater configUpdater, ModManager mods, CollectionStorage collections, Configuration config) + : ITab +{ + public ReadOnlySpan Label + => "Duplicate Mods"u8; + + public ManagementTabType Identifier + => ManagementTabType.DuplicateMods; + + public void PostTabButton() + { + Im.Tooltip.OnHover( + "This tab shows mods with identical names and some additional data to discern which to keep if they are indeed identical."u8); + } + + public void DrawContent() + { + using var child = Im.Child.Begin("c"u8); + if (!child) + return; + + using var table = Im.Table.Begin("duplicates"u8, 7, TableFlags.RowBackground | TableFlags.ScrollY); + if (!table) + return; + + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(configUpdater, mods, collections)); + + table.SetupScrollFreeze(0, 1); + table.SetupColumn(""u8, TableColumnFlags.WidthFixed, Im.Style.FrameHeight * 2 + Im.Style.ItemInnerSpacing.X); + table.SetupColumn("Mod Name"u8, TableColumnFlags.WidthStretch, 0.25f); + table.SetupColumn("Mod Directory"u8, TableColumnFlags.WidthStretch, 0.25f); + table.SetupColumn("Active"u8, TableColumnFlags.WidthFixed, cache.Active.Size.X); + table.SetupColumn("Import Date"u8, TableColumnFlags.WidthFixed, cache.Date.Size.X); + table.SetupColumn("Path"u8, TableColumnFlags.WidthStretch, 0.5f); + table.SetupColumn("Notes"u8, TableColumnFlags.WidthFixed, cache.Notes.Size.X + Im.Style.FrameHeight + Im.Style.ItemInnerSpacing.X); + table.HeaderRow(); + + var lastDrawnName = StringU8.Empty; + var disabled = !config.DeleteModModifier.IsActive(); + using var clipper = new Im.ListClipper(cache.Items.Count, 0); + foreach (var (index, item) in cache.Items.Index()) + { + using var id = Im.Id.Push(index); + table.NextColumn(); + if (ImEx.Icon.Button(LunaStyle.DeleteIcon, "Delete this mod. This is NOT revertible."u8, disabled)) + { + mods.DeleteMod(item.Mod); + cache.Dirty |= IManagedCache.DirtyFlags.Custom; + } + + if (disabled) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteModModifier} to delete this mod."); + Im.Line.SameInner(); + if (ImEx.Icon.Button(LunaStyle.FolderIcon, "Open this mod in the file explorer of your choice."u8)) + Process.Start(new ProcessStartInfo(item.Mod.ModPath.FullName) { UseShellExecute = true }); + + if (lastDrawnName == item.Name) + { + table.NextColumn(); + } + else + { + table.DrawFrameColumnWithTooltip(item.Name); + lastDrawnName = item.Name; + } + + table.DrawFrameColumnWithTooltip(item.Directory); + table.NextColumn(); + Im.Cursor.FrameAlign(); + var count = item.Collections.Length + item.MarkedActive; + ImEx.TextRightAligned($"{count}"); + if (count > 0 && Im.Item.Hovered()) + { + using var tt = Im.Tooltip.Begin(); + foreach (var collection in item.Collections) + { + Im.Text("Active in Collection "u8); + Im.Line.NoSpacing(); + Im.Text(collection.Identity.Name); + } + + if (item.MarkedActive > 0) + Im.Text($"{item.MarkedActive} other Plugins mark this mod as active."); + } + + table.DrawFrameColumn(item.CreationDate); + table.DrawFrameColumnWithTooltip(item.Path); + table.NextColumn(); + UnusedModsTab.DrawNotes(item.Notes); + } + } + + private sealed class Cache(ModConfigUpdater configUpdater, ModManager mods, CollectionStorage collections) : BasicCache + { + public readonly record struct CacheItem( + StringU8 Name, + Mod Mod, + StringU8 Directory, + StringU8 CreationDate, + StringU8 Path, + ModCollection[] Collections, + (StringPair, StringPair)[] Notes, + int MarkedActive); + + public List Items = []; + + public readonly SizedString Active = new("Active"u8); + public readonly SizedString Date = new("00/00/0000 00:00"u8); + public readonly SizedString Notes = new("Notes"u8); + + public override void Update() + { + if (!Dirty.HasFlag(IManagedCache.DirtyFlags.Custom)) + return; + + Items.Clear(); + var data = mods.GroupBy(m => m.Name).Select(g => (g.Key, g.ToList())).Where(p => p.Item2.Count > 1).ToList(); + var dict = new Dictionary(); + foreach (var (name, list) in data) + { + var nameU8 = new StringU8(name); + foreach (var mod in list) + { + configUpdater.QueryUsage(mod, dict); + var creation = new StringU8($"{DateTimeOffset.FromUnixTimeMilliseconds(mod.ImportDate):g}"); + var activeCollections = collections.Where(c => c.GetActualSettings(mod.Index).Settings?.Enabled is true).ToArray(); + var notes = dict.Select(kvp => (new StringPair(kvp.Key.GetName().Name ?? "Unknown"), new StringPair(kvp.Value.Item2))) + .ToArray(); + var markedActive = dict.Values.Count(v => v.Item1); + Items.Add(new CacheItem(nameU8, mod, new StringU8(mod.Identifier), creation, new StringU8(mod.Path.CurrentPath), + activeCollections, notes, markedActive)); + } + } + + Items = Items.OrderBy(i => i.Name).ThenByDescending(i => i.Collections.Length + i.MarkedActive) + .ThenByDescending(i => i.Notes.Length).ThenBy(i => i.Mod.ImportDate) + .ThenBy(i => i.Directory).ToList(); + Dirty = IManagedCache.DirtyFlags.Clean; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + Active.Dispose(); + Date.Dispose(); + Notes.Dispose(); + } + } +} diff --git a/Penumbra/UI/ManagementTab/ManagementTab.cs b/Penumbra/UI/ManagementTab/ManagementTab.cs index aab5cfe8..00c112c9 100644 --- a/Penumbra/UI/ManagementTab/ManagementTab.cs +++ b/Penumbra/UI/ManagementTab/ManagementTab.cs @@ -1,10 +1,7 @@ using ImSharp; using Luna; using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Mods; -using Penumbra.Mods.Manager; +using Penumbra.Services; namespace Penumbra.UI.ManagementTab; @@ -15,152 +12,7 @@ public enum ManagementTabType Cleanup, } -public sealed class DuplicateModsTab(ModManager mods, CollectionStorage collections) : ITab -{ - public ReadOnlySpan Label - => "Duplicate Mods"u8; - - public ManagementTabType Identifier - => ManagementTabType.DuplicateMods; - - public void DrawContent() - { - using var table = Im.Table.Begin("duplicates"u8, 4, TableFlags.RowBackground); - if (!table) - return; - - var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(mods, collections)); - foreach (var item in cache.Items) - { - table.DrawFrameColumn(item.Name); - foreach (var (mod, date, path, enabledCollections) in item.Data) - { - table.GoToColumn(0); - table.DrawFrameColumn(path); - table.DrawFrameColumn(date); - table.DrawFrameColumn($"{enabledCollections.Length}"); - if (Im.Item.Hovered()) - { - using var tt = Im.Tooltip.Begin(); - foreach (var collection in enabledCollections) - Im.Text(collection.Identity.Name); - } - - table.NextRow(); - } - } - } - - private sealed class Cache(ModManager mods, CollectionStorage collections) : BasicCache - { - public readonly record struct CacheItem( - StringU8 Name, - (Mod Mod, StringU8 CreationDate, StringU8 Path, ModCollection[] Collections)[] Data); - - public List Items = []; - - public override void Update() - { - if (!Dirty.HasFlag(IManagedCache.DirtyFlags.Custom)) - return; - - Items.Clear(); - Items.AddRange(mods.GroupBy(m => m.Name) - .Select(kvp => new CacheItem(new StringU8(kvp.Key), - kvp.Select(m => (m, new StringU8($"{DateTimeOffset.FromUnixTimeMilliseconds(m.ImportDate)}"), - new StringU8(m.Path.CurrentPath), - collections.Where(c => c.GetActualSettings(m.Index).Settings?.Enabled is true).ToArray())) - .OrderByDescending(t => t.Item4.Length).ThenBy(t => t.m.ImportDate).ToArray())).Where(p => p.Data.Length > 1)); - - Dirty = IManagedCache.DirtyFlags.Clean; - } - } -} - -public sealed class UnusedModsTab(ModConfigUpdater modConfigUpdater) : ITab -{ - public ReadOnlySpan Label - => "Unused Mods"u8; - - public ManagementTabType Identifier - => ManagementTabType.UnusedMods; - - public void DrawContent() - { - using var table = Im.Table.Begin("unused"u8, 5, TableFlags.RowBackground | TableFlags.SizingFixedFit); - if (!table) - return; - - var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(modConfigUpdater)); - using var clipper = new Im.ListClipper(cache.Data.Count, Im.Style.FrameHeightWithSpacing); - foreach (var item in clipper.Iterate(cache.Data)) - { - table.DrawFrameColumn(item.ModName); - table.DrawFrameColumn(item.ModPath); - table.DrawFrameColumn(item.Duration.Utf8); - table.DrawFrameColumn(item.ModSizeString.Utf8); - table.NextColumn(); - if (item.Notes.Length > 0) - { - ImEx.Icon.DrawAligned(LunaStyle.InfoIcon, LunaStyle.FavoriteColor); - var hovered = Im.Item.Hovered(); - Im.Line.SameInner(); - Im.Text("Notes"u8); - if (hovered || Im.Item.Hovered()) - { - using var tt = Im.Tooltip.Begin(); - using (Im.Group()) - { - foreach (var (plugin, _) in item.Notes) - Im.Text(plugin.Utf8); - } - - using (Im.Group()) - { - foreach (var (_, node) in item.Notes) - Im.Text(node.Utf8); - } - } - } - } - } - - - private sealed class Cache(ModConfigUpdater modConfigUpdater) : BasicCache - { - public readonly record struct CacheItem( - Mod Mod, - StringU8 ModName, - StringU8 ModPath, - long ModSize, - StringPair ModSizeString, - StringPair Duration, - (StringPair, StringPair)[] Notes) - { - public CacheItem(Mod mod, (string, string)[] Notes, DateTime now) - : this(mod, new StringU8(mod.Name), new StringU8(mod.Path.CurrentPath), 0, StringPair.Empty, - new StringPair(FormattingFunctions.DurationString(mod.LastConfigEdit, now)), - Notes.Select(n => (new StringPair(n.Item1), new StringPair(n.Item2))).ToArray()) - { } - } - - public List Data = []; - - public override void Update() - { - if (!Dirty.HasFlag(IManagedCache.DirtyFlags.Custom)) - return; - - var now = DateTime.UtcNow; - Data.Clear(); - foreach (var (mod, notes) in modConfigUpdater.ListUnusedMods(TimeSpan.Zero).OrderBy(mod => mod.Item1.LastConfigEdit)) - Data.Add(new CacheItem(mod, notes, now)); - Dirty = IManagedCache.DirtyFlags.Clean; - } - } -} - -public sealed class CleanupTab : ITab +public sealed class CleanupTab(CleanupService cleanup, Configuration config) : ITab { public ReadOnlySpan Label => "General Cleanup"u8; @@ -169,7 +21,46 @@ public sealed class CleanupTab : ITab => ManagementTabType.Cleanup; public void DrawContent() - { } + { + using var child = Im.Child.Begin("c"u8, Im.ContentRegion.Available); + if (!child) + return; + + var enabled = config.DeleteModModifier.IsActive(); + if (cleanup.Progress is not 0.0 and not 1.0) + { + Im.ProgressBar((float)cleanup.Progress, new Vector2(200 * Im.Style.GlobalScale, Im.Style.FrameHeight), + $"{cleanup.Progress * 100}%"); + Im.Line.Same(); + if (Im.Button("Cancel##FileCleanup"u8)) + cleanup.Cancel(); + } + else + { + Im.Line.New(); + } + + if (ImEx.Button("Clear Unused Local Mod Data Files"u8, default, + "Delete all local mod data files that do not correspond to currently installed mods."u8, + !enabled || cleanup.IsRunning)) + cleanup.CleanUnusedLocalData(); + if (!enabled) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteModModifier} while clicking to delete files."); + + if (ImEx.Button("Clear Backup Files"u8, default, + "Delete all backups of .json configuration files in your configuration folder and all backups of mod group files in your mod directory."u8, + !enabled || cleanup.IsRunning)) + cleanup.CleanBackupFiles(); + if (!enabled) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteModModifier} while clicking to delete files."); + + if (ImEx.Button("Clear All Unused Settings"u8, default, + "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, + !enabled || cleanup.IsRunning)) + cleanup.CleanupAllUnusedSettings(); + if (!enabled) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteModModifier} while clicking to remove settings."); + } } public sealed class ManagementTab : TabBar, ITab diff --git a/Penumbra/UI/ManagementTab/UnusedModsTab.cs b/Penumbra/UI/ManagementTab/UnusedModsTab.cs new file mode 100644 index 00000000..13463f4b --- /dev/null +++ b/Penumbra/UI/ManagementTab/UnusedModsTab.cs @@ -0,0 +1,556 @@ +using ImSharp; +using ImSharp.Table; +using Luna; +using Penumbra.Api.Enums; +using Penumbra.Communication; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ManagementTab; + +public sealed class UnusedModsTab( + ModConfigUpdater modConfigUpdater, + ModManager manager, + Configuration config, + ModExportManager exports, + CommunicatorService communicator) : ITab +{ + public ReadOnlySpan Label + => "Unused Mods"u8; + + public ManagementTabType Identifier + => ManagementTabType.UnusedMods; + + private readonly Table _table = new(modConfigUpdater, manager, config, exports, communicator); + private int _defaultDays = 30; + + public void PostTabButton() + { + if (!Im.Item.Hovered()) + return; + + using var tt = Im.Tooltip.Begin(); + ImEx.TextMultiColored("Here you can list mods that are not currently enabled or have temporary settings in "u8) + .Then("any "u8, ColorId.NewMod.Value()).Then(" collection."u8).End(); + Im.Text( + "Other Plugins subscribing to Penumbras API can mark mods as 'in use' so that they do not appear, or add custom notes to them while still displaying them."u8); + } + + public void DrawContent() + { + using var child = Im.Child.Begin("c"u8, Im.ContentRegion.Available); + if (!child) + return; + + if (Im.Checkbox("Hide All Mods with Notes"u8, _table.HideNodes)) + _table.HideNodes ^= true; + Im.Line.Same(); + if (Im.Button("Show All Inactive Mods"u8)) + _table.UnusedCap = TimeSpan.Zero; + + Im.Line.Same(); + if (Im.Button("Show Inactive Mods Not Configured in"u8)) + _table.UnusedCap = TimeSpan.FromDays(_defaultDays); + Im.Line.SameInner(); + Im.Item.SetNextWidthScaled(40); + Im.Drag("Days"u8, ref _defaultDays, 0, null, 0.1f, SliderFlags.AlwaysClamp); + + _table.Draw(); + } + + private sealed class Table( + ModConfigUpdater modConfigUpdater, + ModManager manager, + Configuration config, + ModExportManager exports, + CommunicatorService communicator) : TableBase(new StringU8("unused"u8), + new ButtonColumn(manager, config, exports), + new NameColumn(communicator), new LastEditColumn(), new ModSizeColumn(), new PathColumn(), new NotesColumn()) + { + public bool HideNodes + { + get; + set + { + if (field == value) + return; + + field = value; + _filterDirty = true; + } + } + + public TimeSpan UnusedCap + { + get; + set + { + if (field == value) + return; + + field = value; + _spanDirty = true; + } + } = TimeSpan.MaxValue; + + + private bool _filterDirty; + private bool _spanDirty; + + public override IEnumerable GetItems() + { + var now = DateTime.UtcNow; + return modConfigUpdater.ListUnusedMods(UnusedCap).Select(m => new CacheItem(m.Item1, m.Item2, now)); + } + + public override Vector2 GetSize() + { + var size = Im.ContentRegion.Available; + size.Y -= Im.Style.TextHeightWithSpacing; + return size; + } + + protected override void PreDraw(in Cache cache) + { + var buttons = (ButtonColumn)Columns[0]; + buttons.DeleteList.Clear(); + var disabled = !config.DeleteModModifier.IsActive(); + Im.Line.Same(); + if (ImEx.Button("Update View"u8, + "The table does not automatically update, so click this to update the visible mods without changing the time limit."u8)) + cache.Dirty |= IManagedCache.DirtyFlags.Custom; + + Im.Line.Same(); + if (ImEx.Button("Delete All Visible Mods"u8, default, "Delete all mods that are currently visible. This is NOT reversible."u8, + disabled)) + foreach (var (mod, globalIndex) in cache.GetItemsWithIndices().ToList()) + { + manager.DeleteMod(mod.Mod); + cache.DeleteSingleItem(globalIndex); + } + + if (disabled) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.DeleteModModifier} to delete the mods."); + } + + protected override void PostDraw(in Cache cache) + { + base.PostDraw(in cache); + var buttons = (ButtonColumn)Columns[0]; + foreach (var item in buttons.DeleteList) + cache.DeleteSingleItem(item); + buttons.DeleteList.Clear(); + + if (cache.Loading) + return; + + Im.Text($"{cache.Count}/{cache.AllItems.Count} visible of {manager.Count} total Mods."); + if (buttons.Exporting is { } mod) + { + Im.Line.Same(); + Im.Text($"Exporting and deleting mod: {mod.Name}"); + Im.Line.Same(); + ImEx.Spinner("s"u8, Im.Style.TextHeight / 3, 2, Im.Color.Get(ImGuiColor.Text)); + } + } + + protected override Cache CreateCache() + => new(this); + + public sealed class Cache : TableCache + { + private readonly Table _parent; + + public Cache(Table parent) + : base(parent) + { + KeepAliveDuration = TimeSpan.FromMinutes(5); + _parent = parent; + } + + protected override bool WouldBeVisible(in CacheItem value, int globalIndex) + => (!_parent.HideNodes || value.Notes.Length == 0) && base.WouldBeVisible(value, globalIndex); + + private CancellationTokenSource? _cancel; + + public override void Update() + { + if (_parent._filterDirty) + { + FilterDirty = true; + _parent._filterDirty = false; + } + + if (_parent._spanDirty) + { + Dirty |= IManagedCache.DirtyFlags.Custom; + _parent._spanDirty = false; + } + + base.Update(); + } + + protected override void UpdateSort() + { + if (Loading) + return; + + base.UpdateSort(); + } + + protected override void UpdateFilter() + { + if (Loading) + return; + + base.UpdateFilter(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _cancel?.Cancel(); + } + + protected override void OnDataUpdate() + { + _cancel?.Cancel(); + base.OnDataUpdate(); + + if (UnfilteredItems.Count < 50) + return; + + Loading = true; + _cancel = new CancellationTokenSource(); + var token = _cancel.Token; + Task.Run(() => + { + foreach (var mod in UnfilteredItems) + { + if (token.IsCancellationRequested) + { + Loading = false; + return; + } + + _ = mod.ModSizeString; + } + + Loading = false; + }, token); + } + } + } + + private sealed class ButtonColumn : BasicColumn + { + public readonly HashSet DeleteList = []; + + private readonly ModManager _manager; + private readonly Configuration _config; + private readonly ModExportManager _exports; + + public Mod? Exporting { get; private set; } + + public ButtonColumn(ModManager manager, Configuration config, ModExportManager exports) + { + _manager = manager; + _config = config; + _exports = exports; + Label = StringU8.Empty; + Flags |= TableColumnFlags.NoSort; + } + + public override void DrawColumn(in CacheItem item, int globalIndex) + { + var inactive = !_config.DeleteModModifier.IsActive(); + var exportingThis = Exporting == item.Mod; + if (ImEx.Icon.Button(LunaStyle.DeleteIcon, "Delete this mod from Penumbra and your drive. This is NOT reversible."u8, + inactive || exportingThis)) + { + _manager.DeleteMod(item.Mod); + DeleteList.Add(globalIndex); + } + + if (inactive) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {_config.DeleteModModifier} to delete."); + if (exportingThis) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "\nCurrently exporting and deleting this mod, please wait."u8); + + Im.Line.SameInner(); + + var exporting = Exporting is not null; + if (ImEx.Icon.Button(LunaStyle.BackupDeleteIcon, + "Export this mod to your export directory, compressing it, and then delete it from Penumbra."u8, inactive || exporting)) + { + Exporting = item.Mod; + _exports.CreateAsync(Exporting).ContinueWith(_ => + { + _manager.DeleteMod(Exporting); + DeleteList.Add(globalIndex); + Exporting = null; + }); + } + + if (inactive) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {_config.DeleteModModifier} to delete."); + if (exporting) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Already exporting and deleting a mod, please wait."u8); + + Im.Line.SameInner(); + if (ImEx.Icon.Button(LunaStyle.FolderIcon, "Open the mod directory in the file explorer of your choice."u8)) + Process.Start(new ProcessStartInfo(item.Mod.ModPath.FullName) { UseShellExecute = true }); + } + + public override float ComputeWidth(IEnumerable _) + => Im.Style.FrameHeight * 3 + 2 * Im.Style.ItemInnerSpacing.X; + } + + private sealed class NameColumn : TextColumn + { + private readonly CommunicatorService _communicator; + + public NameColumn(CommunicatorService communicator) + { + _communicator = communicator; + Label = new StringU8("Mod Name"u8); + Flags |= TableColumnFlags.WidthStretch; + } + + protected override string ComparisonText(in CacheItem item, int globalIndex) + => item.Mod.Name; + + protected override StringU8 DisplayText(in CacheItem item, int globalIndex) + => item.ModName; + + public override float ComputeWidth(IEnumerable _) + => 0.3f; + + public override void DrawColumn(in CacheItem item, int globalIndex) + { + var content = Im.ContentRegion.Available.X; + Im.Cursor.FrameAlign(); + var clicked = Im.Selectable(item.ModName); + Im.Tooltip.OnHover(item.DirectoryName); + if (Im.Font.CalculateSize(item.ModName).X >= content) + Im.Tooltip.OnHover(item.ModName); + Im.Tooltip.OnHover("\nClick to move to mod."u8); + if (clicked) + _communicator.SelectTab.Invoke(new SelectTab.Arguments(TabType.Mods, item.Mod)); + } + } + + private sealed class PathColumn : TextColumn + { + public PathColumn() + { + Label = new StringU8("Mod Path"u8); + Flags |= TableColumnFlags.WidthStretch; + } + + protected override string ComparisonText(in CacheItem item, int globalIndex) + => item.Mod.Path.CurrentPath; + + protected override StringU8 DisplayText(in CacheItem item, int globalIndex) + => item.ModPath; + + public override float ComputeWidth(IEnumerable _) + => 0.7f; + + public override void DrawColumn(in CacheItem item, int globalIndex) + { + var content = Im.ContentRegion.Available.X; + Im.Cursor.FrameAlign(); + base.DrawColumn(in item, globalIndex); + if (Im.Item.Size.X >= content) + Im.Tooltip.OnHover(item.ModPath); + } + } + + + private sealed class LastEditColumn : NumberColumn + { + public LastEditColumn() + { + Label = new StringU8("Last Config Edit"u8); + } + + public override long ToValue(in CacheItem item, int globalIndex) + => item.Mod.LastConfigEdit; + + protected override SizedString DisplayNumber(in CacheItem item, int globalIndex) + => item.Duration; + + protected override string ComparisonText(in CacheItem item, int globalIndex) + => item.Duration.Utf16; + + public override float ComputeWidth(IEnumerable _) + => Im.Font.CalculateSize("Last Config Edit"u8).X + ImEx.Table.ArrowWidth + Im.Style.CellPadding.X * 2; + + public override void DrawColumn(in CacheItem item, int globalIndex) + { + Im.Cursor.FrameAlign(); + base.DrawColumn(in item, globalIndex); + Im.Tooltip.OnHover($"Click to copy Timestamp: {item.Mod.LastConfigEdit}"); + if (Im.Item.Clicked()) + Im.Clipboard.Set($"{item.Mod.LastConfigEdit}"); + } + } + + private sealed class ModSizeColumn : NumberColumn + { + public ModSizeColumn() + { + Label = new StringU8("Size On Disk"u8); + } + + public override long ToValue(in CacheItem item, int globalIndex) + => item.ModSize; + + protected override SizedString DisplayNumber(in CacheItem item, int globalIndex) + => item.ModSizeString; + + protected override string ComparisonText(in CacheItem item, int globalIndex) + => item.ModSizeString.Utf16; + + public override float ComputeWidth(IEnumerable _) + => Im.Font.CalculateSize("Size On Disk"u8).X + ImEx.Table.ArrowWidth + Im.Style.CellPadding.X * 2; + + public override void DrawColumn(in CacheItem item, int globalIndex) + { + Im.Cursor.FrameAlign(); + base.DrawColumn(in item, globalIndex); + } + } + + private sealed class NotesColumn : TextColumn + { + private static readonly StringU8 Notes = new("Notes"u8); + + public NotesColumn() + { + Label = new StringU8("Notes"u8); + } + + public override bool WouldBeVisible(in CacheItem item, int globalIndex) + { + if (item.Notes.Length is 0) + return Filter.WouldBeVisible(string.Empty); + if (Filter.WouldBeVisible("Notes")) + return true; + + return item.Notes.Any(n => Filter.WouldBeVisible(n.Item1.Utf16) || Filter.WouldBeVisible(n.Item2.Utf16)); + } + + public override int Compare(in CacheItem lhs, int lhsGlobalIndex, in CacheItem rhs, int rhsGlobalIndex) + { + var first = lhs.Notes.Length.CompareTo(rhs.Notes.Length); + if (first is not 0) + return first; + + foreach (var (lhsNote, rhsNote) in lhs.Notes.Zip(rhs.Notes)) + { + var second = lhsNote.Item1.Utf16.CompareTo(rhsNote.Item1.Utf16, StringComparison.Ordinal); + if (second is not 0) + return second; + } + + foreach (var (lhsNote, rhsNote) in lhs.Notes.Zip(rhs.Notes)) + { + var third = lhsNote.Item2.Utf16.Length.CompareTo(rhsNote.Item2.Utf16.Length); + if (third is not 0) + return third; + } + + return 0; + } + + protected override string ComparisonText(in CacheItem item, int globalIndex) + => item.Notes.Length is 0 ? string.Empty : "Notes"; + + protected override StringU8 DisplayText(in CacheItem item, int globalIndex) + => item.Notes.Length is 0 ? StringU8.Empty : Notes; + + public override void DrawColumn(in CacheItem item, int globalIndex) + => DrawNotes(item.Notes); + + public override float ComputeWidth(IEnumerable _) + => Im.Style.FrameHeightWithSpacing + Im.Font.CalculateSize(Notes).X; + } + + private record CacheItem( + Mod Mod, + StringU8 ModName, + StringU8 ModPath, + StringU8 DirectoryName, + long ModSize, + SizedStringPair ModSizeString, + SizedStringPair Duration, + (StringPair, StringPair)[] Notes) : IDisposable + { + public long ModSize + { + get => field < 0 ? field = WindowsFunctions.GetDirectorySize(Mod.ModPath.FullName) : field; + } = ModSize; + + private SizedStringPair _modSizeString = ModSizeString; + + public SizedStringPair ModSizeString + { + get => _modSizeString.IsEmpty + ? _modSizeString = new SizedStringPair(FormattingFunctions.HumanReadableSize(ModSize)) + : _modSizeString; + } + + public CacheItem(Mod mod, (string, string)[] notes, DateTime now) + : this(mod, new StringU8(mod.Name), new StringU8(mod.Path.CurrentPath), new StringU8($"Directory Name: {mod.Identifier}"), + -1, SizedStringPair.Empty, new SizedStringPair(FormattingFunctions.DurationString(mod.LastConfigEdit, now)), + notes.Select(n => (new StringPair(n.Item1), new StringPair(n.Item2))).ToArray()) + { } + + public void Dispose() + { + if (!_modSizeString.IsEmpty) + _modSizeString.Dispose(); + Duration.Dispose(); + } + } + + public static void DrawNotes((StringPair, StringPair)[] notes) + { + if (notes.Length is 0) + return; + + ImEx.Icon.DrawAligned(LunaStyle.InfoIcon, LunaStyle.FavoriteColor); + var hovered = Im.Item.Hovered(); + Im.Line.SameInner(); + Im.Text("Notes"u8); + if (!hovered && !Im.Item.Hovered()) + return; + + using var tt = Im.Tooltip.Begin(); + DrawNote(notes[0]); + foreach (var note in notes.Skip(1)) + { + Im.Separator(); + DrawNote(note); + } + + return; + + static void DrawNote((StringPair, StringPair) note) + { + using (Im.Group()) + { + Im.Text(note.Item1.Utf8); + Im.Line.Same(); + using (Im.Group()) + { + Im.Text(note.Item2.Utf8); + } + } + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1917e81d..4c307f0b 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -117,7 +117,7 @@ public class ModPanelEditTab( : backup.Exists ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." : $"Create exported archive of current mod at \"{backup.Name}\".", ModBackup.CreatingBackup)) - backup.CreateAsync(); + _ = backup.CreateAsync(); if (Im.Item.RightClicked()) Im.Popup.Open("context"u8); diff --git a/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs b/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs index ac25d5ed..3cd1e077 100644 --- a/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs +++ b/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs @@ -74,7 +74,7 @@ public sealed class ModFileSystemCache : FileSystemCache node) : BaseFileSystemNodeCache + public sealed class ModData(IFileSystemData node) : BaseFileSystemNodeCache, IDisposable { public readonly IFileSystemData Node = node; public Vector4 TextColor; @@ -82,6 +82,7 @@ public sealed class ModFileSystemCache : FileSystemCache Im.Style.ItemSpacing.X) Im.Window.DrawList.Text(new Vector2(itemPos + offset, line), ColorId.SelectorPriority.Value().Color, PriorityText.Text); } + + public void Dispose() + => PriorityText.Dispose(); } public override void Update() diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 51cd0388..6c1eabd8 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -43,7 +43,7 @@ public static class LoadStateExtensions | LoadStateFlag.None; } -internal sealed unsafe class CachedRecord(Record record) +internal sealed unsafe class CachedRecord(Record record) : IDisposable { public readonly Record Record = record; public readonly string PathU16 = record.Path.ToString(); @@ -58,6 +58,12 @@ internal sealed unsafe class CachedRecord(Record record) public readonly string HandleU16 = $"0x{(nint)record.Handle:X}"; public readonly SizedStringPair Thread = new($"{record.OsThreadId}"); public readonly SizedStringPair RefCount = new($"{record.RefCount}"); + + public void Dispose() + { + Thread.Dispose(); + RefCount.Dispose(); + } } internal sealed class ResourceWatcherTable : TableBase> diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 78afd0bc..9595e7a5 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -872,8 +872,6 @@ public sealed class SettingsTab : ITab Im.Separator(); DrawReloadResourceButton(); DrawReloadFontsButton(); - Im.Separator(); - DrawCleanupButtons(); Im.Line.New(); } @@ -1053,43 +1051,6 @@ public sealed class SettingsTab : ITab _fontReloader.Reload(); } - private void DrawCleanupButtons() - { - var enabled = _config.DeleteModModifier.IsActive(); - if (_cleanupService.Progress is not 0.0 and not 1.0) - { - Im.ProgressBar((float)_cleanupService.Progress, new Vector2(200 * Im.Style.GlobalScale, Im.Style.FrameHeight), - $"{_cleanupService.Progress * 100}%"); - Im.Line.Same(); - if (Im.Button("Cancel##FileCleanup"u8)) - _cleanupService.Cancel(); - } - else - { - Im.Line.New(); - } - - if (ImEx.Button("Clear Unused Local Mod Data Files"u8, default, - "Delete all local mod data files that do not correspond to currently installed mods."u8, - !enabled || _cleanupService.IsRunning)) - _cleanupService.CleanUnusedLocalData(); - if (!enabled) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files."); - - if (ImEx.Button("Clear Backup Files"u8, default, - "Delete all backups of .json configuration files in your configuration folder and all backups of mod group files in your mod directory."u8, - !enabled || _cleanupService.IsRunning)) - _cleanupService.CleanBackupFiles(); - if (!enabled) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files."); - - if (ImEx.Button("Clear All Unused Settings"u8, default, - "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, - !enabled || _cleanupService.IsRunning)) - _cleanupService.CleanupAllUnusedSettings(); - if (!enabled) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to remove settings."); - } /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. private void DrawWaitForPluginsReflection() diff --git a/Penumbra/Util/ArchiveUtility.cs b/Penumbra/Util/ArchiveUtility.cs new file mode 100644 index 00000000..691ebd23 --- /dev/null +++ b/Penumbra/Util/ArchiveUtility.cs @@ -0,0 +1,29 @@ +using SharpCompress.Archives; +using SharpCompress.Common; + +namespace Penumbra.Util; + +public static class ArchiveUtility +{ + //private static readonly ZipWriterOptions DefaultArchiveOptions = new(CompressionType.LZMA, CompressionLevel.Level0); + + private static readonly ExtractionOptions ExtractionOptions = new() + { + ExtractFullPath = true, + Overwrite = true, + }; + + public static void CreateFromDirectory(string directoryPath, string filePath) + { + ZipFile.CreateFromDirectory(directoryPath, filePath, CompressionLevel.SmallestSize, false); + //using var archive = ZipArchive.Create(); + //archive.AddAllFromDirectory(directoryPath); + //archive.SaveTo(filePath, DefaultArchiveOptions); + } + + public static void ExtractToDirectory(string filePath, string directoryPath) + { + Directory.CreateDirectory(directoryPath); + ArchiveFactory.WriteToDirectory(filePath, directoryPath, ExtractionOptions); + } +} diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 94225a3b..7686cba6 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -29,12 +29,9 @@ }, "SharpCompress": { "type": "Direct", - "requested": "[0.40.0, )", - "resolved": "0.40.0", - "contentHash": "yP/aFX1jqGikVF7u2f05VEaWN4aCaKNLxSas82UgA2GGVECxq/BcqZx3STHCJ78qilo1azEOk1XpBglIuGMb7w==", - "dependencies": { - "ZstdSharp.Port": "0.8.5" - } + "requested": "[0.44.1, )", + "resolved": "0.44.1", + "contentHash": "QHhxJHoyIXKeu3vLJ0XY2rDJLd4fLvZ0xXPCMO854OOcSz62uHB5uPloBMtg236MFDMVUSUHuO1E8WY5HpLp6Q==" }, "SharpGLTF.Core": { "type": "Direct", @@ -69,21 +66,21 @@ }, "JetBrains.Annotations": { "type": "Transitive", - "resolved": "2024.3.0", - "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" + "resolved": "2024.2.0", + "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==", + "resolved": "9.0.0", + "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", @@ -145,11 +142,6 @@ "resolved": "3.1.0", "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==" }, - "ZstdSharp.Port": { - "type": "Transitive", - "resolved": "0.8.5", - "contentHash": "TR4j17WeVSEb3ncgL2NqlXEqcy04I+Kk9CaebNDplUeL8XOgjkZ7fP4Wg4grBdPLIqsV86p2QaXTkZoRMVOsew==" - }, "luna": { "type": "Project", "dependencies": { @@ -158,13 +150,6 @@ "System.IO.Hashing": "[9.0.8, )" } }, - "ottergui": { - "type": "Project", - "dependencies": { - "JetBrains.Annotations": "[2024.3.0, )", - "Microsoft.Extensions.DependencyInjection": "[9.0.2, )" - } - }, "penumbra.api": { "type": "Project" }, @@ -177,7 +162,7 @@ "FlatSharp.Compiler": "[7.9.0, )", "FlatSharp.Runtime": "[7.9.0, )", "Luna": "[1.0.0, )", - "Penumbra.Api": "[5.13.0, )", + "Penumbra.Api": "[5.14.0, )", "Penumbra.String": "[1.0.7, )" } },