Make attributes and shapes completely toggleable.

This commit is contained in:
Ottermandias 2025-06-01 13:04:01 +02:00
parent 75f4e66dbf
commit b48c4f440a
6 changed files with 245 additions and 156 deletions

View file

@ -8,10 +8,12 @@ namespace Penumbra.Collections.Cache;
public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtrIdentifier, AtrEntry>(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<ShapeAttributeString, ShapeAttributeHashSet> 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);
}

View file

@ -19,93 +19,126 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI
public static readonly FrozenDictionary<GenderRace, int> 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,
};
}
}

View file

@ -8,7 +8,10 @@ namespace Penumbra.Collections.Cache;
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(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<ShapeAttributeString, ShapeAttributeHashSet> 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<ShapeAttributeString, ShapeAttributeHashSet> _shpData = [];
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _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);
}

View file

@ -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<ShapeAttributeString, ShapeAttributeHashSet> dict, HumanSlot slot1,
void CheckCondition(IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> 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;
}
}

View file

@ -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)

View file

@ -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<ShapeConnectorCondition>())
{
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;
}
}
}