using System; using System.Threading; using Dalamud.Hooking; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Customization; using Glamourer.Events; using Glamourer.Interop.Structs; using OtterGui.Classes; using CustomizeData = Penumbra.GameData.Structs.CustomizeData; namespace Glamourer.Interop; /// /// Access the function the game uses to update customize data on the character screen. /// Changes in Race, body type or Gender are probably ignored. /// This operates on draw objects, not game objects. /// public unsafe class ChangeCustomizeService : EventWrapper>, ChangeCustomizeService.Priority> { private readonly PenumbraReloaded _penumbraReloaded; private readonly IGameInteropProvider _interop; private readonly delegate* unmanaged[Stdcall] _original; /// Check whether we in a manual customize update, in which case we need to not toggle certain flags. public static readonly InMethodChecker InUpdate = new(); public enum Priority { /// StateListener = 0, } public ChangeCustomizeService(PenumbraReloaded penumbraReloaded, IGameInteropProvider interop) : base("ChangeCustomize") { _penumbraReloaded = penumbraReloaded; _interop = interop; _changeCustomizeHook = Create(); _original = Human.MemberFunctionPointers.UpdateDrawData; _penumbraReloaded.Subscribe(Restore, PenumbraReloaded.Priority.ChangeCustomizeService); } protected override void Dispose(bool _) { _changeCustomizeHook.Dispose(); _penumbraReloaded.Unsubscribe(Restore); } private void Restore() { _changeCustomizeHook.Dispose(); _changeCustomizeHook = Create(); } private Hook Create() { var ret = _interop.HookFromAddress((nint)Human.MemberFunctionPointers.UpdateDrawData, ChangeCustomizeDetour); ret.Enable(); return ret; } private delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment); private Hook _changeCustomizeHook; public bool UpdateCustomize(Model model, CustomizeData customize) { if (!model.IsHuman) return false; Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}."); using var _ = InUpdate.EnterMethod(); var ret = _original(model.AsHuman, customize.Data, true); return ret; } public bool UpdateCustomize(Actor actor, CustomizeData customize) => UpdateCustomize(actor.Model, customize); private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment) { if (!InUpdate.InMethod) { var customize = new Ref(new Customize(*(CustomizeData*)data)); Invoke(this, (Model)human, customize); ((Customize*)data)->Load(customize.Value); } return _changeCustomizeHook.Original(human, data, skipEquipment); } }