Rework Interop/Loader Services.

This commit is contained in:
Ottermandias 2023-03-16 15:15:42 +01:00
parent 99fd4b7806
commit 0df12a34cb
32 changed files with 1137 additions and 1421 deletions

@ -1 +1 @@
Subproject commit 3d346700e8800c045aa19d70d516d8a4fda2f2ee Subproject commit df1cd8b02d729b2e7f585c301105b37c70d81c3e

@ -1 +1 @@
Subproject commit 2f999713c5b692fead3fb28c39002d1cd82c9261 Subproject commit 81f384cf96a9257b1ee2c7019772f30df78ba417

View file

@ -4,10 +4,12 @@ using Penumbra.Meta.Manager;
using Penumbra.Mods; using Penumbra.Mods;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Penumbra.Interop; using Penumbra.Interop;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -28,10 +30,10 @@ public partial class ModCollection
// Only create, do not update. // Only create, do not update.
private void CreateCache() private void CreateCache()
{ {
if( _cache == null ) if (_cache == null)
{ {
CalculateEffectiveFileList(); CalculateEffectiveFileList();
Penumbra.Log.Verbose( $"Created new cache for collection {Name}." ); Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
} }
} }
@ -40,21 +42,17 @@ public partial class ModCollection
=> CalculateEffectiveFileList(); => CalculateEffectiveFileList();
// Handle temporary mods for this collection. // Handle temporary mods for this collection.
public void Apply( Mod.TemporaryMod tempMod, bool created ) public void Apply(Mod.TemporaryMod tempMod, bool created)
{ {
if( created ) if (created)
{ _cache?.AddMod(tempMod, tempMod.TotalManipulations > 0);
_cache?.AddMod( tempMod, tempMod.TotalManipulations > 0 );
}
else else
{ _cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0);
_cache?.ReloadMod( tempMod, tempMod.TotalManipulations > 0 );
}
} }
public void Remove( Mod.TemporaryMod tempMod ) public void Remove(Mod.TemporaryMod tempMod)
{ {
_cache?.RemoveMod( tempMod, tempMod.TotalManipulations > 0 ); _cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0);
} }
@ -63,123 +61,122 @@ public partial class ModCollection
{ {
_cache?.Dispose(); _cache?.Dispose();
_cache = null; _cache = null;
Penumbra.Log.Verbose( $"Cleared cache of collection {Name}." ); Penumbra.Log.Verbose($"Cleared cache of collection {Name}.");
} }
public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath path ) public IEnumerable<Utf8GamePath> ReverseResolvePath(FullPath path)
=> _cache?.ReverseResolvePath( path ) ?? Array.Empty< Utf8GamePath >(); => _cache?.ReverseResolvePath(path) ?? Array.Empty<Utf8GamePath>();
public HashSet< Utf8GamePath >[] ReverseResolvePaths( string[] paths ) public HashSet<Utf8GamePath>[] ReverseResolvePaths(string[] paths)
=> _cache?.ReverseResolvePaths( paths ) ?? paths.Select( _ => new HashSet< Utf8GamePath >() ).ToArray(); => _cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet<Utf8GamePath>()).ToArray();
public FullPath? ResolvePath( Utf8GamePath path ) public FullPath? ResolvePath(Utf8GamePath path)
=> _cache?.ResolvePath( path ); => _cache?.ResolvePath(path);
// Force a file to be resolved to a specific path regardless of conflicts. // Force a file to be resolved to a specific path regardless of conflicts.
internal void ForceFile( Utf8GamePath path, FullPath fullPath ) internal void ForceFile(Utf8GamePath path, FullPath fullPath)
{ {
if( CheckFullPath( path, fullPath ) ) if (CheckFullPath(path, fullPath))
{ _cache!.ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath);
_cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath );
}
} }
[MethodImpl( MethodImplOptions.AggressiveInlining )] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CheckFullPath( Utf8GamePath path, FullPath fullPath ) private static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
{ {
if( fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength ) if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
{
return true; return true;
}
Penumbra.Log.Error( $"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}" ); Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}");
return false; return false;
} }
// Force a file resolve to be removed. // Force a file resolve to be removed.
internal void RemoveFile( Utf8GamePath path ) internal void RemoveFile(Utf8GamePath path)
=> _cache!.ResolvedFiles.Remove( path ); => _cache!.ResolvedFiles.Remove(path);
// Obtain data from the cache. // Obtain data from the cache.
internal MetaManager? MetaCache internal MetaManager? MetaCache
=> _cache?.MetaManipulations; => _cache?.MetaManipulations;
internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
=> _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >(); {
if (_cache != null)
return _cache.MetaManipulations.GetImcFile(path, out file);
internal IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems file = null;
=> _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< IMod >, object?) >(); return false;
}
internal IEnumerable< SingleArray< ModConflicts > > AllConflicts internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >(); => _cache?.ResolvedFiles ?? new Dictionary<Utf8GamePath, ModPath>();
internal SingleArray< ModConflicts > Conflicts( Mod mod ) internal IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
=> _cache?.Conflicts( mod ) ?? new SingleArray< ModConflicts >(); => _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, object?)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();
internal SingleArray<ModConflicts> Conflicts(Mod mod)
=> _cache?.Conflicts(mod) ?? new SingleArray<ModConflicts>();
// Update the effective file list for the given cache. // Update the effective file list for the given cache.
// Creates a cache if necessary. // Creates a cache if necessary.
public void CalculateEffectiveFileList() public void CalculateEffectiveFileList()
=> Penumbra.Framework.RegisterImportant( nameof( CalculateEffectiveFileList ) + Name, => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name,
CalculateEffectiveFileListInternal ); CalculateEffectiveFileListInternal);
private void CalculateEffectiveFileListInternal() private void CalculateEffectiveFileListInternal()
{ {
// Skip the empty collection. // Skip the empty collection.
if( Index == 0 ) if (Index == 0)
{
return; return;
}
Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}" ); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}");
_cache ??= new Cache( this ); _cache ??= new Cache(this);
_cache.FullRecalculation(); _cache.FullRecalculation();
Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished." ); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished.");
} }
public void SetFiles() public void SetFiles()
{ {
if( _cache == null ) if (_cache == null)
{ {
Penumbra.CharacterUtility.ResetAll(); Penumbra.CharacterUtility.ResetAll();
} }
else else
{ {
_cache.MetaManipulations.SetFiles(); _cache.MetaManipulations.SetFiles();
Penumbra.Log.Debug( $"Set CharacterUtility resources for collection {Name}." ); Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}.");
} }
} }
public void SetMetaFile( Interop.Structs.CharacterUtility.Index idx ) public void SetMetaFile(Interop.Structs.CharacterUtility.Index idx)
{ {
if( _cache == null ) if (_cache == null)
{ Penumbra.CharacterUtility.ResetResource(idx);
Penumbra.CharacterUtility.ResetResource( idx );
}
else else
{ _cache.MetaManipulations.SetFile(idx);
_cache.MetaManipulations.SetFile( idx );
}
} }
// Used for short periods of changed files. // Used for short periods of changed files.
public CharacterUtility.List.MetaReverter TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) public CharacterUtility.List.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _cache?.MetaManipulations.TemporarilySetEqdpFile( genderRace, accessory ) => _cache?.MetaManipulations.TemporarilySetEqdpFile(genderRace, accessory)
?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.EqdpIdx( genderRace, accessory ) ); ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.EqdpIdx(genderRace, accessory));
public CharacterUtility.List.MetaReverter TemporarilySetEqpFile() public CharacterUtility.List.MetaReverter TemporarilySetEqpFile()
=> _cache?.MetaManipulations.TemporarilySetEqpFile() => _cache?.MetaManipulations.TemporarilySetEqpFile()
?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.Eqp ); ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.Eqp);
public CharacterUtility.List.MetaReverter TemporarilySetGmpFile() public CharacterUtility.List.MetaReverter TemporarilySetGmpFile()
=> _cache?.MetaManipulations.TemporarilySetGmpFile() => _cache?.MetaManipulations.TemporarilySetGmpFile()
?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.Gmp ); ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.Gmp);
public CharacterUtility.List.MetaReverter TemporarilySetCmpFile() public CharacterUtility.List.MetaReverter TemporarilySetCmpFile()
=> _cache?.MetaManipulations.TemporarilySetCmpFile() => _cache?.MetaManipulations.TemporarilySetCmpFile()
?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.HumanCmp ); ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.HumanCmp);
public CharacterUtility.List.MetaReverter TemporarilySetEstFile( EstManipulation.EstType type ) public CharacterUtility.List.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type)
=> _cache?.MetaManipulations.TemporarilySetEstFile( type ) => _cache?.MetaManipulations.TemporarilySetEstFile(type)
?? Penumbra.CharacterUtility.TemporarilyResetResource( ( Interop.Structs.CharacterUtility.Index )type ); ?? Penumbra.CharacterUtility.TemporarilyResetResource((Interop.Structs.CharacterUtility.Index)type);
} }

View file

@ -57,10 +57,11 @@ public class Configuration : IPluginConfiguration
public int TutorialStep { get; set; } = 0; public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false; public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty; public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false; public bool EnableResourceWatcher { get; set; } = false;
public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; public bool OnlyAddMatchingResources { get; set; } = true;
public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;

View file

@ -2,6 +2,7 @@ using System;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Interop; namespace Penumbra.Interop;
@ -11,15 +12,15 @@ public unsafe partial class CharacterUtility
public sealed class DecalReverter : IDisposable public sealed class DecalReverter : IDisposable
{ {
public static readonly Utf8GamePath DecalPath = public static readonly Utf8GamePath DecalPath =
Utf8GamePath.FromString( "chara/common/texture/decal_equip/_stigma.tex", out var p ) ? p : Utf8GamePath.Empty; Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, out var p) ? p : Utf8GamePath.Empty;
public static readonly Utf8GamePath TransparentPath = public static readonly Utf8GamePath TransparentPath =
Utf8GamePath.FromString( "chara/common/texture/transparent.tex", out var p ) ? p : Utf8GamePath.Empty; Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, out var p) ? p : Utf8GamePath.Empty;
private readonly Structs.TextureResourceHandle* _decal; private readonly Structs.TextureResourceHandle* _decal;
private readonly Structs.TextureResourceHandle* _transparent; private readonly Structs.TextureResourceHandle* _transparent;
public DecalReverter( ModCollection? collection, bool doDecal ) public DecalReverter( ResourceService resources, ModCollection? collection, bool doDecal )
{ {
var ptr = Penumbra.CharacterUtility.Address; var ptr = Penumbra.CharacterUtility.Address;
_decal = null; _decal = null;
@ -27,7 +28,7 @@ public unsafe partial class CharacterUtility
if( doDecal ) if( doDecal )
{ {
var decalPath = collection?.ResolvePath( DecalPath )?.InternalName ?? DecalPath.Path; var decalPath = collection?.ResolvePath( DecalPath )?.InternalName ?? DecalPath.Path;
var decalHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, decalPath ); var decalHandle = resources.GetResource( ResourceCategory.Chara, ResourceType.Tex, decalPath );
_decal = ( Structs.TextureResourceHandle* )decalHandle; _decal = ( Structs.TextureResourceHandle* )decalHandle;
if( _decal != null ) if( _decal != null )
{ {
@ -37,7 +38,7 @@ public unsafe partial class CharacterUtility
else else
{ {
var transparentPath = collection?.ResolvePath( TransparentPath )?.InternalName ?? TransparentPath.Path; var transparentPath = collection?.ResolvePath( TransparentPath )?.InternalName ?? TransparentPath.Path;
var transparentHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, transparentPath ); var transparentHandle = resources.GetResource(ResourceCategory.Chara, ResourceType.Tex, transparentPath);
_transparent = ( Structs.TextureResourceHandle* )transparentHandle; _transparent = ( Structs.TextureResourceHandle* )transparentHandle;
if( _transparent != null ) if( _transparent != null )
{ {
@ -54,7 +55,7 @@ public unsafe partial class CharacterUtility
ptr->DecalTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultDecalResource; ptr->DecalTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultDecalResource;
--_decal->Handle.RefCount; --_decal->Handle.RefCount;
} }
if( _transparent != null ) if( _transparent != null )
{ {
ptr->TransparentTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultTransparentResource; ptr->TransparentTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultTransparentResource;

View file

@ -6,52 +6,37 @@ namespace Penumbra.Interop;
// Handle font reloading via game functions. // Handle font reloading via game functions.
// May cause a interface flicker while reloading. // May cause a interface flicker while reloading.
public static unsafe class FontReloader public unsafe class FontReloader
{ {
private static readonly AtkModule* AtkModule = null; public bool Valid
private static readonly delegate* unmanaged<AtkModule*, bool, bool, void> ReloadFontsFunc = null; => _reloadFontsFunc != null;
public static bool Valid public void Reload()
=> ReloadFontsFunc != null;
public static void Reload()
{ {
if( Valid ) if (Valid)
{ _reloadFontsFunc(_atkModule, false, true);
ReloadFontsFunc( AtkModule, false, true );
}
else else
{ Penumbra.Log.Error("Could not reload fonts, function could not be found.");
Penumbra.Log.Error( "Could not reload fonts, function could not be found." );
}
} }
static FontReloader() private readonly AtkModule* _atkModule = null!;
{ private readonly delegate* unmanaged<AtkModule*, bool, bool, void> _reloadFontsFunc = null!;
if( ReloadFontsFunc != null )
{
return;
}
public FontReloader()
{
var framework = Framework.Instance(); var framework = Framework.Instance();
if( framework == null ) if (framework == null)
{
return; return;
}
var uiModule = framework->GetUiModule(); var uiModule = framework->GetUiModule();
if( uiModule == null ) if (uiModule == null)
{
return; return;
}
var atkModule = uiModule->GetRaptureAtkModule();
if( atkModule == null )
{
return;
}
AtkModule = &atkModule->AtkModule; var atkModule = uiModule->GetRaptureAtkModule();
ReloadFontsFunc = ( ( delegate* unmanaged< AtkModule*, bool, bool, void >* )AtkModule->vtbl )[ Offsets.ReloadFontsVfunc ]; if (atkModule == null)
return;
_atkModule = &atkModule->AtkModule;
_reloadFontsFunc = ((delegate* unmanaged< AtkModule*, bool, bool, void >*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc];
} }
} }

View file

@ -0,0 +1,106 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Resolver;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader;
public class CharacterResolver : IDisposable
{
private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager;
private readonly TempCollectionManager _tempCollections;
private readonly ResourceLoader _loader;
private readonly PathResolver _pathResolver;
public unsafe CharacterResolver(Configuration config, ModCollection.Manager collectionManager, TempCollectionManager tempCollections,
ResourceLoader loader, PathResolver pathResolver)
{
_config = config;
_collectionManager = collectionManager;
_tempCollections = tempCollections;
_loader = loader;
_pathResolver = pathResolver;
_loader.ResolvePath = ResolvePath;
_loader.FileLoaded += ImcLoadResource;
}
/// <summary> Obtain a temporary or permanent collection by name. </summary>
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection);
/// <summary> Try to resolve the given game path to the replaced path. </summary>
public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType)
{
// Check if mods are enabled or if we are in a inc-ref at 0 reference count situation.
if (!_config.EnableMods)
return (null, ResolveData.Invalid);
path = path.ToLower();
return category switch
{
// Only Interface collection.
ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path),
_collectionManager.Interface.ToResolveData()),
// Never allow changing scripts.
ResourceCategory.UiScript => (null, ResolveData.Invalid),
ResourceCategory.GameScript => (null, ResolveData.Invalid),
// Use actual resolving.
ResourceCategory.Chara => _pathResolver.CharacterResolver(path, resourceType),
ResourceCategory.Shader => _pathResolver.CharacterResolver(path, resourceType),
ResourceCategory.Vfx => _pathResolver.CharacterResolver(path, resourceType),
ResourceCategory.Sound => _pathResolver.CharacterResolver(path, resourceType),
// None of these files are ever associated with specific characters,
// always use the default resolver for now.
ResourceCategory.Common => DefaultResolver(path),
ResourceCategory.BgCommon => DefaultResolver(path),
ResourceCategory.Bg => DefaultResolver(path),
ResourceCategory.Cut => DefaultResolver(path),
ResourceCategory.Exd => DefaultResolver(path),
ResourceCategory.Music => DefaultResolver(path),
_ => DefaultResolver(path),
};
}
// TODO
public unsafe void Dispose()
{
_loader.ResetResolvePath();
_loader.FileLoaded -= ImcLoadResource;
_pathResolver.Dispose();
}
// Use the default method of path replacement.
private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path)
{
var resolved = _collectionManager.Default.ResolvePath(path);
return (resolved, _collectionManager.Default.ToResolveData());
}
/// <summary> After loading an IMC file, replace its contents with the modded IMC file. </summary>
private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData)
{
if (resource->FileType != ResourceType.Imc)
return;
var lastUnderscore = additionalData.LastIndexOf((byte)'_');
var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString();
if (Utf8GamePath.FromByteString(path, out var gamePath)
&& CollectionByName(name, out var collection)
&& collection.HasCache
&& collection.GetImcFile(gamePath, out var file))
{
file.Replace(resource);
Penumbra.Log.Verbose(
$"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}.");
}
}
}

View file

@ -18,146 +18,145 @@ public unsafe class CreateFileWHook : IDisposable
{ {
public const int RequiredSize = 28; public const int RequiredSize = 28;
// The prefix is not valid for any actual path, so should never run into false-positives.
private const char Prefix = ( char )( ( byte )'P' | ( ( '?' & 0x00FF ) << 8 ) );
private const int BufferSize = Utf8GamePath.MaxGamePathLength;
private delegate nint CreateFileWDelegate( char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template );
private readonly Hook< CreateFileWDelegate > _createFileWHook;
/// <summary> Some storage to skip repeated allocations. </summary>
private readonly ThreadLocal< nint > _fileNameStorage = new(SetupStorage, true);
public CreateFileWHook() public CreateFileWHook()
=> _createFileWHook = Hook< CreateFileWDelegate >.FromImport( null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour ); {
_createFileWHook = Hook<CreateFileWDelegate>.FromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour);
_createFileWHook.Enable();
}
/// <summary>
/// Write the data read specifically in the CreateFileW hook to a buffer array.
/// </summary>
/// <param name="buffer">The buffer the data is written to.</param>
/// <param name="address">The pointer to the UTF8 string containing the path.</param>
/// <param name="length">The length of the path in bytes.</param>
public static void WritePtr(char* buffer, byte* address, int length)
{
// Set the prefix, which is not valid for any actual path.
buffer[0] = Prefix;
var ptr = (byte*)buffer;
var v = (ulong)address;
var l = (uint)length;
// Since the game calls wstrcpy without a length, we need to ensure
// that there is no wchar_t (i.e. 2 bytes) of 0-values before the end.
// Fill everything with 0xFF and use every second byte.
MemoryUtility.MemSet(ptr + 2, 0xFF, 23);
// Write the byte pointer.
ptr[2] = (byte)(v >> 0);
ptr[4] = (byte)(v >> 8);
ptr[6] = (byte)(v >> 16);
ptr[8] = (byte)(v >> 24);
ptr[10] = (byte)(v >> 32);
ptr[12] = (byte)(v >> 40);
ptr[14] = (byte)(v >> 48);
ptr[16] = (byte)(v >> 56);
// Write the length.
ptr[18] = (byte)(l >> 0);
ptr[20] = (byte)(l >> 8);
ptr[22] = (byte)(l >> 16);
ptr[24] = (byte)(l >> 24);
ptr[RequiredSize - 2] = 0;
ptr[RequiredSize - 1] = 0;
}
public void Dispose()
{
_createFileWHook.Disable();
_createFileWHook.Dispose();
foreach (var ptr in _fileNameStorage.Values)
Marshal.FreeHGlobal(ptr);
}
/// <remarks> Long paths in windows need to start with "\\?\", so we keep this static in the pointers. </remarks> /// <remarks> Long paths in windows need to start with "\\?\", so we keep this static in the pointers. </remarks>
private static nint SetupStorage() private static nint SetupStorage()
{ {
var ptr = ( char* )Marshal.AllocHGlobal( 2 * BufferSize ); var ptr = (char*)Marshal.AllocHGlobal(2 * BufferSize);
ptr[ 0 ] = '\\'; ptr[0] = '\\';
ptr[ 1 ] = '\\'; ptr[1] = '\\';
ptr[ 2 ] = '?'; ptr[2] = '?';
ptr[ 3 ] = '\\'; ptr[3] = '\\';
ptr[ 4 ] = '\0'; ptr[4] = '\0';
return ( nint )ptr; return (nint)ptr;
} }
public void Enable() // The prefix is not valid for any actual path, so should never run into false-positives.
=> _createFileWHook.Enable(); private const char Prefix = (char)((byte)'P' | (('?' & 0x00FF) << 8));
private const int BufferSize = Utf8GamePath.MaxGamePathLength;
public void Disable() private delegate nint CreateFileWDelegate(char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags,
=> _createFileWHook.Disable(); nint template);
public void Dispose() private readonly Hook<CreateFileWDelegate> _createFileWHook;
{
Disable();
_createFileWHook.Dispose();
foreach( var ptr in _fileNameStorage.Values )
{
Marshal.FreeHGlobal( ptr );
}
}
private nint CreateFileWDetour( char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template ) /// <summary> Some storage to skip repeated allocations. </summary>
private readonly ThreadLocal<nint> _fileNameStorage = new(SetupStorage, true);
private nint CreateFileWDetour(char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template)
{ {
// Translate data if prefix fits. // Translate data if prefix fits.
if( CheckPtr( fileName, out var name ) ) if (CheckPtr(fileName, out var name))
{ {
// Use static storage. // Use static storage.
var ptr = WriteFileName( name ); var ptr = WriteFileName(name);
Penumbra.Log.Verbose( $"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); Penumbra.Log.Verbose($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}.");
return _createFileWHook.OriginalDisposeSafe( ptr, access, shareMode, security, creation, flags, template ); return _createFileWHook.OriginalDisposeSafe(ptr, access, shareMode, security, creation, flags, template);
} }
return _createFileWHook.OriginalDisposeSafe( fileName, access, shareMode, security, creation, flags, template ); return _createFileWHook.OriginalDisposeSafe(fileName, access, shareMode, security, creation, flags, template);
} }
/// <remarks>Write the UTF8-encoded byte string as UTF16 into the static buffers, /// <remarks>Write the UTF8-encoded byte string as UTF16 into the static buffers,
/// replacing any forward-slashes with back-slashes and adding a terminating null-wchar_t.</remarks> /// replacing any forward-slashes with back-slashes and adding a terminating null-wchar_t.</remarks>
private char* WriteFileName( ReadOnlySpan< byte > actualName ) private char* WriteFileName(ReadOnlySpan<byte> actualName)
{ {
var span = new Span< char >( ( char* )_fileNameStorage.Value + 4, BufferSize - 4 ); var span = new Span<char>((char*)_fileNameStorage.Value + 4, BufferSize - 4);
var written = Encoding.UTF8.GetChars( actualName, span ); var written = Encoding.UTF8.GetChars(actualName, span);
for( var i = 0; i < written; ++i ) for (var i = 0; i < written; ++i)
{ {
if( span[ i ] == '/' ) if (span[i] == '/')
{ span[i] = '\\';
span[ i ] = '\\';
}
} }
span[ written ] = '\0'; span[written] = '\0';
return ( char* )_fileNameStorage.Value; return (char*)_fileNameStorage.Value;
} }
private static bool CheckPtr(char* buffer, out ReadOnlySpan<byte> fileName)
public static void WritePtr( char* buffer, byte* address, int length )
{ {
// Set the prefix, which is not valid for any actual path. if (buffer[0] is not Prefix)
buffer[ 0 ] = Prefix;
var ptr = ( byte* )buffer;
var v = ( ulong )address;
var l = ( uint )length;
// Since the game calls wstrcpy without a length, we need to ensure
// that there is no wchar_t (i.e. 2 bytes) of 0-values before the end.
// Fill everything with 0xFF and use every second byte.
MemoryUtility.MemSet( ptr + 2, 0xFF, 23 );
// Write the byte pointer.
ptr[ 2 ] = ( byte )( v >> 0 );
ptr[ 4 ] = ( byte )( v >> 8 );
ptr[ 6 ] = ( byte )( v >> 16 );
ptr[ 8 ] = ( byte )( v >> 24 );
ptr[ 10 ] = ( byte )( v >> 32 );
ptr[ 12 ] = ( byte )( v >> 40 );
ptr[ 14 ] = ( byte )( v >> 48 );
ptr[ 16 ] = ( byte )( v >> 56 );
// Write the length.
ptr[ 18 ] = ( byte )( l >> 0 );
ptr[ 20 ] = ( byte )( l >> 8 );
ptr[ 22 ] = ( byte )( l >> 16 );
ptr[ 24 ] = ( byte )( l >> 24 );
ptr[ RequiredSize - 2 ] = 0;
ptr[ RequiredSize - 1 ] = 0;
}
private static bool CheckPtr( char* buffer, out ReadOnlySpan< byte > fileName )
{
if( buffer[ 0 ] is not Prefix )
{ {
fileName = ReadOnlySpan< byte >.Empty; fileName = ReadOnlySpan<byte>.Empty;
return false; return false;
} }
var ptr = ( byte* )buffer; var ptr = (byte*)buffer;
// Read the byte pointer. // Read the byte pointer.
var address = 0ul; var address = 0ul;
address |= ( ulong )ptr[ 2 ] << 0; address |= (ulong)ptr[2] << 0;
address |= ( ulong )ptr[ 4 ] << 8; address |= (ulong)ptr[4] << 8;
address |= ( ulong )ptr[ 6 ] << 16; address |= (ulong)ptr[6] << 16;
address |= ( ulong )ptr[ 8 ] << 24; address |= (ulong)ptr[8] << 24;
address |= ( ulong )ptr[ 10 ] << 32; address |= (ulong)ptr[10] << 32;
address |= ( ulong )ptr[ 12 ] << 40; address |= (ulong)ptr[12] << 40;
address |= ( ulong )ptr[ 14 ] << 48; address |= (ulong)ptr[14] << 48;
address |= ( ulong )ptr[ 16 ] << 56; address |= (ulong)ptr[16] << 56;
// Read the length. // Read the length.
var length = 0u; var length = 0u;
length |= ( uint )ptr[ 18 ] << 0; length |= (uint)ptr[18] << 0;
length |= ( uint )ptr[ 20 ] << 8; length |= (uint)ptr[20] << 8;
length |= ( uint )ptr[ 22 ] << 16; length |= (uint)ptr[22] << 16;
length |= ( uint )ptr[ 24 ] << 24; length |= (uint)ptr[24] << 24;
fileName = new ReadOnlySpan< byte >( ( void* )address, ( int )length ); fileName = new ReadOnlySpan<byte>((void*)address, (int)length);
return true; return true;
} }
@ -175,4 +174,4 @@ public unsafe class CreateFileWHook : IDisposable
// var createFileAddress = GetProcAddress( userApi, "CreateFileW" ); // var createFileAddress = GetProcAddress( userApi, "CreateFileW" );
// _createFileWHook = Hook<CreateFileWDelegate>.FromAddress( createFileAddress, CreateFileWDetour ); // _createFileWHook = Hook<CreateFileWDelegate>.FromAddress( createFileAddress, CreateFileWDetour );
//} //}
} }

View file

@ -1,50 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.GameData;
using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Loader;
public unsafe class FileReadHooks : IDisposable
{
private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync);
[Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))]
private readonly Hook<ReadSqPackPrototype> _readSqPackHook = null!;
public FileReadHooks()
{
SignatureHelper.Initialise(this);
_readSqPackHook.Enable();
}
/// <summary> Invoked when a file is supposed to be read from SqPack. </summary>
/// <param name="fileDescriptor">The file descriptor containing what file to read.</param>
/// <param name="priority">The games priority. Should not generally be changed.</param>
/// <param name="isSync">Whether the file needs to be loaded synchronously. Should not generally be changed.</param>
/// <param name="callOriginal">Whether to call the original function after the event is finished.</param>
public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal);
/// <summary>
/// <inheritdoc cref="ReadSqPackDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ReadSqPackDelegate? ReadSqPack;
private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{
var callOriginal = true;
ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal);
return callOriginal
? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync)
: (byte)1;
}
public void Dispose()
{
_readSqPackHook.Disable();
_readSqPackHook.Dispose();
}
}

View file

@ -0,0 +1,90 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.GameData;
using Penumbra.Interop.Structs;
using Penumbra.Util;
namespace Penumbra.Interop.Loader;
public unsafe class FileReadService : IDisposable
{
public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager)
{
_resourceManager = resourceManager;
_performance = performance;
SignatureHelper.Initialise(this);
_readSqPackHook.Enable();
}
/// <summary> Invoked when a file is supposed to be read from SqPack. </summary>
/// <param name="fileDescriptor">The file descriptor containing what file to read.</param>
/// <param name="priority">The games priority. Should not generally be changed.</param>
/// <param name="isSync">Whether the file needs to be loaded synchronously. Should not generally be changed.</param>
/// <param name="returnValue">The return value. If this is set, original will not be called.</param>
public delegate void ReadSqPackDelegate(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue);
/// <summary>
/// <inheritdoc cref="ReadSqPackDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ReadSqPackDelegate? ReadSqPack;
/// <summary>
/// Use the games ReadFile function to read a file from the hard drive instead of an SqPack.
/// </summary>
/// <param name="fileDescriptor">The file to load.</param>
/// <param name="priority">The games priority.</param>
/// <param name="isSync">Whether the file needs to be loaded synchronously.</param>
/// <returns>Unknown, not directly success/failure.</returns>
public byte ReadFile(SeFileDescriptor* fileDescriptor, int priority, bool isSync)
=> _readFile.Invoke(GetResourceManager(), fileDescriptor, priority, isSync);
public byte ReadDefaultSqPack(SeFileDescriptor* fileDescriptor, int priority, bool isSync)
=> _readSqPackHook.Original(GetResourceManager(), fileDescriptor, priority, isSync);
public void Dispose()
{
_readSqPackHook.Dispose();
}
private readonly PerformanceTracker _performance;
private readonly ResourceManagerService _resourceManager;
private delegate byte ReadSqPackPrototype(nint resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync);
[Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))]
private readonly Hook<ReadSqPackPrototype> _readSqPackHook = null!;
private byte ReadSqPackDetour(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{
using var performance = _performance.Measure(PerformanceType.ReadSqPack);
byte? ret = null;
_lastFileThreadResourceManager.Value = resourceManager;
ReadSqPack?.Invoke(fileDescriptor, ref priority, ref isSync, ref ret);
_lastFileThreadResourceManager.Value = IntPtr.Zero;
return ret ?? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
}
private delegate byte ReadFileDelegate(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority,
bool isSync);
/// We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks.
[Signature(Sigs.ReadFile)]
private readonly ReadFileDelegate _readFile = null!;
private readonly ThreadLocal<nint> _lastFileThreadResourceManager = new(true);
/// <summary>
/// Usually files are loaded using the resource manager as a first pointer, but it seems some rare cases are using something else.
/// So we keep track of them per thread and use them.
/// </summary>
private nint GetResourceManager()
=> !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == IntPtr.Zero
? (nint) _resourceManager.ResourceManager
: _lastFileThreadResourceManager.Value;
}

View file

@ -1,251 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.STD;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Interop.Loader;
public unsafe partial class ResourceLoader
{
// If in debug mode, this logs any resource at refcount 0 that gets decremented again, and skips the decrement instead.
private delegate byte ResourceHandleDecRef( ResourceHandle* handle );
private readonly Hook< ResourceHandleDecRef > _decRefHook;
public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle );
[Signature( Sigs.ResourceHandleDestructor, DetourName = nameof( ResourceHandleDestructorDetour ) )]
public static Hook< ResourceHandleDestructor >? ResourceHandleDestructorHook;
private IntPtr ResourceHandleDestructorDetour( ResourceHandle* handle )
{
if( handle != null )
{
Penumbra.Log.Information( $"[ResourceLoader] Destructing Resource Handle {handle->FileName} at 0x{( ulong )handle:X} (Refcount {handle->RefCount})." );
}
return ResourceHandleDestructorHook!.Original( handle );
}
// A static pointer to the SE Resource Manager
[Signature( Sigs.ResourceManager, ScanType = ScanType.StaticAddress)]
public static ResourceManager** ResourceManager;
// Gather some debugging data about penumbra-loaded objects.
public struct DebugData
{
public Structs.ResourceHandle* OriginalResource;
public Structs.ResourceHandle* ManipulatedResource;
public Utf8GamePath OriginalPath;
public FullPath ManipulatedPath;
public ResourceCategory Category;
public ResolveData ResolverInfo;
public ResourceType Extension;
}
private readonly SortedList< FullPath, DebugData > _debugList = new();
public IReadOnlyDictionary< FullPath, DebugData > DebugList
=> _debugList;
public void EnableDebug()
{
_decRefHook.Enable();
ResourceLoaded += AddModifiedDebugInfo;
}
public void DisableDebug()
{
_decRefHook.Disable();
ResourceLoaded -= AddModifiedDebugInfo;
}
private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolverInfo )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.DebugTimes );
if( manipulatedPath == null || manipulatedPath.Value.Crc64 == 0 )
{
return;
}
// Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra.
try
{
var crc = ( uint )originalPath.Path.Crc32;
var originalResource = FindResource( handle->Category, handle->FileType, crc );
_debugList[ manipulatedPath.Value ] = new DebugData()
{
OriginalResource = ( Structs.ResourceHandle* )originalResource,
ManipulatedResource = handle,
Category = handle->Category,
Extension = handle->FileType,
OriginalPath = originalPath.Clone(),
ManipulatedPath = manipulatedPath.Value,
ResolverInfo = resolverInfo,
};
}
catch( Exception e )
{
Penumbra.Log.Error( e.ToString() );
}
}
// Find a key in a StdMap.
private static TValue* FindInMap< TKey, TValue >( StdMap< TKey, TValue >* map, in TKey key )
where TKey : unmanaged, IComparable< TKey >
where TValue : unmanaged
{
if( map == null || map->Count == 0 )
{
return null;
}
var node = map->Head->Parent;
while( !node->IsNil )
{
switch( key.CompareTo( node->KeyValuePair.Item1 ) )
{
case 0: return &node->KeyValuePair.Item2;
case < 0:
node = node->Left;
break;
default:
node = node->Right;
break;
}
}
return null;
}
// Iterate in tree-order through a map, applying action to each KeyValuePair.
private static void IterateMap< TKey, TValue >( StdMap< TKey, TValue >* map, Action< TKey, TValue > action )
where TKey : unmanaged
where TValue : unmanaged
{
if( map == null || map->Count == 0 )
{
return;
}
for( var node = map->SmallestValue; !node->IsNil; node = node->Next() )
{
action( node->KeyValuePair.Item1, node->KeyValuePair.Item2 );
}
}
// Find a resource in the resource manager by its category, extension and crc-hash
public static ResourceHandle* FindResource( ResourceCategory cat, ResourceType ext, uint crc32 )
{
ref var manager = ref *ResourceManager;
var catIdx = ( uint )cat >> 0x18;
cat = ( ResourceCategory )( ushort )cat;
ref var category = ref manager->ResourceGraph->ContainerArraySpan[(int) cat];
var extMap = FindInMap( category.CategoryMapsSpan[ (int) catIdx ].Value, ( uint )ext );
if( extMap == null )
{
return null;
}
var ret = FindInMap( extMap->Value, crc32 );
return ret == null ? null : ret->Value;
}
public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph,
int idx );
public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph );
public delegate void ResourceAction( uint crc32, ResourceHandle* graph );
// Iteration functions through the resource manager.
public static void IterateGraphs( ExtMapAction action )
{
ref var manager = ref *ResourceManager;
foreach( var resourceType in Enum.GetValues< ResourceCategory >().SkipLast( 1 ) )
{
ref var graph = ref manager->ResourceGraph->ContainerArraySpan[(int) resourceType];
for( var i = 0; i < 20; ++i )
{
var map = graph.CategoryMapsSpan[i];
if( map.Value != null )
{
action( resourceType, map, i );
}
}
}
}
public static void IterateExtMap( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, ResourceMapAction action )
=> IterateMap( map, ( ext, m ) => action( ext, m.Value ) );
public static void IterateResourceMap( StdMap< uint, Pointer< ResourceHandle > >* map, ResourceAction action )
=> IterateMap( map, ( crc, r ) => action( crc, r.Value ) );
public static void IterateResources( ResourceAction action )
{
IterateGraphs( ( _, extMap, _ )
=> IterateExtMap( extMap, ( _, resourceMap )
=> IterateResourceMap( resourceMap, action ) ) );
}
// Update the list of currently replaced resources.
// Only used when the Replaced Resources Tab in the Debug tab is open.
public void UpdateDebugInfo()
{
using var performance = Penumbra.Performance.Measure( PerformanceType.DebugTimes );
for( var i = 0; i < _debugList.Count; ++i )
{
var data = _debugList.Values[ i ];
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if( data.OriginalPath.Path == null )
{
_debugList.RemoveAt( i-- );
continue;
}
var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 );
var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 );
if( modifiedResource == null )
{
_debugList.RemoveAt( i-- );
}
else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource )
{
_debugList[ _debugList.Keys[ i ] ] = data with
{
OriginalResource = ( Structs.ResourceHandle* )regularResource,
ManipulatedResource = ( Structs.ResourceHandle* )modifiedResource,
};
}
}
}
// Prevent resource management weirdness.
private byte ResourceHandleDecRefDetour( ResourceHandle* handle )
{
if( handle == null )
{
return 0;
}
if( handle->RefCount != 0 )
{
return _decRefHook.Original( handle );
}
Penumbra.Log.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." );
return 1;
}
}

View file

@ -1,301 +0,0 @@
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using static Penumbra.Interop.Loader.ResourceLoader;
using FileMode = Penumbra.Interop.Structs.FileMode;
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
namespace Penumbra.Interop.Loader;
public unsafe class FileReadHooks : IDisposable
{
private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync);
[Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))]
private readonly Hook<ReadSqPackPrototype> _readSqPackHook = null!;
public FileReadHooks()
{
SignatureHelper.Initialise(this);
_readSqPackHook.Enable();
}
public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal);
public event ReadSqPackDelegate? ReadSqPack;
private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{
var callOriginal = true;
ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal);
return callOriginal
? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync)
: (byte)1;
}
public void Dispose()
{
_readSqPackHook.Disable();
_readSqPackHook.Dispose();
}
}
public unsafe partial class ResourceLoader
{
[Conditional("DEBUG")]
private static void CompareHash(int local, int game, Utf8GamePath path)
{
if (local != game)
Penumbra.Log.Warning($"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}.");
}
private event Action<Utf8GamePath, ResourceType, FullPath?, object?>? PathResolved;
public ResourceHandle* ResolvePathSync(ResourceCategory category, ResourceType type, ByteString path)
{
var hash = path.Crc32;
return GetResourceHandler(true, *ResourceManager, &category, &type, &hash, path.Path, null, false);
}
private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId,
ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk)
{
using var performance = Penumbra.Performance.Measure(PerformanceType.GetResourceHandler);
ResourceHandle* ret;
if (!Utf8GamePath.FromPointer(path, out var gamePath))
{
Penumbra.Log.Error("Could not create GamePath from resource path.");
return CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk);
}
CompareHash(ComputeHash(gamePath.Path, pGetResParams), *resourceHash, gamePath);
ResourceRequested?.Invoke(gamePath, isSync);
// 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 ?? (gamePath.IsRooted() ? new FullPath(gamePath) : null), data);
if (resolvedPath == null)
{
ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk);
ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, null, data);
return ret;
}
// Replace the hash and path with the correct one for the replacement.
*resourceHash = ComputeHash(resolvedPath.Value.InternalName, pGetResParams);
path = resolvedPath.Value.InternalName.Path;
ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk);
ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, resolvedPath.Value, data);
return ret;
}
// Use the default method of path replacement.
public static (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path)
{
var resolved = Penumbra.CollectionManager.Default.ResolvePath(path);
return (resolved, Penumbra.CollectionManager.Default.ToResolveData());
}
// Try all resolve path subscribers or use the default replacer.
private (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash)
{
if (!DoReplacements || _incMode.Value)
return (null, ResolveData.Invalid);
path = path.ToLower();
switch (category)
{
// Only Interface collection.
case ResourceCategory.Ui:
{
var resolved = Penumbra.CollectionManager.Interface.ResolvePath(path);
return (resolved, Penumbra.CollectionManager.Interface.ToResolveData());
}
// Never allow changing scripts.
case ResourceCategory.UiScript:
case ResourceCategory.GameScript:
return (null, ResolveData.Invalid);
// Use actual resolving.
case ResourceCategory.Chara:
case ResourceCategory.Shader:
case ResourceCategory.Vfx:
case ResourceCategory.Sound:
if (ResolvePathCustomization != null)
foreach (var resolver in ResolvePathCustomization.GetInvocationList())
{
if (((ResolvePathDelegate)resolver).Invoke(path, category, resourceType, resourceHash, out var ret))
return ret;
}
break;
// None of these files are ever associated with specific characters,
// always use the default resolver for now.
case ResourceCategory.Common:
case ResourceCategory.BgCommon:
case ResourceCategory.Bg:
case ResourceCategory.Cut:
case ResourceCategory.Exd:
case ResourceCategory.Music:
default:
break;
}
return DefaultResolver(path);
}
// We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks.
public delegate byte ReadFileDelegate(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority,
bool isSync);
[Signature(Sigs.ReadFile)]
public readonly ReadFileDelegate ReadFile = null!;
// We hook ReadSqPack to redirect rooted files to ReadFile.
public delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync);
[Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))]
public readonly Hook<ReadSqPackPrototype> ReadSqPackHook = null!;
private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{
using var performance = Penumbra.Performance.Measure(PerformanceType.ReadSqPack);
if (!DoReplacements)
return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
if (fileDescriptor == null || fileDescriptor->ResourceHandle == null)
{
Penumbra.Log.Error("Failure to load file from SqPack: invalid File Descriptor.");
return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
}
if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0)
return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
// Paths starting with a '|' are handled separately to allow for special treatment.
// They are expected to also have a closing '|'.
if (ResourceLoadCustomization == null || gamePath.Path[0] != (byte)'|')
return DefaultLoadResource(gamePath.Path, resourceManager, fileDescriptor, priority, isSync);
// Split the path into the special-treatment part (between the first and second '|')
// and the actual path.
byte ret = 0;
var split = gamePath.Path.Split((byte)'|', 3, false);
fileDescriptor->ResourceHandle->FileNameData = split[2].Path;
fileDescriptor->ResourceHandle->FileNameLength = split[2].Length;
var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui
&& ResourceLoadCustomization.GetInvocationList()
.Any(f => ((ResourceLoadCustomizationDelegate)f)
.Invoke(split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret));
if (!funcFound)
ret = DefaultLoadResource(split[2], resourceManager, fileDescriptor, priority, isSync);
// Return original resource handle path so that they can be loaded separately.
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length;
return ret;
}
// Load the resource from an SqPack and trigger the FileLoaded event.
private byte DefaultResourceLoad(ByteString path, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{
var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
FileLoaded?.Invoke(fileDescriptor->ResourceHandle, path, ret != 0, false);
return ret;
}
/// <summary> Load the resource from a path on the users hard drives. </summary>
/// <remarks> <see cref="CreateFileWHook" /> </remarks>
private byte DefaultRootedResourceLoad(ByteString gamePath, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{
// Specify that we are loading unpacked files from the drive.
// We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations.
fileDescriptor->FileMode = FileMode.LoadUnpackedResource;
// Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd.
var fd = stackalloc char[0x11 + 0x0B + 14];
fileDescriptor->FileDescriptor = (byte*)fd + 1;
CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length);
CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length);
// Use the SE ReadFile function.
var ret = ReadFile(resourceManager, fileDescriptor, priority, isSync);
FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true);
return ret;
}
// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack.
internal byte DefaultLoadResource(ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority,
bool isSync)
=> Utf8GamePath.IsRooted(gamePath)
? DefaultRootedResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync)
: DefaultResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync);
private void DisposeHooks()
{
DisableHooks();
_createFileWHook.Dispose();
ReadSqPackHook.Dispose();
GetResourceSyncHook.Dispose();
GetResourceAsyncHook.Dispose();
ResourceHandleDestructorHook?.Dispose();
_incRefHook.Dispose();
}
private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams)
{
if (pGetResParams == null || !pGetResParams->IsPartialRead)
return path.Crc32;
// When the game requests file only partially, crc32 includes that information, in format of:
// path/to/file.ext.hex_offset.hex_size
// ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000
return ByteString.Join(
(byte)'.',
path,
ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true),
ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true)
).Crc32;
}
// A resource with ref count 0 that gets incremented goes through GetResourceAsync again.
// This means, that if the path determined from that is different than the resources path,
// a different resource gets loaded or incremented, while the IncRef'd resource stays at 0.
// This causes some problems and is hopefully prevented with this.
private readonly ThreadLocal<bool> _incMode = new();
private readonly Hook<ResourceHandleDestructor> _incRefHook;
private IntPtr ResourceHandleIncRefDetour(ResourceHandle* handle)
{
if (handle->RefCount > 0)
return _incRefHook.Original(handle);
_incMode.Value = true;
var ret = _incRefHook.Original(handle);
_incMode.Value = false;
return ret;
}
}

View file

@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader;
// Since 6.0, Mdl and Tex Files require special treatment, probably due to datamining protection.
public unsafe partial class ResourceLoader
{
// Custom ulong flag to signal our files as opposed to SE files.
public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF);
// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files,
// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes.
private readonly HashSet< ulong > _customFileCrc = new();
public IReadOnlySet< ulong > CustomFileCrc
=> _customFileCrc;
// The function that checks a files CRC64 to determine whether it is 'protected'.
// 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( Sigs.CheckFileState, DetourName = nameof( CheckFileStateDetour ) )]
public readonly Hook< CheckFileStatePrototype > CheckFileStateHook = null!;
private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 )
=> _customFileCrc.Contains( crc64 ) ? CustomFileFlag : CheckFileStateHook.Original( ptr, crc64 );
// We use the local functions for our own files in the extern hook.
public delegate byte LoadTexFileLocalDelegate( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3 );
[Signature( Sigs.LoadTexFileLocal )]
public readonly LoadTexFileLocalDelegate LoadTexFileLocal = null!;
public delegate byte LoadMdlFileLocalPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2 );
[Signature( Sigs.LoadMdlFileLocal )]
public readonly LoadMdlFileLocalPrototype LoadMdlFileLocal = null!;
// 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( Sigs.LoadTexFileExtern, DetourName = nameof( LoadTexFileExternDetour ) )]
public readonly Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!;
private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr )
=> ptr.Equals( CustomFileFlag )
? LoadTexFileLocal.Invoke( resourceHandle, unk1, unk2, unk3 )
: LoadTexFileExternHook.Original( resourceHandle, unk1, unk2, unk3, ptr );
public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 );
[Signature( Sigs.LoadMdlFileExtern, DetourName = nameof( LoadMdlFileExternDetour ) )]
public readonly Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!;
private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr )
=> ptr.Equals( CustomFileFlag )
? LoadMdlFileLocal.Invoke( resourceHandle, unk1, unk2 )
: LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr );
private void AddCrc( Utf8GamePath _, ResourceType type, FullPath? path, object? _2 )
{
if( path.HasValue && type is ResourceType.Mdl or ResourceType.Tex )
{
_customFileCrc.Add( path.Value.Crc64 );
}
}
private void EnableTexMdlTreatment()
{
PathResolved += AddCrc;
CheckFileStateHook.Enable();
LoadTexFileExternHook.Enable();
LoadMdlFileExternHook.Enable();
}
private void DisableTexMdlTreatment()
{
PathResolved -= AddCrc;
_customFileCrc.Clear();
_customFileCrc.TrimExcess();
CheckFileStateHook.Disable();
LoadTexFileExternHook.Disable();
LoadMdlFileExternHook.Disable();
}
private void DisposeTexMdlTreatment()
{
CheckFileStateHook.Dispose();
LoadTexFileExternHook.Dispose();
LoadMdlFileExternHook.Dispose();
}
}

View file

@ -1,6 +1,6 @@
using System; using System;
using Dalamud.Hooking; using System.Diagnostics;
using Dalamud.Utility.Signatures; using System.Threading;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -10,114 +10,219 @@ using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader; namespace Penumbra.Interop.Loader;
public unsafe partial class ResourceLoader : IDisposable public unsafe class ResourceLoader : IDisposable
{ {
// Toggle whether replacing paths is active, independently of hook and event state. private readonly ResourceService _resources;
public bool DoReplacements { get; private set; } private readonly FileReadService _fileReadService;
private readonly TexMdlService _texMdlService;
// Hooks are required for everything, even events firing. public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService,
public bool HooksEnabled { get; private set; } CreateFileWHook _)
public void EnableReplacements()
{ {
if( DoReplacements ) _resources = resources;
{ _fileReadService = fileReadService;
return; _texMdlService = texMdlService;
} ResetResolvePath();
DoReplacements = true; _resources.ResourceRequested += ResourceHandler;
EnableTexMdlTreatment(); _resources.ResourceHandleIncRef += IncRefProtection;
EnableHooks(); _resources.ResourceHandleDecRef += DecRefProtection;
_fileReadService.ReadSqPack += ReadSqPackDetour;
} }
public void DisableReplacements() /// <summary> The function to use to resolve a given path. </summary>
{ public Func<Utf8GamePath, ResourceCategory, ResourceType, (FullPath?, ResolveData)> ResolvePath = null!;
if( !DoReplacements )
{
return;
}
DoReplacements = false; /// <summary> Reset the ResolvePath function to always return null. </summary>
DisableTexMdlTreatment(); public void ResetResolvePath()
} => ResolvePath = (_1, _2, _3) => (null, ResolveData.Invalid);
public void EnableHooks() public delegate void ResourceLoadedDelegate(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
{ ResolveData resolveData);
if( HooksEnabled )
{
return;
}
HooksEnabled = true;
_createFileWHook.Enable();
ReadSqPackHook.Enable();
GetResourceSyncHook.Enable();
GetResourceAsyncHook.Enable();
_incRefHook.Enable();
}
public void DisableHooks()
{
if( !HooksEnabled )
{
return;
}
HooksEnabled = false;
_createFileWHook.Disable();
ReadSqPackHook.Disable();
GetResourceSyncHook.Disable();
GetResourceAsyncHook.Disable();
_incRefHook.Disable();
}
public ResourceLoader( Penumbra _ )
{
SignatureHelper.Initialise( this );
_decRefHook = Hook< ResourceHandleDecRef >.FromAddress(
( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef,
ResourceHandleDecRefDetour );
_incRefHook = Hook< ResourceHandleDestructor >.FromAddress(
( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour );
}
// Event fired whenever a resource is requested.
public delegate void ResourceRequestedDelegate( Utf8GamePath path, bool synchronous );
public event ResourceRequestedDelegate? ResourceRequested;
// Event fired whenever a resource is returned.
// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
// resolveData is additional data returned by the current ResolvePath function which can contain the collection and associated game object.
public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolveData );
/// <summary>
/// Event fired whenever a resource is returned.
/// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
/// resolveData is additional data returned by the current ResolvePath function which can contain the collection and associated game object.
/// </summary>
public event ResourceLoadedDelegate? ResourceLoaded; public event ResourceLoadedDelegate? ResourceLoaded;
public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom,
ByteString additionalData);
// Event fired whenever a resource is newly loaded. /// <summary>
// Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) /// Event fired whenever a resource is newly loaded.
// custom is true if the file was loaded from local files instead of the default SqPacks. /// ReturnValue indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded)
public delegate void FileLoadedDelegate( ResourceHandle* resource, ByteString path, bool success, bool custom ); /// custom is true if the file was loaded from local files instead of the default SqPacks.
/// AdditionalData is either empty or the part of the path inside the leading pipes.
/// </summary>
public event FileLoadedDelegate? FileLoaded; public event FileLoadedDelegate? FileLoaded;
// Customization point to control how path resolving is handled.
// Resolving goes through all subscribed functions in arbitrary order until one returns true,
// or uses default resolving if none return true.
public delegate bool ResolvePathDelegate( Utf8GamePath path, ResourceCategory category, ResourceType type, int hash,
out (FullPath?, ResolveData) ret );
public event ResolvePathDelegate? ResolvePathCustomization;
// Customize file loading for any GamePaths that start with "|".
// Same procedure as above.
public delegate bool ResourceLoadCustomizationDelegate( ByteString split, ByteString path, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte retValue );
public event ResourceLoadCustomizationDelegate? ResourceLoadCustomization;
public void Dispose() public void Dispose()
{ {
DisposeHooks(); _resources.ResourceRequested -= ResourceHandler;
DisposeTexMdlTreatment(); _resources.ResourceHandleIncRef -= IncRefProtection;
_resources.ResourceHandleDecRef -= DecRefProtection;
_fileReadService.ReadSqPack -= ReadSqPackDetour;
} }
}
private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path,
GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue)
{
if (returnValue != null)
return;
CompareHash(ComputeHash(path.Path, parameters), hash, path);
// If no replacements are being made, we still want to be able to trigger the event.
var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) : ResolvePath(path, category, type);
if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p))
{
returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters);
ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data);
return;
}
_texMdlService.AddCrc(type, resolvedPath);
// Replace the hash and path with the correct one for the replacement.
hash = ComputeHash(resolvedPath.Value.InternalName, parameters);
path = p;
returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters);
ResourceLoaded?.Invoke(returnValue, p, resolvedPath.Value, data);
}
private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue)
{
if (fileDescriptor->ResourceHandle == null)
{
Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid File Descriptor.");
return;
}
if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0)
{
Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid path specified.");
return;
}
// Paths starting with a '|' are handled separately to allow for special treatment.
// They are expected to also have a closing '|'.
if (gamePath.Path[0] != (byte)'|')
{
returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, ByteString.Empty);
return;
}
// Split the path into the special-treatment part (between the first and second '|')
// and the actual path.
var split = gamePath.Path.Split((byte)'|', 3, false);
fileDescriptor->ResourceHandle->FileNameData = split[2].Path;
fileDescriptor->ResourceHandle->FileNameLength = split[2].Length;
MtrlForceSync(fileDescriptor, ref isSync);
returnValue = DefaultLoadResource(split[2], fileDescriptor, priority, isSync, split[1]);
// Return original resource handle path so that they can be loaded separately.
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length;
}
/// <summary> Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. </summary>
private byte DefaultLoadResource(ByteString gamePath, SeFileDescriptor* fileDescriptor, int priority,
bool isSync, ByteString additionalData)
{
if (Utf8GamePath.IsRooted(gamePath))
{
// Specify that we are loading unpacked files from the drive.
// We need to obtain the actual file path in UTF16 (Windows-Unicode) on two locations,
// but we write a pointer to the given string instead and use the CreateFileW hook to handle it,
// because otherwise we are limited to 260 characters.
fileDescriptor->FileMode = FileMode.LoadUnpackedResource;
// Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd.
var fd = stackalloc char[0x11 + 0x0B + 14];
fileDescriptor->FileDescriptor = (byte*)fd + 1;
CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length);
CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length);
// Use the SE ReadFile function.
var ret = _fileReadService.ReadFile(fileDescriptor, priority, isSync);
FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true, additionalData);
return ret;
}
else
{
var ret = _fileReadService.ReadDefaultSqPack(fileDescriptor, priority, isSync);
FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, false, additionalData);
return ret;
}
}
/// <summary> Special handling for materials. </summary>
private static void MtrlForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync)
{
// Force isSync = true for Materials. I don't really understand why,
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl;
}
/// <summary>
/// A resource with ref count 0 that gets incremented goes through GetResourceAsync again.
/// This means, that if the path determined from that is different than the resources path,
/// a different resource gets loaded or incremented, while the IncRef'd resource stays at 0.
/// This causes some problems and is hopefully prevented with this.
/// </summary>
private readonly ThreadLocal<bool> _incMode = new(() => false, true);
/// <inheritdoc cref="_incMode"/>
private void IncRefProtection(ResourceHandle* handle, ref nint? returnValue)
{
if (handle->RefCount != 0)
return;
_incMode.Value = true;
returnValue = _resources.IncRef(handle);
_incMode.Value = false;
}
/// <summary>
/// Catch weird errors with invalid decrements of the reference count.
/// </summary>
private void DecRefProtection(ResourceHandle* handle, ref byte? returnValue)
{
if (handle->RefCount != 0)
return;
Penumbra.Log.Error(
$"[ResourceLoader] Caught decrease of Reference Counter for {handle->FileName()} at 0x{(ulong)handle} below 0.");
returnValue = 1;
}
/// <summary> Compute the CRC32 hash for a given path together with potential resource parameters. </summary>
private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams)
{
if (pGetResParams == null || !pGetResParams->IsPartialRead)
return path.Crc32;
// When the game requests file only partially, crc32 includes that information, in format of:
// path/to/file.ext.hex_offset.hex_size
// ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000
return ByteString.Join(
(byte)'.',
path,
ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true),
ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true)
).Crc32;
}
/// <summary>
/// In Debug build, compare the hashes the game computes with those Penumbra computes to notice potential changes in the CRC32 algorithm or resource parameters.
/// </summary>
[Conditional("DEBUG")]
private static void CompareHash(int local, int game, Utf8GamePath path)
{
if (local != game)
Penumbra.Log.Warning($"[ResourceLoader] Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}.");
}
}

View file

@ -0,0 +1,114 @@
using System;
using System.Linq;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.STD;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
namespace Penumbra.Interop.Loader;
public unsafe class ResourceManagerService
{
public ResourceManagerService()
=> SignatureHelper.Initialise(this);
/// <summary> The SE Resource Manager as pointer. </summary>
public ResourceManager* ResourceManager
=> *ResourceManagerAddress;
/// <summary> Find a resource in the resource manager by its category, extension and crc-hash. </summary>
public ResourceHandle* FindResource(ResourceCategory cat, ResourceType ext, uint crc32)
{
ref var manager = ref *ResourceManager;
var catIdx = (uint)cat >> 0x18;
cat = (ResourceCategory)(ushort)cat;
ref var category = ref manager.ResourceGraph->ContainerArraySpan[(int)cat];
var extMap = FindInMap(category.CategoryMapsSpan[(int)catIdx].Value, (uint)ext);
if (extMap == null)
return null;
var ret = FindInMap(extMap->Value, crc32);
return ret == null ? null : ret->Value;
}
public delegate void ExtMapAction(ResourceCategory category, StdMap<uint, Pointer<StdMap<uint, Pointer<ResourceHandle>>>>* graph, int idx);
public delegate void ResourceMapAction(uint ext, StdMap<uint, Pointer<ResourceHandle>>* graph);
public delegate void ResourceAction(uint crc32, ResourceHandle* graph);
/// <summary> Iterate through the entire graph calling an action on every ExtMap. </summary>
public void IterateGraphs(ExtMapAction action)
{
ref var manager = ref *ResourceManager;
foreach (var resourceType in Enum.GetValues<ResourceCategory>().SkipLast(1))
{
ref var graph = ref manager.ResourceGraph->ContainerArraySpan[(int)resourceType];
for (var i = 0; i < 20; ++i)
{
var map = graph.CategoryMapsSpan[i];
if (map.Value != null)
action(resourceType, map, i);
}
}
}
/// <summary> Iterate through a specific ExtMap calling an action on every resource map. </summary>
public void IterateExtMap(StdMap<uint, Pointer<StdMap<uint, Pointer<ResourceHandle>>>>* map, ResourceMapAction action)
=> IterateMap(map, (ext, m) => action(ext, m.Value));
/// <summary> Iterate through a specific resource map calling an action on every resource. </summary>
public void IterateResourceMap(StdMap<uint, Pointer<ResourceHandle>>* map, ResourceAction action)
=> IterateMap(map, (crc, r) => action(crc, r.Value));
/// <summary> Iterate through the entire graph calling an action on every resource. </summary>
public void IterateResources(ResourceAction action)
{
IterateGraphs((_, extMap, _)
=> IterateExtMap(extMap, (_, resourceMap)
=> IterateResourceMap(resourceMap, action)));
}
/// <summary> A static pointer to the SE Resource Manager. </summary>
[Signature(Sigs.ResourceManager, ScanType = ScanType.StaticAddress)]
internal readonly ResourceManager** ResourceManagerAddress = null;
// Find a key in a StdMap.
private static TValue* FindInMap<TKey, TValue>(StdMap<TKey, TValue>* map, in TKey key)
where TKey : unmanaged, IComparable<TKey>
where TValue : unmanaged
{
if (map == null || map->Count == 0)
return null;
var node = map->Head->Parent;
while (!node->IsNil)
{
switch (key.CompareTo(node->KeyValuePair.Item1))
{
case 0: return &node->KeyValuePair.Item2;
case < 0:
node = node->Left;
break;
default:
node = node->Right;
break;
}
}
return null;
}
// Iterate in tree-order through a map, applying action to each KeyValuePair.
private static void IterateMap<TKey, TValue>(StdMap<TKey, TValue>* map, Action<TKey, TValue> action)
where TKey : unmanaged
where TValue : unmanaged
{
if (map == null || map->Count == 0)
return;
for (var node = map->SmallestValue; !node->IsNil; node = node->Next())
action(node->KeyValuePair.Item1, node->KeyValuePair.Item2);
}
}

View file

@ -6,24 +6,48 @@ using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Interop.Loader; namespace Penumbra.Interop.Loader;
public unsafe class ResourceHook : IDisposable public unsafe class ResourceService : IDisposable
{ {
public ResourceHook() private readonly PerformanceTracker _performance;
private readonly ResourceManagerService _resourceManager;
public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager)
{ {
_performance = performance;
_resourceManager = resourceManager;
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_getResourceSyncHook.Enable(); _getResourceSyncHook.Enable();
_getResourceAsyncHook.Enable(); _getResourceAsyncHook.Enable();
_resourceHandleDestructorHook.Enable(); _resourceHandleDestructorHook.Enable();
_incRefHook = Hook<ResourceHandlePrototype>.FromAddress(
(nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef,
ResourceHandleIncRefDetour);
_incRefHook.Enable();
_decRefHook = Hook<ResourceHandleDecRefPrototype>.FromAddress(
(nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef,
ResourceHandleDecRefDetour);
_decRefHook.Enable();
}
public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, ByteString path)
{
var hash = path.Crc32;
return GetResourceHandler(true, (ResourceManager*)_resourceManager.ResourceManagerAddress,
&category, &type, &hash, path.Path, null, false);
} }
public void Dispose() public void Dispose()
{ {
_getResourceSyncHook.Dispose(); _getResourceSyncHook.Dispose();
_getResourceAsyncHook.Dispose(); _getResourceAsyncHook.Dispose();
_resourceHandleDestructorHook.Dispose();
_incRefHook.Dispose();
_decRefHook.Dispose();
} }
#region GetResource #region GetResource
@ -33,24 +57,15 @@ public unsafe class ResourceHook : IDisposable
/// <param name="type">The resource type. Should not generally be changed.</param> /// <param name="type">The resource type. Should not generally be changed.</param>
/// <param name="hash">The resource hash. Should generally fit to the path.</param> /// <param name="hash">The resource hash. Should generally fit to the path.</param>
/// <param name="path">The path of the requested resource.</param> /// <param name="path">The path of the requested resource.</param>
/// <param name="parameters">Mainly used for SCD streaming.</param> /// <param name="parameters">Mainly used for SCD streaming, can be null.</param>
/// <param name="sync">Whether to request the resource synchronously or asynchronously.</param> /// <param name="sync">Whether to request the resource synchronously or asynchronously.</param>
public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref ByteString path, /// <param name="returnValue">The returned resource handle. If this is not null, calling original will be skipped. </param>
ref GetResourceParameters parameters, ref bool sync); public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path,
GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue);
/// <summary> <inheritdoc cref="GetResourcePreDelegate"/> <para/> /// <summary> <inheritdoc cref="GetResourcePreDelegate"/> <para/>
/// Subscribers should be exception-safe.</summary> /// Subscribers should be exception-safe.</summary>
public event GetResourcePreDelegate? GetResourcePre; public event GetResourcePreDelegate? ResourceRequested;
/// <summary>
/// The returned resource handle obtained from a resource request. Contains all the other information from the request.
/// </summary>
public delegate void GetResourcePostDelegate(ref ResourceHandle handle);
/// <summary> <inheritdoc cref="GetResourcePostDelegate"/> <para/>
/// Subscribers should be exception-safe.</summary>
public event GetResourcePostDelegate? GetResourcePost;
private delegate ResourceHandle* GetResourceSyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, private delegate ResourceHandle* GetResourceSyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId,
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams); ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams);
@ -79,15 +94,34 @@ public unsafe class ResourceHook : IDisposable
private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId,
ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk)
{ {
var byteString = new ByteString(path); using var performance = _performance.Measure(PerformanceType.GetResourceHandler);
GetResourcePre?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref byteString, ref *pGetResParams, ref isSync); if (!Utf8GamePath.FromPointer(path, out var gamePath))
var ret = isSync {
? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams) Penumbra.Log.Error("[ResourceService] Could not create GamePath from resource path.");
: _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams, isUnk); return isSync
GetResourcePost?.Invoke(ref *ret); ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams)
return ret; : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk);
}
ResourceHandle* returnValue = null;
ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, pGetResParams, ref isSync,
ref returnValue);
if (returnValue != null)
return returnValue;
return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk);
} }
/// <summary> Call the original GetResource function. </summary>
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, ByteString path,
GetResourceParameters* resourceParameters = null, bool unk = false)
=> sync
? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
resourceParameters)
: _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
resourceParameters,
unk);
#endregion #endregion
private delegate IntPtr ResourceHandlePrototype(ResourceHandle* handle); private delegate IntPtr ResourceHandlePrototype(ResourceHandle* handle);
@ -96,9 +130,8 @@ public unsafe class ResourceHook : IDisposable
/// <summary> Invoked before a resource handle reference count is incremented. </summary> /// <summary> Invoked before a resource handle reference count is incremented. </summary>
/// <param name="handle">The resource handle.</param> /// <param name="handle">The resource handle.</param>
/// <param name="callOriginal">Whether to call original after the event has run.</param> /// <param name="returnValue">The return value to use, setting this value will skip calling original.</param>
/// <param name="returnValue">The return value to use if not calling original.</param> public delegate void ResourceHandleIncRefDelegate(ResourceHandle* handle, ref nint? returnValue);
public delegate void ResourceHandleIncRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref nint returnValue);
/// <summary> /// <summary>
/// <inheritdoc cref="ResourceHandleIncRefDelegate"/> <para/> /// <inheritdoc cref="ResourceHandleIncRefDelegate"/> <para/>
@ -106,21 +139,19 @@ public unsafe class ResourceHook : IDisposable
/// </summary> /// </summary>
public event ResourceHandleIncRefDelegate? ResourceHandleIncRef; public event ResourceHandleIncRefDelegate? ResourceHandleIncRef;
public nint IncRef(ref ResourceHandle handle) /// <summary>
{ /// Call the game function that increases the reference counter of a resource handle.
fixed (ResourceHandle* ptr = &handle) /// </summary>
{ public nint IncRef(ResourceHandle* handle)
return _incRefHook.Original(ptr); => _incRefHook.OriginalDisposeSafe(handle);
}
}
private readonly Hook<ResourceHandlePrototype> _incRefHook; private readonly Hook<ResourceHandlePrototype> _incRefHook;
private nint ResourceHandleIncRefDetour(ResourceHandle* handle) private nint ResourceHandleIncRefDetour(ResourceHandle* handle)
{ {
var callOriginal = true; nint? ret = null;
var ret = IntPtr.Zero; ResourceHandleIncRef?.Invoke(handle, ref ret);
ResourceHandleIncRef?.Invoke(ref *handle, ref callOriginal, ref ret); return ret ?? _incRefHook.OriginalDisposeSafe(handle);
return callOriginal ? _incRefHook.Original(handle) : ret;
} }
#endregion #endregion
@ -129,9 +160,8 @@ public unsafe class ResourceHook : IDisposable
/// <summary> Invoked before a resource handle reference count is decremented. </summary> /// <summary> Invoked before a resource handle reference count is decremented. </summary>
/// <param name="handle">The resource handle.</param> /// <param name="handle">The resource handle.</param>
/// <param name="callOriginal">Whether to call original after the event has run.</param> /// <param name="returnValue">The return value to use, setting this value will skip calling original.</param>
/// <param name="returnValue">The return value to use if not calling original.</param> public delegate void ResourceHandleDecRefDelegate(ResourceHandle* handle, ref byte? returnValue);
public delegate void ResourceHandleDecRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref byte returnValue);
/// <summary> /// <summary>
/// <inheritdoc cref="ResourceHandleDecRefDelegate"/> <para/> /// <inheritdoc cref="ResourceHandleDecRefDelegate"/> <para/>
@ -139,29 +169,29 @@ public unsafe class ResourceHook : IDisposable
/// </summary> /// </summary>
public event ResourceHandleDecRefDelegate? ResourceHandleDecRef; public event ResourceHandleDecRefDelegate? ResourceHandleDecRef;
public byte DecRef(ref ResourceHandle handle) /// <summary>
{ /// Call the original game function that decreases the reference counter of a resource handle.
fixed (ResourceHandle* ptr = &handle) /// </summary>
{ public byte DecRef(ResourceHandle* handle)
return _incRefHook.Original(ptr); => _decRefHook.OriginalDisposeSafe(handle);
}
}
private delegate byte ResourceHandleDecRefPrototype(ResourceHandle* handle); private delegate byte ResourceHandleDecRefPrototype(ResourceHandle* handle);
private readonly Hook<ResourceHandleDecRefPrototype> _decRefHook; private readonly Hook<ResourceHandleDecRefPrototype> _decRefHook;
private byte ResourceHandleDecRefDetour(ResourceHandle* handle) private byte ResourceHandleDecRefDetour(ResourceHandle* handle)
{ {
var callOriginal = true; byte? ret = null;
var ret = byte.MinValue; ResourceHandleDecRef?.Invoke(handle, ref ret);
ResourceHandleDecRef?.Invoke(ref *handle, ref callOriginal, ref ret); return ret ?? _decRefHook.OriginalDisposeSafe(handle);
return callOriginal ? _decRefHook!.Original(handle) : ret;
} }
#endregion #endregion
#region Destructor
/// <summary> Invoked before a resource handle is destructed. </summary> /// <summary> Invoked before a resource handle is destructed. </summary>
/// <param name="handle">The resource handle.</param> /// <param name="handle">The resource handle.</param>
public delegate void ResourceHandleDtorDelegate(ref ResourceHandle handle); public delegate void ResourceHandleDtorDelegate(ResourceHandle* handle);
/// <summary> /// <summary>
/// <inheritdoc cref="ResourceHandleDtorDelegate"/> <para/> /// <inheritdoc cref="ResourceHandleDtorDelegate"/> <para/>
@ -174,8 +204,8 @@ public unsafe class ResourceHook : IDisposable
private nint ResourceHandleDestructorDetour(ResourceHandle* handle) private nint ResourceHandleDestructorDetour(ResourceHandle* handle)
{ {
ResourceHandleDestructor?.Invoke(ref *handle); ResourceHandleDestructor?.Invoke(handle);
return _resourceHandleDestructorHook!.Original(handle); return _resourceHandleDestructorHook.OriginalDisposeSafe(handle);
} }
#endregion #endregion

View file

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader;
public unsafe class TexMdlService
{
/// <summary> Custom ulong flag to signal our files as opposed to SE files. </summary>
public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF);
/// <summary>
/// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files,
/// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes.
/// </summary>
public IReadOnlySet<ulong> CustomFileCrc
=> _customFileCrc;
public TexMdlService()
{
SignatureHelper.Initialise(this);
_checkFileStateHook.Enable();
_loadTexFileExternHook.Enable();
_loadMdlFileExternHook.Enable();
}
/// <summary> Add CRC64 if the given file is a model or texture file and has an associated path. </summary>
public void AddCrc(ResourceType type, FullPath? path)
{
if (path.HasValue && type is ResourceType.Mdl or ResourceType.Tex)
_customFileCrc.Add(path.Value.Crc64);
}
/// <summary> Add a fixed CRC64 value. </summary>
public void AddCrc(ulong crc64)
=> _customFileCrc.Add(crc64);
public void Dispose()
{
_checkFileStateHook.Dispose();
_loadTexFileExternHook.Dispose();
_loadMdlFileExternHook.Dispose();
}
private readonly HashSet<ulong> _customFileCrc = new();
private delegate IntPtr CheckFileStatePrototype(IntPtr unk1, ulong crc64);
[Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))]
private readonly Hook<CheckFileStatePrototype> _checkFileStateHook = null!;
/// <summary>
/// The function that checks a files CRC64 to determine whether it is 'protected'.
/// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag.
/// </summary>
private IntPtr CheckFileStateDetour(IntPtr ptr, ulong crc64)
=> _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64);
private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3);
/// <summary> We use the local functions for our own files in the extern hook. </summary>
[Signature(Sigs.LoadTexFileLocal)]
private readonly LoadTexFileLocalDelegate _loadTexFileLocal = null!;
private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2);
/// <summary> We use the local functions for our own files in the extern hook. </summary>
[Signature(Sigs.LoadMdlFileLocal)]
private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!;
private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4);
[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))]
private readonly Hook<LoadTexFileExternPrototype> _loadTexFileExternHook = null!;
/// <summary> We hook the extern functions to just return the local one if given the custom flag as last argument. </summary>
private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr)
=> ptr.Equals(CustomFileFlag)
? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3)
: _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr);
public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3);
[Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))]
private readonly Hook<LoadMdlFileExternPrototype> _loadMdlFileExternHook = null!;
/// <summary> We hook the extern functions to just return the local one if given the custom flag as last argument. </summary>
private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr)
=> ptr.Equals(CustomFileFlag)
? _loadMdlFileLocal.Invoke(resourceHandle, unk1, unk2)
: _loadMdlFileExternHook.Original(resourceHandle, unk1, unk2, ptr);
}

View file

@ -18,8 +18,6 @@ public unsafe class ResidentResourceManager
[Signature( Sigs.UnloadPlayerResources )] [Signature( Sigs.UnloadPlayerResources )]
public readonly ResidentResourceDelegate UnloadPlayerResources = null!; public readonly ResidentResourceDelegate UnloadPlayerResources = null!;
public Structs.ResidentResourceManager* Address public Structs.ResidentResourceManager* Address
=> *_residentResourceManagerAddress; => *_residentResourceManagerAddress;

View file

@ -144,7 +144,7 @@ public unsafe partial class PathResolver
{ {
_lastCreatedCollection = IdentifyCollection(LastGameObject, false); _lastCreatedCollection = IdentifyCollection(LastGameObject, false);
// Change the transparent or 1.0 Decal if necessary. // Change the transparent or 1.0 Decal if necessary.
var decal = new CharacterUtility.DecalReverter(_lastCreatedCollection.ModCollection, UsesDecal(a, c)); var decal = new CharacterUtility.DecalReverter(Penumbra.ResourceService, _lastCreatedCollection.ModCollection, UsesDecal(a, c));
// Change the rsp parameters. // Change the rsp parameters.
meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal); meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal);
try try

View file

@ -194,7 +194,7 @@ public unsafe partial class PathResolver
_inChangeCustomize = true; _inChangeCustomize = true;
var resolveData = GetResolveData( human ); var resolveData = GetResolveData( human );
using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile();
using var decals = new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); using var decals = new CharacterUtility.DecalReverter( Penumbra.ResourceService, resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) );
var ret = _changeCustomize.Original( human, data, skipEquipment ); var ret = _changeCustomize.Original( human, data, skipEquipment );
_inChangeCustomize = false; _inChangeCustomize = false;
return ret; return ret;

View file

@ -2,6 +2,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
@ -22,28 +23,28 @@ public unsafe partial class PathResolver
// Materials and avfx do contain their own paths to textures and shader packages or atex respectively. // Materials and avfx do contain their own paths to textures and shader packages or atex respectively.
// Those are loaded synchronously. // Those are loaded synchronously.
// Thus, we need to ensure the correct files are loaded when a material is loaded. // Thus, we need to ensure the correct files are loaded when a material is loaded.
public class SubfileHelper : IDisposable, IReadOnlyCollection< KeyValuePair< IntPtr, ResolveData > > public class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<IntPtr, ResolveData>>
{ {
private readonly ResourceLoader _loader; private readonly ResourceLoader _loader;
private readonly GameEventManager _events; private readonly GameEventManager _events;
private readonly ThreadLocal< ResolveData > _mtrlData = new(() => ResolveData.Invalid); private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
private readonly ThreadLocal< ResolveData > _avfxData = new(() => ResolveData.Invalid); private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
private readonly ConcurrentDictionary< IntPtr, ResolveData > _subFileCollection = new(); private readonly ConcurrentDictionary<IntPtr, ResolveData> _subFileCollection = new();
public SubfileHelper( ResourceLoader loader, GameEventManager events ) public SubfileHelper(ResourceLoader loader, GameEventManager events)
{ {
SignatureHelper.Initialise( this ); SignatureHelper.Initialise(this);
_loader = loader; _loader = loader;
_events = events; _events = events;
} }
// Check specifically for shpk and tex files whether we are currently in a material load. // Check specifically for shpk and tex files whether we are currently in a material load.
public bool HandleSubFiles( ResourceType type, out ResolveData collection ) public bool HandleSubFiles(ResourceType type, out ResolveData collection)
{ {
switch( type ) switch (type)
{ {
case ResourceType.Tex when _mtrlData.Value.Valid: case ResourceType.Tex when _mtrlData.Value.Valid:
case ResourceType.Shpk when _mtrlData.Value.Valid: case ResourceType.Shpk when _mtrlData.Value.Valid:
@ -62,22 +63,20 @@ public unsafe partial class PathResolver
} }
// Materials need to be set per collection so they can load their textures independently from each other. // Materials need to be set per collection so they can load their textures independently from each other.
public static void HandleCollection( ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved,
out (FullPath?, ResolveData) data ) out (FullPath?, ResolveData) data)
{ {
if( nonDefault ) if (nonDefault)
{ switch (type)
switch( type )
{ {
case ResourceType.Mtrl: case ResourceType.Mtrl:
case ResourceType.Avfx: case ResourceType.Avfx:
var fullPath = new FullPath( $"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}" ); var fullPath = new FullPath($"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}");
data = ( fullPath, resolveData ); data = (fullPath, resolveData);
return; return;
} }
}
data = ( resolved, resolveData ); data = (resolved, resolveData);
} }
public void Enable() public void Enable()
@ -85,9 +84,8 @@ public unsafe partial class PathResolver
_loadMtrlShpkHook.Enable(); _loadMtrlShpkHook.Enable();
_loadMtrlTexHook.Enable(); _loadMtrlTexHook.Enable();
_apricotResourceLoadHook.Enable(); _apricotResourceLoadHook.Enable();
_loader.ResourceLoadCustomization += SubfileLoadHandler; _loader.ResourceLoaded += SubfileContainerRequested;
_loader.ResourceLoaded += SubfileContainerRequested; _events.ResourceHandleDestructor += ResourceDestroyed;
_events.ResourceHandleDestructor += ResourceDestroyed;
} }
public void Disable() public void Disable()
@ -95,9 +93,8 @@ public unsafe partial class PathResolver
_loadMtrlShpkHook.Disable(); _loadMtrlShpkHook.Disable();
_loadMtrlTexHook.Disable(); _loadMtrlTexHook.Disable();
_apricotResourceLoadHook.Disable(); _apricotResourceLoadHook.Disable();
_loader.ResourceLoadCustomization -= SubfileLoadHandler; _loader.ResourceLoaded -= SubfileContainerRequested;
_loader.ResourceLoaded -= SubfileContainerRequested; _events.ResourceHandleDestructor -= ResourceDestroyed;
_events.ResourceHandleDestructor -= ResourceDestroyed;
} }
public void Dispose() public void Dispose()
@ -108,105 +105,77 @@ public unsafe partial class PathResolver
_apricotResourceLoadHook.Dispose(); _apricotResourceLoadHook.Dispose();
} }
private void SubfileContainerRequested( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData ) private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolveData)
{ {
switch( handle->FileType ) switch (handle->FileType)
{ {
case ResourceType.Mtrl: case ResourceType.Mtrl:
case ResourceType.Avfx: case ResourceType.Avfx:
if( handle->FileSize == 0 ) if (handle->FileSize == 0)
{ _subFileCollection[(nint)handle] = resolveData;
_subFileCollection[ ( IntPtr )handle ] = resolveData;
}
break; break;
} }
} }
private void ResourceDestroyed( ResourceHandle* handle ) private void ResourceDestroyed(ResourceHandle* handle)
=> _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); => _subFileCollection.TryRemove((IntPtr)handle, out _);
// We need to set the correct collection for the actual material path that is loaded private delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle);
// before actually loading the file.
public static bool SubfileLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, [Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))]
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) private readonly Hook<LoadMtrlFilesDelegate> _loadMtrlTexHook = null!;
private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle)
{ {
switch( fileDescriptor->ResourceHandle->FileType ) using var performance = Penumbra.Performance.Measure(PerformanceType.LoadTextures);
{
case ResourceType.Mtrl:
// Force isSync = true for this call. I don't really understand why,
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true );
return true;
case ResourceType.Avfx:
// Do nothing special right now.
ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, isSync );
return true;
default:
ret = 0;
return false;
}
}
private delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle );
[Signature( Sigs.LoadMtrlTex, DetourName = nameof( LoadMtrlTexDetour ) )]
private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlTexHook = null!;
private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadTextures );
var old = _mtrlData.Value; var old = _mtrlData.Value;
_mtrlData.Value = LoadFileHelper( mtrlResourceHandle ); _mtrlData.Value = LoadFileHelper(mtrlResourceHandle);
var ret = _loadMtrlTexHook.Original( mtrlResourceHandle ); var ret = _loadMtrlTexHook.Original(mtrlResourceHandle);
_mtrlData.Value = old; _mtrlData.Value = old;
return ret; return ret;
} }
[Signature( Sigs.LoadMtrlShpk, DetourName = nameof( LoadMtrlShpkDetour ) )] [Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))]
private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlShpkHook = null!; private readonly Hook<LoadMtrlFilesDelegate> _loadMtrlShpkHook = null!;
private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) private byte LoadMtrlShpkDetour(IntPtr mtrlResourceHandle)
{ {
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadShaders ); using var performance = Penumbra.Performance.Measure(PerformanceType.LoadShaders);
var old = _mtrlData.Value; var old = _mtrlData.Value;
_mtrlData.Value = LoadFileHelper( mtrlResourceHandle ); _mtrlData.Value = LoadFileHelper(mtrlResourceHandle);
var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle ); var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle);
_mtrlData.Value = old; _mtrlData.Value = old;
return ret; return ret;
} }
private ResolveData LoadFileHelper( IntPtr resourceHandle ) private ResolveData LoadFileHelper(IntPtr resourceHandle)
{ {
if( resourceHandle == IntPtr.Zero ) if (resourceHandle == IntPtr.Zero)
{
return ResolveData.Invalid; return ResolveData.Invalid;
}
return _subFileCollection.TryGetValue( resourceHandle, out var c ) ? c : ResolveData.Invalid; return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid;
} }
private delegate byte ApricotResourceLoadDelegate( IntPtr handle, IntPtr unk1, byte unk2 ); private delegate byte ApricotResourceLoadDelegate(IntPtr handle, IntPtr unk1, byte unk2);
[Signature( Sigs.ApricotResourceLoad, DetourName = nameof( ApricotResourceLoadDetour ) )] [Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))]
private readonly Hook< ApricotResourceLoadDelegate > _apricotResourceLoadHook = null!; private readonly Hook<ApricotResourceLoadDelegate> _apricotResourceLoadHook = null!;
private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) private byte ApricotResourceLoadDetour(IntPtr handle, IntPtr unk1, byte unk2)
{ {
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadApricotResources ); using var performance = Penumbra.Performance.Measure(PerformanceType.LoadApricotResources);
var old = _avfxData.Value; var old = _avfxData.Value;
_avfxData.Value = LoadFileHelper( handle ); _avfxData.Value = LoadFileHelper(handle);
var ret = _apricotResourceLoadHook.Original( handle, unk1, unk2 ); var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2);
_avfxData.Value = old; _avfxData.Value = old;
return ret; return ret;
} }
public IEnumerator< KeyValuePair< IntPtr, ResolveData > > GetEnumerator() public IEnumerator<KeyValuePair<IntPtr, ResolveData>> GetEnumerator()
=> _subFileCollection.GetEnumerator(); => _subFileCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
@ -221,4 +190,4 @@ public unsafe partial class PathResolver
internal ResolveData AvfxData internal ResolveData AvfxData
=> _avfxData.Value; => _avfxData.Value;
} }
} }

View file

@ -9,6 +9,7 @@ using OtterGui.Classes;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
using Penumbra.Interop.Structs;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -54,7 +55,7 @@ public partial class PathResolver : IDisposable
} }
// The modified resolver that handles game path resolving. // The modified resolver that handles game path resolving.
private bool CharacterResolver(Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data) public (FullPath?, ResolveData) CharacterResolver(Utf8GamePath gamePath, ResourceType type)
{ {
using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver); using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver);
// Check if the path was marked for a specific collection, // Check if the path was marked for a specific collection,
@ -77,8 +78,8 @@ public partial class PathResolver : IDisposable
// so that the functions loading tex and shpk can find that path and use its collection. // so that the functions loading tex and shpk can find that path and use its collection.
// We also need to handle defaulted materials against a non-default collection. // We also need to handle defaulted materials against a non-default collection.
var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; var path = resolved == null ? gamePath.Path : resolved.Value.InternalName;
SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out data); SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair);
return true; return pair;
} }
public void Enable() public void Enable()
@ -95,7 +96,6 @@ public partial class PathResolver : IDisposable
_meta.Enable(); _meta.Enable();
_subFiles.Enable(); _subFiles.Enable();
_loader.ResolvePathCustomization += CharacterResolver;
Penumbra.Log.Debug("Character Path Resolver enabled."); Penumbra.Log.Debug("Character Path Resolver enabled.");
} }
@ -113,7 +113,6 @@ public partial class PathResolver : IDisposable
_meta.Disable(); _meta.Disable();
_subFiles.Disable(); _subFiles.Disable();
_loader.ResolvePathCustomization -= CharacterResolver;
Penumbra.Log.Debug("Character Path Resolver disabled."); Penumbra.Log.Debug("Character Path Resolver disabled.");
} }

View file

@ -1,12 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using System.Diagnostics.CodeAnalysis;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Meta.Manager; namespace Penumbra.Meta.Manager;
@ -15,7 +12,6 @@ public partial class MetaManager
{ {
private readonly Dictionary< Utf8GamePath, ImcFile > _imcFiles = new(); private readonly Dictionary< Utf8GamePath, ImcFile > _imcFiles = new();
private readonly List< ImcManipulation > _imcManipulations = new(); private readonly List< ImcManipulation > _imcManipulations = new();
private static int _imcManagerCount;
public void SetImcFiles() public void SetImcFiles()
{ {
@ -132,52 +128,11 @@ public partial class MetaManager
_imcFiles.Clear(); _imcFiles.Clear();
_imcManipulations.Clear(); _imcManipulations.Clear();
RestoreImcDelegate();
}
private static unsafe void SetupImcDelegate()
{
if( _imcManagerCount++ == 0 )
{
Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler;
}
}
private static unsafe void RestoreImcDelegate()
{
if( --_imcManagerCount == 0 )
{
Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler;
}
} }
private FullPath CreateImcPath( Utf8GamePath path ) private FullPath CreateImcPath( Utf8GamePath path )
=> new($"|{_collection.Name}_{_collection.ChangeCounter}|{path}"); => new($"|{_collection.Name}_{_collection.ChangeCounter}|{path}");
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
private static unsafe bool ImcLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, => _imcFiles.TryGetValue(path, out file);
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret )
{
ret = 0;
if( fileDescriptor->ResourceHandle->FileType != ResourceType.Imc )
{
return false;
}
Penumbra.Log.Verbose( $"Using ImcLoadHandler for path {path}." );
ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
var lastUnderscore = split.LastIndexOf( ( byte )'_' );
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString();
if( ( Penumbra.TempCollections.CollectionByName( name, out var collection )
|| Penumbra.CollectionManager.ByName( name, out collection ) )
&& collection.HasCache
&& collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) )
{
Penumbra.Log.Debug( $"Loaded {path} from file and replaced with IMC from collection {collection.AnonymizedName}." );
file.Replace( fileDescriptor->ResourceHandle );
}
return true;
}
} }

View file

@ -35,7 +35,6 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM
public MetaManager( ModCollection collection ) public MetaManager( ModCollection collection )
{ {
_collection = collection; _collection = collection;
SetupImcDelegate();
if( !Penumbra.CharacterUtility.Ready ) if( !Penumbra.CharacterUtility.Ready )
{ {
Penumbra.CharacterUtility.LoadingFinished += ApplyStoredManipulations; Penumbra.CharacterUtility.LoadingFinished += ApplyStoredManipulations;

View file

@ -83,32 +83,37 @@ public class Penumbra : IDalamudPlugin
private readonly PenumbraNew _tmp; private readonly PenumbraNew _tmp;
public static ItemData ItemData { get; private set; } = null!; public static ItemData ItemData { get; private set; } = null!;
// TODO
public static ResourceManagerService ResourceManagerService { get; private set; } = null!;
public static CharacterResolver CharacterResolver { get; private set; } = null!;
public static ResourceService ResourceService { get; private set; } = null!;
public Penumbra(DalamudPluginInterface pluginInterface) public Penumbra(DalamudPluginInterface pluginInterface)
{ {
Log = PenumbraNew.Log; Log = PenumbraNew.Log;
_tmp = new PenumbraNew(pluginInterface);
Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
_tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
GameEvents = _tmp.Services.GetRequiredService<GameEventManager>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Framework = _tmp.Services.GetRequiredService<FrameworkManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>();
ItemData = _tmp.Services.GetRequiredService<ItemService>().AwaitedService;
Dalamud = _tmp.Services.GetRequiredService<DalamudServices>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
try try
{ {
ResourceLoader = new ResourceLoader(this); _tmp = new PenumbraNew(pluginInterface);
ResourceLoader.EnableHooks(); Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
_resourceWatcher = new ResourceWatcher(ResourceLoader); ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
ResidentResources = new ResidentResourceManager(); _tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
GameEvents = _tmp.Services.GetRequiredService<GameEventManager>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Framework = _tmp.Services.GetRequiredService<FrameworkManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>();
ItemData = _tmp.Services.GetRequiredService<ItemService>().AwaitedService;
Dalamud = _tmp.Services.GetRequiredService<DalamudServices>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
ResourceManagerService = _tmp.Services.GetRequiredService<ResourceManagerService>();
_tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Mods, () => _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Mods, () =>
{ {
ModManager = new Mod.Manager(Config.ModDirectory); ModManager = new Mod.Manager(Config.ModDirectory);
@ -126,19 +131,20 @@ public class Penumbra : IDalamudPlugin
ModFileSystem = ModFileSystem.Load(); ModFileSystem = ModFileSystem.Load();
ObjectReloader = new ObjectReloader(); ObjectReloader = new ObjectReloader();
ResourceService = _tmp.Services.GetRequiredService<ResourceService>();
ResourceLoader = new ResourceLoader(ResourceService, _tmp.Services.GetRequiredService<FileReadService>(), _tmp.Services.GetRequiredService<TexMdlService>(), _tmp.Services.GetRequiredService<CreateFileWHook>());
PathResolver = new PathResolver(_tmp.Services.GetRequiredService<StartTracker>(), _tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<GameEventManager>(), ResourceLoader); PathResolver = new PathResolver(_tmp.Services.GetRequiredService<StartTracker>(), _tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<GameEventManager>(), ResourceLoader);
CharacterResolver = new CharacterResolver(Config, CollectionManager, TempCollections, ResourceLoader, PathResolver);
_resourceWatcher = new ResourceWatcher(Config, ResourceService, ResourceLoader);
SetupInterface(); SetupInterface();
if (Config.EnableMods) if (Config.EnableMods)
{ {
ResourceLoader.EnableReplacements();
PathResolver.Enable(); PathResolver.Enable();
} }
if (Config.DebugMode)
ResourceLoader.EnableDebug();
using (var tApi = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api)) using (var tApi = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api))
{ {
Api = new PenumbraApi(_tmp.Services.GetRequiredService<CommunicatorService>(), this); Api = new PenumbraApi(_tmp.Services.GetRequiredService<CommunicatorService>(), this);
@ -171,7 +177,7 @@ public class Penumbra : IDalamudPlugin
{ {
using var tInterface = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Interface); using var tInterface = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Interface);
var changelog = ConfigWindow.CreateChangelog(); var changelog = ConfigWindow.CreateChangelog();
var cfg = new ConfigWindow(_tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<StartTracker>(), this, _resourceWatcher) var cfg = new ConfigWindow(_tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<StartTracker>(), _tmp.Services.GetRequiredService<FontReloader>(), this, _resourceWatcher)
{ {
IsOpen = Config.DebugMode, IsOpen = Config.DebugMode,
}; };
@ -225,7 +231,6 @@ public class Penumbra : IDalamudPlugin
Config.EnableMods = enabled; Config.EnableMods = enabled;
if (enabled) if (enabled)
{ {
ResourceLoader.EnableReplacements();
PathResolver.Enable(); PathResolver.Enable();
if (CharacterUtility.Ready) if (CharacterUtility.Ready)
{ {
@ -236,7 +241,6 @@ public class Penumbra : IDalamudPlugin
} }
else else
{ {
ResourceLoader.DisableReplacements();
PathResolver.Disable(); PathResolver.Disable();
if (CharacterUtility.Ready) if (CharacterUtility.Ready)
{ {
@ -293,7 +297,7 @@ public class Penumbra : IDalamudPlugin
ObjectReloader?.Dispose(); ObjectReloader?.Dispose();
ModFileSystem?.Dispose(); ModFileSystem?.Dispose();
CollectionManager?.Dispose(); CollectionManager?.Dispose();
PathResolver?.Dispose(); CharacterResolver?.Dispose(); // disposes PathResolver, TODO
_resourceWatcher?.Dispose(); _resourceWatcher?.Dispose();
ResourceLoader?.Dispose(); ResourceLoader?.Dispose();
GameEvents?.Dispose(); GameEvents?.Dispose();

View file

@ -8,8 +8,10 @@ using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Interop; using Penumbra.Interop;
using Penumbra.Interop.Loader;
using Penumbra.Interop.Resolver; using Penumbra.Interop.Resolver;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra; namespace Penumbra;
@ -53,9 +55,15 @@ public class PenumbraNew
.AddSingleton<FrameworkManager>() .AddSingleton<FrameworkManager>()
.AddSingleton<MetaFileManager>() .AddSingleton<MetaFileManager>()
.AddSingleton<CutsceneCharacters>() .AddSingleton<CutsceneCharacters>()
.AddSingleton<CharacterUtility>(); .AddSingleton<CharacterUtility>()
.AddSingleton<ResourceManagerService>()
.AddSingleton<ResourceService>()
.AddSingleton<FileReadService>()
.AddSingleton<TexMdlService>()
.AddSingleton<CreateFileWHook>()
.AddSingleton<ResidentResourceManager>()
.AddSingleton<FontReloader>();
// Add Configuration // Add Configuration
services.AddTransient<ConfigMigrationService>() services.AddTransient<ConfigMigrationService>()
.AddSingleton<Configuration>(); .AddSingleton<Configuration>();

View file

@ -61,8 +61,6 @@ public partial class ConfigWindow
DrawDebugTabGeneral(); DrawDebugTabGeneral();
DrawPerformanceTab(); DrawPerformanceTab();
ImGui.NewLine(); ImGui.NewLine();
DrawDebugTabReplacedResources();
ImGui.NewLine();
DrawPathResolverDebug(); DrawPathResolverDebug();
ImGui.NewLine(); ImGui.NewLine();
DrawActorsDebug(); DrawActorsDebug();
@ -134,53 +132,6 @@ public partial class ConfigWindow
Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName ); Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName );
} }
// Draw all resources currently replaced by Penumbra and (if existing) the resources they replace.
// Resources are collected by iterating through the
private static unsafe void DrawDebugTabReplacedResources()
{
if( !ImGui.CollapsingHeader( "Replaced Resources" ) )
{
return;
}
Penumbra.ResourceLoader.UpdateDebugInfo();
if( Penumbra.ResourceLoader.DebugList.Count == 0 )
{
return;
}
using var table = Table( "##ReplacedResources", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
-Vector2.UnitX );
if( !table )
{
return;
}
foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() )
{
if( data.ManipulatedPath.Crc64 == 0 )
{
continue;
}
var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount;
var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount;
ImGui.TableNextColumn();
ImGui.TextUnformatted( data.ManipulatedPath.ToString() );
ImGui.TableNextColumn();
ImGui.TextUnformatted( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) );
ImGui.TableNextColumn();
ImGui.TextUnformatted( refCountManip.ToString() );
ImGui.TableNextColumn();
ImGui.TextUnformatted( data.OriginalPath.ToString() );
ImGui.TableNextColumn();
ImGui.TextUnformatted( ( ( ulong )data.OriginalResource ).ToString( "X" ) );
ImGui.TableNextColumn();
ImGui.TextUnformatted( refCountOrig.ToString() );
}
}
private static unsafe void DrawActorsDebug() private static unsafe void DrawActorsDebug()
{ {
if( !ImGui.CollapsingHeader( "Actors" ) ) if( !ImGui.CollapsingHeader( "Actors" ) )
@ -635,7 +586,7 @@ public partial class ConfigWindow
return; return;
} }
ResourceLoader.IterateResources( ( _, r ) => Penumbra.ResourceManagerService.IterateResources( ( _, r ) =>
{ {
if( r->RefCount < 10000 ) if( r->RefCount < 10000 )
{ {

View file

@ -10,7 +10,6 @@ using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Interop.Loader;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -46,13 +45,13 @@ public partial class ConfigWindow
unsafe unsafe
{ {
ResourceLoader.IterateGraphs( DrawCategoryContainer ); Penumbra.ResourceManagerService.IterateGraphs( DrawCategoryContainer );
} }
ImGui.NewLine(); ImGui.NewLine();
unsafe unsafe
{ {
ImGui.TextUnformatted( $"Static Address: 0x{( ulong )ResourceLoader.ResourceManager:X} (+0x{( ulong )ResourceLoader.ResourceManager - ( ulong )DalamudServices.SigScanner.Module.BaseAddress:X})" ); ImGui.TextUnformatted( $"Static Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress:X} (+0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress - ( ulong )DalamudServices.SigScanner.Module.BaseAddress:X})" );
ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )*ResourceLoader.ResourceManager:X}" ); ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManager:X}" );
} }
} }
@ -82,7 +81,7 @@ public partial class ConfigWindow
ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth ); ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth );
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
ResourceLoader.IterateResourceMap( map, ( hash, r ) => Penumbra.ResourceManagerService.IterateResourceMap( map, ( hash, r ) =>
{ {
// Filter unwanted names. // Filter unwanted names.
if( _resourceManagerFilter.Length != 0 if( _resourceManagerFilter.Length != 0
@ -129,7 +128,7 @@ public partial class ConfigWindow
if( tree ) if( tree )
{ {
SetTableWidths(); SetTableWidths();
ResourceLoader.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); Penumbra.ResourceManagerService.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) );
} }
} }

View file

@ -66,15 +66,6 @@ public partial class ConfigWindow
var tmp = Penumbra.Config.DebugMode; var tmp = Penumbra.Config.DebugMode;
if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode )
{ {
if( tmp )
{
Penumbra.ResourceLoader.EnableDebug();
}
else
{
Penumbra.ResourceLoader.DisableDebug();
}
Penumbra.Config.DebugMode = tmp; Penumbra.Config.DebugMode = tmp;
Penumbra.Config.Save(); Penumbra.Config.Save();
} }
@ -95,11 +86,11 @@ public partial class ConfigWindow
+ "You usually should not need to do this." ); + "You usually should not need to do this." );
} }
private static void DrawReloadFontsButton() private void DrawReloadFontsButton()
{ {
if( ImGuiUtil.DrawDisabledButton( "Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !FontReloader.Valid ) ) if( ImGuiUtil.DrawDisabledButton( "Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !_fontReloader.Valid ) )
{ {
FontReloader.Reload(); _fontReloader.Reload();
} }
} }

View file

@ -11,6 +11,7 @@ using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Interop;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -22,11 +23,14 @@ public partial class ConfigWindow
{ {
public const int RootDirectoryMaxLength = 64; public const int RootDirectoryMaxLength = 64;
private readonly ConfigWindow _window; private readonly ConfigWindow _window;
private readonly FontReloader _fontReloader;
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Settings"u8; => "Settings"u8;
public SettingsTab( ConfigWindow window ) public SettingsTab( ConfigWindow window, FontReloader fontReloader )
=> _window = window; {
_window = window;
_fontReloader = fontReloader;
}
public void DrawHeader() public void DrawHeader()
{ {

View file

@ -7,6 +7,7 @@ using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Interop;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -35,14 +36,14 @@ public sealed partial class ConfigWindow : Window, IDisposable
public void SelectMod(Mod mod) public void SelectMod(Mod mod)
=> _selector.SelectByValue(mod); => _selector.SelectByValue(mod);
public ConfigWindow(CommunicatorService communicator, StartTracker timer, Penumbra penumbra, ResourceWatcher watcher) public ConfigWindow(CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, Penumbra penumbra, ResourceWatcher watcher)
: base(GetLabel()) : base(GetLabel())
{ {
_penumbra = penumbra; _penumbra = penumbra;
_resourceWatcher = watcher; _resourceWatcher = watcher;
ModEditPopup = new ModEditWindow(communicator); ModEditPopup = new ModEditWindow(communicator);
_settingsTab = new SettingsTab(this); _settingsTab = new SettingsTab(this, fontReloader);
_selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem); _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem);
_modPanel = new ModPanel(this); _modPanel = new ModPanel(this);
_modsTab = new ModsTab(_selector, _modPanel, _penumbra); _modsTab = new ModsTab(_selector, _modPanel, _penumbra);

View file

@ -1,12 +1,16 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Interface; using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using ImGuiNET; using ImGuiNET;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
@ -19,38 +23,38 @@ public partial class ResourceWatcher : IDisposable, ITab
{ {
public const int DefaultMaxEntries = 1024; public const int DefaultMaxEntries = 1024;
private readonly ResourceLoader _loader; private readonly Configuration _config;
private readonly List< Record > _records = new(); private readonly ResourceService _resources;
private readonly ConcurrentQueue< Record > _newRecords = new(); private readonly ResourceLoader _loader;
private readonly Table _table; private readonly List<Record> _records = new();
private bool _writeToLog; private readonly ConcurrentQueue<Record> _newRecords = new();
private bool _isEnabled; private readonly Table _table;
private string _logFilter = string.Empty; private string _logFilter = string.Empty;
private Regex? _logRegex; private Regex? _logRegex;
private int _maxEntries; private int _newMaxEntries;
private int _newMaxEntries;
public unsafe ResourceWatcher( ResourceLoader loader ) public unsafe ResourceWatcher(Configuration config, ResourceService resources, ResourceLoader loader)
{ {
_loader = loader; _config = config;
_table = new Table( _records ); _resources = resources;
_loader.ResourceRequested += OnResourceRequested; _loader = loader;
_loader.ResourceLoaded += OnResourceLoaded; _table = new Table(_records);
_loader.FileLoaded += OnFileLoaded; _resources.ResourceRequested += OnResourceRequested;
UpdateFilter( Penumbra.Config.ResourceLoggingFilter, false ); _resources.ResourceHandleDestructor += OnResourceDestroyed;
_writeToLog = Penumbra.Config.EnableResourceLogging; _loader.ResourceLoaded += OnResourceLoaded;
_isEnabled = Penumbra.Config.EnableResourceWatcher; _loader.FileLoaded += OnFileLoaded;
_maxEntries = Penumbra.Config.MaxResourceWatcherRecords; UpdateFilter(_config.ResourceLoggingFilter, false);
_newMaxEntries = _maxEntries; _newMaxEntries = _config.MaxResourceWatcherRecords;
} }
public unsafe void Dispose() public unsafe void Dispose()
{ {
Clear(); Clear();
_records.TrimExcess(); _records.TrimExcess();
_loader.ResourceRequested -= OnResourceRequested; _resources.ResourceRequested -= OnResourceRequested;
_loader.ResourceLoaded -= OnResourceLoaded; _resources.ResourceHandleDestructor -= OnResourceDestroyed;
_loader.FileLoaded -= OnFileLoaded; _loader.ResourceLoaded -= OnResourceLoaded;
_loader.FileLoaded -= OnFileLoaded;
} }
private void Clear() private void Clear()
@ -67,183 +71,195 @@ public partial class ResourceWatcher : IDisposable, ITab
{ {
UpdateRecords(); UpdateRecords();
ImGui.SetCursorPosY( ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2 ); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2);
if( ImGui.Checkbox( "Enable", ref _isEnabled ) ) var isEnabled = _config.EnableResourceWatcher;
if (ImGui.Checkbox("Enable", ref isEnabled))
{ {
Penumbra.Config.EnableResourceWatcher = _isEnabled; Penumbra.Config.EnableResourceWatcher = isEnabled;
Penumbra.Config.Save(); Penumbra.Config.Save();
} }
ImGui.SameLine(); ImGui.SameLine();
DrawMaxEntries(); DrawMaxEntries();
ImGui.SameLine(); ImGui.SameLine();
if( ImGui.Button( "Clear" ) ) if (ImGui.Button("Clear"))
{
Clear(); Clear();
ImGui.SameLine();
var onlyMatching = _config.OnlyAddMatchingResources;
if (ImGui.Checkbox("Store Only Matching", ref onlyMatching))
{
Penumbra.Config.OnlyAddMatchingResources = onlyMatching;
Penumbra.Config.Save();
} }
ImGui.SameLine(); ImGui.SameLine();
if( ImGui.Checkbox( "Write to Log", ref _writeToLog ) ) var writeToLog = _config.EnableResourceLogging;
if (ImGui.Checkbox("Write to Log", ref writeToLog))
{ {
Penumbra.Config.EnableResourceLogging = _writeToLog; Penumbra.Config.EnableResourceLogging = writeToLog;
Penumbra.Config.Save(); Penumbra.Config.Save();
} }
ImGui.SameLine(); ImGui.SameLine();
DrawFilterInput(); DrawFilterInput();
ImGui.SetCursorPosY( ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2 ); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2);
_table.Draw( ImGui.GetTextLineHeightWithSpacing() ); _table.Draw(ImGui.GetTextLineHeightWithSpacing());
} }
private void DrawFilterInput() private void DrawFilterInput()
{ {
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
var tmp = _logFilter; var tmp = _logFilter;
var invalidRegex = _logRegex == null && _logFilter.Length > 0; var invalidRegex = _logRegex == null && _logFilter.Length > 0;
using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, invalidRegex ); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, invalidRegex);
using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, invalidRegex ); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, invalidRegex);
if( ImGui.InputTextWithHint( "##logFilter", "If path matches this Regex...", ref tmp, 256 ) ) if (ImGui.InputTextWithHint("##logFilter", "If path matches this Regex...", ref tmp, 256))
{ UpdateFilter(tmp, true);
UpdateFilter( tmp, true );
}
} }
private void UpdateFilter( string newString, bool config ) private void UpdateFilter(string newString, bool config)
{ {
if( newString == _logFilter ) if (newString == _logFilter)
{
return; return;
}
_logFilter = newString; _logFilter = newString;
try try
{ {
_logRegex = new Regex( _logFilter, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase ); _logRegex = new Regex(_logFilter, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
} }
catch catch
{ {
_logRegex = null; _logRegex = null;
} }
if( config ) if (config)
{ {
Penumbra.Config.ResourceLoggingFilter = newString; Penumbra.Config.ResourceLoggingFilter = newString;
Penumbra.Config.Save(); Penumbra.Config.Save();
} }
} }
private bool FilterMatch( ByteString path, out string match ) private bool FilterMatch(ByteString path, out string match)
{ {
match = path.ToString(); match = path.ToString();
return _logFilter.Length == 0 || ( _logRegex?.IsMatch( match ) ?? false ) || match.Contains( _logFilter, StringComparison.OrdinalIgnoreCase ); return _logFilter.Length == 0 || (_logRegex?.IsMatch(match) ?? false) || match.Contains(_logFilter, StringComparison.OrdinalIgnoreCase);
} }
private void DrawMaxEntries() private void DrawMaxEntries()
{ {
ImGui.SetNextItemWidth( 80 * ImGuiHelpers.GlobalScale ); ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale);
ImGui.InputInt( "Max. Entries", ref _newMaxEntries, 0, 0 ); ImGui.InputInt("Max. Entries", ref _newMaxEntries, 0, 0);
var change = ImGui.IsItemDeactivatedAfterEdit(); var change = ImGui.IsItemDeactivatedAfterEdit();
if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl)
{ {
change = true; change = true;
_newMaxEntries = DefaultMaxEntries; _newMaxEntries = DefaultMaxEntries;
} }
if( _maxEntries != DefaultMaxEntries && ImGui.IsItemHovered() ) var maxEntries = _config.MaxResourceWatcherRecords;
{ if (maxEntries != DefaultMaxEntries && ImGui.IsItemHovered())
ImGui.SetTooltip( $"CTRL + Right-Click to reset to default {DefaultMaxEntries}." ); ImGui.SetTooltip($"CTRL + Right-Click to reset to default {DefaultMaxEntries}.");
}
if( !change ) if (!change)
{
return; return;
}
_newMaxEntries = Math.Max( 16, _newMaxEntries ); _newMaxEntries = Math.Max(16, _newMaxEntries);
if( _newMaxEntries != _maxEntries ) if (_newMaxEntries != maxEntries)
{ {
_maxEntries = _newMaxEntries; _config.MaxResourceWatcherRecords = _newMaxEntries;
Penumbra.Config.MaxResourceWatcherRecords = _maxEntries;
Penumbra.Config.Save(); Penumbra.Config.Save();
_records.RemoveRange( 0, _records.Count - _maxEntries ); if (_newMaxEntries > _records.Count)
_records.RemoveRange(0, _records.Count - _newMaxEntries);
} }
} }
private void UpdateRecords() private void UpdateRecords()
{ {
var count = _newRecords.Count; var count = _newRecords.Count;
if( count > 0 ) if (count > 0)
{ {
while( _newRecords.TryDequeue( out var rec ) && count-- > 0 ) while (_newRecords.TryDequeue(out var rec) && count-- > 0)
{ _records.Add(rec);
_records.Add( rec );
}
if( _records.Count > _maxEntries ) if (_records.Count > _config.MaxResourceWatcherRecords)
{ _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords);
_records.RemoveRange( 0, _records.Count - _maxEntries );
}
_table.Reset(); _table.Reset();
} }
} }
private void OnResourceRequested( Utf8GamePath data, bool synchronous ) private unsafe void OnResourceRequested(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path,
GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue)
{ {
if( _writeToLog && FilterMatch( data.Path, out var match ) ) if (_config.EnableResourceLogging && FilterMatch(path.Path, out var match))
{ Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}");
Penumbra.Log.Information( $"[ResourceLoader] [REQ] {match} was requested {( synchronous ? "synchronously." : "asynchronously." )}" );
}
if( _isEnabled ) if (_config.EnableResourceWatcher)
{ {
_newRecords.Enqueue( Record.CreateRequest( data.Path, synchronous ) ); var record = Record.CreateRequest(path.Path, sync);
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record);
} }
} }
private unsafe void OnResourceLoaded( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data ) private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data)
{ {
if( _writeToLog ) if (_config.EnableResourceLogging)
{ {
var log = FilterMatch( path.Path, out var name ); var log = FilterMatch(path.Path, out var name);
var name2 = string.Empty; var name2 = string.Empty;
if( manipulatedPath != null ) if (manipulatedPath != null)
{ log |= FilterMatch(manipulatedPath.Value.InternalName, out name2);
log |= FilterMatch( manipulatedPath.Value.InternalName, out name2 );
}
if( log ) if (log)
{ {
var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name;
Penumbra.Log.Information( Penumbra.Log.Information(
$"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) " ); $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) ");
} }
} }
if( _isEnabled ) if (_config.EnableResourceWatcher)
{ {
var record = manipulatedPath == null var record = manipulatedPath == null
? Record.CreateDefaultLoad( path.Path, handle, data.ModCollection ) ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection)
: Record.CreateLoad( path.Path, manipulatedPath.Value.InternalName, handle, data.ModCollection ); : Record.CreateLoad(path.Path, manipulatedPath.Value.InternalName, handle,
_newRecords.Enqueue( record ); data.ModCollection);
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record);
} }
} }
private unsafe void OnFileLoaded( ResourceHandle* resource, ByteString path, bool success, bool custom ) private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _)
{ {
if( _writeToLog && FilterMatch( path, out var match ) ) if (_config.EnableResourceLogging && FilterMatch(path, out var match))
{
Penumbra.Log.Information( Penumbra.Log.Information(
$"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {( custom ? "local files" : "SqPack" )} into 0x{( ulong )resource:X} returned {success}." ); $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}.");
}
if( _isEnabled ) if (_config.EnableResourceWatcher)
{ {
_newRecords.Enqueue( Record.CreateFileLoad( path, resource, success, custom ) ); var record = Record.CreateFileLoad(path, resource, success, custom);
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record);
} }
} }
}
private unsafe void OnResourceDestroyed(ResourceHandle* resource)
{
if (_config.EnableResourceLogging && FilterMatch(resource->FileName(), out var match))
Penumbra.Log.Information(
$"[ResourceLoader] [DEST] [{resource->FileType}] Destroyed {match} at 0x{(ulong)resource:X}.");
if (_config.EnableResourceWatcher)
{
var record = Record.CreateDestruction(resource);
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record);
}
}
}