Add file redirection editing.

This commit is contained in:
Ottermandias 2022-06-02 13:04:00 +02:00
parent 06deddcd8a
commit 385ce4c7e9
8 changed files with 569 additions and 189 deletions

View file

@ -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;
}
}
}

View file

@ -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()
{

View file

@ -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();
}
}
}
}

View file

@ -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}" );
}
}
}

View file

@ -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();
}

View 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" );
}
}

View file

@ -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;

View file

@ -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 )