Consolidate path-data encoding into a single file and make it neater.

This commit is contained in:
Ottermandias 2024-05-30 17:18:39 +02:00
parent 09742e2e50
commit b2e1bff782
14 changed files with 302 additions and 95 deletions

View file

@ -1,3 +1,4 @@
using Penumbra.Interop.PathResolving;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
@ -7,8 +8,8 @@ namespace Penumbra.Collections.Cache;
public readonly struct ImcCache : IDisposable
{
private readonly Dictionary<Utf8GamePath, ImcFile> _imcFiles = new();
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new();
private readonly Dictionary<Utf8GamePath, ImcFile> _imcFiles = [];
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = [];
public ImcCache()
{ }
@ -17,10 +18,10 @@ public readonly struct ImcCache : IDisposable
{
if (fromFullCompute)
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFileSync(path, CreateImcPath(collection, path));
collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection));
else
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFile(path, CreateImcPath(collection, path));
collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection));
}
public void Reset(ModCollection collection)
@ -57,7 +58,7 @@ public readonly struct ImcCache : IDisposable
return false;
_imcFiles[path] = file;
var fullPath = CreateImcPath(collection, path);
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection);
collection._cache!.ForceFile(path, fullPath);
return true;
@ -100,7 +101,7 @@ public readonly struct ImcCache : IDisposable
if (!manip.Apply(file))
return false;
var fullPath = CreateImcPath(collection, file.Path);
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection);
collection._cache!.ForceFile(file.Path, fullPath);
return true;
@ -115,9 +116,6 @@ public readonly struct ImcCache : IDisposable
_imcManipulations.Clear();
}
private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path)
=> new($"|{collection.Id.OptimizedString()}_{collection.ChangeCounter}|{path}");
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
=> _imcFiles.TryGetValue(path, out file);
}

View file

@ -1,3 +1,4 @@
using System;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
@ -10,23 +11,71 @@ using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
namespace Penumbra.Collections.Manager;
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
{
public static readonly LocalCollectionId Zero = new(0);
public static LocalCollectionId operator +(LocalCollectionId left, int right)
=> new(left.Id + right);
}
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
public ModCollection Create(string name, int index, ModCollection? duplicate)
{
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> inheritances)
{
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, CurrentCollectionId, version, Count, allSettings,
inheritances);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
{
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public void Delete(ModCollection collection)
=> _collectionsByLocal.Remove(collection.LocalId);
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections =
[
ModCollection.Empty,
];
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
private readonly Dictionary<LocalCollectionId, ModCollection>
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
public readonly ModCollection DefaultNamed;
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
/// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
@ -69,6 +118,10 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
return ByName(identifier, out collection);
}
/// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary>
public ModCollection ByLocalId(LocalCollectionId localId)
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
@ -100,10 +153,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
var newCollection = duplicate?.Duplicate(name, _collections.Count)
?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
_collections.Add(newCollection);
var newCollection = Create(name, _collections.Count, duplicate);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
@ -132,6 +182,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
// Update indices.
for (var i = collection.Index; i < Count; ++i)
_collections[i].Index = i;
_collectionsByLocal.Remove(collection.LocalId);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
@ -180,7 +231,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
continue;
}
var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance);
var collection = CreateFromData(id, name, version, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
try
@ -293,7 +344,8 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int movedToIdx)
{
type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)

View file

@ -52,7 +52,7 @@ public class TempCollectionManager : IDisposable
{
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++);
var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++);
Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}.");
if (_customCollections.TryAdd(collection.Id, collection))
{
@ -72,6 +72,7 @@ public class TempCollectionManager : IDisposable
return false;
}
_storage.Delete(collection);
Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}.");
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
for (var i = 0; i < Collections.Count; ++i)

View file

@ -25,13 +25,15 @@ public partial class ModCollection
/// Create the always available Empty Collection that will always sit at index 0,
/// can not be deleted and does never create a cache.
/// </summary>
public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, 0, 0, CurrentVersion, [], [], []);
public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, LocalCollectionId.Zero, 0, 0, CurrentVersion, [], [], []);
/// <summary> The name of a collection. </summary>
public string Name { get; set; }
public Guid Id { get; }
public LocalCollectionId LocalId { get; }
public string Identifier
=> Id.ToString();
@ -117,19 +119,20 @@ public partial class ModCollection
/// <summary>
/// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists.
/// </summary>
public ModCollection Duplicate(string name, int index)
public ModCollection Duplicate(string name, LocalCollectionId localId, int index)
{
Debug.Assert(index > 0, "Collection duplicated with non-positive index.");
return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(),
[.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()));
}
/// <summary> Constructor for reading from files. </summary>
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index,
public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, LocalCollectionId localId, int version,
int index,
Dictionary<string, ModSettings.SavedSettings> allSettings, IReadOnlyList<string> inheritances)
{
Debug.Assert(index > 0, "Collection read with non-positive index.");
var ret = new ModCollection(id, name, index, 0, version, [], [], allSettings)
var ret = new ModCollection(id, name, localId, index, 0, version, [], [], allSettings)
{
InheritanceByName = inheritances,
};
@ -139,18 +142,19 @@ public partial class ModCollection
}
/// <summary> Constructor for temporary collections. </summary>
public static ModCollection CreateTemporary(string name, int index, int changeCounter)
public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter)
{
Debug.Assert(index < 0, "Temporary collection created with non-negative index.");
var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []);
var ret = new ModCollection(Guid.NewGuid(), name, localId, index, changeCounter, CurrentVersion, [], [], []);
return ret;
}
/// <summary> Constructor for empty collections. </summary>
public static ModCollection CreateEmpty(string name, int index, int modCount)
public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount)
{
Debug.Assert(index >= 0, "Empty collection created with negative index.");
return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [],
return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion,
Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [],
[]);
}
@ -199,11 +203,12 @@ public partial class ModCollection
saver.ImmediateSave(new ModCollectionSave(mods, this));
}
private ModCollection(Guid id, string name, int index, int changeCounter, int version, List<ModSettings?> appliedSettings,
List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
private ModCollection(Guid id, string name, LocalCollectionId localId, int index, int changeCounter, int version,
List<ModSettings?> appliedSettings, List<ModCollection> inheritsFrom, Dictionary<string, ModSettings.SavedSettings> settings)
{
Name = name;
Id = id;
LocalId = localId;
Index = index;
ChangeCounter = changeCounter;
Settings = appliedSettings;

View file

@ -0,0 +1,162 @@
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.PathResolving;
public static class PathDataHandler
{
public static readonly ushort Discriminator = (ushort)(Environment.TickCount >> 12);
private static readonly string DiscriminatorString = $"{Discriminator:X4}";
private const int MinimumLength = 8;
/// <summary> Additional Data encoded in a path. </summary>
/// <param name="Collection"> The local ID of the collection. </param>
/// <param name="ChangeCounter"> The change counter of that collection when this file was loaded. </param>
/// <param name="OriginalPathCrc32"> The CRC32 of the originally requested path, only used for materials. </param>
/// <param name="Discriminator"> A discriminator to differ between multiple loads of Penumbra. </param>
public readonly record struct AdditionalPathData(
LocalCollectionId Collection,
int ChangeCounter,
int OriginalPathCrc32,
ushort Discriminator)
{
public static readonly AdditionalPathData Invalid = new(LocalCollectionId.Zero, 0, 0, PathDataHandler.Discriminator);
/// <summary> Any collection but the empty collection can appear. In particular, they can be negative for temporary collections. </summary>
public bool Valid
=> Collection.Id != 0;
}
/// <summary> Create the encoding path for an IMC file. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FullPath CreateImc(ByteString path, ModCollection collection)
=> CreateBase(path, collection);
/// <summary> Create the encoding path for a TMB file. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FullPath CreateTmb(ByteString path, ModCollection collection)
=> CreateBase(path, collection);
/// <summary> Create the encoding path for an AVFX file. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FullPath CreateAvfx(ByteString path, ModCollection collection)
=> CreateBase(path, collection);
/// <summary> Create the encoding path for a MTRL file. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FullPath CreateMtrl(ByteString path, ModCollection collection, Utf8GamePath originalPath)
=> new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}");
/// <summary> The base function shared by most file types. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static FullPath CreateBase(ByteString path, ModCollection collection)
=> new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{DiscriminatorString}|{path}");
/// <summary> Read an additional data blurb and parse it into usable data for all file types but Materials. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Read(ReadOnlySpan<byte> additionalData, out AdditionalPathData data)
=> ReadBase(additionalData, out data, out _);
/// <summary> Read an additional data blurb and parse it into usable data for Materials. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ReadMtrl(ReadOnlySpan<byte> additionalData, out AdditionalPathData data)
{
if (!ReadBase(additionalData, out data, out var remaining))
return false;
if (!int.TryParse(remaining, out var crc32))
return false;
data = data with { OriginalPathCrc32 = crc32 };
return true;
}
/// <summary> Parse the common attributes of an additional data blurb and return remaining data if there is any. </summary>
private static bool ReadBase(ReadOnlySpan<byte> additionalData, out AdditionalPathData data, out ReadOnlySpan<byte> remainingData)
{
data = AdditionalPathData.Invalid;
remainingData = [];
// At least (\d_\d_\x\x\x\x)
if (additionalData.Length < MinimumLength)
return false;
// Fetch discriminator, constant length.
var discriminatorSpan = additionalData[^4..];
if (!ushort.TryParse(discriminatorSpan, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var discriminator))
return false;
additionalData = additionalData[..^5];
var collectionSplit = additionalData.IndexOf((byte)'_');
if (collectionSplit == -1)
return false;
var collectionSpan = additionalData[..collectionSplit];
additionalData = additionalData[(collectionSplit + 1)..];
if (!int.TryParse(collectionSpan, out var id))
return false;
var changeCounterSpan = additionalData;
var changeCounterSplit = additionalData.IndexOf((byte)'_');
if (changeCounterSplit != -1)
{
changeCounterSpan = additionalData[..changeCounterSplit];
remainingData = additionalData[(changeCounterSplit + 1)..];
}
if (!int.TryParse(changeCounterSpan, out var changeCounter))
return false;
data = new AdditionalPathData(new LocalCollectionId(id), changeCounter, 0, discriminator);
return true;
}
/// <summary> Split a given span into the actual path and the additional data blurb. Returns true if a blurb exists. </summary>
public static bool Split(ReadOnlySpan<byte> text, out ReadOnlySpan<byte> path, out ReadOnlySpan<byte> data)
{
if (text.IsEmpty || text[0] is not (byte)'|')
{
path = text;
data = [];
return false;
}
var endIdx = text[1..].IndexOf((byte)'|');
if (endIdx++ < 0)
{
path = text;
data = [];
return false;
}
data = text.Slice(1, endIdx - 1);
path = ++endIdx == text.Length ? [] : text[endIdx..];
return true;
}
/// <inheritdoc cref="Split(ReadOnlySpan{byte},out ReadOnlySpan{byte},out ReadOnlySpan{byte})("/>
public static bool Split(ReadOnlySpan<char> text, out ReadOnlySpan<char> path, out ReadOnlySpan<char> data)
{
if (text.Length == 0 || text[0] is not '|')
{
path = text;
data = [];
return false;
}
var endIdx = text[1..].IndexOf('|');
if (endIdx++ < 0)
{
path = text;
data = [];
return false;
}
data = text.Slice(1, endIdx - 1);
path = ++endIdx >= text.Length ? [] : text[endIdx..];
return true;
}
}

View file

@ -13,11 +13,10 @@ namespace Penumbra.Interop.PathResolving;
public class PathResolver : IDisposable
{
private readonly PerformanceTracker _performance;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly TempCollectionManager _tempCollections;
private readonly ResourceLoader _loader;
private readonly PerformanceTracker _performance;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly ResourceLoader _loader;
private readonly SubfileHelper _subfileHelper;
private readonly PathState _pathState;
@ -25,14 +24,12 @@ public class PathResolver : IDisposable
private readonly GameState _gameState;
private readonly CollectionResolver _collectionResolver;
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
TempCollectionManager tempCollections, ResourceLoader loader, SubfileHelper subfileHelper,
PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState)
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader,
SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState)
{
_performance = performance;
_config = config;
_collectionManager = collectionManager;
_tempCollections = tempCollections;
_subfileHelper = subfileHelper;
_pathState = pathState;
_metaState = metaState;
@ -43,9 +40,12 @@ public class PathResolver : IDisposable
_loader.FileLoaded += ImcLoadResource;
}
/// <summary> Obtain a temporary or permanent collection by name. </summary>
public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
=> _tempCollections.CollectionById(id, out collection) || _collectionManager.Storage.ById(id, out collection);
/// <summary> Obtain a temporary or permanent collection by local ID. </summary>
public bool CollectionByLocalId(LocalCollectionId id, out ModCollection collection)
{
collection = _collectionManager.Storage.ByLocalId(id);
return collection != ModCollection.Empty;
}
/// <summary> Try to resolve the given game path to the replaced path. </summary>
public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType)
@ -113,7 +113,7 @@ public class PathResolver : IDisposable
// 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);
SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, gamePath, out var pair);
return pair;
}
@ -131,23 +131,21 @@ public class PathResolver : IDisposable
}
/// <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)
private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom,
ReadOnlySpan<byte> additionalData)
{
if (resource->FileType != ResourceType.Imc)
if (resource->FileType != ResourceType.Imc
|| !PathDataHandler.Read(additionalData, out var data)
|| data.Discriminator != PathDataHandler.Discriminator
|| !Utf8GamePath.FromByteString(path, out var gamePath)
|| !CollectionByLocalId(data.Collection, out var collection)
|| !collection.HasCache
|| !collection.GetImcFile(gamePath, out var file))
return;
var lastUnderscore = additionalData.LastIndexOf((byte)'_');
var idString = lastUnderscore == -1 ? additionalData : additionalData.Substring(0, lastUnderscore);
if (Utf8GamePath.FromByteString(path, out var gamePath)
&& GuidExtensions.FromOptimizedString(idString.Span, out var id)
&& CollectionById(id, 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}.");
}
file.Replace(resource);
Penumbra.Log.Verbose(
$"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}.");
}
/// <summary> Resolve a path from the interface collection. </summary>

View file

@ -66,21 +66,18 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyV
return false;
}
/// <summary> Materials, TMB, and AVFX need to be set per collection so they can load their sub files independently from each other. </summary>
/// <summary> Materials, TMB, and AVFX need to be set per collection, so they can load their sub files independently of each other. </summary>
public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved,
out (FullPath?, ResolveData) data)
Utf8GamePath originalPath, out (FullPath?, ResolveData) data)
{
if (nonDefault)
switch (type)
resolved = type switch
{
case ResourceType.Mtrl:
case ResourceType.Avfx:
case ResourceType.Tmb:
var fullPath = new FullPath($"|{resolveData.ModCollection.Id.OptimizedString()}_{resolveData.ModCollection.ChangeCounter}|{path}");
data = (fullPath, resolveData);
return;
}
ResourceType.Mtrl => PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath),
ResourceType.Avfx => PathDataHandler.CreateAvfx(path, resolveData.ModCollection),
ResourceType.Tmb => PathDataHandler.CreateTmb(path, resolveData.ModCollection),
_ => resolved,
};
data = (resolved, resolveData);
}

View file

@ -1,5 +1,6 @@
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.String.Functions;
@ -11,7 +12,7 @@ namespace Penumbra.Interop.ResourceLoading;
/// we use the fixed size buffers of their formats to only store pointers to the actual path instead.
/// Then we translate the stored pointer to the path in CreateFileW, if the prefix matches.
/// </summary>
public unsafe class CreateFileWHook : IDisposable
public unsafe class CreateFileWHook : IDisposable, IRequiredService
{
public const int Size = 28;

View file

@ -1,6 +1,7 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.SafeHandles;
using Penumbra.Interop.Structs;
using Penumbra.String;
@ -17,8 +18,7 @@ public unsafe class ResourceLoader : IDisposable
private ResolveData _resolvedData = ResolveData.Invalid;
public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService,
CreateFileWHook _)
public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService)
{
_resources = resources;
_fileReadService = fileReadService;
@ -54,7 +54,7 @@ public unsafe class ResourceLoader : IDisposable
/// <summary> Reset the ResolvePath function to always return null. </summary>
public void ResetResolvePath()
=> ResolvePath = (_1, _2, _3) => (null, ResolveData.Invalid);
=> ResolvePath = (_, _, _) => (null, ResolveData.Invalid);
public delegate void ResourceLoadedDelegate(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolveData);
@ -67,7 +67,7 @@ public unsafe class ResourceLoader : IDisposable
public event ResourceLoadedDelegate? ResourceLoaded;
public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom,
ByteString additionalData);
ReadOnlySpan<byte> additionalData);
/// <summary>
/// Event fired whenever a resource is newly loaded.
@ -132,19 +132,17 @@ public unsafe class ResourceLoader : IDisposable
// 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)'|')
if (!PathDataHandler.Split(gamePath.Path.Span, out var actualPath, out var data))
{
returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, ByteString.Empty);
returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, []);
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;
var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii);
fileDescriptor->ResourceHandle->FileNameData = path.Path;
fileDescriptor->ResourceHandle->FileNameLength = path.Length;
MtrlForceSync(fileDescriptor, ref isSync);
returnValue = DefaultLoadResource(split[2], fileDescriptor, priority, isSync, split[1]);
returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data);
// Return original resource handle path so that they can be loaded separately.
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length;
@ -153,7 +151,7 @@ public unsafe class ResourceLoader : IDisposable
/// <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)
bool isSync, ReadOnlySpan<byte> additionalData)
{
if (Utf8GamePath.IsRooted(gamePath))
{

View file

@ -9,6 +9,7 @@ using Penumbra.Collections;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Services;
using Penumbra.String;
using Penumbra.String.Classes;
@ -373,14 +374,8 @@ internal unsafe partial record ResolveContext(
if (name.IsEmpty)
return ByteString.Empty;
if (stripPrefix && name[0] == (byte)'|')
{
var pos = name.IndexOf((byte)'|', 1);
if (pos < 0)
return ByteString.Empty;
name = name.Substring(pos + 1);
}
if (stripPrefix && PathDataHandler.Split(name.Span, out var path, out _))
name = ByteString.FromSpanUnsafe(path, name.IsNullTerminated, name.IsAsciiLowerCase, name.IsAscii);
return name;
}

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
@ -83,10 +84,8 @@ public class TemporaryMod : IMod
}
var targetPath = fullPath.Path.FullName;
if (fullPath.Path.Name.StartsWith('|'))
{
targetPath = targetPath.Split('|', 3, StringSplitOptions.RemoveEmptyEntries).Last();
}
if (PathDataHandler.Split(fullPath.Path.FullName, out var actualPath, out _))
targetPath = actualPath.ToString();
if (Path.IsPathRooted(targetPath))
{

View file

@ -379,7 +379,8 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
var emptyStorage = new ModStorage();
var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, 0, 1, dict, []);
// Only used for saving and immediately discarded, so the local collection id here is irrelevant.
var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, LocalCollectionId.Zero, 0, 1, dict, []);
saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
}
catch (Exception e)

View file

@ -7,6 +7,7 @@ using Penumbra.Communication;
using Penumbra.CrashHandler;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData.Actors;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs;
using Penumbra.String;
@ -286,8 +287,7 @@ public sealed class CrashHandlerService : IDisposable, IService
try
{
var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1;
if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1)))
if (PathDataHandler.Split(manipulatedPath.Value.FullName, out var actualPath, out _) && Path.IsPathRooted(actualPath))
return;
var name = GetActorName(resolveData.AssociatedGameObject);

View file

@ -236,7 +236,7 @@ public sealed class ResourceWatcher : IDisposable, ITab
_newRecords.Enqueue(record);
}
private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _)
private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ReadOnlySpan<byte> _)
{
if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match))
Penumbra.Log.Information(