mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 21:24:18 +01:00
Merge branch 'master' into mdl-export-materials
This commit is contained in:
commit
7c83e30e9f
34 changed files with 353 additions and 355 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1dad8d07047be0851f518cdac2b1c8bc76a7be98
|
Subproject commit 96e95378325ff1533ca41b934fcb712f24d5260b
|
||||||
|
|
@ -72,6 +72,23 @@ public class GameState : IService
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Subfiles
|
||||||
|
|
||||||
|
public readonly ThreadLocal<ResolveData> MtrlData = new(() => ResolveData.Invalid);
|
||||||
|
public readonly ThreadLocal<ResolveData> AvfxData = new(() => ResolveData.Invalid);
|
||||||
|
|
||||||
|
public readonly ConcurrentDictionary<nint, ResolveData> 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
|
||||||
|
|
||||||
/// <summary> Return the correct resolve data from the stored data. </summary>
|
/// <summary> Return the correct resolve data from the stored data. </summary>
|
||||||
public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData)
|
public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.UI.AdvancedWindow;
|
using Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks;
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBase, CharacterBaseDestructor.Priority>, IHookService
|
public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBase, CharacterBaseDestructor.Priority>, IHookService
|
||||||
{
|
{
|
||||||
|
|
@ -4,7 +4,7 @@ using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks;
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, CharacterDestructor.Priority>, IHookService
|
public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, CharacterDestructor.Priority>, IHookService
|
||||||
{
|
{
|
||||||
|
|
@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks;
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
public sealed unsafe class CopyCharacter : EventWrapperPtr<Character, Character, CopyCharacter.Priority>, IHookService
|
public sealed unsafe class CopyCharacter : EventWrapperPtr<Character, Character, CopyCharacter.Priority>, IHookService
|
||||||
{
|
{
|
||||||
|
|
@ -4,7 +4,7 @@ using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks;
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
public sealed unsafe class CreateCharacterBase : EventWrapperPtr<ModelCharaId, CustomizeArray, CharacterArmor, CreateCharacterBase.Priority>, IHookService
|
public sealed unsafe class CreateCharacterBase : EventWrapperPtr<ModelCharaId, CustomizeArray, CharacterArmor, CreateCharacterBase.Priority>, IHookService
|
||||||
{
|
{
|
||||||
|
|
@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks;
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EnableDraw is what creates DrawObjects for gameObjects,
|
/// EnableDraw is what creates DrawObjects for gameObjects,
|
||||||
|
|
@ -4,7 +4,7 @@ using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks;
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
public sealed unsafe class WeaponReload : EventWrapperPtr<DrawDataContainer, Character, CharacterWeapon, WeaponReload.Priority>, IHookService
|
public sealed unsafe class WeaponReload : EventWrapperPtr<DrawDataContainer, Character, CharacterWeapon, WeaponReload.Priority>, IHookService
|
||||||
{
|
{
|
||||||
28
Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs
Normal file
28
Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs
Normal file
|
|
@ -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<ApricotResourceLoad.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _gameState;
|
||||||
|
|
||||||
|
public ApricotResourceLoad(HookManager hooks, GameState gameState)
|
||||||
|
{
|
||||||
|
_gameState = gameState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("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;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs
Normal file
32
Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs
Normal file
|
|
@ -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<LoadMtrlShpk.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _gameState;
|
||||||
|
private readonly CommunicatorService _communicator;
|
||||||
|
|
||||||
|
public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator)
|
||||||
|
{
|
||||||
|
_gameState = gameState;
|
||||||
|
_communicator = communicator;
|
||||||
|
Task = hooks.CreateHook<Delegate>("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;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs
Normal file
28
Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs
Normal file
|
|
@ -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<LoadMtrlTex.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _gameState;
|
||||||
|
|
||||||
|
public LoadMtrlTex(HookManager hooks, GameState gameState)
|
||||||
|
{
|
||||||
|
_gameState = gameState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("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;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs
Normal file
38
Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
using Penumbra.Meta.Manipulations;
|
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
|
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 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 PerSlotResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex);
|
||||||
private delegate nint SingleResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize);
|
private delegate nint SingleResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize);
|
||||||
|
|
||||||
private delegate nint TmbResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, nint timelineName);
|
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.
|
// 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);
|
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;
|
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;
|
_parent = parent;
|
||||||
_resolveDecalPathHook = Create<PerSlotResolveDelegate>(interop, vTable[83], ResolveDecal);
|
// @formatter:off
|
||||||
_resolveEidPathHook = Create<SingleResolveDelegate>(interop, vTable[85], ResolveEid);
|
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[83], ResolveDecal);
|
||||||
_resolveImcPathHook = Create<PerSlotResolveDelegate>(interop, vTable[81], ResolveImc);
|
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[85], ResolveEid);
|
||||||
_resolveMPapPathHook = Create<MPapResolveDelegate>(interop, vTable[79], ResolveMPap);
|
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[81], ResolveImc);
|
||||||
_resolveMdlPathHook = Create<PerSlotResolveDelegate>(interop, vTable[73], type, ResolveMdl, ResolveMdlHuman);
|
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[79], ResolveMPap);
|
||||||
_resolveMtrlPathHook = Create<NamedResolveDelegate>(interop, vTable[82], ResolveMtrl);
|
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[73], type, ResolveMdl, ResolveMdlHuman);
|
||||||
_resolvePapPathHook = Create<NamedResolveDelegate>(interop, vTable[76], type, ResolvePap, ResolvePapHuman);
|
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[82], ResolveMtrl);
|
||||||
_resolvePhybPathHook = Create<PerSlotResolveDelegate>(interop, vTable[75], type, ResolvePhyb, ResolvePhybHuman);
|
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[76], type, ResolvePap, ResolvePapHuman);
|
||||||
_resolveSklbPathHook = Create<PerSlotResolveDelegate>(interop, vTable[72], type, ResolveSklb, ResolveSklbHuman);
|
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[75], type, ResolvePhyb, ResolvePhybHuman);
|
||||||
_resolveSkpPathHook = Create<PerSlotResolveDelegate>(interop, vTable[74], type, ResolveSkp, ResolveSkpHuman);
|
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman);
|
||||||
_resolveTmbPathHook = Create<TmbResolveDelegate>(interop, vTable[77], ResolveTmb);
|
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman);
|
||||||
_resolveVfxPathHook = Create<VfxResolveDelegate>(interop, vTable[84], ResolveVfx);
|
_resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb);
|
||||||
|
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], ResolveVfx);
|
||||||
|
// @formatter:on
|
||||||
|
Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Enable()
|
public void Enable()
|
||||||
|
|
@ -177,9 +183,8 @@ public unsafe class ResolvePathHooks : IDisposable
|
||||||
{
|
{
|
||||||
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
||||||
if (_parent.InInternalResolve)
|
if (_parent.InInternalResolve)
|
||||||
{
|
|
||||||
return DisposableContainer.Empty;
|
return DisposableContainer.Empty;
|
||||||
}
|
|
||||||
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face),
|
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.Body),
|
||||||
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair),
|
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair),
|
||||||
|
|
@ -188,19 +193,19 @@ public unsafe class ResolvePathHooks : IDisposable
|
||||||
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
private static Hook<T> Create<T>(IGameInteropProvider interop, nint address, Type type, T other, T human) where T : Delegate
|
private static Hook<T> Create<T>(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate
|
||||||
{
|
{
|
||||||
var del = type switch
|
var del = type switch
|
||||||
{
|
{
|
||||||
Type.Human => human,
|
Type.Human => human,
|
||||||
_ => other,
|
_ => other,
|
||||||
};
|
};
|
||||||
return interop.HookFromAddress(address, del);
|
return hooks.CreateHook(name, address, del).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
private static Hook<T> Create<T>(IGameInteropProvider interop, nint address, T del) where T : Delegate
|
private static Hook<T> Create<T>(string name, HookManager hooks, nint address, T del) where T : Delegate
|
||||||
=> interop.HookFromAddress(address, del);
|
=> hooks.CreateHook(name, address, del).Result;
|
||||||
|
|
||||||
|
|
||||||
// Implementation
|
// Implementation
|
||||||
|
|
@ -2,10 +2,9 @@ using Dalamud.Hooking;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
using Penumbra.Interop.Services;
|
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks;
|
namespace Penumbra.Interop.Hooks.Resources;
|
||||||
|
|
||||||
public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr<ResourceHandle, ResourceHandleDestructor.Priority>, IHookService
|
public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr<ResourceHandle, ResourceHandleDestructor.Priority>, IHookService
|
||||||
{
|
{
|
||||||
|
|
@ -16,11 +16,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
private readonly Texture** _colorTableTexture;
|
private readonly Texture** _colorTableTexture;
|
||||||
private readonly SafeTextureHandle _originalColorTableTexture;
|
private readonly SafeTextureHandle _originalColorTableTexture;
|
||||||
|
|
||||||
private Half[] _colorTable;
|
|
||||||
private bool _updatePending;
|
private bool _updatePending;
|
||||||
|
|
||||||
public Half[] ColorTable
|
public Half[] ColorTable { get; }
|
||||||
=> _colorTable;
|
|
||||||
|
|
||||||
public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, MaterialInfo materialInfo)
|
public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, MaterialInfo materialInfo)
|
||||||
: base(objects, materialInfo)
|
: base(objects, materialInfo)
|
||||||
|
|
@ -41,7 +39,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
if (_originalColorTableTexture == null)
|
if (_originalColorTableTexture == null)
|
||||||
throw new InvalidOperationException("Material doesn't have a color table");
|
throw new InvalidOperationException("Material doesn't have a color table");
|
||||||
|
|
||||||
_colorTable = new Half[TextureLength];
|
ColorTable = new Half[TextureLength];
|
||||||
_updatePending = true;
|
_updatePending = true;
|
||||||
|
|
||||||
framework.Update += OnFrameworkUpdate;
|
framework.Update += OnFrameworkUpdate;
|
||||||
|
|
@ -84,9 +82,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool success;
|
bool success;
|
||||||
lock (_colorTable)
|
lock (ColorTable)
|
||||||
{
|
{
|
||||||
fixed (Half* colorTable = _colorTable)
|
fixed (Half* colorTable = ColorTable)
|
||||||
{
|
{
|
||||||
success = texture.Texture->InitializeContents(colorTable);
|
success = texture.Texture->InitializeContents(colorTable);
|
||||||
}
|
}
|
||||||
|
|
@ -105,9 +103,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
if (colorSetTextures == null)
|
if (colorSetTextures == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (_colorTableTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot))
|
return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,34 +26,29 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
if (_shaderPackage == null)
|
if (_shaderPackage == null)
|
||||||
throw new InvalidOperationException("Material doesn't have a shader package");
|
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)
|
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)
|
protected override void Clear(bool disposing, bool reset)
|
||||||
{
|
{
|
||||||
base.Clear(disposing, reset);
|
base.Clear(disposing, reset);
|
||||||
|
|
||||||
if (reset)
|
if (!reset)
|
||||||
{
|
return;
|
||||||
var material = Material;
|
|
||||||
|
|
||||||
material->ShaderFlags = _originalShPkFlags;
|
Material->ShaderFlags = _originalShPkFlags;
|
||||||
|
var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer();
|
||||||
var materialParameter = material->MaterialParameterCBuffer->TryGetBuffer();
|
|
||||||
if (!materialParameter.IsEmpty)
|
if (!materialParameter.IsEmpty)
|
||||||
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
|
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
|
||||||
|
|
||||||
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
|
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
|
||||||
material->Textures[i].SamplerFlags = _originalSamplerFlags[i];
|
Material->Textures[i].SamplerFlags = _originalSamplerFlags[i];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetShaderPackageFlags(uint shPkFlags)
|
public void SetShaderPackageFlags(uint shPkFlags)
|
||||||
|
|
@ -80,8 +75,9 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i)
|
for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i)
|
||||||
{
|
{
|
||||||
ref var parameter = ref _shaderPackage->MaterialElementsSpan[i];
|
ref var parameter = ref _shaderPackage->MaterialElementsSpan[i];
|
||||||
if (parameter.CRC == parameterCrc)
|
if (parameter.CRC != parameterCrc)
|
||||||
{
|
continue;
|
||||||
|
|
||||||
if ((parameter.Offset & 0x3) != 0
|
if ((parameter.Offset & 0x3) != 0
|
||||||
|| (parameter.Size & 0x3) != 0
|
|| (parameter.Size & 0x3) != 0
|
||||||
|| (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
|
|| (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
|
||||||
|
|
@ -91,7 +87,6 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
|
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
|
||||||
{
|
{
|
||||||
|
|
@ -104,27 +99,26 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
var samplers = _shaderPackage->Samplers;
|
var samplers = _shaderPackage->Samplers;
|
||||||
for (var i = 0; i < _shaderPackage->SamplerCount; ++i)
|
for (var i = 0; i < _shaderPackage->SamplerCount; ++i)
|
||||||
{
|
{
|
||||||
if (samplers[i].CRC == samplerCrc)
|
if (samplers[i].CRC != samplerCrc)
|
||||||
{
|
continue;
|
||||||
|
|
||||||
id = samplers[i].Id;
|
id = samplers[i].Id;
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
if (!found)
|
||||||
return;
|
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)
|
if (Material->Textures[i].Id != id)
|
||||||
{
|
continue;
|
||||||
material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0;
|
|
||||||
|
Material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool IsStillValid()
|
protected override bool IsStillValid()
|
||||||
{
|
{
|
||||||
|
|
@ -139,9 +133,6 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
if (shpkHandle == null)
|
if (shpkHandle == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (_shaderPackage != shpkHandle->ShaderPackage)
|
return _shaderPackage == shpkHandle->ShaderPackage;
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,6 @@ public abstract unsafe class LiveMaterialPreviewerBase : IDisposable
|
||||||
if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject))
|
if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject))
|
return Material == MaterialInfo.GetDrawObjectMaterial(DrawObject);
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,22 +24,6 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
|
||||||
public nint GetDrawObject(nint address)
|
public nint GetDrawObject(nint address)
|
||||||
=> GetDrawObject(Type, 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)
|
public unsafe Material* GetDrawObjectMaterial(IObjectTable objects)
|
||||||
=> GetDrawObjectMaterial((CharacterBase*)GetDrawObject(GetCharacter(objects)));
|
=> GetDrawObjectMaterial((CharacterBase*)GetDrawObject(GetCharacter(objects)));
|
||||||
|
|
||||||
|
|
@ -103,4 +87,20 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
|
||||||
|
|
||||||
return result;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Interop.Hooks;
|
using Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using OtterGui.Services;
|
||||||
using Penumbra.Interop.Hooks;
|
using Penumbra.Interop.Hooks;
|
||||||
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
|
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.Interop.Hooks;
|
using Penumbra.Interop.Hooks.Objects;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ using Penumbra.Collections;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.Hooks;
|
|
||||||
using Penumbra.Interop.ResourceLoading;
|
using Penumbra.Interop.ResourceLoading;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
|
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
|
||||||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||||
|
using Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,15 @@
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.GameData;
|
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
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)]
|
public readonly CollectionResolver CollectionResolver = collectionResolver;
|
||||||
private readonly nint* _humanVTable = null!;
|
public readonly MetaState MetaState = metaState;
|
||||||
|
public readonly CharacterUtility CharacterUtility = characterUtility;
|
||||||
[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;
|
|
||||||
|
|
||||||
private readonly ThreadLocal<ResolveData> _resolveData = new(() => ResolveData.Invalid, true);
|
private readonly ThreadLocal<ResolveData> _resolveData = new(() => ResolveData.Invalid, true);
|
||||||
private readonly ThreadLocal<uint> _internalResolve = new(() => 0, false);
|
private readonly ThreadLocal<uint> _internalResolve = new(() => 0, false);
|
||||||
|
|
@ -39,31 +20,11 @@ public unsafe class PathState : IDisposable
|
||||||
public bool InInternalResolve
|
public bool InInternalResolve
|
||||||
=> _internalResolve.Value != 0u;
|
=> _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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resolveData.Dispose();
|
_resolveData.Dispose();
|
||||||
_internalResolve.Dispose();
|
_internalResolve.Dispose();
|
||||||
_human.Dispose();
|
|
||||||
_weapon.Dispose();
|
|
||||||
_demiHuman.Dispose();
|
|
||||||
_monster.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Consume(ByteString _, out ResolveData collection)
|
public bool Consume(ByteString _, out ResolveData collection)
|
||||||
|
|
@ -86,9 +47,7 @@ public unsafe class PathState : IDisposable
|
||||||
return path;
|
return path;
|
||||||
|
|
||||||
if (!InInternalResolve)
|
if (!InInternalResolve)
|
||||||
{
|
|
||||||
_resolveData.Value = collection.ToResolveData(gameObject);
|
_resolveData.Value = collection.ToResolveData(gameObject);
|
||||||
}
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,9 +58,7 @@ public unsafe class PathState : IDisposable
|
||||||
return path;
|
return path;
|
||||||
|
|
||||||
if (!InInternalResolve)
|
if (!InInternalResolve)
|
||||||
{
|
|
||||||
_resolveData.Value = data;
|
_resolveData.Value = data;
|
||||||
}
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,7 +83,7 @@ public unsafe class PathState : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
public readonly void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
--_internalResolve.Value;
|
--_internalResolve.Value;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.GameData;
|
using Penumbra.Interop.Hooks.Resources;
|
||||||
using Penumbra.Interop.Hooks;
|
|
||||||
using Penumbra.Interop.ResourceLoading;
|
using Penumbra.Interop.ResourceLoading;
|
||||||
using Penumbra.Interop.Services;
|
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.Services;
|
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
|
@ -20,49 +13,37 @@ namespace Penumbra.Interop.PathResolving;
|
||||||
/// Those are loaded synchronously.
|
/// Those are loaded synchronously.
|
||||||
/// Thus, we need to ensure the correct files are loaded when a material is loaded.
|
/// Thus, we need to ensure the correct files are loaded when a material is loaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
|
public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
|
||||||
{
|
{
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly GameState _gameState;
|
||||||
private readonly ResourceLoader _loader;
|
private readonly ResourceLoader _loader;
|
||||||
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
||||||
private readonly CommunicatorService _communicator;
|
|
||||||
|
|
||||||
private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
|
public SubfileHelper(GameState gameState, ResourceLoader loader, ResourceHandleDestructor resourceHandleDestructor)
|
||||||
private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<nint, ResolveData> _subFileCollection = new();
|
|
||||||
|
|
||||||
public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, CommunicatorService communicator, IGameInteropProvider interop, ResourceHandleDestructor resourceHandleDestructor)
|
|
||||||
{
|
{
|
||||||
interop.InitializeFromAttributes(this);
|
_gameState = gameState;
|
||||||
|
|
||||||
_performance = performance;
|
|
||||||
_loader = loader;
|
_loader = loader;
|
||||||
_communicator = communicator;
|
|
||||||
_resourceHandleDestructor = resourceHandleDestructor;
|
_resourceHandleDestructor = resourceHandleDestructor;
|
||||||
|
|
||||||
_loadMtrlShpkHook.Enable();
|
|
||||||
_loadMtrlTexHook.Enable();
|
|
||||||
_apricotResourceLoadHook.Enable();
|
|
||||||
_loader.ResourceLoaded += SubfileContainerRequested;
|
_loader.ResourceLoaded += SubfileContainerRequested;
|
||||||
_resourceHandleDestructor.Subscribe(ResourceDestroyed, ResourceHandleDestructor.Priority.SubfileHelper);
|
_resourceHandleDestructor.Subscribe(ResourceDestroyed, ResourceHandleDestructor.Priority.SubfileHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<nint, ResolveData>> GetEnumerator()
|
public IEnumerator<KeyValuePair<nint, ResolveData>> GetEnumerator()
|
||||||
=> _subFileCollection.GetEnumerator();
|
=> _gameState.SubFileCollection.GetEnumerator();
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
=> GetEnumerator();
|
=> GetEnumerator();
|
||||||
|
|
||||||
public int Count
|
public int Count
|
||||||
=> _subFileCollection.Count;
|
=> _gameState.SubFileCollection.Count;
|
||||||
|
|
||||||
public ResolveData MtrlData
|
public ResolveData MtrlData
|
||||||
=> _mtrlData.IsValueCreated ? _mtrlData.Value : ResolveData.Invalid;
|
=> _gameState.MtrlData.IsValueCreated ? _gameState.MtrlData.Value : ResolveData.Invalid;
|
||||||
|
|
||||||
public ResolveData AvfxData
|
public ResolveData AvfxData
|
||||||
=> _avfxData.IsValueCreated ? _avfxData.Value : ResolveData.Invalid;
|
=> _gameState.AvfxData.IsValueCreated ? _gameState.AvfxData.Value : ResolveData.Invalid;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check specifically for shpk and tex files whether we are currently in a material load,
|
/// Check specifically for shpk and tex files whether we are currently in a material load,
|
||||||
|
|
@ -71,13 +52,13 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePai
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ResourceType.Tex when _mtrlData.Value.Valid:
|
case ResourceType.Tex when _gameState.MtrlData.Value.Valid:
|
||||||
case ResourceType.Shpk when _mtrlData.Value.Valid:
|
case ResourceType.Shpk when _gameState.MtrlData.Value.Valid:
|
||||||
collection = _mtrlData.Value;
|
collection = _gameState.MtrlData.Value;
|
||||||
return true;
|
return true;
|
||||||
case ResourceType.Scd when _avfxData.Value.Valid:
|
case ResourceType.Scd when _gameState.AvfxData.Value.Valid:
|
||||||
case ResourceType.Atex when _avfxData.Value.Valid:
|
case ResourceType.Atex when _gameState.AvfxData.Value.Valid:
|
||||||
collection = _avfxData.Value;
|
collection = _gameState.AvfxData.Value;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,9 +88,6 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePai
|
||||||
{
|
{
|
||||||
_loader.ResourceLoaded -= SubfileContainerRequested;
|
_loader.ResourceLoaded -= SubfileContainerRequested;
|
||||||
_resourceHandleDestructor.Unsubscribe(ResourceDestroyed);
|
_resourceHandleDestructor.Unsubscribe(ResourceDestroyed);
|
||||||
_loadMtrlShpkHook.Dispose();
|
|
||||||
_loadMtrlTexHook.Dispose();
|
|
||||||
_apricotResourceLoadHook.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
|
private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
|
||||||
|
|
@ -120,66 +98,12 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePai
|
||||||
case ResourceType.Mtrl:
|
case ResourceType.Mtrl:
|
||||||
case ResourceType.Avfx:
|
case ResourceType.Avfx:
|
||||||
if (handle->FileSize == 0)
|
if (handle->FileSize == 0)
|
||||||
_subFileCollection[(nint)handle] = resolveData;
|
_gameState.SubFileCollection[(nint)handle] = resolveData;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResourceDestroyed(ResourceHandle* handle)
|
private void ResourceDestroyed(ResourceHandle* handle)
|
||||||
=> _subFileCollection.TryRemove((nint)handle, out _);
|
=> _gameState.SubFileCollection.TryRemove((nint)handle, out _);
|
||||||
|
|
||||||
private delegate byte LoadMtrlFilesDelegate(nint mtrlResourceHandle);
|
|
||||||
|
|
||||||
[Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))]
|
|
||||||
private readonly Hook<LoadMtrlFilesDelegate> _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<LoadMtrlFilesDelegate> _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<ApricotResourceLoadDelegate> _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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
using Penumbra.Interop.Hooks;
|
using Penumbra.Interop.Hooks.Resources;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Services;
|
namespace Penumbra.Interop.Services;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using OtterGui.Services;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Mods.Subclasses;
|
using Penumbra.Mods.Subclasses;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
@ -5,25 +6,15 @@ using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Mods.Editor;
|
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 SHA256 _hasher = SHA256.Create();
|
||||||
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
|
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = [];
|
||||||
|
|
||||||
public DuplicateManager(ModManager modManager, SaveService saveService, Configuration config)
|
|
||||||
{
|
|
||||||
_modManager = modManager;
|
|
||||||
_saveService = saveService;
|
|
||||||
_config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates
|
public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates
|
||||||
=> _duplicates;
|
=> _duplicates;
|
||||||
|
|
||||||
public long SavedSpace { get; private set; } = 0;
|
public long SavedSpace { get; private set; }
|
||||||
public Task Worker { get; private set; } = Task.CompletedTask;
|
public Task Worker { get; private set; } = Task.CompletedTask;
|
||||||
|
|
||||||
private CancellationTokenSource _cancellationTokenSource = new();
|
private CancellationTokenSource _cancellationTokenSource = new();
|
||||||
|
|
@ -68,6 +59,19 @@ public class DuplicateManager
|
||||||
|
|
||||||
private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager)
|
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)
|
void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx)
|
||||||
{
|
{
|
||||||
var changes = false;
|
var changes = false;
|
||||||
|
|
@ -78,26 +82,15 @@ public class DuplicateManager
|
||||||
|
|
||||||
if (useModManager)
|
if (useModManager)
|
||||||
{
|
{
|
||||||
_modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict);
|
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var sub = (SubMod)subMod;
|
var sub = (SubMod)subMod;
|
||||||
sub.FileData = dict;
|
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)
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recursively delete all empty directories starting from the given directory.
|
/// Recursively delete all empty directories starting from the given directory.
|
||||||
/// Deletes inner directories first, so that a tree of empty directories is actually deleted.
|
/// Deletes inner directories first, so that a tree of empty directories is actually deleted.
|
||||||
|
|
@ -232,14 +216,13 @@ public class DuplicateManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary> Deduplicate a mod simply by its directory without any confirmation or waiting time. </summary>
|
/// <summary> Deduplicate a mod simply by its directory without any confirmation or waiting time. </summary>
|
||||||
internal void DeduplicateMod(DirectoryInfo modDirectory)
|
internal void DeduplicateMod(DirectoryInfo modDirectory)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var mod = new Mod(modDirectory);
|
var mod = new Mod(modDirectory);
|
||||||
_modManager.Creator.ReloadMod(mod, true, out _);
|
modManager.Creator.ReloadMod(mod, true, out _);
|
||||||
|
|
||||||
Clear();
|
Clear();
|
||||||
var files = new ModFileCollection();
|
var files = new ModFileCollection();
|
||||||
|
|
@ -252,4 +235,13 @@ public class DuplicateManager
|
||||||
Penumbra.Log.Warning($"Could not deduplicate mod {modDirectory.Name}:\n{e}");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
using Penumbra.Mods.Subclasses;
|
using Penumbra.Mods.Subclasses;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods.Editor;
|
||||||
|
|
||||||
public class FileRegistry : IEquatable<FileRegistry>
|
public class FileRegistry : IEquatable<FileRegistry>
|
||||||
{
|
{
|
||||||
public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = new();
|
public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = [];
|
||||||
public FullPath File { get; private init; }
|
public FullPath File { get; private init; }
|
||||||
public Utf8RelPath RelPath { get; private init; }
|
public Utf8RelPath RelPath { get; private init; }
|
||||||
public long FileSize { get; private init; }
|
public long FileSize { get; private init; }
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ public class ModMerger : IDisposable
|
||||||
public string OptionGroupName = "Merges";
|
public string OptionGroupName = "Merges";
|
||||||
public string OptionName = string.Empty;
|
public string OptionName = string.Empty;
|
||||||
|
|
||||||
private readonly Dictionary<string, string> _fileToFile = new();
|
private readonly Dictionary<string, string> _fileToFile = [];
|
||||||
private readonly HashSet<string> _createdDirectories = new();
|
private readonly HashSet<string> _createdDirectories = [];
|
||||||
private readonly HashSet<int> _createdGroups = new();
|
private readonly HashSet<int> _createdGroups = [];
|
||||||
private readonly HashSet<SubMod> _createdOptions = new();
|
private readonly HashSet<SubMod> _createdOptions = [];
|
||||||
|
|
||||||
public readonly HashSet<SubMod> SelectedOptions = new();
|
public readonly HashSet<SubMod> SelectedOptions = [];
|
||||||
|
|
||||||
public readonly IReadOnlyList<string> Warnings = new List<string>();
|
public readonly IReadOnlyList<string> Warnings = new List<string>();
|
||||||
public Exception? Error { get; private set; }
|
public Exception? Error { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ public class Penumbra : IDalamudPlugin
|
||||||
_services.GetService<ModelResourceHandleUtility>(); // Initialize because not required anywhere else.
|
_services.GetService<ModelResourceHandleUtility>(); // Initialize because not required anywhere else.
|
||||||
_collectionManager.Caches.CreateNecessaryCaches();
|
_collectionManager.Caches.CreateNecessaryCaches();
|
||||||
_services.GetService<PathResolver>();
|
_services.GetService<PathResolver>();
|
||||||
|
|
||||||
_services.GetService<SkinFixer>();
|
_services.GetService<SkinFixer>();
|
||||||
|
|
||||||
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using OtterGui.Raii;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using OtterGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.Subclasses;
|
using Penumbra.Mods.Subclasses;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ using OtterGui.Raii;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.Hooks;
|
using Penumbra.Interop.Hooks.Objects;
|
||||||
using Penumbra.Interop.MaterialPreview;
|
using Penumbra.Interop.MaterialPreview;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Import.Models;
|
using Penumbra.Import.Models;
|
||||||
using Penumbra.Import.Textures;
|
using Penumbra.Import.Textures;
|
||||||
using Penumbra.Interop.Hooks;
|
using Penumbra.Interop.Hooks.Objects;
|
||||||
using Penumbra.Interop.ResourceTree;
|
using Penumbra.Interop.ResourceTree;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,14 @@ using Penumbra.UI.Classes;
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
public class ModMergeTab
|
public class ModMergeTab(ModMerger modMerger)
|
||||||
{
|
{
|
||||||
private readonly ModMerger _modMerger;
|
private readonly ModCombo _modCombo = new(() => modMerger.ModsWithoutCurrent.ToList());
|
||||||
private readonly ModCombo _modCombo;
|
|
||||||
|
|
||||||
private string _newModName = string.Empty;
|
private string _newModName = string.Empty;
|
||||||
|
|
||||||
public ModMergeTab(ModMerger modMerger)
|
|
||||||
{
|
|
||||||
_modMerger = modMerger;
|
|
||||||
_modCombo = new ModCombo(() => _modMerger.ModsWithoutCurrent.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
if (_modMerger.MergeFromMod == null)
|
if (modMerger.MergeFromMod == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using var tab = ImRaii.TabItem("Merge Mods");
|
using var tab = ImRaii.TabItem("Merge Mods");
|
||||||
|
|
@ -54,23 +46,23 @@ public class ModMergeTab
|
||||||
{
|
{
|
||||||
using var bigGroup = ImRaii.Group();
|
using var bigGroup = ImRaii.Group();
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod!.Name} into ");
|
ImGui.TextUnformatted($"Merge {modMerger.MergeFromMod!.Name} into ");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X);
|
DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X);
|
||||||
|
|
||||||
var width = ImGui.GetItemRectSize();
|
var width = ImGui.GetItemRectSize();
|
||||||
using (var g = ImRaii.Group())
|
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;
|
var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2;
|
||||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1);
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1);
|
||||||
var group = _modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == _modMerger.OptionGroupName);
|
var group = modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == modMerger.OptionGroupName);
|
||||||
var color = group != null || _modMerger.OptionGroupName.Length == 0 && _modMerger.OptionName.Length == 0
|
var color = group != null || modMerger.OptionGroupName.Length == 0 && modMerger.OptionName.Length == 0
|
||||||
? Colors.PressEnterWarningBg
|
? Colors.PressEnterWarningBg
|
||||||
: Colors.DiscordColor;
|
: Colors.DiscordColor;
|
||||||
using var c = ImRaii.PushColor(ImGuiCol.Border, color);
|
using var c = ImRaii.PushColor(ImGuiCol.Border, color);
|
||||||
ImGui.SetNextItemWidth(buttonWidth);
|
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(
|
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"
|
"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.");
|
+ "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
|
color = color == Colors.DiscordColor
|
||||||
? 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.PressEnterWarningBg
|
||||||
: Colors.DiscordColor;
|
: Colors.DiscordColor;
|
||||||
c.Push(ImGuiCol.Border, color);
|
c.Push(ImGuiCol.Border, color);
|
||||||
ImGui.SetNextItemWidth(buttonWidth);
|
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(
|
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"
|
"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.");
|
+ "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.",
|
ImGuiUtil.HoverTooltip("You can only specify a target option if the source mod has no true options itself.",
|
||||||
ImGuiHoveredFlags.AllowWhenDisabled);
|
ImGuiHoveredFlags.AllowWhenDisabled);
|
||||||
|
|
||||||
if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0),
|
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.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !modMerger.CanMerge))
|
||||||
_modMerger.Merge();
|
modMerger.Merge();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMergeIntoDesc()
|
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 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.");
|
: "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,
|
_modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? "Select the target Mod...", string.Empty, width,
|
||||||
ImGui.GetTextLineHeight());
|
ImGui.GetTextLineHeight());
|
||||||
_modMerger.MergeToMod = _modCombo.CurrentSelection;
|
modMerger.MergeToMod = _modCombo.CurrentSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawSplitOff(float size)
|
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.");
|
ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique.");
|
||||||
var tt = _newModName.Length == 0
|
var tt = _newModName.Length == 0
|
||||||
? "Please enter a name for the newly created mod first."
|
? "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."
|
? "Please select at least one option to split off."
|
||||||
: string.Empty;
|
: string.Empty;
|
||||||
var buttonText =
|
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))
|
if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0))
|
||||||
_modMerger.SplitIntoMod(_newModName);
|
modMerger.SplitIntoMod(_newModName);
|
||||||
|
|
||||||
ImGui.Dummy(Vector2.One);
|
ImGui.Dummy(Vector2.One);
|
||||||
var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0);
|
var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0);
|
||||||
if (ImGui.Button("Select All", buttonSize))
|
if (ImGui.Button("Select All", buttonSize))
|
||||||
_modMerger.SelectedOptions.UnionWith(_modMerger.MergeFromMod!.AllSubMods);
|
modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllSubMods);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Unselect All", buttonSize))
|
if (ImGui.Button("Unselect All", buttonSize))
|
||||||
_modMerger.SelectedOptions.Clear();
|
modMerger.SelectedOptions.Clear();
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Invert Selection", buttonSize))
|
if (ImGui.Button("Invert Selection", buttonSize))
|
||||||
_modMerger.SelectedOptions.SymmetricExceptWith(_modMerger.MergeFromMod!.AllSubMods);
|
modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllSubMods);
|
||||||
DrawOptionTable(size);
|
DrawOptionTable(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,8 +144,8 @@ public class ModMergeTab
|
||||||
|
|
||||||
private void DrawOptionTable(float size)
|
private void DrawOptionTable(float size)
|
||||||
{
|
{
|
||||||
var options = _modMerger.MergeFromMod!.AllSubMods.ToList();
|
var options = modMerger.MergeFromMod!.AllSubMods.ToList();
|
||||||
var height = _modMerger.Warnings.Count == 0 && _modMerger.Error == null
|
var height = modMerger.Warnings.Count == 0 && modMerger.Error == null
|
||||||
? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing()
|
? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing()
|
||||||
: 8 * ImGui.GetFrameHeightWithSpacing();
|
: 8 * ImGui.GetFrameHeightWithSpacing();
|
||||||
height = Math.Min(height, (options.Count + 1) * 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())
|
foreach (var (option, idx) in options.WithIndex())
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId(idx);
|
using var id = ImRaii.PushId(idx);
|
||||||
var selected = _modMerger.SelectedOptions.Contains(option);
|
var selected = modMerger.SelectedOptions.Contains(option);
|
||||||
|
|
||||||
void Handle(SubMod option2, bool selected2)
|
|
||||||
{
|
|
||||||
if (selected2)
|
|
||||||
_modMerger.SelectedOptions.Add(option2);
|
|
||||||
else
|
|
||||||
_modMerger.SelectedOptions.Remove(option2);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
if (ImGui.Checkbox("##check", ref selected))
|
if (ImGui.Checkbox("##check", ref selected))
|
||||||
|
|
@ -222,34 +206,43 @@ public class ModMergeTab
|
||||||
ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
|
ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
|
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()
|
private void DrawWarnings()
|
||||||
{
|
{
|
||||||
if (_modMerger.Warnings.Count == 0)
|
if (modMerger.Warnings.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Dummy(Vector2.One);
|
ImGui.Dummy(Vector2.One);
|
||||||
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder);
|
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);
|
ImGuiUtil.TextWrapped(warning);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]);
|
ImGuiUtil.TextWrapped(modMerger.Warnings[^1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawError()
|
private void DrawError()
|
||||||
{
|
{
|
||||||
if (_modMerger.Error == null)
|
if (modMerger.Error == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Dummy(Vector2.One);
|
ImGui.Dummy(Vector2.One);
|
||||||
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
|
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
|
||||||
ImGuiUtil.TextWrapped(_modMerger.Error.ToString());
|
ImGuiUtil.TextWrapped(modMerger.Error.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue