diff --git a/Penumbra/Models/ModMeta.cs b/Penumbra/Models/ModMeta.cs index 7c1c7828..6ca8afd6 100644 --- a/Penumbra/Models/ModMeta.cs +++ b/Penumbra/Models/ModMeta.cs @@ -40,6 +40,7 @@ namespace Penumbra.Models meta.HasGroupWithConfig = meta.Groups.Count > 0 && meta.Groups.Values.Any( G => G.SelectionType == SelectType.Multi || G.Options.Count > 1 ); } + return meta; } catch( Exception ) diff --git a/Penumbra/Mods/MetaManager.cs b/Penumbra/Mods/MetaManager.cs new file mode 100644 index 00000000..35f43513 --- /dev/null +++ b/Penumbra/Mods/MetaManager.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Lumina.Data.Files; +using Penumbra.Game; +using Penumbra.Hooks; +using Penumbra.Util; +using Penumbra.MetaData; + +namespace Penumbra.Mods +{ + public class MetaManager : IDisposable + { + private class FileInformation + { + public readonly object Data; + public bool Changed; + public FileInfo? CurrentFile; + + public FileInformation( object data ) + => Data = data; + + public void Write( DirectoryInfo dir ) + { + byte[] data = Data switch + { + EqdpFile eqdp => eqdp.WriteBytes(), + EqpFile eqp => eqp.WriteBytes(), + GmpFile gmp => gmp.WriteBytes(), + EstFile est => est.WriteBytes(), + ImcFile imc => imc.WriteBytes(), + _ => throw new NotImplementedException() + }; + DisposeFile( CurrentFile ); + CurrentFile = TempFile.WriteNew( dir, data ); + Changed = false; + } + } + + private const string TmpDirectory = "penumbrametatmp"; + + private readonly MetaDefaults _default; + private readonly DirectoryInfo _dir; + private readonly GameResourceManagement _resourceManagement; + private readonly Dictionary< GamePath, FileInfo > _resolvedFiles; + + private readonly HashSet< MetaManipulation > _currentManipulations = new(); + private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); + + private static void DisposeFile( FileInfo? file ) + { + if( !( file?.Exists ?? false ) ) + { + return; + } + + try + { + file.Delete(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete temporary file \"{file.FullName}\":\n{e}" ); + } + } + + public void Dispose() + { + foreach( var file in _currentFiles ) + { + _resolvedFiles.Remove( file.Key ); + DisposeFile( file.Value.CurrentFile ); + } + + _currentManipulations.Clear(); + _currentFiles.Clear(); + ClearDirectory(); + } + + private void ClearDirectory() + { + if( _dir.Exists ) + { + try + { + Directory.Delete( _dir.FullName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not clear temporary metafile directory \"{_dir.FullName}\":\n{e}" ); + } + } + } + + public MetaManager( Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo modDir ) + { + _resolvedFiles = resolvedFiles; + _default = Service< MetaDefaults >.Get(); + _resourceManagement = Service< GameResourceManagement >.Get(); + _dir = new DirectoryInfo( Path.Combine( modDir.FullName, TmpDirectory ) ); + ClearDirectory(); + Directory.CreateDirectory( _dir.FullName ); + } + + public void WriteNewFiles() + { + foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) + { + kvp.Value.Write( _dir ); + _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!; + } + + _resourceManagement.ReloadPlayerResources(); + } + + public bool ApplyMod( MetaManipulation m ) + { + if( !_currentManipulations.Add( m ) ) + { + return false; + } + + var gamePath = m.CorrespondingFilename(); + if( !_currentFiles.TryGetValue( gamePath, out var file ) ) + { + file = new FileInformation( _default.CreateNewFile( m ) ?? throw new IOException() ) + { + Changed = true, + CurrentFile = null + }; + _currentFiles[ gamePath ] = file; + } + + file.Changed |= m.Type switch + { + MetaType.Eqp => m.Apply( ( EqpFile )file.Data ), + MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ), + MetaType.Gmp => m.Apply( ( GmpFile )file.Data ), + MetaType.Est => m.Apply( ( EstFile )file.Data ), + MetaType.Imc => m.Apply( ( ImcFile )file.Data ), + _ => throw new NotImplementedException() + }; + + return true; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index f5e871ba..4890100f 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Dalamud.Plugin; using Penumbra.Hooks; using Penumbra.Models; @@ -13,6 +14,7 @@ namespace Penumbra.Mods private readonly Plugin _plugin; public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); + public MetaManager? MetaManipulations; public ModCollection? Mods { get; set; } private DirectoryInfo? _basePath; @@ -47,12 +49,15 @@ namespace Penumbra.Mods { ResolvedFiles.Clear(); SwappedFiles.Clear(); + MetaManipulations?.Dispose(); if( Mods == null ) { return; } + MetaManipulations = new MetaManager( ResolvedFiles, _basePath! ); + var changedSettings = false; var registeredFiles = new Dictionary< GamePath, string >(); foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings( _plugin!.Configuration!.InvertModListOrder ) ) @@ -63,10 +68,14 @@ namespace Penumbra.Mods ProcessSwappedFiles( registeredFiles, mod, settings ); } - if (changedSettings) + if( changedSettings ) + { Mods.Save(); + } - Service.Get().ReloadPlayerResources(); + MetaManipulations.WriteNewFiles(); + + Service< GameResourceManagement >.Get().ReloadPlayerResources(); } private void ProcessSwappedFiles( Dictionary< GamePath, string > registeredFiles, ResourceMod mod, ModInfo settings ) @@ -94,7 +103,14 @@ namespace Penumbra.Mods RelPath relativeFilePath = new( file, mod.ModBasePath ); var (configChanged, gamePaths) = mod.Meta.GetFilesForConfig( relativeFilePath, settings ); changedConfig |= configChanged; - AddFiles( gamePaths, file, registeredFiles, mod ); + if( file.Extension == ".meta" && gamePaths.Count > 0 ) + { + AddManipulations( file, mod ); + } + else + { + AddFiles( gamePaths, file, registeredFiles, mod ); + } } return changedConfig; @@ -117,6 +133,20 @@ namespace Penumbra.Mods } } + private void AddManipulations( FileInfo file, ResourceMod mod ) + { + if( !mod.MetaManipulations.TryGetValue( file, out var meta ) ) + { + PluginLog.Error( $"{file.FullName} is a TexTools Meta File without meta information." ); + return; + } + + foreach( var manipulation in meta.Manipulations ) + { + MetaManipulations!.ApplyMod( manipulation ); + } + } + public void ChangeModPriority( ModInfo info, bool up = false ) { Mods!.ReorderMod( info, up ); @@ -165,6 +195,7 @@ namespace Penumbra.Mods public void Dispose() { + MetaManipulations?.Dispose(); // _fileSystemWatcher?.Dispose(); } diff --git a/Penumbra/Mods/ResourceMod.cs b/Penumbra/Mods/ResourceMod.cs index 11e6f7dc..3ee800c8 100644 --- a/Penumbra/Mods/ResourceMod.cs +++ b/Penumbra/Mods/ResourceMod.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Plugin; +using Penumbra.Importer; using Penumbra.Models; using Penumbra.Util; @@ -19,16 +22,32 @@ namespace Penumbra.Mods public DirectoryInfo ModBasePath { get; set; } public List< FileInfo > ModFiles { get; } = new(); + public Dictionary< FileInfo, TexToolsMeta > MetaManipulations { get; } = new(); public Dictionary< string, List< GamePath > > FileConflicts { get; } = new(); + public void RefreshModFiles() { + FileConflicts.Clear(); ModFiles.Clear(); + MetaManipulations.Clear(); // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo foreach( var file in ModBasePath.EnumerateDirectories() .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) ) { + if( file.Extension == ".meta" ) + { + try + { + MetaManipulations[ file ] = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse meta file {file.FullName}:\n{e}" ); + } + } + ModFiles.Add( file ); } } diff --git a/Penumbra/Util/TempFile.cs b/Penumbra/Util/TempFile.cs new file mode 100644 index 00000000..0c4cb2d2 --- /dev/null +++ b/Penumbra/Util/TempFile.cs @@ -0,0 +1,31 @@ +using System.IO; + +namespace Penumbra.Util +{ + public static class TempFile + { + public static FileInfo TempFileName( DirectoryInfo baseDir ) + { + const uint maxTries = 15; + for( var i = 0; i < maxTries; ++i ) + { + var name = Path.GetRandomFileName(); + var path = new FileInfo( Path.Combine( baseDir.FullName, name ) ); + if( !path.Exists ) + { + return path; + } + } + + throw new IOException(); + } + + public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data ) + { + var fileName = TempFileName( baseDir ); + File.WriteAllBytes( fileName.FullName, data ); + fileName.Refresh(); + return fileName; + } + } +}