Get rid off all MetaManipulation things.

This commit is contained in:
Ottermandias 2024-06-14 13:38:36 +02:00
parent 361082813b
commit 3170edfeb6
63 changed files with 2422 additions and 2847 deletions

View file

@ -1,5 +1,8 @@
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
@ -7,17 +10,34 @@ namespace Penumbra.Api.Api;
public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
{
public const int CurrentVersion = 0;
public string GetPlayerMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
return CompressMetaManipulations(collection);
}
public string GetMetaManipulations(int gameObjectIdx)
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
var set = collection.MetaCache?.Manipulations.ToArray() ?? [];
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
return CompressMetaManipulations(collection);
}
internal static string CompressMetaManipulations(ModCollection collection)
{
var array = new JArray();
if (collection.MetaCache is { } cache)
{
MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key));
MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair<ImcIdentifier, ImcEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair<EqpIdentifier, EqpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair<EqdpIdentifier, EqdpEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair<EstIdentifier, EstEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
}
return Functions.ToCompressedBase64(array, CurrentVersion);
}
}

View file

@ -163,11 +163,11 @@ public class TemporaryApi(
{
if (manipString.Length == 0)
{
manips = [];
manips = new MetaDictionary();
return true;
}
if (Functions.FromCompressedBase64(manipString, out manips!) == MetaManipulation.CurrentVersion)
if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion)
return true;
manips = null;

View file

@ -5,6 +5,7 @@ using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
@ -102,8 +103,7 @@ public class TemporaryIpcTester(
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>(),
MetaManipulation.CurrentVersion);
var manips = MetaApi.CompressMetaManipulations(copyCollection);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
}
@ -188,8 +188,8 @@ public class TemporaryIpcTester(
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
foreach (var manip in mod.Default.Manipulations)
ImGui.TextUnformatted(manip.ToString());
foreach (var identifier in mod.Default.Manipulations.Identifiers)
ImGui.TextUnformatted(identifier.ToString());
}
}
}

View file

@ -1,56 +0,0 @@
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct CmpCache : IDisposable
{
private CmpFile? _cmpFile = null;
private readonly List<RspManipulation> _cmpManipulations = new();
public CmpCache()
{ }
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public void Reset()
{
if (_cmpFile == null)
return;
_cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute)));
_cmpManipulations.Clear();
}
public bool ApplyMod(MetaFileManager manager, RspManipulation manip)
{
_cmpManipulations.AddOrReplace(manip);
_cmpFile ??= new CmpFile(manager);
return manip.Apply(_cmpFile);
}
public bool RevertMod(MetaFileManager manager, RspManipulation manip)
{
if (!_cmpManipulations.Remove(manip))
return false;
var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute);
manip = new RspManipulation(manip.SubRace, manip.Attribute, def);
return manip.Apply(_cmpFile!);
}
public void Dispose()
{
_cmpFile?.Dispose();
_cmpFile = null;
_cmpManipulations.Clear();
}
}

View file

@ -36,7 +36,7 @@ public sealed class CollectionCache : IDisposable
=> ConflictDict.Values;
public SingleArray<ModConflicts> Conflicts(IMod mod)
=> ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray<ModConflicts>();
=> ConflictDict.TryGetValue(mod, out SingleArray<ModConflicts> c) ? c : new SingleArray<ModConflicts>();
private int _changedItemsSaveCounter = -1;
@ -233,8 +233,20 @@ public sealed class CollectionCache : IDisposable
foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod);
foreach (var manip in files.Manipulations)
AddManipulation(manip, mod);
foreach (var (identifier, entry) in files.Manipulations.Eqp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Eqdp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Est)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Gmp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Rsp)
AddManipulation(mod, identifier, entry);
foreach (var (identifier, entry) in files.Manipulations.Imc)
AddManipulation(mod, identifier, entry);
foreach (var identifier in files.Manipulations.GlobalEqp)
AddManipulation(mod, identifier, null!);
if (addMetaChanges)
{
@ -342,7 +354,7 @@ public sealed class CollectionCache : IDisposable
foreach (var conflict in tmpConflicts)
{
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0)
|| data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0)
AddConflict(data, addedMod, conflict.Mod2);
}
@ -374,12 +386,12 @@ public sealed class CollectionCache : IDisposable
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddManipulation(MetaManipulation manip, IMod mod)
private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry)
{
if (!Meta.TryGetValue(manip, out var existingMod))
if (!Meta.TryGetMod(identifier, out var existingMod))
{
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
return;
}
@ -387,11 +399,11 @@ public sealed class CollectionCache : IDisposable
if (mod == existingMod)
return;
if (AddConflict(manip, mod, existingMod))
if (AddConflict(identifier, mod, existingMod))
{
ModData.RemoveManip(existingMod, manip);
Meta.ApplyMod(manip, mod);
ModData.AddManip(mod, manip);
ModData.RemoveManip(existingMod, identifier);
Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, identifier);
}
}
@ -437,9 +449,9 @@ public sealed class CollectionCache : IDisposable
AddItems(modPath.Mod);
}
foreach (var (manip, mod) in Meta)
foreach (var (manip, mod) in Meta.IdentifierSources)
{
identifier.MetaChangedItems(items, manip);
manip.AddChangedItems(identifier, items);
AddItems(mod);
}

View file

@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache;
/// </summary>
public class CollectionModData
{
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<MetaManipulation>)> _data = new();
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<IMetaIdentifier>)> _data = new();
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<MetaManipulation>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<MetaManipulation>)kvp.Value.Item2));
public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<IMetaIdentifier>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<IMetaIdentifier>)kvp.Value.Item2));
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<MetaManipulation> Manipulations) RemoveMod(IMod mod)
public (IReadOnlyCollection<Utf8GamePath> Paths, IReadOnlyCollection<IMetaIdentifier> Manipulations) RemoveMod(IMod mod)
{
if (_data.Remove(mod, out var data))
return data;
@ -35,7 +35,7 @@ public class CollectionModData
}
}
public void AddManip(IMod mod, MetaManipulation manipulation)
public void AddManip(IMod mod, IMetaIdentifier manipulation)
{
if (_data.TryGetValue(mod, out var data))
{
@ -54,7 +54,7 @@ public class CollectionModData
_data.Remove(mod);
}
public void RemoveManip(IMod mod, MetaManipulation manip)
public void RemoveManip(IMod mod, IMetaIdentifier manip)
{
if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
_data.Remove(mod);

View file

@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
@ -10,28 +10,38 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public readonly struct EqdpCache : IDisposable
public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqdpIdentifier, EqdpEntry>(manager, collection)
{
private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
private readonly List<EqdpManipulation> _eqdpManipulations = new();
private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
public EqdpCache()
{ }
public void SetFiles(MetaFileManager manager)
public override void SetFiles()
{
for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i)
manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]);
Manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
public void SetFile(MetaIndex index)
{
var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
if (i != -1)
manager.SetFile(_eqdpFiles[i], index);
Manager.SetFile(_eqdpFiles[i], index);
}
public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory)
public override void ResetFiles()
=> Manager.SetFile(null, MetaIndex.Eqp);
protected override void IncorporateChangesInternal()
{
foreach (var (identifier, (_, entry)) in this)
Apply(GetFile(identifier)!, identifier, entry);
Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQDP manipulations.");
}
public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory)
=> _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
public MetaList.MetaReverter? TemporarilySetFile(GenderRace genderRace, bool accessory)
{
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
if (idx < 0)
@ -47,44 +57,44 @@ public readonly struct EqdpCache : IDisposable
return null;
}
return manager.TemporarilySetFile(_eqdpFiles[i], idx);
return Manager.TemporarilySetFile(_eqdpFiles[i], idx);
}
public void Reset()
public override void Reset()
{
foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>())
{
var relevant = CharacterUtility.RelevantIndices[file.Index.Value];
file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId));
file.Reset(Keys.Where(m => m.FileIndex() == relevant).Select(m => m.SetId));
}
_eqdpManipulations.Clear();
Clear();
}
public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry)
{
_eqdpManipulations.AddOrReplace(manip);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??=
new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar
return manip.Apply(file);
if (GetFile(identifier) is { } file)
Apply(file, identifier, entry);
}
public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
protected override void RevertModInternal(EqdpIdentifier identifier)
{
if (!_eqdpManipulations.Remove(manip))
if (GetFile(identifier) is { } file)
Apply(file, identifier, ExpandedEqdpFile.GetDefault(Manager, identifier));
}
public static bool Apply(ExpandedEqdpFile file, EqdpIdentifier identifier, EqdpEntry entry)
{
var origEntry = file[identifier.SetId];
var mask = Eqdp.Mask(identifier.Slot);
if ((origEntry & mask) == entry)
return false;
var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!;
manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
return manip.Apply(file);
file[identifier.SetId] = (entry & ~mask) | origEntry;
return true;
}
public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory)
=> _eqdpFiles
[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
public void Dispose()
protected override void Dispose(bool _)
{
for (var i = 0; i < _eqdpFiles.Length; ++i)
{
@ -92,6 +102,15 @@ public readonly struct EqdpCache : IDisposable
_eqdpFiles[i] = null;
}
_eqdpManipulations.Clear();
Clear();
}
private ExpandedEqdpFile? GetFile(EqdpIdentifier identifier)
{
if (!Manager.CharacterUtility.Ready)
return null;
var index = Array.IndexOf(CharacterUtilityData.EqdpIndices, identifier.FileIndex());
return _eqdpFiles[index] ??= new ExpandedEqdpFile(Manager, identifier.GenderRace, identifier.Slot.IsAccessory());
}
}

View file

@ -1,4 +1,4 @@
using OtterGui.Filesystem;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
@ -7,54 +7,77 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EqpCache : IDisposable
public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EqpIdentifier, EqpEntry>(manager, collection)
{
private ExpandedEqpFile? _eqpFile = null;
private readonly List<EqpManipulation> _eqpManipulations = new();
private ExpandedEqpFile? _eqpFile;
public EqpCache()
{ }
public override void SetFiles()
=> Manager.SetFile(_eqpFile, MetaIndex.Eqp);
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_eqpFile, MetaIndex.Eqp);
public override void ResetFiles()
=> Manager.SetFile(null, MetaIndex.Eqp);
public static void ResetFiles(MetaFileManager manager)
=> manager.SetFile(null, MetaIndex.Eqp);
protected override void IncorporateChangesInternal()
{
if (GetFile() is not { } file)
return;
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp);
foreach (var (identifier, (_, entry)) in this)
Apply(file, identifier, entry);
public void Reset()
Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQP manipulations.");
}
public MetaList.MetaReverter TemporarilySetFile()
=> Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp);
public override void Reset()
{
if (_eqpFile == null)
return;
_eqpFile.Reset(_eqpManipulations.Select(m => m.SetId));
_eqpManipulations.Clear();
_eqpFile.Reset(Keys.Select(identifier => identifier.SetId));
Clear();
}
public bool ApplyMod(MetaFileManager manager, EqpManipulation manip)
protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry)
{
_eqpManipulations.AddOrReplace(manip);
_eqpFile ??= new ExpandedEqpFile(manager);
return manip.Apply(_eqpFile);
if (GetFile() is { } file)
Apply(file, identifier, entry);
}
public bool RevertMod(MetaFileManager manager, EqpManipulation manip)
protected override void RevertModInternal(EqpIdentifier identifier)
{
var idx = _eqpManipulations.FindIndex(manip.Equals);
if (idx < 0)
if (GetFile() is { } file)
Apply(file, identifier, ExpandedEqpFile.GetDefault(Manager, identifier.SetId));
}
public static bool Apply(ExpandedEqpFile file, EqpIdentifier identifier, EqpEntry entry)
{
var origEntry = file[identifier.SetId];
var mask = Eqp.Mask(identifier.Slot);
if ((origEntry & mask) == entry)
return false;
var def = ExpandedEqpFile.GetDefault(manager, manip.SetId);
manip = new EqpManipulation(def, manip.Slot, manip.SetId);
return manip.Apply(_eqpFile!);
file[identifier.SetId] = (origEntry & ~mask) | entry;
return true;
}
public void Dispose()
protected override void Dispose(bool _)
{
_eqpFile?.Dispose();
_eqpFile = null;
_eqpManipulations.Clear();
Clear();
}
private ExpandedEqpFile? GetFile()
{
if (_eqpFile != null)
return _eqpFile;
if (!Manager.CharacterUtility.Ready)
return null;
return _eqpFile = new ExpandedEqpFile(Manager);
}
}

View file

@ -1,6 +1,3 @@
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
@ -9,46 +6,41 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct EstCache : IDisposable
public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<EstIdentifier, EstEntry>(manager, collection)
{
private EstFile? _estFaceFile = null;
private EstFile? _estHairFile = null;
private EstFile? _estBodyFile = null;
private EstFile? _estHeadFile = null;
private EstFile? _estFaceFile;
private EstFile? _estHairFile;
private EstFile? _estBodyFile;
private EstFile? _estHeadFile;
private readonly List<EstManipulation> _estManipulations = new();
public EstCache()
{ }
public void SetFiles(MetaFileManager manager)
public override void SetFiles()
{
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
manager.SetFile(_estHairFile, MetaIndex.HairEst);
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
Manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
Manager.SetFile(_estHairFile, MetaIndex.HairEst);
Manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
Manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
}
public void SetFile(MetaFileManager manager, MetaIndex index)
public void SetFile(MetaIndex index)
{
switch (index)
{
case MetaIndex.FaceEst:
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
Manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
break;
case MetaIndex.HairEst:
manager.SetFile(_estHairFile, MetaIndex.HairEst);
Manager.SetFile(_estHairFile, MetaIndex.HairEst);
break;
case MetaIndex.BodyEst:
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
Manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
break;
case MetaIndex.HeadEst:
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
Manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
break;
}
}
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type)
public MetaList.MetaReverter TemporarilySetFiles(EstType type)
{
var (file, idx) = type switch
{
@ -56,74 +48,65 @@ public struct EstCache : IDisposable
EstType.Hair => (_estHairFile, MetaIndex.HairEst),
EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
EstType.Head => (_estHeadFile, MetaIndex.HeadEst),
_ => (null, 0),
_ => (null, 0),
};
return manager.TemporarilySetFile(file, idx);
return Manager.TemporarilySetFile(file, idx);
}
private readonly EstFile? GetEstFile(EstType type)
public override void ResetFiles()
=> Manager.SetFile(null, MetaIndex.Eqp);
protected override void IncorporateChangesInternal()
{
return type switch
{
EstType.Face => _estFaceFile,
EstType.Hair => _estHairFile,
EstType.Body => _estBodyFile,
EstType.Head => _estHeadFile,
_ => null,
};
if (!Manager.CharacterUtility.Ready)
return;
foreach (var (identifier, (_, entry)) in this)
Apply(GetFile(identifier)!, identifier, entry);
Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EST manipulations.");
}
internal EstEntry GetEstEntry(MetaFileManager manager, EstType type, GenderRace genderRace, PrimaryId primaryId)
public EstEntry GetEstEntry(EstIdentifier identifier)
{
var file = GetEstFile(type);
var file = GetFile(identifier);
return file != null
? file[genderRace, primaryId.Id]
: EstFile.GetDefault(manager, type, genderRace, primaryId);
? file[identifier.GenderRace, identifier.SetId]
: EstFile.GetDefault(Manager, identifier);
}
public void Reset()
public override void Reset()
{
_estFaceFile?.Reset();
_estHairFile?.Reset();
_estBodyFile?.Reset();
_estHeadFile?.Reset();
_estManipulations.Clear();
Clear();
}
public bool ApplyMod(MetaFileManager manager, EstManipulation m)
protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry)
{
_estManipulations.AddOrReplace(m);
var file = m.Slot switch
{
EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair),
EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face),
EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body),
EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head),
_ => throw new ArgumentOutOfRangeException(),
};
return m.Apply(file);
if (GetFile(identifier) is { } file)
Apply(file, identifier, entry);
}
public bool RevertMod(MetaFileManager manager, EstManipulation m)
protected override void RevertModInternal(EstIdentifier identifier)
{
if (!_estManipulations.Remove(m))
return false;
var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId);
var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def);
var file = m.Slot switch
{
EstType.Hair => _estHairFile!,
EstType.Face => _estFaceFile!,
EstType.Body => _estBodyFile!,
EstType.Head => _estHeadFile!,
_ => throw new ArgumentOutOfRangeException(),
};
return manip.Apply(file);
if (GetFile(identifier) is { } file)
Apply(file, identifier, EstFile.GetDefault(Manager, identifier.Slot, identifier.GenderRace, identifier.SetId));
}
public void Dispose()
public static bool Apply(EstFile file, EstIdentifier identifier, EstEntry entry)
=> file.SetEntry(identifier.GenderRace, identifier.SetId, entry) switch
{
EstFile.EstEntryChange.Unchanged => false,
EstFile.EstEntryChange.Changed => true,
EstFile.EstEntryChange.Added => true,
EstFile.EstEntryChange.Removed => true,
_ => false,
};
protected override void Dispose(bool _)
{
_estFaceFile?.Dispose();
_estHairFile?.Dispose();
@ -133,6 +116,21 @@ public struct EstCache : IDisposable
_estHairFile = null;
_estBodyFile = null;
_estHeadFile = null;
_estManipulations.Clear();
Clear();
}
private EstFile? GetFile(EstIdentifier identifier)
{
if (Manager.CharacterUtility.Ready)
return null;
return identifier.Slot switch
{
EstType.Hair => _estHairFile ??= new EstFile(Manager, EstType.Hair),
EstType.Face => _estFaceFile ??= new EstFile(Manager, EstType.Face),
EstType.Body => _estBodyFile ??= new EstFile(Manager, EstType.Body),
EstType.Head => _estHeadFile ??= new EstFile(Manager, EstType.Head),
_ => null,
};
}
}

View file

@ -1,9 +1,11 @@
using OtterGui.Services;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct GlobalEqpCache : IService
public class GlobalEqpCache : Dictionary<GlobalEqpManipulation, IMod>, IService
{
private readonly HashSet<PrimaryId> _doNotHideEarrings = [];
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
@ -13,11 +15,9 @@ public struct GlobalEqpCache : IService
private bool _doNotHideVieraHats;
private bool _doNotHideHrothgarHats;
public GlobalEqpCache()
{ }
public void Clear()
public new void Clear()
{
base.Clear();
_doNotHideEarrings.Clear();
_doNotHideNecklace.Clear();
_doNotHideBracelets.Clear();
@ -29,6 +29,9 @@ public struct GlobalEqpCache : IService
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
{
if (Count == 0)
return original;
if (_doNotHideVieraHats)
original |= EqpEntry.HeadShowVieraHat;
@ -52,8 +55,13 @@ public struct GlobalEqpCache : IService
return original;
}
public bool Add(GlobalEqpManipulation manipulation)
=> manipulation.Type switch
public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation)
{
if (Remove(manipulation, out var oldMod) && oldMod == mod)
return false;
this[manipulation] = mod;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition),
@ -61,12 +69,18 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
_ => false,
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
_ => false,
};
return true;
}
public bool Remove(GlobalEqpManipulation manipulation)
=> manipulation.Type switch
public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod)
{
if (!Remove(manipulation, out mod))
return false;
_ = manipulation.Type switch
{
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition),
@ -74,7 +88,9 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
_ => false,
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
_ => false,
};
return true;
}
}

View file

@ -1,4 +1,4 @@
using OtterGui.Filesystem;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
@ -7,50 +7,76 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public struct GmpCache : IDisposable
public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<GmpIdentifier, GmpEntry>(manager, collection)
{
private ExpandedGmpFile? _gmpFile = null;
private readonly List<GmpManipulation> _gmpManipulations = new();
private ExpandedGmpFile? _gmpFile;
public GmpCache()
{ }
public override void SetFiles()
=> Manager.SetFile(_gmpFile, MetaIndex.Gmp);
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_gmpFile, MetaIndex.Gmp);
public override void ResetFiles()
=> Manager.SetFile(null, MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp);
protected override void IncorporateChangesInternal()
{
if (GetFile() is not { } file)
return;
public void Reset()
foreach (var (identifier, (_, entry)) in this)
Apply(file, identifier, entry);
Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed GMP manipulations.");
}
public MetaList.MetaReverter TemporarilySetFile()
=> Manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp);
public override void Reset()
{
if (_gmpFile == null)
return;
_gmpFile.Reset(_gmpManipulations.Select(m => m.SetId));
_gmpManipulations.Clear();
_gmpFile.Reset(Keys.Select(identifier => identifier.SetId));
Clear();
}
public bool ApplyMod(MetaFileManager manager, GmpManipulation manip)
protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry)
{
_gmpManipulations.AddOrReplace(manip);
_gmpFile ??= new ExpandedGmpFile(manager);
return manip.Apply(_gmpFile);
if (GetFile() is { } file)
Apply(file, identifier, entry);
}
public bool RevertMod(MetaFileManager manager, GmpManipulation manip)
protected override void RevertModInternal(GmpIdentifier identifier)
{
if (!_gmpManipulations.Remove(manip))
if (GetFile() is { } file)
Apply(file, identifier, ExpandedGmpFile.GetDefault(Manager, identifier.SetId));
}
public static bool Apply(ExpandedGmpFile file, GmpIdentifier identifier, GmpEntry entry)
{
var origEntry = file[identifier.SetId];
if (entry == origEntry)
return false;
var def = ExpandedGmpFile.GetDefault(manager, manip.SetId);
manip = new GmpManipulation(def, manip.SetId);
return manip.Apply(_gmpFile!);
file[identifier.SetId] = entry;
return true;
}
public void Dispose()
protected override void Dispose(bool _)
{
_gmpFile?.Dispose();
_gmpFile = null;
_gmpManipulations.Clear();
Clear();
}
private ExpandedGmpFile? GetFile()
{
if (_gmpFile != null)
return _gmpFile;
if (!Manager.CharacterUtility.Ready)
return null;
return _gmpFile = new ExpandedGmpFile(Manager);
}
}

View file

@ -0,0 +1,89 @@
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public interface IMetaCache : IDisposable
{
public void SetFiles();
public void Reset();
public void ResetFiles();
public int Count { get; }
}
public abstract class MetaCacheBase<TIdentifier, TEntry>
: Dictionary<TIdentifier, (IMod Source, TEntry Entry)>, IMetaCache
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
public MetaCacheBase(MetaFileManager manager, ModCollection collection)
{
Manager = manager;
Collection = collection;
if (!Manager.CharacterUtility.Ready)
Manager.CharacterUtility.LoadingFinished += IncorporateChanges;
}
protected readonly MetaFileManager Manager;
protected readonly ModCollection Collection;
public void Dispose()
{
Manager.CharacterUtility.LoadingFinished -= IncorporateChanges;
Dispose(true);
}
public abstract void SetFiles();
public abstract void Reset();
public abstract void ResetFiles();
public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry)
{
lock (this)
{
if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer<TEntry>.Default.Equals(pair.Entry, entry))
return false;
this[identifier] = (source, entry);
}
ApplyModInternal(identifier, entry);
return true;
}
public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
lock (this)
{
if (!Remove(identifier, out var pair))
{
mod = null;
return false;
}
mod = pair.Source;
}
RevertModInternal(identifier);
return true;
}
private void IncorporateChanges()
{
lock (this)
{
IncorporateChangesInternal();
}
if (Manager.ActiveCollections.Default == Collection && Manager.Config.EnableMods)
SetFiles();
}
protected abstract void ApplyModInternal(TIdentifier identifier, TEntry entry);
protected abstract void RevertModInternal(TIdentifier identifier);
protected abstract void IncorporateChangesInternal();
protected virtual void Dispose(bool _)
{ }
}

View file

@ -1,3 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta;
using Penumbra.Meta.Files;
@ -6,116 +9,132 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public readonly struct ImcCache : IDisposable
public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ImcIdentifier, ImcEntry>(manager, collection)
{
private readonly Dictionary<Utf8GamePath, ImcFile> _imcFiles = [];
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = [];
private readonly Dictionary<Utf8GamePath, (ImcFile, HashSet<ImcIdentifier>)> _imcFiles = [];
public ImcCache()
{ }
public override void SetFiles()
=> SetFiles(false);
public void SetFiles(ModCollection collection, bool fromFullCompute)
public bool GetFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
{
if (!_imcFiles.TryGetValue(path, out var p))
{
file = null;
return false;
}
file = p.Item1;
return true;
}
public void SetFiles(bool fromFullCompute)
{
if (fromFullCompute)
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection));
foreach (var (path, _) in _imcFiles)
Collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, Collection));
else
foreach (var path in _imcFiles.Keys)
collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection));
foreach (var (path, _) in _imcFiles)
Collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, Collection));
}
public void Reset(ModCollection collection)
public override void ResetFiles()
{
foreach (var (path, file) in _imcFiles)
foreach (var (path, _) in _imcFiles)
Collection._cache!.ForceFile(path, FullPath.Empty);
}
protected override void IncorporateChangesInternal()
{
if (!Manager.CharacterUtility.Ready)
return;
foreach (var (identifier, (_, entry)) in this)
ApplyFile(identifier, entry);
Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed IMC manipulations.");
}
public override void Reset()
{
foreach (var (path, (file, set)) in _imcFiles)
{
collection._cache!.RemovePath(path);
Collection._cache!.RemovePath(path);
file.Reset();
set.Clear();
}
_imcManipulations.Clear();
Clear();
}
public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip)
protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry)
{
if (!manip.Validate(true))
return false;
if (Manager.CharacterUtility.Ready)
ApplyFile(identifier, entry);
}
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip));
if (idx < 0)
{
idx = _imcManipulations.Count;
_imcManipulations.Add((manip, null!));
}
var path = manip.GamePath();
private void ApplyFile(ImcIdentifier identifier, ImcEntry entry)
{
var path = identifier.GamePath();
try
{
if (!_imcFiles.TryGetValue(path, out var file))
file = new ImcFile(manager, manip.Identifier);
if (!_imcFiles.TryGetValue(path, out var pair))
pair = (new ImcFile(Manager, identifier), []);
_imcManipulations[idx] = (manip, file);
if (!manip.Apply(file))
return false;
_imcFiles[path] = file;
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection);
collection._cache!.ForceFile(path, fullPath);
if (!Apply(pair.Item1, identifier, entry))
return;
return true;
pair.Item2.Add(identifier);
_imcFiles[path] = pair;
var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection);
Collection._cache!.ForceFile(path, fullPath);
}
catch (ImcException e)
{
manager.ValidityChecker.ImcExceptions.Add(e);
Manager.ValidityChecker.ImcExceptions.Add(e);
Penumbra.Log.Error(e.ToString());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}");
Penumbra.Log.Error($"Could not apply IMC Manipulation {identifier}:\n{e}");
}
return false;
}
public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m)
protected override void RevertModInternal(ImcIdentifier identifier)
{
if (!m.Validate(false))
return false;
var path = identifier.GamePath();
if (!_imcFiles.TryGetValue(path, out var pair))
return;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m));
if (idx < 0)
return false;
if (!pair.Item2.Remove(identifier))
return;
var (_, file) = _imcManipulations[idx];
_imcManipulations.RemoveAt(idx);
if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file)))
if (pair.Item2.Count == 0)
{
_imcFiles.Remove(file.Path);
collection._cache!.ForceFile(file.Path, FullPath.Empty);
file.Dispose();
return true;
_imcFiles.Remove(path);
Collection._cache!.ForceFile(pair.Item1.Path, FullPath.Empty);
pair.Item1.Dispose();
return;
}
var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _);
var manip = m.Copy(def);
if (!manip.Apply(file))
return false;
var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _);
if (!Apply(pair.Item1, identifier, def))
return;
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection);
collection._cache!.ForceFile(file.Path, fullPath);
return true;
var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection);
Collection._cache!.ForceFile(pair.Item1.Path, fullPath);
}
public void Dispose()
public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry)
=> file.SetEntry(ImcFile.PartIndex(identifier.EquipSlot), identifier.Variant.Id, entry);
protected override void Dispose(bool _)
{
foreach (var file in _imcFiles.Values)
foreach (var (_, (file, _)) in _imcFiles)
file.Dispose();
Clear();
_imcFiles.Clear();
_imcManipulations.Clear();
}
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
=> _imcFiles.TryGetValue(path, out file);
}

View file

@ -1,3 +1,4 @@
using System.IO.Pipes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
@ -9,238 +10,174 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>>
public class MetaCache(MetaFileManager manager, ModCollection collection)
{
private readonly MetaFileManager _manager;
private readonly ModCollection _collection;
private readonly Dictionary<MetaManipulation, IMod> _manipulations = new();
private EqpCache _eqpCache = new();
private readonly EqdpCache _eqdpCache = new();
private EstCache _estCache = new();
private GmpCache _gmpCache = new();
private CmpCache _cmpCache = new();
private readonly ImcCache _imcCache = new();
private GlobalEqpCache _globalEqpCache = new();
public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
return _manipulations.TryGetValue(manip, out mod);
}
}
public readonly EqpCache Eqp = new(manager, collection);
public readonly EqdpCache Eqdp = new(manager, collection);
public readonly EstCache Est = new(manager, collection);
public readonly GmpCache Gmp = new(manager, collection);
public readonly RspCache Rsp = new(manager, collection);
public readonly ImcCache Imc = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new();
public int Count
=> _manipulations.Count;
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count;
public IReadOnlyCollection<MetaManipulation> Manipulations
=> _manipulations.Keys;
public IEnumerator<KeyValuePair<MetaManipulation, IMod>> GetEnumerator()
=> _manipulations.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public MetaCache(MetaFileManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
if (!_manager.CharacterUtility.Ready)
_manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
}
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
.Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public void SetFiles()
{
_eqpCache.SetFiles(_manager);
_eqdpCache.SetFiles(_manager);
_estCache.SetFiles(_manager);
_gmpCache.SetFiles(_manager);
_cmpCache.SetFiles(_manager);
_imcCache.SetFiles(_collection, false);
Eqp.SetFiles();
Eqdp.SetFiles();
Est.SetFiles();
Gmp.SetFiles();
Rsp.SetFiles();
Imc.SetFiles(false);
}
public void Reset()
{
_eqpCache.Reset();
_eqdpCache.Reset();
_estCache.Reset();
_gmpCache.Reset();
_cmpCache.Reset();
_imcCache.Reset(_collection);
_manipulations.Clear();
_globalEqpCache.Clear();
Eqp.Reset();
Eqdp.Reset();
Est.Reset();
Gmp.Reset();
Rsp.Reset();
Imc.Reset();
GlobalEqp.Clear();
}
public void Dispose()
{
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
_eqpCache.Dispose();
_eqdpCache.Dispose();
_estCache.Dispose();
_gmpCache.Dispose();
_cmpCache.Dispose();
_imcCache.Dispose();
_manipulations.Clear();
Eqp.Dispose();
Eqdp.Dispose();
Est.Dispose();
Gmp.Dispose();
Rsp.Dispose();
Imc.Dispose();
}
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
mod = null;
return identifier switch
{
EqdpIdentifier i => Eqdp.TryGetValue(i, out var p) && Convert(p, out mod),
EqpIdentifier i => Eqp.TryGetValue(i, out var p) && Convert(p, out mod),
EstIdentifier i => Est.TryGetValue(i, out var p) && Convert(p, out mod),
GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod),
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
_ => false,
};
static bool Convert<T>((IMod, T) pair, out IMod mod)
{
mod = pair.Item1;
return true;
}
}
public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
=> identifier switch
{
EqdpIdentifier i => Eqdp.RevertMod(i, out mod),
EqpIdentifier i => Eqp.RevertMod(i, out mod),
EstIdentifier i => Est.RevertMod(i, out mod),
GmpIdentifier i => Gmp.RevertMod(i, out mod),
ImcIdentifier i => Imc.RevertMod(i, out mod),
RspIdentifier i => Rsp.RevertMod(i, out mod),
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
_ => (mod = null) != null,
};
public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry)
=> identifier switch
{
EqdpIdentifier i when entry is EqdpEntry e => Eqdp.ApplyMod(mod, i, e),
EqdpIdentifier i when entry is EqdpEntryInternal e => Eqdp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EqpIdentifier i when entry is EqpEntry e => Eqp.ApplyMod(mod, i, e),
EqpIdentifier i when entry is EqpEntryInternal e => Eqp.ApplyMod(mod, i, e.ToEntry(i.Slot)),
EstIdentifier i when entry is EstEntry e => Est.ApplyMod(mod, i, e),
GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e),
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
_ => false,
};
~MetaCache()
=> Dispose();
public bool ApplyMod(MetaManipulation manip, IMod mod)
{
lock (_manipulations)
{
if (_manipulations.ContainsKey(manip))
_manipulations.Remove(manip);
_manipulations[manip] = mod;
}
if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp)
return _globalEqpCache.Add(manip.GlobalEqp);
if (!_manager.CharacterUtility.Ready)
return true;
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
{
lock (_manipulations)
{
var ret = _manipulations.Remove(manip, out mod);
if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp)
return _globalEqpCache.Remove(manip.GlobalEqp);
if (!_manager.CharacterUtility.Ready)
return ret;
}
// Imc manipulations do not require character utility,
// but they do require the file space to be ready.
return manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
/// <summary> Set a single file. </summary>
public void SetFile(MetaIndex metaIndex)
{
switch (metaIndex)
{
case MetaIndex.Eqp:
_eqpCache.SetFiles(_manager);
Eqp.SetFiles();
break;
case MetaIndex.Gmp:
_gmpCache.SetFiles(_manager);
Gmp.SetFiles();
break;
case MetaIndex.HumanCmp:
_cmpCache.SetFiles(_manager);
Rsp.SetFiles();
break;
case MetaIndex.FaceEst:
case MetaIndex.HairEst:
case MetaIndex.HeadEst:
case MetaIndex.BodyEst:
_estCache.SetFile(_manager, metaIndex);
Est.SetFile(metaIndex);
break;
default:
_eqdpCache.SetFile(_manager, metaIndex);
Eqdp.SetFile(metaIndex);
break;
}
}
/// <summary> Set the currently relevant IMC files for the collection cache. </summary>
public void SetImcFiles(bool fromFullCompute)
=> _imcCache.SetFiles(_collection, fromFullCompute);
=> Imc.SetFiles(fromFullCompute);
public MetaList.MetaReverter TemporarilySetEqpFile()
=> _eqpCache.TemporarilySetFiles(_manager);
=> Eqp.TemporarilySetFile();
public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory);
=> Eqdp.TemporarilySetFile(genderRace, accessory);
public MetaList.MetaReverter TemporarilySetGmpFile()
=> _gmpCache.TemporarilySetFiles(_manager);
=> Gmp.TemporarilySetFile();
public MetaList.MetaReverter TemporarilySetCmpFile()
=> _cmpCache.TemporarilySetFiles(_manager);
=> Rsp.TemporarilySetFile();
public MetaList.MetaReverter TemporarilySetEstFile(EstType type)
=> _estCache.TemporarilySetFiles(_manager, type);
=> Est.TemporarilySetFiles(type);
public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor)
=> _globalEqpCache.Apply(baseEntry, armor);
=> GlobalEqp.Apply(baseEntry, armor);
/// <summary> Try to obtain a manipulated IMC file. </summary>
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
=> _imcCache.GetImcFile(path, out file);
=> Imc.GetFile(path, out file);
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId)
{
var eqdpFile = _eqdpCache.EqdpFile(race, accessory);
var eqdpFile = Eqdp.EqdpFile(race, accessory);
if (eqdpFile != null)
return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default;
return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId);
return Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId);
}
internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
=> _estCache.GetEstEntry(_manager, type, genderRace, primaryId);
/// <summary> Use this when CharacterUtility becomes ready. </summary>
private void ApplyStoredManipulations()
{
if (!_manager.CharacterUtility.Ready)
return;
var loaded = 0;
lock (_manipulations)
{
foreach (var manip in Manipulations)
{
loaded += manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp),
MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp),
MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est),
MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp),
MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp),
MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc),
MetaManipulation.Type.GlobalEqp => false,
MetaManipulation.Type.Unknown => false,
_ => false,
}
? 1
: 0;
}
}
_manager.ApplyDefaultFiles(_collection);
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations.");
}
=> Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace));
}

View file

@ -0,0 +1,81 @@
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache;
public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<RspIdentifier, RspEntry>(manager, collection)
{
private CmpFile? _cmpFile;
public override void SetFiles()
=> Manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public override void ResetFiles()
=> Manager.SetFile(null, MetaIndex.HumanCmp);
protected override void IncorporateChangesInternal()
{
if (GetFile() is not { } file)
return;
foreach (var (identifier, (_, entry)) in this)
Apply(file, identifier, entry);
Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed RSP manipulations.");
}
public MetaList.MetaReverter TemporarilySetFile()
=> Manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public override void Reset()
{
if (_cmpFile == null)
return;
_cmpFile.Reset(Keys.Select(identifier => (identifier.SubRace, identifier.Attribute)));
Clear();
}
protected override void ApplyModInternal(RspIdentifier identifier, RspEntry entry)
{
if (GetFile() is { } file)
Apply(file, identifier, entry);
}
protected override void RevertModInternal(RspIdentifier identifier)
{
if (GetFile() is { } file)
Apply(file, identifier, CmpFile.GetDefault(Manager, identifier.SubRace, identifier.Attribute));
}
public static bool Apply(CmpFile file, RspIdentifier identifier, RspEntry entry)
{
var value = file[identifier.SubRace, identifier.Attribute];
if (value == entry)
return false;
file[identifier.SubRace, identifier.Attribute] = entry;
return true;
}
protected override void Dispose(bool _)
{
_cmpFile?.Dispose();
_cmpFile = null;
Clear();
}
private CmpFile? GetFile()
{
if (_cmpFile != null)
return _cmpFile;
if (!Manager.CharacterUtility.Ready)
return null;
return _cmpFile = new CmpFile(Manager);
}
}

View file

@ -37,7 +37,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
_tasks.Clear();
}
public Task<IoNotifier> ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]?> read, string outputPath)
public Task<IoNotifier> ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable<string> sklbPaths, Func<string, byte[]?> read,
string outputPath)
=> EnqueueWithResult(
new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath),
action => action.Notifier
@ -52,7 +53,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
/// <summary> Try to find the .sklb paths for a .mdl file. </summary>
/// <param name="mdlPath"> .mdl file to look up the skeletons for. </param>
/// <param name="estManipulations"> Modified extra skeleton template parameters. </param>
public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations)
public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations)
{
var info = parser.GetFileInfo(mdlPath);
if (info.FileType is not FileType.Model)
@ -81,20 +82,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
};
}
private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations)
private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair<EstIdentifier, EstEntry>[] estManipulations)
{
// Try to find an EST entry from the manipulations provided.
var (gender, race) = info.GenderRace.Split();
var modEst = estManipulations
.FirstOrNull(est =>
est.Gender == gender
&& est.Race == race
&& est.Slot == type
&& est.SetId == info.PrimaryId
.FirstOrNull(
est => est.Key.GenderRace == info.GenderRace
&& est.Key.Slot == type
&& est.Key.SetId == info.PrimaryId
);
// Try to use an entry from provided manipulations, falling back to the current collection.
var targetId = modEst?.Entry
var targetId = modEst?.Value
?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
?? EstEntry.Zero;
@ -102,7 +101,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
if (targetId == EstEntry.Zero)
return [];
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId.AsId)];
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)];
}
/// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary>
@ -250,9 +249,11 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
var path = manager.ResolveMtrlPath(relativePath, notifier);
if (path == null)
return null;
var bytes = read(path);
if (bytes == null)
return null;
var mtrl = new MtrlFile(bytes);
return new MaterialExporter.Material

View file

@ -13,15 +13,15 @@ public partial class TexToolsMeta
private void DeserializeEqpEntry(MetaFileInfo metaFileInfo, byte[]? data)
{
// Eqp can only be valid for equipment.
if (data == null || !metaFileInfo.EquipSlot.IsEquipment())
var mask = Eqp.Mask(metaFileInfo.EquipSlot);
if (data == null || mask == 0)
return;
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data);
var def = new EqpManipulation(ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId), metaFileInfo.EquipSlot,
metaFileInfo.PrimaryId);
var manip = new EqpManipulation(value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId);
if (_keepDefault || def.Entry != manip.Entry)
MetaManipulations.Add(manip);
var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot);
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask;
var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask;
if (_keepDefault || def != value)
MetaManipulations.TryAdd(identifier, value);
}
// Deserialize and check Eqdp Entries and add them to the list if they are non-default.
@ -40,14 +40,12 @@ public partial class TexToolsMeta
if (!gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory())
continue;
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2);
var def = new EqdpManipulation(
ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId),
metaFileInfo.EquipSlot,
gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId);
var manip = new EqdpManipulation(value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId);
if (_keepDefault || def.Entry != manip.Entry)
MetaManipulations.Add(manip);
var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr);
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask;
if (_keepDefault || def != value)
MetaManipulations.TryAdd(identifier, value);
}
}
@ -57,10 +55,10 @@ public partial class TexToolsMeta
if (data == null)
return;
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
if (_keepDefault || value != def)
MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId));
MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value);
}
// Deserialize and check Est Entries and add them to the list if they are non-default.
@ -74,7 +72,7 @@ public partial class TexToolsMeta
for (var i = 0; i < num; ++i)
{
var gr = (GenderRace)reader.ReadUInt16();
var id = reader.ReadUInt16();
var id = (PrimaryId)reader.ReadUInt16();
var value = new EstEntry(reader.ReadUInt16());
var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch
{
@ -87,9 +85,10 @@ public partial class TexToolsMeta
if (!gr.IsValid() || type == 0)
continue;
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
var identifier = new EstIdentifier(id, type, gr);
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
if (_keepDefault || def != value)
MetaManipulations.Add(new EstManipulation(gr.Split().Item1, gr.Split().Item2, type, id, value));
MetaManipulations.TryAdd(identifier, value);
}
}
@ -107,20 +106,16 @@ public partial class TexToolsMeta
ushort i = 0;
try
{
var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId,
metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot,
new ImcEntry());
var def = new ImcFile(_metaFileManager, manip.Identifier);
var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId,
metaFileInfo.EquipSlot, metaFileInfo.SecondaryType);
var file = new ImcFile(_metaFileManager, identifier);
var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
foreach (var value in values)
{
if (_keepDefault || !value.Equals(def.GetEntry(partIdx, (Variant)i)))
{
var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot,
value);
if (imc.Validate(true))
MetaManipulations.Add(imc);
}
identifier = identifier with { Variant = (Variant)i };
var def = file.GetEntry(partIdx, (Variant)i);
if (_keepDefault || def != value && identifier.Validate())
MetaManipulations.TryAdd(identifier, value);
++i;
}

View file

@ -1,3 +1,4 @@
using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
@ -8,7 +9,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta
{
public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable<MetaManipulation> manipulations, DirectoryInfo basePath)
public static void WriteTexToolsMeta(MetaFileManager manager, MetaDictionary manipulations, DirectoryInfo basePath)
{
var files = ConvertToTexTools(manager, manipulations);
@ -27,49 +28,81 @@ public partial class TexToolsMeta
}
}
public static Dictionary<string, byte[]> ConvertToTexTools(MetaFileManager manager, IEnumerable<MetaManipulation> manips)
public static Dictionary<string, byte[]> ConvertToTexTools(MetaFileManager manager, MetaDictionary manips)
{
var ret = new Dictionary<string, byte[]>();
foreach (var group in manips.GroupBy(ManipToPath))
foreach (var group in manips.Rsp.GroupBy(ManipToPath))
{
if (group.Key.Length == 0)
continue;
var bytes = group.Key.EndsWith(".rgsp")
? WriteRgspFile(manager, group.Key, group)
: WriteMetaFile(manager, group.Key, group);
var bytes = WriteRgspFile(manager, group);
if (bytes.Length == 0)
continue;
ret.Add(group.Key, bytes);
}
foreach (var (file, dict) in SplitByFile(manips))
{
var bytes = WriteMetaFile(manager, file, dict);
if (bytes.Length == 0)
continue;
ret.Add(file, bytes);
}
return ret;
}
private static byte[] WriteRgspFile(MetaFileManager manager, string path, IEnumerable<MetaManipulation> manips)
private static Dictionary<string, MetaDictionary> SplitByFile(MetaDictionary manips)
{
var list = manips.GroupBy(m => m.Rsp.Attribute).ToDictionary(m => m.Key, m => m.Last().Rsp);
var ret = new Dictionary<string, MetaDictionary>();
foreach (var (identifier, key) in manips.Imc)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Eqp)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Eqdp)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Est)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
foreach (var (identifier, key) in manips.Gmp)
GetDict(ManipToPath(identifier)).TryAdd(identifier, key);
ret.Remove(string.Empty);
return ret;
MetaDictionary GetDict(string path)
{
if (!ret.TryGetValue(path, out var dict))
{
dict = new MetaDictionary();
ret.Add(path, dict);
}
return dict;
}
}
private static byte[] WriteRgspFile(MetaFileManager manager, IEnumerable<KeyValuePair<RspIdentifier, RspEntry>> manips)
{
var list = manips.GroupBy(m => m.Key.Attribute).ToDictionary(g => g.Key, g => g.Last());
using var m = new MemoryStream(45);
using var b = new BinaryWriter(m);
// Version
b.Write(byte.MaxValue);
b.Write((ushort)2);
var race = list.First().Value.SubRace;
var gender = list.First().Value.Attribute.ToGender();
var race = list.First().Value.Key.SubRace;
var gender = list.First().Value.Key.Attribute.ToGender();
b.Write((byte)(race - 1)); // offset by one due to Unknown
b.Write((byte)(gender - 1)); // offset by one due to Unknown
void Add(params RspAttribute[] attributes)
{
foreach (var attribute in attributes)
{
var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute);
b.Write(value.Value);
}
}
if (gender == Gender.Male)
{
Add(RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail);
@ -82,12 +115,24 @@ public partial class TexToolsMeta
}
return m.GetBuffer();
void Add(params RspAttribute[] attributes)
{
foreach (var attribute in attributes)
{
var value = list.TryGetValue(attribute, out var tmp) ? tmp.Value : CmpFile.GetDefault(manager, race, attribute);
b.Write(value.Value);
}
}
}
private static byte[] WriteMetaFile(MetaFileManager manager, string path, IEnumerable<MetaManipulation> manips)
private static byte[] WriteMetaFile(MetaFileManager manager, string path, MetaDictionary manips)
{
var filteredManips = manips.GroupBy(m => m.ManipulationType).ToDictionary(p => p.Key, p => p.Select(x => x));
var headerCount = (manips.Imc.Count > 0 ? 1 : 0)
+ (manips.Eqp.Count > 0 ? 1 : 0)
+ (manips.Eqdp.Count > 0 ? 1 : 0)
+ (manips.Est.Count > 0 ? 1 : 0)
+ (manips.Gmp.Count > 0 ? 1 : 0);
using var m = new MemoryStream();
using var b = new BinaryWriter(m);
@ -101,7 +146,7 @@ public partial class TexToolsMeta
b.Write((byte)0);
// Number of Headers
b.Write((uint)filteredManips.Count);
b.Write((uint)headerCount);
// Current TT Size of Headers
b.Write((uint)12);
@ -109,88 +154,44 @@ public partial class TexToolsMeta
var headerStart = b.BaseStream.Position + 4;
b.Write((uint)headerStart);
var offset = (uint)(b.BaseStream.Position + 12 * filteredManips.Count);
foreach (var (header, data) in filteredManips)
{
b.Write((uint)header);
b.Write(offset);
var size = WriteData(manager, b, offset, header, data);
b.Write(size);
offset += size;
}
var offset = (uint)(b.BaseStream.Position + 12 * manips.Count);
offset += WriteData(manager, b, offset, manips.Imc);
offset += WriteData(b, offset, manips.Eqdp);
offset += WriteData(b, offset, manips.Eqp);
offset += WriteData(b, offset, manips.Est);
offset += WriteData(b, offset, manips.Gmp);
return m.ToArray();
}
private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type,
IEnumerable<MetaManipulation> manips)
private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, IReadOnlyDictionary<ImcIdentifier, ImcEntry> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
switch (type)
var refIdentifier = manips.First().Key;
var baseFile = new ImcFile(manager, refIdentifier);
foreach (var (identifier, entry) in manips)
ImcCache.Apply(baseFile, identifier, entry);
var partIdx = refIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory
? ImcFile.PartIndex(refIdentifier.EquipSlot)
: 0;
for (var i = 0; i <= baseFile.Count; ++i)
{
case MetaManipulation.Type.Imc:
var allManips = manips.ToList();
var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier);
foreach (var manip in allManips)
manip.Imc.Apply(baseFile);
var partIdx = allManips[0].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory
? ImcFile.PartIndex(allManips[0].Imc.EquipSlot)
: 0;
for (var i = 0; i <= baseFile.Count; ++i)
{
var entry = baseFile.GetEntry(partIdx, (Variant)i);
b.Write(entry.MaterialId);
b.Write(entry.DecalId);
b.Write(entry.AttributeAndSound);
b.Write(entry.VfxId);
b.Write(entry.MaterialAnimationId);
}
break;
case MetaManipulation.Type.Eqdp:
foreach (var manip in manips)
{
b.Write((uint)Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race));
var entry = (byte)(((uint)manip.Eqdp.Entry >> Eqdp.Offset(manip.Eqdp.Slot)) & 0x03);
b.Write(entry);
}
break;
case MetaManipulation.Type.Eqp:
foreach (var manip in manips)
{
var bytes = BitConverter.GetBytes((ulong)manip.Eqp.Entry);
var (numBytes, byteOffset) = Eqp.BytesAndOffset(manip.Eqp.Slot);
for (var i = byteOffset; i < numBytes + byteOffset; ++i)
b.Write(bytes[i]);
}
break;
case MetaManipulation.Type.Est:
foreach (var manip in manips)
{
b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race));
b.Write(manip.Est.SetId.Id);
b.Write(manip.Est.Entry.Value);
}
break;
case MetaManipulation.Type.Gmp:
foreach (var manip in manips)
{
b.Write((uint)manip.Gmp.Entry.Value);
b.Write(manip.Gmp.Entry.UnknownTotal);
}
break;
case MetaManipulation.Type.GlobalEqp:
// Not Supported
break;
var entry = baseFile.GetEntry(partIdx, (Variant)i);
b.Write(entry.MaterialId);
b.Write(entry.DecalId);
b.Write(entry.AttributeAndSound);
b.Write(entry.VfxId);
b.Write(entry.MaterialAnimationId);
}
var size = b.BaseStream.Position - offset;
@ -198,19 +199,98 @@ public partial class TexToolsMeta
return (uint)size;
}
private static string ManipToPath(MetaManipulation manip)
=> manip.ManipulationType switch
{
MetaManipulation.Type.Imc => ManipToPath(manip.Imc),
MetaManipulation.Type.Eqdp => ManipToPath(manip.Eqdp),
MetaManipulation.Type.Eqp => ManipToPath(manip.Eqp),
MetaManipulation.Type.Est => ManipToPath(manip.Est),
MetaManipulation.Type.Gmp => ManipToPath(manip.Gmp),
MetaManipulation.Type.Rsp => ManipToPath(manip.Rsp),
_ => string.Empty,
};
private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> manips)
{
if (manips.Count == 0)
return 0;
private static string ManipToPath(ImcManipulation manip)
b.Write((uint)MetaManipulationType.Eqdp);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var (identifier, entry) in manips)
{
b.Write((uint)identifier.GenderRace);
b.Write(entry.AsByte);
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static uint WriteData(BinaryWriter b, uint offset,
IReadOnlyDictionary<EqpIdentifier, EqpEntryInternal> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var (identifier, entry) in manips)
{
var numBytes = Eqp.BytesAndOffset(identifier.Slot).Item1;
for (var i = 0; i < numBytes; ++i)
b.Write((byte)(entry.Value >> (8 * i)));
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary<EstIdentifier, EstEntry> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var (identifier, entry) in manips)
{
b.Write((ushort)identifier.GenderRace);
b.Write(identifier.SetId.Id);
b.Write(entry.Value);
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary<GmpIdentifier, GmpEntry> manips)
{
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin);
foreach (var entry in manips.Values)
{
b.Write((uint)entry.Value);
b.Write(entry.UnknownTotal);
}
var size = b.BaseStream.Position - offset;
b.Seek((int)oldPos, SeekOrigin.Begin);
return (uint)size;
}
private static string ManipToPath(ImcIdentifier manip)
{
var path = manip.GamePath().ToString();
var replacement = manip.ObjectType switch
@ -224,33 +304,33 @@ public partial class TexToolsMeta
return path.Replace(".imc", replacement);
}
private static string ManipToPath(EqdpManipulation manip)
private static string ManipToPath(EqdpIdentifier manip)
=> manip.Slot.IsAccessory()
? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta";
? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta";
private static string ManipToPath(EqpManipulation manip)
private static string ManipToPath(EqpIdentifier manip)
=> manip.Slot.IsAccessory()
? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta";
? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"
: $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta";
private static string ManipToPath(EstManipulation manip)
private static string ManipToPath(EstIdentifier manip)
{
var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode();
return manip.Slot switch
{
EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta",
EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta",
EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta",
EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta",
_ => throw new ArgumentOutOfRangeException(),
EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId.Id:D4}/c{raceCode}h{manip.SetId.Id:D4}_hir.meta",
EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId.Id:D4}/c{raceCode}f{manip.SetId.Id:D4}_fac.meta",
EstType.Body => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Body.ToSuffix()}.meta",
EstType.Head => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta",
_ => throw new ArgumentOutOfRangeException(),
};
}
private static string ManipToPath(GmpManipulation manip)
=> $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta";
private static string ManipToPath(GmpIdentifier manip)
=> $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta";
private static string ManipToPath(RspManipulation manip)
=> $"chara/xls/charamake/rgsp/{(int)manip.SubRace - 1}-{(int)manip.Attribute.ToGender() - 1}.rgsp";
private static string ManipToPath(KeyValuePair<RspIdentifier, RspEntry> manip)
=> $"chara/xls/charamake/rgsp/{(int)manip.Key.SubRace - 1}-{(int)manip.Key.Attribute.ToGender() - 1}.rgsp";
}

View file

@ -42,14 +42,6 @@ public partial class TexToolsMeta
return Invalid;
}
// Add the given values to the manipulations if they are not default.
void Add(RspAttribute attribute, float value)
{
var def = CmpFile.GetDefault(manager, subRace, attribute);
if (keepDefault || value != def.Value)
ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, new RspEntry(value)));
}
if (gender == 1)
{
Add(RspAttribute.FemaleMinSize, br.ReadSingle());
@ -73,5 +65,14 @@ public partial class TexToolsMeta
}
return ret;
// Add the given values to the manipulations if they are not default.
void Add(RspAttribute attribute, float value)
{
var identifier = new RspIdentifier(subRace, attribute);
var def = CmpFile.GetDefault(manager, subRace, attribute);
if (keepDefault || value != def.Value)
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
}
}
}

View file

@ -1,4 +1,3 @@
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.Import.Structs;
using Penumbra.Meta;
@ -22,10 +21,10 @@ public partial class TexToolsMeta
public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0);
// The info class determines the files or table locations the changes need to apply to from the filename.
public readonly uint Version;
public readonly string FilePath;
public readonly List<MetaManipulation> MetaManipulations = new();
private readonly bool _keepDefault = false;
public readonly uint Version;
public readonly string FilePath;
public readonly MetaDictionary MetaManipulations = new();
private readonly bool _keepDefault;
private readonly MetaFileManager _metaFileManager;
@ -44,18 +43,18 @@ public partial class TexToolsMeta
var headerStart = reader.ReadUInt32();
reader.BaseStream.Seek(headerStart, SeekOrigin.Begin);
List<(MetaManipulation.Type type, uint offset, int size)> entries = [];
List<(MetaManipulationType type, uint offset, int size)> entries = [];
for (var i = 0; i < numHeaders; ++i)
{
var currentOffset = reader.BaseStream.Position;
var type = (MetaManipulation.Type)reader.ReadUInt32();
var type = (MetaManipulationType)reader.ReadUInt32();
var offset = reader.ReadUInt32();
var size = reader.ReadInt32();
entries.Add((type, offset, size));
reader.BaseStream.Seek(currentOffset + headerSize, SeekOrigin.Begin);
}
byte[]? ReadEntry(MetaManipulation.Type type)
byte[]? ReadEntry(MetaManipulationType type)
{
var idx = entries.FindIndex(t => t.type == type);
if (idx < 0)
@ -65,11 +64,11 @@ public partial class TexToolsMeta
return reader.ReadBytes(entries[idx].size);
}
DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Eqp));
DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Gmp));
DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulation.Type.Eqdp));
DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulation.Type.Est));
DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulation.Type.Imc));
DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulationType.Eqp));
DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulationType.Gmp));
DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulationType.Eqdp));
DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulationType.Est));
DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulationType.Imc));
}
catch (Exception e)
{

View file

@ -1,7 +1,6 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Microsoft.VisualBasic;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;

View file

@ -273,7 +273,7 @@ internal partial record ResolveContext
{
var metaCache = Global.Collection.MetaCache;
var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default;
return (raceCode, EstManipulation.ToName(type), skeletonSet.AsId);
return (raceCode, type.ToName(), skeletonSet.AsId);
}
private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex)

View file

@ -51,10 +51,6 @@ public class ImcChecker
return entry;
}
public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache)
=> GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id,
imcManip.EquipSlot, imcManip.BodySlot), storeCache);
private static ImcFile? GetFile(ImcIdentifier identifier)
{
if (_dataManager == null)

View file

@ -69,10 +69,16 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge
jObj["Slot"] = Slot.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Eqdp;
}
public readonly record struct EqdpEntryInternal(bool Material, bool Model)
{
public byte AsByte
=> (byte)(Material ? Model ? 3 : 1 : Model ? 2 : 0);
private EqdpEntryInternal((bool, bool) val)
: this(val.Item1, val.Item2)
{ }
@ -83,4 +89,7 @@ public readonly record struct EqdpEntryInternal(bool Material, bool Model)
public EqdpEntry ToEntry(EquipSlot slot)
=> Eqdp.FromSlotAndBits(slot, Material, Model);
public override string ToString()
=> $"Material: {Material}, Model: {Model}";
}

View file

@ -1,110 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EqdpManipulation(EqdpIdentifier identifier, EqdpEntry entry) : IMetaManipulation<EqdpManipulation>
{
[JsonIgnore]
public EqdpIdentifier Identifier { get; } = identifier;
public EqdpEntry Entry { get; } = entry;
[JsonConverter(typeof(StringEnumConverter))]
public Gender Gender
=> Identifier.Gender;
[JsonConverter(typeof(StringEnumConverter))]
public ModelRace Race
=> Identifier.Race;
public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))]
public EquipSlot Slot
=> Identifier.Slot;
[JsonConstructor]
public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId)
: this(new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)), Eqdp.Mask(slot) & entry)
{ }
public EqdpManipulation Copy(EqdpManipulation entry)
{
if (entry.Slot != Slot)
{
var (bit1, bit2) = entry.Entry.ToBits(entry.Slot);
return new EqdpManipulation(Identifier, Eqdp.FromSlotAndBits(Slot, bit1, bit2));
}
return new EqdpManipulation(Identifier, entry.Entry);
}
public EqdpManipulation Copy(EqdpEntry entry)
=> new(entry, Slot, Gender, Race, SetId);
public override string ToString()
=> $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}";
public bool Equals(EqdpManipulation other)
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Slot == other.Slot;
public override bool Equals(object? obj)
=> obj is EqdpManipulation other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot);
public int CompareTo(EqdpManipulation other)
{
var r = Race.CompareTo(other.Race);
if (r != 0)
return r;
var g = Gender.CompareTo(other.Gender);
if (g != 0)
return g;
var set = SetId.Id.CompareTo(other.SetId.Id);
return set != 0 ? set : Slot.CompareTo(other.Slot);
}
public MetaIndex FileIndex()
=> CharacterUtilityData.EqdpIdx(Names.CombinedRace(Gender, Race), Slot.IsAccessory());
public bool Apply(ExpandedEqdpFile file)
{
var entry = file[SetId];
var mask = Eqdp.Mask(Slot);
if ((entry & mask) == Entry)
return false;
file[SetId] = (entry & ~mask) | Entry;
return true;
}
public bool Validate()
{
var mask = Eqdp.Mask(Slot);
if (mask == 0)
return false;
if ((mask & Entry) != Entry)
return false;
if (FileIndex() == (MetaIndex)(-1))
return false;
// No check for set id.
return true;
}
}

View file

@ -50,6 +50,9 @@ public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : I
jObj["Slot"] = Slot.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Eqp;
}
public readonly record struct EqpEntryInternal(uint Value)

View file

@ -1,80 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Util;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EqpManipulation(EqpIdentifier identifier, EqpEntry entry) : IMetaManipulation<EqpManipulation>
{
[JsonIgnore]
public EqpIdentifier Identifier { get; } = identifier;
[JsonConverter(typeof(ForceNumericFlagEnumConverter))]
public EqpEntry Entry { get; } = entry;
public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))]
public EquipSlot Slot
=> Identifier.Slot;
[JsonConstructor]
public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId)
: this(new EqpIdentifier(setId, slot), Eqp.Mask(slot) & entry)
{ }
public EqpManipulation Copy(EqpEntry entry)
=> new(Identifier, entry);
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
public bool Equals(EqpManipulation other)
=> Slot == other.Slot
&& SetId == other.SetId;
public override bool Equals(object? obj)
=> obj is EqpManipulation other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Slot, SetId);
public int CompareTo(EqpManipulation other)
{
var set = SetId.Id.CompareTo(other.SetId.Id);
return set != 0 ? set : Slot.CompareTo(other.Slot);
}
public MetaIndex FileIndex()
=> MetaIndex.Eqp;
public bool Apply(ExpandedEqpFile file)
{
var entry = file[SetId];
var mask = Eqp.Mask(Slot);
if ((entry & mask) == Entry)
return false;
file[SetId] = (entry & ~mask) | Entry;
return true;
}
public bool Validate()
{
var mask = Eqp.Mask(Slot);
if (mask == 0)
return false;
if ((Entry & mask) != Entry)
return false;
// No check for set id.
return true;
}
}

View file

@ -91,6 +91,9 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende
jObj["Slot"] = Slot.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Est;
}
[JsonConverter(typeof(Converter))]
@ -111,3 +114,16 @@ public readonly record struct EstEntry(ushort Value)
=> new(serializer.Deserialize<ushort>(reader));
}
}
public static class EstTypeExtension
{
public static string ToName(this EstType type)
=> type switch
{
EstType.Hair => "hair",
EstType.Face => "face",
EstType.Body => "top",
EstType.Head => "met",
_ => "unk",
};
}

View file

@ -1,108 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EstManipulation(EstIdentifier identifier, EstEntry entry) : IMetaManipulation<EstManipulation>
{
public static string ToName(EstType type)
=> type switch
{
EstType.Hair => "hair",
EstType.Face => "face",
EstType.Body => "top",
EstType.Head => "met",
_ => "unk",
};
[JsonIgnore]
public EstIdentifier Identifier { get; } = identifier;
public EstEntry Entry { get; } = entry;
[JsonConverter(typeof(StringEnumConverter))]
public Gender Gender
=> Identifier.Gender;
[JsonConverter(typeof(StringEnumConverter))]
public ModelRace Race
=> Identifier.Race;
public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))]
public EstType Slot
=> Identifier.Slot;
[JsonConstructor]
public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry)
: this(new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)), entry)
{ }
public EstManipulation Copy(EstEntry entry)
=> new(Identifier, entry);
public override string ToString()
=> $"Est - {SetId} - {Slot} - {Race.ToName()} {Gender.ToName()}";
public bool Equals(EstManipulation other)
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Slot == other.Slot;
public override bool Equals(object? obj)
=> obj is EstManipulation other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot);
public int CompareTo(EstManipulation other)
{
var r = Race.CompareTo(other.Race);
if (r != 0)
return r;
var g = Gender.CompareTo(other.Gender);
if (g != 0)
return g;
var s = Slot.CompareTo(other.Slot);
return s != 0 ? s : SetId.Id.CompareTo(other.SetId.Id);
}
public MetaIndex FileIndex()
=> (MetaIndex)Slot;
public bool Apply(EstFile file)
{
return file.SetEntry(Names.CombinedRace(Gender, Race), SetId.Id, Entry) switch
{
EstFile.EstEntryChange.Unchanged => false,
EstFile.EstEntryChange.Changed => true,
EstFile.EstEntryChange.Added => true,
EstFile.EstEntryChange.Removed => true,
_ => throw new ArgumentOutOfRangeException(),
};
}
public bool Validate()
{
if (!Enum.IsDefined(Slot))
return false;
if (Names.CombinedRace(Gender, Race) == GenderRace.Unknown)
return false;
// No known check for set id or entry.
return true;
}
}

View file

@ -1,11 +1,12 @@
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public readonly struct GlobalEqpManipulation : IMetaManipulation<GlobalEqpManipulation>, IMetaIdentifier
public readonly struct GlobalEqpManipulation : IMetaIdentifier
{
public GlobalEqpType Type { get; init; }
public PrimaryId Condition { get; init; }
@ -70,8 +71,29 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation<GlobalEqpManipu
=> $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
{ }
{
var path = Type switch
{
GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Ears),
GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Neck),
GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists),
GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger),
GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger),
GlobalEqpType.DoNotHideHrothgarHats => string.Empty,
GlobalEqpType.DoNotHideVieraHats => string.Empty,
_ => string.Empty,
};
if (path.Length > 0)
identifier.Identify(changedItems, path);
else if (Type is GlobalEqpType.DoNotHideVieraHats)
changedItems["All Hats for Viera"] = null;
else if (Type is GlobalEqpType.DoNotHideHrothgarHats)
changedItems["All Hats for Hrothgar"] = null;
}
public MetaIndex FileIndex()
=> MetaIndex.Eqp;
MetaManipulationType IMetaIdentifier.Type
=> MetaManipulationType.GlobalEqp;
}

View file

@ -36,4 +36,7 @@ public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier,
jObj["SetId"] = SetId.Id.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Gmp;
}

View file

@ -1,58 +0,0 @@
using Newtonsoft.Json;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct GmpManipulation(GmpIdentifier identifier, GmpEntry entry) : IMetaManipulation<GmpManipulation>
{
[JsonIgnore]
public GmpIdentifier Identifier { get; } = identifier;
public GmpEntry Entry { get; } = entry;
public PrimaryId SetId
=> Identifier.SetId;
[JsonConstructor]
public GmpManipulation(GmpEntry entry, PrimaryId setId)
: this(new GmpIdentifier(setId), entry)
{ }
public GmpManipulation Copy(GmpEntry entry)
=> new(Identifier, entry);
public override string ToString()
=> $"Gmp - {SetId}";
public bool Equals(GmpManipulation other)
=> SetId == other.SetId;
public override bool Equals(object? obj)
=> obj is GmpManipulation other && Equals(other);
public override int GetHashCode()
=> SetId.GetHashCode();
public int CompareTo(GmpManipulation other)
=> SetId.Id.CompareTo(other.SetId.Id);
public MetaIndex FileIndex()
=> MetaIndex.Gmp;
public bool Apply(ExpandedGmpFile file)
{
var entry = file[SetId];
if (entry == Entry)
return false;
file[SetId] = Entry;
return true;
}
public bool Validate()
// No known conditions.
=> true;
}

View file

@ -4,6 +4,18 @@ using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public enum MetaManipulationType : byte
{
Unknown = 0,
Imc = 1,
Eqdp = 2,
Eqp = 3,
Est = 4,
Gmp = 5,
Rsp = 6,
GlobalEqp = 7,
}
public interface IMetaIdentifier
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems);
@ -13,4 +25,8 @@ public interface IMetaIdentifier
public bool Validate();
public JObject AddToJson(JObject jObj);
public MetaManipulationType Type { get; }
public string ToString();
}

View file

@ -27,9 +27,6 @@ public readonly record struct ImcIdentifier(
: this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown)
{ }
public ImcManipulation ToManipulation(ImcEntry entry)
=> new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
=> AddChangedItems(identifier, changedItems, false);
@ -193,4 +190,7 @@ public readonly record struct ImcIdentifier(
jObj["BodySlot"] = BodySlot.ToString();
return jObj;
}
public MetaManipulationType Type
=> MetaManipulationType.Imc;
}

View file

@ -1,108 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.String.Classes;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ImcManipulation : IMetaManipulation<ImcManipulation>
{
[JsonIgnore]
public ImcIdentifier Identifier { get; private init; }
public ImcEntry Entry { get; private init; }
public PrimaryId PrimaryId
=> Identifier.PrimaryId;
public SecondaryId SecondaryId
=> Identifier.SecondaryId;
public Variant Variant
=> Identifier.Variant;
[JsonConverter(typeof(StringEnumConverter))]
public ObjectType ObjectType
=> Identifier.ObjectType;
[JsonConverter(typeof(StringEnumConverter))]
public EquipSlot EquipSlot
=> Identifier.EquipSlot;
[JsonConverter(typeof(StringEnumConverter))]
public BodySlot BodySlot
=> Identifier.BodySlot;
public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry)
: this(new ImcIdentifier(equipSlot, primaryId, variant), entry)
{ }
public ImcManipulation(ImcIdentifier identifier, ImcEntry entry)
{
Identifier = identifier;
Entry = entry;
}
// Variants were initially ushorts but got shortened to bytes.
// There are still some manipulations around that have values > 255 for variant,
// so we change the unused value to something nonsensical in that case, just so they do not compare equal,
// and clamp the variant to 255.
[JsonConstructor]
internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, SecondaryId secondaryId, ushort variant,
EquipSlot equipSlot, ImcEntry entry)
{
Entry = entry;
var v = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue);
Identifier = objectType switch
{
ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot,
variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown),
ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown),
_ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, bodySlot == BodySlot.Unknown ? BodySlot.Body : BodySlot.Unknown),
};
}
public ImcManipulation Copy(ImcEntry entry)
=> new(Identifier, entry);
public override string ToString()
=> Identifier.ToString();
public bool Equals(ImcManipulation other)
=> Identifier == other.Identifier;
public override bool Equals(object? obj)
=> obj is ImcManipulation other && Equals(other);
public override int GetHashCode()
=> Identifier.GetHashCode();
public int CompareTo(ImcManipulation other)
=> Identifier.CompareTo(other.Identifier);
public MetaIndex FileIndex()
=> Identifier.FileIndex();
public Utf8GamePath GamePath()
=> Identifier.GamePath();
public bool Apply(ImcFile file)
=> file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry);
public bool Validate(bool withMaterial)
{
if (!Identifier.Validate())
return false;
if (withMaterial && Entry.MaterialId == 0)
return false;
return true;
}
}

View file

@ -1,5 +1,6 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Structs;
using Penumbra.Util;
using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
@ -7,7 +8,7 @@ using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
namespace Penumbra.Meta.Manipulations;
[JsonConverter(typeof(Converter))]
public class MetaDictionary : IEnumerable<MetaManipulation>
public class MetaDictionary
{
private readonly Dictionary<ImcIdentifier, ImcEntry> _imc = [];
private readonly Dictionary<EqpIdentifier, EqpEntryInternal> _eqp = [];
@ -20,32 +21,50 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
=> _imc;
public IReadOnlyDictionary<EqpIdentifier, EqpEntryInternal> Eqp
=> _eqp;
public IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> Eqdp
=> _eqdp;
public IReadOnlyDictionary<EstIdentifier, EstEntry> Est
=> _est;
public IReadOnlyDictionary<GmpIdentifier, GmpEntry> Gmp
=> _gmp;
public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp
=> _rsp;
public IReadOnlySet<GlobalEqpManipulation> GlobalEqp
=> _globalEqp;
public int Count { get; private set; }
public int GetCount(MetaManipulation.Type type)
public int GetCount(MetaManipulationType type)
=> type switch
{
MetaManipulation.Type.Imc => _imc.Count,
MetaManipulation.Type.Eqdp => _eqdp.Count,
MetaManipulation.Type.Eqp => _eqp.Count,
MetaManipulation.Type.Est => _est.Count,
MetaManipulation.Type.Gmp => _gmp.Count,
MetaManipulation.Type.Rsp => _rsp.Count,
MetaManipulation.Type.GlobalEqp => _globalEqp.Count,
_ => 0,
MetaManipulationType.Imc => _imc.Count,
MetaManipulationType.Eqdp => _eqdp.Count,
MetaManipulationType.Eqp => _eqp.Count,
MetaManipulationType.Est => _est.Count,
MetaManipulationType.Gmp => _gmp.Count,
MetaManipulationType.Rsp => _rsp.Count,
MetaManipulationType.GlobalEqp => _globalEqp.Count,
_ => 0,
};
public bool CanAdd(IMetaIdentifier identifier)
public bool Contains(IMetaIdentifier identifier)
=> identifier switch
{
EqdpIdentifier eqdpIdentifier => !_eqdp.ContainsKey(eqdpIdentifier),
EqpIdentifier eqpIdentifier => !_eqp.ContainsKey(eqpIdentifier),
EstIdentifier estIdentifier => !_est.ContainsKey(estIdentifier),
GlobalEqpManipulation globalEqpManipulation => !_globalEqp.Contains(globalEqpManipulation),
GmpIdentifier gmpIdentifier => !_gmp.ContainsKey(gmpIdentifier),
ImcIdentifier imcIdentifier => !_imc.ContainsKey(imcIdentifier),
RspIdentifier rspIdentifier => !_rsp.ContainsKey(rspIdentifier),
_ => false,
EqdpIdentifier i => _eqdp.ContainsKey(i),
EqpIdentifier i => _eqp.ContainsKey(i),
EstIdentifier i => _est.ContainsKey(i),
GlobalEqpManipulation i => _globalEqp.Contains(i),
GmpIdentifier i => _gmp.ContainsKey(i),
ImcIdentifier i => _imc.ContainsKey(i),
RspIdentifier i => _rsp.ContainsKey(i),
_ => false,
};
public void Clear()
@ -69,17 +88,16 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
&& _gmp.SetEquals(other._gmp)
&& _globalEqp.SetEquals(other._globalEqp);
public IEnumerator<MetaManipulation> GetEnumerator()
=> _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value)))
.Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)))))
.Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)))))
.Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value))))
.Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value))))
.Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value))))
.Concat(_globalEqp.Select(manip => new MetaManipulation(manip))).GetEnumerator();
public IEnumerable<IMetaIdentifier> Identifiers
=> _imc.Keys.Cast<IMetaIdentifier>()
.Concat(_eqdp.Keys.Cast<IMetaIdentifier>())
.Concat(_eqp.Keys.Cast<IMetaIdentifier>())
.Concat(_est.Keys.Cast<IMetaIdentifier>())
.Concat(_gmp.Keys.Cast<IMetaIdentifier>())
.Concat(_rsp.Keys.Cast<IMetaIdentifier>())
.Concat(_globalEqp.Cast<IMetaIdentifier>());
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
#region TryAdd
public bool TryAdd(ImcIdentifier identifier, ImcEntry entry)
{
@ -90,7 +108,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true;
}
public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry)
{
if (!_eqp.TryAdd(identifier, entry))
@ -103,7 +120,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
public bool TryAdd(EqpIdentifier identifier, EqpEntry entry)
=> TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot));
public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry)
{
if (!_eqdp.TryAdd(identifier, entry))
@ -152,6 +168,10 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true;
}
#endregion
#region Update
public bool Update(ImcIdentifier identifier, ImcEntry entry)
{
if (!_imc.ContainsKey(identifier))
@ -161,7 +181,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true;
}
public bool Update(EqpIdentifier identifier, EqpEntryInternal entry)
{
if (!_eqp.ContainsKey(identifier))
@ -174,7 +193,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
public bool Update(EqpIdentifier identifier, EqpEntry entry)
=> Update(identifier, new EqpEntryInternal(entry, identifier.Slot));
public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry)
{
if (!_eqdp.ContainsKey(identifier))
@ -214,6 +232,50 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true;
}
#endregion
#region TryGetValue
public bool TryGetValue(EstIdentifier identifier, out EstEntry value)
=> _est.TryGetValue(identifier, out value);
public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value)
=> _eqp.TryGetValue(identifier, out value);
public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value)
=> _eqdp.TryGetValue(identifier, out value);
public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value)
=> _gmp.TryGetValue(identifier, out value);
public bool TryGetValue(RspIdentifier identifier, out RspEntry value)
=> _rsp.TryGetValue(identifier, out value);
public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value)
=> _imc.TryGetValue(identifier, out value);
#endregion
public bool Remove(IMetaIdentifier identifier)
{
var ret = identifier switch
{
EqdpIdentifier i => _eqdp.Remove(i),
EqpIdentifier i => _eqp.Remove(i),
EstIdentifier i => _est.Remove(i),
GlobalEqpManipulation i => _globalEqp.Remove(i),
GmpIdentifier i => _gmp.Remove(i),
ImcIdentifier i => _imc.Remove(i),
RspIdentifier i => _rsp.Remove(i),
_ => false,
};
if (ret)
--Count;
return ret;
}
#region Merging
public void UnionWith(MetaDictionary manips)
{
foreach (var (identifier, entry) in manips._imc)
@ -287,24 +349,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return false;
}
public bool TryGetValue(EstIdentifier identifier, out EstEntry value)
=> _est.TryGetValue(identifier, out value);
public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value)
=> _eqp.TryGetValue(identifier, out value);
public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value)
=> _eqdp.TryGetValue(identifier, out value);
public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value)
=> _gmp.TryGetValue(identifier, out value);
public bool TryGetValue(RspIdentifier identifier, out RspEntry value)
=> _rsp.TryGetValue(identifier, out value);
public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value)
=> _imc.TryGetValue(identifier, out value);
public void SetTo(MetaDictionary other)
{
_imc.SetTo(other._imc);
@ -329,6 +373,8 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count;
}
#endregion
public MetaDictionary Clone()
{
var ret = new MetaDictionary();
@ -336,29 +382,124 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return ret;
}
private static void WriteJson(JsonWriter writer, JsonSerializer serializer, IMetaIdentifier identifier, object entry)
{
var type = identifier switch
public static JObject Serialize(EqpIdentifier identifier, EqpEntryInternal entry)
=> Serialize(identifier, entry.ToEntry(identifier.Slot));
public static JObject Serialize(EqpIdentifier identifier, EqpEntry entry)
=> new()
{
ImcIdentifier => "Imc",
EqdpIdentifier => "Eqdp",
EqpIdentifier => "Eqp",
EstIdentifier => "Est",
GmpIdentifier => "Gmp",
RspIdentifier => "Rsp",
GlobalEqpManipulation => "GlobalEqp",
_ => string.Empty,
["Type"] = MetaManipulationType.Eqp.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = (ulong)entry,
}),
};
if (type.Length == 0)
return;
public static JObject Serialize(EqdpIdentifier identifier, EqdpEntryInternal entry)
=> Serialize(identifier, entry.ToEntry(identifier.Slot));
writer.WriteStartObject();
writer.WritePropertyName("Type");
writer.WriteValue(type);
writer.WritePropertyName("Manipulation");
public static JObject Serialize(EqdpIdentifier identifier, EqdpEntry entry)
=> new()
{
["Type"] = MetaManipulationType.Eqdp.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = (ushort)entry,
}),
};
writer.WriteEndObject();
public static JObject Serialize(EstIdentifier identifier, EstEntry entry)
=> new()
{
["Type"] = MetaManipulationType.Est.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = entry.Value,
}),
};
public static JObject Serialize(GmpIdentifier identifier, GmpEntry entry)
=> new()
{
["Type"] = MetaManipulationType.Gmp.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = JObject.FromObject(entry),
}),
};
public static JObject Serialize(ImcIdentifier identifier, ImcEntry entry)
=> new()
{
["Type"] = MetaManipulationType.Imc.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = JObject.FromObject(entry),
}),
};
public static JObject Serialize(RspIdentifier identifier, RspEntry entry)
=> new()
{
["Type"] = MetaManipulationType.Rsp.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = entry.Value,
}),
};
public static JObject Serialize(GlobalEqpManipulation identifier)
=> new()
{
["Type"] = MetaManipulationType.GlobalEqp.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject()),
};
public static JObject? Serialize<TIdentifier, TEntry>(TIdentifier identifier, TEntry entry)
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntryInternal))
return Serialize(Unsafe.As<TIdentifier, EqpIdentifier>(ref identifier), Unsafe.As<TEntry, EqpEntryInternal>(ref entry));
if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntry))
return Serialize(Unsafe.As<TIdentifier, EqpIdentifier>(ref identifier), Unsafe.As<TEntry, EqpEntry>(ref entry));
if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntryInternal))
return Serialize(Unsafe.As<TIdentifier, EqdpIdentifier>(ref identifier), Unsafe.As<TEntry, EqdpEntryInternal>(ref entry));
if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntry))
return Serialize(Unsafe.As<TIdentifier, EqdpIdentifier>(ref identifier), Unsafe.As<TEntry, EqdpEntry>(ref entry));
if (typeof(TIdentifier) == typeof(EstIdentifier) && typeof(TEntry) == typeof(EstEntry))
return Serialize(Unsafe.As<TIdentifier, EstIdentifier>(ref identifier), Unsafe.As<TEntry, EstEntry>(ref entry));
if (typeof(TIdentifier) == typeof(GmpIdentifier) && typeof(TEntry) == typeof(GmpEntry))
return Serialize(Unsafe.As<TIdentifier, GmpIdentifier>(ref identifier), Unsafe.As<TEntry, GmpEntry>(ref entry));
if (typeof(TIdentifier) == typeof(RspIdentifier) && typeof(TEntry) == typeof(RspEntry))
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(GlobalEqpManipulation))
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier));
return null;
}
public static JArray SerializeTo<TIdentifier, TEntry>(JArray array, IEnumerable<KeyValuePair<TIdentifier, TEntry>> manipulations)
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
foreach (var (identifier, entry) in manipulations)
{
if (Serialize(identifier, entry) is { } jObj)
array.Add(jObj);
}
return array;
}
public static JArray SerializeTo(JArray array, IEnumerable<GlobalEqpManipulation> manipulations)
{
foreach (var manip in manipulations)
array.Add(Serialize(manip));
return array;
}
private class Converter : JsonConverter<MetaDictionary>
@ -371,30 +512,27 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return;
}
writer.WriteStartArray();
foreach (var item in value)
{
writer.WriteStartObject();
writer.WritePropertyName("Type");
writer.WriteValue(item.ManipulationType.ToString());
writer.WritePropertyName("Manipulation");
serializer.Serialize(writer, item.Manipulation);
writer.WriteEndObject();
}
writer.WriteEndArray();
var array = new JArray();
SerializeTo(array, value._imc);
SerializeTo(array, value._eqp);
SerializeTo(array, value._eqdp);
SerializeTo(array, value._est);
SerializeTo(array, value._rsp);
SerializeTo(array, value._gmp);
SerializeTo(array, value._globalEqp);
array.WriteTo(writer);
}
public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var dict = existingValue ?? [];
var dict = existingValue ?? new MetaDictionary();
dict.Clear();
var jObj = JArray.Load(reader);
foreach (var item in jObj)
{
var type = item["Type"]?.ToObject<MetaManipulation.Type>() ?? MetaManipulation.Type.Unknown;
if (type is MetaManipulation.Type.Unknown)
var type = item["Type"]?.ToObject<MetaManipulationType>() ?? MetaManipulationType.Unknown;
if (type is MetaManipulationType.Unknown)
{
Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered.");
continue;
@ -408,7 +546,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
switch (type)
{
case MetaManipulation.Type.Imc:
case MetaManipulationType.Imc:
{
var identifier = ImcIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<ImcEntry>();
@ -418,7 +556,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid IMC Manipulation encountered.");
break;
}
case MetaManipulation.Type.Eqdp:
case MetaManipulationType.Eqdp:
{
var identifier = EqdpIdentifier.FromJson(manip);
var entry = (EqdpEntry?)manip["Entry"]?.ToObject<ushort>();
@ -428,7 +566,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid EQDP Manipulation encountered.");
break;
}
case MetaManipulation.Type.Eqp:
case MetaManipulationType.Eqp:
{
var identifier = EqpIdentifier.FromJson(manip);
var entry = (EqpEntry?)manip["Entry"]?.ToObject<ulong>();
@ -438,7 +576,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid EQP Manipulation encountered.");
break;
}
case MetaManipulation.Type.Est:
case MetaManipulationType.Est:
{
var identifier = EstIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<EstEntry>();
@ -448,7 +586,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid EST Manipulation encountered.");
break;
}
case MetaManipulation.Type.Gmp:
case MetaManipulationType.Gmp:
{
var identifier = GmpIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<GmpEntry>();
@ -458,7 +596,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid GMP Manipulation encountered.");
break;
}
case MetaManipulation.Type.Rsp:
case MetaManipulationType.Rsp:
{
var identifier = RspIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<RspEntry>();
@ -468,7 +606,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid RSP Manipulation encountered.");
break;
}
case MetaManipulation.Type.GlobalEqp:
case MetaManipulationType.GlobalEqp:
{
var identifier = GlobalEqpManipulation.FromJson(manip);
if (identifier.HasValue)
@ -483,4 +621,22 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return dict;
}
}
public MetaDictionary()
{ }
public MetaDictionary(MetaCache? cache)
{
if (cache == null)
return;
_imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
_eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
_eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
_est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
_gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
_rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
_globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet();
Count = cache.Count;
}
}

View file

@ -1,457 +0,0 @@
using Dalamud.Interface;
using ImGuiNET;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Mods.Editor;
using Penumbra.String.Functions;
using Penumbra.UI;
using Penumbra.UI.ModsTab;
namespace Penumbra.Meta.Manipulations;
#if false
private static class ImcRow
{
private static ImcIdentifier _newIdentifier = ImcIdentifier.Default;
private static float IdWidth
=> 80 * UiHelpers.Scale;
private static float SmallIdWidth
=> 45 * UiHelpers.Scale;
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize,
editor.MetaEditor.Imc.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true);
var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry);
var canAdd = fileExists && editor.MetaEditor.CanAdd(manip);
var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(manip);
// Identifier
ImGui.TableNextColumn();
var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier);
ImGui.TableNextColumn();
change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
ImGui.TableNextColumn();
// Equipment and accessories are slightly different imcs than other types.
if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory)
change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier);
else
change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier);
ImGui.TableNextColumn();
change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier);
ImGui.TableNextColumn();
if (_newIdentifier.ObjectType is ObjectType.DemiHuman)
change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70);
else
ImUtf8.ScaledDummy(new Vector2(70 * UiHelpers.Scale, 0));
if (change)
defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry;
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false);
ImGui.SameLine();
ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false);
ImGui.TableNextColumn();
ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false);
ImGui.SameLine();
ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false);
ImGui.SameLine();
ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false);
ImGui.TableNextColumn();
ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry);
}
public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.ObjectType.ToName());
ImGuiUtil.HoverTooltip(ObjectTypeTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.PrimaryId.ToString());
ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory)
{
ImGui.TextUnformatted(meta.EquipSlot.ToName());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
}
else
{
ImGui.TextUnformatted(meta.SecondaryId.ToString());
ImGuiUtil.HoverTooltip(SecondaryIdTooltip);
}
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Variant.ToString());
ImGuiUtil.HoverTooltip(VariantIdTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
if (meta.ObjectType is ObjectType.DemiHuman)
{
ImGui.TextUnformatted(meta.EquipSlot.ToName());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
}
// Values
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
ImGui.TableNextColumn();
var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry;
var newEntry = meta.Entry;
var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true);
ImGui.SameLine();
changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true);
ImGui.TableNextColumn();
changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true);
ImGui.SameLine();
changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true);
ImGui.SameLine();
changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true);
ImGui.TableNextColumn();
changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry);
if (changes)
editor.MetaEditor.Change(meta.Copy(newEntry));
}
}
#endif
public interface IMetaManipulation
{
public MetaIndex FileIndex();
}
public interface IMetaManipulation<T>
: IMetaManipulation, IComparable<T>, IEquatable<T> where T : struct
{ }
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 16)]
public readonly struct MetaManipulation : IEquatable<MetaManipulation>, IComparable<MetaManipulation>
{
public const int CurrentVersion = 0;
public enum Type : byte
{
Unknown = 0,
Imc = 1,
Eqdp = 2,
Eqp = 3,
Est = 4,
Gmp = 5,
Rsp = 6,
GlobalEqp = 7,
}
[FieldOffset(0)]
[JsonIgnore]
public readonly EqpManipulation Eqp = default;
[FieldOffset(0)]
[JsonIgnore]
public readonly GmpManipulation Gmp = default;
[FieldOffset(0)]
[JsonIgnore]
public readonly EqdpManipulation Eqdp = default;
[FieldOffset(0)]
[JsonIgnore]
public readonly EstManipulation Est = default;
[FieldOffset(0)]
[JsonIgnore]
public readonly RspManipulation Rsp = default;
[FieldOffset(0)]
[JsonIgnore]
public readonly ImcManipulation Imc = default;
[FieldOffset(0)]
[JsonIgnore]
public readonly GlobalEqpManipulation GlobalEqp = default;
[FieldOffset(15)]
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("Type")]
public readonly Type ManipulationType;
public object? Manipulation
{
get => ManipulationType switch
{
Type.Unknown => null,
Type.Imc => Imc,
Type.Eqdp => Eqdp,
Type.Eqp => Eqp,
Type.Est => Est,
Type.Gmp => Gmp,
Type.Rsp => Rsp,
Type.GlobalEqp => GlobalEqp,
_ => null,
};
init
{
switch (value)
{
case EqpManipulation m:
Eqp = m;
ManipulationType = m.Validate() ? Type.Eqp : Type.Unknown;
return;
case EqdpManipulation m:
Eqdp = m;
ManipulationType = m.Validate() ? Type.Eqdp : Type.Unknown;
return;
case GmpManipulation m:
Gmp = m;
ManipulationType = m.Validate() ? Type.Gmp : Type.Unknown;
return;
case EstManipulation m:
Est = m;
ManipulationType = m.Validate() ? Type.Est : Type.Unknown;
return;
case RspManipulation m:
Rsp = m;
ManipulationType = m.Validate() ? Type.Rsp : Type.Unknown;
return;
case ImcManipulation m:
Imc = m;
ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown;
return;
case GlobalEqpManipulation m:
GlobalEqp = m;
ManipulationType = m.Validate() ? Type.GlobalEqp : Type.Unknown;
return;
}
}
}
public bool Validate()
{
return ManipulationType switch
{
Type.Imc => Imc.Validate(true),
Type.Eqdp => Eqdp.Validate(),
Type.Eqp => Eqp.Validate(),
Type.Est => Est.Validate(),
Type.Gmp => Gmp.Validate(),
Type.Rsp => Rsp.Validate(),
Type.GlobalEqp => GlobalEqp.Validate(),
_ => false,
};
}
public MetaManipulation(EqpManipulation eqp)
{
Eqp = eqp;
ManipulationType = Type.Eqp;
}
public MetaManipulation(GmpManipulation gmp)
{
Gmp = gmp;
ManipulationType = Type.Gmp;
}
public MetaManipulation(EqdpManipulation eqdp)
{
Eqdp = eqdp;
ManipulationType = Type.Eqdp;
}
public MetaManipulation(EstManipulation est)
{
Est = est;
ManipulationType = Type.Est;
}
public MetaManipulation(RspManipulation rsp)
{
Rsp = rsp;
ManipulationType = Type.Rsp;
}
public MetaManipulation(ImcManipulation imc)
{
Imc = imc;
ManipulationType = Type.Imc;
}
public MetaManipulation(GlobalEqpManipulation eqp)
{
GlobalEqp = eqp;
ManipulationType = Type.GlobalEqp;
}
public static implicit operator MetaManipulation(EqpManipulation eqp)
=> new(eqp);
public static implicit operator MetaManipulation(GmpManipulation gmp)
=> new(gmp);
public static implicit operator MetaManipulation(EqdpManipulation eqdp)
=> new(eqdp);
public static implicit operator MetaManipulation(EstManipulation est)
=> new(est);
public static implicit operator MetaManipulation(RspManipulation rsp)
=> new(rsp);
public static implicit operator MetaManipulation(ImcManipulation imc)
=> new(imc);
public static implicit operator MetaManipulation(GlobalEqpManipulation eqp)
=> new(eqp);
public bool EntryEquals(MetaManipulation other)
{
if (ManipulationType != other.ManipulationType)
return false;
return ManipulationType switch
{
Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry),
Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry),
Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry),
Type.Est => Est.Entry.Equals(other.Est.Entry),
Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry),
Type.Imc => Imc.Entry.Equals(other.Imc.Entry),
Type.GlobalEqp => true,
_ => throw new ArgumentOutOfRangeException(),
};
}
public bool Equals(MetaManipulation other)
{
if (ManipulationType != other.ManipulationType)
return false;
return ManipulationType switch
{
Type.Eqp => Eqp.Equals(other.Eqp),
Type.Gmp => Gmp.Equals(other.Gmp),
Type.Eqdp => Eqdp.Equals(other.Eqdp),
Type.Est => Est.Equals(other.Est),
Type.Rsp => Rsp.Equals(other.Rsp),
Type.Imc => Imc.Equals(other.Imc),
Type.GlobalEqp => GlobalEqp.Equals(other.GlobalEqp),
_ => false,
};
}
public MetaManipulation WithEntryOf(MetaManipulation other)
{
if (ManipulationType != other.ManipulationType)
return this;
return ManipulationType switch
{
Type.Eqp => Eqp.Copy(other.Eqp.Entry),
Type.Gmp => Gmp.Copy(other.Gmp.Entry),
Type.Eqdp => Eqdp.Copy(other.Eqdp),
Type.Est => Est.Copy(other.Est.Entry),
Type.Rsp => Rsp.Copy(other.Rsp.Entry),
Type.Imc => Imc.Copy(other.Imc.Entry),
Type.GlobalEqp => GlobalEqp,
_ => throw new ArgumentOutOfRangeException(),
};
}
public override bool Equals(object? obj)
=> obj is MetaManipulation other && Equals(other);
public override int GetHashCode()
=> ManipulationType switch
{
Type.Eqp => Eqp.GetHashCode(),
Type.Gmp => Gmp.GetHashCode(),
Type.Eqdp => Eqdp.GetHashCode(),
Type.Est => Est.GetHashCode(),
Type.Rsp => Rsp.GetHashCode(),
Type.Imc => Imc.GetHashCode(),
Type.GlobalEqp => GlobalEqp.GetHashCode(),
_ => 0,
};
public unsafe int CompareTo(MetaManipulation other)
{
fixed (MetaManipulation* lhs = &this)
{
return MemoryUtility.MemCmpUnchecked(lhs, &other, sizeof(MetaManipulation));
}
}
public override string ToString()
=> ManipulationType switch
{
Type.Eqp => Eqp.ToString(),
Type.Gmp => Gmp.ToString(),
Type.Eqdp => Eqdp.ToString(),
Type.Est => Est.ToString(),
Type.Rsp => Rsp.ToString(),
Type.Imc => Imc.ToString(),
Type.GlobalEqp => GlobalEqp.ToString(),
_ => "Invalid",
};
public string EntryToString()
=> ManipulationType switch
{
Type.Imc =>
$"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}",
Type.Eqdp => $"{(ushort)Eqdp.Entry:X}",
Type.Eqp => $"{(ulong)Eqp.Entry:X}",
Type.Est => $"{Est.Entry}",
Type.Gmp => $"{Gmp.Entry.Value}",
Type.Rsp => $"{Rsp.Entry}",
Type.GlobalEqp => string.Empty,
_ => string.Empty,
};
public static bool operator ==(MetaManipulation left, MetaManipulation right)
=> left.Equals(right);
public static bool operator !=(MetaManipulation left, MetaManipulation right)
=> !(left == right);
public static bool operator <(MetaManipulation left, MetaManipulation right)
=> left.CompareTo(right) < 0;
public static bool operator <=(MetaManipulation left, MetaManipulation right)
=> left.CompareTo(right) <= 0;
public static bool operator >(MetaManipulation left, MetaManipulation right)
=> left.CompareTo(right) > 0;
public static bool operator >=(MetaManipulation left, MetaManipulation right)
=> left.CompareTo(right) >= 0;
}

View file

@ -38,6 +38,9 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib
var ret = new RspIdentifier(subRace, attribute);
return ret.Validate() ? ret : null;
}
public MetaManipulationType Type
=> MetaManipulationType.Rsp;
}
[JsonConverter(typeof(Converter))]

View file

@ -1,67 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct RspManipulation(RspIdentifier identifier, RspEntry entry) : IMetaManipulation<RspManipulation>
{
[JsonIgnore]
public RspIdentifier Identifier { get; } = identifier;
public RspEntry Entry { get; } = entry;
[JsonConverter(typeof(StringEnumConverter))]
public SubRace SubRace
=> Identifier.SubRace;
[JsonConverter(typeof(StringEnumConverter))]
public RspAttribute Attribute
=> Identifier.Attribute;
[JsonConstructor]
public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry)
: this(new RspIdentifier(subRace, attribute), entry)
{ }
public RspManipulation Copy(RspEntry entry)
=> new(Identifier, entry);
public override string ToString()
=> $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}";
public bool Equals(RspManipulation other)
=> SubRace == other.SubRace
&& Attribute == other.Attribute;
public override bool Equals(object? obj)
=> obj is RspManipulation other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)SubRace, (int)Attribute);
public int CompareTo(RspManipulation other)
{
var s = SubRace.CompareTo(other.SubRace);
return s != 0 ? s : Attribute.CompareTo(other.Attribute);
}
public MetaIndex FileIndex()
=> MetaIndex.HumanCmp;
public bool Apply(CmpFile file)
{
var value = file[SubRace, Attribute];
if (value == Entry)
return false;
file[SubRace, Attribute] = Entry;
return true;
}
public bool Validate()
=> Identifier.Validate() && Entry.Validate();
}

View file

@ -10,7 +10,7 @@ public record struct AppliedModData(
Dictionary<Utf8GamePath, FullPath> FileRedirections,
MetaDictionary Manipulations)
{
public static readonly AppliedModData Empty = new([], []);
public static readonly AppliedModData Empty = new([], new MetaDictionary());
}
public interface IMod

View file

@ -26,20 +26,20 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
}
}
public readonly FrozenDictionary<MetaManipulation.Type, OtherOptionData> OtherData =
Enum.GetValues<MetaManipulation.Type>().ToFrozenDictionary(t => t, _ => new OtherOptionData());
public readonly FrozenDictionary<MetaManipulationType, OtherOptionData> OtherData =
Enum.GetValues<MetaManipulationType>().ToFrozenDictionary(t => t, _ => new OtherOptionData());
public bool Changes { get; private set; }
public bool Changes { get; set; }
public new void Clear()
{
Changes = Count > 0;
base.Clear();
Changes = true;
}
public void Load(Mod mod, IModDataContainer currentOption)
{
foreach (var type in Enum.GetValues<MetaManipulation.Type>())
foreach (var type in Enum.GetValues<MetaManipulationType>())
OtherData[type].Clear();
foreach (var option in mod.AllDataContainers)
@ -48,13 +48,13 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
continue;
var name = option.GetFullName();
OtherData[MetaManipulation.Type.Imc].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Imc));
OtherData[MetaManipulation.Type.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqp));
OtherData[MetaManipulation.Type.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqdp));
OtherData[MetaManipulation.Type.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Gmp));
OtherData[MetaManipulation.Type.Est].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Est));
OtherData[MetaManipulation.Type.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Rsp));
OtherData[MetaManipulation.Type.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.GlobalEqp));
OtherData[MetaManipulationType.Imc].Add(name, option.Manipulations.GetCount(MetaManipulationType.Imc));
OtherData[MetaManipulationType.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqp));
OtherData[MetaManipulationType.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqdp));
OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp));
OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est));
OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp));
OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp));
}
Clear();

View file

@ -125,8 +125,8 @@ public static class EquipmentSwap
_ => (EstType)0,
};
var skipFemale = false;
var skipMale = false;
var skipFemale = false;
var skipMale = false;
foreach (var gr in Enum.GetValues<GenderRace>())
{
switch (gr.Split().Item1)
@ -242,8 +242,8 @@ public static class EquipmentSwap
private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom,
PrimaryId idFrom, PrimaryId idTo, Variant variantFrom)
{
var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default);
var imc = new ImcFile(manager, entry.Identifier);
var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom);
var imc = new ImcFile(manager, ident);
EquipItem[] items;
Variant[] variants;
if (idFrom == idTo)
@ -273,7 +273,8 @@ public static class EquipmentSwap
var manipToIdentifier = new GmpIdentifier(idTo);
var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier);
var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier);
return new MetaSwap<GmpIdentifier, GmpEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault);
return new MetaSwap<GmpIdentifier, GmpEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
manipToIdentifier, manipToDefault);
}
public static MetaSwap<ImcIdentifier, ImcEntry> CreateImc(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
@ -286,16 +287,17 @@ public static class EquipmentSwap
Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
{
var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom);
var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo);
var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo);
var imc = new MetaSwap<ImcIdentifier, ImcEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault);
var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo);
var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo);
var imc = new MetaSwap<ImcIdentifier, ImcEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
manipToIdentifier, manipToDefault);
var decal = CreateDecal(manager, redirections, imc.SwapToModdedEntry.DecalId);
if (decal != null)
imc.ChildSwaps.Add(decal);
var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModdedEntry.VfxId);
var avfx = CreateAvfx(manager, redirections, slotFrom, slotTo, idFrom, idTo, imc.SwapToModdedEntry.VfxId);
if (avfx != null)
imc.ChildSwaps.Add(avfx);
@ -316,19 +318,21 @@ public static class EquipmentSwap
// Example: Abyssos Helm / Body
public static FileSwap? CreateAvfx(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, PrimaryId idFrom, PrimaryId idTo,
public static FileSwap? CreateAvfx(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slotFrom, EquipSlot slotTo,
PrimaryId idFrom, PrimaryId idTo,
byte vfxId)
{
if (vfxId == 0)
return null;
var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId);
var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId);
var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo);
vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom);
var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId);
var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo);
foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan())
{
var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged);
var atex = CreateAtex(manager, redirections, slotFrom, slotTo, idFrom, ref filePath, ref avfx.DataWasChanged);
avfx.ChildSwaps.Add(atex);
}
@ -394,8 +398,7 @@ public static class EquipmentSwap
}
public static FileSwap CreateTex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, char prefix, PrimaryId idFrom,
PrimaryId idTo,
ref MtrlFile.Texture texture, ref bool dataWasChanged)
PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged)
=> CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged);
public static FileSwap CreateTex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, char prefix, EquipSlot slotFrom,
@ -404,6 +407,7 @@ public static class EquipmentSwap
var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path);
var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom);
newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom);
newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom);
newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}");
if (newPath != path)
{
@ -421,11 +425,12 @@ public static class EquipmentSwap
return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path);
}
public static FileSwap CreateAtex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, ref string filePath,
ref bool dataWasChanged)
public static FileSwap CreateAtex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slotFrom, EquipSlot slotTo,
PrimaryId idFrom, ref string filePath, ref bool dataWasChanged)
{
var oldPath = filePath;
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom);
dataWasChanged = true;
return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath);

View file

@ -136,14 +136,14 @@ public static class ItemSwap
public static FileSwap CreatePhyb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstType type,
GenderRace race, EstEntry estEntry)
{
var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry.AsId);
var phybPath = GamePaths.Skeleton.Phyb.Path(race, type.ToName(), estEntry.AsId);
return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath);
}
public static FileSwap CreateSklb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstType type,
GenderRace race, EstEntry estEntry)
{
var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry.AsId);
var sklbPath = GamePaths.Skeleton.Sklb.Path(race, type.ToName(), estEntry.AsId);
return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath);
}
@ -154,10 +154,11 @@ public static class ItemSwap
return null;
var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace);
var manipToIdentifier = new EstIdentifier(idTo, type, genderRace);
var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier);
var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier);
var est = new MetaSwap<EstIdentifier, EstEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault);
var manipToIdentifier = new EstIdentifier(idTo, type, genderRace);
var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier);
var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier);
var est = new MetaSwap<EstIdentifier, EstEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
manipToIdentifier, manipToDefault);
if (ownMdl && est.SwapToModdedEntry.Value >= 2)
{
@ -215,6 +216,22 @@ public static class ItemSwap
? path.Replace($"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_")
: path;
public static string ReplaceType(string path, EquipSlot from, EquipSlot to, PrimaryId idFrom)
{
var isAccessoryFrom = from.IsAccessory();
if (isAccessoryFrom == to.IsAccessory())
return path;
if (isAccessoryFrom)
{
path = path.Replace("accessory/a", "equipment/e");
return path.Replace($"a{idFrom.Id:D4}", $"e{idFrom.Id:D4}");
}
path = path.Replace("equipment/e", "accessory/a");
return path.Replace($"e{idFrom.Id:D4}", $"a{idFrom.Id:D4}");
}
public static string ReplaceRace(string path, GenderRace from, GenderRace to, bool condition = true)
=> ReplaceId(path, 'c', (ushort)from, (ushort)to, condition);

View file

@ -123,8 +123,8 @@ public class ItemSwapContainer
: p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p);
private MetaDictionary MetaResolver(ModCollection? collection)
=> collection?.MetaCache?.Manipulations is { } cache
? [] // [.. cache] TODO
=> collection?.MetaCache is { } cache
? new MetaDictionary(cache)
: _appliedModData.Manipulations;
public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true,

View file

@ -198,8 +198,7 @@ public partial class ModCreator(
Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
// TODO
option.Manipulations.UnionWith([]);//[.. meta.MetaManipulations]);
option.Manipulations.UnionWith(meta.MetaManipulations);
}
else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{
@ -213,8 +212,7 @@ public partial class ModCreator(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
// TODO
option.Manipulations.UnionWith([]);//[.. rgsp.MetaManipulations]);
option.Manipulations.UnionWith(rgsp.MetaManipulations);
}
}
catch (Exception e)

View file

@ -13,7 +13,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public MetaDictionary Manipulations { get; set; } = [];
public MetaDictionary Manipulations { get; set; } = new();
IMod IModDataContainer.Mod
=> Mod;

View file

@ -33,7 +33,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public MetaDictionary Manipulations { get; set; } = [];
public MetaDictionary Manipulations { get; set; } = new();
public void AddDataTo(Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
=> SubMod.AddContainerTo(this, redirections, manipulations);

View file

@ -93,8 +93,7 @@ public class TemporaryMod : IMod
}
}
// TODO
MetaDictionary manips = []; // [.. collection.MetaCache?.Manipulations ?? []];
var manips = new MetaDictionary(collection.MetaCache);
defaultMod.Manipulations.UnionWith(manips);
saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));

View file

@ -0,0 +1,159 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Meta;
public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
: MetaDrawer<EqdpIdentifier, EqdpEntryInternal>(editor, metaFiles), IService
{
public override ReadOnlySpan<byte> Label
=> "Racial Model Edits (EQDP)###EQDP"u8;
public override int NumColumns
=> 7;
protected override void Initialize()
{
Identifier = new EqdpIdentifier(1, EquipSlot.Head, GenderRace.MidlanderMale);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, Identifier), Identifier.Slot);
protected override void DrawNew()
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqdp));
ImGui.TableNextColumn();
var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0;
var canAdd = validRaceCode && !Editor.Contains(Identifier);
var tt = canAdd ? "Stage this edit."u8 :
validRaceCode ? "This entry is already edited."u8 : "This combination of race and gender can not be used."u8;
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
if (DrawIdentifierInput(ref Identifier))
UpdateEntry();
DrawEntry(Entry, ref Entry, true);
}
protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntryInternal entry)
{
DrawMetaButtons(identifier, entry);
DrawIdentifier(identifier);
var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, identifier), identifier.Slot);
if (DrawEntry(defaultEntry, ref entry, false))
Editor.Changes |= Editor.Update(identifier, entry);
}
protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate()
=> Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value));
private static bool DrawIdentifierInput(ref EqdpIdentifier identifier)
{
ImGui.TableNextColumn();
var changes = DrawPrimaryId(ref identifier);
ImGui.TableNextColumn();
changes |= DrawRace(ref identifier);
ImGui.TableNextColumn();
changes |= DrawGender(ref identifier);
ImGui.TableNextColumn();
changes |= DrawEquipSlot(ref identifier);
return changes;
}
private static void DrawIdentifier(EqdpIdentifier identifier)
{
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor);
ImUtf8.HoverTooltip("Model Set ID"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor);
ImUtf8.HoverTooltip("Model Race"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor);
ImUtf8.HoverTooltip("Gender"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor);
ImUtf8.HoverTooltip("Equip Slot"u8);
}
private static bool DrawEntry(EqdpEntryInternal defaultEntry, ref EqdpEntryInternal entry, bool disabled)
{
var changes = false;
using var dis = ImRaii.Disabled(disabled);
ImGui.TableNextColumn();
if (Checkmark("Material##eqdp"u8, "\0"u8, entry.Material, defaultEntry.Material, out var newMaterial))
{
entry = entry with { Material = newMaterial };
changes = true;
}
ImGui.SameLine();
if (Checkmark("Model##eqdp"u8, "\0"u8, entry.Model, defaultEntry.Model, out var newModel))
{
entry = entry with { Material = newModel };
changes = true;
}
return changes;
}
public static bool DrawPrimaryId(ref EqdpIdentifier identifier, float unscaledWidth = 100)
{
var ret = IdInput("##eqdpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1,
identifier.SetId.Id <= 1);
ImUtf8.HoverTooltip(
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8);
if (ret)
identifier = identifier with { SetId = setId };
return ret;
}
public static bool DrawRace(ref EqdpIdentifier identifier, float unscaledWidth = 100)
{
var ret = Combos.Race("##eqdpRace", identifier.Race, out var race, unscaledWidth);
ImUtf8.HoverTooltip("Model Race"u8);
if (ret)
identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) };
return ret;
}
public static bool DrawGender(ref EqdpIdentifier identifier, float unscaledWidth = 120)
{
var ret = Combos.Gender("##eqdpGender", identifier.Gender, out var gender, unscaledWidth);
ImUtf8.HoverTooltip("Gender"u8);
if (ret)
identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) };
return ret;
}
public static bool DrawEquipSlot(ref EqdpIdentifier identifier, float unscaledWidth = 100)
{
var ret = Combos.EqdpEquipSlot("##eqdpSlot", identifier.Slot, out var slot, unscaledWidth);
ImUtf8.HoverTooltip("Equip Slot"u8);
if (ret)
identifier = identifier with { Slot = slot };
return ret;
}
}

View file

@ -0,0 +1,134 @@
using Dalamud.Interface;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Meta;
public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
: MetaDrawer<EqpIdentifier, EqpEntryInternal>(editor, metaFiles), IService
{
public override ReadOnlySpan<byte> Label
=> "Equipment Parameter Edits (EQP)###EQP"u8;
public override int NumColumns
=> 5;
protected override void Initialize()
{
Identifier = new EqpIdentifier(1, EquipSlot.Body);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, Identifier.SetId), Identifier.Slot);
protected override void DrawNew()
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqp));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);
var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8;
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
if (DrawIdentifierInput(ref Identifier))
UpdateEntry();
DrawEntry(Identifier.Slot, Entry, ref Entry, true);
}
protected override void DrawEntry(EqpIdentifier identifier, EqpEntryInternal entry)
{
DrawMetaButtons(identifier, entry);
DrawIdentifier(identifier);
var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, identifier.SetId), identifier.Slot);
if (DrawEntry(identifier.Slot, defaultEntry, ref entry, false))
Editor.Changes |= Editor.Update(identifier, entry);
}
protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate()
=> Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value));
private static bool DrawIdentifierInput(ref EqpIdentifier identifier)
{
ImGui.TableNextColumn();
var changes = DrawPrimaryId(ref identifier);
ImGui.TableNextColumn();
changes |= DrawEquipSlot(ref identifier);
return changes;
}
private static void DrawIdentifier(EqpIdentifier identifier)
{
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor);
ImUtf8.HoverTooltip("Model Set ID"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor);
ImUtf8.HoverTooltip("Equip Slot"u8);
}
private static bool DrawEntry(EquipSlot slot, EqpEntryInternal defaultEntry, ref EqpEntryInternal entry, bool disabled)
{
var changes = false;
using var dis = ImRaii.Disabled(disabled);
ImGui.TableNextColumn();
var offset = Eqp.OffsetAndMask(slot).Item1;
DrawBox(ref entry, 0);
for (var i = 1; i < Eqp.EqpAttributes[slot].Count; ++i)
{
ImUtf8.SameLineInner();
DrawBox(ref entry, i);
}
return changes;
void DrawBox(ref EqpEntryInternal entry, int i)
{
using var id = ImUtf8.PushId(i);
var flag = 1u << i;
var eqpFlag = (EqpEntry)((ulong)flag << offset);
var defaultValue = (flag & defaultEntry.Value) != 0;
var value = (flag & entry.Value) != 0;
if (Checkmark("##eqp"u8, eqpFlag.ToLocalName(), value, defaultValue, out var newValue))
{
entry = new EqpEntryInternal(newValue ? entry.Value | flag : entry.Value & ~flag);
changes = true;
}
}
}
public static bool DrawPrimaryId(ref EqpIdentifier identifier, float unscaledWidth = 100)
{
var ret = IdInput("##eqpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1,
identifier.SetId.Id <= 1);
ImUtf8.HoverTooltip(
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8);
if (ret)
identifier = identifier with { SetId = setId };
return ret;
}
public static bool DrawEquipSlot(ref EqpIdentifier identifier, float unscaledWidth = 100)
{
var ret = Combos.EqpEquipSlot("##eqpSlot", identifier.Slot, out var slot, unscaledWidth);
ImUtf8.HoverTooltip("Equip Slot"u8);
if (ret)
identifier = identifier with { Slot = slot };
return ret;
}
}

View file

@ -0,0 +1,147 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.GameData.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Meta;
public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
: MetaDrawer<EstIdentifier, EstEntry>(editor, metaFiles), IService
{
public override ReadOnlySpan<byte> Label
=> "Extra Skeleton Parameters (EST)###EST"u8;
public override int NumColumns
=> 7;
protected override void Initialize()
{
Identifier = new EstIdentifier(1, EstType.Hair, GenderRace.MidlanderMale);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = EstFile.GetDefault(MetaFiles, Identifier.Slot, Identifier.GenderRace, Identifier.SetId);
protected override void DrawNew()
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Est));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);
var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8;
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
if (DrawIdentifierInput(ref Identifier))
UpdateEntry();
DrawEntry(Entry, ref Entry, true);
}
protected override void DrawEntry(EstIdentifier identifier, EstEntry entry)
{
DrawMetaButtons(identifier, entry);
DrawIdentifier(identifier);
var defaultEntry = EstFile.GetDefault(MetaFiles, identifier.Slot, identifier.GenderRace, identifier.SetId);
if (DrawEntry(defaultEntry, ref entry, false))
Editor.Changes |= Editor.Update(identifier, entry);
}
protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate()
=> Editor.Est.Select(kvp => (kvp.Key, kvp.Value));
private static bool DrawIdentifierInput(ref EstIdentifier identifier)
{
ImGui.TableNextColumn();
var changes = DrawPrimaryId(ref identifier);
ImGui.TableNextColumn();
changes |= DrawRace(ref identifier);
ImGui.TableNextColumn();
changes |= DrawGender(ref identifier);
ImGui.TableNextColumn();
changes |= DrawSlot(ref identifier);
return changes;
}
private static void DrawIdentifier(EstIdentifier identifier)
{
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor);
ImUtf8.HoverTooltip("Model Set ID"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor);
ImUtf8.HoverTooltip("Model Race"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor);
ImUtf8.HoverTooltip("Gender"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Slot.ToString(), FrameColor);
ImUtf8.HoverTooltip("Extra Skeleton Type"u8);
}
private static bool DrawEntry(EstEntry defaultEntry, ref EstEntry entry, bool disabled)
{
using var dis = ImRaii.Disabled(disabled);
ImGui.TableNextColumn();
var ret = DragInput("##estValue"u8, [], 100f * ImUtf8.GlobalScale, entry.Value, defaultEntry.Value, out var newValue, (ushort)0,
ushort.MaxValue, 0.05f, !disabled);
if (ret)
entry = new EstEntry(newValue);
return ret;
}
public static bool DrawPrimaryId(ref EstIdentifier identifier, float unscaledWidth = 100)
{
var ret = IdInput("##estPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1,
identifier.SetId.Id <= 1);
ImUtf8.HoverTooltip(
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8);
if (ret)
identifier = identifier with { SetId = setId };
return ret;
}
public static bool DrawRace(ref EstIdentifier identifier, float unscaledWidth = 100)
{
var ret = Combos.Race("##estRace", identifier.Race, out var race, unscaledWidth);
ImUtf8.HoverTooltip("Model Race"u8);
if (ret)
identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) };
return ret;
}
public static bool DrawGender(ref EstIdentifier identifier, float unscaledWidth = 120)
{
var ret = Combos.Gender("##estGender", identifier.Gender, out var gender, unscaledWidth);
ImUtf8.HoverTooltip("Gender"u8);
if (ret)
identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) };
return ret;
}
public static bool DrawSlot(ref EstIdentifier identifier, float unscaledWidth = 200)
{
var ret = Combos.EstSlot("##estSlot", identifier.Slot, out var slot, unscaledWidth);
ImUtf8.HoverTooltip("Extra Skeleton Type"u8);
if (ret)
identifier = identifier with { Slot = slot };
return ret;
}
}

View file

@ -0,0 +1,111 @@
using Dalamud.Interface;
using ImGuiNET;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.UI.AdvancedWindow.Meta;
public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
: MetaDrawer<GlobalEqpManipulation, byte>(editor, metaFiles), IService
{
public override ReadOnlySpan<byte> Label
=> "Global Equipment Parameter Edits (Global EQP)###GEQP"u8;
public override int NumColumns
=> 4;
protected override void Initialize()
{
Identifier = new GlobalEqpManipulation()
{
Condition = 1,
Type = GlobalEqpType.DoNotHideEarrings,
};
}
protected override void DrawNew()
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.GlobalEqp));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);
var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8;
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
Editor.Changes |= Editor.TryAdd(Identifier);
DrawIdentifierInput(ref Identifier);
}
protected override void DrawEntry(GlobalEqpManipulation identifier, byte _)
{
DrawMetaButtons(identifier, 0);
DrawIdentifier(identifier);
}
protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate()
=> Editor.GlobalEqp.Select(identifier => (identifier, (byte)0));
private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier)
{
ImGui.TableNextColumn();
DrawType(ref identifier);
ImGui.TableNextColumn();
if (identifier.Type.HasCondition())
DrawCondition(ref identifier);
else
ImUtf8.ScaledDummy(100);
}
private static void DrawIdentifier(GlobalEqpManipulation identifier)
{
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor);
ImUtf8.HoverTooltip("Global EQP Type"u8);
ImGui.TableNextColumn();
if (identifier.Type.HasCondition())
{
ImUtf8.TextFramed($"{identifier.Condition.Id}", FrameColor);
ImUtf8.HoverTooltip("Conditional Model ID"u8);
}
}
public static bool DrawType(ref GlobalEqpManipulation identifier, float unscaledWidth = 250)
{
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
using var combo = ImUtf8.Combo("##geqpType"u8, identifier.Type.ToName());
if (!combo)
return false;
var ret = false;
foreach (var type in Enum.GetValues<GlobalEqpType>())
{
if (ImUtf8.Selectable(type.ToName(), type == identifier.Type))
{
identifier = new GlobalEqpManipulation
{
Type = type,
Condition = type.HasCondition() ? identifier.Type.HasCondition() ? identifier.Condition : 1 : 0,
};
ret = true;
}
ImUtf8.HoverTooltip(type.ToDescription());
}
return ret;
}
public static void DrawCondition(ref GlobalEqpManipulation identifier, float unscaledWidth = 100)
{
if (IdInput("##geqpCond"u8, unscaledWidth, identifier.Condition.Id, out var newId, 1, ushort.MaxValue,
identifier.Condition.Id <= 1))
identifier = identifier with { Condition = newId };
ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8);
}
}

View file

@ -0,0 +1,148 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
namespace Penumbra.UI.AdvancedWindow.Meta;
public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
: MetaDrawer<GmpIdentifier, GmpEntry>(editor, metaFiles), IService
{
public override ReadOnlySpan<byte> Label
=> "Visor/Gimmick Edits (GMP)###GMP"u8;
public override int NumColumns
=> 7;
protected override void Initialize()
{
Identifier = new GmpIdentifier(1);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = ExpandedGmpFile.GetDefault(MetaFiles, Identifier.SetId);
protected override void DrawNew()
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Gmp));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);
var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8;
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
if (DrawIdentifierInput(ref Identifier))
UpdateEntry();
DrawEntry(Entry, ref Entry, true);
}
protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry)
{
DrawMetaButtons(identifier, entry);
DrawIdentifier(identifier);
var defaultEntry = ExpandedGmpFile.GetDefault(MetaFiles, identifier.SetId);
if (DrawEntry(defaultEntry, ref entry, false))
Editor.Changes |= Editor.Update(identifier, entry);
}
protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate()
=> Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value));
private static bool DrawIdentifierInput(ref GmpIdentifier identifier)
{
ImGui.TableNextColumn();
return DrawPrimaryId(ref identifier);
}
private static void DrawIdentifier(GmpIdentifier identifier)
{
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor);
ImUtf8.HoverTooltip("Model Set ID"u8);
}
private static bool DrawEntry(GmpEntry defaultEntry, ref GmpEntry entry, bool disabled)
{
using var dis = ImRaii.Disabled(disabled);
ImGui.TableNextColumn();
var changes = false;
if (Checkmark("##gmpEnabled"u8, "Gimmick Enabled", entry.Enabled, defaultEntry.Enabled, out var enabled))
{
entry = entry with { Enabled = enabled };
changes = true;
}
ImGui.TableNextColumn();
if (Checkmark("##gmpAnimated"u8, "Gimmick Animated", entry.Animated, defaultEntry.Animated, out var animated))
{
entry = entry with { Animated = animated };
changes = true;
}
var rotationWidth = 75 * ImUtf8.GlobalScale;
ImGui.TableNextColumn();
if (DragInput("##gmpRotationA"u8, "Rotation A in Degrees"u8, rotationWidth, entry.RotationA, defaultEntry.RotationA, out var rotationA,
(ushort)0, (ushort)360, 0.05f, !disabled))
{
entry = entry with { RotationA = rotationA };
changes = true;
}
ImUtf8.SameLineInner();
if (DragInput("##gmpRotationB"u8, "Rotation B in Degrees"u8, rotationWidth, entry.RotationB, defaultEntry.RotationB, out var rotationB,
(ushort)0, (ushort)360, 0.05f, !disabled))
{
entry = entry with { RotationB = rotationB };
changes = true;
}
ImUtf8.SameLineInner();
if (DragInput("##gmpRotationC"u8, "Rotation C in Degrees"u8, rotationWidth, entry.RotationC, defaultEntry.RotationC, out var rotationC,
(ushort)0, (ushort)360, 0.05f, !disabled))
{
entry = entry with { RotationC = rotationC };
changes = true;
}
var unkWidth = 50 * ImUtf8.GlobalScale;
ImGui.TableNextColumn();
if (DragInput("##gmpUnkA"u8, "Animation Type A?"u8, unkWidth, entry.UnknownA, defaultEntry.UnknownA, out var unknownA,
(byte)0, (byte)15, 0.01f, !disabled))
{
entry = entry with { UnknownA = unknownA };
changes = true;
}
ImUtf8.SameLineInner();
if (DragInput("##gmpUnkB"u8, "Animation Type B?"u8, unkWidth, entry.UnknownB, defaultEntry.UnknownB, out var unknownB,
(byte)0, (byte)15, 0.01f, !disabled))
{
entry = entry with { UnknownB = unknownB };
changes = true;
}
return changes;
}
public static bool DrawPrimaryId(ref GmpIdentifier identifier, float unscaledWidth = 100)
{
var ret = IdInput("##gmpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1,
identifier.SetId.Id <= 1);
ImUtf8.HoverTooltip(
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8);
if (ret)
identifier = new GmpIdentifier(setId);
return ret;
}
}

View file

@ -12,22 +12,16 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Meta;
public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
: MetaDrawer<ImcIdentifier, ImcEntry>(editor, metaFiles), IService
{
private bool _fileExists;
public override ReadOnlySpan<byte> Label
=> "Variant Edits (IMC)###IMC"u8;
private const string ModelSetIdTooltipShort = "Model Set ID";
private const string EquipSlotTooltip = "Equip Slot";
private const string ModelRaceTooltip = "Model Race";
private const string GenderTooltip = "Gender";
private const string ObjectTypeTooltip = "Object Type";
private const string SecondaryIdTooltip = "Secondary ID";
private const string PrimaryIdTooltipShort = "Primary ID";
private const string VariantIdTooltip = "Variant ID";
private const string EstTypeTooltip = "EST Type";
private const string RacialTribeTooltip = "Racial Tribe";
private const string ScalingTypeTooltip = "Scaling Type";
public override int NumColumns
=> 10;
private bool _fileExists;
protected override void Initialize()
{
@ -41,14 +35,14 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
protected override void DrawNew()
{
ImGui.TableNextColumn();
// Copy To Clipboard
CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc));
ImGui.TableNextColumn();
var canAdd = _fileExists && Editor.MetaEditor.CanAdd(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;
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
Editor.MetaEditor.TryAdd(Identifier, Entry);
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
if (DrawIdentifier(ref Identifier))
if (DrawIdentifierInput(ref Identifier))
UpdateEntry();
using var disabled = ImRaii.Disabled();
@ -57,46 +51,15 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
protected override void DrawEntry(ImcIdentifier identifier, ImcEntry entry)
{
const uint frameColor = 0;
// Meta Buttons
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.ObjectType.ToName(), frameColor);
ImUtf8.HoverTooltip("Object Type"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", frameColor);
ImUtf8.HoverTooltip("Primary ID");
ImGui.TableNextColumn();
if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory)
{
ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor);
ImUtf8.HoverTooltip("Equip Slot"u8);
}
else
{
ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", frameColor);
ImUtf8.HoverTooltip("Secondary ID"u8);
}
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.Variant.Id}", frameColor);
ImUtf8.HoverTooltip("Variant"u8);
ImGui.TableNextColumn();
if (identifier.ObjectType is ObjectType.DemiHuman)
{
ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor);
ImUtf8.HoverTooltip("Equip Slot"u8);
}
DrawMetaButtons(identifier, entry);
DrawIdentifier(identifier);
var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry;
if (DrawEntry(defaultEntry, ref entry, true))
Editor.MetaEditor.Update(identifier, entry);
Editor.Changes |= Editor.Update(identifier, entry);
}
private static bool DrawIdentifier(ref ImcIdentifier identifier)
private static bool DrawIdentifierInput(ref ImcIdentifier identifier)
{
ImGui.TableNextColumn();
var change = DrawObjectType(ref identifier);
@ -121,6 +84,41 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
return change;
}
private static void DrawIdentifier(ImcIdentifier identifier)
{
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.ObjectType.ToName(), FrameColor);
ImUtf8.HoverTooltip("Object Type"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", FrameColor);
ImUtf8.HoverTooltip("Primary ID");
ImGui.TableNextColumn();
if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory)
{
ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor);
ImUtf8.HoverTooltip("Equip Slot"u8);
}
else
{
ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", FrameColor);
ImUtf8.HoverTooltip("Secondary ID"u8);
}
ImGui.TableNextColumn();
ImUtf8.TextFramed($"{identifier.Variant.Id}", FrameColor);
ImUtf8.HoverTooltip("Variant"u8);
ImGui.TableNextColumn();
if (identifier.ObjectType is ObjectType.DemiHuman)
{
ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor);
ImUtf8.HoverTooltip("Equip Slot"u8);
}
}
private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault)
{
ImGui.TableNextColumn();
@ -142,7 +140,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate()
=> Editor.MetaEditor.Imc.Select(kvp => (kvp.Key, kvp.Value));
=> Editor.Imc.Select(kvp => (kvp.Key, kvp.Value));
public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110)
{
@ -270,7 +268,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
return true;
}
public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry)
private static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry)
{
var changes = false;
for (var i = 0; i < ImcEntry.NumAttributes; ++i)
@ -292,62 +290,4 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
return changes;
}
/// <summary>
/// A number input for ids with an optional max id of given width.
/// Returns true if newId changed against currentId.
/// </summary>
private static bool IdInput(ReadOnlySpan<byte> label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId,
bool border)
{
int tmp = currentId;
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border);
using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border);
if (ImUtf8.InputScalar(label, ref tmp))
tmp = Math.Clamp(tmp, minId, maxId);
newId = (ushort)tmp;
return newId != currentId;
}
/// <summary>
/// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max.
/// Returns true if newValue changed against currentValue.
/// </summary>
private static bool DragInput<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> tooltip, float width, T currentValue, T defaultValue,
out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber<T>
{
newValue = currentValue;
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(),
defaultValue != currentValue);
ImGui.SetNextItemWidth(width);
if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed))
newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue;
if (addDefault)
ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}");
else
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip);
return newValue != currentValue;
}
/// <summary>
/// A checkmark that compares against a default value and shows a tooltip.
/// Returns true if newValue is changed against currentValue.
/// </summary>
private static bool Checkmark(ReadOnlySpan<byte> label, ReadOnlySpan<byte> tooltip, bool currentValue, bool defaultValue,
out bool newValue)
{
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(),
defaultValue != currentValue);
newValue = currentValue;
ImUtf8.Checkbox(label, ref newValue);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip);
return newValue != currentValue;
}
}

View file

@ -0,0 +1,154 @@
using Dalamud.Interface;
using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Api.Api;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Meta;
public interface IMetaDrawer
{
public ReadOnlySpan<byte> Label { get; }
public int NumColumns { get; }
public void Draw();
}
public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, MetaFileManager metaFiles) : IMetaDrawer
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
protected const uint FrameColor = 0;
protected readonly ModMetaEditor Editor = editor;
protected readonly MetaFileManager MetaFiles = metaFiles;
protected TIdentifier Identifier;
protected TEntry Entry;
private bool _initialized;
public void Draw()
{
if (!_initialized)
{
Initialize();
_initialized = true;
}
using var id = ImUtf8.PushId((int)Identifier.Type);
DrawNew();
foreach (var ((identifier, entry), idx) in Enumerate().WithIndex())
{
id.Push(idx);
DrawEntry(identifier, entry);
id.Pop();
}
}
public abstract ReadOnlySpan<byte> Label { get; }
public abstract int NumColumns { get; }
protected abstract void DrawNew();
protected abstract void Initialize();
protected abstract void DrawEntry(TIdentifier identifier, TEntry entry);
protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate();
/// <summary>
/// A number input for ids with an optional max id of given width.
/// Returns true if newId changed against currentId.
/// </summary>
protected static bool IdInput(ReadOnlySpan<byte> label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId,
bool border)
{
int tmp = currentId;
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border);
using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border);
if (ImUtf8.InputScalar(label, ref tmp))
tmp = Math.Clamp(tmp, minId, maxId);
newId = (ushort)tmp;
return newId != currentId;
}
/// <summary>
/// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max.
/// Returns true if newValue changed against currentValue.
/// </summary>
protected static bool DragInput<T>(ReadOnlySpan<byte> label, ReadOnlySpan<byte> tooltip, float width, T currentValue, T defaultValue,
out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber<T>
{
newValue = currentValue;
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(),
defaultValue != currentValue);
ImGui.SetNextItemWidth(width);
if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed))
newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue;
if (addDefault)
ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}");
else
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip);
return newValue != currentValue;
}
/// <summary>
/// A checkmark that compares against a default value and shows a tooltip.
/// Returns true if newValue is changed against currentValue.
/// </summary>
protected static bool Checkmark(ReadOnlySpan<byte> label, ReadOnlySpan<byte> tooltip, bool currentValue, bool defaultValue,
out bool newValue)
{
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(),
defaultValue != currentValue);
newValue = currentValue;
ImUtf8.Checkbox(label, ref newValue);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip);
return newValue != currentValue;
}
/// <summary>
/// A checkmark that compares against a default value and shows a tooltip.
/// Returns true if newValue is changed against currentValue.
/// </summary>
protected static bool Checkmark(ReadOnlySpan<byte> label, ReadOnlySpan<char> tooltip, bool currentValue, bool defaultValue,
out bool newValue)
{
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(),
defaultValue != currentValue);
newValue = currentValue;
ImUtf8.Checkbox(label, ref newValue);
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip);
return newValue != currentValue;
}
protected void DrawMetaButtons(TIdentifier identifier, TEntry entry)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! });
ImGui.TableNextColumn();
if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8))
Editor.Changes |= Editor.Remove(identifier);
}
protected void CopyToClipboardButton(ReadOnlySpan<byte> tooltip, JToken? manipulations)
{
if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip))
return;
var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion);
if (text.Length > 0)
ImGui.SetClipboardText(text);
}
}

View file

@ -0,0 +1,35 @@
using OtterGui.Services;
using Penumbra.Meta.Manipulations;
namespace Penumbra.UI.AdvancedWindow.Meta;
public class MetaDrawers(
EqdpMetaDrawer eqdp,
EqpMetaDrawer eqp,
EstMetaDrawer est,
GlobalEqpMetaDrawer globalEqp,
GmpMetaDrawer gmp,
ImcMetaDrawer imc,
RspMetaDrawer rsp) : IService
{
public readonly EqdpMetaDrawer Eqdp = eqdp;
public readonly EqpMetaDrawer Eqp = eqp;
public readonly EstMetaDrawer Est = est;
public readonly GmpMetaDrawer Gmp = gmp;
public readonly RspMetaDrawer Rsp = rsp;
public readonly ImcMetaDrawer Imc = imc;
public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp;
public IMetaDrawer? Get(MetaManipulationType type)
=> type switch
{
MetaManipulationType.Imc => Imc,
MetaManipulationType.Eqdp => Eqdp,
MetaManipulationType.Eqp => Eqp,
MetaManipulationType.Est => Est,
MetaManipulationType.Gmp => Gmp,
MetaManipulationType.Rsp => Rsp,
MetaManipulationType.GlobalEqp => GlobalEqp,
_ => null,
};
}

View file

@ -0,0 +1,112 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.GameData.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Meta;
public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
: MetaDrawer<RspIdentifier, RspEntry>(editor, metaFiles), IService
{
public override ReadOnlySpan<byte> Label
=> "Racial Scaling Edits (RSP)###RSP"u8;
public override int NumColumns
=> 5;
protected override void Initialize()
{
Identifier = new RspIdentifier(SubRace.Midlander, RspAttribute.MaleMinSize);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = CmpFile.GetDefault(MetaFiles, Identifier.SubRace, Identifier.Attribute);
protected override void DrawNew()
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Rsp));
ImGui.TableNextColumn();
var canAdd = !Editor.Contains(Identifier);
var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8;
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
Editor.Changes |= Editor.TryAdd(Identifier, Entry);
if (DrawIdentifierInput(ref Identifier))
UpdateEntry();
DrawEntry(Entry, ref Entry, true);
}
protected override void DrawEntry(RspIdentifier identifier, RspEntry entry)
{
DrawMetaButtons(identifier, entry);
DrawIdentifier(identifier);
var defaultEntry = CmpFile.GetDefault(MetaFiles, identifier.SubRace, identifier.Attribute);
if (DrawEntry(defaultEntry, ref entry, false))
Editor.Changes |= Editor.Update(identifier, entry);
}
protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate()
=> Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value));
private static bool DrawIdentifierInput(ref RspIdentifier identifier)
{
ImGui.TableNextColumn();
var changes = DrawSubRace(ref identifier);
ImGui.TableNextColumn();
changes |= DrawAttribute(ref identifier);
return changes;
}
private static void DrawIdentifier(RspIdentifier identifier)
{
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.SubRace.ToName(), FrameColor);
ImUtf8.HoverTooltip("Model Set ID"u8);
ImGui.TableNextColumn();
ImUtf8.TextFramed(identifier.Attribute.ToFullString(), FrameColor);
ImUtf8.HoverTooltip("Equip Slot"u8);
}
private static bool DrawEntry(RspEntry defaultEntry, ref RspEntry entry, bool disabled)
{
using var dis = ImRaii.Disabled(disabled);
ImGui.TableNextColumn();
var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, defaultEntry.Value, entry.Value, out var newValue,
RspEntry.MinValue, RspEntry.MaxValue, 0.001f, !disabled);
if (ret)
entry = new RspEntry(newValue);
return ret;
}
public static bool DrawSubRace(ref RspIdentifier identifier, float unscaledWidth = 150)
{
var ret = Combos.SubRace("##rspSubRace", identifier.SubRace, out var subRace, unscaledWidth);
ImUtf8.HoverTooltip("Racial Clan"u8);
if (ret)
identifier = identifier with { SubRace = subRace };
return ret;
}
public static bool DrawAttribute(ref RspIdentifier identifier, float unscaledWidth = 200)
{
var ret = Combos.RspAttribute("##rspAttribute", identifier.Attribute, out var attribute, unscaledWidth);
ImUtf8.HoverTooltip("Scaling Attribute"u8);
if (ret)
identifier = identifier with { Attribute = attribute };
return ret;
}
}

View file

@ -1,27 +1,18 @@
using System.Reflection.Emit;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Api.Api;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.UI.AdvancedWindow.Meta;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private const string ModelSetIdTooltip =
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that.";
private readonly MetaDrawers _metaDrawers;
private void DrawMetaTab()
{
@ -56,80 +47,42 @@ public partial class ModEditWindow
if (!child)
return;
DrawEditHeader(MetaManipulation.Type.Eqp);
DrawEditHeader(MetaManipulation.Type.Eqdp);
DrawEditHeader(MetaManipulation.Type.Imc);
DrawEditHeader(MetaManipulation.Type.Est);
DrawEditHeader(MetaManipulation.Type.Gmp);
DrawEditHeader(MetaManipulation.Type.Rsp);
DrawEditHeader(MetaManipulation.Type.GlobalEqp);
DrawEditHeader(MetaManipulationType.Eqp);
DrawEditHeader(MetaManipulationType.Eqdp);
DrawEditHeader(MetaManipulationType.Imc);
DrawEditHeader(MetaManipulationType.Est);
DrawEditHeader(MetaManipulationType.Gmp);
DrawEditHeader(MetaManipulationType.Rsp);
DrawEditHeader(MetaManipulationType.GlobalEqp);
}
private static ReadOnlySpan<byte> Label(MetaManipulation.Type type)
=> type switch
{
MetaManipulation.Type.Imc => "Variant Edits (IMC)###IMC"u8,
MetaManipulation.Type.Eqdp => "Racial Model Edits (EQDP)###EQDP"u8,
MetaManipulation.Type.Eqp => "Equipment Parameter Edits (EQP)###EQP"u8,
MetaManipulation.Type.Est => "Extra Skeleton Parameters (EST)###EST"u8,
MetaManipulation.Type.Gmp => "Visor/Gimmick Edits (GMP)###GMP"u8,
MetaManipulation.Type.Rsp => "Racial Scaling Edits (RSP)###RSP"u8,
MetaManipulation.Type.GlobalEqp => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8,
_ => "\0"u8,
};
private static int ColumnCount(MetaManipulation.Type type)
=> type switch
{
MetaManipulation.Type.Imc => 10,
MetaManipulation.Type.Eqdp => 7,
MetaManipulation.Type.Eqp => 5,
MetaManipulation.Type.Est => 7,
MetaManipulation.Type.Gmp => 7,
MetaManipulation.Type.Rsp => 5,
MetaManipulation.Type.GlobalEqp => 4,
_ => 0,
};
private void DrawEditHeader(MetaManipulation.Type type)
private void DrawEditHeader(MetaManipulationType type)
{
var drawer = _metaDrawers.Get(type);
if (drawer == null)
return;
var oldPos = ImGui.GetCursorPosY();
var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {Label(type)}");
var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {drawer.Label}");
DrawOtherOptionData(type, oldPos, ImGui.GetCursorPos());
if (!header)
return;
DrawTable(type);
DrawTable(drawer);
}
private IMetaDrawer? Drawer(MetaManipulation.Type type)
=> type switch
{
//MetaManipulation.Type.Imc => expr,
//MetaManipulation.Type.Eqdp => expr,
//MetaManipulation.Type.Eqp => expr,
//MetaManipulation.Type.Est => expr,
//MetaManipulation.Type.Gmp => expr,
//MetaManipulation.Type.Rsp => expr,
//MetaManipulation.Type.GlobalEqp => expr,
_ => null,
};
private void DrawTable(MetaManipulation.Type type)
private static void DrawTable(IMetaDrawer drawer)
{
const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV;
using var table = ImUtf8.Table(Label(type), ColumnCount(type), flags);
using var table = ImUtf8.Table(drawer.Label, drawer.NumColumns, flags);
if (!table)
return;
if (Drawer(type) is not { } drawer)
return;
drawer.Draw();
ImGui.NewLine();
}
private void DrawOtherOptionData(MetaManipulation.Type type, float oldPos, Vector2 newPos)
private void DrawOtherOptionData(MetaManipulationType type, float oldPos, Vector2 newPos)
{
var otherOptionData = _editor.MetaEditor.OtherData[type];
if (otherOptionData.TotalCount <= 0)
@ -149,577 +102,12 @@ public partial class ModEditWindow
ImGui.SetCursorPos(newPos);
}
#if false
private static class EqpRow
{
private static EqpIdentifier _newIdentifier = new(1, EquipSlot.Body);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize,
editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##eqpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
_new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), _new.Slot, setId);
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
ImGui.TableNextColumn();
if (Combos.EqpEquipSlot("##eqpSlot", _new.Slot, out var slot))
_new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId);
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
foreach (var flag in Eqp.EqpAttributes[_new.Slot])
{
var value = defaultEntry.HasFlag(flag);
Checkmark("##eqp", flag.ToLocalName(), value, value, out _);
ImGui.SameLine();
}
ImGui.NewLine();
}
public static void Draw(MetaFileManager metaFileManager, EqpManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, meta.SetId);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Slot.ToName());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
ImGui.TableNextColumn();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
var idx = 0;
foreach (var flag in Eqp.EqpAttributes[meta.Slot])
{
using var id = ImRaii.PushId(idx++);
var defaultValue = defaultEntry.HasFlag(flag);
var currentValue = meta.Entry.HasFlag(flag);
if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value))
editor.MetaEditor.Change(meta.Copy(value ? meta.Entry | flag : meta.Entry & ~flag));
ImGui.SameLine();
}
ImGui.NewLine();
}
}
private static class EqdpRow
{
private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize,
editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var raceCode = Names.CombinedRace(_new.Gender, _new.Race);
var validRaceCode = CharacterUtilityData.EqdpIdx(raceCode, false) >= 0;
var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." :
validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used.";
var defaultEntry = validRaceCode
? ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId)
: 0;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##eqdpId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race),
_new.Slot.IsAccessory(), setId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId);
}
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
ImGui.TableNextColumn();
if (Combos.Race("##eqdpRace", _new.Race, out var race))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race),
_new.Slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId);
}
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
if (Combos.Gender("##eqdpGender", _new.Gender, out var gender))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race),
_new.Slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId);
}
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race),
slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId);
}
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
var (bit1, bit2) = defaultEntry.ToBits(_new.Slot);
Checkmark("Material##eqdpCheck1", string.Empty, bit1, bit1, out _);
ImGui.SameLine();
Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _);
}
public static void Draw(MetaFileManager metaFileManager, EqdpManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Race.ToName());
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Gender.ToName());
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Slot.ToName());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(),
meta.SetId);
var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot);
var (bit1, bit2) = meta.Entry.ToBits(meta.Slot);
ImGui.TableNextColumn();
if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1))
editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2)));
ImGui.SameLine();
if (Checkmark("Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2))
editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2)));
}
}
private static class EstRow
{
private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize,
editor.MetaEditor.Est.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##estId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
{
var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId);
_new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
ImGui.TableNextColumn();
if (Combos.Race("##estRace", _new.Race, out var race))
{
var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId);
_new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
if (Combos.Gender("##estGender", _new.Gender, out var gender))
{
var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId);
_new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
if (Combos.EstSlot("##estSlot", _new.Slot, out var slot))
{
var newDefaultEntry = EstFile.GetDefault(metaFileManager, slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId);
_new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(EstTypeTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry.Value, defaultEntry.Value, out _, 0, ushort.MaxValue, 0.05f);
}
public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Race.ToName());
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Gender.ToName());
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Slot.ToString());
ImGuiUtil.HoverTooltip(EstTypeTooltip);
// Values
var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId);
ImGui.TableNextColumn();
if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value,
out var entry, 0, ushort.MaxValue, 0.05f))
editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry)));
}
}
private static class GmpRow
{
private static GmpManipulation _new = new(GmpEntry.Default, 1);
private static float RotationWidth
=> 75 * UiHelpers.Scale;
private static float UnkWidth
=> 50 * UiHelpers.Scale;
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize,
editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##gmpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
_new = new GmpManipulation(ExpandedGmpFile.GetDefault(metaFileManager, setId), setId);
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
Checkmark("##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _);
ImGui.TableNextColumn();
Checkmark("##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _);
ImGui.TableNextColumn();
IntDragInput("##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0,
360, 0f);
ImGui.SameLine();
IntDragInput("##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0,
360, 0f);
ImGui.SameLine();
IntDragInput("##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0,
360, 0f);
ImGui.TableNextColumn();
IntDragInput("##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f);
ImGui.SameLine();
IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f);
}
public static void Draw(MetaFileManager metaFileManager, GmpManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
// Values
var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, meta.SetId);
ImGui.TableNextColumn();
if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled }));
ImGui.TableNextColumn();
if (Checkmark("##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { Animated = animated }));
ImGui.TableNextColumn();
if (IntDragInput("##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth,
meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationA = (ushort)rotationA }));
ImGui.SameLine();
if (IntDragInput("##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth,
meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationB = (ushort)rotationB }));
ImGui.SameLine();
if (IntDragInput("##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth,
meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationC = (ushort)rotationC }));
ImGui.TableNextColumn();
if (IntDragInput("##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA,
defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkA }));
ImGui.SameLine();
if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB,
defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB }));
}
}
private static class RspRow
{
private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One);
private static float FloatWidth
=> 150 * UiHelpers.Scale;
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize,
editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace))
_new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(metaFileManager, subRace, _new.Attribute));
ImGuiUtil.HoverTooltip(RacialTribeTooltip);
ImGui.TableNextColumn();
if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute))
_new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(metaFileManager, subRace, attribute));
ImGuiUtil.HoverTooltip(ScalingTypeTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(FloatWidth);
var value = defaultEntry.Value;
ImGui.DragFloat("##rspValue", ref value, 0f);
}
public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SubRace.ToName());
ImGuiUtil.HoverTooltip(RacialTribeTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Attribute.ToFullString());
ImGuiUtil.HoverTooltip(ScalingTypeTooltip);
ImGui.TableNextColumn();
// Values
var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value;
var value = meta.Entry.Value;
ImGui.SetNextItemWidth(FloatWidth);
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(),
def != value);
if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspEntry.MinValue, RspEntry.MaxValue)
&& value is >= RspEntry.MinValue and <= RspEntry.MaxValue)
editor.MetaEditor.Change(meta.Copy(new RspEntry(value)));
ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}");
}
}
private static class GlobalEqpRow
{
private static GlobalEqpManipulation _new = new()
{
Type = GlobalEqpType.DoNotHideEarrings,
Condition = 1,
};
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current global EQP manipulations to clipboard.", iconSize,
editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already manipulated.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new);
// Identifier
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(250 * ImUtf8.GlobalScale);
using (var combo = ImUtf8.Combo("##geqpType"u8, _new.Type.ToName()))
{
if (combo)
foreach (var type in Enum.GetValues<GlobalEqpType>())
{
if (ImUtf8.Selectable(type.ToName(), type == _new.Type))
_new = new GlobalEqpManipulation
{
Type = type,
Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0,
};
ImUtf8.HoverTooltip(type.ToDescription());
}
}
ImUtf8.HoverTooltip(_new.Type.ToDescription());
ImGui.TableNextColumn();
if (!_new.Type.HasCondition())
return;
if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1))
_new = _new with { Condition = newId };
ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8);
}
public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImUtf8.Text(meta.Type.ToName());
ImUtf8.HoverTooltip(meta.Type.ToDescription());
ImGui.TableNextColumn();
if (meta.Type.HasCondition())
{
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImUtf8.Text($"{meta.Condition.Id}");
}
}
}
#endif
// A number input for ids with a optional max id of given width.
// Returns true if newId changed against currentId.
private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border)
{
int tmp = currentId;
ImGui.SetNextItemWidth(width);
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border);
using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border);
if (ImGui.InputInt(label, ref tmp, 0))
tmp = Math.Clamp(tmp, minId, maxId);
newId = (ushort)tmp;
return newId != currentId;
}
// A checkmark that compares against a default value and shows a tooltip.
// Returns true if newValue is changed against currentValue.
private static bool Checkmark(string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue)
{
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(),
defaultValue != currentValue);
newValue = currentValue;
ImGui.Checkbox(label, ref newValue);
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
return newValue != currentValue;
}
// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max.
// Returns true if newValue changed against currentValue.
private static bool IntDragInput(string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue,
int minValue, int maxValue, float speed)
{
newValue = currentValue;
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(),
defaultValue != currentValue);
ImGui.SetNextItemWidth(width);
if (ImGui.DragInt(label, ref newValue, speed, minValue, maxValue))
newValue = Math.Clamp(newValue, minValue, maxValue);
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
return newValue != currentValue;
}
private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, MetaDictionary manipulations)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true))
return;
var text = Functions.ToCompressedBase64(manipulations, MetaManipulation.CurrentVersion);
var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion);
if (text.Length > 0)
ImGui.SetClipboardText(text);
}
@ -731,8 +119,11 @@ public partial class ModEditWindow
var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64<MetaDictionary>(clipboard, out var manips);
if (version == MetaManipulation.CurrentVersion && manips != null)
if (version == MetaApi.CurrentVersion && manips != null)
{
_editor.MetaEditor.UpdateTo(manips);
_editor.MetaEditor.Changes = true;
}
}
ImGuiUtil.HoverTooltip(
@ -745,194 +136,14 @@ public partial class ModEditWindow
{
var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64<MetaDictionary>(clipboard, out var manips);
if (version == MetaManipulation.CurrentVersion && manips != null)
if (version == MetaApi.CurrentVersion && manips != null)
{
_editor.MetaEditor.SetTo(manips);
_editor.MetaEditor.Changes = true;
}
}
ImGuiUtil.HoverTooltip(
"Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations.");
}
private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize)
{
//ImGui.TableNextColumn();
//CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty<MetaManipulation>().Append(meta));
//
//ImGui.TableNextColumn();
//if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true))
// editor.MetaEditor.Delete(meta);
}
}
public interface IMetaDrawer
{
public void Draw();
}
public abstract class MetaDrawer<TIdentifier, TEntry>(ModEditor editor, MetaFileManager metaFiles) : IMetaDrawer
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
protected readonly ModEditor Editor = editor;
protected readonly MetaFileManager MetaFiles = metaFiles;
protected TIdentifier Identifier;
protected TEntry Entry;
private bool _initialized;
public void Draw()
{
if (!_initialized)
{
Initialize();
_initialized = true;
}
DrawNew();
foreach (var ((identifier, entry), idx) in Enumerate().WithIndex())
{
using var id = ImUtf8.PushId(idx);
DrawEntry(identifier, entry);
}
}
protected abstract void DrawNew();
protected abstract void Initialize();
protected abstract void DrawEntry(TIdentifier identifier, TEntry entry);
protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate();
}
#if false
public sealed class GmpMetaDrawer(ModEditor editor) : MetaDrawer<GmpIdentifier, GmpEntry>, IService
{
protected override void Initialize()
{
Identifier = new GmpIdentifier(1, EquipSlot.Body);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId);
protected override void DrawNew()
{ }
protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry)
{ }
protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate()
=> editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)));
}
public sealed class EstMetaDrawer(ModEditor editor) : MetaDrawer<EstIdentifier, EstEntry>, IService
{
protected override void Initialize()
{
Identifier = new EqpIdentifier(1, EquipSlot.Body);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId);
protected override void DrawNew()
{ }
protected override void DrawEntry(EstIdentifier identifier, EstEntry entry)
{ }
protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate()
=> editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)));
}
public sealed class EqdpMetaDrawer(ModEditor editor) : MetaDrawer<EqdpIdentifier, EqdpEntry>, IService
{
protected override void Initialize()
{
Identifier = new EqdpIdentifier(1, EquipSlot.Body);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId);
protected override void DrawNew()
{ }
protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntry entry)
{ }
protected override IEnumerable<(EqdpIdentifier, EqdpEntry)> Enumerate()
=> editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)));
}
public sealed class EqpMetaDrawer(ModEditor editor, MetaFileManager metaManager) : MetaDrawer<EqpIdentifier, EqpEntry>, IService
{
protected override void Initialize()
{
Identifier = new EqpIdentifier(1, EquipSlot.Body);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId);
protected override void DrawNew()
{ }
protected override void DrawEntry(EqpIdentifier identifier, EqpEntry entry)
{ }
protected override IEnumerable<(EqpIdentifier, EqpEntry)> Enumerate()
=> editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)));
}
public sealed class RspMetaDrawer(ModEditor editor) : MetaDrawer<RspIdentifier, RspEntry>, IService
{
protected override void Initialize()
{
Identifier = new RspIdentifier(1, EquipSlot.Body);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId);
protected override void DrawNew()
{ }
protected override void DrawEntry(RspIdentifier identifier, RspEntry entry)
{ }
protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate()
=> editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)));
}
public sealed class GlobalEqpMetaDrawer(ModEditor editor) : MetaDrawer<GlobalEqpManipulation, byte>, IService
{
protected override void Initialize()
{
Identifier = new EqpIdentifier(1, EquipSlot.Body);
UpdateEntry();
}
private void UpdateEntry()
=> Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId);
protected override void DrawNew()
{ }
protected override void DrawEntry(GlobalEqpManipulation identifier, byte _)
{ }
protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate()
=> editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot)));
}
#endif

View file

@ -95,7 +95,7 @@ public partial class ModEditWindow
task.ContinueWith(t => { GamePaths = FinalizeIo(t); }, TaskScheduler.Default);
}
private EstManipulation[] GetCurrentEstManipulations()
private KeyValuePair<EstIdentifier, EstEntry>[] GetCurrentEstManipulations()
{
var mod = _edit._editor.Mod;
var option = _edit._editor.Option;
@ -106,9 +106,7 @@ public partial class ModEditWindow
return mod.AllDataContainers
.Where(subMod => subMod != option)
.Prepend(option)
.SelectMany(subMod => subMod.Manipulations)
.Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est)
.Select(manipulation => manipulation.Est)
.SelectMany(subMod => subMod.Manipulations.Est)
.ToArray();
}

View file

@ -25,6 +25,7 @@ using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow.Meta;
using Penumbra.UI.Classes;
using Penumbra.Util;
using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor;
@ -586,7 +587,7 @@ public partial class ModEditWindow : Window, IDisposable
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework,
CharacterBaseDestructor characterBaseDestructor)
CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers)
: base(WindowBaseLabel)
{
_performance = performance;
@ -606,6 +607,7 @@ public partial class ModEditWindow : Window, IDisposable
_objects = objects;
_framework = framework;
_characterBaseDestructor = characterBaseDestructor;
_metaDrawers = metaDrawers;
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));

View file

@ -120,9 +120,9 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy
{
var _ = data switch
{
Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0,
MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty),
_ => false,
Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0,
IMetaIdentifier m => ImGui.Selectable(m.ToString()),
_ => false,
};
}
}

View file

@ -98,7 +98,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH
// Filters mean we can not use the known counts.
if (hasFilters)
{
var it2 = m.Select(p => (p.Key.ToString(), p.Value.Name));
var it2 = m.IdentifierSources.Select(p => (p.Item1.ToString(), p.Item2.Name));
if (stop >= 0)
{
ImGuiClip.DrawEndDummy(stop + it2.Count(CheckFilters), height);
@ -117,7 +117,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH
}
else
{
stop = ImGuiClip.ClippedDraw(m, skips, DrawLine, m.Count, ~stop);
stop = ImGuiClip.ClippedDraw(m.IdentifierSources, skips, DrawLine, m.Count, ~stop);
ImGuiClip.DrawEndDummy(stop, height);
}
}
@ -152,11 +152,11 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH
ImGui.TableNextColumn();
ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft);
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(name);
ImGuiUtil.CopyOnClickSelectable(name.Text);
}
/// <summary> Draw a line for a unfiltered/unconverted manipulation and mod-index pair. </summary>
private static void DrawLine(KeyValuePair<MetaManipulation, IMod> pair)
private static void DrawLine((IMetaIdentifier, IMod) pair)
{
var (manipulation, mod) = pair;
ImGui.TableNextColumn();
@ -165,7 +165,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH
ImGui.TableNextColumn();
ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft);
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(mod.Name);
ImGuiUtil.CopyOnClickSelectable(mod.Name.Text);
}
/// <summary> Check filters for file replacements. </summary>

View file

@ -2,7 +2,6 @@ using OtterGui.Classes;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods;
@ -10,103 +9,14 @@ namespace Penumbra.Util;
public static class IdentifierExtensions
{
/// <summary> Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. </summary>
public static void MetaChangedItems(this ObjectIdentification identifier, IDictionary<string, object?> changedItems,
MetaManipulation manip)
{
switch (manip.ManipulationType)
{
case MetaManipulation.Type.Imc:
switch (manip.Imc.ObjectType)
{
case ObjectType.Equipment:
case ObjectType.Accessory:
identifier.Identify(changedItems,
GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant,
"a"));
break;
case ObjectType.Weapon:
identifier.Identify(changedItems,
GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a"));
break;
case ObjectType.DemiHuman:
identifier.Identify(changedItems,
GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant,
"a"));
break;
case ObjectType.Monster:
identifier.Identify(changedItems,
GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a"));
break;
}
break;
case MetaManipulation.Type.Eqdp:
identifier.Identify(changedItems,
GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot));
break;
case MetaManipulation.Type.Eqp:
identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot));
break;
case MetaManipulation.Type.Est:
switch (manip.Est.Slot)
{
case EstType.Hair:
changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null);
break;
case EstType.Face:
changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null);
break;
case EstType.Body:
identifier.Identify(changedItems,
GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race),
EquipSlot.Body));
break;
case EstType.Head:
identifier.Identify(changedItems,
GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race),
EquipSlot.Head));
break;
}
break;
case MetaManipulation.Type.Gmp:
identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head));
break;
case MetaManipulation.Type.Rsp:
changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null);
break;
case MetaManipulation.Type.GlobalEqp:
var path = manip.GlobalEqp.Type switch
{
GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale,
EquipSlot.Ears),
GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale,
EquipSlot.Neck),
GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale,
EquipSlot.Wrists),
GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale,
EquipSlot.RFinger),
GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale,
EquipSlot.LFinger),
GlobalEqpType.DoNotHideHrothgarHats => string.Empty,
GlobalEqpType.DoNotHideVieraHats => string.Empty,
_ => string.Empty,
};
if (path.Length > 0)
identifier.Identify(changedItems, path);
break;
}
}
public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container,
IDictionary<string, object?> changedItems)
{
foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys))
identifier.Identify(changedItems, gamePath.ToString());
foreach (var manip in container.Manipulations)
MetaChangedItems(identifier, changedItems, manip);
foreach (var manip in container.Manipulations.Identifiers)
manip.AddChangedItems(identifier, changedItems);
}
public static void RemoveMachinistOffhands(this SortedList<string, object?> changedItems)