From b1be868a6a9eaa94fec3f307cd0223e0c6b38e3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 12:19:14 +0100 Subject: [PATCH] Atch stuff. --- Penumbra.GameData | 2 +- Penumbra/Api/Api/MetaApi.cs | 12 + Penumbra/Collections/Cache/AtchCache.cs | 122 +++++++++ Penumbra/Collections/Cache/CollectionCache.cs | 33 ++- Penumbra/Collections/Cache/MetaCache.cs | 10 +- Penumbra/Interop/Hooks/DebugHook.cs | 10 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../Interop/Hooks/Meta/AtchCallerHook1.cs | 39 +++ .../Interop/Hooks/Meta/AtchCallerHook2.cs | 38 +++ Penumbra/Interop/PathResolving/MetaState.cs | 1 + .../Interop/PathResolving/PathResolver.cs | 9 +- .../Processing/AtchFilePostProcessor.cs | 43 +++ .../Processing/AtchPathPreProcessor.cs | 44 ++++ .../Processing/GamePathPreProcessService.cs | 3 +- Penumbra/Meta/AtchManager.cs | 26 ++ Penumbra/Meta/Manipulations/AtchIdentifier.cs | 77 ++++++ .../Meta/Manipulations/IMetaIdentifier.cs | 1 + Penumbra/Meta/Manipulations/MetaDictionary.cs | 72 ++++- Penumbra/Meta/MetaFileManager.cs | 5 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 1 + Penumbra/Penumbra.cs | 1 + .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 245 ++++++++++++++++++ .../UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 3 +- .../Meta/GlobalEqpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 9 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 14 +- .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 7 +- .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + 32 files changed, 802 insertions(+), 43 deletions(-) create mode 100644 Penumbra/Collections/Cache/AtchCache.cs create mode 100644 Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs create mode 100644 Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs create mode 100644 Penumbra/Interop/Processing/AtchFilePostProcessor.cs create mode 100644 Penumbra/Interop/Processing/AtchPathPreProcessor.cs create mode 100644 Penumbra/Meta/AtchManager.cs create mode 100644 Penumbra/Meta/Manipulations/AtchIdentifier.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 07d18f7f..2b0c7f3b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07d18f7f7218811956e6663592e53c4145f2d862 +Subproject commit 2b0c7f3bee0bc2eb466540d2fac265804354493d diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 6f3ed51e..217cb1e3 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -5,6 +5,7 @@ using OtterGui; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -66,6 +67,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver 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))); + MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); } return Functions.ToCompressedBase64(array, 0); @@ -97,6 +99,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver WriteCache(zipStream, cache.Est); WriteCache(zipStream, cache.Rsp); WriteCache(zipStream, cache.Gmp); + WriteCache(zipStream, cache.Atch); cache.GlobalEqp.EnterReadLock(); try @@ -246,6 +249,15 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver return false; } + var atchCount = r.ReadInt32(); + for (var i = 0; i < atchCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + var globalEqpCount = r.ReadInt32(); for (var i = 0; i < globalEqpCount; ++i) { diff --git a/Penumbra/Collections/Cache/AtchCache.cs b/Penumbra/Collections/Cache/AtchCache.cs new file mode 100644 index 00000000..9e0f6caf --- /dev/null +++ b/Penumbra/Collections/Cache/AtchCache.cs @@ -0,0 +1,122 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class AtchCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + private readonly Dictionary)> _atchFiles = []; + + public bool HasFile(GenderRace gr) + => _atchFiles.ContainsKey(gr); + + public bool GetFile(GenderRace gr, [NotNullWhen(true)] out AtchFile? file) + { + if (!_atchFiles.TryGetValue(gr, out var p)) + { + file = null; + return false; + } + + file = p.Item1; + return true; + } + + public void Reset() + { + foreach (var (_, (_, set)) in _atchFiles) + set.Clear(); + + _atchFiles.Clear(); + Clear(); + } + + protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry) + { + ++Collection.AtchChangeCounter; + ApplyFile(identifier, entry); + } + + private void ApplyFile(AtchIdentifier identifier, AtchEntry entry) + { + try + { + if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) + { + if (!Manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile)) + throw new Exception($"Invalid Atch File for {identifier.GenderRace.ToName()} requested."); + + pair = (baseFile.Clone(), []); + } + + + if (!Apply(pair.Item1, identifier, entry)) + return; + + pair.Item2.Add(identifier); + _atchFiles[identifier.GenderRace] = pair; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not apply ATCH Manipulation {identifier}:\n{e}"); + } + } + + protected override void RevertModInternal(AtchIdentifier identifier) + { + ++Collection.AtchChangeCounter; + if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) + return; + + if (!pair.Item2.Remove(identifier)) + return; + + if (pair.Item2.Count == 0) + { + _atchFiles.Remove(identifier.GenderRace); + return; + } + + var def = GetDefault(Manager, identifier); + if (def == null) + throw new Exception($"Reverting an .atch mod had no default value for the identifier to revert to."); + + Apply(pair.Item1, identifier, def.Value); + } + + public static AtchEntry? GetDefault(MetaFileManager manager, AtchIdentifier identifier) + { + if (!manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile)) + return null; + + if (baseFile.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point) + return null; + + if (point.Entries.Length <= identifier.EntryIndex) + return null; + + return point.Entries[identifier.EntryIndex]; + } + + public static bool Apply(AtchFile file, AtchIdentifier identifier, in AtchEntry entry) + { + if (file.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point) + return false; + + if (point.Entries.Length <= identifier.EntryIndex) + return false; + + point.Entries[identifier.EntryIndex] = entry; + return true; + } + + protected override void Dispose(bool _) + { + Clear(); + _atchFiles.Clear(); + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index abc0dff8..64cf54ea 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -228,20 +228,25 @@ public sealed class CollectionCache : IDisposable foreach (var (path, file) in files.FileRedirections) AddFile(path, file, 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 (files.Manipulations.Count > 0) + { + 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, entry) in files.Manipulations.Atch) + AddManipulation(mod, identifier, entry); + foreach (var identifier in files.Manipulations.GlobalEqp) + AddManipulation(mod, identifier, null!); + } if (addMetaChanges) { diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 05a94ac5..7d8586c3 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,4 +1,5 @@ using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; @@ -14,11 +15,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly GmpCache Gmp = new(manager, collection); public readonly RspCache Rsp = new(manager, collection); public readonly ImcCache Imc = new(manager, collection); + public readonly AtchCache Atch = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -27,6 +29,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .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(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -37,6 +40,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Gmp.Reset(); Rsp.Reset(); Imc.Reset(); + Atch.Reset(); GlobalEqp.Clear(); } @@ -52,6 +56,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Gmp.Dispose(); Rsp.Dispose(); Imc.Dispose(); + Atch.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -65,6 +70,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) 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), + AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -85,6 +91,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) GmpIdentifier i => Gmp.RevertMod(i, out mod), ImcIdentifier i => Imc.RevertMod(i, out mod), RspIdentifier i => Rsp.RevertMod(i, out mod), + AtchIdentifier i => Atch.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -100,6 +107,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) 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), + AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Interop/Hooks/DebugHook.cs b/Penumbra/Interop/Hooks/DebugHook.cs index db14805c..fe9754f9 100644 --- a/Penumbra/Interop/Hooks/DebugHook.cs +++ b/Penumbra/Interop/Hooks/DebugHook.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using OtterGui.Services; +using Penumbra.Interop.Structs; namespace Penumbra.Interop.Hooks; @@ -31,12 +32,13 @@ public sealed unsafe class DebugHook : IHookService public bool Finished => _task?.IsCompletedSuccessfully ?? true; - private delegate void Delegate(nint a, int b, nint c, float* d); + private delegate nint Delegate(ResourceHandle* a, int b, int c); - private void Detour(nint a, int b, nint c, float* d) + private nint Detour(ResourceHandle* a, int b, int c) { - _task!.Result.Original(a, b, c, d); - Penumbra.Log.Information($"[Debug Hook] Results with 0x{a:X} {b} {c:X} {d[0]} {d[1]} {d[2]} {d[3]}."); + var ret = _task!.Result.Original(a, b, c); + Penumbra.Log.Information($"[Debug Hook] Results with 0x{(nint)a:X}, {b}, {c} -> 0x{ret:X}."); + return ret; } } #endif diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 3deeb107..63d93c9d 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -62,6 +62,8 @@ public class HookOverrides public bool SetupVisor; public bool UpdateModel; public bool UpdateRender; + public bool AtchCaller1; + public bool AtchCaller2; } public struct ObjectHooks diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs new file mode 100644 index 00000000..748ca93a --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -0,0 +1,39 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class AtchCallerHook1 : FastHook, IDisposable +{ + public delegate void Delegate(DrawObjectData* data, uint slot, nint unk, Model playerModel); + + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public AtchCallerHook1(HookManager hooks, MetaState metaState, CollectionResolver collectionResolver) + { + _metaState = metaState; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("AtchCaller1", Sigs.AtchCaller1, Detour, + metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller1); + if (!HookOverrides.Instance.Meta.AtchCaller1) + _metaState.Config.ModsEnabled += Toggle; + } + + private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel) + { + var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + _metaState.AtchCollection.Push(collection); + Task.Result.Original(data, slot, unk, playerModel); + _metaState.AtchCollection.Pop(); + Penumbra.Log.Excessive( + $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); + } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; +} diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs new file mode 100644 index 00000000..9b3349f2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs @@ -0,0 +1,38 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class AtchCallerHook2 : FastHook, IDisposable +{ + public delegate void Delegate(DrawObjectData* data, uint slot, nint unk, Model playerModel, uint unk2); + + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public AtchCallerHook2(HookManager hooks, MetaState metaState, CollectionResolver collectionResolver) + { + _metaState = metaState; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("AtchCaller2", Sigs.AtchCaller2, Detour, + metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller2); + if (!HookOverrides.Instance.Meta.AtchCaller2) + _metaState.Config.ModsEnabled += Toggle; + } + + private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel, uint unk2) + { + var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + _metaState.AtchCollection.Push(collection); + Task.Result.Original(data, slot, unk, playerModel, unk2); + _metaState.AtchCollection.Pop(); + Penumbra.Log.Excessive( + $"[AtchCaller2] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, {unk2}, identified to {collection.ModCollection.AnonymizedName}."); + } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; +} diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index e709c210..e7fc3176 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -49,6 +49,7 @@ public sealed unsafe class MetaState : IDisposable, IService public readonly Stack EqdpCollection = []; public readonly Stack EstCollection = []; public readonly Stack RspCollection = []; + public readonly Stack AtchCollection = []; public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = []; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index a7af42e3..0b6c8340 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,3 +1,4 @@ +using System.Linq; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; @@ -54,7 +55,7 @@ public class PathResolver : IDisposable, IService // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently. if (resourceType is ResourceType.Atch) - return (null, ResolveData.Invalid); + return ResolveAtch(path); return category switch { @@ -142,4 +143,10 @@ public class PathResolver : IDisposable, IService private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) => (_collectionManager.Active.Interface.ResolvePath(path), _collectionManager.Active.Interface.ToResolveData()); + + public (FullPath?, ResolveData) ResolveAtch(Utf8GamePath gamePath) + { + _metaState.AtchCollection.TryPeek(out var resolveData); + return _preprocessor.PreProcess(resolveData, gamePath.Path, false, ResourceType.Atch, null, gamePath); + } } diff --git a/Penumbra/Interop/Processing/AtchFilePostProcessor.cs b/Penumbra/Interop/Processing/AtchFilePostProcessor.cs new file mode 100644 index 00000000..e4fab022 --- /dev/null +++ b/Penumbra/Interop/Processing/AtchFilePostProcessor.cs @@ -0,0 +1,43 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class AtchFilePostProcessor(CollectionStorage collections, XivFileAllocator allocator) + : IFilePostProcessor +{ + private readonly IFileAllocator _allocator = allocator; + + public ResourceType Type + => ResourceType.Atch; + + public unsafe void PostProcess(ResourceHandle* resource, CiByteString 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 (!AtchPathPreProcessor.TryGetAtchGenderRace(originalGamePath, out var gr)) + return; + + if (!collection.MetaCache.Atch.GetFile(gr, out var file)) + return; + + using var bytes = file.Write(); + var length = (int)bytes.Position; + var alloc = _allocator.Allocate(length, 1); + bytes.GetBuffer().AsSpan(0, length).CopyTo(new Span(alloc, length)); + var (oldData, oldLength) = resource->GetData(); + _allocator.Release((void*)oldData, oldLength); + resource->SetData((nint)alloc, length); + Penumbra.Log.Information($"Post-Processed {originalGamePath} on resource 0x{(nint)resource:X} with {collection} for {gr.ToName()}."); + } +} diff --git a/Penumbra/Interop/Processing/AtchPathPreProcessor.cs b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs new file mode 100644 index 00000000..9a9096f3 --- /dev/null +++ b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs @@ -0,0 +1,44 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class AtchPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Atch; + + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + { + if (!resolveData.Valid) + return resolved; + + if (!TryGetAtchGenderRace(path, out var gr)) + return resolved; + + Penumbra.Log.Information($"Pre-Processed {path} with {resolveData.ModCollection} for {gr.ToName()}."); + if (resolveData.ModCollection.MetaCache?.Atch.GetFile(gr, out var file) == true) + return PathDataHandler.CreateAtch(path, resolveData.ModCollection); + + return resolved; + } + + public static bool TryGetAtchGenderRace(CiByteString originalGamePath, out GenderRace genderRace) + { + if (originalGamePath[^6] != '1' + || originalGamePath[^7] != '0' + || !ushort.TryParse(originalGamePath.Span[^9..^7], out var grInt) + || grInt > 18) + { + genderRace = GenderRace.Unknown; + return false; + } + + genderRace = (GenderRace)(grInt * 100 + 1); + return true; + } +} diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs index 65608ba0..875eb254 100644 --- a/Penumbra/Interop/Processing/GamePathPreProcessService.cs +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -25,8 +25,7 @@ public class GamePathPreProcessService : IService public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, CiByteString path, bool nonDefault, ResourceType type, - FullPath? resolved, - Utf8GamePath originalPath) + FullPath? resolved, Utf8GamePath originalPath) { if (!_processors.TryGetValue(type, out var processor)) return (resolved, resolveData); diff --git a/Penumbra/Meta/AtchManager.cs b/Penumbra/Meta/AtchManager.cs new file mode 100644 index 00000000..68f2f815 --- /dev/null +++ b/Penumbra/Meta/AtchManager.cs @@ -0,0 +1,26 @@ +using System.Collections.Frozen; +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class AtchManager : IService +{ + private static readonly IReadOnlyList GenderRaces = + [ + GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale, GenderRace.ElezenMale, + GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale, GenderRace.RoegadynFemale, + GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale, GenderRace.HrothgarMale, + GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale, + ]; + + public readonly IReadOnlyDictionary AtchFileBase; + + public AtchManager(IDataManager manager) + { + AtchFileBase = GenderRaces.ToFrozenDictionary(gr => gr, + gr => new AtchFile(manager.GetFile($"chara/xls/attachOffset/c{gr.ToRaceCode()}.atch")!.DataSpan)); + } +} diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs new file mode 100644 index 00000000..bce37620 --- /dev/null +++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs @@ -0,0 +1,77 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRace, ushort EntryIndex) + : IComparable, IMetaIdentifier +{ + public Gender Gender + => GenderRace.Split().Item1; + + public ModelRace Race + => GenderRace.Split().Item2; + + public int CompareTo(AtchIdentifier other) + { + var typeComparison = Type.CompareTo(other.Type); + if (typeComparison != 0) + return typeComparison; + + var genderRaceComparison = GenderRace.CompareTo(other.GenderRace); + if (genderRaceComparison != 0) + return genderRaceComparison; + + return EntryIndex.CompareTo(other.EntryIndex); + } + + public override string ToString() + => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing specific + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + var race = (int)GenderRace / 100; + var remainder = (int)GenderRace - 100 * race; + if (remainder != 1) + return false; + + return race is >= 0 and <= 18; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["Type"] = Type.ToAbbreviation(); + jObj["Index"] = EntryIndex; + return jObj; + } + + public static AtchIdentifier? FromJson(JObject jObj) + { + var gender = jObj["Gender"]?.ToObject() ?? Gender.Unknown; + var race = jObj["Race"]?.ToObject() ?? ModelRace.Unknown; + var type = AtchExtensions.FromString(jObj["Type"]?.ToObject() ?? string.Empty); + var entryIndex = jObj["Index"]?.ToObject() ?? ushort.MaxValue; + if (entryIndex == ushort.MaxValue || type is AtchType.Unknown) + return null; + + var ret = new AtchIdentifier(type, Names.CombinedRace(gender, race), entryIndex); + return ret.Validate() ? ret : null; + } + + MetaManipulationType IMetaIdentifier.Type + => MetaManipulationType.Atch; +} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index d1668a4d..999fd906 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -14,6 +14,7 @@ public enum MetaManipulationType : byte Gmp = 5, Rsp = 6, GlobalEqp = 7, + Atch = 8, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index da061bec..ca45c777 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Structs; using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; @@ -16,6 +17,7 @@ public class MetaDictionary private readonly Dictionary _est = []; private readonly Dictionary _rsp = []; private readonly Dictionary _gmp = []; + private readonly Dictionary _atch = []; private readonly HashSet _globalEqp = []; public IReadOnlyDictionary Imc @@ -36,6 +38,9 @@ public class MetaDictionary public IReadOnlyDictionary Rsp => _rsp; + public IReadOnlyDictionary Atch + => _atch; + public IReadOnlySet GlobalEqp => _globalEqp; @@ -50,6 +55,7 @@ public class MetaDictionary MetaManipulationType.Est => _est.Count, MetaManipulationType.Gmp => _gmp.Count, MetaManipulationType.Rsp => _rsp.Count, + MetaManipulationType.Atch => _atch.Count, MetaManipulationType.GlobalEqp => _globalEqp.Count, _ => 0, }; @@ -63,6 +69,7 @@ public class MetaDictionary GlobalEqpManipulation i => _globalEqp.Contains(i), GmpIdentifier i => _gmp.ContainsKey(i), ImcIdentifier i => _imc.ContainsKey(i), + AtchIdentifier i => _atch.ContainsKey(i), RspIdentifier i => _rsp.ContainsKey(i), _ => false, }; @@ -76,6 +83,7 @@ public class MetaDictionary _est.Clear(); _rsp.Clear(); _gmp.Clear(); + _atch.Clear(); _globalEqp.Clear(); } @@ -88,6 +96,7 @@ public class MetaDictionary _est.Clear(); _rsp.Clear(); _gmp.Clear(); + _atch.Clear(); } public bool Equals(MetaDictionary other) @@ -98,6 +107,7 @@ public class MetaDictionary && _est.SetEquals(other._est) && _rsp.SetEquals(other._rsp) && _gmp.SetEquals(other._gmp) + && _atch.SetEquals(other._atch) && _globalEqp.SetEquals(other._globalEqp); public IEnumerable Identifiers @@ -107,6 +117,7 @@ public class MetaDictionary .Concat(_est.Keys.Cast()) .Concat(_gmp.Keys.Cast()) .Concat(_rsp.Keys.Cast()) + .Concat(_atch.Keys.Cast()) .Concat(_globalEqp.Cast()); #region TryAdd @@ -171,6 +182,15 @@ public class MetaDictionary return true; } + public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry) + { + if (!_atch.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { if (!_globalEqp.Add(identifier)) @@ -244,6 +264,15 @@ public class MetaDictionary return true; } + public bool Update(AtchIdentifier identifier, in AtchEntry entry) + { + if (!_atch.ContainsKey(identifier)) + return false; + + _atch[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -266,6 +295,9 @@ public class MetaDictionary public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) => _imc.TryGetValue(identifier, out value); + public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) + => _atch.TryGetValue(identifier, out value); + #endregion public bool Remove(IMetaIdentifier identifier) @@ -279,6 +311,7 @@ public class MetaDictionary GmpIdentifier i => _gmp.Remove(i), ImcIdentifier i => _imc.Remove(i), RspIdentifier i => _rsp.Remove(i), + AtchIdentifier i => _atch.Remove(i), _ => false, }; if (ret) @@ -308,6 +341,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._est) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._atch) + TryAdd(identifier, entry); + foreach (var identifier in manips._globalEqp) TryAdd(identifier); } @@ -351,6 +387,12 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; @@ -369,8 +411,9 @@ public class MetaDictionary _est.SetTo(other._est); _rsp.SetTo(other._rsp); _gmp.SetTo(other._gmp); + _atch.SetTo(other._atch); _globalEqp.SetTo(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; } public void UpdateTo(MetaDictionary other) @@ -381,8 +424,9 @@ public class MetaDictionary _est.UpdateTo(other._est); _rsp.UpdateTo(other._rsp); _gmp.UpdateTo(other._gmp); + _atch.UpdateTo(other._atch); _globalEqp.UnionWith(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; } #endregion @@ -460,6 +504,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(AtchIdentifier identifier, AtchEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Atch.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.ToJson(), + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -487,6 +541,8 @@ public class MetaDictionary 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(AtchIdentifier) && typeof(TEntry) == typeof(AtchEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -531,6 +587,7 @@ public class MetaDictionary SerializeTo(array, value._est); SerializeTo(array, value._rsp); SerializeTo(array, value._gmp); + SerializeTo(array, value._atch); SerializeTo(array, value._globalEqp); array.WriteTo(writer); } @@ -618,6 +675,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); break; } + case MetaManipulationType.Atch: + { + var identifier = AtchIdentifier.FromJson(manip); + var entry = AtchEntry.FromJson(manip["Entry"] as JObject); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid ATCH Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); @@ -648,6 +715,7 @@ public class MetaDictionary _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); + _atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); Count = cache.Count; } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 3755afa2..5250273b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -5,6 +5,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Data; using Penumbra.Import; +using Penumbra.Interop.Hooks.Meta; using Penumbra.Interop.Services; using Penumbra.Meta.Files; using Penumbra.Mods; @@ -25,13 +26,14 @@ public class MetaFileManager : IService internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; internal readonly ImcChecker ImcChecker; + internal readonly AtchManager AtchManager; 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, - FileCompactor compactor, IGameInteropProvider interop) + FileCompactor compactor, IGameInteropProvider interop, AtchManager atchManager) { CharacterUtility = characterUtility; ResidentResources = residentResources; @@ -41,6 +43,7 @@ public class MetaFileManager : IService ValidityChecker = validityChecker; Identifier = identifier; Compactor = compactor; + AtchManager = atchManager; ImcChecker = new ImcChecker(this); XivAllocator = new XivFileAllocator(interop); interop.InitializeFromAttributes(this); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 217ba93d..7a5142dc 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -58,6 +58,7 @@ public class ModMetaEditor( 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.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch)); OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 917dba6c..534911df 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData.Data; +using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs new file mode 100644 index 00000000..4cf01faa --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -0,0 +1,245 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class AtchMetaDrawer : MetaDrawer, IService +{ + public override ReadOnlySpan Label + => "Attachment Points (ATCH)###ATCH"u8; + + public override int NumColumns + => 10; + + public override float ColumnHeight + => 2 * ImUtf8.FrameHeightSpacing; + + private AtchFile? _currentBaseAtchFile; + private AtchPoint? _currentBaseAtchPoint; + private AtchPointCombo _combo; + + public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : base(editor, metaFiles) + { + _combo = new AtchPointCombo(() => _currentBaseAtchFile?.Points.Select(p => p.Type).ToList() ?? []); + } + + private sealed class AtchPointCombo(Func> generator) + : FilterComboCache(generator, MouseWheelType.Control, Penumbra.Log) + { + protected override string ToString(AtchType obj) + => obj.ToName(); + } + + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current ATCH manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Atch))); + + 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(); + + var defaultEntry = AtchCache.GetDefault(MetaFiles, Identifier) ?? default; + DrawEntry(defaultEntry, ref defaultEntry, true); + } + + private void UpdateEntry() + => Entry = _currentBaseAtchPoint!.Entries[Identifier.EntryIndex]; + + protected override void Initialize() + { + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[GenderRace.MidlanderMale]; + _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); + Identifier = new AtchIdentifier(_currentBaseAtchPoint.Type, GenderRace.MidlanderMale, 0); + Entry = _currentBaseAtchPoint.Entries[0]; + } + + protected override void DrawEntry(AtchIdentifier identifier, AtchEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = AtchCache.GetDefault(MetaFiles, identifier) ?? default; + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(AtchIdentifier, AtchEntry)> Enumerate() + => Editor.Atch.Select(kvp => (kvp.Key, kvp.Value)) + .OrderBy(p => p.Key.GenderRace) + .ThenBy(p => p.Key.Type) + .ThenBy(p => p.Key.EntryIndex); + + protected override int Count + => Editor.Atch.Count; + + private bool DrawIdentifierInput(ref AtchIdentifier identifier) + { + var changes = false; + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier, false); + if (changes) + UpdateFile(); + ImGui.TableNextColumn(); + if (DrawPointInput(ref identifier, _combo)) + { + _currentBaseAtchPoint = _currentBaseAtchFile?.GetPoint(identifier.Type); + changes = true; + } + + ImGui.TableNextColumn(); + changes |= DrawEntryIndexInput(ref identifier, _currentBaseAtchPoint!); + + return changes; + } + + private void UpdateFile() + { + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; + _currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type); + if (_currentBaseAtchPoint == null) + { + _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); + Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; + } + + if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length) + Identifier = Identifier with { EntryIndex = 0 }; + } + + private static void DrawIdentifier(AtchIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + DrawGender(ref identifier, true); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor); + ImUtf8.HoverTooltip("Attachment Point Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.EntryIndex.ToString(), FrameColor); + ImUtf8.HoverTooltip("State Entry Index"u8); + } + + private static bool DrawEntry(in AtchEntry defaultEntry, ref AtchEntry entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + if (defaultEntry.Bone.Length == 0) + return false; + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##BoneName"u8, entry.FullSpan, out TerminatedByteString newBone)) + { + entry.SetBoneName(newBone); + changes = true; + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Bone Name"u8); + + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchScale"u8, ref entry.Scale); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Scale"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetX"u8, ref entry.OffsetX); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Offset X-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationX"u8, ref entry.RotationX); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation X-Axis"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetY"u8, ref entry.OffsetY); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Offset Y-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationY"u8, ref entry.RotationY); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation Y-Axis"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetZ"u8, ref entry.OffsetZ); + ImUtf8.HoverTooltip("Offset Z-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationZ"u8, ref entry.RotationZ); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation Z-Axis"u8); + + return changes; + } + + private static bool DrawRace(ref AtchIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##atchRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + + return ret; + } + + private static bool DrawGender(ref AtchIdentifier identifier, bool disabled) + { + var isMale = identifier.Gender is Gender.Male; + + if (!ImUtf8.IconButton(isMale ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus, "Gender"u8, buttonColor: disabled ? 0x000F0000u : 0) + || disabled) + return false; + + identifier = identifier with { GenderRace = Names.CombinedRace(isMale ? Gender.Female : Gender.Male, identifier.Race) }; + return true; + } + + private static bool DrawPointInput(ref AtchIdentifier identifier, AtchPointCombo combo) + { + if (!combo.Draw("##AtchPoint", identifier.Type.ToName(), "Attachment Point Type", 160 * ImUtf8.GlobalScale, + ImGui.GetTextLineHeightWithSpacing())) + return false; + + identifier = identifier with { Type = combo.CurrentSelection }; + return true; + } + + private static bool DrawEntryIndexInput(ref AtchIdentifier identifier, AtchPoint currentAtchPoint) + { + var index = identifier.EntryIndex; + ImGui.SetNextItemWidth(40 * ImUtf8.GlobalScale); + var ret = ImUtf8.DragScalar("##AtchEntry"u8, ref index, 0, (ushort)(currentAtchPoint.Entries.Length - 1), 0.05f, + ImGuiSliderFlags.AlwaysClamp); + ImUtf8.HoverTooltip("State Entry Index"u8); + if (!ret) + return false; + + index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint!.Entries.Length - 1)); + identifier = identifier with { EntryIndex = index }; + return true; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index f9baddbe..348a0d4c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -34,7 +35,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqdp)); + CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Eqdp))); ImGui.TableNextColumn(); var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 51b14459..d6df95cb 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -34,7 +35,7 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqp)); + CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Eqp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 09075319..e5e28a3d 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -33,7 +34,7 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Est)); + CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Est))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 1aa9060e..929feadd 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.Meta; @@ -29,7 +30,7 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.GlobalEqp)); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.GlobalEqp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 9532d8e7..3691a4f7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -8,6 +8,7 @@ using Penumbra.Meta.Files; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Newtonsoft.Json.Linq; namespace Penumbra.UI.AdvancedWindow.Meta; @@ -32,7 +33,7 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Gmp)); + CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Gmp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index c8310cf7..34488a87 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -35,7 +36,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc)); + CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, new Lazy(() => 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; @@ -116,7 +117,6 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); ImUtf8.HoverTooltip("Equip Slot"u8); } - } private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) @@ -161,8 +161,9 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile { var (equipSlot, secondaryId) = type switch { - ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0), - ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), + ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId)0), + ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0), _ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 75de20a7..4c9142d8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -14,8 +14,9 @@ namespace Penumbra.UI.AdvancedWindow.Meta; public interface IMetaDrawer { - public ReadOnlySpan Label { get; } - public int NumColumns { get; } + public ReadOnlySpan Label { get; } + public int NumColumns { get; } + public float ColumnHeight { get; } public void Draw(); } @@ -42,7 +43,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta using var id = ImUtf8.PushId((int)Identifier.Type); DrawNew(); - var height = ImUtf8.FrameHeightSpacing; + var height = ColumnHeight; var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); ImGuiClip.DrawEndDummy(remainder, height); @@ -54,6 +55,9 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta public abstract ReadOnlySpan Label { get; } public abstract int NumColumns { get; } + public virtual float ColumnHeight + => ImUtf8.FrameHeightSpacing; + protected abstract void DrawNew(); protected abstract void Initialize(); protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); @@ -138,14 +142,14 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta protected void DrawMetaButtons(TIdentifier identifier, TEntry entry) { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! }); + CopyToClipboardButton("Copy this manipulation to clipboard."u8, new Lazy(() => 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) + protected void CopyToClipboardButton(ReadOnlySpan tooltip, Lazy manipulations) { if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) return; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index b3dd9299..d1c7cd52 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -10,7 +10,8 @@ public class MetaDrawers( GlobalEqpMetaDrawer globalEqp, GmpMetaDrawer gmp, ImcMetaDrawer imc, - RspMetaDrawer rsp) : IService + RspMetaDrawer rsp, + AtchMetaDrawer atch) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -19,6 +20,7 @@ public class MetaDrawers( public readonly RspMetaDrawer Rsp = rsp; public readonly ImcMetaDrawer Imc = imc; public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; + public readonly AtchMetaDrawer Atch = atch; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -29,7 +31,8 @@ public class MetaDrawers( MetaManipulationType.Est => Est, MetaManipulationType.Gmp => Gmp, MetaManipulationType.Rsp => Rsp, + MetaManipulationType.Atch => Atch, MetaManipulationType.GlobalEqp => GlobalEqp, - _ => null, + _ => null, }; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index 87e8c5b8..d60f877b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -33,7 +34,7 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Rsp)); + CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Rsp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 49eac96e..22271d38 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -56,6 +56,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Est); DrawEditHeader(MetaManipulationType.Gmp); DrawEditHeader(MetaManipulationType.Rsp); + DrawEditHeader(MetaManipulationType.Atch); DrawEditHeader(MetaManipulationType.GlobalEqp); }