From a953febfba522579b338d2c8015e3dcfd6315168 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Jul 2025 21:59:20 +0200 Subject: [PATCH] Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models. --- Penumbra/Meta/ShapeAttributeManager.cs | 64 +++++++++++++++++++++++++ Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 49 ++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index a742806f..16901741 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -5,6 +6,8 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; @@ -58,11 +61,72 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable _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 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 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) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 5c67df52..216b5841 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -234,9 +234,56 @@ public static class EquipmentSwap mdl.ChildSwaps.Add(mtrl); } + FixAttributes(mdl, slotFrom, slotTo); + 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 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) { slot = i.Type.ToSlot(); @@ -399,7 +446,7 @@ public static class EquipmentSwap return null; var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo); - var pathTo = $"{folderTo}{fileName}"; + var pathTo = $"{folderTo}{fileName}"; var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo); var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom);