diff --git a/OtterGui b/OtterGui index d7867dfa..3d346700 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d7867dfa6579d4e69876753e9cde72e13d3372ce +Subproject commit 3d346700e8800c045aa19d70d516d8a4fda2f2ee diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 6c097704..7e7fac3b 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1404,10 +1404,10 @@ public class IpcTester : IDisposable return; } - foreach( var collection in Penumbra.TempMods.CustomCollections.Values ) + foreach( var collection in Penumbra.TempCollections.Values ) { ImGui.TableNextColumn(); - var character = Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown"; + var character = Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown"; if( ImGui.Button( $"Save##{collection.Name}" ) ) { Mod.TemporaryMod.SaveTempCollection( collection, character ); @@ -1416,7 +1416,7 @@ public class IpcTester : IDisposable ImGuiUtil.DrawTableColumn( collection.Name ); ImGuiUtil.DrawTableColumn( collection.ResolvedFiles.Count.ToString() ); ImGuiUtil.DrawTableColumn( collection.MetaCache?.Count.ToString() ?? "0" ); - ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) ); + ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) ); } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 88181927..7105ec72 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,22 +18,23 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 19 ); + => (4, 19); - private Penumbra? _penumbra; - private Lumina.GameData? _lumina; + private CommunicatorService? _communicator; + private Penumbra? _penumbra; + private Lumina.GameData? _lumina; - private readonly Dictionary< ModCollection, ModCollection.ModSettingChangeDelegate > _delegates = new(); + private readonly Dictionary _delegates = new(); - public event Action< string >? PreSettingsPanelDraw; - public event Action< string >? PostSettingsPanelDraw; + public event Action? PreSettingsPanelDraw; + public event Action? PostSettingsPanelDraw; public event GameObjectRedrawnDelegate? GameObjectRedrawn { @@ -82,35 +83,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi public bool Valid => _penumbra != null; - public unsafe PenumbraApi( Penumbra penumbra ) + public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra) { - _penumbra = penumbra; - _lumina = ( Lumina.GameData? )DalamudServices.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( DalamudServices.GameData ); - foreach( var collection in Penumbra.CollectionManager ) - { - SubscribeToCollection( collection ); - } + _communicator = communicator; + _penumbra = penumbra; + _lumina = (Lumina.GameData?)DalamudServices.GameData.GetType() + .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) + ?.GetValue(DalamudServices.GameData); + foreach (var collection in Penumbra.CollectionManager) + SubscribeToCollection(collection); - Penumbra.CollectionManager.CollectionChanged += SubscribeToNewCollections; - Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; - Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; + _communicator.CollectionChange.Event += SubscribeToNewCollections; + Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; + Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; } public unsafe void Dispose() { - Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; - Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; - Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; - _penumbra = null; - _lumina = null; - foreach( var collection in Penumbra.CollectionManager ) + Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator!.CollectionChange.Event -= SubscribeToNewCollections; + Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; + _penumbra = null; + _lumina = null; + _communicator = null; + foreach (var collection in Penumbra.CollectionManager) { - if( _delegates.TryGetValue( collection, out var del ) ) - { + if (_delegates.TryGetValue(collection, out var del)) collection.ModSettingChanged -= del; - } } } @@ -122,17 +121,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.Config.ModDirectory; } - private unsafe void OnResourceLoaded( ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, - ResolveData resolveData ) + private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, + ResolveData resolveData) { - if( resolveData.AssociatedGameObject != IntPtr.Zero ) - { - GameObjectResourceResolved?.Invoke( resolveData.AssociatedGameObject, originalPath.ToString(), - manipulatedPath?.ToString() ?? originalPath.ToString() ); - } + if (resolveData.AssociatedGameObject != IntPtr.Zero) + GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), + manipulatedPath?.ToString() ?? originalPath.ToString()); } - public event Action< string, bool >? ModDirectoryChanged + public event Action? ModDirectoryChanged { add { @@ -149,7 +146,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public bool GetEnabledState() => Penumbra.Config.EnableMods; - public event Action< bool >? EnabledChange + public event Action? EnabledChange { add { @@ -166,42 +163,32 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string GetConfiguration() { CheckInitialized(); - return JsonConvert.SerializeObject( Penumbra.Config, Formatting.Indented ); + return JsonConvert.SerializeObject(Penumbra.Config, Formatting.Indented); } - public event ChangedItemHover? ChangedItemTooltip; + public event ChangedItemHover? ChangedItemTooltip; public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; - public PenumbraApiEc OpenMainWindow( TabType tab, string modDirectory, string modName ) + public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) { CheckInitialized(); - if( _penumbra!.ConfigWindow == null ) - { + if (_penumbra!.ConfigWindow == null) return PenumbraApiEc.SystemDisposed; - } _penumbra!.ConfigWindow.IsOpen = true; - if( !Enum.IsDefined( tab ) ) - { + if (!Enum.IsDefined(tab)) return PenumbraApiEc.InvalidArgument; - } - if( tab != TabType.None ) - { + if (tab != TabType.None) _penumbra!.ConfigWindow.SelectTab = tab; - } - if( tab == TabType.Mods && ( modDirectory.Length > 0 || modName.Length > 0 ) ) + if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) { - if( Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { - _penumbra!.ConfigWindow.SelectMod( mod ); - } + if (Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + _penumbra!.ConfigWindow.SelectMod(mod); else - { return PenumbraApiEc.ModMissing; - } } return PenumbraApiEc.Success; @@ -210,295 +197,269 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void CloseMainWindow() { CheckInitialized(); - if( _penumbra!.ConfigWindow == null ) - { + if (_penumbra!.ConfigWindow == null) return; - } _penumbra!.ConfigWindow.IsOpen = false; } - public void RedrawObject( int tableIndex, RedrawType setting ) + public void RedrawObject(int tableIndex, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( tableIndex, setting ); + _penumbra!.ObjectReloader.RedrawObject(tableIndex, setting); } - public void RedrawObject( string name, RedrawType setting ) + public void RedrawObject(string name, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( name, setting ); + _penumbra!.ObjectReloader.RedrawObject(name, setting); } - public void RedrawObject( GameObject? gameObject, RedrawType setting ) + public void RedrawObject(GameObject? gameObject, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); + _penumbra!.ObjectReloader.RedrawObject(gameObject, setting); } - public void RedrawAll( RedrawType setting ) + public void RedrawAll(RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawAll( setting ); + _penumbra!.ObjectReloader.RedrawAll(setting); } - public string ResolveDefaultPath( string path ) + public string ResolveDefaultPath(string path) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default ); + return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Default); } - public string ResolveInterfacePath( string path ) + public string ResolveInterfacePath(string path) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Interface ); + return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Interface); } - public string ResolvePlayerPath( string path ) + public string ResolvePlayerPath(string path) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, PathResolver.PlayerCollection() ); + return ResolvePath(path, Penumbra.ModManager, PathResolver.PlayerCollection()); } // TODO: cleanup when incrementing API level - public string ResolvePath( string path, string characterName ) - => ResolvePath( path, characterName, ushort.MaxValue ); + public string ResolvePath(string path, string characterName) + => ResolvePath(path, characterName, ushort.MaxValue); - public string ResolveGameObjectPath( string path, int gameObjectIdx ) + public string ResolveGameObjectPath(string path, int gameObjectIdx) { CheckInitialized(); - AssociatedCollection( gameObjectIdx, out var collection ); - return ResolvePath( path, Penumbra.ModManager, collection ); + AssociatedCollection(gameObjectIdx, out var collection); + return ResolvePath(path, Penumbra.ModManager, collection); } - public string ResolvePath( string path, string characterName, ushort worldId ) + public string ResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, - Penumbra.CollectionManager.Individual( NameToIdentifier( characterName, worldId ) ) ); + return ResolvePath(path, Penumbra.ModManager, + Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId))); } // TODO: cleanup when incrementing API level - public string[] ReverseResolvePath( string path, string characterName ) - => ReverseResolvePath( path, characterName, ushort.MaxValue ); + public string[] ReverseResolvePath(string path, string characterName) + => ReverseResolvePath(path, characterName, ushort.MaxValue); - public string[] ReverseResolvePath( string path, string characterName, ushort worldId ) + public string[] ReverseResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return new[] { path }; - } + if (!Penumbra.Config.EnableMods) + return new[] + { + path, + }; - var ret = Penumbra.CollectionManager.Individual( NameToIdentifier( characterName, worldId ) ).ReverseResolvePath( new FullPath( path ) ); - return ret.Select( r => r.ToString() ).ToArray(); + var ret = Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); + return ret.Select(r => r.ToString()).ToArray(); } - public string[] ReverseResolveGameObjectPath( string path, int gameObjectIdx ) + public string[] ReverseResolveGameObjectPath(string path, int gameObjectIdx) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return new[] { path }; - } + if (!Penumbra.Config.EnableMods) + return new[] + { + path, + }; - AssociatedCollection( gameObjectIdx, out var collection ); - var ret = collection.ReverseResolvePath( new FullPath( path ) ); - return ret.Select( r => r.ToString() ).ToArray(); + AssociatedCollection(gameObjectIdx, out var collection); + var ret = collection.ReverseResolvePath(new FullPath(path)); + return ret.Select(r => r.ToString()).ToArray(); } - public string[] ReverseResolvePlayerPath( string path ) + public string[] ReverseResolvePlayerPath(string path) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return new[] { path }; - } + if (!Penumbra.Config.EnableMods) + return new[] + { + path, + }; - var ret = PathResolver.PlayerCollection().ReverseResolvePath( new FullPath( path ) ); - return ret.Select( r => r.ToString() ).ToArray(); + var ret = PathResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); + return ret.Select(r => r.ToString()).ToArray(); } - public (string[], string[][]) ResolvePlayerPaths( string[] forward, string[] reverse ) + public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return ( forward, reverse.Select( p => new[] { p } ).ToArray() ); - } + if (!Penumbra.Config.EnableMods) + return (forward, reverse.Select(p => new[] + { + p, + }).ToArray()); var playerCollection = PathResolver.PlayerCollection(); - var resolved = forward.Select( p => ResolvePath( p, Penumbra.ModManager, playerCollection ) ).ToArray(); - var reverseResolved = playerCollection.ReverseResolvePaths( reverse ); - return ( resolved, reverseResolved.Select( a => a.Select( p => p.ToString() ).ToArray() ).ToArray() ); + var resolved = forward.Select(p => ResolvePath(p, Penumbra.ModManager, playerCollection)).ToArray(); + var reverseResolved = playerCollection.ReverseResolvePaths(reverse); + return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); } - public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolveDefaultPath( gamePath ) ); + public T? GetFile(string gamePath) where T : FileResource + => GetFileIntern(ResolveDefaultPath(gamePath)); - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + public T? GetFile(string gamePath, string characterName) where T : FileResource + => GetFileIntern(ResolvePath(gamePath, characterName)); - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary GetChangedItemsForCollection(string collectionName) { CheckInitialized(); try { - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) collection = ModCollection.Empty; - } - if( collection.HasCache ) - { - return collection.ChangedItems.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Item2 ); - } + if (collection.HasCache) + return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); - Penumbra.Log.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary< string, object? >(); + Penumbra.Log.Warning($"Collection {collectionName} does not exist or is not loaded."); + return new Dictionary(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); + Penumbra.Log.Error($"Could not obtain Changed Items for {collectionName}:\n{e}"); throw; } } - public string GetCollectionForType( ApiCollectionType type ) + public string GetCollectionForType(ApiCollectionType type) { CheckInitialized(); - if( !Enum.IsDefined( type ) ) - { + if (!Enum.IsDefined(type)) return string.Empty; - } - var collection = Penumbra.CollectionManager.ByType( ( CollectionType )type ); + var collection = Penumbra.CollectionManager.ByType((CollectionType)type); return collection?.Name ?? string.Empty; } - public (PenumbraApiEc, string OldCollection) SetCollectionForType( ApiCollectionType type, string collectionName, bool allowCreateNew, bool allowDelete ) + public (PenumbraApiEc, string OldCollection) SetCollectionForType(ApiCollectionType type, string collectionName, bool allowCreateNew, + bool allowDelete) { CheckInitialized(); - if( !Enum.IsDefined( type ) ) + if (!Enum.IsDefined(type)) + return (PenumbraApiEc.InvalidArgument, string.Empty); + + var oldCollection = Penumbra.CollectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; + + if (collectionName.Length == 0) { - return ( PenumbraApiEc.InvalidArgument, string.Empty ); + if (oldCollection.Length == 0) + return (PenumbraApiEc.NothingChanged, oldCollection); + + if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) + return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); + + Penumbra.CollectionManager.RemoveSpecialCollection((CollectionType)type); + return (PenumbraApiEc.Success, oldCollection); } - var oldCollection = Penumbra.CollectionManager.ByType( ( CollectionType )type )?.Name ?? string.Empty; + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + return (PenumbraApiEc.CollectionMissing, oldCollection); - if( collectionName.Length == 0 ) + if (oldCollection.Length == 0) { - if( oldCollection.Length == 0 ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - if( !allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface ) - { - return ( PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection ); - } - - Penumbra.CollectionManager.RemoveSpecialCollection( ( CollectionType )type ); - return ( PenumbraApiEc.Success, oldCollection ); + Penumbra.CollectionManager.CreateSpecialCollection((CollectionType)type); + } + else if (oldCollection == collection.Name) + { + return (PenumbraApiEc.NothingChanged, oldCollection); } - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - return ( PenumbraApiEc.CollectionMissing, oldCollection ); - } - - if( oldCollection.Length == 0 ) - { - if( !allowCreateNew ) - { - return ( PenumbraApiEc.AssignmentCreationDisallowed, oldCollection ); - } - - Penumbra.CollectionManager.CreateSpecialCollection( ( CollectionType )type ); - } - else if( oldCollection == collection.Name ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } - - Penumbra.CollectionManager.SetCollection( collection, ( CollectionType )type ); - return ( PenumbraApiEc.Success, oldCollection ); + Penumbra.CollectionManager.SetCollection(collection, (CollectionType)type); + return (PenumbraApiEc.Success, oldCollection); } - public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject( int gameObjectIdx ) + public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject(int gameObjectIdx) { CheckInitialized(); - var id = AssociatedIdentifier( gameObjectIdx ); - if( !id.IsValid ) - { - return ( false, false, Penumbra.CollectionManager.Default.Name ); - } + var id = AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (false, false, Penumbra.CollectionManager.Default.Name); - if( Penumbra.CollectionManager.Individuals.Individuals.TryGetValue( id, out var collection ) ) - { - return ( true, true, collection.Name ); - } + if (Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) + return (true, true, collection.Name); - AssociatedCollection( gameObjectIdx, out collection ); - return ( true, false, collection.Name ); + AssociatedCollection(gameObjectIdx, out collection); + return (true, false, collection.Name); } - public (PenumbraApiEc, string OldCollection) SetCollectionForObject( int gameObjectIdx, string collectionName, bool allowCreateNew, bool allowDelete ) + public (PenumbraApiEc, string OldCollection) SetCollectionForObject(int gameObjectIdx, string collectionName, bool allowCreateNew, + bool allowDelete) { CheckInitialized(); - var id = AssociatedIdentifier( gameObjectIdx ); - if( !id.IsValid ) + var id = AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name); + + var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; + + if (collectionName.Length == 0) { - return ( PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name ); + if (oldCollection.Length == 0) + return (PenumbraApiEc.NothingChanged, oldCollection); + + if (!allowDelete) + return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); + + var idx = Penumbra.CollectionManager.Individuals.Index(id); + Penumbra.CollectionManager.RemoveIndividualCollection(idx); + return (PenumbraApiEc.Success, oldCollection); } - var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue( id, out var c ) ? c.Name : string.Empty; + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + return (PenumbraApiEc.CollectionMissing, oldCollection); - if( collectionName.Length == 0 ) + if (oldCollection.Length == 0) { - if( oldCollection.Length == 0 ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - if( !allowDelete ) - { - return ( PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection ); - } - - var idx = Penumbra.CollectionManager.Individuals.Index( id ); - Penumbra.CollectionManager.RemoveIndividualCollection( idx ); - return ( PenumbraApiEc.Success, oldCollection ); + var ids = Penumbra.CollectionManager.Individuals.GetGroup(id); + Penumbra.CollectionManager.CreateIndividualCollection(ids); + } + else if (oldCollection == collection.Name) + { + return (PenumbraApiEc.NothingChanged, oldCollection); } - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - return ( PenumbraApiEc.CollectionMissing, oldCollection ); - } - - if( oldCollection.Length == 0 ) - { - if( !allowCreateNew ) - { - return ( PenumbraApiEc.AssignmentCreationDisallowed, oldCollection ); - } - - var ids = Penumbra.CollectionManager.Individuals.GetGroup( id ); - Penumbra.CollectionManager.CreateIndividualCollection( ids ); - } - else if( oldCollection == collection.Name ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } - - Penumbra.CollectionManager.SetCollection( collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index( id ) ); - return ( PenumbraApiEc.Success, oldCollection ); + Penumbra.CollectionManager.SetCollection(collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index(id)); + return (PenumbraApiEc.Success, oldCollection); } - public IList< string > GetCollections() + public IList GetCollections() { CheckInitialized(); - return Penumbra.CollectionManager.Select( c => c.Name ).ToArray(); + return Penumbra.CollectionManager.Select(c => c.Name).ToArray(); } public string GetCurrentCollection() @@ -520,158 +481,140 @@ public class PenumbraApi : IDisposable, IPenumbraApi } // TODO: cleanup when incrementing API level - public (string, bool) GetCharacterCollection( string characterName ) - => GetCharacterCollection( characterName, ushort.MaxValue ); + public (string, bool) GetCharacterCollection(string characterName) + => GetCharacterCollection(characterName, ushort.MaxValue); - public (string, bool) GetCharacterCollection( string characterName, ushort worldId ) + public (string, bool) GetCharacterCollection(string characterName, ushort worldId) { CheckInitialized(); - return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName, worldId ), out var collection ) - ? ( collection.Name, true ) - : ( Penumbra.CollectionManager.Default.Name, false ); + return Penumbra.CollectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) + ? (collection.Name, true) + : (Penumbra.CollectionManager.Default.Name, false); } - public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) + public (IntPtr, string) GetDrawObjectInfo(IntPtr drawObject) { CheckInitialized(); - var (obj, collection) = PathResolver.IdentifyDrawObject( drawObject ); - return ( obj, collection.ModCollection.Name ); + var (obj, collection) = PathResolver.IdentifyDrawObject(drawObject); + return (obj, collection.ModCollection.Name); } - public int GetCutsceneParentIndex( int actorIdx ) + public int GetCutsceneParentIndex(int actorIdx) { CheckInitialized(); - return _penumbra!.PathResolver.CutsceneActor( actorIdx ); + return _penumbra!.PathResolver.CutsceneActor(actorIdx); } - public IList< (string, string) > GetModList() + public IList<(string, string)> GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); + return Penumbra.ModManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); } - public IDictionary< string, (IList< string >, GroupType) >? GetAvailableModSettings( string modDirectory, string modName ) + public IDictionary, GroupType)>? GetAvailableModSettings(string modDirectory, string modName) { CheckInitialized(); - return Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) - ? mod.Groups.ToDictionary( g => g.Name, g => ( ( IList< string > )g.Select( o => o.Name ).ToList(), g.Type ) ) + return Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + ? mod.Groups.ToDictionary(g => g.Name, g => ((IList)g.Select(o => o.Name).ToList(), g.Type)) : null; } - public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName, - string modDirectory, string modName, bool allowInheritance ) + public (PenumbraApiEc, (bool, int, IDictionary>, bool)?) GetCurrentModSettings(string collectionName, + string modDirectory, string modName, bool allowInheritance) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - return ( PenumbraApiEc.CollectionMissing, null ); - } + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + return (PenumbraApiEc.CollectionMissing, null); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { - return ( PenumbraApiEc.ModMissing, null ); - } + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + return (PenumbraApiEc.ModMissing, null); - var settings = allowInheritance ? collection.Settings[ mod.Index ] : collection[ mod.Index ].Settings; - if( settings == null ) - { - return ( PenumbraApiEc.Success, null ); - } + var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings; + if (settings == null) + return (PenumbraApiEc.Success, null); - var shareSettings = settings.ConvertToShareable( mod ); - return ( PenumbraApiEc.Success, - ( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) ); + var shareSettings = settings.ConvertToShareable(mod); + return (PenumbraApiEc.Success, + (shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[mod.Index] != null)); } - public PenumbraApiEc ReloadMod( string modDirectory, string modName ) + public PenumbraApiEc ReloadMod(string modDirectory, string modName) { CheckInitialized(); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - Penumbra.ModManager.ReloadMod( mod.Index ); + Penumbra.ModManager.ReloadMod(mod.Index); return PenumbraApiEc.Success; } - public PenumbraApiEc AddMod( string modDirectory ) + public PenumbraApiEc AddMod(string modDirectory) { CheckInitialized(); - var dir = new DirectoryInfo( Path.Join( Penumbra.ModManager.BasePath.FullName, Path.GetFileName( modDirectory ) ) ); - if( !dir.Exists ) - { + var dir = new DirectoryInfo(Path.Join(Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory))); + if (!dir.Exists) return PenumbraApiEc.FileMissing; - } - Penumbra.ModManager.AddMod( dir ); + Penumbra.ModManager.AddMod(dir); return PenumbraApiEc.Success; } - public PenumbraApiEc DeleteMod( string modDirectory, string modName ) + public PenumbraApiEc DeleteMod(string modDirectory, string modName) { CheckInitialized(); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.NothingChanged; - } - Penumbra.ModManager.DeleteMod( mod.Index ); + Penumbra.ModManager.DeleteMod(mod.Index); return PenumbraApiEc.Success; } - public event Action< string >? ModDeleted; - public event Action< string >? ModAdded; - public event Action< string, string >? ModMoved; + public event Action? ModDeleted; + public event Action? ModAdded; + public event Action? ModMoved; - private void ModPathChangeSubscriber( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory ) + private void ModPathChangeSubscriber(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) { - switch( type ) + switch (type) { case ModPathChangeType.Deleted when oldDirectory != null: - ModDeleted?.Invoke( oldDirectory.Name ); + ModDeleted?.Invoke(oldDirectory.Name); break; case ModPathChangeType.Added when newDirectory != null: - ModAdded?.Invoke( newDirectory.Name ); + ModAdded?.Invoke(newDirectory.Name); break; case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: - ModMoved?.Invoke( oldDirectory.Name, newDirectory.Name ); + ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); break; } } - public (PenumbraApiEc, string, bool) GetModPath( string modDirectory, string modName ) + public (PenumbraApiEc, string, bool) GetModPath(string modDirectory, string modName) { CheckInitialized(); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) - || !_penumbra!.ModFileSystem.FindLeaf( mod, out var leaf ) ) - { - return ( PenumbraApiEc.ModMissing, string.Empty, false ); - } + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) + return (PenumbraApiEc.ModMissing, string.Empty, false); var fullPath = leaf.FullName(); - return ( PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath( mod, fullPath ) ); + return (PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath(mod, fullPath)); } - public PenumbraApiEc SetModPath( string modDirectory, string modName, string newPath ) + public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) { CheckInitialized(); - if( newPath.Length == 0 ) - { + if (newPath.Length == 0) return PenumbraApiEc.InvalidArgument; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) - || !_penumbra!.ModFileSystem.FindLeaf( mod, out var leaf ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) return PenumbraApiEc.ModMissing; - } try { - _penumbra.ModFileSystem.RenameAndMove( leaf, newPath ); + _penumbra.ModFileSystem.RenameAndMove(leaf, newPath); return PenumbraApiEc.Success; } catch @@ -680,311 +623,248 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) + public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModInheritance(mod.Index, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) + public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) + public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, - string optionName ) + public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName, + string optionName) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); - if( groupIdx < 0 ) - { + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) return PenumbraApiEc.OptionGroupMissing; - } - var optionIdx = mod.Groups[ groupIdx ].IndexOf( o => o.Name == optionName ); - if( optionIdx < 0 ) - { + var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); + if (optionIdx < 0) return PenumbraApiEc.OptionMissing; - } - var setting = mod.Groups[ groupIdx ].Type == GroupType.Multi ? 1u << optionIdx : ( uint )optionIdx; + var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx; - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList< string > optionNames ) + public PenumbraApiEc TrySetModSettings(string collectionName, string modDirectory, string modName, string optionGroupName, + IReadOnlyList optionNames) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); - if( groupIdx < 0 ) - { + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) return PenumbraApiEc.OptionGroupMissing; - } - var group = mod.Groups[ groupIdx ]; + var group = mod.Groups[groupIdx]; uint setting = 0; - if( group.Type == GroupType.Single ) + if (group.Type == GroupType.Single) { - var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf( o => o.Name == optionNames[ ^1 ] ); - if( optionIdx < 0 ) - { + var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) return PenumbraApiEc.OptionMissing; - } - setting = ( uint )optionIdx; + setting = (uint)optionIdx; } else { - foreach( var name in optionNames ) + foreach (var name in optionNames) { - var optionIdx = group.IndexOf( o => o.Name == name ); - if( optionIdx < 0 ) - { + var optionIdx = group.IndexOf(o => o.Name == name); + if (optionIdx < 0) return PenumbraApiEc.OptionMissing; - } setting |= 1u << optionIdx; } } - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc CopyModSettings( string? collectionName, string modDirectoryFrom, string modDirectoryTo ) + public PenumbraApiEc CopyModSettings(string? collectionName, string modDirectoryFrom, string modDirectoryTo) { CheckInitialized(); - var sourceModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; - var targetModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; - if( string.IsNullOrEmpty( collectionName ) ) - { - foreach( var collection in Penumbra.CollectionManager ) - { - collection.CopyModSettings( sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo ); - } - } - else if( Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - collection.CopyModSettings( sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo ); - } + var sourceModIdx = Penumbra.ModManager + .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase))?.Index + ?? -1; + var targetModIdx = Penumbra.ModManager + .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index + ?? -1; + if (string.IsNullOrEmpty(collectionName)) + foreach (var collection in Penumbra.CollectionManager) + collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); + else if (Penumbra.CollectionManager.ByName(collectionName, out var collection)) + collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); else - { return PenumbraApiEc.CollectionMissing; - } return PenumbraApiEc.Success; } - public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) + public (PenumbraApiEc, string) CreateTemporaryCollection(string tag, string character, bool forceOverwriteCharacter) { CheckInitialized(); - if( !ActorManager.VerifyPlayerName( character.AsSpan() ) || tag.Length == 0 ) - { - return ( PenumbraApiEc.InvalidArgument, string.Empty ); - } + if (!ActorManager.VerifyPlayerName(character.AsSpan()) || tag.Length == 0) + return (PenumbraApiEc.InvalidArgument, string.Empty); - var identifier = NameToIdentifier( character, ushort.MaxValue ); - if( !identifier.IsValid ) - { - return ( PenumbraApiEc.InvalidArgument, string.Empty ); - } + var identifier = NameToIdentifier(character, ushort.MaxValue); + if (!identifier.IsValid) + return (PenumbraApiEc.InvalidArgument, string.Empty); - if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) - || Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) ) - { - return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); - } + if (!forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier) + || Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier)) + return (PenumbraApiEc.CharacterCollectionExists, string.Empty); var name = $"{tag}_{character}"; - var ret = CreateNamedTemporaryCollection( name ); - if( ret != PenumbraApiEc.Success ) - { - return ( ret, name ); - } + var ret = CreateNamedTemporaryCollection(name); + if (ret != PenumbraApiEc.Success) + return (ret, name); - if( Penumbra.TempMods.AddIdentifier( name, identifier ) ) - { - return ( PenumbraApiEc.Success, name ); - } + if (Penumbra.TempCollections.AddIdentifier(name, identifier)) + return (PenumbraApiEc.Success, name); - Penumbra.TempMods.RemoveTemporaryCollection( name ); - return ( PenumbraApiEc.UnknownError, string.Empty ); + Penumbra.TempCollections.RemoveTemporaryCollection(name); + return (PenumbraApiEc.UnknownError, string.Empty); } - public PenumbraApiEc CreateNamedTemporaryCollection( string name ) + public PenumbraApiEc CreateNamedTemporaryCollection(string name) { CheckInitialized(); - if( name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols( name ) != name ) - { + if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name) return PenumbraApiEc.InvalidArgument; - } - return Penumbra.TempMods.CreateTemporaryCollection( name ).Length > 0 + return Penumbra.TempCollections.CreateTemporaryCollection(name).Length > 0 ? PenumbraApiEc.Success : PenumbraApiEc.CollectionExists; } - public PenumbraApiEc AssignTemporaryCollection( string collectionName, int actorIndex, bool forceAssignment ) + public PenumbraApiEc AssignTemporaryCollection(string collectionName, int actorIndex, bool forceAssignment) { CheckInitialized(); - if( actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length ) - { + if (actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length) return PenumbraApiEc.InvalidArgument; - } - var identifier = Penumbra.Actors.FromObject( DalamudServices.Objects[ actorIndex ], false, false, true ); - if( !identifier.IsValid ) - { + var identifier = Penumbra.Actors.FromObject(DalamudServices.Objects[actorIndex], false, false, true); + if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; - } - if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) ) - { + if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !forceAssignment - && ( Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) ) ) - { + if (!forceAssignment + && (Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier) + || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier))) return PenumbraApiEc.CharacterCollectionExists; - } - var group = Penumbra.TempMods.Collections.GetGroup( identifier ); - return Penumbra.TempMods.AddIdentifier( collection, group ) + var group = Penumbra.TempCollections.Collections.GetGroup(identifier); + return Penumbra.TempCollections.AddIdentifier(collection, group) ? PenumbraApiEc.Success : PenumbraApiEc.UnknownError; } - public PenumbraApiEc RemoveTemporaryCollection( string character ) + public PenumbraApiEc RemoveTemporaryCollection(string character) { CheckInitialized(); - return Penumbra.TempMods.RemoveByCharacterName( character ) + return Penumbra.TempCollections.RemoveByCharacterName(character) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc RemoveTemporaryCollectionByName( string name ) + public PenumbraApiEc RemoveTemporaryCollectionByName(string name) { CheckInitialized(); - return Penumbra.TempMods.RemoveTemporaryCollection( name ) + return Penumbra.TempCollections.RemoveTemporaryCollection(name) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority ) + public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority) { CheckInitialized(); - if( !ConvertPaths( paths, out var p ) ) - { + if (!ConvertPaths(paths, out var p)) return PenumbraApiEc.InvalidGamePath; - } - if( !ConvertManips( manipString, out var m ) ) - { + if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - } - return Penumbra.TempMods.Register( tag, null, p, m, priority ) switch + return Penumbra.TempMods.Register(tag, null, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, }; } - public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, string manipString, - int priority ) + public PenumbraApiEc AddTemporaryMod(string tag, string collectionName, Dictionary paths, string manipString, + int priority) { CheckInitialized(); - if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) - { + if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) + && !Penumbra.CollectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - } - if( !ConvertPaths( paths, out var p ) ) - { + if (!ConvertPaths(paths, out var p)) return PenumbraApiEc.InvalidGamePath; - } - if( !ConvertManips( manipString, out var m ) ) - { + if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - } - return Penumbra.TempMods.Register( tag, collection, p, m, priority ) switch + return Penumbra.TempMods.Register(tag, collection, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, }; } - public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority ) + public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) { CheckInitialized(); - return Penumbra.TempMods.Unregister( tag, null, priority ) switch + return Penumbra.TempMods.Unregister(tag, null, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -992,16 +872,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; } - public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ) + public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority) { CheckInitialized(); - if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) - { + if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) + && !Penumbra.CollectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - } - return Penumbra.TempMods.Unregister( tag, collection, priority ) switch + return Penumbra.TempMods.Unregister(tag, collection, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -1013,115 +891,103 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); var collection = PathResolver.PlayerCollection(); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); - return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } // TODO: cleanup when incrementing API - public string GetMetaManipulations( string characterName ) - => GetMetaManipulations( characterName, ushort.MaxValue ); + public string GetMetaManipulations(string characterName) + => GetMetaManipulations(characterName, ushort.MaxValue); - public string GetMetaManipulations( string characterName, ushort worldId ) + public string GetMetaManipulations(string characterName, ushort worldId) { CheckInitialized(); - var identifier = NameToIdentifier( characterName, worldId ); - var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c ) + var identifier = NameToIdentifier(characterName, worldId); + var collection = Penumbra.TempCollections.Collections.TryGetCollection(identifier, out var c) ? c - : Penumbra.CollectionManager.Individual( identifier ); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); - return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + : Penumbra.CollectionManager.Individual(identifier); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } - public string GetGameObjectMetaManipulations( int gameObjectIdx ) + public string GetGameObjectMetaManipulations(int gameObjectIdx) { CheckInitialized(); - AssociatedCollection( gameObjectIdx, out var collection ); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); - return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + AssociatedCollection(gameObjectIdx, out var collection); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } internal bool HasTooltip => ChangedItemTooltip != null; - internal void InvokeTooltip( object? it ) - => ChangedItemTooltip?.Invoke( it ); + internal void InvokeTooltip(object? it) + => ChangedItemTooltip?.Invoke(it); - internal void InvokeClick( MouseButton button, object? it ) - => ChangedItemClicked?.Invoke( button, it ); + internal void InvokeClick(MouseButton button, object? it) + => ChangedItemClicked?.Invoke(button, it); - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void CheckInitialized() { - if( !Valid ) - { - throw new Exception( "PluginShare is not initialized." ); - } + if (!Valid) + throw new Exception("PluginShare is not initialized."); } // Return the collection associated to a current game object. If it does not exist, return the default collection. // If the index is invalid, returns false and the default collection. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe bool AssociatedCollection( int gameObjectIdx, out ModCollection collection ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { collection = Penumbra.CollectionManager.Default; - if( gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length ) - { + if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) return false; - } - var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); - var data = PathResolver.IdentifyCollection( ptr, false ); - if( data.Valid ) - { + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + var data = PathResolver.IdentifyCollection(ptr, false); + if (data.Valid) collection = data.ModCollection; - } return true; } - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe ActorIdentifier AssociatedIdentifier( int gameObjectIdx ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if( gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length ) - { + if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) return ActorIdentifier.Invalid; - } - var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); - return Penumbra.Actors.FromObject( ptr, out _, false, true, true ); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + return Penumbra.Actors.FromObject(ptr, out _, false, true, true); } // Resolve a path given by string for a specific collection. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static string ResolvePath(string path, Mod.Manager _, ModCollection collection) { - if( !Penumbra.Config.EnableMods ) - { + if (!Penumbra.Config.EnableMods) return path; - } - var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; + var ret = collection.ResolvePath(gamePath); return ret?.ToString() ?? path; } // Get a file for a resolved path. - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + private T? GetFileIntern(string resolvedPath) where T : FileResource { CheckInitialized(); try { - if( Path.IsPathRooted( resolvedPath ) ) - { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); - } + if (Path.IsPathRooted(resolvedPath)) + return _lumina?.GetFileFromDisk(resolvedPath); - return DalamudServices.GameData.GetFile< T >( resolvedPath ); + return DalamudServices.GameData.GetFile(resolvedPath); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Warning( $"Could not load file {resolvedPath}:\n{e}" ); + Penumbra.Log.Warning($"Could not load file {resolvedPath}:\n{e}"); return null; } } @@ -1129,20 +995,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Convert a dictionary of strings to a dictionary of gamepaths to full paths. // Only returns true if all paths can successfully be converted and added. - private static bool ConvertPaths( IReadOnlyDictionary< string, string > redirections, - [NotNullWhen( true )] out Dictionary< Utf8GamePath, FullPath >? paths ) + private static bool ConvertPaths(IReadOnlyDictionary redirections, + [NotNullWhen(true)] out Dictionary? paths) { - paths = new Dictionary< Utf8GamePath, FullPath >( redirections.Count ); - foreach( var (gString, fString) in redirections ) + paths = new Dictionary(redirections.Count); + foreach (var (gString, fString) in redirections) { - if( !Utf8GamePath.FromString( gString, out var path, false ) ) + if (!Utf8GamePath.FromString(gString, out var path, false)) { paths = null; return false; } - var fullPath = new FullPath( fString ); - if( !paths.TryAdd( path, fullPath ) ) + var fullPath = new FullPath(fString); + if (!paths.TryAdd(path, fullPath)) { paths = null; return false; @@ -1155,25 +1021,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Convert manipulations from a transmitted base64 string to actual manipulations. // The empty string is treated as an empty set. // Only returns true if all conversions are successful and distinct. - private static bool ConvertManips( string manipString, - [NotNullWhen( true )] out HashSet< MetaManipulation >? manips ) + private static bool ConvertManips(string manipString, + [NotNullWhen(true)] out HashSet? manips) { - if( manipString.Length == 0 ) + if (manipString.Length == 0) { - manips = new HashSet< MetaManipulation >(); + manips = new HashSet(); return true; } - if( Functions.FromCompressedBase64< MetaManipulation[] >( manipString, out var manipArray ) != MetaManipulation.CurrentVersion ) + if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) { manips = null; return false; } - manips = new HashSet< MetaManipulation >( manipArray!.Length ); - foreach( var manip in manipArray.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) + manips = new HashSet(manipArray!.Length); + foreach (var manip in manipArray.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) { - if( !manips.Add( manip ) ) + if (!manips.Add(manip)) { manips = null; return false; @@ -1183,46 +1049,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } - private void SubscribeToCollection( ModCollection c ) + private void SubscribeToCollection(ModCollection c) { var name = c.Name; - void Del( ModSettingChange type, int idx, int _, int _2, bool inherited ) - => ModSettingChanged?.Invoke( type, name, idx >= 0 ? Penumbra.ModManager[ idx ].ModPath.Name : string.Empty, inherited ); + void Del(ModSettingChange type, int idx, int _, int _2, bool inherited) + => ModSettingChanged?.Invoke(type, name, idx >= 0 ? Penumbra.ModManager[idx].ModPath.Name : string.Empty, inherited); - _delegates[ c ] = Del; + _delegates[c] = Del; c.ModSettingChanged += Del; } - private void SubscribeToNewCollections( CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _ ) + private void SubscribeToNewCollections(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _) { - if( type != CollectionType.Inactive ) - { + if (type != CollectionType.Inactive) return; - } - if( oldCollection != null && _delegates.TryGetValue( oldCollection, out var del ) ) - { + if (oldCollection != null && _delegates.TryGetValue(oldCollection, out var del)) oldCollection.ModSettingChanged -= del; - } - if( newCollection != null ) - { - SubscribeToCollection( newCollection ); - } + if (newCollection != null) + SubscribeToCollection(newCollection); } - public void InvokePreSettingsPanel( string modDirectory ) - => PreSettingsPanelDraw?.Invoke( modDirectory ); + public void InvokePreSettingsPanel(string modDirectory) + => PreSettingsPanelDraw?.Invoke(modDirectory); - public void InvokePostSettingsPanel( string modDirectory ) - => PostSettingsPanelDraw?.Invoke( modDirectory ); + public void InvokePostSettingsPanel(string modDirectory) + => PostSettingsPanelDraw?.Invoke(modDirectory); // TODO: replace all usages with ActorIdentifier stuff when incrementing API - private static ActorIdentifier NameToIdentifier( string name, ushort worldId ) + private static ActorIdentifier NameToIdentifier(string name, ushort worldId) { // Verified to be valid name beforehand. - var b = ByteString.FromStringUnsafe( name, false ); - return Penumbra.Actors.CreatePlayer( b, worldId ); + var b = ByteString.FromStringUnsafe(name, false); + return Penumbra.Actors.CreatePlayer(b, worldId); } -} \ No newline at end of file +} diff --git a/Penumbra/Api/TempCollectionManager.cs b/Penumbra/Api/TempCollectionManager.cs new file mode 100644 index 00000000..fab7eace --- /dev/null +++ b/Penumbra/Api/TempCollectionManager.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.String; + +namespace Penumbra.Api; + +public class TempCollectionManager : IDisposable +{ + public int GlobalChangeCounter { get; private set; } = 0; + public readonly IndividualCollections Collections; + + private readonly CommunicatorService _communicator; + private readonly Dictionary _customCollections = new(); + + public TempCollectionManager(CommunicatorService communicator, IndividualCollections collections) + { + _communicator = communicator; + Collections = collections; + + _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; + } + + public void Dispose() + { + _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + } + + private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_customCollections.Values, mod, created, removed); + + public int Count + => _customCollections.Count; + + public IEnumerable Values + => _customCollections.Values; + + public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _customCollections.TryGetValue(name.ToLowerInvariant(), out collection); + + public string CreateTemporaryCollection(string name) + { + if (Penumbra.CollectionManager.ByName(name, out _)) + return string.Empty; + + if (GlobalChangeCounter == int.MaxValue) + GlobalChangeCounter = 0; + var collection = ModCollection.CreateNewTemporary(name, GlobalChangeCounter++); + if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection)) + return collection.Name; + + collection.ClearCache(); + return string.Empty; + } + + public bool RemoveTemporaryCollection(string collectionName) + { + if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection)) + return false; + + GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); + collection.ClearCache(); + for (var i = 0; i < Collections.Count; ++i) + { + if (Collections[i].Collection == collection) + { + _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); + Collections.Delete(i); + } + } + + return true; + } + + public bool AddIdentifier(ModCollection collection, params ActorIdentifier[] identifiers) + { + if (Collections.Add(identifiers, collection)) + { + _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); + return true; + } + + return false; + } + + public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers) + { + if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection)) + return false; + + return AddIdentifier(collection, identifiers); + } + + public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue) + { + if (!ByteString.FromString(characterName, out var byteString, false)) + return false; + + var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId); + if (!identifier.IsValid) + return false; + + return AddIdentifier(collectionName, identifier); + } + + internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue) + { + if (!ByteString.FromString(characterName, out var byteString, false)) + return false; + + var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId); + return Collections.Individuals.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); + } +} diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 08393ebd..3064d326 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -3,10 +3,7 @@ using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Penumbra.GameData.Actors; -using Penumbra.String; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Api; @@ -19,319 +16,120 @@ public enum RedirectResult FilteredGamePath = 3, } -public class TempModManager +public class TempModManager : IDisposable { - public int GlobalChangeCounter { get; private set; } = 0; - private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new(); - private readonly List< Mod.TemporaryMod > _modsForAllCollections = new(); - private readonly Dictionary< string, ModCollection > _customCollections = new(); - public readonly IndividualCollections Collections = new(Penumbra.Actors); + private readonly CommunicatorService _communicator; - public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; + private readonly Dictionary> _mods = new(); + private readonly List _modsForAllCollections = new(); - public IReadOnlyDictionary< ModCollection, List< Mod.TemporaryMod > > Mods + public TempModManager(CommunicatorService communicator) + { + _communicator = communicator; + _communicator.CollectionChange.Event += OnCollectionChange; + } + + public void Dispose() + { + _communicator.CollectionChange.Event -= OnCollectionChange; + } + + public IReadOnlyDictionary> Mods => _mods; - public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections + public IReadOnlyList ModsForAllCollections => _modsForAllCollections; - public IReadOnlyDictionary< string, ModCollection > CustomCollections - => _customCollections; - - public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => _customCollections.TryGetValue( name.ToLowerInvariant(), out collection ); - - // These functions to check specific redirections or meta manipulations for existence are currently unused. - //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) - //{ - // var mod = GetExistingMod( tag, collection, null ); - // if( mod == null ) - // { - // priority = 0; - // fullPath = null; - // return false; - // } - // - // priority = mod.Priority; - // if( mod.Default.Files.TryGetValue( gamePath, out var f ) ) - // { - // fullPath = f; - // return true; - // } - // - // fullPath = null; - // return false; - //} - // - //public bool IsRegistered( string tag, ModCollection? collection, MetaManipulation meta, out MetaManipulation? manipulation, - // out int priority ) - //{ - // var mod = GetExistingMod( tag, collection, null ); - // if( mod == null ) - // { - // priority = 0; - // manipulation = null; - // return false; - // } - // - // priority = mod.Priority; - // // IReadOnlySet has no TryGetValue for some reason. - // if( ( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var manip ) ) - // { - // manipulation = manip; - // return true; - // } - // - // manipulation = null; - // return false; - //} - - // These functions for setting single redirections or manips are currently unused. - //public RedirectResult Register( string tag, ModCollection? collection, Utf8GamePath path, FullPath file, int priority ) - //{ - // if( Mod.FilterFile( path ) ) - // { - // return RedirectResult.FilteredGamePath; - // } - // - // var mod = GetOrCreateMod( tag, collection, priority, out var created ); - // - // var changes = !mod.Default.Files.TryGetValue( path, out var oldFile ) || !oldFile.Equals( file ); - // mod.SetFile( path, file ); - // ApplyModChange( mod, collection, created, false ); - // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; - //} - // - //public RedirectResult Register( string tag, ModCollection? collection, MetaManipulation meta, int priority ) - //{ - // var mod = GetOrCreateMod( tag, collection, priority, out var created ); - // var changes = !( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var oldMeta ) - // || !oldMeta.Equals( meta ); - // mod.SetManipulation( meta ); - // ApplyModChange( mod, collection, created, false ); - // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; - //} - - public RedirectResult Register( string tag, ModCollection? collection, Dictionary< Utf8GamePath, FullPath > dict, - HashSet< MetaManipulation > manips, int priority ) + public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, + HashSet manips, int priority) { - var mod = GetOrCreateMod( tag, collection, priority, out var created ); - mod.SetAll( dict, manips ); - ApplyModChange( mod, collection, created, false ); + var mod = GetOrCreateMod(tag, collection, priority, out var created); + mod.SetAll(dict, manips); + ApplyModChange(mod, collection, created, false); return RedirectResult.Success; } - public RedirectResult Unregister( string tag, ModCollection? collection, int? priority ) + public RedirectResult Unregister(string tag, ModCollection? collection, int? priority) { - var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; - if( list == null ) - { + var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null; + if (list == null) return RedirectResult.NotRegistered; - } - var removed = list.RemoveAll( m => + var removed = list.RemoveAll(m => { - if( m.Name != tag || priority != null && m.Priority != priority.Value ) - { + if (m.Name != tag || priority != null && m.Priority != priority.Value) return false; - } - ApplyModChange( m, collection, false, true ); + ApplyModChange(m, collection, false, true); return true; - } ); + }); - if( removed == 0 ) - { + if (removed == 0) return RedirectResult.NotRegistered; - } - if( list.Count == 0 && collection != null ) - { - _mods.Remove( collection ); - } + if (list.Count == 0 && collection != null) + _mods.Remove(collection); return RedirectResult.Success; } - public string CreateTemporaryCollection( string name ) - { - if( Penumbra.CollectionManager.ByName( name, out _ ) ) - { - return string.Empty; - } - - if( GlobalChangeCounter == int.MaxValue ) - GlobalChangeCounter = 0; - var collection = ModCollection.CreateNewTemporary( name, GlobalChangeCounter++ ); - if( _customCollections.TryAdd( collection.Name.ToLowerInvariant(), collection ) ) - { - return collection.Name; - } - - collection.ClearCache(); - return string.Empty; - } - - public bool RemoveTemporaryCollection( string collectionName ) - { - if( !_customCollections.Remove( collectionName.ToLowerInvariant(), out var collection ) ) - { - return false; - } - - GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); - _mods.Remove( collection ); - collection.ClearCache(); - for( var i = 0; i < Collections.Count; ++i ) - { - if( Collections[ i ].Collection == collection ) - { - CollectionChanged?.Invoke( CollectionType.Temporary, collection, null, Collections[ i ].DisplayName ); - Collections.Delete( i ); - } - } - - return true; - } - - public bool AddIdentifier( ModCollection collection, params ActorIdentifier[] identifiers ) - { - if( Collections.Add( identifiers, collection ) ) - { - CollectionChanged?.Invoke( CollectionType.Temporary, null, collection, Collections.Last().DisplayName ); - return true; - } - - return false; - } - - public bool AddIdentifier( string collectionName, params ActorIdentifier[] identifiers ) - { - if( !_customCollections.TryGetValue( collectionName.ToLowerInvariant(), out var collection ) ) - { - return false; - } - - return AddIdentifier( collection, identifiers ); - } - - public bool AddIdentifier( string collectionName, string characterName, ushort worldId = ushort.MaxValue ) - { - if( !ByteString.FromString( characterName, out var byteString, false ) ) - { - return false; - } - - var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId ); - if( !identifier.IsValid ) - { - return false; - } - - return AddIdentifier( collectionName, identifier ); - } - - internal bool RemoveByCharacterName( string characterName, ushort worldId = ushort.MaxValue ) - { - if( !ByteString.FromString( characterName, out var byteString, false ) ) - { - return false; - } - - var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId ); - return Collections.Individuals.TryGetValue( identifier, out var collection ) && RemoveTemporaryCollection( collection.Name ); - } - - // Apply any new changes to the temporary mod. - private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed ) + private void ApplyModChange(Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed) { - if( collection == null ) + if (collection != null) { - if( removed ) - { - foreach( var c in Penumbra.CollectionManager ) - { - c.Remove( mod ); - } - } + if (removed) + collection.Remove(mod); else - { - foreach( var c in Penumbra.CollectionManager ) - { - c.Apply( mod, created ); - } - } + collection.Apply(mod, created); } else { - if( removed ) - { - collection.Remove( mod ); - } - else - { - collection.Apply( mod, created ); - } + _communicator.TemporaryGlobalModChange.Invoke(mod, created, removed); } } - - // Only find already existing mods, currently unused. - //private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority ) - //{ - // var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; - // if( list == null ) - // { - // return null; - // } - // - // if( priority != null ) - // { - // return list.Find( m => m.Priority == priority.Value && m.Name == tag ); - // } - // - // Mod.TemporaryMod? highestMod = null; - // var highestPriority = int.MinValue; - // foreach( var m in list ) - // { - // if( highestPriority < m.Priority && m.Name == tag ) - // { - // highestPriority = m.Priority; - // highestMod = m; - // } - // } - // - // return highestMod; - //} + + /// + /// Apply a mod change to a set of collections. + /// + public static void OnGlobalModChange(IEnumerable collections, Mod.TemporaryMod mod, bool created, bool removed) + { + if (removed) + foreach (var c in collections) + c.Remove(mod); + else + foreach (var c in collections) + c.Apply(mod, created); + } // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). // Returns the found or created mod and whether it was newly created. - private Mod.TemporaryMod GetOrCreateMod( string tag, ModCollection? collection, int priority, out bool created ) + private Mod.TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created) { - List< Mod.TemporaryMod > list; - if( collection == null ) + List list; + if (collection == null) { list = _modsForAllCollections; } - else if( _mods.TryGetValue( collection, out var l ) ) + else if (_mods.TryGetValue(collection, out var l)) { list = l; } else { - list = new List< Mod.TemporaryMod >(); - _mods.Add( collection, list ); + list = new List(); + _mods.Add(collection, list); } - var mod = list.Find( m => m.Priority == priority && m.Name == tag ); - if( mod == null ) + var mod = list.Find(m => m.Priority == priority && m.Name == tag); + if (mod == null) { mod = new Mod.TemporaryMod() { Name = tag, Priority = priority, }; - list.Add( mod ); + list.Add(mod); created = true; } else @@ -341,4 +139,11 @@ public class TempModManager return mod; } -} \ No newline at end of file + + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, + string _) + { + if (collectionType is CollectionType.Temporary or CollectionType.Inactive && newCollection == null && oldCollection != null) + _mods.Remove(oldCollection); + } +} diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 81750870..fd6da3ad 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -9,10 +9,11 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin; using Penumbra.GameData.Actors; using Penumbra.Util; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Collections; public partial class ModCollection @@ -21,9 +22,6 @@ public partial class ModCollection { public const int Version = 1; - // Is invoked after the collections actually changed. - public event CollectionChangeDelegate CollectionChanged; - // The collection currently selected for changing settings. public ModCollection Current { get; private set; } = Empty; @@ -40,65 +38,62 @@ public partial class ModCollection private ModCollection DefaultName { get; set; } = Empty; // The list of character collections. + // TODO public readonly IndividualCollections Individuals = new(Penumbra.Actors); - public ModCollection Individual( ActorIdentifier identifier ) - => Individuals.TryGetCollection( identifier, out var c ) ? c : Default; + public ModCollection Individual(ActorIdentifier identifier) + => Individuals.TryGetCollection(identifier, out var c) ? c : Default; // Special Collections - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< Api.Enums.ApiCollectionType >().Length - 3]; + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; // Return the configured collection for the given type or null. // Does not handle Inactive, use ByName instead. - public ModCollection? ByType( CollectionType type ) - => ByType( type, ActorIdentifier.Invalid ); + public ModCollection? ByType(CollectionType type) + => ByType(type, ActorIdentifier.Invalid); - public ModCollection? ByType( CollectionType type, ActorIdentifier identifier ) + public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) { - if( type.IsSpecial() ) - { - return _specialCollections[ ( int )type ]; - } + if (type.IsSpecial()) + return _specialCollections[(int)type]; return type switch { CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : null, + CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null, _ => null, }; } // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. - private void SetCollection( int newIdx, CollectionType collectionType, int individualIndex = -1 ) + private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) { var oldCollectionIdx = collectionType switch { - CollectionType.Default => Default.Index, - CollectionType.Interface => Interface.Index, - CollectionType.Current => Current.Index, - CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count ? -1 : Individuals[ individualIndex ].Collection.Index, - _ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Default => Default.Index, + CollectionType.Interface => Interface.Index, + CollectionType.Current => Current.Index, + CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count + ? -1 + : Individuals[individualIndex].Collection.Index, + _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, _ => -1, }; - if( oldCollectionIdx == -1 || newIdx == oldCollectionIdx ) - { + if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) return; - } - var newCollection = this[ newIdx ]; - if( newIdx > Empty.Index ) - { + var newCollection = this[newIdx]; + if (newIdx > Empty.Index) newCollection.CreateCache(); - } - switch( collectionType ) + switch (collectionType) { case CollectionType.Default: Default = newCollection; - if( Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if (Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) { Penumbra.ResidentResources.Reload(); Default.SetFiles(); @@ -112,362 +107,336 @@ public partial class ModCollection Current = newCollection; break; case CollectionType.Individual: - if( !Individuals.ChangeCollection( individualIndex, newCollection ) ) + if (!Individuals.ChangeCollection(individualIndex, newCollection)) { - RemoveCache( newIdx ); + RemoveCache(newIdx); return; } break; default: - _specialCollections[ ( int )collectionType ] = newCollection; + _specialCollections[(int)collectionType] = newCollection; break; } - RemoveCache( oldCollectionIdx ); + RemoveCache(oldCollectionIdx); UpdateCurrentCollectionInUse(); - CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, collectionType == CollectionType.Individual ? Individuals[ individualIndex ].DisplayName : string.Empty ); + _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, + collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); } private void UpdateCurrentCollectionInUse() => CurrentCollectionInUse = _specialCollections - .OfType< ModCollection >() - .Prepend( Interface ) - .Prepend( Default ) - .Concat( Individuals.Assignments.Select( kvp => kvp.Collection ) ) - .SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); + .OfType() + .Prepend(Interface) + .Prepend(Default) + .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) + .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); - public void SetCollection( ModCollection collection, CollectionType collectionType, int individualIndex = -1 ) - => SetCollection( collection.Index, collectionType, individualIndex ); + public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) + => SetCollection(collection.Index, collectionType, individualIndex); // Create a special collection if it does not exist and set it to Empty. - public bool CreateSpecialCollection( CollectionType collectionType ) + public bool CreateSpecialCollection(CollectionType collectionType) { - if( !collectionType.IsSpecial() || _specialCollections[ ( int )collectionType ] != null ) - { + if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) return false; - } - _specialCollections[ ( int )collectionType ] = Default; - CollectionChanged.Invoke( collectionType, null, Default ); + _specialCollections[(int)collectionType] = Default; + _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); return true; } // Remove a special collection if it exists - public void RemoveSpecialCollection( CollectionType collectionType ) + public void RemoveSpecialCollection(CollectionType collectionType) { - if( !collectionType.IsSpecial() ) - { + if (!collectionType.IsSpecial()) return; - } - var old = _specialCollections[ ( int )collectionType ]; - if( old != null ) + var old = _specialCollections[(int)collectionType]; + if (old != null) { - _specialCollections[ ( int )collectionType ] = null; - CollectionChanged.Invoke( collectionType, old, null ); + _specialCollections[(int)collectionType] = null; + _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); } } // Wrappers around Individual Collection handling. - public void CreateIndividualCollection( params ActorIdentifier[] identifiers ) + public void CreateIndividualCollection(params ActorIdentifier[] identifiers) { - if( Individuals.Add( identifiers, Default ) ) - { - CollectionChanged.Invoke( CollectionType.Individual, null, Default, Individuals.Last().DisplayName ); - } + if (Individuals.Add(identifiers, Default)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); } - public void RemoveIndividualCollection( int individualIndex ) + public void RemoveIndividualCollection(int individualIndex) { - if( individualIndex < 0 || individualIndex >= Individuals.Count ) - { + if (individualIndex < 0 || individualIndex >= Individuals.Count) return; - } - var (name, old) = Individuals[ individualIndex ]; - if( Individuals.Delete( individualIndex ) ) - { - CollectionChanged.Invoke( CollectionType.Individual, old, null, name ); - } + var (name, old) = Individuals[individualIndex]; + if (Individuals.Delete(individualIndex)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); } - public void MoveIndividualCollection( int from, int to ) + public void MoveIndividualCollection(int from, int to) { - if( Individuals.Move( from, to ) ) - { + if (Individuals.Move(from, to)) SaveActiveCollections(); - } } // Obtain the index of a collection by name. - private int GetIndexForCollectionName( string name ) - => name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name ); + private int GetIndexForCollectionName(string name) + => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name); - public static string ActiveCollectionFile - => Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); + public static string ActiveCollectionFile(DalamudPluginInterface pi) + => Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); // Load default, current, special, and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. private void LoadCollections() { - var configChanged = !ReadActiveCollections( out var jObject ); + var configChanged = !ReadActiveCollections(out var jObject); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); - var defaultIdx = GetIndexForCollectionName( defaultName ); - if( defaultIdx < 0 ) + var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? DefaultCollection : Empty.Name); + var defaultIdx = GetIndexForCollectionName(defaultName); + if (defaultIdx < 0) { - ChatUtil.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning ); + ChatUtil.NotificationMessage( + $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", + NotificationType.Warning); Default = Empty; configChanged = true; } else { - Default = this[ defaultIdx ]; + Default = this[defaultIdx]; } // Load the interface collection. - var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? Default.Name; - var interfaceIdx = GetIndexForCollectionName( interfaceName ); - if( interfaceIdx < 0 ) + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + var interfaceIdx = GetIndexForCollectionName(interfaceName); + if (interfaceIdx < 0) { ChatUtil.NotificationMessage( - $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning ); + $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", + "Load Failure", NotificationType.Warning); Interface = Empty; configChanged = true; } else { - Interface = this[ interfaceIdx ]; + Interface = this[interfaceIdx]; } // Load the current collection. - var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; - var currentIdx = GetIndexForCollectionName( currentName ); - if( currentIdx < 0 ) + var currentName = jObject[nameof(Current)]?.ToObject() ?? DefaultCollection; + var currentIdx = GetIndexForCollectionName(currentName); + if (currentIdx < 0) { ChatUtil.NotificationMessage( - $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning ); + $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", + "Load Failure", NotificationType.Warning); Current = DefaultName; configChanged = true; } else { - Current = this[ currentIdx ]; + Current = this[currentIdx]; } // Load special collections. - foreach( var (type, name, _) in CollectionTypeExtensions.Special ) + foreach (var (type, name, _) in CollectionTypeExtensions.Special) { - var typeName = jObject[ type.ToString() ]?.ToObject< string >(); - if( typeName != null ) + var typeName = jObject[type.ToString()]?.ToObject(); + if (typeName != null) { - var idx = GetIndexForCollectionName( typeName ); - if( idx < 0 ) + var idx = GetIndexForCollectionName(typeName); + if (idx < 0) { - ChatUtil.NotificationMessage( $"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", NotificationType.Warning ); + ChatUtil.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", + NotificationType.Warning); configChanged = true; } else { - _specialCollections[ ( int )type ] = this[ idx ]; + _specialCollections[(int)type] = this[idx]; } } } - configChanged |= MigrateIndividualCollections( jObject ); - configChanged |= Individuals.ReadJObject( jObject[ nameof( Individuals ) ] as JArray, this ); + configChanged |= MigrateIndividualCollections(jObject); + configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); // Save any changes and create all required caches. - if( configChanged ) - { + if (configChanged) SaveActiveCollections(); - } } // Migrate ungendered collections to Male and Female for 0.5.9.0. - public static void MigrateUngenderedCollections() + public static void MigrateUngenderedCollections(FilenameService fileNames) { - if( !ReadActiveCollections( out var jObject ) ) - { + if (!ReadActiveCollections(out var jObject)) return; - } - foreach( var (type, _, _) in CollectionTypeExtensions.Special.Where( t => t.Item2.StartsWith( "Male " ) ) ) + foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) { - var oldName = type.ToString()[ 4.. ]; - var value = jObject[ oldName ]; - if( value == null ) - { + var oldName = type.ToString()[4..]; + var value = jObject[oldName]; + if (value == null) continue; - } - jObject.Remove( oldName ); - jObject.Add( "Male" + oldName, value ); - jObject.Add( "Female" + oldName, value ); + jObject.Remove(oldName); + jObject.Add("Male" + oldName, value); + jObject.Add("Female" + oldName, value); } - using var stream = File.Open( ActiveCollectionFile, FileMode.Truncate ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); + using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; - jObject.WriteTo( j ); + jObject.WriteTo(j); } // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections( JObject jObject ) + private bool MigrateIndividualCollections(JObject jObject) { - var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0; - if( version > 0 ) - { + var version = jObject[nameof(Version)]?.Value() ?? 0; + if (version > 0) return false; - } // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject[ "Characters" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); - var dict = new Dictionary< string, ModCollection >( characters.Count ); - foreach( var (player, collectionName) in characters ) + var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); + var dict = new Dictionary(characters.Count); + foreach (var (player, collectionName) in characters) { - var idx = GetIndexForCollectionName( collectionName ); - if( idx < 0 ) + var idx = GetIndexForCollectionName(collectionName); + if (idx < 0) { - ChatUtil.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning ); - dict.Add( player, Empty ); + ChatUtil.NotificationMessage( + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", + NotificationType.Warning); + dict.Add(player, Empty); } else { - dict.Add( player, this[ idx ] ); + dict.Add(player, this[idx]); } } - Individuals.Migrate0To1( dict ); + Individuals.Migrate0To1(dict); return true; } public void SaveActiveCollections() { - Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), - SaveActiveCollectionsInternal ); + Penumbra.Framework.RegisterDelayed(nameof(SaveActiveCollections), + SaveActiveCollectionsInternal); } internal void SaveActiveCollectionsInternal() { - var file = ActiveCollectionFile; + // TODO + var file = ActiveCollectionFile(DalamudServices.PluginInterface); try { var jObj = new JObject { - { nameof( Version ), Version }, - { nameof( Default ), Default.Name }, - { nameof( Interface ), Interface.Name }, - { nameof( Current ), Current.Name }, + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, }; - foreach( var (type, collection) in _specialCollections.WithIndex().Where( p => p.Value != null ).Select( p => ( ( CollectionType )p.Index, p.Value! ) ) ) - { - jObj.Add( type.ToString(), collection.Name ); - } + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); - jObj.Add( nameof( Individuals ), Individuals.ToJObject() ); - using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ) - { Formatting = Formatting.Indented }; - jObj.WriteTo( j ); - Penumbra.Log.Verbose( "Active Collections saved." ); + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + Penumbra.Log.Verbose("Active Collections saved."); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" ); + Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); } } // Read the active collection file into a jObject. // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections( out JObject ret ) + private static bool ReadActiveCollections(out JObject ret) { - var file = ActiveCollectionFile; - if( File.Exists( file ) ) - { + // TODO + var file = ActiveCollectionFile(DalamudServices.PluginInterface); + if (File.Exists(file)) try { - ret = JObject.Parse( File.ReadAllText( file ) ); + ret = JObject.Parse(File.ReadAllText(file)); return true; } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not read active collections from file {file}:\n{e}" ); + Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); } - } ret = new JObject(); return false; } // Save if any of the active collections is changed. - private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3 ) + private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) { - if( collectionType != CollectionType.Inactive ) - { + if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) SaveActiveCollections(); - } } // Cache handling. Usually recreate caches on the next framework tick, // but at launch create all of them at once. public void CreateNecessaryCaches() { - var tasks = _specialCollections.OfType< ModCollection >() - .Concat( Individuals.Select( p => p.Collection ) ) - .Prepend( Current ) - .Prepend( Default ) - .Prepend( Interface ) - .Distinct() - .Select( c => Task.Run( c.CalculateEffectiveFileListInternal ) ) - .ToArray(); + var tasks = _specialCollections.OfType() + .Concat(Individuals.Select(p => p.Collection)) + .Prepend(Current) + .Prepend(Default) + .Prepend(Interface) + .Distinct() + .Select(c => Task.Run(c.CalculateEffectiveFileListInternal)) + .ToArray(); - Task.WaitAll( tasks ); + Task.WaitAll(tasks); } - private void RemoveCache( int idx ) + private void RemoveCache(int idx) { - if( idx != Empty.Index - && idx != Default.Index - && idx != Interface.Index - && idx != Current.Index - && _specialCollections.All( c => c == null || c.Index != idx ) - && Individuals.Select( p => p.Collection ).All( c => c.Index != idx ) ) - { - _collections[ idx ].ClearCache(); - } + if (idx != Empty.Index + && idx != Default.Index + && idx != Interface.Index + && idx != Current.Index + && _specialCollections.All(c => c == null || c.Index != idx) + && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) + _collections[idx].ClearCache(); } // Recalculate effective files for active collections on events. - private void OnModAddedActive( Mod mod ) + private void OnModAddedActive(Mod mod) { - foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) - { - collection._cache!.AddMod( mod, true ); - } + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.AddMod(mod, true); } - private void OnModRemovedActive( Mod mod ) + private void OnModRemovedActive(Mod mod) { - foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) - { - collection._cache!.RemoveMod( mod, true ); - } + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.RemoveMod(mod, true); } - private void OnModMovedActive( Mod mod ) + private void OnModMovedActive(Mod mod) { - foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) - { - collection._cache!.ReloadMod( mod, true ); - } + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.ReloadMod(mod, true); } } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 2610c455..a07bc5af 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -7,60 +7,60 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Penumbra.Api; +using Penumbra.Services; namespace Penumbra.Collections; public partial class ModCollection { - public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > + public sealed partial class Manager : IDisposable, IEnumerable { - // On addition, oldCollection is null. On deletion, newCollection is null. - // displayName is only set for type == Individual. - public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection, - ModCollection? newCollection, string displayName = "" ); - - private readonly Mod.Manager _modManager; + private readonly Mod.Manager _modManager; + private readonly CommunicatorService _communicator; // The empty collection is always available and always has index 0. // It can not be deleted or moved. - private readonly List< ModCollection > _collections = new() + private readonly List _collections = new() { Empty, }; - public ModCollection this[ Index idx ] - => _collections[ idx ]; + public ModCollection this[Index idx] + => _collections[idx]; - public ModCollection? this[ string name ] - => ByName( name, out var c ) ? c : null; + public ModCollection? this[string name] + => ByName(name, out var c) ? c : null; public int Count => _collections.Count; // Obtain a collection case-independently by name. - public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); + public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); // Default enumeration skips the empty collection. - public IEnumerator< ModCollection > GetEnumerator() - => _collections.Skip( 1 ).GetEnumerator(); + public IEnumerator GetEnumerator() + => _collections.Skip(1).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public IEnumerable< ModCollection > GetEnumeratorWithEmpty() + public IEnumerable GetEnumeratorWithEmpty() => _collections; - public Manager( Mod.Manager manager ) + public Manager(CommunicatorService communicator, Mod.Manager manager) { - _modManager = manager; + _communicator = communicator; + _modManager = manager; // The collection manager reacts to changes in mods by itself. - _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; - _modManager.ModOptionChanged += OnModOptionsChanged; - _modManager.ModPathChanged += OnModPathChange; - CollectionChanged += SaveOnChange; + _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; + _modManager.ModOptionChanged += OnModOptionsChanged; + _modManager.ModPathChanged += OnModPathChange; + _communicator.CollectionChange.Event += SaveOnChange; + _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; ReadCollections(); LoadCollections(); UpdateCurrentCollectionInUse(); @@ -68,26 +68,31 @@ public partial class ModCollection public void Dispose() { - _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; - _modManager.ModOptionChanged -= OnModOptionsChanged; - _modManager.ModPathChanged -= OnModPathChange; + _communicator.CollectionChange.Event -= SaveOnChange; + _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; + _modManager.ModOptionChanged -= OnModOptionsChanged; + _modManager.ModPathChanged -= OnModPathChange; } + private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + // Returns true if the name is not empty, it is not the name of the empty collection // and no existing collection results in the same filename as name. - public bool CanAddCollection( string name, out string fixedName ) + public bool CanAddCollection(string name, out string fixedName) { - if( !IsValidName( name ) ) + if (!IsValidName(name)) { fixedName = string.Empty; return false; } name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( name.Length == 0 - || name == Empty.Name.ToLowerInvariant() - || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name ) ) + if (name.Length == 0 + || name == Empty.Name.ToLowerInvariant() + || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) { fixedName = string.Empty; return false; @@ -102,217 +107,179 @@ public partial class ModCollection // If the name of the collection would result in an already existing filename, skip it. // Returns true if the collection was successfully created and fires a Inactive event. // Also sets the current collection to the new collection afterwards. - public bool AddCollection( string name, ModCollection? duplicate ) + public bool AddCollection(string name, ModCollection? duplicate) { - if( !CanAddCollection( name, out var fixedName ) ) + if (!CanAddCollection(name, out var fixedName)) { - Penumbra.Log.Warning( $"The new collection {name} would lead to the same path {fixedName} as one that already exists." ); + Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); return false; } - var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); + var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); newCollection.Index = _collections.Count; - _collections.Add( newCollection ); + _collections.Add(newCollection); newCollection.Save(); - Penumbra.Log.Debug( $"Added collection {newCollection.AnonymizedName}." ); - CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); - SetCollection( newCollection.Index, CollectionType.Current ); + Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); + SetCollection(newCollection.Index, CollectionType.Current); return true; } // Remove the given collection if it exists and is neither the empty nor the default-named collection. // If the removed collection was active, it also sets the corresponding collection to the appropriate default. // Also removes the collection from inheritances of all other collections. - public bool RemoveCollection( int idx ) + public bool RemoveCollection(int idx) { - if( idx <= Empty.Index || idx >= _collections.Count ) + if (idx <= Empty.Index || idx >= _collections.Count) { - Penumbra.Log.Error( "Can not remove the empty collection." ); + Penumbra.Log.Error("Can not remove the empty collection."); return false; } - if( idx == DefaultName.Index ) + if (idx == DefaultName.Index) { - Penumbra.Log.Error( "Can not remove the default collection." ); + Penumbra.Log.Error("Can not remove the default collection."); return false; } - if( idx == Current.Index ) + if (idx == Current.Index) + SetCollection(DefaultName.Index, CollectionType.Current); + + if (idx == Default.Index) + SetCollection(Empty.Index, CollectionType.Default); + + for (var i = 0; i < _specialCollections.Length; ++i) { - SetCollection( DefaultName.Index, CollectionType.Current ); + if (idx == _specialCollections[i]?.Index) + SetCollection(Empty, (CollectionType)i); } - if( idx == Default.Index ) + for (var i = 0; i < Individuals.Count; ++i) { - SetCollection( Empty.Index, CollectionType.Default ); + if (Individuals[i].Collection.Index == idx) + SetCollection(Empty, CollectionType.Individual, i); } - for( var i = 0; i < _specialCollections.Length; ++i ) - { - if( idx == _specialCollections[ i ]?.Index ) - { - SetCollection( Empty, ( CollectionType )i ); - } - } - - for( var i = 0; i < Individuals.Count; ++i ) - { - if( Individuals[ i ].Collection.Index == idx ) - { - SetCollection( Empty, CollectionType.Individual, i ); - } - } - - var collection = _collections[ idx ]; + var collection = _collections[idx]; // Clear own inheritances. - foreach( var inheritance in collection.Inheritance ) - { - collection.ClearSubscriptions( inheritance ); - } + foreach (var inheritance in collection.Inheritance) + collection.ClearSubscriptions(inheritance); collection.Delete(); - _collections.RemoveAt( idx ); + _collections.RemoveAt(idx); // Clear external inheritances. - foreach( var c in _collections ) + foreach (var c in _collections) { - var inheritedIdx = c._inheritance.IndexOf( collection ); - if( inheritedIdx >= 0 ) - { - c.RemoveInheritance( inheritedIdx ); - } + var inheritedIdx = c._inheritance.IndexOf(collection); + if (inheritedIdx >= 0) + c.RemoveInheritance(inheritedIdx); - if( c.Index > idx ) - { + if (c.Index > idx) --c.Index; - } } - Penumbra.Log.Debug( $"Removed collection {collection.AnonymizedName}." ); - CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); + Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); return true; } - public bool RemoveCollection( ModCollection collection ) - => RemoveCollection( collection.Index ); + public bool RemoveCollection(ModCollection collection) + => RemoveCollection(collection.Index); private void OnModDiscoveryStarted() { - foreach( var collection in this ) - { + foreach (var collection in this) collection.PrepareModDiscovery(); - } } private void OnModDiscoveryFinished() { // First, re-apply all mod settings. - foreach( var collection in this ) - { + foreach (var collection in this) collection.ApplyModSettings(); - } // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. - foreach( var collection in this.Where( c => c.HasCache ) ) - { + foreach (var collection in this.Where(c => c.HasCache)) collection.ForceCacheUpdate(); - } } // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory ) + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) { - switch( type ) + switch (type) { case ModPathChangeType.Added: - foreach( var collection in this ) - { - collection.AddMod( mod ); - } + foreach (var collection in this) + collection.AddMod(mod); - OnModAddedActive( mod ); + OnModAddedActive(mod); break; case ModPathChangeType.Deleted: - OnModRemovedActive( mod ); - foreach( var collection in this ) - { - collection.RemoveMod( mod, mod.Index ); - } + OnModRemovedActive(mod); + foreach (var collection in this) + collection.RemoveMod(mod, mod.Index); break; case ModPathChangeType.Moved: - OnModMovedActive( mod ); - foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) ) - { + OnModMovedActive(mod); + foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) collection.Save(); - } break; case ModPathChangeType.StartingReload: - OnModRemovedActive( mod ); + OnModRemovedActive(mod); break; case ModPathChangeType.Reloaded: - OnModAddedActive( mod ); + OnModAddedActive(mod); break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } // Automatically update all relevant collections when a mod is changed. // This means saving if options change in a way where the settings may change and the collection has settings for this mod. // And also updating effective file and meta manipulation lists if necessary. - private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) + private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) { // Handle changes that break revertability. - if( type == ModOptionChangeType.PrepareChange ) + if (type == ModOptionChangeType.PrepareChange) { - foreach( var collection in this.Where( c => c.HasCache ) ) + foreach (var collection in this.Where(c => c.HasCache)) { - if( collection[ mod.Index ].Settings is { Enabled: true } ) - { - collection._cache!.RemoveMod( mod, false ); - } + if (collection[mod.Index].Settings is { Enabled: true }) + collection._cache!.RemoveMod(mod, false); } return; } - type.HandlingInfo( out var requiresSaving, out var recomputeList, out var reload ); + type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); // Handle changes that require overwriting the collection. - if( requiresSaving ) - { - foreach( var collection in this ) + if (requiresSaving) + foreach (var collection in this) { - if( collection._settings[ mod.Index ]?.HandleChanges( type, mod, groupIdx, optionIdx, movedToIdx ) ?? false ) - { + if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) collection.Save(); - } } - } // Handle changes that reload the mod if the changes did not need to be prepared, // or re-add the mod if they were prepared. - if( recomputeList ) - { - foreach( var collection in this.Where( c => c.HasCache ) ) + if (recomputeList) + foreach (var collection in this.Where(c => c.HasCache)) { - if( collection[ mod.Index ].Settings is { Enabled: true } ) + if (collection[mod.Index].Settings is { Enabled: true }) { - if( reload ) - { - collection._cache!.ReloadMod( mod, true ); - } + if (reload) + collection._cache!.ReloadMod(mod, true); else - { - collection._cache!.AddMod( mod, true ); - } + collection._cache!.AddMod(mod, true); } } - } } // Add the collection with the default name if it does not exist. @@ -320,44 +287,42 @@ public partial class ModCollection // This can also not be deleted, so there are always at least the empty and a collection with default name. private void AddDefaultCollection() { - var idx = GetIndexForCollectionName( DefaultCollection ); - if( idx >= 0 ) + var idx = GetIndexForCollectionName(DefaultCollection); + if (idx >= 0) { - DefaultName = this[ idx ]; + DefaultName = this[idx]; return; } - var defaultCollection = CreateNewEmpty( DefaultCollection ); + var defaultCollection = CreateNewEmpty(DefaultCollection); defaultCollection.Save(); defaultCollection.Index = _collections.Count; - _collections.Add( defaultCollection ); + _collections.Add(defaultCollection); } // Inheritances can not be setup before all collections are read, // so this happens after reading the collections. - private void ApplyInheritances( IEnumerable< IReadOnlyList< string > > inheritances ) + private void ApplyInheritances(IEnumerable> inheritances) { - foreach( var (collection, inheritance) in this.Zip( inheritances ) ) + foreach (var (collection, inheritance) in this.Zip(inheritances)) { var changes = false; - foreach( var subCollectionName in inheritance ) + foreach (var subCollectionName in inheritance) { - if( !ByName( subCollectionName, out var subCollection ) ) + if (!ByName(subCollectionName, out var subCollection)) { changes = true; - Penumbra.Log.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); + Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); } - else if( !collection.AddInheritance( subCollection, false ) ) + else if (!collection.AddInheritance(subCollection, false)) { changes = true; - Penumbra.Log.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); + Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); } } - if( changes ) - { + if (changes) collection.Save(); - } } } @@ -366,38 +331,33 @@ public partial class ModCollection // Duplicate collection files are not deleted, just not added here. private void ReadCollections() { - var collectionDir = new DirectoryInfo( CollectionDirectory ); - var inheritances = new List< IReadOnlyList< string > >(); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + // TODO + var collectionDir = new DirectoryInfo(CollectionDirectory(DalamudServices.PluginInterface)); + var inheritances = new List>(); + if (collectionDir.Exists) + foreach (var file in collectionDir.EnumerateFiles("*.json")) { - var collection = LoadFromFile( file, out var inheritance ); - if( collection == null || collection.Name.Length == 0 ) - { + var collection = LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) continue; - } - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - Penumbra.Log.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } + if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") + Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - if( this[ collection.Name ] != null ) + if (this[collection.Name] != null) { - Penumbra.Log.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); } else { - inheritances.Add( inheritance ); + inheritances.Add(inheritance); collection.Index = _collections.Count; - _collections.Add( collection ); + _collections.Add(collection); } } - } AddDefaultCollection(); - ApplyInheritances( inheritances ); + ApplyInheritances(inheritances); } } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index 61ea2c0e..d52fd69f 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using OtterGui.Filesystem; using Penumbra.GameData.Actors; +using Penumbra.Services; using Penumbra.String; namespace Penumbra.Collections; @@ -19,8 +20,12 @@ public sealed partial class IndividualCollections public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals => _individuals; + + // TODO + public IndividualCollections( ActorService actorManager ) + => _actorManager = actorManager.AwaitedService; - public IndividualCollections( ActorManager actorManager ) + public IndividualCollections(ActorManager actorManager) => _actorManager = actorManager; public enum AddResult diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 60ab44e0..6474cbf5 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -2,24 +2,26 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Dalamud.Plugin; namespace Penumbra.Collections; // File operations like saving, loading and deleting for a collection. public partial class ModCollection { - public static string CollectionDirectory - => Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "collections" ); + public static string CollectionDirectory(DalamudPluginInterface pi) + => Path.Combine( pi.GetPluginConfigDirectory(), "collections" ); - // We need to remove all invalid path symbols from the collection name to be able to save it to file. + // We need to remove all invalid path symbols from the collection name to be able to save it to file. + // TODO public FileInfo FileName - => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); + => new(Path.Combine( CollectionDirectory(DalamudServices.PluginInterface), $"{Name.RemoveInvalidPathSymbols()}.json" )); // Custom serialization due to shared mod information across managers. private void SaveCollection() diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index f5ce009d..f1060880 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -90,7 +90,7 @@ public partial class ModCollection var collection = new ModCollection( name, Empty ); collection.ModSettingChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange; - collection.Index = ~Penumbra.TempMods.Collections.Count; + collection.Index = ~Penumbra.TempCollections.Count; collection.ChangeCounter = changeCounter; collection.CreateCache(); return collection; diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs deleted file mode 100644 index a4b368dd..00000000 --- a/Penumbra/Configuration.Migration.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; -using Penumbra.Collections; -using Penumbra.Mods; -using Penumbra.Services; - -namespace Penumbra; - -public partial class Configuration -{ - // Contains everything to migrate from older versions of the config to the current, - // including deprecated fields. - private class Migration - { - private Configuration _config = null!; - private JObject _data = null!; - - public string CurrentCollection = ModCollection.DefaultCollection; - public string DefaultCollection = ModCollection.DefaultCollection; - public string ForcedCollection = string.Empty; - public Dictionary< string, string > CharacterCollections = new(); - public Dictionary< string, string > ModSortOrder = new(); - public bool InvertModListOrder; - public bool SortFoldersFirst; - public SortModeV3 SortMode = SortModeV3.FoldersFirst; - - public static void Migrate( Configuration config ) - { - if( !File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) ) - { - return; - } - - var m = new Migration - { - _config = config, - _data = JObject.Parse( File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ) ), - }; - - CreateBackup(); - m.Version0To1(); - m.Version1To2(); - m.Version2To3(); - m.Version3To4(); - m.Version4To5(); - m.Version5To6(); - m.Version6To7(); - } - - // Gendered special collections were added. - private void Version6To7() - { - if( _config.Version != 6 ) - return; - - ModCollection.Manager.MigrateUngenderedCollections(); - _config.Version = 7; - } - - - // A new tutorial step was inserted in the middle. - // The UI collection and a new tutorial for it was added. - // The migration for the UI collection itself happens in the ActiveCollections file. - private void Version5To6() - { - if( _config.Version != 5 ) - { - return; - } - if( _config.TutorialStep == 25 ) - { - _config.TutorialStep = 27; - } - - _config.Version = 6; - } - - // Mod backup extension was changed from .zip to .pmp. - // Actual migration takes place in ModManager. - private void Version4To5() - { - if( _config.Version != 4 ) - { - return; - } - - Mod.Manager.MigrateModBackups = true; - _config.Version = 5; - } - - // SortMode was changed from an enum to a type. - private void Version3To4() - { - if( _config.Version != 3 ) - { - return; - } - - SortMode = _data[ nameof( SortMode ) ]?.ToObject< SortModeV3 >() ?? SortMode; - _config.SortMode = SortMode switch - { - SortModeV3.FoldersFirst => ISortMode< Mod >.FoldersFirst, - SortModeV3.Lexicographical => ISortMode< Mod >.Lexicographical, - SortModeV3.InverseFoldersFirst => ISortMode< Mod >.InverseFoldersFirst, - SortModeV3.InverseLexicographical => ISortMode< Mod >.InverseLexicographical, - SortModeV3.FoldersLast => ISortMode< Mod >.FoldersLast, - SortModeV3.InverseFoldersLast => ISortMode< Mod >.InverseFoldersLast, - SortModeV3.InternalOrder => ISortMode< Mod >.InternalOrder, - SortModeV3.InternalOrderInverse => ISortMode< Mod >.InverseInternalOrder, - _ => ISortMode< Mod >.FoldersFirst, - }; - _config.Version = 4; - } - - // SortFoldersFirst was changed from a bool to the enum SortMode. - private void Version2To3() - { - if( _config.Version != 2 ) - { - return; - } - - SortFoldersFirst = _data[ nameof( SortFoldersFirst ) ]?.ToObject< bool >() ?? false; - SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; - _config.Version = 3; - } - - // The forced collection was removed due to general inheritance. - // Sort Order was moved to a separate file and may contain empty folders. - // Active collections in general were moved to their own file. - // Delete the penumbrametatmp folder if it exists. - private void Version1To2() - { - if( _config.Version != 1 ) - { - return; - } - - // Ensure the right meta files are loaded. - DeleteMetaTmp(); - Penumbra.CharacterUtility.LoadCharacterResources(); - ResettleSortOrder(); - ResettleCollectionSettings(); - ResettleForcedCollection(); - _config.Version = 2; - } - - private void DeleteMetaTmp() - { - var path = Path.Combine( _config.ModDirectory, "penumbrametatmp" ); - if( Directory.Exists( path ) ) - { - try - { - Directory.Delete( path, true ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete the outdated penumbrametatmp folder:\n{e}" ); - } - } - } - - private void ResettleForcedCollection() - { - ForcedCollection = _data[ nameof( ForcedCollection ) ]?.ToObject< string >() ?? ForcedCollection; - if( ForcedCollection.Length <= 0 ) - { - return; - } - - // Add the previous forced collection to all current collections except itself as an inheritance. - foreach( var collection in Directory.EnumerateFiles( ModCollection.CollectionDirectory, "*.json" ) ) - { - try - { - var jObject = JObject.Parse( File.ReadAllText( collection ) ); - if( jObject[ nameof( ModCollection.Name ) ]?.ToObject< string >() != ForcedCollection ) - { - jObject[ nameof( ModCollection.Inheritance ) ] = JToken.FromObject( new List< string >() { ForcedCollection } ); - File.WriteAllText( collection, jObject.ToString() ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( - $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}" ); - } - } - } - - // Move the current sort order to its own file. - private void ResettleSortOrder() - { - ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder; - var file = ModFileSystem.ModFileSystemFile; - using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName( "Data" ); - j.WriteStartObject(); - foreach( var (mod, path) in ModSortOrder.Where( kvp => Directory.Exists( Path.Combine( _config.ModDirectory, kvp.Key ) ) ) ) - { - j.WritePropertyName( mod, true ); - j.WriteValue( path ); - } - - j.WriteEndObject(); - j.WritePropertyName( "EmptyFolders" ); - j.WriteStartArray(); - j.WriteEndArray(); - j.WriteEndObject(); - } - - // Move the active collections to their own file. - private void ResettleCollectionSettings() - { - CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; - DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; - CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; - SaveActiveCollectionsV0( DefaultCollection, CurrentCollection, DefaultCollection, - CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() ); - } - - // Outdated saving using the Characters list. - private static void SaveActiveCollectionsV0( string def, string ui, string current, IEnumerable<(string, string)> characters, - IEnumerable<(CollectionType, string)> special ) - { - var file = ModCollection.Manager.ActiveCollectionFile; - try - { - using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName( nameof( ModCollection.Manager.Default ) ); - j.WriteValue( def ); - j.WritePropertyName( nameof( ModCollection.Manager.Interface ) ); - j.WriteValue( ui ); - j.WritePropertyName( nameof( ModCollection.Manager.Current ) ); - j.WriteValue( current ); - foreach( var (type, collection) in special ) - { - j.WritePropertyName( type.ToString() ); - j.WriteValue( collection ); - } - - j.WritePropertyName( "Characters" ); - j.WriteStartObject(); - foreach( var (character, collection) in characters ) - { - j.WritePropertyName( character, true ); - j.WriteValue( collection ); - } - - j.WriteEndObject(); - j.WriteEndObject(); - Penumbra.Log.Verbose( "Active Collections saved." ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" ); - } - } - - // Collections were introduced and the previous CurrentCollection got put into ModDirectory. - private void Version0To1() - { - if( _config.Version != 0 ) - { - return; - } - - _config.ModDirectory = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? string.Empty; - _config.Version = 1; - ResettleCollectionJson(); - } - - // Move the previous mod configurations to a new default collection file. - private void ResettleCollectionJson() - { - var collectionJson = new FileInfo( Path.Combine( _config.ModDirectory, "collection.json" ) ); - if( !collectionJson.Exists ) - { - return; - } - - var defaultCollection = ModCollection.CreateNewEmpty( ModCollection.DefaultCollection ); - var defaultCollectionFile = defaultCollection.FileName; - if( defaultCollectionFile.Exists ) - { - return; - } - - try - { - var text = File.ReadAllText( collectionJson.FullName ); - var data = JArray.Parse( text ); - - var maxPriority = 0; - var dict = new Dictionary< string, ModSettings.SavedSettings >(); - foreach( var setting in data.Cast< JObject >() ) - { - var modName = ( string )setting[ "FolderName" ]!; - var enabled = ( bool )setting[ "Enabled" ]!; - var priority = ( int )setting[ "Priority" ]!; - var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, long > >() - ?? setting[ "Conf" ]!.ToObject< Dictionary< string, long > >(); - - dict[ modName ] = new ModSettings.SavedSettings() - { - Enabled = enabled, - Priority = priority, - Settings = settings!, - }; - maxPriority = Math.Max( maxPriority, priority ); - } - - InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder; - if( !InvertModListOrder ) - { - dict = dict.ToDictionary( kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority } ); - } - - defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict ); - defaultCollection.Save(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); - throw; - } - } - - // Create a backup of the configuration file specifically. - private static void CreateBackup() - { - var name = DalamudServices.PluginInterface.ConfigFile.FullName; - var bakName = name + ".bak"; - try - { - File.Copy( name, bakName, true ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); - } - } - - public enum SortModeV3 : byte - { - FoldersFirst = 0x00, - Lexicographical = 0x01, - InverseFoldersFirst = 0x02, - InverseLexicographical = 0x03, - FoldersLast = 0x04, - InverseFoldersLast = 0x05, - InternalOrder = 0x06, - InternalOrderInverse = 0x07, - } - } -} \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 1821ca1f..4d98a4a1 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -19,8 +19,14 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; [Serializable] -public partial class Configuration : IPluginConfiguration +public class Configuration : IPluginConfiguration { + [JsonIgnore] + private readonly string _fileName; + + [JsonIgnore] + private readonly FrameworkManager _framework; + public int Version { get; set; } = Constants.CurrentVersion; public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion; @@ -86,47 +92,44 @@ public partial class Configuration : IPluginConfiguration public Dictionary< ColorId, uint > Colors { get; set; } = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); - // Load the current configuration. - // Includes adding new colors and migrating from old versions. - public static Configuration Load() + /// + /// Load the current configuration. + /// Includes adding new colors and migrating from old versions. + /// + public Configuration(FilenameService fileNames, ConfigMigrationService migrator, FrameworkManager framework) { - void HandleDeserializationError( object? sender, ErrorEventArgs errorArgs ) + _fileName = fileNames.ConfigFile; + _framework = framework; + Load(migrator); + } + + public void Load(ConfigMigrationService migrator) + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) { Penumbra.Log.Error( - $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}" ); + $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); errorArgs.ErrorContext.Handled = true; } - Configuration? configuration = null; - if( File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) ) + if (File.Exists(_fileName)) { - var text = File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ); - configuration = JsonConvert.DeserializeObject< Configuration >( text, new JsonSerializerSettings + var text = File.ReadAllText(_fileName); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings { Error = HandleDeserializationError, - } ); + }); } - - configuration ??= new Configuration(); - if( configuration.Version == Constants.CurrentVersion ) - { - configuration.AddColors( false ); - return configuration; - } - - Migration.Migrate( configuration ); - configuration.AddColors( true ); - - return configuration; + migrator.Migrate(this); } - // Save the current configuration. + /// Save the current configuration. private void SaveConfiguration() { try { var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( DalamudServices.PluginInterface.ConfigFile.FullName, text ); + File.WriteAllText( _fileName, text ); } catch( Exception e ) { @@ -135,24 +138,9 @@ public partial class Configuration : IPluginConfiguration } public void Save() - => Penumbra.Framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); + => _framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); - // Add missing colors to the dictionary if necessary. - private void AddColors( bool forceSave ) - { - var save = false; - foreach( var color in Enum.GetValues< ColorId >() ) - { - save |= Colors.TryAdd( color, color.Data().DefaultColor ); - } - - if( save || forceSave ) - { - Save(); - } - } - - // Contains some default values or boundaries for config values. + /// Contains some default values or boundaries for config values. public static class Constants { public const int CurrentVersion = 7; @@ -178,6 +166,7 @@ public partial class Configuration : IPluginConfiguration }; } + /// Convert SortMode Types to their name. private class SortModeConverter : JsonConverter< ISortMode< Mod > > { public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer ) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 307e55c3..0f3a46d5 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Game; using Dalamud.Utility.Signatures; using Penumbra.GameData; -using Penumbra.Services; namespace Penumbra.Interop; @@ -52,14 +52,17 @@ public unsafe partial class CharacterUtility : IDisposable public (IntPtr Address, int Size) DefaultResource( InternalIndex idx ) => _lists[ idx.Value ].DefaultResource; - public CharacterUtility() + private readonly Framework _framework; + + public CharacterUtility(Framework framework) { SignatureHelper.Initialise( this ); + _framework = framework; LoadingFinished += () => Penumbra.Log.Debug( "Loading of CharacterUtility finished." ); LoadDefaultResources( null! ); if( !Ready ) { - DalamudServices.Framework.Update += LoadDefaultResources; + _framework.Update += LoadDefaultResources; } } @@ -99,7 +102,7 @@ public unsafe partial class CharacterUtility : IDisposable if( !anyMissing ) { Ready = true; - DalamudServices.Framework.Update -= LoadDefaultResources; + _framework.Update -= LoadDefaultResources; LoadingFinished.Invoke(); } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index d921e7f3..8e60d26f 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -10,7 +10,6 @@ using FFXIVClientStructs.STD; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; -using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -249,18 +248,4 @@ public unsafe partial class ResourceLoader Penumbra.Log.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); return 1; } - - // Logging functions for EnableFullLogging. - private static void LogPath( Utf8GamePath path, bool synchronous ) - => Penumbra.Log.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - - private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data ) - { - var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); - Penumbra.Log.Information( - $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) " ); - } - - private static void LogLoadedFile( Structs.ResourceHandle* resource, ByteString path, bool success, bool custom ) - => Penumbra.Log.Information( $"[ResourceLoader] Loading {path} from {( custom ? "local files" : "SqPack" )} into 0x{( ulong )resource:X} returned {success}." ); } \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 2eb0e010..5ad06e93 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -18,39 +18,6 @@ public unsafe partial class ResourceLoader : IDisposable // Hooks are required for everything, even events firing. public bool HooksEnabled { get; private set; } - // This Logging just logs all file requests, returns and loads to the Dalamud log. - // Events can be used to make smarter logging. - public bool IsLoggingEnabled { get; private set; } - - public void EnableFullLogging() - { - if( IsLoggingEnabled ) - { - return; - } - - IsLoggingEnabled = true; - ResourceRequested += LogPath; - ResourceLoaded += LogResource; - FileLoaded += LogLoadedFile; - ResourceHandleDestructorHook?.Enable(); - EnableHooks(); - } - - public void DisableFullLogging() - { - if( !IsLoggingEnabled ) - { - return; - } - - IsLoggingEnabled = false; - ResourceRequested -= LogPath; - ResourceLoaded -= LogResource; - FileLoaded -= LogLoadedFile; - ResourceHandleDestructorHook?.Disable(); - } - public void EnableReplacements() { if( DoReplacements ) @@ -150,7 +117,6 @@ public unsafe partial class ResourceLoader : IDisposable public void Dispose() { - DisableFullLogging(); DisposeHooks(); DisposeTexMdlTreatment(); } diff --git a/Penumbra/Interop/Loader/ResourceLogger.cs b/Penumbra/Interop/Loader/ResourceLogger.cs deleted file mode 100644 index dceaad36..00000000 --- a/Penumbra/Interop/Loader/ResourceLogger.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Penumbra.String; -using Penumbra.String.Classes; - -namespace Penumbra.Interop.Loader; - -// A logger class that contains the relevant data to log requested files via regex. -// Filters are case-insensitive. -public class ResourceLogger : IDisposable -{ - // Enable or disable the logging of resources subject to the current filter. - public void SetState( bool value ) - { - if( value == Penumbra.Config.EnableResourceLogging ) - { - return; - } - - Penumbra.Config.EnableResourceLogging = value; - Penumbra.Config.Save(); - if( value ) - { - _resourceLoader.ResourceRequested += OnResourceRequested; - } - else - { - _resourceLoader.ResourceRequested -= OnResourceRequested; - } - } - - // Set the current filter to a new string, doing all other necessary work. - public void SetFilter( string newFilter ) - { - if( newFilter == Filter ) - { - return; - } - - Penumbra.Config.ResourceLoggingFilter = newFilter; - Penumbra.Config.Save(); - SetupRegex(); - } - - // Returns whether the current filter is a valid regular expression. - public bool ValidRegex - => _filterRegex != null; - - private readonly ResourceLoader _resourceLoader; - private Regex? _filterRegex; - - private static string Filter - => Penumbra.Config.ResourceLoggingFilter; - - private void SetupRegex() - { - try - { - _filterRegex = new Regex( Filter, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant ); - } - catch - { - _filterRegex = null; - } - } - - public ResourceLogger( ResourceLoader loader ) - { - _resourceLoader = loader; - SetupRegex(); - if( Penumbra.Config.EnableResourceLogging ) - { - _resourceLoader.ResourceRequested += OnResourceRequested; - } - } - - private void OnResourceRequested( Utf8GamePath data, bool synchronous ) - { - var path = Match( data.Path ); - if( path != null ) - { - Penumbra.Log.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" ); - } - } - - // Returns the converted string if the filter matches, and null otherwise. - // The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it. - private string? Match( ByteString data ) - { - var s = data.ToString(); - return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.OrdinalIgnoreCase ) ) - ? s - : null; - } - - public void Dispose() - => _resourceLoader.ResourceRequested -= OnResourceRequested; -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index 71e54d07..9cf765d0 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Penumbra.Services; - + namespace Penumbra.Interop.Resolver; public class CutsceneCharacters : IDisposable @@ -14,39 +14,39 @@ public class CutsceneCharacters : IDisposable public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; private readonly GameEventManager _events; - private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray(); + private readonly ObjectTable _objects; + private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); - public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors - => Enumerable.Range( CutsceneStartIdx, CutsceneSlots ) - .Where( i => DalamudServices.Objects[ i ] != null ) - .Select( i => KeyValuePair.Create( i, this[ i ] ?? DalamudServices.Objects[ i ]! ) ); + public IEnumerable> Actors + => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) + .Where(i => _objects[i] != null) + .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public CutsceneCharacters(GameEventManager events) + public CutsceneCharacters(ObjectTable objects, GameEventManager events) { - _events = events; + _objects = objects; + _events = events; Enable(); } // Get the related actor to a cutscene actor. // Does not check for valid input index. // Returns null if no connected actor is set or the actor does not exist anymore. - public global::Dalamud.Game.ClientState.Objects.Types.GameObject? this[ int idx ] + public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] { get { - Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx ); - idx = _copiedCharacters[ idx - CutsceneStartIdx ]; - return idx < 0 ? null : DalamudServices.Objects[ idx ]; + Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx); + idx = _copiedCharacters[idx - CutsceneStartIdx]; + return idx < 0 ? null : _objects[idx]; } } // Return the currently set index of a parent or -1 if none is set or the index is invalid. - public int GetParentIndex( int idx ) + public int GetParentIndex(int idx) { - if( idx is >= CutsceneStartIdx and < CutsceneEndIdx ) - { - return _copiedCharacters[ idx - CutsceneStartIdx ]; - } + if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) + return _copiedCharacters[idx - CutsceneStartIdx]; return -1; } @@ -66,21 +66,21 @@ public class CutsceneCharacters : IDisposable public void Dispose() => Disable(); - private unsafe void OnCharacterDestructor( Character* character ) + private unsafe void OnCharacterDestructor(Character* character) { - if( character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) + if (character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx) { var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; - _copiedCharacters[ idx ] = -1; + _copiedCharacters[idx] = -1; } } - private unsafe void OnCharacterCopy( Character* target, Character* source ) + private unsafe void OnCharacterCopy(Character* target, Character* source) { - if( target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) + if (target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx) { var idx = target->GameObject.ObjectIndex - CutsceneStartIdx; - _copiedCharacters[idx] = (short) (source != null ? source->GameObject.ObjectIndex : -1); + _copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1); } } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index efdb67c5..cad29dc0 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -5,72 +5,68 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; using Penumbra.GameData.Actors; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Interop.Resolver; -public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > +public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> { - private readonly GameEventManager _events; - private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317); - private bool _dirty = false; - private bool _enabled = false; + private readonly CommunicatorService _communicator; + private readonly GameEventManager _events; + private readonly Dictionary _cache = new(317); + private bool _dirty = false; + private bool _enabled = false; - public IdentifiedCollectionCache(GameEventManager events) + public IdentifiedCollectionCache(CommunicatorService communicator, GameEventManager events) { - _events = events; + _communicator = communicator; + _events = events; } public void Enable() { - if( _enabled ) - { + if (_enabled) return; - } - Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; - Penumbra.TempMods.CollectionChanged += CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged += TerritoryClear; + _communicator.CollectionChange.Event += CollectionChangeClear; + DalamudServices.ClientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; _enabled = true; } public void Disable() { - if( !_enabled ) - { + if (!_enabled) return; - } - Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; - Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; + _communicator.CollectionChange.Event -= CollectionChangeClear; + DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; _events.CharacterDestructor -= OnCharacterDestruct; _enabled = false; } - public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data ) + public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) { - if( _dirty ) + if (_dirty) { _dirty = false; _cache.Clear(); } - _cache[ ( IntPtr )data ] = ( identifier, collection ); - return collection.ToResolveData( data ); + _cache[(IntPtr)data] = (identifier, collection); + return collection.ToResolveData(data); } - public bool TryGetValue( GameObject* gameObject, out ResolveData resolve ) + public bool TryGetValue(GameObject* gameObject, out ResolveData resolve) { - if( _dirty ) + if (_dirty) { _dirty = false; _cache.Clear(); } - else if( _cache.TryGetValue( ( IntPtr )gameObject, out var p ) ) + else if (_cache.TryGetValue((IntPtr)gameObject, out var p)) { - resolve = p.Item2.ToResolveData( gameObject ); + resolve = p.Item2.ToResolveData(gameObject); return true; } @@ -81,19 +77,17 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt public void Dispose() { Disable(); - GC.SuppressFinalize( this ); + GC.SuppressFinalize(this); } - public IEnumerator< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > GetEnumerator() + public IEnumerator<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() { - foreach( var (address, (identifier, collection)) in _cache ) + foreach (var (address, (identifier, collection)) in _cache) { - if( _dirty ) - { + if (_dirty) yield break; - } - yield return ( address, identifier, collection ); + yield return (address, identifier, collection); } } @@ -103,17 +97,15 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private void CollectionChangeClear( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + private void CollectionChangeClear(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) { - if( type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive) ) - { + if (type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive)) _dirty = _cache.Count > 0; - } } - private void TerritoryClear( object? _1, ushort _2 ) + private void TerritoryClear(object? _1, ushort _2) => _dirty = _cache.Count > 0; - private void OnCharacterDestruct( Character* character ) - => _cache.Remove( ( IntPtr )character ); -} \ No newline at end of file + private void OnCharacterDestruct(Character* character) + => _cache.Remove((IntPtr)character); +} diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index efb7e61c..7f0ee266 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -12,43 +12,42 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.String.Classes; using Penumbra.Util; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { public class DrawObjectState { + private readonly CommunicatorService _communicator; public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; - public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; + public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; - public IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjects + public IEnumerable> DrawObjects => _drawObjectToObject; public int Count => _drawObjectToObject.Count; - public bool TryGetValue( IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject ) + public bool TryGetValue(IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject) { gameObject = null; - if( !_drawObjectToObject.TryGetValue( drawObject, out value ) ) - { + if (!_drawObjectToObject.TryGetValue(drawObject, out value)) return false; - } var gameObjectIdx = value.Item2; - return VerifyEntry( drawObject, gameObjectIdx, out gameObject ); + return VerifyEntry(drawObject, gameObjectIdx, out gameObject); } // Set and update a parent object if it exists and a last game object is set. - public ResolveData CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject ) + public ResolveData CheckParentDrawObject(IntPtr drawObject, IntPtr parentObject) { - if( parentObject == IntPtr.Zero && LastGameObject != null ) + if (parentObject == IntPtr.Zero && LastGameObject != null) { - var collection = IdentifyCollection( LastGameObject, true ); - _drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex ); + var collection = IdentifyCollection(LastGameObject, true); + _drawObjectToObject[drawObject] = (collection, LastGameObject->ObjectIndex); return collection; } @@ -56,11 +55,11 @@ public unsafe partial class PathResolver } - public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData ) + public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) { - if( type == ResourceType.Tex - && LastCreatedCollection.Valid - && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( "decal"u8 ) ) + if (type == ResourceType.Tex + && LastCreatedCollection.Valid + && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8)) { resolveData = LastCreatedCollection; return true; @@ -76,9 +75,10 @@ public unsafe partial class PathResolver public GameObject* LastGameObject { get; private set; } - public DrawObjectState() + public DrawObjectState(CommunicatorService communicator) { - SignatureHelper.Initialise( this ); + SignatureHelper.Initialise(this); + _communicator = communicator; } public void Enable() @@ -88,8 +88,7 @@ public unsafe partial class PathResolver _enableDrawHook.Enable(); _weaponReloadHook.Enable(); InitializeDrawObjects(); - Penumbra.CollectionManager.CollectionChanged += CheckCollections; - Penumbra.TempMods.CollectionChanged += CheckCollections; + _communicator.CollectionChange.Event += CheckCollections; } public void Disable() @@ -98,8 +97,7 @@ public unsafe partial class PathResolver _characterBaseDestructorHook.Disable(); _enableDrawHook.Disable(); _weaponReloadHook.Disable(); - Penumbra.CollectionManager.CollectionChanged -= CheckCollections; - Penumbra.TempMods.CollectionChanged -= CheckCollections; + _communicator.CollectionChange.Event -= CheckCollections; } public void Dispose() @@ -112,63 +110,61 @@ public unsafe partial class PathResolver } // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. - private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) + private bool VerifyEntry(IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject) { - gameObject = ( GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); - var draw = ( DrawObject* )drawObject; - if( gameObject != null - && ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) ) - { + gameObject = (GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + var draw = (DrawObject*)drawObject; + if (gameObject != null + && (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject)) return true; - } gameObject = null; - _drawObjectToObject.Remove( drawObject ); + _drawObjectToObject.Remove(drawObject); return false; } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - private readonly Dictionary< IntPtr, (ResolveData, int) > _drawObjectToObject = new(); - private ResolveData _lastCreatedCollection = ResolveData.Invalid; + private readonly Dictionary _drawObjectToObject = new(); + private ResolveData _lastCreatedCollection = ResolveData.Invalid; // Keep track of created DrawObjects that are CharacterBase, // and use the last game object that called EnableDraw to link them. - private delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); + private delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d); - [Signature( Sigs.CharacterBaseCreate, DetourName = nameof( CharacterBaseCreateDetour ) )] - private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!; + [Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))] + private readonly Hook _characterBaseCreateHook = null!; - private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) + private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d) { - using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterBaseCreate ); + using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterBaseCreate); var meta = DisposableContainer.Empty; - if( LastGameObject != null ) + if (LastGameObject != null) { - _lastCreatedCollection = IdentifyCollection( LastGameObject, false ); + _lastCreatedCollection = IdentifyCollection(LastGameObject, false); // Change the transparent or 1.0 Decal if necessary. - var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) ); + var decal = new CharacterUtility.DecalReverter(_lastCreatedCollection.ModCollection, UsesDecal(a, c)); // Change the rsp parameters. - meta = new DisposableContainer( _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal ); + meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal); try { var modelPtr = &a; - CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ( IntPtr )modelPtr, b, c ); + CreatingCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, (IntPtr)modelPtr, b, c); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Unknown Error during CreatingCharacterBase:\n{e}" ); + Penumbra.Log.Error($"Unknown Error during CreatingCharacterBase:\n{e}"); } } - var ret = _characterBaseCreateHook.Original( a, b, c, d ); + var ret = _characterBaseCreateHook.Original(a, b, c, d); try { - if( LastGameObject != null && ret != IntPtr.Zero ) + if (LastGameObject != null && ret != IntPtr.Zero) { - _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); - CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret ); + _drawObjectToObject[ret] = (_lastCreatedCollection!, LastGameObject->ObjectIndex); + CreatedCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret); } } finally @@ -181,70 +177,66 @@ public unsafe partial class PathResolver // Check the customize array for the FaceCustomization byte and the last bit of that. // Also check for humans. - public static bool UsesDecal( uint modelId, IntPtr customizeData ) - => modelId == 0 && ( ( byte* )customizeData )[ 12 ] > 0x7F; + public static bool UsesDecal(uint modelId, IntPtr customizeData) + => modelId == 0 && ((byte*)customizeData)[12] > 0x7F; // Remove DrawObjects from the list when they are destroyed. - private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); + private delegate void CharacterBaseDestructorDelegate(IntPtr drawBase); - [Signature( Sigs.CharacterBaseDestructor, DetourName = nameof( CharacterBaseDestructorDetour ) )] - private readonly Hook< CharacterBaseDestructorDelegate > _characterBaseDestructorHook = null!; + [Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))] + private readonly Hook _characterBaseDestructorHook = null!; - private void CharacterBaseDestructorDetour( IntPtr drawBase ) + private void CharacterBaseDestructorDetour(IntPtr drawBase) { - _drawObjectToObject.Remove( drawBase ); - _characterBaseDestructorHook!.Original.Invoke( drawBase ); + _drawObjectToObject.Remove(drawBase); + _characterBaseDestructorHook!.Original.Invoke(drawBase); } // EnableDraw is what creates DrawObjects for gameObjects, // so we always keep track of the current GameObject to be able to link it to the DrawObject. - private delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); + private delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d); - [Signature( Sigs.EnableDraw, DetourName = nameof( EnableDrawDetour ) )] - private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!; + [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))] + private readonly Hook _enableDrawHook = null!; - private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) + private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d) { var oldObject = LastGameObject; - LastGameObject = ( GameObject* )gameObject; - _enableDrawHook!.Original.Invoke( gameObject, b, c, d ); + LastGameObject = (GameObject*)gameObject; + _enableDrawHook!.Original.Invoke(gameObject, b, c, d); LastGameObject = oldObject; } // Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8, // so we use that. - private delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ); + private delegate void WeaponReloadFunc(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7); - [Signature( Sigs.WeaponReload, DetourName = nameof( WeaponReloadDetour ) )] - private readonly Hook< WeaponReloadFunc > _weaponReloadHook = null!; + [Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))] + private readonly Hook _weaponReloadHook = null!; - public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ) + public void WeaponReloadDetour(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7) { var oldGame = LastGameObject; - LastGameObject = *( GameObject** )( a1 + 8 ); - _weaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 ); + LastGameObject = *(GameObject**)(a1 + 8); + _weaponReloadHook!.Original(a1, a2, a3, a4, a5, a6, a7); LastGameObject = oldGame; } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + private void CheckCollections(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) { - if( type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface ) - { + if (type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface) return; - } - foreach( var (key, (_, idx)) in _drawObjectToObject.ToArray() ) + foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray()) { - if( !VerifyEntry( key, idx, out var obj ) ) - { - _drawObjectToObject.Remove( key ); - } + if (!VerifyEntry(key, idx, out var obj)) + _drawObjectToObject.Remove(key); - var newCollection = IdentifyCollection( obj, false ); - _drawObjectToObject[ key ] = ( newCollection, idx ); + var newCollection = IdentifyCollection(obj, false); + _drawObjectToObject[key] = (newCollection, idx); } } @@ -252,14 +244,12 @@ public unsafe partial class PathResolver // We do not iterate the Dalamud table because it does not work when not logged in. private void InitializeDrawObjects() { - for( var i = 0; i < DalamudServices.Objects.Length; ++i ) + for (var i = 0; i < DalamudServices.Objects.Length; ++i) { - var ptr = ( GameObject* )DalamudServices.Objects.GetObjectAddress( i ); - if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) - { - _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr, false ), ptr->ObjectIndex ); - } + var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i); + if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) + _drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex); } } } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index cacd3b20..516d2a2b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -103,7 +103,7 @@ public unsafe partial class PathResolver // Check both temporary and permanent character collections. Temporary first. private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) - => Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection ) + => Penumbra.TempCollections.Collections.TryGetCollection( identifier, out var collection ) || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) ? collection : null; diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index e3fb47e0..1568b363 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -265,7 +265,7 @@ public partial class PathResolver } var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; - var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject ); + var parentCollection = _drawObjects.CheckParentDrawObject( drawObject, parentObject ); if( parentCollection.Valid ) { return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path ); diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index b64323a2..140804ce 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -5,10 +5,11 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -24,70 +25,70 @@ public partial class PathResolver : IDisposable { public bool Enabled { get; private set; } - private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(Penumbra.GameEvents); - private static readonly DrawObjectState DrawObjects = new(); - private static readonly BitArray ValidHumanModels; - internal static readonly IdentifiedCollectionCache IdentifiedCache = new(Penumbra.GameEvents); - private readonly AnimationState _animations; - private readonly PathState _paths; - private readonly MetaState _meta; - private readonly SubfileHelper _subFiles; + private readonly CommunicatorService _communicator; + private readonly ResourceLoader _loader; + private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.Objects, Penumbra.GameEvents); // TODO + private static DrawObjectState _drawObjects = null!; // TODO + private static readonly BitArray ValidHumanModels; + internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO + private readonly AnimationState _animations; + private readonly PathState _paths; + private readonly MetaState _meta; + private readonly SubfileHelper _subFiles; static PathResolver() - => ValidHumanModels = GetValidHumanModels( DalamudServices.GameData ); + => ValidHumanModels = GetValidHumanModels(DalamudServices.GameData); - public unsafe PathResolver( ResourceLoader loader ) + public unsafe PathResolver(StartTracker timer, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) { - using var tApi = Penumbra.StartTimer.Measure( StartTimeType.PathResolver ); - SignatureHelper.Initialise( this ); + using var tApi = timer.Measure(StartTimeType.PathResolver); + _communicator = communicator; + IdentifiedCache = new IdentifiedCollectionCache(communicator, events); + SignatureHelper.Initialise(this); + _drawObjects = new DrawObjectState(_communicator); _loader = loader; - _animations = new AnimationState( DrawObjects ); - _paths = new PathState( this ); - _meta = new MetaState( _paths.HumanVTable ); - _subFiles = new SubfileHelper( _loader, Penumbra.GameEvents ); + _animations = new AnimationState(_drawObjects); + _paths = new PathState(this); + _meta = new MetaState(_paths.HumanVTable); + _subFiles = new SubfileHelper(_loader, Penumbra.GameEvents); } // The modified resolver that handles game path resolving. - private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data ) + private bool CharacterResolver(Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data) { - using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterResolver ); + using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver); // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load, // or if it is a face decal path and the current mod collection is set. // If not use the default collection. // We can remove paths after they have actually been loaded. // A potential next request will add the path anew. - var nonDefault = _subFiles.HandleSubFiles( type, out var resolveData ) - || _paths.Consume( gamePath.Path, out resolveData ) - || _animations.HandleFiles( type, gamePath, out resolveData ) - || DrawObjects.HandleDecalFile( type, gamePath, out resolveData ); - if( !nonDefault || !resolveData.Valid ) - { + var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData) + || _paths.Consume(gamePath.Path, out resolveData) + || _animations.HandleFiles(type, gamePath, out resolveData) + || _drawObjects.HandleDecalFile(type, gamePath, out resolveData); + if (!nonDefault || !resolveData.Valid) resolveData = Penumbra.CollectionManager.Default.ToResolveData(); - } // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = resolveData.ModCollection.ResolvePath( gamePath ); + var resolved = resolveData.ModCollection.ResolvePath(gamePath); // Since mtrl files load their files separately, we need to add the new, resolved path // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); + SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out data); return true; } public void Enable() { - if( Enabled ) - { + if (Enabled) return; - } Enabled = true; Cutscenes.Enable(); - DrawObjects.Enable(); + _drawObjects.Enable(); IdentifiedCache.Enable(); _animations.Enable(); _paths.Enable(); @@ -95,19 +96,17 @@ public partial class PathResolver : IDisposable _subFiles.Enable(); _loader.ResolvePathCustomization += CharacterResolver; - Penumbra.Log.Debug( "Character Path Resolver enabled." ); + Penumbra.Log.Debug("Character Path Resolver enabled."); } public void Disable() { - if( !Enabled ) - { + if (!Enabled) return; - } Enabled = false; _animations.Disable(); - DrawObjects.Disable(); + _drawObjects.Disable(); Cutscenes.Disable(); IdentifiedCache.Disable(); _paths.Disable(); @@ -115,7 +114,7 @@ public partial class PathResolver : IDisposable _subFiles.Disable(); _loader.ResolvePathCustomization -= CharacterResolver; - Penumbra.Log.Debug( "Character Path Resolver disabled." ); + Penumbra.Log.Debug("Character Path Resolver disabled."); } public void Dispose() @@ -123,58 +122,58 @@ public partial class PathResolver : IDisposable Disable(); _paths.Dispose(); _animations.Dispose(); - DrawObjects.Dispose(); + _drawObjects.Dispose(); Cutscenes.Dispose(); IdentifiedCache.Dispose(); _meta.Dispose(); _subFiles.Dispose(); } - public static unsafe (IntPtr, ResolveData) IdentifyDrawObject( IntPtr drawObject ) + public static unsafe (IntPtr, ResolveData) IdentifyDrawObject(IntPtr drawObject) { - var parent = FindParent( drawObject, out var resolveData ); - return ( ( IntPtr )parent, resolveData ); + var parent = FindParent(drawObject, out var resolveData); + return ((IntPtr)parent, resolveData); } - public int CutsceneActor( int idx ) - => Cutscenes.GetParentIndex( idx ); + public int CutsceneActor(int idx) + => Cutscenes.GetParentIndex(idx); // Use the stored information to find the GameObject and Collection linked to a DrawObject. - public static unsafe GameObject* FindParent( IntPtr drawObject, out ResolveData resolveData ) + public static unsafe GameObject* FindParent(IntPtr drawObject, out ResolveData resolveData) { - if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) ) + if (_drawObjects.TryGetValue(drawObject, out var data, out var gameObject)) { resolveData = data.Item1; return gameObject; } - if( DrawObjects.LastGameObject != null - && ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) + if (_drawObjects.LastGameObject != null + && (_drawObjects.LastGameObject->DrawObject == null || _drawObjects.LastGameObject->DrawObject == (DrawObject*)drawObject)) { - resolveData = IdentifyCollection( DrawObjects.LastGameObject, true ); - return DrawObjects.LastGameObject; + resolveData = IdentifyCollection(_drawObjects.LastGameObject, true); + return _drawObjects.LastGameObject; } - resolveData = IdentifyCollection( null, true ); + resolveData = IdentifyCollection(null, true); return null; } - private static unsafe ResolveData GetResolveData( IntPtr drawObject ) + private static unsafe ResolveData GetResolveData(IntPtr drawObject) { - var _ = FindParent( drawObject, out var resolveData ); + var _ = FindParent(drawObject, out var resolveData); return resolveData; } - internal IEnumerable< KeyValuePair< ByteString, ResolveData > > PathCollections + internal IEnumerable> PathCollections => _paths.Paths; - internal IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjectMap - => DrawObjects.DrawObjects; + internal IEnumerable> DrawObjectMap + => _drawObjects.DrawObjects; - internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors + internal IEnumerable> CutsceneActors => Cutscenes.Actors; - internal IEnumerable< KeyValuePair< IntPtr, ResolveData > > ResourceCollections + internal IEnumerable> ResourceCollections => _subFiles; internal int SubfileCount @@ -187,8 +186,8 @@ public partial class PathResolver : IDisposable => _subFiles.AvfxData; internal ResolveData LastGameObjectData - => DrawObjects.LastCreatedCollection; + => _drawObjects.LastCreatedCollection; internal unsafe nint LastGameObject - => (nint) DrawObjects.LastGameObject; -} \ No newline at end of file + => (nint)_drawObjects.LastGameObject; +} diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 661a692c..3ee984b6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -169,7 +169,7 @@ public partial class MetaManager var lastUnderscore = split.LastIndexOf( ( byte )'_' ); var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( ( Penumbra.TempMods.CollectionByName( name, out var collection ) + if( ( Penumbra.TempCollections.CollectionByName( name, out var collection ) || Penumbra.CollectionManager.ByName( name, out collection ) ) && collection.HasCache && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs index ad321b5e..57699fcb 100644 --- a/Penumbra/Mods/Mod.LocalData.cs +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Plugin; using Newtonsoft.Json; using Penumbra.Services; @@ -10,8 +11,8 @@ namespace Penumbra.Mods; public sealed partial class Mod { - public static DirectoryInfo LocalDataDirectory - => new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data" )); + public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi) + => new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" )); public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 977db6f3..fa9fec1d 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Dalamud.Plugin; using OtterGui.Filesystem; using Penumbra.Services; @@ -11,15 +12,16 @@ namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { - public static string ModFileSystemFile - => Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); + public static string ModFileSystemFile(DalamudPluginInterface pi) + => Path.Combine( pi.GetPluginConfigDirectory(), "sort_order.json" ); // Save the current sort order. // Does not save or copy the backup in the current mod directory, - // as this is done on mod directory changes only. + // as this is done on mod directory changes only. + // TODO private void SaveFilesystem() { - SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); + SaveToFile( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), SaveMod, true ); Penumbra.Log.Verbose( "Saved mod filesystem." ); } @@ -74,8 +76,9 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable // Reload the whole filesystem from currently loaded mods and the current sort order file. // Used on construction and on mod rediscoveries. private void Reload() - { - if( Load( new FileInfo( ModFileSystemFile ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) + { + // TODO + if( Load( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) { Save(); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d14f94e3..0a85e113 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -29,45 +28,10 @@ using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; +using Penumbra.Services; namespace Penumbra; -public class PenumbraNew -{ - public string Name - => "Penumbra"; - - public static readonly Logger Log = new(); - public readonly StartTimeTracker< StartTimeType > StartTimer = new(); - - public readonly IServiceCollection Services = new ServiceCollection(); - - - public PenumbraNew( DalamudPluginInterface pi ) - { - using var time = StartTimer.Measure( StartTimeType.Total ); - - // Add meta services. - Services.AddSingleton( Log ); - Services.AddSingleton( StartTimer ); - Services.AddSingleton< ValidityChecker >(); - Services.AddSingleton< PerformanceTracker< PerformanceType > >(); - - // Add Dalamud services - var dalamud = new DalamudServices( pi ); - dalamud.AddServices( Services ); - - // Add Game Data - - - // Add Configuration - Services.AddSingleton< Configuration >(); - } - - public void Dispose() - { } -} - public class Penumbra : IDalamudPlugin { public string Name @@ -76,135 +40,123 @@ public class Penumbra : IDalamudPlugin public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; public static readonly string CommitHash = - Assembly.GetExecutingAssembly().GetCustomAttribute< AssemblyInformationalVersionAttribute >()?.InformationalVersion ?? "Unknown"; + Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - public static Logger Log { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; - public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static GameEventManager GameEvents { get; private set; } = null!; - public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static Mod.Manager ModManager { get; private set; } = null!; - public static ModCollection.Manager CollectionManager { get; private set; } = null!; - public static TempModManager TempMods { get; private set; } = null!; - public static ResourceLoader ResourceLoader { get; private set; } = null!; - public static FrameworkManager Framework { get; private set; } = null!; - public static ActorManager Actors { get; private set; } = null!; - public static IObjectIdentifier Identifier { get; private set; } = null!; - public static IGamePathParser GamePathParser { get; private set; } = null!; - public static StainManager StainManager { get; private set; } = null!; + public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static GameEventManager GameEvents { get; private set; } = null!; + public static MetaFileManager MetaFileManager { get; private set; } = null!; + public static Mod.Manager ModManager { get; private set; } = null!; + public static ModCollection.Manager CollectionManager { get; private set; } = null!; + public static TempCollectionManager TempCollections { get; private set; } = null!; + public static TempModManager TempMods { get; private set; } = null!; + public static ResourceLoader ResourceLoader { get; private set; } = null!; + public static FrameworkManager Framework { get; private set; } = null!; + public static ActorManager Actors { get; private set; } = null!; + public static IObjectIdentifier Identifier { get; private set; } = null!; + public static IGamePathParser GamePathParser { get; private set; } = null!; + public static StainService StainService { get; private set; } = null!; + + // TODO + public static DalamudServices Dalamud { get; private set; } = null!; public static ValidityChecker ValidityChecker { get; private set; } = null!; - public static PerformanceTracker< PerformanceType > Performance { get; private set; } = null!; + public static PerformanceTracker Performance { get; private set; } = null!; - public static readonly StartTimeTracker< StartTimeType > StartTimer = new(); + public readonly PathResolver PathResolver; + public readonly ObjectReloader ObjectReloader; + public readonly ModFileSystem ModFileSystem; + public readonly PenumbraApi Api; + public readonly HttpApi HttpApi; + public readonly PenumbraIpcProviders IpcProviders; + internal ConfigWindow? ConfigWindow { get; private set; } + private LaunchButton? _launchButton; + private WindowSystem? _windowSystem; + private Changelog? _changelog; + private CommandHandler? _commandHandler; + private readonly ResourceWatcher _resourceWatcher; + private bool _disposed; - public readonly ResourceLogger ResourceLogger; - public readonly PathResolver PathResolver; - public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystem ModFileSystem; - public readonly PenumbraApi Api; - public readonly HttpApi HttpApi; - public readonly PenumbraIpcProviders IpcProviders; - internal ConfigWindow? ConfigWindow { get; private set; } - private LaunchButton? _launchButton; - private WindowSystem? _windowSystem; - private Changelog? _changelog; - private CommandHandler? _commandHandler; - private readonly ResourceWatcher _resourceWatcher; - private bool _disposed; + private readonly PenumbraNew _tmp; + public static ItemData ItemData { get; private set; } = null!; - public static ItemData ItemData { get; private set; } = null!; - - public Penumbra( DalamudPluginInterface pluginInterface ) + public Penumbra(DalamudPluginInterface pluginInterface) { - using var time = StartTimer.Measure( StartTimeType.Total ); - + Log = PenumbraNew.Log; + _tmp = new PenumbraNew(pluginInterface); + Performance = _tmp.Services.GetRequiredService(); + ValidityChecker = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + GameEvents = _tmp.Services.GetRequiredService(); + MetaFileManager = _tmp.Services.GetRequiredService(); + Framework = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; + Identifier = _tmp.Services.GetRequiredService().AwaitedService; + GamePathParser = _tmp.Services.GetRequiredService(); + StainService = _tmp.Services.GetRequiredService(); + ItemData = _tmp.Services.GetRequiredService().AwaitedService; + Dalamud = _tmp.Services.GetRequiredService(); + TempMods = _tmp.Services.GetRequiredService(); try { - DalamudServices.Initialize( pluginInterface ); - - Performance = new PerformanceTracker< PerformanceType >( DalamudServices.Framework ); - Log = new Logger(); - ValidityChecker = new ValidityChecker( DalamudServices.PluginInterface ); - - GameEvents = new GameEventManager(); - StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( DalamudServices.PluginInterface, DalamudServices.GameData ) ); - StartTimer.Measure( StartTimeType.GamePathParser, () => GamePathParser = GameData.GameData.GetGamePathParser() ); - StartTimer.Measure( StartTimeType.Stains, () => StainManager = new StainManager( DalamudServices.PluginInterface, DalamudServices.GameData ) ); - ItemData = StartTimer.Measure( StartTimeType.Items, - () => new ItemData( DalamudServices.PluginInterface, DalamudServices.GameData, DalamudServices.GameData.Language ) ); - StartTimer.Measure( StartTimeType.Actors, - () => Actors = new ActorManager( DalamudServices.PluginInterface, DalamudServices.Objects, DalamudServices.ClientState, DalamudServices.Framework, - DalamudServices.GameData, DalamudServices.GameGui, - ResolveCutscene ) ); - - Framework = new FrameworkManager( DalamudServices.Framework, Log ); - CharacterUtility = new CharacterUtility(); - - StartTimer.Measure( StartTimeType.Backup, () => Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ) ); - Config = Configuration.Load(); - - TempMods = new TempModManager(); - MetaFileManager = new MetaFileManager(); - ResourceLoader = new ResourceLoader( this ); + ResourceLoader = new ResourceLoader(this); ResourceLoader.EnableHooks(); - _resourceWatcher = new ResourceWatcher( ResourceLoader ); - ResourceLogger = new ResourceLogger( ResourceLoader ); + _resourceWatcher = new ResourceWatcher(ResourceLoader); ResidentResources = new ResidentResourceManager(); - StartTimer.Measure( StartTimeType.Mods, () => + _tmp.Services.GetRequiredService().Measure(StartTimeType.Mods, () => { - ModManager = new Mod.Manager( Config.ModDirectory ); + ModManager = new Mod.Manager(Config.ModDirectory); ModManager.DiscoverMods(); - } ); + }); - StartTimer.Measure( StartTimeType.Collections, () => + _tmp.Services.GetRequiredService().Measure(StartTimeType.Collections, () => { - CollectionManager = new ModCollection.Manager( ModManager ); + CollectionManager = new ModCollection.Manager(_tmp.Services.GetRequiredService(), ModManager); CollectionManager.CreateNecessaryCaches(); - } ); + }); + + + TempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); - PathResolver = new PathResolver( ResourceLoader ); + PathResolver = new PathResolver(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), ResourceLoader); SetupInterface(); - if( Config.EnableMods ) + if (Config.EnableMods) { ResourceLoader.EnableReplacements(); PathResolver.Enable(); } - if( Config.DebugMode ) - { + if (Config.DebugMode) ResourceLoader.EnableDebug(); - } - using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) + using (var tApi = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api)) { - Api = new PenumbraApi( this ); - IpcProviders = new PenumbraIpcProviders( DalamudServices.PluginInterface, Api ); - HttpApi = new HttpApi( Api ); - if( Config.EnableHttpApi ) - { + Api = new PenumbraApi(_tmp.Services.GetRequiredService(), this); + IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api); + HttpApi = new HttpApi(Api); + if (Config.EnableHttpApi) HttpApi.CreateWebServer(); - } SubscribeItemLinks(); } ValidityChecker.LogExceptions(); - Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); - OtterTex.NativeDll.Initialize( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName ); - Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); + Log.Information($"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); + OtterTex.NativeDll.Initialize(DalamudServices.PluginInterface.AssemblyLocation.DirectoryName); + Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); - if( CharacterUtility.Ready ) - { + if (CharacterUtility.Ready) ResidentResources.Reload(); - } } catch { @@ -215,21 +167,22 @@ public class Penumbra : IDalamudPlugin private void SetupInterface() { - Task.Run( () => + Task.Run(() => { - using var tInterface = StartTimer.Measure( StartTimeType.Interface ); + using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); var changelog = ConfigWindow.CreateChangelog(); - var cfg = new ConfigWindow( this, _resourceWatcher ) + var cfg = new ConfigWindow(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), this, _resourceWatcher) { IsOpen = Config.DebugMode, }; - var btn = new LaunchButton( cfg ); - var system = new WindowSystem( Name ); - var cmd = new CommandHandler( DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); - system.AddWindow( cfg ); - system.AddWindow( cfg.ModEditPopup ); - system.AddWindow( changelog ); - if( !_disposed ) + var btn = new LaunchButton(cfg); + var system = new WindowSystem(Name); + var cmd = new CommandHandler(DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, + Actors); + system.AddWindow(cfg); + system.AddWindow(cfg.ModEditPopup); + system.AddWindow(changelog); + if (!_disposed) { _changelog = changelog; ConfigWindow = cfg; @@ -251,100 +204,87 @@ public class Penumbra : IDalamudPlugin private void DisposeInterface() { - if( _windowSystem != null ) - { + if (_windowSystem != null) DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; - } _launchButton?.Dispose(); - if( ConfigWindow != null ) + if (ConfigWindow != null) { DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; ConfigWindow.Dispose(); } } - public event Action< bool >? EnabledChange; + public event Action? EnabledChange; - public bool SetEnabled( bool enabled ) + public bool SetEnabled(bool enabled) { - if( enabled == Config.EnableMods ) - { + if (enabled == Config.EnableMods) return false; - } Config.EnableMods = enabled; - if( enabled ) + if (enabled) { ResourceLoader.EnableReplacements(); PathResolver.Enable(); - if( CharacterUtility.Ready ) + if (CharacterUtility.Ready) { CollectionManager.Default.SetFiles(); ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); + ObjectReloader.RedrawAll(RedrawType.Redraw); } } else { ResourceLoader.DisableReplacements(); PathResolver.Disable(); - if( CharacterUtility.Ready ) + if (CharacterUtility.Ready) { CharacterUtility.ResetAll(); ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); + ObjectReloader.RedrawAll(RedrawType.Redraw); } } Config.Save(); - EnabledChange?.Invoke( enabled ); + EnabledChange?.Invoke(enabled); return true; } public void ForceChangelogOpen() { - if( _changelog != null ) - { + if (_changelog != null) _changelog.ForceOpen = true; - } } private void SubscribeItemLinks() { Api.ChangedItemTooltip += it => { - if( it is Item ) - { - ImGui.TextUnformatted( "Left Click to create an item link in chat." ); - } + if (it is Item) + ImGui.TextUnformatted("Left Click to create an item link in chat."); }; - Api.ChangedItemClicked += ( button, it ) => + Api.ChangedItemClicked += (button, it) => { - if( button == MouseButton.Left && it is Item item ) - { - ChatUtil.LinkItem( item ); - } + if (button == MouseButton.Left && it is Item item) + ChatUtil.LinkItem(item); }; - } - - private short ResolveCutscene( ushort index ) - => ( short )PathResolver.CutsceneActor( index ); - + } + public void Dispose() { - if( _disposed ) - { + if (_disposed) return; - } - + + // TODO + _tmp?.Dispose(); _disposed = true; HttpApi?.Dispose(); IpcProviders?.Dispose(); Api?.Dispose(); _commandHandler?.Dispose(); - StainManager?.Dispose(); + StainService?.Dispose(); ItemData?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); @@ -354,99 +294,85 @@ public class Penumbra : IDalamudPlugin ModFileSystem?.Dispose(); CollectionManager?.Dispose(); PathResolver?.Dispose(); - ResourceLogger?.Dispose(); _resourceWatcher?.Dispose(); ResourceLoader?.Dispose(); GameEvents?.Dispose(); CharacterUtility?.Dispose(); - Performance?.Dispose(); + Performance?.Dispose(); } - // Collect all relevant files for penumbra configuration. - private static IReadOnlyList< FileInfo > PenumbraBackupFiles() + public string GatherSupportInformation() { - var collectionDir = ModCollection.CollectionDirectory; - var list = Directory.Exists( collectionDir ) - ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() - : new List< FileInfo >(); - list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() ); - list.Add( DalamudServices.PluginInterface.ConfigFile ); - list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); - list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); - return list; - } - - public static string GatherSupportInformation() - { - var sb = new StringBuilder( 10240 ); - var exists = Config.ModDirectory.Length > 0 && Directory.Exists( Config.ModDirectory ); - var drive = exists ? new DriveInfo( new DirectoryInfo( Config.ModDirectory ).Root.FullName ) : null; - sb.AppendLine( "**Settings**" ); - sb.Append( $"> **`Plugin Version: `** {Version}\n" ); - sb.Append( $"> **`Commit Hash: `** {CommitHash}\n" ); - sb.Append( $"> **`Enable Mods: `** {Config.EnableMods}\n" ); - sb.Append( $"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n" ); - sb.Append( $"> **`Operating System: `** {( DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows" )}\n" ); - sb.Append( $"> **`Root Directory: `** `{Config.ModDirectory}`, {( exists ? "Exists" : "Not Existing" )}\n" ); - sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); - sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); - sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" ); + var sb = new StringBuilder(10240); + var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory); + var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null; + sb.AppendLine("**Settings**"); + sb.Append($"> **`Plugin Version: `** {Version}\n"); + sb.Append($"> **`Commit Hash: `** {CommitHash}\n"); + sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n"); + sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n"); + sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); + sb.Append($"> **`Root Directory: `** `{Config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( - $"> **`Synchronous Load (Dalamud): `** {( DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); - sb.Append( $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n" ); - sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); - sb.AppendLine( "**Mods**" ); - sb.Append( $"> **`Installed Mods: `** {ModManager.Count}\n" ); - sb.Append( $"> **`Mods with Config: `** {ModManager.Count( m => m.HasOptions )}\n" ); - sb.Append( $"> **`Mods with File Redirections: `** {ModManager.Count( m => m.TotalFileCount > 0 )}, Total: {ModManager.Sum( m => m.TotalFileCount )}\n" ); - sb.Append( $"> **`Mods with FileSwaps: `** {ModManager.Count( m => m.TotalSwapCount > 0 )}, Total: {ModManager.Sum( m => m.TotalSwapCount )}\n" ); - sb.Append( $"> **`Mods with Meta Manipulations:`** {ModManager.Count( m => m.TotalManipulations > 0 )}, Total {ModManager.Sum( m => m.TotalManipulations )}\n" ); - sb.Append( $"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n" ); - sb.Append( $"> **`#Temp Mods: `** {TempMods.Mods.Sum( kvp => kvp.Value.Count ) + TempMods.ModsForAllCollections.Count}\n" ); + $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); + sb.Append($"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n"); + sb.Append($"> **`Debug Mode: `** {Config.DebugMode}\n"); + sb.Append( + $"> **`Synchronous Load (Dalamud): `** {(_tmp.Services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); + sb.Append( + $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n"); + sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n"); + sb.AppendLine("**Mods**"); + sb.Append($"> **`Installed Mods: `** {ModManager.Count}\n"); + sb.Append($"> **`Mods with Config: `** {ModManager.Count(m => m.HasOptions)}\n"); + sb.Append( + $"> **`Mods with File Redirections: `** {ModManager.Count(m => m.TotalFileCount > 0)}, Total: {ModManager.Sum(m => m.TotalFileCount)}\n"); + sb.Append( + $"> **`Mods with FileSwaps: `** {ModManager.Count(m => m.TotalSwapCount > 0)}, Total: {ModManager.Sum(m => m.TotalSwapCount)}\n"); + sb.Append( + $"> **`Mods with Meta Manipulations:`** {ModManager.Count(m => m.TotalManipulations > 0)}, Total {ModManager.Sum(m => m.TotalManipulations)}\n"); + sb.Append($"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n"); + sb.Append( + $"> **`#Temp Mods: `** {TempMods.Mods.Sum(kvp => kvp.Value.Count) + TempMods.ModsForAllCollections.Count}\n"); - string CharacterName( ActorIdentifier id, string name ) + string CharacterName(ActorIdentifier id, string name) { - if( id.Type is IdentifierType.Player or IdentifierType.Owned ) + if (id.Type is IdentifierType.Player or IdentifierType.Owned) { - var parts = name.Split( ' ', 3 ); - return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) ); + var parts = name.Split(' ', 3); + return string.Join(" ", + parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); } return name + ':'; } - void PrintCollection( ModCollection c ) - => sb.Append( $"**Collection {c.AnonymizedName}**\n" + void PrintCollection(ModCollection c) + => sb.Append($"**Collection {c.AnonymizedName}**\n" + $"> **`Inheritances: `** {c.Inheritance.Count}\n" - + $"> **`Enabled Mods: `** {c.ActualSettings.Count( s => s is { Enabled: true } )}\n" - + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count )}/{c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count )}\n" ); + + $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n" + + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? 0 : x.Conflicts.Count)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count)}\n"); - sb.AppendLine( "**Collections**" ); - sb.Append( $"> **`#Collections: `** {CollectionManager.Count - 1}\n" ); - sb.Append( $"> **`#Temp Collections: `** {TempMods.CustomCollections.Count}\n" ); - sb.Append( $"> **`Active Collections: `** {CollectionManager.Count( c => c.HasCache )}\n" ); - sb.Append( $"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n" ); - sb.Append( $"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n" ); - sb.Append( $"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n" ); - foreach( var (type, name, _) in CollectionTypeExtensions.Special ) + sb.AppendLine("**Collections**"); + sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n"); + sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n"); + sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n"); + sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n"); + foreach (var (type, name, _) in CollectionTypeExtensions.Special) { - var collection = CollectionManager.ByType( type ); - if( collection != null ) - { - sb.Append( $"> **`{name,-30}`** {collection.AnonymizedName}\n" ); - } + var collection = CollectionManager.ByType(type); + if (collection != null) + sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); } - foreach( var (name, id, collection) in CollectionManager.Individuals.Assignments ) - { - sb.Append( $"> **`{CharacterName( id[ 0 ], name ),-30}`** {collection.AnonymizedName}\n" ); - } + foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments) + sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n"); - foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) - { - PrintCollection( collection ); - } + foreach (var collection in CollectionManager.Where(c => c.HasCache)) + PrintCollection(collection); return sb.ToString(); } -} \ No newline at end of file +} diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index b08055d7..18da6c62 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -1,11 +1,14 @@ -using System.IO; +using System; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; +using Penumbra.Api; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop; +using Penumbra.Interop.Resolver; using Penumbra.Services; using Penumbra.Util; @@ -16,35 +19,61 @@ public class PenumbraNew public string Name => "Penumbra"; - public static readonly Logger Log = new(); - public readonly StartTimeTracker StartTimer = new(); - - public readonly IServiceCollection Services = new ServiceCollection(); - + public static readonly Logger Log = new(); + public readonly ServiceProvider Services; public PenumbraNew(DalamudPluginInterface pi) { - using var time = StartTimer.Measure(StartTimeType.Total); + var startTimer = new StartTracker(); + using var time = startTimer.Measure(StartTimeType.Total); + var services = new ServiceCollection(); // Add meta services. - Services.AddSingleton(Log); - Services.AddSingleton(StartTimer); - Services.AddSingleton(); - Services.AddSingleton>(); + services.AddSingleton(Log) + .AddSingleton(startTimer) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Dalamud services var dalamud = new DalamudServices(pi); - dalamud.AddServices(Services); + dalamud.AddServices(services); // Add Game Data - Services.AddSingleton(); - Services.AddSingleton(); - Services.AddSingleton(); + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Add Game Services + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + // Add Configuration - Services.AddSingleton(); + services.AddTransient() + .AddSingleton(); + + // Add Collection Services + services.AddTransient() + .AddSingleton(); + + // Add Mod Services + // TODO + services.AddSingleton(); + + // Add Interface + Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); } public void Dispose() - { } -} \ No newline at end of file + { + Services.Dispose(); + } +} diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs new file mode 100644 index 00000000..23fb20cc --- /dev/null +++ b/Penumbra/Services/BackupService.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OtterGui.Classes; +using OtterGui.Log; +using Penumbra.Util; + +namespace Penumbra.Services; + +public class BackupService +{ + public BackupService(Logger logger, StartTracker timer, FilenameService fileNames) + { + using var t = timer.Measure(StartTimeType.Backup); + var files = PenumbraFiles(fileNames); + Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); + } + + // Collect all relevant files for penumbra configuration. + private static IReadOnlyList PenumbraFiles(FilenameService fileNames) + { + var list = fileNames.CollectionFiles.ToList(); + list.AddRange(fileNames.LocalDataFiles); + list.Add(new FileInfo(fileNames.ConfigFile)); + list.Add(new FileInfo(fileNames.FilesystemFile)); + list.Add(new FileInfo(fileNames.ActiveCollectionsFile)); + return list; + } +} diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs new file mode 100644 index 00000000..47d215c6 --- /dev/null +++ b/Penumbra/Services/CommunicatorService.cs @@ -0,0 +1,30 @@ +using System; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Services; + +public class CommunicatorService : IDisposable +{ + /// + /// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) + /// Parameter is the old collection, or null on additions. + /// Parameter is the new collection, or null on deletions. + /// Parameter is the display name for Individual collections or an empty string otherwise. + /// + public readonly EventWrapper CollectionChange = new(nameof(CollectionChange)); + + /// + /// Parameter added, deleted or edited temporary mod. + /// Parameter is whether the mod was newly created. + /// Parameter is whether the mod was deleted. + /// + public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); + + public void Dispose() + { + CollectionChange.Dispose(); + TemporaryGlobalModChange.Dispose(); + } +} diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs new file mode 100644 index 00000000..4d37f693 --- /dev/null +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using SixLabors.ImageSharp; + +namespace Penumbra.Services; + +/// +/// Contains everything to migrate from older versions of the config to the current, +/// including deprecated fields. +/// +public class ConfigMigrationService +{ + private readonly FilenameService _fileNames; + private readonly DalamudPluginInterface _pluginInterface; + + private Configuration _config = null!; + private JObject _data = null!; + + public string CurrentCollection = ModCollection.DefaultCollection; + public string DefaultCollection = ModCollection.DefaultCollection; + public string ForcedCollection = string.Empty; + public Dictionary CharacterCollections = new(); + public Dictionary ModSortOrder = new(); + public bool InvertModListOrder; + public bool SortFoldersFirst; + public SortModeV3 SortMode = SortModeV3.FoldersFirst; + + public ConfigMigrationService(FilenameService fileNames, DalamudPluginInterface pi) + { + _fileNames = fileNames; + _pluginInterface = pi; + } + + /// Add missing colors to the dictionary if necessary. + private static void AddColors(Configuration config, bool forceSave) + { + var save = false; + foreach (var color in Enum.GetValues()) + { + save |= config.Colors.TryAdd(color, color.Data().DefaultColor); + } + + if (save || forceSave) + { + config.Save(); + } + } + + public void Migrate(Configuration config) + { + _config = config; + // Do this on every migration from now on for a while + // because it stayed alive for a bunch of people for some reason. + DeleteMetaTmp(); + + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_fileNames.ConfigFile)) + { + AddColors(config, false); + return; + } + + _data = JObject.Parse(File.ReadAllText(_fileNames.ConfigFile)); + CreateBackup(); + + Version0To1(); + Version1To2(); + Version2To3(); + Version3To4(); + Version4To5(); + Version5To6(); + Version6To7(); + AddColors(config, true); + } + + // Gendered special collections were added. + private void Version6To7() + { + if (_config.Version != 6) + return; + + ModCollection.Manager.MigrateUngenderedCollections(_fileNames); + _config.Version = 7; + } + + + // A new tutorial step was inserted in the middle. + // The UI collection and a new tutorial for it was added. + // The migration for the UI collection itself happens in the ActiveCollections file. + private void Version5To6() + { + if (_config.Version != 5) + return; + + if (_config.TutorialStep == 25) + _config.TutorialStep = 27; + + _config.Version = 6; + } + + // Mod backup extension was changed from .zip to .pmp. + // Actual migration takes place in ModManager. + private void Version4To5() + { + if (_config.Version != 4) + return; + + Mod.Manager.MigrateModBackups = true; + _config.Version = 5; + } + + // SortMode was changed from an enum to a type. + private void Version3To4() + { + if (_config.Version != 3) + return; + + SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode; + _config.SortMode = SortMode switch + { + SortModeV3.FoldersFirst => ISortMode.FoldersFirst, + SortModeV3.Lexicographical => ISortMode.Lexicographical, + SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst, + SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical, + SortModeV3.FoldersLast => ISortMode.FoldersLast, + SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast, + SortModeV3.InternalOrder => ISortMode.InternalOrder, + SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder, + _ => ISortMode.FoldersFirst, + }; + _config.Version = 4; + } + + // SortFoldersFirst was changed from a bool to the enum SortMode. + private void Version2To3() + { + if (_config.Version != 2) + return; + + SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false; + SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; + _config.Version = 3; + } + + // The forced collection was removed due to general inheritance. + // Sort Order was moved to a separate file and may contain empty folders. + // Active collections in general were moved to their own file. + // Delete the penumbrametatmp folder if it exists. + private void Version1To2() + { + if (_config.Version != 1) + return; + + // Ensure the right meta files are loaded. + DeleteMetaTmp(); + Penumbra.CharacterUtility.LoadCharacterResources(); + ResettleSortOrder(); + ResettleCollectionSettings(); + ResettleForcedCollection(); + _config.Version = 2; + } + + private void DeleteMetaTmp() + { + var path = Path.Combine(_config.ModDirectory, "penumbrametatmp"); + if (!Directory.Exists(path)) + return; + + try + { + Directory.Delete(path, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}"); + } + } + + private void ResettleForcedCollection() + { + ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject() ?? ForcedCollection; + if (ForcedCollection.Length <= 0) + return; + + // Add the previous forced collection to all current collections except itself as an inheritance. + foreach (var collection in _fileNames.CollectionFiles) + { + try + { + var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); + if (jObject[nameof(ModCollection.Name)]?.ToObject() == ForcedCollection) + continue; + + jObject[nameof(ModCollection.Inheritance)] = JToken.FromObject(new List { ForcedCollection }); + File.WriteAllText(collection.FullName, jObject.ToString()); + } + catch (Exception e) + { + Penumbra.Log.Error( + $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}"); + } + } + } + + // Move the current sort order to its own file. + private void ResettleSortOrder() + { + ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; + var file = _fileNames.FilesystemFile; + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName("Data"); + j.WriteStartObject(); + foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key)))) + { + j.WritePropertyName(mod, true); + j.WriteValue(path); + } + + j.WriteEndObject(); + j.WritePropertyName("EmptyFolders"); + j.WriteStartArray(); + j.WriteEndArray(); + j.WriteEndObject(); + } + + // Move the active collections to their own file. + private void ResettleCollectionSettings() + { + CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection; + DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection; + CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections; + SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, + CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>()); + } + + // Outdated saving using the Characters list. + private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, + IEnumerable<(CollectionType, string)> special) + { + var file = _fileNames.ActiveCollectionsFile; + try + { + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName(nameof(ModCollection.Manager.Default)); + j.WriteValue(def); + j.WritePropertyName(nameof(ModCollection.Manager.Interface)); + j.WriteValue(ui); + j.WritePropertyName(nameof(ModCollection.Manager.Current)); + j.WriteValue(current); + foreach (var (type, collection) in special) + { + j.WritePropertyName(type.ToString()); + j.WriteValue(collection); + } + + j.WritePropertyName("Characters"); + j.WriteStartObject(); + foreach (var (character, collection) in characters) + { + j.WritePropertyName(character, true); + j.WriteValue(collection); + } + + j.WriteEndObject(); + j.WriteEndObject(); + Penumbra.Log.Verbose("Active Collections saved."); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); + } + } + + // Collections were introduced and the previous CurrentCollection got put into ModDirectory. + private void Version0To1() + { + if (_config.Version != 0) + return; + + _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty; + _config.Version = 1; + ResettleCollectionJson(); + } + + // Move the previous mod configurations to a new default collection file. + private void ResettleCollectionJson() + { + var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json")); + if (!collectionJson.Exists) + return; + + var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection); + var defaultCollectionFile = defaultCollection.FileName; + if (defaultCollectionFile.Exists) + return; + + try + { + var text = File.ReadAllText(collectionJson.FullName); + var data = JArray.Parse(text); + + var maxPriority = 0; + var dict = new Dictionary(); + foreach (var setting in data.Cast()) + { + var modName = (string)setting["FolderName"]!; + var enabled = (bool)setting["Enabled"]!; + var priority = (int)setting["Priority"]!; + var settings = setting["Settings"]!.ToObject>() + ?? setting["Conf"]!.ToObject>(); + + dict[modName] = new ModSettings.SavedSettings() + { + Enabled = enabled, + Priority = priority, + Settings = settings!, + }; + maxPriority = Math.Max(maxPriority, priority); + } + + InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder; + if (!InvertModListOrder) + dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); + + defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict); + defaultCollection.Save(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}"); + throw; + } + } + + // Create a backup of the configuration file specifically. + private void CreateBackup() + { + var name = _fileNames.ConfigFile; + var bakName = name + ".bak"; + try + { + File.Copy(name, bakName, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}"); + } + } + + public enum SortModeV3 : byte + { + FoldersFirst = 0x00, + Lexicographical = 0x01, + InverseFoldersFirst = 0x02, + InverseLexicographical = 0x03, + FoldersLast = 0x04, + InverseFoldersLast = 0x05, + InternalOrder = 0x06, + InternalOrderInverse = 0x07, + } +} diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 42d52067..a3227b92 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -78,21 +78,22 @@ public class DalamudServices services.AddSingleton(SigScanner); services.AddSingleton(this); } - + + // TODO remove static // @formatter:off - [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; // @formatter:on public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs new file mode 100644 index 00000000..27934da8 --- /dev/null +++ b/Penumbra/Services/FilenameService.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Plugin; +using OtterGui.Filesystem; + +namespace Penumbra.Services; + +public class FilenameService +{ + public readonly string ConfigDirectory; + public readonly string CollectionDirectory; + public readonly string LocalDataDirectory; + public readonly string ConfigFile; + public readonly string FilesystemFile; + public readonly string ActiveCollectionsFile; + + public FilenameService(DalamudPluginInterface pi) + { + ConfigDirectory = pi.ConfigDirectory.FullName; + CollectionDirectory = Path.Combine(pi.GetPluginConfigDirectory(), "collections"); + LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data"); + ConfigFile = pi.ConfigFile.FullName; + FilesystemFile = Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json"); + ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); + } + + public string CollectionFile(string collectionName) + => Path.Combine(CollectionDirectory, $"{collectionName.RemoveInvalidPathSymbols()}.json"); + + public string LocalDataFile(string modPath) + => Path.Combine(LocalDataDirectory, $"{modPath}.json"); + + public IEnumerable CollectionFiles + { + get + { + var directory = new DirectoryInfo(CollectionDirectory); + return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + } + } + + public IEnumerable LocalDataFiles + { + get + { + var directory = new DirectoryInfo(LocalDataDirectory); + return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + } + } +} diff --git a/Penumbra/Services/ObjectIdentifier.cs b/Penumbra/Services/ObjectIdentifier.cs deleted file mode 100644 index 7223017b..00000000 --- a/Penumbra/Services/ObjectIdentifier.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Dalamud.Data; -using Dalamud.Plugin; -using Lumina.Excel.GeneratedSheets; -using OtterGui.Classes; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Util; -using Action = System.Action; - -namespace Penumbra.Services; - -public sealed class ObjectIdentifier : IObjectIdentifier -{ - private const string Prefix = $"[{nameof(ObjectIdentifier)}]"; - - public IObjectIdentifier? Identifier { get; private set; } - - public bool IsDisposed { get; private set; } - - public bool Ready - => Identifier != null && !IsDisposed; - - public event Action? FinishedCreation; - - public ObjectIdentifier(StartTimeTracker tracker, DalamudPluginInterface pi, DataManager data) - { - Task.Run(() => - { - using var timer = tracker.Measure(StartTimeType.Identifier); - var identifier = GameData.GameData.GetIdentifier(pi, data); - if (IsDisposed) - { - identifier.Dispose(); - } - else - { - Identifier = identifier; - Penumbra.Log.Verbose($"{Prefix} Created."); - FinishedCreation?.Invoke(); - } - }); - } - - public void Dispose() - { - Identifier?.Dispose(); - IsDisposed = true; - Penumbra.Log.Verbose($"{Prefix} Disposed."); - } - - public IGamePathParser GamePathParser - => Identifier?.GamePathParser ?? throw new Exception($"{Prefix} Not yet ready."); - - public void Identify(IDictionary set, string path) - => Identifier?.Identify(set, path); - - public Dictionary Identify(string path) - => Identifier?.Identify(path) ?? new Dictionary(); - - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) - => Identifier?.Identify(setId, weaponType, variant, slot) ?? Array.Empty(); -} diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs new file mode 100644 index 00000000..403626b6 --- /dev/null +++ b/Penumbra/Services/ServiceWrapper.cs @@ -0,0 +1,117 @@ +using System; +using System.Threading.Tasks; +using OtterGui.Classes; +using Penumbra.Util; + +namespace Penumbra.Services; + +public interface IServiceWrapper : IDisposable +{ + public string Name { get; } + public T? Service { get; } + public bool Valid { get; } +} + +public abstract class SyncServiceWrapper : IServiceWrapper +{ + public string Name { get; } + public T Service { get; } + private bool _isDisposed; + + public bool Valid + => !_isDisposed; + + protected SyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func factory) + { + Name = name; + using var timer = tracker.Measure(type); + Service = factory(); + Penumbra.Log.Verbose($"[{Name}] Created."); + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + if (Service is IDisposable d) + d.Dispose(); + Penumbra.Log.Verbose($"[{Name}] Disposed."); + } +} + +public abstract class AsyncServiceWrapper : IServiceWrapper +{ + public string Name { get; } + public T? Service { get; private set; } + + public T AwaitedService + { + get + { + _task.Wait(); + return Service!; + } + } + + public bool Valid + => Service != null && !_isDisposed; + + public event Action? FinishedCreation; + private readonly Task _task; + + private bool _isDisposed; + + protected AsyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func factory) + { + Name = name; + _task = Task.Run(() => + { + using var timer = tracker.Measure(type); + var service = factory(); + if (_isDisposed) + { + if (service is IDisposable d) + d.Dispose(); + } + else + { + Service = service; + Penumbra.Log.Verbose($"[{Name}] Created."); + FinishedCreation?.Invoke(); + } + }); + } + + protected AsyncServiceWrapper(string name, Func factory) + { + Name = name; + _task = Task.Run(() => + { + var service = factory(); + if (_isDisposed) + { + if (service is IDisposable d) + d.Dispose(); + } + else + { + Service = service; + Penumbra.Log.Verbose($"[{Name}] Created."); + FinishedCreation?.Invoke(); + } + }); + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + if (Service is IDisposable d) + d.Dispose(); + Penumbra.Log.Verbose($"[{Name}] Disposed."); + } +} diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs new file mode 100644 index 00000000..cf493716 --- /dev/null +++ b/Penumbra/Services/StainService.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Data; +using Dalamud.Plugin; +using OtterGui.Classes; +using OtterGui.Widgets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.Util; + +namespace Penumbra.Services; + +public class StainService : IDisposable +{ + public sealed class StainTemplateCombo : FilterComboCache + { + public StainTemplateCombo(IEnumerable items) + : base(items) + { } + } + + public readonly StainData StainData; + public readonly FilterComboColors StainCombo; + public readonly StmFile StmFile; + public readonly StainTemplateCombo TemplateCombo; + + public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, DataManager dataManager) + { + using var t = timer.Measure(StartTimeType.Stains); + StainData = new StainData(pluginInterface, dataManager, dataManager.Language); + StainCombo = new FilterComboColors(140, StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); + StmFile = new StmFile(dataManager); + TemplateCombo = new StainTemplateCombo(StmFile.Entries.Keys.Prepend((ushort)0)); + Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); + } + + public void Dispose() + { + StainData.Dispose(); + Penumbra.Log.Verbose($"[{nameof(StainService)}] Disposed."); + } +} \ No newline at end of file diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs new file mode 100644 index 00000000..90a3cdd3 --- /dev/null +++ b/Penumbra/Services/Wrappers.cs @@ -0,0 +1,37 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Gui; +using Dalamud.Plugin; +using OtterGui.Classes; +using Penumbra.GameData; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Data; +using Penumbra.Interop.Resolver; +using Penumbra.Util; + +namespace Penumbra.Services; + +public sealed class IdentifierService : AsyncServiceWrapper +{ + public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, DataManager data) + : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, () => GameData.GameData.GetIdentifier(pi, data)) + { } +} + +public sealed class ItemService : AsyncServiceWrapper +{ + public ItemService(StartTracker tracker, DalamudPluginInterface pi, DataManager gameData) + : base(nameof(ItemService), tracker, StartTimeType.Items, () => new ItemData(pi, gameData, gameData.Language)) + { } +} + +public sealed class ActorService : AsyncServiceWrapper +{ + public ActorService(StartTracker tracker, DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, + Framework framework, DataManager gameData, GameGui gui, CutsceneCharacters cutscene) + : base(nameof(ActorService), tracker, StartTimeType.Actors, + () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)cutscene.GetParentIndex(idx))) + { } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 6c7ae4c6..c79f46d9 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -17,6 +17,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; +using Penumbra.Services; using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -42,49 +43,61 @@ public class ItemSwapWindow : IDisposable Weapon, } - private class ItemSelector : FilterComboCache< (string, Item) > + private class ItemSelector : FilterComboCache<(string, Item)> { - public ItemSelector( FullEquipType type ) - : base( () => Penumbra.ItemData[ type ].Select( i => ( i.Name.ToDalamudString().TextValue, i ) ).ToArray() ) + public ItemSelector(FullEquipType type) + : base(() => Penumbra.ItemData[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray()) { } - protected override string ToString( (string, Item) obj ) + protected override string ToString((string, Item) obj) => obj.Item1; } - private class WeaponSelector : FilterComboCache< FullEquipType > + private class WeaponSelector : FilterComboCache { public WeaponSelector() - : base( FullEquipTypeExtensions.WeaponTypes.Concat( FullEquipTypeExtensions.ToolTypes ) ) + : base(FullEquipTypeExtensions.WeaponTypes.Concat(FullEquipTypeExtensions.ToolTypes)) { } - protected override string ToString( FullEquipType type ) + protected override string ToString(FullEquipType type) => type.ToName(); } - public ItemSwapWindow() + private readonly CommunicatorService _communicator; + + public ItemSwapWindow(CommunicatorService communicator) { - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + _communicator = communicator; + _communicator.CollectionChange.Event += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; } public void Dispose() { - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + _communicator.CollectionChange.Event -= OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; } - private readonly Dictionary< SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo) > _selectors = new() + private readonly Dictionary _selectors = new() { - [ SwapType.Hat ] = ( new ItemSelector( FullEquipType.Head ), new ItemSelector( FullEquipType.Head ), "Take this Hat", "and put it on this one" ), - [ SwapType.Top ] = ( new ItemSelector( FullEquipType.Body ), new ItemSelector( FullEquipType.Body ), "Take this Top", "and put it on this one" ), - [ SwapType.Gloves ] = ( new ItemSelector( FullEquipType.Hands ), new ItemSelector( FullEquipType.Hands ), "Take these Gloves", "and put them on these" ), - [ SwapType.Pants ] = ( new ItemSelector( FullEquipType.Legs ), new ItemSelector( FullEquipType.Legs ), "Take these Pants", "and put them on these" ), - [ SwapType.Shoes ] = ( new ItemSelector( FullEquipType.Feet ), new ItemSelector( FullEquipType.Feet ), "Take these Shoes", "and put them on these" ), - [ SwapType.Earrings ] = ( new ItemSelector( FullEquipType.Ears ), new ItemSelector( FullEquipType.Ears ), "Take these Earrings", "and put them on these" ), - [ SwapType.Necklace ] = ( new ItemSelector( FullEquipType.Neck ), new ItemSelector( FullEquipType.Neck ), "Take this Necklace", "and put it on this one" ), - [ SwapType.Bracelet ] = ( new ItemSelector( FullEquipType.Wrists ), new ItemSelector( FullEquipType.Wrists ), "Take these Bracelets", "and put them on these" ), - [ SwapType.Ring ] = ( new ItemSelector( FullEquipType.Finger ), new ItemSelector( FullEquipType.Finger ), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = + (new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"), + [SwapType.Top] = + (new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "and put it on this one"), + [SwapType.Gloves] = + (new ItemSelector(FullEquipType.Hands), new ItemSelector(FullEquipType.Hands), "Take these Gloves", "and put them on these"), + [SwapType.Pants] = + (new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"), + [SwapType.Shoes] = + (new ItemSelector(FullEquipType.Feet), new ItemSelector(FullEquipType.Feet), "Take these Shoes", "and put them on these"), + [SwapType.Earrings] = + (new ItemSelector(FullEquipType.Ears), new ItemSelector(FullEquipType.Ears), "Take these Earrings", "and put them on these"), + [SwapType.Necklace] = + (new ItemSelector(FullEquipType.Neck), new ItemSelector(FullEquipType.Neck), "Take this Necklace", "and put it on this one"), + [SwapType.Bracelet] = + (new ItemSelector(FullEquipType.Wrists), new ItemSelector(FullEquipType.Wrists), "Take these Bracelets", "and put them on these"), + [SwapType.Ring] = (new ItemSelector(FullEquipType.Finger), new ItemSelector(FullEquipType.Finger), "Take this Ring", + "and put it on this one"), }; private ItemSelector? _weaponSource = null; @@ -117,39 +130,33 @@ public class ItemSwapWindow : IDisposable private Item[]? _affectedItems; - public void UpdateMod( Mod mod, ModSettings? settings ) + public void UpdateMod(Mod mod, ModSettings? settings) { - if( mod == _mod && settings == _modSettings ) - { + if (mod == _mod && settings == _modSettings) return; - } var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)"; - if( _newModName.Length == 0 || oldDefaultName == _newModName ) - { + if (_newModName.Length == 0 || oldDefaultName == _newModName) _newModName = $"{mod.Name.Text} (Swapped)"; - } _mod = mod; _modSettings = settings; - _swapData.LoadMod( _mod, _modSettings ); + _swapData.LoadMod(_mod, _modSettings); UpdateOption(); _dirty = true; } private void UpdateState() { - if( !_dirty ) - { + if (!_dirty) return; - } _swapData.Clear(); _loadException = null; _affectedItems = null; try { - switch( _lastTab ) + switch (_lastTab) { case SwapType.Hat: case SwapType.Top: @@ -178,27 +185,31 @@ public class ItemSwapWindow : IDisposable } break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Weapon: break; } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" ); + Penumbra.Log.Error($"Could not get Customization Data container for {_lastTab}:\n{e}"); _loadException = e; _affectedItems = null; _swapData.Clear(); @@ -207,13 +218,14 @@ public class ItemSwapWindow : IDisposable _dirty = false; } - private static string SwapToString( Swap swap ) + private static string SwapToString(Swap swap) { return swap switch { MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", - FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{( file.DataWasChanged ? " (EDITED)" : string.Empty )}", - _ => string.Empty, + FileSwap file => + $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}", + _ => string.Empty, }; } @@ -222,28 +234,28 @@ public class ItemSwapWindow : IDisposable private void UpdateOption() { - _selectedGroup = _mod?.Groups.FirstOrDefault( g => g.Name == _newGroupName ); - _subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 && ( _selectedGroup?.All( o => o.Name != _newOptionName ) ?? true ); + _selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName); + _subModValid = _mod != null + && _newGroupName.Length > 0 + && _newOptionName.Length > 0 + && (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true); } private void CreateMod() { - var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); - Mod.Creator.CreateDefaultFiles( newDir ); - Penumbra.ModManager.AddMod( newDir ); - if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) - { - Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); - } + var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); + Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); + Mod.Creator.CreateDefaultFiles(newDir); + Penumbra.ModManager.AddMod(newDir); + if (!_swapData.WriteMod(Penumbra.ModManager.Last(), + _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) + Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1); } private void CreateOption() { - if( _mod == null || !_subModValid ) - { + if (_mod == null || !_subModValid) return; - } var groupCreated = false; var dirCreated = false; @@ -251,52 +263,47 @@ public class ItemSwapWindow : IDisposable DirectoryInfo? optionFolderName = null; try { - optionFolderName = Mod.Creator.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); - if( optionFolderName?.Exists == true ) - { - throw new Exception( $"The folder {optionFolderName.FullName} for the option already exists." ); - } + optionFolderName = + Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), + _newOptionName); + if (optionFolderName?.Exists == true) + throw new Exception($"The folder {optionFolderName.FullName} for the option already exists."); - if( optionFolderName != null ) + if (optionFolderName != null) { - if( _selectedGroup == null ) + if (_selectedGroup == null) { - Penumbra.ModManager.AddModGroup( _mod, GroupType.Multi, _newGroupName ); + Penumbra.ModManager.AddModGroup(_mod, GroupType.Multi, _newGroupName); _selectedGroup = _mod.Groups.Last(); groupCreated = true; } - Penumbra.ModManager.AddOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _newOptionName ); + Penumbra.ModManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); optionCreated = true; - optionFolderName = Directory.CreateDirectory( optionFolderName.FullName ); + optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if( !_swapData.WriteMod( _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, - _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ) ) - { - throw new Exception( "Failure writing files for mod swap." ); - } + if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, + optionFolderName, + _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) + throw new Exception("Failure writing files for mod swap."); } } - catch( Exception e ) + catch (Exception e) { - ChatUtil.NotificationMessage( $"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error ); + ChatUtil.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); try { - if( optionCreated && _selectedGroup != null ) - { - Penumbra.ModManager.DeleteOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ); - } + if (optionCreated && _selectedGroup != null) + Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); - if( groupCreated ) + if (groupCreated) { - Penumbra.ModManager.DeleteModGroup( _mod, _mod.Groups.IndexOf( _selectedGroup! ) ); + Penumbra.ModManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); _selectedGroup = null; } - if( dirCreated && optionFolderName != null ) - { - Directory.Delete( optionFolderName.FullName, true ); - } + if (dirCreated && optionFolderName != null) + Directory.Delete(optionFolderName.FullName, true); } catch { @@ -307,12 +314,12 @@ public class ItemSwapWindow : IDisposable UpdateOption(); } - private void DrawHeaderLine( float width ) + private void DrawHeaderLine(float width) { var newModAvailable = _loadException == null && _swapData.Loaded; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputTextWithHint( "##newModName", "New Mod Name...", ref _newModName, 64 ) ) + ImGui.SetNextItemWidth(width); + if (ImGui.InputTextWithHint("##newModName", "New Mod Name...", ref _newModName, 64)) { } ImGui.SameLine(); @@ -321,29 +328,23 @@ public class ItemSwapWindow : IDisposable : _newModName.Length == 0 ? "Please enter a name for your mod." : "Create a new mod of the given name containing only the swap."; - if( ImGuiUtil.DrawDisabledButton( "Create New Mod", new Vector2( width / 2, 0 ), tt, !newModAvailable || _newModName.Length == 0 ) ) - { + if (ImGuiUtil.DrawDisabledButton("Create New Mod", new Vector2(width / 2, 0), tt, !newModAvailable || _newModName.Length == 0)) CreateMod(); - } ImGui.SameLine(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); - ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps ); - ImGuiUtil.HoverTooltip( "Instead of writing every single non-default file to the newly created mod or option,\n" - + "even those available from game files, use File Swaps to default game files where possible." ); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + ImGui.Checkbox("Use File Swaps", ref _useFileSwaps); + ImGuiUtil.HoverTooltip("Instead of writing every single non-default file to the newly created mod or option,\n" + + "even those available from game files, use File Swaps to default game files where possible."); - ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); - if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) ) - { + ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2); + if (ImGui.InputTextWithHint("##groupName", "Group Name...", ref _newGroupName, 32)) UpdateOption(); - } ImGui.SameLine(); - ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); - if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) ) - { + ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2); + if (ImGui.InputTextWithHint("##optionName", "New Option Name...", ref _newOptionName, 32)) UpdateOption(); - } ImGui.SameLine(); tt = !_subModValid @@ -351,16 +352,15 @@ public class ItemSwapWindow : IDisposable : !newModAvailable ? "Create a new option inside this mod containing only the swap." : "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap."; - if( ImGuiUtil.DrawDisabledButton( "Create New Option", new Vector2( width / 2, 0 ), tt, !newModAvailable || !_subModValid ) ) - { + if (ImGuiUtil.DrawDisabledButton("Create New Option", new Vector2(width / 2, 0), tt, !newModAvailable || !_subModValid)) CreateOption(); - } ImGui.SameLine(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); - _dirty |= ImGui.Checkbox( "Use Entire Collection", ref _useCurrentCollection ); - ImGuiUtil.HoverTooltip( "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" - + "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance." ); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + _dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection); + ImGuiUtil.HoverTooltip( + "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" + + "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance."); } private void DrawSwapBar() @@ -491,61 +491,58 @@ public class ItemSwapWindow : IDisposable return (article1, article2, source ? tuple.Source : tuple.Target); } - private void DrawEquipmentSwap( SwapType type ) + private void DrawEquipmentSwap(SwapType type) { - using var tab = DrawTab( type ); - if( !tab ) - { + using var tab = DrawTab(type); + if (!tab) return; - } - var (sourceSelector, targetSelector, text1, text2) = _selectors[ type ]; - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + var (sourceSelector, targetSelector, text1, text2) = _selectors[type]; + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text1 ); + ImGui.TextUnformatted(text1); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); - if( type == SwapType.Ring ) + if (type == SwapType.Ring) { ImGui.SameLine(); - _dirty |= ImGui.Checkbox( "Swap Right Ring", ref _useRightRing ); + _dirty |= ImGui.Checkbox("Swap Right Ring", ref _useRightRing); } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text2 ); + ImGui.TextUnformatted(text2); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); - if( type == SwapType.Ring ) + _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); + if (type == SwapType.Ring) { ImGui.SameLine(); - _dirty |= ImGui.Checkbox( "Swap Left Ring", ref _useLeftRing ); + _dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing); } - if( _affectedItems is { Length: > 1 } ) + if (_affectedItems is { Length: > 1 }) { ImGui.SameLine(); - ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, targetSelector.CurrentSelection.Item2 ) ) - .Select( i => i.Name.ToDalamudString().TextValue ) ) ); - } + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + Colors.PressEnterWarningBg); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) + .Select(i => i.Name.ToDalamudString().TextValue))); } } private void DrawHairSwap() { - using var tab = DrawTab( SwapType.Hair ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Hair); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Hairstyle" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Hairstyle"); DrawSourceIdInput(); DrawGenderInput(); } @@ -553,145 +550,139 @@ public class ItemSwapWindow : IDisposable private void DrawFaceSwap() { using var disabled = ImRaii.Disabled(); - using var tab = DrawTab( SwapType.Face ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Face); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Face Type" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Face Type"); DrawSourceIdInput(); DrawGenderInput(); } private void DrawTailSwap() { - using var tab = DrawTab( SwapType.Tail ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Tail); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Tail Type" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Tail Type"); DrawSourceIdInput(); - DrawGenderInput( "for all", 2 ); + DrawGenderInput("for all", 2); } private void DrawEarSwap() { - using var tab = DrawTab( SwapType.Ears ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Ears); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Ear Type" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Ear Type"); DrawSourceIdInput(); - DrawGenderInput( "for all Viera", 0 ); + DrawGenderInput("for all Viera", 0); } private void DrawWeaponSwap() { using var disabled = ImRaii.Disabled(); - using var tab = DrawTab( SwapType.Weapon ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Weapon); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "Select the weapon or tool you want" ); + ImGui.TextUnformatted("Select the weapon or tool you want"); ImGui.TableNextColumn(); - if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) + if (_slotSelector.Draw("##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing())) { _dirty = true; - _weaponSource = new ItemSelector( _slotSelector.CurrentSelection ); - _weaponTarget = new ItemSelector( _slotSelector.CurrentSelection ); + _weaponSource = new ItemSelector(_slotSelector.CurrentSelection); + _weaponTarget = new ItemSelector(_slotSelector.CurrentSelection); } else { _dirty = _weaponSource == null || _weaponTarget == null; - _weaponSource ??= new ItemSelector( _slotSelector.CurrentSelection ); - _weaponTarget ??= new ItemSelector( _slotSelector.CurrentSelection ); + _weaponSource ??= new ItemSelector(_slotSelector.CurrentSelection); + _weaponTarget ??= new ItemSelector(_slotSelector.CurrentSelection); } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "and put this variant of it" ); + ImGui.TextUnformatted("and put this variant of it"); ImGui.TableNextColumn(); - _dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "onto this one" ); + ImGui.TextUnformatted("onto this one"); ImGui.TableNextColumn(); - _dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); } private const float InputWidth = 120; - private void DrawTargetIdInput( string text = "Take this ID" ) + private void DrawTargetIdInput(string text = "Take this ID") { ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text ); + ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) ) - { - _targetId = Math.Clamp( _targetId, 0, byte.MaxValue ); - } + ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##targetId", ref _targetId, 0, 0)) + _targetId = Math.Clamp(_targetId, 0, byte.MaxValue); _dirty |= ImGui.IsItemDeactivatedAfterEdit(); } - private void DrawSourceIdInput( string text = "and put it on this one" ) + private void DrawSourceIdInput(string text = "and put it on this one") { ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text ); + ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ) ) - { - _sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue ); - } + ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0)) + _sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue); _dirty |= ImGui.IsItemDeactivatedAfterEdit(); } - private void DrawGenderInput( string text = "for all", int drawRace = 1 ) + private void DrawGenderInput(string text = "for all", int drawRace = 1) { ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text ); + ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - _dirty |= Combos.Gender( "##Gender", InputWidth, _currentGender, out _currentGender ); - if( drawRace == 1 ) + _dirty |= Combos.Gender("##Gender", InputWidth, _currentGender, out _currentGender); + if (drawRace == 1) { ImGui.SameLine(); - _dirty |= Combos.Race( "##Race", InputWidth, _currentRace, out _currentRace ); + _dirty |= Combos.Race("##Race", InputWidth, _currentRace, out _currentRace); } - else if( drawRace == 2 ) + else if (drawRace == 2) { ImGui.SameLine(); - if( _currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar ) - { + if (_currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar) _currentRace = ModelRace.Miqote; - } - _dirty |= ImGuiUtil.GenericEnumCombo( "##Race", InputWidth, _currentRace, out _currentRace, new[] { ModelRace.Miqote, ModelRace.AuRa, ModelRace.Hrothgar }, - RaceEnumExtensions.ToName ); + _dirty |= ImGuiUtil.GenericEnumCombo("##Race", InputWidth, _currentRace, out _currentRace, new[] + { + ModelRace.Miqote, + ModelRace.AuRa, + ModelRace.Hrothgar, + }, + RaceEnumExtensions.ToName); } } @@ -718,72 +709,54 @@ public class ItemSwapWindow : IDisposable public void DrawItemSwapPanel() { - using var tab = ImRaii.TabItem( "Item Swap (WIP)" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Item Swap (WIP)"); + if (!tab) return; - } ImGui.NewLine(); - DrawHeaderLine( 300 * ImGuiHelpers.GlobalScale ); + DrawHeaderLine(300 * ImGuiHelpers.GlobalScale); ImGui.NewLine(); DrawSwapBar(); - using var table = ImRaii.ListBox( "##swaps", -Vector2.One ); - if( _loadException != null ) - { - ImGuiUtil.TextWrapped( $"Could not load Customization Swap:\n{_loadException}" ); - } - else if( _swapData.Loaded ) - { - foreach( var swap in _swapData.Swaps ) - { - DrawSwap( swap ); - } - } + using var table = ImRaii.ListBox("##swaps", -Vector2.One); + if (_loadException != null) + ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}"); + else if (_swapData.Loaded) + foreach (var swap in _swapData.Swaps) + DrawSwap(swap); else - { - ImGui.TextUnformatted( NonExistentText() ); - } + ImGui.TextUnformatted(NonExistentText()); } - private static void DrawSwap( Swap swap ) + private static void DrawSwap(Swap swap) { var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; - using var tree = ImRaii.TreeNode( SwapToString( swap ), flags ); - if( !tree ) - { + using var tree = ImRaii.TreeNode(SwapToString(swap), flags); + if (!tree) return; - } - foreach( var child in swap.ChildSwaps ) - { - DrawSwap( child ); - } + foreach (var child in swap.ChildSwaps) + DrawSwap(child); } - private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, - ModCollection? newCollection, string _ ) + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, + ModCollection? newCollection, string _) { - if( collectionType != CollectionType.Current || _mod == null || newCollection == null ) - { + if (collectionType != CollectionType.Current || _mod == null || newCollection == null) return; - } - UpdateMod( _mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[ _mod.Index ] : null ); + UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[_mod.Index] : null); newCollection.ModSettingChanged += OnSettingChange; - if( oldCollection != null ) - { + if (oldCollection != null) oldCollection.ModSettingChanged -= OnSettingChange; - } } - private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) + private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) { - if( modIdx == _mod?.Index ) + if (modIdx == _mod?.Index) { - _swapData.LoadMod( _mod, _modSettings ); + _swapData.LoadMod(_mod, _modSettings); _dirty = true; } } diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs index d5cd5a44..bdfae0b8 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs @@ -97,7 +97,7 @@ public partial class ModEditWindow private static bool DrawPreviewDye( MtrlFile file, bool disabled ) { - var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; + var (dyeId, (name, dyeColor, _)) = Penumbra.StainService.StainCombo.CurrentSelection; var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) { @@ -106,7 +106,7 @@ public partial class ModEditWindow { for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); + ret |= file.ApplyDyeTemplate( Penumbra.StainService.StmFile, j, i, dyeId ); } } @@ -115,7 +115,7 @@ public partial class ModEditWindow ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); + Penumbra.StainService.StainCombo.Draw( label, dyeColor, string.Empty, true ); return false; } @@ -355,10 +355,10 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( hasDye ) { - if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + if( Penumbra.StainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainService.TemplateCombo.CurrentSelection; ret = true; } @@ -378,8 +378,8 @@ public partial class ModEditWindow private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) { - var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; - if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + var stain = Penumbra.StainService.StainCombo.CurrentSelection.Key; + if( stain == 0 || !Penumbra.StainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) { return false; } @@ -390,7 +390,7 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Apply the selected dye to this row.", disabled, true ); - ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); + ret = ret && file.ApplyDyeTemplate( Penumbra.StainService.StmFile, colorSetIdx, rowIdx, stain ); ImGui.SameLine(); ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 3fe48126..b1eea353 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -13,6 +13,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.Mods.Mod; @@ -22,7 +23,7 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - internal readonly ItemSwapWindow _swapWindow = new(); + internal readonly ItemSwapWindow _swapWindow; private Editor? _editor; private Mod? _mod; @@ -567,9 +568,10 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath( path ); } - public ModEditWindow() + public ModEditWindow(CommunicatorService communicator) : base( WindowBaseLabel ) - { + { + _swapWindow = new ItemSwapWindow( communicator ); _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index f1b9dbe3..c9ba7b13 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,41 +14,43 @@ using System.IO; using System.Linq; using System.Numerics; using Penumbra.Api.Enums; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.UI.Classes; -public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > +public sealed partial class ModFileSystemSelector : FileSystemSelector { - private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); - private TexToolsImporter? _import; - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; - public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + private readonly CommunicatorService _communicator; + private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); + private TexToolsImporter? _import; + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - public ModFileSystemSelector( ModFileSystem fileSystem ) - : base( fileSystem, DalamudServices.KeyState ) + public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem) + : base(fileSystem, DalamudServices.KeyState) { - SubscribeRightClickFolder( EnableDescendants, 10 ); - SubscribeRightClickFolder( DisableDescendants, 10 ); - SubscribeRightClickFolder( InheritDescendants, 15 ); - SubscribeRightClickFolder( OwnDescendants, 15 ); - SubscribeRightClickFolder( SetDefaultImportFolder, 100 ); - SubscribeRightClickLeaf( ToggleLeafFavorite, 0 ); - SubscribeRightClickMain( ClearDefaultImportFolder, 100 ); - AddButton( AddNewModButton, 0 ); - AddButton( AddImportModButton, 1 ); - AddButton( AddHelpButton, 2 ); - AddButton( DeleteModButton, 1000 ); + _communicator = communicator; + SubscribeRightClickFolder(EnableDescendants, 10); + SubscribeRightClickFolder(DisableDescendants, 10); + SubscribeRightClickFolder(InheritDescendants, 15); + SubscribeRightClickFolder(OwnDescendants, 15); + SubscribeRightClickFolder(SetDefaultImportFolder, 100); + SubscribeRightClickLeaf(ToggleLeafFavorite, 0); + SubscribeRightClickMain(ClearDefaultImportFolder, 100); + AddButton(AddNewModButton, 0); + AddButton(AddImportModButton, 1); + AddButton(AddHelpButton, 2); + AddButton(DeleteModButton, 1000); SetFilterTooltip(); SelectionChanged += OnSelectionChange; - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + _communicator.CollectionChange.Event += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; Penumbra.ModManager.ModDataChanged += OnModDataChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; - OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, "" ); + OnCollectionChange(CollectionType.Current, null, Penumbra.CollectionManager.Current, ""); } public override void Dispose() @@ -59,7 +61,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.ModManager.ModDataChanged -= OnModDataChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; - Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; + _communicator.CollectionChange.Event -= OnCollectionChange; _import?.Dispose(); _import = null; } @@ -68,7 +70,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod => base.SelectedLeaf; // Customization points. - public override ISortMode< Mod > SortMode + public override ISortMode SortMode => Penumbra.Config.SortMode; protected override uint ExpandedFolderColor @@ -89,91 +91,79 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod DrawHelpPopup(); DrawInfoPopup(); - if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) ) - { + if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName)) try { - var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); - Mod.Creator.CreateDefaultFiles( newDir ); - Penumbra.ModManager.AddMod( newDir ); + var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); + Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty); + Mod.Creator.CreateDefaultFiles(newDir); + Penumbra.ModManager.AddMod(newDir); _newModName = string.Empty; } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); + Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}"); } - } - while( _modsToAdd.TryDequeue( out var dir ) ) + while (_modsToAdd.TryDequeue(out var dir)) { - Penumbra.ModManager.AddMod( dir ); + Penumbra.ModManager.AddMod(dir); var mod = Penumbra.ModManager.LastOrDefault(); - if( mod != null ) + if (mod != null) { - MoveModToDefaultDirectory( mod ); - SelectByValue( mod ); + MoveModToDefaultDirectory(mod); + SelectByValue(mod); } } } - protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) + protected override void DrawLeafName(FileSystem.Leaf leaf, in ModState state, bool selected) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ) - .Push( ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite ); - using var id = ImRaii.PushId( leaf.Value.Index ); - ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose(); + using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value()) + .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); + using var id = ImRaii.PushId(leaf.Value.Index); + ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); } // Add custom context menu items. - private static void EnableDescendants( ModFileSystem.Folder folder ) + private static void EnableDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Enable Descendants" ) ) - { - SetDescendants( folder, true ); - } + if (ImGui.MenuItem("Enable Descendants")) + SetDescendants(folder, true); } - private static void DisableDescendants( ModFileSystem.Folder folder ) + private static void DisableDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Disable Descendants" ) ) - { - SetDescendants( folder, false ); - } + if (ImGui.MenuItem("Disable Descendants")) + SetDescendants(folder, false); } - private static void InheritDescendants( ModFileSystem.Folder folder ) + private static void InheritDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Inherit Descendants" ) ) - { - SetDescendants( folder, true, true ); - } + if (ImGui.MenuItem("Inherit Descendants")) + SetDescendants(folder, true, true); } - private static void OwnDescendants( ModFileSystem.Folder folder ) + private static void OwnDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Stop Inheriting Descendants" ) ) - { - SetDescendants( folder, false, true ); - } + if (ImGui.MenuItem("Stop Inheriting Descendants")) + SetDescendants(folder, false, true); } - private static void ToggleLeafFavorite( FileSystem< Mod >.Leaf mod ) + private static void ToggleLeafFavorite(FileSystem.Leaf mod) { - if( ImGui.MenuItem( mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite" ) ) - { - Penumbra.ModManager.ChangeModFavorite( mod.Value.Index, !mod.Value.Favorite ); - } + if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) + Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); } - private static void SetDefaultImportFolder( ModFileSystem.Folder folder ) + private static void SetDefaultImportFolder(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Set As Default Import Folder" ) ) + if (ImGui.MenuItem("Set As Default Import Folder")) { var newName = folder.FullName(); - if( newName != Penumbra.Config.DefaultImportFolder ) + if (newName != Penumbra.Config.DefaultImportFolder) { Penumbra.Config.DefaultImportFolder = newName; Penumbra.Config.Save(); @@ -183,7 +173,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private static void ClearDefaultImportFolder() { - if( ImGui.MenuItem( "Clear Default Import Folder" ) && Penumbra.Config.DefaultImportFolder.Length > 0 ) + if (ImGui.MenuItem("Clear Default Import Folder") && Penumbra.Config.DefaultImportFolder.Length > 0) { Penumbra.Config.DefaultImportFolder = string.Empty; Penumbra.Config.Save(); @@ -194,71 +184,63 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod // Add custom buttons. private string _newModName = string.Empty; - private static void AddNewModButton( Vector2 size ) + private static void AddNewModButton(Vector2 size) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", - !Penumbra.ModManager.Valid, true ) ) - { - ImGui.OpenPopup( "Create New Mod" ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", + !Penumbra.ModManager.Valid, true)) + ImGui.OpenPopup("Create New Mod"); } // Add an import mods button that opens a file selector. // Only set the initial directory once. private bool _hasSetFolder; - private void AddImportModButton( Vector2 size ) + private void AddImportModButton(Vector2 size) { - var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true ); - ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModImport ); - if( !button ) - { + var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, + "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); + ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.ModImport); + if (!button) return; - } var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath - : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; + : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; _hasSetFolder = true; - _fileManager.OpenFileDialog( "Import Mod Pack", - "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", ( s, f ) => + _fileManager.OpenFileDialog("Import Mod Pack", + "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) => { - if( s ) + if (s) { - _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ), - AddNewMod ); - ImGui.OpenPopup( "Import Status" ); + _import = new TexToolsImporter(Penumbra.ModManager.BasePath, f.Count, f.Select(file => new FileInfo(file)), + AddNewMod); + ImGui.OpenPopup("Import Status"); } - }, 0, modPath ); + }, 0, modPath); } // Draw the progress information for import. private void DrawInfoPopup() { var display = ImGui.GetIO().DisplaySize; - var height = Math.Max( display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing() ); + var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); var width = display.X / 8; - var size = new Vector2( width * 2, height ); - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2 ); - ImGui.SetNextWindowSize( size ); - using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); - if( _import == null || !popup.Success ) - { + var size = new Vector2(width * 2, height); + ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); + ImGui.SetNextWindowSize(size); + using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); + if (_import == null || !popup.Success) return; - } - using( var child = ImRaii.Child( "##import", new Vector2( -1, size.Y - ImGui.GetFrameHeight() * 2 ) ) ) + using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) { - if( child ) - { - _import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) ); - } + if (child) + _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); } - if( _import.State == ImporterState.Done && ImGui.Button( "Close", -Vector2.UnitX ) - || _import.State != ImporterState.Done && _import.DrawCancelButton( -Vector2.UnitX ) ) + if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX) + || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX)) { _import?.Dispose(); _import = null; @@ -267,100 +249,84 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } // Mods need to be added thread-safely outside of iteration. - private readonly ConcurrentQueue< DirectoryInfo > _modsToAdd = new(); + private readonly ConcurrentQueue _modsToAdd = new(); // Clean up invalid directory if necessary. // Add successfully extracted mods. - private void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error ) + private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) { - if( error != null ) + if (error != null) { - if( dir != null && Directory.Exists( dir.FullName ) ) - { + if (dir != null && Directory.Exists(dir.FullName)) try { - Directory.Delete( dir.FullName, true ); + Directory.Delete(dir.FullName, true); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); + Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); } - } - if( error is not OperationCanceledException ) - { - Penumbra.Log.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" ); - } + if (error is not OperationCanceledException) + Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); } - else if( dir != null ) + else if (dir != null) { - _modsToAdd.Enqueue( dir ); + _modsToAdd.Enqueue(dir); } } - private void DeleteModButton( Vector2 size ) + private void DeleteModButton(Vector2 size) { var keys = Penumbra.Config.DeleteModModifier.IsActive(); var tt = SelectedLeaf == null ? "No mod selected." : "Delete the currently selected mod entirely from your drive.\n" + "This can not be undone."; - if( !keys ) - { + if (!keys) tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; - } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true ) - && Selected != null ) - { - Penumbra.ModManager.DeleteMod( Selected.Index ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) + && Selected != null) + Penumbra.ModManager.DeleteMod(Selected.Index); } - private static void AddHelpButton( Vector2 size ) + private static void AddHelpButton(Vector2 size) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true ) ) - { - ImGui.OpenPopup( "ExtendedHelp" ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true)) + ImGui.OpenPopup("ExtendedHelp"); - ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.AdvancedHelp ); + ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp); } // Helpers. - private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false ) + private static void SetDescendants(ModFileSystem.Folder folder, bool enabled, bool inherit = false) { - var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => + var mods = folder.GetAllDescendants(ISortMode.Lexicographical).OfType().Select(l => { // Any mod handled here should not stay new. - Penumbra.ModManager.NewMods.Remove( l.Value ); + Penumbra.ModManager.NewMods.Remove(l.Value); return l.Value; - } ); + }); - if( inherit ) - { - Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled ); - } + if (inherit) + Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled); else - { - Penumbra.CollectionManager.Current.SetMultipleModStates( mods, enabled ); - } + Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled); } // Automatic cache update functions. - private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) + private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) { // TODO: maybe make more efficient SetFilterDirty(); - if( modIdx == Selected?.Index ) - { - OnSelectionChange( Selected, Selected, default ); - } + if (modIdx == Selected?.Index) + OnSelectionChange(Selected, Selected, default); } - private void OnModDataChange( ModDataChangeType type, Mod mod, string? oldName ) + private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) { - switch( type ) + switch (type) { case ModDataChangeType.Name: case ModDataChangeType.Author: @@ -372,46 +338,44 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } - private void OnInheritanceChange( bool _ ) + private void OnInheritanceChange(bool _) { SetFilterDirty(); - OnSelectionChange( Selected, Selected, default ); + OnSelectionChange(Selected, Selected, default); } - private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _ ) + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) { - if( collectionType != CollectionType.Current || oldCollection == newCollection ) - { + if (collectionType != CollectionType.Current || oldCollection == newCollection) return; - } - if( oldCollection != null ) + if (oldCollection != null) { oldCollection.ModSettingChanged -= OnSettingChange; oldCollection.InheritanceChanged -= OnInheritanceChange; } - if( newCollection != null ) + if (newCollection != null) { newCollection.ModSettingChanged += OnSettingChange; newCollection.InheritanceChanged += OnInheritanceChange; } SetFilterDirty(); - OnSelectionChange( Selected, Selected, default ); + OnSelectionChange(Selected, Selected, default); } - private void OnSelectionChange( Mod? _1, Mod? newSelection, in ModState _2 ) + private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2) { - if( newSelection == null ) + if (newSelection == null) { SelectedSettings = ModSettings.Empty; SelectedSettingCollection = ModCollection.Empty; } else { - ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ]; - SelectedSettings = settings ?? ModSettings.Empty; + (var settings, SelectedSettingCollection) = Penumbra.CollectionManager.Current[newSelection.Index]; + SelectedSettings = settings ?? ModSettings.Empty; } } @@ -426,92 +390,89 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void RestoreLastSelection() { - if( _lastSelectedDirectory.Length > 0 ) + if (_lastSelectedDirectory.Length > 0) { - var leaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) - .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory ); - Select( leaf ); + var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode.Lexicographical) + .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory); + Select(leaf); _lastSelectedDirectory = string.Empty; } } // If a default import folder is setup, try to move the given mod in there. // If the folder does not exist, create it if possible. - private void MoveModToDefaultDirectory( Mod mod ) + private void MoveModToDefaultDirectory(Mod mod) { - if( Penumbra.Config.DefaultImportFolder.Length == 0 ) - { + if (Penumbra.Config.DefaultImportFolder.Length == 0) return; - } try { - var leaf = FileSystem.Root.GetChildren( ISortMode< Mod >.Lexicographical ) - .FirstOrDefault( f => f is FileSystem< Mod >.Leaf l && l.Value == mod ); - if( leaf == null ) - { - throw new Exception( "Mod was not found at root." ); - } + var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) + .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); + if (leaf == null) + throw new Exception("Mod was not found at root."); - var folder = FileSystem.FindOrCreateAllFolders( Penumbra.Config.DefaultImportFolder ); - FileSystem.Move( leaf, folder ); + var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder); + FileSystem.Move(leaf, folder); } - catch( Exception e ) + catch (Exception e) { Penumbra.Log.Warning( - $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}" ); + $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}"); } } private static void DrawHelpPopup() { - ImGuiUtil.HelpPopup( "ExtendedHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing() ), () => + ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing()), () => { - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.TextUnformatted( "Mod Management" ); - ImGui.BulletText( "You can create empty mods or import mods with the buttons in this row." ); + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Management"); + ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); using var indent = ImRaii.PushIndent(); - ImGui.BulletText( "Supported formats for import are: .ttmp, .ttmp2, .pmp." ); - ImGui.BulletText( "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata." ); - indent.Pop( 1 ); - ImGui.BulletText( "You can also create empty mod folders and delete mods." ); - ImGui.BulletText( "For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.TextUnformatted( "Mod Selector" ); - ImGui.BulletText( "Select a mod to obtain more information or change settings." ); - ImGui.BulletText( "Names are colored according to your config and their current state in the collection:" ); - indent.Push(); - ImGuiUtil.BulletTextColored( ColorId.EnabledMod.Value(), "enabled in the current collection." ); - ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." ); - ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." ); - ImGuiUtil.BulletTextColored( ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection." ); - ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections." ); - ImGuiUtil.BulletTextColored( ColorId.NewMod.Value(), - "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); - ImGuiUtil.BulletTextColored( ColorId.HandledConflictMod.Value(), - "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); - ImGuiUtil.BulletTextColored( ColorId.ConflictingMod.Value(), - "enabled and conflicting with another enabled Mod on the same priority." ); - ImGuiUtil.BulletTextColored( ColorId.FolderExpanded.Value(), "expanded mod folder." ); - ImGuiUtil.BulletTextColored( ColorId.FolderCollapsed.Value(), "collapsed mod folder" ); - indent.Pop( 1 ); - ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number." ); - indent.Push(); - ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); + ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically." ); - indent.Pop( 1 ); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); - ImGui.BulletText( "Right-clicking a folder opens a context menu." ); - ImGui.BulletText( "Right-clicking empty space allows you to expand or collapse all folders at once." ); - ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text." ); + "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); + indent.Pop(1); + ImGui.BulletText("You can also create empty mod folders and delete mods."); + ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Selector"); + ImGui.BulletText("Select a mod to obtain more information or change settings."); + ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); indent.Push(); - ImGui.BulletText( "You can enter n:[string] to filter only for names, without path." ); - ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); - ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); - indent.Pop( 1 ); - ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); - } ); + ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."); + ImGuiUtil.BulletTextColored(ColorId.NewMod.Value(), + "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded."); + ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); + ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(), + "enabled and conflicting with another enabled Mod on the same priority."); + ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."); + ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"); + indent.Pop(1); + ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); + indent.Push(); + ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); + indent.Pop(1); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); + ImGui.BulletText("Right-clicking a folder opens a context menu."); + ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); + ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); + indent.Push(); + ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); + ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); + ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); + indent.Pop(1); + ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); + }); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 42417134..bac726a2 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Services; namespace Penumbra.UI; @@ -16,20 +17,22 @@ public partial class ConfigWindow // Encapsulate for less pollution. private partial class CollectionsTab : IDisposable, ITab { - private readonly ConfigWindow _window; + private readonly CommunicatorService _communicator; + private readonly ConfigWindow _window; - public CollectionsTab( ConfigWindow window ) + public CollectionsTab( CommunicatorService communicator, ConfigWindow window ) { - _window = window; + _window = window; + _communicator = communicator; - Penumbra.CollectionManager.CollectionChanged += UpdateIdentifiers; + _communicator.CollectionChange.Event += UpdateIdentifiers; } public ReadOnlySpan Label => "Collections"u8; public void Dispose() - => Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers; + => _communicator.CollectionChange.Event -= UpdateIdentifiers; public void DrawHeader() => OpenTutorial( BasicTutorialSteps.Collections ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index b59ce96a..e5b0fe84 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Widgets; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; @@ -28,10 +29,14 @@ public partial class ConfigWindow { private class DebugTab : ITab { + private readonly StartTracker _timer; private readonly ConfigWindow _window; - public DebugTab( ConfigWindow window ) - => _window = window; + public DebugTab( ConfigWindow window, StartTracker timer) + { + _window = window; + _timer = timer; + } public ReadOnlySpan Label => "Debug"u8; @@ -109,7 +114,7 @@ public partial class ConfigWindow PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() ); } - private static void DrawPerformanceTab() + private void DrawPerformanceTab() { ImGui.NewLine(); if( ImGui.CollapsingHeader( "Performance" ) ) @@ -121,7 +126,7 @@ public partial class ConfigWindow { if( start ) { - Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); + _timer.Draw( "##startTimer", TimingExtensions.ToName ); ImGui.NewLine(); } } @@ -397,7 +402,7 @@ public partial class ConfigWindow return; } - foreach( var (key, data) in Penumbra.StainManager.StmFile.Entries ) + foreach( var (key, data) in Penumbra.StainService.StmFile.Entries ) { using var tree = TreeNode( $"Template {key}" ); if( !tree ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 4bc6454e..8a539b4f 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -105,7 +105,7 @@ public partial class ConfigWindow private static void DrawWaitForPluginsReflection() { - if( !DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) + if( !Penumbra.Dalamud.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) { using var disabled = ImRaii.Disabled(); Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } ); @@ -113,7 +113,7 @@ public partial class ConfigWindow else { Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, - v => DalamudServices.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); + v => Penumbra.Dalamud.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); } } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 8300e83e..e3037d68 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -299,11 +299,11 @@ public partial class ConfigWindow private const string SupportInfoButtonText = "Copy Support Info to Clipboard"; - public static void DrawSupportButton() + public static void DrawSupportButton(Penumbra penumbra) { if( ImGui.Button( SupportInfoButtonText ) ) { - var text = Penumbra.GatherSupportInformation(); + var text = penumbra.GatherSupportInformation(); ImGui.SetClipboardText( text ); } } @@ -345,7 +345,7 @@ public partial class ConfigWindow } ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) ); - DrawSupportButton(); + DrawSupportButton(_window._penumbra); ImGui.SetCursorPos( new Vector2( xPos, 0 ) ); DrawDiscordButton( width ); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 7342037c..c925ce16 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -8,7 +8,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; @@ -19,7 +19,7 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly Penumbra _penumbra; private readonly ModFileSystemSelector _selector; private readonly ModPanel _modPanel; - public readonly ModEditWindow ModEditPopup = new(); + public readonly ModEditWindow ModEditPopup; private readonly SettingsTab _settingsTab; private readonly CollectionsTab _collectionsTab; @@ -31,43 +31,43 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly ResourceWatcher _resourceWatcher; public TabType SelectTab = TabType.None; - public void SelectMod( Mod mod ) - => _selector.SelectByValue( mod ); - public ConfigWindow( Penumbra penumbra, ResourceWatcher watcher ) - : base( GetLabel() ) + public void SelectMod(Mod mod) + => _selector.SelectByValue(mod); + + public ConfigWindow(CommunicatorService communicator, StartTracker timer, Penumbra penumbra, ResourceWatcher watcher) + : base(GetLabel()) { _penumbra = penumbra; _resourceWatcher = watcher; - _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); - _modPanel = new ModPanel( this ); - _modsTab = new ModsTab( _selector, _modPanel, _penumbra ); + ModEditPopup = new ModEditWindow(communicator); + _settingsTab = new SettingsTab(this); + _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem); + _modPanel = new ModPanel(this); + _modsTab = new ModsTab(_selector, _modPanel, _penumbra); _selector.SelectionChanged += _modPanel.OnSelectionChange; - _collectionsTab = new CollectionsTab( this ); - _changedItemsTab = new ChangedItemsTab( this ); + _collectionsTab = new CollectionsTab(communicator, this); + _changedItemsTab = new ChangedItemsTab(this); _effectiveTab = new EffectiveTab(); - _debugTab = new DebugTab( this ); + _debugTab = new DebugTab(this, timer); _resourceTab = new ResourceTab(); - if( Penumbra.Config.FixMainWindow ) - { + if (Penumbra.Config.FixMainWindow) Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; - } DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; - RespectCloseHotkey = true; + RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2( 800, 600 ), - MaximumSize = new Vector2( 4096, 2160 ), + MinimumSize = new Vector2(800, 600), + MaximumSize = new Vector2(4096, 2160), }; UpdateTutorialStep(); } - private ReadOnlySpan< byte > ToLabel( TabType type ) + private ReadOnlySpan ToLabel(TabType type) => type switch { TabType.Settings => _settingsTab.Label, @@ -78,85 +78,85 @@ public sealed partial class ConfigWindow : Window, IDisposable TabType.ResourceWatcher => _resourceWatcher.Label, TabType.Debug => _debugTab.Label, TabType.ResourceManager => _resourceTab.Label, - _ => ReadOnlySpan< byte >.Empty, + _ => ReadOnlySpan.Empty, }; public override void Draw() { - using var performance = Penumbra.Performance.Measure( PerformanceType.UiMainWindow ); + using var performance = Penumbra.Performance.Measure(PerformanceType.UiMainWindow); try { - if( Penumbra.ValidityChecker.ImcExceptions.Count > 0 ) + if (Penumbra.ValidityChecker.ImcExceptions.Count > 0) { - DrawProblemWindow( $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow(_penumbra, + $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" - + "Please use the Launcher's Repair Game Files function to repair your client installation.", true ); + + "Please use the Launcher's Repair Game Files function to repair your client installation.", true); } - else if( !Penumbra.ValidityChecker.IsValidSourceRepo ) + else if (!Penumbra.ValidityChecker.IsValidSourceRepo) { - DrawProblemWindow( + DrawProblemWindow(_penumbra, $"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); } - else if( Penumbra.ValidityChecker.IsNotInstalledPenumbra ) + else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra) { - DrawProblemWindow( + DrawProblemWindow(_penumbra, $"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); } - else if( Penumbra.ValidityChecker.DevPenumbraExists ) + else if (Penumbra.ValidityChecker.DevPenumbraExists) { - DrawProblemWindow( + DrawProblemWindow(_penumbra, $"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" - + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", false ); + + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", + false); } else { SetupSizes(); - if( TabBar.Draw( string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel( SelectTab ), _settingsTab, _modsTab, _collectionsTab, - _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab ) ) - { + if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab, + _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab)) SelectTab = TabType.None; - } } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Exception thrown during UI Render:\n{e}" ); + Penumbra.Log.Error($"Exception thrown during UI Render:\n{e}"); } } - private static void DrawProblemWindow( string text, bool withExceptions ) + private static void DrawProblemWindow(Penumbra penumbra, string text, bool withExceptions) { - using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); ImGui.NewLine(); ImGui.NewLine(); - ImGuiUtil.TextWrapped( text ); + ImGuiUtil.TextWrapped(text); color.Pop(); ImGui.NewLine(); ImGui.NewLine(); - SettingsTab.DrawDiscordButton( 0 ); + SettingsTab.DrawDiscordButton(0); ImGui.SameLine(); - SettingsTab.DrawSupportButton(); + SettingsTab.DrawSupportButton(penumbra); ImGui.NewLine(); ImGui.NewLine(); - if( withExceptions ) + if (withExceptions) { - ImGui.TextUnformatted( "Exceptions" ); + ImGui.TextUnformatted("Exceptions"); ImGui.Separator(); - using var box = ImRaii.ListBox( "##Exceptions", new Vector2( -1, -1 ) ); - foreach( var exception in Penumbra.ValidityChecker.ImcExceptions ) + using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1)); + foreach (var exception in Penumbra.ValidityChecker.ImcExceptions) { - ImGuiUtil.TextWrapped( exception.ToString() ); + ImGuiUtil.TextWrapped(exception.ToString()); ImGui.Separator(); ImGui.NewLine(); } @@ -182,8 +182,8 @@ public sealed partial class ConfigWindow : Window, IDisposable private void SetupSizes() { - _defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale ); - _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); - _iconButtonSize = new Vector2( ImGui.GetFrameHeight() ); + _defaultSpace = new Vector2(0, 10 * ImGuiHelpers.GlobalScale); + _inputTextWidth = new Vector2(350f * ImGuiHelpers.GlobalScale, 0); + _iconButtonSize = new Vector2(ImGui.GetFrameHeight()); } -} \ No newline at end of file +} diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs new file mode 100644 index 00000000..e25cc99c --- /dev/null +++ b/Penumbra/Util/EventWrapper.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Penumbra.Util; + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke() + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3, arg4); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3, arg4, arg5); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3, arg4, arg5, arg6); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index de692ad8..c0ad766d 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,4 +1,5 @@ -using System; +global using StartTracker = OtterGui.Classes.StartTimeTracker; +global using PerformanceTracker = OtterGui.Classes.PerformanceTracker; namespace Penumbra.Util; @@ -47,24 +48,24 @@ public enum PerformanceType public static class TimingExtensions { - public static string ToName( this StartTimeType type ) + public static string ToName(this StartTimeType type) => type switch { - StartTimeType.Total => "Total Construction", - StartTimeType.Identifier => "Identification Data", - StartTimeType.Stains => "Stain Data", - StartTimeType.Items => "Item Data", - StartTimeType.Actors => "Actor Data", - StartTimeType.Backup => "Checking Backups", - StartTimeType.Mods => "Loading Mods", - StartTimeType.Collections => "Loading Collections", - StartTimeType.Api => "Setting Up API", - StartTimeType.Interface => "Setting Up Interface", - StartTimeType.PathResolver => "Setting Up Path Resolver", - _ => $"Unknown {(int) type}", + StartTimeType.Total => "Total Construction", + StartTimeType.Identifier => "Identification Data", + StartTimeType.Stains => "Stain Data", + StartTimeType.Items => "Item Data", + StartTimeType.Actors => "Actor Data", + StartTimeType.Backup => "Checking Backups", + StartTimeType.Mods => "Loading Mods", + StartTimeType.Collections => "Loading Collections", + StartTimeType.Api => "Setting Up API", + StartTimeType.Interface => "Setting Up Interface", + StartTimeType.PathResolver => "Setting Up Path Resolver", + _ => $"Unknown {(int)type}", }; - public static string ToName( this PerformanceType type ) + public static string ToName(this PerformanceType type) => type switch { PerformanceType.UiMainWindow => "Main Interface Drawing", @@ -91,6 +92,6 @@ public static class TimingExtensions PerformanceType.LoadPap => "LoadPap Hook", PerformanceType.LoadAction => "LoadAction Hook", PerformanceType.DebugTimes => "Debug Tracking", - _ => $"Unknown {( int )type}", + _ => $"Unknown {(int)type}", }; -} \ No newline at end of file +} diff --git a/Penumbra/Util/StainManager.cs b/Penumbra/Util/StainManager.cs deleted file mode 100644 index 91c9a731..00000000 --- a/Penumbra/Util/StainManager.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Data; -using Dalamud.Plugin; -using OtterGui.Widgets; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; - -namespace Penumbra.Util; - -public class StainManager : IDisposable -{ - public sealed class StainTemplateCombo : FilterComboCache< ushort > - { - public StainTemplateCombo( IEnumerable< ushort > items ) - : base( items ) - { } - } - - public readonly StainData StainData; - public readonly FilterComboColors StainCombo; - public readonly StmFile StmFile; - public readonly StainTemplateCombo TemplateCombo; - - public StainManager( DalamudPluginInterface pluginInterface, DataManager dataManager ) - { - StainData = new StainData( pluginInterface, dataManager, dataManager.Language ); - StainCombo = new FilterComboColors( 140, StainData.Data.Prepend( new KeyValuePair< byte, (string Name, uint Dye, bool Gloss) >( 0, ( "None", 0, false ) ) ) ); - StmFile = new StmFile( dataManager ); - TemplateCombo = new StainTemplateCombo( StmFile.Entries.Keys.Prepend( ( ushort )0 ) ); - } - - public void Dispose() - => StainData.Dispose(); -} \ No newline at end of file