Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models.

This commit is contained in:
Ottermandias 2025-07-05 21:59:20 +02:00
parent c0aa2e36ea
commit a953febfba
2 changed files with 112 additions and 1 deletions

View file

@ -1,3 +1,4 @@
using System.Collections.Frozen;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
@ -5,6 +6,8 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop; using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta; namespace Penumbra.Meta;
@ -58,11 +61,72 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable
_ids[(int)_modelIndex] = model.GetModelId(_modelIndex); _ids[(int)_modelIndex] = model.GetModelId(_modelIndex);
CheckShapes(collection.MetaCache!.Shp); CheckShapes(collection.MetaCache!.Shp);
CheckAttributes(collection.MetaCache!.Atr); CheckAttributes(collection.MetaCache!.Atr);
if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears)
AccessoryImcCheck(model);
} }
UpdateDefaultMasks(model, collection.MetaCache!.Shp); 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) private void CheckAttributes(AtrCache attributeCache)
{ {
if (attributeCache.DisabledCount is 0) if (attributeCache.DisabledCount is 0)

View file

@ -234,9 +234,56 @@ public static class EquipmentSwap
mdl.ChildSwaps.Add(mtrl); mdl.ChildSwaps.Add(mtrl);
} }
FixAttributes(mdl, slotFrom, slotTo);
return mdl; return mdl;
} }
private static void FixAttributes(FileSwap swap, EquipSlot slotFrom, EquipSlot slotTo)
{
if (slotFrom == slotTo)
return;
var needle = slotTo switch
{
EquipSlot.Head => "atr_mv_",
EquipSlot.Ears => "atr_ev_",
EquipSlot.Neck => "atr_nv_",
EquipSlot.Wrists => "atr_wv_",
EquipSlot.RFinger or EquipSlot.LFinger => "atr_rv_",
_ => string.Empty,
};
var replacement = slotFrom switch
{
EquipSlot.Head => 'm',
EquipSlot.Ears => 'e',
EquipSlot.Neck => 'n',
EquipSlot.Wrists => 'w',
EquipSlot.RFinger or EquipSlot.LFinger => 'r',
_ => 'm',
};
var attributes = swap.AsMdl()!.Attributes;
for (var i = 0; i < attributes.Length; ++i)
{
if (FixAttribute(ref attributes[i], needle, replacement))
swap.DataWasChanged = true;
}
}
private static unsafe bool FixAttribute(ref string attribute, string from, char to)
{
if (!attribute.StartsWith(from) || attribute.Length != from.Length + 1 || attribute[^1] is < 'a' or > 'j')
return false;
Span<char> stack = stackalloc char[attribute.Length];
attribute.CopyTo(stack);
stack[4] = to;
attribute = new string(stack);
return true;
}
private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant) private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant)
{ {
slot = i.Type.ToSlot(); slot = i.Type.ToSlot();