diff --git a/OtterGui b/OtterGui index e95c0f04e..caa9e9b9a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e95c0f04edc7e85aea67498fd8bf495a7fe6d3c8 +Subproject commit caa9e9b9a5dc3928eba10b315cf6a0f6f1d84b65 diff --git a/Penumbra.GameData b/Penumbra.GameData index 0a2e2650d..3fbc70451 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0a2e2650d693d1bba267498f96112682cc09dbab +Subproject commit 3fbc704515b7b5fa9be02fb2a44719fc333747c1 diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index c467df58c..ce1a9def3 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -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(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + } + + return Functions.ToCompressedBase64(array, CurrentVersion); } } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 38d080cca..0894a8e57 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -159,32 +159,18 @@ private static bool ConvertPaths(IReadOnlyDictionary redirection /// The empty string is treated as an empty set. /// Only returns true if all conversions are successful and distinct. /// - private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out HashSet? 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(manipString, out var manipArray) != MetaManipulation.CurrentVersion) - { - manips = null; - return false; - } - - manips = new HashSet(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; - } + if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion) + return true; - return true; + manips = null; + return false; } } diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index a8405eb29..0aa6821cb 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -4,6 +4,8 @@ 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 void Draw() 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 void Draw() && 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.CurrentVersion); + var manips = MetaApi.CompressMetaManipulations(copyCollection); _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); } @@ -187,8 +188,8 @@ void PrintList(string collectionName, IReadOnlyList list) 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()); } } } diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index aee2b4477..0b52e64a4 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -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 IReadOnlyList ModsForAllCollections => _modsForAllCollections; public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, - HashSet 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}."); diff --git a/Penumbra/Collections/Cache/CmpCache.cs b/Penumbra/Collections/Cache/CmpCache.cs deleted file mode 100644 index 470cadd47..000000000 --- a/Penumbra/Collections/Cache/CmpCache.cs +++ /dev/null @@ -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 _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(); - } -} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 4d8d0b4a0..4755840e0 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -125,12 +125,6 @@ public HashSet[] ReverseResolvePaths(IReadOnlyCollection f 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 @@ internal void AddModSync(IMod mod, bool addMetaChanges) 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 @@ private bool AddConflict(object data, IMod addedMod, IMod existingMod) 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 @@ private bool AddConflict(object data, IMod addedMod, IMod existingMod) // 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 @@ private void AddManipulation(MetaManipulation manip, IMod mod) 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 @@ void AddItems(IMod mod) 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); } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index ae424b946..44c12856f 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -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 @@ 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 @@ private void FullRecalculation(ModCollection collection) foreach (var mod in _modStorage) cache.AddModSync(mod, false); - cache.AddMetaFiles(true); - collection.IncrementCounter(); MetaFileManager.ApplyDefaultFiles(collection); diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index d0a3bc767..295191d25 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache; /// public class CollectionModData { - private readonly Dictionary, HashSet)> _data = new(); + private readonly Dictionary, HashSet)> _data = new(); - public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data - => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2)); + public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data + => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2)); - public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod) + public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod) { if (_data.Remove(mod, out var data)) return data; @@ -35,7 +35,7 @@ public void AddPath(IMod mod, Utf8GamePath path) } } - 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 void RemovePath(IMod mod, Utf8GamePath path) _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); diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index a0f27c234..5e0626cf7 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -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(manager, collection) { - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar - private readonly List _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()) - { - 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; - public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) - => _eqdpFiles - [Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar + var mask = Eqdp.Mask(identifier.Slot); + var newMask = pair.InverseMask | mask; + if (newMask is not EqdpEntry.FullMask) + _fullEntries[tuple] = (pair.Entry & ~mask, newMask); + } - 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(); } } diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 972ee5a59..60e38aefa 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -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(manager, collection) { - private ExpandedEqpFile? _eqpFile = null; - private readonly List _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; - - _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!); - } + => Clear(); - public void Dispose() - { - _eqpFile?.Dispose(); - _eqpFile = null; - _eqpManipulations.Clear(); - } + protected override void Dispose(bool _) + => Clear(); } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 3a0b46950..aff8beef2 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -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(manager, collection) { - private EstFile? _estFaceFile = null; - private EstFile? _estHairFile = null; - private EstFile? _estBodyFile = null; - private EstFile? _estHeadFile = null; - - private readonly List _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(); - } - - 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); - } + => Clear(); - 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(); } diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs similarity index 75% rename from Penumbra/Meta/Manipulations/GlobalEqpCache.cs rename to Penumbra/Collections/Cache/GlobalEqpCache.cs index 26eb1d058..1c80b47da 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -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, IService { private readonly HashSet _doNotHideEarrings = []; private readonly HashSet _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 void Clear() public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) { + if (Count == 0) + return original; + if (_doNotHideVieraHats) original |= EqpEntry.HeadShowVieraHat; @@ -52,8 +55,13 @@ public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) 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 bool Add(GlobalEqpManipulation manipulation) 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 RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod) + { + if (!Remove(manipulation, out mod)) + return false; - public bool Remove(GlobalEqpManipulation manipulation) - => manipulation.Type switch + _ = manipulation.Type switch { GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition), GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition), @@ -74,7 +88,9 @@ public bool Remove(GlobalEqpManipulation manipulation) 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; + } } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 0a713867f..9170b871d 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -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(manager, collection) { - private ExpandedGmpFile? _gmpFile = null; - private readonly List _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; - - _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!); - } + => Clear(); - public void Dispose() - { - _gmpFile?.Dispose(); - _gmpFile = null; - _gmpManipulations.Clear(); - } + protected override void Dispose(bool _) + => Clear(); } diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs new file mode 100644 index 000000000..fecc6f50d --- /dev/null +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -0,0 +1,60 @@ +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.Collections.Cache; + +public abstract class MetaCacheBase(MetaFileManager manager, ModCollection collection) + : Dictionary + 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.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 _) + { } +} diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 7990122ab..40c3d2c73 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -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(manager, collection) { - private readonly Dictionary _imcFiles = []; - private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = []; + private readonly Dictionary)> _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)); + if (!_imcFiles.TryGetValue(path, out var p)) + { + file = null; + return false; + } + + file = p.Item1; + return true; } - public void Reset(ModCollection collection) + public void Reset() { - foreach (var (path, file) in _imcFiles) + foreach (var (_, (file, set)) in _imcFiles) { - collection._cache!.RemovePath(path); file.Reset(); + set.Clear(); } - _imcManipulations.Clear(); + _imcFiles.Clear(); + Clear(); } - public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip) + protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { - if (!manip.Validate(true)) - return false; - - var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip)); - if (idx < 0) - { - idx = _imcManipulations.Count; - _imcManipulations.Add((manip, null!)); - } + ++Collection.ImcChangeCounter; + ApplyFile(identifier, entry); + } - var path = manip.GamePath(); + 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; - - var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); - if (idx < 0) - return false; + ++Collection.ImcChangeCounter; + var path = identifier.GamePath().Path; + if (!_imcFiles.TryGetValue(path, out var pair)) + return; - var (_, file) = _imcManipulations[idx]; - _imcManipulations.RemoveAt(idx); + if (!pair.Item2.Remove(identifier)) + return; - 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); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index fbca9c0ea..02056fad9 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -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 @@ namespace Penumbra.Collections.Cache; -public class MetaCache : IDisposable, IEnumerable> +public class MetaCache(MetaFileManager manager, ModCollection collection) { - private readonly MetaFileManager _manager; - private readonly ModCollection _collection; - private readonly Dictionary _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; - - public IReadOnlyCollection Manipulations - => _manipulations.Keys; - - public IEnumerator> GetEnumerator() - => _manipulations.GetEnumerator(); + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; - 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(); } - ~MetaCache() - => Dispose(); - - public bool ApplyMod(MetaManipulation manip, IMod mod) + public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) { - lock (_manipulations) + mod = null; + return identifier switch { - 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, + 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, }; - } - public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) - { - lock (_manipulations) + static bool Convert((IMod, T) pair, out IMod mod) { - 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; + mod = pair.Item1; + return true; } + } - // Imc manipulations do not require character utility, - // but they do require the file space to be ready. - return manip.ManipulationType switch + public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) + => identifier 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, + 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, }; - } - /// Set a single file. - public void SetFile(MetaIndex metaIndex) - { - switch (metaIndex) + public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry) + => identifier switch { - 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; - } - } - - /// Set the currently relevant IMC files for the collection cache. - 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); + 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(); /// Try to obtain a manipulated IMC file. 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); - - /// Use this when CharacterUtility becomes ready. - 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)); } diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs new file mode 100644 index 000000000..064b1f447 --- /dev/null +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -0,0 +1,13 @@ +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + public void Reset() + => Clear(); + + protected override void Dispose(bool _) + => Clear(); +} diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 4e8ebe36f..6d48f74b3 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -3,6 +3,7 @@ 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 @@ 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().Length - 3]; } -public class ActiveCollections : ISavable, IDisposable +public class ActiveCollections : ISavable, IDisposable, IService { public const int Version = 2; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 0243de1ef..caff2c86d 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -7,7 +8,7 @@ namespace Penumbra.Collections.Manager; -public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) +public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService { /// Enable or disable the mod inheritance of mod idx. public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit) diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index e95617b19..85f5b9570 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -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; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 67de3a035..cd680d362 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -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.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 new(left.Id + right); } -public class CollectionStorage : IReadOnlyList, IDisposable +public class CollectionStorage : IReadOnlyList, IDisposable, IService { private readonly CommunicatorService _communicator; private readonly SaveService _saveService; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 6003b5f9f..f3482cdf7 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -2,11 +2,10 @@ 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. /// -public class InheritanceManager : IDisposable +public class InheritanceManager : IDisposable, IService { public enum ValidInheritance { @@ -144,7 +143,8 @@ private void ApplyInheritances() 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 @@ private void ApplyInheritances() 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; } } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index ce438a6b7..5c8932328 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Communication; using Penumbra.GameData.Actors; @@ -8,7 +9,7 @@ namespace Penumbra.Collections.Manager; -public class TempCollectionManager : IDisposable +public class TempCollectionManager : IDisposable, IService { public int GlobalChangeCounter { get; private set; } public readonly IndividualCollections Collections; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 484d4dd23..81751128d 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -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 @@ internal IEnumerable> AllConflicts internal SingleArray Conflicts(Mod mod) => _cache?.Conflicts(mod) ?? new SingleArray(); - - 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; } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9286d4591..eb5ab46af 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -56,6 +56,8 @@ public string AnonymizedName /// public int ChangeCounter { get; private set; } + public uint ImcChangeCounter { get; set; } + /// Increment the number of changes in the effective file list. public int IncrementCounter() => ++ChangeCounter; diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 4e1d64534..484dd9547 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -3,6 +3,7 @@ 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.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"; diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 8a906ca0f..51d558681 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -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 { - /// + /// Api = 0, /// diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 02286cc71..f6100b621 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -4,6 +4,7 @@ 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 @@ namespace Penumbra; [Serializable] -public class Configuration : IPluginConfiguration, ISavable +public class Configuration : IPluginConfiguration, ISavable, IService { [JsonIgnore] private readonly SaveService _saveService; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 0a542d041..52e276c79 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -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 @@ namespace Penumbra; -public class EphemeralConfig : ISavable, IDisposable +public class EphemeralConfig : ISavable, IDisposable, IService { [JsonIgnore] private readonly SaveService _saveService; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index fdd28ef11..01396cfb8 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -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 void Dispose() _tasks.Clear(); } - public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, + string outputPath) => EnqueueWithResult( new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => action.Notifier @@ -52,7 +55,7 @@ public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnume /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. /// Modified extra skeleton template parameters. - public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) + public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair[] estManipulations) { var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) @@ -81,20 +84,18 @@ ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear }; } - private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations) + private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair[] 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 @@ private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipu 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)]; } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. @@ -250,9 +251,11 @@ Task CreateHavokTask((SklbFile Sklb, int Index) pair) 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 diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index f61577477..1f970dfeb 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -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 @@ private void DeserializeEqdpEntries(MetaFileInfo metaFileInfo, byte[]? data) 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 @@ private void DeserializeGmpEntry(MetaFileInfo metaFileInfo, byte[]? data) 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 @@ private void DeserializeEstEntries(MetaFileInfo metaFileInfo, byte[]? data) 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 @@ private void DeserializeEstEntries(MetaFileInfo metaFileInfo, byte[]? data) 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 @@ private void DeserializeImcEntries(MetaFileInfo metaFileInfo, byte[]? data) 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; } diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 4fb56df60..9cce60e3f 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -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 manipulations, DirectoryInfo basePath) + public static void WriteTexToolsMeta(MetaFileManager manager, MetaDictionary manipulations, DirectoryInfo basePath) { var files = ConvertToTexTools(manager, manipulations); @@ -27,49 +28,81 @@ public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable ConvertToTexTools(MetaFileManager manager, IEnumerable manips) + public static Dictionary ConvertToTexTools(MetaFileManager manager, MetaDictionary manips) { var ret = new Dictionary(); - 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 manips) + private static Dictionary SplitByFile(MetaDictionary manips) { - var list = manips.GroupBy(m => m.Rsp.Attribute).ToDictionary(m => m.Key, m => m.Last().Rsp); + var ret = new Dictionary(); + 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> 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 @@ void Add(params RspAttribute[] attributes) } 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 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 @@ private static byte[] WriteMetaFile(MetaFileManager manager, string path, IEnume 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,113 @@ private static byte[] WriteMetaFile(MetaFileManager manager, string path, IEnume 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) + 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, IReadOnlyDictionary 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); + + 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) { - b.Write((uint)header); - b.Write(offset); + 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; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Eqdp); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); - var size = WriteData(manager, b, offset, header, data); - b.Write(size); - offset += size; + foreach (var (identifier, entry) in manips) + { + b.Write((uint)identifier.GenderRace); + b.Write(entry.AsByte); } - return m.ToArray(); + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, + IReadOnlyDictionary 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(MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, - IEnumerable manips) + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary 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) + foreach (var (identifier, entry) in manips) { - 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; + b.Write((ushort)identifier.GenderRace); + b.Write(identifier.SetId.Id); + b.Write(entry.Value); } var size = b.BaseStream.Position - offset; @@ -198,19 +268,29 @@ private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offs return (uint)size; } - private static string ManipToPath(MetaManipulation manip) - => manip.ManipulationType switch + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary 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) { - 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, - }; + 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(ImcManipulation manip) + private static string ManipToPath(ImcIdentifier manip) { var path = manip.GamePath().ToString(); var replacement = manip.ObjectType switch @@ -224,33 +304,33 @@ private static string ManipToPath(ImcManipulation manip) 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 manip) + => $"chara/xls/charamake/rgsp/{(int)manip.Key.SubRace - 1}-{(int)manip.Key.Attribute.ToGender() - 1}.rgsp"; } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 71b9165fa..7b0bb5a8a 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -42,14 +42,6 @@ public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath 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 @@ void Add(RspAttribute attribute, float value) } 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)); + } } } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 25e00bd78..c4a8e81fe 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -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 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 TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte 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 TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte 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) { diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 976bc1798..4aa64209e 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -3,6 +3,7 @@ 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 @@ 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 _tasks = new(); + private readonly ConcurrentDictionary _tasks = new(); private bool _disposed; - public TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) - { - _uiBuilder = uiBuilder; - _gameData = gameData; - _logger = logger; - } - public IReadOnlyDictionary Tasks => _tasks; @@ -64,7 +57,8 @@ private Task Enqueue(IAction action) { 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 IDalamudTextureWrap LoadTextureWrap(BaseImage image, byte[]? rgba = null, /// Load a texture wrap for a given image. public IDalamudTextureWrap LoadTextureWrap(byte[] rgba, int width, int height) - => _uiBuilder.LoadImageRaw(rgba, width, height, 4); + => uiBuilder.LoadImageRaw(rgba, width, height, 4); /// Load any supported file from game data or drive depending on extension and if the path is rooted. public (BaseImage, TextureType) Load(string path) @@ -326,7 +320,7 @@ public static BaseImage ConvertToDds(byte[] rgba, int width, int height) } public bool GameFileExists(string path) - => _gameData.FileExists(path); + => gameData.FileExists(path); /// Add up to 13 mip maps to the input if mip maps is true, otherwise return input. public static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps) @@ -382,7 +376,7 @@ private Stream OpenTexStream(string path) 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."); } diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 2fd87f6e6..7936b831e 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -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 CalculateHeight(HookManager hooks, CollectionResolver collectionResolver, [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; } } diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 2f7174910..f589cf4ef 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -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 { private readonly CollectionResolver _collectionResolver; @@ -24,13 +24,15 @@ public ChangeCustomize(HookManager hooks, CollectionResolver collectionResolver, [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; } -} +} diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs new file mode 100644 index 000000000..aaaaccd46 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -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 +{ + 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("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}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs new file mode 100644 index 000000000..2711f1955 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -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 +{ + 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("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}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 6663c211c..7107e26bc 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -19,11 +19,10 @@ public EqpHook(HookManager hooks, MetaState metaState) 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 { diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs new file mode 100644 index 000000000..239311826 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -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 +{ + 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("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); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index beae6acc4..a10b511ad 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -29,8 +29,9 @@ private void Detour(DrawObject* drawObject) 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(); } } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 89aaa9b0e..30ec25973 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -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 { private readonly CollectionResolver _collectionResolver; @@ -29,8 +29,9 @@ private void Detour(DrawObject* drawObject) 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(); } -} +} diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs new file mode 100644 index 000000000..256d87028 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -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 +{ + 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("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, true); + } + + /// + /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . + /// 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. + /// + 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; + } + + /// Allocate and clean up our single stable ulong pointer. + private class Finalizer + { + public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); + + ~Finalizer() + { + Marshal.FreeHGlobal((nint)Pointer); + } + } +} diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 10c12594c..2c17362dc 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -23,10 +23,11 @@ public ModelLoadComplete(HookManager hooks, CollectionResolver collectionResolve 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(); } } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs new file mode 100644 index 000000000..86759460f --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -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 +{ + 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("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; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs new file mode 100644 index 000000000..cf88c34a1 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -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 +{ + 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("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; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 8f8f1d789..58856f521 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -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 @@ private void Detour(DrawObject* drawObject, nint unk2, float unk3, nint unk4, by 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(); } } diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs new file mode 100644 index 000000000..e40f01611 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -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 +{ + 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("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; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index e451f1183..82b24dc48 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -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; + /// /// 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 SetupVisor(HookManager hooks, CollectionResolver collectionResolver, Meta [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; } } diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index b0298ac7d..76854bca0 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -29,10 +29,11 @@ private void Detour(DrawObject* drawObject) 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(); } } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 9a68160bc..5941773fc 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -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 @@ private nint ResolveVfx(nint drawObject, nint pathBuffer, nint pathBufferSize, u 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 @@ private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSi 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 Create(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate { diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index b42571ac4..bc4749529 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -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; diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 93fee11e8..feb27341f 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -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 @@ 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; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 32090f7c2..eeff7eeea 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -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 @@ 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; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 6fa5c2638..f7dcfc07a 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -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 EqpCollection = []; + public readonly Stack EqdpCollection = []; + public readonly Stack EstCollection = []; + public readonly Stack RspCollection = []; + + public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = []; + private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; @@ -78,48 +83,9 @@ public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out Resolv 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 @@ private void OnCreatingCharacterBase(ModelCharaId* modelCharaId, CustomizeArray* 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 @@ private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, Charact if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection, (nint)drawObject); + RspCollection.Pop(); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 5627e0158..a8be97c85 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -32,7 +32,7 @@ public bool Valid /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateImc(ByteString path, ModCollection collection) - => CreateBase(path, collection); + => new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index e5c75327a..cc3e0e9b4 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -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; - } - - /// Obtain a temporary or permanent collection by local ID. - 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; } /// Try to resolve the given game path to the replaced path. @@ -113,14 +107,12 @@ public bool CollectionByLocalId(LocalCollectionId id, out ModCollection collecti // 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; } /// Use the default method of path replacement. @@ -130,24 +122,6 @@ public unsafe void Dispose() return (resolved, _collectionManager.Active.Default.ToResolveData()); } - /// After loading an IMC file, replace its contents with the modded IMC file. - private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, - ReadOnlySpan 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}."); - } - /// Resolve a path from the interface collection. private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) => (_collectionManager.Active.Interface.ResolvePath(path), diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index f4218e9ce..bf9d1e256 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Interop.Services; using Penumbra.String; @@ -5,7 +6,7 @@ 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; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 793ea20ba..44a152f00 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -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. /// -public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> +public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection>, IService { private readonly GameState _gameState; private readonly ResourceLoader _loader; @@ -66,21 +66,6 @@ public bool HandleSubFiles(ResourceType type, out ResolveData collection) return false; } - /// Materials, TMB, and AVFX need to be set per collection, so they can load their sub files independently of each other. - 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; diff --git a/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs new file mode 100644 index 000000000..56f693e67 --- /dev/null +++ b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs @@ -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; +} diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs new file mode 100644 index 000000000..0dc62b3d6 --- /dev/null +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -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 additionalData); +} + +public unsafe class FilePostProcessService : IRequiredService, IDisposable +{ + private readonly ResourceLoader _resourceLoader; + private readonly FrozenDictionary _processors; + + public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) + { + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().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 additionalData) + { + if (_processors.TryGetValue(resource->FileType, out var processor)) + processor.PostProcess(resource, path, additionalData); + } +} diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs new file mode 100644 index 000000000..004b71682 --- /dev/null +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -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 _processors; + + public GamePathPreProcessService(ServiceManager services) + { + _processors = services.GetServicesImplementing().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); + } +} diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs new file mode 100644 index 000000000..4a0ebe222 --- /dev/null +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -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 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}."); + } +} diff --git a/Penumbra/Interop/Processing/ImcPathPreProcessor.cs b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs new file mode 100644 index 000000000..907d7587a --- /dev/null +++ b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs @@ -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; +} diff --git a/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs new file mode 100644 index 000000000..02b5d46cf --- /dev/null +++ b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs @@ -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 additionalData) + { + if (!PathDataHandler.ReadMtrl(additionalData, out var data)) + return; + } +} diff --git a/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs new file mode 100644 index 000000000..8fb2400b3 --- /dev/null +++ b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs @@ -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; +} diff --git a/Penumbra/Interop/Processing/TmbPathPreProcessor.cs b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs new file mode 100644 index 000000000..dd8878196 --- /dev/null +++ b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs @@ -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; +} diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index 644427718..f1d7fe241 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -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) { diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 7b49beab8..4a4239935 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -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 @@ 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 @@ private void IncRefProtection(ResourceHandle* handle, ref nint? returnValue) /// /// Catch weird errors with invalid decrements of the reference count. /// - private void DecRefProtection(ResourceHandle* handle, ref byte? returnValue) + private static void DecRefProtection(ResourceHandle* handle, ref byte? returnValue) { if (handle->RefCount != 0) return; diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index a087a6593..c885c3178 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -4,12 +4,13 @@ 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); diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index e3338e6c5..54c867770 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -2,6 +2,7 @@ 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 @@ namespace Penumbra.Interop.ResourceLoading; -public unsafe class ResourceService : IDisposable +public unsafe class ResourceService : IDisposable, IRequiredService { private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index b9279f543..e617673ea 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -2,13 +2,14 @@ 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 { /// Custom ulong flag to signal our files as opposed to SE files. public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 2b87e6882..4dfefd962 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -273,7 +273,7 @@ private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEqu { 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) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 5a190e526..e26c14364 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -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); diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index da04bf90f..532dc8230 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -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 static readonly InternalIndex[] ReverseIndices private readonly MetaList[] _lists; - public IReadOnlyList 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 @@ private void LoadDefaultResources(object _) 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(); - } - /// Return all relevant resources to the default resource. 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; diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 2f4a3cfd5..259fdd109 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -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. /// -public unsafe class FontReloader +public unsafe class FontReloader : IService { public bool Valid => _reloadFontsFunc != null; diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index e956040b1..839c289e5 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -2,26 +2,14 @@ namespace Penumbra.Interop.Services; -public unsafe class MetaList : IDisposable +public class MetaList(CharacterUtility.InternalIndex index) { - private readonly CharacterUtility _utility; - private readonly LinkedList _entries = new(); - public readonly CharacterUtility.InternalIndex Index; - public readonly MetaIndex GlobalMetaIndex; - - public IReadOnlyCollection 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 void SetDefaultResource(nint data, int size) _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(); - } - - /// Set the currently stored data of this resource to new values. - private void SetResourceInternal(nint data, int length) - { - if (!Ready) - return; - - var resource = _utility.Address->Resource(GlobalMetaIndex); - resource->SetData(data, length); - } - - /// Reset the currently stored data of this resource to its default values. - 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; - } - } } diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index 7df83cf70..b268b395c 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -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 @@ private void LoadDefaultResources(object _) if (DefaultCharacterGlassShaderPackage == null) { - DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; - anyMissing |= DefaultCharacterGlassShaderPackage == null; + DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; + anyMissing |= DefaultCharacterGlassShaderPackage == null; } if (anyMissing) return; - Ready = true; + Ready = true; _framework.Update -= LoadDefaultResources; } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 21ecfd4ff..61d7b90c6 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -6,6 +6,7 @@ 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 @@ 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 @@ private void WriteInvisible(GameObject? actor) 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 @@ private void WriteVisible(GameObject? actor) 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 bool GetName(string lowerName, out GameObject? actor) 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; diff --git a/Penumbra/Interop/Services/ResidentResourceManager.cs b/Penumbra/Interop/Services/ResidentResourceManager.cs index 726971851..4f430aa10 100644 --- a/Penumbra/Interop/Services/ResidentResourceManager.cs +++ b/Penumbra/Interop/Services/ResidentResourceManager.cs @@ -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)] diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs index 3809ecbdf..95e70b450 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -139,9 +139,9 @@ private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRende // 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 diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 96cda496d..5028a3de5 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -34,7 +34,7 @@ public void Reset(IEnumerable<(SubRace, RspAttribute)> entries) } public CmpFile(MetaFileManager manager) - : base(manager, MetaIndex.HumanCmp) + : base(manager, manager.MarshalAllocator, MetaIndex.HumanCmp) { AllocateData(DefaultData.Length); Reset(); @@ -46,6 +46,14 @@ public static RspEntry GetDefault(MetaFileManager manager, SubRace subRace, RspA 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 { diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index c76c4efd3..34b4f25be 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -2,6 +2,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; @@ -86,7 +87,7 @@ public void Reset(IEnumerable entries) } 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 static EqdpEntry GetDefault(byte* data, PrimaryId primaryId) 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); } diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 70067c2b8..a7540f4b2 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -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; /// 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 sealed override void Reset() } 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 @@ protected static ulong GetDefaultInternal(MetaFileManager manager, CharacterUtil } } -public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable +public sealed class ExpandedEqpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, false), IEnumerable { 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 @@ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } -public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable +public sealed class ExpandedGmpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, true), IEnumerable { 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 GmpEntry this[PrimaryId idx] 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 entries) { foreach (var entry in entries) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index ee38ea1e2..ba38d6d9f 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -157,7 +157,7 @@ public override void Reset() } 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 static EstEntry GetDefault(MetaFileManager manager, MetaIndex metaIndex, 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); } diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs index 3d0b4dbef..6ab1591cf 100644 --- a/Penumbra/Meta/Files/EvpFile.cs +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -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. /// -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 EvpFlag Flag(ushort modelSet, int arrayIndex) return EvpFlag.None; } - - public EvpFile(MetaFileManager manager) - : base(manager, (MetaIndex)1) // TODO: Name - { } } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 5d704cf81..01ef3f167 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -7,16 +7,10 @@ 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 ImcEntry GetEntry(int partIdx, Variant variantIdx) 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 override void Reset() } 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 static ImcEntry GetEntry(ReadOnlySpan imcFileData, EquipSlot slot, 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 void Replace(ResourceHandle* resource) 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); } } diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index ab08efc26..86a551013 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -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(int length, int alignment = 1) where T : unmanaged; + public void Release(ref T* pointer, int length) where T : unmanaged; - public byte* Data { get; private set; } - public int Length { get; private set; } - public CharacterUtility.InternalIndex Index { get; } + public void Release(void* pointer, int length) + { + var tmp = (byte*)pointer; + Release(ref tmp, length); + } + + public byte* Allocate(int length, int alignment = 1) + => Allocate(length, alignment); +} - public MetaBaseFile(MetaFileManager manager, MetaIndex idx) +public sealed class MarshalAllocator : IFileAllocator +{ + public unsafe T* Allocate(int length, int alignment = 1) where T : unmanaged + => (T*)Marshal.AllocHGlobal(length * sizeof(T)); + + public unsafe void Release(ref T* pointer, int length) where T : unmanaged { - Manager = manager; - Index = CharacterUtility.ReverseIndices[(int)idx]; + Marshal.FreeHGlobal((nint)pointer); + pointer = null; } +} + +public sealed unsafe class XivFileAllocator : IFileAllocator, IService +{ + /// + /// Allocate in the games space for file storage. + /// We only need this if using any meta file. + /// + [Signature(Sigs.GetFileSpace)] + private readonly nint _getFileSpaceAddress = nint.Zero; + + public XivFileAllocator(IGameInteropProvider provider) + => provider.InitializeFromAttributes(this); + + public IMemorySpace* GetFileSpace() + => ((delegate* unmanaged)_getFileSpaceAddress)(); + + public T* Allocate(int length, int alignment = 1) where T : unmanaged + => (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + + public void Release(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; } = CharacterUtility.ReverseIndices[(int)idx]; protected (IntPtr Data, int Length) DefaultData => Manager.CharacterUtility.DefaultResource(Index); @@ -30,7 +82,7 @@ public virtual void Reset() 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 @@ protected void AllocateData(int length) /// Free memory. 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 @@ protected void ResizeResources(int newLength) if (newLength == Length) return; - var data = (byte*)Manager.AllocateFileMemory((ulong)newLength); + var data = Allocator.Allocate(newLength); if (newLength > Length) { MemoryUtility.MemCpyUnchecked(data, Data, Length); diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 650919a35..751113a05 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -51,10 +51,6 @@ public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) 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) diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 6d6942e65..3f856bd21 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -69,19 +69,27 @@ public JObject AddToJson(JObject jObj) 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}"; } diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs deleted file mode 100644 index 2c01ce3f7..000000000 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ /dev/null @@ -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 -{ - [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; - } -} diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index 572dc203d..ec4dd6e7b 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -50,6 +50,9 @@ public JObject AddToJson(JObject jObj) jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Eqp; } public readonly record struct EqpEntryInternal(uint Value) diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs deleted file mode 100644 index 3bced0968..000000000 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ /dev/null @@ -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 -{ - [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; - } -} diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 9f878f976..2955dba48 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -91,6 +91,9 @@ public JObject AddToJson(JObject jObj) jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Est; } [JsonConverter(typeof(Converter))] @@ -111,3 +114,16 @@ public override EstEntry ReadJson(JsonReader reader, Type objectType, EstEntry e => new(serializer.Deserialize(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", + }; +} diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs deleted file mode 100644 index c3f9792fe..000000000 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ /dev/null @@ -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 -{ - 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; - } -} - - diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index ada543dce..2b88d9620 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -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 +public readonly struct GlobalEqpManipulation : IMetaIdentifier { public GlobalEqpType Type { get; init; } public PrimaryId Condition { get; init; } @@ -19,6 +22,28 @@ public bool Validate() 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)100; + var condition = jObj[nameof(Condition)]?.ToObject() ?? 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 override int GetHashCode() public override string ToString() => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; + public void AddChangedItems(ObjectIdentification identifier, IDictionary 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; } diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 1b7c70ba1..a6fcf58b7 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -36,4 +36,7 @@ public JObject AddToJson(JObject jObj) jObj["SetId"] = SetId.Id.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Gmp; } diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs deleted file mode 100644 index 0b2a9f4b5..000000000 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Newtonsoft.Json; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct GmpManipulation : IMetaManipulation -{ - public GmpIdentifier Identifier { get; private init; } - - public GmpEntry Entry { get; private init; } - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConstructor] - public GmpManipulation(GmpEntry entry, PrimaryId setId) - { - Entry = entry; - Identifier = new GmpIdentifier(setId); - } - - public GmpManipulation Copy(GmpEntry entry) - => new(entry, SetId); - - public override string ToString() - => $"Gmp - {SetId}"; - - public bool Equals(GmpManipulation other) - => SetId == other.SetId; - - public override bool Equals(object? obj) - => obj is GmpManipulation other && Equals(other); - - public override int GetHashCode() - => SetId.GetHashCode(); - - public int CompareTo(GmpManipulation other) - => SetId.Id.CompareTo(other.SetId.Id); - - public MetaIndex FileIndex() - => MetaIndex.Gmp; - - public bool Apply(ExpandedGmpFile file) - { - var entry = file[SetId]; - if (entry == Entry) - return false; - - file[SetId] = Entry; - return true; - } - - public bool Validate() - // No known conditions. - => true; -} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 4ad6bd3d5..5707ffca9 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -4,6 +4,18 @@ namespace Penumbra.Meta.Manipulations; +public enum MetaManipulationType : byte +{ + Unknown = 0, + Imc = 1, + Eqdp = 2, + Eqp = 3, + Est = 4, + Gmp = 5, + Rsp = 6, + GlobalEqp = 7, +} + public interface IMetaIdentifier { public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); @@ -13,4 +25,8 @@ public interface IMetaIdentifier public bool Validate(); public JObject AddToJson(JObject jObj); + + public MetaManipulationType Type { get; } + + public string ToString(); } diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 2a2f4c03a..44c609426 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,9 +27,6 @@ public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, Variant variant) : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public ImcManipulation ToManipulation(ImcEntry entry) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); @@ -193,4 +190,7 @@ public JObject AddToJson(JObject jObj) jObj["BodySlot"] = BodySlot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Imc; } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs deleted file mode 100644 index 5065a06eb..000000000 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ /dev/null @@ -1,108 +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.String.Classes; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct ImcManipulation : IMetaManipulation -{ - [JsonIgnore] - public ImcIdentifier Identifier { get; private init; } - - public ImcEntry Entry { get; private init; } - - - public PrimaryId PrimaryId - => Identifier.PrimaryId; - - public SecondaryId SecondaryId - => Identifier.SecondaryId; - - public Variant Variant - => Identifier.Variant; - - [JsonConverter(typeof(StringEnumConverter))] - public ObjectType ObjectType - => Identifier.ObjectType; - - [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot EquipSlot - => Identifier.EquipSlot; - - [JsonConverter(typeof(StringEnumConverter))] - public BodySlot BodySlot - => Identifier.BodySlot; - - public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry) - : this(new ImcIdentifier(equipSlot, primaryId, variant), entry) - { } - - public ImcManipulation(ImcIdentifier identifier, ImcEntry entry) - { - Identifier = identifier; - Entry = entry; - } - - - // Variants were initially ushorts but got shortened to bytes. - // There are still some manipulations around that have values > 255 for variant, - // so we change the unused value to something nonsensical in that case, just so they do not compare equal, - // and clamp the variant to 255. - [JsonConstructor] - internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, SecondaryId secondaryId, ushort variant, - EquipSlot equipSlot, ImcEntry entry) - { - Entry = entry; - var v = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - Identifier = objectType switch - { - ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, - variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, bodySlot == BodySlot.Unknown ? BodySlot.Body : BodySlot.Unknown), - }; - } - - public ImcManipulation Copy(ImcEntry entry) - => new(Identifier, entry); - - public override string ToString() - => Identifier.ToString(); - - public bool Equals(ImcManipulation other) - => Identifier == other.Identifier; - - public override bool Equals(object? obj) - => obj is ImcManipulation other && Equals(other); - - public override int GetHashCode() - => Identifier.GetHashCode(); - - public int CompareTo(ImcManipulation other) - => Identifier.CompareTo(other.Identifier); - - public MetaIndex FileIndex() - => Identifier.FileIndex(); - - public Utf8GamePath GamePath() - => Identifier.GamePath(); - - public bool Apply(ImcFile file) - => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); - - public bool Validate(bool withMaterial) - { - if (!Identifier.Validate()) - return false; - - if (withMaterial && Entry.MaterialId == 0) - return false; - - return true; - } -} diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs new file mode 100644 index 000000000..1093c6c5c --- /dev/null +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -0,0 +1,642 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Structs; +using Penumbra.Util; +using ImcEntry = Penumbra.GameData.Structs.ImcEntry; + +namespace Penumbra.Meta.Manipulations; + +[JsonConverter(typeof(Converter))] +public class MetaDictionary +{ + private readonly Dictionary _imc = []; + private readonly Dictionary _eqp = []; + private readonly Dictionary _eqdp = []; + private readonly Dictionary _est = []; + private readonly Dictionary _rsp = []; + private readonly Dictionary _gmp = []; + private readonly HashSet _globalEqp = []; + + public IReadOnlyDictionary Imc + => _imc; + + public IReadOnlyDictionary Eqp + => _eqp; + + public IReadOnlyDictionary Eqdp + => _eqdp; + + public IReadOnlyDictionary Est + => _est; + + public IReadOnlyDictionary Gmp + => _gmp; + + public IReadOnlyDictionary Rsp + => _rsp; + + public IReadOnlySet GlobalEqp + => _globalEqp; + + public int Count { get; private set; } + + public int GetCount(MetaManipulationType type) + => type switch + { + MetaManipulationType.Imc => _imc.Count, + MetaManipulationType.Eqdp => _eqdp.Count, + MetaManipulationType.Eqp => _eqp.Count, + MetaManipulationType.Est => _est.Count, + MetaManipulationType.Gmp => _gmp.Count, + MetaManipulationType.Rsp => _rsp.Count, + MetaManipulationType.GlobalEqp => _globalEqp.Count, + _ => 0, + }; + + public bool Contains(IMetaIdentifier identifier) + => identifier switch + { + EqdpIdentifier i => _eqdp.ContainsKey(i), + EqpIdentifier i => _eqp.ContainsKey(i), + EstIdentifier i => _est.ContainsKey(i), + GlobalEqpManipulation i => _globalEqp.Contains(i), + GmpIdentifier i => _gmp.ContainsKey(i), + ImcIdentifier i => _imc.ContainsKey(i), + RspIdentifier i => _rsp.ContainsKey(i), + _ => false, + }; + + public void Clear() + { + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _est.Clear(); + _rsp.Clear(); + _gmp.Clear(); + _globalEqp.Clear(); + } + + public bool Equals(MetaDictionary other) + => Count == other.Count + && _imc.SetEquals(other._imc) + && _eqp.SetEquals(other._eqp) + && _eqdp.SetEquals(other._eqdp) + && _est.SetEquals(other._est) + && _rsp.SetEquals(other._rsp) + && _gmp.SetEquals(other._gmp) + && _globalEqp.SetEquals(other._globalEqp); + + public IEnumerable Identifiers + => _imc.Keys.Cast() + .Concat(_eqdp.Keys.Cast()) + .Concat(_eqp.Keys.Cast()) + .Concat(_est.Keys.Cast()) + .Concat(_gmp.Keys.Cast()) + .Concat(_rsp.Keys.Cast()) + .Concat(_globalEqp.Cast()); + + #region TryAdd + + public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) + { + if (!_imc.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) + => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); + + public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EqdpIdentifier identifier, EqdpEntry entry) + => TryAdd(identifier, new EqdpEntryInternal(entry, identifier.Slot)); + + public bool TryAdd(EstIdentifier identifier, EstEntry entry) + { + if (!_est.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(GmpIdentifier identifier, GmpEntry entry) + { + if (!_gmp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(RspIdentifier identifier, RspEntry entry) + { + if (!_rsp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(GlobalEqpManipulation identifier) + { + if (!_globalEqp.Add(identifier)) + return false; + + ++Count; + return true; + } + + #endregion + + #region Update + + public bool Update(ImcIdentifier identifier, ImcEntry entry) + { + if (!_imc.ContainsKey(identifier)) + return false; + + _imc[identifier] = entry; + return true; + } + + public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.ContainsKey(identifier)) + return false; + + _eqp[identifier] = entry; + return true; + } + + public bool Update(EqpIdentifier identifier, EqpEntry entry) + => Update(identifier, new EqpEntryInternal(entry, identifier.Slot)); + + public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.ContainsKey(identifier)) + return false; + + _eqdp[identifier] = entry; + return true; + } + + public bool Update(EqdpIdentifier identifier, EqdpEntry entry) + => Update(identifier, new EqdpEntryInternal(entry, identifier.Slot)); + + public bool Update(EstIdentifier identifier, EstEntry entry) + { + if (!_est.ContainsKey(identifier)) + return false; + + _est[identifier] = entry; + return true; + } + + public bool Update(GmpIdentifier identifier, GmpEntry entry) + { + if (!_gmp.ContainsKey(identifier)) + return false; + + _gmp[identifier] = entry; + return true; + } + + public bool Update(RspIdentifier identifier, RspEntry entry) + { + if (!_rsp.ContainsKey(identifier)) + return false; + + _rsp[identifier] = entry; + return true; + } + + #endregion + + #region TryGetValue + + public bool TryGetValue(EstIdentifier identifier, out EstEntry value) + => _est.TryGetValue(identifier, out value); + + public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) + => _eqp.TryGetValue(identifier, out value); + + public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) + => _eqdp.TryGetValue(identifier, out value); + + public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) + => _gmp.TryGetValue(identifier, out value); + + public bool TryGetValue(RspIdentifier identifier, out RspEntry value) + => _rsp.TryGetValue(identifier, out value); + + public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) + => _imc.TryGetValue(identifier, out value); + + #endregion + + public bool Remove(IMetaIdentifier identifier) + { + var ret = identifier switch + { + EqdpIdentifier i => _eqdp.Remove(i), + EqpIdentifier i => _eqp.Remove(i), + EstIdentifier i => _est.Remove(i), + GlobalEqpManipulation i => _globalEqp.Remove(i), + GmpIdentifier i => _gmp.Remove(i), + ImcIdentifier i => _imc.Remove(i), + RspIdentifier i => _rsp.Remove(i), + _ => false, + }; + if (ret) + --Count; + return ret; + } + + #region Merging + + public void UnionWith(MetaDictionary manips) + { + foreach (var (identifier, entry) in manips._imc) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._eqp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._eqdp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._gmp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._rsp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._est) + TryAdd(identifier, entry); + + foreach (var identifier in manips._globalEqp) + TryAdd(identifier); + } + + /// Try to merge all manipulations from manips into this, and return the first failure, if any. + public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier) + { + foreach (var (identifier, _) in manips._imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + + foreach (var (identifier, _) in manips._eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + + foreach (var (identifier, _) in manips._eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + + foreach (var (identifier, _) in manips._gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + + foreach (var (identifier, _) in manips._rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + + foreach (var (identifier, _) in manips._est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) + { + failedIdentifier = identifier; + return false; + } + + failedIdentifier = default; + return true; + } + + public void SetTo(MetaDictionary other) + { + _imc.SetTo(other._imc); + _eqp.SetTo(other._eqp); + _eqdp.SetTo(other._eqdp); + _est.SetTo(other._est); + _rsp.SetTo(other._rsp); + _gmp.SetTo(other._gmp); + _globalEqp.SetTo(other._globalEqp); + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + } + + public void UpdateTo(MetaDictionary other) + { + _imc.UpdateTo(other._imc); + _eqp.UpdateTo(other._eqp); + _eqdp.UpdateTo(other._eqdp); + _est.UpdateTo(other._est); + _rsp.UpdateTo(other._rsp); + _gmp.UpdateTo(other._gmp); + _globalEqp.UnionWith(other._globalEqp); + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + } + + #endregion + + public MetaDictionary Clone() + { + var ret = new MetaDictionary(); + ret.SetTo(this); + return ret; + } + + public static JObject Serialize(EqpIdentifier identifier, EqpEntryInternal entry) + => Serialize(identifier, entry.ToEntry(identifier.Slot)); + + public static JObject Serialize(EqpIdentifier identifier, EqpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Eqp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = (ulong)entry, + }), + }; + + public static JObject Serialize(EqdpIdentifier identifier, EqdpEntryInternal entry) + => Serialize(identifier, entry.ToEntry(identifier.Slot)); + + public static JObject Serialize(EqdpIdentifier identifier, EqdpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Eqdp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = (ushort)entry, + }), + }; + + public static JObject Serialize(EstIdentifier identifier, EstEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Est.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + + public static JObject Serialize(GmpIdentifier identifier, GmpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Gmp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = JObject.FromObject(entry), + }), + }; + + public static JObject Serialize(ImcIdentifier identifier, ImcEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Imc.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = JObject.FromObject(entry), + }), + }; + + public static JObject Serialize(RspIdentifier identifier, RspEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Rsp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + + public static JObject Serialize(GlobalEqpManipulation identifier) + => new() + { + ["Type"] = MetaManipulationType.GlobalEqp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject()), + }; + + public static JObject? Serialize(TIdentifier identifier, TEntry entry) + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged + { + if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntryInternal)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntryInternal)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EstIdentifier) && typeof(TEntry) == typeof(EstEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(GmpIdentifier) && typeof(TEntry) == typeof(GmpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(RspIdentifier) && typeof(TEntry) == typeof(RspEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) + return Serialize(Unsafe.As(ref identifier)); + + return null; + } + + public static JArray SerializeTo(JArray array, IEnumerable> manipulations) + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged + { + foreach (var (identifier, entry) in manipulations) + { + if (Serialize(identifier, entry) is { } jObj) + array.Add(jObj); + } + + return array; + } + + public static JArray SerializeTo(JArray array, IEnumerable manipulations) + { + foreach (var manip in manipulations) + array.Add(Serialize(manip)); + + return array; + } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MetaDictionary? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + var array = new JArray(); + SerializeTo(array, value._imc); + SerializeTo(array, value._eqp); + SerializeTo(array, value._eqdp); + SerializeTo(array, value._est); + SerializeTo(array, value._rsp); + SerializeTo(array, value._gmp); + SerializeTo(array, value._globalEqp); + array.WriteTo(writer); + } + + public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var dict = existingValue ?? new MetaDictionary(); + dict.Clear(); + var jObj = JArray.Load(reader); + foreach (var item in jObj) + { + var type = item["Type"]?.ToObject() ?? MetaManipulationType.Unknown; + if (type is MetaManipulationType.Unknown) + { + Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered."); + continue; + } + + if (item["Manipulation"] is not JObject manip) + { + Penumbra.Log.Warning($"Manipulation of type {type} does not contain manipulation data."); + continue; + } + + switch (type) + { + case MetaManipulationType.Imc: + { + var identifier = ImcIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); + break; + } + case MetaManipulationType.Eqdp: + { + var identifier = EqdpIdentifier.FromJson(manip); + var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); + break; + } + case MetaManipulationType.Eqp: + { + var identifier = EqpIdentifier.FromJson(manip); + var entry = (EqpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); + break; + } + case MetaManipulationType.Est: + { + var identifier = EstIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid EST Manipulation encountered."); + break; + } + case MetaManipulationType.Gmp: + { + var identifier = GmpIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); + break; + } + case MetaManipulationType.Rsp: + { + var identifier = RspIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); + break; + } + case MetaManipulationType.GlobalEqp: + { + var identifier = GlobalEqpManipulation.FromJson(manip); + if (identifier.HasValue) + dict.TryAdd(identifier.Value); + else + Penumbra.Log.Warning("Invalid Global EQP Manipulation encountered."); + break; + } + } + } + + return dict; + } + } + + public MetaDictionary() + { } + + public MetaDictionary(MetaCache? cache) + { + if (cache == null) + return; + + _imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + _eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); + Count = cache.Count; + } +} diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs deleted file mode 100644 index f22de809b..000000000 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ /dev/null @@ -1,317 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.Interop.Structs; -using Penumbra.String.Functions; - -namespace Penumbra.Meta.Manipulations; - -public interface IMetaManipulation -{ - public MetaIndex FileIndex(); -} - -public interface IMetaManipulation - : IMetaManipulation, IComparable, IEquatable where T : struct -{ } - -[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 16)] -public readonly struct MetaManipulation : IEquatable, IComparable -{ - public const int CurrentVersion = 0; - - public enum Type : byte - { - Unknown = 0, - Imc = 1, - Eqdp = 2, - Eqp = 3, - Est = 4, - Gmp = 5, - Rsp = 6, - GlobalEqp = 7, - } - - [FieldOffset(0)] - [JsonIgnore] - public readonly EqpManipulation Eqp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly GmpManipulation Gmp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly EqdpManipulation Eqdp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly EstManipulation Est = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly RspManipulation Rsp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly ImcManipulation Imc = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly GlobalEqpManipulation GlobalEqp = default; - - [FieldOffset(15)] - [JsonConverter(typeof(StringEnumConverter))] - [JsonProperty("Type")] - public readonly Type ManipulationType; - - public object? Manipulation - { - get => ManipulationType switch - { - Type.Unknown => null, - Type.Imc => Imc, - Type.Eqdp => Eqdp, - Type.Eqp => Eqp, - Type.Est => Est, - Type.Gmp => Gmp, - Type.Rsp => Rsp, - Type.GlobalEqp => GlobalEqp, - _ => null, - }; - init - { - switch (value) - { - case EqpManipulation m: - Eqp = m; - ManipulationType = m.Validate() ? Type.Eqp : Type.Unknown; - return; - case EqdpManipulation m: - Eqdp = m; - ManipulationType = m.Validate() ? Type.Eqdp : Type.Unknown; - return; - case GmpManipulation m: - Gmp = m; - ManipulationType = m.Validate() ? Type.Gmp : Type.Unknown; - return; - case EstManipulation m: - Est = m; - ManipulationType = m.Validate() ? Type.Est : Type.Unknown; - return; - case RspManipulation m: - Rsp = m; - ManipulationType = m.Validate() ? Type.Rsp : Type.Unknown; - return; - case ImcManipulation m: - Imc = m; - ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown; - return; - case GlobalEqpManipulation m: - GlobalEqp = m; - ManipulationType = m.Validate() ? Type.GlobalEqp : Type.Unknown; - return; - } - } - } - - public bool Validate() - { - return ManipulationType switch - { - Type.Imc => Imc.Validate(true), - Type.Eqdp => Eqdp.Validate(), - Type.Eqp => Eqp.Validate(), - Type.Est => Est.Validate(), - Type.Gmp => Gmp.Validate(), - Type.Rsp => Rsp.Validate(), - Type.GlobalEqp => GlobalEqp.Validate(), - _ => false, - }; - } - - public MetaManipulation(EqpManipulation eqp) - { - Eqp = eqp; - ManipulationType = Type.Eqp; - } - - public MetaManipulation(GmpManipulation gmp) - { - Gmp = gmp; - ManipulationType = Type.Gmp; - } - - public MetaManipulation(EqdpManipulation eqdp) - { - Eqdp = eqdp; - ManipulationType = Type.Eqdp; - } - - public MetaManipulation(EstManipulation est) - { - Est = est; - ManipulationType = Type.Est; - } - - public MetaManipulation(RspManipulation rsp) - { - Rsp = rsp; - ManipulationType = Type.Rsp; - } - - public MetaManipulation(ImcManipulation imc) - { - Imc = imc; - ManipulationType = Type.Imc; - } - - public MetaManipulation(GlobalEqpManipulation eqp) - { - GlobalEqp = eqp; - ManipulationType = Type.GlobalEqp; - } - - public static implicit operator MetaManipulation(EqpManipulation eqp) - => new(eqp); - - public static implicit operator MetaManipulation(GmpManipulation gmp) - => new(gmp); - - public static implicit operator MetaManipulation(EqdpManipulation eqdp) - => new(eqdp); - - public static implicit operator MetaManipulation(EstManipulation est) - => new(est); - - public static implicit operator MetaManipulation(RspManipulation rsp) - => new(rsp); - - public static implicit operator MetaManipulation(ImcManipulation imc) - => new(imc); - - public static implicit operator MetaManipulation(GlobalEqpManipulation eqp) - => new(eqp); - - public bool EntryEquals(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return false; - - return ManipulationType switch - { - Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), - Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), - Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), - Type.Est => Est.Entry.Equals(other.Est.Entry), - Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), - Type.Imc => Imc.Entry.Equals(other.Imc.Entry), - Type.GlobalEqp => true, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public bool Equals(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return false; - - return ManipulationType switch - { - Type.Eqp => Eqp.Equals(other.Eqp), - Type.Gmp => Gmp.Equals(other.Gmp), - Type.Eqdp => Eqdp.Equals(other.Eqdp), - Type.Est => Est.Equals(other.Est), - Type.Rsp => Rsp.Equals(other.Rsp), - Type.Imc => Imc.Equals(other.Imc), - Type.GlobalEqp => GlobalEqp.Equals(other.GlobalEqp), - _ => false, - }; - } - - public MetaManipulation WithEntryOf(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return this; - - return ManipulationType switch - { - Type.Eqp => Eqp.Copy(other.Eqp.Entry), - Type.Gmp => Gmp.Copy(other.Gmp.Entry), - Type.Eqdp => Eqdp.Copy(other.Eqdp), - Type.Est => Est.Copy(other.Est.Entry), - Type.Rsp => Rsp.Copy(other.Rsp.Entry), - Type.Imc => Imc.Copy(other.Imc.Entry), - Type.GlobalEqp => GlobalEqp, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public override bool Equals(object? obj) - => obj is MetaManipulation other && Equals(other); - - public override int GetHashCode() - => ManipulationType switch - { - Type.Eqp => Eqp.GetHashCode(), - Type.Gmp => Gmp.GetHashCode(), - Type.Eqdp => Eqdp.GetHashCode(), - Type.Est => Est.GetHashCode(), - Type.Rsp => Rsp.GetHashCode(), - Type.Imc => Imc.GetHashCode(), - Type.GlobalEqp => GlobalEqp.GetHashCode(), - _ => 0, - }; - - public unsafe int CompareTo(MetaManipulation other) - { - fixed (MetaManipulation* lhs = &this) - { - return MemoryUtility.MemCmpUnchecked(lhs, &other, sizeof(MetaManipulation)); - } - } - - public override string ToString() - => ManipulationType switch - { - Type.Eqp => Eqp.ToString(), - Type.Gmp => Gmp.ToString(), - Type.Eqdp => Eqdp.ToString(), - Type.Est => Est.ToString(), - Type.Rsp => Rsp.ToString(), - Type.Imc => Imc.ToString(), - Type.GlobalEqp => GlobalEqp.ToString(), - _ => "Invalid", - }; - - public string EntryToString() - => ManipulationType switch - { - Type.Imc => - $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", - Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", - Type.Eqp => $"{(ulong)Eqp.Entry:X}", - Type.Est => $"{Est.Entry}", - Type.Gmp => $"{Gmp.Entry.Value}", - Type.Rsp => $"{Rsp.Entry}", - Type.GlobalEqp => string.Empty, - _ => string.Empty, - }; - - public static bool operator ==(MetaManipulation left, MetaManipulation right) - => left.Equals(right); - - public static bool operator !=(MetaManipulation left, MetaManipulation right) - => !(left == right); - - public static bool operator <(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) < 0; - - public static bool operator <=(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) <= 0; - - public static bool operator >(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) > 0; - - public static bool operator >=(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) >= 0; -} diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 29cdfd714..73d1d7e5b 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -1,3 +1,4 @@ +using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -12,13 +13,34 @@ public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); public MetaIndex FileIndex() - => throw new NotImplementedException(); + => MetaIndex.HumanCmp; public bool Validate() - => throw new NotImplementedException(); + => SubRace is not SubRace.Unknown + && Enum.IsDefined(SubRace) + && Attribute is not RspAttribute.NumAttributes + && Enum.IsDefined(Attribute); public JObject AddToJson(JObject jObj) - => throw new NotImplementedException(); + { + jObj["SubRace"] = SubRace.ToString(); + jObj["Attribute"] = Attribute.ToString(); + return jObj; + } + + public static RspIdentifier? FromJson(JObject? jObj) + { + if (jObj == null) + return null; + + var subRace = jObj["SubRace"]?.ToObject() ?? SubRace.Unknown; + var attribute = jObj["Attribute"]?.ToObject() ?? RspAttribute.NumAttributes; + var ret = new RspIdentifier(subRace, attribute); + return ret.Validate() ? ret : null; + } + + public MetaManipulationType Type + => MetaManipulationType.Rsp; } [JsonConverter(typeof(Converter))] @@ -28,6 +50,9 @@ public readonly record struct RspEntry(float Value) : IComparisonOperators Value is >= MinValue and <= MaxValue; + private class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, RspEntry value, JsonSerializer serializer) diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs deleted file mode 100644 index 04691c9f0..000000000 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct RspManipulation : IMetaManipulation -{ - public RspIdentifier Identifier { get; private init; } - public RspEntry Entry { get; private init; } - - [JsonConverter(typeof(StringEnumConverter))] - public SubRace SubRace - => Identifier.SubRace; - - [JsonConverter(typeof(StringEnumConverter))] - public RspAttribute Attribute - => Identifier.Attribute; - - [JsonConstructor] - public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) - { - Entry = entry; - Identifier = new RspIdentifier(subRace, attribute); - } - - public RspManipulation Copy(RspEntry entry) - => new(SubRace, Attribute, entry); - - public override string ToString() - => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; - - public bool Equals(RspManipulation other) - => SubRace == other.SubRace - && Attribute == other.Attribute; - - public override bool Equals(object? obj) - => obj is RspManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)SubRace, (int)Attribute); - - public int CompareTo(RspManipulation other) - { - var s = SubRace.CompareTo(other.SubRace); - return s != 0 ? s : Attribute.CompareTo(other.Attribute); - } - - public MetaIndex FileIndex() - => MetaIndex.HumanCmp; - - public bool Apply(CmpFile file) - { - var value = file[SubRace, Attribute]; - if (value == Entry) - return false; - - file[SubRace, Attribute] = Entry; - return true; - } - - public bool Validate() - { - if (SubRace is SubRace.Unknown || !Enum.IsDefined(SubRace)) - return false; - if (!Enum.IsDefined(Attribute)) - return false; - if (Entry.Value is < RspEntry.MinValue or > RspEntry.MaxValue) - return false; - - return true; - } -} diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 40fceb07b..3755afa2f 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,14 +1,11 @@ using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Memory; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Mods; using Penumbra.Mods.Groups; @@ -17,7 +14,7 @@ namespace Penumbra.Meta; -public unsafe class MetaFileManager +public class MetaFileManager : IService { internal readonly Configuration Config; internal readonly CharacterUtility CharacterUtility; @@ -28,6 +25,9 @@ public unsafe class MetaFileManager internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; internal readonly ImcChecker ImcChecker; + internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); + internal readonly IFileAllocator XivAllocator; + public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, @@ -42,6 +42,7 @@ public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManage Identifier = identifier; Compactor = compactor; ImcChecker = new ImcChecker(this); + XivAllocator = new XivFileAllocator(interop); interop.InitializeFromAttributes(this); } @@ -76,57 +77,11 @@ public void WriteAllTexToolsMeta(Mod mod) } } - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public void SetFile(MetaBaseFile? file, MetaIndex metaIndex) - { - if (file == null || !Config.EnableMods) - CharacterUtility.ResetResource(metaIndex); - else - CharacterUtility.SetResource(metaIndex, (nint)file.Data, file.Length); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) - => Config.EnableMods - ? file == null - ? CharacterUtility.TemporarilyResetResource(metaIndex) - : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length) - : MetaList.MetaReverter.Disabled; - public void ApplyDefaultFiles(ModCollection? collection) { if (ActiveCollections.Default != collection || !CharacterUtility.Ready || !Config.EnableMods) return; ResidentResources.Reload(); - if (collection._cache == null) - CharacterUtility.ResetAll(); - else - collection._cache.Meta.SetFiles(); } - - /// - /// Allocate in the games space for file storage. - /// We only need this if using any meta file. - /// - [Signature(Sigs.GetFileSpace)] - private readonly nint _getFileSpaceAddress = nint.Zero; - - public IMemorySpace* GetFileSpace() - => ((delegate* unmanaged)_getFileSpaceAddress)(); - - public void* AllocateFileMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateFileMemory(int length, int alignment = 0) - => AllocateFileMemory((ulong)length, (ulong)alignment); - - public void* AllocateDefaultMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateDefaultMemory(int length, int alignment = 0) - => IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment); - - public void Free(nint ptr, int length) - => IMemorySpace.Free((void*)ptr, (ulong)length); } diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 47aa18dca..bcecf2646 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -7,7 +8,7 @@ namespace Penumbra.Mods.Editor; -public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) +public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) : IService { private readonly SHA256 _hasher = SHA256.Create(); private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = []; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index d4c881e97..3da38829c 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -8,9 +8,9 @@ namespace Penumbra.Mods.Editor; public record struct AppliedModData( Dictionary FileRedirections, - HashSet Manipulations) + MetaDictionary Manipulations) { - public static readonly AppliedModData Empty = new([], []); + public static readonly AppliedModData Empty = new([], new MetaDictionary()); } public interface IMod diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 738e606e1..2a23ffadb 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -1,11 +1,12 @@ using OtterGui; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; namespace Penumbra.Mods.Editor; -public partial class MdlMaterialEditor(ModFileCollection files) +public partial class MdlMaterialEditor(ModFileCollection files) : IService { [GeneratedRegex(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] private static partial Regex MaterialRegex(); diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 37524da1e..cacb7f88e 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.SubMods; @@ -14,7 +14,7 @@ public class ModEditor( ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor, FileCompactor compactor) - : IDisposable + : IDisposable, IService { public readonly ModNormalizer ModNormalizer = modNormalizer; public readonly ModMetaEditor MetaEditor = metaEditor; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 551d04cf4..241f5b3ba 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,10 +1,11 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModFileCollection : IDisposable +public class ModFileCollection : IDisposable, IService { private readonly List _available = []; private readonly List _mtrl = []; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index e2c0b7264..55e0e94e5 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.Services; @@ -5,7 +6,7 @@ namespace Penumbra.Mods.Editor; -public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) +public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) : IService { public bool Changes { get; private set; } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index f5fc9cd7e..9d31664ba 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -2,6 +2,7 @@ using Dalamud.Utility; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Manager; @@ -13,7 +14,7 @@ namespace Penumbra.Mods.Editor; -public class ModMerger : IDisposable +public class ModMerger : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -158,16 +159,13 @@ private void MergeIntoOption(IEnumerable mergeOptions, IModDa { var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var manips = option.Manipulations.ToHashSet(); + var manips = option.Manipulations.Clone(); foreach (var originalOption in mergeOptions) { - foreach (var manip in originalOption.Manipulations) - { - if (!manips.Add(manip)) - throw new Exception( - $"Could not add meta manipulation {manip} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); - } + if (!manips.MergeForced(originalOption.Manipulations, out var failed)) + throw new Exception( + $"Could not add meta manipulation {failed} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); foreach (var (swapA, swapB) in originalOption.FileSwaps) { diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 868537551..bacf4122c 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,24 +1,24 @@ using System.Collections.Frozen; +using OtterGui.Services; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; -public class ModMetaEditor(ModManager modManager) +public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService { - private readonly HashSet _imc = []; - private readonly HashSet _eqp = []; - private readonly HashSet _eqdp = []; - private readonly HashSet _gmp = []; - private readonly HashSet _est = []; - private readonly HashSet _rsp = []; - private readonly HashSet _globalEqp = []; - public sealed class OtherOptionData : HashSet { public int TotalCount; + public void Add(string name, int count) + { + if (count > 0) + Add(name); + TotalCount += count; + } + public new void Clear() { TotalCount = 0; @@ -26,102 +26,20 @@ public sealed class OtherOptionData : HashSet } } - public readonly FrozenDictionary OtherData = - Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); - - public bool Changes { get; private set; } - - public IReadOnlySet Imc - => _imc; - - public IReadOnlySet Eqp - => _eqp; - - public IReadOnlySet Eqdp - => _eqdp; - - public IReadOnlySet Gmp - => _gmp; - - public IReadOnlySet Est - => _est; - - public IReadOnlySet Rsp - => _rsp; - - public IReadOnlySet GlobalEqp - => _globalEqp; - - public bool CanAdd(MetaManipulation m) - { - return m.ManipulationType switch - { - MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), - MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), - MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), - MetaManipulation.Type.Est => !_est.Contains(m.Est), - MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), - MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), - MetaManipulation.Type.GlobalEqp => !_globalEqp.Contains(m.GlobalEqp), - _ => false, - }; - } - - public bool Add(MetaManipulation m) - { - var added = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Add(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), - MetaManipulation.Type.Est => _est.Add(m.Est), - MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), - MetaManipulation.Type.GlobalEqp => _globalEqp.Add(m.GlobalEqp), - _ => false, - }; - Changes |= added; - return added; - } - - public bool Delete(MetaManipulation m) - { - var deleted = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Remove(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), - MetaManipulation.Type.Est => _est.Remove(m.Est), - MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), - MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(m.GlobalEqp), - _ => false, - }; - Changes |= deleted; - return deleted; - } - - public bool Change(MetaManipulation m) - => Delete(m) && Add(m); + public readonly FrozenDictionary OtherData = + Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); - public bool Set(MetaManipulation m) - => Delete(m) | Add(m); + public bool Changes { get; set; } - public void Clear() + public new void Clear() { - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _gmp.Clear(); - _est.Clear(); - _rsp.Clear(); - _globalEqp.Clear(); - Changes = true; + Changes = Count > 0; + base.Clear(); } public void Load(Mod mod, IModDataContainer currentOption) { - foreach (var type in Enum.GetValues()) + foreach (var type in Enum.GetValues()) OtherData[type].Clear(); foreach (var option in mod.AllDataContainers) @@ -129,15 +47,19 @@ public void Load(Mod mod, IModDataContainer currentOption) if (option == currentOption) continue; - foreach (var manip in option.Manipulations) - { - var data = OtherData[manip.ManipulationType]; - ++data.TotalCount; - data.Add(option.GetFullName()); - } + var name = option.GetFullName(); + OtherData[MetaManipulationType.Imc].Add(name, option.Manipulations.GetCount(MetaManipulationType.Imc)); + OtherData[MetaManipulationType.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqp)); + OtherData[MetaManipulationType.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqdp)); + OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp)); + OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); + OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); + OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } - Split(currentOption.Manipulations); + Clear(); + UnionWith(currentOption.Manipulations); + Changes = false; } public void Apply(IModDataContainer container) @@ -145,50 +67,7 @@ public void Apply(IModDataContainer container) if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet()); + modManager.OptionEditor.SetManipulations(container, this); Changes = false; } - - private void Split(IEnumerable manips) - { - Clear(); - foreach (var manip in manips) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - _imc.Add(manip.Imc); - break; - case MetaManipulation.Type.Eqdp: - _eqdp.Add(manip.Eqdp); - break; - case MetaManipulation.Type.Eqp: - _eqp.Add(manip.Eqp); - break; - case MetaManipulation.Type.Est: - _est.Add(manip.Est); - break; - case MetaManipulation.Type.Gmp: - _gmp.Add(manip.Gmp); - break; - case MetaManipulation.Type.Rsp: - _rsp.Add(manip.Rsp); - break; - case MetaManipulation.Type.GlobalEqp: - _globalEqp.Add(manip.GlobalEqp); - break; - } - } - - Changes = false; - } - - public IEnumerable Recombine() - => _imc.Select(m => (MetaManipulation)m) - .Concat(_eqdp.Select(m => (MetaManipulation)m)) - .Concat(_eqp.Select(m => (MetaManipulation)m)) - .Concat(_est.Select(m => (MetaManipulation)m)) - .Concat(_gmp.Select(m => (MetaManipulation)m)) - .Concat(_rsp.Select(m => (MetaManipulation)m)) - .Concat(_globalEqp.Select(m => (MetaManipulation)m)); } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 58e4fc086..c6bc49392 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,15 +1,15 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using OtterGui.Tasks; -using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModNormalizer(ModManager _modManager, Configuration _config) +public class ModNormalizer(ModManager _modManager, Configuration _config) : IService { private readonly List>> _redirections = []; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0250efaed..1a8ff2eb1 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,10 +1,10 @@ -using Penumbra.Mods; +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; using Penumbra.Util; -public class ModSwapEditor(ModManager modManager) +public class ModSwapEditor(ModManager modManager) : IService { private readonly Dictionary _swaps = []; diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index fcc8c0938..00f47e257 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -43,7 +43,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index b336203dd..c52828c03 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -95,28 +95,28 @@ public int GetIndex() public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcManipulation GetManip(ushort mask, Variant variant) - => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, - Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); + public ImcEntry GetEntry(ushort mask) + => DefaultEntry with { AttributeMask = mask }; - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; - var mask = GetCurrentMask(setting); + var mask = GetCurrentMask(setting); + var entry = GetEntry(mask); if (AllVariants) { var count = ImcChecker.GetVariantCount(Identifier); if (count == 0) - manipulations.Add(GetManip(mask, Identifier.Variant)); + manipulations.TryAdd(Identifier, entry); else for (var i = 0; i <= count; ++i) - manipulations.Add(GetManip(mask, (Variant)i)); + manipulations.TryAdd(Identifier with { Variant = (Variant)i }, entry); } else { - manipulations.Add(GetManip(mask, Identifier.Variant)); + manipulations.TryAdd(Identifier, entry); } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7816d6288..220d0a7c2 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -116,7 +116,7 @@ public int GetIndex() public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new MultiModGroupEditDrawer(editDrawer, this); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) { diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index a6ebd8466..a559d6094 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -101,7 +101,7 @@ public int GetIndex() public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new SingleModGroupEditDrawer(editDrawer, this); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (OptionData.Count == 0) return; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 3efee857f..b7827c478 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -1,4 +1,4 @@ -using Penumbra.Api.Enums; +using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -16,32 +16,19 @@ public static class EquipmentSwap private static EquipSlot[] ConvertSlots(EquipSlot slot, bool rFinger, bool lFinger) { if (slot != EquipSlot.RFinger) - return new[] - { - slot, - }; + return [slot]; return rFinger ? lFinger - ? new[] - { - EquipSlot.RFinger, - EquipSlot.LFinger, - } - : new[] - { - EquipSlot.RFinger, - } + ? [EquipSlot.RFinger, EquipSlot.LFinger] + : [EquipSlot.RFinger] : lFinger - ? new[] - { - EquipSlot.LFinger, - } - : Array.Empty(); + ? [EquipSlot.LFinger] + : []; } public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, - Func redirections, Func manips, + Func redirections, MetaDictionary manips, EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); @@ -50,11 +37,14 @@ public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentifi throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slotTo, variantTo.Id, idTo.Id, default); - var imcFileTo = new ImcFile(manager, imcManip.Identifier); + var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) + ? entry + : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); + var mtrlVariantTo = imcEntry.MaterialId; var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo.Id))).Imc.Entry.MaterialId; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -99,7 +89,7 @@ public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentifi } public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, - Func redirections, Func manips, EquipItem itemFrom, + Func redirections, MetaDictionary manips, EquipItem itemFrom, EquipItem itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. @@ -120,8 +110,12 @@ public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentifi foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slot, variantTo.Id, idTo, default); - var imcFileTo = new ImcFile(manager, imcManip.Identifier); + var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) + ? entry + : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); + var mtrlVariantTo = imcEntry.MaterialId; var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -131,9 +125,8 @@ public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentifi _ => (EstType)0, }; - var skipFemale = false; - var skipMale = false; - var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slot), variantTo))).Imc.Entry.MaterialId; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -154,7 +147,7 @@ public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentifi if (eqdp != null) swaps.Add(eqdp); - var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits(slot).Item2 ?? false; + var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); @@ -184,22 +177,22 @@ public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentifi return affectedItems; } - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, - PrimaryId idTo, byte mtrlTo) + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) { - var (gender, race) = gr.Split(); - var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom), slotFrom, gender, - race, idFrom); - var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo), slotTo, gender, race, - idTo); - var meta = new MetaSwap(manips, eqdpFrom, eqdpTo); - var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits(slotFrom); + var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpToIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, + eqdpFromDefault, eqdpToIdentifier, + eqdpToDefault); + var (ownMtrl, ownMdl) = meta.SwapToModdedEntry; if (ownMdl) { var mdl = CreateMdl(manager, redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo); @@ -249,8 +242,8 @@ private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId mo private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); - var imc = new ImcFile(manager, entry.Identifier); + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); EquipItem[] items; Variant[] variants; if (idFrom == idTo) @@ -270,38 +263,41 @@ private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager man return (imc, variants, items); } - public static MetaSwap? CreateGmp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, - PrimaryId idTo) + public static MetaSwap? CreateGmp(MetaFileManager manager, MetaDictionary manips, + EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) { if (slot is not EquipSlot.Head) return null; - var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom), idFrom); - var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo), idTo); - return new MetaSwap(manips, manipFrom, manipTo); + var manipFromIdentifier = new GmpIdentifier(idFrom); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); } - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slot, - PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slot, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, + ImcFile imcFileFrom, ImcFile imcFileTo) => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo); - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, - Func manips, - EquipSlot slotFrom, EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slotFrom, EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { - var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var entryTo = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); - var manipulationFrom = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, entryFrom); - var manipulationTo = new ImcManipulation(slotTo, variantTo.Id, idTo, entryTo); - var imc = new MetaSwap(manips, manipulationFrom, manipulationTo); - - var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId); + var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); + + var decal = CreateDecal(manager, redirections, imc.SwapToModdedEntry.DecalId); if (decal != null) imc.ChildSwaps.Add(decal); - var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId); + var avfx = CreateAvfx(manager, redirections, slotFrom, slotTo, idFrom, idTo, imc.SwapToModdedEntry.VfxId); if (avfx != null) imc.ChildSwaps.Add(avfx); @@ -322,35 +318,39 @@ public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, byte vfxId) + public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + PrimaryId idFrom, PrimaryId idTo, + byte vfxId) { if (vfxId == 0) return null; var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); - var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); + var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { - var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged); + var atex = CreateAtex(manager, redirections, slotFrom, slotTo, idFrom, ref filePath, ref avfx.DataWasChanged); avfx.ChildSwaps.Add(atex); } return avfx; } - public static MetaSwap? CreateEqp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, - PrimaryId idTo) + public static MetaSwap? CreateEqp(MetaFileManager manager, MetaDictionary manips, + EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) { if (slot.IsAccessory()) return null; - var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom); - var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo); - var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom); - var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom); - return new MetaSwap(manips, eqpFrom, eqpTo); + var manipFromIdentifier = new EqpIdentifier(idFrom, slot); + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, + manipFromDefault, manipToIdentifier, manipToDefault); } public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, PrimaryId idFrom, @@ -397,8 +397,8 @@ public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, PrimaryId idTo, - ref MtrlFile.Texture texture, ref bool dataWasChanged) + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, + PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, @@ -407,6 +407,7 @@ public static FileSwap CreateTex(MetaFileManager manager, Func redirections, ref string filePath, - ref bool dataWasChanged) + public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 7fac52c14..1f4c5e7a3 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -136,35 +136,35 @@ public static bool LoadAvfx(MetaFileManager manager, FullPath path, [NotNullWhen public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry.AsId); + var phybPath = GamePaths.Skeleton.Phyb.Path(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry.AsId); + var sklbPath = GamePaths.Skeleton.Sklb.Path(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } - /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. - public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, - Func manips, EstType type, - GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) + public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, + MetaDictionary manips, EstType type, GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) { if (type == 0) return null; - var (gender, race) = genderRace.Split(); - var fromDefault = new EstManipulation(gender, race, type, idFrom, EstFile.GetDefault(manager, type, genderRace, idFrom)); - var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo)); - var est = new MetaSwap(manips, fromDefault, toDefault); + var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace); + var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); + var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); + var est = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); - if (ownMdl && est.SwapApplied.Est.Entry.Value >= 2) + if (ownMdl && est.SwapToModdedEntry.Value >= 2) { - var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapToModdedEntry); est.ChildSwaps.Add(phyb); - var sklb = CreateSklb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + var sklb = CreateSklb(manager, redirections, type, genderRace, est.SwapToModdedEntry); est.ChildSwaps.Add(sklb); } else if (est.SwapAppliedIsDefault) @@ -216,6 +216,22 @@ public static string ReplaceSlot(string path, EquipSlot from, EquipSlot to, bool ? path.Replace($"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_") : path; + public static string ReplaceType(string path, EquipSlot from, EquipSlot to, PrimaryId idFrom) + { + var isAccessoryFrom = from.IsAccessory(); + if (isAccessoryFrom == to.IsAccessory()) + return path; + + if (isAccessoryFrom) + { + path = path.Replace("accessory/a", "equipment/e"); + return path.Replace($"a{idFrom.Id:D4}", $"e{idFrom.Id:D4}"); + } + + path = path.Replace("equipment/e", "accessory/a"); + return path.Replace($"e{idFrom.Id:D4}", $"a{idFrom.Id:D4}"); + } + public static string ReplaceRace(string path, GenderRace from, GenderRace to, bool condition = true) => ReplaceId(path, 'c', (ushort)from, (ushort)to, condition); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index af5b2d3a7..d2deb9ef1 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -23,7 +23,7 @@ public class ItemSwapContainer public IReadOnlyDictionary ModRedirections => _appliedModData.FileRedirections; - public IReadOnlySet ModManipulations + public MetaDictionary ModManipulations => _appliedModData.Manipulations; public readonly List Swaps = []; @@ -42,9 +42,10 @@ public enum WriteType NoSwaps, } - public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null) + public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, + DirectoryInfo? directory = null) { - var convertedManips = new HashSet(Swaps.Count); + var convertedManips = new MetaDictionary(); var convertedFiles = new Dictionary(Swaps.Count); var convertedSwaps = new Dictionary(Swaps.Count); directory ??= mod.ModPath; @@ -52,32 +53,38 @@ public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, W { foreach (var swap in Swaps.SelectMany(s => s.WithChildren())) { - switch (swap) + if (swap is FileSwap file) { - case FileSwap file: - // Skip, nothing to do - if (file.SwapToModdedEqualsOriginal) - continue; - - if (writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged) - { - convertedSwaps.TryAdd(file.SwapFromRequestPath, file.SwapToModded); - } - else - { - var path = file.GetNewPath(directory.FullName); - var bytes = file.FileData.Write(); - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - _manager.Compactor.WriteAllBytes(path, bytes); - convertedFiles.TryAdd(file.SwapFromRequestPath, new FullPath(path)); - } - - break; - case MetaSwap meta: - if (!meta.SwapAppliedIsDefault) - convertedManips.Add(meta.SwapApplied); - - break; + // Skip, nothing to do + if (file.SwapToModdedEqualsOriginal) + continue; + + if (writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged) + { + convertedSwaps.TryAdd(file.SwapFromRequestPath, file.SwapToModded); + } + else + { + var path = file.GetNewPath(directory.FullName); + var bytes = file.FileData.Write(); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _manager.Compactor.WriteAllBytes(path, bytes); + convertedFiles.TryAdd(file.SwapFromRequestPath, new FullPath(path)); + } + } + else if (swap is IMetaSwap { SwapAppliedIsDefault: false }) + { + // @formatter:off + _ = swap switch + { + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwapmeta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwapmeta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + _ => false, + }; + // @formatter:on } } @@ -98,13 +105,9 @@ public void LoadMod(Mod? mod, ModSettings? settings) { Clear(); if (mod == null || mod.Index < 0) - { - _appliedModData = AppliedModData.Empty; - } + _appliedModData = AppliedModData.Empty; else - { _appliedModData = ModSettings.GetResolveData(mod, settings); - } } public ItemSwapContainer(MetaFileManager manager, ObjectIdentification identifier) @@ -119,11 +122,10 @@ private Func PathResolver(ModCollection? collection) ? p => collection.ResolvePath(p) ?? new FullPath(p) : p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p); - private Func MetaResolver(ModCollection? collection) - { - var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _appliedModData.Manipulations; - return m => set.TryGetValue(m, out var a) ? a : m; - } + private MetaDictionary MetaResolver(ModCollection? collection) + => collection?.MetaCache is { } cache + ? new MetaDictionary(cache) + : _appliedModData.Manipulations; public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true) @@ -158,8 +160,8 @@ public bool LoadCustomization(MetaFileManager manager, BodySlot slot, GenderRace _ => (EstType)0, }; - var metaResolver = MetaResolver(collection); - var est = ItemSwap.CreateEst(manager, pathResolver, metaResolver, type, race, from, to, true); + var estResolver = MetaResolver(collection); + var est = ItemSwap.CreateEst(manager, pathResolver, estResolver, type, race, from, to, true); Swaps.Add(mdl); if (est != null) diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 27935ffb8..36c54203a 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,58 +1,91 @@ -using Penumbra.Api.Enums; +using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Meta; using static Penumbra.Mods.ItemSwap.ItemSwap; -using Penumbra.Services; namespace Penumbra.Mods.ItemSwap; public class Swap { /// Any further swaps belonging specifically to this tree of changes. - public readonly List ChildSwaps = new(); + public readonly List ChildSwaps = []; public IEnumerable WithChildren() => ChildSwaps.SelectMany(c => c.WithChildren()).Prepend(this); } -public sealed class MetaSwap : Swap +public interface IMetaSwap { + public IMetaIdentifier SwapFromIdentifier { get; } + public IMetaIdentifier SwapToIdentifier { get; } + + public object SwapFromDefaultEntry { get; } + public object SwapToDefaultEntry { get; } + public object SwapToModdedEntry { get; } + + public bool SwapToIsDefault { get; } + public bool SwapAppliedIsDefault { get; } +} + +public sealed class MetaSwap : Swap, IMetaSwap + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged, IEquatable +{ + public TIdentifier SwapFromIdentifier; + public TIdentifier SwapToIdentifier; + /// The default value of a specific meta manipulation that needs to be redirected. - public MetaManipulation SwapFrom; + public TEntry SwapFromDefaultEntry; /// The default value of the same Meta entry of the redirected item. - public MetaManipulation SwapToDefault; + public TEntry SwapToDefaultEntry; /// The modded value of the same Meta entry of the redirected item, or the same as SwapToDefault if unmodded. - public MetaManipulation SwapToModded; - - /// The modded value applied to the specific meta manipulation target before redirection. - public MetaManipulation SwapApplied; + public TEntry SwapToModdedEntry; - /// Whether SwapToModded equals SwapToDefault. - public bool SwapToIsDefault; + /// Whether SwapToModdedEntry equals SwapToDefaultEntry. + public bool SwapToIsDefault { get; } /// Whether the applied meta manipulation does not change anything against the default. - public bool SwapAppliedIsDefault; + public bool SwapAppliedIsDefault { get; } /// /// Create a new MetaSwap from the original meta identifier and the target meta identifier. /// - /// A function that converts the given manipulation to the modded one. - /// The original meta identifier with its default value. - /// The target meta identifier with its default value. - public MetaSwap(Func manipulations, MetaManipulation manipFrom, MetaManipulation manipTo) + /// A function that obtains a modded meta entry if it exists. + /// The original meta identifier. + /// The default value for the original meta identifier. + /// The target meta identifier. + /// The default value for the target meta identifier. + public MetaSwap(Func manipulations, TIdentifier manipFromIdentifier, TEntry manipFromEntry, + TIdentifier manipToIdentifier, TEntry manipToEntry) { - SwapFrom = manipFrom; - SwapToDefault = manipTo; - - SwapToModded = manipulations(manipTo); - SwapToIsDefault = manipTo.EntryEquals(SwapToModded); - SwapApplied = SwapFrom.WithEntryOf(SwapToModded); - SwapAppliedIsDefault = SwapApplied.EntryEquals(SwapFrom); + SwapFromIdentifier = manipFromIdentifier; + SwapToIdentifier = manipToIdentifier; + SwapFromDefaultEntry = manipFromEntry; + SwapToDefaultEntry = manipToEntry; + + SwapToModdedEntry = manipulations(SwapToIdentifier) ?? SwapToDefaultEntry; + SwapToIsDefault = SwapToModdedEntry.Equals(SwapToDefaultEntry); + SwapAppliedIsDefault = SwapToModdedEntry.Equals(SwapFromDefaultEntry); } + + IMetaIdentifier IMetaSwap.SwapFromIdentifier + => SwapFromIdentifier; + + IMetaIdentifier IMetaSwap.SwapToIdentifier + => SwapToIdentifier; + + object IMetaSwap.SwapFromDefaultEntry + => SwapFromDefaultEntry; + + object IMetaSwap.SwapToDefaultEntry + => SwapToDefaultEntry; + + object IMetaSwap.SwapToModdedEntry + => SwapToModdedEntry; } public sealed class FileSwap : Swap @@ -113,8 +146,7 @@ public string GetNewPath(string newMod) /// A full swap container with the actual file in memory. /// True if everything could be read correctly, false otherwise. public static FileSwap CreateSwap(MetaFileManager manager, ResourceType type, Func redirections, - string swapFromRequest, string swapToRequest, - string? swapFromPreChange = null) + string swapFromRequest, string swapToRequest, string? swapFromPreChange = null) { var swap = new FileSwap { diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 8ab8cf337..38d98d7c5 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.Mods.Groups; @@ -8,7 +9,7 @@ namespace Penumbra.Mods.Manager; -public class ModCacheManager : IDisposable +public class ModCacheManager : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index c7c7c2ccc..4ab9deb13 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -1,6 +1,7 @@ using Dalamud.Utility; using Newtonsoft.Json.Linq; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -23,7 +24,7 @@ public enum ModDataChangeType : ushort Note = 0x0800, } -public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) +public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService { /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, @@ -49,7 +50,6 @@ public ModDataChangeType LoadLocalData(Mod mod) var save = true; if (File.Exists(dataFile)) - { try { var text = File.ReadAllText(dataFile); @@ -65,7 +65,6 @@ public ModDataChangeType LoadLocalData(Mod mod) { Penumbra.Log.Error($"Could not load local mod data:\n{e}"); } - } if (importDate == 0) importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index 676018be9..38b9c0fd9 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -1,10 +1,11 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; namespace Penumbra.Mods.Manager; -public class ModExportManager : IDisposable +public class ModExportManager : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index c8a0a5dbc..e32fec0cb 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,12 +1,11 @@ -using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.Mods.Manager; -public sealed class ModFileSystem : FileSystem, IDisposable, ISavable +public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, IService { private readonly ModManager _modManager; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index c99b7d0e3..ff39b0211 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,11 +1,12 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Import; using Penumbra.Mods.Editor; namespace Penumbra.Mods.Manager; -public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable +public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable, IService { private readonly ConcurrentQueue _modsToUnpack = new(); @@ -32,7 +33,8 @@ public void TryUnpacking() if (File.Exists(s)) return true; - Penumbra.Messager.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", NotificationType.Warning, false); + Penumbra.Messager.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", NotificationType.Warning, + false); return false; }).Select(s => new FileInfo(s)).ToArray(); diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 62b548656..4b19ea4c2 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager.OptionEditor; @@ -27,7 +28,7 @@ public enum ModPathChangeType StartingReload, } -public sealed class ModManager : ModStorage, IDisposable +public sealed class ModManager : ModStorage, IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -261,7 +262,7 @@ public void Dispose() /// /// Set the mod base directory. - /// If its not the first time, check if it is the same directory as before. + /// If it's not the first time, check if it is the same directory as before. /// Also checks if the directory is available and tries to create it if it is not. /// private void SetBaseDirectory(string newPath, bool firstTime, out string resultNewDir) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 55e010154..594ec9d2a 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -142,10 +142,9 @@ public void ChangeOptionDescription(IModOption option, string newDescription) } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void SetManipulations(IModDataContainer subMod, HashSet manipulations, SaveType saveType = SaveType.Queue) + public void SetManipulations(IModDataContainer subMod, MetaDictionary manipulations, SaveType saveType = SaveType.Queue) { - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + if (subMod.Manipulations.Equals(manipulations)) return; communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 783ef3e6f..a7f87dcdc 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -71,7 +71,7 @@ public AppliedModData GetData(ModSettings? settings = null) return AppliedModData.Empty; var dictRedirections = new Dictionary(TotalFileCount); - var setManips = new HashSet(TotalManipulations); + var setManips = new MetaDictionary(); foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) { var config = settings.Settings[groupIndex]; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index ed4245c4f..0e66367ae 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Import; @@ -23,7 +24,7 @@ public partial class ModCreator( Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, - GamePathParser _gamePathParser) + GamePathParser _gamePathParser) : IService { public readonly Configuration Config = config; diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 1a234879c..3840468f1 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -13,7 +13,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); IMod IModDataContainer.Mod => Mod; @@ -21,7 +21,7 @@ IMod IModDataContainer.Mod IModGroup? IModDataContainer.Group => null; - public void AddTo(Dictionary redirections, HashSet manipulations) + public void AddTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 7f7ef4a6e..1a89ec17d 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -10,9 +10,9 @@ public interface IModDataContainer public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public MetaDictionary Manipulations { get; set; } public string GetName(); public string GetFullName(); diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 02d86af26..8fac52d87 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -33,9 +33,9 @@ IModGroup IModOption.Group public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); - public void AddDataTo(Dictionary redirections, HashSet manipulations) + public void AddDataTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index b984b5701..a8c37369f 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -21,7 +21,7 @@ public static int GetIndex(IModOption option) /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void AddContainerTo(IModDataContainer container, Dictionary redirections, - HashSet manipulations) + MetaDictionary manipulations) { foreach (var (path, file) in container.Files) redirections.TryAdd(path, file); @@ -37,7 +37,7 @@ public static void Clone(IModDataContainer from, IModDataContainer to) { to.Files = new Dictionary(from.Files); to.FileSwaps = new Dictionary(from.FileSwaps); - to.Manipulations = [.. from.Manipulations]; + to.Manipulations = from.Manipulations.Clone(); } /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. @@ -64,11 +64,9 @@ public static void LoadDataContainer(JToken json, IModDataContainer data, Direct data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } - var manips = json[nameof(data.Manipulations)]; + var manips = json[nameof(data.Manipulations)]?.ToObject(); if (manips != null) - foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.Validate())) - data.Manipulations.Add(s); + data.Manipulations.UnionWith(manips); } /// Load the relevant data for a selectable option from a JToken of that option. diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6e6e72ab9..e1cf9f2b0 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -51,13 +51,7 @@ public IReadOnlyList Groups public TemporaryMod() => Default = new DefaultSubMod(this); - public void SetFile(Utf8GamePath gamePath, FullPath fullPath) - => Default.Files[gamePath] = fullPath; - - public bool SetManipulation(MetaManipulation manip) - => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); - - public void SetAll(Dictionary dict, HashSet manips) + public void SetAll(Dictionary dict, MetaDictionary manips) { Default.Files = dict; Default.Manipulations = manips; @@ -99,8 +93,8 @@ public static void SaveTempCollection(Configuration config, SaveService saveServ } } - foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) - defaultMod.Manipulations.Add(manip); + var manips = new MetaDictionary(collection.MetaCache); + defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3bbfdf650..905b998d9 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -144,7 +144,6 @@ public bool SetEnabled(bool enabled) { if (_characterUtility.Ready) { - _collectionManager.Active.Default.SetFiles(_characterUtility); _residentResources.Reload(); _redrawService.RedrawAll(RedrawType.Redraw); } @@ -153,7 +152,6 @@ public bool SetEnabled(bool enabled) { if (_characterUtility.Ready) { - _characterUtility.ResetAll(); _residentResources.Reload(); _redrawService.RedrawAll(RedrawType.Redraw); } diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 0c6648ba6..3279da961 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -5,36 +5,15 @@ using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using OtterGui; -using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; -using Penumbra.Api; using Penumbra.Api.Api; -using Penumbra.Collections.Cache; -using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; -using Penumbra.Import.Models; using Penumbra.GameData.Structs; -using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; -using Penumbra.Interop.ResourceTree; -using Penumbra.Interop.Services; using Penumbra.Meta; -using Penumbra.Mods; -using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.UI; -using Penumbra.UI.AdvancedWindow; -using Penumbra.UI.Classes; -using Penumbra.UI.ModsTab; -using Penumbra.UI.ResourceWatcher; -using Penumbra.UI.Tabs; -using Penumbra.UI.Tabs.Debug; using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; -using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; -using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; -using Penumbra.Api.IpcTester; namespace Penumbra.Services; @@ -45,19 +24,18 @@ public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInte var services = new ServiceManager(log) .AddDalamudServices(pi) .AddExistingService(log) - .AddExistingService(penumbra) - .AddInterop() - .AddConfiguration() - .AddCollections() - .AddMods() - .AddResources() - .AddResolvers() - .AddInterface() - .AddModEditor() - .AddApi(); + .AddExistingService(penumbra); services.AddIServices(typeof(EquipItem).Assembly); services.AddIServices(typeof(Penumbra).Assembly); services.AddIServices(typeof(ImGuiUtil).Assembly); + services.AddSingleton(p => + { + var cutsceneService = p.GetRequiredService(); + return new CutsceneResolver(cutsceneService.GetParentIndex); + }) + .AddSingleton(p => p.GetRequiredService().ImcChecker) + .AddSingleton(s => (ModStorage)s.GetRequiredService()) + .AddSingleton(x => x.GetRequiredService()); services.CreateProvider(); return services; } @@ -83,119 +61,4 @@ private static ServiceManager AddDalamudServices(this ServiceManager services, D .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi); - - private static ServiceManager AddInterop(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(p => - { - var cutsceneService = p.GetRequiredService(); - return new CutsceneResolver(cutsceneService.GetParentIndex); - }) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(p => p.GetRequiredService().ImcChecker); - - private static ServiceManager AddConfiguration(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddCollections(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddMods(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(s => (ModStorage)s.GetRequiredService()); - - private static ServiceManager AddResources(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddResolvers(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddInterface(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(p => new Diagnostics(p)); - - private static ServiceManager AddModEditor(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddApi(this ServiceManager services) - => services.AddSingleton(x => x.GetRequiredService()); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6010cdafe..652f928db 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -3,6 +3,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -24,7 +25,7 @@ namespace Penumbra.UI.AdvancedWindow; -public class ItemSwapTab : IDisposable, ITab +public class ItemSwapTab : IDisposable, ITab, IUiService { private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -240,7 +241,7 @@ private static string SwapToString(Swap swap) { return swap switch { - MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", + IMetaSwap meta => $"{meta.SwapFromIdentifier}: {meta.SwapFromDefaultEntry} -> {meta.SwapToModdedEntry}", FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}", _ => string.Empty, @@ -410,7 +411,7 @@ private void DrawSwapBar() private ImRaii.IEndObject DrawTab(SwapType newTab) { - using var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString()); + var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString()); if (tab) { _dirty |= _lastTab != newTab; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs new file mode 100644 index 000000000..970b70cb3 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -0,0 +1,159 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Racial Model Edits (EQDP)###EQDP"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new EqdpIdentifier(1, EquipSlot.Head, GenderRace.MidlanderMale); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, Identifier), Identifier.Slot); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqdp)); + + ImGui.TableNextColumn(); + var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0; + var canAdd = validRaceCode && !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : + validRaceCode ? "This entry is already edited."u8 : "This combination of race and gender can not be used."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, identifier), identifier.Slot); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate() + => Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EqdpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawEquipSlot(ref identifier); + return changes; + } + + private static void DrawIdentifier(EqdpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Gender"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(EqdpEntryInternal defaultEntry, ref EqdpEntryInternal entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + if (Checkmark("Material##eqdp"u8, "\0"u8, entry.Material, defaultEntry.Material, out var newMaterial)) + { + entry = entry with { Material = newMaterial }; + changes = true; + } + + ImGui.SameLine(); + if (Checkmark("Model##eqdp"u8, "\0"u8, entry.Model, defaultEntry.Model, out var newModel)) + { + entry = entry with { Model = newModel }; + changes = true; + } + + return changes; + } + + public static bool DrawPrimaryId(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##eqdpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawRace(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##eqdpRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + return ret; + } + + public static bool DrawGender(ref EqdpIdentifier identifier, float unscaledWidth = 120) + { + var ret = Combos.Gender("##eqdpGender", identifier.Gender, out var gender, unscaledWidth); + ImUtf8.HoverTooltip("Gender"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) }; + return ret; + } + + public static bool DrawEquipSlot(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.EqdpEquipSlot("##eqdpSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs new file mode 100644 index 000000000..56c06bc9b --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -0,0 +1,134 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Equipment Parameter Edits (EQP)###EQP"u8; + + public override int NumColumns + => 5; + + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, Identifier.SetId), Identifier.Slot); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Identifier.Slot, Entry, ref Entry, true); + } + + protected override void DrawEntry(EqpIdentifier identifier, EqpEntryInternal entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, identifier.SetId), identifier.Slot); + if (DrawEntry(identifier.Slot, defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate() + => Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EqpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawEquipSlot(ref identifier); + return changes; + } + + private static void DrawIdentifier(EqpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(EquipSlot slot, EqpEntryInternal defaultEntry, ref EqpEntryInternal entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var offset = Eqp.OffsetAndMask(slot).Item1; + DrawBox(ref entry, 0); + for (var i = 1; i < Eqp.EqpAttributes[slot].Count; ++i) + { + ImUtf8.SameLineInner(); + DrawBox(ref entry, i); + } + + return changes; + + void DrawBox(ref EqpEntryInternal entry, int i) + { + using var id = ImUtf8.PushId(i); + var flag = 1u << i; + var eqpFlag = (EqpEntry)((ulong)flag << offset); + var defaultValue = (flag & defaultEntry.Value) != 0; + var value = (flag & entry.Value) != 0; + if (Checkmark("##eqp"u8, eqpFlag.ToLocalName(), value, defaultValue, out var newValue)) + { + entry = new EqpEntryInternal(newValue ? entry.Value | flag : entry.Value & ~flag); + changes = true; + } + } + } + + public static bool DrawPrimaryId(ref EqpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##eqpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawEquipSlot(ref EqpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.EqpEquipSlot("##eqpSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs new file mode 100644 index 000000000..5c3c5df5a --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -0,0 +1,147 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Extra Skeleton Parameters (EST)###EST"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new EstIdentifier(1, EstType.Hair, GenderRace.MidlanderMale); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = EstFile.GetDefault(MetaFiles, Identifier.Slot, Identifier.GenderRace, Identifier.SetId); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Est)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(EstIdentifier identifier, EstEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = EstFile.GetDefault(MetaFiles, identifier.Slot, identifier.GenderRace, identifier.SetId); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() + => Editor.Est.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EstIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawSlot(ref identifier); + + return changes; + } + + private static void DrawIdentifier(EstIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.HoverTooltip("Gender"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToString(), FrameColor); + ImUtf8.HoverTooltip("Extra Skeleton Type"u8); + } + + private static bool DrawEntry(EstEntry defaultEntry, ref EstEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var ret = DragInput("##estValue"u8, [], 100f * ImUtf8.GlobalScale, entry.Value, defaultEntry.Value, out var newValue, (ushort)0, + ushort.MaxValue, 0.05f, !disabled); + if (ret) + entry = new EstEntry(newValue); + return ret; + } + + public static bool DrawPrimaryId(ref EstIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##estPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawRace(ref EstIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##estRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + return ret; + } + + public static bool DrawGender(ref EstIdentifier identifier, float unscaledWidth = 120) + { + var ret = Combos.Gender("##estGender", identifier.Gender, out var gender, unscaledWidth); + ImUtf8.HoverTooltip("Gender"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) }; + return ret; + } + + public static bool DrawSlot(ref EstIdentifier identifier, float unscaledWidth = 200) + { + var ret = Combos.EstSlot("##estSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Extra Skeleton Type"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs new file mode 100644 index 000000000..130831a0f --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -0,0 +1,111 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8; + + public override int NumColumns + => 4; + + protected override void Initialize() + { + Identifier = new GlobalEqpManipulation() + { + Condition = 1, + Type = GlobalEqpType.DoNotHideEarrings, + }; + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.GlobalEqp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier); + + DrawIdentifierInput(ref Identifier); + } + + protected override void DrawEntry(GlobalEqpManipulation identifier, byte _) + { + DrawMetaButtons(identifier, 0); + DrawIdentifier(identifier); + } + + protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() + => Editor.GlobalEqp.Select(identifier => (identifier, (byte)0)); + + private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) + { + ImGui.TableNextColumn(); + DrawType(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.Type.HasCondition()) + DrawCondition(ref identifier); + else + ImUtf8.ScaledDummy(100); + } + + private static void DrawIdentifier(GlobalEqpManipulation identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor); + ImUtf8.HoverTooltip("Global EQP Type"u8); + + ImGui.TableNextColumn(); + if (identifier.Type.HasCondition()) + { + ImUtf8.TextFramed($"{identifier.Condition.Id}", FrameColor); + ImUtf8.HoverTooltip("Conditional Model ID"u8); + } + } + + public static bool DrawType(ref GlobalEqpManipulation identifier, float unscaledWidth = 250) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var combo = ImUtf8.Combo("##geqpType"u8, identifier.Type.ToName()); + if (!combo) + return false; + + var ret = false; + foreach (var type in Enum.GetValues()) + { + if (ImUtf8.Selectable(type.ToName(), type == identifier.Type)) + { + identifier = new GlobalEqpManipulation + { + Type = type, + Condition = type.HasCondition() ? identifier.Type.HasCondition() ? identifier.Condition : 1 : 0, + }; + ret = true; + } + + ImUtf8.HoverTooltip(type.ToDescription()); + } + + return ret; + } + + public static void DrawCondition(ref GlobalEqpManipulation identifier, float unscaledWidth = 100) + { + if (IdInput("##geqpCond"u8, unscaledWidth, identifier.Condition.Id, out var newId, 1, ushort.MaxValue, + identifier.Condition.Id <= 1)) + identifier = identifier with { Condition = newId }; + ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs new file mode 100644 index 000000000..87ed21dc4 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -0,0 +1,148 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Visor/Gimmick Edits (GMP)###GMP"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new GmpIdentifier(1); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedGmpFile.GetDefault(MetaFiles, Identifier.SetId); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Gmp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = ExpandedGmpFile.GetDefault(MetaFiles, identifier.SetId); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() + => Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref GmpIdentifier identifier) + { + ImGui.TableNextColumn(); + return DrawPrimaryId(ref identifier); + } + + private static void DrawIdentifier(GmpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + } + + private static bool DrawEntry(GmpEntry defaultEntry, ref GmpEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var changes = false; + if (Checkmark("##gmpEnabled"u8, "Gimmick Enabled", entry.Enabled, defaultEntry.Enabled, out var enabled)) + { + entry = entry with { Enabled = enabled }; + changes = true; + } + + ImGui.TableNextColumn(); + if (Checkmark("##gmpAnimated"u8, "Gimmick Animated", entry.Animated, defaultEntry.Animated, out var animated)) + { + entry = entry with { Animated = animated }; + changes = true; + } + + var rotationWidth = 75 * ImUtf8.GlobalScale; + ImGui.TableNextColumn(); + if (DragInput("##gmpRotationA"u8, "Rotation A in Degrees"u8, rotationWidth, entry.RotationA, defaultEntry.RotationA, out var rotationA, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationA = rotationA }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpRotationB"u8, "Rotation B in Degrees"u8, rotationWidth, entry.RotationB, defaultEntry.RotationB, out var rotationB, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationB = rotationB }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpRotationC"u8, "Rotation C in Degrees"u8, rotationWidth, entry.RotationC, defaultEntry.RotationC, out var rotationC, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationC = rotationC }; + changes = true; + } + + var unkWidth = 50 * ImUtf8.GlobalScale; + ImGui.TableNextColumn(); + if (DragInput("##gmpUnkA"u8, "Animation Type A?"u8, unkWidth, entry.UnknownA, defaultEntry.UnknownA, out var unknownA, + (byte)0, (byte)15, 0.01f, !disabled)) + { + entry = entry with { UnknownA = unknownA }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpUnkB"u8, "Animation Type B?"u8, unkWidth, entry.UnknownB, defaultEntry.UnknownB, out var unknownB, + (byte)0, (byte)15, 0.01f, !disabled)) + { + entry = entry with { UnknownB = unknownB }; + changes = true; + } + + return changes; + } + + public static bool DrawPrimaryId(ref GmpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##gmpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = new GmpIdentifier(setId); + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs new file mode 100644 index 000000000..58f626fc7 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -0,0 +1,293 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Variant Edits (IMC)###IMC"u8; + + public override int NumColumns + => 10; + + private bool _fileExists; + + protected override void Initialize() + { + Identifier = ImcIdentifier.Default; + UpdateEntry(); + } + + private void UpdateEntry() + => (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc)); + ImGui.TableNextColumn(); + var canAdd = _fileExists && !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : !_fileExists ? "This IMC file does not exist."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + using var disabled = ImRaii.Disabled(); + DrawEntry(Entry, ref Entry, false); + } + + protected override void DrawEntry(ImcIdentifier identifier, ImcEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; + if (DrawEntry(defaultEntry, ref entry, true)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + private static bool DrawIdentifierInput(ref ImcIdentifier identifier) + { + ImGui.TableNextColumn(); + var change = DrawObjectType(ref identifier); + + ImGui.TableNextColumn(); + change |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= DrawSlot(ref identifier); + else + change |= DrawSecondaryId(ref identifier); + + ImGui.TableNextColumn(); + change |= DrawVariant(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + change |= DrawSlot(ref identifier, 70f); + else + ImUtf8.ScaledDummy(70f); + return change; + } + + private static void DrawIdentifier(ImcIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.ObjectType.ToName(), FrameColor); + ImUtf8.HoverTooltip("Object Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", FrameColor); + ImUtf8.HoverTooltip("Primary ID"); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + else + { + ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", FrameColor); + ImUtf8.HoverTooltip("Secondary ID"u8); + } + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.Variant.Id}", FrameColor); + ImUtf8.HoverTooltip("Variant"u8); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + } + + private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) + { + ImGui.TableNextColumn(); + var change = DrawMaterialId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawMaterialAnimationId(defaultEntry, ref entry, addDefault); + + ImGui.TableNextColumn(); + change |= DrawDecalId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawVfxId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawSoundId(defaultEntry, ref entry, addDefault); + + ImGui.TableNextColumn(); + change |= DrawAttributes(defaultEntry, ref entry); + return change; + } + + + protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() + => Editor.Imc.Select(kvp => (kvp.Key, kvp.Value)); + + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) + { + var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + identifier = identifier with + { + ObjectType = type, + EquipSlot = equipSlot, + SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + }; + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + identifier.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { PrimaryId = newId }; + return ret; + } + + public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + identifier = identifier with { SecondaryId = newId }; + return ret; + } + + public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + identifier = identifier with { Variant = (byte)newId }; + return ret; + } + + public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (identifier.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { EquipSlot = slot }; + return ret; + } + + public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, + out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialId = newValue }; + return true; + } + + public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, + defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialAnimationId = newValue }; + return true; + } + + public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { DecalId = newValue }; + return true; + } + + public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, + byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { VfxId = newValue }; + return true; + } + + public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { SoundId = newValue }; + return true; + } + + private static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) + { + var changes = false; + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var flag = 1 << i; + var value = (entry.AttributeMask & flag) != 0; + var def = (defaultEntry.AttributeMask & flag) != 0; + if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) + { + var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); + entry = entry with { AttributeMask = newMask }; + changes = true; + } + + if (i < ImcEntry.NumAttributes - 1) + ImUtf8.SameLineInner(); + } + + return changes; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs new file mode 100644 index 000000000..229526c46 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -0,0 +1,154 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Api.Api; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public interface IMetaDrawer +{ + public ReadOnlySpan Label { get; } + public int NumColumns { get; } + public void Draw(); +} + +public abstract class MetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : IMetaDrawer + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged +{ + protected const uint FrameColor = 0; + + protected readonly ModMetaEditor Editor = editor; + protected readonly MetaFileManager MetaFiles = metaFiles; + protected TIdentifier Identifier; + protected TEntry Entry; + private bool _initialized; + + public void Draw() + { + if (!_initialized) + { + Initialize(); + _initialized = true; + } + + using var id = ImUtf8.PushId((int)Identifier.Type); + DrawNew(); + foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) + { + id.Push(idx); + DrawEntry(identifier, entry); + id.Pop(); + } + } + + public abstract ReadOnlySpan Label { get; } + public abstract int NumColumns { get; } + + protected abstract void DrawNew(); + protected abstract void Initialize(); + protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); + + protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); + + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + protected static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + /// + /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + /// Returns true if newValue changed against currentValue. + /// + protected static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, + out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) + newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; + + if (addDefault) + ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + protected static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + protected static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } + + protected void DrawMetaButtons(TIdentifier identifier, TEntry entry) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! }); + + ImGui.TableNextColumn(); + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8)) + Editor.Changes |= Editor.Remove(identifier); + } + + protected void CopyToClipboardButton(ReadOnlySpan tooltip, JToken? manipulations) + { + if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) + return; + + var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); + if (text.Length > 0) + ImGui.SetClipboardText(text); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs new file mode 100644 index 000000000..b3dd9299d --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -0,0 +1,35 @@ +using OtterGui.Services; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public class MetaDrawers( + EqdpMetaDrawer eqdp, + EqpMetaDrawer eqp, + EstMetaDrawer est, + GlobalEqpMetaDrawer globalEqp, + GmpMetaDrawer gmp, + ImcMetaDrawer imc, + RspMetaDrawer rsp) : IService +{ + public readonly EqdpMetaDrawer Eqdp = eqdp; + public readonly EqpMetaDrawer Eqp = eqp; + public readonly EstMetaDrawer Est = est; + public readonly GmpMetaDrawer Gmp = gmp; + public readonly RspMetaDrawer Rsp = rsp; + public readonly ImcMetaDrawer Imc = imc; + public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; + + public IMetaDrawer? Get(MetaManipulationType type) + => type switch + { + MetaManipulationType.Imc => Imc, + MetaManipulationType.Eqdp => Eqdp, + MetaManipulationType.Eqp => Eqp, + MetaManipulationType.Est => Est, + MetaManipulationType.Gmp => Gmp, + MetaManipulationType.Rsp => Rsp, + MetaManipulationType.GlobalEqp => GlobalEqp, + _ => null, + }; +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs new file mode 100644 index 000000000..be02e321c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -0,0 +1,112 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Racial Scaling Edits (RSP)###RSP"u8; + + public override int NumColumns + => 5; + + protected override void Initialize() + { + Identifier = new RspIdentifier(SubRace.Midlander, RspAttribute.MaleMinSize); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = CmpFile.GetDefault(MetaFiles, Identifier.SubRace, Identifier.Attribute); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Rsp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(RspIdentifier identifier, RspEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = CmpFile.GetDefault(MetaFiles, identifier.SubRace, identifier.Attribute); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() + => Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref RspIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawSubRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawAttribute(ref identifier); + return changes; + } + + private static void DrawIdentifier(RspIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.SubRace.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Attribute.ToFullString(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(RspEntry defaultEntry, ref RspEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, entry.Value, defaultEntry.Value, out var newValue, + RspEntry.MinValue, RspEntry.MaxValue, 0.001f, !disabled); + if (ret) + entry = new RspEntry(newValue); + return ret; + } + + public static bool DrawSubRace(ref RspIdentifier identifier, float unscaledWidth = 150) + { + var ret = Combos.SubRace("##rspSubRace", identifier.SubRace, out var subRace, unscaledWidth); + ImUtf8.HoverTooltip("Racial Clan"u8); + if (ret) + identifier = identifier with { SubRace = subRace }; + return ret; + } + + public static bool DrawAttribute(ref RspIdentifier identifier, float unscaledWidth = 200) + { + var ret = Combos.RspAttribute("##rspAttribute", identifier.Attribute, out var attribute, unscaledWidth); + ImUtf8.HoverTooltip("Scaling Attribute"u8); + if (ret) + identifier = identifier with { Attribute = attribute }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index b0a74637c..3ec6a4d53 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -3,34 +3,16 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Files; +using Penumbra.Api.Api; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; -using Penumbra.UI.ModsTab; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const string ModelSetIdTooltip = - "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIdTooltipShort = "Primary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; + private readonly MetaDrawers _metaDrawers; private void DrawMetaTab() { @@ -56,7 +38,7 @@ private void DrawMetaTab() ImGui.SameLine(); SetFromClipboardButton(); ImGui.SameLine(); - CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); + CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor); ImGui.SameLine(); if (ImGui.Button("Write as TexTools Files")) _metaFileManager.WriteAllTexToolsMeta(Mod!); @@ -65,771 +47,67 @@ private void DrawMetaTab() if (!child) return; - DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqp]); - DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqdp]); - DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Imc]); - DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Est]); - DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Gmp]); - DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Rsp]); - DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, - GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherData[MetaManipulation.Type.GlobalEqp]); + DrawEditHeader(MetaManipulationType.Eqp); + DrawEditHeader(MetaManipulationType.Eqdp); + DrawEditHeader(MetaManipulationType.Imc); + DrawEditHeader(MetaManipulationType.Est); + DrawEditHeader(MetaManipulationType.Gmp); + DrawEditHeader(MetaManipulationType.Rsp); + DrawEditHeader(MetaManipulationType.GlobalEqp); } - - /// The headers for the different meta changes all have basically the same structure for different types. - private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, - Action draw, Action drawNew, - ModMetaEditor.OtherOptionData otherOptionData) + private void DrawEditHeader(MetaManipulationType type) { - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + var drawer = _metaDrawers.Get(type); + if (drawer == null) + return; var oldPos = ImGui.GetCursorPosY(); - var header = ImGui.CollapsingHeader($"{items.Count} {label}"); - var newPos = ImGui.GetCursorPos(); - if (otherOptionData.TotalCount > 0) - { - var text = $"{otherOptionData.TotalCount} Edits in other Options"; - var size = ImGui.CalcTextSize(text).X; - ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); - ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); - if (ImGui.IsItemHovered()) - { - using var tt = ImUtf8.Tooltip(); - foreach (var name in otherOptionData) - ImUtf8.Text(name); - } - - ImGui.SetCursorPos(newPos); - } - + var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {drawer.Label}"); + DrawOtherOptionData(type, oldPos, ImGui.GetCursorPos()); if (!header) return; - using (var table = ImRaii.Table(label, numColumns, flags)) - { - if (table) - { - drawNew(_metaFileManager, _editor, _iconSize); - foreach (var (item, index) in items.ToArray().WithIndex()) - { - using var id = ImRaii.PushId(index); - draw(_metaFileManager, item, _editor, _iconSize); - } - } - } - - ImGui.NewLine(); - } - - private static class EqpRow - { - private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, - editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##eqpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), _new.Slot, setId); - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.EqpEquipSlot("##eqpSlot", _new.Slot, out var slot)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - foreach (var flag in Eqp.EqpAttributes[_new.Slot]) - { - var value = defaultEntry.HasFlag(flag); - Checkmark("##eqp", flag.ToLocalName(), value, value, out _); - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - - public static void Draw(MetaFileManager metaFileManager, EqpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, meta.SetId); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - var idx = 0; - foreach (var flag in Eqp.EqpAttributes[meta.Slot]) - { - using var id = ImRaii.PushId(idx++); - var defaultValue = defaultEntry.HasFlag(flag); - var currentValue = meta.Entry.HasFlag(flag); - if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value)) - editor.MetaEditor.Change(meta.Copy(value ? meta.Entry | flag : meta.Entry & ~flag)); - - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - } - - - private static class EqdpRow - { - private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, - editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var raceCode = Names.CombinedRace(_new.Gender, _new.Race); - var validRaceCode = CharacterUtilityData.EqdpIdx(raceCode, false) >= 0; - var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : - validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; - var defaultEntry = validRaceCode - ? ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId) - : 0; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##eqdpId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), - _new.Slot.IsAccessory(), setId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); - } - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.Race("##eqdpRace", _new.Race, out var race)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race), - _new.Slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - - ImGui.TableNextColumn(); - if (Combos.Gender("##eqdpGender", _new.Gender, out var gender)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race), - _new.Slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(GenderTooltip); - - ImGui.TableNextColumn(); - if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), - slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - var (bit1, bit2) = defaultEntry.ToBits(_new.Slot); - Checkmark("Material##eqdpCheck1", string.Empty, bit1, bit1, out _); - ImGui.SameLine(); - Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _); - } - - public static void Draw(MetaFileManager metaFileManager, EqdpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Race.ToName()); - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Gender.ToName()); - ImGuiUtil.HoverTooltip(GenderTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), - meta.SetId); - var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); - var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); - ImGui.TableNextColumn(); - if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1)) - editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2))); - - ImGui.SameLine(); - if (Checkmark("Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2)) - editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2))); - } - } - - private static class ImcRow - { - private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; - - private static float IdWidth - => 80 * UiHelpers.Scale; - - private static float SmallIdWidth - => 45 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, - editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); - var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); - var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); - var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(manip); - - // Identifier - ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - - ImGui.TableNextColumn(); - // Equipment and accessories are slightly different imcs than other types. - if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); - else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); - - ImGui.TableNextColumn(); - if (_newIdentifier.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); - else - ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); - - if (change) - defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); - } - - public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.ObjectType.ToName()); - ImGuiUtil.HoverTooltip(ObjectTypeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - else - { - ImGui.TextUnformatted(meta.SecondaryId.ToString()); - ImGuiUtil.HoverTooltip(SecondaryIdTooltip); - } - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Variant.ToString()); - ImGuiUtil.HoverTooltip(VariantIdTooltip); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.DemiHuman) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - - // Values - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.TableNextColumn(); - var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; - var newEntry = meta.Entry; - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); - - if (changes) - editor.MetaEditor.Change(meta.Copy(newEntry)); - } - } - - private static class EstRow - { - private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, - editor.MetaEditor.Est.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##estId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); - _new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.Race("##estRace", _new.Race, out var race)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId); - _new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - - ImGui.TableNextColumn(); - if (Combos.Gender("##estGender", _new.Gender, out var gender)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId); - _new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(GenderTooltip); - - ImGui.TableNextColumn(); - if (Combos.EstSlot("##estSlot", _new.Slot, out var slot)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); - _new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(EstTypeTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry.Value, defaultEntry.Value, out _, 0, ushort.MaxValue, 0.05f); - } - - public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Race.ToName()); - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Gender.ToName()); - ImGuiUtil.HoverTooltip(GenderTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToString()); - ImGuiUtil.HoverTooltip(EstTypeTooltip); - - // Values - var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); - ImGui.TableNextColumn(); - if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, - out var entry, 0, ushort.MaxValue, 0.05f)) - editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry))); - } - } - - private static class GmpRow - { - private static GmpManipulation _new = new(GmpEntry.Default, 1); - - private static float RotationWidth - => 75 * UiHelpers.Scale; - - private static float UnkWidth - => 50 * UiHelpers.Scale; - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, - editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##gmpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new GmpManipulation(ExpandedGmpFile.GetDefault(metaFileManager, setId), setId); - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - Checkmark("##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _); - ImGui.TableNextColumn(); - Checkmark("##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _); - ImGui.TableNextColumn(); - IntDragInput("##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0, - 360, 0f); - ImGui.SameLine(); - IntDragInput("##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0, - 360, 0f); - ImGui.SameLine(); - IntDragInput("##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0, - 360, 0f); - ImGui.TableNextColumn(); - IntDragInput("##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f); - ImGui.SameLine(); - IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f); - } - - public static void Draw(MetaFileManager metaFileManager, GmpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - - // Values - var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, meta.SetId); - ImGui.TableNextColumn(); - if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled })); - - ImGui.TableNextColumn(); - if (Checkmark("##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { Animated = animated })); - - ImGui.TableNextColumn(); - if (IntDragInput("##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, - meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationA = (ushort)rotationA })); - - ImGui.SameLine(); - if (IntDragInput("##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, - meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationB = (ushort)rotationB })); - - ImGui.SameLine(); - if (IntDragInput("##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, - meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationC = (ushort)rotationC })); - - ImGui.TableNextColumn(); - if (IntDragInput("##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, - defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkA })); - - ImGui.SameLine(); - if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, - defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB })); - } + DrawTable(drawer); } - private static class RspRow + private static void DrawTable(IMetaDrawer drawer) { - private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One); - - private static float FloatWidth - => 150 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, - editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace)) - _new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(metaFileManager, subRace, _new.Attribute)); - - ImGuiUtil.HoverTooltip(RacialTribeTooltip); - - ImGui.TableNextColumn(); - if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute)) - _new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(metaFileManager, subRace, attribute)); - - ImGuiUtil.HoverTooltip(ScalingTypeTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(FloatWidth); - var value = defaultEntry.Value; - ImGui.DragFloat("##rspValue", ref value, 0f); - } - - public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SubRace.ToName()); - ImGuiUtil.HoverTooltip(RacialTribeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Attribute.ToFullString()); - ImGuiUtil.HoverTooltip(ScalingTypeTooltip); - ImGui.TableNextColumn(); - - // Values - var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; - var value = meta.Entry.Value; - ImGui.SetNextItemWidth(FloatWidth); - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), - def != value); - if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspEntry.MinValue, RspEntry.MaxValue) - && value is >= RspEntry.MinValue and <= RspEntry.MaxValue) - editor.MetaEditor.Change(meta.Copy(new RspEntry(value))); + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + using var table = ImUtf8.Table(drawer.Label, drawer.NumColumns, flags); + if (!table) + return; - ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); - } + drawer.Draw(); + ImGui.NewLine(); } - private static class GlobalEqpRow + private void DrawOtherOptionData(MetaManipulationType type, float oldPos, Vector2 newPos) { - private static GlobalEqpManipulation _new = new() - { - Type = GlobalEqpType.DoNotHideEarrings, - Condition = 1, - }; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current global EQP manipulations to clipboard.", iconSize, - editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(250 * ImUtf8.GlobalScale); - using (var combo = ImUtf8.Combo("##geqpType"u8, _new.Type.ToName())) - { - if (combo) - foreach (var type in Enum.GetValues()) - { - if (ImUtf8.Selectable(type.ToName(), type == _new.Type)) - _new = new GlobalEqpManipulation - { - Type = type, - Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0, - }; - ImUtf8.HoverTooltip(type.ToDescription()); - } - } - - ImUtf8.HoverTooltip(_new.Type.ToDescription()); - - ImGui.TableNextColumn(); - if (!_new.Type.HasCondition()) - return; - - if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1)) - _new = _new with { Condition = newId }; - ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); - } + var otherOptionData = _editor.MetaEditor.OtherData[type]; + if (otherOptionData.TotalCount <= 0) + return; - public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize) + var text = $"{otherOptionData.TotalCount} Edits in other Options"; + var size = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); + if (ImGui.IsItemHovered()) { - DrawMetaButtons(meta, editor, iconSize); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImUtf8.Text(meta.Type.ToName()); - ImUtf8.HoverTooltip(meta.Type.ToDescription()); - ImGui.TableNextColumn(); - if (meta.Type.HasCondition()) - { - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImUtf8.Text($"{meta.Condition.Id}"); - } + using var tt = ImUtf8.Tooltip(); + foreach (var name in otherOptionData) + ImUtf8.Text(name); } - } - - // A number input for ids with a optional max id of given width. - // Returns true if newId changed against currentId. - private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(width); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImGui.InputInt(label, ref tmp, 0)) - tmp = Math.Clamp(tmp, minId, maxId); - newId = (ushort)tmp; - return newId != currentId; + ImGui.SetCursorPos(newPos); } - // A checkmark that compares against a default value and shows a tooltip. - // Returns true if newValue is changed against currentValue. - private static bool Checkmark(string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImGui.Checkbox(label, ref newValue); - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - return newValue != currentValue; - } - - // A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - // Returns true if newValue changed against currentValue. - private static bool IntDragInput(string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue, - int minValue, int maxValue, float speed) - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImGui.DragInt(label, ref newValue, speed, minValue, maxValue)) - newValue = Math.Clamp(newValue, minValue, maxValue); - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - return newValue != currentValue; - } - - private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, IEnumerable manipulations) + private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, MetaDictionary manipulations) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) return; - var text = Functions.ToCompressedBase64(manipulations, MetaManipulation.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); if (text.Length > 0) ImGui.SetClipboardText(text); } @@ -840,10 +118,12 @@ private void AddFromClipboardButton() { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaManipulation.CurrentVersion && manips != null) - foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) - _editor.MetaEditor.Set(manip); + var version = Functions.FromCompressedBase64(clipboard, out var manips); + if (version == MetaApi.CurrentVersion && manips != null) + { + _editor.MetaEditor.UpdateTo(manips); + _editor.MetaEditor.Changes = true; + } } ImGuiUtil.HoverTooltip( @@ -855,26 +135,15 @@ private void SetFromClipboardButton() if (ImGui.Button("Set from Clipboard")) { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaManipulation.CurrentVersion && manips != null) + var version = Functions.FromCompressedBase64(clipboard, out var manips); + if (version == MetaApi.CurrentVersion && manips != null) { - _editor.MetaEditor.Clear(); - foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) - _editor.MetaEditor.Set(manip); + _editor.MetaEditor.SetTo(manips); + _editor.MetaEditor.Changes = true; } } ImGuiUtil.HoverTooltip( "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."); } - - private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) - editor.MetaEditor.Delete(meta); - } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 1f935eb65..72ab37b27 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -95,7 +95,7 @@ private void FindGamePaths(string path) task.ContinueWith(t => { GamePaths = FinalizeIo(t); }, TaskScheduler.Default); } - private EstManipulation[] GetCurrentEstManipulations() + private KeyValuePair[] GetCurrentEstManipulations() { var mod = _edit._editor.Mod; var option = _edit._editor.Option; @@ -106,9 +106,7 @@ private EstManipulation[] GetCurrentEstManipulations() return mod.AllDataContainers .Where(subMod => subMod != option) .Prepend(option) - .SelectMany(subMod => subMod.Manipulations) - .Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est) - .Select(manipulation => manipulation.Est) + .SelectMany(subMod => subMod.Manipulations.Est) .ToArray(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index af01047b2..90fdc48e0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -7,6 +7,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -25,13 +26,14 @@ using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; using Penumbra.Util; using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; -public partial class ModEditWindow : Window, IDisposable +public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; @@ -586,7 +588,7 @@ public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialo StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor) + CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers) : base(WindowBaseLabel) { _performance = performance; @@ -606,6 +608,7 @@ public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialo _objects = objects; _framework = framework; _characterBaseDestructor = characterBaseDestructor; + _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 5dad66b4e..b5f0255c1 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -9,7 +10,7 @@ namespace Penumbra.UI.AdvancedWindow; -public class ModMergeTab(ModMerger modMerger) +public class ModMergeTab(ModMerger modMerger) : IUiService { private readonly ModCombo _modCombo = new(() => modMerger.ModsWithoutCurrent.ToList()); private string _newModName = string.Empty; @@ -183,7 +184,7 @@ private void DrawOptionTable(float size) else { ImGuiUtil.DrawTableColumn(option.GetName()); - + ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 29a1f2918..0afeeeeb8 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -10,6 +10,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -19,7 +20,7 @@ namespace Penumbra.UI; -public class ChangedItemDrawer : IDisposable +public class ChangedItemDrawer : IDisposable, IUiService { [Flags] public enum ChangedItemIcon : uint @@ -99,8 +100,10 @@ public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot) slot = 0; foreach (var (item, flag) in LowerNames.Zip(Order)) + { if (item.Contains(lowerInput, StringComparison.Ordinal)) slot |= flag; + } return slot != 0; } diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 184633f23..f4cedf7de 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -1,8 +1,9 @@ +using OtterGui.Services; using OtterGui.Widgets; namespace Penumbra.UI; -public class PenumbraChangelog +public class PenumbraChangelog : IUiService { public const int LastChangelogVersion = 0; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index bff0092a5..6c8bbf646 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,6 +1,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -9,7 +10,7 @@ namespace Penumbra.UI.Classes; -public class CollectionSelectHeader +public class CollectionSelectHeader : IUiService { private readonly CollectionCombo _collectionCombo; private readonly ActiveCollections _activeCollections; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 9ae11fc35..67b0a50ca 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,8 @@ using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Services; using Penumbra.UI.Classes; @@ -12,7 +14,7 @@ namespace Penumbra.UI; -public sealed class ConfigWindow : Window +public sealed class ConfigWindow : Window, IUiService { private readonly DalamudPluginInterface _pluginInterface; private readonly Configuration _config; @@ -144,7 +146,7 @@ private void DrawProblemWindow(string text) using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); ImGui.NewLine(); ImGui.NewLine(); - ImGuiUtil.TextWrapped(text); + ImUtf8.TextWrapped(text); color.Pop(); ImGui.NewLine(); diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 88c0b00fd..cc2a7f6ac 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -3,12 +3,13 @@ using Dalamud.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.UI; -public class FileDialogService : IDisposable +public class FileDialogService : IDisposable, IUiService { private readonly CommunicatorService _communicator; private readonly FileDialogManager _manager; diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 71164d1dd..fb2028b51 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,13 +1,14 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Import.Structs; using Penumbra.Mods.Manager; namespace Penumbra.UI; /// Draw the progress information for import. -public sealed class ImportPopup : Window +public sealed class ImportPopup : Window, IUiService { public const string WindowLabel = "Penumbra Import Status"; diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 9650ccf8c..14e16432b 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using OtterGui.Services; namespace Penumbra.UI; @@ -9,7 +10,7 @@ namespace Penumbra.UI; /// A Launch Button used in the title screen of the game, /// using the Dalamud-provided collapsible submenu. /// -public class LaunchButton : IDisposable +public class LaunchButton : IDisposable, IUiService { private readonly ConfigWindow _configWindow; private readonly UiBuilder _uiBuilder; diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 3ac10cd0c..689571f33 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -9,6 +9,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab.Groups; @@ -79,29 +80,29 @@ private void DrawMultiGroupButton(Mod mod, Vector2 width) private void DrawImcInput(float width) { - var change = ImcManipulationDrawer.DrawObjectType(ref _imcIdentifier, width); + var change = ImcMetaDrawer.DrawObjectType(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawPrimaryId(ref _imcIdentifier, width); if (_imcIdentifier.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, width); } else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); + change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); } else { - change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, width); } if (change) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 5c8edce6a..5d10febd3 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -7,6 +7,7 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; +using Penumbra.UI.AdvancedWindow.Meta; namespace Penumbra.UI.ModsTab.Groups; @@ -37,9 +38,9 @@ public void Draw() ImGui.SameLine(); using (ImUtf8.Group()) { - changes |= ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true); } ImGui.SameLine(0, editor.PriorityWidth); @@ -54,8 +55,8 @@ public void Draw() using (ImUtf8.Group()) { - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true); var canBeDisabled = group.CanBeDisabled; if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index 694ae11c1..1291f568a 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -10,210 +10,5 @@ namespace Penumbra.UI.ModsTab; public static class ImcManipulationDrawer { - public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) - { - var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); - ImUtf8.HoverTooltip("Object Type"u8); - - if (ret) - { - var equipSlot = type switch - { - ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - identifier = identifier with - { - ObjectType = type, - EquipSlot = equipSlot, - SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, - }; - } - - return ret; - } - - public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) - { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - identifier.PrimaryId.Id <= 1); - ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 - + "This should generally not be left <= 1 unless you explicitly want that."u8); - if (ret) - identifier = identifier with { PrimaryId = newId }; - return ret; - } - - public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) - { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); - ImUtf8.HoverTooltip("Secondary ID"u8); - if (ret) - identifier = identifier with { SecondaryId = newId }; - return ret; - } - - public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) - { - var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); - ImUtf8.HoverTooltip("Variant ID"u8); - if (ret) - identifier = identifier with { Variant = (byte)newId }; - return ret; - } - - public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) - { - bool ret; - EquipSlot slot; - switch (identifier.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); - break; - case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); - break; - default: return false; - } - - ImUtf8.HoverTooltip("Equip Slot"u8); - if (ret) - identifier = identifier with { EquipSlot = slot }; - return ret; - } - - public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, - out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { MaterialId = newValue }; - return true; - } - - public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, - defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { MaterialAnimationId = newValue }; - return true; - } - - public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, - (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { DecalId = newValue }; - return true; - } - - public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, - byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { VfxId = newValue }; - return true; - } - - public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, - (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { SoundId = newValue }; - return true; - } - - public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) - { - var changes = false; - for (var i = 0; i < ImcEntry.NumAttributes; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - var value = (entry.AttributeMask & flag) != 0; - var def = (defaultEntry.AttributeMask & flag) != 0; - if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) - { - var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); - entry = entry with { AttributeMask = newMask }; - changes = true; - } - - if (i < ImcEntry.NumAttributes - 1) - ImGui.SameLine(); - } - - return changes; - } - - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } - - /// - /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - /// Returns true if newValue changed against currentValue. - /// - private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, - out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) - newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; - - if (addDefault) - ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - - return newValue != currentValue; - } - - /// - /// A checkmark that compares against a default value and shows a tooltip. - /// Returns true if newValue is changed against currentValue. - /// - private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, - out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImUtf8.Checkbox(label, ref newValue); - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - return newValue != currentValue; - } + } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 58f0b615d..0ca4d40c4 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -8,6 +8,7 @@ using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -21,7 +22,7 @@ namespace Penumbra.UI.ModsTab; -public sealed class ModFileSystemSelector : FileSystemSelector +public sealed class ModFileSystemSelector : FileSystemSelector, IUiService { private readonly CommunicatorService _communicator; private readonly MessageService _messager; @@ -33,9 +34,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector().Aggregate((a, b) => a | b); - public ReadOnlySpan Label => "Changed Items"u8; - public ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) - { - _selector = selector; - _drawer = drawer; - } - public bool IsVisible - => _selector.Selected!.ChangedItems.Count > 0; + => selector.Selected!.ChangedItems.Count > 0; public void DrawContent() { - _drawer.DrawTypeFilter(); + drawer.DrawTypeFilter(); ImGui.Separator(); using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); if (!table) return; - var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); + var zipList = ZipList.FromSortedList((SortedList)selector.Selected!.ChangedItems); var height = ImGui.GetFrameHeightWithSpacing(); ImGui.TableNextColumn(); var skips = ImGuiClip.GetNecessarySkips(height); @@ -43,14 +33,14 @@ public void DrawContent() } private bool CheckFilter((string Name, object? Data) kvp) - => _drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); private void DrawChangedItem((string Name, object? Data) kvp) { ImGui.TableNextColumn(); - _drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + drawer.DrawCategoryIcon(kvp.Name, kvp.Data); ImGui.SameLine(); - _drawer.DrawChangedItem(kvp.Name, kvp.Data); - _drawer.DrawModelData(kvp.Data); + drawer.DrawChangedItem(kvp.Name, kvp.Data); + drawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index aa598557a..9f37f8479 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -10,25 +11,16 @@ namespace Penumbra.UI.ModsTab; -public class ModPanelCollectionsTab : ITab +public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) : ITab, IUiService { - private readonly ModFileSystemSelector _selector; - private readonly CollectionStorage _collections; - - private readonly List<(ModCollection, ModCollection, uint, string)> _cache = new(); - - public ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) - { - _collections = storage; - _selector = selector; - } + private readonly List<(ModCollection, ModCollection, uint, string)> _cache = []; public ReadOnlySpan Label => "Collections"u8; public void DrawContent() { - var (direct, inherited) = CountUsage(_selector.Selected!); + var (direct, inherited) = CountUsage(selector.Selected!); ImGui.NewLine(); if (direct == 1) ImGui.TextUnformatted("This Mod is directly configured in 1 collection."); @@ -80,7 +72,7 @@ public void DrawContent() var disInherited = ColorId.InheritedDisabledMod.Value(); var directCount = 0; var inheritedCount = 0; - foreach (var collection in _collections) + foreach (var collection in storage) { var (settings, parent) = collection[mod.Index]; var (color, text) = settings == null diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 5e3aac483..bee48068c 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -3,6 +3,8 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; @@ -15,7 +17,7 @@ namespace Penumbra.UI.ModsTab; -public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab +public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab, IUiService { private int? _currentPriority; @@ -116,15 +118,12 @@ private bool DrawExpandedFiles(ModConflicts conflict) using var indent = ImRaii.PushIndent(30f); foreach (var data in conflict.Conflicts) { - unsafe + _ = data switch { - var _ = data switch - { - Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, - MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), - _ => false, - }; - } + Utf8GamePath p => ImUtf8.Selectable(p.Path.Span, false), + IMetaIdentifier m => ImUtf8.Selectable(m.ToString(), false), + _ => false, + }; } return true; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index ed6340ab5..6fe3e4c69 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Mods.Manager; @@ -12,7 +13,7 @@ public class ModPanelDescriptionTab( TutorialService tutorial, ModManager modManager, PredefinedTagManager predefinedTagsConfig) - : ITab + : ITab, IUiService { private readonly TagButtons _localTags = new(); private readonly TagButtons _modTags = new(); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 468e97b95..1e3710656 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -6,13 +6,13 @@ using OtterGui.Raii; using OtterGui.Widgets; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Settings; -using Penumbra.Mods.Manager.OptionEditor; using Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab; @@ -31,7 +31,7 @@ public class ModPanelEditTab( ModGroupEditDrawer groupEditDrawer, DescriptionEditPopup descriptionPopup, AddGroupDrawer addGroupDrawer) - : ITab + : ITab, IUiService { private readonly TagButtons _modTags = new(); diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 8b09d8b9a..639118f51 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -9,7 +10,7 @@ namespace Penumbra.UI.ModsTab; -public class ModPanelTabBar +public class ModPanelTabBar : IUiService { private enum ModPanelTabType { @@ -33,7 +34,7 @@ private enum ModPanelTabType public readonly ITab[] Tabs; private ModPanelTabType _preferredTab = ModPanelTabType.Settings; - private Mod? _lastMod = null; + private Mod? _lastMod; public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, @@ -49,15 +50,15 @@ public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, _tutorial = tutorial; Collections = collections; - Tabs = new ITab[] - { + Tabs = + [ Settings, Description, Conflicts, ChangedItems, Collections, Edit, - }; + ]; } public void Draw(Mod mod) diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 595240f42..4079748e6 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -3,12 +3,13 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) +public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) : IUiService { public void Draw() { @@ -65,8 +66,8 @@ private void DrawModList() } private string _tag = string.Empty; - private readonly List _addMods = []; - private readonly List<(Mod, int)> _removeMods = []; + private readonly List _addMods = []; + private readonly List<(Mod, int)> _removeMods = []; private void DrawMultiTagger() { diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 0e5377d6a..d531b1a23 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -6,14 +6,14 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; -using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class PredefinedTagManager : ISavable, IReadOnlyList +public sealed class PredefinedTagManager : ISavable, IReadOnlyList, IService { public const int Version = 1; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 65a8fe76b..a7d1a8c62 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -15,7 +16,7 @@ namespace Penumbra.UI.ResourceWatcher; -public sealed class ResourceWatcher : IDisposable, ITab +public sealed class ResourceWatcher : IDisposable, ITab, IUiService { public const int DefaultMaxEntries = 1024; public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index ab0badf41..2aeaaea0e 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -2,6 +2,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -17,7 +18,7 @@ public class ChangedItemsTab( CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer, CommunicatorService communicator) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Changed Items"u8; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index fabf75612..34e2cbcf5 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; @@ -11,7 +12,7 @@ namespace Penumbra.UI.Tabs; -public sealed class CollectionsTab : IDisposable, ITab +public sealed class CollectionsTab : IDisposable, ITab, IUiService { private readonly EphemeralConfig _config; private readonly CollectionSelector _selector; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 9fd07f27d..28827ad92 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -1,4 +1,5 @@ using ImGuiNET; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; @@ -8,7 +9,7 @@ namespace Penumbra.UI.Tabs; -public class ConfigTabBar : IDisposable +public class ConfigTabBar : IDisposable, IUiService { private readonly CommunicatorService _communicator; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 1813a7e34..0122a6f5b 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -44,7 +44,7 @@ namespace Penumbra.UI.Tabs.Debug; -public class Diagnostics(IServiceProvider provider) +public class Diagnostics(ServiceManager provider) : IUiService { public void DrawDiagnostics() { @@ -55,7 +55,7 @@ public void DrawDiagnostics() foreach (var type in typeof(ActorManager).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) { - var container = (IAsyncDataContainer)provider.GetRequiredService(type); + var container = (IAsyncDataContainer)provider.Provider!.GetRequiredService(type); ImGuiUtil.DrawTableColumn(container.Name); ImGuiUtil.DrawTableColumn(container.Time.ToString()); ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory)); @@ -64,7 +64,7 @@ public void DrawDiagnostics() } } -public class DebugTab : Window, ITab +public class DebugTab : Window, ITab, IUiService { private readonly PerformanceTracker _performance; private readonly Configuration _config; @@ -179,8 +179,6 @@ public void DrawContent() ImGui.NewLine(); DrawData(); ImGui.NewLine(); - DrawDebugTabMetaLists(); - ImGui.NewLine(); DrawResourceProblems(); ImGui.NewLine(); DrawPlayerModelInfo(); @@ -432,7 +430,7 @@ private unsafe void DrawActorsDebug() foreach (var obj in _objects) { - ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); + ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? string.Empty @@ -788,23 +786,6 @@ private unsafe void DrawDebugCharacterUtility() } } - private void DrawDebugTabMetaLists() - { - if (!ImGui.CollapsingHeader("Metadata Changes")) - return; - - using var table = Table("##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - foreach (var list in _characterUtility.Lists) - { - ImGuiUtil.DrawTableColumn(list.GlobalMetaIndex.ToString()); - ImGuiUtil.DrawTableColumn(list.Entries.Count.ToString()); - ImGuiUtil.DrawTableColumn(string.Join(", ", list.Entries.Select(e => $"0x{e.Data:X}"))); - } - } - /// Draw information about the resident resource files. private unsafe void DrawDebugResidentResources() { diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 37561000f..1b9af75c2 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -3,6 +3,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -15,7 +16,7 @@ namespace Penumbra.UI.Tabs; public class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Effective Changes"u8; @@ -98,7 +99,7 @@ private void DrawEffectiveRows(ModCollection active, int skips, float height, bo // Filters mean we can not use the known counts. if (hasFilters) { - var it2 = m.Select(p => (p.Key.ToString(), p.Value.Name)); + var it2 = m.IdentifierSources.Select(p => (p.Item1.ToString(), p.Item2.Name)); if (stop >= 0) { ImGuiClip.DrawEndDummy(stop + it2.Count(CheckFilters), height); @@ -117,7 +118,7 @@ private void DrawEffectiveRows(ModCollection active, int skips, float height, bo } else { - stop = ImGuiClip.ClippedDraw(m, skips, DrawLine, m.Count, ~stop); + stop = ImGuiClip.ClippedDraw(m.IdentifierSources, skips, DrawLine, m.Count, ~stop); ImGuiClip.DrawEndDummy(stop, height); } } @@ -152,11 +153,11 @@ private static void DrawLine((string, LowerString) pair) ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(name); + ImGuiUtil.CopyOnClickSelectable(name.Text); } /// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine(KeyValuePair pair) + private static void DrawLine((IMetaIdentifier, IMod) pair) { var (manipulation, mod) = pair; ImGui.TableNextColumn(); @@ -165,7 +166,7 @@ private static void DrawLine(KeyValuePair pair) ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(mod.Name); + ImGuiUtil.CopyOnClickSelectable(mod.Name.Text); } /// Check filters for file replacements. diff --git a/Penumbra/UI/Tabs/MessagesTab.cs b/Penumbra/UI/Tabs/MessagesTab.cs index abaf2ba6f..190f94078 100644 --- a/Penumbra/UI/Tabs/MessagesTab.cs +++ b/Penumbra/UI/Tabs/MessagesTab.cs @@ -1,9 +1,10 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Services; namespace Penumbra.UI.Tabs; -public class MessagesTab(MessageService messages) : ITab +public class MessagesTab(MessageService messages) : ITab, IUiService { public ReadOnlySpan Label => "Messages"u8; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index e4d94bb5e..7faa3da88 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -6,6 +6,7 @@ using Dalamud.Interface; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Housing; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Interop.Services; @@ -30,7 +31,7 @@ public class ModsTab( CollectionSelectHeader collectionHeader, ITargetManager targets, ObjectManager objects) - : ITab + : ITab, IUiService { private readonly ActiveCollections _activeCollections = collectionManager.Active; diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 787e07a1d..fa33f702c 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -1,16 +1,12 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.Tabs; -public class OnScreenTab : ITab +public class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab, IUiService { - private readonly ResourceTreeViewer _viewer; - - public OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) - { - _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); - } + private readonly ResourceTreeViewer _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); public ReadOnlySpan Label => "On-Screen"u8; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index bbb0561be..0b54c5e23 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; @@ -13,7 +14,7 @@ namespace Penumbra.UI.Tabs; public class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Resource Manager"u8; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 0de4f7908..17db21c9a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -9,6 +9,7 @@ using OtterGui.Compression; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Interop.Services; @@ -19,7 +20,7 @@ namespace Penumbra.UI.Tabs; -public class SettingsTab : ITab +public class SettingsTab : ITab, IUiService { public const int RootDirectoryMaxLength = 64; diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index d87df19e5..7d2a0d2ae 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -40,7 +41,7 @@ public enum BasicTutorialSteps } /// Service for the in-game tutorial. -public class TutorialService +public class TutorialService : IUiService { public const string SelectedCollection = "Selected Collection"; public const string DefaultCollection = "Base Collection"; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index c5418eb38..99819fce3 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -1,12 +1,13 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; +using OtterGui.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Tabs.Debug; namespace Penumbra.UI; -public class PenumbraWindowSystem : IDisposable +public class PenumbraWindowSystem : IDisposable, IUiService { private readonly UiBuilder _uiBuilder; private readonly WindowSystem _windowSystem; diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index abf715e65..f7aa55988 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -45,6 +45,18 @@ public static void SetTo(this Dictionary lhs, IReadO lhs.Add(key, value); } + /// Set all entries in the right-hand dictionary to the same values in the left-hand dictionary, ensuring capacity beforehand. + public static void UpdateTo(this Dictionary lhs, IReadOnlyDictionary rhs) + where TKey : notnull + { + if (ReferenceEquals(lhs, rhs)) + return; + + lhs.EnsureCapacity(rhs.Count); + foreach (var (key, value) in rhs) + lhs[key] = value; + } + /// Set one set to the other, deleting previous entries and ensuring capacity beforehand. public static void SetTo(this HashSet lhs, IReadOnlySet rhs) { diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 0647ea8e1..cb43ac061 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -2,7 +2,6 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.SubMods; @@ -10,103 +9,14 @@ namespace Penumbra.Util; public static class IdentifierExtensions { - /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. - public static void MetaChangedItems(this ObjectIdentification identifier, IDictionary changedItems, - MetaManipulation manip) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - switch (manip.Imc.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - identifier.Identify(changedItems, - GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Weapon: - identifier.Identify(changedItems, - GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - case ObjectType.DemiHuman: - identifier.Identify(changedItems, - GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Monster: - identifier.Identify(changedItems, - GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - } - - break; - case MetaManipulation.Type.Eqdp: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); - break; - case MetaManipulation.Type.Eqp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); - break; - case MetaManipulation.Type.Est: - switch (manip.Est.Slot) - { - case EstType.Hair: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); - break; - case EstType.Face: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); - break; - case EstType.Body: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Body)); - break; - case EstType.Head: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Head)); - break; - } - - break; - case MetaManipulation.Type.Gmp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); - break; - case MetaManipulation.Type.Rsp: - changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); - break; - case MetaManipulation.Type.GlobalEqp: - var path = manip.GlobalEqp.Type switch - { - GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Ears), - GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Neck), - GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Wrists), - GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.RFinger), - GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.LFinger), - GlobalEqpType.DoNotHideHrothgarHats => string.Empty, - GlobalEqpType.DoNotHideVieraHats => string.Empty, - _ => string.Empty, - }; - if (path.Length > 0) - identifier.Identify(changedItems, path); - break; - } - } - public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); - foreach (var manip in container.Manipulations) - MetaChangedItems(identifier, changedItems, manip); + foreach (var manip in container.Manipulations.Identifiers) + manip.AddChangedItems(identifier, changedItems); } public static void RemoveMachinistOffhands(this SortedList changedItems)