mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 20:54:16 +01:00
140 lines
6.9 KiB
C#
140 lines
6.9 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
|
using Penumbra.Api;
|
|
using Penumbra.Collections;
|
|
using Penumbra.GameData.Enums;
|
|
using Penumbra.Interop.ResourceLoading;
|
|
using Penumbra.Interop.Structs;
|
|
using Penumbra.String;
|
|
using Penumbra.String.Classes;
|
|
using Penumbra.Util;
|
|
|
|
namespace Penumbra.Interop.PathResolving;
|
|
|
|
public class PathResolver : IDisposable
|
|
{
|
|
private readonly PerformanceTracker _performance;
|
|
private readonly Configuration _config;
|
|
private readonly ModCollection.Manager _collectionManager;
|
|
private readonly TempCollectionManager _tempCollections;
|
|
private readonly ResourceLoader _loader;
|
|
|
|
private readonly AnimationHookService _animationHookService;
|
|
private readonly SubfileHelper _subfileHelper;
|
|
private readonly PathState _pathState;
|
|
private readonly MetaState _metaState;
|
|
|
|
public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager,
|
|
TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
|
|
PathState pathState, MetaState metaState)
|
|
{
|
|
_performance = performance;
|
|
_config = config;
|
|
_collectionManager = collectionManager;
|
|
_tempCollections = tempCollections;
|
|
_animationHookService = animationHookService;
|
|
_subfileHelper = subfileHelper;
|
|
_pathState = pathState;
|
|
_metaState = metaState;
|
|
_loader = loader;
|
|
_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 => Resolve(path, resourceType),
|
|
ResourceCategory.Shader => Resolve(path, resourceType),
|
|
ResourceCategory.Vfx => Resolve(path, resourceType),
|
|
ResourceCategory.Sound => Resolve(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),
|
|
};
|
|
}
|
|
|
|
public (FullPath?, ResolveData) Resolve(Utf8GamePath gamePath, ResourceType type)
|
|
{
|
|
using var performance = _performance.Measure(PerformanceType.CharacterResolver);
|
|
// Check if the path was marked for a specific collection,
|
|
// or if it is a file loaded by a material, and if we are currently in a material load,
|
|
// or if it is a face decal path and the current mod collection is set.
|
|
// If not use the default collection.
|
|
// We can remove paths after they have actually been loaded.
|
|
// A potential next request will add the path anew.
|
|
var nonDefault = _subfileHelper.HandleSubFiles(type, out var resolveData)
|
|
|| _pathState.Consume(gamePath.Path, out resolveData)
|
|
|| _animationHookService.HandleFiles(type, gamePath, out resolveData)
|
|
|| _metaState.HandleDecalFile(type, gamePath, out resolveData);
|
|
if (!nonDefault || !resolveData.Valid)
|
|
resolveData = _collectionManager.Default.ToResolveData();
|
|
|
|
// Resolve using character/default collection first, otherwise forced, as usual.
|
|
var resolved = resolveData.ModCollection.ResolvePath(gamePath);
|
|
|
|
// Since mtrl files load their files separately, we need to add the new, resolved path
|
|
// 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.
|
|
var path = resolved == null ? gamePath.Path : resolved.Value.InternalName;
|
|
SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair);
|
|
return pair;
|
|
}
|
|
|
|
public unsafe void Dispose()
|
|
{
|
|
_loader.ResetResolvePath();
|
|
_loader.FileLoaded -= ImcLoadResource;
|
|
}
|
|
|
|
/// <summary> Use the default method of path replacement. </summary>
|
|
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}.");
|
|
}
|
|
}
|
|
}
|