From ea023ebb5cd00eb06d74ab8e482437c65b56662d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 19 Sep 2022 18:52:49 +0200 Subject: [PATCH] Add handling for the 1.0 Decal texture. --- Penumbra.GameData/ByteString/FullPath.cs | 7 ++ .../Interop/CharacterUtility.DecalReverter.cs | 65 +++++++++++++++++++ Penumbra/Interop/CharacterUtility.cs | 25 +++++-- .../Loader/ResourceLoader.Replacement.cs | 10 ++- .../Interop/Loader/ResourceLoader.TexMdl.cs | 6 +- .../Resolver/PathResolver.DrawObjectState.cs | 23 ++++--- .../Interop/Resolver/PathResolver.Meta.cs | 3 + Penumbra/Interop/Structs/CharacterUtility.cs | 9 +++ Penumbra/Interop/Structs/ResourceHandle.cs | 16 +++++ 9 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 Penumbra/Interop/CharacterUtility.DecalReverter.cs 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/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.cs b/Penumbra/Interop/CharacterUtility.cs index 52e842c9..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,6 +78,18 @@ 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; @@ -86,15 +100,15 @@ public unsafe partial class CharacterUtility : IDisposable 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 3732dd9a..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 = 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 9c71a121..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; @@ -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 23d7e21c..ba0c453b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -201,6 +201,9 @@ 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 ); } 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 {