diff --git a/Penumbra.GameData b/Penumbra.GameData
index 1dad8d07..96e95378 160000
--- a/Penumbra.GameData
+++ b/Penumbra.GameData
@@ -1 +1 @@
-Subproject commit 1dad8d07047be0851f518cdac2b1c8bc76a7be98
+Subproject commit 96e95378325ff1533ca41b934fcb712f24d5260b
diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs
index 2552f1a7..7e7abcd8 100644
--- a/Penumbra/Interop/GameState.cs
+++ b/Penumbra/Interop/GameState.cs
@@ -72,7 +72,24 @@ public class GameState : IService
#endregion
- /// Return the correct resolve data from the stored data.
+ #region Subfiles
+
+ public readonly ThreadLocal MtrlData = new(() => ResolveData.Invalid);
+ public readonly ThreadLocal AvfxData = new(() => ResolveData.Invalid);
+
+ public readonly ConcurrentDictionary SubFileCollection = new();
+
+ public ResolveData LoadSubFileHelper(nint resourceHandle)
+ {
+ if (resourceHandle == nint.Zero)
+ return ResolveData.Invalid;
+
+ return SubFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid;
+ }
+
+ #endregion
+
+ /// Return the correct resolve data from the stored data.
public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData)
{
switch (type)
diff --git a/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs
similarity index 96%
rename from Penumbra/Interop/Hooks/CharacterBaseDestructor.cs
rename to Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs
index 435ddea6..fc6dbfe6 100644
--- a/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs
+++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs
@@ -4,7 +4,7 @@ using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.UI.AdvancedWindow;
-namespace Penumbra.Interop.Hooks;
+namespace Penumbra.Interop.Hooks.Objects;
public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr, IHookService
{
diff --git a/Penumbra/Interop/Hooks/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs
similarity index 96%
rename from Penumbra/Interop/Hooks/CharacterDestructor.cs
rename to Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs
index 4a0e9367..6e10c5e3 100644
--- a/Penumbra/Interop/Hooks/CharacterDestructor.cs
+++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs
@@ -4,7 +4,7 @@ using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData;
-namespace Penumbra.Interop.Hooks;
+namespace Penumbra.Interop.Hooks.Objects;
public sealed unsafe class CharacterDestructor : EventWrapperPtr, IHookService
{
diff --git a/Penumbra/Interop/Hooks/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs
similarity index 96%
rename from Penumbra/Interop/Hooks/CopyCharacter.cs
rename to Penumbra/Interop/Hooks/Objects/CopyCharacter.cs
index d2e8d816..7b730f84 100644
--- a/Penumbra/Interop/Hooks/CopyCharacter.cs
+++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs
@@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character;
using OtterGui.Classes;
using OtterGui.Services;
-namespace Penumbra.Interop.Hooks;
+namespace Penumbra.Interop.Hooks.Objects;
public sealed unsafe class CopyCharacter : EventWrapperPtr, IHookService
{
diff --git a/Penumbra/Interop/Hooks/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs
similarity index 94%
rename from Penumbra/Interop/Hooks/CreateCharacterBase.cs
rename to Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs
index 7dbde666..299f312a 100644
--- a/Penumbra/Interop/Hooks/CreateCharacterBase.cs
+++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs
@@ -4,7 +4,7 @@ using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Structs;
-namespace Penumbra.Interop.Hooks;
+namespace Penumbra.Interop.Hooks.Objects;
public sealed unsafe class CreateCharacterBase : EventWrapperPtr, IHookService
{
@@ -39,7 +39,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr
/// EnableDraw is what creates DrawObjects for gameObjects,
@@ -12,12 +12,12 @@ namespace Penumbra.Interop.Hooks;
public sealed unsafe class EnableDraw : IHookService
{
private readonly Task> _task;
- private readonly GameState _state;
+ private readonly GameState _state;
public EnableDraw(HookManager hooks, GameState state)
{
_state = state;
- _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true);
+ _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true);
}
private delegate void Delegate(GameObject* gameObject);
@@ -26,7 +26,7 @@ public sealed unsafe class EnableDraw : IHookService
private void Detour(GameObject* gameObject)
{
_state.QueueGameObject(gameObject);
- Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint) gameObject:X}.");
+ Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X}.");
_task.Result.Original.Invoke(gameObject);
_state.DequeueGameObject();
}
diff --git a/Penumbra/Interop/Hooks/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs
similarity index 98%
rename from Penumbra/Interop/Hooks/WeaponReload.cs
rename to Penumbra/Interop/Hooks/Objects/WeaponReload.cs
index b931f8fb..31c6b883 100644
--- a/Penumbra/Interop/Hooks/WeaponReload.cs
+++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs
@@ -4,7 +4,7 @@ using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Structs;
-namespace Penumbra.Interop.Hooks;
+namespace Penumbra.Interop.Hooks.Objects;
public sealed unsafe class WeaponReload : EventWrapperPtr, IHookService
{
diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs
new file mode 100644
index 00000000..2e5698a3
--- /dev/null
+++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs
@@ -0,0 +1,28 @@
+using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
+using OtterGui.Services;
+using Penumbra.GameData;
+
+namespace Penumbra.Interop.Hooks.Resources;
+
+public sealed unsafe class ApricotResourceLoad : FastHook
+{
+ private readonly GameState _gameState;
+
+ public ApricotResourceLoad(HookManager hooks, GameState gameState)
+ {
+ _gameState = gameState;
+ Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, true);
+ }
+
+ public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private byte Detour(ResourceHandle* handle, nint unk1, byte unk2)
+ {
+ var last = _gameState.AvfxData.Value;
+ _gameState.AvfxData.Value = _gameState.LoadSubFileHelper((nint)handle);
+ var ret = Task.Result.Original(handle, unk1, unk2);
+ _gameState.AvfxData.Value = last;
+ return ret;
+ }
+}
diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs
new file mode 100644
index 00000000..5ef3bf37
--- /dev/null
+++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs
@@ -0,0 +1,32 @@
+using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
+using OtterGui.Services;
+using Penumbra.GameData;
+using Penumbra.Services;
+
+namespace Penumbra.Interop.Hooks.Resources;
+
+public sealed unsafe class LoadMtrlShpk : FastHook
+{
+ private readonly GameState _gameState;
+ private readonly CommunicatorService _communicator;
+
+ public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator)
+ {
+ _gameState = gameState;
+ _communicator = communicator;
+ Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, true);
+ }
+
+ public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle);
+
+ private byte Detour(MaterialResourceHandle* handle)
+ {
+ var last = _gameState.MtrlData.Value;
+ var mtrlData = _gameState.LoadSubFileHelper((nint)handle);
+ _gameState.MtrlData.Value = mtrlData;
+ var ret = Task.Result.Original(handle);
+ _gameState.MtrlData.Value = last;
+ _communicator.MtrlShpkLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject);
+ return ret;
+ }
+}
diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs
new file mode 100644
index 00000000..14a011ea
--- /dev/null
+++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs
@@ -0,0 +1,28 @@
+using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
+using OtterGui.Services;
+using Penumbra.GameData;
+
+namespace Penumbra.Interop.Hooks.Resources;
+
+public sealed unsafe class LoadMtrlTex : FastHook
+{
+ private readonly GameState _gameState;
+
+ public LoadMtrlTex(HookManager hooks, GameState gameState)
+ {
+ _gameState = gameState;
+ Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, true);
+ }
+
+ public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private byte Detour(MaterialResourceHandle* handle)
+ {
+ var last = _gameState.MtrlData.Value;
+ _gameState.MtrlData.Value = _gameState.LoadSubFileHelper((nint)handle);
+ var ret = Task.Result.Original(handle);
+ _gameState.MtrlData.Value = last;
+ return ret;
+ }
+}
diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs
new file mode 100644
index 00000000..8a52acd2
--- /dev/null
+++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs
@@ -0,0 +1,38 @@
+using OtterGui.Services;
+using Penumbra.Interop.PathResolving;
+
+namespace Penumbra.Interop.Hooks.Resources;
+
+public sealed unsafe class ResolvePathHooks(HookManager hooks, CharacterBaseVTables vTables, PathState pathState) : IDisposable, IRequiredService
+{
+ // @formatter:off
+ private readonly ResolvePathHooksBase _human = new("Human", hooks, pathState, vTables.HumanVTable, ResolvePathHooksBase.Type.Human);
+ private readonly ResolvePathHooksBase _weapon = new("Weapon", hooks, pathState, vTables.WeaponVTable, ResolvePathHooksBase.Type.Other);
+ private readonly ResolvePathHooksBase _demiHuman = new("DemiHuman", hooks, pathState, vTables.DemiHumanVTable, ResolvePathHooksBase.Type.Other);
+ private readonly ResolvePathHooksBase _monster = new("Monster", hooks, pathState, vTables.MonsterVTable, ResolvePathHooksBase.Type.Other);
+ // @formatter:on
+
+ public void Enable()
+ {
+ _human.Enable();
+ _weapon.Enable();
+ _demiHuman.Enable();
+ _monster.Enable();
+ }
+
+ public void Disable()
+ {
+ _human.Disable();
+ _weapon.Disable();
+ _demiHuman.Disable();
+ _monster.Disable();
+ }
+
+ public void Dispose()
+ {
+ _human.Dispose();
+ _weapon.Dispose();
+ _demiHuman.Dispose();
+ _monster.Dispose();
+ }
+}
diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs
similarity index 78%
rename from Penumbra/Interop/PathResolving/ResolvePathHooks.cs
rename to Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs
index 3be7ffdd..6b4abf90 100644
--- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs
+++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs
@@ -1,13 +1,14 @@
using Dalamud.Hooking;
-using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
+using OtterGui.Services;
using Penumbra.Collections;
+using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
-namespace Penumbra.Interop.PathResolving;
+namespace Penumbra.Interop.Hooks.Resources;
-public unsafe class ResolvePathHooks : IDisposable
+public sealed unsafe class ResolvePathHooksBase : IDisposable
{
public enum Type
{
@@ -19,7 +20,9 @@ public unsafe class ResolvePathHooks : IDisposable
private delegate nint NamedResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint name);
private delegate nint PerSlotResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex);
private delegate nint SingleResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize);
+
private delegate nint TmbResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, nint timelineName);
+
// Kept separate from NamedResolveDelegate because the 5th parameter has out semantics here, instead of in.
private delegate nint VfxResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam);
@@ -38,21 +41,24 @@ public unsafe class ResolvePathHooks : IDisposable
private readonly PathState _parent;
- public ResolvePathHooks(IGameInteropProvider interop, PathState parent, nint* vTable, Type type)
+ public ResolvePathHooksBase(string name, HookManager hooks, PathState parent, nint* vTable, Type type)
{
- _parent = parent;
- _resolveDecalPathHook = Create(interop, vTable[83], ResolveDecal);
- _resolveEidPathHook = Create(interop, vTable[85], ResolveEid);
- _resolveImcPathHook = Create(interop, vTable[81], ResolveImc);
- _resolveMPapPathHook = Create(interop, vTable[79], ResolveMPap);
- _resolveMdlPathHook = Create(interop, vTable[73], type, ResolveMdl, ResolveMdlHuman);
- _resolveMtrlPathHook = Create(interop, vTable[82], ResolveMtrl);
- _resolvePapPathHook = Create(interop, vTable[76], type, ResolvePap, ResolvePapHuman);
- _resolvePhybPathHook = Create(interop, vTable[75], type, ResolvePhyb, ResolvePhybHuman);
- _resolveSklbPathHook = Create(interop, vTable[72], type, ResolveSklb, ResolveSklbHuman);
- _resolveSkpPathHook = Create(interop, vTable[74], type, ResolveSkp, ResolveSkpHuman);
- _resolveTmbPathHook = Create(interop, vTable[77], ResolveTmb);
- _resolveVfxPathHook = Create(interop, vTable[84], ResolveVfx);
+ _parent = parent;
+ // @formatter:off
+ _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[83], ResolveDecal);
+ _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[85], ResolveEid);
+ _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[81], ResolveImc);
+ _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[79], ResolveMPap);
+ _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[73], type, ResolveMdl, ResolveMdlHuman);
+ _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[82], ResolveMtrl);
+ _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[76], type, ResolvePap, ResolvePapHuman);
+ _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[75], type, ResolvePhyb, ResolvePhybHuman);
+ _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman);
+ _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman);
+ _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb);
+ _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], ResolveVfx);
+ // @formatter:on
+ Enable();
}
public void Enable()
@@ -177,9 +183,8 @@ public unsafe class ResolvePathHooks : IDisposable
{
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
if (_parent.InInternalResolve)
- {
return DisposableContainer.Empty;
- }
+
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair),
@@ -188,19 +193,19 @@ public unsafe class ResolvePathHooks : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
- private static Hook Create(IGameInteropProvider interop, nint address, Type type, T other, T human) where T : Delegate
+ private static Hook Create(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate
{
var del = type switch
{
- Type.Human => human,
- _ => other,
+ Type.Human => human,
+ _ => other,
};
- return interop.HookFromAddress(address, del);
+ return hooks.CreateHook(name, address, del).Result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
- private static Hook Create(IGameInteropProvider interop, nint address, T del) where T : Delegate
- => interop.HookFromAddress(address, del);
+ private static Hook Create(string name, HookManager hooks, nint address, T del) where T : Delegate
+ => hooks.CreateHook(name, address, del).Result;
// Implementation
diff --git a/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs
similarity index 95%
rename from Penumbra/Interop/Hooks/ResourceHandleDestructor.cs
rename to Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs
index 99eb1c23..776f2f92 100644
--- a/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs
+++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs
@@ -2,10 +2,9 @@ using Dalamud.Hooking;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData;
-using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
-namespace Penumbra.Interop.Hooks;
+namespace Penumbra.Interop.Hooks.Resources;
public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr, IHookService
{
diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs
index 0b7bafe0..801c3bf0 100644
--- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs
+++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs
@@ -16,11 +16,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
private readonly Texture** _colorTableTexture;
private readonly SafeTextureHandle _originalColorTableTexture;
- private Half[] _colorTable;
- private bool _updatePending;
+ private bool _updatePending;
- public Half[] ColorTable
- => _colorTable;
+ public Half[] ColorTable { get; }
public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, MaterialInfo materialInfo)
: base(objects, materialInfo)
@@ -41,7 +39,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
if (_originalColorTableTexture == null)
throw new InvalidOperationException("Material doesn't have a color table");
- _colorTable = new Half[TextureLength];
+ ColorTable = new Half[TextureLength];
_updatePending = true;
framework.Update += OnFrameworkUpdate;
@@ -84,9 +82,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
return;
bool success;
- lock (_colorTable)
+ lock (ColorTable)
{
- fixed (Half* colorTable = _colorTable)
+ fixed (Half* colorTable = ColorTable)
{
success = texture.Texture->InitializeContents(colorTable);
}
@@ -105,9 +103,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
if (colorSetTextures == null)
return false;
- if (_colorTableTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot))
- return false;
-
- return true;
+ return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
}
}
diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs
index 972d81be..9ed7ca3d 100644
--- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs
+++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs
@@ -26,34 +26,29 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
if (_shaderPackage == null)
throw new InvalidOperationException("Material doesn't have a shader package");
- var material = Material;
+ _originalShPkFlags = Material->ShaderFlags;
- _originalShPkFlags = material->ShaderFlags;
+ _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray();
- _originalMaterialParameter = material->MaterialParameterCBuffer->TryGetBuffer().ToArray();
-
- _originalSamplerFlags = new uint[material->TextureCount];
+ _originalSamplerFlags = new uint[Material->TextureCount];
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
- _originalSamplerFlags[i] = material->Textures[i].SamplerFlags;
+ _originalSamplerFlags[i] = Material->Textures[i].SamplerFlags;
}
protected override void Clear(bool disposing, bool reset)
{
base.Clear(disposing, reset);
- if (reset)
- {
- var material = Material;
+ if (!reset)
+ return;
- material->ShaderFlags = _originalShPkFlags;
+ Material->ShaderFlags = _originalShPkFlags;
+ var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer();
+ if (!materialParameter.IsEmpty)
+ _originalMaterialParameter.AsSpan().CopyTo(materialParameter);
- var materialParameter = material->MaterialParameterCBuffer->TryGetBuffer();
- if (!materialParameter.IsEmpty)
- _originalMaterialParameter.AsSpan().CopyTo(materialParameter);
-
- for (var i = 0; i < _originalSamplerFlags.Length; ++i)
- material->Textures[i].SamplerFlags = _originalSamplerFlags[i];
- }
+ for (var i = 0; i < _originalSamplerFlags.Length; ++i)
+ Material->Textures[i].SamplerFlags = _originalSamplerFlags[i];
}
public void SetShaderPackageFlags(uint shPkFlags)
@@ -80,16 +75,16 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i)
{
ref var parameter = ref _shaderPackage->MaterialElementsSpan[i];
- if (parameter.CRC == parameterCrc)
- {
- if ((parameter.Offset & 0x3) != 0
- || (parameter.Size & 0x3) != 0
- || (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
- return;
+ if (parameter.CRC != parameterCrc)
+ continue;
- value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
+ if ((parameter.Offset & 0x3) != 0
+ || (parameter.Size & 0x3) != 0
+ || (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
return;
- }
+
+ value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
+ return;
}
}
@@ -104,25 +99,24 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
var samplers = _shaderPackage->Samplers;
for (var i = 0; i < _shaderPackage->SamplerCount; ++i)
{
- if (samplers[i].CRC == samplerCrc)
- {
- id = samplers[i].Id;
- found = true;
- break;
- }
+ if (samplers[i].CRC != samplerCrc)
+ continue;
+
+ id = samplers[i].Id;
+ found = true;
+ break;
}
if (!found)
return;
- var material = Material;
- for (var i = 0; i < material->TextureCount; ++i)
+ for (var i = 0; i < Material->TextureCount; ++i)
{
- if (material->Textures[i].Id == id)
- {
- material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0;
- break;
- }
+ if (Material->Textures[i].Id != id)
+ continue;
+
+ Material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0;
+ break;
}
}
@@ -139,9 +133,6 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
if (shpkHandle == null)
return false;
- if (_shaderPackage != shpkHandle->ShaderPackage)
- return false;
-
- return true;
+ return _shaderPackage == shpkHandle->ShaderPackage;
}
}
diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs
index 86fee976..07986f52 100644
--- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs
+++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs
@@ -61,9 +61,6 @@ public abstract unsafe class LiveMaterialPreviewerBase : IDisposable
if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject))
return false;
- if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject))
- return false;
-
- return true;
+ return Material == MaterialInfo.GetDrawObjectMaterial(DrawObject);
}
}
diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs
index ec0ddd29..686b5a86 100644
--- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs
+++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs
@@ -24,22 +24,6 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
public nint GetDrawObject(nint address)
=> GetDrawObject(Type, address);
- public static unsafe nint GetDrawObject(DrawObjectType type, nint address)
- {
- var gameObject = (Character*)address;
- if (gameObject == null)
- return nint.Zero;
-
- return type switch
- {
- DrawObjectType.Character => (nint)gameObject->GameObject.GetDrawObject(),
- DrawObjectType.Mainhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject,
- DrawObjectType.Offhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject,
- DrawObjectType.Vfx => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.Unk).DrawObject,
- _ => nint.Zero,
- };
- }
-
public unsafe Material* GetDrawObjectMaterial(IObjectTable objects)
=> GetDrawObjectMaterial((CharacterBase*)GetDrawObject(GetCharacter(objects)));
@@ -103,4 +87,20 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
return result;
}
+
+ private static unsafe nint GetDrawObject(DrawObjectType type, nint address)
+ {
+ var gameObject = (Character*)address;
+ if (gameObject == null)
+ return nint.Zero;
+
+ return type switch
+ {
+ DrawObjectType.Character => (nint)gameObject->GameObject.GetDrawObject(),
+ DrawObjectType.Mainhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject,
+ DrawObjectType.Offhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject,
+ DrawObjectType.Vfx => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.Unk).DrawObject,
+ _ => nint.Zero,
+ };
+ }
}
diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs
index c7b24bd7..2eeefbd8 100644
--- a/Penumbra/Interop/PathResolving/CutsceneService.cs
+++ b/Penumbra/Interop/PathResolving/CutsceneService.cs
@@ -2,7 +2,7 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using OtterGui.Services;
using Penumbra.GameData.Enums;
-using Penumbra.Interop.Hooks;
+using Penumbra.Interop.Hooks.Objects;
namespace Penumbra.Interop.PathResolving;
diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs
index 19c0fd10..dd4b03f2 100644
--- a/Penumbra/Interop/PathResolving/DrawObjectState.cs
+++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs
@@ -6,6 +6,7 @@ using OtterGui.Services;
using Penumbra.Interop.Hooks;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
using Penumbra.GameData.Structs;
+using Penumbra.Interop.Hooks.Objects;
namespace Penumbra.Interop.PathResolving;
diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs
index b944011d..32090f7c 100644
--- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs
+++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs
@@ -5,7 +5,7 @@ using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.GameData.Actors;
-using Penumbra.Interop.Hooks;
+using Penumbra.Interop.Hooks.Objects;
using Penumbra.Services;
namespace Penumbra.Interop.PathResolving;
diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs
index 9d899648..a3400540 100644
--- a/Penumbra/Interop/PathResolving/MetaState.cs
+++ b/Penumbra/Interop/PathResolving/MetaState.cs
@@ -4,13 +4,13 @@ using Penumbra.Collections;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
-using Penumbra.Interop.Hooks;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Services;
using Penumbra.Services;
using Penumbra.String.Classes;
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
+using Penumbra.Interop.Hooks.Objects;
namespace Penumbra.Interop.PathResolving;
diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs
index 6d7840d8..f4218e9c 100644
--- a/Penumbra/Interop/PathResolving/PathState.cs
+++ b/Penumbra/Interop/PathResolving/PathState.cs
@@ -1,34 +1,15 @@
-using Dalamud.Plugin.Services;
-using Dalamud.Utility.Signatures;
using Penumbra.Collections;
-using Penumbra.GameData;
using Penumbra.Interop.Services;
using Penumbra.String;
namespace Penumbra.Interop.PathResolving;
-public unsafe class PathState : IDisposable
+public sealed class PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility)
+ : IDisposable
{
- [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
- private readonly nint* _humanVTable = null!;
-
- [Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)]
- private readonly nint* _weaponVTable = null!;
-
- [Signature(Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress)]
- private readonly nint* _demiHumanVTable = null!;
-
- [Signature(Sigs.MonsterVTable, ScanType = ScanType.StaticAddress)]
- private readonly nint* _monsterVTable = null!;
-
- public readonly CollectionResolver CollectionResolver;
- public readonly MetaState MetaState;
- public readonly CharacterUtility CharacterUtility;
-
- private readonly ResolvePathHooks _human;
- private readonly ResolvePathHooks _weapon;
- private readonly ResolvePathHooks _demiHuman;
- private readonly ResolvePathHooks _monster;
+ public readonly CollectionResolver CollectionResolver = collectionResolver;
+ public readonly MetaState MetaState = metaState;
+ public readonly CharacterUtility CharacterUtility = characterUtility;
private readonly ThreadLocal _resolveData = new(() => ResolveData.Invalid, true);
private readonly ThreadLocal _internalResolve = new(() => 0, false);
@@ -39,31 +20,11 @@ public unsafe class PathState : IDisposable
public bool InInternalResolve
=> _internalResolve.Value != 0u;
- public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop)
- {
- interop.InitializeFromAttributes(this);
- CollectionResolver = collectionResolver;
- MetaState = metaState;
- CharacterUtility = characterUtility;
- _human = new ResolvePathHooks(interop, this, _humanVTable, ResolvePathHooks.Type.Human);
- _weapon = new ResolvePathHooks(interop, this, _weaponVTable, ResolvePathHooks.Type.Other);
- _demiHuman = new ResolvePathHooks(interop, this, _demiHumanVTable, ResolvePathHooks.Type.Other);
- _monster = new ResolvePathHooks(interop, this, _monsterVTable, ResolvePathHooks.Type.Other);
- _human.Enable();
- _weapon.Enable();
- _demiHuman.Enable();
- _monster.Enable();
- }
-
public void Dispose()
{
_resolveData.Dispose();
_internalResolve.Dispose();
- _human.Dispose();
- _weapon.Dispose();
- _demiHuman.Dispose();
- _monster.Dispose();
}
public bool Consume(ByteString _, out ResolveData collection)
@@ -86,9 +47,7 @@ public unsafe class PathState : IDisposable
return path;
if (!InInternalResolve)
- {
_resolveData.Value = collection.ToResolveData(gameObject);
- }
return path;
}
@@ -99,9 +58,7 @@ public unsafe class PathState : IDisposable
return path;
if (!InInternalResolve)
- {
_resolveData.Value = data;
- }
return path;
}
@@ -126,7 +83,7 @@ public unsafe class PathState : IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
- public readonly void Dispose()
+ public void Dispose()
{
--_internalResolve.Value;
}
diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs
index 370118ea..2359c36e 100644
--- a/Penumbra/Interop/PathResolving/SubfileHelper.cs
+++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs
@@ -1,17 +1,10 @@
-using Dalamud.Hooking;
-using Dalamud.Plugin.Services;
-using Dalamud.Utility.Signatures;
using Penumbra.Api.Enums;
using Penumbra.Collections;
-using Penumbra.GameData;
-using Penumbra.Interop.Hooks;
+using Penumbra.Interop.Hooks.Resources;
using Penumbra.Interop.ResourceLoading;
-using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
-using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
-using Penumbra.Util;
namespace Penumbra.Interop.PathResolving;
@@ -20,49 +13,37 @@ namespace Penumbra.Interop.PathResolving;
/// Those are loaded synchronously.
/// Thus, we need to ensure the correct files are loaded when a material is loaded.
///
-public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection>
+public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection>
{
- private readonly PerformanceTracker _performance;
+ private readonly GameState _gameState;
private readonly ResourceLoader _loader;
private readonly ResourceHandleDestructor _resourceHandleDestructor;
- private readonly CommunicatorService _communicator;
- private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid);
- private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid);
-
- private readonly ConcurrentDictionary _subFileCollection = new();
-
- public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, CommunicatorService communicator, IGameInteropProvider interop, ResourceHandleDestructor resourceHandleDestructor)
+ public SubfileHelper(GameState gameState, ResourceLoader loader, ResourceHandleDestructor resourceHandleDestructor)
{
- interop.InitializeFromAttributes(this);
-
- _performance = performance;
- _loader = loader;
- _communicator = communicator;
+ _gameState = gameState;
+ _loader = loader;
_resourceHandleDestructor = resourceHandleDestructor;
- _loadMtrlShpkHook.Enable();
- _loadMtrlTexHook.Enable();
- _apricotResourceLoadHook.Enable();
- _loader.ResourceLoaded += SubfileContainerRequested;
+ _loader.ResourceLoaded += SubfileContainerRequested;
_resourceHandleDestructor.Subscribe(ResourceDestroyed, ResourceHandleDestructor.Priority.SubfileHelper);
}
public IEnumerator> GetEnumerator()
- => _subFileCollection.GetEnumerator();
+ => _gameState.SubFileCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
- => _subFileCollection.Count;
+ => _gameState.SubFileCollection.Count;
public ResolveData MtrlData
- => _mtrlData.IsValueCreated ? _mtrlData.Value : ResolveData.Invalid;
+ => _gameState.MtrlData.IsValueCreated ? _gameState.MtrlData.Value : ResolveData.Invalid;
public ResolveData AvfxData
- => _avfxData.IsValueCreated ? _avfxData.Value : ResolveData.Invalid;
+ => _gameState.AvfxData.IsValueCreated ? _gameState.AvfxData.Value : ResolveData.Invalid;
///
/// Check specifically for shpk and tex files whether we are currently in a material load,
@@ -71,13 +52,13 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollectionFileSize == 0)
- _subFileCollection[(nint)handle] = resolveData;
+ _gameState.SubFileCollection[(nint)handle] = resolveData;
break;
}
}
private void ResourceDestroyed(ResourceHandle* handle)
- => _subFileCollection.TryRemove((nint)handle, out _);
-
- private delegate byte LoadMtrlFilesDelegate(nint mtrlResourceHandle);
-
- [Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))]
- private readonly Hook _loadMtrlTexHook = null!;
-
- private byte LoadMtrlTexDetour(nint mtrlResourceHandle)
- {
- using var performance = _performance.Measure(PerformanceType.LoadTextures);
- var last = _mtrlData.Value;
- _mtrlData.Value = LoadFileHelper(mtrlResourceHandle);
- var ret = _loadMtrlTexHook.Original(mtrlResourceHandle);
- _mtrlData.Value = last;
- return ret;
- }
-
- [Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))]
- private readonly Hook _loadMtrlShpkHook = null!;
-
- private byte LoadMtrlShpkDetour(nint mtrlResourceHandle)
- {
- using var performance = _performance.Measure(PerformanceType.LoadShaders);
- var last = _mtrlData.Value;
- var mtrlData = LoadFileHelper(mtrlResourceHandle);
- _mtrlData.Value = mtrlData;
- var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle);
- _mtrlData.Value = last;
- _communicator.MtrlShpkLoaded.Invoke(mtrlResourceHandle, mtrlData.AssociatedGameObject);
- return ret;
- }
-
- private ResolveData LoadFileHelper(nint resourceHandle)
- {
- if (resourceHandle == nint.Zero)
- return ResolveData.Invalid;
-
- return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid;
- }
-
-
- private delegate byte ApricotResourceLoadDelegate(nint handle, nint unk1, byte unk2);
-
- [Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))]
- private readonly Hook _apricotResourceLoadHook = null!;
-
- private byte ApricotResourceLoadDetour(nint handle, nint unk1, byte unk2)
- {
- using var performance = _performance.Measure(PerformanceType.LoadApricotResources);
- var last = _avfxData.Value;
- _avfxData.Value = LoadFileHelper(handle);
- var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2);
- _avfxData.Value = last;
- return ret;
- }
+ => _gameState.SubFileCollection.TryRemove((nint)handle, out _);
}
diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs
index 444b9a48..21331916 100644
--- a/Penumbra/Interop/Services/SkinFixer.cs
+++ b/Penumbra/Interop/Services/SkinFixer.cs
@@ -6,7 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using OtterGui.Classes;
using Penumbra.Communication;
using Penumbra.GameData;
-using Penumbra.Interop.Hooks;
+using Penumbra.Interop.Hooks.Resources;
using Penumbra.Services;
namespace Penumbra.Interop.Services;
diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs
index 47c34ce5..4773d840 100644
--- a/Penumbra/Mods/Editor/DuplicateManager.cs
+++ b/Penumbra/Mods/Editor/DuplicateManager.cs
@@ -1,3 +1,4 @@
+using OtterGui.Services;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Services;
@@ -5,25 +6,15 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Editor;
-public class DuplicateManager
+public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config)
{
- private readonly Configuration _config;
- private readonly SaveService _saveService;
- private readonly ModManager _modManager;
- private readonly SHA256 _hasher = SHA256.Create();
- private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
-
- public DuplicateManager(ModManager modManager, SaveService saveService, Configuration config)
- {
- _modManager = modManager;
- _saveService = saveService;
- _config = config;
- }
+ private readonly SHA256 _hasher = SHA256.Create();
+ private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = [];
public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates
=> _duplicates;
- public long SavedSpace { get; private set; } = 0;
+ public long SavedSpace { get; private set; }
public Task Worker { get; private set; } = Task.CompletedTask;
private CancellationTokenSource _cancellationTokenSource = new();
@@ -68,6 +59,19 @@ public class DuplicateManager
private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager)
{
+ ModEditor.ApplyToAllOptions(mod, HandleSubMod);
+
+ try
+ {
+ File.Delete(duplicate.FullName);
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error($"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}");
+ }
+
+ return;
+
void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx)
{
var changes = false;
@@ -78,26 +82,15 @@ public class DuplicateManager
if (useModManager)
{
- _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict);
+ modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict);
}
else
{
var sub = (SubMod)subMod;
sub.FileData = dict;
- _saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport));
+ saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
}
}
-
- ModEditor.ApplyToAllOptions(mod, HandleSubMod);
-
- try
- {
- File.Delete(duplicate.FullName);
- }
- catch (Exception e)
- {
- Penumbra.Log.Error($"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}");
- }
}
private static FullPath ChangeDuplicatePath(Mod mod, FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes)
@@ -199,15 +192,6 @@ public class DuplicateManager
}
}
- public static bool CompareHashes(byte[] f1, byte[] f2)
- => StructuralComparisons.StructuralEqualityComparer.Equals(f1, f2);
-
- public byte[] ComputeHash(FullPath f)
- {
- using var stream = File.OpenRead(f.FullName);
- return _hasher.ComputeHash(stream);
- }
-
///
/// Recursively delete all empty directories starting from the given directory.
/// Deletes inner directories first, so that a tree of empty directories is actually deleted.
@@ -232,14 +216,13 @@ public class DuplicateManager
}
}
-
/// Deduplicate a mod simply by its directory without any confirmation or waiting time.
internal void DeduplicateMod(DirectoryInfo modDirectory)
{
try
{
var mod = new Mod(modDirectory);
- _modManager.Creator.ReloadMod(mod, true, out _);
+ modManager.Creator.ReloadMod(mod, true, out _);
Clear();
var files = new ModFileCollection();
@@ -252,4 +235,13 @@ public class DuplicateManager
Penumbra.Log.Warning($"Could not deduplicate mod {modDirectory.Name}:\n{e}");
}
}
+
+ private static bool CompareHashes(byte[] f1, byte[] f2)
+ => StructuralComparisons.StructuralEqualityComparer.Equals(f1, f2);
+
+ private byte[] ComputeHash(FullPath f)
+ {
+ using var stream = File.OpenRead(f.FullName);
+ return _hasher.ComputeHash(stream);
+ }
}
diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs
index a223b51e..96d027b3 100644
--- a/Penumbra/Mods/Editor/FileRegistry.cs
+++ b/Penumbra/Mods/Editor/FileRegistry.cs
@@ -1,11 +1,11 @@
using Penumbra.Mods.Subclasses;
using Penumbra.String.Classes;
-namespace Penumbra.Mods;
+namespace Penumbra.Mods.Editor;
public class FileRegistry : IEquatable
{
- public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = new();
+ public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = [];
public FullPath File { get; private init; }
public Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; }
diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs
index 1dfe9e76..f5d0e4a4 100644
--- a/Penumbra/Mods/Editor/ModMerger.cs
+++ b/Penumbra/Mods/Editor/ModMerger.cs
@@ -29,12 +29,12 @@ public class ModMerger : IDisposable
public string OptionGroupName = "Merges";
public string OptionName = string.Empty;
- private readonly Dictionary _fileToFile = new();
- private readonly HashSet _createdDirectories = new();
- private readonly HashSet _createdGroups = new();
- private readonly HashSet _createdOptions = new();
+ private readonly Dictionary _fileToFile = [];
+ private readonly HashSet _createdDirectories = [];
+ private readonly HashSet _createdGroups = [];
+ private readonly HashSet _createdOptions = [];
- public readonly HashSet SelectedOptions = new();
+ public readonly HashSet SelectedOptions = [];
public readonly IReadOnlyList Warnings = new List();
public Exception? Error { get; private set; }
diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs
index 5a03dc04..706e4a01 100644
--- a/Penumbra/Penumbra.cs
+++ b/Penumbra/Penumbra.cs
@@ -79,7 +79,6 @@ public class Penumbra : IDalamudPlugin
_services.GetService(); // Initialize because not required anywhere else.
_collectionManager.Caches.CreateNecessaryCaches();
_services.GetService();
-
_services.GetService();
_services.GetService(); // Initialize before Interface.
diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs
index 4783e76b..16cacaa4 100644
--- a/Penumbra/UI/AdvancedWindow/FileEditor.cs
+++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs
@@ -9,6 +9,7 @@ using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.Files;
using Penumbra.Mods;
+using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs
index 4a193591..bae23729 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs
@@ -4,6 +4,7 @@ using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.Mods;
+using Penumbra.Mods.Editor;
using Penumbra.Mods.Subclasses;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs
index 376bbcf7..b4801f5f 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs
@@ -9,7 +9,7 @@ using OtterGui.Raii;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
-using Penumbra.Interop.Hooks;
+using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.MaterialPreview;
using Penumbra.String;
using Penumbra.String.Classes;
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
index 8d3e32f9..8b6ef331 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
@@ -14,7 +14,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Import.Models;
using Penumbra.Import.Textures;
-using Penumbra.Interop.Hooks;
+using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.ResourceTree;
using Penumbra.Meta;
using Penumbra.Mods;
diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs
index 7d4fa96f..1df814da 100644
--- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs
+++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs
@@ -9,22 +9,14 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
-public class ModMergeTab
+public class ModMergeTab(ModMerger modMerger)
{
- private readonly ModMerger _modMerger;
- private readonly ModCombo _modCombo;
-
- private string _newModName = string.Empty;
-
- public ModMergeTab(ModMerger modMerger)
- {
- _modMerger = modMerger;
- _modCombo = new ModCombo(() => _modMerger.ModsWithoutCurrent.ToList());
- }
+ private readonly ModCombo _modCombo = new(() => modMerger.ModsWithoutCurrent.ToList());
+ private string _newModName = string.Empty;
public void Draw()
{
- if (_modMerger.MergeFromMod == null)
+ if (modMerger.MergeFromMod == null)
return;
using var tab = ImRaii.TabItem("Merge Mods");
@@ -54,23 +46,23 @@ public class ModMergeTab
{
using var bigGroup = ImRaii.Group();
ImGui.AlignTextToFramePadding();
- ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod!.Name} into ");
+ ImGui.TextUnformatted($"Merge {modMerger.MergeFromMod!.Name} into ");
ImGui.SameLine();
DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X);
var width = ImGui.GetItemRectSize();
using (var g = ImRaii.Group())
{
- using var disabled = ImRaii.Disabled(_modMerger.MergeFromMod.HasOptions);
+ using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions);
var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2;
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1);
- var group = _modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == _modMerger.OptionGroupName);
- var color = group != null || _modMerger.OptionGroupName.Length == 0 && _modMerger.OptionName.Length == 0
+ var group = modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == modMerger.OptionGroupName);
+ var color = group != null || modMerger.OptionGroupName.Length == 0 && modMerger.OptionName.Length == 0
? Colors.PressEnterWarningBg
: Colors.DiscordColor;
using var c = ImRaii.PushColor(ImGuiCol.Border, color);
ImGui.SetNextItemWidth(buttonWidth);
- ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64);
+ ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref modMerger.OptionGroupName, 64);
ImGuiUtil.HoverTooltip(
"The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n"
+ "A red border indicates an existing option group, a blue border indicates a new one.");
@@ -79,29 +71,29 @@ public class ModMergeTab
color = color == Colors.DiscordColor
? Colors.DiscordColor
- : group == null || group.Any(o => o.Name == _modMerger.OptionName)
+ : group == null || group.Any(o => o.Name == modMerger.OptionName)
? Colors.PressEnterWarningBg
: Colors.DiscordColor;
c.Push(ImGuiCol.Border, color);
ImGui.SetNextItemWidth(buttonWidth);
- ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64);
+ ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref modMerger.OptionName, 64);
ImGuiUtil.HoverTooltip(
"The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n"
+ "A red border indicates an existing option, a blue border indicates a new one.");
}
- if (_modMerger.MergeFromMod.HasOptions)
+ if (modMerger.MergeFromMod.HasOptions)
ImGuiUtil.HoverTooltip("You can only specify a target option if the source mod has no true options itself.",
ImGuiHoveredFlags.AllowWhenDisabled);
if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0),
- _modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !_modMerger.CanMerge))
- _modMerger.Merge();
+ modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !modMerger.CanMerge))
+ modMerger.Merge();
}
private void DrawMergeIntoDesc()
{
- ImGuiUtil.TextWrapped(_modMerger.MergeFromMod!.HasOptions
+ ImGuiUtil.TextWrapped(modMerger.MergeFromMod!.HasOptions
? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break."
: "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted.");
}
@@ -110,7 +102,7 @@ public class ModMergeTab
{
_modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? "Select the target Mod...", string.Empty, width,
ImGui.GetTextLineHeight());
- _modMerger.MergeToMod = _modCombo.CurrentSelection;
+ modMerger.MergeToMod = _modCombo.CurrentSelection;
}
private void DrawSplitOff(float size)
@@ -121,24 +113,24 @@ public class ModMergeTab
ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique.");
var tt = _newModName.Length == 0
? "Please enter a name for the newly created mod first."
- : _modMerger.SelectedOptions.Count == 0
+ : modMerger.SelectedOptions.Count == 0
? "Please select at least one option to split off."
: string.Empty;
var buttonText =
- $"Split Off {_modMerger.SelectedOptions.Count} Option{(_modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff";
+ $"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff";
if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0))
- _modMerger.SplitIntoMod(_newModName);
+ modMerger.SplitIntoMod(_newModName);
ImGui.Dummy(Vector2.One);
var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0);
if (ImGui.Button("Select All", buttonSize))
- _modMerger.SelectedOptions.UnionWith(_modMerger.MergeFromMod!.AllSubMods);
+ modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllSubMods);
ImGui.SameLine();
if (ImGui.Button("Unselect All", buttonSize))
- _modMerger.SelectedOptions.Clear();
+ modMerger.SelectedOptions.Clear();
ImGui.SameLine();
if (ImGui.Button("Invert Selection", buttonSize))
- _modMerger.SelectedOptions.SymmetricExceptWith(_modMerger.MergeFromMod!.AllSubMods);
+ modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllSubMods);
DrawOptionTable(size);
}
@@ -152,8 +144,8 @@ public class ModMergeTab
private void DrawOptionTable(float size)
{
- var options = _modMerger.MergeFromMod!.AllSubMods.ToList();
- var height = _modMerger.Warnings.Count == 0 && _modMerger.Error == null
+ var options = modMerger.MergeFromMod!.AllSubMods.ToList();
+ var height = modMerger.Warnings.Count == 0 && modMerger.Error == null
? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing()
: 8 * ImGui.GetFrameHeightWithSpacing();
height = Math.Min(height, (options.Count + 1) * ImGui.GetFrameHeightWithSpacing());
@@ -178,15 +170,7 @@ public class ModMergeTab
foreach (var (option, idx) in options.WithIndex())
{
using var id = ImRaii.PushId(idx);
- var selected = _modMerger.SelectedOptions.Contains(option);
-
- void Handle(SubMod option2, bool selected2)
- {
- if (selected2)
- _modMerger.SelectedOptions.Add(option2);
- else
- _modMerger.SelectedOptions.Remove(option2);
- }
+ var selected = modMerger.SelectedOptions.Contains(option);
ImGui.TableNextColumn();
if (ImGui.Checkbox("##check", ref selected))
@@ -222,34 +206,43 @@ public class ModMergeTab
ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
ImGui.TableNextColumn();
ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
+ continue;
+
+ void Handle(SubMod option2, bool selected2)
+ {
+ if (selected2)
+ modMerger.SelectedOptions.Add(option2);
+ else
+ modMerger.SelectedOptions.Remove(option2);
+ }
}
}
private void DrawWarnings()
{
- if (_modMerger.Warnings.Count == 0)
+ if (modMerger.Warnings.Count == 0)
return;
ImGui.Separator();
ImGui.Dummy(Vector2.One);
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder);
- foreach (var warning in _modMerger.Warnings.SkipLast(1))
+ foreach (var warning in modMerger.Warnings.SkipLast(1))
{
ImGuiUtil.TextWrapped(warning);
ImGui.Separator();
}
- ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]);
+ ImGuiUtil.TextWrapped(modMerger.Warnings[^1]);
}
private void DrawError()
{
- if (_modMerger.Error == null)
+ if (modMerger.Error == null)
return;
ImGui.Separator();
ImGui.Dummy(Vector2.One);
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
- ImGuiUtil.TextWrapped(_modMerger.Error.ToString());
+ ImGuiUtil.TextWrapped(modMerger.Error.ToString());
}
}