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;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -7,17 +10,34 @@ namespace Penumbra.Api.Api;
public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
{ {
public const int CurrentVersion = 0;
public string GetPlayerMetaManipulations() public string GetPlayerMetaManipulations()
{ {
var collection = collectionResolver.PlayerCollection(); var collection = collectionResolver.PlayerCollection();
var set = collection.MetaCache?.Manipulations.ToArray() ?? []; return CompressMetaManipulations(collection);
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
} }
public string GetMetaManipulations(int gameObjectIdx) public string GetMetaManipulations(int gameObjectIdx)
{ {
helpers.AssociatedCollection(gameObjectIdx, out var collection); helpers.AssociatedCollection(gameObjectIdx, out var collection);
var set = collection.MetaCache?.Manipulations.ToArray() ?? []; return CompressMetaManipulations(collection);
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); }
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) if (manipString.Length == 0)
{ {
manips = []; manips = new MetaDictionary();
return true; return true;
} }
if (Functions.FromCompressedBase64(manipString, out manips!) == MetaManipulation.CurrentVersion) if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion)
return true; return true;
manips = null; manips = null;

View file

@ -5,6 +5,7 @@ using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text; using OtterGui.Text;
using Penumbra.Api.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers; using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
@ -102,8 +103,7 @@ public class TemporaryIpcTester(
&& copyCollection is { HasCache: true }) && copyCollection is { HasCache: true })
{ {
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); 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>(), var manips = MetaApi.CompressMetaManipulations(copyCollection);
MetaManipulation.CurrentVersion);
_lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999);
} }
@ -188,8 +188,8 @@ public class TemporaryIpcTester(
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {
using var tt = ImRaii.Tooltip(); using var tt = ImRaii.Tooltip();
foreach (var manip in mod.Default.Manipulations) foreach (var identifier in mod.Default.Manipulations.Identifiers)
ImGui.TextUnformatted(manip.ToString()); 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; => ConflictDict.Values;
public SingleArray<ModConflicts> Conflicts(IMod mod) 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; private int _changedItemsSaveCounter = -1;
@ -233,8 +233,20 @@ public sealed class CollectionCache : IDisposable
foreach (var (path, file) in files.FileRedirections) foreach (var (path, file) in files.FileRedirections)
AddFile(path, file, mod); AddFile(path, file, mod);
foreach (var manip in files.Manipulations) foreach (var (identifier, entry) in files.Manipulations.Eqp)
AddManipulation(manip, mod); 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) if (addMetaChanges)
{ {
@ -342,7 +354,7 @@ public sealed class CollectionCache : IDisposable
foreach (var conflict in tmpConflicts) foreach (var conflict in tmpConflicts)
{ {
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0 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); 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, // For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering. // which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded. // 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); Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, manip); ModData.AddManip(mod, identifier);
return; return;
} }
@ -387,11 +399,11 @@ public sealed class CollectionCache : IDisposable
if (mod == existingMod) if (mod == existingMod)
return; return;
if (AddConflict(manip, mod, existingMod)) if (AddConflict(identifier, mod, existingMod))
{ {
ModData.RemoveManip(existingMod, manip); ModData.RemoveManip(existingMod, identifier);
Meta.ApplyMod(manip, mod); Meta.ApplyMod(mod, identifier, entry);
ModData.AddManip(mod, manip); ModData.AddManip(mod, identifier);
} }
} }
@ -437,9 +449,9 @@ public sealed class CollectionCache : IDisposable
AddItems(modPath.Mod); AddItems(modPath.Mod);
} }
foreach (var (manip, mod) in Meta) foreach (var (manip, mod) in Meta.IdentifierSources)
{ {
identifier.MetaChangedItems(items, manip); manip.AddChangedItems(identifier, items);
AddItems(mod); AddItems(mod);
} }

View file

@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache;
/// </summary> /// </summary>
public class CollectionModData 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 public IEnumerable<(IMod, IReadOnlySet<Utf8GamePath>, IReadOnlySet<IMetaIdentifier>)> Data
=> _data.Select(kvp => (kvp.Key, (IReadOnlySet<Utf8GamePath>)kvp.Value.Item1, (IReadOnlySet<MetaManipulation>)kvp.Value.Item2)); => _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)) if (_data.Remove(mod, out var data))
return 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)) if (_data.TryGetValue(mod, out var data))
{ {
@ -54,7 +54,7 @@ public class CollectionModData
_data.Remove(mod); _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) if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0)
_data.Remove(mod); _data.Remove(mod);

View file

@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using OtterGui; using OtterGui;
using OtterGui.Filesystem;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
@ -10,28 +10,38 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache; 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 ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar
private readonly List<EqdpManipulation> _eqdpManipulations = new();
public EqdpCache() public override void SetFiles()
{ }
public void SetFiles(MetaFileManager manager)
{ {
for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) 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); var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
if (i != -1) 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); var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
if (idx < 0) if (idx < 0)
@ -47,44 +57,44 @@ public readonly struct EqdpCache : IDisposable
return null; 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>()) foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>())
{ {
var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; 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); if (GetFile(identifier) is { } file)
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??= Apply(file, identifier, entry);
new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar
return manip.Apply(file);
} }
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; return false;
var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId); file[identifier.SetId] = (entry & ~mask) | origEntry;
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!; return true;
manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
return manip.Apply(file);
} }
public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) protected override void Dispose(bool _)
=> _eqdpFiles
[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar
public void Dispose()
{ {
for (var i = 0; i < _eqdpFiles.Length; ++i) for (var i = 0; i < _eqdpFiles.Length; ++i)
{ {
@ -92,6 +102,15 @@ public readonly struct EqdpCache : IDisposable
_eqdpFiles[i] = null; _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.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta; using Penumbra.Meta;
@ -7,54 +7,77 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache; 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 ExpandedEqpFile? _eqpFile;
private readonly List<EqpManipulation> _eqpManipulations = new();
public EqpCache() public override void SetFiles()
{ } => Manager.SetFile(_eqpFile, MetaIndex.Eqp);
public void SetFiles(MetaFileManager manager) public override void ResetFiles()
=> manager.SetFile(_eqpFile, MetaIndex.Eqp); => Manager.SetFile(null, MetaIndex.Eqp);
public static void ResetFiles(MetaFileManager manager) protected override void IncorporateChangesInternal()
=> manager.SetFile(null, MetaIndex.Eqp); {
if (GetFile() is not { } file)
return;
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) foreach (var (identifier, (_, entry)) in this)
=> manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); 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) if (_eqpFile == null)
return; return;
_eqpFile.Reset(_eqpManipulations.Select(m => m.SetId)); _eqpFile.Reset(Keys.Select(identifier => identifier.SetId));
_eqpManipulations.Clear(); Clear();
} }
public bool ApplyMod(MetaFileManager manager, EqpManipulation manip) protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry)
{ {
_eqpManipulations.AddOrReplace(manip); if (GetFile() is { } file)
_eqpFile ??= new ExpandedEqpFile(manager); Apply(file, identifier, entry);
return manip.Apply(_eqpFile);
} }
public bool RevertMod(MetaFileManager manager, EqpManipulation manip) protected override void RevertModInternal(EqpIdentifier identifier)
{ {
var idx = _eqpManipulations.FindIndex(manip.Equals); if (GetFile() is { } file)
if (idx < 0) 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; return false;
var def = ExpandedEqpFile.GetDefault(manager, manip.SetId); file[identifier.SetId] = (origEntry & ~mask) | entry;
manip = new EqpManipulation(def, manip.Slot, manip.SetId); return true;
return manip.Apply(_eqpFile!);
} }
public void Dispose() protected override void Dispose(bool _)
{ {
_eqpFile?.Dispose(); _eqpFile?.Dispose();
_eqpFile = null; _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.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta; using Penumbra.Meta;
@ -9,46 +6,41 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache; 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? _estFaceFile;
private EstFile? _estHairFile = null; private EstFile? _estHairFile;
private EstFile? _estBodyFile = null; private EstFile? _estBodyFile;
private EstFile? _estHeadFile = null; private EstFile? _estHeadFile;
private readonly List<EstManipulation> _estManipulations = new(); public override void SetFiles()
public EstCache()
{ }
public void SetFiles(MetaFileManager manager)
{ {
manager.SetFile(_estFaceFile, MetaIndex.FaceEst); Manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
manager.SetFile(_estHairFile, MetaIndex.HairEst); Manager.SetFile(_estHairFile, MetaIndex.HairEst);
manager.SetFile(_estBodyFile, MetaIndex.BodyEst); Manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
manager.SetFile(_estHeadFile, MetaIndex.HeadEst); Manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
} }
public void SetFile(MetaFileManager manager, MetaIndex index) public void SetFile(MetaIndex index)
{ {
switch (index) switch (index)
{ {
case MetaIndex.FaceEst: case MetaIndex.FaceEst:
manager.SetFile(_estFaceFile, MetaIndex.FaceEst); Manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
break; break;
case MetaIndex.HairEst: case MetaIndex.HairEst:
manager.SetFile(_estHairFile, MetaIndex.HairEst); Manager.SetFile(_estHairFile, MetaIndex.HairEst);
break; break;
case MetaIndex.BodyEst: case MetaIndex.BodyEst:
manager.SetFile(_estBodyFile, MetaIndex.BodyEst); Manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
break; break;
case MetaIndex.HeadEst: case MetaIndex.HeadEst:
manager.SetFile(_estHeadFile, MetaIndex.HeadEst); Manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
break; break;
} }
} }
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type) public MetaList.MetaReverter TemporarilySetFiles(EstType type)
{ {
var (file, idx) = type switch var (file, idx) = type switch
{ {
@ -56,74 +48,65 @@ public struct EstCache : IDisposable
EstType.Hair => (_estHairFile, MetaIndex.HairEst), EstType.Hair => (_estHairFile, MetaIndex.HairEst),
EstType.Body => (_estBodyFile, MetaIndex.BodyEst), EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
EstType.Head => (_estHeadFile, MetaIndex.HeadEst), 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 if (!Manager.CharacterUtility.Ready)
{ return;
EstType.Face => _estFaceFile,
EstType.Hair => _estHairFile, foreach (var (identifier, (_, entry)) in this)
EstType.Body => _estBodyFile, Apply(GetFile(identifier)!, identifier, entry);
EstType.Head => _estHeadFile, Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EST manipulations.");
_ => null,
};
} }
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 return file != null
? file[genderRace, primaryId.Id] ? file[identifier.GenderRace, identifier.SetId]
: EstFile.GetDefault(manager, type, genderRace, primaryId); : EstFile.GetDefault(Manager, identifier);
} }
public void Reset() public override void Reset()
{ {
_estFaceFile?.Reset(); _estFaceFile?.Reset();
_estHairFile?.Reset(); _estHairFile?.Reset();
_estBodyFile?.Reset(); _estBodyFile?.Reset();
_estHeadFile?.Reset(); _estHeadFile?.Reset();
_estManipulations.Clear(); Clear();
} }
public bool ApplyMod(MetaFileManager manager, EstManipulation m) protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry)
{ {
_estManipulations.AddOrReplace(m); if (GetFile(identifier) is { } file)
var file = m.Slot switch Apply(file, identifier, entry);
{
EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair),
EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face),
EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body),
EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head),
_ => throw new ArgumentOutOfRangeException(),
};
return m.Apply(file);
} }
public bool RevertMod(MetaFileManager manager, EstManipulation m) protected override void RevertModInternal(EstIdentifier identifier)
{ {
if (!_estManipulations.Remove(m)) if (GetFile(identifier) is { } file)
return false; Apply(file, identifier, EstFile.GetDefault(Manager, identifier.Slot, identifier.GenderRace, identifier.SetId));
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);
} }
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(); _estFaceFile?.Dispose();
_estHairFile?.Dispose(); _estHairFile?.Dispose();
@ -133,6 +116,21 @@ public struct EstCache : IDisposable
_estHairFile = null; _estHairFile = null;
_estBodyFile = null; _estBodyFile = null;
_estHeadFile = 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 OtterGui.Services;
using Penumbra.GameData.Structs; 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> _doNotHideEarrings = [];
private readonly HashSet<PrimaryId> _doNotHideNecklace = []; private readonly HashSet<PrimaryId> _doNotHideNecklace = [];
@ -13,11 +15,9 @@ public struct GlobalEqpCache : IService
private bool _doNotHideVieraHats; private bool _doNotHideVieraHats;
private bool _doNotHideHrothgarHats; private bool _doNotHideHrothgarHats;
public GlobalEqpCache() public new void Clear()
{ }
public void Clear()
{ {
base.Clear();
_doNotHideEarrings.Clear(); _doNotHideEarrings.Clear();
_doNotHideNecklace.Clear(); _doNotHideNecklace.Clear();
_doNotHideBracelets.Clear(); _doNotHideBracelets.Clear();
@ -29,6 +29,9 @@ public struct GlobalEqpCache : IService
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
{ {
if (Count == 0)
return original;
if (_doNotHideVieraHats) if (_doNotHideVieraHats)
original |= EqpEntry.HeadShowVieraHat; original |= EqpEntry.HeadShowVieraHat;
@ -52,8 +55,13 @@ public struct GlobalEqpCache : IService
return original; return original;
} }
public bool Add(GlobalEqpManipulation manipulation) public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation)
=> manipulation.Type switch {
if (Remove(manipulation, out var oldMod) && oldMod == mod)
return false;
this[manipulation] = mod;
_ = manipulation.Type switch
{ {
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition), GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition), GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition),
@ -61,12 +69,18 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition), GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition), GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true), GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
_ => false, _ => false,
}; };
return true;
}
public bool Remove(GlobalEqpManipulation manipulation) public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod)
=> manipulation.Type switch {
if (!Remove(manipulation, out mod))
return false;
_ = manipulation.Type switch
{ {
GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition), GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition), GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition),
@ -74,7 +88,9 @@ public struct GlobalEqpCache : IService
GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition), GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition), GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false), GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
_ => false, _ => false,
}; };
return true;
}
} }

View file

@ -1,4 +1,4 @@
using OtterGui.Filesystem; using Penumbra.GameData.Structs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta; using Penumbra.Meta;
@ -7,50 +7,76 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections.Cache; 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 ExpandedGmpFile? _gmpFile;
private readonly List<GmpManipulation> _gmpManipulations = new();
public GmpCache() public override void SetFiles()
{ } => Manager.SetFile(_gmpFile, MetaIndex.Gmp);
public void SetFiles(MetaFileManager manager) public override void ResetFiles()
=> manager.SetFile(_gmpFile, MetaIndex.Gmp); => Manager.SetFile(null, MetaIndex.Gmp);
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) protected override void IncorporateChangesInternal()
=> manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); {
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) if (_gmpFile == null)
return; return;
_gmpFile.Reset(_gmpManipulations.Select(m => m.SetId)); _gmpFile.Reset(Keys.Select(identifier => identifier.SetId));
_gmpManipulations.Clear(); Clear();
} }
public bool ApplyMod(MetaFileManager manager, GmpManipulation manip) protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry)
{ {
_gmpManipulations.AddOrReplace(manip); if (GetFile() is { } file)
_gmpFile ??= new ExpandedGmpFile(manager); Apply(file, identifier, entry);
return manip.Apply(_gmpFile);
} }
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; return false;
var def = ExpandedGmpFile.GetDefault(manager, manip.SetId); file[identifier.SetId] = entry;
manip = new GmpManipulation(def, manip.SetId); return true;
return manip.Apply(_gmpFile!);
} }
public void Dispose() protected override void Dispose(bool _)
{ {
_gmpFile?.Dispose(); _gmpFile?.Dispose();
_gmpFile = null; _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.Interop.PathResolving;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
@ -6,116 +9,132 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache; 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 Dictionary<Utf8GamePath, (ImcFile, HashSet<ImcIdentifier>)> _imcFiles = [];
private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = [];
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) if (fromFullCompute)
foreach (var path in _imcFiles.Keys) foreach (var (path, _) in _imcFiles)
collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection)); Collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, Collection));
else else
foreach (var path in _imcFiles.Keys) foreach (var (path, _) in _imcFiles)
collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection)); 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(); 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)) if (Manager.CharacterUtility.Ready)
return false; ApplyFile(identifier, entry);
}
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip)); private void ApplyFile(ImcIdentifier identifier, ImcEntry entry)
if (idx < 0) {
{ var path = identifier.GamePath();
idx = _imcManipulations.Count;
_imcManipulations.Add((manip, null!));
}
var path = manip.GamePath();
try try
{ {
if (!_imcFiles.TryGetValue(path, out var file)) if (!_imcFiles.TryGetValue(path, out var pair))
file = new ImcFile(manager, manip.Identifier); pair = (new ImcFile(Manager, identifier), []);
_imcManipulations[idx] = (manip, file);
if (!manip.Apply(file))
return false;
_imcFiles[path] = file; if (!Apply(pair.Item1, identifier, entry))
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); return;
collection._cache!.ForceFile(path, fullPath);
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) catch (ImcException e)
{ {
manager.ValidityChecker.ImcExceptions.Add(e); Manager.ValidityChecker.ImcExceptions.Add(e);
Penumbra.Log.Error(e.ToString()); Penumbra.Log.Error(e.ToString());
} }
catch (Exception e) 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)) var path = identifier.GamePath();
return false; if (!_imcFiles.TryGetValue(path, out var pair))
return;
var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); if (!pair.Item2.Remove(identifier))
if (idx < 0) return;
return false;
var (_, file) = _imcManipulations[idx]; if (pair.Item2.Count == 0)
_imcManipulations.RemoveAt(idx);
if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file)))
{ {
_imcFiles.Remove(file.Path); _imcFiles.Remove(path);
collection._cache!.ForceFile(file.Path, FullPath.Empty); Collection._cache!.ForceFile(pair.Item1.Path, FullPath.Empty);
file.Dispose(); pair.Item1.Dispose();
return true; return;
} }
var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _); var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _);
var manip = m.Copy(def); if (!Apply(pair.Item1, identifier, def))
if (!manip.Apply(file)) return;
return false;
var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection);
collection._cache!.ForceFile(file.Path, fullPath); Collection._cache!.ForceFile(pair.Item1.Path, fullPath);
return true;
} }
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(); file.Dispose();
Clear();
_imcFiles.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.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
@ -9,238 +10,174 @@ using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache; namespace Penumbra.Collections.Cache;
public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>> public class MetaCache(MetaFileManager manager, ModCollection collection)
{ {
private readonly MetaFileManager _manager; public readonly EqpCache Eqp = new(manager, collection);
private readonly ModCollection _collection; public readonly EqdpCache Eqdp = new(manager, collection);
private readonly Dictionary<MetaManipulation, IMod> _manipulations = new(); public readonly EstCache Est = new(manager, collection);
private EqpCache _eqpCache = new(); public readonly GmpCache Gmp = new(manager, collection);
private readonly EqdpCache _eqdpCache = new(); public readonly RspCache Rsp = new(manager, collection);
private EstCache _estCache = new(); public readonly ImcCache Imc = new(manager, collection);
private GmpCache _gmpCache = new(); public readonly GlobalEqpCache GlobalEqp = 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 int Count public int Count
=> _manipulations.Count; => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count;
public IReadOnlyCollection<MetaManipulation> Manipulations public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
=> _manipulations.Keys; => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
.Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
public IEnumerator<KeyValuePair<MetaManipulation, IMod>> GetEnumerator() .Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
=> _manipulations.GetEnumerator(); .Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
IEnumerator IEnumerable.GetEnumerator() .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
=> GetEnumerator(); .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
public MetaCache(MetaFileManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
if (!_manager.CharacterUtility.Ready)
_manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
}
public void SetFiles() public void SetFiles()
{ {
_eqpCache.SetFiles(_manager); Eqp.SetFiles();
_eqdpCache.SetFiles(_manager); Eqdp.SetFiles();
_estCache.SetFiles(_manager); Est.SetFiles();
_gmpCache.SetFiles(_manager); Gmp.SetFiles();
_cmpCache.SetFiles(_manager); Rsp.SetFiles();
_imcCache.SetFiles(_collection, false); Imc.SetFiles(false);
} }
public void Reset() public void Reset()
{ {
_eqpCache.Reset(); Eqp.Reset();
_eqdpCache.Reset(); Eqdp.Reset();
_estCache.Reset(); Est.Reset();
_gmpCache.Reset(); Gmp.Reset();
_cmpCache.Reset(); Rsp.Reset();
_imcCache.Reset(_collection); Imc.Reset();
_manipulations.Clear(); GlobalEqp.Clear();
_globalEqpCache.Clear();
} }
public void Dispose() public void Dispose()
{ {
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; Eqp.Dispose();
_eqpCache.Dispose(); Eqdp.Dispose();
_eqdpCache.Dispose(); Est.Dispose();
_estCache.Dispose(); Gmp.Dispose();
_gmpCache.Dispose(); Rsp.Dispose();
_cmpCache.Dispose(); Imc.Dispose();
_imcCache.Dispose();
_manipulations.Clear();
} }
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() ~MetaCache()
=> Dispose(); => 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> /// <summary> Set a single file. </summary>
public void SetFile(MetaIndex metaIndex) public void SetFile(MetaIndex metaIndex)
{ {
switch (metaIndex) switch (metaIndex)
{ {
case MetaIndex.Eqp: case MetaIndex.Eqp:
_eqpCache.SetFiles(_manager); Eqp.SetFiles();
break; break;
case MetaIndex.Gmp: case MetaIndex.Gmp:
_gmpCache.SetFiles(_manager); Gmp.SetFiles();
break; break;
case MetaIndex.HumanCmp: case MetaIndex.HumanCmp:
_cmpCache.SetFiles(_manager); Rsp.SetFiles();
break; break;
case MetaIndex.FaceEst: case MetaIndex.FaceEst:
case MetaIndex.HairEst: case MetaIndex.HairEst:
case MetaIndex.HeadEst: case MetaIndex.HeadEst:
case MetaIndex.BodyEst: case MetaIndex.BodyEst:
_estCache.SetFile(_manager, metaIndex); Est.SetFile(metaIndex);
break; break;
default: default:
_eqdpCache.SetFile(_manager, metaIndex); Eqdp.SetFile(metaIndex);
break; break;
} }
} }
/// <summary> Set the currently relevant IMC files for the collection cache. </summary> /// <summary> Set the currently relevant IMC files for the collection cache. </summary>
public void SetImcFiles(bool fromFullCompute) public void SetImcFiles(bool fromFullCompute)
=> _imcCache.SetFiles(_collection, fromFullCompute); => Imc.SetFiles(fromFullCompute);
public MetaList.MetaReverter TemporarilySetEqpFile() public MetaList.MetaReverter TemporarilySetEqpFile()
=> _eqpCache.TemporarilySetFiles(_manager); => Eqp.TemporarilySetFile();
public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); => Eqdp.TemporarilySetFile(genderRace, accessory);
public MetaList.MetaReverter TemporarilySetGmpFile() public MetaList.MetaReverter TemporarilySetGmpFile()
=> _gmpCache.TemporarilySetFiles(_manager); => Gmp.TemporarilySetFile();
public MetaList.MetaReverter TemporarilySetCmpFile() public MetaList.MetaReverter TemporarilySetCmpFile()
=> _cmpCache.TemporarilySetFiles(_manager); => Rsp.TemporarilySetFile();
public MetaList.MetaReverter TemporarilySetEstFile(EstType type) public MetaList.MetaReverter TemporarilySetEstFile(EstType type)
=> _estCache.TemporarilySetFiles(_manager, type); => Est.TemporarilySetFiles(type);
public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) 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> /// <summary> Try to obtain a manipulated IMC file. </summary>
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) 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) internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId)
{ {
var eqdpFile = _eqdpCache.EqdpFile(race, accessory); var eqdpFile = Eqdp.EqdpFile(race, accessory);
if (eqdpFile != null) if (eqdpFile != null)
return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default; 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) internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
=> _estCache.GetEstEntry(_manager, type, genderRace, primaryId); => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace));
/// <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.");
}
} }

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(); _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( => EnqueueWithResult(
new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath),
action => action.Notifier 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> /// <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="mdlPath"> .mdl file to look up the skeletons for. </param>
/// <param name="estManipulations"> Modified extra skeleton template parameters. </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); var info = parser.GetFileInfo(mdlPath);
if (info.FileType is not FileType.Model) 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. // Try to find an EST entry from the manipulations provided.
var (gender, race) = info.GenderRace.Split();
var modEst = estManipulations var modEst = estManipulations
.FirstOrNull(est => .FirstOrNull(
est.Gender == gender est => est.Key.GenderRace == info.GenderRace
&& est.Race == race && est.Key.Slot == type
&& est.Slot == type && est.Key.SetId == info.PrimaryId
&& est.SetId == info.PrimaryId
); );
// Try to use an entry from provided manipulations, falling back to the current collection. // 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) ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
?? EstEntry.Zero; ?? EstEntry.Zero;
@ -102,7 +101,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
if (targetId == EstEntry.Zero) if (targetId == EstEntry.Zero)
return []; 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> /// <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); var path = manager.ResolveMtrlPath(relativePath, notifier);
if (path == null) if (path == null)
return null; return null;
var bytes = read(path); var bytes = read(path);
if (bytes == null) if (bytes == null)
return null; return null;
var mtrl = new MtrlFile(bytes); var mtrl = new MtrlFile(bytes);
return new MaterialExporter.Material return new MaterialExporter.Material

View file

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

View file

@ -1,3 +1,4 @@
using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Meta; using Penumbra.Meta;
@ -8,7 +9,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta 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); 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[]>(); 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) if (group.Key.Length == 0)
continue; continue;
var bytes = group.Key.EndsWith(".rgsp") var bytes = WriteRgspFile(manager, group);
? WriteRgspFile(manager, group.Key, group)
: WriteMetaFile(manager, group.Key, group);
if (bytes.Length == 0) if (bytes.Length == 0)
continue; continue;
ret.Add(group.Key, bytes); 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; 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 m = new MemoryStream(45);
using var b = new BinaryWriter(m); using var b = new BinaryWriter(m);
// Version // Version
b.Write(byte.MaxValue); b.Write(byte.MaxValue);
b.Write((ushort)2); b.Write((ushort)2);
var race = list.First().Value.SubRace; var race = list.First().Value.Key.SubRace;
var gender = list.First().Value.Attribute.ToGender(); var gender = list.First().Value.Key.Attribute.ToGender();
b.Write((byte)(race - 1)); // offset by one due to Unknown b.Write((byte)(race - 1)); // offset by one due to Unknown
b.Write((byte)(gender - 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) if (gender == Gender.Male)
{ {
Add(RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail); Add(RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail);
@ -82,12 +115,24 @@ public partial class TexToolsMeta
} }
return m.GetBuffer(); 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 m = new MemoryStream();
using var b = new BinaryWriter(m); using var b = new BinaryWriter(m);
@ -101,7 +146,7 @@ public partial class TexToolsMeta
b.Write((byte)0); b.Write((byte)0);
// Number of Headers // Number of Headers
b.Write((uint)filteredManips.Count); b.Write((uint)headerCount);
// Current TT Size of Headers // Current TT Size of Headers
b.Write((uint)12); b.Write((uint)12);
@ -109,88 +154,44 @@ public partial class TexToolsMeta
var headerStart = b.BaseStream.Position + 4; var headerStart = b.BaseStream.Position + 4;
b.Write((uint)headerStart); b.Write((uint)headerStart);
var offset = (uint)(b.BaseStream.Position + 12 * filteredManips.Count); var offset = (uint)(b.BaseStream.Position + 12 * manips.Count);
foreach (var (header, data) in filteredManips) offset += WriteData(manager, b, offset, manips.Imc);
{ offset += WriteData(b, offset, manips.Eqdp);
b.Write((uint)header); offset += WriteData(b, offset, manips.Eqp);
b.Write(offset); offset += WriteData(b, offset, manips.Est);
offset += WriteData(b, offset, manips.Gmp);
var size = WriteData(manager, b, offset, header, data);
b.Write(size);
offset += size;
}
return m.ToArray(); return m.ToArray();
} }
private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, IReadOnlyDictionary<ImcIdentifier, ImcEntry> manips)
IEnumerable<MetaManipulation> manips)
{ {
if (manips.Count == 0)
return 0;
b.Write((uint)MetaManipulationType.Imc);
b.Write(offset);
var oldPos = b.BaseStream.Position; var oldPos = b.BaseStream.Position;
b.Seek((int)offset, SeekOrigin.Begin); 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 entry = baseFile.GetEntry(partIdx, (Variant)i);
var allManips = manips.ToList(); b.Write(entry.MaterialId);
var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier); b.Write(entry.DecalId);
foreach (var manip in allManips) b.Write(entry.AttributeAndSound);
manip.Imc.Apply(baseFile); b.Write(entry.VfxId);
b.Write(entry.MaterialAnimationId);
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 size = b.BaseStream.Position - offset; var size = b.BaseStream.Position - offset;
@ -198,19 +199,98 @@ public partial class TexToolsMeta
return (uint)size; return (uint)size;
} }
private static string ManipToPath(MetaManipulation manip) private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> manips)
=> manip.ManipulationType switch {
{ if (manips.Count == 0)
MetaManipulation.Type.Imc => ManipToPath(manip.Imc), return 0;
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 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 path = manip.GamePath().ToString();
var replacement = manip.ObjectType switch var replacement = manip.ObjectType switch
@ -224,33 +304,33 @@ public partial class TexToolsMeta
return path.Replace(".imc", replacement); return path.Replace(".imc", replacement);
} }
private static string ManipToPath(EqdpManipulation manip) private static string ManipToPath(EqdpIdentifier manip)
=> manip.Slot.IsAccessory() => manip.Slot.IsAccessory()
? $"chara/accessory/a{manip.SetId:D4}/a{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:D4}/e{manip.SetId: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() => manip.Slot.IsAccessory()
? $"chara/accessory/a{manip.SetId:D4}/a{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:D4}/e{manip.SetId: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(); var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode();
return manip.Slot switch 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.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:D4}/c{raceCode}f{manip.SetId:D4}_fac.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:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.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:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", EstType.Head => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta",
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
} }
private static string ManipToPath(GmpManipulation manip) private static string ManipToPath(GmpIdentifier manip)
=> $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta"; => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta";
private static string ManipToPath(RspManipulation manip) private static string ManipToPath(KeyValuePair<RspIdentifier, RspEntry> manip)
=> $"chara/xls/charamake/rgsp/{(int)manip.SubRace - 1}-{(int)manip.Attribute.ToGender() - 1}.rgsp"; => $"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; 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) if (gender == 1)
{ {
Add(RspAttribute.FemaleMinSize, br.ReadSingle()); Add(RspAttribute.FemaleMinSize, br.ReadSingle());
@ -73,5 +65,14 @@ public partial class TexToolsMeta
} }
return ret; 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.GameData.Data;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Meta; using Penumbra.Meta;
@ -22,10 +21,10 @@ public partial class TexToolsMeta
public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0); 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. // The info class determines the files or table locations the changes need to apply to from the filename.
public readonly uint Version; public readonly uint Version;
public readonly string FilePath; public readonly string FilePath;
public readonly List<MetaManipulation> MetaManipulations = new(); public readonly MetaDictionary MetaManipulations = new();
private readonly bool _keepDefault = false; private readonly bool _keepDefault;
private readonly MetaFileManager _metaFileManager; private readonly MetaFileManager _metaFileManager;
@ -44,18 +43,18 @@ public partial class TexToolsMeta
var headerStart = reader.ReadUInt32(); var headerStart = reader.ReadUInt32();
reader.BaseStream.Seek(headerStart, SeekOrigin.Begin); 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) for (var i = 0; i < numHeaders; ++i)
{ {
var currentOffset = reader.BaseStream.Position; var currentOffset = reader.BaseStream.Position;
var type = (MetaManipulation.Type)reader.ReadUInt32(); var type = (MetaManipulationType)reader.ReadUInt32();
var offset = reader.ReadUInt32(); var offset = reader.ReadUInt32();
var size = reader.ReadInt32(); var size = reader.ReadInt32();
entries.Add((type, offset, size)); entries.Add((type, offset, size));
reader.BaseStream.Seek(currentOffset + headerSize, SeekOrigin.Begin); reader.BaseStream.Seek(currentOffset + headerSize, SeekOrigin.Begin);
} }
byte[]? ReadEntry(MetaManipulation.Type type) byte[]? ReadEntry(MetaManipulationType type)
{ {
var idx = entries.FindIndex(t => t.type == type); var idx = entries.FindIndex(t => t.type == type);
if (idx < 0) if (idx < 0)
@ -65,11 +64,11 @@ public partial class TexToolsMeta
return reader.ReadBytes(entries[idx].size); return reader.ReadBytes(entries[idx].size);
} }
DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Eqp)); DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulationType.Eqp));
DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Gmp)); DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulationType.Gmp));
DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulation.Type.Eqdp)); DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulationType.Eqdp));
DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulation.Type.Est)); DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulationType.Est));
DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulation.Type.Imc)); DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulationType.Imc));
} }
catch (Exception e) catch (Exception e)
{ {

View file

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

View file

@ -273,7 +273,7 @@ internal partial record ResolveContext
{ {
var metaCache = Global.Collection.MetaCache; var metaCache = Global.Collection.MetaCache;
var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; 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) private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex)

View file

@ -51,10 +51,6 @@ public class ImcChecker
return entry; 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) private static ImcFile? GetFile(ImcIdentifier identifier)
{ {
if (_dataManager == null) if (_dataManager == null)

View file

@ -69,10 +69,16 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge
jObj["Slot"] = Slot.ToString(); jObj["Slot"] = Slot.ToString();
return jObj; return jObj;
} }
public MetaManipulationType Type
=> MetaManipulationType.Eqdp;
} }
public readonly record struct EqdpEntryInternal(bool Material, bool Model) 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) private EqdpEntryInternal((bool, bool) val)
: this(val.Item1, val.Item2) : this(val.Item1, val.Item2)
{ } { }
@ -83,4 +89,7 @@ public readonly record struct EqdpEntryInternal(bool Material, bool Model)
public EqdpEntry ToEntry(EquipSlot slot) public EqdpEntry ToEntry(EquipSlot slot)
=> Eqdp.FromSlotAndBits(slot, Material, Model); => 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(); jObj["Slot"] = Slot.ToString();
return jObj; return jObj;
} }
public MetaManipulationType Type
=> MetaManipulationType.Eqp;
} }
public readonly record struct EqpEntryInternal(uint Value) 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(); jObj["Slot"] = Slot.ToString();
return jObj; return jObj;
} }
public MetaManipulationType Type
=> MetaManipulationType.Est;
} }
[JsonConverter(typeof(Converter))] [JsonConverter(typeof(Converter))]
@ -111,3 +114,16 @@ public readonly record struct EstEntry(ushort Value)
=> new(serializer.Deserialize<ushort>(reader)); => 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 Newtonsoft.Json.Linq;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Manipulations;
public readonly struct GlobalEqpManipulation : IMetaManipulation<GlobalEqpManipulation>, IMetaIdentifier public readonly struct GlobalEqpManipulation : IMetaIdentifier
{ {
public GlobalEqpType Type { get; init; } public GlobalEqpType Type { get; init; }
public PrimaryId Condition { 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)}"; => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems) 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() public MetaIndex FileIndex()
=> MetaIndex.Eqp; => 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(); jObj["SetId"] = SetId.Id.ToString();
return jObj; 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; 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 interface IMetaIdentifier
{ {
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems); public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems);
@ -13,4 +25,8 @@ public interface IMetaIdentifier
public bool Validate(); public bool Validate();
public JObject AddToJson(JObject jObj); 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) : 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) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
=> AddChangedItems(identifier, changedItems, false); => AddChangedItems(identifier, changedItems, false);
@ -193,4 +190,7 @@ public readonly record struct ImcIdentifier(
jObj["BodySlot"] = BodySlot.ToString(); jObj["BodySlot"] = BodySlot.ToString();
return jObj; 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;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Util; using Penumbra.Util;
using ImcEntry = Penumbra.GameData.Structs.ImcEntry; using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
@ -7,7 +8,7 @@ using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
namespace Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Manipulations;
[JsonConverter(typeof(Converter))] [JsonConverter(typeof(Converter))]
public class MetaDictionary : IEnumerable<MetaManipulation> public class MetaDictionary
{ {
private readonly Dictionary<ImcIdentifier, ImcEntry> _imc = []; private readonly Dictionary<ImcIdentifier, ImcEntry> _imc = [];
private readonly Dictionary<EqpIdentifier, EqpEntryInternal> _eqp = []; private readonly Dictionary<EqpIdentifier, EqpEntryInternal> _eqp = [];
@ -20,32 +21,50 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
=> _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 Count { get; private set; }
public int GetCount(MetaManipulation.Type type) public int GetCount(MetaManipulationType type)
=> type switch => type switch
{ {
MetaManipulation.Type.Imc => _imc.Count, MetaManipulationType.Imc => _imc.Count,
MetaManipulation.Type.Eqdp => _eqdp.Count, MetaManipulationType.Eqdp => _eqdp.Count,
MetaManipulation.Type.Eqp => _eqp.Count, MetaManipulationType.Eqp => _eqp.Count,
MetaManipulation.Type.Est => _est.Count, MetaManipulationType.Est => _est.Count,
MetaManipulation.Type.Gmp => _gmp.Count, MetaManipulationType.Gmp => _gmp.Count,
MetaManipulation.Type.Rsp => _rsp.Count, MetaManipulationType.Rsp => _rsp.Count,
MetaManipulation.Type.GlobalEqp => _globalEqp.Count, MetaManipulationType.GlobalEqp => _globalEqp.Count,
_ => 0, _ => 0,
}; };
public bool CanAdd(IMetaIdentifier identifier) public bool Contains(IMetaIdentifier identifier)
=> identifier switch => identifier switch
{ {
EqdpIdentifier eqdpIdentifier => !_eqdp.ContainsKey(eqdpIdentifier), EqdpIdentifier i => _eqdp.ContainsKey(i),
EqpIdentifier eqpIdentifier => !_eqp.ContainsKey(eqpIdentifier), EqpIdentifier i => _eqp.ContainsKey(i),
EstIdentifier estIdentifier => !_est.ContainsKey(estIdentifier), EstIdentifier i => _est.ContainsKey(i),
GlobalEqpManipulation globalEqpManipulation => !_globalEqp.Contains(globalEqpManipulation), GlobalEqpManipulation i => _globalEqp.Contains(i),
GmpIdentifier gmpIdentifier => !_gmp.ContainsKey(gmpIdentifier), GmpIdentifier i => _gmp.ContainsKey(i),
ImcIdentifier imcIdentifier => !_imc.ContainsKey(imcIdentifier), ImcIdentifier i => _imc.ContainsKey(i),
RspIdentifier rspIdentifier => !_rsp.ContainsKey(rspIdentifier), RspIdentifier i => _rsp.ContainsKey(i),
_ => false, _ => false,
}; };
public void Clear() public void Clear()
@ -69,17 +88,16 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
&& _gmp.SetEquals(other._gmp) && _gmp.SetEquals(other._gmp)
&& _globalEqp.SetEquals(other._globalEqp); && _globalEqp.SetEquals(other._globalEqp);
public IEnumerator<MetaManipulation> GetEnumerator() public IEnumerable<IMetaIdentifier> Identifiers
=> _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value))) => _imc.Keys.Cast<IMetaIdentifier>()
.Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) .Concat(_eqdp.Keys.Cast<IMetaIdentifier>())
.Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) .Concat(_eqp.Keys.Cast<IMetaIdentifier>())
.Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value)))) .Concat(_est.Keys.Cast<IMetaIdentifier>())
.Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value)))) .Concat(_gmp.Keys.Cast<IMetaIdentifier>())
.Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value)))) .Concat(_rsp.Keys.Cast<IMetaIdentifier>())
.Concat(_globalEqp.Select(manip => new MetaManipulation(manip))).GetEnumerator(); .Concat(_globalEqp.Cast<IMetaIdentifier>());
IEnumerator IEnumerable.GetEnumerator() #region TryAdd
=> GetEnumerator();
public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) public bool TryAdd(ImcIdentifier identifier, ImcEntry entry)
{ {
@ -90,7 +108,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true; return true;
} }
public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry)
{ {
if (!_eqp.TryAdd(identifier, entry)) if (!_eqp.TryAdd(identifier, entry))
@ -103,7 +120,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) public bool TryAdd(EqpIdentifier identifier, EqpEntry entry)
=> TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot));
public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry)
{ {
if (!_eqdp.TryAdd(identifier, entry)) if (!_eqdp.TryAdd(identifier, entry))
@ -152,6 +168,10 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true; return true;
} }
#endregion
#region Update
public bool Update(ImcIdentifier identifier, ImcEntry entry) public bool Update(ImcIdentifier identifier, ImcEntry entry)
{ {
if (!_imc.ContainsKey(identifier)) if (!_imc.ContainsKey(identifier))
@ -161,7 +181,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true; return true;
} }
public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) public bool Update(EqpIdentifier identifier, EqpEntryInternal entry)
{ {
if (!_eqp.ContainsKey(identifier)) if (!_eqp.ContainsKey(identifier))
@ -174,7 +193,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
public bool Update(EqpIdentifier identifier, EqpEntry entry) public bool Update(EqpIdentifier identifier, EqpEntry entry)
=> Update(identifier, new EqpEntryInternal(entry, identifier.Slot)); => Update(identifier, new EqpEntryInternal(entry, identifier.Slot));
public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry)
{ {
if (!_eqdp.ContainsKey(identifier)) if (!_eqdp.ContainsKey(identifier))
@ -214,6 +232,50 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return true; 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) public void UnionWith(MetaDictionary manips)
{ {
foreach (var (identifier, entry) in manips._imc) foreach (var (identifier, entry) in manips._imc)
@ -287,24 +349,6 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return false; 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) public void SetTo(MetaDictionary other)
{ {
_imc.SetTo(other._imc); _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; Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count;
} }
#endregion
public MetaDictionary Clone() public MetaDictionary Clone()
{ {
var ret = new MetaDictionary(); var ret = new MetaDictionary();
@ -336,29 +382,124 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return ret; return ret;
} }
private static void WriteJson(JsonWriter writer, JsonSerializer serializer, IMetaIdentifier identifier, object entry) public static JObject Serialize(EqpIdentifier identifier, EqpEntryInternal entry)
{ => Serialize(identifier, entry.ToEntry(identifier.Slot));
var type = identifier switch
public static JObject Serialize(EqpIdentifier identifier, EqpEntry entry)
=> new()
{ {
ImcIdentifier => "Imc", ["Type"] = MetaManipulationType.Eqp.ToString(),
EqdpIdentifier => "Eqdp", ["Manipulation"] = identifier.AddToJson(new JObject
EqpIdentifier => "Eqp", {
EstIdentifier => "Est", ["Entry"] = (ulong)entry,
GmpIdentifier => "Gmp", }),
RspIdentifier => "Rsp",
GlobalEqpManipulation => "GlobalEqp",
_ => string.Empty,
}; };
if (type.Length == 0) public static JObject Serialize(EqdpIdentifier identifier, EqdpEntryInternal entry)
return; => Serialize(identifier, entry.ToEntry(identifier.Slot));
writer.WriteStartObject(); public static JObject Serialize(EqdpIdentifier identifier, EqdpEntry entry)
writer.WritePropertyName("Type"); => new()
writer.WriteValue(type); {
writer.WritePropertyName("Manipulation"); ["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> private class Converter : JsonConverter<MetaDictionary>
@ -371,30 +512,27 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return; return;
} }
writer.WriteStartArray(); var array = new JArray();
foreach (var item in value) SerializeTo(array, value._imc);
{ SerializeTo(array, value._eqp);
writer.WriteStartObject(); SerializeTo(array, value._eqdp);
writer.WritePropertyName("Type"); SerializeTo(array, value._est);
writer.WriteValue(item.ManipulationType.ToString()); SerializeTo(array, value._rsp);
writer.WritePropertyName("Manipulation"); SerializeTo(array, value._gmp);
serializer.Serialize(writer, item.Manipulation); SerializeTo(array, value._globalEqp);
writer.WriteEndObject(); array.WriteTo(writer);
}
writer.WriteEndArray();
} }
public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue, public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue,
JsonSerializer serializer) JsonSerializer serializer)
{ {
var dict = existingValue ?? []; var dict = existingValue ?? new MetaDictionary();
dict.Clear(); dict.Clear();
var jObj = JArray.Load(reader); var jObj = JArray.Load(reader);
foreach (var item in jObj) foreach (var item in jObj)
{ {
var type = item["Type"]?.ToObject<MetaManipulation.Type>() ?? MetaManipulation.Type.Unknown; var type = item["Type"]?.ToObject<MetaManipulationType>() ?? MetaManipulationType.Unknown;
if (type is MetaManipulation.Type.Unknown) if (type is MetaManipulationType.Unknown)
{ {
Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered."); Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered.");
continue; continue;
@ -408,7 +546,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
switch (type) switch (type)
{ {
case MetaManipulation.Type.Imc: case MetaManipulationType.Imc:
{ {
var identifier = ImcIdentifier.FromJson(manip); var identifier = ImcIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<ImcEntry>(); var entry = manip["Entry"]?.ToObject<ImcEntry>();
@ -418,7 +556,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); Penumbra.Log.Warning("Invalid IMC Manipulation encountered.");
break; break;
} }
case MetaManipulation.Type.Eqdp: case MetaManipulationType.Eqdp:
{ {
var identifier = EqdpIdentifier.FromJson(manip); var identifier = EqdpIdentifier.FromJson(manip);
var entry = (EqdpEntry?)manip["Entry"]?.ToObject<ushort>(); var entry = (EqdpEntry?)manip["Entry"]?.ToObject<ushort>();
@ -428,7 +566,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); Penumbra.Log.Warning("Invalid EQDP Manipulation encountered.");
break; break;
} }
case MetaManipulation.Type.Eqp: case MetaManipulationType.Eqp:
{ {
var identifier = EqpIdentifier.FromJson(manip); var identifier = EqpIdentifier.FromJson(manip);
var entry = (EqpEntry?)manip["Entry"]?.ToObject<ulong>(); var entry = (EqpEntry?)manip["Entry"]?.ToObject<ulong>();
@ -438,7 +576,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); Penumbra.Log.Warning("Invalid EQP Manipulation encountered.");
break; break;
} }
case MetaManipulation.Type.Est: case MetaManipulationType.Est:
{ {
var identifier = EstIdentifier.FromJson(manip); var identifier = EstIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<EstEntry>(); var entry = manip["Entry"]?.ToObject<EstEntry>();
@ -448,7 +586,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid EST Manipulation encountered."); Penumbra.Log.Warning("Invalid EST Manipulation encountered.");
break; break;
} }
case MetaManipulation.Type.Gmp: case MetaManipulationType.Gmp:
{ {
var identifier = GmpIdentifier.FromJson(manip); var identifier = GmpIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<GmpEntry>(); var entry = manip["Entry"]?.ToObject<GmpEntry>();
@ -458,7 +596,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); Penumbra.Log.Warning("Invalid GMP Manipulation encountered.");
break; break;
} }
case MetaManipulation.Type.Rsp: case MetaManipulationType.Rsp:
{ {
var identifier = RspIdentifier.FromJson(manip); var identifier = RspIdentifier.FromJson(manip);
var entry = manip["Entry"]?.ToObject<RspEntry>(); var entry = manip["Entry"]?.ToObject<RspEntry>();
@ -468,7 +606,7 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); Penumbra.Log.Warning("Invalid RSP Manipulation encountered.");
break; break;
} }
case MetaManipulation.Type.GlobalEqp: case MetaManipulationType.GlobalEqp:
{ {
var identifier = GlobalEqpManipulation.FromJson(manip); var identifier = GlobalEqpManipulation.FromJson(manip);
if (identifier.HasValue) if (identifier.HasValue)
@ -483,4 +621,22 @@ public class MetaDictionary : IEnumerable<MetaManipulation>
return dict; 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); var ret = new RspIdentifier(subRace, attribute);
return ret.Validate() ? ret : null; return ret.Validate() ? ret : null;
} }
public MetaManipulationType Type
=> MetaManipulationType.Rsp;
} }
[JsonConverter(typeof(Converter))] [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, Dictionary<Utf8GamePath, FullPath> FileRedirections,
MetaDictionary Manipulations) MetaDictionary Manipulations)
{ {
public static readonly AppliedModData Empty = new([], []); public static readonly AppliedModData Empty = new([], new MetaDictionary());
} }
public interface IMod public interface IMod

View file

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

View file

@ -125,8 +125,8 @@ public static class EquipmentSwap
_ => (EstType)0, _ => (EstType)0,
}; };
var skipFemale = false; var skipFemale = false;
var skipMale = false; var skipMale = false;
foreach (var gr in Enum.GetValues<GenderRace>()) foreach (var gr in Enum.GetValues<GenderRace>())
{ {
switch (gr.Split().Item1) switch (gr.Split().Item1)
@ -242,8 +242,8 @@ public static class EquipmentSwap
private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom,
PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) PrimaryId idFrom, PrimaryId idTo, Variant variantFrom)
{ {
var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom);
var imc = new ImcFile(manager, entry.Identifier); var imc = new ImcFile(manager, ident);
EquipItem[] items; EquipItem[] items;
Variant[] variants; Variant[] variants;
if (idFrom == idTo) if (idFrom == idTo)
@ -273,7 +273,8 @@ public static class EquipmentSwap
var manipToIdentifier = new GmpIdentifier(idTo); var manipToIdentifier = new GmpIdentifier(idTo);
var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier);
var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); 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, 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) Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
{ {
var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom);
var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo);
var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); 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 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); var decal = CreateDecal(manager, redirections, imc.SwapToModdedEntry.DecalId);
if (decal != null) if (decal != null)
imc.ChildSwaps.Add(decal); 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) if (avfx != null)
imc.ChildSwaps.Add(avfx); imc.ChildSwaps.Add(avfx);
@ -316,19 +318,21 @@ public static class EquipmentSwap
// Example: Abyssos Helm / Body // 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) byte vfxId)
{ {
if (vfxId == 0) if (vfxId == 0)
return null; return null;
var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId);
var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom);
var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); 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()) 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); 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, public static FileSwap CreateTex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, char prefix, PrimaryId idFrom,
PrimaryId idTo, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged)
ref MtrlFile.Texture texture, ref bool dataWasChanged)
=> CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref 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, 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 addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path);
var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom);
newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); 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}"); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}");
if (newPath != path) if (newPath != path)
{ {
@ -421,11 +425,12 @@ public static class EquipmentSwap
return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path);
} }
public static FileSwap CreateAtex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, ref string filePath, public static FileSwap CreateAtex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slotFrom, EquipSlot slotTo,
ref bool dataWasChanged) PrimaryId idFrom, ref string filePath, ref bool dataWasChanged)
{ {
var oldPath = filePath; var oldPath = filePath;
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom);
dataWasChanged = true; dataWasChanged = true;
return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); 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, public static FileSwap CreatePhyb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstType type,
GenderRace race, EstEntry estEntry) 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); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath);
} }
public static FileSwap CreateSklb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstType type, public static FileSwap CreateSklb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstType type,
GenderRace race, EstEntry estEntry) 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); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath);
} }
@ -154,10 +154,11 @@ public static class ItemSwap
return null; return null;
var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace); var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace);
var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); var manipToIdentifier = new EstIdentifier(idTo, type, genderRace);
var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier);
var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); 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 est = new MetaSwap<EstIdentifier, EstEntry>(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault,
manipToIdentifier, manipToDefault);
if (ownMdl && est.SwapToModdedEntry.Value >= 2) if (ownMdl && est.SwapToModdedEntry.Value >= 2)
{ {
@ -215,6 +216,22 @@ public static class ItemSwap
? path.Replace($"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_") ? path.Replace($"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_")
: path; : 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) public static string ReplaceRace(string path, GenderRace from, GenderRace to, bool condition = true)
=> ReplaceId(path, 'c', (ushort)from, (ushort)to, condition); => 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); : p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p);
private MetaDictionary MetaResolver(ModCollection? collection) private MetaDictionary MetaResolver(ModCollection? collection)
=> collection?.MetaCache?.Manipulations is { } cache => collection?.MetaCache is { } cache
? [] // [.. cache] TODO ? new MetaDictionary(cache)
: _appliedModData.Manipulations; : _appliedModData.Manipulations;
public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, 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( Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName); deleteList.Add(file.FullName);
// TODO option.Manipulations.UnionWith(meta.MetaManipulations);
option.Manipulations.UnionWith([]);//[.. meta.MetaManipulations]);
} }
else if (ext1 == ".rgsp" || ext2 == ".rgsp") 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}"); $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName); deleteList.Add(file.FullName);
// TODO option.Manipulations.UnionWith(rgsp.MetaManipulations);
option.Manipulations.UnionWith([]);//[.. rgsp.MetaManipulations]);
} }
} }
catch (Exception e) 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> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public MetaDictionary Manipulations { get; set; } = []; public MetaDictionary Manipulations { get; set; } = new();
IMod IModDataContainer.Mod IMod IModDataContainer.Mod
=> 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> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { 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) public void AddDataTo(Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
=> SubMod.AddContainerTo(this, redirections, manipulations); => SubMod.AddContainerTo(this, redirections, manipulations);

View file

@ -93,8 +93,7 @@ public class TemporaryMod : IMod
} }
} }
// TODO var manips = new MetaDictionary(collection.MetaCache);
MetaDictionary manips = []; // [.. collection.MetaCache?.Manipulations ?? []];
defaultMod.Manipulations.UnionWith(manips); defaultMod.Manipulations.UnionWith(manips);
saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); 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; 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 : 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"; public override int NumColumns
private const string EquipSlotTooltip = "Equip Slot"; => 10;
private const string ModelRaceTooltip = "Model Race";
private const string GenderTooltip = "Gender"; private bool _fileExists;
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";
protected override void Initialize() protected override void Initialize()
{ {
@ -41,14 +35,14 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
protected override void DrawNew() protected override void DrawNew()
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
// Copy To Clipboard CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc));
ImGui.TableNextColumn(); 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; 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)) 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(); UpdateEntry();
using var disabled = ImRaii.Disabled(); 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) protected override void DrawEntry(ImcIdentifier identifier, ImcEntry entry)
{ {
const uint frameColor = 0; DrawMetaButtons(identifier, entry);
// Meta Buttons DrawIdentifier(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);
}
var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry;
if (DrawEntry(defaultEntry, ref entry, true)) 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(); ImGui.TableNextColumn();
var change = DrawObjectType(ref identifier); var change = DrawObjectType(ref identifier);
@ -121,6 +84,41 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
return change; 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) private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -142,7 +140,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() 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) public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110)
{ {
@ -270,7 +268,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
return true; return true;
} }
public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) private static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry)
{ {
var changes = false; var changes = false;
for (var i = 0; i < ImcEntry.NumAttributes; ++i) for (var i = 0; i < ImcEntry.NumAttributes; ++i)
@ -292,62 +290,4 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles)
return changes; 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 Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Text; using OtterGui.Text;
using OtterGui.Text.EndObjects; using Penumbra.Api.Api;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor; using Penumbra.UI.AdvancedWindow.Meta;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow public partial class ModEditWindow
{ {
private const string ModelSetIdTooltip = private readonly MetaDrawers _metaDrawers;
"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 void DrawMetaTab() private void DrawMetaTab()
{ {
@ -56,80 +47,42 @@ public partial class ModEditWindow
if (!child) if (!child)
return; return;
DrawEditHeader(MetaManipulation.Type.Eqp); DrawEditHeader(MetaManipulationType.Eqp);
DrawEditHeader(MetaManipulation.Type.Eqdp); DrawEditHeader(MetaManipulationType.Eqdp);
DrawEditHeader(MetaManipulation.Type.Imc); DrawEditHeader(MetaManipulationType.Imc);
DrawEditHeader(MetaManipulation.Type.Est); DrawEditHeader(MetaManipulationType.Est);
DrawEditHeader(MetaManipulation.Type.Gmp); DrawEditHeader(MetaManipulationType.Gmp);
DrawEditHeader(MetaManipulation.Type.Rsp); DrawEditHeader(MetaManipulationType.Rsp);
DrawEditHeader(MetaManipulation.Type.GlobalEqp); DrawEditHeader(MetaManipulationType.GlobalEqp);
} }
private static ReadOnlySpan<byte> Label(MetaManipulation.Type type) private void DrawEditHeader(MetaManipulationType 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)
{ {
var drawer = _metaDrawers.Get(type);
if (drawer == null)
return;
var oldPos = ImGui.GetCursorPosY(); 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()); DrawOtherOptionData(type, oldPos, ImGui.GetCursorPos());
if (!header) if (!header)
return; return;
DrawTable(type); DrawTable(drawer);
} }
private IMetaDrawer? Drawer(MetaManipulation.Type type) private static void DrawTable(IMetaDrawer drawer)
=> 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)
{ {
const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; 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) if (!table)
return; return;
if (Drawer(type) is not { } drawer)
return;
drawer.Draw(); drawer.Draw();
ImGui.NewLine(); 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]; var otherOptionData = _editor.MetaEditor.OtherData[type];
if (otherOptionData.TotalCount <= 0) if (otherOptionData.TotalCount <= 0)
@ -149,577 +102,12 @@ public partial class ModEditWindow
ImGui.SetCursorPos(newPos); 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) private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, MetaDictionary manipulations)
{ {
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true))
return; return;
var text = Functions.ToCompressedBase64(manipulations, MetaManipulation.CurrentVersion); var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion);
if (text.Length > 0) if (text.Length > 0)
ImGui.SetClipboardText(text); ImGui.SetClipboardText(text);
} }
@ -731,8 +119,11 @@ public partial class ModEditWindow
var clipboard = ImGuiUtil.GetClipboardText(); var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64<MetaDictionary>(clipboard, out var manips); 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.UpdateTo(manips);
_editor.MetaEditor.Changes = true;
}
} }
ImGuiUtil.HoverTooltip( ImGuiUtil.HoverTooltip(
@ -745,194 +136,14 @@ public partial class ModEditWindow
{ {
var clipboard = ImGuiUtil.GetClipboardText(); var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64<MetaDictionary>(clipboard, out var manips); 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.SetTo(manips);
_editor.MetaEditor.Changes = true;
}
} }
ImGuiUtil.HoverTooltip( ImGuiUtil.HoverTooltip(
"Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."); "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); task.ContinueWith(t => { GamePaths = FinalizeIo(t); }, TaskScheduler.Default);
} }
private EstManipulation[] GetCurrentEstManipulations() private KeyValuePair<EstIdentifier, EstEntry>[] GetCurrentEstManipulations()
{ {
var mod = _edit._editor.Mod; var mod = _edit._editor.Mod;
var option = _edit._editor.Option; var option = _edit._editor.Option;
@ -106,9 +106,7 @@ public partial class ModEditWindow
return mod.AllDataContainers return mod.AllDataContainers
.Where(subMod => subMod != option) .Where(subMod => subMod != option)
.Prepend(option) .Prepend(option)
.SelectMany(subMod => subMod.Manipulations) .SelectMany(subMod => subMod.Manipulations.Est)
.Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est)
.Select(manipulation => manipulation.Est)
.ToArray(); .ToArray();
} }

View file

@ -25,6 +25,7 @@ using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow.Meta;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.Util; using Penumbra.Util;
using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor;
@ -586,7 +587,7 @@ public partial class ModEditWindow : Window, IDisposable
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework,
CharacterBaseDestructor characterBaseDestructor) CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers)
: base(WindowBaseLabel) : base(WindowBaseLabel)
{ {
_performance = performance; _performance = performance;
@ -606,6 +607,7 @@ public partial class ModEditWindow : Window, IDisposable
_objects = objects; _objects = objects;
_framework = framework; _framework = framework;
_characterBaseDestructor = characterBaseDestructor; _characterBaseDestructor = characterBaseDestructor;
_metaDrawers = metaDrawers;
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", _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, () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); (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 var _ = data switch
{ {
Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0,
MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), IMetaIdentifier m => ImGui.Selectable(m.ToString()),
_ => false, _ => false,
}; };
} }
} }

View file

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

View file

@ -2,7 +2,6 @@ using OtterGui.Classes;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
@ -10,103 +9,14 @@ namespace Penumbra.Util;
public static class IdentifierExtensions 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, public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container,
IDictionary<string, object?> changedItems) IDictionary<string, object?> changedItems)
{ {
foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys))
identifier.Identify(changedItems, gamePath.ToString()); identifier.Identify(changedItems, gamePath.ToString());
foreach (var manip in container.Manipulations) foreach (var manip in container.Manipulations.Identifiers)
MetaChangedItems(identifier, changedItems, manip); manip.AddChangedItems(identifier, changedItems);
} }
public static void RemoveMachinistOffhands(this SortedList<string, object?> changedItems) public static void RemoveMachinistOffhands(this SortedList<string, object?> changedItems)