From e2a6274b33ea74a6234b23a8263532bb2055a776 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 May 2022 18:06:21 +0200 Subject: [PATCH] Add empty option for single select groups with empty options. More Editor stuff. --- OtterGui | 2 +- Penumbra/Collections/CollectionManager.cs | 1 + Penumbra/Import/TexToolsImporter.ModPack.cs | 15 +- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 207 +++++++++++ Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 43 +++ Penumbra/Mods/Editor/Mod.Editor.Files.cs | 112 ++++++ Penumbra/Mods/Editor/Mod.Editor.Groups.cs | 73 ++++ Penumbra/Mods/Editor/Mod.Editor.cs | 57 ++++ Penumbra/Mods/{ => Editor}/ModCleanup.cs | 88 +---- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 43 ++- Penumbra/Mods/Mod.Creation.cs | 7 + Penumbra/Mods/Mod.Files.cs | 3 - Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 6 +- Penumbra/Penumbra.cs | 2 +- Penumbra/Penumbra.csproj.DotSettings | 1 + Penumbra/UI/Classes/ModEditWindow.cs | 322 ++++++++++++++++++ Penumbra/UI/Classes/SubModEditWindow.cs | 225 ------------ Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 18 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 1 + Penumbra/UI/ConfigWindow.cs | 3 +- Penumbra/Util/DictionaryExtensions.cs | 44 +++ 21 files changed, 937 insertions(+), 336 deletions(-) create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Edit.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Files.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Groups.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.cs rename Penumbra/Mods/{ => Editor}/ModCleanup.cs (89%) create mode 100644 Penumbra/UI/Classes/ModEditWindow.cs delete mode 100644 Penumbra/UI/Classes/SubModEditWindow.cs diff --git a/OtterGui b/OtterGui index cce4e9ed..5cb708ff 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cce4e9ed2cf5fa0068d6c8fadff5acd0d54f8359 +Subproject commit 5cb708ff692d397a9e71f3315d9d054f6558f42d diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index bb6bb826..81c0aef1 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -282,6 +282,7 @@ public partial class ModCollection if( recomputeList ) { + // TODO: Does not check if the option that was changed is actually enabled. foreach( var collection in this.Where( c => c.HasCache ) ) { if( collection[ mod.Index ].Settings is { Enabled: true } ) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 5533b75e..65b757c2 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -109,7 +109,8 @@ public partial class TexToolsImporter .Sum( page => page.ModGroups .Where( g => g.GroupName.Length > 0 && g.OptionList.Length > 0 ) .Sum( group => group.OptionList - .Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) ) ); + .Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) + + ( group.OptionList.Any( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ) ? 1 : 0 ) ) ); // Extended V2 mod packs contain multiple options that need to be handled separately. private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) @@ -173,6 +174,18 @@ public partial class TexToolsImporter ++_currentOptionIdx; } + // Handle empty options for single select groups without creating a folder for them. + // We only want one of those at most, and it should usually be the first option. + if( group.SelectionType == SelectType.Single ) + { + var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ); + if( empty != null ) + { + _currentOptionName = empty.Name; + options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) ); + } + } + Mod.CreateOptionGroup( _currentModDirectory, group, groupPriority, groupPriority, description.ToString(), options ); ++groupPriority; } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs new file mode 100644 index 00000000..e8225db4 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + private readonly SHA256 _hasher = SHA256.Create(); + private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); + + public IReadOnlyList< (FullPath[] Paths, long Size, byte[] Hash) > Duplicates + => _duplicates; + + public long SavedSpace { get; private set; } = 0; + + public bool DuplicatesFinished { get; private set; } = true; + + public void DeleteDuplicates() + { + if( !DuplicatesFinished || _duplicates.Count == 0 ) + { + return; + } + + foreach( var (set, _, _) in _duplicates ) + { + if( set.Length < 2 ) + { + continue; + } + + var remaining = set[ 0 ]; + foreach( var duplicate in set.Skip( 1 ) ) + { + HandleDuplicate( duplicate, remaining ); + } + } + _availableFiles.RemoveAll( p => !p.Item1.Exists ); + _duplicates.Clear(); + } + + public void Cancel() + { + DuplicatesFinished = true; + } + + private void HandleDuplicate( FullPath duplicate, FullPath remaining ) + { + void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx ) + { + var changes = false; + var dict = subMod.Files.ToDictionary( kvp => kvp.Key, + kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) ); + if( changes ) + { + Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict ); + } + } + + ApplyToAllOptions( _mod, HandleSubMod ); + + try + { + File.Delete( duplicate.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}" ); + } + } + + + private FullPath ChangeDuplicatePath( FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes ) + { + if( !value.Equals( from ) ) + { + return value; + } + + changes = true; + PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to); + return to; + } + + + public void StartDuplicateCheck() + { + DuplicatesFinished = false; + Task.Run( CheckDuplicates ); + } + + private void CheckDuplicates() + { + _duplicates.Clear(); + SavedSpace = 0; + var list = new List< FullPath >(); + var lastSize = -1L; + foreach( var (p, size) in AvailableFiles ) + { + if( DuplicatesFinished ) + { + return; + } + + if( size == lastSize ) + { + list.Add( p ); + continue; + } + + if( list.Count >= 2 ) + { + CheckMultiDuplicates( list, lastSize ); + } + lastSize = size; + + list.Clear(); + list.Add( p ); + } + if( list.Count >= 2 ) + { + CheckMultiDuplicates( list, lastSize ); + } + + DuplicatesFinished = true; + } + + private void CheckMultiDuplicates( IReadOnlyList< FullPath > list, long size ) + { + var hashes = list.Select( f => (f, ComputeHash(f)) ).ToList(); + while( hashes.Count > 0 ) + { + if( DuplicatesFinished ) + { + return; + } + + var set = new HashSet< FullPath > { hashes[ 0 ].Item1 }; + var hash = hashes[ 0 ]; + for( var j = 1; j < hashes.Count; ++j ) + { + if( DuplicatesFinished ) + { + return; + } + + if( CompareHashes( hash.Item2, hashes[ j ].Item2 ) && CompareFilesDirectly( hashes[ 0 ].Item1, hashes[ j ].Item1 ) ) + { + set.Add( hashes[ j ].Item1 ); + } + } + + hashes.RemoveAll( p => set.Contains(p.Item1) ); + if( set.Count > 1 ) + { + _duplicates.Add( (set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2) ); + SavedSpace += ( set.Count - 1 ) * size; + } + } + } + + private static unsafe bool CompareFilesDirectly( FullPath f1, FullPath f2 ) + { + if( !f1.Exists || !f2.Exists ) + return false; + + using var s1 = File.OpenRead( f1.FullName ); + using var s2 = File.OpenRead( f2.FullName ); + var buffer1 = stackalloc byte[256]; + var buffer2 = stackalloc byte[256]; + var span1 = new Span< byte >( buffer1, 256 ); + var span2 = new Span< byte >( buffer2, 256 ); + + while( true ) + { + var bytes1 = s1.Read( span1 ); + var bytes2 = s2.Read( span2 ); + if( bytes1 != bytes2 ) + return false; + + if( !span1[ ..bytes1 ].SequenceEqual( span2[ ..bytes2 ] ) ) + return false; + + if( bytes1 < 256 ) + return true; + } + } + + public static bool CompareHashes( byte[] f1, byte[] f2 ) + => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); + + public byte[] ComputeHash( FullPath f ) + { + using var stream = File.OpenRead( f.FullName ); + return _hasher.ComputeHash( stream ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs new file mode 100644 index 00000000..65596021 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + private int _groupIdx = -1; + private int _optionIdx = 0; + + private IModGroup? _modGroup; + private SubMod _subMod; + + public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); + public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); + public readonly HashSet< MetaManipulation > CurrentManipulations = new(); + + public void SetSubMod( int groupIdx, int optionIdx ) + { + _groupIdx = groupIdx; + _optionIdx = optionIdx; + if( groupIdx >= 0 ) + { + _modGroup = _mod.Groups[ groupIdx ]; + _subMod = ( SubMod )_modGroup![ optionIdx ]; + } + else + { + _modGroup = null; + _subMod = _mod._default; + } + + CurrentFiles.SetTo( _subMod.Files ); + CurrentSwaps.SetTo( _subMod.FileSwaps ); + CurrentManipulations.Clear(); + CurrentManipulations.UnionWith( _subMod.Manipulations ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs new file mode 100644 index 00000000..e39e9138 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + // All files in subdirectories of the mod directory. + public IReadOnlyList< (FullPath, long) > AvailableFiles + => _availableFiles; + + private readonly List< (FullPath, long) > _availableFiles; + + // All files that are available but not currently used in any option. + private readonly SortedSet< FullPath > _unusedFiles; + + public IReadOnlySet< FullPath > UnusedFiles + => _unusedFiles; + + // All paths that are used in any option in the mod. + private readonly SortedSet< FullPath > _usedPaths; + + public IReadOnlySet< FullPath > UsedPaths + => _usedPaths; + + // All paths that are used in + private readonly SortedSet< FullPath > _missingPaths; + + public IReadOnlySet< FullPath > MissingPaths + => _missingPaths; + + // Adds all currently unused paths, relative to the mod directory, to the replacements. + public void AddUnusedPathsToDefault() + { + var dict = new Dictionary< Utf8GamePath, FullPath >( UnusedFiles.Count ); + foreach( var file in UnusedFiles ) + { + var gamePath = file.ToGamePath( _mod.BasePath, out var g ) ? g : Utf8GamePath.Empty; + if( !gamePath.IsEmpty && !dict.ContainsKey( gamePath ) ) + { + dict.Add( gamePath, file ); + PluginLog.Debug( "[AddUnusedPaths] Adding {GamePath} -> {File} to default option of {Mod}.", gamePath, file, _mod.Name ); + } + } + + Penumbra.ModManager.OptionAddFiles( _mod, -1, 0, dict ); + _usedPaths.UnionWith( _mod.Default.Files.Values ); + _unusedFiles.RemoveWhere( f => _mod.Default.Files.Values.Contains( f ) ); + } + + // Delete all currently unused paths from your filesystem. + public void DeleteUnusedPaths() + { + foreach( var file in UnusedFiles ) + { + try + { + File.Delete( file.FullName ); + PluginLog.Debug( "[DeleteUnusedPaths] Deleted {File} from {Mod}.", file, _mod.Name ); + } + catch( Exception e ) + { + PluginLog.Error($"[DeleteUnusedPaths] Could not delete {file} from {_mod.Name}:\n{e}" ); + } + } + + _unusedFiles.RemoveWhere( f => !f.Exists ); + _availableFiles.RemoveAll( p => !p.Item1.Exists ); + } + + // Remove all path redirections where the pointed-to file does not exist. + public void RemoveMissingPaths() + { + void HandleSubMod( ISubMod mod, int groupIdx, int optionIdx ) + { + var newDict = mod.Files.Where( kvp => CheckAgainstMissing( kvp.Value, kvp.Key ) ) + .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); + if( newDict.Count != mod.Files.Count ) + { + Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, newDict ); + } + } + + ApplyToAllOptions( _mod, HandleSubMod ); + _usedPaths.RemoveWhere( _missingPaths.Contains ); + _missingPaths.Clear(); + } + + private bool CheckAgainstMissing( FullPath file, Utf8GamePath key ) + { + if( !_missingPaths.Contains( file ) ) + { + return true; + } + + PluginLog.Debug( "[RemoveMissingPaths] Removing {GamePath} -> {File} from {Mod}.", key, file, _mod.Name ); + return false; + } + + + private static List<(FullPath, long)> GetAvailablePaths( Mod mod ) + => mod.BasePath.EnumerateDirectories() + .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ).Select( f => (new FullPath( f ), f.Length) ) ) + .OrderBy( p => -p.Length ).ToList(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs new file mode 100644 index 00000000..9f2a35e9 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + public void Normalize() + {} + + public void AutoGenerateGroups() + { + //ClearEmptySubDirectories( _mod.BasePath ); + //for( var i = _mod.Groups.Count - 1; i >= 0; --i ) + //{ + // if (_mod.Groups.) + // Penumbra.ModManager.DeleteModGroup( _mod, i ); + //} + //Penumbra.ModManager.OptionSetFiles( _mod, -1, 0, new Dictionary< Utf8GamePath, FullPath >() ); + // + //foreach( var groupDir in _mod.BasePath.EnumerateDirectories() ) + //{ + // var groupName = groupDir.Name; + // foreach( var optionDir in groupDir.EnumerateDirectories() ) + // { } + //} + + //var group = new OptionGroup + // { + // GroupName = groupDir.Name, + // SelectionType = SelectType.Single, + // Options = new List< Option >(), + // }; + // + // foreach( var optionDir in groupDir.EnumerateDirectories() ) + // { + // var option = new Option + // { + // OptionDesc = string.Empty, + // OptionName = optionDir.Name, + // OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), + // }; + // foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + // { + // if( Utf8RelPath.FromFile( file, baseDir, out var rel ) + // && Utf8GamePath.FromFile( file, optionDir, out var game ) ) + // { + // option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; + // } + // } + // + // if( option.OptionFiles.Count > 0 ) + // { + // group.Options.Add( option ); + // } + // } + // + // if( group.Options.Count > 0 ) + // { + // meta.Groups.Add( groupDir.Name, group ); + // } + //} + // + //var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); + //foreach( var collection in Penumbra.CollectionManager ) + //{ + // collection.Settings[ idx ]?.FixInvalidSettings( meta ); + //} + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs new file mode 100644 index 00000000..9007bb2d --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor : IDisposable + { + private readonly Mod _mod; + + public Editor( Mod mod ) + { + _mod = mod; + _availableFiles = GetAvailablePaths( mod ); + _usedPaths = new SortedSet< FullPath >( mod.AllFiles ); + _missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) ); + _unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) ); + _subMod = _mod._default; + } + + public void Dispose() + { + DuplicatesFinished = true; + } + + // Does not delete the base directory itself even if it is completely empty at the end. + private static void ClearEmptySubDirectories( DirectoryInfo baseDir ) + { + foreach( var subDir in baseDir.GetDirectories() ) + { + ClearEmptySubDirectories( subDir ); + if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) + { + subDir.Delete(); + } + } + } + + // Apply a option action to all available option in a mod, including the default option. + private static void ApplyToAllOptions( Mod mod, Action< ISubMod, int, int > action ) + { + action( mod.Default, -1, 0 ); + foreach( var (group, groupIdx) in mod.Groups.WithIndex() ) + { + for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) + { + action( @group[ optionIdx ], groupIdx, optionIdx ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/Editor/ModCleanup.cs similarity index 89% rename from Penumbra/Mods/ModCleanup.cs rename to Penumbra/Mods/Editor/ModCleanup.cs index ed1afc6f..c19c1b1f 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/Editor/ModCleanup.cs @@ -1,17 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text.RegularExpressions; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.Import; -using Penumbra.Meta.Manipulations; -using Penumbra.Util; - namespace Penumbra.Mods; public partial class Mod @@ -202,15 +188,10 @@ public partial class Mod // // private readonly DirectoryInfo _baseDir; // private readonly ModMeta _mod; -// private SHA256? _hasher; + // // private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); // -// private SHA256 Sha() -// { -// _hasher ??= SHA256.Create(); -// return _hasher; -// } // // private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) // { @@ -366,47 +347,6 @@ public partial class Mod // return meta.Groups[ Duplicates ].Options.First(); // } // -// public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) -// { -// var dedup = new ModCleanup( baseDir, mod ); -// foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) -// { -// if( value.Count == 2 ) -// { -// if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) ) -// { -// dedup.ReplaceFile( value[ 0 ], value[ 1 ] ); -// } -// } -// else -// { -// var deleted = Enumerable.Repeat( false, value.Count ).ToArray(); -// var hashes = value.Select( dedup.ComputeHash ).ToArray(); -// -// for( var i = 0; i < value.Count; ++i ) -// { -// if( deleted[ i ] ) -// { -// continue; -// } -// -// for( var j = i + 1; j < value.Count; ++j ) -// { -// if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) -// { -// continue; -// } -// -// dedup.ReplaceFile( value[ i ], value[ j ] ); -// deleted[ j ] = true; -// } -// } -// } -// } -// -// CleanUpDuplicates( mod ); -// ClearEmptySubDirectories( dedup._baseDir ); -// } // // private void ReplaceFile( FileInfo f1, FileInfo f2 ) // { @@ -458,32 +398,8 @@ public partial class Mod // f2.Delete(); // } // -// public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) -// => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); // -// public static bool CompareHashes( byte[] f1, byte[] f2 ) -// => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); -// -// public byte[] ComputeHash( FileInfo f ) -// { -// var stream = File.OpenRead( f.FullName ); -// var ret = Sha().ComputeHash( stream ); -// stream.Dispose(); -// return ret; -// } -// -// // Does not delete the base directory itself even if it is completely empty at the end. -// public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) -// { -// foreach( var subDir in baseDir.GetDirectories() ) -// { -// ClearEmptySubDirectories( subDir ); -// if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) -// { -// subDir.Delete(); -// } -// } -// } + // // private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) // { diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index ec1a4beb..ea30845e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -283,8 +283,7 @@ public sealed partial class Mod return; } - subMod.ManipulationData.Clear(); - subMod.ManipulationData.UnionWith( manipulations ); + subMod.ManipulationData = manipulations; ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); } @@ -300,15 +299,26 @@ public sealed partial class Mod public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileData.Equals( replacements ) ) + if( subMod.FileData.SetEquals( replacements ) ) { return; } - subMod.FileData.SetTo( replacements ); + subMod.FileData = replacements; ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); } + public void OptionAddFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > additions ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + var oldCount = subMod.FileData.Count; + subMod.FileData.AddFrom( additions ); + if( oldCount != subMod.FileData.Count ) + { + ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); + } + } + public void OptionSetFileSwap( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); @@ -321,12 +331,12 @@ public sealed partial class Mod public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileSwapData.Equals( swaps ) ) + if( subMod.FileSwapData.SetEquals( swaps ) ) { return; } - subMod.FileSwapData.SetTo( swaps ); + subMod.FileSwapData = swaps; ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); } @@ -334,10 +344,9 @@ public sealed partial class Mod HashSet< MetaManipulation > manipulations, Dictionary< Utf8GamePath, FullPath > swaps ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - subMod.FileData.SetTo( replacements ); - subMod.ManipulationData.Clear(); - subMod.ManipulationData.UnionWith( manipulations ); - subMod.FileSwapData.SetTo( swaps ); + subMod.FileData = replacements; + subMod.ManipulationData = manipulations; + subMod.FileSwapData = swaps; ModOptionChanged.Invoke( ModOptionChangeType.OptionUpdated, mod, groupIdx, optionIdx, -1 ); } @@ -361,6 +370,11 @@ public sealed partial class Mod private static SubMod GetSubMod( Mod mod, int groupIdx, int optionIdx ) { + if( groupIdx == -1 && optionIdx == 0 ) + { + return mod._default; + } + return mod._groups[ groupIdx ] switch { SingleModGroup s => s.OptionData[ optionIdx ], @@ -406,7 +420,14 @@ public sealed partial class Mod } else { - IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx ); + if( groupIdx == -1 ) + { + mod.SaveDefaultMod(); + } + else + { + IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx ); + } } // State can not change on adding groups, as they have no immediate options. diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 211d2574..392e7751 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -110,6 +110,13 @@ public partial class Mod return mod; } + // Create an empty sub mod for single groups with None options. + internal static ISubMod CreateEmptySubMod( string name ) + => new SubMod() + { + Name = name, + }; + // Create the default data file from all unused files that were not handled before // and are used in sub mods. internal static void CreateDefaultFiles( DirectoryInfo directory ) diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 8d14f2ea..a3700ec8 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -78,9 +78,6 @@ public partial class Mod => !Penumbra.Config.DisableSoundStreaming && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); - public List< FullPath > FindMissingFiles() - => AllFiles.Where( f => !f.Exists ).ToList(); - private static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) { if( !File.Exists( file.FullName ) ) diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 3748fdeb..85379873 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -69,9 +69,9 @@ public partial class Mod { public string Name { get; set; } = "Default"; - public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); - public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); - public readonly HashSet< MetaManipulation > ManipulationData = new(); + public Dictionary< Utf8GamePath, FullPath > FileData = new(); + public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + public HashSet< MetaManipulation > ManipulationData = new(); public IReadOnlyDictionary< Utf8GamePath, FullPath > Files => FileData; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 94b22f86..a0f1025b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -129,7 +129,7 @@ public class Penumbra : IDalamudPlugin btn = new LaunchButton( _configWindow ); system = new WindowSystem( Name ); system.AddWindow( _configWindow ); - system.AddWindow( cfg.SubModPopup ); + system.AddWindow( cfg.ModEditPopup ); Dalamud.PluginInterface.UiBuilder.Draw += system.Draw; Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; } diff --git a/Penumbra/Penumbra.csproj.DotSettings b/Penumbra/Penumbra.csproj.DotSettings index b43e7ec2..4e906820 100644 --- a/Penumbra/Penumbra.csproj.DotSettings +++ b/Penumbra/Penumbra.csproj.DotSettings @@ -1,3 +1,4 @@  + True True True \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs new file mode 100644 index 00000000..3dacf25b --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -0,0 +1,322 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; + +namespace Penumbra.UI.Classes; + +public class ModEditWindow : Window, IDisposable +{ + private const string WindowBaseLabel = "###SubModEdit"; + private Mod.Editor? _editor; + private Mod? _mod; + + public void ChangeMod( Mod mod ) + { + if( mod == _mod ) + { + return; + } + + _editor?.Dispose(); + _editor = new Mod.Editor( mod ); + _mod = mod; + WindowName = $"{mod.Name}{WindowBaseLabel}"; + } + + public void ChangeOption( int groupIdx, int optionIdx ) + => _editor?.SetSubMod( groupIdx, optionIdx ); + + public override bool DrawConditions() + => _editor != null; + + public override void Draw() + { + using var tabBar = ImRaii.TabBar( "##tabs" ); + if( !tabBar ) + { + return; + } + + DrawFileTab(); + DrawMetaTab(); + DrawSwapTab(); + DrawMissingFilesTab(); + DrawUnusedFilesTab(); + DrawDuplicatesTab(); + } + + private void DrawMissingFilesTab() + { + using var tab = ImRaii.TabItem( "Missing Files" ); + if( !tab ) + { + return; + } + + if( _editor!.MissingPaths.Count == 0 ) + { + ImGui.TextUnformatted( "No missing files detected." ); + } + else + { + if( ImGui.Button( "Remove Missing Files from Mod" ) ) + { + _editor.RemoveMissingPaths(); + } + + using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !table ) + { + return; + } + + foreach( var path in _editor.MissingPaths ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( path.FullName ); + } + } + } + + private void DrawDuplicatesTab() + { + using var tab = ImRaii.TabItem( "Duplicates" ); + if( !tab ) + { + return; + } + + var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton"; + if( ImGuiUtil.DrawDisabledButton( buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", + !_editor.DuplicatesFinished ) ) + { + _editor.StartDuplicateCheck(); + } + + if( !_editor.DuplicatesFinished ) + { + ImGui.SameLine(); + if( ImGui.Button( "Cancel" ) ) + { + _editor.Cancel(); + } + + return; + } + + if( _editor.Duplicates.Count == 0 ) + { + ImGui.TextUnformatted( "No duplicates found." ); + } + + if( ImGui.Button( "Delete and Redirect Duplicates" ) ) + { + _editor.DeleteDuplicates(); + } + + if( _editor.SavedSpace > 0 ) + { + ImGui.SameLine(); + ImGui.TextUnformatted( $"Frees up {Functions.HumanReadableSize( _editor.SavedSpace )} from your hard drive." ); + } + + using var child = ImRaii.Child( "##duptable", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); + if( !table ) + { + return; + } + + var width = ImGui.CalcTextSize( "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN " ).X; + ImGui.TableSetupColumn( "file", ImGuiTableColumnFlags.WidthStretch ); + ImGui.TableSetupColumn( "size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "NNN.NNN " ).X ); + ImGui.TableSetupColumn( "hash", ImGuiTableColumnFlags.WidthFixed, + ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize( "NNNNNNNN... " ).X ); + foreach( var (set, size, hash) in _editor.Duplicates.Where( s => s.Paths.Length > 1 ) ) + { + ImGui.TableNextColumn(); + using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], + ImGuiTreeNodeFlags.NoTreePushOnOpen ); + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) ); + ImGui.TableNextColumn(); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + if( ImGui.GetWindowWidth() > 2 * width ) + { + ImGuiUtil.RightAlign( string.Concat( hash.Select( b => b.ToString( "X2" ) ) ) ); + } + else + { + ImGuiUtil.RightAlign( string.Concat( hash.Take( 4 ).Select( b => b.ToString( "X2" ) ) ) + "..." ); + } + } + + if( !tree ) + { + continue; + } + + using var indent = ImRaii.PushIndent(); + foreach( var duplicate in set.Skip( 1 ) ) + { + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + } + } + } + + private void DrawUnusedFilesTab() + { + using var tab = ImRaii.TabItem( "Unused Files" ); + if( !tab ) + { + return; + } + + if( _editor!.UnusedFiles.Count == 0 ) + { + ImGui.TextUnformatted( "No unused files detected." ); + } + else + { + if( ImGui.Button( "Add Unused Files to Default" ) ) + { + _editor.AddUnusedPathsToDefault(); + } + + if( ImGui.Button( "Delete Unused Files from Filesystem" ) ) + { + _editor.DeleteUnusedPaths(); + } + + using var table = ImRaii.Table( "##unusedFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !table ) + { + return; + } + + foreach( var path in _editor.UnusedFiles ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( path.FullName ); + } + } + } + + + private void DrawFileTab() + { + using var tab = ImRaii.TabItem( "File Redirections" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##files", 2 ); + if( !list ) + { + return; + } + + foreach( var (gamePath, file) in _editor!.CurrentFiles ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( gamePath.Path ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( file.FullName ); + } + } + + private void DrawMetaTab() + { + using var tab = ImRaii.TabItem( "Meta Manipulations" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##meta", 3 ); + if( !list ) + { + return; + } + + foreach( var manip in _editor!.CurrentManipulations ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( manip.ManipulationType.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( manip.ManipulationType switch + { + MetaManipulation.Type.Imc => manip.Imc.ToString(), + MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), + MetaManipulation.Type.Eqp => manip.Eqp.ToString(), + MetaManipulation.Type.Est => manip.Est.ToString(), + MetaManipulation.Type.Gmp => manip.Gmp.ToString(), + MetaManipulation.Type.Rsp => manip.Rsp.ToString(), + _ => string.Empty, + } ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( manip.ManipulationType switch + { + MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), + MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), + MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(), + MetaManipulation.Type.Est => manip.Est.Entry.ToString(), + MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(), + MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(), + _ => string.Empty, + } ); + } + } + + private void DrawSwapTab() + { + using var tab = ImRaii.TabItem( "File Swaps" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##swaps", 3 ); + if( !list ) + { + return; + } + + foreach( var (gamePath, file) in _editor!.CurrentSwaps ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( gamePath.Path ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( file.FullName ); + } + } + + public ModEditWindow() + : base( WindowBaseLabel ) + { } + + public void Dispose() + { + _editor?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/SubModEditWindow.cs b/Penumbra/UI/Classes/SubModEditWindow.cs deleted file mode 100644 index b56897f5..00000000 --- a/Penumbra/UI/Classes/SubModEditWindow.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using OtterGui.Raii; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.Util; - -namespace Penumbra.UI.Classes; - -public class SubModEditWindow : Window -{ - private const string WindowBaseLabel = "###SubModEdit"; - private Mod? _mod; - private int _groupIdx = -1; - private int _optionIdx = -1; - private IModGroup? _group; - private ISubMod? _subMod; - private readonly List< FilePathInfo > _availableFiles = new(); - - private readonly struct FilePathInfo - { - public readonly FullPath File; - public readonly Utf8RelPath RelFile; - public readonly long Size; - public readonly List< (int, int, Utf8GamePath) > SubMods; - - public FilePathInfo( FileInfo file, Mod mod ) - { - File = new FullPath( file ); - RelFile = Utf8RelPath.FromFile( File, mod.BasePath, out var f ) ? f : Utf8RelPath.Empty; - Size = file.Length; - SubMods = new List< (int, int, Utf8GamePath) >(); - var path = File; - foreach( var (group, groupIdx) in mod.Groups.WithIndex() ) - { - foreach( var (subMod, optionIdx) in group.WithIndex() ) - { - SubMods.AddRange( subMod.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => ( groupIdx, optionIdx, kvp.Key ) ) ); - } - } - SubMods.AddRange( mod.Default.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => (-1, 0, kvp.Key) ) ); - } - } - - private readonly HashSet< MetaManipulation > _manipulations = new(); - private readonly Dictionary< Utf8GamePath, FullPath > _files = new(); - private readonly Dictionary< Utf8GamePath, FullPath > _fileSwaps = new(); - - public void Activate( Mod mod, int groupIdx, int optionIdx ) - { - IsOpen = true; - _mod = mod; - _groupIdx = groupIdx; - _group = groupIdx >= 0 ? mod.Groups[ groupIdx ] : null; - _optionIdx = optionIdx; - _subMod = groupIdx >= 0 ? _group![ optionIdx ] : _mod.Default; - _availableFiles.Clear(); - _availableFiles.AddRange( mod.BasePath.EnumerateDirectories() - .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - .Select( f => new FilePathInfo( f, _mod ) ) ); - - _manipulations.Clear(); - _manipulations.UnionWith( _subMod.Manipulations ); - _files.SetTo( _subMod.Files ); - _fileSwaps.SetTo( _subMod.FileSwaps ); - - WindowName = $"{_mod.Name}: {(_group != null ? $"{_group.Name} - " : string.Empty)}{_subMod.Name}"; - } - - public override bool DrawConditions() - => _subMod != null; - - public override void Draw() - { - using var tabBar = ImRaii.TabBar( "##tabs" ); - if( !tabBar ) - { - return; - } - - DrawFileTab(); - DrawMetaTab(); - DrawSwapTab(); - } - - private void Save() - { - if( _mod != null ) - { - Penumbra.ModManager.OptionUpdate( _mod, _groupIdx, _optionIdx, _files, _manipulations, _fileSwaps ); - } - } - - public override void OnClose() - { - _subMod = null; - } - - private void DrawFileTab() - { - using var tab = ImRaii.TabItem( "File Redirections" ); - if( !tab ) - { - return; - } - - using var list = ImRaii.Table( "##files", 3 ); - if( !list ) - { - return; - } - - foreach( var file in _availableFiles ) - { - ImGui.TableNextColumn(); - ConfigWindow.Text( file.RelFile.Path ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( file.Size.ToString() ); - ImGui.TableNextColumn(); - if( file.SubMods.Count == 0 ) - { - ImGui.TextUnformatted( "Unused" ); - } - - foreach( var (groupIdx, optionIdx, gamePath) in file.SubMods ) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - var group = groupIdx >= 0 ? _mod!.Groups[ groupIdx ] : null; - var option = groupIdx >= 0 ? group![ optionIdx ] : _mod!.Default; - var text = groupIdx >= 0 - ? $"{group!.Name} - {option.Name}" - : option.Name; - ImGui.TextUnformatted( text ); - ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); - } - } - - ImGui.TableNextRow(); - foreach( var (gamePath, fullPath) in _files ) - { - ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( fullPath.FullName ); - ImGui.TableNextColumn(); - } - } - - private void DrawMetaTab() - { - using var tab = ImRaii.TabItem( "Meta Manipulations" ); - if( !tab ) - { - return; - } - - using var list = ImRaii.Table( "##meta", 3 ); - if( !list ) - { - return; - } - - foreach( var manip in _manipulations ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.ToString(), - MetaManipulation.Type.Est => manip.Est.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.ToString(), - _ => string.Empty, - } ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(), - MetaManipulation.Type.Est => manip.Est.Entry.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(), - _ => string.Empty, - } ); - } - } - - private void DrawSwapTab() - { - using var tab = ImRaii.TabItem( "File Swaps" ); - if( !tab ) - { - return; - } - - using var list = ImRaii.Table( "##swaps", 3 ); - if( !list ) - { - return; - } - - foreach( var (from, to) in _fileSwaps ) - { - ImGui.TableNextColumn(); - ConfigWindow.Text( from.Path ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( to.FullName ); - ImGui.TableNextColumn(); - } - } - - public SubModEditWindow() - : base( WindowBaseLabel ) - { } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index eb64503a..a453449b 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -158,7 +158,9 @@ public partial class ConfigWindow if( ImGui.Button( "Edit Default Mod", reducedSize ) ) { - _window.SubModPopup.Activate( _mod, -1, 0 ); + _window.ModEditPopup.ChangeMod( _mod ); + _window.ModEditPopup.ChangeOption( -1, 0 ); + _window.ModEditPopup.IsOpen = true; } ImGui.SameLine(); @@ -180,6 +182,7 @@ public partial class ConfigWindow private int _currentField = -1; private int _optionIndex = -1; + private int _newOptionNameIdx = -1; private string _newGroupName = string.Empty; private string _newOptionName = string.Empty; private string _newDescription = string.Empty; @@ -287,10 +290,15 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##newOption", "Add new option...", ref _newOptionName, 256 ); + var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; + if( ImGui.InputTextWithHint( "##newOption", "Add new option...", ref tmp, 256 ) ) + { + _newOptionName = tmp; + _newOptionNameIdx = groupIdx; + } ImGui.TableNextColumn(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, - "Add a new option to this group.", _newOptionName.Length == 0, true ) ) + "Add a new option to this group.", _newOptionName.Length == 0 || _newOptionNameIdx != groupIdx, true ) ) { Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName ); _newOptionName = string.Empty; @@ -385,7 +393,9 @@ public partial class ConfigWindow if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, "Edit this option.", false, true ) ) { - _window.SubModPopup.Activate( _mod, groupIdx, optionIdx ); + _window.ModEditPopup.ChangeMod( _mod ); + _window.ModEditPopup.ChangeOption( groupIdx, optionIdx ); + _window.ModEditPopup.IsOpen = true; } ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index e3574c99..b9d80489 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -105,6 +105,7 @@ public partial class ConfigWindow ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) { + _currentPriority = priority; } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 48fd594a..1652c414 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -21,7 +21,7 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly EffectiveTab _effectiveTab; private readonly DebugTab _debugTab; private readonly ResourceTab _resourceTab; - public readonly SubModEditWindow SubModPopup = new(); + public readonly ModEditWindow ModEditPopup = new(); public ConfigWindow( Penumbra penumbra ) : base( GetLabel() ) @@ -70,6 +70,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { _selector.Dispose(); _modPanel.Dispose(); + ModEditPopup.Dispose(); } private static string GetLabel() diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index 97e2638a..2aeb44e5 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; namespace Penumbra.Util; @@ -7,6 +10,12 @@ public static class DictionaryExtensions // Returns whether two dictionaries contain equal keys and values. public static bool SetEquals< TKey, TValue >( this IReadOnlyDictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) { + if( ReferenceEquals( lhs, rhs ) ) + { + return true; + } + + if( lhs.Count != rhs.Count ) { return false; @@ -42,6 +51,11 @@ public static class DictionaryExtensions public static void SetTo< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) where TKey : notnull { + if( ReferenceEquals( lhs, rhs ) ) + { + return; + } + lhs.Clear(); lhs.EnsureCapacity( rhs.Count ); foreach( var (key, value) in rhs ) @@ -49,4 +63,34 @@ public static class DictionaryExtensions lhs.Add( key, value ); } } + + // Add all entries from the other dictionary that would not overwrite current keys. + public static void AddFrom< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) + where TKey : notnull + { + if( ReferenceEquals( lhs, rhs ) ) + { + return; + } + + lhs.EnsureCapacity( lhs.Count + rhs.Count ); + foreach( var (key, value) in rhs ) + { + lhs.Add( key, value ); + } + } + + public static int ReplaceValue< TKey, TValue >( this Dictionary< TKey, TValue > dict, TValue from, TValue to ) + where TKey : notnull + where TValue : IEquatable< TValue > + { + var count = 0; + foreach( var (key, _) in dict.ToArray().Where( kvp => kvp.Value.Equals( from ) ) ) + { + dict[ key ] = to; + ++count; + } + + return count; + } } \ No newline at end of file