Merge branch 'master' into Textures

This commit is contained in:
Ottermandias 2022-09-20 21:11:54 +02:00
commit aeb2e9facd
35 changed files with 581 additions and 166 deletions

@ -1 +1 @@
Subproject commit b92dbe60887503a77a89aeae80729236fb2bfa10
Subproject commit 98064e790042c90c82a58fbfa79201bd69800758

View file

@ -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 );

View file

@ -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 );

View file

@ -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 );

View file

@ -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();

View file

@ -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<string>( 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();
}

View file

@ -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 ) )

View file

@ -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,

View file

@ -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 );
}

View file

@ -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) >() );
}

View file

@ -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;

View file

@ -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;
}
}
}
}

View file

@ -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
{

View file

@ -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()

View file

@ -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() )

View file

@ -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 )

View file

@ -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 );

View file

@ -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,
};
}
}
}

View file

@ -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();

View file

@ -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.
}

View file

@ -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
{

View file

@ -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 )
{

View file

@ -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() );

View file

@ -272,7 +272,7 @@ public partial class Mod
if( deletions > 0 )
{
_mod.Reload( out _ );
_mod.Reload( false, out _ );
UpdateFiles();
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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 );
}
}
}

View file

@ -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 ) )

View file

@ -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 )
{

View file

@ -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" )

View file

@ -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();

View file

@ -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 ) )

View file

@ -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();

View file

@ -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;

View file

@ -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 '<me>': your own character\n"
+ " - 'target' or '<t>': your target\n"
+ " - 'focus' or '<f>: your focus target\n"
+ " - 'mouseover' or '<mo>': 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." )