diff --git a/Penumbra/API/ModsController.cs b/Penumbra/API/ModsController.cs index 3535380c..9edf64de 100644 --- a/Penumbra/API/ModsController.cs +++ b/Penumbra/API/ModsController.cs @@ -19,7 +19,7 @@ namespace Penumbra.API public object? GetMods() { var modManager = Service< ModManager >.Get(); - return modManager.CurrentCollection.Cache?.AvailableMods.Select( x => new + return modManager.Collections.CurrentCollection.Cache?.AvailableMods.Select( x => new { x.Settings.Enabled, x.Settings.Priority, @@ -39,7 +39,7 @@ namespace Penumbra.API public object GetFiles() { var modManager = Service< ModManager >.Get(); - return modManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( + return modManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( o => ( string )o.Key, o => o.Value.FullName ) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 0ce12f15..53606f35 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Dalamud.Configuration; using Dalamud.Plugin; using Penumbra.Util; @@ -24,7 +25,8 @@ namespace Penumbra public string ModDirectory { get; set; } = @"D:/ffxiv/fs_mods/"; public string CurrentCollection { get; set; } = "Default"; - + public string ForcedCollection { get; set; } = ""; + public Dictionary< string, string > CharacterCollections { get; set; } = new(); public bool InvertModListOrder { internal get; set; } public static Configuration Load( DalamudPluginInterface pi ) diff --git a/Penumbra/Interop/ActorRefresher.cs b/Penumbra/Interop/ActorRefresher.cs index 1c5172c8..e2d024f5 100644 --- a/Penumbra/Interop/ActorRefresher.cs +++ b/Penumbra/Interop/ActorRefresher.cs @@ -23,6 +23,7 @@ namespace Penumbra.Interop { private const int RenderModeOffset = 0x0104; private const int ModelInvisibilityFlag = 0b10; + private const int ModelIsLoadingFlag = 0x800; private const int UnloadAllRedrawDelay = 250; private const int NpcActorId = -536870912; @@ -30,10 +31,11 @@ namespace Penumbra.Interop private readonly ModManager _mods; private readonly Queue< (int actorId, string name, Redraw s) > _actorIds = new(); - private int _currentFrame = 0; - private bool _changedSettings = false; - private int _currentActorId = -1; - private string? _currentActorName = null; + private int _currentFrame = 0; + private bool _changedSettings = false; + private int _currentActorId = -1; + private string? _currentActorName = null; + private Redraw _currentActorRedraw = Redraw.Unload; public ActorRefresher( DalamudPluginInterface pi, ModManager mods ) { @@ -42,10 +44,19 @@ namespace Penumbra.Interop } private void ChangeSettings() - => _changedSettings = true; + { + if( _currentActorName != null && _mods.Collections.CharacterCollection.TryGetValue( _currentActorName, out var collection ) ) + { + _changedSettings = true; + _mods.Collections.ActiveCollection = collection; + } + } private void RestoreSettings() - => _changedSettings = false; + { + _mods.Collections.ActiveCollection = _mods.Collections.CurrentCollection; + _changedSettings = false; + } private static unsafe void WriteInvisible( IntPtr renderPtr ) { @@ -55,6 +66,16 @@ namespace Penumbra.Interop } } + private static unsafe bool StillLoading( IntPtr renderPtr ) + { + if( renderPtr != IntPtr.Zero ) + { + return *( int* )renderPtr != 0; + } + + return false; + } + private static unsafe void WriteVisible( IntPtr renderPtr ) { if( renderPtr != IntPtr.Zero ) @@ -81,54 +102,21 @@ namespace Penumbra.Interop private Actor? FindCurrentActor() => _pi.ClientState.Actors.FirstOrDefault( CheckActor ); - private void ChangeSettingsAndUndraw() + private void PopActor() { if( _actorIds.Count > 0 ) { - var (id, name, s) = _actorIds.Dequeue(); - _currentActorName = name; - _currentActorId = id; + var (id, name, s) = _actorIds.Dequeue(); + _currentActorName = name; + _currentActorId = id; + _currentActorRedraw = s; var actor = FindCurrentActor(); if( actor == null ) { return; } - switch( s ) - { - case Redraw.Unload: - WriteInvisible( actor.Address + RenderModeOffset ); - _currentFrame = 0; - break; - case Redraw.RedrawWithSettings: - ChangeSettings(); - ++_currentFrame; - break; - case Redraw.RedrawWithoutSettings: - WriteVisible( actor.Address + RenderModeOffset ); - _currentFrame = 0; - break; - case Redraw.WithoutSettings: - WriteInvisible( actor.Address + RenderModeOffset ); - ++_currentFrame; - break; - case Redraw.WithSettings: - ChangeSettings(); - WriteInvisible( actor.Address + RenderModeOffset ); - ++_currentFrame; - break; - case Redraw.OnlyWithSettings: - ChangeSettings(); - if( !_changedSettings ) - { - return; - } - - WriteInvisible( actor.Address + RenderModeOffset ); - ++_currentFrame; - break; - default: throw new InvalidEnumArgumentException(); - } + ++_currentFrame; } else { @@ -136,7 +124,57 @@ namespace Penumbra.Interop } } - private void StartRedraw() + private void ApplySettingsOrRedraw() + { + var actor = FindCurrentActor(); + if( actor == null ) + { + return; + } + + if( StillLoading( actor.Address + RenderModeOffset ) ) + { + return; + } + + switch( _currentActorRedraw ) + { + case Redraw.Unload: + WriteInvisible( actor.Address + RenderModeOffset ); + _currentFrame = 0; + break; + case Redraw.RedrawWithSettings: + ChangeSettings(); + ++_currentFrame; + break; + case Redraw.RedrawWithoutSettings: + WriteVisible( actor.Address + RenderModeOffset ); + _currentFrame = 0; + break; + case Redraw.WithoutSettings: + WriteInvisible( actor.Address + RenderModeOffset ); + ++_currentFrame; + break; + case Redraw.WithSettings: + ChangeSettings(); + WriteInvisible( actor.Address + RenderModeOffset ); + ++_currentFrame; + break; + case Redraw.OnlyWithSettings: + ChangeSettings(); + if( !_changedSettings ) + { + return; + } + + WriteInvisible( actor.Address + RenderModeOffset ); + ++_currentFrame; + break; + default: throw new InvalidEnumArgumentException(); + } + } + + private void StartRedrawAndWait() { var actor = FindCurrentActor(); if( actor == null ) @@ -151,8 +189,19 @@ namespace Penumbra.Interop private void RevertSettings() { - RestoreSettings(); - _currentFrame = 0; + var actor = FindCurrentActor(); + if( actor != null ) + { + if( !StillLoading( actor.Address + RenderModeOffset ) ) + { + RestoreSettings(); + _currentFrame = 0; + } + } + else + { + _currentFrame = 0; + } } private void OnUpdateEvent( object framework ) @@ -160,12 +209,15 @@ namespace Penumbra.Interop switch( _currentFrame ) { case 0: - ChangeSettingsAndUndraw(); + PopActor(); break; case 1: - StartRedraw(); + ApplySettingsOrRedraw(); break; case 2: + StartRedrawAndWait(); + break; + case 3: RevertSettings(); break; default: diff --git a/Penumbra/Interop/PlayerWatcher.cs b/Penumbra/Interop/PlayerWatcher.cs index b049a0df..5fa9ea9f 100644 --- a/Penumbra/Interop/PlayerWatcher.cs +++ b/Penumbra/Interop/PlayerWatcher.cs @@ -9,7 +9,7 @@ namespace Penumbra.Interop { public class PlayerWatcher : IDisposable { - private const int ActorsPerFrame = 4; + private const int ActorsPerFrame = 8; private readonly DalamudPluginInterface _pi; private readonly Dictionary< string, CharEquipment > _equip = new(); @@ -29,6 +29,11 @@ namespace Penumbra.Interop } } + public void RemovePlayerFromWatch( string playerName ) + { + _equip.Remove( playerName ); + } + public void SetActorWatch( bool on ) { if( on ) @@ -58,10 +63,10 @@ namespace Penumbra.Interop public void Dispose() => DisableActorWatch(); - public void OnTerritoryChange( object _1, ushort _2 ) + private void OnTerritoryChange( object _1, ushort _2 ) => Clear(); - public void OnLogout( object _1, object _2 ) + private void OnLogout( object _1, object _2 ) => Clear(); public void Clear() @@ -74,7 +79,7 @@ namespace Penumbra.Interop _frameTicker = 0; } - public void OnFrameworkUpdate( object framework ) + private void OnFrameworkUpdate( object framework ) { var actors = _pi.ClientState.Actors; for( var i = 0; i < ActorsPerFrame; ++i ) diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 04d2fb2b..c221042e 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -164,7 +164,7 @@ namespace Penumbra.Interop file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; var gameFsPath = GamePath.GenerateUncheckedLower( file ); - var replacementPath = modManager.CurrentCollection.ResolveSwappedOrReplacementPath( gameFsPath ); + var replacementPath = modManager.ResolveSwappedOrReplacementPath( gameFsPath ); if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) ) { PluginLog.Log( "[GetResourceHandler] {0}", file ); diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs new file mode 100644 index 00000000..847020b7 --- /dev/null +++ b/Penumbra/Mods/CollectionManager.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Penumbra.Interop; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Mods +{ + // Contains all collections and respective functions, as well as the collection settings. + public class CollectionManager + { + private readonly Plugin _plugin; + private readonly ModManager _manager; + + public Dictionary< string, ModCollection > Collections { get; } = new(); + + public ModCollection CurrentCollection { get; private set; } = null!; + public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty; + public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); + + public ModCollection ActiveCollection { get; set; } + + public CollectionManager( Plugin plugin, ModManager manager ) + { + _plugin = plugin; + _manager = manager; + + ReadCollections(); + LoadConfigCollections( _plugin.Configuration ); + ActiveCollection = CurrentCollection; + } + + public void RecreateCaches() + { + foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) + { + collection.CreateCache( _manager.BasePath, _manager.Mods, false ); + } + } + + public void RemoveModFromCaches( DirectoryInfo modDir ) + { + foreach( var collection in Collections.Values ) + { + collection.Cache?.RemoveMod( modDir ); + } + } + + internal void UpdateCollections( ModData mod, bool metaChanges, ResourceChange fileChanges, bool nameChange, bool recomputeMeta ) + { + foreach( var collection in Collections.Values ) + { + if( metaChanges ) + { + collection.UpdateSetting( mod ); + if( nameChange ) + { + collection.Cache?.SortMods(); + } + } + + if( fileChanges.HasFlag( ResourceChange.Files ) + && collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) + && settings.Enabled ) + { + collection.Cache?.CalculateEffectiveFileList(); + } + + if( recomputeMeta ) + { + collection.Cache?.UpdateMetaManipulations(); + } + } + } + + public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) + { + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + { + PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + return false; + } + + var newCollection = new ModCollection( name, settings ); + Collections.Add( name, newCollection ); + SaveCollection( newCollection ); + SetCurrentCollection( newCollection ); + return true; + } + + public bool RemoveCollection( string name ) + { + if( name == ModCollection.DefaultCollection ) + { + PluginLog.Error( "Can not remove the default collection." ); + return false; + } + + if( Collections.TryGetValue( name, out var collection ) ) + { + if( CurrentCollection == collection ) + { + SetCurrentCollection( Collections[ ModCollection.DefaultCollection ] ); + } + + if( ForcedCollection == collection ) + { + SetForcedCollection( ModCollection.Empty ); + } + + foreach( var kvp in CharacterCollection.ToArray() ) + { + if( kvp.Value == collection ) + { + SetCharacterCollection( kvp.Key, ModCollection.Empty ); + } + } + + collection.Delete( _plugin.PluginInterface! ); + Collections.Remove( name ); + return true; + } + + return false; + } + + private void AddCache( ModCollection collection ) + { + if( collection.Cache == null && collection.Name != string.Empty ) + { + collection.CreateCache( _manager.BasePath, _manager.Mods, false ); + } + } + + private void RemoveCache( ModCollection collection ) + { + if( collection.Name != ForcedCollection.Name + && collection.Name != CurrentCollection.Name + && CharacterCollection.All( kvp => kvp.Value.Name != collection.Name ) ) + { + collection.ClearCache(); + } + } + + private void SetCollection( ModCollection newCollection, ModCollection oldCollection, Action< ModCollection > setter, + Action< string > configSetter ) + { + if( newCollection.Name == oldCollection.Name ) + { + return; + } + + AddCache( newCollection ); + + setter( newCollection ); + RemoveCache( oldCollection ); + configSetter( newCollection.Name ); + _plugin.Configuration.Save(); + } + + public void SetCurrentCollection( ModCollection newCollection ) + => SetCollection( newCollection, CurrentCollection, c => + { + if( ActiveCollection == CurrentCollection ) + { + ActiveCollection = c; + } + + CurrentCollection = c; + var resourceManager = Service< GameResourceManagement >.Get(); + resourceManager.ReloadPlayerResources(); + }, s => _plugin.Configuration.CurrentCollection = s ); + + public void SetForcedCollection( ModCollection newCollection ) + => SetCollection( newCollection, ForcedCollection, c => ForcedCollection = c, s => _plugin.Configuration.ForcedCollection = s ); + + public void SetCharacterCollection( string characterName, ModCollection newCollection ) + => SetCollection( newCollection, + CharacterCollection.TryGetValue( characterName, out var oldCollection ) ? oldCollection : ModCollection.Empty, + c => CharacterCollection[ characterName ] = c, s => _plugin.Configuration.CharacterCollections[ characterName ] = s ); + + public bool CreateCharacterCollection( string characterName ) + { + if( !CharacterCollection.ContainsKey( characterName ) ) + { + CharacterCollection[ characterName ] = ModCollection.Empty; + _plugin.Configuration.CharacterCollections[ characterName ] = string.Empty; + _plugin.Configuration.Save(); + _plugin.PlayerWatcher.AddPlayerToWatch( characterName ); + return true; + } + + return false; + } + + public void RemoveCharacterCollection( string characterName ) + { + if( CharacterCollection.TryGetValue( characterName, out var collection ) ) + { + RemoveCache( collection ); + CharacterCollection.Remove( characterName ); + _plugin.PlayerWatcher.RemovePlayerFromWatch( characterName ); + } + + if( _plugin.Configuration.CharacterCollections.Remove( characterName ) ) + { + _plugin.Configuration.Save(); + } + } + + private bool LoadCurrentCollection( Configuration config ) + { + if( Collections.TryGetValue( config.CurrentCollection, out var currentCollection ) ) + { + CurrentCollection = currentCollection; + AddCache( CurrentCollection ); + return false; + } + + PluginLog.Error( $"Last choice of CurrentCollection {config.CurrentCollection} is not available, reset to Default." ); + CurrentCollection = Collections[ ModCollection.DefaultCollection ]; + config.CurrentCollection = ModCollection.DefaultCollection; + return true; + } + + private bool LoadForcedCollection( Configuration config ) + { + if( config.ForcedCollection == string.Empty ) + { + ForcedCollection = ModCollection.Empty; + return false; + } + + if( Collections.TryGetValue( config.ForcedCollection, out var forcedCollection ) ) + { + ForcedCollection = forcedCollection; + AddCache( ForcedCollection ); + return false; + } + + PluginLog.Error( $"Last choice of ForcedCollection {config.ForcedCollection} is not available, reset to None." ); + ForcedCollection = ModCollection.Empty; + config.ForcedCollection = string.Empty; + return true; + } + + private bool LoadCharacterCollections( Configuration config ) + { + var configChanged = false; + foreach( var kvp in config.CharacterCollections.ToArray() ) + { + _plugin.PlayerWatcher.AddPlayerToWatch( kvp.Key ); + if( kvp.Value == string.Empty ) + { + CharacterCollection.Add( kvp.Key, ModCollection.Empty ); + } + else if( Collections.TryGetValue( kvp.Value, out var charCollection ) ) + { + AddCache( charCollection ); + CharacterCollection.Add( kvp.Key, charCollection ); + } + else + { + PluginLog.Error( $"Last choice of <{kvp.Key}>'s Collection {kvp.Value} is not available, reset to None." ); + CharacterCollection.Add( kvp.Key, ModCollection.Empty ); + config.CharacterCollections[ kvp.Key ] = string.Empty; + configChanged = true; + } + } + + return configChanged; + } + + private void LoadConfigCollections( Configuration config ) + { + var configChanged = LoadCurrentCollection( config ); + configChanged |= LoadForcedCollection( config ); + configChanged |= LoadCharacterCollections( config ); + + if( configChanged ) + { + config.Save(); + } + } + + private void ReadCollections() + { + var collectionDir = ModCollection.CollectionDir( _plugin.PluginInterface! ); + if( collectionDir.Exists ) + { + foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + { + var collection = ModCollection.LoadFromFile( file ); + if( collection != null ) + { + if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) + { + PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + } + + if( Collections.ContainsKey( collection.Name ) ) + { + PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + } + else + { + Collections.Add( collection.Name, collection ); + } + } + } + } + + if( !Collections.ContainsKey( ModCollection.DefaultCollection ) ) + { + var defaultCollection = new ModCollection(); + SaveCollection( defaultCollection ); + Collections.Add( defaultCollection.Name, defaultCollection ); + } + } + + public void SaveCollection( ModCollection collection ) + => collection.Save( _plugin.PluginInterface! ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index df524d2a..c828f377 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -90,6 +90,9 @@ namespace Penumbra.Mods CalculateEffectiveFileList( modDirectory, true ); } + public void ClearCache() + => Cache = null; + public void UpdateSetting( ModData mod ) { if( !Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) @@ -236,5 +239,7 @@ namespace Penumbra.Mods public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) => Cache?.ResolveSwappedOrReplacementPath( gameResourcePath ); + + public static readonly ModCollection Empty = new(){ Name = "" }; } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 0f7d14f7..5c97ba52 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Dalamud.Plugin; using Penumbra.Meta; using Penumbra.Mod; @@ -9,132 +8,19 @@ using Penumbra.Util; namespace Penumbra.Mods { - // The ModManager handles the basic mods installed to the mod directory, - // as well as all saved collections. - // It also handles manual changes to mods that require changes in all collections, - // updating the state of a mod from the filesystem, - // and collection swapping. + // The ModManager handles the basic mods installed to the mod directory. + // It also contains the CollectionManager that handles all collections. public class ModManager { - private readonly Plugin _plugin; public DirectoryInfo BasePath { get; private set; } public Dictionary< string, ModData > Mods { get; } = new(); - public Dictionary< string, ModCollection > Collections { get; } = new(); - - public ModCollection CurrentCollection { get; private set; } + public CollectionManager Collections { get; } public ModManager( Plugin plugin ) { - _plugin = plugin; - BasePath = new DirectoryInfo( plugin.Configuration!.ModDirectory ); - ReadCollections(); - CurrentCollection = Collections.Values.First(); - if( !SetCurrentCollection( plugin.Configuration!.CurrentCollection ) ) - { - PluginLog.Debug( "Last choice of collection {Name} is not available, reset to Default.", - plugin.Configuration!.CurrentCollection ); - - if( SetCurrentCollection( ModCollection.DefaultCollection ) ) - { - PluginLog.Error( "Could not load any collection. Default collection unavailable." ); - CurrentCollection = new ModCollection(); - } - } - } - - public bool SetCurrentCollection( string name ) - { - if( Collections.TryGetValue( name, out var collection ) ) - { - CurrentCollection = collection; - if( CurrentCollection.Cache == null ) - { - CurrentCollection.CreateCache( BasePath, Mods ); - } - - return true; - } - - return false; - } - - public void ReadCollections() - { - var collectionDir = ModCollection.CollectionDir( _plugin.PluginInterface! ); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) - { - var collection = ModCollection.LoadFromFile( file ); - if( collection != null ) - { - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } - - if( Collections.ContainsKey( collection.Name ) ) - { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); - } - else - { - Collections.Add( collection.Name, collection ); - } - } - } - } - - if( !Collections.ContainsKey( ModCollection.DefaultCollection ) ) - { - var defaultCollection = new ModCollection(); - SaveCollection( defaultCollection ); - Collections.Add( defaultCollection.Name, defaultCollection ); - } - } - - public void SaveCollection( ModCollection collection ) - => collection.Save( _plugin.PluginInterface! ); - - - public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) - { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) - { - PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); - return false; - } - - var newCollection = new ModCollection( name, settings ); - Collections.Add( name, newCollection ); - SaveCollection( newCollection ); - CurrentCollection = newCollection; - return true; - } - - public bool RemoveCollection( string name ) - { - if( name == ModCollection.DefaultCollection ) - { - PluginLog.Error( "Can not remove the default collection." ); - return false; - } - - if( Collections.TryGetValue( name, out var collection ) ) - { - if( CurrentCollection == collection ) - { - SetCurrentCollection( ModCollection.DefaultCollection ); - } - - collection.Delete( _plugin.PluginInterface! ); - Collections.Remove( name ); - return true; - } - - return false; + BasePath = new DirectoryInfo( plugin.Configuration.ModDirectory ); + Collections = new CollectionManager( plugin, this ); } public void DiscoverMods( DirectoryInfo basePath ) @@ -171,10 +57,7 @@ namespace Penumbra.Mods Mods.Add( modFolder.Name, mod ); } - foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) - { - collection.CreateCache( BasePath, Mods ); - } + Collections.RecreateCaches(); } public void DeleteMod( DirectoryInfo modFolder ) @@ -192,10 +75,7 @@ namespace Penumbra.Mods } Mods.Remove( modFolder.Name ); - foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) - { - collection.Cache!.RemoveMod( modFolder ); - } + Collections.RemoveModFromCaches( modFolder ); } } @@ -213,7 +93,7 @@ namespace Penumbra.Mods } Mods.Add( modFolder.Name, mod ); - foreach( var collection in Collections.Values ) + foreach( var collection in Collections.Collections.Values ) { collection.AddMod( mod ); } @@ -241,77 +121,62 @@ namespace Penumbra.Mods mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); } - foreach( var collection in Collections.Values ) - { - if( metaChanges ) - { - collection.UpdateSetting( mod ); - if( nameChange ) - { - collection.Cache?.SortMods(); - } - } - - if( fileChanges.HasFlag( ResourceChange.Files ) - && collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) - && settings.Enabled ) - { - collection.Cache?.CalculateEffectiveFileList(); - } - - if( recomputeMeta ) - { - collection.Cache?.UpdateMetaManipulations(); - } - } + Collections.UpdateCollections( mod, metaChanges, fileChanges, nameChange, recomputeMeta ); return true; } -// private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e ) -// { -// #if DEBUG -// PluginLog.Verbose( "file changed: {FullPath}", e.FullPath ); -// #endif -// -// if( _plugin.ImportInProgress ) -// { -// return; -// } -// -// if( _plugin.Configuration.DisableFileSystemNotifications ) -// { -// return; -// } -// -// var file = e.FullPath; -// -// if( !ResolvedFiles.Any( x => x.Value.FullName == file ) ) -// { -// return; -// } -// -// PluginLog.Log( "a loaded file has been modified - file: {FullPath}", file ); -// _plugin.GameUtils.ReloadPlayerResources(); -// } -// -// private void FileSystemPasta() -// { -// haha spaghet -// _fileSystemWatcher?.Dispose(); -// _fileSystemWatcher = new FileSystemWatcher( _basePath.FullName ) -// { -// NotifyFilter = NotifyFilters.LastWrite | -// NotifyFilters.FileName | -// NotifyFilters.DirectoryName, -// IncludeSubdirectories = true, -// EnableRaisingEvents = true -// }; -// -// _fileSystemWatcher.Changed += FileSystemWatcherOnChanged; -// _fileSystemWatcher.Created += FileSystemWatcherOnChanged; -// _fileSystemWatcher.Deleted += FileSystemWatcherOnChanged; -// _fileSystemWatcher.Renamed += FileSystemWatcherOnChanged; -// } + public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) + { + var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); + ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); + return ret; + } + + // private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e ) + // { + // #if DEBUG + // PluginLog.Verbose( "file changed: {FullPath}", e.FullPath ); + // #endif + // + // if( _plugin.ImportInProgress ) + // { + // return; + // } + // + // if( _plugin.Configuration.DisableFileSystemNotifications ) + // { + // return; + // } + // + // var file = e.FullPath; + // + // if( !ResolvedFiles.Any( x => x.Value.FullName == file ) ) + // { + // return; + // } + // + // PluginLog.Log( "a loaded file has been modified - file: {FullPath}", file ); + // _plugin.GameUtils.ReloadPlayerResources(); + // } + // + // private void FileSystemPasta() + // { + // haha spaghet + // _fileSystemWatcher?.Dispose(); + // _fileSystemWatcher = new FileSystemWatcher( _basePath.FullName ) + // { + // NotifyFilter = NotifyFilters.LastWrite | + // NotifyFilters.FileName | + // NotifyFilters.DirectoryName, + // IncludeSubdirectories = true, + // EnableRaisingEvents = true + // }; + // + // _fileSystemWatcher.Changed += FileSystemWatcherOnChanged; + // _fileSystemWatcher.Created += FileSystemWatcherOnChanged; + // _fileSystemWatcher.Deleted += FileSystemWatcherOnChanged; + // _fileSystemWatcher.Renamed += FileSystemWatcherOnChanged; + // } } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index d281d710..f19cb449 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -22,7 +22,7 @@ namespace Penumbra.Mods mod.Meta.Name = newName; mod.SaveMeta(); - foreach( var collection in manager.Collections.Values.Where( c => c.Cache != null ) ) + foreach( var collection in manager.Collections.Collections.Values.Where( c => c.Cache != null ) ) { collection.Cache!.SortMods(); } @@ -60,13 +60,13 @@ namespace Penumbra.Mods mod.MetaFile = ModData.MetaFileInfo( newDir ); manager.UpdateMod( mod ); - foreach( var collection in manager.Collections.Values ) + foreach( var collection in manager.Collections.Collections.Values ) { if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) { collection.Settings[ newDir.Name ] = settings; collection.Settings.Remove( oldBasePath.Name ); - manager.SaveCollection( collection ); + manager.Collections.SaveCollection( collection ); } if( collection.Cache != null ) @@ -118,7 +118,7 @@ namespace Penumbra.Mods mod.SaveMeta(); - foreach( var collection in manager.Collections.Values ) + foreach( var collection in manager.Collections.Collections.Values ) { if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -131,7 +131,7 @@ namespace Penumbra.Mods } settings.Settings.Remove( oldGroupName ); - manager.SaveCollection( collection ); + manager.Collections.SaveCollection( collection ); } return true; @@ -154,7 +154,7 @@ namespace Penumbra.Mods return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); } - foreach( var collection in manager.Collections.Values ) + foreach( var collection in manager.Collections.Collections.Values ) { if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -176,7 +176,7 @@ namespace Penumbra.Mods if( newSetting != setting ) { settings.Settings[ group.GroupName ] = newSetting; - manager.SaveCollection( collection ); + manager.Collections.SaveCollection( collection ); if( collection.Cache != null && settings.Enabled ) { collection.CalculateEffectiveFileList( manager.BasePath, mod.Resources.MetaManipulations.Count > 0 ); diff --git a/Penumbra/Plugin.cs b/Penumbra/Plugin.cs index bf0d6236..8d9a7a4b 100644 --- a/Penumbra/Plugin.cs +++ b/Penumbra/Plugin.cs @@ -48,13 +48,13 @@ namespace Penumbra SoundShit.DisableStreaming(); var gameUtils = Service< GameResourceManagement >.Set( PluginInterface ); + PlayerWatcher = new PlayerWatcher( PluginInterface ); Service< MetaDefaults >.Set( PluginInterface ); var modManager = Service< ModManager >.Set( this ); modManager.DiscoverMods(); ActorRefresher = new ActorRefresher( PluginInterface, modManager ); - PlayerWatcher = new PlayerWatcher( PluginInterface ); ResourceLoader = new ResourceLoader( this ); @@ -77,10 +77,16 @@ namespace Penumbra CreateWebServer(); } - if( Configuration.EnableActorWatch ) + if( Configuration.EnableActorWatch && Configuration.IsEnabled ) { PlayerWatcher.EnableActorWatch(); } + + PlayerWatcher.ActorChanged += a => + { + PluginLog.Debug( "Triggered Redraw of {Actor}.", a.Name ); + ActorRefresher.RedrawActor( a, Redraw.OnlyWithSettings ); + }; } public void CreateWebServer() diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 4f1ce388..c2f9018a 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using Dalamud.Interface; using Dalamud.Plugin; using ImGuiNET; using Penumbra.Interop; @@ -14,41 +16,76 @@ namespace Penumbra.UI { private class TabCollections { - private readonly Selector _selector; - private readonly ModManager _manager; - private string[] _collectionNames = null!; - private int _currentIndex = 0; - private string _newCollectionName = string.Empty; + private readonly Selector _selector; + private readonly ModManager _manager; + private string _collectionNames = null!; + private string _collectionNamesWithNone = null!; + private ModCollection[] _collections = null!; + private int _currentCollectionIndex = 0; + private int _currentForcedIndex = 0; + private readonly Dictionary< string, int > _currentCharacterIndices = new(); + private string _newCollectionName = string.Empty; + private string _newCharacterName = string.Empty; private void UpdateNames() - => _collectionNames = _manager.Collections.Values.Select( c => c.Name ).ToArray(); + { + _collections = _manager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); + _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ); + _collectionNamesWithNone = "None\0" + _collectionNames; + UpdateIndices(); + } + + + private int GetIndex( ModCollection collection ) + { + var ret = _collections.IndexOf( c => c.Name == collection.Name ); + if( ret < 0 ) + { + PluginLog.Error( $"Collection {collection.Name} is not found in collections." ); + return 0; + } + + return ret; + } private void UpdateIndex() + => _currentCollectionIndex = GetIndex( _manager.Collections.CurrentCollection ) - 1; + + private void UpdateForcedIndex() + => _currentForcedIndex = GetIndex( _manager.Collections.ForcedCollection ); + + private void UpdateCharacterIndices() { - _currentIndex = _collectionNames.IndexOf( c => c == _manager.CurrentCollection.Name ); - if( _currentIndex < 0 ) + _currentCharacterIndices.Clear(); + foreach( var kvp in _manager.Collections.CharacterCollection ) { - PluginLog.Error( $"Current Collection {_manager.CurrentCollection.Name} is not found in collections." ); - _currentIndex = 0; + _currentCharacterIndices[ kvp.Key ] = GetIndex( kvp.Value ); } } + private void UpdateIndices() + { + UpdateIndex(); + UpdateForcedIndex(); + UpdateCharacterIndices(); + } + public TabCollections( Selector selector ) { _selector = selector; _manager = Service< ModManager >.Get(); UpdateNames(); - UpdateIndex(); } - private void CreateNewCollection( Dictionary< string, ModSettings > settings ) { - _manager.AddCollection( _newCollectionName, settings ); - _manager.SetCurrentCollection( _newCollectionName ); + if( _manager.Collections.AddCollection( _newCollectionName, settings ) ) + { + _manager.Collections.SetCurrentCollection( _manager.Collections.Collections[ _newCollectionName ] ); + UpdateNames(); + } + _newCollectionName = string.Empty; - UpdateNames(); - UpdateIndex(); } private void DrawNewCollectionInput() @@ -62,7 +99,6 @@ namespace Penumbra.UI ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f ); } - if( ImGui.Button( "Create New Empty Collection" ) && _newCollectionName.Length > 0 ) { CreateNewCollection( new Dictionary< string, ModSettings >() ); @@ -71,7 +107,7 @@ namespace Penumbra.UI ImGui.SameLine(); if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) { - CreateNewCollection( _manager.CurrentCollection.Settings ); + CreateNewCollection( _manager.Collections.CurrentCollection.Settings ); } if( changedStyle ) @@ -79,18 +115,119 @@ namespace Penumbra.UI ImGui.PopStyleVar(); } - if( _manager.Collections.Count > 1 ) + if( _manager.Collections.Collections.Count > 1 + && _manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection ) { ImGui.SameLine(); if( ImGui.Button( "Delete Current Collection" ) ) { - _manager.RemoveCollection( _manager.CurrentCollection.Name ); + _manager.Collections.RemoveCollection( _manager.Collections.CurrentCollection.Name ); UpdateNames(); - UpdateIndex(); } } } + private void DrawCurrentCollectionSelector() + { + var index = _currentCollectionIndex; + if( !ImGui.Combo( "Current Collection", ref index, _collectionNames ) ) + { + return; + } + + if( index != _currentCollectionIndex ) + { + _manager.Collections.SetCurrentCollection( _collections[ index + 1 ] ); + _currentCollectionIndex = index; + _selector.ReloadSelection(); + } + } + + private void DrawForcedCollectionSelector() + { + var index = _currentForcedIndex; + if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) && index != _currentForcedIndex ) + { + _manager.Collections.SetForcedCollection( _collections[ index ] ); + _currentForcedIndex = index; + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( + "Mods in the forced collection are always loaded if not overwritten by anything in the current or character-based collection.\n" + + "Please avoid mixing meta-manipulating mods in Forced and other collections, as this will probably not work correctly." ); + } + + ImGui.SameLine(); + ImGui.Dummy( new Vector2( 24, 0 ) ); + ImGui.SameLine(); + ImGui.Text( "Forced Collection" ); + } + + private void DrawNewCharacterCollection() + { + ImGui.InputTextWithHint( "##New Character", "New Character Name", ref _newCharacterName, 32 ); + + var changedStyle = false; + if( _newCharacterName.Length == 0 ) + { + changedStyle = true; + ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f ); + } + + ImGui.SameLine(); + if( ImGui.Button( "Create New Character Collection" ) && _newCharacterName.Length > 0 ) + { + _manager.Collections.CreateCharacterCollection( _newCharacterName ); + _currentCharacterIndices[ _newCharacterName ] = 0; + } + + if( changedStyle ) + { + ImGui.PopStyleVar(); + } + } + + + private void DrawCharacterCollectionSelectors() + { + if( !ImGui.BeginChild( "##CollectionChild", AutoFillSize, true ) ) + { + return; + } + + DrawForcedCollectionSelector(); + + foreach( var name in _manager.Collections.CharacterCollection.Keys.ToArray() ) + { + var idx = _currentCharacterIndices[ name ]; + var tmp = idx; + if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) + { + _manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); + _currentCharacterIndices[ name ] = tmp; + } + + ImGui.SameLine(); + ImGui.PushFont( UiBuilder.IconFont ); + + if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}" ) ) + { + _manager.Collections.RemoveCharacterCollection( name ); + } + + ImGui.PopFont(); + + ImGui.SameLine(); + ImGui.Text( name ); + } + + DrawNewCharacterCollection(); + + ImGui.EndChild(); + } + public void Draw() { if( !ImGui.BeginTabItem( "Collections" ) ) @@ -98,20 +235,20 @@ namespace Penumbra.UI return; } - var index = _currentIndex; - if( ImGui.Combo( "Current Collection", ref index, _collectionNames, _collectionNames.Length ) ) + if( !ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 6 ), true ) ) { - if( index != _currentIndex && _manager.SetCurrentCollection( _collectionNames[ index ] ) ) - { - _currentIndex = index; - _selector.ReloadSelection(); - var resourceManager = Service< GameResourceManagement >.Get(); - resourceManager.ReloadPlayerResources(); - } + ImGui.EndTabItem(); + return; } - ImGui.Dummy( new Vector2( 0, 5 ) ); + DrawCurrentCollectionSelector(); + + ImGui.Dummy( new Vector2( 0, 10 ) ); DrawNewCollectionInput(); + ImGui.EndChild(); + + DrawCharacterCollectionSelectors(); + ImGui.EndTabItem(); } diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index fdfe0424..e0b4f6fa 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -59,7 +59,7 @@ namespace Penumbra.UI if( ImGui.BeginTable( "##effective_changes", 3, flags, AutoFillSize ) ) { - var currentCollection = _modManager.CurrentCollection.Cache!; + var currentCollection = _modManager.Collections.CurrentCollection.Cache!; foreach( var file in currentCollection.ResolvedFiles ) { DrawFileLine( file.Value, file.Key ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 2bf53ff8..576561a1 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -200,7 +200,7 @@ namespace Penumbra.UI if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) { Mod.Settings.Priority = priority; - var collection = _modManager.CurrentCollection; + var collection = _modManager.Collections.CurrentCollection; collection.Save( _base._plugin.PluginInterface! ); collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0 ); } @@ -218,7 +218,7 @@ namespace Penumbra.UI if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) { Mod.Settings.Enabled = enabled; - var collection = _modManager.CurrentCollection; + var collection = _modManager.Collections.CurrentCollection; collection.Save( _base._plugin.PluginInterface! ); collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0 ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index f18e3fed..b8c1242a 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -75,7 +75,7 @@ namespace Penumbra.UI private readonly ModManager _modManager; private List< Mod.Mod >? Mods - => _modManager.CurrentCollection.Cache?.AvailableMods; + => _modManager.Collections.CurrentCollection.Cache?.AvailableMods; public Mod.Mod? Mod { get; private set; } private int _index; @@ -269,6 +269,7 @@ namespace Penumbra.UI { ImGui.CloseCurrentPopup(); _modManager.DeleteMod( Mod.Data.BasePath ); + ResetModNamesLower(); ClearSelection(); } diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 755ef0a6..fb8b8dc2 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -69,7 +69,11 @@ namespace Penumbra.UI { _config.IsEnabled = enabled; _configChanged = true; - _base._plugin.ActorRefresher.RedrawAll( ); + _base._plugin.ActorRefresher.RedrawAll( enabled ? Redraw.WithSettings : Redraw.WithoutSettings ); + if( _config.EnableActorWatch ) + { + _base._plugin.PlayerWatcher.SetActorWatch( enabled ); + } } }