diff --git a/Penumbra.GameData b/Penumbra.GameData index b15c0f07..bb3b462b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b15c0f07ba270a7b6a350411006e003da9818d1b +Subproject commit bb3b462bbc5bc2a598c1ad8c372b0cb255551fe1 diff --git a/Penumbra.sln b/Penumbra.sln index 642876ef..fbcd6080 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F schemas\structs\group_single.json = schemas\structs\group_single.json schemas\structs\manipulation.json = schemas\structs\manipulation.json schemas\structs\meta_atch.json = schemas\structs\meta_atch.json + schemas\structs\meta_atr.json = schemas\structs\meta_atr.json schemas\structs\meta_enums.json = schemas\structs\meta_enums.json schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json diff --git a/Penumbra/Collections/Cache/AtrCache.cs b/Penumbra/Collections/Cache/AtrCache.cs new file mode 100644 index 00000000..757ddaa2 --- /dev/null +++ b/Penumbra/Collections/Cache/AtrCache.cs @@ -0,0 +1,56 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.Contains(slot, id, genderRace); + + public int DisabledCount { get; private set; } + + internal IReadOnlyDictionary Data + => _atrData; + + private readonly Dictionary _atrData = []; + + public void Reset() + { + Clear(); + _atrData.Clear(); + } + + protected override void Dispose(bool _) + => Clear(); + + protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry) + { + if (!_atrData.TryGetValue(identifier.Attribute, out var value)) + { + if (entry.Value) + return; + + value = []; + _atrData.Add(identifier.Attribute, value); + } + + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, !entry.Value)) + ++DisabledCount; + } + + protected override void RevertModInternal(AtrIdentifier identifier) + { + if (!_atrData.TryGetValue(identifier.Attribute, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + { + --DisabledCount; + if (value.IsEmpty) + _atrData.Remove(identifier.Attribute); + } + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index c48a487c..8294624b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -247,6 +247,8 @@ public sealed class CollectionCache : IDisposable AddManipulation(mod, identifier, entry); foreach (var (identifier, entry) in files.Manipulations.Shp) AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Atr) + AddManipulation(mod, identifier, entry); foreach (var identifier in files.Manipulations.GlobalEqp) AddManipulation(mod, identifier, null!); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 790dd3af..011cdd23 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -17,11 +17,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly ImcCache Imc = new(manager, collection); public readonly AtchCache Atch = new(manager, collection); public readonly ShpCache Shp = new(manager, collection); + public readonly AtrCache Atr = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -32,6 +33,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -44,6 +46,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Imc.Reset(); Atch.Reset(); Shp.Reset(); + Atr.Reset(); GlobalEqp.Clear(); } @@ -61,6 +64,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Imc.Dispose(); Atch.Dispose(); Shp.Dispose(); + Atr.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -76,6 +80,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod), + AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -98,6 +103,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i => Rsp.RevertMod(i, out mod), AtchIdentifier i => Atch.RevertMod(i, out mod), ShpIdentifier i => Shp.RevertMod(i, out mod), + AtrIdentifier i => Atr.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -115,6 +121,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e), + AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs new file mode 100644 index 00000000..f1fc7127 --- /dev/null +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -0,0 +1,123 @@ +using System.Collections.Frozen; +using OtterGui.Extensions; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; + +namespace Penumbra.Collections.Cache; + +public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryId Id), ulong> +{ + public static readonly IReadOnlyList GenderRaceValues = + [ + GenderRace.Unknown, GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale, + GenderRace.ElezenMale, GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale, + GenderRace.RoegadynFemale, GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale, + GenderRace.HrothgarMale, GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale, + ]; + + public static readonly FrozenDictionary GenderRaceIndices = + GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index); + + private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + + public bool this[HumanSlot slot] + => slot is HumanSlot.Unknown ? All : _allIds[(int)slot * GenderRaceIndices.Count]; + + public bool this[GenderRace genderRace] + => GenderRaceIndices.TryGetValue(genderRace, out var index) + && _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]; + + public bool this[HumanSlot slot, GenderRace genderRace] + { + get + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; + + if (_allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]) + return true; + + return _allIds[(int)slot * GenderRaceIndices.Count + index]; + } + set + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return; + + var genderRaceCount = GenderRaceValues.Count; + if (slot is HumanSlot.Unknown) + _allIds[ShapeAttributeManager.ModelSlotSize * genderRaceCount + index] = value; + else + _allIds[(int)slot * genderRaceCount + index] = value; + } + } + + public bool All + => _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count]; + + public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) + => All || this[slot, genderRace] || ContainsEntry(slot, id, genderRace); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) + => GenderRaceIndices.TryGetValue(genderRace, out var index) + && TryGetValue((slot, id), out var flags) + && (flags & (1ul << index)) is not 0; + + public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; + + if (!id.HasValue) + { + var slotIndex = slot is HumanSlot.Unknown ? ShapeAttributeManager.ModelSlotSize : (int)slot; + var old = _allIds[slotIndex * GenderRaceIndices.Count + index]; + _allIds[slotIndex * GenderRaceIndices.Count + index] = value; + return old != value; + } + + if (value) + { + if (TryGetValue((slot, id.Value), out var flags)) + { + var newFlags = flags | (1ul << index); + if (newFlags == flags) + return false; + + this[(slot, id.Value)] = newFlags; + return true; + } + + this[(slot, id.Value)] = 1ul << index; + return true; + } + else if (TryGetValue((slot, id.Value), out var flags)) + { + var newFlags = flags & ~(1ul << index); + if (newFlags == flags) + return false; + + if (newFlags is 0) + { + Remove((slot, id.Value)); + return true; + } + + this[(slot, id.Value)] = newFlags; + return true; + } + + return false; + } + + public new void Clear() + { + base.Clear(); + _allIds.SetAll(false); + } + + public bool IsEmpty + => !_allIds.HasAnySet() && Count is 0; +} diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 2fe7f933..22547d25 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -7,10 +7,10 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) - => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); + public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id, genderRace); - internal IReadOnlyDictionary State(ShapeConnectorCondition connector) + internal IReadOnlyDictionary State(ShapeConnectorCondition connector) => connector switch { ShapeConnectorCondition.None => _shpData, @@ -22,73 +22,10 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) public int EnabledCount { get; private set; } - public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> - { - private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); - - public bool All - { - get => _allIds[^1]; - set => _allIds[^1] = value; - } - - public bool this[HumanSlot slot] - { - get - { - if (slot is HumanSlot.Unknown) - return All; - - return _allIds[(int)slot]; - } - set - { - if (slot is HumanSlot.Unknown) - _allIds[^1] = value; - else - _allIds[(int)slot] = value; - } - } - - public bool Contains(HumanSlot slot, PrimaryId id) - => All || this[slot] || Contains((slot, id)); - - public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value) - { - if (slot is HumanSlot.Unknown) - { - var old = All; - All = value.Value; - return old != value.Value; - } - - if (!id.HasValue) - { - var old = this[slot]; - this[slot] = value.Value; - return old != value.Value; - } - - if (value.Value) - return Add((slot, id.Value)); - - return Remove((slot, id.Value)); - } - - public new void Clear() - { - base.Clear(); - _allIds.SetAll(false); - } - - public bool IsEmpty - => !_allIds.HasAnySet() && Count is 0; - } - - private readonly Dictionary _shpData = []; - private readonly Dictionary _wristConnectors = []; - private readonly Dictionary _waistConnectors = []; - private readonly Dictionary _ankleConnectors = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary _wristConnectors = []; + private readonly Dictionary _waistConnectors = []; + private readonly Dictionary _ankleConnectors = []; public void Reset() { @@ -114,7 +51,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; - void Func(Dictionary dict) + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) { @@ -125,7 +62,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) dict.Add(identifier.Shape, value); } - if (value.TrySet(identifier.Slot, identifier.Id, entry)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value)) ++EnabledCount; } } @@ -142,12 +79,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; - void Func(Dictionary dict) + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) { --EnabledCount; if (value.IsEmpty) diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs index cad049ad..00e5851f 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs @@ -22,8 +22,8 @@ public sealed unsafe class AttributeHook : EventWrapper - ShapeManager = 0, + /// + ShapeAttributeManager = 0, } private readonly CollectionResolver _resolver; diff --git a/Penumbra/Meta/Manipulations/AtrIdentifier.cs b/Penumbra/Meta/Manipulations/AtrIdentifier.cs new file mode 100644 index 00000000..ca65f6aa --- /dev/null +++ b/Penumbra/Meta/Manipulations/AtrIdentifier.cs @@ -0,0 +1,145 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeAttributeString Attribute, GenderRace GenderRaceCondition) + : IComparable, IMetaIdentifier +{ + public int CompareTo(AtrIdentifier other) + { + var slotComparison = Slot.CompareTo(other.Slot); + if (slotComparison is not 0) + return slotComparison; + + if (Id.HasValue) + { + if (other.Id.HasValue) + { + var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id); + if (idComparison is not 0) + return idComparison; + } + else + { + return -1; + } + } + else if (other.Id.HasValue) + { + return 1; + } + + var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition); + if (genderRaceComparison is not 0) + return genderRaceComparison; + + return Attribute.CompareTo(other.Attribute); + } + + + public override string ToString() + { + var sb = new StringBuilder(64); + sb.Append("Shp - ") + .Append(Attribute); + if (Slot is HumanSlot.Unknown) + { + sb.Append(" - All Slots & IDs"); + } + else + { + sb.Append(" - ") + .Append(Slot.ToName()) + .Append(" - "); + if (Id.HasValue) + sb.Append(Id.Value.Id); + else + sb.Append("All IDs"); + } + + if (GenderRaceCondition is not GenderRace.Unknown) + sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode()); + + return sb.ToString(); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing for now since it depends entirely on the shape key. + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) + return false; + + if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition)) + return false; + + if (Slot is HumanSlot.Unknown && Id is not null) + return false; + + if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue }) + return false; + + if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) + return false; + + return Attribute.ValidateCustomAttributeString(); + } + + public JObject AddToJson(JObject jObj) + { + if (Slot is not HumanSlot.Unknown) + jObj["Slot"] = Slot.ToString(); + if (Id.HasValue) + jObj["Id"] = Id.Value.Id.ToString(); + jObj["Attribute"] = Attribute.ToString(); + if (GenderRaceCondition is not GenderRace.Unknown) + jObj["GenderRaceCondition"] = (uint)GenderRaceCondition; + return jObj; + } + + public static AtrIdentifier? FromJson(JObject jObj) + { + var attribute = jObj["Attribute"]?.ToObject(); + if (attribute is null || !ShapeAttributeString.TryRead(attribute, out var attributeString)) + return null; + + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject() ?? 0; + var identifier = new AtrIdentifier(slot, id, attributeString, genderRaceCondition); + return identifier.Validate() ? identifier : null; + } + + public MetaManipulationType Type + => MetaManipulationType.Atr; +} + +[JsonConverter(typeof(Converter))] +public readonly record struct AtrEntry(bool Value) +{ + public static readonly AtrEntry True = new(true); + public static readonly AtrEntry False = new(false); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, AtrEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override AtrEntry ReadJson(JsonReader reader, Type objectType, AtrEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 13feba51..922825c3 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -16,6 +16,7 @@ public enum MetaManipulationType : byte GlobalEqp = 7, Atch = 8, Shp = 9, + Atr = 10, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 51ca09ab..23eaec76 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -21,6 +21,7 @@ public class MetaDictionary public readonly Dictionary Gmp = []; public readonly Dictionary Atch = []; public readonly Dictionary Shp = []; + public readonly Dictionary Atr = []; public Wrapper() { } @@ -35,6 +36,7 @@ public class MetaDictionary Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Atr = cache.Atr.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); foreach (var geqp in cache.GlobalEqp.Keys) Add(geqp); } @@ -66,6 +68,9 @@ public class MetaDictionary public IReadOnlyDictionary Shp => _data?.Shp ?? []; + public IReadOnlyDictionary Atr + => _data?.Atr ?? []; + public IReadOnlySet GlobalEqp => _data ?? []; @@ -84,6 +89,7 @@ public class MetaDictionary MetaManipulationType.Rsp => _data.Rsp.Count, MetaManipulationType.Atch => _data.Atch.Count, MetaManipulationType.Shp => _data.Shp.Count, + MetaManipulationType.Atr => _data.Atr.Count, MetaManipulationType.GlobalEqp => _data.Count, _ => 0, }; @@ -100,6 +106,7 @@ public class MetaDictionary ImcIdentifier i => _data.Imc.ContainsKey(i), AtchIdentifier i => _data.Atch.ContainsKey(i), ShpIdentifier i => _data.Shp.ContainsKey(i), + AtrIdentifier i => _data.Atr.ContainsKey(i), RspIdentifier i => _data.Rsp.ContainsKey(i), _ => false, }; @@ -115,13 +122,13 @@ public class MetaDictionary if (_data is null) return; - if (_data.Count is 0 && Shp.Count is 0) + if (_data.Count is 0 && Shp.Count is 0 && Atr.Count is 0) { _data = null; Count = 0; } - Count = GlobalEqp.Count + Shp.Count; + Count = GlobalEqp.Count + Shp.Count + Atr.Count; _data!.Imc.Clear(); _data!.Eqp.Clear(); _data!.Eqdp.Clear(); @@ -147,6 +154,7 @@ public class MetaDictionary && _data.Gmp.SetEquals(other._data!.Gmp) && _data.Atch.SetEquals(other._data!.Atch) && _data.Shp.SetEquals(other._data!.Shp) + && _data.Atr.SetEquals(other._data!.Atr) && _data.SetEquals(other._data!); } @@ -161,6 +169,7 @@ public class MetaDictionary .Concat(_data!.Rsp.Keys.Cast()) .Concat(_data!.Atch.Keys.Cast()) .Concat(_data!.Shp.Keys.Cast()) + .Concat(_data!.Atr.Keys.Cast()) .Concat(_data!.Cast()); #region TryAdd @@ -251,6 +260,16 @@ public class MetaDictionary return true; } + public bool TryAdd(AtrIdentifier identifier, in AtrEntry entry) + { + _data ??= []; + if (!_data!.Atr.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { _data ??= []; @@ -343,6 +362,15 @@ public class MetaDictionary return true; } + public bool Update(AtrIdentifier identifier, in AtrEntry entry) + { + if (_data is null || !_data.Atr.ContainsKey(identifier)) + return false; + + _data.Atr[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -371,6 +399,9 @@ public class MetaDictionary public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) => _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value); + public bool TryGetValue(AtrIdentifier identifier, out AtrEntry value) + => _data?.Atr.TryGetValue(identifier, out value) ?? SetDefault(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static bool SetDefault(out T? value) { @@ -396,6 +427,7 @@ public class MetaDictionary RspIdentifier i => _data.Rsp.Remove(i), AtchIdentifier i => _data.Atch.Remove(i), ShpIdentifier i => _data.Shp.Remove(i), + AtrIdentifier i => _data.Atr.Remove(i), _ => false, }; if (ret && --Count is 0) @@ -436,6 +468,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._data!.Shp) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._data!.Atr) + TryAdd(identifier, entry); + foreach (var identifier in manips._data!) TryAdd(identifier); } @@ -498,6 +533,12 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._data!.Atr.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; @@ -526,6 +567,7 @@ public class MetaDictionary _data!.Gmp.SetTo(other._data!.Gmp); _data!.Atch.SetTo(other._data!.Atch); _data!.Shp.SetTo(other._data!.Shp); + _data!.Atr.SetTo(other._data!.Atr); _data!.SetTo(other._data!); Count = other.Count; } @@ -544,6 +586,7 @@ public class MetaDictionary _data!.Gmp.UpdateTo(other._data!.Gmp); _data!.Atch.UpdateTo(other._data!.Atch); _data!.Shp.UpdateTo(other._data!.Shp); + _data!.Atr.UpdateTo(other._data!.Atr); _data!.UnionWith(other._data!); Count = _data!.Imc.Count + _data!.Eqp.Count @@ -651,6 +694,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(AtrIdentifier identifier, AtrEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Atr.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -682,6 +735,8 @@ public class MetaDictionary return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry)) return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(AtrIdentifier) && typeof(TEntry) == typeof(AtrEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -730,6 +785,7 @@ public class MetaDictionary SerializeTo(array, value._data!.Gmp); SerializeTo(array, value._data!.Atch); SerializeTo(array, value._data!.Shp); + SerializeTo(array, value._data!.Atr); SerializeTo(array, value._data!); } @@ -839,6 +895,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid SHP Manipulation encountered."); break; } + case MetaManipulationType.Atr: + { + var identifier = AtrIdentifier.FromJson(manip); + var entry = new AtrEntry(manip["Entry"]?.Value() ?? true); + if (identifier.HasValue) + dict.TryAdd(identifier.Value, entry); + else + Penumbra.Log.Warning("Invalid ATR Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index 3be46d32..0a5b71b7 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -18,7 +19,12 @@ public enum ShapeConnectorCondition : byte Ankles = 3, } -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeConnectorCondition ConnectorCondition) +public readonly record struct ShpIdentifier( + HumanSlot Slot, + PrimaryId? Id, + ShapeAttributeString Shape, + ShapeConnectorCondition ConnectorCondition, + GenderRace GenderRaceCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -49,6 +55,10 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (conditionComparison is not 0) return conditionComparison; + var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition); + if (genderRaceComparison is not 0) + return genderRaceComparison; + return Shape.CompareTo(other.Shape); } @@ -80,6 +90,9 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break; } + if (GenderRaceCondition is not GenderRace.Unknown) + sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode()); + return sb.ToString(); } @@ -96,6 +109,12 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) return false; + if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition)) + return false; + + if (!Enum.IsDefined(ConnectorCondition)) + return false; + if (Slot is HumanSlot.Unknown && Id is not null) return false; @@ -105,10 +124,7 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) return false; - if (!ValidateCustomShapeString(Shape)) - return false; - - if (!Enum.IsDefined(ConnectorCondition)) + if (!Shape.ValidateCustomShapeString()) return false; return ConnectorCondition switch @@ -121,40 +137,6 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape }; } - public static unsafe bool ValidateCustomShapeString(byte* shape) - { - // "shpx_*" - if (shape is null) - return false; - - if (*shape++ is not (byte)'s' - || *shape++ is not (byte)'h' - || *shape++ is not (byte)'p' - || *shape++ is not (byte)'x' - || *shape++ is not (byte)'_' - || *shape is 0) - return false; - - return true; - } - - public static bool ValidateCustomShapeString(in ShapeString shape) - { - // "shpx_*" - if (shape.Length < 6) - return false; - - var span = shape.AsSpan; - if (span[0] is not (byte)'s' - || span[1] is not (byte)'h' - || span[2] is not (byte)'p' - || span[3] is not (byte)'x' - || span[4] is not (byte)'_') - return false; - - return true; - } - public JObject AddToJson(JObject jObj) { if (Slot is not HumanSlot.Unknown) @@ -164,19 +146,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape jObj["Shape"] = Shape.ToString(); if (ConnectorCondition is not ShapeConnectorCondition.None) jObj["ConnectorCondition"] = ConnectorCondition.ToString(); + if (GenderRaceCondition is not GenderRace.Unknown) + jObj["GenderRaceCondition"] = (uint)GenderRaceCondition; return jObj; } public static ShpIdentifier? FromJson(JObject jObj) { var shape = jObj["Shape"]?.ToObject(); - if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) + if (shape is null || !ShapeAttributeString.TryRead(shape, out var shapeString)) return null; - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); - var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; - var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; + var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject() ?? 0; + var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition, genderRaceCondition); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs new file mode 100644 index 00000000..c6800141 --- /dev/null +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -0,0 +1,153 @@ +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta; + +public unsafe class ShapeAttributeManager : IRequiredService, IDisposable +{ + public const int NumSlots = 14; + public const int ModelSlotSize = 18; + private readonly AttributeHook _attributeHook; + + public static ReadOnlySpan UsedModels + => + [ + HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, + HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, + ]; + + public ShapeAttributeManager(AttributeHook attributeHook) + { + _attributeHook = attributeHook; + _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeAttributeManager); + } + + private readonly Dictionary[] _temporaryShapes = + Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); + + private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; + + private HumanSlot _modelIndex; + private int _slotIndex; + private GenderRace _genderRace; + + private FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* _model; + + public void Dispose() + => _attributeHook.Unsubscribe(OnAttributeComputed); + + private void OnAttributeComputed(Actor actor, Model model, ModCollection collection) + { + if (!collection.HasCache) + return; + + _genderRace = (GenderRace)model.AsHuman->RaceSexId; + for (_slotIndex = 0; _slotIndex < NumSlots; ++_slotIndex) + { + _modelIndex = UsedModels[_slotIndex]; + _model = model.AsHuman->Models[_modelIndex.ToIndex()]; + if (_model is null || _model->ModelResourceHandle is null) + continue; + + _ids[(int)_modelIndex] = model.GetModelId(_modelIndex); + CheckShapes(collection.MetaCache!.Shp); + CheckAttributes(collection.MetaCache!.Atr); + } + + UpdateDefaultMasks(model, collection.MetaCache!.Shp); + } + + private void CheckAttributes(AtrCache attributeCache) + { + if (attributeCache.DisabledCount is 0) + return; + + ref var attributes = ref _model->ModelResourceHandle->Attributes; + foreach (var (attribute, index) in attributes.Where(kvp => ShapeAttributeString.ValidateCustomAttributeString(kvp.Key.Value))) + { + if (ShapeAttributeString.TryRead(attribute.Value, out var attributeString)) + { + // Mask out custom attributes if they are disabled. Attributes are enabled by default. + if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) + _model->EnabledAttributeIndexMask &= (ushort)~(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a attribute string that is too long: {attribute}."); + } + } + } + + private void CheckShapes(ShpCache shapeCache) + { + _temporaryShapes[_slotIndex].Clear(); + ref var shapes = ref _model->ModelResourceHandle->Shapes; + foreach (var (shape, index) in shapes.Where(kvp => ShapeAttributeString.ValidateCustomShapeString(kvp.Key.Value))) + { + if (ShapeAttributeString.TryRead(shape.Value, out var shapeString)) + { + _temporaryShapes[_slotIndex].TryAdd(shapeString, index); + // Add custom shapes if they are enabled. Shapes are disabled by default. + if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) + _model->EnabledShapeKeyIndexMask |= (ushort)(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); + } + } + } + + private void UpdateDefaultMasks(Model human, ShpCache cache) + { + foreach (var (shape, topIndex) in _temporaryShapes[1]) + { + if (shape.IsWrist() && _temporaryShapes[2].TryGetValue(shape, out var handIndex)) + { + human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; + human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); + } + + if (shape.IsWaist() && _temporaryShapes[3].TryGetValue(shape, out var legIndex)) + { + human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; + human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); + } + } + + foreach (var (shape, bottomIndex) in _temporaryShapes[3]) + { + if (shape.IsAnkle() && _temporaryShapes[4].TryGetValue(shape, out var footIndex)) + { + human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex; + human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); + } + } + + return; + + void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, + HumanSlot slot2, int idx1, int idx2) + { + if (dict.Count is 0) + return; + + foreach (var (shape, set) in dict) + { + if (set.Contains(slot1, _ids[idx1], GenderRace.Unknown) && _temporaryShapes[idx1].TryGetValue(shape, out var index1)) + human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1; + if (set.Contains(slot2, _ids[idx2], GenderRace.Unknown) && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) + human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2; + } + } + } +} diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeAttributeString.cs similarity index 55% rename from Penumbra/Meta/ShapeString.cs rename to Penumbra/Meta/ShapeAttributeString.cs index 95ca0933..55e3f021 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeAttributeString.cs @@ -6,11 +6,11 @@ using Penumbra.String.Functions; namespace Penumbra.Meta; [JsonConverter(typeof(Converter))] -public struct ShapeString : IEquatable, IComparable +public struct ShapeAttributeString : IEquatable, IComparable { public const int MaxLength = 30; - public static readonly ShapeString Empty = new(); + public static readonly ShapeAttributeString Empty = new(); private FixedString32 _buffer; @@ -37,6 +37,72 @@ public struct ShapeString : IEquatable, IComparable } } + public static unsafe bool ValidateCustomShapeString(byte* shape) + { + // "shpx_*" + if (shape is null) + return false; + + if (*shape++ is not (byte)'s' + || *shape++ is not (byte)'h' + || *shape++ is not (byte)'p' + || *shape++ is not (byte)'x' + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public bool ValidateCustomShapeString() + { + // "shpx_*" + if (Length < 6) + return false; + + if (_buffer[0] is not (byte)'s' + || _buffer[1] is not (byte)'h' + || _buffer[2] is not (byte)'p' + || _buffer[3] is not (byte)'x' + || _buffer[4] is not (byte)'_') + return false; + + return true; + } + + public static unsafe bool ValidateCustomAttributeString(byte* shape) + { + // "atrx_*" + if (shape is null) + return false; + + if (*shape++ is not (byte)'a' + || *shape++ is not (byte)'t' + || *shape++ is not (byte)'r' + || *shape++ is not (byte)'x' + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public bool ValidateCustomAttributeString() + { + // "atrx_*" + if (Length < 6) + return false; + + if (_buffer[0] is not (byte)'a' + || _buffer[1] is not (byte)'t' + || _buffer[2] is not (byte)'r' + || _buffer[3] is not (byte)'x' + || _buffer[4] is not (byte)'_') + return false; + + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool IsAnkle() => CheckCenter('a', 'n'); @@ -53,28 +119,28 @@ public struct ShapeString : IEquatable, IComparable private bool CheckCenter(char first, char second) => Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_'; - public bool Equals(ShapeString other) + public bool Equals(ShapeAttributeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); public override bool Equals(object? obj) - => obj is ShapeString other && Equals(other); + => obj is ShapeAttributeString other && Equals(other); public override int GetHashCode() => (int)Crc32.Get(_buffer[..Length]); - public static bool operator ==(ShapeString left, ShapeString right) + public static bool operator ==(ShapeAttributeString left, ShapeAttributeString right) => left.Equals(right); - public static bool operator !=(ShapeString left, ShapeString right) + public static bool operator !=(ShapeAttributeString left, ShapeAttributeString right) => !left.Equals(right); - public static unsafe bool TryRead(byte* pointer, out ShapeString ret) + public static unsafe bool TryRead(byte* pointer, out ShapeAttributeString ret) { var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer); return TryRead(span, out ret); } - public unsafe int CompareTo(ShapeString other) + public unsafe int CompareTo(ShapeAttributeString other) { fixed (void* lhs = &this) { @@ -82,7 +148,7 @@ public struct ShapeString : IEquatable, IComparable } } - public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) + public static bool TryRead(ReadOnlySpan utf8, out ShapeAttributeString ret) { if (utf8.Length is 0 or > MaxLength) { @@ -97,7 +163,7 @@ public struct ShapeString : IEquatable, IComparable return true; } - public static bool TryRead(ReadOnlySpan utf16, out ShapeString ret) + public static bool TryRead(ReadOnlySpan utf16, out ShapeAttributeString ret) { ret = Empty; if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written)) @@ -116,19 +182,20 @@ public struct ShapeString : IEquatable, IComparable _buffer[31] = length; } - private sealed class Converter : JsonConverter + private sealed class Converter : JsonConverter { - public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, ShapeAttributeString value, JsonSerializer serializer) { writer.WriteValue(value.ToString()); } - public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue, + public override ShapeAttributeString ReadJson(JsonReader reader, Type objectType, ShapeAttributeString existingValue, + bool hasExistingValue, JsonSerializer serializer) { var value = serializer.Deserialize(reader); if (!TryRead(value, out existingValue)) - throw new JsonReaderException($"Could not parse {value} into ShapeString."); + throw new JsonReaderException($"Could not parse {value} into ShapeAttributeString."); return existingValue; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs deleted file mode 100644 index abd4c3b8..00000000 --- a/Penumbra/Meta/ShapeManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.Collections.Cache; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.PostProcessing; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Meta; - -public class ShapeManager : IRequiredService, IDisposable -{ - public const int NumSlots = 14; - public const int ModelSlotSize = 18; - private readonly AttributeHook _attributeHook; - - public static ReadOnlySpan UsedModels - => - [ - HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, - HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, - ]; - - public ShapeManager(AttributeHook attributeHook) - { - _attributeHook = attributeHook; - _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager); - } - - private readonly Dictionary[] _temporaryIndices = - Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); - - private readonly uint[] _temporaryMasks = new uint[NumSlots]; - private readonly uint[] _temporaryValues = new uint[NumSlots]; - private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; - - public void Dispose() - => _attributeHook.Unsubscribe(OnAttributeComputed); - - private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection) - { - if (!collection.HasCache) - return; - - ComputeCache(model, collection.MetaCache!.Shp); - for (var i = 0; i < NumSlots; ++i) - { - if (_temporaryMasks[i] is 0) - continue; - - var modelIndex = UsedModels[i]; - var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask; - var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i]; - Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); - model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask; - } - } - - private unsafe void ComputeCache(Model human, ShpCache cache) - { - for (var i = 0; i < NumSlots; ++i) - { - _temporaryMasks[i] = 0; - _temporaryValues[i] = 0; - _temporaryIndices[i].Clear(); - - var modelIndex = UsedModels[i]; - var model = human.AsHuman->Models[modelIndex.ToIndex()]; - if (model is null || model->ModelResourceHandle is null) - continue; - - _ids[(int)modelIndex] = human.GetModelId(modelIndex); - - ref var shapes = ref model->ModelResourceHandle->Shapes; - foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) - { - if (ShapeString.TryRead(shape.Value, out var shapeString)) - { - _temporaryIndices[i].TryAdd(shapeString, index); - _temporaryMasks[i] |= (ushort)(1 << index); - if (cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) - _temporaryValues[i] |= (ushort)(1 << index); - } - else - { - Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); - } - } - } - - UpdateDefaultMasks(cache); - } - - private void UpdateDefaultMasks(ShpCache cache) - { - foreach (var (shape, topIndex) in _temporaryIndices[1]) - { - if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) - { - _temporaryValues[1] |= 1u << topIndex; - _temporaryValues[2] |= 1u << handIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); - } - - if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) - { - _temporaryValues[1] |= 1u << topIndex; - _temporaryValues[3] |= 1u << legIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); - } - } - - foreach (var (shape, bottomIndex) in _temporaryIndices[3]) - { - if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) - { - _temporaryValues[3] |= 1u << bottomIndex; - _temporaryValues[4] |= 1u << footIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); - } - } - - return; - - void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) - { - if (dict.Count is 0) - return; - - foreach (var (shape, set) in dict) - { - if (set.Contains(slot1, _ids[idx1]) && _temporaryIndices[idx1].TryGetValue(shape, out var index1)) - _temporaryValues[idx1] |= 1u << index1; - if (set.Contains(slot2, _ids[idx2]) && _temporaryIndices[idx2].TryGetValue(shape, out var index2)) - _temporaryValues[idx2] |= 1u << index2; - } - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs new file mode 100644 index 00000000..89fadfa8 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs @@ -0,0 +1,274 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Collections.Cache; +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 AtrMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Attributes(ATR)###ATR"u8; + + private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("atrx_"u8, out var s) ? s : ShapeAttributeString.Empty; + private bool _identifierValid; + + public override int NumColumns + => 7; + + public override float ColumnHeight + => ImUtf8.FrameHeightSpacing; + + protected override void Initialize() + { + Identifier = new AtrIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, GenderRace.Unknown); + Entry = AtrEntry.True; + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current ATR manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Atr))); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var tt = canAdd + ? "Stage this edit."u8 + : _identifierValid + ? "This entry does not contain a valid attribute."u8 + : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, AtrEntry.False); + + DrawIdentifierInput(ref Identifier); + DrawEntry(ref Entry, true); + } + + protected override void DrawEntry(AtrIdentifier identifier, AtrEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + if (DrawEntry(ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(AtrIdentifier, AtrEntry)> Enumerate() + => Editor.Atr + .OrderBy(kvp => kvp.Key.Attribute) + .ThenBy(kvp => kvp.Key.Slot) + .ThenBy(kvp => kvp.Key.Id) + .Select(kvp => (kvp.Key, kvp.Value)); + + protected override int Count + => Editor.Atr.Count; + + private bool DrawIdentifierInput(ref AtrIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawHumanSlot(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGenderRaceConditionInput(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawAttributeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + return changes; + } + + private static void DrawIdentifier(AtrIdentifier identifier) + { + ImGui.TableNextColumn(); + + ImUtf8.TextFramed(ShpMetaDrawer.SlotName(identifier.Slot), FrameColor); + ImUtf8.HoverTooltip("Model Slot"u8); + + ImGui.TableNextColumn(); + if (identifier.GenderRaceCondition is not GenderRace.Unknown) + { + ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor); + ImUtf8.HoverTooltip("Gender & Race Code for this attribute to be set."); + } + else + { + ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor); + } + + ImGui.TableNextColumn(); + if (identifier.Id.HasValue) + ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); + else + ImUtf8.TextFramed("All IDs"u8, FrameColor); + ImUtf8.HoverTooltip("Primary ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Attribute.AsSpan, FrameColor); + } + + private static bool DrawEntry(ref AtrEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var value = entry.Value; + var changes = ImUtf8.Checkbox("##atrEntry"u8, ref value); + if (changes) + entry = new AtrEntry(value); + ImUtf8.HoverTooltip("Whether to enable or disable this attribute for the selected items."); + return changes; + } + + public static bool DrawPrimaryId(ref AtrIdentifier identifier, float unscaledWidth = 100) + { + var allSlots = identifier.Slot is HumanSlot.Unknown; + var all = !identifier.Id.HasValue; + var ret = false; + using (ImRaii.Disabled(allSlots)) + { + if (ImUtf8.Checkbox("##atrAll"u8, ref all)) + { + identifier = identifier with { Id = all ? null : 0 }; + ret = true; + } + } + + ImUtf8.HoverTooltip(allSlots + ? "When using all slots, you also need to use all IDs."u8 + : "Enable this attribute for all model IDs."u8); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (all) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f)); + ImUtf8.TextFramed("All IDs"u8, ImGui.GetColorU32(ImGuiCol.FrameBg, all || allSlots ? ImGui.GetStyle().DisabledAlpha : 1f), + new Vector2(unscaledWidth, 0), ImGui.GetColorU32(ImGuiCol.TextDisabled)); + } + else + { + var max = identifier.Slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1; + if (IdInput("##atrPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, max, false)) + { + identifier = identifier with { Id = setId }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'e####' part of an item path or similar for customizations."u8); + + return ret; + } + + public bool DrawHumanSlot(ref AtrIdentifier identifier, float unscaledWidth = 150) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using (var combo = ImUtf8.Combo("##atrSlot"u8, ShpMetaDrawer.SlotName(identifier.Slot))) + { + if (combo) + foreach (var slot in ShpMetaDrawer.AvailableSlots) + { + if (!ImUtf8.Selectable(ShpMetaDrawer.SlotName(slot), slot == identifier.Slot) || slot == identifier.Slot) + continue; + + ret = true; + if (slot is HumanSlot.Unknown) + { + identifier = identifier with + { + Id = null, + Slot = slot, + }; + } + else + { + identifier = identifier with + { + Id = identifier.Id.HasValue + ? (PrimaryId)Math.Clamp(identifier.Id.Value.Id, 0, + slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1) + : null, + Slot = slot, + }; + ret = true; + } + } + } + + ImUtf8.HoverTooltip("Model Slot"u8); + return ret; + } + + private static bool DrawGenderRaceConditionInput(ref AtrIdentifier identifier, float unscaledWidth = 250) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + + using (var combo = ImUtf8.Combo("##shpGenderRace"u8, + identifier.GenderRaceCondition is GenderRace.Unknown + ? "Any Gender & Race" + : $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})")) + { + if (combo) + { + if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown) + && identifier.GenderRaceCondition is not GenderRace.Unknown) + { + identifier = identifier with { GenderRaceCondition = GenderRace.Unknown }; + ret = true; + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr) + && identifier.GenderRaceCondition != gr) + { + identifier = identifier with { GenderRaceCondition = gr }; + ret = true; + } + } + } + } + + ImUtf8.HoverTooltip( + "Only activate this attribute for this gender & race code."u8); + + return ret; + } + + public static unsafe bool DrawAttributeKeyInput(ref AtrIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid, + float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeAttributeString.MaxLength + 1); + using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##atrAttribute"u8, span, out int newLength, "Attribute..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = buffer.ValidateCustomAttributeString(); + if (valid) + identifier = identifier with { Attribute = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Supported attribute need to have the format `atrx_*` and a maximum length of 30 characters."u8); + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index 70b5f83b..792611e2 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -12,7 +12,8 @@ public class MetaDrawers( ImcMetaDrawer imc, RspMetaDrawer rsp, AtchMetaDrawer atch, - ShpMetaDrawer shp) : IService + ShpMetaDrawer shp, + AtrMetaDrawer atr) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -23,6 +24,7 @@ public class MetaDrawers( public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; public readonly AtchMetaDrawer Atch = atch; public readonly ShpMetaDrawer Shp = shp; + public readonly AtrMetaDrawer Atr = atr; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -35,6 +37,7 @@ public class MetaDrawers( MetaManipulationType.Rsp => Rsp, MetaManipulationType.Atch => Atch, MetaManipulationType.Shp => Shp, + MetaManipulationType.Atr => Atr, MetaManipulationType.GlobalEqp => GlobalEqp, _ => null, }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 6505ecc0..35c8ccec 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -20,18 +21,18 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; - private bool _identifierValid; + private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("shpx_"u8, out var s) ? s : ShapeAttributeString.Empty; + private bool _identifierValid; public override int NumColumns - => 7; + => 8; public override float ColumnHeight => ImUtf8.FrameHeightSpacing; protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeConnectorCondition.None); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, ShapeConnectorCondition.None, GenderRace.Unknown); } protected override void DrawNew() @@ -79,6 +80,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); var changes = DrawHumanSlot(ref identifier); + ImGui.TableNextColumn(); + changes |= DrawGenderRaceConditionInput(ref identifier); + ImGui.TableNextColumn(); changes |= DrawPrimaryId(ref identifier); @@ -97,6 +101,17 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(SlotName(identifier.Slot), FrameColor); ImUtf8.HoverTooltip("Model Slot"u8); + ImGui.TableNextColumn(); + if (identifier.GenderRaceCondition is not GenderRace.Unknown) + { + ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor); + ImUtf8.HoverTooltip("Gender & Race Code for this shape key to be set."); + } + else + { + ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor); + } + ImGui.TableNextColumn(); if (identifier.Id.HasValue) ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); @@ -165,7 +180,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 170) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -212,18 +227,19 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, float unscaledWidth = 150) + public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid, + float unscaledWidth = 200) { var ret = false; var ptr = Unsafe.AsPointer(ref buffer); - var span = new Span(ptr, ShapeString.MaxLength + 1); + var span = new Span(ptr, ShapeAttributeString.MaxLength + 1); using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) { ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); if (ImUtf8.InputText("##shpShape"u8, span, out int newLength, "Shape Key..."u8)) { buffer.ForceLength((byte)newLength); - valid = ShpIdentifier.ValidateCustomShapeString(buffer); + valid = buffer.ValidateCustomShapeString(); if (valid) identifier = identifier with { Shape = buffer }; ret = true; @@ -234,7 +250,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 150) + private static bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 80) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -271,7 +287,43 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - private static ReadOnlySpan AvailableSlots + private static bool DrawGenderRaceConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 250) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + + using (var combo = ImUtf8.Combo("##shpGenderRace"u8, identifier.GenderRaceCondition is GenderRace.Unknown + ? "Any Gender & Race" + : $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})")) + { + if (combo) + { + if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown) + && identifier.GenderRaceCondition is not GenderRace.Unknown) + { + identifier = identifier with { GenderRaceCondition = GenderRace.Unknown }; + ret = true; + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr) + && identifier.GenderRaceCondition != gr) + { + identifier = identifier with { GenderRaceCondition = gr }; + ret = true; + } + } + } + } + + ImUtf8.HoverTooltip( + "Only activate this shape key for this gender & race code."u8); + + return ret; + } + + public static ReadOnlySpan AvailableSlots => [ HumanSlot.Unknown, @@ -291,7 +343,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile HumanSlot.Ear, ]; - private static ReadOnlySpan SlotName(HumanSlot slot) + public static ReadOnlySpan SlotName(HumanSlot slot) => slot switch { HumanSlot.Unknown => "All Slots"u8, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 70a15373..3f19da5e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -62,6 +62,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Rsp); DrawEditHeader(MetaManipulationType.Atch); DrawEditHeader(MetaManipulationType.Shp); + DrawEditHeader(MetaManipulationType.Atr); DrawEditHeader(MetaManipulationType.GlobalEqp); } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 109cb5c4..9290e52d 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -35,6 +35,27 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) DrawCollectionShapeCache(actor); DrawCharacterShapes(human); + DrawCollectionAttributeCache(actor); + DrawCharacterAttributes(human); + } + + private unsafe void DrawCollectionAttributeCache(Actor actor) + { + var data = resolver.IdentifyCollection(actor.AsObject, true); + using var treeNode1 = ImUtf8.TreeNode($"Collection Attribute Cache ({data.ModCollection})"); + if (!treeNode1.Success || !data.ModCollection.HasCache) + return; + + using var table = ImUtf8.Table("##aCache"u8, 2, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("Attribute"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Disabled"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data) + DrawShapeAttribute(attribute, set); } private unsafe void DrawCollectionShapeCache(Actor actor) @@ -44,7 +65,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode1.Success || !data.ModCollection.HasCache) return; - using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##sCache"u8, 3, ImGuiTableFlags.RowBg); if (!table) return; @@ -58,14 +79,14 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) { ImUtf8.DrawTableColumn(condition.ToString()); - DrawShape(shape, set); + DrawShapeAttribute(shape, set); } } } - private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set) + private static void DrawShapeAttribute(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) { - ImUtf8.DrawTableColumn(shape.AsSpan); + ImUtf8.DrawTableColumn(shapeAttribute.AsSpan); if (set.All) { ImUtf8.DrawTableColumn("All"u8); @@ -73,7 +94,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) else { ImGui.TableNextColumn(); - foreach (var slot in ShapeManager.UsedModels) + foreach (var slot in ShapeAttributeManager.UsedModels) { if (!set[slot]) continue; @@ -82,10 +103,52 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImGui.SameLine(0, 0); } - foreach (var item in set.Where(i => !set[i.Slot])) + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) { - ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); - ImGui.SameLine(0, 0); + if (set[gr]) + { + ImUtf8.Text($"All {gr.ToName()}, "); + ImGui.SameLine(0, 0); + } + else + { + foreach (var slot in ShapeAttributeManager.UsedModels) + { + if (!set[slot, gr]) + continue; + + ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + } + } + + + foreach (var ((slot, id), flags) in set) + { + if ((flags & 1ul) is not 0) + { + if (set[slot]) + continue; + + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + else + { + var currentFlags = flags >> 1; + var currentIndex = BitOperations.TrailingZeroCount(currentFlags); + while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) + { + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr]) + continue; + + ImUtf8.Text($"{gr.ToName()} {slot.ToName()} {id.Id:D4}, "); + currentFlags >>= currentIndex; + currentIndex = BitOperations.TrailingZeroCount(currentFlags); + } + } } } } @@ -96,7 +159,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##shapes"u8, 5, ImGuiTableFlags.RowBg); if (!table) return; @@ -140,4 +203,55 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) } } } + + private unsafe void DrawCharacterAttributes(Model human) + { + using var treeNode2 = ImUtf8.TreeNode("Character Model Attributes"u8); + if (!treeNode2) + return; + + using var table = ImUtf8.Table("##attributes"u8, 5, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + for (var i = 0; i < human.AsHuman->SlotCount; ++i) + { + ImUtf8.DrawTableColumn($"{(uint)i:D2}"); + ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[i]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledAttributeIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(attribute.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if (idx % 8 < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } + } } diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json index 55fc5cad..81f2cef3 100644 --- a/schemas/structs/manipulation.json +++ b/schemas/structs/manipulation.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "Type": { - "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp" ] + "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp", "Atr" ] }, "Manipulation": { "type": "object" @@ -100,6 +100,16 @@ "$ref": "meta_shp.json" } } + }, + { + "properties": { + "Type": { + "const": "Atr" + }, + "Manipulation": { + "$ref": "meta_atr.json" + } + } } ] } diff --git a/schemas/structs/meta_atr.json b/schemas/structs/meta_atr.json new file mode 100644 index 00000000..479d4127 --- /dev/null +++ b/schemas/structs/meta_atr.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "boolean" + }, + "Slot": { + "$ref": "meta_enums.json#HumanSlot" + }, + "Id": { + "$ref": "meta_enums.json#U16" + }, + "Attribute": { + "type": "string", + "minLength": 5, + "maxLength": 30, + "pattern": "^atrx_" + }, + "GenderRaceCondition": { + "enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ] + } + }, + "required": [ + "Attribute" + ] +} diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index 851842a4..cb7fd0ec 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -19,6 +19,9 @@ }, "ConnectorCondition": { "$ref": "meta_enums.json#ShapeConnectorCondition" + }, + "GenderRaceCondition": { + "enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ] } }, "required": [