Merge branch 'master' into mdl-export-materials

This commit is contained in:
Ottermandias 2024-01-14 13:17:36 +01:00
commit 7c83e30e9f
34 changed files with 353 additions and 355 deletions

@ -1 +1 @@
Subproject commit 1dad8d07047be0851f518cdac2b1c8bc76a7be98
Subproject commit 96e95378325ff1533ca41b934fcb712f24d5260b

View file

@ -72,7 +72,24 @@ public class GameState : IService
#endregion
/// <summary> Return the correct resolve data from the stored data. </summary>
#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>
public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData)
{
switch (type)

View file

@ -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<CharacterBase, CharacterBaseDestructor.Priority>, IHookService
{

View file

@ -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<Character, CharacterDestructor.Priority>, IHookService
{

View file

@ -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<Character, Character, CopyCharacter.Priority>, IHookService
{

View file

@ -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<ModelCharaId, CustomizeArray, CharacterArmor, CreateCharacterBase.Priority>, IHookService
{
@ -39,7 +39,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr<ModelCharaId, C
private CharacterBase* Detour(ModelCharaId model, CustomizeArray* customize, CharacterArmor* equipment, byte unk)
{
Penumbra.Log.Verbose($"[{Name}] Triggered with model: {model.Id}, customize: 0x{(nint) customize:X}, equipment: 0x{(nint)equipment:X}, unk: {unk}.");
Penumbra.Log.Verbose($"[{Name}] Triggered with model: {model.Id}, customize: 0x{(nint)customize:X}, equipment: 0x{(nint)equipment:X}, unk: {unk}.");
Invoke(&model, customize, equipment);
var ret = _task.Result.Original(model, customize, equipment, unk);
_postEvent.Invoke(model, customize, equipment, ret);

View file

@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.GameData;
namespace Penumbra.Interop.Hooks;
namespace Penumbra.Interop.Hooks.Objects;
/// <summary>
/// EnableDraw is what creates DrawObjects for gameObjects,
@ -12,12 +12,12 @@ namespace Penumbra.Interop.Hooks;
public sealed unsafe class EnableDraw : IHookService
{
private readonly Task<Hook<Delegate>> _task;
private readonly GameState _state;
private readonly GameState _state;
public EnableDraw(HookManager hooks, GameState state)
{
_state = state;
_task = hooks.CreateHook<Delegate>("Enable Draw", Sigs.EnableDraw, Detour, true);
_task = hooks.CreateHook<Delegate>("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();
}

View file

@ -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<DrawDataContainer, Character, CharacterWeapon, WeaponReload.Priority>, IHookService
{

View 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;
}
}

View 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;
}
}

View 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;
}
}

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

View file

@ -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<PerSlotResolveDelegate>(interop, vTable[83], ResolveDecal);
_resolveEidPathHook = Create<SingleResolveDelegate>(interop, vTable[85], ResolveEid);
_resolveImcPathHook = Create<PerSlotResolveDelegate>(interop, vTable[81], ResolveImc);
_resolveMPapPathHook = Create<MPapResolveDelegate>(interop, vTable[79], ResolveMPap);
_resolveMdlPathHook = Create<PerSlotResolveDelegate>(interop, vTable[73], type, ResolveMdl, ResolveMdlHuman);
_resolveMtrlPathHook = Create<NamedResolveDelegate>(interop, vTable[82], ResolveMtrl);
_resolvePapPathHook = Create<NamedResolveDelegate>(interop, vTable[76], type, ResolvePap, ResolvePapHuman);
_resolvePhybPathHook = Create<PerSlotResolveDelegate>(interop, vTable[75], type, ResolvePhyb, ResolvePhybHuman);
_resolveSklbPathHook = Create<PerSlotResolveDelegate>(interop, vTable[72], type, ResolveSklb, ResolveSklbHuman);
_resolveSkpPathHook = Create<PerSlotResolveDelegate>(interop, vTable[74], type, ResolveSkp, ResolveSkpHuman);
_resolveTmbPathHook = Create<TmbResolveDelegate>(interop, vTable[77], ResolveTmb);
_resolveVfxPathHook = Create<VfxResolveDelegate>(interop, vTable[84], ResolveVfx);
_parent = parent;
// @formatter:off
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[83], ResolveDecal);
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[85], ResolveEid);
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[81], ResolveImc);
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[79], ResolveMPap);
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[73], type, ResolveMdl, ResolveMdlHuman);
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[82], ResolveMtrl);
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[76], type, ResolvePap, ResolvePapHuman);
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[75], type, ResolvePhyb, ResolvePhybHuman);
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman);
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman);
_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()
@ -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<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
{
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<T> Create<T>(IGameInteropProvider interop, nint address, T del) where T : Delegate
=> interop.HookFromAddress(address, del);
private static Hook<T> Create<T>(string name, HookManager hooks, nint address, T del) where T : Delegate
=> hooks.CreateHook(name, address, del).Result;
// Implementation

View file

@ -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<ResourceHandle, ResourceHandleDestructor.Priority>, IHookService
{

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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,
};
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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> _resolveData = new(() => ResolveData.Invalid, true);
private readonly ThreadLocal<uint> _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;
}

View file

@ -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.
/// </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 ResourceHandleDestructor _resourceHandleDestructor;
private readonly CommunicatorService _communicator;
private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
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)
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<KeyValuePair<nint, ResolveData>> 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;
/// <summary>
/// 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)
{
case ResourceType.Tex when _mtrlData.Value.Valid:
case ResourceType.Shpk when _mtrlData.Value.Valid:
collection = _mtrlData.Value;
case ResourceType.Tex when _gameState.MtrlData.Value.Valid:
case ResourceType.Shpk when _gameState.MtrlData.Value.Valid:
collection = _gameState.MtrlData.Value;
return true;
case ResourceType.Scd when _avfxData.Value.Valid:
case ResourceType.Atex when _avfxData.Value.Valid:
collection = _avfxData.Value;
case ResourceType.Scd when _gameState.AvfxData.Value.Valid:
case ResourceType.Atex when _gameState.AvfxData.Value.Valid:
collection = _gameState.AvfxData.Value;
return true;
}
@ -105,11 +86,8 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePai
public void Dispose()
{
_loader.ResourceLoaded -= SubfileContainerRequested;
_loader.ResourceLoaded -= SubfileContainerRequested;
_resourceHandleDestructor.Unsubscribe(ResourceDestroyed);
_loadMtrlShpkHook.Dispose();
_loadMtrlTexHook.Dispose();
_apricotResourceLoadHook.Dispose();
}
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.Avfx:
if (handle->FileSize == 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<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;
}
=> _gameState.SubFileCollection.TryRemove((nint)handle, out _);
}

View file

@ -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;

View file

@ -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);
}
/// <summary>
/// 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
}
}
/// <summary> Deduplicate a mod simply by its directory without any confirmation or waiting time. </summary>
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);
}
}

View file

@ -1,11 +1,11 @@
using Penumbra.Mods.Subclasses;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
namespace Penumbra.Mods.Editor;
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 Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; }

View file

@ -29,12 +29,12 @@ public class ModMerger : IDisposable
public string OptionGroupName = "Merges";
public string OptionName = string.Empty;
private readonly Dictionary<string, string> _fileToFile = new();
private readonly HashSet<string> _createdDirectories = new();
private readonly HashSet<int> _createdGroups = new();
private readonly HashSet<SubMod> _createdOptions = new();
private readonly Dictionary<string, string> _fileToFile = [];
private readonly HashSet<string> _createdDirectories = [];
private readonly HashSet<int> _createdGroups = [];
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 Exception? Error { get; private set; }

View file

@ -79,7 +79,6 @@ public class Penumbra : IDalamudPlugin
_services.GetService<ModelResourceHandleUtility>(); // Initialize because not required anywhere else.
_collectionManager.Caches.CreateNecessaryCaches();
_services.GetService<PathResolver>();
_services.GetService<SkinFixer>();
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

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