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();