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 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<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(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<AtchIdentifier>();
var value = r.Read<AtchEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var globalEqpCount = r.ReadInt32();
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)
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)
{

View file

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

View file

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

View file

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

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> EstCollection = [];
public readonly Stack<ResolveData> RspCollection = [];
public readonly Stack<ResolveData> AtchCollection = [];
public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = [];

View file

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

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,
FullPath? resolved,
Utf8GamePath originalPath)
FullPath? resolved, Utf8GamePath originalPath)
{
if (!_processors.TryGetValue(type, out var processor))
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,
Rsp = 6,
GlobalEqp = 7,
Atch = 8,
}
public interface IMetaIdentifier

View file

@ -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<EstIdentifier, EstEntry> _est = [];
private readonly Dictionary<RspIdentifier, RspEntry> _rsp = [];
private readonly Dictionary<GmpIdentifier, GmpEntry> _gmp = [];
private readonly Dictionary<AtchIdentifier, AtchEntry> _atch = [];
private readonly HashSet<GlobalEqpManipulation> _globalEqp = [];
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
@ -36,6 +38,9 @@ public class MetaDictionary
public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp
=> _rsp;
public IReadOnlyDictionary<AtchIdentifier, AtchEntry> Atch
=> _atch;
public IReadOnlySet<GlobalEqpManipulation> 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<IMetaIdentifier> Identifiers
@ -107,6 +117,7 @@ public class MetaDictionary
.Concat(_est.Keys.Cast<IMetaIdentifier>())
.Concat(_gmp.Keys.Cast<IMetaIdentifier>())
.Concat(_rsp.Keys.Cast<IMetaIdentifier>())
.Concat(_atch.Keys.Cast<IMetaIdentifier>())
.Concat(_globalEqp.Cast<IMetaIdentifier>());
#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<TIdentifier, RspIdentifier>(ref identifier), Unsafe.As<TEntry, RspEntry>(ref entry));
if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry))
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))
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(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;
}

View file

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

View file

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

View file

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

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.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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Eqdp)));
ImGui.TableNextColumn();
var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0;

View file

@ -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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Eqp)));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);

View file

@ -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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Est)));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);

View file

@ -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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.GlobalEqp)));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);

View file

@ -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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Gmp)));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);

View file

@ -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<JToken?>(() => 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),
};

View file

@ -14,8 +14,9 @@ namespace Penumbra.UI.AdvancedWindow.Meta;
public interface IMetaDrawer
{
public ReadOnlySpan<byte> Label { get; }
public int NumColumns { get; }
public ReadOnlySpan<byte> Label { get; }
public int NumColumns { get; }
public float ColumnHeight { get; }
public void Draw();
}
@ -42,7 +43,7 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(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<TIdentifier, TEntry>(ModMetaEditor editor, Meta
public abstract ReadOnlySpan<byte> 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<TIdentifier, TEntry>(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<JToken?>(() => 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<byte> tooltip, JToken? manipulations)
protected void CopyToClipboardButton(ReadOnlySpan<byte> tooltip, Lazy<JToken?> manipulations)
{
if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip))
return;

View file

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

View file

@ -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<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Rsp)));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);

View file

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