mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 05:04:15 +01:00
Add file redirection editing.
This commit is contained in:
parent
06deddcd8a
commit
385ce4c7e9
8 changed files with 569 additions and 189 deletions
|
|
@ -14,8 +14,8 @@ 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();
|
||||
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;
|
||||
|
|
@ -44,7 +44,8 @@ public partial class Mod
|
|||
HandleDuplicate( duplicate, remaining );
|
||||
}
|
||||
}
|
||||
_availableFiles.RemoveAll( p => !p.Item1.Exists );
|
||||
|
||||
_availableFiles.RemoveAll( p => !p.File.Exists );
|
||||
_duplicates.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ public partial class Mod
|
|||
}
|
||||
|
||||
changes = true;
|
||||
PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to);
|
||||
PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to );
|
||||
return to;
|
||||
}
|
||||
|
||||
|
|
@ -92,26 +93,28 @@ public partial class Mod
|
|||
if( DuplicatesFinished )
|
||||
{
|
||||
DuplicatesFinished = false;
|
||||
Task.Run( CheckDuplicates );
|
||||
UpdateFiles();
|
||||
var files = _availableFiles.OrderByDescending(f => f.FileSize).ToArray();
|
||||
Task.Run( () => CheckDuplicates( files ) );
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckDuplicates()
|
||||
private void CheckDuplicates( IReadOnlyList< FileRegistry > files )
|
||||
{
|
||||
_duplicates.Clear();
|
||||
SavedSpace = 0;
|
||||
var list = new List< FullPath >();
|
||||
var lastSize = -1L;
|
||||
foreach( var (p, size) in AvailableFiles )
|
||||
foreach( var file in files )
|
||||
{
|
||||
if( DuplicatesFinished )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( size == lastSize )
|
||||
if( file.FileSize == lastSize )
|
||||
{
|
||||
list.Add( p );
|
||||
list.Add( file.File );
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -119,22 +122,25 @@ public partial class Mod
|
|||
{
|
||||
CheckMultiDuplicates( list, lastSize );
|
||||
}
|
||||
lastSize = size;
|
||||
|
||||
lastSize = file.FileSize;
|
||||
|
||||
list.Clear();
|
||||
list.Add( p );
|
||||
list.Add( file.File );
|
||||
}
|
||||
|
||||
if( list.Count >= 2 )
|
||||
{
|
||||
CheckMultiDuplicates( list, lastSize );
|
||||
}
|
||||
|
||||
_duplicates.Sort( ( a, b ) => a.Size != b.Size ? b.Size.CompareTo( a.Size ) : a.Paths[ 0 ].CompareTo( b.Paths[ 0 ] ) );
|
||||
DuplicatesFinished = true;
|
||||
}
|
||||
|
||||
private void CheckMultiDuplicates( IReadOnlyList< FullPath > list, long size )
|
||||
{
|
||||
var hashes = list.Select( f => (f, ComputeHash(f)) ).ToList();
|
||||
var hashes = list.Select( f => ( f, ComputeHash( f ) ) ).ToList();
|
||||
while( hashes.Count > 0 )
|
||||
{
|
||||
if( DuplicatesFinished )
|
||||
|
|
@ -157,10 +163,10 @@ public partial class Mod
|
|||
}
|
||||
}
|
||||
|
||||
hashes.RemoveAll( p => set.Contains(p.Item1) );
|
||||
hashes.RemoveAll( p => set.Contains( p.Item1 ) );
|
||||
if( set.Count > 1 )
|
||||
{
|
||||
_duplicates.Add( (set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2) );
|
||||
_duplicates.Add( ( set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2 ) );
|
||||
SavedSpace += ( set.Count - 1 ) * size;
|
||||
}
|
||||
}
|
||||
|
|
@ -169,27 +175,35 @@ public partial class Mod
|
|||
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 );
|
||||
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 );
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,38 +18,52 @@ public partial class Mod
|
|||
public ISubMod CurrentOption
|
||||
=> _subMod;
|
||||
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new();
|
||||
|
||||
public void SetSubMod( int groupIdx, int optionIdx )
|
||||
{
|
||||
GroupIdx = groupIdx;
|
||||
OptionIdx = optionIdx;
|
||||
if( groupIdx >= 0 )
|
||||
if( groupIdx >= 0 && groupIdx < _mod.Groups.Count && optionIdx >= 0 && optionIdx < _mod.Groups[ groupIdx ].Count )
|
||||
{
|
||||
_modGroup = _mod.Groups[ groupIdx ];
|
||||
_subMod = ( SubMod )_modGroup![ optionIdx ];
|
||||
}
|
||||
else
|
||||
{
|
||||
GroupIdx = -1;
|
||||
OptionIdx = 0;
|
||||
_modGroup = null;
|
||||
_subMod = _mod._default;
|
||||
}
|
||||
|
||||
RevertFiles();
|
||||
UpdateFiles();
|
||||
RevertSwaps();
|
||||
RevertManipulations();
|
||||
}
|
||||
|
||||
public void ApplyFiles()
|
||||
public int ApplyFiles()
|
||||
{
|
||||
Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, CurrentFiles.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) );
|
||||
var dict = new Dictionary< Utf8GamePath, FullPath >();
|
||||
var num = 0;
|
||||
foreach( var file in _availableFiles )
|
||||
{
|
||||
foreach( var path in file.SubModUsage.Where( p => p.Item1 == CurrentOption ) )
|
||||
{
|
||||
num += dict.TryAdd( path.Item2, file.File ) ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, dict );
|
||||
if( num > 0 )
|
||||
RevertFiles();
|
||||
else
|
||||
FileChanges = false;
|
||||
return num;
|
||||
}
|
||||
|
||||
public void RevertFiles()
|
||||
{
|
||||
CurrentFiles.SetTo( _subMod.Files );
|
||||
}
|
||||
=> UpdateFiles();
|
||||
|
||||
public void ApplySwaps()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
|
|
@ -11,68 +12,75 @@ public partial class Mod
|
|||
{
|
||||
public partial class Editor
|
||||
{
|
||||
public class FileRegistry : IEquatable< FileRegistry >
|
||||
{
|
||||
public readonly List< (ISubMod, Utf8GamePath) > SubModUsage = new();
|
||||
public FullPath File { get; private init; }
|
||||
public Utf8RelPath RelPath { get; private init; }
|
||||
public long FileSize { get; private init; }
|
||||
public int CurrentUsage;
|
||||
|
||||
public static bool FromFile( Mod mod, FileInfo file, [NotNullWhen( true )] out FileRegistry? registry )
|
||||
{
|
||||
var fullPath = new FullPath( file.FullName );
|
||||
if( !fullPath.ToRelPath( mod.ModPath, out var relPath ) )
|
||||
{
|
||||
registry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
registry = new FileRegistry
|
||||
{
|
||||
File = fullPath,
|
||||
RelPath = relPath,
|
||||
FileSize = file.Length,
|
||||
CurrentUsage = 0,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Equals( FileRegistry? other )
|
||||
{
|
||||
if( ReferenceEquals( null, other ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ReferenceEquals( this, other ) || File.Equals( other.File );
|
||||
}
|
||||
|
||||
public override bool Equals( object? obj )
|
||||
{
|
||||
if( ReferenceEquals( null, obj ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( ReferenceEquals( this, obj ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return obj.GetType() == GetType() && Equals( ( FileRegistry )obj );
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
=> File.GetHashCode();
|
||||
}
|
||||
|
||||
// All files in subdirectories of the mod directory.
|
||||
public IReadOnlyList< (FullPath, long) > AvailableFiles
|
||||
public IReadOnlyList< FileRegistry > 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;
|
||||
public bool FileChanges { get; private set; }
|
||||
private List< FileRegistry > _availableFiles = null!;
|
||||
private readonly HashSet< Utf8GamePath > _usedPaths = new();
|
||||
|
||||
// All paths that are used in
|
||||
private readonly SortedSet< FullPath > _missingPaths;
|
||||
private readonly SortedSet< FullPath > _missingFiles = new();
|
||||
|
||||
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.ModPath, 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 );
|
||||
}
|
||||
public IReadOnlySet< FullPath > MissingFiles
|
||||
=> _missingFiles;
|
||||
|
||||
// Remove all path redirections where the pointed-to file does not exist.
|
||||
public void RemoveMissingPaths()
|
||||
|
|
@ -88,13 +96,12 @@ public partial class Mod
|
|||
}
|
||||
|
||||
ApplyToAllOptions( _mod, HandleSubMod );
|
||||
_usedPaths.RemoveWhere( _missingPaths.Contains );
|
||||
_missingPaths.Clear();
|
||||
_missingFiles.Clear();
|
||||
}
|
||||
|
||||
private bool CheckAgainstMissing( FullPath file, Utf8GamePath key )
|
||||
{
|
||||
if( !_missingPaths.Contains( file ) )
|
||||
if( !_missingFiles.Contains( file ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -104,9 +111,157 @@ public partial class Mod
|
|||
}
|
||||
|
||||
|
||||
private static List<(FullPath, long)> GetAvailablePaths( Mod mod )
|
||||
=> mod.ModPath.EnumerateDirectories()
|
||||
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ).Select( f => (new FullPath( f ), f.Length) ) )
|
||||
.OrderBy( p => -p.Length ).ToList();
|
||||
// Fetch all files inside subdirectories of the main mod directory.
|
||||
// Then check which options use them and how often.
|
||||
private void UpdateFiles()
|
||||
{
|
||||
_availableFiles = _mod.ModPath.EnumerateDirectories()
|
||||
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories )
|
||||
.Select( f => FileRegistry.FromFile( _mod, f, out var r ) ? r : null )
|
||||
.OfType< FileRegistry >() )
|
||||
.ToList();
|
||||
|
||||
_usedPaths.Clear();
|
||||
FileChanges = false;
|
||||
foreach( var subMod in _mod.AllSubMods )
|
||||
{
|
||||
foreach( var (gamePath, file) in subMod.Files )
|
||||
{
|
||||
if( !file.Exists )
|
||||
{
|
||||
_missingFiles.Add( file );
|
||||
if( subMod == _subMod )
|
||||
{
|
||||
_usedPaths.Add( gamePath );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var registry = _availableFiles.FirstOrDefault( x => x.File.Equals( file ) );
|
||||
if( registry != null )
|
||||
{
|
||||
if( subMod == _subMod )
|
||||
{
|
||||
++registry.CurrentUsage;
|
||||
_usedPaths.Add( gamePath );
|
||||
}
|
||||
|
||||
registry.SubModUsage.Add( ( subMod, gamePath ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return whether the given path is already used in the current option.
|
||||
public bool CanAddGamePath( Utf8GamePath path )
|
||||
=> !_usedPaths.Contains( path );
|
||||
|
||||
// Try to set a given path for a given file.
|
||||
// Returns false if this is not possible.
|
||||
// If path is empty, it will be deleted instead.
|
||||
// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
|
||||
public bool SetGamePath( int fileIdx, int pathIdx, Utf8GamePath path )
|
||||
{
|
||||
if( _usedPaths.Contains( path ) || fileIdx < 0 || fileIdx > _availableFiles.Count || pathIdx < 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var registry = _availableFiles[ fileIdx ];
|
||||
if( pathIdx > registry.SubModUsage.Count )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( pathIdx == registry.SubModUsage.Count )
|
||||
{
|
||||
registry.SubModUsage.Add( ( CurrentOption, path ) );
|
||||
++registry.CurrentUsage;
|
||||
_usedPaths.Add( path );
|
||||
}
|
||||
else
|
||||
{
|
||||
_usedPaths.Remove( registry.SubModUsage[ pathIdx ].Item2 );
|
||||
if( path.IsEmpty )
|
||||
{
|
||||
registry.SubModUsage.RemoveAt( pathIdx );
|
||||
--registry.CurrentUsage;
|
||||
}
|
||||
else
|
||||
{
|
||||
registry.SubModUsage[ pathIdx ] = ( registry.SubModUsage[ pathIdx ].Item1, path );
|
||||
}
|
||||
}
|
||||
|
||||
FileChanges = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Transform a set of files to the appropriate game paths with the given number of folders skipped,
|
||||
// and add them to the given option.
|
||||
public int AddPathsToSelected( IEnumerable< FileRegistry > files, int skipFolders = 0 )
|
||||
{
|
||||
var failed = 0;
|
||||
foreach( var file in files )
|
||||
{
|
||||
var gamePath = file.RelPath.ToGamePath( skipFolders );
|
||||
if( gamePath.IsEmpty )
|
||||
{
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if( CanAddGamePath( gamePath ) )
|
||||
{
|
||||
++file.CurrentUsage;
|
||||
file.SubModUsage.Add( ( CurrentOption, gamePath ) );
|
||||
_usedPaths.Add( gamePath );
|
||||
FileChanges = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
return failed;
|
||||
}
|
||||
|
||||
// Remove all paths in the current option from the given files.
|
||||
public void RemovePathsFromSelected( IEnumerable< FileRegistry > files )
|
||||
{
|
||||
foreach( var file in files )
|
||||
{
|
||||
file.CurrentUsage = 0;
|
||||
FileChanges |= file.SubModUsage.RemoveAll( p => p.Item1 == CurrentOption && _usedPaths.Remove( p.Item2 ) ) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all given files from your filesystem
|
||||
public void DeleteFiles( IEnumerable< FileRegistry > files )
|
||||
{
|
||||
var deletions = 0;
|
||||
foreach( var file in files )
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete( file.File.FullName );
|
||||
PluginLog.Debug( "[DeleteFiles] Deleted {File} from {Mod}.", file.File.FullName, _mod.Name );
|
||||
++deletions;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"[DeleteFiles] Could not delete {file.File.FullName} from {_mod.Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
if( deletions > 0 )
|
||||
{
|
||||
_mod.Reload( out _ );
|
||||
UpdateFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,22 +72,22 @@ public partial class Mod
|
|||
private void ScanModels()
|
||||
{
|
||||
_modelFiles.Clear();
|
||||
foreach( var (file, _) in AvailableFiles.Where( f => f.Item1.Extension == ".mdl" ) )
|
||||
foreach( var file in AvailableFiles.Where( f => f.File.Extension == ".mdl" ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes( file.FullName );
|
||||
var bytes = File.ReadAllBytes( file.File.FullName );
|
||||
var mdlFile = new MdlFile( bytes );
|
||||
var materials = mdlFile.Materials.WithIndex().Where( p => MaterialRegex.IsMatch( p.Item1 ) )
|
||||
.Select( p => p.Item2 ).ToArray();
|
||||
if( materials.Length > 0 )
|
||||
{
|
||||
_modelFiles.Add( new MaterialInfo( file, mdlFile, materials ) );
|
||||
_modelFiles.Add( new MaterialInfo( file.File, mdlFile, materials ) );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.FullName} for materials:\n{e}" );
|
||||
PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.File.FullName} for materials:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,13 @@ public partial class Mod
|
|||
{
|
||||
private readonly Mod _mod;
|
||||
|
||||
public Editor( Mod mod )
|
||||
public Editor( Mod mod, int groupIdx, int optionIdx )
|
||||
{
|
||||
_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;
|
||||
_mod = mod;
|
||||
SetSubMod( groupIdx, optionIdx );
|
||||
GroupIdx = groupIdx;
|
||||
_subMod = _mod._default;
|
||||
UpdateFiles();
|
||||
ScanModels();
|
||||
}
|
||||
|
||||
|
|
|
|||
276
Penumbra/UI/Classes/ModEditWindow.Files.cs
Normal file
276
Penumbra/UI/Classes/ModEditWindow.Files.cs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Logging;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly HashSet< Mod.Editor.FileRegistry > _selectedFiles = new(256);
|
||||
private LowerString _fileFilter = LowerString.Empty;
|
||||
private bool _showGamePaths = true;
|
||||
private string _gamePathEdit = string.Empty;
|
||||
private int _fileIdx = -1;
|
||||
private int _pathIdx = -1;
|
||||
private int _folderSkip = 0;
|
||||
|
||||
private bool CheckFilter( Mod.Editor.FileRegistry registry )
|
||||
=> _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.InvariantCultureIgnoreCase );
|
||||
|
||||
private bool CheckFilter( (Mod.Editor.FileRegistry, int) p )
|
||||
=> CheckFilter( p.Item1 );
|
||||
|
||||
private void DrawFileTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "File Redirections" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawOptionSelectHeader();
|
||||
DrawButtonHeader();
|
||||
|
||||
using var child = ImRaii.Child( "##files", -Vector2.One, true );
|
||||
if( !child )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##table", 1 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (registry, i) in _editor!.AvailableFiles.WithIndex().Where( CheckFilter ) )
|
||||
{
|
||||
using var id = ImRaii.PushId( i );
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
DrawSelectable( registry );
|
||||
|
||||
if( !_showGamePaths )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var indent = ImRaii.PushIndent( 50f );
|
||||
for( var j = 0; j < registry.SubModUsage.Count; ++j )
|
||||
{
|
||||
var (subMod, gamePath) = registry.SubModUsage[ j ];
|
||||
if( subMod != _editor.CurrentOption )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PrintGamePath( i, j, registry, subMod, gamePath );
|
||||
}
|
||||
|
||||
PrintNewGamePath( i, registry, _editor.CurrentOption );
|
||||
}
|
||||
}
|
||||
|
||||
private string DrawFileTooltip( Mod.Editor.FileRegistry registry, ColorId color )
|
||||
{
|
||||
(string, int) GetMulti()
|
||||
{
|
||||
var groups = registry.SubModUsage.GroupBy( s => s.Item1 ).ToArray();
|
||||
return ( string.Join( "\n", groups.Select( g => g.Key.Name ) ), groups.Length );
|
||||
}
|
||||
|
||||
var (text, groupCount) = color switch
|
||||
{
|
||||
ColorId.ConflictingMod => ( string.Empty, 0 ),
|
||||
ColorId.NewMod => ( registry.SubModUsage[ 0 ].Item1.Name, 1 ),
|
||||
ColorId.InheritedMod => GetMulti(),
|
||||
_ => ( string.Empty, 0 ),
|
||||
};
|
||||
|
||||
if( text.Length > 0 && ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( text );
|
||||
}
|
||||
|
||||
|
||||
return ( groupCount, registry.SubModUsage.Count ) switch
|
||||
{
|
||||
(0, 0) => "(unused)",
|
||||
(1, 1) => "(used 1 time)",
|
||||
(1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)",
|
||||
_ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)",
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawSelectable( Mod.Editor.FileRegistry registry )
|
||||
{
|
||||
var selected = _selectedFiles.Contains( registry );
|
||||
var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod :
|
||||
registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod;
|
||||
using var c = ImRaii.PushColor( ImGuiCol.Text, color.Value() );
|
||||
if( ConfigWindow.Selectable( registry.RelPath.Path, selected ) )
|
||||
{
|
||||
if( selected )
|
||||
{
|
||||
_selectedFiles.Remove( registry );
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedFiles.Add( registry );
|
||||
}
|
||||
}
|
||||
|
||||
var rightText = DrawFileTooltip( registry, color );
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.RightAlign( rightText );
|
||||
}
|
||||
|
||||
private void PrintGamePath( int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath )
|
||||
{
|
||||
using var id = ImRaii.PushId( j );
|
||||
ImGui.TableNextColumn();
|
||||
var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString();
|
||||
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength ) )
|
||||
{
|
||||
_fileIdx = i;
|
||||
_pathIdx = j;
|
||||
_gamePathEdit = tmp;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Clear completely to remove the path from this mod." );
|
||||
|
||||
if( ImGui.IsItemDeactivatedAfterEdit() )
|
||||
{
|
||||
_fileIdx = -1;
|
||||
_pathIdx = -1;
|
||||
if( _gamePathEdit.Length == 0 )
|
||||
{
|
||||
registry.SubModUsage.RemoveAt( j-- );
|
||||
--registry.CurrentUsage;
|
||||
}
|
||||
else if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) )
|
||||
{
|
||||
registry.SubModUsage[ j ] = ( subMod, path );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintNewGamePath( int i, Mod.Editor.FileRegistry registry, ISubMod subMod )
|
||||
{
|
||||
var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty;
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( ImGui.InputTextWithHint( "##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength ) )
|
||||
{
|
||||
_fileIdx = i;
|
||||
_pathIdx = -1;
|
||||
_gamePathEdit = tmp;
|
||||
}
|
||||
|
||||
if( ImGui.IsItemDeactivatedAfterEdit() )
|
||||
{
|
||||
_fileIdx = -1;
|
||||
_pathIdx = -1;
|
||||
if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) && !path.IsEmpty )
|
||||
{
|
||||
registry.SubModUsage.Add( ( subMod, path ) );
|
||||
++registry.CurrentUsage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawButtonHeader()
|
||||
{
|
||||
ImGui.NewLine();
|
||||
|
||||
using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) );
|
||||
ImGui.SetNextItemWidth( 30 * ImGuiHelpers.GlobalScale );
|
||||
ImGui.DragInt( "##skippedFolders", ref _folderSkip, 0.01f, 0, 10 );
|
||||
ImGuiUtil.HoverTooltip( "Skip the first N folders when automatically constructing the game path from the file path." );
|
||||
ImGui.SameLine();
|
||||
spacing.Pop( );
|
||||
if( ImGui.Button( "Add Paths" ) )
|
||||
{
|
||||
_editor!.AddPathsToSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ), _folderSkip );
|
||||
}
|
||||
ImGuiUtil.HoverTooltip( "Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders." );
|
||||
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Remove Paths" ) )
|
||||
{
|
||||
_editor!.RemovePathsFromSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) );
|
||||
}
|
||||
ImGuiUtil.HoverTooltip( "Remove all game paths associated with the selected files in the current option." );
|
||||
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Delete Selected Files" ) )
|
||||
{
|
||||
_editor!.DeleteFiles( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) );
|
||||
}
|
||||
ImGuiUtil.HoverTooltip( "Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!" );
|
||||
ImGui.SameLine();
|
||||
var changes = _editor!.FileChanges;
|
||||
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
|
||||
if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, !changes ) )
|
||||
{
|
||||
var failedFiles = _editor!.ApplyFiles();
|
||||
PluginLog.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.Name}." );
|
||||
}
|
||||
|
||||
|
||||
ImGui.SameLine();
|
||||
var label = changes ? "Revert Changes" : "Reload Files";
|
||||
var length = new Vector2( ImGui.CalcTextSize( "Revert Changes" ).X, 0 );
|
||||
if( ImGui.Button( label, length ) )
|
||||
{
|
||||
_editor!.RevertFiles();
|
||||
}
|
||||
ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." );
|
||||
|
||||
ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale );
|
||||
LowerString.InputWithHint( "##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength );
|
||||
ImGui.SameLine();
|
||||
ImGui.Checkbox( "Show Game Paths", ref _showGamePaths );
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Unselect All" ) )
|
||||
{
|
||||
_selectedFiles.Clear();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Select Visible" ) )
|
||||
{
|
||||
_selectedFiles.UnionWith( _editor!.AvailableFiles.Where( CheckFilter ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Select Unused" ) )
|
||||
{
|
||||
_selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.SubModUsage.Count == 0 ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Select Used Here" ) )
|
||||
{
|
||||
_selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.CurrentUsage > 0 ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGuiUtil.RightAlign( $"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected" );
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
}
|
||||
|
||||
_editor?.Dispose();
|
||||
_editor = new Mod.Editor( mod );
|
||||
_editor = new Mod.Editor( mod, -1, 0 );
|
||||
_mod = mod;
|
||||
WindowName = $"{mod.Name}{WindowBaseLabel}";
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
|
|
@ -37,6 +37,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
MinimumSize = ImGuiHelpers.ScaledVector2( 1000, 600 ),
|
||||
MaximumSize = 4000 * Vector2.One,
|
||||
};
|
||||
_selectedFiles.Clear();
|
||||
}
|
||||
|
||||
public void ChangeOption( int groupIdx, int optionIdx )
|
||||
|
|
@ -58,7 +59,6 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
DrawMetaTab();
|
||||
DrawSwapTab();
|
||||
DrawMissingFilesTab();
|
||||
DrawUnusedFilesTab();
|
||||
DrawDuplicatesTab();
|
||||
DrawMaterialChangeTab();
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
if( _editor!.MissingPaths.Count == 0 )
|
||||
if( _editor!.MissingFiles.Count == 0 )
|
||||
{
|
||||
ImGui.NewLine();
|
||||
ImGui.TextUnformatted( "No missing files detected." );
|
||||
|
|
@ -257,7 +257,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
foreach( var path in _editor.MissingPaths )
|
||||
foreach( var path in _editor.MissingFiles )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( path.FullName );
|
||||
|
|
@ -421,90 +421,6 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawUnusedFilesTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Unused Files" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( ImGui.Button( "Refresh" ) )
|
||||
{
|
||||
_editor!.Dispose();
|
||||
_editor = new Mod.Editor( _mod! );
|
||||
}
|
||||
|
||||
if( _editor!.UnusedFiles.Count == 0 )
|
||||
{
|
||||
ImGui.NewLine();
|
||||
ImGui.TextUnformatted( "No unused files detected." );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Add Unused Files to Default" ) )
|
||||
{
|
||||
_editor.AddUnusedPathsToDefault();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Delete Unused Files from Filesystem" ) )
|
||||
{
|
||||
_editor.DeleteUnusedPaths();
|
||||
}
|
||||
|
||||
using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true );
|
||||
if( !child )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##table", 1, ImGuiTableFlags.RowBg );
|
||||
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;
|
||||
}
|
||||
|
||||
DrawOptionSelectHeader();
|
||||
using var child = ImRaii.Child( "##files", -Vector2.One, true );
|
||||
if( !child )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##table", 2 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (gamePath, file) in _editor!.CurrentFiles )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ConfigWindow.Text( gamePath.Path );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( file.FullName );
|
||||
}
|
||||
}
|
||||
|
||||
private string _newSwapKey = string.Empty;
|
||||
private string _newSwapValue = string.Empty;
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ public partial class ConfigWindow
|
|||
private static unsafe void Text( ResourceHandle* resource )
|
||||
=> Text( resource->FileName(), resource->FileNameLength );
|
||||
|
||||
// Draw a Utf8String as a selectable.
|
||||
internal static unsafe bool Selectable( Utf8String s, bool selected )
|
||||
{
|
||||
var tmp = ( byte )( selected ? 1 : 0 );
|
||||
return ImGuiNative.igSelectable_Bool( s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero ) != 0;
|
||||
}
|
||||
|
||||
// Apply Changed Item Counters to the Name if necessary.
|
||||
private static string ChangedItemName( string name, object? data )
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue