Penumbra/Penumbra/Interop/PathResolving/PathResolver.cs
2024-07-28 23:56:28 +02:00

128 lines
6.4 KiB
C#

using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.Processing;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Interop.PathResolving;
public class PathResolver : IDisposable, IService
{
private readonly PerformanceTracker _performance;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly ResourceLoader _loader;
private readonly SubfileHelper _subfileHelper;
private readonly PathState _pathState;
private readonly MetaState _metaState;
private readonly GameState _gameState;
private readonly CollectionResolver _collectionResolver;
private readonly GamePathPreProcessService _preprocessor;
public PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader,
SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState,
GamePathPreProcessService preprocessor)
{
_performance = performance;
_config = config;
_collectionManager = collectionManager;
_subfileHelper = subfileHelper;
_pathState = pathState;
_metaState = metaState;
_gameState = gameState;
_preprocessor = preprocessor;
_collectionResolver = collectionResolver;
_loader = loader;
_loader.ResolvePath = ResolvePath;
}
/// <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);
// Do not allow manipulating layers to prevent very obvious cheating and softlocks.
if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb)
return (null, ResolveData.Invalid);
return category switch
{
// Only Interface collection.
ResourceCategory.Ui => ResolveUi(path),
// 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),
// EXD Modding in general should probably be prohibited but is currently used for fan translations.
// We prevent WebURL specifically because it technically allows launching arbitrary programs / to execute arbitrary code.
ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8)
? (null, ResolveData.Invalid)
: DefaultResolver(path),
// None of these files are ever associated with specific characters,
// always use the default resolver for now,
// except that common/font is conceptually more UI.
ResourceCategory.Common => path.Path.StartsWith("common/font"u8)
? ResolveUi(path)
: DefaultResolver(path),
ResourceCategory.BgCommon => DefaultResolver(path),
ResourceCategory.Bg => DefaultResolver(path),
ResourceCategory.Cut => 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)
|| _gameState.HandleFiles(_collectionResolver, type, gamePath, out resolveData)
|| _metaState.HandleDecalFile(type, gamePath, out resolveData);
if (!nonDefault || !resolveData.Valid)
resolveData = _collectionManager.Active.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;
return _preprocessor.PreProcess(resolveData, path, nonDefault, type, resolved, gamePath);
}
public void Dispose()
{
_loader.ResetResolvePath();
}
/// <summary> Use the default method of path replacement. </summary>
private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path)
{
var resolved = _collectionManager.Active.Default.ResolvePath(path);
return (resolved, _collectionManager.Active.Default.ToResolveData());
}
/// <summary> Resolve a path from the interface collection. </summary>
private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path)
=> (_collectionManager.Active.Interface.ResolvePath(path),
_collectionManager.Active.Interface.ToResolveData());
}