diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index c75b7e44..5e950228 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -39,19 +39,59 @@ public partial class ModCollection public ModCollection Character( string name ) => _characters.TryGetValue( name, out var c ) ? c : Default; - // Set a active collection, can be used to set Default, Current or Character collections. - private void SetCollection( int newIdx, Type type, string? characterName = null ) + // Special Collections + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; + + // Return the configured collection for the given type or null. + public ModCollection? ByType( CollectionType type, string? name = null ) { - var oldCollectionIdx = type switch + if( type.IsSpecial() ) { - Type.Default => Default.Index, - Type.Current => Current.Index, - Type.Character => characterName?.Length > 0 + return _specialCollections[ ( int )type ]; + } + + return type switch + { + CollectionType.Default => Default, + CollectionType.Current => Current, + CollectionType.Character => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, + CollectionType.Inactive => name != null ? ByName( name, out var c ) ? c : null : null, + _ => null, + }; + } + + // Set a active collection, can be used to set Default, Current or Character collections. + private void SetCollection( int newIdx, CollectionType collectionType, string? characterName = null ) + { + var oldCollectionIdx = collectionType switch + { + CollectionType.Default => Default.Index, + CollectionType.Current => Current.Index, + CollectionType.Character => characterName?.Length > 0 ? _characters.TryGetValue( characterName, out var c ) ? c.Index : Default.Index : -1, - _ => -1, + CollectionType.Yourself => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.PlayerCharacter => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.NonPlayerCharacter => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Midlander => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Highlander => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Wildwood => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Duskwight => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Plainsfolk => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Dunesfolk => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.SeekerOfTheSun => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.KeeperOfTheMoon => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Seawolf => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Hellsguard => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Raen => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Xaela => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Helion => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Lost => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Rava => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Veena => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + _ => -1, }; if( oldCollectionIdx == -1 || newIdx == oldCollectionIdx ) @@ -66,31 +106,109 @@ public partial class ModCollection } RemoveCache( oldCollectionIdx ); - switch( type ) + switch( collectionType ) { - case Type.Default: + case CollectionType.Default: Default = newCollection; Penumbra.ResidentResources.Reload(); Default.SetFiles(); break; - case Type.Current: + case CollectionType.Current: Current = newCollection; break; - case Type.Character: + case CollectionType.Character: _characters[ characterName! ] = newCollection; break; + default: + _specialCollections[ ( int )collectionType ] = newCollection; + break; } - + UpdateCurrentCollectionInUse(); - CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); + CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, characterName ); } private void UpdateCurrentCollectionInUse() - => CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); + => CurrentCollectionInUse = _specialCollections + .OfType< ModCollection >() + .Prepend( Default ) + .Concat( Characters.Values ) + .SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); - public void SetCollection( ModCollection collection, Type type, string? characterName = null ) - => SetCollection( collection.Index, type, characterName ); + public void SetCollection( ModCollection collection, CollectionType collectionType, string? characterName = null ) + => SetCollection( collection.Index, collectionType, characterName ); + + // Create a special collection if it does not exist and set it to Empty. + public bool CreateSpecialCollection( CollectionType collectionType ) + { + switch( collectionType ) + { + case CollectionType.Yourself: + case CollectionType.PlayerCharacter: + case CollectionType.NonPlayerCharacter: + case CollectionType.Midlander: + case CollectionType.Highlander: + case CollectionType.Wildwood: + case CollectionType.Duskwight: + case CollectionType.Plainsfolk: + case CollectionType.Dunesfolk: + case CollectionType.SeekerOfTheSun: + case CollectionType.KeeperOfTheMoon: + case CollectionType.Seawolf: + case CollectionType.Hellsguard: + case CollectionType.Raen: + case CollectionType.Xaela: + case CollectionType.Helion: + case CollectionType.Lost: + case CollectionType.Rava: + case CollectionType.Veena: + if( _specialCollections[ ( int )collectionType ] != null ) + { + return false; + } + + _specialCollections[ ( int )collectionType ] = Empty; + CollectionChanged.Invoke( collectionType, null, Empty, null ); + return true; + default: return false; + } + } + + // Remove a special collection if it exists + public void RemoveSpecialCollection( CollectionType collectionType ) + { + switch( collectionType ) + { + case CollectionType.Yourself: + case CollectionType.PlayerCharacter: + case CollectionType.NonPlayerCharacter: + case CollectionType.Midlander: + case CollectionType.Highlander: + case CollectionType.Wildwood: + case CollectionType.Duskwight: + case CollectionType.Plainsfolk: + case CollectionType.Dunesfolk: + case CollectionType.SeekerOfTheSun: + case CollectionType.KeeperOfTheMoon: + case CollectionType.Seawolf: + case CollectionType.Hellsguard: + case CollectionType.Raen: + case CollectionType.Xaela: + case CollectionType.Helion: + case CollectionType.Lost: + case CollectionType.Rava: + case CollectionType.Veena: + var old = _specialCollections[ ( int )collectionType ]; + if( old != null ) + { + _specialCollections[ ( int )collectionType ] = null; + CollectionChanged.Invoke( collectionType, old, null, null ); + } + + return; + } + } // Create a new character collection. Returns false if the character name already has a collection. public bool CreateCharacterCollection( string characterName ) @@ -101,7 +219,7 @@ public partial class ModCollection } _characters[ characterName ] = Empty; - CollectionChanged.Invoke( Type.Character, null, Empty, characterName ); + CollectionChanged.Invoke( CollectionType.Character, null, Empty, characterName ); return true; } @@ -112,7 +230,7 @@ public partial class ModCollection { RemoveCache( collection.Index ); _characters.Remove( characterName ); - CollectionChanged.Invoke( Type.Character, collection, null, characterName ); + CollectionChanged.Invoke( CollectionType.Character, collection, null, characterName ); } } @@ -157,6 +275,25 @@ public partial class ModCollection Current = this[ currentIdx ]; } + // Load special collections. + foreach( var type in CollectionTypeExtensions.Special ) + { + var typeName = jObject[ type.ToString() ]?.ToObject< string >(); + if( typeName != null ) + { + var idx = GetIndexForCollectionName( typeName ); + if( idx < 0 ) + { + PluginLog.Error( $"Last choice of {type.ToName()} Collection {typeName} is not available, removed." ); + configChanged = true; + } + else + { + _specialCollections[ ( int )type ] = this[ idx ]; + } + } + } + // Load character collections. If a player name comes up multiple times, the last one is applied. var characters = jObject[ nameof( Characters ) ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); foreach( var (player, collectionName) in characters ) @@ -187,10 +324,14 @@ public partial class ModCollection public void SaveActiveCollections() { Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), - () => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ) ) ); + () => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ), + _specialCollections.WithIndex() + .Where( c => c.Item1 != null ) + .Select( c => ( ( CollectionType )c.Item2, c.Item1!.Name ) ) ) ); } - internal static void SaveActiveCollections( string def, string current, IEnumerable< (string, string) > characters ) + internal static void SaveActiveCollections( string def, string current, IEnumerable< (string, string) > characters, + IEnumerable< (CollectionType, string) > special ) { var file = ActiveCollectionFile; try @@ -204,6 +345,12 @@ public partial class ModCollection j.WriteValue( def ); j.WritePropertyName( nameof( Current ) ); j.WriteValue( current ); + foreach( var (type, collection) in special ) + { + j.WritePropertyName( type.ToString() ); + j.WriteValue( collection ); + } + j.WritePropertyName( nameof( Characters ) ); j.WriteStartObject(); foreach( var (character, collection) in characters ) @@ -246,9 +393,9 @@ public partial class ModCollection // Save if any of the active collections is changed. - private void SaveOnChange( Type type, ModCollection? _1, ModCollection? _2, string? _3 ) + private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string? _3 ) { - if( type != Type.Inactive ) + if( collectionType != CollectionType.Inactive ) { SaveActiveCollections(); } @@ -261,7 +408,7 @@ public partial class ModCollection Default.CreateCache(); Current.CreateCache(); - foreach( var collection in _characters.Values ) + foreach( var collection in _specialCollections.OfType< ModCollection >().Concat( _characters.Values ) ) { collection.CreateCache(); } @@ -269,7 +416,10 @@ public partial class ModCollection private void RemoveCache( int idx ) { - if( idx != Default.Index && idx != Current.Index && _characters.Values.All( c => c.Index != idx ) ) + if( idx != Default.Index + && idx != Current.Index + && _specialCollections.All( c => c == null || c.Index != idx ) + && _characters.Values.All( c => c.Index != idx ) ) { _collections[ idx ].ClearCache(); } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index d1c5b2a2..0715d987 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -13,20 +13,12 @@ namespace Penumbra.Collections; public partial class ModCollection { - public enum Type : byte - { - Inactive, // A collection was added or removed - Default, // The default collection was changed - Character, // A character collection was changed - Current, // The current collection was changed. - } - public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > { // On addition, oldCollection is null. On deletion, newCollection is null. - // CharacterName is onls set for type == Character. - public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection, - string? characterName = null ); + // CharacterName is only set for type == Character. + public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection, + ModCollection? newCollection, string? characterName = null ); private readonly Mod.Manager _modManager; @@ -124,8 +116,8 @@ public partial class ModCollection _collections.Add( newCollection ); newCollection.Save(); PluginLog.Debug( "Added collection {Name:l}.", newCollection.Name ); - CollectionChanged.Invoke( Type.Inactive, null, newCollection ); - SetCollection( newCollection.Index, Type.Current ); + CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); + SetCollection( newCollection.Index, CollectionType.Current ); return true; } @@ -148,17 +140,17 @@ public partial class ModCollection if( idx == Current.Index ) { - SetCollection( DefaultName, Type.Current ); + SetCollection( DefaultName, CollectionType.Current ); } if( idx == Default.Index ) { - SetCollection( Empty, Type.Default ); + SetCollection( Empty, CollectionType.Default ); } foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() ) { - SetCollection( Empty, Type.Character, characterName ); + SetCollection( Empty, CollectionType.Character, characterName ); } var collection = _collections[ idx ]; @@ -179,7 +171,7 @@ public partial class ModCollection } PluginLog.Debug( "Removed collection {Name:l}.", collection.Name ); - CollectionChanged.Invoke( Type.Inactive, collection, null ); + CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); return true; } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs new file mode 100644 index 00000000..f8fc6d48 --- /dev/null +++ b/Penumbra/Collections/CollectionType.cs @@ -0,0 +1,115 @@ +using System; +using System.Linq; +using Penumbra.GameData.Enums; + +namespace Penumbra.Collections; + +public enum CollectionType : byte +{ + // Special Collections + Yourself = 0, + PlayerCharacter, + NonPlayerCharacter, + Midlander, + Highlander, + Wildwood, + Duskwight, + Plainsfolk, + Dunesfolk, + SeekerOfTheSun, + KeeperOfTheMoon, + Seawolf, + Hellsguard, + Raen, + Xaela, + Helion, + Lost, + Rava, + Veena, + + Inactive, // A collection was added or removed + Default, // The default collection was changed + Character, // A character collection was changed + Current, // The current collection was changed. +} + +public static class CollectionTypeExtensions +{ + public static bool IsSpecial( this CollectionType collectionType ) + => collectionType is >= CollectionType.Yourself and < CollectionType.Inactive; + + public static readonly CollectionType[] Special = Enum.GetValues< CollectionType >().Where( IsSpecial ).ToArray(); + + public static string ToName( this CollectionType collectionType ) + => collectionType switch + { + CollectionType.Yourself => "Your Character", + CollectionType.PlayerCharacter => "Player Characters", + CollectionType.NonPlayerCharacter => "Non-Player Characters", + CollectionType.Midlander => SubRace.Midlander.ToName(), + CollectionType.Highlander => SubRace.Highlander.ToName(), + CollectionType.Wildwood => SubRace.Wildwood.ToName(), + CollectionType.Duskwight => SubRace.Duskwight.ToName(), + CollectionType.Plainsfolk => SubRace.Plainsfolk.ToName(), + CollectionType.Dunesfolk => SubRace.Dunesfolk.ToName(), + CollectionType.SeekerOfTheSun => SubRace.SeekerOfTheSun.ToName(), + CollectionType.KeeperOfTheMoon => SubRace.KeeperOfTheMoon.ToName(), + CollectionType.Seawolf => SubRace.Seawolf.ToName(), + CollectionType.Hellsguard => SubRace.Hellsguard.ToName(), + CollectionType.Raen => SubRace.Raen.ToName(), + CollectionType.Xaela => SubRace.Xaela.ToName(), + CollectionType.Helion => SubRace.Helion.ToName(), + CollectionType.Lost => SubRace.Lost.ToName(), + CollectionType.Rava => SubRace.Rava.ToName(), + CollectionType.Veena => SubRace.Veena.ToName(), + CollectionType.Inactive => "Collection", + CollectionType.Default => "Default", + CollectionType.Character => "Character", + CollectionType.Current => "Current", + _ => string.Empty, + }; + + public static string ToDescription( this CollectionType collectionType ) + => collectionType switch + { + CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" + + "It takes precedence before all other collections except for explicitly named character collections.", + CollectionType.PlayerCharacter => + "This collection applies to all player characters that do not have a more specific character or racial collections associated.", + CollectionType.NonPlayerCharacter => + "This collection applies to all human non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.Midlander => + "This collection applies to all player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.Highlander => + "This collection applies to all player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.Wildwood => + "This collection applies to all player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.Duskwight => + "This collection applies to all player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.Plainsfolk => + "This collection applies to all player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.Dunesfolk => + "This collection applies to all player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.SeekerOfTheSun => + "This collection applies to all player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.KeeperOfTheMoon => + "This collection applies to all player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.Seawolf => + "This collection applies to all player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.Hellsguard => + "This collection applies to all player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.Raen => + "This collection applies to all player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.Xaela => + "This collection applies to all player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.Helion => + "This collection applies to all player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.Lost => + "This collection applies to all player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.Rava => + "This collection applies to all player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.Veena => + "This collection applies to all player character Veena Viera that do not have a more specific character collection associated.", + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index dc93d966..b1714470 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -182,7 +182,7 @@ public partial class Configuration DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, - CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ) ); + CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty<(CollectionType, string)>() ); } // Collections were introduced and the previous CurrentCollection got put into ModDirectory. diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index e7f95a6d..f51d1371 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -13,6 +13,7 @@ using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.Collections; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.Resolver; @@ -320,8 +321,12 @@ public unsafe partial class PathResolver } ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); - // First check temporary character collections, then the own configuration. - return CollectionByActorName( actualName, out var c ) ? c : Penumbra.CollectionManager.Default; + // First check temporary character collections, then the own configuration, then special collections. + return CollectionByActorName( actualName, out var c ) + ? c + : CollectionByActor( actualName, gameObject, out c ) + ? c + : Penumbra.CollectionManager.Default; } catch( Exception e ) { @@ -335,11 +340,75 @@ public unsafe partial class PathResolver => Penumbra.TempMods.Collections.TryGetValue( name, out collection ) || Penumbra.CollectionManager.Characters.TryGetValue( name, out collection ); + // Check special collections given the actor. + private static bool CollectionByActor( string name, GameObject* actor, [NotNullWhen( true )] out ModCollection? collection ) + { + collection = null; + // Check for the Yourself collection. + if( actor->ObjectIndex == 0 || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) + { + collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ); + if( collection != null ) + { + return true; + } + } + + if( actor->IsCharacter() ) + { + var character = ( Character* )actor; + // Only handle human models. + if( character->ModelCharaId == 0 ) + { + // Check if the object is a non-player human NPC. + if( actor->ObjectKind != ( byte )ObjectKind.Player ) + { + collection = Penumbra.CollectionManager.ByType( CollectionType.NonPlayerCharacter ); + if( collection != null ) + { + return true; + } + } + else + { + // Check the subrace. If it does not fit any or no subrace collection is set, check the player character collection. + collection = ( SubRace )( ( Character* )actor )->CustomizeData[ 4 ] switch + { + SubRace.Midlander => Penumbra.CollectionManager.ByType( CollectionType.Midlander ), + SubRace.Highlander => Penumbra.CollectionManager.ByType( CollectionType.Highlander ), + SubRace.Wildwood => Penumbra.CollectionManager.ByType( CollectionType.Wildwood ), + SubRace.Duskwight => Penumbra.CollectionManager.ByType( CollectionType.Duskwight ), + SubRace.Plainsfolk => Penumbra.CollectionManager.ByType( CollectionType.Plainsfolk ), + SubRace.Dunesfolk => Penumbra.CollectionManager.ByType( CollectionType.Dunesfolk ), + SubRace.SeekerOfTheSun => Penumbra.CollectionManager.ByType( CollectionType.SeekerOfTheSun ), + SubRace.KeeperOfTheMoon => Penumbra.CollectionManager.ByType( CollectionType.KeeperOfTheMoon ), + SubRace.Seawolf => Penumbra.CollectionManager.ByType( CollectionType.Seawolf ), + SubRace.Hellsguard => Penumbra.CollectionManager.ByType( CollectionType.Hellsguard ), + SubRace.Raen => Penumbra.CollectionManager.ByType( CollectionType.Raen ), + SubRace.Xaela => Penumbra.CollectionManager.ByType( CollectionType.Xaela ), + SubRace.Helion => Penumbra.CollectionManager.ByType( CollectionType.Helion ), + SubRace.Lost => Penumbra.CollectionManager.ByType( CollectionType.Lost ), + SubRace.Rava => Penumbra.CollectionManager.ByType( CollectionType.Rava ), + SubRace.Veena => Penumbra.CollectionManager.ByType( CollectionType.Veena ), + _ => null, + }; + collection ??= Penumbra.CollectionManager.ByType( CollectionType.PlayerCharacter ); + if( collection != null ) + { + return true; + } + } + } + } + + return false; + } + // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( ModCollection.Type type, ModCollection? _1, ModCollection? _2, string? name ) + private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name ) { - if( type is not (ModCollection.Type.Character or ModCollection.Type.Default) ) + if( type is not (CollectionType.Character or CollectionType.Default) ) { return; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c715eba5..82c6bcf1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -324,23 +324,34 @@ public class Penumbra : IDisposable return false; } - switch( type ) + foreach( var t in Enum.GetValues< CollectionType >() ) { - case "default": - if( collection == CollectionManager.Default ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); - return false; - } + if( t is CollectionType.Inactive or CollectionType.Character + || !string.Equals( t.ToString(), type, StringComparison.OrdinalIgnoreCase ) ) + { + continue; + } - CollectionManager.SetCollection( collection, ModCollection.Type.Default ); - Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); - return true; - default: - Dalamud.Chat.Print( - "Second command argument is not default, the correct command format is: /penumbra collection default " ); + var oldCollection = CollectionManager.ByType( t ); + if( collection == oldCollection ) + { + Dalamud.Chat.Print( $"{collection.Name} already is the {t.ToName()} Collection." ); return false; + } + + if( oldCollection == null && t.IsSpecial() ) + { + CollectionManager.CreateSpecialCollection( t ); + } + + CollectionManager.SetCollection( collection, t, null ); + Dalamud.Chat.Print( $"Set {collection.Name} as {t.ToName()} Collection." ); + return true; } + + Dalamud.Chat.Print( + "Second command argument is not default, the correct command format is: /penumbra collection " ); + return false; } private void OnCommand( string command, string rawArgs ) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ab5de6b5..888a385e 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -47,7 +47,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.ModManager.ModMetaChanged += OnModMetaChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; - OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null ); + OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, null ); } public override void Dispose() @@ -354,9 +354,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod OnSelectionChange( Selected, Selected, default ); } - private void OnCollectionChange( ModCollection.Type type, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) + private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) { - if( type != ModCollection.Type.Current || oldCollection == newCollection ) + if( collectionType != CollectionType.Current || oldCollection == newCollection ) { return; } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index bc9af12f..be2ba20a 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -152,7 +152,7 @@ public partial class ConfigWindow { if( _newCurrentCollection != null ) { - Penumbra.CollectionManager.SetCollection( _newCurrentCollection, ModCollection.Type.Current ); + Penumbra.CollectionManager.SetCollection( _newCurrentCollection, CollectionType.Current ); _newCurrentCollection = null; } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index efbb6532..02e2c578 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; +using Penumbra.Util; namespace Penumbra.UI; @@ -33,9 +34,10 @@ public partial class ConfigWindow // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection = false; - private string _newCharacterName = string.Empty; + private string _newCollectionName = string.Empty; + private bool _canAddCollection = false; + private string _newCharacterName = string.Empty; + private CollectionType? _currentType = CollectionType.Yourself; // Create a new collection that is either empty or a duplicate of the current collection. // Resets the new collection name. @@ -104,7 +106,7 @@ public partial class ConfigWindow private void DrawCurrentCollectionSelector() { - DrawCollectionSelector( "##current", _window._inputTextWidth.X, ModCollection.Type.Current, false, null ); + DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( "Current Collection", "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); @@ -112,12 +114,56 @@ public partial class ConfigWindow private void DrawDefaultCollectionSelector() { - DrawCollectionSelector( "##default", _window._inputTextWidth.X, ModCollection.Type.Default, true, null ); + DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( "Default Collection", "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" ); } + // We do not check for valid character names. + private void DrawNewSpecialCollection() + { + const string description = "Special Collections apply to certain types of characters.\n" + + "All of them take precedence before the Default collection,\n" + + "but all character collections take precedence before them."; + + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + if( _currentType == null || Penumbra.CollectionManager.ByType( _currentType.Value ) != null ) + { + _currentType = CollectionTypeExtensions.Special.FindFirst( t => Penumbra.CollectionManager.ByType( t ) == null, out var t2 ) + ? t2 + : null; + } + + if( _currentType == null ) + { + return; + } + + using( var combo = ImRaii.Combo( "##NewSpecial", _currentType.Value.ToName() ) ) + { + if( combo ) + { + foreach( var type in CollectionTypeExtensions.Special.Where( t => Penumbra.CollectionManager.ByType( t ) == null ) ) + { + if( ImGui.Selectable( type.ToName(), type == _currentType.Value ) ) + { + _currentType = type; + } + } + } + } + + ImGui.SameLine(); + var disabled = _currentType == null; + var tt = disabled ? "Please select a special collection type before creating the collection.\n\n" + description : description; + if( ImGuiUtil.DrawDisabledButton( "Create New Special Collection", Vector2.Zero, tt, disabled ) ) + { + Penumbra.CollectionManager.CreateSpecialCollection( _currentType!.Value ); + _currentType = null; + } + } + // We do not check for valid character names. private void DrawNewCharacterCollection() { @@ -145,14 +191,36 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); DrawDefaultCollectionSelector(); ImGui.Dummy( _window._defaultSpace ); + foreach( var type in CollectionTypeExtensions.Special ) + { + var collection = Penumbra.CollectionManager.ByType( type ); + if( collection != null ) + { + using var id = ImRaii.PushId( ( int )type ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true, null ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, + false, true ) ) + { + Penumbra.CollectionManager.RemoveSpecialCollection( type ); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGuiUtil.LabeledHelpMarker( type.ToName(), type.ToDescription() ); + } + } + + DrawNewSpecialCollection(); + ImGui.Dummy( _window._defaultSpace ); + foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) { using var id = ImRaii.PushId( name ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, ModCollection.Type.Character, true, name ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Character, true, name ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, - true ) ) + false, true ) ) { Penumbra.CollectionManager.RemoveCharacterCollection( name ); } @@ -163,7 +231,7 @@ public partial class ConfigWindow } DrawNewCharacterCollection(); - ImGui.NewLine(); + ImGui.Dummy( _window._defaultSpace ); } } diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 92689787..c6f1727f 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -93,18 +93,13 @@ public partial class ConfigWindow } // Draw a collection selector of a certain width for a certain type. - private static void DrawCollectionSelector( string label, float width, ModCollection.Type type, bool withEmpty, string? characterName ) + private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty, + string? characterName ) { ImGui.SetNextItemWidth( width ); - var current = type switch - { - ModCollection.Type.Default => Penumbra.CollectionManager.Default, - ModCollection.Type.Character => Penumbra.CollectionManager.Character( characterName ?? string.Empty ), - ModCollection.Type.Current => Penumbra.CollectionManager.Current, - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - using var combo = ImRaii.Combo( label, current.Name ); + var current = Penumbra.CollectionManager.ByType( collectionType, characterName ); + using var combo = ImRaii.Combo( label, current?.Name ?? string.Empty ); if( combo ) { foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) @@ -112,7 +107,7 @@ public partial class ConfigWindow using var id = ImRaii.PushId( collection.Index ); if( ImGui.Selectable( collection.Name, collection == current ) ) { - Penumbra.CollectionManager.SetCollection( collection, type, characterName ); + Penumbra.CollectionManager.SetCollection( collection, collectionType, characterName ); } } } @@ -140,7 +135,7 @@ public partial class ConfigWindow } // Add Penumbra Root. This is not updated if the root changes right now. - fileManager.CustomSideBarItems.Add( ("Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0) ); + fileManager.CustomSideBarItems.Add( ( "Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0 ) ); // Remove Videos and Music. fileManager.CustomSideBarItems.Add( ( "Videos", string.Empty, 0, -1 ) ); diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index d85ff38a..e5f3b5c3 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -62,7 +62,7 @@ public partial class ConfigWindow ImGui.SameLine(); DrawInheritedCollectionButton( 3 * buttonSize ); ImGui.SameLine(); - DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); + DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false, null ); if( !Penumbra.CollectionManager.CurrentCollectionInUse ) { ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); @@ -79,7 +79,7 @@ public partial class ConfigWindow : "Set the current collection to the configured default collection."; if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) { - Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, ModCollection.Type.Current ); + Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current ); } } @@ -97,7 +97,7 @@ public partial class ConfigWindow }; if( ImGuiUtil.DrawDisabledButton( name, width, tt, noModSelected || !modInherited ) ) { - Penumbra.CollectionManager.SetCollection( collection, ModCollection.Type.Current ); + Penumbra.CollectionManager.SetCollection( collection, CollectionType.Current ); } }