diff --git a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs deleted file mode 100644 index 2e1a4ab2..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs +++ /dev/null @@ -1,70 +0,0 @@ -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.Normalization.cs b/Penumbra/Mods/Editor/Mod.Normalization.cs new file mode 100644 index 00000000..73a92a09 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Normalization.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public void Normalize( Manager manager ) + => ModNormalizer.Normalize( manager, this ); + + private struct ModNormalizer + { + private readonly Mod _mod; + private readonly string _normalizationDirName; + private readonly string _oldDirName; + private Dictionary< Utf8GamePath, FullPath >[][]? _redirections = null; + + private ModNormalizer( Mod mod ) + { + _mod = mod; + _normalizationDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalization" ); + _oldDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalizationOld" ); + } + + public static void Normalize( Manager manager, Mod mod ) + { + var normalizer = new ModNormalizer( mod ); + try + { + Penumbra.Log.Debug( $"[Normalization] Starting Normalization of {mod.ModPath.Name}..." ); + if( !normalizer.CheckDirectories() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Copying files to temporary directory structure..." ); + if( !normalizer.CopyNewFiles() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Moving old files out of the way..." ); + if( !normalizer.MoveOldFiles() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Moving new directory structure in place..." ); + if( !normalizer.MoveNewFiles() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Applying new redirections..." ); + normalizer.ApplyRedirections( manager ); + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + } + finally + { + Penumbra.Log.Debug( "[Normalization] Cleaning up remaining directories..." ); + normalizer.Cleanup(); + } + } + + private bool CheckDirectories() + { + if( Directory.Exists( _normalizationDirName ) ) + { + ChatUtil.NotificationMessage( "Could not normalize mod:\n" + + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", + NotificationType.Error ); + return false; + } + + if( Directory.Exists( _oldDirName ) ) + { + ChatUtil.NotificationMessage( "Could not normalize mod:\n" + + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", + NotificationType.Error ); + return false; + } + + return true; + } + + private void Cleanup() + { + if( Directory.Exists( _normalizationDirName ) ) + { + try + { + Directory.Delete( _normalizationDirName, true ); + } + catch + { + // ignored + } + } + + if( Directory.Exists( _oldDirName ) ) + { + try + { + foreach( var dir in new DirectoryInfo( _oldDirName ).EnumerateDirectories() ) + { + dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) ); + } + + Directory.Delete( _oldDirName, true ); + } + catch + { + // ignored + } + } + } + + private bool CopyNewFiles() + { + // We copy all files to a temporary folder to ensure that we can revert the operation on failure. + try + { + var directory = Directory.CreateDirectory( _normalizationDirName ); + _redirections = new Dictionary< Utf8GamePath, FullPath >[_mod.Groups.Count + 1][]; + _redirections[ 0 ] = new Dictionary< Utf8GamePath, FullPath >[] { new(_mod.Default.Files.Count) }; + + // Normalize the default option. + var newDict = new Dictionary< Utf8GamePath, FullPath >( _mod.Default.Files.Count ); + _redirections[ 0 ][ 0 ] = newDict; + foreach( var (gamePath, fullPath) in _mod._default.FileData ) + { + var relPath = new Utf8RelPath( gamePath ).ToString(); + var newFullPath = Path.Combine( directory.FullName, relPath ); + var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, relPath ) ); + Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! ); + File.Copy( fullPath.FullName, newFullPath, true ); + newDict.Add( gamePath, redirectPath ); + } + + // Normalize all other options. + foreach( var (group, groupIdx) in _mod.Groups.WithIndex() ) + { + _redirections[ groupIdx + 1 ] = new Dictionary< Utf8GamePath, FullPath >[group.Count]; + var groupDir = CreateModFolder( directory, group.Name ); + + foreach( var option in group.OfType< SubMod >() ) + { + var optionDir = CreateModFolder( groupDir, option.Name ); + newDict = new Dictionary< Utf8GamePath, FullPath >( option.FileData.Count ); + _redirections[ groupIdx + 1 ][ option.OptionIdx ] = newDict; + foreach( var (gamePath, fullPath) in option.FileData ) + { + var relPath = new Utf8RelPath( gamePath ).ToString(); + var newFullPath = Path.Combine( optionDir.FullName, relPath ); + var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath ) ); + Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! ); + File.Copy( fullPath.FullName, newFullPath, true ); + newDict.Add( gamePath, redirectPath ); + } + } + } + + return true; + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + _redirections = null; + } + + return false; + } + + private bool MoveOldFiles() + { + try + { + // Clean old directories and files. + var oldDirectory = Directory.CreateDirectory( _oldDirName ); + foreach( var dir in _mod.ModPath.EnumerateDirectories() ) + { + if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) + || dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) ) + { + continue; + } + + dir.MoveTo( Path.Combine( oldDirectory.FullName, dir.Name ) ); + } + + return true; + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + } + + return false; + } + + private bool MoveNewFiles() + { + try + { + var mainDir = new DirectoryInfo( _normalizationDirName ); + foreach( var dir in mainDir.EnumerateDirectories() ) + { + dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) ); + } + + mainDir.Delete(); + Directory.Delete( _oldDirName, true ); + return true; + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + foreach( var dir in _mod.ModPath.EnumerateDirectories() ) + { + if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) + || dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) ) + { + continue; + } + + try + { + dir.Delete( true ); + } + catch + { + // ignored + } + } + } + + return false; + } + + private void ApplyRedirections( Manager manager ) + { + if( _redirections == null ) + { + return; + } + + foreach( var option in _mod.AllSubMods.OfType< SubMod >() ) + { + manager.OptionSetFiles( _mod, option.GroupIdx, option.OptionIdx, _redirections[ option.GroupIdx + 1 ][ option.OptionIdx ] ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModCleanup.cs b/Penumbra/Mods/Editor/ModCleanup.cs deleted file mode 100644 index a2e7dec0..00000000 --- a/Penumbra/Mods/Editor/ModCleanup.cs +++ /dev/null @@ -1,629 +0,0 @@ -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Manager - { - //public class Normalizer - //{ - // private Dictionary< Utf8GamePath, (FullPath Path, int GroupPriority) > Files = new(); - // private Dictionary< Utf8GamePath, (FullPath Path, int GroupPriority) > Swaps = new(); - // private HashSet< (MetaManipulation Manipulation, int GroupPriority) > Manips = new(); - // - // public Normalizer( Mod mod ) - // { - // // Default changes are irrelevant since they can only be overwritten. - // foreach( var group in mod.Groups ) - // { - // foreach( var option in group ) - // { - // foreach( var (key, value) in option.Files ) - // { - // if( !Files.TryGetValue( key, out var list ) ) - // { - // list = new List< (FullPath Path, IModGroup Group, ISubMod Option) > { ( value, @group, option ) }; - // Files[ key ] = list; - // } - // else - // { - // list.Add( ( value, @group, option ) ); - // } - // } - // } - // } - // } - // - // // Normalize a mod, this entails: - // // - If - // public static void Normalize( Mod mod ) - // { - // NormalizeOptions( mod ); - // MergeSingleGroups( mod ); - // DeleteEmptyGroups( mod ); - // } - // - // - // // Delete every option group that has either no options, - // // or exclusively empty options. - // // Triggers changes through calling ModManager. - // private static void DeleteEmptyGroups( Mod mod ) - // { - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // DeleteIdenticalOptions( mod, i ); - // var group = mod.Groups[ i ]; - // if( group.Count == 0 || group.All( o => o.FileSwaps.Count == 0 && o.Files.Count == 0 && o.Manipulations.Count == 0 ) ) - // { - // Penumbra.ModManager.DeleteModGroup( mod, i-- ); - // } - // } - // } - // - // // Merge every non-optional group into the default mod. - // // Overwrites default mod entries if necessary. - // // Deletes the non-optional group afterwards. - // // Triggers changes through calling ModManager. - // private static void MergeSingleGroup( Mod mod ) - // { - // var defaultMod = ( SubMod )mod.Default; - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // var group = mod.Groups[ i ]; - // if( group.Type == SelectType.Single && group.Count == 1 ) - // { - // defaultMod.MergeIn( group[ 0 ] ); - // - // Penumbra.ModManager.DeleteModGroup( mod, i-- ); - // } - // } - // } - // - // private static void NotifyChanges( Mod mod, int groupIdx, ModOptionChangeType type, ref bool anyChanges ) - // { - // if( anyChanges ) - // { - // for( var i = 0; i < mod.Groups[ groupIdx ].Count; ++i ) - // { - // Penumbra.ModManager.ModOptionChanged.Invoke( type, mod, groupIdx, i, -1 ); - // } - // - // anyChanges = false; - // } - // } - // - // private static void NormalizeOptions( Mod mod ) - // { - // var defaultMod = ( SubMod )mod.Default; - // - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // var group = mod.Groups[ i ]; - // if( group.Type == SelectType.Multi || group.Count < 2 ) - // { - // continue; - // } - // - // var firstOption = mod.Groups[ i ][ 0 ]; - // var anyChanges = false; - // foreach( var (key, value) in firstOption.Files.ToList() ) - // { - // if( group.Skip( 1 ).All( o => o.Files.TryGetValue( key, out var v ) && v.Equals( value ) ) ) - // { - // anyChanges = true; - // defaultMod.FileData[ key ] = value; - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.FileData.Remove( key ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionFilesChanged, ref anyChanges ); - // - // foreach( var (key, value) in firstOption.FileSwaps.ToList() ) - // { - // if( group.Skip( 1 ).All( o => o.FileSwaps.TryGetValue( key, out var v ) && v.Equals( value ) ) ) - // { - // anyChanges = true; - // defaultMod.FileData[ key ] = value; - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.FileSwapData.Remove( key ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionSwapsChanged, ref anyChanges ); - // - // anyChanges = false; - // foreach( var manip in firstOption.Manipulations.ToList() ) - // { - // if( group.Skip( 1 ).All( o => ( ( HashSet< MetaManipulation > )o.Manipulations ).TryGetValue( manip, out var m ) - // && manip.EntryEquals( m ) ) ) - // { - // anyChanges = true; - // defaultMod.ManipulationData.Remove( manip ); - // defaultMod.ManipulationData.Add( manip ); - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.ManipulationData.Remove( manip ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionMetaChanged, ref anyChanges ); - // } - // } - // - // - // // Delete all options that are entirely identical. - // // Deletes the later occurring option. - // private static void DeleteIdenticalOptions( Mod mod, int groupIdx ) - // { - // var group = mod.Groups[ groupIdx ]; - // for( var i = 0; i < group.Count; ++i ) - // { - // var option = group[ i ]; - // for( var j = i + 1; j < group.Count; ++j ) - // { - // var option2 = group[ j ]; - // if( option.Files.SetEquals( option2.Files ) - // && option.FileSwaps.SetEquals( option2.FileSwaps ) - // && option.Manipulations.SetEquals( option2.Manipulations ) ) - // { - // Penumbra.ModManager.DeleteOption( mod, groupIdx, j-- ); - // } - // } - // } - // } - //} - } -} - -// TODO Everything -//ublic class ModCleanup -// -// private const string Duplicates = "Duplicates"; -// private const string Required = "Required"; -// -// private readonly DirectoryInfo _baseDir; -// private readonly ModMeta _mod; - -// -// private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); -// -// -// private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) -// { -// _baseDir = baseDir; -// _mod = mod; -// BuildDict(); -// } -// -// private void BuildDict() -// { -// foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) -// { -// var fileLength = file.Length; -// if( _filesBySize.TryGetValue( fileLength, out var files ) ) -// { -// files.Add( file ); -// } -// else -// { -// _filesBySize[ fileLength ] = new List< FileInfo > { file }; -// } -// } -// } -// -// private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option ) -// { -// var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; -// return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); -// } -// -// private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder ) -// { -// var idx = Penumbra.ModManager.AddMod( newDir ); -// var newMod = Penumbra.ModManager.Mods[ idx ]; -// newMod.Move( newSortOrder ); -// newMod.ComputeChangedItems(); -// ModFileSystem.InvokeChange(); -// return newMod; -// } -// -// private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option ) -// { -// var newMeta = new ModMeta -// { -// Author = mod.Meta.Author, -// Name = name, -// Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", -// }; -// var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); -// newMeta.SaveToFile( metaFile ); -// return newMeta; -// } -// -// private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option ) -// { -// try -// { -// var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName ); -// var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; -// var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName ); -// foreach( var (fileName, paths) in option.OptionFiles ) -// { -// var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() ); -// unseenPaths.Remove( oldPath ); -// if( File.Exists( oldPath ) ) -// { -// foreach( var path in paths ) -// { -// var newPath = Path.Combine( newDir.FullName, path.ToString() ); -// Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); -// File.Copy( oldPath, newPath, true ); -// } -// } -// } -// -// var newSortOrder = group.SelectionType == SelectType.Single -// ? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" -// : $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; -// CreateNewMod( newDir, newSortOrder ); -// } -// catch( Exception e ) -// { -// Penumbra.Log.Error( $"Could not split Mod:\n{e}" ); -// } -// } -// -// public static void SplitMod( Mod mod ) -// { -// if( mod.Meta.Groups.Count == 0 ) -// { -// return; -// } -// -// var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); -// foreach( var group in mod.Meta.Groups.Values ) -// { -// foreach( var option in group.Options ) -// { -// CreateModSplit( unseenPaths, mod, group, option ); -// } -// } -// -// if( unseenPaths.Count == 0 ) -// { -// return; -// } -// -// var defaultGroup = new OptionGroup() -// { -// GroupName = "Default", -// SelectionType = SelectType.Multi, -// }; -// var defaultOption = new Option() -// { -// OptionName = "Files", -// OptionFiles = unseenPaths.ToDictionary( -// p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty, -// p => new HashSet< Utf8GamePath >() -// { Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ), -// }; -// CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); -// } -// -// private static Option FindOrCreateDuplicates( ModMeta meta ) -// { -// static Option RequiredOption() -// => new() -// { -// OptionName = Required, -// OptionDesc = "", -// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), -// }; -// -// if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) -// { -// var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); -// if( idx >= 0 ) -// { -// return duplicates.Options[ idx ]; -// } -// -// duplicates.Options.Add( RequiredOption() ); -// return duplicates.Options.Last(); -// } -// -// meta.Groups.Add( Duplicates, new OptionGroup -// { -// GroupName = Duplicates, -// SelectionType = SelectType.Single, -// Options = new List< Option > { RequiredOption() }, -// } ); -// -// return meta.Groups[ Duplicates ].Options.First(); -// } -// -// -// private void ReplaceFile( FileInfo f1, FileInfo f2 ) -// { -// if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 ) -// || !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) ) -// { -// return; -// } -// -// var inOption1 = false; -// var inOption2 = false; -// foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) -// { -// if( option.OptionFiles.ContainsKey( relName1 ) ) -// { -// inOption1 = true; -// } -// -// if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) -// { -// continue; -// } -// -// inOption2 = true; -// -// foreach( var value in values ) -// { -// option.AddFile( relName1, value ); -// } -// -// option.OptionFiles.Remove( relName2 ); -// } -// -// if( !inOption1 || !inOption2 ) -// { -// var duplicates = FindOrCreateDuplicates( _mod ); -// if( !inOption1 ) -// { -// duplicates.AddFile( relName1, relName2.ToGamePath() ); -// } -// -// if( !inOption2 ) -// { -// duplicates.AddFile( relName1, relName1.ToGamePath() ); -// } -// } -// -// Penumbra.Log.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); -// f2.Delete(); -// } -// -// - -// -// private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) -// { -// var groupEnumerator = exceptDuplicates -// ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) -// : meta.Groups.Values; -// return groupEnumerator.SelectMany( group => group.Options ) -// .Any( option => option.OptionFiles.ContainsKey( relPath ) ); -// } -// -// private static void CleanUpDuplicates( ModMeta meta ) -// { -// if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) -// { -// return; -// } -// -// var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); -// if( requiredIdx >= 0 ) -// { -// var required = info.Options[ requiredIdx ]; -// foreach( var (key, value) in required.OptionFiles.ToArray() ) -// { -// if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) ) -// { -// continue; -// } -// -// if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 ) -// { -// required.OptionFiles.Remove( key ); -// } -// } -// -// if( required.OptionFiles.Count == 0 ) -// { -// info.Options.RemoveAt( requiredIdx ); -// } -// } -// -// if( info.Options.Count == 0 ) -// { -// meta.Groups.Remove( Duplicates ); -// } -// } -// -// public enum GroupType -// { -// Both = 0, -// Single = 1, -// Multi = 2, -// }; -// -// private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both, -// bool skipDuplicates = true ) -// { -// if( meta.Groups.Count == 0 ) -// { -// return; -// } -// -// var enumerator = type switch -// { -// GroupType.Both => meta.Groups.Values, -// GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), -// GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), -// _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), -// }; -// foreach( var group in enumerator ) -// { -// var optionEnum = skipDuplicates -// ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) -// : group.Options; -// foreach( var option in optionEnum ) -// { -// if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) -// { -// option.OptionFiles.Remove( relPath ); -// } -// } -// } -// } -// -// public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath ) -// { -// if( oldRelPath.Equals( newRelPath ) ) -// { -// return true; -// } -// -// try -// { -// var newFullPath = Path.Combine( basePath, newRelPath.ToString() ); -// new FileInfo( newFullPath ).Directory!.Create(); -// File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath ); -// } -// catch( Exception e ) -// { -// Penumbra.Log.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); -// return false; -// } -// -// foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) -// { -// if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) -// { -// option.OptionFiles.Add( newRelPath, gamePaths ); -// option.OptionFiles.Remove( oldRelPath ); -// } -// } -// -// return true; -// } -// -// -// private static void RemoveUselessGroups( ModMeta meta ) -// { -// meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) -// .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); -// } -// -// // Goes through all Single-Select options and checks if file links are in each of them. -// // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). -// public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) -// { -// foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) -// { -// var firstOption = true; -// HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new(); -// foreach( var option in group.Options ) -// { -// HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new(); -// foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) -// { -// optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); -// } -// -// if( firstOption ) -// { -// groupList = optionList; -// } -// else -// { -// groupList.IntersectWith( optionList ); -// } -// -// firstOption = false; -// } -// -// var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >(); -// foreach( var (path, gamePath) in groupList ) -// { -// var relPath = new Utf8RelPath( gamePath ); -// if( newPath.TryGetValue( path, out var usedGamePath ) ) -// { -// var required = FindOrCreateDuplicates( meta ); -// var usedRelPath = new Utf8RelPath( usedGamePath ); -// required.AddFile( usedRelPath, gamePath ); -// required.AddFile( usedRelPath, usedGamePath ); -// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); -// } -// else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) -// { -// newPath[ path ] = gamePath; -// if( FileIsInAnyGroup( meta, relPath ) ) -// { -// FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); -// } -// -// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); -// } -// } -// } -// -// RemoveUselessGroups( meta ); -// ClearEmptySubDirectories( baseDir ); -// } -// -// public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) -// { -// meta.Groups.Clear(); -// ClearEmptySubDirectories( baseDir ); -// foreach( var groupDir in baseDir.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/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 5c7cd701..afb59c94 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -13,10 +13,17 @@ namespace Penumbra.Mods; public partial class Mod { - // Create and return a new directory based on the given directory and name, that is - // - Not Empty - // - Unique, by appending (digit) for duplicates. - // - Containing no symbols invalid for FFXIV or windows paths. + /// + /// Create and return a new directory based on the given directory and name, that is
+ /// - Not Empty.
+ /// - Unique, by appending (digit) for duplicates.
+ /// - Containing no symbols invalid for FFXIV or windows paths.
+ ///
+ /// + /// + /// + /// + /// internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) { var name = modListName; @@ -160,8 +167,9 @@ public partial class Mod { return replacement + replacement; } + StringBuilder sb = new(s.Length); - foreach( var c in s.Normalize(NormalizationForm.FormKC) ) + foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) { if( c.IsInvalidAscii() || c.IsInvalidInPath() ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index bbe21025..f33127b3 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -20,10 +20,11 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow : Window, IDisposable { - private const string WindowBaseLabel = "###SubModEdit"; - private Editor? _editor; - private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; + private const string WindowBaseLabel = "###SubModEdit"; + private Editor? _editor; + private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; + private bool _allowReduplicate = false; public void ChangeMod( Mod mod ) { @@ -118,6 +119,7 @@ public partial class ModEditWindow : Window, IDisposable sb.Append( $" | {swaps} Swaps" ); } + _allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0; sb.Append( WindowBaseLabel ); WindowName = sb.ToString(); } @@ -288,6 +290,15 @@ public partial class ModEditWindow : Window, IDisposable _editor.StartDuplicateCheck(); } + const string desc = "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + + "This will also delete all unused files and directories if it succeeds.\n" + + "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway."; + if( ImGuiUtil.DrawDisabledButton( "Re-Duplicate and Normalize Mod", Vector2.Zero, desc, !_allowReduplicate ) ) + { + _mod!.Normalize( Penumbra.ModManager ); + _editor.RevertFiles(); + } + if( !_editor.DuplicatesFinished ) { ImGui.SameLine();