mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-06 16:04:38 +01:00
Merge branch 'meta_rework'
This commit is contained in:
commit
819afc518c
192 changed files with 4126 additions and 4124 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit e95c0f04edc7e85aea67498fd8bf495a7fe6d3c8
|
||||
Subproject commit caa9e9b9a5dc3928eba10b315cf6a0f6f1d84b65
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0a2e2650d693d1bba267498f96112682cc09dbab
|
||||
Subproject commit 3fbc704515b7b5fa9be02fb2a44719fc333747c1
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
60
Penumbra/Collections/Cache/IMetaCache.cs
Normal file
60
Penumbra/Collections/Cache/IMetaCache.cs
Normal 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 _)
|
||||
{ }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
13
Penumbra/Collections/Cache/RspCache.cs
Normal file
13
Penumbra/Collections/Cache/RspCache.cs
Normal 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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs
Normal file
30
Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
30
Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs
Normal file
30
Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
49
Penumbra/Interop/Hooks/Meta/EstHook.cs
Normal file
49
Penumbra/Interop/Hooks/Meta/EstHook.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
Penumbra/Interop/Hooks/Meta/GmpHook.cs
Normal file
64
Penumbra/Interop/Hooks/Meta/GmpHook.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
Penumbra/Interop/Hooks/Meta/RspBustHook.cs
Normal file
66
Penumbra/Interop/Hooks/Meta/RspBustHook.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
69
Penumbra/Interop/Hooks/Meta/RspHeightHook.cs
Normal file
69
Penumbra/Interop/Hooks/Meta/RspHeightHook.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
Penumbra/Interop/Hooks/Meta/RspTailHook.cs
Normal file
68
Penumbra/Interop/Hooks/Meta/RspTailHook.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
16
Penumbra/Interop/Processing/AvfxPathPreProcessor.cs
Normal file
16
Penumbra/Interop/Processing/AvfxPathPreProcessor.cs
Normal 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;
|
||||
}
|
||||
39
Penumbra/Interop/Processing/FilePostProcessService.cs
Normal file
39
Penumbra/Interop/Processing/FilePostProcessService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
37
Penumbra/Interop/Processing/GamePathPreProcessService.cs
Normal file
37
Penumbra/Interop/Processing/GamePathPreProcessService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
30
Penumbra/Interop/Processing/ImcFilePostProcessor.cs
Normal file
30
Penumbra/Interop/Processing/ImcFilePostProcessor.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
18
Penumbra/Interop/Processing/ImcPathPreProcessor.cs
Normal file
18
Penumbra/Interop/Processing/ImcPathPreProcessor.cs
Normal 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;
|
||||
}
|
||||
18
Penumbra/Interop/Processing/MaterialFilePostProcessor.cs
Normal file
18
Penumbra/Interop/Processing/MaterialFilePostProcessor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
16
Penumbra/Interop/Processing/MtrlPathPreProcessor.cs
Normal file
16
Penumbra/Interop/Processing/MtrlPathPreProcessor.cs
Normal 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;
|
||||
}
|
||||
16
Penumbra/Interop/Processing/TmbPathPreProcessor.cs
Normal file
16
Penumbra/Interop/Processing/TmbPathPreProcessor.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{ }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue