diff --git a/OtterGui b/OtterGui index b92dbe60..98064e79 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b92dbe60887503a77a89aeae80729236fb2bfa10 +Subproject commit 98064e790042c90c82a58fbfa79201bd69800758 diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs index 2284bf98..6d3cc0bd 100644 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -31,6 +31,13 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > Crc64 = Functions.ComputeCrc64( InternalName.Span ); } + public FullPath( Utf8GamePath path ) + { + FullName = path.ToString().Replace( '/', '\\' ); + InternalName = path.Path; + Crc64 = Functions.ComputeCrc64( InternalName.Span ); + } + public bool Exists => File.Exists( FullName ); diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index cfc373df..257cfbde 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -96,9 +96,13 @@ public interface IPenumbraApi : IPenumbraApiBase // Queue redrawing of all currently available actors with the given RedrawType. public void RedrawAll( RedrawType setting ); - // Resolve a given gamePath via Penumbra using the Default and Forced collections. + // Resolve a given gamePath via Penumbra using the Default collection. // Returns the given gamePath if penumbra would not manipulate it. - public string ResolvePath( string gamePath ); + public string ResolveDefaultPath( string gamePath ); + + // Resolve a given gamePath via Penumbra using the Interface collection. + // Returns the given gamePath if penumbra would not manipulate it. + public string ResolveInterfacePath( string gamePath ); // Resolve a given gamePath via Penumbra using the character collection for the given name (if it exists) and the Forced collections. // Returns the given gamePath if penumbra would not manipulate it. @@ -133,6 +137,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain the name of the default collection. public string GetDefaultCollection(); + // Obtain the name of the interface collection. + public string GetInterfaceCollection(); + // Obtain the name of the collection associated with characterName and whether it is configured or inferred from default. public (string, bool) GetCharacterCollection( string characterName ); diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 133f5f7e..1fd29dec 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -311,6 +311,13 @@ public class IpcTester : IDisposable .InvokeFunc( _currentResolvePath ) ); } + DrawIntro( PenumbraIpc.LabelProviderResolveInterface, "Interface Collection Resolve" ); + if( _currentResolvePath.Length != 0 ) + { + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderResolveInterface ) + .InvokeFunc( _currentResolvePath ) ); + } + DrawIntro( PenumbraIpc.LabelProviderResolveCharacter, "Character Collection Resolve" ); if( _currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0 ) { @@ -568,6 +575,8 @@ public class IpcTester : IDisposable ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderCurrentCollectionName ).InvokeFunc() ); DrawIntro( PenumbraIpc.LabelProviderDefaultCollectionName, "Default Collection" ); ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderDefaultCollectionName ).InvokeFunc() ); + DrawIntro( PenumbraIpc.LabelProviderInterfaceCollectionName, "Interface Collection" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderInterfaceCollectionName ).InvokeFunc() ); DrawIntro( PenumbraIpc.LabelProviderCharacterCollectionName, "Character" ); ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index eb1a189b..fad36b1c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -21,7 +21,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 13 ); + => ( 4, 14 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -141,12 +141,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - public string ResolvePath( string path ) + public string ResolveDefaultPath( string path ) { CheckInitialized(); return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default ); } + public string ResolveInterfacePath( string path ) + { + CheckInitialized(); + return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Interface ); + } + public string ResolvePlayerPath( string path ) { CheckInitialized(); @@ -185,7 +191,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath ) ); + => GetFileIntern< T >( ResolveDefaultPath( gamePath ) ); public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); @@ -233,6 +239,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.CollectionManager.Default.Name; } + public string GetInterfaceCollection() + { + CheckInitialized(); + return Penumbra.CollectionManager.Interface.Name; + } + public (string, bool) GetCharacterCollection( string characterName ) { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index eb9035e1..ab9496c5 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -274,6 +274,7 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveInterface = "Penumbra.ResolveInterfacePath"; public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; @@ -285,6 +286,7 @@ public partial class PenumbraIpc public const string LabelProviderGameObjectResourcePathResolved = "Penumbra.GameObjectResourcePathResolved"; internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string >? ProviderResolveInterface; internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; internal ICallGateProvider< string, string >? ProviderResolvePlayer; internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; @@ -300,13 +302,23 @@ public partial class PenumbraIpc try { ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); - ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); + ProviderResolveDefault.RegisterFunc( Api.ResolveDefaultPath ); } catch( Exception e ) { Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); } + try + { + ProviderResolveInterface = pi.GetIpcProvider< string, string >( LabelProviderResolveInterface ); + ProviderResolveInterface.RegisterFunc( Api.ResolveInterfacePath ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveInterface}:\n{e}" ); + } + try { ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); @@ -411,6 +423,7 @@ public partial class PenumbraIpc ProviderGetDrawObjectInfo?.UnregisterFunc(); ProviderGetCutsceneParentIndex?.UnregisterFunc(); ProviderResolveDefault?.UnregisterFunc(); + ProviderResolveInterface?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); ProviderReverseResolvePath?.UnregisterFunc(); ProviderReverseResolvePathPlayer?.UnregisterFunc(); @@ -499,6 +512,7 @@ public partial class PenumbraIpc public const string LabelProviderGetCollections = "Penumbra.GetCollections"; public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderInterfaceCollectionName = "Penumbra.GetInterfaceCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; public const string LabelProviderGetPlayerMetaManipulations = "Penumbra.GetPlayerMetaManipulations"; public const string LabelProviderGetMetaManipulations = "Penumbra.GetMetaManipulations"; @@ -507,6 +521,7 @@ public partial class PenumbraIpc internal ICallGateProvider< IList< string > >? ProviderGetCollections; internal ICallGateProvider< string >? ProviderCurrentCollectionName; internal ICallGateProvider< string >? ProviderDefaultCollectionName; + internal ICallGateProvider< string >? ProviderInterfaceCollectionName; internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; internal ICallGateProvider< string >? ProviderGetPlayerMetaManipulations; internal ICallGateProvider< string, string >? ProviderGetMetaManipulations; @@ -553,6 +568,16 @@ public partial class PenumbraIpc Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderDefaultCollectionName}:\n{e}" ); } + try + { + ProviderInterfaceCollectionName = pi.GetIpcProvider( LabelProviderInterfaceCollectionName ); + ProviderInterfaceCollectionName.RegisterFunc( Api.GetInterfaceCollection ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderInterfaceCollectionName}:\n{e}" ); + } + try { ProviderCharacterCollectionName = pi.GetIpcProvider< string, (string, bool) >( LabelProviderCharacterCollectionName ); @@ -590,6 +615,7 @@ public partial class PenumbraIpc ProviderGetCollections?.UnregisterFunc(); ProviderCurrentCollectionName?.UnregisterFunc(); ProviderDefaultCollectionName?.UnregisterFunc(); + ProviderInterfaceCollectionName?.UnregisterFunc(); ProviderCharacterCollectionName?.UnregisterFunc(); ProviderGetMetaManipulations?.UnregisterFunc(); } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 6885cdee..12b42e91 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace Penumbra.Collections; @@ -26,6 +27,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 +57,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 +70,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 +103,9 @@ public partial class ModCollection Default.SetFiles(); } + break; + case CollectionType.Interface: + Interface = newCollection; break; case CollectionType.Current: Current = newCollection; @@ -118,6 +127,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 +202,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 +215,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; } @@ -260,21 +286,20 @@ public partial class ModCollection { SaveActiveCollections(); } - - CreateNecessaryCaches(); } 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 +312,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 ) @@ -335,7 +362,6 @@ public partial class ModCollection return false; } - // Save if any of the active collections is changed. private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string? _3 ) { @@ -345,23 +371,27 @@ public partial class ModCollection } } - - // Cache handling. - private void CreateNecessaryCaches() + // Cache handling. Usually recreate caches on the next framework tick, + // but at launch create all of them at once. + public void CreateNecessaryCaches() { - Default.CreateCache(); - Current.CreateCache(); + var tasks = _specialCollections.OfType< ModCollection >() + .Concat( _characters.Values ) + .Prepend( Current ) + .Prepend( Default ) + .Prepend( Interface ) + .Distinct() + .Select( c => Task.Run( c.CalculateEffectiveFileListInternal ) ) + .ToArray(); - foreach( var collection in _specialCollections.OfType< ModCollection >().Concat( _characters.Values ) ) - { - collection.CreateCache(); - } + Task.WaitAll( tasks ); } 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 ) && _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/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 351ae2fc..7f379dc5 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -133,67 +133,6 @@ public partial class ModCollection Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished." ); } - // Set Metadata files. - public void SetEqpFiles() - { - if( _cache == null ) - { - MetaManager.ResetEqpFiles(); - } - else - { - _cache.MetaManipulations.SetEqpFiles(); - } - } - - public void SetEqdpFiles() - { - if( _cache == null ) - { - MetaManager.ResetEqdpFiles(); - } - else - { - _cache.MetaManipulations.SetEqdpFiles(); - } - } - - public void SetGmpFiles() - { - if( _cache == null ) - { - MetaManager.ResetGmpFiles(); - } - else - { - _cache.MetaManipulations.SetGmpFiles(); - } - } - - public void SetEstFiles() - { - if( _cache == null ) - { - MetaManager.ResetEstFiles(); - } - else - { - _cache.MetaManipulations.SetEstFiles(); - } - } - - public void SetCmpFiles() - { - if( _cache == null ) - { - MetaManager.ResetCmpFiles(); - } - else - { - _cache.MetaManipulations.SetCmpFiles(); - } - } - public void SetFiles() { if( _cache == null ) @@ -207,6 +146,18 @@ public partial class ModCollection } } + public void SetMetaFile( Interop.Structs.CharacterUtility.Index idx ) + { + if( _cache == null ) + { + Penumbra.CharacterUtility.ResetResource( idx ); + } + else + { + _cache.MetaManipulations.SetFile( idx ); + } + } + // Used for short periods of changed files. public CharacterUtility.List.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) => _cache?.MetaManipulations.TemporarilySetEqdpFile( genderRace, accessory ); @@ -222,5 +173,4 @@ public partial class ModCollection public CharacterUtility.List.MetaReverter? TemporarilySetEstFile( EstManipulation.EstType type ) => _cache?.MetaManipulations.TemporarilySetEstFile( type ); - -} +} \ No newline at end of file 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/CharacterUtility.DecalReverter.cs b/Penumbra/Interop/CharacterUtility.DecalReverter.cs new file mode 100644 index 00000000..a439ac48 --- /dev/null +++ b/Penumbra/Interop/CharacterUtility.DecalReverter.cs @@ -0,0 +1,65 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; + +namespace Penumbra.Interop; + +public unsafe partial class CharacterUtility +{ + public sealed class DecalReverter : IDisposable + { + public static readonly Utf8GamePath DecalPath = + Utf8GamePath.FromString( "chara/common/texture/decal_equip/_stigma.tex", out var p ) ? p : Utf8GamePath.Empty; + + public static readonly Utf8GamePath TransparentPath = + Utf8GamePath.FromString( "chara/common/texture/transparent.tex", out var p ) ? p : Utf8GamePath.Empty; + + private readonly Structs.TextureResourceHandle* _decal; + private readonly Structs.TextureResourceHandle* _transparent; + + public DecalReverter( ModCollection? collection, bool doDecal ) + { + var ptr = Penumbra.CharacterUtility.Address; + _decal = null; + _transparent = null; + if( doDecal ) + { + var decalPath = collection?.ResolvePath( DecalPath )?.InternalName ?? DecalPath.Path; + var decalHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, decalPath ); + _decal = ( Structs.TextureResourceHandle* )decalHandle; + if( _decal != null ) + { + ptr->DecalTexResource = _decal; + } + } + else + { + var transparentPath = collection?.ResolvePath( TransparentPath )?.InternalName ?? TransparentPath.Path; + var transparentHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, transparentPath ); + _transparent = ( Structs.TextureResourceHandle* )transparentHandle; + if( _transparent != null ) + { + ptr->TransparentTexResource = _transparent; + } + } + } + + public void Dispose() + { + var ptr = Penumbra.CharacterUtility.Address; + if( _decal != null ) + { + ptr->DecalTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultDecalResource; + --_decal->Handle.RefCount; + } + + if( _transparent != null ) + { + ptr->TransparentTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultTransparentResource; + --_transparent->Handle.RefCount; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs index 27146af6..e6f412ae 100644 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -50,7 +50,8 @@ public unsafe partial class CharacterUtility public MetaReverter TemporarilyResetResource() { - Penumbra.Log.Verbose( $"Temporarily reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); + Penumbra.Log.Verbose( + $"Temporarily reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); var reverter = new MetaReverter( this ); _entries.AddFirst( reverter ); ResetResourceInternal(); @@ -84,6 +85,9 @@ public unsafe partial class CharacterUtility private void ResetResourceInternal() => SetResourceInternal( _defaultResourceData, _defaultResourceSize ); + private void SetResourceToDefaultCollection() + => Penumbra.CollectionManager.Default.SetMetaFile( GlobalIndex ); + public void Dispose() { if( _entries.Count > 0 ) @@ -127,14 +131,14 @@ public unsafe partial class CharacterUtility if( list.Count == 0 ) { - List.ResetResourceInternal(); + List.SetResourceToDefaultCollection(); } else { var next = list.First!.Value; if( next.Resetter ) { - List.ResetResourceInternal(); + List.SetResourceToDefaultCollection(); } else { diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index a5b155c1..44b9204e 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -26,6 +26,8 @@ public unsafe partial class CharacterUtility : IDisposable public bool Ready { get; private set; } public event Action LoadingFinished; + private IntPtr _defaultTransparentResource; + private IntPtr _defaultDecalResource; // The relevant indices depend on which meta manipulations we allow for. // The defines are set in the project configuration. @@ -76,25 +78,37 @@ public unsafe partial class CharacterUtility : IDisposable } } + if( _defaultTransparentResource == IntPtr.Zero ) + { + _defaultTransparentResource = ( IntPtr )Address->TransparentTexResource; + anyMissing |= _defaultTransparentResource == IntPtr.Zero; + } + + if( _defaultDecalResource == IntPtr.Zero ) + { + _defaultDecalResource = ( IntPtr )Address->DecalTexResource; + anyMissing |= _defaultDecalResource == IntPtr.Zero; + } + if( !anyMissing ) { - Ready = true; - LoadingFinished.Invoke(); + Ready = true; Dalamud.Framework.Update -= LoadDefaultResources; + LoadingFinished.Invoke(); } } public void SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) { - var idx = ReverseIndices[( int )resourceIdx]; - var list = _lists[idx.Value]; + var idx = ReverseIndices[ ( int )resourceIdx ]; + var list = _lists[ idx.Value ]; list.SetResource( data, length ); } public void ResetResource( Structs.CharacterUtility.Index resourceIdx ) { - var idx = ReverseIndices[( int )resourceIdx]; - var list = _lists[idx.Value]; + var idx = ReverseIndices[ ( int )resourceIdx ]; + var list = _lists[ idx.Value ]; list.ResetResource(); } @@ -119,6 +133,9 @@ public unsafe partial class CharacterUtility : IDisposable { list.Dispose(); } + + Address->TransparentTexResource = ( Structs.TextureResourceHandle* )_defaultTransparentResource; + Address->DecalTexResource = ( Structs.TextureResourceHandle* )_defaultDecalResource; } public void Dispose() diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index f0cd656b..a0ca377f 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -71,7 +71,13 @@ public unsafe partial class ResourceLoader private event Action< Utf8GamePath, ResourceType, FullPath?, object? >? PathResolved; - private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, Utf8String path ) + { + var hash = path.Crc32; + return GetResourceHandler( true, *ResourceManager, &category, &type, &hash, path.Path, null, false ); + } + + internal ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) { if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) @@ -86,7 +92,7 @@ public unsafe partial class ResourceLoader // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); - PathResolved?.Invoke( gamePath, *resourceType, resolvedPath, data ); + PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); if( resolvedPath == null ) { var retUnmodified = @@ -121,6 +127,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/Interop/Loader/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs index 2393a87d..67454300 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs @@ -26,7 +26,7 @@ public unsafe partial class ResourceLoader // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); - [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = "CheckFileStateDetour" )] + [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = nameof(CheckFileStateDetour) )] public Hook< CheckFileStatePrototype > CheckFileStateHook = null!; private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) @@ -48,7 +48,7 @@ public unsafe partial class ResourceLoader // We hook the extern functions to just return the local one if given the custom flag as last argument. public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = "LoadTexFileExternDetour" )] + [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = nameof(LoadTexFileExternDetour) )] public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) @@ -59,7 +59,7 @@ public unsafe partial class ResourceLoader public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); - [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = "LoadMdlFileExternDetour" )] + [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = nameof(LoadMdlFileExternDetour) )] public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index d6e6ec0e..d30248f9 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -7,6 +7,7 @@ using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -56,7 +57,7 @@ public unsafe partial class PathResolver { if( type == ResourceType.Tex && LastCreatedCollection.Valid - && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) + && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l' ) ) { resolveData = LastCreatedCollection; return true; @@ -135,18 +136,19 @@ public unsafe partial class PathResolver private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { - CharacterUtility.List.MetaReverter? cmp = null; + var meta = DisposableContainer.Empty; if( LastGameObject != null ) { _lastCreatedCollection = IdentifyCollection( LastGameObject ); - var modelPtr = &a; - if( _lastCreatedCollection.ModCollection != Penumbra.CollectionManager.Default ) - { - cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(); - } - + // Change the transparent or 1.0 Decal if necessary. + var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) ); + // Change the rsp parameters if necessary. + meta = new DisposableContainer( _lastCreatedCollection.ModCollection != Penumbra.CollectionManager.Default + ? _lastCreatedCollection.ModCollection.TemporarilySetCmpFile() + : null, decal ); try { + var modelPtr = &a; CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ( IntPtr )modelPtr, b, c ); } catch( Exception e ) @@ -156,7 +158,7 @@ public unsafe partial class PathResolver } var ret = _characterBaseCreateHook.Original( a, b, c, d ); - using( cmp ) + using( meta ) { if( LastGameObject != null ) { @@ -168,6 +170,11 @@ 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; + // Remove DrawObjects from the list when they are destroyed. private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index ba52182a..1121cb29 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -1,9 +1,13 @@ using System; +using System.Linq; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using Penumbra.Collections; using Penumbra.GameData.Enums; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; +using static Penumbra.GameData.Enums.GenderRace; namespace Penumbra.Interop.Resolver; @@ -77,10 +81,8 @@ public unsafe partial class PathResolver var collection = GetResolveData( drawObject ); if( collection.Valid ) { - var race = GetDrawObjectGenderRace( drawObject ); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp1 = collection.ModCollection.TemporarilySetEqdpFile( race, false ); - using var eqdp2 = collection.ModCollection.TemporarilySetEqdpFile( race, true ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); _onModelLoadCompleteHook.Original.Invoke( drawObject ); } else @@ -106,10 +108,8 @@ public unsafe partial class PathResolver var collection = GetResolveData( drawObject ); if( collection.Valid ) { - var race = GetDrawObjectGenderRace( drawObject ); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp1 = collection.ModCollection.TemporarilySetEqdpFile( race, false ); - using var eqdp2 = collection.ModCollection.TemporarilySetEqdpFile( race, true ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); _updateModelsHook.Original.Invoke( drawObject ); } else @@ -130,7 +130,7 @@ public unsafe partial class PathResolver } } - return GenderRace.Unknown; + return Unknown; } public static GenderRace GetHumanGenderRace( IntPtr human ) @@ -201,7 +201,73 @@ public unsafe partial class PathResolver _inChangeCustomize = true; var resolveData = GetResolveData( human ); using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; + using var decals = resolveData.Valid + ? new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ) + : null; return _changeCustomize.Original( human, data, skipEquipment ); } + + public static DisposableContainer ResolveEqdpData( ModCollection collection, GenderRace race, bool equipment, bool accessory ) + { + DisposableContainer Convert( params GenderRace[] races ) + { + var equipmentEnumerable = + equipment + ? races.Select( r => collection.TemporarilySetEqdpFile( r, false ) ) + : Array.Empty< IDisposable? >().AsEnumerable(); + var accessoryEnumerable = + accessory + ? races.Select( r => collection.TemporarilySetEqdpFile( r, true ) ) + : Array.Empty< IDisposable? >().AsEnumerable(); + return new DisposableContainer( equipmentEnumerable.Concat( accessoryEnumerable ) ); + } + + return race switch + { + MidlanderMale => Convert( MidlanderMale ), + HighlanderMale => Convert( MidlanderMale, HighlanderMale ), + ElezenMale => Convert( MidlanderMale, ElezenMale ), + MiqoteMale => Convert( MidlanderMale, MiqoteMale ), + RoegadynMale => Convert( MidlanderMale, RoegadynMale ), + LalafellMale => Convert( MidlanderMale, LalafellMale ), + AuRaMale => Convert( MidlanderMale, AuRaMale ), + HrothgarMale => Convert( MidlanderMale, RoegadynMale, HrothgarMale ), + VieraMale => Convert( MidlanderMale, VieraMale ), + + MidlanderFemale => Convert( MidlanderMale, MidlanderFemale ), + HighlanderFemale => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale ), + ElezenFemale => Convert( MidlanderMale, MidlanderFemale, ElezenFemale ), + MiqoteFemale => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale ), + RoegadynFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale ), + LalafellFemale => Convert( MidlanderMale, LalafellMale, LalafellFemale ), + AuRaFemale => Convert( MidlanderMale, MidlanderFemale, AuRaFemale ), + HrothgarFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale ), + VieraFemale => Convert( MidlanderMale, MidlanderFemale, VieraFemale ), + + MidlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc ), + HighlanderMaleNpc => Convert( MidlanderMale, HighlanderMale, HighlanderMaleNpc ), + ElezenMaleNpc => Convert( MidlanderMale, ElezenMale, ElezenMaleNpc ), + MiqoteMaleNpc => Convert( MidlanderMale, MiqoteMale, MiqoteMaleNpc ), + RoegadynMaleNpc => Convert( MidlanderMale, RoegadynMale, RoegadynMaleNpc ), + LalafellMaleNpc => Convert( MidlanderMale, LalafellMale, LalafellMaleNpc ), + AuRaMaleNpc => Convert( MidlanderMale, AuRaMale, AuRaMaleNpc ), + HrothgarMaleNpc => Convert( MidlanderMale, RoegadynMale, HrothgarMale, HrothgarMaleNpc ), + VieraMaleNpc => Convert( MidlanderMale, VieraMale, VieraMaleNpc ), + + MidlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MidlanderFemaleNpc ), + HighlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale, HighlanderFemaleNpc ), + ElezenFemaleNpc => Convert( MidlanderMale, MidlanderFemale, ElezenFemale, ElezenFemaleNpc ), + MiqoteFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale, MiqoteFemaleNpc ), + RoegadynFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, RoegadynFemaleNpc ), + LalafellFemaleNpc => Convert( MidlanderMale, LalafellMale, LalafellFemale, LalafellFemaleNpc ), + AuRaFemaleNpc => Convert( MidlanderMale, MidlanderFemale, AuRaFemale, AuRaFemaleNpc ), + HrothgarFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale, HrothgarFemaleNpc ), + VieraFemaleNpc => Convert( MidlanderMale, MidlanderFemale, VieraFemale, VieraFemaleNpc ), + + UnknownMaleNpc => Convert( MidlanderMale, UnknownMaleNpc ), + UnknownFemaleNpc => Convert( MidlanderMale, MidlanderFemale, UnknownFemaleNpc ), + _ => DisposableContainer.Empty, + }; + } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index b5335dc8..7841027a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -143,21 +143,15 @@ public partial class PathResolver private IntPtr ResolveMdlHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) { - CharacterUtility.List.MetaReverter? Get() + DisposableContainer Get() { if( modelType > 9 ) { - return null; - } - - var race = MetaState.GetHumanGenderRace( drawObject ); - if( race == GenderRace.Unknown ) - { - return null; + return DisposableContainer.Empty; } var data = GetResolveData( drawObject ); - return !data.Valid ? null : data.ModCollection.TemporarilySetEqdpFile( race, modelType > 4 ); + return !data.Valid ? DisposableContainer.Empty : MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace( drawObject ), modelType < 5, modelType > 4); } using var eqdp = Get(); diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 40e346c1..e491de7b 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -80,6 +80,9 @@ public unsafe struct CharacterUtility BodyEst, } + public const int IndexTransparentTex = 72; + public const int IndexDecalTex = 73; + public static readonly Index[] EqdpIndices = Enum.GetNames< Index >() .Zip( Enum.GetValues< Index >() ) .Where( n => n.First.StartsWith( "Eqdp" ) ) @@ -157,5 +160,11 @@ public unsafe struct CharacterUtility [FieldOffset( 8 + ( int )Index.HeadEst * 8 )] public ResourceHandle* HeadEstResource; + [FieldOffset( 8 + IndexTransparentTex * 8 )] + public TextureResourceHandle* TransparentTexResource; + + [FieldOffset( 8 + IndexDecalTex * 8 )] + public TextureResourceHandle* DecalTexResource; + // not included resources have no known use case. } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 6b3a8a72..c8b3522e 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -5,6 +5,22 @@ using Penumbra.GameData.Enums; namespace Penumbra.Interop.Structs; +[StructLayout( LayoutKind.Explicit )] +public unsafe struct TextureResourceHandle +{ + [FieldOffset( 0x0 )] + public ResourceHandle Handle; + + [FieldOffset( 0x38 )] + public IntPtr Unk; + + [FieldOffset( 0x118 )] + public IntPtr KernelTexture; + + [FieldOffset( 0x20 )] + public IntPtr NewKernelTexture; +} + [StructLayout( LayoutKind.Explicit )] public unsafe struct ResourceHandle { diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 701b77c0..d440e106 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using OtterGui; using Penumbra.Collections; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; @@ -83,17 +84,14 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM } _manipulations[ manip ] = mod; - // Imc manipulations do not require character utility. - if( manip.ManipulationType == MetaManipulation.Type.Imc ) - { - return ApplyMod( manip.Imc ); - } if( !Penumbra.CharacterUtility.Ready ) { return true; } + // Imc manipulations do not require character utility, + // but they do require the file space to be ready. return manip.ManipulationType switch { MetaManipulation.Type.Eqp => ApplyMod( manip.Eqp ), @@ -101,6 +99,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), MetaManipulation.Type.Est => ApplyMod( manip.Est ), MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), + MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; @@ -109,17 +108,13 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public bool RevertMod( MetaManipulation manip ) { var ret = _manipulations.Remove( manip ); - // Imc manipulations do not require character utility. - if( manip.ManipulationType == MetaManipulation.Type.Imc ) - { - return RevertMod( manip.Imc ); - } - if( !Penumbra.CharacterUtility.Ready ) { return ret; } + // Imc manipulations do not require character utility, + // but they do require the file space to be ready. return manip.ManipulationType switch { MetaManipulation.Type.Eqp => RevertMod( manip.Eqp ), @@ -127,6 +122,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => RevertMod( manip.Eqdp ), MetaManipulation.Type.Est => RevertMod( manip.Est ), MetaManipulation.Type.Rsp => RevertMod( manip.Rsp ), + MetaManipulation.Type.Imc => RevertMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; @@ -150,6 +146,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), MetaManipulation.Type.Est => ApplyMod( manip.Est ), MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), + MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, } @@ -167,6 +164,42 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM Penumbra.Log.Debug( $"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations." ); } + public void SetFile( CharacterUtility.Index index ) + { + switch( index ) + { + case CharacterUtility.Index.Eqp: + SetFile( _eqpFile, index ); + break; + case CharacterUtility.Index.Gmp: + SetFile( _gmpFile, index ); + break; + case CharacterUtility.Index.HumanCmp: + SetFile( _cmpFile, index ); + break; + case CharacterUtility.Index.FaceEst: + SetFile( _estFaceFile, index ); + break; + case CharacterUtility.Index.HairEst: + SetFile( _estHairFile, index ); + break; + case CharacterUtility.Index.HeadEst: + SetFile( _estHeadFile, index ); + break; + case CharacterUtility.Index.BodyEst: + SetFile( _estBodyFile, index ); + break; + default: + var i = CharacterUtility.EqdpIndices.IndexOf( index ); + if( i != -1 ) + { + SetFile( _eqdpFiles[ i ], index ); + } + + break; + } + } + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private static unsafe void SetFile( MetaBaseFile? file, CharacterUtility.Index index ) { diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 0f9ef463..b0a136af 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -273,7 +273,7 @@ public partial class Mod try { var mod = new Mod( modDirectory ); - mod.Reload( out _ ); + mod.Reload( true, out _ ); var editor = new Editor( mod, mod.Default ); editor.DuplicatesFinished = false; editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() ); diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 4263b83f..28cb2906 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -272,7 +272,7 @@ public partial class Mod if( deletions > 0 ) { - _mod.Reload( out _ ); + _mod.Reload( false, out _ ); UpdateFiles(); } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 0d908866..a169cecc 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -60,7 +60,7 @@ public partial class Mod dir.Refresh(); mod.ModPath = dir; - if( !mod.Reload( out var metaChange ) ) + if( !mod.Reload( false, out var metaChange ) ) { Penumbra.Log.Error( $"Error reloading moved mod {mod.Name}." ); return; @@ -81,7 +81,7 @@ public partial class Mod var oldName = mod.Name; ModPathChanged.Invoke( ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); - if( !mod.Reload( out var metaChange ) ) + if( !mod.Reload( true, out var metaChange ) ) { Penumbra.Log.Warning( mod.Name.Length == 0 ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." @@ -135,7 +135,7 @@ public partial class Mod return; } - var mod = LoadMod( modFolder ); + var mod = LoadMod( modFolder, true ); if( mod == null ) { return; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 2bf658c2..7ad2140f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -84,7 +84,7 @@ public sealed partial class Mod { foreach( var modFolder in BasePath.EnumerateDirectories() ) { - var mod = LoadMod( modFolder ); + var mod = LoadMod( modFolder, false ); if( mod == null ) { continue; diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 2456dec2..22760adf 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; namespace Penumbra.Mods; @@ -26,7 +27,7 @@ public partial class Mod _default = new SubMod( this ); } - private static Mod? LoadMod( DirectoryInfo modPath ) + private static Mod? LoadMod( DirectoryInfo modPath, bool incorporateMetaChanges ) { modPath.Refresh(); if( !modPath.Exists ) @@ -36,7 +37,7 @@ public partial class Mod } var mod = new Mod( modPath ); - if( !mod.Reload( out _ ) ) + if( !mod.Reload( incorporateMetaChanges, out _ ) ) { // Can not be base path not existing because that is checked before. Penumbra.Log.Error( $"Mod at {modPath} without name is not supported." ); @@ -46,7 +47,7 @@ public partial class Mod return mod; } - private bool Reload( out MetaChangeType metaChange ) + private bool Reload( bool incorporateMetaChanges, out MetaChangeType metaChange ) { metaChange = MetaChangeType.Deletion; ModPath.Refresh(); @@ -63,8 +64,23 @@ public partial class Mod LoadDefaultOption(); LoadAllGroups(); + if( incorporateMetaChanges ) + { + IncorporateAllMetaChanges( true ); + } + ComputeChangedItems(); SetCounts(); return true; } + + // Convert all .meta and .rgsp files to their respective meta changes and add them to their options. + // Deletes the source files if delete is true. + private void IncorporateAllMetaChanges( bool delete ) + { + foreach( var subMod in AllSubMods.OfType< SubMod >() ) + { + subMod.IncorporateMetaChanges( ModPath, delete ); + } + } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index f56f9572..de120796 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -126,7 +126,7 @@ public partial class Mod internal static void CreateDefaultFiles( DirectoryInfo directory ) { var mod = new Mod( directory ); - mod.Reload( out _ ); + mod.Reload( false, out _ ); foreach( var file in mod.FindUnusedFiles() ) { if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 62e70d7a..b8ae544d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -94,6 +94,7 @@ public class Penumbra : IDalamudPlugin ModManager = new Mod.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); + CollectionManager.CreateNecessaryCaches(); ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -276,6 +277,7 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + ShutdownWebServer(); DisposeInterface(); Ipc?.Dispose(); Api?.Dispose(); @@ -289,8 +291,6 @@ public class Penumbra : IDalamudPlugin ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); CharacterUtility?.Dispose(); - - ShutdownWebServer(); } public static bool SetCollection( string type, string collectionName ) @@ -481,6 +481,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.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index d4c6cb04..013e7357 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -18,16 +18,29 @@ public partial class ConfigWindow Add5_7_0( ret ); Add5_7_1( ret ); + Add5_8_0( ret ); return ret; } private static void Add5_8_0( Changelog log ) => log.NextVersion( "Version 0.5.8.0" ) - .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) + .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) + .RegisterEntry( "Added an Interface Collection assignment." ) + .RegisterEntry( "All your UI mods will have to be in the interface collection.", 1 ) + .RegisterEntry( "Files that are categorized as UI files by the game will only check for redirections in this collection.", 1 ) + .RegisterHighlight( + "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1 ) + .RegisterEntry( "New API / IPC for the Interface Collection added.", 1 ) + .RegisterHighlight( "API / IPC consumers should verify whether they need to change resolving to the new collection.", 1 ) + .RegisterEntry( + "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it." ) + .RegisterEntry( "Collection Selectors now display None at the top if available." ) .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) .RegisterEntry( "Fixed issues with and improved mod option editing." ) - .RegisterEntry( "Backend optimizations." ); + .RegisterEntry( "Backend optimizations." ) + .RegisterEntry( "Changed metadata change system again.", 1 ) + .RegisterEntry( "Improved logging efficiency.", 1 ); private static void Add5_7_1( Changelog log ) => log.NextVersion( "Version 0.5.7.1" ) 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.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index c4141b73..5b302ade 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -100,7 +100,10 @@ public partial class ConfigWindow 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 ) ) + var enumerator = Penumbra.CollectionManager.OrderBy( c => c.Name ).AsEnumerable(); + if( withEmpty ) + enumerator = enumerator.Prepend( ModCollection.Empty ); + foreach( var collection in enumerator ) { using var id = ImRaii.PushId( collection.Index ); if( ImGui.Selectable( collection.Name, collection == current ) ) diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index c3288214..2a0e193e 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -7,6 +7,8 @@ using Penumbra.UI.Classes; using System; using System.Linq; using System.Numerics; +using Dalamud.Interface; +using Penumbra.GameData.Enums; namespace Penumbra.UI; @@ -33,11 +35,22 @@ public partial class ConfigWindow using var group = ImRaii.Group(); DrawHeaderLine(); - using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar ); - if( child ) + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + + using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, -ImGui.GetFrameHeight() ), true, + ImGuiWindowFlags.HorizontalScrollbar ) ) { - _modPanel.Draw( _selector ); + style.Pop(); + if( child ) + { + _modPanel.Draw( _selector ); + } + + style.Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); } + + style.Push( ImGuiStyleVar.FrameRounding, 0 ); + DrawRedrawLine(); } catch( Exception e ) { @@ -48,18 +61,69 @@ public partial class ConfigWindow + $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select(c => c.AnonymizedName) )} Inheritances\n" + + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select( c => c.AnonymizedName ) )} Inheritances\n" + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n" ); } } + private void DrawRedrawLine() + { + var frameHeight = new Vector2( 0, ImGui.GetFrameHeight() ); + var frameColor = ImGui.GetColorU32( ImGuiCol.FrameBg ); + using( var _ = ImRaii.Group() ) + { + using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) + { + ImGuiUtil.DrawTextButton( FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor ); + ImGui.SameLine(); + } + + ImGuiUtil.DrawTextButton( "Redraw: ", frameHeight, frameColor ); + } + + var hovered = ImGui.IsItemHovered(); + OpenTutorial( BasicTutorialSteps.Redrawing ); + if( hovered ) + { + ImGui.SetTooltip( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); + } + + void DrawButton( Vector2 size, string label, string lower ) + { + if( ImGui.Button( label, size ) ) + { + if( lower.Length > 0 ) + { + _penumbra.ObjectReloader.RedrawObject( lower, RedrawType.Redraw ); + } + else + { + _penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + } + + ImGuiUtil.HoverTooltip( lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'." ); + } + + using var disabled = ImRaii.Disabled( Dalamud.ClientState.LocalPlayer == null ); + ImGui.SameLine(); + var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; + DrawButton( buttonWidth, "All", string.Empty ); + ImGui.SameLine(); + DrawButton( buttonWidth, "Self", "self" ); + ImGui.SameLine(); + DrawButton( buttonWidth, "Target", "target" ); + ImGui.SameLine(); + DrawButton( frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus" ); + } + // Draw the header line that can quick switch between collections. private void DrawHeaderLine() { using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); - using( var group = ImRaii.Group() ) + using( var _ = ImRaii.Group() ) { DrawDefaultCollectionButton( 3 * buttonSize ); ImGui.SameLine(); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 2cbd2c24..10f5e1b3 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -11,6 +11,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.GameData.Enums; using Penumbra.UI.Classes; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index ef75566d..04fa3513 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"; @@ -18,6 +19,13 @@ public partial class ConfigWindow public const string ConditionalIndividual = "Character"; public const string IndividualAssignments = "Individual Assignments"; + public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" + + " - '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 over\n" + + " - any specific actor name to redraw all actors of that exactly matching name."; + private static void UpdateTutorialStep() { var tutorial = Tutorial.CurrentEnabledId( Penumbra.Config.TutorialStep ); @@ -49,6 +57,7 @@ public partial class ConfigWindow Inheritance, ActiveCollections, DefaultCollection, + InterfaceCollection, SpecialCollections1, SpecialCollections2, Mods, @@ -56,6 +65,7 @@ public partial class ConfigWindow AdvancedHelp, ModFilters, CollectionSelectors, + Redrawing, EnablingMods, Priority, ModOptions, @@ -100,11 +110,14 @@ 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" - + $"{IndividualAssignments} always take precedence before groups.") + + $"{IndividualAssignments} always take precedence before groups." ) .Register( IndividualAssignments, "Collections assigned here are used only for individual characters or NPCs that have the specified name.\n\n" + "They may also apply to objects 'owned' by those characters, e.g. minions or mounts - see the general settings for options on this.\n\n" ) @@ -121,6 +134,10 @@ public partial class ConfigWindow + $"The first button sets it to your {DefaultCollection} (if any).\n\n" + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n" + "The third is a regular collection selector to let you choose among all your collections." ) + .Register( "Redrawing", + "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" + + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" + + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too." ) .Register( "Initial Setup, Step 11: Enabling Mods", "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n" + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance." )