using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using MareSynchronos.MareConfiguration; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Core; using System.Buffers; namespace MareSynchronos.Interop; public unsafe sealed class ColorTableHook : IDisposable { // Based on https://github.com/Exter-N/MemShield/blob/main/MemShield/Plugin.cs private readonly ILogger _logger; private const int ColorTableSize = 2048; private const int ColorDyeTableSize = 128; private readonly MareConfigService _configService; #region signatures #pragma warning disable CS0649 [Signature("E8 ?? ?? ?? ?? 49 89 04 3E", DetourName = nameof(PrepareColorTableDetour))] private Hook _prepareColorTable = null!; #pragma warning restore CS0649 #endregion public ColorTableHook(ILogger logger, IGameInteropProvider gameInteropProvider, MareConfigService configService) { _logger = logger; _configService = configService; logger.LogInformation("Initializing ColorTableHook"); gameInteropProvider.InitializeFromAttributes(this); if (_configService.Current.AttemptColorTableProtection) { _prepareColorTable.Enable(); } } public void Dispose() { if (_configService.Current.AttemptColorTableProtection) { _prepareColorTable.Disable(); } _prepareColorTable.Dispose(); } public void EnableHooks() { _prepareColorTable.Enable(); } public void DisableHooks() { _prepareColorTable.Disable(); } private static int GetDataSetExpectedSize(uint dataFlags) => (dataFlags & 16) != 0 ? ColorTableSize + ((dataFlags & 8) != 0 ? ColorDyeTableSize : 0) : 0; private Texture* PrepareColorTableDetour(MaterialResourceHandle* @this, byte stain0Id, byte stain1Id) { if(!_configService.Current.AttemptColorTableProtection) { return _prepareColorTable.Original(@this, stain0Id, stain1Id); } var expectedSize = GetDataSetExpectedSize(@this->DataFlags); if (@this->DataSetSize >= expectedSize) return _prepareColorTable.Original(@this, stain0Id, stain1Id); var originalDataSetPtr = @this->DataSet; var originalDataSet = new ReadOnlySpan(originalDataSetPtr, @this->DataSetSize); var pool = ArrayPool.Shared; var buffer = pool.Rent(expectedSize); try { var bufferSpan = buffer.AsSpan(..expectedSize); originalDataSet.CopyTo(bufferSpan); bufferSpan[@this->DataSetSize..].Clear(); fixed (byte* bufferPtr = buffer) { @this->DataSet = bufferPtr; try { return _prepareColorTable.Original(@this, stain0Id, stain1Id); } finally { @this->DataSet = originalDataSetPtr; } } } finally { pool.Return(buffer); } } }