mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Rework Interop/Loader Services.
This commit is contained in:
parent
99fd4b7806
commit
0df12a34cb
32 changed files with 1137 additions and 1421 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3d346700e8800c045aa19d70d516d8a4fda2f2ee
|
Subproject commit df1cd8b02d729b2e7f585c301105b37c70d81c3e
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2f999713c5b692fead3fb28c39002d1cd82c9261
|
Subproject commit 81f384cf96a9257b1ee2c7019772f30df78ba417
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -43,14 +45,10 @@ public partial class ModCollection
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
|
@ -79,18 +77,14 @@ public partial class ModCollection
|
||||||
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;
|
||||||
|
|
@ -104,6 +98,15 @@ public partial class ModCollection
|
||||||
internal MetaManager? MetaCache
|
internal MetaManager? MetaCache
|
||||||
=> _cache?.MetaManipulations;
|
=> _cache?.MetaManipulations;
|
||||||
|
|
||||||
|
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
|
||||||
|
{
|
||||||
|
if (_cache != null)
|
||||||
|
return _cache.MetaManipulations.GetImcFile(path, out file);
|
||||||
|
|
||||||
|
file = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
|
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
|
||||||
=> _cache?.ResolvedFiles ?? new Dictionary<Utf8GamePath, ModPath>();
|
=> _cache?.ResolvedFiles ?? new Dictionary<Utf8GamePath, ModPath>();
|
||||||
|
|
||||||
|
|
@ -126,9 +129,7 @@ public partial class ModCollection
|
||||||
{
|
{
|
||||||
// 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);
|
||||||
|
|
@ -153,14 +154,10 @@ public partial class ModCollection
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ public class Configuration : IPluginConfiguration
|
||||||
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 bool OnlyAddMatchingResources { get; set; } = true;
|
||||||
public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries;
|
public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries;
|
||||||
|
|
||||||
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
|
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
|
||||||
|
|
|
||||||
|
|
@ -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 )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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();
|
var atkModule = uiModule->GetRaptureAtkModule();
|
||||||
if (atkModule == null)
|
if (atkModule == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
AtkModule = &atkModule->AtkModule;
|
_atkModule = &atkModule->AtkModule;
|
||||||
ReloadFontsFunc = ( ( delegate* unmanaged< AtkModule*, bool, bool, void >* )AtkModule->vtbl )[ Offsets.ReloadFontsVfunc ];
|
_reloadFontsFunc = ((delegate* unmanaged< AtkModule*, bool, bool, void >*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
106
Penumbra/Interop/Loader/CharacterResolver.cs
Normal file
106
Penumbra/Interop/Loader/CharacterResolver.cs
Normal 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}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,83 +18,18 @@ 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 );
|
|
||||||
|
|
||||||
/// <remarks> Long paths in windows need to start with "\\?\", so we keep this static in the pointers. </remarks>
|
|
||||||
private static nint SetupStorage()
|
|
||||||
{
|
{
|
||||||
var ptr = ( char* )Marshal.AllocHGlobal( 2 * BufferSize );
|
_createFileWHook = Hook<CreateFileWDelegate>.FromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour);
|
||||||
ptr[ 0 ] = '\\';
|
_createFileWHook.Enable();
|
||||||
ptr[ 1 ] = '\\';
|
|
||||||
ptr[ 2 ] = '?';
|
|
||||||
ptr[ 3 ] = '\\';
|
|
||||||
ptr[ 4 ] = '\0';
|
|
||||||
return ( nint )ptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Enable()
|
/// <summary>
|
||||||
=> _createFileWHook.Enable();
|
/// Write the data read specifically in the CreateFileW hook to a buffer array.
|
||||||
|
/// </summary>
|
||||||
public void Disable()
|
/// <param name="buffer">The buffer the data is written to.</param>
|
||||||
=> _createFileWHook.Disable();
|
/// <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 void Dispose()
|
|
||||||
{
|
|
||||||
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 )
|
|
||||||
{
|
|
||||||
// Translate data if prefix fits.
|
|
||||||
if( CheckPtr( fileName, out var name ) )
|
|
||||||
{
|
|
||||||
// Use static storage.
|
|
||||||
var ptr = WriteFileName( name );
|
|
||||||
Penumbra.Log.Verbose( $"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." );
|
|
||||||
return _createFileWHook.OriginalDisposeSafe( ptr, 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,
|
|
||||||
/// replacing any forward-slashes with back-slashes and adding a terminating null-wchar_t.</remarks>
|
|
||||||
private char* WriteFileName( ReadOnlySpan< byte > actualName )
|
|
||||||
{
|
|
||||||
var span = new Span< char >( ( char* )_fileNameStorage.Value + 4, BufferSize - 4 );
|
|
||||||
var written = Encoding.UTF8.GetChars( actualName, span );
|
|
||||||
for( var i = 0; i < written; ++i )
|
|
||||||
{
|
|
||||||
if( span[ i ] == '/' )
|
|
||||||
{
|
|
||||||
span[ i ] = '\\';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span[ written ] = '\0';
|
|
||||||
|
|
||||||
return ( char* )_fileNameStorage.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void WritePtr(char* buffer, byte* address, int length)
|
public static void WritePtr(char* buffer, byte* address, int length)
|
||||||
{
|
{
|
||||||
// Set the prefix, which is not valid for any actual path.
|
// Set the prefix, which is not valid for any actual path.
|
||||||
|
|
@ -129,6 +64,70 @@ public unsafe class CreateFileWHook : IDisposable
|
||||||
ptr[RequiredSize - 1] = 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>
|
||||||
|
private static nint SetupStorage()
|
||||||
|
{
|
||||||
|
var ptr = (char*)Marshal.AllocHGlobal(2 * BufferSize);
|
||||||
|
ptr[0] = '\\';
|
||||||
|
ptr[1] = '\\';
|
||||||
|
ptr[2] = '?';
|
||||||
|
ptr[3] = '\\';
|
||||||
|
ptr[4] = '\0';
|
||||||
|
return (nint)ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
private nint CreateFileWDetour(char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template)
|
||||||
|
{
|
||||||
|
// Translate data if prefix fits.
|
||||||
|
if (CheckPtr(fileName, out var name))
|
||||||
|
{
|
||||||
|
// Use static storage.
|
||||||
|
var ptr = WriteFileName(name);
|
||||||
|
Penumbra.Log.Verbose($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}.");
|
||||||
|
return _createFileWHook.OriginalDisposeSafe(ptr, 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,
|
||||||
|
/// replacing any forward-slashes with back-slashes and adding a terminating null-wchar_t.</remarks>
|
||||||
|
private char* WriteFileName(ReadOnlySpan<byte> actualName)
|
||||||
|
{
|
||||||
|
var span = new Span<char>((char*)_fileNameStorage.Value + 4, BufferSize - 4);
|
||||||
|
var written = Encoding.UTF8.GetChars(actualName, span);
|
||||||
|
for (var i = 0; i < written; ++i)
|
||||||
|
{
|
||||||
|
if (span[i] == '/')
|
||||||
|
span[i] = '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
span[written] = '\0';
|
||||||
|
|
||||||
|
return (char*)_fileNameStorage.Value;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool CheckPtr(char* buffer, out ReadOnlySpan<byte> fileName)
|
private static bool CheckPtr(char* buffer, out ReadOnlySpan<byte> fileName)
|
||||||
{
|
{
|
||||||
if (buffer[0] is not Prefix)
|
if (buffer[0] is not Prefix)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
90
Penumbra/Interop/Loader/FileReadService.cs
Normal file
90
Penumbra/Interop/Loader/FileReadService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 _)
|
||||||
|
{
|
||||||
|
_resources = resources;
|
||||||
|
_fileReadService = fileReadService;
|
||||||
|
_texMdlService = texMdlService;
|
||||||
|
ResetResolvePath();
|
||||||
|
|
||||||
public void EnableReplacements()
|
_resources.ResourceRequested += ResourceHandler;
|
||||||
{
|
_resources.ResourceHandleIncRef += IncRefProtection;
|
||||||
if( DoReplacements )
|
_resources.ResourceHandleDecRef += DecRefProtection;
|
||||||
{
|
_fileReadService.ReadSqPack += ReadSqPackDetour;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DoReplacements = true;
|
/// <summary> The function to use to resolve a given path. </summary>
|
||||||
EnableTexMdlTreatment();
|
public Func<Utf8GamePath, ResourceCategory, ResourceType, (FullPath?, ResolveData)> ResolvePath = null!;
|
||||||
EnableHooks();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisableReplacements()
|
/// <summary> Reset the ResolvePath function to always return null. </summary>
|
||||||
{
|
public void ResetResolvePath()
|
||||||
if( !DoReplacements )
|
=> ResolvePath = (_1, _2, _3) => (null, ResolveData.Invalid);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DoReplacements = false;
|
|
||||||
DisableTexMdlTreatment();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnableHooks()
|
|
||||||
{
|
|
||||||
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,
|
public delegate void ResourceLoadedDelegate(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
|
||||||
ResolveData resolveData);
|
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}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
114
Penumbra/Interop/Loader/ResourceManagerService.cs
Normal file
114
Penumbra/Interop/Loader/ResourceManagerService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
100
Penumbra/Interop/Loader/TexMdlService.cs
Normal file
100
Penumbra/Interop/Loader/TexMdlService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -66,7 +67,6 @@ public unsafe partial class PathResolver
|
||||||
out (FullPath?, ResolveData) data)
|
out (FullPath?, ResolveData) data)
|
||||||
{
|
{
|
||||||
if (nonDefault)
|
if (nonDefault)
|
||||||
{
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ResourceType.Mtrl:
|
case ResourceType.Mtrl:
|
||||||
|
|
@ -75,7 +75,6 @@ public unsafe partial class PathResolver
|
||||||
data = (fullPath, resolveData);
|
data = (fullPath, resolveData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data = (resolved, resolveData);
|
data = (resolved, resolveData);
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +84,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +93,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -108,16 +105,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -126,31 +122,6 @@ public unsafe partial class PathResolver
|
||||||
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
|
|
||||||
// before actually loading the file.
|
|
||||||
public static bool SubfileLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager,
|
|
||||||
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret )
|
|
||||||
{
|
|
||||||
switch( fileDescriptor->ResourceHandle->FileType )
|
|
||||||
{
|
|
||||||
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);
|
private delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle);
|
||||||
|
|
||||||
[Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))]
|
[Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))]
|
||||||
|
|
@ -182,9 +153,7 @@ public unsafe partial class PathResolver
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,16 @@ 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;
|
||||||
|
try
|
||||||
|
{
|
||||||
_tmp = new PenumbraNew(pluginInterface);
|
_tmp = new PenumbraNew(pluginInterface);
|
||||||
Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
|
Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
|
||||||
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
|
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
|
||||||
|
|
@ -103,12 +110,10 @@ public class Penumbra : IDalamudPlugin
|
||||||
ItemData = _tmp.Services.GetRequiredService<ItemService>().AwaitedService;
|
ItemData = _tmp.Services.GetRequiredService<ItemService>().AwaitedService;
|
||||||
Dalamud = _tmp.Services.GetRequiredService<DalamudServices>();
|
Dalamud = _tmp.Services.GetRequiredService<DalamudServices>();
|
||||||
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
|
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
|
||||||
try
|
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
|
||||||
{
|
|
||||||
ResourceLoader = new ResourceLoader(this);
|
ResourceManagerService = _tmp.Services.GetRequiredService<ResourceManagerService>();
|
||||||
ResourceLoader.EnableHooks();
|
|
||||||
_resourceWatcher = new ResourceWatcher(ResourceLoader);
|
|
||||||
ResidentResources = new ResidentResourceManager();
|
|
||||||
_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();
|
||||||
|
|
|
||||||
|
|
@ -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,8 +55,14 @@ 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>()
|
||||||
|
|
|
||||||
|
|
@ -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 )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,36 +23,36 @@ public partial class ResourceWatcher : IDisposable, ITab
|
||||||
{
|
{
|
||||||
public const int DefaultMaxEntries = 1024;
|
public const int DefaultMaxEntries = 1024;
|
||||||
|
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly ResourceService _resources;
|
||||||
private readonly ResourceLoader _loader;
|
private readonly ResourceLoader _loader;
|
||||||
private readonly List<Record> _records = new();
|
private readonly List<Record> _records = new();
|
||||||
private readonly ConcurrentQueue<Record> _newRecords = new();
|
private readonly ConcurrentQueue<Record> _newRecords = new();
|
||||||
private readonly Table _table;
|
private readonly Table _table;
|
||||||
private bool _writeToLog;
|
|
||||||
private bool _isEnabled;
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
_config = config;
|
||||||
|
_resources = resources;
|
||||||
_loader = loader;
|
_loader = loader;
|
||||||
_table = new Table(_records);
|
_table = new Table(_records);
|
||||||
_loader.ResourceRequested += OnResourceRequested;
|
_resources.ResourceRequested += OnResourceRequested;
|
||||||
|
_resources.ResourceHandleDestructor += OnResourceDestroyed;
|
||||||
_loader.ResourceLoaded += OnResourceLoaded;
|
_loader.ResourceLoaded += OnResourceLoaded;
|
||||||
_loader.FileLoaded += OnFileLoaded;
|
_loader.FileLoaded += OnFileLoaded;
|
||||||
UpdateFilter( Penumbra.Config.ResourceLoggingFilter, false );
|
UpdateFilter(_config.ResourceLoggingFilter, false);
|
||||||
_writeToLog = Penumbra.Config.EnableResourceLogging;
|
_newMaxEntries = _config.MaxResourceWatcherRecords;
|
||||||
_isEnabled = Penumbra.Config.EnableResourceWatcher;
|
|
||||||
_maxEntries = Penumbra.Config.MaxResourceWatcherRecords;
|
|
||||||
_newMaxEntries = _maxEntries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void Dispose()
|
public unsafe void Dispose()
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
_records.TrimExcess();
|
_records.TrimExcess();
|
||||||
_loader.ResourceRequested -= OnResourceRequested;
|
_resources.ResourceRequested -= OnResourceRequested;
|
||||||
|
_resources.ResourceHandleDestructor -= OnResourceDestroyed;
|
||||||
_loader.ResourceLoaded -= OnResourceLoaded;
|
_loader.ResourceLoaded -= OnResourceLoaded;
|
||||||
_loader.FileLoaded -= OnFileLoaded;
|
_loader.FileLoaded -= OnFileLoaded;
|
||||||
}
|
}
|
||||||
|
|
@ -68,9 +72,10 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,14 +83,21 @@ public partial class ResourceWatcher : IDisposable, ITab
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,17 +117,13 @@ public partial class ResourceWatcher : IDisposable, ITab
|
||||||
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
|
||||||
|
|
@ -152,23 +160,20 @@ public partial class ResourceWatcher : IDisposable, ITab
|
||||||
_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,43 +183,38 @@ public partial class ResourceWatcher : IDisposable, ITab
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
|
@ -224,26 +224,42 @@ public partial class ResourceWatcher : IDisposable, ITab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
data.ModCollection);
|
||||||
|
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
|
||||||
_newRecords.Enqueue(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 (_config.EnableResourceWatcher)
|
||||||
|
{
|
||||||
|
var record = Record.CreateFileLoad(path, resource, success, custom);
|
||||||
|
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
|
||||||
|
_newRecords.Enqueue(record);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if( _isEnabled )
|
private unsafe void OnResourceDestroyed(ResourceHandle* resource)
|
||||||
{
|
{
|
||||||
_newRecords.Enqueue( Record.CreateFileLoad( path, resource, success, custom ) );
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue