Atch stuff.

This commit is contained in:
Ottermandias 2024-11-22 12:19:14 +01:00
parent 65538868c3
commit b1be868a6a
32 changed files with 802 additions and 43 deletions

@ -1 +1 @@
Subproject commit 07d18f7f7218811956e6663592e53c4145f2d862 Subproject commit 2b0c7f3bee0bc2eb466540d2fac265804354493d

View file

@ -5,6 +5,7 @@ using OtterGui;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Files.Utility;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
@ -66,6 +67,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
} }
return Functions.ToCompressedBase64(array, 0); return Functions.ToCompressedBase64(array, 0);
@ -97,6 +99,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
WriteCache(zipStream, cache.Est); WriteCache(zipStream, cache.Est);
WriteCache(zipStream, cache.Rsp); WriteCache(zipStream, cache.Rsp);
WriteCache(zipStream, cache.Gmp); WriteCache(zipStream, cache.Gmp);
WriteCache(zipStream, cache.Atch);
cache.GlobalEqp.EnterReadLock(); cache.GlobalEqp.EnterReadLock();
try try
@ -246,6 +249,15 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
return false; return false;
} }
var atchCount = r.ReadInt32();
for (var i = 0; i < atchCount; ++i)
{
var identifier = r.Read<AtchIdentifier>();
var value = r.Read<AtchEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var globalEqpCount = r.ReadInt32(); var globalEqpCount = r.ReadInt32();
for (var i = 0; i < globalEqpCount; ++i) for (var i = 0; i < globalEqpCount; ++i)
{ {

View file

@ -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<AtchIdentifier, AtchEntry>(manager, collection)
{
private readonly Dictionary<GenderRace, (AtchFile, HashSet<AtchIdentifier>)> _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();
}
}

View file

@ -228,20 +228,25 @@ public sealed class CollectionCache : IDisposable
foreach (var (path, file) in files.FileRedirections) foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod); AddFile(path, file, mod);
foreach (var (identifier, entry) in files.Manipulations.Eqp) if (files.Manipulations.Count > 0)
AddManipulation(mod, identifier, entry); {
foreach (var (identifier, entry) in files.Manipulations.Eqdp) foreach (var (identifier, entry) in files.Manipulations.Eqp)
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Est) foreach (var (identifier, entry) in files.Manipulations.Eqdp)
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Gmp) foreach (var (identifier, entry) in files.Manipulations.Est)
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Rsp) foreach (var (identifier, entry) in files.Manipulations.Gmp)
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Imc) foreach (var (identifier, entry) in files.Manipulations.Rsp)
AddManipulation(mod, identifier, entry); AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp) foreach (var (identifier, entry) in files.Manipulations.Imc)
AddManipulation(mod, identifier, null!); 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) if (addMetaChanges)
{ {

View file

@ -1,4 +1,5 @@
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -14,11 +15,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
public readonly GmpCache Gmp = new(manager, collection); public readonly GmpCache Gmp = new(manager, collection);
public readonly RspCache Rsp = new(manager, collection); public readonly RspCache Rsp = new(manager, collection);
public readonly ImcCache Imc = new(manager, collection); public readonly ImcCache Imc = new(manager, collection);
public readonly AtchCache Atch = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new(); public readonly GlobalEqpCache GlobalEqp = new();
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
public int Count 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 public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) => 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(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Rsp.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(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))); .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void Reset() public void Reset()
@ -37,6 +40,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Gmp.Reset(); Gmp.Reset();
Rsp.Reset(); Rsp.Reset();
Imc.Reset(); Imc.Reset();
Atch.Reset();
GlobalEqp.Clear(); GlobalEqp.Clear();
} }
@ -52,6 +56,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
Gmp.Dispose(); Gmp.Dispose();
Rsp.Dispose(); Rsp.Dispose();
Imc.Dispose(); Imc.Dispose();
Atch.Dispose();
} }
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) 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), GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod),
ImcIdentifier i => Imc.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), 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), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
_ => false, _ => false,
}; };
@ -85,6 +91,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
GmpIdentifier i => Gmp.RevertMod(i, out mod), GmpIdentifier i => Gmp.RevertMod(i, out mod),
ImcIdentifier i => Imc.RevertMod(i, out mod), ImcIdentifier i => Imc.RevertMod(i, out mod),
RspIdentifier i => Rsp.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), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
_ => (mod = null) != null, _ => (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), GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e),
ImcIdentifier i when entry is ImcEntry e => Imc.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), 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), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
_ => false, _ => false,
}; };

View file

@ -1,5 +1,6 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Hooks; namespace Penumbra.Interop.Hooks;
@ -31,12 +32,13 @@ public sealed unsafe class DebugHook : IHookService
public bool Finished public bool Finished
=> _task?.IsCompletedSuccessfully ?? true; => _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); var ret = _task!.Result.Original(a, b, c);
Penumbra.Log.Information($"[Debug Hook] Results with 0x{a:X} {b} {c:X} {d[0]} {d[1]} {d[2]} {d[3]}."); Penumbra.Log.Information($"[Debug Hook] Results with 0x{(nint)a:X}, {b}, {c} -> 0x{ret:X}.");
return ret;
} }
} }
#endif #endif

View file

@ -62,6 +62,8 @@ public class HookOverrides
public bool SetupVisor; public bool SetupVisor;
public bool UpdateModel; public bool UpdateModel;
public bool UpdateRender; public bool UpdateRender;
public bool AtchCaller1;
public bool AtchCaller2;
} }
public struct ObjectHooks public struct ObjectHooks

View file

@ -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<AtchCallerHook1.Delegate>, 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<Delegate>("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;
}

View file

@ -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<AtchCallerHook2.Delegate>, 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<Delegate>("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;
}

View file

@ -49,6 +49,7 @@ public sealed unsafe class MetaState : IDisposable, IService
public readonly Stack<ResolveData> EqdpCollection = []; public readonly Stack<ResolveData> EqdpCollection = [];
public readonly Stack<ResolveData> EstCollection = []; public readonly Stack<ResolveData> EstCollection = [];
public readonly Stack<ResolveData> RspCollection = []; public readonly Stack<ResolveData> RspCollection = [];
public readonly Stack<ResolveData> AtchCollection = [];
public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = []; public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = [];

View file

@ -1,3 +1,4 @@
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Api.Enums; 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. // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently.
if (resourceType is ResourceType.Atch) if (resourceType is ResourceType.Atch)
return (null, ResolveData.Invalid); return ResolveAtch(path);
return category switch return category switch
{ {
@ -142,4 +143,10 @@ public class PathResolver : IDisposable, IService
private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path)
=> (_collectionManager.Active.Interface.ResolvePath(path), => (_collectionManager.Active.Interface.ResolvePath(path),
_collectionManager.Active.Interface.ToResolveData()); _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);
}
} }

View file

@ -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<byte> additionalData)
{
if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator)
return;
var collection = collections.ByLocalId(data.Collection);
if (collection.MetaCache is not { } cache)
return;
if (!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<byte>(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()}.");
}
}

View file

@ -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;
}
}

View file

@ -25,8 +25,7 @@ public class GamePathPreProcessService : IService
public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, CiByteString path, bool nonDefault, ResourceType type, public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, CiByteString path, bool nonDefault, ResourceType type,
FullPath? resolved, FullPath? resolved, Utf8GamePath originalPath)
Utf8GamePath originalPath)
{ {
if (!_processors.TryGetValue(type, out var processor)) if (!_processors.TryGetValue(type, out var processor))
return (resolved, resolveData); return (resolved, resolveData);

View file

@ -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<GenderRace> 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<GenderRace, AtchFile> AtchFileBase;
public AtchManager(IDataManager manager)
{
AtchFileBase = GenderRaces.ToFrozenDictionary(gr => gr,
gr => new AtchFile(manager.GetFile($"chara/xls/attachOffset/c{gr.ToRaceCode()}.atch")!.DataSpan));
}
}

View file

@ -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<AtchIdentifier>, 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<string, IIdentifiedObjectData?> 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>() ?? Gender.Unknown;
var race = jObj["Race"]?.ToObject<ModelRace>() ?? ModelRace.Unknown;
var type = AtchExtensions.FromString(jObj["Type"]?.ToObject<string>() ?? string.Empty);
var entryIndex = jObj["Index"]?.ToObject<ushort>() ?? 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;
}

View file

@ -14,6 +14,7 @@ public enum MetaManipulationType : byte
Gmp = 5, Gmp = 5,
Rsp = 6, Rsp = 6,
GlobalEqp = 7, GlobalEqp = 7,
Atch = 8,
} }
public interface IMetaIdentifier public interface IMetaIdentifier

View file

@ -1,6 +1,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
using Penumbra.GameData.Files.AtchStructs;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Util; using Penumbra.Util;
using ImcEntry = Penumbra.GameData.Structs.ImcEntry; using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
@ -16,6 +17,7 @@ public class MetaDictionary
private readonly Dictionary<EstIdentifier, EstEntry> _est = []; private readonly Dictionary<EstIdentifier, EstEntry> _est = [];
private readonly Dictionary<RspIdentifier, RspEntry> _rsp = []; private readonly Dictionary<RspIdentifier, RspEntry> _rsp = [];
private readonly Dictionary<GmpIdentifier, GmpEntry> _gmp = []; private readonly Dictionary<GmpIdentifier, GmpEntry> _gmp = [];
private readonly Dictionary<AtchIdentifier, AtchEntry> _atch = [];
private readonly HashSet<GlobalEqpManipulation> _globalEqp = []; private readonly HashSet<GlobalEqpManipulation> _globalEqp = [];
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
@ -36,6 +38,9 @@ public class MetaDictionary
public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp
=> _rsp; => _rsp;
public IReadOnlyDictionary<AtchIdentifier, AtchEntry> Atch
=> _atch;
public IReadOnlySet<GlobalEqpManipulation> GlobalEqp public IReadOnlySet<GlobalEqpManipulation> GlobalEqp
=> _globalEqp; => _globalEqp;
@ -50,6 +55,7 @@ public class MetaDictionary
MetaManipulationType.Est => _est.Count, MetaManipulationType.Est => _est.Count,
MetaManipulationType.Gmp => _gmp.Count, MetaManipulationType.Gmp => _gmp.Count,
MetaManipulationType.Rsp => _rsp.Count, MetaManipulationType.Rsp => _rsp.Count,
MetaManipulationType.Atch => _atch.Count,
MetaManipulationType.GlobalEqp => _globalEqp.Count, MetaManipulationType.GlobalEqp => _globalEqp.Count,
_ => 0, _ => 0,
}; };
@ -63,6 +69,7 @@ public class MetaDictionary
GlobalEqpManipulation i => _globalEqp.Contains(i), GlobalEqpManipulation i => _globalEqp.Contains(i),
GmpIdentifier i => _gmp.ContainsKey(i), GmpIdentifier i => _gmp.ContainsKey(i),
ImcIdentifier i => _imc.ContainsKey(i), ImcIdentifier i => _imc.ContainsKey(i),
AtchIdentifier i => _atch.ContainsKey(i),
RspIdentifier i => _rsp.ContainsKey(i), RspIdentifier i => _rsp.ContainsKey(i),
_ => false, _ => false,
}; };
@ -76,6 +83,7 @@ public class MetaDictionary
_est.Clear(); _est.Clear();
_rsp.Clear(); _rsp.Clear();
_gmp.Clear(); _gmp.Clear();
_atch.Clear();
_globalEqp.Clear(); _globalEqp.Clear();
} }
@ -88,6 +96,7 @@ public class MetaDictionary
_est.Clear(); _est.Clear();
_rsp.Clear(); _rsp.Clear();
_gmp.Clear(); _gmp.Clear();
_atch.Clear();
} }
public bool Equals(MetaDictionary other) public bool Equals(MetaDictionary other)
@ -98,6 +107,7 @@ public class MetaDictionary
&& _est.SetEquals(other._est) && _est.SetEquals(other._est)
&& _rsp.SetEquals(other._rsp) && _rsp.SetEquals(other._rsp)
&& _gmp.SetEquals(other._gmp) && _gmp.SetEquals(other._gmp)
&& _atch.SetEquals(other._atch)
&& _globalEqp.SetEquals(other._globalEqp); && _globalEqp.SetEquals(other._globalEqp);
public IEnumerable<IMetaIdentifier> Identifiers public IEnumerable<IMetaIdentifier> Identifiers
@ -107,6 +117,7 @@ public class MetaDictionary
.Concat(_est.Keys.Cast<IMetaIdentifier>()) .Concat(_est.Keys.Cast<IMetaIdentifier>())
.Concat(_gmp.Keys.Cast<IMetaIdentifier>()) .Concat(_gmp.Keys.Cast<IMetaIdentifier>())
.Concat(_rsp.Keys.Cast<IMetaIdentifier>()) .Concat(_rsp.Keys.Cast<IMetaIdentifier>())
.Concat(_atch.Keys.Cast<IMetaIdentifier>())
.Concat(_globalEqp.Cast<IMetaIdentifier>()); .Concat(_globalEqp.Cast<IMetaIdentifier>());
#region TryAdd #region TryAdd
@ -171,6 +182,15 @@ public class MetaDictionary
return true; 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) public bool TryAdd(GlobalEqpManipulation identifier)
{ {
if (!_globalEqp.Add(identifier)) if (!_globalEqp.Add(identifier))
@ -244,6 +264,15 @@ public class MetaDictionary
return true; return true;
} }
public bool Update(AtchIdentifier identifier, in AtchEntry entry)
{
if (!_atch.ContainsKey(identifier))
return false;
_atch[identifier] = entry;
return true;
}
#endregion #endregion
#region TryGetValue #region TryGetValue
@ -266,6 +295,9 @@ public class MetaDictionary
public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value)
=> _imc.TryGetValue(identifier, out value); => _imc.TryGetValue(identifier, out value);
public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value)
=> _atch.TryGetValue(identifier, out value);
#endregion #endregion
public bool Remove(IMetaIdentifier identifier) public bool Remove(IMetaIdentifier identifier)
@ -279,6 +311,7 @@ public class MetaDictionary
GmpIdentifier i => _gmp.Remove(i), GmpIdentifier i => _gmp.Remove(i),
ImcIdentifier i => _imc.Remove(i), ImcIdentifier i => _imc.Remove(i),
RspIdentifier i => _rsp.Remove(i), RspIdentifier i => _rsp.Remove(i),
AtchIdentifier i => _atch.Remove(i),
_ => false, _ => false,
}; };
if (ret) if (ret)
@ -308,6 +341,9 @@ public class MetaDictionary
foreach (var (identifier, entry) in manips._est) foreach (var (identifier, entry) in manips._est)
TryAdd(identifier, entry); TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._atch)
TryAdd(identifier, entry);
foreach (var identifier in manips._globalEqp) foreach (var identifier in manips._globalEqp)
TryAdd(identifier); TryAdd(identifier);
} }
@ -351,6 +387,12 @@ public class MetaDictionary
return false; 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))) foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier)))
{ {
failedIdentifier = identifier; failedIdentifier = identifier;
@ -369,8 +411,9 @@ public class MetaDictionary
_est.SetTo(other._est); _est.SetTo(other._est);
_rsp.SetTo(other._rsp); _rsp.SetTo(other._rsp);
_gmp.SetTo(other._gmp); _gmp.SetTo(other._gmp);
_atch.SetTo(other._atch);
_globalEqp.SetTo(other._globalEqp); _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) public void UpdateTo(MetaDictionary other)
@ -381,8 +424,9 @@ public class MetaDictionary
_est.UpdateTo(other._est); _est.UpdateTo(other._est);
_rsp.UpdateTo(other._rsp); _rsp.UpdateTo(other._rsp);
_gmp.UpdateTo(other._gmp); _gmp.UpdateTo(other._gmp);
_atch.UpdateTo(other._atch);
_globalEqp.UnionWith(other._globalEqp); _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 #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) public static JObject Serialize(GlobalEqpManipulation identifier)
=> new() => new()
{ {
@ -487,6 +541,8 @@ public class MetaDictionary
return Serialize(Unsafe.As<TIdentifier, RspIdentifier>(ref identifier), Unsafe.As<TEntry, RspEntry>(ref entry)); return Serialize(Unsafe.As<TIdentifier, RspIdentifier>(ref identifier), Unsafe.As<TEntry, RspEntry>(ref entry));
if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry)) if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry))
return Serialize(Unsafe.As<TIdentifier, ImcIdentifier>(ref identifier), Unsafe.As<TEntry, ImcEntry>(ref entry)); return Serialize(Unsafe.As<TIdentifier, ImcIdentifier>(ref identifier), Unsafe.As<TEntry, ImcEntry>(ref entry));
if (typeof(TIdentifier) == typeof(AtchIdentifier) && typeof(TEntry) == typeof(AtchEntry))
return Serialize(Unsafe.As<TIdentifier, AtchIdentifier>(ref identifier), Unsafe.As<TEntry, AtchEntry>(ref entry));
if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) if (typeof(TIdentifier) == typeof(GlobalEqpManipulation))
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier)); return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier));
@ -531,6 +587,7 @@ public class MetaDictionary
SerializeTo(array, value._est); SerializeTo(array, value._est);
SerializeTo(array, value._rsp); SerializeTo(array, value._rsp);
SerializeTo(array, value._gmp); SerializeTo(array, value._gmp);
SerializeTo(array, value._atch);
SerializeTo(array, value._globalEqp); SerializeTo(array, value._globalEqp);
array.WriteTo(writer); array.WriteTo(writer);
} }
@ -618,6 +675,16 @@ public class MetaDictionary
Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); Penumbra.Log.Warning("Invalid RSP Manipulation encountered.");
break; 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: case MetaManipulationType.GlobalEqp:
{ {
var identifier = GlobalEqpManipulation.FromJson(manip); var identifier = GlobalEqpManipulation.FromJson(manip);
@ -648,6 +715,7 @@ public class MetaDictionary
_est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
_gmp = cache.Gmp.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); _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(); _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet();
Count = cache.Count; Count = cache.Count;
} }

View file

@ -5,6 +5,7 @@ using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Import; using Penumbra.Import;
using Penumbra.Interop.Hooks.Meta;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Mods; using Penumbra.Mods;
@ -25,13 +26,14 @@ public class MetaFileManager : IService
internal readonly ObjectIdentification Identifier; internal readonly ObjectIdentification Identifier;
internal readonly FileCompactor Compactor; internal readonly FileCompactor Compactor;
internal readonly ImcChecker ImcChecker; internal readonly ImcChecker ImcChecker;
internal readonly AtchManager AtchManager;
internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator();
internal readonly IFileAllocator XivAllocator; internal readonly IFileAllocator XivAllocator;
public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData,
ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier,
FileCompactor compactor, IGameInteropProvider interop) FileCompactor compactor, IGameInteropProvider interop, AtchManager atchManager)
{ {
CharacterUtility = characterUtility; CharacterUtility = characterUtility;
ResidentResources = residentResources; ResidentResources = residentResources;
@ -41,6 +43,7 @@ public class MetaFileManager : IService
ValidityChecker = validityChecker; ValidityChecker = validityChecker;
Identifier = identifier; Identifier = identifier;
Compactor = compactor; Compactor = compactor;
AtchManager = atchManager;
ImcChecker = new ImcChecker(this); ImcChecker = new ImcChecker(this);
XivAllocator = new XivFileAllocator(interop); XivAllocator = new XivFileAllocator(interop);
interop.InitializeFromAttributes(this); interop.InitializeFromAttributes(this);

View file

@ -58,6 +58,7 @@ public class ModMetaEditor(
OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp)); OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp));
OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est));
OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); 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)); OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp));
} }

View file

@ -21,6 +21,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks;
using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Hooks.ResourceLoading;

View file

@ -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<AtchIdentifier, AtchEntry>, IService
{
public override ReadOnlySpan<byte> 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<IReadOnlyList<AtchType>> generator)
: FilterComboCache<AtchType>(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<JToken?>(() => 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;
}
}

View file

@ -1,6 +1,7 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text; using OtterGui.Text;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -34,7 +35,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); 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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Eqdp)));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0; var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0;

View file

@ -1,5 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text; using OtterGui.Text;
@ -34,7 +35,7 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); 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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Eqp)));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier); var canAdd = !Editor.Contains(Identifier);

View file

@ -1,6 +1,7 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text; using OtterGui.Text;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -33,7 +34,7 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); 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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Est)));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier); var canAdd = !Editor.Contains(Identifier);

View file

@ -1,5 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text; using OtterGui.Text;
using Penumbra.Meta; using Penumbra.Meta;
@ -29,7 +30,7 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); 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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.GlobalEqp)));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier); var canAdd = !Editor.Contains(Identifier);

View file

@ -8,6 +8,7 @@ using Penumbra.Meta.Files;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Newtonsoft.Json.Linq;
namespace Penumbra.UI.AdvancedWindow.Meta; namespace Penumbra.UI.AdvancedWindow.Meta;
@ -32,7 +33,7 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); 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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Gmp)));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier); var canAdd = !Editor.Contains(Identifier);

View file

@ -1,5 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text; using OtterGui.Text;
@ -35,7 +36,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); 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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Imc)));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = _fileExists && !Editor.Contains(Identifier); 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; 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.TextFramed(identifier.EquipSlot.ToName(), FrameColor);
ImUtf8.HoverTooltip("Equip Slot"u8); ImUtf8.HoverTooltip("Equip Slot"u8);
} }
} }
private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) 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 var (equipSlot, secondaryId) = type switch
{ {
ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0), 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.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), ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0),
_ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), _ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId),
}; };

View file

@ -14,8 +14,9 @@ namespace Penumbra.UI.AdvancedWindow.Meta;
public interface IMetaDrawer public interface IMetaDrawer
{ {
public ReadOnlySpan<byte> Label { get; } public ReadOnlySpan<byte> Label { get; }
public int NumColumns { get; } public int NumColumns { get; }
public float ColumnHeight { get; }
public void Draw(); public void Draw();
} }
@ -42,7 +43,7 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
using var id = ImUtf8.PushId((int)Identifier.Type); using var id = ImUtf8.PushId((int)Identifier.Type);
DrawNew(); DrawNew();
var height = ImUtf8.FrameHeightSpacing; var height = ColumnHeight;
var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY());
var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count);
ImGuiClip.DrawEndDummy(remainder, height); ImGuiClip.DrawEndDummy(remainder, height);
@ -54,6 +55,9 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
public abstract ReadOnlySpan<byte> Label { get; } public abstract ReadOnlySpan<byte> Label { get; }
public abstract int NumColumns { get; } public abstract int NumColumns { get; }
public virtual float ColumnHeight
=> ImUtf8.FrameHeightSpacing;
protected abstract void DrawNew(); protected abstract void DrawNew();
protected abstract void Initialize(); protected abstract void Initialize();
protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); protected abstract void DrawEntry(TIdentifier identifier, TEntry entry);
@ -138,14 +142,14 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
protected void DrawMetaButtons(TIdentifier identifier, TEntry entry) protected void DrawMetaButtons(TIdentifier identifier, TEntry entry)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! }); CopyToClipboardButton("Copy this manipulation to clipboard."u8, new Lazy<JToken?>(() => new JArray { MetaDictionary.Serialize(identifier, entry)! }));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8)) if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8))
Editor.Changes |= Editor.Remove(identifier); Editor.Changes |= Editor.Remove(identifier);
} }
protected void CopyToClipboardButton(ReadOnlySpan<byte> tooltip, JToken? manipulations) protected void CopyToClipboardButton(ReadOnlySpan<byte> tooltip, Lazy<JToken?> manipulations)
{ {
if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip))
return; return;

View file

@ -10,7 +10,8 @@ public class MetaDrawers(
GlobalEqpMetaDrawer globalEqp, GlobalEqpMetaDrawer globalEqp,
GmpMetaDrawer gmp, GmpMetaDrawer gmp,
ImcMetaDrawer imc, ImcMetaDrawer imc,
RspMetaDrawer rsp) : IService RspMetaDrawer rsp,
AtchMetaDrawer atch) : IService
{ {
public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqdpMetaDrawer Eqdp = eqdp;
public readonly EqpMetaDrawer Eqp = eqp; public readonly EqpMetaDrawer Eqp = eqp;
@ -19,6 +20,7 @@ public class MetaDrawers(
public readonly RspMetaDrawer Rsp = rsp; public readonly RspMetaDrawer Rsp = rsp;
public readonly ImcMetaDrawer Imc = imc; public readonly ImcMetaDrawer Imc = imc;
public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp;
public readonly AtchMetaDrawer Atch = atch;
public IMetaDrawer? Get(MetaManipulationType type) public IMetaDrawer? Get(MetaManipulationType type)
=> type switch => type switch
@ -29,7 +31,8 @@ public class MetaDrawers(
MetaManipulationType.Est => Est, MetaManipulationType.Est => Est,
MetaManipulationType.Gmp => Gmp, MetaManipulationType.Gmp => Gmp,
MetaManipulationType.Rsp => Rsp, MetaManipulationType.Rsp => Rsp,
MetaManipulationType.Atch => Atch,
MetaManipulationType.GlobalEqp => GlobalEqp, MetaManipulationType.GlobalEqp => GlobalEqp,
_ => null, _ => null,
}; };
} }

View file

@ -1,6 +1,7 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text; using OtterGui.Text;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -33,7 +34,7 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); 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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Rsp)));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier); var canAdd = !Editor.Contains(Identifier);

View file

@ -56,6 +56,7 @@ public partial class ModEditWindow
DrawEditHeader(MetaManipulationType.Est); DrawEditHeader(MetaManipulationType.Est);
DrawEditHeader(MetaManipulationType.Gmp); DrawEditHeader(MetaManipulationType.Gmp);
DrawEditHeader(MetaManipulationType.Rsp); DrawEditHeader(MetaManipulationType.Rsp);
DrawEditHeader(MetaManipulationType.Atch);
DrawEditHeader(MetaManipulationType.GlobalEqp); DrawEditHeader(MetaManipulationType.GlobalEqp);
} }