Penumbra/Penumbra/Mods/Editor/Mod.Editor.Files.cs
2023-02-21 16:54:24 +01:00

290 lines
No EOL
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
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( other is null )
{
return false;
}
return ReferenceEquals( this, other ) || File.Equals( other.File );
}
public override bool Equals( object? obj )
{
if( obj is null )
{
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< FileRegistry > AvailableFiles
=> _availableFiles;
public bool FileChanges { get; private set; }
private List< FileRegistry > _availableFiles = null!;
private List< FileRegistry > _mtrlFiles = null!;
private List< FileRegistry > _mdlFiles = null!;
private List< FileRegistry > _texFiles = null!;
private List< FileRegistry > _shpkFiles = null!;
private readonly HashSet< Utf8GamePath > _usedPaths = new();
// All paths that are used in
private readonly SortedSet< FullPath > _missingFiles = new();
public IReadOnlySet< FullPath > MissingFiles
=> _missingFiles;
public IReadOnlyList< FileRegistry > MtrlFiles
=> _mtrlFiles;
public IReadOnlyList< FileRegistry > MdlFiles
=> _mdlFiles;
public IReadOnlyList< FileRegistry > TexFiles
=> _texFiles;
public IReadOnlyList< FileRegistry > ShpkFiles
=> _shpkFiles;
// Remove all path redirections where the pointed-to file does not exist.
public void RemoveMissingPaths()
{
void HandleSubMod( ISubMod mod, int groupIdx, int optionIdx )
{
var newDict = mod.Files.Where( kvp => CheckAgainstMissing( kvp.Value, kvp.Key, mod == _subMod ) )
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
if( newDict.Count != mod.Files.Count )
{
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, newDict );
}
}
ApplyToAllOptions( _mod, HandleSubMod );
_missingFiles.Clear();
}
private bool CheckAgainstMissing( FullPath file, Utf8GamePath key, bool removeUsed )
{
if( !_missingFiles.Contains( file ) )
{
return true;
}
if( removeUsed )
{
_usedPaths.Remove( key );
}
Penumbra.Log.Debug( $"[RemoveMissingPaths] Removing {key} -> {file} from {_mod.Name}." );
return false;
}
// 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();
_mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList();
_mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList();
_texFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".tex", StringComparison.OrdinalIgnoreCase ) ).ToList();
_shpkFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".shpk", StringComparison.OrdinalIgnoreCase ) ).ToList();
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.Find( 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 )
{
return false;
}
var registry = _availableFiles[ fileIdx ];
if( pathIdx > registry.SubModUsage.Count )
{
return false;
}
if( ( pathIdx == -1 || pathIdx == registry.SubModUsage.Count ) && !path.IsEmpty )
{
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 );
Penumbra.Log.Debug( $"[DeleteFiles] Deleted {file.File.FullName} from {_mod.Name}." );
++deletions;
}
catch( Exception e )
{
Penumbra.Log.Error( $"[DeleteFiles] Could not delete {file.File.FullName} from {_mod.Name}:\n{e}" );
}
}
if( deletions > 0 )
{
_mod.Reload( false, out _ );
UpdateFiles();
}
}
}
}