mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 21:24:18 +01:00
Consolidate path-data encoding into a single file and make it neater.
This commit is contained in:
parent
09742e2e50
commit
b2e1bff782
14 changed files with 302 additions and 95 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
162
Penumbra/Interop/PathResolving/PathDataHandler.cs
Normal file
162
Penumbra/Interop/PathResolving/PathDataHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue