Merge branch 'meta_rework'

This commit is contained in:
Ottermandias 2024-06-18 22:05:15 +02:00
commit 819afc518c
192 changed files with 4126 additions and 4124 deletions

@ -1 +1 @@
Subproject commit e95c0f04edc7e85aea67498fd8bf495a7fe6d3c8
Subproject commit caa9e9b9a5dc3928eba10b315cf6a0f6f1d84b65

@ -1 +1 @@
Subproject commit 0a2e2650d693d1bba267498f96112682cc09dbab
Subproject commit 3fbc704515b7b5fa9be02fb2a44719fc333747c1

View file

@ -1,5 +1,8 @@
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
@ -7,17 +10,34 @@ namespace Penumbra.Api.Api;
public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
{
public const int CurrentVersion = 0;
public string GetPlayerMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
return CompressMetaManipulations(collection);
}
public string GetMetaManipulations(int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
return CompressMetaManipulations(collection);
}
internal static string CompressMetaManipulations(ModCollection collection)
{
var array = new JArray();
if (collection.MetaCache is { } cache)
{
MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key));
MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair<ImcIdentifier, ImcEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair<EqpIdentifier, EqpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair<EqdpIdentifier, EqdpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
}
return Functions.ToCompressedBase64(array, CurrentVersion);
}
}

View file

@ -159,32 +159,18 @@ public class TemporaryApi(
/// The empty string is treated as an empty set.
/// Only returns true if all conversions are successful and distinct.
/// </summary>
private static bool ConvertManips(string manipString,
[NotNullWhen(true)] out HashSet<MetaManipulation>? manips)
private static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (manipString.Length == 0)
{
manips = [];
manips = new MetaDictionary();
return true;
}
if (Functions.FromCompressedBase64<MetaManipulation[]>(manipString, out var manipArray) != MetaManipulation.CurrentVersion)
{
manips = null;
return false;
}
if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion)
return true;
manips = new HashSet<MetaManipulation>(manipArray!.Length);
foreach (var manip in manipArray.Where(m => m.Validate()))
{
if (manips.Add(manip))
continue;
Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped.");
manips = null;
return false;
}
return true;
manips = null;
return false;
}
}

View file

@ -4,6 +4,8 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
@ -49,7 +51,7 @@ public class TemporaryIpcTester(
ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32);
ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256);
ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256);
ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256);
ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8);
ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
@ -101,8 +103,7 @@ public class TemporaryIpcTester(
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>(),
MetaManipulation.CurrentVersion);
var manips = MetaApi.CompressMetaManipulations(copyCollection);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
}
@ -187,8 +188,8 @@ public class TemporaryIpcTester(
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
foreach (var manip in mod.Default.Manipulations)
ImGui.TextUnformatted(manip.ToString());
foreach (var identifier in mod.Default.Manipulations.Identifiers)
ImGui.TextUnformatted(identifier.ToString());
}
}
}

View file

@ -1,3 +1,4 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
@ -18,7 +19,7 @@ public enum RedirectResult
FilteredGamePath = 3,
}
public class TempModManager : IDisposable
public class TempModManager : IDisposable, IService
{
private readonly CommunicatorService _communicator;
@ -43,7 +44,7 @@ public class TempModManager : IDisposable
=> _modsForAllCollections;
public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
HashSet<MetaManipulation> manips, ModPriority priority)
MetaDictionary manips, ModPriority priority)
{
var mod = GetOrCreateMod(tag, collection, priority, out var created);
Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}.");

View file

@ -1,56 +0,0 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct CmpCache : IDisposable
{
private CmpFile? _cmpFile = null;
private readonly List<RspManipulation> _cmpManipulations = new();
public CmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public void Reset()
{
if (_cmpFile == null)
return;
_cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute)));
_cmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, RspManipulation manip)
{
_cmpManipulations.AddOrReplace(manip);
_cmpFile ??= new CmpFile(manager);
return manip.Apply(_cmpFile);
}
public bool RevertMod(MetaFileManager manager, RspManipulation manip)
{
if (!_cmpManipulations.Remove(manip))
return false;
var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute);
manip = new RspManipulation(manip.SubRace, manip.Attribute, def);
return manip.Apply(_cmpFile!);
}
public void Dispose()
{
_cmpFile?.Dispose();
_cmpFile = null;
_cmpManipulations.Clear();
}
}

View file

@ -125,12 +125,6 @@ public sealed class CollectionCache : IDisposable
return ret;
}
public void ForceFile(Utf8GamePath path, FullPath fullPath)
=> _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath));
public void RemovePath(Utf8GamePath path)
=> _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty));
public void ReloadMod(IMod mod, bool addMetaChanges)
=> _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges));
@ -233,15 +227,24 @@ public sealed class CollectionCache : IDisposable
foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod);
foreach (var manip in files.Manipulations)
AddManipulation(manip, mod);
foreach (var (identifier, entry) in files.Manipulations.Eqp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Eqdp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Est)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Gmp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Rsp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Imc)
AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!);
if (addMetaChanges)
{
_collection.IncrementCounter();
if (mod.TotalManipulations > 0)
AddMetaFiles(false);
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
@ -342,7 +345,7 @@ public sealed class CollectionCache : IDisposable
foreach (var conflict in tmpConflicts)
{
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0)
|| data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0)
AddConflict(data, addedMod, conflict.Mod2);
}
@ -374,12 +377,12 @@ public sealed class CollectionCache : IDisposable
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddManipulation(MetaManipulation manip, IMod mod)
private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry)
{
if (!Meta.TryGetValue(manip, out var existingMod))
if (!Meta.TryGetMod(identifier, out var existingMod))
{
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
return;
}
@ -387,20 +390,15 @@ public sealed class CollectionCache : IDisposable
if (mod == existingMod)
return;
if (AddConflict(manip, mod, existingMod))
if (AddConflict(identifier, mod, existingMod))
{
ModData.RemoveManip(existingMod, manip);
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
ModData.RemoveManip(existingMod, identifier);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
}
}
// Add all necessary meta file redirects.
public void AddMetaFiles(bool fromFullCompute)
=> Meta.SetImcFiles(fromFullCompute);
// Identify and record all manipulated objects for this entire collection.
private void SetChangedItems()
{
@ -437,9 +435,9 @@ public sealed class CollectionCache : IDisposable
AddItems(modPath.Mod);
}
foreach (var (manip, mod) in Meta)
foreach (var (manip, mod) in Meta.IdentifierSources)
{
identifier.MetaChangedItems(items, manip);
manip.AddChangedItems(identifier, items);
AddItems(mod);
}

View file

@ -1,5 +1,6 @@
using Dalamud.Plugin.Services;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
@ -17,7 +18,7 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class CollectionCacheManager : IDisposable
public class CollectionCacheManager : IDisposable, IService
{
private readonly FrameworkManager _framework;
private readonly CommunicatorService _communicator;
@ -180,8 +181,6 @@ public class CollectionCacheManager : IDisposable
foreach (var mod in _modStorage)
cache.AddModSync(mod, false);
cache.AddMetaFiles(true);
collection.IncrementCounter();
MetaFileManager.ApplyDefaultFiles(collection);

View file

@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache;
/// </summary>
public class CollectionModData
{
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<MetaManipulation>)> _data = new();
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<IMetaIdentifier>)> _data = new();
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<MetaManipulation>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<MetaManipulation>)kvp.Value.Item2));
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<IMetaIdentifier>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<IMetaIdentifier>)kvp.Value.Item2));
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<MetaManipulation> Manipulations) RemoveMod(IMod mod)
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<IMetaIdentifier> Manipulations) RemoveMod(IMod mod)
{
if (_data.Remove(mod, out var data))
return data;
@ -35,7 +35,7 @@ public class CollectionModData
}
}
public void AddManip(IMod mod, MetaManipulation manipulation)
public void AddManip(IMod mod, IMetaIdentifier manipulation)
{
if (_data.TryGetValue(mod, out var data))
{
@ -54,7 +54,7 @@ public class CollectionModData
_data.Remove(mod);
}
public void RemoveManip(IMod mod, MetaManipulation manip)
public void RemoveManip(IMod mod, IMetaIdentifier manip)
{
if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
_data.Remove(mod);

View file

@ -1,97 +1,54 @@
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public readonly struct EqdpCache : IDisposable
public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqdpIdentifier, EqdpEntry>(manager, collection)
{
private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
private readonly List<EqdpManipulation> _eqdpManipulations = new();
private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries =
[];
public EqdpCache()
{ }
public void SetFiles(MetaFileManager manager)
{
for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i)
manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
{
var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
if (i != -1)
manager.SetFile(_eqdpFiles[i], index);
}
public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory)
{
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
if (idx < 0)
{
Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
return null;
}
var i = CharacterUtilityData.EqdpIndices.IndexOf(idx);
if (i < 0)
{
Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
return null;
}
return manager.TemporarilySetFile(_eqdpFiles[i], idx);
}
public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry)
=> _fullEntries.TryGetValue((id, genderRace, accessory), out var pair)
? (originalEntry & pair.InverseMask) | pair.Entry
: originalEntry;
public void Reset()
{
foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>())
{
var relevant = CharacterUtility.RelevantIndices[file.Index.Value];
file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId));
}
_eqdpManipulations.Clear();
Clear();
_fullEntries.Clear();
}
public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry)
{
_eqdpManipulations.AddOrReplace(manip);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??=
new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar
return manip.Apply(file);
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
var mask = Eqdp.Mask(identifier.Slot);
var inverseMask = ~mask;
if (_fullEntries.TryGetValue(tuple, out var pair))
pair = ((pair.Entry & inverseMask) | (entry & mask), pair.InverseMask & inverseMask);
else
pair = (entry & mask, inverseMask);
_fullEntries[tuple] = pair;
}
public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
protected override void RevertModInternal(EqdpIdentifier identifier)
{
if (!_eqdpManipulations.Remove(manip))
return false;
var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory());
var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!;
manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
return manip.Apply(file);
if (!_fullEntries.Remove(tuple, out var pair))
return;
var mask = Eqdp.Mask(identifier.Slot);
var newMask = pair.InverseMask | mask;
if (newMask is not EqdpEntry.FullMask)
_fullEntries[tuple] = (pair.Entry & ~mask, newMask);
}
public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory)
=> _eqdpFiles
[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
public void Dispose()
protected override void Dispose(bool _)
{
for (var i = 0; i < _eqdpFiles.Length; ++i)
{
_eqdpFiles[i]?.Dispose();
_eqdpFiles[i] = null;
}
_eqdpManipulations.Clear();
Clear();
_fullEntries.Clear();
}
}

View file

@ -1,60 +1,27 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EqpCache : IDisposable
public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqpIdentifier, EqpEntry>(manager, collection)
{
private ExpandedEqpFile? _eqpFile = null;
private readonly List<EqpManipulation> _eqpManipulations = new();
public unsafe EqpEntry GetValues(CharacterArmor* armor)
=> GetSingleValue(armor[0].Set, EquipSlot.Head)
| GetSingleValue(armor[1].Set, EquipSlot.Body)
| GetSingleValue(armor[2].Set, EquipSlot.Hands)
| GetSingleValue(armor[3].Set, EquipSlot.Legs)
| GetSingleValue(armor[4].Set, EquipSlot.Feet);
public EqpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_eqpFile, MetaIndex.Eqp);
public static void ResetFiles(MetaFileManager manager)
=> manager.SetFile(null, MetaIndex.Eqp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot)
=> TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot);
public void Reset()
{
if (_eqpFile == null)
return;
=> Clear();
_eqpFile.Reset(_eqpManipulations.Select(m => m.SetId));
_eqpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, EqpManipulation manip)
{
_eqpManipulations.AddOrReplace(manip);
_eqpFile ??= new ExpandedEqpFile(manager);
return manip.Apply(_eqpFile);
}
public bool RevertMod(MetaFileManager manager, EqpManipulation manip)
{
var idx = _eqpManipulations.FindIndex(manip.Equals);
if (idx < 0)
return false;
var def = ExpandedEqpFile.GetDefault(manager, manip.SetId);
manip = new EqpManipulation(def, manip.Slot, manip.SetId);
return manip.Apply(_eqpFile!);
}
public void Dispose()
{
_eqpFile?.Dispose();
_eqpFile = null;
_eqpManipulations.Clear();
}
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -1,138 +1,19 @@
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EstCache : IDisposable
public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EstIdentifier, EstEntry>(manager, collection)
{
private EstFile? _estFaceFile = null;
private EstFile? _estHairFile = null;
private EstFile? _estBodyFile = null;
private EstFile? _estHeadFile = null;
private readonly List<EstManipulation> _estManipulations = new();
public EstCache()
{ }
public void SetFiles(MetaFileManager manager)
{
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
manager.SetFile(_estHairFile, MetaIndex.HairEst);
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
{
switch (index)
{
case MetaIndex.FaceEst:
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
break;
case MetaIndex.HairEst:
manager.SetFile(_estHairFile, MetaIndex.HairEst);
break;
case MetaIndex.BodyEst:
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
break;
case MetaIndex.HeadEst:
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
break;
}
}
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type)
{
var (file, idx) = type switch
{
EstType.Face => (_estFaceFile, MetaIndex.FaceEst),
EstType.Hair => (_estHairFile, MetaIndex.HairEst),
EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
EstType.Head => (_estHeadFile, MetaIndex.HeadEst),
_ => (null, 0),
};
return manager.TemporarilySetFile(file, idx);
}
private readonly EstFile? GetEstFile(EstType type)
{
return type switch
{
EstType.Face => _estFaceFile,
EstType.Hair => _estHairFile,
EstType.Body => _estBodyFile,
EstType.Head => _estHeadFile,
_ => null,
};
}
internal EstEntry GetEstEntry(MetaFileManager manager, EstType type, GenderRace genderRace, PrimaryId primaryId)
{
var file = GetEstFile(type);
return file != null
? file[genderRace, primaryId.Id]
: EstFile.GetDefault(manager, type, genderRace, primaryId);
}
public EstEntry GetEstEntry(EstIdentifier identifier)
=> TryGetValue(identifier, out var entry)
? entry.Entry
: EstFile.GetDefault(Manager, identifier);
public void Reset()
{
_estFaceFile?.Reset();
_estHairFile?.Reset();
_estBodyFile?.Reset();
_estHeadFile?.Reset();
_estManipulations.Clear();
}
=> Clear();
public bool ApplyMod(MetaFileManager manager, EstManipulation m)
{
_estManipulations.AddOrReplace(m);
var file = m.Slot switch
{
EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair),
EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face),
EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body),
EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head),
_ => throw new ArgumentOutOfRangeException(),
};
return m.Apply(file);
}
public bool RevertMod(MetaFileManager manager, EstManipulation m)
{
if (!_estManipulations.Remove(m))
return false;
var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId);
var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def);
var file = m.Slot switch
{
EstType.Hair => _estHairFile!,
EstType.Face => _estFaceFile!,
EstType.Body => _estBodyFile!,
EstType.Head => _estHeadFile!,
_ => throw new ArgumentOutOfRangeException(),
};
return manip.Apply(file);
}
public void Dispose()
{
_estFaceFile?.Dispose();
_estHairFile?.Dispose();
_estBodyFile?.Dispose();
_estHeadFile?.Dispose();
_estFaceFile = null;
_estHairFile = null;
_estBodyFile = null;
_estHeadFile = null;
_estManipulations.Clear();
}
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -1,9 +1,11 @@
using OtterGui.Services;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct GlobalEqpCache : IService
public class GlobalEqpCache : Dictionary<GlobalEqpManipulation, IMod>, IService
{
private readonly HashSet<PrimaryId> _doNotHideEarrings = [];
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
@ -13,11 +15,9 @@ public struct GlobalEqpCache : IService
private bool _doNotHideVieraHats;
private bool _doNotHideHrothgarHats;
public GlobalEqpCache()
{ }
public void Clear()
public new void Clear()
{
base.Clear();
_doNotHideEarrings.Clear();
_doNotHideNecklace.Clear();
_doNotHideBracelets.Clear();
@ -29,6 +29,9 @@ public struct GlobalEqpCache : IService
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
{
if (Count == 0)
return original;
if (_doNotHideVieraHats)
original |= EqpEntry.HeadShowVieraHat;
@ -52,8 +55,13 @@ public struct GlobalEqpCache : IService
return original;
}
public bool Add(GlobalEqpManipulation manipulation)
=> manipulation.Type switch
public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation)
{
if (Remove(manipulation, out var oldMod) && oldMod == mod)
return false;
this[manipulation] = mod;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition),
@ -61,12 +69,18 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
_ => false,
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
_ => false,
};
return true;
}
public bool Remove(GlobalEqpManipulation manipulation)
=> manipulation.Type switch
public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod)
{
if (!Remove(manipulation, out mod))
return false;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition),
@ -74,7 +88,9 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
_ => false,
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
_ => false,
};
return true;
}
}

View file

@ -1,56 +1,14 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct GmpCache : IDisposable
public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<GmpIdentifier, GmpEntry>(manager, collection)
{
private ExpandedGmpFile? _gmpFile = null;
private readonly List<GmpManipulation> _gmpManipulations = new();
public GmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_gmpFile, MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp);
public void Reset()
{
if (_gmpFile == null)
return;
=> Clear();
_gmpFile.Reset(_gmpManipulations.Select(m => m.SetId));
_gmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, GmpManipulation manip)
{
_gmpManipulations.AddOrReplace(manip);
_gmpFile ??= new ExpandedGmpFile(manager);
return manip.Apply(_gmpFile);
}
public bool RevertMod(MetaFileManager manager, GmpManipulation manip)
{
if (!_gmpManipulations.Remove(manip))
return false;
var def = ExpandedGmpFile.GetDefault(manager, manip.SetId);
manip = new GmpManipulation(def, manip.SetId);
return manip.Apply(_gmpFile!);
}
public void Dispose()
{
_gmpFile?.Dispose();
_gmpFile = null;
_gmpManipulations.Clear();
}
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -0,0 +1,60 @@
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager, ModCollection collection)
: Dictionary<TIdentifier, (IMod Source, TEntry Entry)>
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
protected readonly MetaFileManager Manager = manager;
protected readonly ModCollection Collection = collection;
public void Dispose()
{
Dispose(true);
}
public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry)
{
lock (this)
{
if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer<TEntry>.Default.Equals(pair.Entry, entry))
return false;
this[identifier] = (source, entry);
}
ApplyModInternal(identifier, entry);
return true;
}
public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
lock (this)
{
if (!Remove(identifier, out var pair))
{
mod = null;
return false;
}
mod = pair.Source;
}
RevertModInternal(identifier);
return true;
}
protected virtual void ApplyModInternal(TIdentifier identifier, TEntry entry)
{ }
protected virtual void RevertModInternal(TIdentifier identifier)
{ }
protected virtual void Dispose(bool _)
{ }
}

View file

@ -1,121 +1,103 @@
using Penumbra.Interop.PathResolving;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.String;
namespace Penumbra.Collections.Cache;
public readonly struct ImcCache : IDisposable
public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ImcIdentifier, ImcEntry>(manager, collection)
{
private readonly Dictionary<Utf8GamePath, ImcFile> _imcFiles = [];
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = [];
private readonly Dictionary<ByteString, (ImcFile, HashSet<ImcIdentifier>)> _imcFiles = [];
public ImcCache()
{ }
public bool HasFile(ByteString path)
=> _imcFiles.ContainsKey(path);
public void SetFiles(ModCollection collection, bool fromFullCompute)
public bool GetFile(ByteString path, [NotNullWhen(true)] out ImcFile? file)
{
if (fromFullCompute)
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection));
else
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection));
}
public void Reset(ModCollection collection)
{
foreach (var (path, file) in _imcFiles)
if (!_imcFiles.TryGetValue(path, out var p))
{
collection._cache!.RemovePath(path);
file.Reset();
}
_imcManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip)
{
if (!manip.Validate(true))
file = null;
return false;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip));
if (idx < 0)
{
idx = _imcManipulations.Count;
_imcManipulations.Add((manip, null!));
}
var path = manip.GamePath();
file = p.Item1;
return true;
}
public void Reset()
{
foreach (var (_, (file, set)) in _imcFiles)
{
file.Reset();
set.Clear();
}
_imcFiles.Clear();
Clear();
}
protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry)
{
++Collection.ImcChangeCounter;
ApplyFile(identifier, entry);
}
private void ApplyFile(ImcIdentifier identifier, ImcEntry entry)
{
var path = identifier.GamePath().Path;
try
{
if (!_imcFiles.TryGetValue(path, out var file))
file = new ImcFile(manager, manip.Identifier);
if (!_imcFiles.TryGetValue(path, out var pair))
pair = (new ImcFile(Manager, identifier), []);
_imcManipulations[idx] = (manip, file);
if (!manip.Apply(file))
return false;
_imcFiles[path] = file;
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection);
collection._cache!.ForceFile(path, fullPath);
if (!Apply(pair.Item1, identifier, entry))
return;
return true;
pair.Item2.Add(identifier);
_imcFiles[path] = pair;
}
catch (ImcException e)
{
manager.ValidityChecker.ImcExceptions.Add(e);
Manager.ValidityChecker.ImcExceptions.Add(e);
Penumbra.Log.Error(e.ToString());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}");
Penumbra.Log.Error($"Could not apply IMC Manipulation {identifier}:\n{e}");
}
return false;
}
public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m)
protected override void RevertModInternal(ImcIdentifier identifier)
{
if (!m.Validate(false))
return false;
++Collection.ImcChangeCounter;
var path = identifier.GamePath().Path;
if (!_imcFiles.TryGetValue(path, out var pair))
return;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m));
if (idx < 0)
return false;
if (!pair.Item2.Remove(identifier))
return;
var (_, file) = _imcManipulations[idx];
_imcManipulations.RemoveAt(idx);
if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file)))
if (pair.Item2.Count == 0)
{
_imcFiles.Remove(file.Path);
collection._cache!.ForceFile(file.Path, FullPath.Empty);
file.Dispose();
return true;
_imcFiles.Remove(path);
pair.Item1.Dispose();
return;
}
var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _);
var manip = m.Copy(def);
if (!manip.Apply(file))
return false;
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection);
collection._cache!.ForceFile(file.Path, fullPath);
return true;
var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _);
Apply(pair.Item1, identifier, def);
}
public void Dispose()
public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry)
=> file.SetEntry(ImcFile.PartIndex(identifier.EquipSlot), identifier.Variant.Id, entry);
protected override void Dispose(bool _)
{
foreach (var file in _imcFiles.Values)
foreach (var (_, (file, _)) in _imcFiles)
file.Dispose();
Clear();
_imcFiles.Clear();
_imcManipulations.Clear();
}
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
=> _imcFiles.TryGetValue(path, out file);
}

View file

@ -1,7 +1,5 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
@ -9,238 +7,109 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>>
public class MetaCache(MetaFileManager manager, ModCollection collection)
{
private readonly MetaFileManager _manager;
private readonly ModCollection _collection;
private readonly Dictionary<MetaManipulation, IMod> _manipulations = new();
private EqpCache _eqpCache = new();
private readonly EqdpCache _eqdpCache = new();
private EstCache _estCache = new();
private GmpCache _gmpCache = new();
private CmpCache _cmpCache = new();
private readonly ImcCache _imcCache = new();
private GlobalEqpCache _globalEqpCache = new();
public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
return _manipulations.TryGetValue(manip, out mod);
}
}
public readonly EqpCache Eqp = new(manager, collection);
public readonly EqdpCache Eqdp = new(manager, collection);
public readonly EstCache Est = new(manager, collection);
public readonly GmpCache Gmp = new(manager, collection);
public readonly RspCache Rsp = new(manager, collection);
public readonly ImcCache Imc = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new();
public int Count
=> _manipulations.Count;
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count;
public IReadOnlyCollection<MetaManipulation> Manipulations
=> _manipulations.Keys;
public IEnumerator<KeyValuePair<MetaManipulation, IMod>> GetEnumerator()
=> _manipulations.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public MetaCache(MetaFileManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
if (!_manager.CharacterUtility.Ready)
_manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
}
public void SetFiles()
{
_eqpCache.SetFiles(_manager);
_eqdpCache.SetFiles(_manager);
_estCache.SetFiles(_manager);
_gmpCache.SetFiles(_manager);
_cmpCache.SetFiles(_manager);
_imcCache.SetFiles(_collection, false);
}
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
.Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void Reset()
{
_eqpCache.Reset();
_eqdpCache.Reset();
_estCache.Reset();
_gmpCache.Reset();
_cmpCache.Reset();
_imcCache.Reset(_collection);
_manipulations.Clear();
_globalEqpCache.Clear();
Eqp.Reset();
Eqdp.Reset();
Est.Reset();
Gmp.Reset();
Rsp.Reset();
Imc.Reset();
GlobalEqp.Clear();
}
public void Dispose()
{
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
_eqpCache.Dispose();
_eqdpCache.Dispose();
_estCache.Dispose();
_gmpCache.Dispose();
_cmpCache.Dispose();
_imcCache.Dispose();
_manipulations.Clear();
Eqp.Dispose();
Eqdp.Dispose();
Est.Dispose();
Gmp.Dispose();
Rsp.Dispose();
Imc.Dispose();
}
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
mod = null;
return identifier switch
{
EqdpIdentifier i => Eqdp.TryGetValue(i, out var p) && Convert(p, out mod),
EqpIdentifier i => Eqp.TryGetValue(i, out var p) && Convert(p, out mod),
EstIdentifier i => Est.TryGetValue(i, out var p) && Convert(p, out mod),
GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod),
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
_ => false,
};
static bool Convert<T>((IMod, T) pair, out IMod mod)
{
mod = pair.Item1;
return true;
}
}
public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
=> identifier switch
{
EqdpIdentifier i => Eqdp.RevertMod(i, out mod),
EqpIdentifier i => Eqp.RevertMod(i, out mod),
EstIdentifier i => Est.RevertMod(i, out mod),
GmpIdentifier i => Gmp.RevertMod(i, out mod),
ImcIdentifier i => Imc.RevertMod(i, out mod),
RspIdentifier i => Rsp.RevertMod(i, out mod),
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
_ => (mod = null) != null,
};
public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry)
=> identifier switch
{
EqdpIdentifier i when entry is EqdpEntry e => Eqdp.ApplyMod(mod, i, e),
EqdpIdentifier i when entry is EqdpEntryInternal e => Eqdp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EqpIdentifier i when entry is EqpEntry e => Eqp.ApplyMod(mod, i, e),
EqpIdentifier i when entry is EqpEntryInternal e => Eqp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EstIdentifier i when entry is EstEntry e => Est.ApplyMod(mod, i, e),
GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e),
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
_ => false,
};
~MetaCache()
=> Dispose();
public bool ApplyMod(MetaManipulation manip, IMod mod)
{
lock (_manipulations)
{
if (_manipulations.ContainsKey(manip))
_manipulations.Remove(manip);
_manipulations[manip] = mod;
}
if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp)
return _globalEqpCache.Add(manip.GlobalEqp);
if (!_manager.CharacterUtility.Ready)
return true;
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
var ret = _manipulations.Remove(manip, out mod);
if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp)
return _globalEqpCache.Remove(manip.GlobalEqp);
if (!_manager.CharacterUtility.Ready)
return ret;
}
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
/// <summary> Set a single file. </summary>
public void SetFile(MetaIndex metaIndex)
{
switch (metaIndex)
{
case MetaIndex.Eqp:
_eqpCache.SetFiles(_manager);
break;
case MetaIndex.Gmp:
_gmpCache.SetFiles(_manager);
break;
case MetaIndex.HumanCmp:
_cmpCache.SetFiles(_manager);
break;
case MetaIndex.FaceEst:
case MetaIndex.HairEst:
case MetaIndex.HeadEst:
case MetaIndex.BodyEst:
_estCache.SetFile(_manager, metaIndex);
break;
default:
_eqdpCache.SetFile(_manager, metaIndex);
break;
}
}
/// <summary> Set the currently relevant IMC files for the collection cache. </summary>
public void SetImcFiles(bool fromFullCompute)
=> _imcCache.SetFiles(_collection, fromFullCompute);
public MetaList.MetaReverter TemporarilySetEqpFile()
=> _eqpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory);
public MetaList.MetaReverter TemporarilySetGmpFile()
=> _gmpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter TemporarilySetCmpFile()
=> _cmpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter TemporarilySetEstFile(EstType type)
=> _estCache.TemporarilySetFiles(_manager, type);
public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor)
=> _globalEqpCache.Apply(baseEntry, armor);
/// <summary> Try to obtain a manipulated IMC file. </summary>
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
=> _imcCache.GetImcFile(path, out file);
=> Imc.GetFile(path.Path, out file);
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId)
{
var eqdpFile = _eqdpCache.EqdpFile(race, accessory);
if (eqdpFile != null)
return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default;
return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId);
}
=> Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId));
internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
=> _estCache.GetEstEntry(_manager, type, genderRace, primaryId);
/// <summary> Use this when CharacterUtility becomes ready. </summary>
private void ApplyStoredManipulations()
{
if (!_manager.CharacterUtility.Ready)
return;
var loaded = 0;
lock (_manipulations)
{
foreach (var manip in Manipulations)
{
loaded += manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.GlobalEqp => false,
MetaManipulation.Type.Unknown => false,
_ => false,
}
? 1
: 0;
}
}
_manager.ApplyDefaultFiles(_collection);
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations.");
}
=> Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace));
}

View file

@ -0,0 +1,13 @@
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<RspIdentifier, RspEntry>(manager, collection)
{
public void Reset()
=> Clear();
protected override void Dispose(bool _)
=> Clear();
}

View file

@ -3,6 +3,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
@ -11,7 +12,7 @@ using Penumbra.UI;
namespace Penumbra.Collections.Manager;
public class ActiveCollectionData
public class ActiveCollectionData : IService
{
public ModCollection Current { get; internal set; } = ModCollection.Empty;
public ModCollection Default { get; internal set; } = ModCollection.Empty;
@ -20,7 +21,7 @@ public class ActiveCollectionData
public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
}
public class ActiveCollections : ISavable, IDisposable
public class ActiveCollections : ISavable, IDisposable, IService
{
public const int Version = 2;

View file

@ -1,4 +1,5 @@
using OtterGui;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
@ -7,7 +8,7 @@ using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage)
public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService
{
/// <summary> Enable or disable the mod inheritance of mod idx. </summary>
public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit)

View file

@ -1,3 +1,4 @@
using OtterGui.Services;
using Penumbra.Collections.Cache;
namespace Penumbra.Collections.Manager;
@ -8,7 +9,7 @@ public class CollectionManager(
InheritanceManager inheritances,
CollectionCacheManager caches,
TempCollectionManager temp,
CollectionEditor editor)
CollectionEditor editor) : IService
{
public readonly CollectionStorage Storage = storage;
public readonly ActiveCollections Active = active;

View file

@ -1,7 +1,7 @@
using System;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
@ -11,7 +11,6 @@ using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
namespace Penumbra.Collections.Manager;
@ -24,7 +23,7 @@ public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<Loc
=> new(left.Id + right);
}
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;

View file

@ -2,11 +2,10 @@ using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
@ -15,7 +14,7 @@ namespace Penumbra.Collections.Manager;
/// This is transitive, so a collection A inheriting from B also inherits from everything B inherits.
/// Circular dependencies are resolved by distinctness.
/// </summary>
public class InheritanceManager : IDisposable
public class InheritanceManager : IDisposable, IService
{
public enum ValidInheritance
{
@ -144,7 +143,8 @@ public class InheritanceManager : IDisposable
continue;
changes = true;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.",
NotificationType.Warning);
}
else if (_storage.ByName(subCollectionName, out subCollection))
{
@ -153,12 +153,14 @@ public class InheritanceManager : IDisposable
if (AddInheritance(collection, subCollection, false))
continue;
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning);
Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.",
NotificationType.Warning);
}
else
{
Penumbra.Messager.NotificationMessage(
$"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", NotificationType.Warning);
$"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.",
NotificationType.Warning);
changes = true;
}
}

View file

@ -1,4 +1,5 @@
using OtterGui;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
@ -8,7 +9,7 @@ using Penumbra.String;
namespace Penumbra.Collections.Manager;
public class TempCollectionManager : IDisposable
public class TempCollectionManager : IDisposable, IService
{
public int GlobalChangeCounter { get; private set; }
public readonly IndividualCollections Collections;

View file

@ -1,14 +1,9 @@
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.Mods;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.Collections.Cache;
using Penumbra.Interop.Services;
using Penumbra.Mods.Editor;
using Penumbra.GameData.Structs;
namespace Penumbra.Collections;
@ -68,54 +63,4 @@ public partial class ModCollection
internal SingleArray<ModConflicts> Conflicts(Mod mod)
=> _cache?.Conflicts(mod) ?? new SingleArray<ModConflicts>();
public void SetFiles(CharacterUtility utility)
{
if (_cache == null)
{
utility.ResetAll();
}
else
{
_cache.Meta.SetFiles();
Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Identifier}.");
}
}
public void SetMetaFile(CharacterUtility utility, MetaIndex idx)
{
if (_cache == null)
utility.ResetResource(idx);
else
_cache.Meta.SetFile(idx);
}
// Used for short periods of changed files.
public MetaList.MetaReverter? TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory)
{
if (_cache != null)
return _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory);
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
return idx >= 0 ? utility.TemporarilyResetResource(idx) : null;
}
public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetEqpFile()
?? utility.TemporarilyResetResource(MetaIndex.Eqp);
public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetGmpFile()
?? utility.TemporarilyResetResource(MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility)
=> _cache?.Meta.TemporarilySetCmpFile()
?? utility.TemporarilyResetResource(MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type)
=> _cache?.Meta.TemporarilySetEstFile(type)
?? utility.TemporarilyResetResource((MetaIndex)type);
public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor)
=> _cache?.Meta.ApplyGlobalEqp(baseEntry, armor) ?? baseEntry;
}

View file

@ -56,6 +56,8 @@ public partial class ModCollection
/// </summary>
public int ChangeCounter { get; private set; }
public uint ImcChangeCounter { get; set; }
/// <summary> Increment the number of changes in the effective file list. </summary>
public int IncrementCounter()
=> ++ChangeCounter;

View file

@ -3,6 +3,7 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using ImGuiNET;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -10,12 +11,11 @@ using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
namespace Penumbra;
public class CommandHandler : IDisposable
public class CommandHandler : IDisposable, IApiService
{
private const string CommandName = "/penumbra";

View file

@ -1,5 +1,4 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Services;
@ -19,7 +18,7 @@ public sealed class CreatingCharacterBase()
{
public enum Priority
{
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
/// <seealso cref="GameStateApi.CreatingCharacterBase"/>
Api = 0,
/// <seealso cref="CrashHandlerService.OnCreatingCharacterBase"/>

View file

@ -4,6 +4,7 @@ using Newtonsoft.Json;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Services;
using OtterGui.Widgets;
using Penumbra.Import.Structs;
using Penumbra.Interop.Services;
@ -18,7 +19,7 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
[Serializable]
public class Configuration : IPluginConfiguration, ISavable
public class Configuration : IPluginConfiguration, ISavable, IService
{
[JsonIgnore]
private readonly SaveService _saveService;

View file

@ -1,6 +1,7 @@
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Enums;
@ -14,7 +15,7 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
public class EphemeralConfig : ISavable, IDisposable
public class EphemeralConfig : ISavable, IDisposable, IService
{
[JsonIgnore]
private readonly SaveService _saveService;

View file

@ -1,6 +1,7 @@
using Dalamud.Plugin.Services;
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Services;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;
using Penumbra.GameData;
@ -21,7 +22,8 @@ namespace Penumbra.Import.Models;
using Schema2 = SharpGLTF.Schema2;
using LuminaMaterial = Lumina.Models.Materials.Material;
public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable
public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser)
: SingleTaskQueue, IDisposable, IService
{
private readonly IFramework _framework = framework;
@ -37,7 +39,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
_tasks.Clear();
}
public Task<IoNotifier> ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]?> read, string outputPath)
public Task<IoNotifier> ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]?> read,
string outputPath)
=> EnqueueWithResult(
new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath),
action => action.Notifier
@ -52,7 +55,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
/// <summary> Try to find the .sklb paths for a .mdl file. </summary>
/// <param name="mdlPath"> .mdl file to look up the skeletons for. </param>
/// <param name="estManipulations"> Modified extra skeleton template parameters. </param>
public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations)
public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations)
{
var info = parser.GetFileInfo(mdlPath);
if (info.FileType is not FileType.Model)
@ -81,20 +84,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
};
}
private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations)
private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations)
{
// Try to find an EST entry from the manipulations provided.
var (gender, race) = info.GenderRace.Split();
var modEst = estManipulations
.FirstOrNull(est =>
est.Gender == gender
&& est.Race == race
&& est.Slot == type
&& est.SetId == info.PrimaryId
.FirstOrNull(
est => est.Key.GenderRace == info.GenderRace
&& est.Key.Slot == type
&& est.Key.SetId == info.PrimaryId
);
// Try to use an entry from provided manipulations, falling back to the current collection.
var targetId = modEst?.Entry
var targetId = modEst?.Value
?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
?? EstEntry.Zero;
@ -102,7 +103,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
if (targetId == EstEntry.Zero)
return [];
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId.AsId)];
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)];
}
/// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary>
@ -250,9 +251,11 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
var path = manager.ResolveMtrlPath(relativePath, notifier);
if (path == null)
return null;
var bytes = read(path);
if (bytes == null)
return null;
var mtrl = new MtrlFile(bytes);
return new MaterialExporter.Material

View file

@ -13,15 +13,15 @@ public partial class TexToolsMeta
private void DeserializeEqpEntry(MetaFileInfo metaFileInfo, byte[]? data)
{
// Eqp can only be valid for equipment.
if (data == null || !metaFileInfo.EquipSlot.IsEquipment())
var mask = Eqp.Mask(metaFileInfo.EquipSlot);
if (data == null || mask == 0)
return;
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data);
var def = new EqpManipulation(ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId), metaFileInfo.EquipSlot,
metaFileInfo.PrimaryId);
var manip = new EqpManipulation(value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId);
if (_keepDefault || def.Entry != manip.Entry)
MetaManipulations.Add(manip);
var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot);
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask;
var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask;
if (_keepDefault || def != value)
MetaManipulations.TryAdd(identifier, value);
}
// Deserialize and check Eqdp Entries and add them to the list if they are non-default.
@ -40,14 +40,12 @@ public partial class TexToolsMeta
if (!gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory())
continue;
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2);
var def = new EqdpManipulation(
ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId),
metaFileInfo.EquipSlot,
gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId);
var manip = new EqdpManipulation(value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId);
if (_keepDefault || def.Entry != manip.Entry)
MetaManipulations.Add(manip);
var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr);
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask;
if (_keepDefault || def != value)
MetaManipulations.TryAdd(identifier, value);
}
}
@ -57,10 +55,10 @@ public partial class TexToolsMeta
if (data == null)
return;
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
if (_keepDefault || value != def)
MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId));
MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value);
}
// Deserialize and check Est Entries and add them to the list if they are non-default.
@ -74,7 +72,7 @@ public partial class TexToolsMeta
for (var i = 0; i < num; ++i)
{
var gr = (GenderRace)reader.ReadUInt16();
var id = reader.ReadUInt16();
var id = (PrimaryId)reader.ReadUInt16();
var value = new EstEntry(reader.ReadUInt16());
var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch
{
@ -87,9 +85,10 @@ public partial class TexToolsMeta
if (!gr.IsValid() || type == 0)
continue;
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
var identifier = new EstIdentifier(id, type, gr);
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
if (_keepDefault || def != value)
MetaManipulations.Add(new EstManipulation(gr.Split().Item1, gr.Split().Item2, type, id, value));
MetaManipulations.TryAdd(identifier, value);
}
}
@ -107,20 +106,16 @@ public partial class TexToolsMeta
ushort i = 0;
try
{
var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId,
metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot,
new ImcEntry());
var def = new ImcFile(_metaFileManager, manip.Identifier);
var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId,
metaFileInfo.EquipSlot, metaFileInfo.SecondaryType);
var file = new ImcFile(_metaFileManager, identifier);
var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
foreach (var value in values)
{
if (_keepDefault || !value.Equals(def.GetEntry(partIdx, (Variant)i)))
{
var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot,
value);
if (imc.Validate(true))
MetaManipulations.Add(imc);
}
identifier = identifier with { Variant = (Variant)i };
var def = file.GetEntry(partIdx, (Variant)i);
if (_keepDefault || def != value && identifier.Validate())
MetaManipulations.TryAdd(identifier, value);
++i;
}

View file

@ -1,3 +1,4 @@
using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
@ -8,7 +9,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta
{
public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable<MetaManipulation> manipulations, DirectoryInfo basePath)
public static void WriteTexToolsMeta(MetaFileManager manager, MetaDictionary manipulations, DirectoryInfo basePath)
{
var files = ConvertToTexTools(manager, manipulations);
@ -27,49 +28,81 @@ public partial class TexToolsMeta
}
}
public static Dictionary<string, byte[]> ConvertToTexTools(MetaFileManager manager, IEnumerable<MetaManipulation> manips)
public static Dictionary<string, byte[]> ConvertToTexTools(MetaFileManager manager, MetaDictionary manips)
{
var ret = new Dictionary<string, byte[]>();
foreach (var group in manips.GroupBy(ManipToPath))
foreach (var group in manips.Rsp.GroupBy(ManipToPath))
{
if (group.Key.Length == 0)
continue;
var bytes = group.Key.EndsWith(".rgsp")
? WriteRgspFile(manager, group.Key, group)
: WriteMetaFile(manager, group.Key, group);
var bytes = WriteRgspFile(manager, group);
if (bytes.Length == 0)
continue;
ret.Add(group.Key, bytes);
}
foreach (var (file, dict) in SplitByFile(manips))
{
var bytes = WriteMetaFile(manager, file, dict);
if (bytes.Length == 0)
continue;
ret.Add(file, bytes);
}
return ret;
}
private static byte[] WriteRgspFile(MetaFileManager manager, string path, IEnumerable<MetaManipulation> manips)
private static Dictionary<string, MetaDictionary> SplitByFile(MetaDictionary manips)
{
var list = manips.GroupBy(m => m.Rsp.Attribute).ToDictionary(m => m.Key, m => m.Last().Rsp);
var ret = new Dictionary<string, MetaDictionary>();
foreach (var (identifier, key) in manips.Imc)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Eqp)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Eqdp)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Est)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Gmp)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
ret.Remove(string.Empty);
return ret;
MetaDictionary GetDict(string path)
{
if (!ret.TryGetValue(path, out var dict))
{
dict = new MetaDictionary();
ret.Add(path, dict);
}
return dict;
}
}
private static byte[] WriteRgspFile(MetaFileManager manager, IEnumerable<KeyValuePair<RspIdentifier, RspEntry>> manips)
{
var list = manips.GroupBy(m => m.Key.Attribute).ToDictionary(g => g.Key, g => g.Last());
using var m = new MemoryStream(45);
using var b = new BinaryWriter(m);
// Version
b.Write(byte.MaxValue);
b.Write((ushort)2);
var race = list.First().Value.SubRace;
var gender = list.First().Value.Attribute.ToGender();
var race = list.First().Value.Key.SubRace;
var gender = list.First().Value.Key.Attribute.ToGender();
b.Write((byte)(race - 1)); // offset by one due to Unknown
b.Write((byte)(gender - 1)); // offset by one due to Unknown
void Add(params RspAttribute[] attributes)
{
foreach (var attribute in attributes)
{
var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute);
b.Write(value.Value);
}
}
if (gender == Gender.Male)
{
Add(RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail);
@ -82,12 +115,24 @@ public partial class TexToolsMeta
}
return m.GetBuffer();
void Add(params RspAttribute[] attributes)
{
foreach (var attribute in attributes)
{
var value = list.TryGetValue(attribute, out var tmp) ? tmp.Value : CmpFile.GetDefault(manager, race, attribute);
b.Write(value.Value);
}
}
}
private static byte[] WriteMetaFile(MetaFileManager manager, string path, IEnumerable<MetaManipulation> manips)
private static byte[] WriteMetaFile(MetaFileManager manager, string path, MetaDictionary manips)
{
var filteredManips = manips.GroupBy(m => m.ManipulationType).ToDictionary(p => p.Key, p => p.Select(x => x));
var headerCount = (manips.Imc.Count > 0 ? 1 : 0)
+ (manips.Eqp.Count > 0 ? 1 : 0)
+ (manips.Eqdp.Count > 0 ? 1 : 0)
+ (manips.Est.Count > 0 ? 1 : 0)
+ (manips.Gmp.Count > 0 ? 1 : 0);
using var m = new MemoryStream();
using var b = new BinaryWriter(m);
@ -101,7 +146,7 @@ public partial class TexToolsMeta
b.Write((byte)0);
// Number of Headers
b.Write((uint)filteredManips.Count);
b.Write((uint)headerCount);
// Current TT Size of Headers
b.Write((uint)12);
@ -109,88 +154,44 @@ public partial class TexToolsMeta
var headerStart = b.BaseStream.Position + 4;
b.Write((uint)headerStart);
var offset = (uint)(b.BaseStream.Position + 12 * filteredManips.Count);
foreach (var (header, data) in filteredManips)
{
b.Write((uint)header);
b.Write(offset);
var size = WriteData(manager, b, offset, header, data);
b.Write(size);
offset += size;
}
var offset = (uint)(b.BaseStream.Position + 12 * manips.Count);
offset += WriteData(manager, b, offset, manips.Imc);
offset += WriteData(b, offset, manips.Eqdp);
offset += WriteData(b, offset, manips.Eqp);
offset += WriteData(b, offset, manips.Est);
offset += WriteData(b, offset, manips.Gmp);
return m.ToArray();
}
private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type,
IEnumerable<MetaManipulation> manips)
private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, IReadOnlyDictionary<ImcIdentifier, ImcEntry> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
switch (type)
var refIdentifier = manips.First().Key;
var baseFile = new ImcFile(manager, refIdentifier);
foreach (var (identifier, entry) in manips)
ImcCache.Apply(baseFile, identifier, entry);
var partIdx = refIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory
? ImcFile.PartIndex(refIdentifier.EquipSlot)
: 0;
for (var i = 0; i <= baseFile.Count; ++i)
{
case MetaManipulation.Type.Imc:
var allManips = manips.ToList();
var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier);
foreach (var manip in allManips)
manip.Imc.Apply(baseFile);
var partIdx = allManips[0].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory
? ImcFile.PartIndex(allManips[0].Imc.EquipSlot)
: 0;
for (var i = 0; i <= baseFile.Count; ++i)
{
var entry = baseFile.GetEntry(partIdx, (Variant)i);
b.Write(entry.MaterialId);
b.Write(entry.DecalId);
b.Write(entry.AttributeAndSound);
b.Write(entry.VfxId);
b.Write(entry.MaterialAnimationId);
}
break;
case MetaManipulation.Type.Eqdp:
foreach (var manip in manips)
{
b.Write((uint)Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race));
var entry = (byte)(((uint)manip.Eqdp.Entry >> Eqdp.Offset(manip.Eqdp.Slot)) & 0x03);
b.Write(entry);
}
break;
case MetaManipulation.Type.Eqp:
foreach (var manip in manips)
{
var bytes = BitConverter.GetBytes((ulong)manip.Eqp.Entry);
var (numBytes, byteOffset) = Eqp.BytesAndOffset(manip.Eqp.Slot);
for (var i = byteOffset; i < numBytes + byteOffset; ++i)
b.Write(bytes[i]);
}
break;
case MetaManipulation.Type.Est:
foreach (var manip in manips)
{
b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race));
b.Write(manip.Est.SetId.Id);
b.Write(manip.Est.Entry.Value);
}
break;
case MetaManipulation.Type.Gmp:
foreach (var manip in manips)
{
b.Write((uint)manip.Gmp.Entry.Value);
b.Write(manip.Gmp.Entry.UnknownTotal);
}
break;
case MetaManipulation.Type.GlobalEqp:
// Not Supported
break;
var entry = baseFile.GetEntry(partIdx, (Variant)i);
b.Write(entry.MaterialId);
b.Write(entry.DecalId);
b.Write(entry.AttributeAndSound);
b.Write(entry.VfxId);
b.Write(entry.MaterialAnimationId);
}
var size = b.BaseStream.Position - offset;
@ -198,19 +199,98 @@ public partial class TexToolsMeta
return (uint)size;
}
private static string ManipToPath(MetaManipulation manip)
=> manip.ManipulationType switch
{
MetaManipulation.Type.Imc => ManipToPath(manip.Imc),
MetaManipulation.Type.Eqdp => ManipToPath(manip.Eqdp),
MetaManipulation.Type.Eqp => ManipToPath(manip.Eqp),
MetaManipulation.Type.Est => ManipToPath(manip.Est),
MetaManipulation.Type.Gmp => ManipToPath(manip.Gmp),
MetaManipulation.Type.Rsp => ManipToPath(manip.Rsp),
_ => string.Empty,
};
private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> manips)
{
if (manips.Count == 0)
return 0;
private static string ManipToPath(ImcManipulation manip)
b.Write((uint)MetaManipulationType.Eqdp);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var (identifier, entry) in manips)
{
b.Write((uint)identifier.GenderRace);
b.Write(entry.AsByte);
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static uint WriteData(BinaryWriter b, uint offset,
IReadOnlyDictionary<EqpIdentifier, EqpEntryInternal> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var (identifier, entry) in manips)
{
var numBytes = Eqp.BytesAndOffset(identifier.Slot).Item1;
for (var i = 0; i < numBytes; ++i)
b.Write((byte)(entry.Value >> (8 * i)));
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary<EstIdentifier, EstEntry> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var (identifier, entry) in manips)
{
b.Write((ushort)identifier.GenderRace);
b.Write(identifier.SetId.Id);
b.Write(entry.Value);
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary<GmpIdentifier, GmpEntry> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var entry in manips.Values)
{
b.Write((uint)entry.Value);
b.Write(entry.UnknownTotal);
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static string ManipToPath(ImcIdentifier manip)
{
var path = manip.GamePath().ToString();
var replacement = manip.ObjectType switch
@ -224,33 +304,33 @@ public partial class TexToolsMeta
return path.Replace(".imc", replacement);
}
private static string ManipToPath(EqdpManipulation manip)
private static string ManipToPath(EqdpIdentifier manip)
=> manip.Slot.IsAccessory()
? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta";
? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta";
private static string ManipToPath(EqpManipulation manip)
private static string ManipToPath(EqpIdentifier manip)
=> manip.Slot.IsAccessory()
? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta";
? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta";
private static string ManipToPath(EstManipulation manip)
private static string ManipToPath(EstIdentifier manip)
{
var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode();
return manip.Slot switch
{
EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta",
EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta",
EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta",
EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta",
_ => throw new ArgumentOutOfRangeException(),
EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId.Id:D4}/c{raceCode}h{manip.SetId.Id:D4}_hir.meta",
EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId.Id:D4}/c{raceCode}f{manip.SetId.Id:D4}_fac.meta",
EstType.Body => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Body.ToSuffix()}.meta",
EstType.Head => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta",
_ => throw new ArgumentOutOfRangeException(),
};
}
private static string ManipToPath(GmpManipulation manip)
=> $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta";
private static string ManipToPath(GmpIdentifier manip)
=> $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta";
private static string ManipToPath(RspManipulation manip)
=> $"chara/xls/charamake/rgsp/{(int)manip.SubRace - 1}-{(int)manip.Attribute.ToGender() - 1}.rgsp";
private static string ManipToPath(KeyValuePair<RspIdentifier, RspEntry> manip)
=> $"chara/xls/charamake/rgsp/{(int)manip.Key.SubRace - 1}-{(int)manip.Key.Attribute.ToGender() - 1}.rgsp";
}

View file

@ -42,14 +42,6 @@ public partial class TexToolsMeta
return Invalid;
}
// Add the given values to the manipulations if they are not default.
void Add(RspAttribute attribute, float value)
{
var def = CmpFile.GetDefault(manager, subRace, attribute);
if (keepDefault || value != def.Value)
ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, new RspEntry(value)));
}
if (gender == 1)
{
Add(RspAttribute.FemaleMinSize, br.ReadSingle());
@ -73,5 +65,14 @@ public partial class TexToolsMeta
}
return ret;
// Add the given values to the manipulations if they are not default.
void Add(RspAttribute attribute, float value)
{
var identifier = new RspIdentifier(subRace, attribute);
var def = CmpFile.GetDefault(manager, subRace, attribute);
if (keepDefault || value != def.Value)
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
}
}
}

View file

@ -1,4 +1,3 @@
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.Import.Structs;
using Penumbra.Meta;
@ -22,10 +21,10 @@ public partial class TexToolsMeta
public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0);
// The info class determines the files or table locations the changes need to apply to from the filename.
public readonly uint Version;
public readonly string FilePath;
public readonly List<MetaManipulation> MetaManipulations = new();
private readonly bool _keepDefault = false;
public readonly uint Version;
public readonly string FilePath;
public readonly MetaDictionary MetaManipulations = new();
private readonly bool _keepDefault;
private readonly MetaFileManager _metaFileManager;
@ -44,18 +43,18 @@ public partial class TexToolsMeta
var headerStart = reader.ReadUInt32();
reader.BaseStream.Seek(headerStart, SeekOrigin.Begin);
List<(MetaManipulation.Type type, uint offset, int size)> entries = [];
List<(MetaManipulationType type, uint offset, int size)> entries = [];
for (var i = 0; i < numHeaders; ++i)
{
var currentOffset = reader.BaseStream.Position;
var type = (MetaManipulation.Type)reader.ReadUInt32();
var type = (MetaManipulationType)reader.ReadUInt32();
var offset = reader.ReadUInt32();
var size = reader.ReadInt32();
entries.Add((type, offset, size));
reader.BaseStream.Seek(currentOffset + headerSize, SeekOrigin.Begin);
}
byte[]? ReadEntry(MetaManipulation.Type type)
byte[]? ReadEntry(MetaManipulationType type)
{
var idx = entries.FindIndex(t => t.type == type);
if (idx < 0)
@ -65,11 +64,11 @@ public partial class TexToolsMeta
return reader.ReadBytes(entries[idx].size);
}
DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Eqp));
DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Gmp));
DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulation.Type.Eqdp));
DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulation.Type.Est));
DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulation.Type.Imc));
DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulationType.Eqp));
DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulationType.Gmp));
DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulationType.Eqdp));
DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulationType.Est));
DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulationType.Imc));
}
catch (Exception e)
{

View file

@ -3,6 +3,7 @@ using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using Lumina.Data.Files;
using OtterGui.Log;
using OtterGui.Services;
using OtterGui.Tasks;
using OtterTex;
using SixLabors.ImageSharp;
@ -12,22 +13,14 @@ using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures;
public sealed class TextureManager : SingleTaskQueue, IDisposable
public sealed class TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger)
: SingleTaskQueue, IDisposable, IService
{
private readonly Logger _logger;
private readonly UiBuilder _uiBuilder;
private readonly IDataManager _gameData;
private readonly Logger _logger = logger;
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
private bool _disposed;
public TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger)
{
_uiBuilder = uiBuilder;
_gameData = gameData;
_logger = logger;
}
public IReadOnlyDictionary<IAction, (Task, CancellationTokenSource)> Tasks
=> _tasks;
@ -64,7 +57,8 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable
{
var token = new CancellationTokenSource();
var task = Enqueue(a, token.Token);
task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.Default);
return (task, token);
}).Item1;
}
@ -217,7 +211,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable
/// <summary> Load a texture wrap for a given image. </summary>
public IDalamudTextureWrap LoadTextureWrap(byte[] rgba, int width, int height)
=> _uiBuilder.LoadImageRaw(rgba, width, height, 4);
=> uiBuilder.LoadImageRaw(rgba, width, height, 4);
/// <summary> Load any supported file from game data or drive depending on extension and if the path is rooted. </summary>
public (BaseImage, TextureType) Load(string path)
@ -326,7 +320,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable
}
public bool GameFileExists(string path)
=> _gameData.FileExists(path);
=> gameData.FileExists(path);
/// <summary> Add up to 13 mip maps to the input if mip maps is true, otherwise return input. </summary>
public static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps)
@ -382,7 +376,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable
if (Path.IsPathRooted(path))
return File.OpenRead(path);
var file = _gameData.GetFile(path);
var file = gameData.GetFile(path);
return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{path}\" from game files.");
}

View file

@ -1,5 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
@ -22,10 +23,11 @@ public sealed unsafe class CalculateHeight : FastHook<CalculateHeight.Delegate>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private ulong Detour(Character* character)
{
var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true);
using var cmp = _metaState.ResolveRspData(collection.ModCollection);
var ret = Task.Result.Original.Invoke(character);
var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true);
_metaState.RspCollection.Push(collection);
var ret = Task.Result.Original.Invoke(character);
Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}.");
_metaState.RspCollection.Pop();
return ret;
}
}

View file

@ -1,12 +1,12 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
using Penumbra.GameData;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
{
private readonly CollectionResolver _collectionResolver;
@ -24,13 +24,15 @@ public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment)
{
_metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true);
using var cmp = _metaState.ResolveRspData(_metaState.CustomizeChangeCollection.ModCollection);
var collection = _collectionResolver.IdentifyCollection((DrawObject*)human, true);
_metaState.CustomizeChangeCollection = collection;
_metaState.RspCollection.Push(collection);
using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true);
using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false);
var ret = Task.Result.Original.Invoke(human, data, skipEquipment);
Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}.");
_metaState.CustomizeChangeCollection = ResolveData.Invalid;
_metaState.RspCollection.Pop();
return ret;
}
}
}

View file

@ -0,0 +1,30 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
public unsafe class EqdpAccessoryHook : FastHook<EqdpAccessoryHook.Delegate>
{
public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode);
private readonly MetaState _metaState;
public EqdpAccessoryHook(HookManager hooks, MetaState metaState)
{
_metaState = metaState;
Task = hooks.CreateHook<Delegate>("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, true);
}
private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode)
{
Task.Result.Original(utility, entry, setId, raceCode);
if (_metaState.EqdpCollection.TryPeek(out var collection)
&& collection is { Valid: true, ModCollection.MetaCache: { } cache })
*entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, *entry);
Penumbra.Log.Excessive(
$"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}.");
}
}

View file

@ -0,0 +1,30 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
public unsafe class EqdpEquipHook : FastHook<EqdpEquipHook.Delegate>
{
public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode);
private readonly MetaState _metaState;
public EqdpEquipHook(HookManager hooks, MetaState metaState)
{
_metaState = metaState;
Task = hooks.CreateHook<Delegate>("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, true);
}
private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode)
{
Task.Result.Original(utility, entry, setId, raceCode);
if (_metaState.EqdpCollection.TryPeek(out var collection)
&& collection is { Valid: true, ModCollection.MetaCache: { } cache })
*entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, *entry);
Penumbra.Log.Excessive(
$"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}.");
}
}

View file

@ -19,11 +19,10 @@ public unsafe class EqpHook : FastHook<EqpHook.Delegate>
private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor)
{
if (_metaState.EqpCollection.Valid)
if (_metaState.EqpCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache })
{
using var eqp = _metaState.ResolveEqpData(_metaState.EqpCollection.ModCollection);
Task.Result.Original(utility, flags, armor);
*flags = _metaState.EqpCollection.ModCollection.ApplyGlobalEqp(*flags, armor);
*flags = cache.Eqp.GetValues(armor);
*flags = cache.GlobalEqp.Apply(*flags, armor);
}
else
{

View file

@ -0,0 +1,49 @@
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Hooks.Meta;
public class EstHook : FastHook<EstHook.Delegate>
{
public delegate EstEntry Delegate(uint id, int estType, uint genderRace);
private readonly MetaState _metaState;
public EstHook(HookManager hooks, MetaState metaState)
{
_metaState = metaState;
Task = hooks.CreateHook<Delegate>("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, true);
}
private EstEntry Detour(uint genderRace, int estType, uint id)
{
EstEntry ret;
if (_metaState.EstCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }
&& cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry))
ret = entry.Entry;
else
ret = Task.Result.Original(genderRace, estType, id);
Penumbra.Log.Excessive($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}.");
return ret;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static EstIdentifier Convert(uint genderRace, int estType, uint id)
{
var i = new PrimaryId((ushort)id);
var gr = (GenderRace)genderRace;
var type = estType switch
{
1 => EstType.Face,
2 => EstType.Hair,
3 => EstType.Head,
4 => EstType.Body,
_ => (EstType)0,
};
return new EstIdentifier(i, type, gr);
}
}

View file

@ -29,8 +29,9 @@ public sealed unsafe class GetEqpIndirect : FastHook<GetEqpIndirect.Delegate>
return;
Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}.");
_metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true);
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
_metaState.EqpCollection.Push(collection);
Task.Result.Original(drawObject);
_metaState.EqpCollection = ResolveData.Invalid;
_metaState.EqpCollection.Pop();
}
}

View file

@ -1,11 +1,11 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
public sealed unsafe class GetEqpIndirect2 : FastHook<GetEqpIndirect2.Delegate>
{
private readonly CollectionResolver _collectionResolver;
@ -29,8 +29,9 @@ public sealed unsafe class GetEqpIndirect2 : FastHook<GetEqpIndirect2.Delegate>
return;
Penumbra.Log.Excessive($"[Get EQP Indirect 2] Invoked on {(nint)drawObject:X}.");
_metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true);
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
_metaState.EqpCollection.Push(collection);
Task.Result.Original(drawObject);
_metaState.EqpCollection = ResolveData.Invalid;
_metaState.EqpCollection.Pop();
}
}
}

View file

@ -0,0 +1,64 @@
using OtterGui.Services;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Hooks.Meta;
public unsafe class GmpHook : FastHook<GmpHook.Delegate>
{
public delegate nint Delegate(nint gmpResource, uint dividedHeadId);
private readonly MetaState _metaState;
private static readonly Finalizer StablePointer = new();
public GmpHook(HookManager hooks, MetaState metaState)
{
_metaState = metaState;
Task = hooks.CreateHook<Delegate>("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, true);
}
/// <remarks>
/// This function returns a pointer to the correct block in the GMP file, if it exists - cf. <see cref="ExpandedEqpGmpBase"/>.
/// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance,
/// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry.
/// </remarks>
private nint Detour(nint gmpResource, uint dividedHeadId)
{
nint ret;
if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache }
&& cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry))
{
if (entry.Entry.Enabled)
{
*StablePointer.Pointer = entry.Entry.Value;
// This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition.
// We then go backwards from our pointer because this gets added by the calling functions.
ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize));
}
else
{
ret = nint.Zero;
}
}
else
{
ret = Task.Result.Original(gmpResource, dividedHeadId);
}
Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}.");
return ret;
}
/// <summary> Allocate and clean up our single stable ulong pointer. </summary>
private class Finalizer
{
public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8);
~Finalizer()
{
Marshal.FreeHGlobal((nint)Pointer);
}
}
}

View file

@ -23,10 +23,11 @@ public sealed unsafe class ModelLoadComplete : FastHook<ModelLoadComplete.Delega
private void Detour(DrawObject* drawObject)
{
Penumbra.Log.Excessive($"[Model Load Complete] Invoked on {(nint)drawObject:X}.");
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true);
_metaState.EqpCollection = collection;
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
_metaState.EqpCollection.Push(collection);
_metaState.EqdpCollection.Push(collection);
Task.Result.Original(drawObject);
_metaState.EqpCollection = ResolveData.Invalid;
_metaState.EqpCollection.Pop();
_metaState.EqdpCollection.Pop();
}
}

View file

@ -0,0 +1,66 @@
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Hooks.Meta;
public unsafe class RspBustHook : FastHook<RspBustHook.Delegate>
{
public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType,
byte bustSize);
private readonly MetaState _metaState;
private readonly MetaFileManager _metaFileManager;
public RspBustHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager)
{
_metaState = metaState;
_metaFileManager = metaFileManager;
Task = hooks.CreateHook<Delegate>("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, true);
}
private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize)
{
if (gender == 0)
{
storage[0] = 1f;
storage[1] = 1f;
storage[2] = 1f;
return storage;
}
var ret = storage;
if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache })
{
var bustScale = bustSize / 100f;
var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace);
var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX);
storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX);
storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY);
storage[2] = GetValue(2, RspAttribute.BustMinZ, RspAttribute.BustMaxZ);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
float GetValue(int dimension, RspAttribute min, RspAttribute max)
{
var minValue = cache.Rsp.TryGetValue(new RspIdentifier(clan, min), out var minEntry)
? minEntry.Entry.Value
: (ptr + dimension)->Value;
var maxValue = cache.Rsp.TryGetValue(new RspIdentifier(clan, max), out var maxEntry)
? maxEntry.Entry.Value
: (ptr + 3 + dimension)->Value;
return (maxValue - minValue) * bustScale + minValue;
}
}
else
{
ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize);
}
Penumbra.Log.Excessive(
$"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}.");
return ret;
}
}

View file

@ -0,0 +1,69 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Hooks.Meta;
public class RspHeightHook : FastHook<RspHeightHook.Delegate>
{
public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height);
private readonly MetaState _metaState;
private readonly MetaFileManager _metaFileManager;
public RspHeightHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager)
{
_metaState = metaState;
_metaFileManager = metaFileManager;
Task = hooks.CreateHook<Delegate>("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, true);
}
private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height)
{
float scale;
if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache })
{
var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace);
var (minIdent, maxIdent) = gender == 0
? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize))
: (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize));
float minEntry, maxEntry;
if (cache.Rsp.TryGetValue(minIdent, out var min))
{
minEntry = min.Entry.Value;
maxEntry = cache.Rsp.TryGetValue(maxIdent, out var max)
? max.Entry.Value
: CmpFile.GetDefault(_metaFileManager, minIdent.SubRace, maxIdent.Attribute).Value;
}
else
{
var ptr = CmpFile.GetDefaults(_metaFileManager, minIdent.SubRace, minIdent.Attribute);
if (cache.Rsp.TryGetValue(maxIdent, out var max))
{
minEntry = ptr->Value;
maxEntry = max.Entry.Value;
}
else
{
minEntry = ptr[0].Value;
maxEntry = ptr[1].Value;
}
}
scale = (maxEntry - minEntry) * height / 100f + minEntry;
}
else
{
scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height);
}
Penumbra.Log.Excessive(
$"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}.");
return scale;
}
}

View file

@ -1,5 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
@ -30,8 +31,9 @@ public sealed unsafe class RspSetupCharacter : FastHook<RspSetupCharacter.Delega
return;
}
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
using var cmp = _metaState.ResolveRspData(collection.ModCollection);
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
_metaState.RspCollection.Push(collection);
Task.Result.Original.Invoke(drawObject, unk2, unk3, unk4, unk5);
_metaState.RspCollection.Pop();
}
}

View file

@ -0,0 +1,68 @@
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Hooks.Meta;
public class RspTailHook : FastHook<RspTailHook.Delegate>
{
public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height);
private readonly MetaState _metaState;
private readonly MetaFileManager _metaFileManager;
public RspTailHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager)
{
_metaState = metaState;
_metaFileManager = metaFileManager;
Task = hooks.CreateHook<Delegate>("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, true);
}
private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength)
{
float scale;
if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache })
{
var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace);
var (minIdent, maxIdent) = gender == 0
? (new RspIdentifier(clan, RspAttribute.MaleMinTail), new RspIdentifier(clan, RspAttribute.MaleMaxTail))
: (new RspIdentifier(clan, RspAttribute.FemaleMinTail), new RspIdentifier(clan, RspAttribute.FemaleMaxTail));
float minEntry, maxEntry;
if (cache.Rsp.TryGetValue(minIdent, out var min))
{
minEntry = min.Entry.Value;
maxEntry = cache.Rsp.TryGetValue(maxIdent, out var max)
? max.Entry.Value
: CmpFile.GetDefault(_metaFileManager, minIdent.SubRace, maxIdent.Attribute).Value;
}
else
{
var ptr = CmpFile.GetDefaults(_metaFileManager, minIdent.SubRace, minIdent.Attribute);
if (cache.Rsp.TryGetValue(maxIdent, out var max))
{
minEntry = ptr->Value;
maxEntry = max.Entry.Value;
}
else
{
minEntry = ptr[0].Value;
maxEntry = ptr[1].Value;
}
}
scale = (maxEntry - minEntry) * tailLength / 100f + minEntry;
}
else
{
scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, tailLength);
}
Penumbra.Log.Excessive(
$"[GetRspTail] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {tailLength}, returned {scale}.");
return scale;
}
}

View file

@ -1,10 +1,11 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Meta;
/// <summary>
/// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself,
/// but it only applies a changed gmp file after a redraw for some reason.
@ -26,10 +27,11 @@ public sealed unsafe class SetupVisor : FastHook<SetupVisor.Delegate>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState)
{
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
using var gmp = _metaState.ResolveGmpData(collection.ModCollection);
var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState);
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
_metaState.GmpCollection.Push((collection, modelId));
var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState);
Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}.");
_metaState.GmpCollection.Pop();
return ret;
}
}

View file

@ -29,10 +29,11 @@ public sealed unsafe class UpdateModel : FastHook<UpdateModel.Delegate>
return;
Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}.");
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true);
_metaState.EqpCollection = collection;
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
_metaState.EqpCollection.Push(collection);
_metaState.EqdpCollection.Push(collection);
Task.Result.Original(drawObject);
_metaState.EqpCollection = ResolveData.Invalid;
_metaState.EqpCollection.Pop();
_metaState.EqdpCollection.Pop();
}
}

View file

@ -1,11 +1,9 @@
using System.Text.Unicode;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Hooks.Resources;
@ -149,35 +147,52 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex)
{
var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var eqdp = slotIndex > 9 || _parent.InInternalResolve
? DisposableContainer.Empty
: _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4);
return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex));
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
if (slotIndex < 10)
_parent.MetaState.EqdpCollection.Push(collection);
var ret = ResolvePath(collection, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex));
if (slotIndex < 10)
_parent.MetaState.EqdpCollection.Pop();
return ret;
}
private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
_parent.MetaState.EstCollection.Push(collection);
var ret = ResolvePath(collection,
_resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));
_parent.MetaState.EstCollection.Pop();
return ret;
}
private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
_parent.MetaState.EstCollection.Push(collection);
var ret = ResolvePath(collection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
_parent.MetaState.EstCollection.Pop();
return ret;
}
private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
_parent.MetaState.EstCollection.Push(collection);
var ret = ResolvePath(collection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
_parent.MetaState.EstCollection.Pop();
return ret;
}
private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
_parent.MetaState.EstCollection.Push(collection);
var ret = ResolvePath(collection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
_parent.MetaState.EstCollection.Pop();
return ret;
}
private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam)
@ -206,19 +221,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
return ResolvePath(drawObject, pathBuffer);
}
private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data)
{
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
if (_parent.InInternalResolve)
return DisposableContainer.Empty;
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Face),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Body),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Hair),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Head));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static Hook<T> Create<T>(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate
{

View file

@ -1,7 +1,6 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Microsoft.VisualBasic;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;

View file

@ -1,6 +1,5 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
@ -9,7 +8,7 @@ using Penumbra.String;
namespace Penumbra.Interop.PathResolving;
public sealed class CutsceneService : IService, IDisposable
public sealed class CutsceneService : IRequiredService, IDisposable
{
public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart;
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;

View file

@ -1,6 +1,7 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@ -10,7 +11,8 @@ using Penumbra.Services;
namespace Penumbra.Interop.PathResolving;
public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>
public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>,
IService
{
private readonly CommunicatorService _communicator;
private readonly CharacterDestructor _characterDestructor;

View file

@ -1,14 +1,13 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Services;
using Penumbra.Services;
using Penumbra.String.Classes;
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
using Penumbra.Interop.Hooks.Objects;
@ -18,7 +17,7 @@ namespace Penumbra.Interop.PathResolving;
// GetSlotEqpData seems to be the only function using the EQP table.
// It is only called by CheckSlotsForUnload (called by UpdateModels),
// SetupModelAttributes (called by UpdateModels and OnModelLoadComplete)
// and a unnamed function called by UpdateRender.
// and an unnamed function called by UpdateRender.
// It seems to be enough to change the EQP entries for UpdateModels.
// GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables.
@ -35,8 +34,8 @@ namespace Penumbra.Interop.PathResolving;
// they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create,
// ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight.
// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter.
public sealed unsafe class MetaState : IDisposable
// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which is SetupVisor.
public sealed unsafe class MetaState : IDisposable, IService
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
@ -45,8 +44,14 @@ public sealed unsafe class MetaState : IDisposable
private readonly CharacterUtility _characterUtility;
private readonly CreateCharacterBase _createCharacterBase;
public ResolveData CustomizeChangeCollection = ResolveData.Invalid;
public ResolveData EqpCollection = ResolveData.Invalid;
public ResolveData CustomizeChangeCollection = ResolveData.Invalid;
public readonly Stack<ResolveData> EqpCollection = [];
public readonly Stack<ResolveData> EqdpCollection = [];
public readonly Stack<ResolveData> EstCollection = [];
public readonly Stack<ResolveData> RspCollection = [];
public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = [];
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty;
@ -78,48 +83,9 @@ public sealed unsafe class MetaState : IDisposable
return false;
}
public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
=> (equipment, accessory) switch
{
(true, true) => new DisposableContainer(race.Dependencies().SelectMany(r => new[]
{
collection.TemporarilySetEqdpFile(_characterUtility, r, false),
collection.TemporarilySetEqdpFile(_characterUtility, r, true),
})),
(true, false) => new DisposableContainer(race.Dependencies()
.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))),
(false, true) => new DisposableContainer(race.Dependencies()
.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))),
_ => DisposableContainer.Empty,
};
public MetaList.MetaReverter ResolveEqpData(ModCollection collection)
=> collection.TemporarilySetEqpFile(_characterUtility);
public MetaList.MetaReverter ResolveGmpData(ModCollection collection)
=> collection.TemporarilySetGmpFile(_characterUtility);
public MetaList.MetaReverter ResolveRspData(ModCollection collection)
=> collection.TemporarilySetCmpFile(_characterUtility);
public DecalReverter ResolveDecal(ResolveData resolve, bool which)
=> new(_config, _characterUtility, _resources, resolve, which);
public static GenderRace GetHumanGenderRace(nint human)
=> (GenderRace)((Human*)human)->RaceSexId;
public static GenderRace GetDrawObjectGenderRace(nint drawObject)
{
var draw = (DrawObject*)drawObject;
if (draw->Object.GetObjectType() != ObjectType.CharacterBase)
return GenderRace.Unknown;
var c = (CharacterBase*)drawObject;
return c->GetModelType() == CharacterBase.ModelType.Human
? GetHumanGenderRace(drawObject)
: GenderRace.Unknown;
}
public void Dispose()
{
_createCharacterBase.Unsubscribe(OnCreatingCharacterBase);
@ -135,9 +101,9 @@ public sealed unsafe class MetaState : IDisposable
var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection,
UsesDecal(*(uint*)modelCharaId, (nint)customize));
var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility);
RspCollection.Push(_lastCreatedCollection);
_characterBaseCreateMetaChanges.Dispose(); // Should always be empty.
_characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp);
_characterBaseCreateMetaChanges = new DisposableContainer(decal);
}
private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, CharacterArmor* _3, CharacterBase* drawObject)
@ -147,6 +113,7 @@ public sealed unsafe class MetaState : IDisposable
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null)
_communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection, (nint)drawObject);
RspCollection.Pop();
_lastCreatedCollection = ResolveData.Invalid;
}

View file

@ -32,7 +32,7 @@ public static class PathDataHandler
/// <summary> Create the encoding path for an IMC file. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FullPath CreateImc(ByteString path, ModCollection collection)
=> CreateBase(path, collection);
=> new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}");
/// <summary> Create the encoding path for a TMB file. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View file

@ -1,50 +1,44 @@
using System.Runtime;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Interop.Processing;
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
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 SubfileHelper _subfileHelper;
private readonly PathState _pathState;
private readonly MetaState _metaState;
private readonly GameState _gameState;
private readonly CollectionResolver _collectionResolver;
private readonly GamePathPreProcessService _preprocessor;
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader,
SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState)
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;
_collectionResolver = collectionResolver;
_loader = loader;
_loader.ResolvePath = ResolvePath;
_loader.FileLoaded += ImcLoadResource;
}
/// <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;
_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>
@ -113,14 +107,12 @@ 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, gamePath, out var pair);
return pair;
return _preprocessor.PreProcess(resolveData, path, nonDefault, type, resolved, gamePath);
}
public unsafe void Dispose()
public void Dispose()
{
_loader.ResetResolvePath();
_loader.FileLoaded -= ImcLoadResource;
}
/// <summary> Use the default method of path replacement. </summary>
@ -130,24 +122,6 @@ public class PathResolver : IDisposable
return (resolved, _collectionManager.Active.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,
ReadOnlySpan<byte> additionalData)
{
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;
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>
private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path)
=> (_collectionManager.Active.Interface.ResolvePath(path),

View file

@ -1,3 +1,4 @@
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Interop.Services;
using Penumbra.String;
@ -5,7 +6,7 @@ using Penumbra.String;
namespace Penumbra.Interop.PathResolving;
public sealed class PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility)
: IDisposable
: IDisposable, IService
{
public readonly CollectionResolver CollectionResolver = collectionResolver;
public readonly MetaState MetaState = metaState;

View file

@ -1,9 +1,9 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.Hooks.Resources;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.PathResolving;
@ -13,7 +13,7 @@ namespace Penumbra.Interop.PathResolving;
/// Those are loaded synchronously.
/// Thus, we need to ensure the correct files are loaded when a material is loaded.
/// </summary>
public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>, IService
{
private readonly GameState _gameState;
private readonly ResourceLoader _loader;
@ -66,21 +66,6 @@ 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 of each other. </summary>
public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved,
Utf8GamePath originalPath, out (FullPath?, ResolveData) data)
{
if (nonDefault)
resolved = type switch
{
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);
}
public void Dispose()
{
_loader.ResourceLoaded -= SubfileContainerRequested;

View file

@ -0,0 +1,16 @@
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Processing;
public sealed class AvfxPathPreProcessor : IPathPreProcessor
{
public ResourceType Type
=> ResourceType.Avfx;
public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved)
=> nonDefault ? PathDataHandler.CreateAvfx(path, resolveData.ModCollection) : resolved;
}

View file

@ -0,0 +1,39 @@
using System.Collections.Frozen;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs;
using Penumbra.String;
namespace Penumbra.Interop.Processing;
public interface IFilePostProcessor : IService
{
public ResourceType Type { get; }
public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan<byte> additionalData);
}
public unsafe class FilePostProcessService : IRequiredService, IDisposable
{
private readonly ResourceLoader _resourceLoader;
private readonly FrozenDictionary<ResourceType, IFilePostProcessor> _processors;
public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services)
{
_resourceLoader = resourceLoader;
_processors = services.GetServicesImplementing<IFilePostProcessor>().ToFrozenDictionary(s => s.Type, s => s);
_resourceLoader.FileLoaded += OnFileLoaded;
}
public void Dispose()
{
_resourceLoader.FileLoaded -= OnFileLoaded;
}
private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom,
ReadOnlySpan<byte> additionalData)
{
if (_processors.TryGetValue(resource->FileType, out var processor))
processor.PostProcess(resource, path, additionalData);
}
}

View file

@ -0,0 +1,37 @@
using System.Collections.Frozen;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Processing;
public interface IPathPreProcessor : IService
{
public ResourceType Type { get; }
public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved);
}
public class GamePathPreProcessService : IService
{
private readonly FrozenDictionary<ResourceType, IPathPreProcessor> _processors;
public GamePathPreProcessService(ServiceManager services)
{
_processors = services.GetServicesImplementing<IPathPreProcessor>().ToFrozenDictionary(s => s.Type, s => s);
}
public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type,
FullPath? resolved,
Utf8GamePath originalPath)
{
if (!_processors.TryGetValue(type, out var processor))
return (resolved, resolveData);
resolved = processor.PreProcess(resolveData, path, originalPath, nonDefault, resolved);
return (resolved, resolveData);
}
}

View file

@ -0,0 +1,30 @@
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
using Penumbra.String;
namespace Penumbra.Interop.Processing;
public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFilePostProcessor
{
public ResourceType Type
=> ResourceType.Imc;
public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan<byte> additionalData)
{
if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator)
return;
var collection = collections.ByLocalId(data.Collection);
if (collection.MetaCache is not { } cache)
return;
if (!cache.Imc.GetFile(originalGamePath, out var file))
return;
file.Replace(resource);
Penumbra.Log.Information(
$"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}.");
}
}

View file

@ -0,0 +1,18 @@
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Processing;
public sealed class ImcPathPreProcessor : IPathPreProcessor
{
public ResourceType Type
=> ResourceType.Imc;
public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool _, FullPath? resolved)
=> resolveData.ModCollection.MetaCache?.Imc.HasFile(originalGamePath.Path) ?? false
? PathDataHandler.CreateImc(path, resolveData.ModCollection)
: resolved;
}

View file

@ -0,0 +1,18 @@
using Penumbra.Api.Enums;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
using Penumbra.String;
namespace Penumbra.Interop.Processing;
public sealed class MaterialFilePostProcessor //: IFilePostProcessor
{
public ResourceType Type
=> ResourceType.Mtrl;
public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan<byte> additionalData)
{
if (!PathDataHandler.ReadMtrl(additionalData, out var data))
return;
}
}

View file

@ -0,0 +1,16 @@
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Processing;
public sealed class MtrlPathPreProcessor : IPathPreProcessor
{
public ResourceType Type
=> ResourceType.Mtrl;
public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved)
=> nonDefault ? PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalGamePath) : resolved;
}

View file

@ -0,0 +1,16 @@
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Processing;
public sealed class TmbPathPreProcessor : IPathPreProcessor
{
public ResourceType Type
=> ResourceType.Tmb;
public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved)
=> nonDefault ? PathDataHandler.CreateTmb(path, resolveData.ModCollection) : resolved;
}

View file

@ -1,13 +1,14 @@
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using OtterGui.Services;
using Penumbra.GameData;
using Penumbra.Interop.Structs;
using Penumbra.Util;
namespace Penumbra.Interop.ResourceLoading;
public unsafe class FileReadService : IDisposable
public unsafe class FileReadService : IDisposable, IRequiredService
{
public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop)
{

View file

@ -1,4 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Interop.PathResolving;
@ -10,7 +11,7 @@ using FileMode = Penumbra.Interop.Structs.FileMode;
namespace Penumbra.Interop.ResourceLoading;
public unsafe class ResourceLoader : IDisposable
public unsafe class ResourceLoader : IDisposable, IService
{
private readonly ResourceService _resources;
private readonly FileReadService _fileReadService;
@ -212,7 +213,7 @@ public unsafe class ResourceLoader : IDisposable
/// <summary>
/// Catch weird errors with invalid decrements of the reference count.
/// </summary>
private void DecRefProtection(ResourceHandle* handle, ref byte? returnValue)
private static void DecRefProtection(ResourceHandle* handle, ref byte? returnValue)
{
if (handle->RefCount != 0)
return;

View file

@ -4,12 +4,13 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.STD;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData;
namespace Penumbra.Interop.ResourceLoading;
public unsafe class ResourceManagerService
public unsafe class ResourceManagerService : IRequiredService
{
public ResourceManagerService(IGameInteropProvider interop)
=> interop.InitializeFromAttributes(this);

View file

@ -2,6 +2,7 @@ using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.Interop.SafeHandles;
@ -13,7 +14,7 @@ using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.
namespace Penumbra.Interop.ResourceLoading;
public unsafe class ResourceService : IDisposable
public unsafe class ResourceService : IDisposable, IRequiredService
{
private readonly PerformanceTracker _performance;
private readonly ResourceManagerService _resourceManager;

View file

@ -2,13 +2,14 @@ using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.String.Classes;
namespace Penumbra.Interop.ResourceLoading;
public unsafe class TexMdlService : IDisposable
public unsafe class TexMdlService : IDisposable, IRequiredService
{
/// <summary> Custom ulong flag to signal our files as opposed to SE files. </summary>
public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF);

View file

@ -273,7 +273,7 @@ internal partial record ResolveContext
{
var metaCache = Global.Collection.MetaCache;
var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default;
return (raceCode, EstManipulation.ToName(type), skeletonSet.AsId);
return (raceCode, type.ToName(), skeletonSet.AsId);
}
private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex)

View file

@ -1,5 +1,6 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
@ -17,7 +18,7 @@ public class ResourceTreeFactory(
ObjectIdentification identifier,
Configuration config,
ActorManager actors,
PathState pathState)
PathState pathState) : IService
{
private TreeBuildCache CreateTreeBuildCache()
=> new(objects, gameData, actors);

View file

@ -1,12 +1,12 @@
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using Penumbra.Collections.Manager;
using OtterGui.Services;
using Penumbra.GameData;
using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Services;
public unsafe class CharacterUtility : IDisposable
public unsafe class CharacterUtility : IDisposable, IRequiredService
{
public record struct InternalIndex(int Value);
@ -47,23 +47,18 @@ public unsafe class CharacterUtility : IDisposable
private readonly MetaList[] _lists;
public IReadOnlyList<MetaList> Lists
=> _lists;
public (nint Address, int Size) DefaultResource(InternalIndex idx)
=> _lists[idx.Value].DefaultResource;
private readonly IFramework _framework;
public readonly ActiveCollectionData Active;
private readonly IFramework _framework;
public CharacterUtility(IFramework framework, IGameInteropProvider interop, ActiveCollectionData active)
public CharacterUtility(IFramework framework, IGameInteropProvider interop)
{
interop.InitializeFromAttributes(this);
_lists = Enumerable.Range(0, RelevantIndices.Length)
.Select(idx => new MetaList(this, new InternalIndex(idx)))
.Select(idx => new MetaList(new InternalIndex(idx)))
.ToArray();
_framework = framework;
Active = active;
LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished.");
LoadDefaultResources(null!);
if (!Ready)
@ -121,43 +116,12 @@ public unsafe class CharacterUtility : IDisposable
LoadingFinished.Invoke();
}
public void SetResource(MetaIndex resourceIdx, nint data, int length)
{
var idx = ReverseIndices[(int)resourceIdx];
var list = _lists[idx.Value];
list.SetResource(data, length);
}
public void ResetResource(MetaIndex resourceIdx)
{
var idx = ReverseIndices[(int)resourceIdx];
var list = _lists[idx.Value];
list.ResetResource();
}
public MetaList.MetaReverter TemporarilySetResource(MetaIndex resourceIdx, nint data, int length)
{
var idx = ReverseIndices[(int)resourceIdx];
var list = _lists[idx.Value];
return list.TemporarilySetResource(data, length);
}
public MetaList.MetaReverter TemporarilyResetResource(MetaIndex resourceIdx)
{
var idx = ReverseIndices[(int)resourceIdx];
var list = _lists[idx.Value];
return list.TemporarilyResetResource();
}
/// <summary> Return all relevant resources to the default resource. </summary>
public void ResetAll()
{
if (!Ready)
return;
foreach (var list in _lists)
list.Dispose();
Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource;
Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource;
Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource;

View file

@ -1,6 +1,7 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Component.GUI;
using OtterGui.Services;
using Penumbra.GameData;
namespace Penumbra.Interop.Services;
@ -9,7 +10,7 @@ namespace Penumbra.Interop.Services;
/// Handle font reloading via game functions.
/// May cause a interface flicker while reloading.
/// </summary>
public unsafe class FontReloader
public unsafe class FontReloader : IService
{
public bool Valid
=> _reloadFontsFunc != null;

View file

@ -2,26 +2,14 @@ using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Services;
public unsafe class MetaList : IDisposable
public class MetaList(CharacterUtility.InternalIndex index)
{
private readonly CharacterUtility _utility;
private readonly LinkedList<MetaReverter> _entries = new();
public readonly CharacterUtility.InternalIndex Index;
public readonly MetaIndex GlobalMetaIndex;
public IReadOnlyCollection<MetaReverter> Entries
=> _entries;
public readonly CharacterUtility.InternalIndex Index = index;
public readonly MetaIndex GlobalMetaIndex = CharacterUtility.RelevantIndices[index.Value];
private nint _defaultResourceData = nint.Zero;
private int _defaultResourceSize = 0;
public bool Ready { get; private set; } = false;
public MetaList(CharacterUtility utility, CharacterUtility.InternalIndex index)
{
_utility = utility;
Index = index;
GlobalMetaIndex = CharacterUtility.RelevantIndices[index.Value];
}
private int _defaultResourceSize;
public bool Ready { get; private set; }
public void SetDefaultResource(nint data, int size)
{
@ -31,127 +19,8 @@ public unsafe class MetaList : IDisposable
_defaultResourceData = data;
_defaultResourceSize = size;
Ready = _defaultResourceData != nint.Zero && size != 0;
if (_entries.Count <= 0)
return;
var first = _entries.First!.Value;
SetResource(first.Data, first.Length);
}
public (nint Address, int Size) DefaultResource
=> (_defaultResourceData, _defaultResourceSize);
public MetaReverter TemporarilySetResource(nint data, int length)
{
Penumbra.Log.Excessive($"Temporarily set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes).");
var reverter = new MetaReverter(this, data, length);
_entries.AddFirst(reverter);
SetResourceInternal(data, length);
return reverter;
}
public MetaReverter TemporarilyResetResource()
{
Penumbra.Log.Excessive(
$"Temporarily reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes).");
var reverter = new MetaReverter(this);
_entries.AddFirst(reverter);
ResetResourceInternal();
return reverter;
}
public void SetResource(nint data, int length)
{
Penumbra.Log.Excessive($"Set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes).");
SetResourceInternal(data, length);
}
public void ResetResource()
{
Penumbra.Log.Excessive($"Reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes).");
ResetResourceInternal();
}
/// <summary> Set the currently stored data of this resource to new values. </summary>
private void SetResourceInternal(nint data, int length)
{
if (!Ready)
return;
var resource = _utility.Address->Resource(GlobalMetaIndex);
resource->SetData(data, length);
}
/// <summary> Reset the currently stored data of this resource to its default values. </summary>
private void ResetResourceInternal()
=> SetResourceInternal(_defaultResourceData, _defaultResourceSize);
private void SetResourceToDefaultCollection()
=> _utility.Active.Default.SetMetaFile(_utility, GlobalMetaIndex);
public void Dispose()
{
if (_entries.Count > 0)
{
foreach (var entry in _entries)
entry.Disposed = true;
_entries.Clear();
}
ResetResourceInternal();
}
public sealed class MetaReverter : IDisposable
{
public static readonly MetaReverter Disabled = new(null!) { Disposed = true };
public readonly MetaList MetaList;
public readonly nint Data;
public readonly int Length;
public readonly bool Resetter;
public bool Disposed;
public MetaReverter(MetaList metaList, nint data, int length)
{
MetaList = metaList;
Data = data;
Length = length;
}
public MetaReverter(MetaList metaList)
{
MetaList = metaList;
Data = nint.Zero;
Length = 0;
Resetter = true;
}
public void Dispose()
{
if (Disposed)
return;
var list = MetaList._entries;
var wasCurrent = ReferenceEquals(this, list.First?.Value);
list.Remove(this);
if (!wasCurrent)
return;
if (list.Count == 0)
{
MetaList.SetResourceToDefaultCollection();
}
else
{
var next = list.First!.Value;
if (next.Resetter)
MetaList.ResetResourceInternal();
else
MetaList.SetResourceInternal(next.Data, next.Length);
}
Disposed = true;
}
}
}

View file

@ -1,10 +1,11 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using OtterGui.Services;
namespace Penumbra.Interop.Services;
public unsafe class ModelRenderer : IDisposable
public unsafe class ModelRenderer : IDisposable, IRequiredService
{
public bool Ready { get; private set; }
@ -37,14 +38,14 @@ public unsafe class ModelRenderer : IDisposable
if (DefaultCharacterGlassShaderPackage == null)
{
DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage;
anyMissing |= DefaultCharacterGlassShaderPackage == null;
DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage;
anyMissing |= DefaultCharacterGlassShaderPackage == null;
}
if (anyMissing)
return;
Ready = true;
Ready = true;
_framework.Update -= LoadDefaultResources;
}

View file

@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Housing;
using FFXIVClientStructs.Interop;
using OtterGui.Services;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Communication;
@ -20,7 +21,7 @@ using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
namespace Penumbra.Interop.Services;
public unsafe partial class RedrawService
public unsafe partial class RedrawService : IService
{
public const int GPosePlayerIdx = 201;
public const int GPoseSlots = 42;
@ -171,7 +172,8 @@ public sealed unsafe partial class RedrawService : IDisposable
if (gPose)
DisableDraw(actor!);
if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament)
if (actor is PlayerCharacter
&& _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament)
{
*ActorDrawState(mountOrOrnament) |= DrawState.Invisibility;
if (gPose)
@ -190,7 +192,8 @@ public sealed unsafe partial class RedrawService : IDisposable
if (gPose)
EnableDraw(actor!);
if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament)
if (actor is PlayerCharacter
&& _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament)
{
*ActorDrawState(mountOrOrnament) &= ~DrawState.Invisibility;
if (gPose)
@ -380,7 +383,7 @@ public sealed unsafe partial class RedrawService : IDisposable
if (!ret && lowerName.Length > 1 && lowerName[0] == '#' && ushort.TryParse(lowerName[1..], out var objectIndex))
{
ret = true;
actor = _objects.GetDalamudObject((int) objectIndex);
actor = _objects.GetDalamudObject((int)objectIndex);
}
return ret;

View file

@ -1,10 +1,11 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using OtterGui.Services;
using Penumbra.GameData;
namespace Penumbra.Interop.Services;
public unsafe class ResidentResourceManager
public unsafe class ResidentResourceManager : IService
{
// A static pointer to the resident resource manager address.
[Signature(Sigs.ResidentResourceManager, ScanType = ScanType.StaticAddress)]

View file

@ -139,9 +139,9 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
// Performance considerations:
// - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ;
// - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ;
// - Function is called each frame for each material on screen, after culling, i.e. up to thousands of times a frame in crowded areas ;
// - Swapping path is taken up to hundreds of times a frame.
// At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible.
// At the time of writing, the lock doesn't seem to have a noticeable impact in either frame rate or CPU usage, but the swapping path shall still be avoided as much as possible.
lock (_skinLock)
{
try

View file

@ -34,7 +34,7 @@ public sealed unsafe class CmpFile : MetaBaseFile
}
public CmpFile(MetaFileManager manager)
: base(manager, MetaIndex.HumanCmp)
: base(manager, manager.MarshalAllocator, MetaIndex.HumanCmp)
{
AllocateData(DefaultData.Length);
Reset();
@ -46,6 +46,14 @@ public sealed unsafe class CmpFile : MetaBaseFile
return *(RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4);
}
public static RspEntry* GetDefaults(MetaFileManager manager, SubRace subRace, RspAttribute attribute)
{
{
var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address;
return (RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4);
}
}
private static int ToRspIndex(SubRace subRace)
=> subRace switch
{

View file

@ -2,6 +2,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Functions;
namespace Penumbra.Meta.Files;
@ -86,7 +87,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
}
public ExpandedEqdpFile(MetaFileManager manager, GenderRace raceCode, bool accessory)
: base(manager, CharacterUtilityData.EqdpIdx(raceCode, accessory))
: base(manager, manager.MarshalAllocator, CharacterUtilityData.EqdpIdx(raceCode, accessory))
{
var def = (byte*)DefaultData.Data;
var blockSize = *(ushort*)(def + IdentifierSize);
@ -126,4 +127,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, PrimaryId primaryId)
=> GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], primaryId);
public static EqdpEntry GetDefault(MetaFileManager manager, EqdpIdentifier identifier)
=> GetDefault(manager, CharacterUtility.ReverseIndices[(int)identifier.FileIndex()], identifier.SetId);
}

View file

@ -1,6 +1,7 @@
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Functions;
namespace Penumbra.Meta.Files;
@ -14,10 +15,10 @@ namespace Penumbra.Meta.Files;
/// </summary>
public unsafe class ExpandedEqpGmpBase : MetaBaseFile
{
protected const int BlockSize = 160;
protected const int NumBlocks = 64;
protected const int EntrySize = 8;
protected const int MaxSize = BlockSize * NumBlocks * EntrySize;
public const int BlockSize = 160;
public const int NumBlocks = 64;
public const int EntrySize = 8;
public const int MaxSize = BlockSize * NumBlocks * EntrySize;
public const int Count = BlockSize * NumBlocks;
@ -75,7 +76,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
}
public ExpandedEqpGmpBase(MetaFileManager manager, bool gmp)
: base(manager, gmp ? MetaIndex.Gmp : MetaIndex.Eqp)
: base(manager, manager.MarshalAllocator, gmp ? MetaIndex.Gmp : MetaIndex.Eqp)
{
AllocateData(MaxSize);
Reset();
@ -103,15 +104,11 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
}
}
public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable<EqpEntry>
public sealed class ExpandedEqpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, false), IEnumerable<EqpEntry>
{
public static readonly CharacterUtility.InternalIndex InternalIndex =
CharacterUtility.ReverseIndices[(int)MetaIndex.Eqp];
public ExpandedEqpFile(MetaFileManager manager)
: base(manager, false)
{ }
public EqpEntry this[PrimaryId idx]
{
get => (EqpEntry)GetInternal(idx);
@ -146,15 +143,11 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable<EqpEntry>
=> GetEnumerator();
}
public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable<GmpEntry>
public sealed class ExpandedGmpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, true), IEnumerable<GmpEntry>
{
public static readonly CharacterUtility.InternalIndex InternalIndex =
CharacterUtility.ReverseIndices[(int)MetaIndex.Gmp];
public ExpandedGmpFile(MetaFileManager manager)
: base(manager, true)
{ }
public GmpEntry this[PrimaryId idx]
{
get => new() { Value = GetInternal(idx) };
@ -164,6 +157,9 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable<GmpEntry>
public static GmpEntry GetDefault(MetaFileManager manager, PrimaryId primaryIdx)
=> new() { Value = GetDefaultInternal(manager, InternalIndex, primaryIdx, GmpEntry.Default.Value) };
public static GmpEntry GetDefault(MetaFileManager manager, GmpIdentifier identifier)
=> new() { Value = GetDefaultInternal(manager, InternalIndex, identifier.SetId, GmpEntry.Default.Value) };
public void Reset(IEnumerable<PrimaryId> entries)
{
foreach (var entry in entries)

View file

@ -157,7 +157,7 @@ public sealed unsafe class EstFile : MetaBaseFile
}
public EstFile(MetaFileManager manager, EstType estType)
: base(manager, (MetaIndex)estType)
: base(manager, manager.MarshalAllocator, (MetaIndex)estType)
{
var length = DefaultData.Length;
AllocateData(length + IncreaseSize);
@ -184,4 +184,7 @@ public sealed unsafe class EstFile : MetaBaseFile
public static EstEntry GetDefault(MetaFileManager manager, EstType estType, GenderRace genderRace, PrimaryId primaryId)
=> GetDefault(manager, (MetaIndex)estType, genderRace, primaryId);
public static EstEntry GetDefault(MetaFileManager manager, EstIdentifier identifier)
=> GetDefault(manager, identifier.FileIndex(), identifier.GenderRace, identifier.SetId);
}

View file

@ -12,7 +12,7 @@ namespace Penumbra.Meta.Files;
/// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet.
/// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect.
/// </summary>
public unsafe class EvpFile : MetaBaseFile
public unsafe class EvpFile(MetaFileManager manager) : MetaBaseFile(manager, manager.MarshalAllocator, (MetaIndex)1)
{
public const int FlagArraySize = 512;
@ -57,8 +57,4 @@ public unsafe class EvpFile : MetaBaseFile
return EvpFlag.None;
}
public EvpFile(MetaFileManager manager)
: base(manager, (MetaIndex)1) // TODO: Name
{ }
}

View file

@ -7,16 +7,10 @@ using Penumbra.String.Functions;
namespace Penumbra.Meta.Files;
public class ImcException : Exception
public class ImcException(ImcIdentifier identifier, Utf8GamePath path) : Exception
{
public readonly ImcIdentifier Identifier;
public readonly string GamePath;
public ImcException(ImcIdentifier identifier, Utf8GamePath path)
{
Identifier = identifier;
GamePath = path.ToString();
}
public readonly ImcIdentifier Identifier = identifier;
public readonly string GamePath = path.ToString();
public override string Message
=> "Could not obtain default Imc File.\n"
@ -65,6 +59,9 @@ public unsafe class ImcFile : MetaBaseFile
return ptr == null ? new ImcEntry() : *ptr;
}
public ImcEntry GetEntry(EquipSlot slot, Variant variantIdx)
=> GetEntry(PartIndex(slot), variantIdx);
public ImcEntry GetEntry(int partIdx, Variant variantIdx, out bool exists)
{
var ptr = VariantPtr(Data, partIdx, variantIdx);
@ -143,7 +140,11 @@ public unsafe class ImcFile : MetaBaseFile
}
public ImcFile(MetaFileManager manager, ImcIdentifier identifier)
: base(manager, 0)
: this(manager, manager.MarshalAllocator, identifier)
{ }
public ImcFile(MetaFileManager manager, IFileAllocator alloc, ImcIdentifier identifier)
: base(manager, alloc, 0)
{
var path = identifier.GamePathString();
Path = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty;
@ -191,7 +192,13 @@ public unsafe class ImcFile : MetaBaseFile
public void Replace(ResourceHandle* resource)
{
var (data, length) = resource->GetData();
var newData = Manager.AllocateDefaultMemory(ActualLength, 8);
if (length == ActualLength)
{
MemoryUtility.MemCpyUnchecked((byte*)data, Data, ActualLength);
return;
}
var newData = Manager.XivAllocator.Allocate(ActualLength, 8);
if (newData == null)
{
Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed.");
@ -200,7 +207,7 @@ public unsafe class ImcFile : MetaBaseFile
MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength);
Manager.Free(data, length);
resource->SetData((IntPtr)newData, ActualLength);
Manager.XivAllocator.Release((void*)data, length);
resource->SetData((nint)newData, ActualLength);
}
}

View file

@ -1,23 +1,75 @@
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using OtterGui.Services;
using Penumbra.GameData;
using Penumbra.Interop.Structs;
using Penumbra.String.Functions;
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
namespace Penumbra.Meta.Files;
public unsafe class MetaBaseFile : IDisposable
public unsafe interface IFileAllocator
{
protected readonly MetaFileManager Manager;
public T* Allocate<T>(int length, int alignment = 1) where T : unmanaged;
public void Release<T>(ref T* pointer, int length) where T : unmanaged;
public void Release(void* pointer, int length)
{
var tmp = (byte*)pointer;
Release(ref tmp, length);
}
public byte* Allocate(int length, int alignment = 1)
=> Allocate<byte>(length, alignment);
}
public sealed class MarshalAllocator : IFileAllocator
{
public unsafe T* Allocate<T>(int length, int alignment = 1) where T : unmanaged
=> (T*)Marshal.AllocHGlobal(length * sizeof(T));
public unsafe void Release<T>(ref T* pointer, int length) where T : unmanaged
{
Marshal.FreeHGlobal((nint)pointer);
pointer = null;
}
}
public sealed unsafe class XivFileAllocator : IFileAllocator, IService
{
/// <summary>
/// Allocate in the games space for file storage.
/// We only need this if using any meta file.
/// </summary>
[Signature(Sigs.GetFileSpace)]
private readonly nint _getFileSpaceAddress = nint.Zero;
public XivFileAllocator(IGameInteropProvider provider)
=> provider.InitializeFromAttributes(this);
public IMemorySpace* GetFileSpace()
=> ((delegate* unmanaged<IMemorySpace*>)_getFileSpaceAddress)();
public T* Allocate<T>(int length, int alignment = 1) where T : unmanaged
=> (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment);
public void Release<T>(ref T* pointer, int length) where T : unmanaged
{
IMemorySpace.Free(pointer, (ulong)(length * sizeof(T)));
pointer = null;
}
}
public unsafe class MetaBaseFile(MetaFileManager manager, IFileAllocator alloc, MetaIndex idx) : IDisposable
{
protected readonly MetaFileManager Manager = manager;
protected readonly IFileAllocator Allocator = alloc;
public byte* Data { get; private set; }
public int Length { get; private set; }
public CharacterUtility.InternalIndex Index { get; }
public MetaBaseFile(MetaFileManager manager, MetaIndex idx)
{
Manager = manager;
Index = CharacterUtility.ReverseIndices[(int)idx];
}
public CharacterUtility.InternalIndex Index { get; } = CharacterUtility.ReverseIndices[(int)idx];
protected (IntPtr Data, int Length) DefaultData
=> Manager.CharacterUtility.DefaultResource(Index);
@ -30,7 +82,7 @@ public unsafe class MetaBaseFile : IDisposable
protected void AllocateData(int length)
{
Length = length;
Data = (byte*)Manager.AllocateFileMemory(length);
Data = Allocator.Allocate(length);
if (length > 0)
GC.AddMemoryPressure(length);
}
@ -38,8 +90,7 @@ public unsafe class MetaBaseFile : IDisposable
/// <summary> Free memory. </summary>
protected void ReleaseUnmanagedResources()
{
var ptr = (IntPtr)Data;
MemoryHelper.GameFree(ref ptr, (ulong)Length);
Allocator.Release(Data, Length);
if (Length > 0)
GC.RemoveMemoryPressure(Length);
@ -53,7 +104,7 @@ public unsafe class MetaBaseFile : IDisposable
if (newLength == Length)
return;
var data = (byte*)Manager.AllocateFileMemory((ulong)newLength);
var data = Allocator.Allocate(newLength);
if (newLength > Length)
{
MemoryUtility.MemCpyUnchecked(data, Data, Length);

View file

@ -51,10 +51,6 @@ public class ImcChecker
return entry;
}
public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache)
=> GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id,
imcManip.EquipSlot, imcManip.BodySlot), storeCache);
private static ImcFile? GetFile(ImcIdentifier identifier)
{
if (_dataManager == null)

View file

@ -69,19 +69,27 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge
jObj["Slot"] = Slot.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Eqdp;
}
public readonly record struct InternalEqdpEntry(bool Model, bool Material)
public readonly record struct EqdpEntryInternal(bool Material, bool Model)
{
private InternalEqdpEntry((bool, bool) val)
public byte AsByte
=> (byte)(Material ? Model ? 3 : 1 : Model ? 2 : 0);
private EqdpEntryInternal((bool, bool) val)
: this(val.Item1, val.Item2)
{ }
public InternalEqdpEntry(EqdpEntry entry, EquipSlot slot)
public EqdpEntryInternal(EqdpEntry entry, EquipSlot slot)
: this(entry.ToBits(slot))
{ }
public EqdpEntry ToEntry(EquipSlot slot)
=> Eqdp.FromSlotAndBits(slot, Model, Material);
=> Eqdp.FromSlotAndBits(slot, Material, Model);
public override string ToString()
=> $"Material: {Material}, Model: {Model}";
}

View file

@ -1,111 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EqdpManipulation : IMetaManipulation<EqdpManipulation>
{
[JsonIgnore]
public EqdpIdentifier Identifier { get; private init; }
public EqdpEntry Entry { get; private init; }
[JsonConverter(typeof(StringEnumConverter))]
public Gender Gender
=> Identifier.Gender;
[JsonConverter(typeof(StringEnumConverter))]
public ModelRace Race
=> Identifier.Race;
public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))]
public EquipSlot Slot
=> Identifier.Slot;
[JsonConstructor]
public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId)
{
Identifier = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race));
Entry = Eqdp.Mask(Slot) & entry;
}
public EqdpManipulation Copy(EqdpManipulation entry)
{
if (entry.Slot != Slot)
{
var (bit1, bit2) = entry.Entry.ToBits(entry.Slot);
return new EqdpManipulation(Eqdp.FromSlotAndBits(Slot, bit1, bit2), Slot, Gender, Race, SetId);
}
return new EqdpManipulation(entry.Entry, Slot, Gender, Race, SetId);
}
public EqdpManipulation Copy(EqdpEntry entry)
=> new(entry, Slot, Gender, Race, SetId);
public override string ToString()
=> $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}";
public bool Equals(EqdpManipulation other)
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Slot == other.Slot;
public override bool Equals(object? obj)
=> obj is EqdpManipulation other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot);
public int CompareTo(EqdpManipulation other)
{
var r = Race.CompareTo(other.Race);
if (r != 0)
return r;
var g = Gender.CompareTo(other.Gender);
if (g != 0)
return g;
var set = SetId.Id.CompareTo(other.SetId.Id);
return set != 0 ? set : Slot.CompareTo(other.Slot);
}
public MetaIndex FileIndex()
=> CharacterUtilityData.EqdpIdx(Names.CombinedRace(Gender, Race), Slot.IsAccessory());
public bool Apply(ExpandedEqdpFile file)
{
var entry = file[SetId];
var mask = Eqdp.Mask(Slot);
if ((entry & mask) == Entry)
return false;
file[SetId] = (entry & ~mask) | Entry;
return true;
}
public bool Validate()
{
var mask = Eqdp.Mask(Slot);
if (mask == 0)
return false;
if ((mask & Entry) != Entry)
return false;
if (FileIndex() == (MetaIndex)(-1))
return false;
// No check for set id.
return true;
}
}

View file

@ -50,6 +50,9 @@ public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : I
jObj["Slot"] = Slot.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Eqp;
}
public readonly record struct EqpEntryInternal(uint Value)

View file

@ -1,81 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Util;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EqpManipulation : IMetaManipulation<EqpManipulation>
{
[JsonConverter(typeof(ForceNumericFlagEnumConverter))]
public EqpEntry Entry { get; private init; }
public EqpIdentifier Identifier { get; private init; }
public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))]
public EquipSlot Slot
=> Identifier.Slot;
[JsonConstructor]
public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId)
{
Identifier = new EqpIdentifier(setId, slot);
Entry = Eqp.Mask(slot) & entry;
}
public EqpManipulation Copy(EqpEntry entry)
=> new(entry, Slot, SetId);
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
public bool Equals(EqpManipulation other)
=> Slot == other.Slot
&& SetId == other.SetId;
public override bool Equals(object? obj)
=> obj is EqpManipulation other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Slot, SetId);
public int CompareTo(EqpManipulation other)
{
var set = SetId.Id.CompareTo(other.SetId.Id);
return set != 0 ? set : Slot.CompareTo(other.Slot);
}
public MetaIndex FileIndex()
=> MetaIndex.Eqp;
public bool Apply(ExpandedEqpFile file)
{
var entry = file[SetId];
var mask = Eqp.Mask(Slot);
if ((entry & mask) == Entry)
return false;
file[SetId] = (entry & ~mask) | Entry;
return true;
}
public bool Validate()
{
var mask = Eqp.Mask(Slot);
if (mask == 0)
return false;
if ((Entry & mask) != Entry)
return false;
// No check for set id.
return true;
}
}

View file

@ -91,6 +91,9 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende
jObj["Slot"] = Slot.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Est;
}
[JsonConverter(typeof(Converter))]
@ -111,3 +114,16 @@ public readonly record struct EstEntry(ushort Value)
=> new(serializer.Deserialize<ushort>(reader));
}
}
public static class EstTypeExtension
{
public static string ToName(this EstType type)
=> type switch
{
EstType.Hair => "hair",
EstType.Face => "face",
EstType.Body => "top",
EstType.Head => "met",
_ => "unk",
};
}

View file

@ -1,109 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EstManipulation : IMetaManipulation<EstManipulation>
{
public static string ToName(EstType type)
=> type switch
{
EstType.Hair => "hair",
EstType.Face => "face",
EstType.Body => "top",
EstType.Head => "met",
_ => "unk",
};
public EstIdentifier Identifier { get; private init; }
public EstEntry Entry { get; private init; }
[JsonConverter(typeof(StringEnumConverter))]
public Gender Gender
=> Identifier.Gender;
[JsonConverter(typeof(StringEnumConverter))]
public ModelRace Race
=> Identifier.Race;
public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))]
public EstType Slot
=> Identifier.Slot;
[JsonConstructor]
public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry)
{
Entry = entry;
Identifier = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race));
}
public EstManipulation Copy(EstEntry entry)
=> new(Gender, Race, Slot, SetId, entry);
public override string ToString()
=> $"Est - {SetId} - {Slot} - {Race.ToName()} {Gender.ToName()}";
public bool Equals(EstManipulation other)
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Slot == other.Slot;
public override bool Equals(object? obj)
=> obj is EstManipulation other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot);
public int CompareTo(EstManipulation other)
{
var r = Race.CompareTo(other.Race);
if (r != 0)
return r;
var g = Gender.CompareTo(other.Gender);
if (g != 0)
return g;
var s = Slot.CompareTo(other.Slot);
return s != 0 ? s : SetId.Id.CompareTo(other.SetId.Id);
}
public MetaIndex FileIndex()
=> (MetaIndex)Slot;
public bool Apply(EstFile file)
{
return file.SetEntry(Names.CombinedRace(Gender, Race), SetId.Id, Entry) switch
{
EstFile.EstEntryChange.Unchanged => false,
EstFile.EstEntryChange.Changed => true,
EstFile.EstEntryChange.Added => true,
EstFile.EstEntryChange.Removed => true,
_ => throw new ArgumentOutOfRangeException(),
};
}
public bool Validate()
{
if (!Enum.IsDefined(Slot))
return false;
if (Names.CombinedRace(Gender, Race) == GenderRace.Unknown)
return false;
// No known check for set id or entry.
return true;
}
}

View file

@ -1,9 +1,12 @@
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public readonly struct GlobalEqpManipulation : IMetaManipulation<GlobalEqpManipulation>
public readonly struct GlobalEqpManipulation : IMetaIdentifier
{
public GlobalEqpType Type { get; init; }
public PrimaryId Condition { get; init; }
@ -19,6 +22,28 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation<GlobalEqpManipu
return Condition != 0;
}
public JObject AddToJson(JObject jObj)
{
jObj[nameof(Type)] = Type.ToString();
jObj[nameof(Condition)] = Condition.Id;
return jObj;
}
public static GlobalEqpManipulation? FromJson(JObject? jObj)
{
if (jObj == null)
return null;
var type = jObj[nameof(Type)]?.ToObject<GlobalEqpType>() ?? (GlobalEqpType)100;
var condition = jObj[nameof(Condition)]?.ToObject<PrimaryId>() ?? 0;
var ret = new GlobalEqpManipulation
{
Type = type,
Condition = condition,
};
return ret.Validate() ? ret : null;
}
public bool Equals(GlobalEqpManipulation other)
=> Type == other.Type
@ -45,6 +70,30 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation<GlobalEqpManipu
public override string ToString()
=> $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
{
var path = Type switch
{
GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Ears),
GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Neck),
GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists),
GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger),
GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger),
GlobalEqpType.DoNotHideHrothgarHats => string.Empty,
GlobalEqpType.DoNotHideVieraHats => string.Empty,
_ => string.Empty,
};
if (path.Length > 0)
identifier.Identify(changedItems, path);
else if (Type is GlobalEqpType.DoNotHideVieraHats)
changedItems["All Hats for Viera"] = null;
else if (Type is GlobalEqpType.DoNotHideHrothgarHats)
changedItems["All Hats for Hrothgar"] = null;
}
public MetaIndex FileIndex()
=> (MetaIndex)(-1);
=> MetaIndex.Eqp;
MetaManipulationType IMetaIdentifier.Type
=> MetaManipulationType.GlobalEqp;
}

View file

@ -36,4 +36,7 @@ public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier,
jObj["SetId"] = SetId.Id.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Gmp;
}

Some files were not shown because too many files have changed in this diff Show more