diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 6885cdee..7a8bb658 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -26,6 +26,9 @@ public partial class ModCollection // The collection used for general file redirections and all characters not specifically named. public ModCollection Default { get; private set; } = Empty; + // The collection used for all files categorized as UI files. + public ModCollection Interface { get; private set; } = Empty; + // A single collection that can not be deleted as a fallback for the current collection. private ModCollection DefaultName { get; set; } = Empty; @@ -53,6 +56,7 @@ public partial class ModCollection return type switch { CollectionType.Default => Default, + CollectionType.Interface => Interface, 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, @@ -65,8 +69,9 @@ public partial class ModCollection { var oldCollectionIdx = collectionType switch { - CollectionType.Default => Default.Index, - CollectionType.Current => Current.Index, + CollectionType.Default => Default.Index, + CollectionType.Interface => Interface.Index, + CollectionType.Current => Current.Index, CollectionType.Character => characterName?.Length > 0 ? _characters.TryGetValue( characterName, out var c ) ? c.Index @@ -97,6 +102,9 @@ public partial class ModCollection Default.SetFiles(); } + break; + case CollectionType.Interface: + Interface = newCollection; break; case CollectionType.Current: Current = newCollection; @@ -118,6 +126,7 @@ public partial class ModCollection private void UpdateCurrentCollectionInUse() => CurrentCollectionInUse = _specialCollections .OfType< ModCollection >() + .Prepend( Interface ) .Prepend( Default ) .Concat( Characters.Values ) .SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); @@ -192,7 +201,7 @@ public partial class ModCollection var configChanged = !ReadActiveCollections( out var jObject ); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); + var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? Empty.Name : DefaultCollection ); var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { @@ -205,12 +214,28 @@ public partial class ModCollection Default = this[ defaultIdx ]; } + // Load the interface collection. + var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? (configChanged ? Empty.Name : Default.Name); + var interfaceIdx = GetIndexForCollectionName( interfaceName ); + if( interfaceIdx < 0 ) + { + Penumbra.Log.Error( + $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}." ); + Interface = Empty; + configChanged = true; + } + else + { + Interface = this[ interfaceIdx ]; + } + // Load the current collection. var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) { - Penumbra.Log.Error( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); + Penumbra.Log.Error( + $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); Current = DefaultName; configChanged = true; } @@ -268,13 +293,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, Interface.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 ui, string current, IEnumerable< (string, string) > characters, IEnumerable< (CollectionType, string) > special ) { var file = ActiveCollectionFile; @@ -287,6 +313,8 @@ public partial class ModCollection j.WriteStartObject(); j.WritePropertyName( nameof( Default ) ); j.WriteValue( def ); + j.WritePropertyName( nameof( Interface ) ); + j.WriteValue( ui ); j.WritePropertyName( nameof( Current ) ); j.WriteValue( current ); foreach( var (type, collection) in special ) @@ -350,6 +378,7 @@ public partial class ModCollection private void CreateNecessaryCaches() { Default.CreateCache(); + Interface.CreateCache(); Current.CreateCache(); foreach( var collection in _specialCollections.OfType< ModCollection >().Concat( _characters.Values ) ) @@ -362,6 +391,7 @@ public partial class ModCollection { if( idx != Empty.Index && idx != Default.Index + && idx != Interface.Index && idx != Current.Index && _specialCollections.All( c => c == null || c.Index != idx ) && _characters.Values.All( c => c.Index != idx ) ) diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 2946498d..e71c0433 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -45,8 +45,9 @@ public enum CollectionType : byte Inactive, // A collection was added or removed Default, // The default collection was changed + Interface, // The ui collection was changed Character, // A character collection was changed - Current, // The current collection was changed. + Current, // The current collection was changed } public static class CollectionTypeExtensions @@ -96,6 +97,7 @@ public static class CollectionTypeExtensions CollectionType.VeenaNpc => SubRace.Veena.ToName() + " (NPC)", CollectionType.Inactive => "Collection", CollectionType.Default => "Default", + CollectionType.Interface => "Interface", CollectionType.Character => "Character", CollectionType.Current => "Current", _ => string.Empty, diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index cd11594b..2a2335c3 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -47,12 +47,35 @@ public partial class Configuration m.Version2To3(); m.Version3To4(); m.Version4To5(); + m.Version5To6(); + } + + // 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; } @@ -189,7 +212,7 @@ public partial class Configuration CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; - ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, + ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, DefaultCollection, CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() ); } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 65eeb5db..bd4231b9 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -142,7 +142,7 @@ public partial class Configuration : IPluginConfiguration // Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 5; + public const int CurrentVersion = 6; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index f0cd656b..3732dd9a 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -121,6 +121,12 @@ public unsafe partial class ResourceLoader } path = path.ToLower(); + if( category == ResourceCategory.Ui ) + { + var resolved = Penumbra.CollectionManager.Interface.ResolvePath( path ); + return ( resolved, Penumbra.CollectionManager.Interface.ToResolveData() ); + } + if( ResolvePathCustomization != null ) { foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 62e70d7a..fa5b3fe7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -276,6 +276,7 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + ShutdownWebServer(); DisposeInterface(); Ipc?.Dispose(); Api?.Dispose(); @@ -289,8 +290,6 @@ public class Penumbra : IDalamudPlugin ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); CharacterUtility?.Dispose(); - - ShutdownWebServer(); } public static bool SetCollection( string type, string collectionName ) @@ -481,6 +480,7 @@ public class Penumbra : IDalamudPlugin sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 ); sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); sb.AppendFormat( "> **`Base Collection: `** {0}\n", CollectionManager.Default.AnonymizedName ); + sb.AppendFormat( "> **`Interface Collection: `** {0}\n", CollectionManager.Interface.AnonymizedName ); sb.AppendFormat( "> **`Selected Collection: `** {0}\n", CollectionManager.Current.AnonymizedName ); foreach( var type in CollectionTypeExtensions.Special ) { diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 9c4a4547..ffd7adf3 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -129,10 +129,19 @@ public partial class ConfigWindow DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( DefaultCollection, - $"Mods in the {DefaultCollection} are loaded for anything that is not associated with a character in the game " + $"Mods in the {DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," + "as well as any character for whom no more specific conditions from below apply." ); } + private void DrawInterfaceCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true, null ); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( InterfaceCollection, + $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); + } + // We do not check for valid character names. private void DrawNewSpecialCollection() { @@ -272,6 +281,8 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); DrawDefaultCollectionSelector(); OpenTutorial( BasicTutorialSteps.DefaultCollection ); + DrawInterfaceCollectionSelector(); + OpenTutorial( BasicTutorialSteps.InterfaceCollection ); ImGui.Dummy( _window._defaultSpace ); DrawSpecialAssignments(); diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index f4973eb8..97941f7b 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -10,6 +10,7 @@ public partial class ConfigWindow { public const string SelectedCollection = "Selected Collection"; public const string DefaultCollection = "Base Collection"; + public const string InterfaceCollection = "Interface Collection"; public const string ActiveCollections = "Active Collections"; public const string AssignedCollections = "Assigned Collections"; public const string GroupAssignment = "Group Assignment"; @@ -21,7 +22,7 @@ public partial class ConfigWindow public const string SupportedRedrawModifiers = " - 'self' or '': your own character\n" + " - 'target' or '': your target\n" + " - 'focus' or ': your focus target\n" - + " - 'mouseover' or '': the actor you are currently hovering\n" + + " - 'mouseover' or '': the actor you are currently hovering over\n" + " - any specific actor name to redraw all actors of that exactly matching name."; private static void UpdateTutorialStep() @@ -56,6 +57,7 @@ public partial class ConfigWindow Inheritance, ActiveCollections, DefaultCollection, + InterfaceCollection, SpecialCollections1, SpecialCollections2, Mods, @@ -111,7 +113,10 @@ public partial class ConfigWindow .Register( $"Initial Setup, Step 7: {DefaultCollection}", $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" - + "This is also the collection you need to use for all UI mods, music mods or any mods not associated with a character in the game at all." ) + + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods." ) + .Register( "Interface Collection", + $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" + + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one.") .Register( GroupAssignment + 's', "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n"