mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
290 lines
No EOL
10 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
} |