Penumbra/Penumbra/Meta/ShapeAttributeManager.cs
2025-08-08 00:45:24 +02:00

227 lines
9.2 KiB
C#

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<HumanSlot> 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<ShapeAttributeString, short>[] _temporaryShapes =
Enumerable.Range(0, NumSlots).Select(_ => new Dictionary<ShapeAttributeString, short>()).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);
if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears)
AccessoryImcCheck(model);
}
UpdateDefaultMasks(model, collection.MetaCache!.Shp);
}
private void AccessoryImcCheck(Model model)
{
var imcMask = (ushort)(0x03FF & *(ushort*)(model.Address + 0xAAC + 6 * (int)_modelIndex));
Span<byte> attr =
[
(byte)'a',
(byte)'t',
(byte)'r',
(byte)'_',
AccessoryByte(_modelIndex),
(byte)'v',
(byte)'_',
(byte)'a',
0,
];
for (var i = 1; i < 10; ++i)
{
var flag = (ushort)(1 << i);
if ((imcMask & flag) is not 0)
continue;
attr[^2] = (byte)('a' + i);
foreach (var (attribute, index) in _model->ModelResourceHandle->Attributes)
{
if (!EqualAttribute(attr, attribute.Value))
continue;
_model->EnabledAttributeIndexMask &= ~(1u << index);
break;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
private static bool EqualAttribute(Span<byte> needle, byte* haystack)
{
foreach (var character in needle)
{
if (*haystack++ != character)
return false;
}
return true;
}
private static byte AccessoryByte(HumanSlot slot)
=> slot switch
{
HumanSlot.Head => (byte)'m',
HumanSlot.Ears => (byte)'e',
HumanSlot.Neck => (byte)'n',
HumanSlot.Wrists => (byte)'w',
HumanSlot.RFinger => (byte)'r',
HumanSlot.LFinger => (byte)'r',
_ => 0,
};
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 &= ~(1u << 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 |= 1u << index;
}
else
{
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
}
}
}
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)
&& !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), genderRace, HumanSlot.Body, HumanSlot.Hands, 1, 2);
}
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), genderRace, HumanSlot.Body, HumanSlot.Legs, 1, 3);
}
}
foreach (var (shape, bottomIndex) in _temporaryShapes[3])
{
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), genderRace, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
}
}
return;
void CheckCondition(IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> dict, GenderRace genderRace, HumanSlot slot1,
HumanSlot slot2, int idx1, int idx2)
{
foreach (var (shape, set) in dict)
{
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.CheckEntry(slot2, _ids[idx2], genderRace) is true && _temporaryShapes[idx2].TryGetValue(shape, out var index2))
human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2;
}
}
}
}