mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-13 12:14:18 +01:00
start
This commit is contained in:
parent
0e3d3d1839
commit
447e748ed7
9 changed files with 906 additions and 4 deletions
|
|
@ -1,6 +1,4 @@
|
||||||
using Newtonsoft.Json;
|
namespace Glamourer.GameData;
|
||||||
|
|
||||||
namespace Glamourer.GameData;
|
|
||||||
|
|
||||||
public readonly struct CustomizeParameterValue
|
public readonly struct CustomizeParameterValue
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using Glamourer.Designs;
|
||||||
using Glamourer.Gui.Customization;
|
using Glamourer.Gui.Customization;
|
||||||
using Glamourer.Gui.Equipment;
|
using Glamourer.Gui.Equipment;
|
||||||
using Glamourer.Interop;
|
using Glamourer.Interop;
|
||||||
|
using Glamourer.Interop.Material;
|
||||||
using Glamourer.Interop.Structs;
|
using Glamourer.Interop.Structs;
|
||||||
using Glamourer.State;
|
using Glamourer.State;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
@ -97,6 +98,12 @@ public class ActorPanel(
|
||||||
return (_selector.IncognitoMode ? _identifier.Incognito(null) : _identifier.ToString(), Actor.Null);
|
return (_selector.IncognitoMode ? _identifier.Incognito(null) : _identifier.ToString(), Actor.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector3 _test;
|
||||||
|
private int _rowId;
|
||||||
|
private MaterialValueIndex.ColorTableIndex _index;
|
||||||
|
private int _materialId;
|
||||||
|
private int _slotId;
|
||||||
|
|
||||||
private unsafe void DrawPanel()
|
private unsafe void DrawPanel()
|
||||||
{
|
{
|
||||||
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
|
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
|
||||||
|
|
@ -114,6 +121,17 @@ public class ActorPanel(
|
||||||
|
|
||||||
RevertButtons();
|
RevertButtons();
|
||||||
|
|
||||||
|
ImGui.InputInt("Row", ref _rowId);
|
||||||
|
ImGui.InputInt("Material", ref _materialId);
|
||||||
|
ImGui.InputInt("Slot", ref _slotId);
|
||||||
|
ImGuiUtil.GenericEnumCombo("Value", 300, _index, out _index);
|
||||||
|
|
||||||
|
var index = new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte) _slotId, (byte) _materialId, (byte)_rowId, _index);
|
||||||
|
index.TryGetValue(_actor, out _test);
|
||||||
|
if (ImGui.ColorPicker3("TestPicker", ref _test) && _actor.Valid)
|
||||||
|
MaterialService.Test(_actor, index, _test);
|
||||||
|
|
||||||
|
|
||||||
using var disabled = ImRaii.Disabled(transformationId != 0);
|
using var disabled = ImRaii.Disabled(transformationId != 0);
|
||||||
if (_state.ModelData.IsHuman)
|
if (_state.ModelData.IsHuman)
|
||||||
DrawHumanPanel();
|
DrawHumanPanel();
|
||||||
|
|
|
||||||
116
Glamourer/Interop/Material/DirectXTextureHelper.cs
Normal file
116
Glamourer/Interop/Material/DirectXTextureHelper.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
|
using Penumbra.GameData.Files;
|
||||||
|
using Penumbra.String.Functions;
|
||||||
|
using SharpGen.Runtime;
|
||||||
|
using Vortice.Direct3D11;
|
||||||
|
using Vortice.DXGI;
|
||||||
|
using MapFlags = Vortice.Direct3D11.MapFlags;
|
||||||
|
|
||||||
|
namespace Glamourer.Interop.Material;
|
||||||
|
|
||||||
|
public static unsafe class DirectXTextureHelper
|
||||||
|
{
|
||||||
|
/// <summary> Try to turn a color table GPU-loaded texture (R16G16B16A16Float, 4 Width, 16 Height) into an actual color table. </summary>
|
||||||
|
/// <param name="texture"> A pointer to the internal texture struct containing the GPU handle. </param>
|
||||||
|
/// <param name="table"> The returned color table. </param>
|
||||||
|
/// <returns> Whether the table could be fetched. </returns>
|
||||||
|
public static bool TryGetColorTable(Texture* texture, out MtrlFile.ColorTable table)
|
||||||
|
{
|
||||||
|
if (texture == null)
|
||||||
|
{
|
||||||
|
table = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create direct x resource and ensure that it is kept alive.
|
||||||
|
using var tex = new ID3D11Texture2D1((nint)texture->D3D11Texture2D);
|
||||||
|
tex.AddRef();
|
||||||
|
|
||||||
|
table = GetResourceData(tex, CreateStagedClone, GetTextureData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Create a staging clone of the existing texture handle for stability reasons. </summary>
|
||||||
|
private static ID3D11Texture2D1 CreateStagedClone(ID3D11Texture2D1 resource)
|
||||||
|
{
|
||||||
|
var desc = resource.Description1 with
|
||||||
|
{
|
||||||
|
Usage = ResourceUsage.Staging,
|
||||||
|
BindFlags = 0,
|
||||||
|
CPUAccessFlags = CpuAccessFlags.Read,
|
||||||
|
MiscFlags = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return resource.Device.As<ID3D11Device3>().CreateTexture2D1(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Turn a mapped texture into a color table. </summary>
|
||||||
|
private static MtrlFile.ColorTable GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map)
|
||||||
|
{
|
||||||
|
var desc = resource.Description1;
|
||||||
|
|
||||||
|
if (desc.Format is not Format.R16G16B16A16_Float
|
||||||
|
|| desc.Width != MaterialService.TextureWidth
|
||||||
|
|| desc.Height != MaterialService.TextureHeight
|
||||||
|
|| map.DepthPitch != map.RowPitch * desc.Height)
|
||||||
|
throw new InvalidDataException("The texture was not a valid color table texture.");
|
||||||
|
|
||||||
|
return ReadTexture(map.DataPointer, map.DepthPitch, desc.Height, map.RowPitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Transform the GPU data into the color table. </summary>
|
||||||
|
/// <param name="data"> The pointer to the raw texture data. </param>
|
||||||
|
/// <param name="length"> The size of the raw texture data. </param>
|
||||||
|
/// <param name="height"> The height of the texture. (Needs to be 16).</param>
|
||||||
|
/// <param name="pitch"> The stride in the texture data. </param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static MtrlFile.ColorTable ReadTexture(nint data, int length, int height, int pitch)
|
||||||
|
{
|
||||||
|
// Check that the data has sufficient dimension and size.
|
||||||
|
var expectedSize = sizeof(Half) * MaterialService.TextureWidth * height * 4;
|
||||||
|
if (length < expectedSize || sizeof(MtrlFile.ColorTable) != expectedSize || height != MaterialService.TextureHeight)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var ret = new MtrlFile.ColorTable();
|
||||||
|
var target = (byte*)&ret;
|
||||||
|
// If the stride is the same as in the table, just copy.
|
||||||
|
if (pitch == MaterialService.TextureWidth)
|
||||||
|
MemoryUtility.MemCpyUnchecked(target, (void*)data, length);
|
||||||
|
// Otherwise, adapt the stride.
|
||||||
|
else
|
||||||
|
|
||||||
|
for (var y = 0; y < height; ++y)
|
||||||
|
{
|
||||||
|
MemoryUtility.MemCpyUnchecked(target + y * MaterialService.TextureWidth * sizeof(Half) * 4, (byte*)data + y * pitch,
|
||||||
|
MaterialService.TextureWidth * sizeof(Half) * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Get resources of a texture. </summary>
|
||||||
|
private static TRet GetResourceData<T, TRet>(T res, Func<T, T> cloneResource, Func<T, MappedSubresource, TRet> getData)
|
||||||
|
where T : ID3D11Resource
|
||||||
|
{
|
||||||
|
using var stagingRes = cloneResource(res);
|
||||||
|
|
||||||
|
res.Device.ImmediateContext.CopyResource(stagingRes, res);
|
||||||
|
stagingRes.Device.ImmediateContext.Map(stagingRes, 0, MapMode.Read, MapFlags.None, out var mapInfo).CheckError();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return getData(stagingRes, mapInfo);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stagingRes.Device.ImmediateContext.Unmap(stagingRes, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
Glamourer/Interop/Material/MaterialService.cs
Normal file
130
Glamourer/Interop/Material/MaterialService.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using Glamourer.Interop.Structs;
|
||||||
|
using ImGuiNET;
|
||||||
|
using Lumina.Data.Files;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData.Actors;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using static Penumbra.GameData.Files.MtrlFile;
|
||||||
|
using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture;
|
||||||
|
|
||||||
|
namespace Glamourer.Interop.Material;
|
||||||
|
|
||||||
|
|
||||||
|
public class MaterialServiceDrawer(ActorManager actors) : IService
|
||||||
|
{
|
||||||
|
private ActorIdentifier _openIdentifier;
|
||||||
|
private uint _openSlotIndex;
|
||||||
|
|
||||||
|
public unsafe void PopupButton(Actor actor, EquipSlot slot)
|
||||||
|
{
|
||||||
|
var slotIndex = slot.ToIndex();
|
||||||
|
|
||||||
|
var identifier = actor.GetIdentifier(actors);
|
||||||
|
var buttonActive = actor.Valid
|
||||||
|
&& identifier.IsValid
|
||||||
|
&& actor.Model.IsCharacterBase
|
||||||
|
&& slotIndex < actor.Model.AsCharacterBase->SlotCount
|
||||||
|
&& (actor.Model.AsCharacterBase->HasModelInSlotLoaded & (1 << (int)slotIndex)) != 0;
|
||||||
|
using var id = ImRaii.PushId((int)slot);
|
||||||
|
if (ImGuiUtil.DrawDisabledButton("Advanced", Vector2.Zero, "Open advanced window.", !buttonActive))
|
||||||
|
{
|
||||||
|
_openIdentifier = identifier;
|
||||||
|
_openSlotIndex = slotIndex;
|
||||||
|
ImGui.OpenPopup($"Popup{slot}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe class MaterialService
|
||||||
|
{
|
||||||
|
public const int TextureWidth = 4;
|
||||||
|
public const int TextureHeight = ColorTable.NumRows;
|
||||||
|
public const int MaterialsPerModel = 4;
|
||||||
|
|
||||||
|
/// <summary> Generate a color table the way the game does inside the original texture, and release the original. </summary>
|
||||||
|
/// <param name="original"> The original texture that will be replaced with a new one. </param>
|
||||||
|
/// <param name="colorTable"> The input color table. </param>
|
||||||
|
/// <returns> Success or failure. </returns>
|
||||||
|
public static bool GenerateColorTable(Texture** original, in ColorTable colorTable)
|
||||||
|
{
|
||||||
|
if (original == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var textureSize = stackalloc int[2];
|
||||||
|
textureSize[0] = TextureWidth;
|
||||||
|
textureSize[1] = TextureHeight;
|
||||||
|
|
||||||
|
using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F,
|
||||||
|
(uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7), false);
|
||||||
|
if (texture.IsInvalid)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
fixed(ColorTable* ptr = &colorTable)
|
||||||
|
{
|
||||||
|
if (!texture.Texture->InitializeContents(ptr))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
texture.Exchange(ref *(nint*)original);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary> Obtain a pointer to the models pointer to a specific color table texture. </summary>
|
||||||
|
/// <param name="model"></param>
|
||||||
|
/// <param name="modelSlot"></param>
|
||||||
|
/// <param name="materialSlot"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Texture** GetColorTableTexture(Model model, int modelSlot, byte materialSlot)
|
||||||
|
{
|
||||||
|
if (!model.IsCharacterBase)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var index = modelSlot * MaterialsPerModel + materialSlot;
|
||||||
|
if (index < 0 || index >= model.AsCharacterBase->ColorTableTexturesSpan.Length)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var texture = (Texture**)Unsafe.AsPointer(ref model.AsCharacterBase->ColorTableTexturesSpan[index]);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Obtain a pointer to the color table of a certain material from a model. </summary>
|
||||||
|
/// <param name="model"> The draw object. </param>
|
||||||
|
/// <param name="modelSlot"> The model slot. </param>
|
||||||
|
/// <param name="materialSlot"> The material slot in the model. </param>
|
||||||
|
/// <returns> A pointer to the color table or null. </returns>
|
||||||
|
public static ColorTable* GetMaterialColorTable(Model model, int modelSlot, byte materialSlot)
|
||||||
|
{
|
||||||
|
if (!model.IsCharacterBase)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var index = modelSlot * MaterialsPerModel + materialSlot;
|
||||||
|
if (index < 0 || index >= model.AsCharacterBase->MaterialsSpan.Length)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var material = (MaterialResourceHandle*)model.AsCharacterBase->MaterialsSpan[index].Value;
|
||||||
|
if (material == null || material->ColorTable == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (ColorTable*)material->ColorTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Test(Actor actor, MaterialValueIndex index, Vector3 value)
|
||||||
|
{
|
||||||
|
if (!index.TryGetColorTable(actor, out var table))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ref var row = ref table[index.RowIndex];
|
||||||
|
if (!index.DataIndex.SetValue(ref row, value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var texture = GetColorTableTexture(index.TryGetModel(actor, out var model) ? model : Model.Null, index.SlotIndex,
|
||||||
|
index.MaterialIndex);
|
||||||
|
if (texture != null)
|
||||||
|
GenerateColorTable(texture, table);
|
||||||
|
}
|
||||||
|
}
|
||||||
266
Glamourer/Interop/Material/MaterialValueIndex.cs
Normal file
266
Glamourer/Interop/Material/MaterialValueIndex.cs
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
|
using FFXIVClientStructs.Interop;
|
||||||
|
using Glamourer.Interop.Structs;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Penumbra.GameData.Files;
|
||||||
|
|
||||||
|
namespace Glamourer.Interop.Material;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(Converter))]
|
||||||
|
public readonly record struct MaterialValueIndex(
|
||||||
|
MaterialValueIndex.DrawObjectType DrawObject,
|
||||||
|
byte SlotIndex,
|
||||||
|
byte MaterialIndex,
|
||||||
|
byte RowIndex,
|
||||||
|
MaterialValueIndex.ColorTableIndex DataIndex)
|
||||||
|
{
|
||||||
|
public uint Key
|
||||||
|
=> ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex, DataIndex);
|
||||||
|
|
||||||
|
public bool Valid
|
||||||
|
=> Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex) && Validate(DataIndex);
|
||||||
|
|
||||||
|
public static bool FromKey(uint key, out MaterialValueIndex index)
|
||||||
|
{
|
||||||
|
index = new MaterialValueIndex(key);
|
||||||
|
return index.Valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryGetModel(Actor actor, out Model model)
|
||||||
|
{
|
||||||
|
if (!actor.Valid)
|
||||||
|
{
|
||||||
|
model = Model.Null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
model = DrawObject switch
|
||||||
|
{
|
||||||
|
DrawObjectType.Human => actor.Model,
|
||||||
|
DrawObjectType.Mainhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject : Model.Null,
|
||||||
|
DrawObjectType.Offhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject : Model.Null,
|
||||||
|
_ => Model.Null,
|
||||||
|
};
|
||||||
|
return model.IsCharacterBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryGetTextures(Actor actor, out ReadOnlySpan<Pointer<Texture>> textures)
|
||||||
|
{
|
||||||
|
if (!TryGetModel(actor, out var model)
|
||||||
|
|| SlotIndex >= model.AsCharacterBase->SlotCount
|
||||||
|
|| model.AsCharacterBase->ColorTableTexturesSpan.Length < (SlotIndex + 1) * MaterialService.MaterialsPerModel)
|
||||||
|
{
|
||||||
|
textures = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(SlotIndex * MaterialService.MaterialsPerModel,
|
||||||
|
MaterialService.MaterialsPerModel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryGetTexture(Actor actor, out Texture* texture)
|
||||||
|
{
|
||||||
|
if (!TryGetTextures(actor, out var textures) || MaterialIndex >= MaterialService.MaterialsPerModel)
|
||||||
|
{
|
||||||
|
texture = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
texture = textures[MaterialIndex].Value;
|
||||||
|
return texture != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryGetColorTable(Actor actor, out MtrlFile.ColorTable table)
|
||||||
|
{
|
||||||
|
if (TryGetTexture(actor, out var texture))
|
||||||
|
return DirectXTextureHelper.TryGetColorTable(texture, out table);
|
||||||
|
|
||||||
|
table = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row)
|
||||||
|
{
|
||||||
|
if (!TryGetColorTable(actor, out var table))
|
||||||
|
{
|
||||||
|
row = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
row = table[RowIndex];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryGetValue(Actor actor, out Vector3 value)
|
||||||
|
{
|
||||||
|
if (!TryGetColorRow(actor, out var row))
|
||||||
|
{
|
||||||
|
value = Vector3.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = DataIndex switch
|
||||||
|
{
|
||||||
|
ColorTableIndex.Diffuse => row.Diffuse,
|
||||||
|
ColorTableIndex.Specular => row.Specular,
|
||||||
|
ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0),
|
||||||
|
ColorTableIndex.Emissive => row.Emissive,
|
||||||
|
ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0),
|
||||||
|
ColorTableIndex.TileSet => new Vector3(row.TileSet),
|
||||||
|
ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0),
|
||||||
|
ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0),
|
||||||
|
_ => new Vector3(float.NaN),
|
||||||
|
};
|
||||||
|
return !float.IsNaN(value.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MaterialValueIndex FromKey(uint key)
|
||||||
|
=> new(key);
|
||||||
|
|
||||||
|
public static MaterialValueIndex Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0,
|
||||||
|
ColorTableIndex dataIndex = 0)
|
||||||
|
=> new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex);
|
||||||
|
|
||||||
|
public static MaterialValueIndex Max(DrawObjectType drawObject = (DrawObjectType)byte.MaxValue, byte slotIndex = byte.MaxValue,
|
||||||
|
byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue,
|
||||||
|
ColorTableIndex dataIndex = (ColorTableIndex)byte.MaxValue)
|
||||||
|
=> new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex);
|
||||||
|
|
||||||
|
public enum DrawObjectType : byte
|
||||||
|
{
|
||||||
|
Human,
|
||||||
|
Mainhand,
|
||||||
|
Offhand,
|
||||||
|
};
|
||||||
|
|
||||||
|
public enum ColorTableIndex : byte
|
||||||
|
{
|
||||||
|
Diffuse,
|
||||||
|
Specular,
|
||||||
|
SpecularStrength,
|
||||||
|
Emissive,
|
||||||
|
GlossStrength,
|
||||||
|
TileSet,
|
||||||
|
MaterialRepeat,
|
||||||
|
MaterialSkew,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Validate(DrawObjectType type)
|
||||||
|
=> Enum.IsDefined(type);
|
||||||
|
|
||||||
|
public static bool ValidateSlot(byte slotIndex)
|
||||||
|
=> slotIndex < 10;
|
||||||
|
|
||||||
|
public static bool ValidateMaterial(byte materialIndex)
|
||||||
|
=> materialIndex < MaterialService.MaterialsPerModel;
|
||||||
|
|
||||||
|
public static bool ValidateRow(byte rowIndex)
|
||||||
|
=> rowIndex < MtrlFile.ColorTable.NumRows;
|
||||||
|
|
||||||
|
public static bool Validate(ColorTableIndex dataIndex)
|
||||||
|
=> Enum.IsDefined(dataIndex);
|
||||||
|
|
||||||
|
private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex, ColorTableIndex index)
|
||||||
|
{
|
||||||
|
var result = (uint)index & 0xFF;
|
||||||
|
result |= (uint)(rowIndex & 0xFF) << 8;
|
||||||
|
result |= (uint)(materialIndex & 0xF) << 16;
|
||||||
|
result |= (uint)(slotIndex & 0xFF) << 20;
|
||||||
|
result |= (uint)((byte)type & 0xF) << 28;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MaterialValueIndex(uint key)
|
||||||
|
: this((DrawObjectType)((key >> 28) & 0xF), (byte)(key >> 20), (byte)((key >> 16) & 0xF), (byte)(key >> 8),
|
||||||
|
(ColorTableIndex)(key & 0xFF))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
private class Converter : JsonConverter<MaterialValueIndex>
|
||||||
|
{
|
||||||
|
public override void WriteJson(JsonWriter writer, MaterialValueIndex value, JsonSerializer serializer)
|
||||||
|
=> serializer.Serialize(writer, value.Key);
|
||||||
|
|
||||||
|
public override MaterialValueIndex ReadJson(JsonReader reader, Type objectType, MaterialValueIndex existingValue, bool hasExistingValue,
|
||||||
|
JsonSerializer serializer)
|
||||||
|
=> FromKey(serializer.Deserialize<uint>(reader), out var value) ? value : throw new Exception($"Invalid material key {value.Key}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MaterialExtensions
|
||||||
|
{
|
||||||
|
public static bool TryGetValue(this MaterialValueIndex.ColorTableIndex index, in MtrlFile.ColorTable.Row row, out Vector3 value)
|
||||||
|
{
|
||||||
|
value = index switch
|
||||||
|
{
|
||||||
|
MaterialValueIndex.ColorTableIndex.Diffuse => row.Diffuse,
|
||||||
|
MaterialValueIndex.ColorTableIndex.Specular => row.Specular,
|
||||||
|
MaterialValueIndex.ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0),
|
||||||
|
MaterialValueIndex.ColorTableIndex.Emissive => row.Emissive,
|
||||||
|
MaterialValueIndex.ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0),
|
||||||
|
MaterialValueIndex.ColorTableIndex.TileSet => new Vector3(row.TileSet),
|
||||||
|
MaterialValueIndex.ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0),
|
||||||
|
MaterialValueIndex.ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0),
|
||||||
|
_ => new Vector3(float.NaN),
|
||||||
|
};
|
||||||
|
return !float.IsNaN(value.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SetValue(this MaterialValueIndex.ColorTableIndex index, ref MtrlFile.ColorTable.Row row, in Vector3 value)
|
||||||
|
{
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case MaterialValueIndex.ColorTableIndex.Diffuse:
|
||||||
|
if (value == row.Diffuse)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.Diffuse = value;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MaterialValueIndex.ColorTableIndex.Specular:
|
||||||
|
if (value == row.Specular)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.Specular = value;
|
||||||
|
return true;
|
||||||
|
case MaterialValueIndex.ColorTableIndex.SpecularStrength:
|
||||||
|
if (value.X == row.SpecularStrength)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.SpecularStrength = value.X;
|
||||||
|
return true;
|
||||||
|
case MaterialValueIndex.ColorTableIndex.Emissive:
|
||||||
|
if (value == row.Emissive)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.Emissive = value;
|
||||||
|
return true;
|
||||||
|
case MaterialValueIndex.ColorTableIndex.GlossStrength:
|
||||||
|
if (value.X == row.GlossStrength)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.GlossStrength = value.X;
|
||||||
|
return true;
|
||||||
|
case MaterialValueIndex.ColorTableIndex.TileSet:
|
||||||
|
var @ushort = (ushort)(value.X + 0.5f);
|
||||||
|
if (@ushort == row.TileSet)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.TileSet = @ushort;
|
||||||
|
return true;
|
||||||
|
case MaterialValueIndex.ColorTableIndex.MaterialRepeat:
|
||||||
|
if (value.X == row.MaterialRepeat.X && value.Y == row.MaterialRepeat.Y)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.MaterialRepeat = new Vector2(value.X, value.Y);
|
||||||
|
return true;
|
||||||
|
case MaterialValueIndex.ColorTableIndex.MaterialSkew:
|
||||||
|
if (value.X == row.MaterialSkew.X && value.Y == row.MaterialSkew.Y)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
row.MaterialSkew = new Vector2(value.X, value.Y);
|
||||||
|
return true;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
Glamourer/Interop/Material/MaterialValueManager.cs
Normal file
162
Glamourer/Interop/Material/MaterialValueManager.cs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
namespace Glamourer.Interop.Material;
|
||||||
|
|
||||||
|
public readonly struct MaterialValueManager
|
||||||
|
{
|
||||||
|
private readonly List<(uint Key, Vector3 Value)> _values = [];
|
||||||
|
|
||||||
|
public MaterialValueManager()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public bool TryGetValue(MaterialValueIndex index, out Vector3 value)
|
||||||
|
{
|
||||||
|
if (_values.Count == 0)
|
||||||
|
{
|
||||||
|
value = Vector3.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Search(index.Key);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
value = _values[idx].Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Vector3.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryAddValue(MaterialValueIndex index, in Vector3 value)
|
||||||
|
{
|
||||||
|
var key = index.Key;
|
||||||
|
var idx = Search(key);
|
||||||
|
if (idx >= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_values.Insert(~idx, (key, value));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveValue(MaterialValueIndex index)
|
||||||
|
{
|
||||||
|
if (_values.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var idx = Search(index.Key);
|
||||||
|
if (idx < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_values.RemoveAt(idx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddOrUpdateValue(MaterialValueIndex index, in Vector3 value)
|
||||||
|
{
|
||||||
|
var key = index.Key;
|
||||||
|
var idx = Search(key);
|
||||||
|
if (idx < 0)
|
||||||
|
_values.Insert(~idx, (key, value));
|
||||||
|
else
|
||||||
|
_values[idx] = (key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateValue(MaterialValueIndex index, in Vector3 value, out Vector3 oldValue)
|
||||||
|
{
|
||||||
|
if (_values.Count == 0)
|
||||||
|
{
|
||||||
|
oldValue = Vector3.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = index.Key;
|
||||||
|
var idx = Search(key);
|
||||||
|
if (idx < 0)
|
||||||
|
{
|
||||||
|
oldValue = Vector3.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldValue = _values[idx].Value;
|
||||||
|
_values[idx] = (key, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max)
|
||||||
|
{
|
||||||
|
var (minIdx, maxIdx) = GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key);
|
||||||
|
if (minIdx < 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var count = maxIdx - minIdx;
|
||||||
|
_values.RemoveRange(minIdx, count);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlySpan<(uint key, Vector3 Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max)
|
||||||
|
=> Filter(CollectionsMarshal.AsSpan(_values), min, max);
|
||||||
|
|
||||||
|
public static ReadOnlySpan<(uint Key, Vector3 Value)> Filter(ReadOnlySpan<(uint Key, Vector3 Value)> values, MaterialValueIndex min,
|
||||||
|
MaterialValueIndex max)
|
||||||
|
{
|
||||||
|
var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key);
|
||||||
|
return minIdx < 0 ? [] : values[minIdx..(maxIdx - minIdx)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Obtain the minimum index and maximum index for a minimum and maximum key. </summary>
|
||||||
|
private static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, Vector3 Value)> values, uint minKey, uint maxKey)
|
||||||
|
{
|
||||||
|
// Find the minimum index by binary search.
|
||||||
|
var idx = values.BinarySearch((minKey, Vector3.Zero), Comparer.Instance);
|
||||||
|
var minIdx = idx;
|
||||||
|
|
||||||
|
// If the key does not exist, check if it is an invalid range or set it correctly.
|
||||||
|
if (minIdx < 0)
|
||||||
|
{
|
||||||
|
minIdx = ~minIdx;
|
||||||
|
if (minIdx == values.Length || values[minIdx].Key > maxKey)
|
||||||
|
return (-1, -1);
|
||||||
|
|
||||||
|
idx = minIdx;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If it does exist, go upwards until the first key is reached that is actually smaller.
|
||||||
|
while (minIdx > 0 && values[minIdx - 1].Key >= minKey)
|
||||||
|
--minIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the range can be valid.
|
||||||
|
if (values[minIdx].Key < minKey || values[minIdx].Key > maxKey)
|
||||||
|
return (-1, -1);
|
||||||
|
|
||||||
|
|
||||||
|
// Do pretty much the same but in the other direction with the maximum key.
|
||||||
|
var maxIdx = values[idx..].BinarySearch((maxKey, Vector3.Zero), Comparer.Instance);
|
||||||
|
if (maxIdx < 0)
|
||||||
|
{
|
||||||
|
maxIdx = ~maxIdx;
|
||||||
|
return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey)
|
||||||
|
++maxIdx;
|
||||||
|
|
||||||
|
if (values[maxIdx].Key < minKey || values[maxIdx].Key > maxKey)
|
||||||
|
return (-1, -1);
|
||||||
|
|
||||||
|
return (minIdx, maxIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private int Search(uint key)
|
||||||
|
=> _values.BinarySearch((key, Vector3.Zero), Comparer.Instance);
|
||||||
|
|
||||||
|
private class Comparer : IComparer<(uint Key, Vector3 Value)>
|
||||||
|
{
|
||||||
|
public static readonly Comparer Instance = new();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public int Compare((uint Key, Vector3 Value) x, (uint Key, Vector3 Value) y)
|
||||||
|
=> x.Key.CompareTo(y.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
162
Glamourer/Interop/Material/PrepareColorSet.cs
Normal file
162
Glamourer/Interop/Material/PrepareColorSet.cs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using Glamourer.Interop.Penumbra;
|
||||||
|
using Glamourer.Interop.Structs;
|
||||||
|
using Glamourer.State;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData.Actors;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Glamourer.Interop.Material;
|
||||||
|
|
||||||
|
public sealed unsafe class PrepareColorSet
|
||||||
|
: EventWrapperPtr12Ref34<CharacterBase, MaterialResourceHandle, StainId, nint, PrepareColorSet.Priority>, IHookService
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="MaterialManager.OnPrepareColorSet"/>
|
||||||
|
MaterialManager = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrepareColorSet(HookManager hooks)
|
||||||
|
: base("Prepare Color Set ")
|
||||||
|
=> _task = hooks.CreateHook<Delegate>(Name, "40 55 56 41 56 48 83 EC ?? 80 BA", Detour, true);
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> (nint)CharacterBase.MemberFunctionPointers.Destroy;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
private delegate Texture* Delegate(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId);
|
||||||
|
|
||||||
|
private Texture* Detour(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId)
|
||||||
|
{
|
||||||
|
Glamourer.Log.Excessive($"[{Name}] Triggered with 0x{(nint)characterBase:X} 0x{(nint)material:X} {stainId.Id}.");
|
||||||
|
var ret = nint.Zero;
|
||||||
|
Invoke(characterBase, material, ref stainId, ref ret);
|
||||||
|
if (ret != nint.Zero)
|
||||||
|
return (Texture*)ret;
|
||||||
|
|
||||||
|
return _task.Result.Original(characterBase, material, stainId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed unsafe class MaterialManager : IRequiredService, IDisposable
|
||||||
|
{
|
||||||
|
private readonly PrepareColorSet _event;
|
||||||
|
private readonly StateManager _stateManager;
|
||||||
|
private readonly PenumbraService _penumbra;
|
||||||
|
private readonly ActorManager _actors;
|
||||||
|
|
||||||
|
private int _lastSlot;
|
||||||
|
|
||||||
|
public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra)
|
||||||
|
{
|
||||||
|
_stateManager = stateManager;
|
||||||
|
_actors = actors;
|
||||||
|
_penumbra = penumbra;
|
||||||
|
_event = prepareColorSet;
|
||||||
|
|
||||||
|
_event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> _event.Unsubscribe(OnPrepareColorSet);
|
||||||
|
|
||||||
|
private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainId stain, ref nint ret)
|
||||||
|
{
|
||||||
|
var actor = _penumbra.GameObjectFromDrawObject(characterBase);
|
||||||
|
var validType = FindType(characterBase, actor, out var type);
|
||||||
|
var (slotId, materialId) = FindMaterial(characterBase, material);
|
||||||
|
Glamourer.Log.Information(
|
||||||
|
$" Triggered with 0x{(nint)characterBase:X} 0x{(nint)material:X} {stain.Id} --- Actor: 0x{actor.Address:X} Slot: {slotId} Material: {materialId} DrawObject: {type}.");
|
||||||
|
if (!validType
|
||||||
|
|| slotId == byte.MaxValue
|
||||||
|
|| !actor.Identifier(_actors, out var identifier)
|
||||||
|
|| !_stateManager.TryGetValue(identifier, out var state))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var min = MaterialValueIndex.Min(type, slotId, materialId);
|
||||||
|
var max = MaterialValueIndex.Max(type, slotId, materialId);
|
||||||
|
var manager = new MaterialValueManager();
|
||||||
|
var values = manager.GetValues(min, max);
|
||||||
|
foreach (var (key, value) in values)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material)
|
||||||
|
{
|
||||||
|
for (var i = _lastSlot; i < characterBase->SlotCount; ++i)
|
||||||
|
{
|
||||||
|
var idx = MaterialService.MaterialsPerModel * i;
|
||||||
|
for (var j = 0; j < MaterialService.MaterialsPerModel; ++j)
|
||||||
|
{
|
||||||
|
var mat = (nint)characterBase->Materials[idx++];
|
||||||
|
if (mat != (nint)material)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_lastSlot = i;
|
||||||
|
return ((byte)i, (byte)j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < _lastSlot; ++i)
|
||||||
|
{
|
||||||
|
var idx = MaterialService.MaterialsPerModel * i;
|
||||||
|
for (var j = 0; j < MaterialService.MaterialsPerModel; ++j)
|
||||||
|
{
|
||||||
|
var mat = (nint)characterBase->Materials[idx++];
|
||||||
|
if (mat != (nint)material)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_lastSlot = i;
|
||||||
|
return ((byte)i, (byte)j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (byte.MaxValue, byte.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type)
|
||||||
|
{
|
||||||
|
type = MaterialValueIndex.DrawObjectType.Human;
|
||||||
|
if (!actor.Valid)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (actor.Model.AsCharacterBase == characterBase)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!actor.AsObject->IsCharacter())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject == characterBase)
|
||||||
|
{
|
||||||
|
type = MaterialValueIndex.DrawObjectType.Mainhand;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject == characterBase)
|
||||||
|
{
|
||||||
|
type = MaterialValueIndex.DrawObjectType.Offhand;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Glamourer/Interop/Material/SafeTextureHandle.cs
Normal file
49
Glamourer/Interop/Material/SafeTextureHandle.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
|
|
||||||
|
namespace Glamourer.Interop.Material;
|
||||||
|
|
||||||
|
public unsafe class SafeTextureHandle : SafeHandle
|
||||||
|
{
|
||||||
|
public Texture* Texture
|
||||||
|
=> (Texture*)handle;
|
||||||
|
|
||||||
|
public override bool IsInvalid
|
||||||
|
=> handle == 0;
|
||||||
|
|
||||||
|
public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true)
|
||||||
|
: base(0, ownsHandle)
|
||||||
|
{
|
||||||
|
if (incRef && !ownsHandle)
|
||||||
|
throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported");
|
||||||
|
|
||||||
|
if (incRef && handle != null)
|
||||||
|
handle->IncRef();
|
||||||
|
SetHandle((nint)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Exchange(ref nint ppTexture)
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
handle = Interlocked.Exchange(ref ppTexture, handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SafeTextureHandle CreateInvalid()
|
||||||
|
=> new(null, false);
|
||||||
|
|
||||||
|
protected override bool ReleaseHandle()
|
||||||
|
{
|
||||||
|
nint handle;
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
handle = this.handle;
|
||||||
|
this.handle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle != 0)
|
||||||
|
((Texture*)handle)->DecRef();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ using Glamourer.Unlocks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
|
|
@ -47,7 +48,7 @@ public static class ServiceManagerA
|
||||||
DalamudServices.AddServices(services, pi);
|
DalamudServices.AddServices(services, pi);
|
||||||
services.AddIServices(typeof(EquipItem).Assembly);
|
services.AddIServices(typeof(EquipItem).Assembly);
|
||||||
services.AddIServices(typeof(Glamourer).Assembly);
|
services.AddIServices(typeof(Glamourer).Assembly);
|
||||||
services.AddIServices(typeof(EquipFlag).Assembly);
|
services.AddIServices(typeof(ImRaii).Assembly);
|
||||||
services.CreateProvider();
|
services.CreateProvider();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue