mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Some Deduplicator fixes and introduce Normalize function, untested.
This commit is contained in:
parent
db4942224e
commit
565db65bde
3 changed files with 402 additions and 170 deletions
|
|
@ -1,168 +0,0 @@
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Models
|
|
||||||
{
|
|
||||||
public class Deduplicator
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Deduplicator( 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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run()
|
|
||||||
{
|
|
||||||
foreach( var pair in _filesBySize.Where( pair => pair.Value.Count >= 2 ) )
|
|
||||||
{
|
|
||||||
if( pair.Value.Count == 2 )
|
|
||||||
{
|
|
||||||
if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) )
|
|
||||||
{
|
|
||||||
ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray();
|
|
||||||
var hashes = pair.Value.Select( ComputeHash ).ToArray();
|
|
||||||
|
|
||||||
for( var i = 0; i < pair.Value.Count; ++i )
|
|
||||||
{
|
|
||||||
if( deleted[ i ] )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for( var j = i + 1; j < pair.Value.Count; ++j )
|
|
||||||
{
|
|
||||||
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReplaceFile( pair.Value[ i ], pair.Value[ j ] );
|
|
||||||
deleted[ j ] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearEmptySubDirectories( _baseDir );
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReplaceFile( FileInfo f1, FileInfo f2 )
|
|
||||||
{
|
|
||||||
RelPath relName1 = new( f1, _baseDir );
|
|
||||||
RelPath relName2 = new( f2, _baseDir );
|
|
||||||
|
|
||||||
var inOption = false;
|
|
||||||
foreach( var group in _mod.Groups.Select( g => g.Value.Options ) )
|
|
||||||
{
|
|
||||||
foreach( var option in group )
|
|
||||||
{
|
|
||||||
if( option.OptionFiles.TryGetValue( relName2, out var values ) )
|
|
||||||
{
|
|
||||||
inOption = true;
|
|
||||||
foreach( var value in values )
|
|
||||||
{
|
|
||||||
option.AddFile( relName1, value );
|
|
||||||
}
|
|
||||||
|
|
||||||
option.OptionFiles.Remove( relName2 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !inOption )
|
|
||||||
{
|
|
||||||
const string duplicates = "Duplicates";
|
|
||||||
if( !_mod.Groups.ContainsKey( duplicates ) )
|
|
||||||
{
|
|
||||||
OptionGroup info = new()
|
|
||||||
{
|
|
||||||
GroupName = duplicates,
|
|
||||||
SelectionType = SelectType.Single,
|
|
||||||
Options = new List< Option >()
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
OptionName = "Required",
|
|
||||||
OptionDesc = "",
|
|
||||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_mod.Groups.Add( duplicates, info );
|
|
||||||
}
|
|
||||||
|
|
||||||
_mod.Groups[ duplicates ].Options[ 0 ].AddFile( relName1, new GamePath( relName2 ) );
|
|
||||||
_mod.Groups[ duplicates ].Options[ 0 ].AddFile( relName1, new GamePath( relName1 ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
378
Penumbra/Models/ModCleanup.cs
Normal file
378
Penumbra/Models/ModCleanup.cs
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Models
|
||||||
|
{
|
||||||
|
public class ModCleanup
|
||||||
|
{
|
||||||
|
private const string Duplicates = "Duplicates";
|
||||||
|
private const string Required = "Required";
|
||||||
|
|
||||||
|
|
||||||
|
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 )
|
||||||
|
{
|
||||||
|
_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 Option FindOrCreateDuplicates( ModMeta meta )
|
||||||
|
{
|
||||||
|
static Option RequiredOption()
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
OptionName = Required,
|
||||||
|
OptionDesc = "",
|
||||||
|
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
|
||||||
|
{
|
||||||
|
var dedup = new ModCleanup( baseDir, mod );
|
||||||
|
foreach( var pair in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
|
||||||
|
{
|
||||||
|
if( pair.Value.Count == 2 )
|
||||||
|
{
|
||||||
|
if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) )
|
||||||
|
{
|
||||||
|
dedup.ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray();
|
||||||
|
var hashes = pair.Value.Select( dedup.ComputeHash ).ToArray();
|
||||||
|
|
||||||
|
for( var i = 0; i < pair.Value.Count; ++i )
|
||||||
|
{
|
||||||
|
if( deleted[ i ] )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for( var j = i + 1; j < pair.Value.Count; ++j )
|
||||||
|
{
|
||||||
|
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dedup.ReplaceFile( pair.Value[ i ], pair.Value[ j ] );
|
||||||
|
deleted[ j ] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanUpDuplicates( mod );
|
||||||
|
ClearEmptySubDirectories( dedup._baseDir );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReplaceFile( FileInfo f1, FileInfo f2 )
|
||||||
|
{
|
||||||
|
RelPath relName1 = new( f1, _baseDir );
|
||||||
|
RelPath relName2 = new( f2, _baseDir );
|
||||||
|
|
||||||
|
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, new GamePath( relName2, 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !inOption2 )
|
||||||
|
{
|
||||||
|
duplicates.AddFile( relName1, new GamePath( relName1, 0 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
|
||||||
|
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, RelPath 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 kvp in required.OptionFiles.ToArray() )
|
||||||
|
{
|
||||||
|
if( kvp.Value.Count > 1 || FileIsInAnyGroup( meta, kvp.Key, true ) )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( kvp.Value.Count == 0 || kvp.Value.First().CompareTo( new GamePath( kvp.Key, 0 ) ) == 0 )
|
||||||
|
{
|
||||||
|
required.OptionFiles.Remove( kvp.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, RelPath relPath, GamePath 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, RelPath oldRelPath, RelPath newRelPath )
|
||||||
|
{
|
||||||
|
if( oldRelPath == newRelPath )
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newFullPath = Path.Combine( basePath, newRelPath );
|
||||||
|
new FileInfo( newFullPath ).Directory!.Create();
|
||||||
|
File.Move( Path.Combine( basePath, oldRelPath ), newFullPath );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.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< (RelPath, GamePath) > groupList = new();
|
||||||
|
foreach( var option in group.Options )
|
||||||
|
{
|
||||||
|
HashSet< (RelPath, GamePath) > 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< RelPath, GamePath >();
|
||||||
|
foreach( var (path, gamePath) in groupList )
|
||||||
|
{
|
||||||
|
var relPath = new RelPath( gamePath );
|
||||||
|
if( newPath.TryGetValue( path, out var usedGamePath ) )
|
||||||
|
{
|
||||||
|
var required = FindOrCreateDuplicates( meta );
|
||||||
|
var usedRelPath = new RelPath( usedGamePath );
|
||||||
|
required.AddFile( usedRelPath, gamePath );
|
||||||
|
required.AddFile( usedRelPath, usedGamePath );
|
||||||
|
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single, true );
|
||||||
|
}
|
||||||
|
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, true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveUselessGroups( meta );
|
||||||
|
ClearEmptySubDirectories( baseDir );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ namespace Penumbra.UI
|
||||||
private const string ButtonEditJson = "Edit JSON";
|
private const string ButtonEditJson = "Edit JSON";
|
||||||
private const string ButtonReloadJson = "Reload JSON";
|
private const string ButtonReloadJson = "Reload JSON";
|
||||||
private const string ButtonDeduplicate = "Deduplicate";
|
private const string ButtonDeduplicate = "Deduplicate";
|
||||||
|
private const string ButtonNormalize = "Normalize";
|
||||||
private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer.";
|
private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer.";
|
||||||
private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application.";
|
private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application.";
|
||||||
private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json.";
|
private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json.";
|
||||||
|
|
@ -34,7 +35,10 @@ namespace Penumbra.UI
|
||||||
|
|
||||||
private const string TooltipDeduplicate =
|
private const string TooltipDeduplicate =
|
||||||
"Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n"
|
"Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n"
|
||||||
+ "Introduces an invisible single-option Group \"Duplicates\".";
|
+ "Introduces an invisible single-option Group \"Duplicates\".\nExperimental - use at own risk!";
|
||||||
|
|
||||||
|
private const string TooltipNormalize =
|
||||||
|
"Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!";
|
||||||
|
|
||||||
private const float HeaderLineDistance = 10f;
|
private const float HeaderLineDistance = 10f;
|
||||||
private static readonly Vector4 GreyColor = new( 1f, 1f, 1f, 0.66f );
|
private static readonly Vector4 GreyColor = new( 1f, 1f, 1f, 0.66f );
|
||||||
|
|
@ -389,7 +393,7 @@ namespace Penumbra.UI
|
||||||
{
|
{
|
||||||
if( ImGui.Button( ButtonDeduplicate ) )
|
if( ImGui.Button( ButtonDeduplicate ) )
|
||||||
{
|
{
|
||||||
new Deduplicator( Mod!.Mod.ModBasePath, Meta! ).Run();
|
ModCleanup.Deduplicate( Mod!.Mod.ModBasePath, Meta! );
|
||||||
_selector.SaveCurrentMod();
|
_selector.SaveCurrentMod();
|
||||||
Mod.Mod.RefreshModFiles();
|
Mod.Mod.RefreshModFiles();
|
||||||
Service< ModManager >.Get().CalculateEffectiveFileList();
|
Service< ModManager >.Get().CalculateEffectiveFileList();
|
||||||
|
|
@ -401,6 +405,22 @@ namespace Penumbra.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawNormalizeButton()
|
||||||
|
{
|
||||||
|
if( ImGui.Button( ButtonNormalize ) )
|
||||||
|
{
|
||||||
|
ModCleanup.Normalize( Mod!.Mod.ModBasePath, Meta! );
|
||||||
|
_selector.SaveCurrentMod();
|
||||||
|
Mod.Mod.RefreshModFiles();
|
||||||
|
Service< ModManager >.Get().CalculateEffectiveFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.IsItemHovered() )
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip( TooltipNormalize );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawEditLine()
|
private void DrawEditLine()
|
||||||
{
|
{
|
||||||
DrawOpenModFolderButton();
|
DrawOpenModFolderButton();
|
||||||
|
|
@ -412,6 +432,8 @@ namespace Penumbra.UI
|
||||||
DrawReloadJsonButton();
|
DrawReloadJsonButton();
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
DrawDeduplicateButton();
|
DrawDeduplicateButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawNormalizeButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue