From c849e310343b465d88619f05e9b30384d1caa709 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 19:48:42 +0200 Subject: [PATCH 1/3] RT: Use SpanTextWriter to assemble paths --- .../ResolveContext.PathResolution.cs | 28 +++++++++++++------ .../Interop/ResourceTree/ResolveContext.cs | 25 +++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 85b3284a..b99468f8 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Text.HelperObjects; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -8,6 +9,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.String; using Penumbra.String.Classes; using static Penumbra.Interop.Structs.StructExtensions; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -95,7 +97,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -125,7 +127,7 @@ internal partial record ResolveContext fileName.CopyTo(mirroredFileName); WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); @@ -144,7 +146,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -175,13 +177,21 @@ internal partial record ResolveContext var baseDirectory = modelPath[..modelPosition]; - baseDirectory.CopyTo(materialPathBuffer); - "/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]); - WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant); - materialPathBuffer[baseDirectory.Length + 15] = (byte)'/'; - mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]); + var writer = new SpanTextWriter(materialPathBuffer); + writer.Append(baseDirectory); + writer.Append("/material/v"u8); + WriteZeroPaddedNumber(ref writer, 4, variant); + writer.Append((byte)'/'); + writer.Append(mtrlFileName); + writer.EnsureNullTerminated(); - return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)]; + return materialPathBuffer[..writer.Position]; + } + + private static void WriteZeroPaddedNumber(ref SpanTextWriter writer, int width, ushort number) + { + WriteZeroPaddedNumber(writer.GetRemainingSpan()[..width], number); + writer.Advance(width); } private static void WriteZeroPaddedNumber(Span destination, ushort number) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..29e15055 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,9 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using OtterGui; +using OtterGui.Text.HelperObjects; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Data; @@ -16,7 +16,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; using static Penumbra.Interop.Structs.StructExtensions; -using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; namespace Penumbra.Interop.ResourceTree; @@ -29,25 +29,25 @@ internal record GlobalResolveContext( { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu, + public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu, EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } internal unsafe partial record ResolveContext( GlobalResolveContext Global, - Pointer CharacterBasePointer, + Pointer CharacterBasePointer, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { - public CharacterBase* CharacterBase + public CharaBase* CharacterBase => CharacterBasePointer.Value; private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private ModelType ModelType + private CharaBase.ModelType ModelType => CharacterBase->GetModelType(); private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) @@ -75,11 +75,14 @@ internal unsafe partial record ResolveContext( if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3) return null; - Span prefixed = stackalloc byte[260]; - gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); - prefixed[lastDirectorySeparator + 1] = (byte)'-'; - prefixed[lastDirectorySeparator + 2] = (byte)'-'; - gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); + Span prefixed = stackalloc byte[CharaBase.PathBufferSize]; + + var writer = new SpanTextWriter(prefixed); + writer.Append(gamePath.Span[..(lastDirectorySeparator + 1)]); + writer.Append((byte)'-'); + writer.Append((byte)'-'); + writer.Append(gamePath.Span[(lastDirectorySeparator + 1)..]); + writer.EnsureNullTerminated(); if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp)) return null; From 243593e30f74c43a14b7d0ccdd9a264830158a59 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 19:52:39 +0200 Subject: [PATCH 2/3] RT: Fix VPR offhand material paths --- .../ResolveContext.PathResolution.cs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b99468f8..c3894b05 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -111,31 +111,25 @@ internal partial record ResolveContext if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; - // MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand - if (setIdHigh is 3 or 16 or 18 or 26) + // Some offhands share materials with the corresponding mainhand + if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId)) { - var setIdLow = Equipment.Set.Id % 100; - if (setIdLow > 50) - { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - var mirroredSetId = (ushort)(Equipment.Set.Id - 50); + Span mirroredFileName = stackalloc byte[32]; + mirroredFileName = mirroredFileName[..fileName.Length]; + fileName.CopyTo(mirroredFileName); + WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId.Id); - Span mirroredFileName = stackalloc byte[32]; - mirroredFileName = mirroredFileName[..fileName.Length]; - fileName.CopyTo(mirroredFileName); - WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); - Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); + var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); + if (weaponPosition >= 0) + WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId.Id); - var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); - if (weaponPosition >= 0) - WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); - - return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; - } + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); From 75e3ef72f3dbaff37db0ba18d2770a5e7885f3ae Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 20:27:16 +0200 Subject: [PATCH 3/3] RT: Fix Facewear --- .../ResolveContext.PathResolution.cs | 16 ++++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 30 ++++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index c3894b05..43324516 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -16,20 +16,26 @@ namespace Penumbra.Interop.ResourceTree; internal partial record ResolveContext { + private static bool IsEquipmentOrAccessorySlot(uint slotIndex) + => slotIndex is < 10 or 16 or 17; + + private static bool IsEquipmentSlot(uint slotIndex) + => slotIndex is < 5 or 16 or 17; + private Utf8GamePath ResolveModelPath() { // Correctness: // Resolving a model path through the game's code can use EQDP metadata for human equipment models. return ModelType switch { - ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(), - _ => ResolveModelPathNative(), + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) => ResolveEquipmentModelPath(), + _ => ResolveModelPathNative(), }; } private Utf8GamePath ResolveEquipmentModelPath() { - var path = SlotIndex < 5 + var path = IsEquipmentSlot(SlotIndex) ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; @@ -41,7 +47,7 @@ internal partial record ResolveContext private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { var slotIndex = slot.ToIndex(); - if (slotIndex >= 10 || ModelType != ModelType.Human) + if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human) return GenderRace.MidlanderMale; var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId; @@ -82,7 +88,7 @@ internal partial record ResolveContext // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. return ModelType switch { - ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b' + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 6663fb40..f1507294 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -9,6 +9,7 @@ using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; +using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -44,8 +45,8 @@ public class ResourceTree PlayerRelated = playerRelated; CollectionName = collectionName; AnonymizedCollectionName = anonymizedCollectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Nodes = []; + FlatNodes = []; } public void ProcessPostfix(Action action) @@ -59,13 +60,13 @@ public class ResourceTree var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; var modelType = model->GetModelType(); - var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; + var human = modelType == ModelType.Human ? (Human*)model : null; var equipment = modelType switch { - CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan( + ModelType.Human => new ReadOnlySpan(&human->Head, 12), + ModelType.DemiHuman => new ReadOnlySpan( Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), - _ => ReadOnlySpan.Empty, + _ => [], }; ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; @@ -75,9 +76,18 @@ public class ResourceTree for (var i = 0u; i < model->SlotCount; ++i) { - var slotContext = i < equipment.Length - ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) - : globalContext.CreateContext(model, i); + var slotContext = modelType switch + { + ModelType.Human => i switch + { + < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]), + 16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]), + _ => globalContext.CreateContext(model, i), + }, + _ => i < equipment.Length + ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + : globalContext.CreateContext(model, i), + }; var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); @@ -117,7 +127,7 @@ public class ResourceTree var subObject = (CharacterBase*)baseSubObject; - if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) + if (subObject->GetModelType() != ModelType.Weapon) continue; var weapon = (Weapon*)subObject;