diff --git a/Penumbra/Collections/Cache/AtrCache.cs b/Penumbra/Collections/Cache/AtrCache.cs index 757ddaa2..b017da32 100644 --- a/Penumbra/Collections/Cache/AtrCache.cs +++ b/Penumbra/Collections/Cache/AtrCache.cs @@ -8,10 +8,12 @@ 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); + => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.CheckEntry(slot, id, genderRace) is false; + public int EnabledCount { get; private set; } public int DisabledCount { get; private set; } + internal IReadOnlyDictionary Data => _atrData; @@ -21,24 +23,28 @@ public sealed class AtrCache(MetaFileManager manager, ModCollection collection) { Clear(); _atrData.Clear(); + DisabledCount = 0; + EnabledCount = 0; } protected override void Dispose(bool _) - => Clear(); + => Reset(); 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; + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _)) + { + if (entry.Value) + ++EnabledCount; + else + ++DisabledCount; + } } protected override void RevertModInternal(AtrIdentifier identifier) @@ -46,9 +52,12 @@ public sealed class AtrCache(MetaFileManager manager, ModCollection collection) if (!_atrData.TryGetValue(identifier.Attribute, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which)) { - --DisabledCount; + if (which) + --EnabledCount; + else + --DisabledCount; if (value.IsEmpty) _atrData.Remove(identifier.Attribute); } diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index 9670928f..e50ceaa2 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -19,93 +19,126 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI public static readonly FrozenDictionary GenderRaceIndices = GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index); - private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + private readonly BitArray _allIds = new(2 * (ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + + public bool? this[HumanSlot slot] + => AllCheck(ToIndex(slot, 0)); + + public bool? this[GenderRace genderRace] + => ToIndex(HumanSlot.Unknown, genderRace, out var index) ? AllCheck(index) : null; + + public bool? this[HumanSlot slot, GenderRace genderRace] + => ToIndex(slot, genderRace, out var index) ? AllCheck(index) : null; + + public bool? All + => Convert(_allIds[2 * AllIndex], _allIds[2 * AllIndex + 1]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private bool CheckGroups(HumanSlot slot, GenderRace genderRace) - { - if (All || this[slot]) - return true; - - if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) - return false; - - if (_allIds[ToIndex(HumanSlot.Unknown, index)]) - return true; - - return _allIds[ToIndex(slot, index)]; - } - - public bool this[HumanSlot slot] - => _allIds[ToIndex(slot, 0)]; - - public bool this[GenderRace genderRace] - => ToIndex(HumanSlot.Unknown, genderRace, out var index) && _allIds[index]; - - public bool this[HumanSlot slot, GenderRace genderRace] - => ToIndex(slot, genderRace, out var index) && _allIds[index]; - - public bool All - => _allIds[AllIndex]; + private bool? AllCheck(int idx) + => Convert(_allIds[idx], _allIds[idx + 1]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static int ToIndex(HumanSlot slot, int genderRaceIndex) - => slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count; + => 2 * (slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count); - public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) - => CheckGroups(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) is not 0 || (flags & (1ul << index)) is not 0); - - public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public bool? CheckEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return null; + + // Check for specific ID. + if (TryGetValue((slot, id), out var flags)) + { + // Check completely specified entry. + if (Convert(flags, 2 * index) is { } specified) + return specified; + + // Check any gender / race. + if (Convert(flags, 0) is { } anyGr) + return anyGr; + } + + // Check for specified gender / race and slot, but no ID. + if (AllCheck(ToIndex(slot, index)) is { } noIdButGr) + return noIdButGr; + + // Check for specified gender / race but no slot or ID. + if (AllCheck(ToIndex(HumanSlot.Unknown, index)) is { } noSlotButGr) + return noSlotButGr; + + // Check for specified slot but no gender / race or ID. + if (AllCheck(ToIndex(slot, 0)) is { } noGrButSlot) + return noGrButSlot; + + return All; + } + + public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool? value, out bool which) + { + which = false; if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) return false; if (!id.HasValue) { var slotIndex = ToIndex(slot, index); - var old = _allIds[slotIndex]; - _allIds[slotIndex] = value; - return old != value; - } - - if (value) - { - if (TryGetValue((slot, id.Value), out var flags)) + var ret = false; + if (value is true) { - var newFlags = flags | (1ul << index); - if (newFlags == flags) - return false; + if (!_allIds[slotIndex]) + ret = true; + _allIds[slotIndex] = true; + _allIds[slotIndex + 1] = false; + } + else if (value is false) + { + if (!_allIds[slotIndex + 1]) + ret = true; + _allIds[slotIndex] = false; + _allIds[slotIndex + 1] = true; + } + else + { + if (_allIds[slotIndex]) + { + which = true; + ret = true; + } + else if (_allIds[slotIndex + 1]) + { + which = false; + ret = true; + } - this[(slot, id.Value)] = newFlags; - return true; + _allIds[slotIndex] = false; + _allIds[slotIndex + 1] = false; } - this[(slot, id.Value)] = 1ul << index; - return true; + return ret; } - else if (TryGetValue((slot, id.Value), out var flags)) + + if (TryGetValue((slot, id.Value), out var flags)) { - var newFlags = flags & ~(1ul << index); + var newFlags = value switch + { + true => (flags | (1ul << index)) & ~(1ul << (index + 1)), + false => (flags & ~(1ul << index)) | (1ul << (index + 1)), + _ => flags & ~(1ul << index) & ~(1ul << (index + 1)), + }; if (newFlags == flags) return false; - if (newFlags is 0) - { - Remove((slot, id.Value)); - return true; - } - this[(slot, id.Value)] = newFlags; + which = (flags & (1ul << index)) is not 0; return true; } - return false; + if (value is null) + return false; + + this[(slot, id.Value)] = 1ul << (index + (value.Value ? 0 : 1)); + return true; } public new void Clear() @@ -128,4 +161,20 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI index = ToIndex(slot, index); return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool? Convert(bool trueValue, bool falseValue) + => trueValue ? true : falseValue ? false : null; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool? Convert(ulong mask, int idx) + { + mask >>= idx; + return (mask & 3) switch + { + 1 => true, + 2 => false, + _ => null, + }; + } } diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 22547d25..d8c3a036 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -8,7 +8,10 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { 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); + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is true; + + public bool ShouldBeDisabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => DisabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is false; internal IReadOnlyDictionary State(ShapeConnectorCondition connector) => connector switch @@ -20,7 +23,8 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) _ => [], }; - public int EnabledCount { get; private set; } + public int EnabledCount { get; private set; } + public int DisabledCount { get; private set; } private readonly Dictionary _shpData = []; private readonly Dictionary _wristConnectors = []; @@ -34,10 +38,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) _wristConnectors.Clear(); _waistConnectors.Clear(); _ankleConnectors.Clear(); + EnabledCount = 0; + DisabledCount = 0; } protected override void Dispose(bool _) - => Clear(); + => Reset(); protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { @@ -55,15 +61,17 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) { if (!dict.TryGetValue(identifier.Shape, out var value)) { - if (!entry.Value) - return; - value = []; dict.Add(identifier.Shape, value); } - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value)) - ++EnabledCount; + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _)) + { + if (entry.Value) + ++EnabledCount; + else + ++DisabledCount; + } } } @@ -84,9 +92,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which)) { - --EnabledCount; + if (which) + --EnabledCount; + else + --DisabledCount; if (value.IsEmpty) dict.Remove(identifier.Shape); } diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index e9c9c169..a742806f 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -106,46 +106,59 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable private void UpdateDefaultMasks(Model human, ShpCache cache) { + var genderRace = (GenderRace)human.AsHuman->RaceSexId; foreach (var (shape, topIndex) in _temporaryShapes[1]) { - if (shape.IsWrist() && _temporaryShapes[2].TryGetValue(shape, out var handIndex)) + if (shape.IsWrist() + && _temporaryShapes[2].TryGetValue(shape, out var handIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Hands, _ids[2], genderRace) + && human.AsHuman->Models[1] is not null + && human.AsHuman->Models[2] is not null) { 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); + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), genderRace, HumanSlot.Body, HumanSlot.Hands, 1, 2); } - if (shape.IsWaist() && _temporaryShapes[3].TryGetValue(shape, out var legIndex)) + if (shape.IsWaist() + && _temporaryShapes[3].TryGetValue(shape, out var legIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace) + && human.AsHuman->Models[1] is not null + && human.AsHuman->Models[3] is not null) { 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); + CheckCondition(cache.State(ShapeConnectorCondition.Waist), genderRace, HumanSlot.Body, HumanSlot.Legs, 1, 3); } } foreach (var (shape, bottomIndex) in _temporaryShapes[3]) { - if (shape.IsAnkle() && _temporaryShapes[4].TryGetValue(shape, out var footIndex)) + if (shape.IsAnkle() + && _temporaryShapes[4].TryGetValue(shape, out var footIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Feet, _ids[4], genderRace) + && human.AsHuman->Models[3] is not null + && human.AsHuman->Models[4] is not null) { 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); + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), genderRace, HumanSlot.Legs, HumanSlot.Feet, 3, 4); } } return; - void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, + void CheckCondition(IReadOnlyDictionary dict, GenderRace genderRace, 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)) + if (set.CheckEntry(slot1, _ids[idx1], genderRace) is true && _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)) + if (set.CheckEntry(slot2, _ids[idx2], genderRace) is true && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2; } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 53fa0b33..64d370b5 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -17,12 +17,12 @@ namespace Penumbra.UI; public sealed class ConfigWindow : Window, IUiService { private readonly IDalamudPluginInterface _pluginInterface; - private readonly Configuration _config; - private readonly PerformanceTracker _tracker; - private readonly ValidityChecker _validityChecker; - private Penumbra? _penumbra; - private ConfigTabBar _configTabs = null!; - private string? _lastException; + private readonly Configuration _config; + private readonly PerformanceTracker _tracker; + private readonly ValidityChecker _validityChecker; + private Penumbra? _penumbra; + private ConfigTabBar _configTabs = null!; + private string? _lastException; public ConfigWindow(PerformanceTracker tracker, IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index fd37bf35..2de78c66 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -52,11 +52,14 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) return; ImUtf8.TableSetupColumn("Attribute"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("Disabled"u8, ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data) - DrawShapeAttribute(attribute, set); + foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data.OrderBy(a => a.Key)) + { + ImUtf8.DrawTableColumn(attribute.AsSpan); + DrawValues(attribute, set); + } } private unsafe void DrawCollectionShapeCache(Actor actor) @@ -72,83 +75,87 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Condition"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); foreach (var condition in Enum.GetValues()) { - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition).OrderBy(shp => shp.Key)) { ImUtf8.DrawTableColumn(condition.ToString()); - DrawShapeAttribute(shape, set); + ImUtf8.DrawTableColumn(shape.AsSpan); + DrawValues(shape, set); } } } - private static void DrawShapeAttribute(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) + private static void DrawValues(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) { - ImUtf8.DrawTableColumn(shapeAttribute.AsSpan); - if (set.All) - { - ImUtf8.DrawTableColumn("All"u8); - } - else - { - ImGui.TableNextColumn(); - foreach (var slot in ShapeAttributeManager.UsedModels) - { - if (!set[slot]) - continue; + ImGui.TableNextColumn(); - ImUtf8.Text($"All {slot.ToName()}, "); + if (set.All is { } value) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value); + ImUtf8.Text("All"u8); + ImGui.SameLine(0, 0); + } + + foreach (var slot in ShapeAttributeManager.UsedModels) + { + if (set[slot] is not { } value2) + continue; + + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value2); + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (set[gr] is { } value3) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value3); + ImUtf8.Text($"All {gr.ToName()}, "); ImGui.SameLine(0, 0); } - - foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + else { - if (set[gr]) + foreach (var slot in ShapeAttributeManager.UsedModels) { - 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]) + if (set[slot, gr] is not { } value4) continue; - ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value4); + ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); 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); + foreach (var ((slot, id), flags) in set) + { + if ((flags & 3) is not 0) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), (flags & 2) is not 0); + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + else + { + var currentFlags = flags >> 2; + var currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; + while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) + { + var value5 = (currentFlags & 1) is 1; + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr] != value5) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value5); + ImUtf8.Text($"{gr.ToName()} {slot.ToName()} #{id.Id:D4}, "); } + + currentFlags >>= currentIndex * 2; + currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; } } }