diff --git a/Penumbra.Api b/Penumbra.Api index 79ffdd69..34921fd2 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 79ffdd69a28141a1ac93daa24d76573b2fa0d71e +Subproject commit 34921fd2c5a9aff5d34aef664bdb78331e8b9436 diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a7f6d86..c0c7eb0d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a7f6d86c9975a4892f58be3c629b7664e6c3733 +Subproject commit c0c7eb0dedb32ea83b019626abba041e90a95319 diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index bd560fd8..8aab0e0e 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper - SkinFixer = 0, + /// + ShaderReplacementFixer = 0, } } diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index df315094..d3ca87dc 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -214,10 +214,18 @@ public class MeshExporter var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count); shapeNames.Add(shape.ShapeName); - foreach (var shapeValue in shapeValues) + foreach (var (shapeValue, shapeValueIndex) in shapeValues.WithIndex()) { + var gltfIndex = gltfIndices[shapeValue.BaseIndicesIndex - indexBase]; + + if (gltfIndex == -1) + { + _notifier.Warning($"{name}: Shape {shape.ShapeName} mapping {shapeValueIndex} targets a degenerate triangle, ignoring."); + continue; + } + morphBuilder.SetVertex( - primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(), + primitiveVertices[gltfIndex].GetGeometry(), vertices[shapeValue.ReplacingVertexIndex].GetGeometry() ); } diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 8ab55734..1d4b223d 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -80,7 +80,6 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) StartIndex = 0, IndexCount = (uint)_indices.Count, - // TODO: import material names MaterialIndex = 0, SubMeshIndex = 0, SubMeshCount = (ushort)_subMeshes.Count, @@ -167,7 +166,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // And finally, merge in the sub-mesh struct itself. _subMeshes.Add(subMesh.SubMeshStruct with { - IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset), + IndexOffset = (uint)(subMesh.SubMeshStruct.IndexOffset + indexOffset), AttributeIndexMask = Utility.GetMergedAttributeMask( subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes), }); diff --git a/Penumbra/Import/Models/Import/Utility.cs b/Penumbra/Import/Models/Import/Utility.cs index a1e44136..21655563 100644 --- a/Penumbra/Import/Models/Import/Utility.cs +++ b/Penumbra/Import/Models/Import/Utility.cs @@ -1,4 +1,5 @@ using Lumina.Data.Parsing; +using Penumbra.GameData.Files; namespace Penumbra.Import.Models.Import; @@ -43,15 +44,15 @@ public static class Utility throw notifier.Exception( $""" All sub-meshes of a mesh must have equivalent vertex declarations. - Current: {FormatVertexDeclaration(current)} - New: {FormatVertexDeclaration(@new)} + Current: {FormatVertexDeclaration(current)} + New: {FormatVertexDeclaration(@new)} """ ); } private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration) => string.Join(", ", - vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); + vertexDeclaration.VertexElements.Select(element => $"{(MdlFile.VertexUsage)element.Usage} ({(MdlFile.VertexType)element.Type}@{element.Stream}:{element.Offset})")); private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) { diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 31387101..5ddb7eaa 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -13,8 +13,8 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr SubfileHelper, - /// - SkinFixer, + /// + ShaderReplacementFixer, } public ResourceHandleDestructor(HookManager hooks) diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs new file mode 100644 index 00000000..6a3bf776 --- /dev/null +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -0,0 +1,71 @@ +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData; + +namespace Penumbra.Interop.Services; + +// TODO ClientStructs-ify (https://github.com/aers/FFXIVClientStructs/pull/817) +public unsafe class ModelRenderer : IDisposable +{ + // Will be Manager.Instance()->ModelRenderer.CharacterGlassShaderPackage in CS + private const nint ModelRendererOffset = 0x13660; + private const nint CharacterGlassShaderPackageOffset = 0xD0; + + /// A static pointer to the Render::Manager address. + [Signature(Sigs.RenderManager, ScanType = ScanType.StaticAddress)] + private readonly nint* _renderManagerAddress = null; + + public bool Ready { get; private set; } + + public ShaderPackageResourceHandle** CharacterGlassShaderPackage + => *_renderManagerAddress == 0 + ? null + : (ShaderPackageResourceHandle**)(*_renderManagerAddress + ModelRendererOffset + CharacterGlassShaderPackageOffset).ToPointer(); + + public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; } + + private readonly IFramework _framework; + + public ModelRenderer(IFramework framework, IGameInteropProvider interop) + { + interop.InitializeFromAttributes(this); + _framework = framework; + LoadDefaultResources(null!); + if (!Ready) + _framework.Update += LoadDefaultResources; + } + + /// We store the default data of the resources so we can always restore them. + private void LoadDefaultResources(object _) + { + if (*_renderManagerAddress == 0) + return; + + var anyMissing = false; + + if (DefaultCharacterGlassShaderPackage == null) + { + DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; + anyMissing |= DefaultCharacterGlassShaderPackage == null; + } + + if (anyMissing) + return; + + Ready = true; + _framework.Update -= LoadDefaultResources; + } + + /// Return all relevant resources to the default resource. + public void ResetAll() + { + if (!Ready) + return; + + *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + } + + public void Dispose() + => ResetAll(); +} diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs new file mode 100644 index 00000000..e57fe313 --- /dev/null +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -0,0 +1,197 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Classes; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.Interop.Hooks.Resources; +using Penumbra.Services; + +namespace Penumbra.Interop.Services; + +public sealed unsafe class ShaderReplacementFixer : IDisposable +{ + public static ReadOnlySpan SkinShpkName + => "skin.shpk"u8; + + public static ReadOnlySpan CharacterGlassShpkName + => "characterglass.shpk"u8; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + private delegate nint CharacterBaseOnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); + private delegate nint ModelRendererOnRenderMaterialDelegate(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex); + + [StructLayout(LayoutKind.Explicit)] + private struct OnRenderMaterialParams + { + [FieldOffset(0x0)] + public Model* Model; + + [FieldOffset(0x8)] + public uint MaterialIndex; + } + + private readonly Hook _humanOnRenderMaterialHook; + + [Signature(Sigs.ModelRendererOnRenderMaterial, DetourName = nameof(ModelRendererOnRenderMaterialDetour))] + private readonly Hook _modelRendererOnRenderMaterialHook = null!; + + private readonly ResourceHandleDestructor _resourceHandleDestructor; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _utility; + private readonly ModelRenderer _modelRenderer; + + // MaterialResourceHandle set + private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); + private readonly ConcurrentSet _moddedCharacterGlassShpkMaterials = new(); + + private readonly object _skinLock = new(); + private readonly object _characterGlassLock = new(); + + // ConcurrentDictionary.Count uses a lock in its current implementation. + private int _moddedSkinShpkCount; + private int _moddedCharacterGlassShpkCount; + private ulong _skinSlowPathCallDelta; + private ulong _characterGlassSlowPathCallDelta; + + public bool Enabled { get; internal set; } = true; + + public int ModdedSkinShpkCount + => _moddedSkinShpkCount; + + public int ModdedCharacterGlassShpkCount + => _moddedCharacterGlassShpkCount; + + public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, + CommunicatorService communicator, IGameInteropProvider interop) + { + interop.InitializeFromAttributes(this); + _resourceHandleDestructor = resourceHandleDestructor; + _utility = utility; + _modelRenderer = modelRenderer; + _communicator = communicator; + _humanOnRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); + _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); + _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); + _humanOnRenderMaterialHook.Enable(); + _modelRendererOnRenderMaterialHook.Enable(); + } + + public void Dispose() + { + _modelRendererOnRenderMaterialHook.Dispose(); + _humanOnRenderMaterialHook.Dispose(); + _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); + _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); + _moddedCharacterGlassShpkMaterials.Clear(); + _moddedSkinShpkMaterials.Clear(); + _moddedCharacterGlassShpkCount = 0; + _moddedSkinShpkCount = 0; + } + + public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() + => (Interlocked.Exchange(ref _skinSlowPathCallDelta, 0), Interlocked.Exchange(ref _characterGlassSlowPathCallDelta, 0)); + + private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan shpkName) + { + if (mtrlResource == null) + return false; + + return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan); + } + + private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) + { + var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; + var shpk = mtrl->ShaderPackageResourceHandle; + if (shpk == null) + return; + + var shpkName = mtrl->ShpkNameSpan; + + if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource) + { + if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + + if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage) + { + if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _moddedCharacterGlassShpkCount); + } + } + + private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) + { + if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _moddedSkinShpkCount); + + if (_moddedCharacterGlassShpkMaterials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _moddedCharacterGlassShpkCount); + } + + private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) + { + // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + if (!Enabled || _moddedSkinShpkCount == 0) + return _humanOnRenderMaterialHook.Original(human, param); + + var material = param->Model->Materials[param->MaterialIndex]; + var mtrlResource = material->MaterialResourceHandle; + if (!IsMaterialWithShpk(mtrlResource, SkinShpkName)) + return _humanOnRenderMaterialHook.Original(human, param); + + Interlocked.Increment(ref _skinSlowPathCallDelta); + + // Performance considerations: + // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; + // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Swapping path is taken up to hundreds of times a frame. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + lock (_skinLock) + { + try + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; + return _humanOnRenderMaterialHook.Original(human, param); + } + finally + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + } + } + } + + private nint ModelRendererOnRenderMaterialDetour(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex) + { + + // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. + if (!Enabled || _moddedCharacterGlassShpkCount == 0) + return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); + + var mtrlResource = material->MaterialResourceHandle; + if (!IsMaterialWithShpk(mtrlResource, CharacterGlassShpkName)) + return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); + + Interlocked.Increment(ref _characterGlassSlowPathCallDelta); + + // Same performance considerations as above. + lock (_characterGlassLock) + { + try + { + *_modelRenderer.CharacterGlassShaderPackage = mtrlResource->ShaderPackageResourceHandle; + return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); + } + finally + { + *_modelRenderer.CharacterGlassShaderPackage = _modelRenderer.DefaultCharacterGlassShaderPackage; + } + } + } +} diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs deleted file mode 100644 index 21331916..00000000 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using OtterGui.Classes; -using Penumbra.Communication; -using Penumbra.GameData; -using Penumbra.Interop.Hooks.Resources; -using Penumbra.Services; - -namespace Penumbra.Interop.Services; - -public sealed unsafe class SkinFixer : IDisposable -{ - public static ReadOnlySpan SkinShpkName - => "skin.shpk"u8; - - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - - private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); - - [StructLayout(LayoutKind.Explicit)] - private struct OnRenderMaterialParams - { - [FieldOffset(0x0)] - public Model* Model; - - [FieldOffset(0x8)] - public uint MaterialIndex; - } - - private readonly Hook _onRenderMaterialHook; - - private readonly ResourceHandleDestructor _resourceHandleDestructor; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; - - // MaterialResourceHandle set - private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); - - private readonly object _lock = new(); - - // ConcurrentDictionary.Count uses a lock in its current implementation. - private int _moddedSkinShpkCount; - private ulong _slowPathCallDelta; - - public bool Enabled { get; internal set; } = true; - - public int ModdedSkinShpkCount - => _moddedSkinShpkCount; - - public SkinFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, CommunicatorService communicator, - IGameInteropProvider interop) - { - interop.InitializeFromAttributes(this); - _resourceHandleDestructor = resourceHandleDestructor; - _utility = utility; - _communicator = communicator; - _onRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); - _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); - _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.SkinFixer); - _onRenderMaterialHook.Enable(); - } - - public void Dispose() - { - _onRenderMaterialHook.Dispose(); - _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); - _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); - _moddedSkinShpkMaterials.Clear(); - _moddedSkinShpkCount = 0; - } - - public ulong GetAndResetSlowPathCallDelta() - => Interlocked.Exchange(ref _slowPathCallDelta, 0); - - private static bool IsSkinMaterial(MaterialResourceHandle* mtrlResource) - { - if (mtrlResource == null) - return false; - - var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkName); - return SkinShpkName.SequenceEqual(shpkName); - } - - private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) - { - var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; - var shpk = mtrl->ShaderPackageResourceHandle; - if (shpk == null) - return; - - if (!IsSkinMaterial(mtrl) || (nint)shpk == _utility.DefaultSkinShpkResource) - return; - - if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) - Interlocked.Increment(ref _moddedSkinShpkCount); - } - - private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) - { - if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedSkinShpkCount); - } - - private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) - { - // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - if (!Enabled || _moddedSkinShpkCount == 0) - return _onRenderMaterialHook.Original(human, param); - - var material = param->Model->Materials[param->MaterialIndex]; - var mtrlResource = material->MaterialResourceHandle; - if (!IsSkinMaterial(mtrlResource)) - return _onRenderMaterialHook.Original(human, param); - - Interlocked.Increment(ref _slowPathCallDelta); - - // Performance considerations: - // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; - // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; - // - Swapping path is taken up to hundreds of times a frame. - // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. - lock (_lock) - { - try - { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; - return _onRenderMaterialHook.Original(human, param); - } - finally - { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; - } - } - } -} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 15b7ce56..67f523ba 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -78,7 +78,7 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); - _services.GetService(); + _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index b0ecdcf0..cb2032a2 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -92,6 +92,7 @@ public static class ServiceManagerA return new CutsceneResolver(cutsceneService.GetParentIndex); }) .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -133,7 +134,7 @@ public static class ServiceManagerA .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 7adc4379..637c8401 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -220,24 +220,9 @@ public partial class ModEditWindow /// Model to copy element ids from. private static void MergeElementIds(MdlFile target, MdlFile source) { - var elementIds = new List(); - - foreach (var sourceElement in source.ElementIds) - { - var sourceBone = source.Bones[sourceElement.ParentBoneName]; - var targetIndex = target.Bones.IndexOf(sourceBone); - // Given that there's no means of authoring these at the moment, this should probably remain a hard error. - if (targetIndex == -1) - throw new Exception( - $"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model."); - - elementIds.Add(sourceElement with - { - ParentBoneName = (uint)targetIndex, - }); - } - - target.ElementIds = [.. elementIds]; + // This is overly simplistic, but effectively reproduces what TT did, sort of. + // TODO: Get a better idea of what these values represent. `ParentBoneName`, if it is a pointer into the bone array, does not seem to be _bounded_ by the bone array length, at least in the model. I'm guessing it _may_ be pointing into a .sklb instead? (i.e. the weapon's skeleton). EID stuff in general needs more work. + target.ElementIds = [.. source.ElementIds]; } private void BeginIo() diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 66b93b04..f4ddbe31 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -86,7 +86,7 @@ public class DebugTab : Window, ITab private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; - private readonly SkinFixer _skinFixer; + private readonly ShaderReplacementFixer _shaderReplacementFixer; private readonly RedrawService _redraws; private readonly DictEmote _emotes; private readonly Diagnostics _diagnostics; @@ -99,7 +99,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, SkinFixer skinFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester) + TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -130,7 +130,7 @@ public class DebugTab : Window, ITab _importPopup = importPopup; _framework = framework; _textureManager = textureManager; - _skinFixer = skinFixer; + _shaderReplacementFixer = shaderReplacementFixer; _redraws = redraws; _emotes = emotes; _diagnostics = diagnostics; @@ -702,20 +702,25 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Character Utility")) return; - var enableSkinFixer = _skinFixer.Enabled; - if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer)) - _skinFixer.Enabled = enableSkinFixer; + var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; + if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) + _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; - if (enableSkinFixer) + if (enableShaderReplacementFixer) { ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); ImGui.SameLine(); - ImGui.TextUnformatted($"\u0394 Slow-Path Calls: {_skinFixer.GetAndResetSlowPathCallDelta()}"); + ImGui.TextUnformatted($"\u0394 Slow-Path Calls for skin.shpk: {slowPathCallDeltas.Skin}"); + ImGui.SameLine(); + ImGui.TextUnformatted($"characterglass.shpk: {slowPathCallDeltas.CharacterGlass}"); ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_shaderReplacementFixer.ModdedSkinShpkCount}"); + ImGui.SameLine(); + ImGui.TextUnformatted($"characterglass.shpk: {_shaderReplacementFixer.ModdedCharacterGlassShpkCount}"); } using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 71f108c2..b311bb93 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -228,7 +229,15 @@ public class SettingsTab : ITab using var group = ImRaii.Group(); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - var save = ImGui.InputText("##rootDirectory", ref _newModDirectory, RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + bool save; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) + { + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) + .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); + save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, + RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + } + var selected = ImGui.IsItemActive(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); ImGui.SameLine();