diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 2e90052d..eaf949d9 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -13,7 +13,23 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) internal IReadOnlyDictionary State => _shpData; - internal sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> + internal IEnumerable<(ShapeString, IReadOnlyDictionary)> ConditionState + => _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary)kvp.Value)); + + public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary? dict) + { + if (_conditionalSet.TryGetValue(condition, out var d)) + { + dict = d; + return true; + } + + dict = null; + return false; + } + + + public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> { private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); @@ -76,12 +92,14 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) => !_allIds.HasAnySet() && Count is 0; } - private readonly Dictionary _shpData = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary> _conditionalSet = []; public void Reset() { Clear(); _shpData.Clear(); + _conditionalSet.Clear(); } protected override void Dispose(bool _) @@ -89,21 +107,62 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) + if (identifier.ShapeCondition.Length > 0) { - value = []; - _shpData.Add(identifier.Shape, value); + if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) + { + if (!entry.Value) + return; + + shapes = new Dictionary(); + _conditionalSet.Add(identifier.ShapeCondition, shapes); + } + + Func(shapes); + } + else + { + Func(_shpData); } - value.TrySet(identifier.Slot, identifier.Id, entry); + void Func(Dictionary dict) + { + if (!dict.TryGetValue(identifier.Shape, out var value)) + { + if (!entry.Value) + return; + + value = []; + dict.Add(identifier.Shape, value); + } + + value.TrySet(identifier.Slot, identifier.Id, entry); + } } protected override void RevertModInternal(ShpIdentifier identifier) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) - return; + if (identifier.ShapeCondition.Length > 0) + { + if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) + return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) - _shpData.Remove(identifier.Shape); + Func(shapes); + } + else + { + Func(_shpData); + } + + return; + + void Func(Dictionary dict) + { + if (!_shpData.TryGetValue(identifier.Shape, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + _shpData.Remove(identifier.Shape); + } } } diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index c642167f..777be512 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -7,7 +7,7 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape) +public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -34,12 +34,39 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape return 1; } + var shapeComparison = Shape.CompareTo(other.Shape); + if (shapeComparison is not 0) + return shapeComparison; - return Shape.CompareTo(other.Shape); + return ShapeCondition.CompareTo(other.ShapeCondition); } + public override string ToString() - => $"Shp - {Shape}{(Slot is HumanSlot.Unknown ? " - All Slots & IDs" : $" - {Slot.ToName()}{(Id.HasValue ? $" - {Id.Value.Id}" : " - All IDs")}")}"; + { + var sb = new StringBuilder(64); + sb.Append("Shp - ") + .Append(Shape); + 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 (ShapeCondition.Length > 0) + sb.Append(" - ") + .Append(ShapeCondition); + return sb.ToString(); + } public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { @@ -57,7 +84,24 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Slot is HumanSlot.Unknown && Id is not null) return false; - return ValidateCustomShapeString(Shape); + if (!ValidateCustomShapeString(Shape)) + return false; + + if (ShapeCondition.Length is 0) + return true; + + if (!ValidateCustomShapeString(ShapeCondition)) + return false; + + return Slot switch + { + HumanSlot.Hands when ShapeCondition.IsWrist() => true, + HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true, + HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, + HumanSlot.Feet when ShapeCondition.IsAnkle() => true, + HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, + _ => false, + }; } public static unsafe bool ValidateCustomShapeString(byte* shape) @@ -101,18 +145,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id.HasValue) jObj["Id"] = Id.Value.Id.ToString(); jObj["Shape"] = Shape.ToString(); + if (ShapeCondition.Length > 0) + jObj["ShapeCondition"] = ShapeCondition.ToString(); return jObj; } public static ShpIdentifier? FromJson(JObject jObj) { - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); var shape = jObj["Shape"]?.ToObject(); if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) return null; - var identifier = new ShpIdentifier(slot, id, shapeString); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var shapeCondition = jObj["ShapeCondition"]?.ToObject(); + var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s; + var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index dc3e1a1c..57f6f23f 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -1,8 +1,10 @@ using System.Reflection.Metadata.Ecma335; 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; @@ -30,15 +32,19 @@ public class ShapeManager : IRequiredService, IDisposable 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 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) { - ComputeCache(model, collection); + if (!collection.HasCache) + return; + + ComputeCache(model, collection.MetaCache!.Shp); for (var i = 0; i < NumSlots; ++i) { if (_temporaryMasks[i] is 0) @@ -52,11 +58,8 @@ public class ShapeManager : IRequiredService, IDisposable } } - private unsafe void ComputeCache(Model human, ModCollection collection) + private unsafe void ComputeCache(Model human, ShpCache cache) { - if (!collection.HasCache) - return; - for (var i = 0; i < NumSlots; ++i) { _temporaryMasks[i] = 0; @@ -68,6 +71,8 @@ public class ShapeManager : IRequiredService, IDisposable if (model is null || model->ModelResourceHandle is null) continue; + _ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set; + ref var shapes = ref model->ModelResourceHandle->Shapes; foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) { @@ -75,8 +80,8 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryIndices[i].TryAdd(shapeString, index); _temporaryMasks[i] |= (ushort)(1 << index); - if (collection.MetaCache!.Shp.State.Count > 0 - && collection.MetaCache!.Shp.ShouldBeEnabled(shapeString, modelIndex, human.GetArmorChanged(modelIndex).Set)) + if (cache.State.Count > 0 + && cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) _temporaryValues[i] |= (ushort)(1 << index); } else @@ -86,37 +91,54 @@ public class ShapeManager : IRequiredService, IDisposable } } - UpdateDefaultMasks(); + UpdateDefaultMasks(cache); } - private void UpdateDefaultMasks() + private void UpdateDefaultMasks(ShpCache cache) { foreach (var (shape, topIndex) in _temporaryIndices[1]) { - if (CheckCenter(shape, 'w', 'r') && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) + if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[2] |= 1u << handIndex; + CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2); } - if (CheckCenter(shape, 'w', 'a') && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) + if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[3] |= 1u << legIndex; + CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3); } } foreach (var (shape, bottomIndex) in _temporaryIndices[3]) { - if (CheckCenter(shape, 'a', 'n') && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) + if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) { _temporaryValues[3] |= 1u << bottomIndex; _temporaryValues[4] |= 1u << footIndex; + CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4); + } + } + + return; + + void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) + { + if (!cache.CheckConditionState(shape, out var dict)) + return; + + foreach (var (subShape, set) in dict) + { + if (set.Contains(slot1, _ids[idx1])) + if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex)) + _temporaryValues[idx1] |= 1u << subIndex; + if (set.Contains(slot2, _ids[idx2])) + if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex)) + _temporaryValues[idx2] |= 1u << subIndex; } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static bool CheckCenter(in ShapeString shape, char first, char second) - => shape.Length > 8 && shape[4] == first && shape[5] == second && shape[6] is (byte)'_'; } diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs index 5b6f9c52..95ca0933 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeString.cs @@ -37,6 +37,22 @@ public struct ShapeString : IEquatable, IComparable } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsAnkle() + => CheckCenter('a', 'n'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsWaist() + => CheckCenter('w', 'a'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsWrist() + => CheckCenter('w', 'r'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool CheckCenter(char first, char second) + => Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_'; + public bool Equals(ShapeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 2c99af02..fe7e743c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -19,18 +19,20 @@ 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 ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; + private ShapeString _conditionBuffer = ShapeString.Empty; private bool _identifierValid; + private bool _conditionValid = true; public override int NumColumns - => 6; + => 7; public override float ColumnHeight => ImUtf8.FrameHeightSpacing; protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeString.Empty); } protected override void DrawNew() @@ -40,7 +42,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile new Lazy(() => MetaDictionary.SerializeTo([], Editor.Shp))); ImGui.TableNextColumn(); - var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var canAdd = !Editor.Contains(Identifier) && _identifierValid && _conditionValid; var tt = canAdd ? "Stage this edit."u8 : _identifierValid @@ -67,6 +69,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .OrderBy(kvp => kvp.Key.Shape) .ThenBy(kvp => kvp.Key.Slot) .ThenBy(kvp => kvp.Key.Id) + .ThenBy(kvp => kvp.Key.ShapeCondition) .Select(kvp => (kvp.Key, kvp.Value)); protected override int Count @@ -82,6 +85,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + + ImGui.TableNextColumn(); + changes |= DrawShapeConditionInput(ref identifier, ref _conditionBuffer, ref _conditionValid); return changes; } @@ -101,6 +107,13 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor); + + ImGui.TableNextColumn(); + if (identifier.ShapeCondition.Length > 0) + { + ImUtf8.TextFramed(identifier.ShapeCondition.AsSpan, FrameColor); + ImUtf8.HoverTooltip("Connector condition for this shape to be activated."); + } } private static bool DrawEntry(ref ShpEntry entry, bool disabled) @@ -154,7 +167,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -168,13 +181,37 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ret = true; if (slot is HumanSlot.Unknown) + { identifier = identifier with { Id = null, Slot = slot, }; + } else - identifier = identifier with { Slot = slot }; + { + if (_conditionBuffer.Length > 0 + && (_conditionBuffer.IsAnkle() && slot is not HumanSlot.Feet and not HumanSlot.Legs + || _conditionBuffer.IsWrist() && slot is not HumanSlot.Hands and not HumanSlot.Body + || _conditionBuffer.IsWaist() && slot is not HumanSlot.Body and not HumanSlot.Legs)) + { + identifier = identifier with + { + Slot = slot, + ShapeCondition = ShapeString.Empty, + }; + _conditionValid = false; + } + else + { + identifier = identifier with + { + Slot = slot, + ShapeCondition = _conditionBuffer, + }; + _conditionValid = true; + } + } } } @@ -204,6 +241,33 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } + public static unsafe bool DrawShapeConditionInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, + float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeString.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("##shpCondition"u8, span, out int newLength, "Shape Condition..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = ShpIdentifier.ValidateCustomShapeString(buffer) + && (buffer.IsAnkle() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Feet or HumanSlot.Legs + || buffer.IsWaist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Legs + || buffer.IsWrist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Hands); + if (valid) + identifier = identifier with { ShapeCondition = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip( + "Supported conditional shape keys need to have the format `shpx_an_*` (Legs or Feet), `shpx_wr_*` (Body or Hands), or `shpx_wa_*` (Body or Legs) and a maximum length of 30 characters."u8); + return ret; + } + private static ReadOnlySpan AvailableSlots => [ diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 5292bd17..8439587c 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; @@ -12,9 +13,9 @@ namespace Penumbra.UI.Tabs.Debug; public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService { - private int _objectIndex = 0; + private int _objectIndex; - public unsafe void Draw() + public void Draw() { ImUtf8.InputScalar("Object Index"u8, ref _objectIndex); var actor = objects[0]; @@ -31,93 +32,117 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) return; } - var data = resolver.IdentifyCollection(actor.AsObject, true); - using (var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})")) + DrawCollectionShapeCache(actor); + DrawCharacterShapes(human); + } + + private unsafe void DrawCollectionShapeCache(Actor actor) + { + var data = resolver.IdentifyCollection(actor.AsObject, true); + using var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})"); + if (!treeNode1.Success || !data.ModCollection.HasCache) + return; + + using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("Condition"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) { - if (treeNode1.Success && data.ModCollection.HasCache) - { - using var table = ImUtf8.Table("##cacheTable"u8, 2, ImGuiTableFlags.RowBg); - if (!table) - return; - - ImUtf8.TableSetupColumn("shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("enabled"u8, ImGuiTableColumnFlags.WidthStretch); - - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) - { - ImUtf8.DrawTableColumn(shape.AsSpan); - if (set.All) - { - ImUtf8.DrawTableColumn("All"u8); - } - else - { - ImGui.TableNextColumn(); - foreach (var slot in ShapeManager.UsedModels) - { - if (!set[slot]) - continue; - - ImUtf8.Text($"All {slot.ToName()}, "); - ImGui.SameLine(0, 0); - } - - foreach (var item in set.Where(i => !set[i.Slot])) - { - ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); - ImGui.SameLine(0, 0); - } - } - } - } + ImGui.TableNextColumn(); + DrawShape(shape, set); } - using (var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8)) + foreach (var (condition, dict) in data.ModCollection.MetaCache!.Shp.ConditionState) { - if (treeNode2) + foreach (var (shape, set) in dict) { - using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); - if (!table) - return; + ImUtf8.DrawTableColumn(condition.AsSpan); + DrawShape(shape, set); + } + } + } - ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("name"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); - ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); - ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set) + { + ImUtf8.DrawTableColumn(shape.AsSpan); + if (set.All) + { + ImUtf8.DrawTableColumn("All"u8); + } + else + { + ImGui.TableNextColumn(); + foreach (var slot in ShapeManager.UsedModels) + { + if (!set[slot]) + continue; - var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); - for (var i = 0; i < human.AsHuman->SlotCount; ++i) + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var item in set.Where(i => !set[i.Slot])) + { + ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + } + } + + private unsafe void DrawCharacterShapes(Model human) + { + using var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8); + if (!treeNode2) + return; + + using var table = ImUtf8.Table("##table"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("Shapes"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->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) { - 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->EnabledShapeKeyIndexMask; - ImUtf8.DrawTableColumn($"{mask:X8}"); - ImGui.TableNextColumn(); - foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) - { - var disabled = (mask & (1u << idx)) is 0; - using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); - ImUtf8.Text(shape.AsSpan()); - ImGui.SameLine(0, 0); - ImUtf8.Text(", "u8); - if (idx % 8 < 7) - ImGui.SameLine(0, 0); - } - } - else - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - } + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.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/meta_shp.json b/schemas/structs/meta_shp.json index 197f3104..4f868a0a 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -16,6 +16,12 @@ "minLength": 5, "maxLength": 30, "pattern": "^shpx_" + }, + "ShapeCondition": { + "type": "string", + "minLength": 8, + "maxLength": 30, + "pattern": "^shpx_(wa|an|wr)_" } }, "required": [